草庐IT

SpringBoot统一API魔改

Jingzhiwindy 2023-03-28 原文

配置注解

package com.example.demo.api;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface API {

    boolean request() default true;

    boolean response() default true;
}

以注解的形式对接口进行配置,可配置在Controller的类或方法上。request属性为true,表示请求体需要分公有域和私有域,私有域数据封装在data节点内;为false,表示不区分公有域和私有域。response同理,如果属性为true,会将返回的业务数据封装在data节点内。

响应码的定义

package com.example.demo.api;

import cn.hutool.core.util.StrUtil;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonValue;

import java.util.Arrays;

public enum State {

    SUCCESS(0, "success"),
    VALID_FAIL(100, "valid fail"),
    PASSWORD_ERROR(101, "password error"),
    UNKNOWN_ERROR(999, "unknow error");

    @JsonValue
    private int code;
    private String message;

    State(int code, String message) {
        this.code = code;
        this.message = message;
    }

    public int getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }

    @Override
    public String toString() {
        return String.format("[%03d] %s", code, message);
    }

    public void tryThrow() {
        throw new StatefulException(this);
    }

    public void tryThrow(String message) {
        throw new StatefulException(this, message);
    }

    public void tryThrow(String messageTemplate, Object... params) {
        throw new StatefulException(this, messageTemplate, params);
    }

    public void tryThrow(Throwable cause) {
        throw new StatefulException(this, cause);
    }

    public void isTrue(boolean expression) {
        isFalse(!expression);
    }

    public void isTrue(boolean expression, String message) {
        isFalse(!expression, message);
    }

    public void isTrue(boolean expression, String messageTemplate, Object... params) {
        isFalse(!expression, messageTemplate, params);
    }

    public void isFalse(boolean expression) {
        if (expression) {
            throw new StatefulException(this);
        }
    }

    public void isFalse(boolean expression, String message) {
        if (expression) {
            throw new StatefulException(this, message);
        }
    }

    public void isFalse(boolean expression, String messageTemplate, Object... params) {
        if (expression) {
            throw new StatefulException(this, messageTemplate, params);
        }
    }

    public <T> T notNull(T obj) {
        if (obj == null) {
            throw new StatefulException(this);
        }
        return obj;
    }

    public <T> T notNull(T obj, String message) {
        if (obj == null) {
            throw new StatefulException(this, message);
        }
        return obj;
    }

    public <T> T notNull(T obj, String messageTemplate, Object... params) {
        if (obj == null) {
            throw new StatefulException(this, messageTemplate, params);
        }
        return obj;
    }

    public <T extends CharSequence> T notBlank(CharSequence str) {
        if (StrUtil.isBlank(str)) {
            throw new StatefulException(this);
        }
        return (T) str;
    }

    public <T extends CharSequence> T notBlank(CharSequence str, String message) {
        if (StrUtil.isBlank(str)) {
            throw new StatefulException(this, message);
        }
        return (T) str;
    }

    public <T extends CharSequence> T notBlank(CharSequence str, String messageTemplate, Object... params) {
        if (StrUtil.isBlank(str)) {
            throw new StatefulException(this, messageTemplate, params);
        }
        return (T) str;
    }

    public <T> List<T> notEmpty(List<T> list) {
        if (list == null || list.isEmpty()) {
            tryThrow();
        }
        return list;
    }

    public <T> List<T> notEmpty(List<T> list, String errorMessage) {
        if (list == null || list.isEmpty()) {
            tryThrow(errorMessage);
        }
        return list;
    }

    public <T> List<T> notEmpty(List<T> list, String messageTemplate, Object... params) {
        if (list == null || list.isEmpty()) {
            tryThrow(messageTemplate, params);
        }
        return list;
    }

    @JsonCreator
    public static State valueOf(int code) {
        return Arrays.stream(values()).filter(state -> state.getCode() == code).findAny().orElse(null);
    }
}

定义了错误码、错误信息,以及一些断言。使用断言可以抛出包含错误码和错误信息的异常

支持响应码的异常

package com.example.demo.api;

import cn.hutool.core.util.StrUtil;

public class StatefulException extends RuntimeException {

    private State state;

    public StatefulException(State state) {
        this.state = state;
    }

    public StatefulException(State state, String message) {
        super(message);
        this.state = state;
    }

    public StatefulException(State state, String messageTemplate, Object... params) {
        super(StrUtil.format(messageTemplate, params));
        this.state = state;
    }

    public StatefulException(State state, Throwable cause) {
        super(cause);
        this.state = state;
    }

    public State getState() {
        return state;
    }

    public void setState(State state) {
        this.state = state;
    }

    @Override
    public String toString() {
        return StrUtil.isBlank(getMessage()) ? state.toString() : state + ": " + getMessage();
    }
}

公有域的定义

package com.example.demo.api;

import java.io.Serializable;

public class PublicDomain<T> implements Serializable {

    private T data;

    public PublicDomain() {
    }

    public PublicDomain(T data) {
        this.data = data;
    }

    public T getData() {
        return data;
    }

    public void setData(T data) {
        this.data = data;
    }
}

将请求和响应分为公有域部分和私有域部分。响应中公有域包含响应码、响应信息和由data封装的私有域。

响应体的定义

package com.example.demo.api;

import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonPropertyOrder;

@JsonPropertyOrder({"resCode", "resMessage", "errorMessage", "data"})
public class ResultVo<T> extends PublicDomain<T> {

    // 响应状态码
    private int resCode;
    // 响应状态信息
    private String resMessage;
    // 错误详情
    @JsonInclude(JsonInclude.Include.NON_NULL)
    private String errorMessage;

    public ResultVo() {
        this(State.SUCCESS);
    }

    public ResultVo(T data) {
        super(data);
        this.resCode = State.SUCCESS.getCode();
        this.resMessage = State.SUCCESS.getMessage();
    }

    public ResultVo(State state) {
        this.resCode = state.getCode();
        this.resMessage = state.getMessage();
    }

    public ResultVo(StatefulException statefulException) {
        State state = statefulException.getState();
        this.resCode = state.getCode();
        this.resMessage = state.getMessage();
        this.errorMessage = statefulException.getMessage();
    }

    public ResultVo(State state, Throwable ex) {
        this.resCode = state.getCode();
        this.resMessage = state.getMessage();
        this.errorMessage = ex.getMessage();
    }

    public int getResCode() {
        return resCode;
    }

    public void setResCode(int resCode) {
        this.resCode = resCode;
    }

    public String getResMessage() {
        return resMessage;
    }

    public void setResMessage(String resMessage) {
        this.resMessage = resMessage;
    }

    public String getErrorMessage() {
        return errorMessage;
    }

    public void setErrorMessage(String errorMessage) {
        this.errorMessage = errorMessage;
    }
}

请求体的处理

package com.example.demo.api;

import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.MethodParameter;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.util.StreamUtils;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.RequestBodyAdviceAdapter;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;

@RestControllerAdvice(basePackages = "com.example.demo")
public class RequesstApiAdvice extends RequestBodyAdviceAdapter {

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
        API apiAnn = methodParameter.hasMethodAnnotation(API.class) ?
                methodParameter.getMethodAnnotation(API.class) : methodParameter.getDeclaringClass().getAnnotation(API.class);
        return apiAnn != null && apiAnn.request() && !PublicDomain.class.equals(methodParameter.getParameterType());
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage, MethodParameter parameter,
                                           Type targetType, Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
        JsonNode jsonNode = objectMapper.readTree(inputMessage.getBody());
        return new HttpInputMessage() {
            @Override
            public InputStream getBody() {
                JsonNode dataJsonNode = State.VALID_FAIL.notNull(jsonNode.get("data"), "请求体未包含私有域节点data");
                return new ByteArrayInputStream(dataJsonNode.toString().getBytes(StandardCharsets.UTF_8));
            }

            @Override
            public HttpHeaders getHeaders() {
                return inputMessage.getHeaders();
            }
        };
    }
}

响应体的处理

响应体的处理可以基于ResponseBodyAdvice,也可以基于HandlerMethodReturnValueHandler,两种方案可以任选其一

基于ResponseBodyAdvice的实现
package com.example.demo.api;

import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

@RestControllerAdvice(basePackages = "com.example.demo")
public class ResponseApiAdvice implements ResponseBodyAdvice {
    @Override
    public boolean supports(MethodParameter returnType, Class converterType) {
        API apiAnn = returnType.hasMethodAnnotation(API.class) ?
                returnType.getMethodAnnotation(API.class) : returnType.getDeclaringClass().getAnnotation(API.class);
        return apiAnn != null && apiAnn.response();
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
        return body instanceof ResultVo ? body : new ResultVo<>(body);
    }
}
基于HandlerMethodReturnValueHandler的实现
package com.example.demo.api;

import org.springframework.core.MethodParameter;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.NativeWebRequest;
import org.springframework.web.method.support.HandlerMethodReturnValueHandler;
import org.springframework.web.method.support.ModelAndViewContainer;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerAdapter;
import org.springframework.web.servlet.mvc.method.annotation.RequestResponseBodyMethodProcessor;

import java.util.ArrayList;
import java.util.List;

@Component
public class ApiHandlerMethodReturnValueHandler implements HandlerMethodReturnValueHandler {

    private HandlerMethodReturnValueHandler handler;

    public ApiHandlerMethodReturnValueHandler(RequestMappingHandlerAdapter requestMappingHandlerAdapter) {
        List<HandlerMethodReturnValueHandler> originHandlers = requestMappingHandlerAdapter.getReturnValueHandlers();
        List<HandlerMethodReturnValueHandler> newHandlers = new ArrayList<>(originHandlers.size());
        for (HandlerMethodReturnValueHandler originHandler : originHandlers) {
            if (originHandler instanceof RequestResponseBodyMethodProcessor) {
                newHandlers.add(this);
                handler = originHandler;
            } else {
                newHandlers.add(originHandler);
            }
        }
        requestMappingHandlerAdapter.setReturnValueHandlers(newHandlers);
    }

    @Override
    public boolean supportsReturnType(MethodParameter returnType) {
        API apiAnn = returnType.hasMethodAnnotation(API.class) ?
                returnType.getMethodAnnotation(API.class) : returnType.getDeclaringClass().getAnnotation(API.class);
        return handler.supportsReturnType(returnType) && apiAnn != null && apiAnn.response();
    }

    @Override
    public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
        Object response = returnValue instanceof ResultVo ? returnValue : new ResultVo<>(returnValue);
        handler.handleReturnValue(response, returnType, mavContainer, webRequest);
    }
}

HandlerMethodReturnValueHandler 的作用是对处理器的处理结果再进行一次二次加工

全局异常处理

package com.example.demo.api;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionResolver {
    private final static Logger LOGGER = LoggerFactory.getLogger(GlobalExceptionResolver.class);

    @ExceptionHandler(Exception.class)
    public ResultVo exceptionHandle(Exception ex) {
        LOGGER.error("error", ex);
        return new ResultVo(State.UNKNOWN_ERROR, ex);
    }

    @ExceptionHandler(RuntimeException.class)
    public ResultVo exceptionHandle(RuntimeException ex) {
        LOGGER.error("valid fail", ex);
        return new ResultVo(State.VALID_FAIL, ex);
    }

    @ExceptionHandler(StatefulException.class)
    public ResultVo exceptionHandle(StatefulException ex) {
        LOGGER.error(ex.getState().toString(), ex);
        return new ResultVo(ex);
    }
}

测试

package com.example.demo.user.entity;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
public class User {

    private String account;
    private String name;
    private String password;
}
package com.example.demo.user.controller;

import com.example.demo.api.API;
import com.example.demo.api.State;
import com.example.demo.user.entity.User;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class UserController {

    @API
    @PostMapping("/login/passport")
    public User test(@RequestBody User user) {
        State.VALID_FAIL.notBlank(user.getAccount(), "账号为空");
        State.VALID_FAIL.notBlank(user.getPassword(), "密码为空");
        State.PASSWORD_ERROR.isTrue(user.getPassword().equals("123456"), "密码错误");
        return user;
    }
}

正确请求

{
    "data": {
        "account": "demo",
        "password": "123456"
    }
}

响应

{
    "resCode": 0,
    "resMessage": "success",
    "data": {
        "account": "demo",
        "name": null,
        "password": "123456"
    }
}

密码错误请求

{
    "data": {
        "account": "demo",
        "password": "123"
    }
}

响应

{
    "resCode": 101,
    "resMessage": "password error",
    "errorMessage": "密码错误",
    "data": null
}

有关SpringBoot统一API魔改的更多相关文章

  1. ruby - 解析 RDFa、微数据等的最佳方式是什么,使用统一的模式/词汇(例如 schema.org)存储和显示信息 - 2

    我主要使用Ruby来执行此操作,但到目前为止我的攻击计划如下:使用gemsrdf、rdf-rdfa和rdf-microdata或mida来解析给定任何URI的数据。我认为最好映射到像schema.org这样的统一模式,例如使用这个yaml文件,它试图描述数据词汇表和opengraph到schema.org之间的转换:#SchemaXtoschema.orgconversion#data-vocabularyDV:name:namestreet-address:streetAddressregion:addressRegionlocality:addressLocalityphoto:i

  2. ruby-on-rails - ActionController::RoutingError: 未初始化常量 Api::V1::ApiController - 2

    我有用于控制用户任务的Rails5API项目,我有以下错误,但并非总是针对相同的Controller和路由。ActionController::RoutingError:uninitializedconstantApi::V1::ApiController我向您描述了一些我的项目,以更详细地解释错误。应用结构路线scopemodule:'api'donamespace:v1do#=>Loginroutesscopemodule:'login'domatch'login',to:'sessions#login',as:'login',via::postend#=>Teamroutessc

  3. 叮咚买菜基于 Apache Doris 统一 OLAP 引擎的应用实践 - 2

    导读:随着叮咚买菜业务的发展,不同的业务场景对数据分析提出了不同的需求,他们希望引入一款实时OLAP数据库,构建一个灵活的多维实时查询和分析的平台,统一数据的接入和查询方案,解决各业务线对数据高效实时查询和精细化运营的需求。经过调研选型,最终引入ApacheDoris作为最终的OLAP分析引擎,Doris作为核心的OLAP引擎支持复杂地分析操作、提供多维的数据视图,在叮咚买菜数十个业务场景中广泛应用。作者|叮咚买菜资深数据工程师韩青叮咚买菜创立于2017年5月,是一家专注美好食物的创业公司。叮咚买菜专注吃的事业,为满足更多人“想吃什么”而努力,通过美好食材的供应、美好滋味的开发以及美食品牌的孵

  4. 【鸿蒙应用开发系列】- 获取系统设备信息以及版本API兼容调用方式 - 2

    在应用开发中,有时候我们需要获取系统的设备信息,用于数据上报和行为分析。那在鸿蒙系统中,我们应该怎么去获取设备的系统信息呢,比如说获取手机的系统版本号、手机的制造商、手机型号等数据。1、获取方式这里分为两种情况,一种是设备信息的获取,一种是系统信息的获取。1.1、获取设备信息获取设备信息,鸿蒙的SDK包为我们提供了DeviceInfo类,通过该类的一些静态方法,可以获取设备信息,DeviceInfo类的包路径为:ohos.system.DeviceInfo.具体的方法如下:ModifierandTypeMethodDescriptionstatic StringgetAbiList​()Obt

  5. ruby-on-rails - Mandrill API 模板 - 2

    我正在使用Mandrill的RubyAPIGem并使用以下简单的测试模板:testastic按照Heroku指南中的示例,我有以下Ruby代码:require'mandrill'm=Mandrill::API.newrendered=m.templates.render'test-template',[{:header=>'someheadertext',:main_section=>'Themaincontentblock',:footer=>'asdf'}]mail(:to=>"JaysonLane",:subject=>"TestEmail")do|format|format.h

  6. ruby-on-rails - 在 Ruby (on Rails) 中使用 imgur API 获取图像 - 2

    我正在尝试使用Ruby2.0.0和Rails4.0.0提供的API从imgur中提取图像。我已尝试按照Ruby2.0.0文档中列出的各种方式构建http请求,但均无济于事。代码如下:require'net/http'require'net/https'defimgurheaders={"Authorization"=>"Client-ID"+my_client_id}path="/3/gallery/image/#{img_id}.json"uri=URI("https://api.imgur.com"+path)request,data=Net::HTTP::Get.new(path

  7. ruby-on-rails - 使用 HTTParty 的非常基本的 Rails 4.1 API 调用 - 2

    Rails相对较新。我正在尝试调用一个API,它应该向我返回一个唯一的URL。我的应用程序中捆绑了HTTParty。我已经创建了一个UniqueNumberController,并且我已经阅读了几个HTTParty指南,直到我想要什么,但也许我只是有点迷路,真的不知道该怎么做。基本上,我需要做的就是调用API,获取它返回的URL,然后将该URL插入到用户的数据库中。谁能给我指出正确的方向或与我分享一些代码? 最佳答案 假设API为JSON格式并返回如下数据:{"url":"http://example.com/unique-url"

  8. ruby-on-rails - 是否使用 API - 2

    关闭。这个问题是opinion-based.它目前不接受答案。想要改进这个问题?更新问题,以便editingthispost可以用事实和引用来回答它.关闭4年前。Improvethisquestion我的公司有一个巨大的数据库,该数据库接收来自多个来源的(许多)事件,用于监控和报告目的。到目前为止,数据中的每个新仪表板或图形都是一个新的Rails应用程序,在巨大的数据库中有额外的表,并且可以完全访问数据库内容。最近,有一个想法让外部(不是我们公司,而是姊妹公司)客户访问我们的数据,并且决定我们应该公开一个只读的RESTfulAPI来查询我们的数据。我的观点是-我们是否也应该为我们的自己

  9. ruby - Ruby 中的必应搜索 API - 2

    我读了"BingSearchAPI-QuickStart"但我不知道如何在Ruby中发出这个http请求(Weary)如何在Ruby中翻译“Stream_context_create()”?这是什么意思?"BingSearchAPI-QuickStart"我想使用RubySDK,但我发现那些已被弃用前(Rbing)https://github.com/mikedemers/rbing您知道Bing搜索API的最新包装器(仅限Web的结果)吗? 最佳答案 好吧,经过一个小时的挫折,我想出了一个办法来做到这一点。这段代码很糟糕,因为它是

  10. python - 用于 Python 或 Ruby 的 Amazon Book API? - 2

    这个问题在这里已经有了答案:关闭10年前。PossibleDuplicate:AmazonAPIlibraryforPython?我正在寻找一个AmazonAPI,它可以让我:按书名或作者查找书籍显示书籍封面获取有关每本书的信息(价格、评级、评论数、格式、页数等)Python或Ruby库都可以(我只想要最容易使用的库)。有什么建议么?我知道在SO上还有其他一些关于此的帖子,但这些API似乎很快就过时了。[几个月前我尝试了几个建议的Ruby库,但无法让它们中的任何一个工作。]

随机推荐