SpringBoot自定义starter之标准化处理

3,467 阅读4分钟

前言

许久没更新博客了, 甚是抱歉。工作中需要下沉一些通用组件, 所以这里先做了一个接口标准化包。功能包括:

  • 统一异常处理
  • 统一接口返回格式
  • 统一参数校验(业务无需加@Valid和Validated注解)

实现

pom.xml

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-webmvc</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>com.fasterxml.jackson.core</groupId>
            <artifactId>jackson-databind</artifactId>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.slf4j</groupId>
            <artifactId>slf4j-api</artifactId>
            <scope>provided</scope>
            <optional>true</optional>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>cn.idea360</groupId>
            <artifactId>idea360-core</artifactId>
            <version>0.0.1</version>
        </dependency>
        <dependency>
            <groupId>javax.servlet</groupId>
            <artifactId>javax.servlet-api</artifactId>
            <scope>provided</scope>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>2.8.0</version>
            <scope>compile</scope>
        </dependency>
    </dependencies>

spring.factories

/resources/META-INF/spring.factories 目录下

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.idea360.unified.UnifiedResponseBodyAdvice,\
cn.idea360.unified.UnifiedExceptionHandler,\
cn.idea360.unified.filter.FilterAutoConfig,\
cn.idea360.unified.interceptor.InterceptorAutoConfig

通用异常处理

  1. 返回格式
/**
 * @author cuishiying
 * @date 2021-01-22
 */
public class UnifiedResult<T> implements Serializable {
    public static final int SUCCESS = 0;
    public static final int ERROR = -1;


    private String msg;
    private int code = SUCCESS;
    private T data;


    public UnifiedResult(int code, String msg, T data) {
        this.code = code;
        this.msg = msg;
        this.data = data;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }

    public int getCode() {
        return code;
    }

    public void setCode(int code) {
        this.code = code;
    }

    public T getData() {
        return data;
    }

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

    public static class Builder<T> {

        private String msg = "OK";
        private int code = SUCCESS;
        private T data;

        public String getMsg() {
            return msg;
        }

        public void setMsg(String msg) {
            this.msg = msg;
        }

        public int getCode() {
            return code;
        }

        public void setCode(int code) {
            this.code = code;
        }

        public T getData() {
            return data;
        }

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


        public Builder<T> data(T data) {
            this.data = data;
            return this;
        }

        public Builder<T> error(int code, String msg) {
            this.code = code;
            this.msg = msg;
            return this;
        }

        public Builder<T> error(int code) {
            this.code = code;
            return this;
        }

        public UnifiedResult<T> build() {
            return new UnifiedResult<T>(this.code, this.msg, this.data);
        }

        public Builder<T> message(String msg) {
            this.msg = msg;
            return this;
        }
    }
}
  1. 异常拦截
/**
 * @author cuishiying
 * @date 2021-01-22
 */
@RestControllerAdvice
public class UnifiedExceptionHandler {

    private final Logger log = LoggerFactory.getLogger(UnifiedExceptionHandler.class);

    /**
     * 参数绑定异常
     */
    @ExceptionHandler({BindException.class})
    public UnifiedResult exceptionHandler(BindException e) {
        log.error("BindException:", e);
        BindingResult bindingResult = e.getBindingResult();
        return new UnifiedResult.Builder<>().error(UnifiedResult.ERROR, Objects.requireNonNull(bindingResult.getFieldError()).getDefaultMessage()).build();
    }

    /**
     * 参数校验异常
     */
    @ExceptionHandler({MethodArgumentNotValidException.class})
    public UnifiedResult exceptionHandler(MethodArgumentNotValidException e) {
        log.error("MethodArgumentNotValidException:", e);
        BindingResult bindingResult = e.getBindingResult();
        return new UnifiedResult.Builder<>().error(UnifiedResult.ERROR, Objects.requireNonNull(bindingResult.getFieldError()).getDefaultMessage()).build();
    }

    /**
     * 参数验证异常
     */
    @ExceptionHandler(value = ConstraintViolationException.class)
    public UnifiedResult handler(ConstraintViolationException e) {
        return new UnifiedResult.Builder<>().error(UnifiedResult.ERROR, e.getMessage()).build();
    }

    /**
     * 参数类型转换错误
     */
    @ExceptionHandler(HttpMessageConversionException.class)
    public UnifiedResult handler(HttpMessageConversionException e) {
        return new UnifiedResult.Builder<>().error(UnifiedResult.ERROR, e.getMessage()).build();
    }

    /**
     * 参数格式异常
     */
    @ExceptionHandler(value = HttpMessageNotReadableException.class)
    public UnifiedResult handler(HttpMessageNotReadableException e) {
        return new UnifiedResult.Builder<>().error(UnifiedResult.ERROR, e.getMessage()).build();
    }

    /**
     * 请求方式异常
     */
    @ExceptionHandler(value = HttpRequestMethodNotSupportedException.class)
    public UnifiedResult handler(HttpRequestMethodNotSupportedException e) {
        return new UnifiedResult.Builder<>().error(UnifiedResult.ERROR, "请求方式错误").build();
    }

    /**
     * 媒体类型异常
     */
    @ExceptionHandler(value = HttpMediaTypeNotSupportedException.class)
    public UnifiedResult handler(HttpMediaTypeNotSupportedException e) {
        return new UnifiedResult.Builder<>().error(UnifiedResult.ERROR, "媒体类型错误").build();
    }

    /**
     * 请求参数丢失
     */
    @ExceptionHandler({MissingServletRequestParameterException.class})
    public UnifiedResult handler(MissingServletRequestParameterException e) {
        return new UnifiedResult.Builder<>().error(UnifiedResult.ERROR, e.getMessage()).build();
    }

    /**
     * 统一业务异常
     */
    @ExceptionHandler({BsException.class})
    public UnifiedResult exceptionHandler(BsException e) {
        log.error("BsException:", e);
        return new UnifiedResult.Builder<>().error(e.getCode(), e.getMessage()).build();
    }


    /**
     * 默认异常
     */
    @ExceptionHandler(value = Throwable.class)
    public UnifiedResult exceptionHandler(Throwable e) {
        log.error("UnifiedExceptionHandler: {}", ExceptionUtils.getStackTrace(e));
        return new UnifiedResult.Builder<>().error(UnifiedResult.ERROR, e.getMessage()).build();
    }
}

统一包装接口格式

  1. 全局格式
/**
 * @author cuishiying
 * @date 2021-01-22
 */
@RestControllerAdvice
public class UnifiedResponseBodyAdvice implements ResponseBodyAdvice<Object> {

    private static final String[] ignores = new String[]{
            //过滤swagger相关的请求的接口,不然swagger会提示base-url被拦截
            "/swagger-resources", "/swagger-ui", "/v3/api-docs"
    };

    @Override
    public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
//        return MappingJackson2HttpMessageConverter.class.isAssignableFrom(converterType) &&
//                (AnnotatedElementUtils.hasAnnotation(returnType.getContainingClass(), ResponseBody.class) || returnType.hasMethodAnnotation(ResponseBody.class));
        return !returnType.getGenericParameterType().equals(UnifiedResult.class) && !returnType.hasMethodAnnotation(UnifiedIgnore.class);
    }

    @Override
    public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType,
                                  Class<? extends HttpMessageConverter<?>> selectedConverterType, ServerHttpRequest request,
                                  ServerHttpResponse response) {

        if (this.ignoring(request.getURI().toString())) {
            return body;
        }

        if (body instanceof String) {
            ObjectMapper objectMapper = new ObjectMapper();
            try {
                return objectMapper.writeValueAsString(new UnifiedResult.Builder<>().data(body).build());
            } catch (JsonProcessingException e) {
                throw new RuntimeException("返回String类型错误");
            }
        }

        return new UnifiedResult.Builder<>().data(body).build();
    }

    private boolean ignoring(String uri) {
        for (String string : ignores) {
            if (uri.contains(string)) {
                return true;
            }
        }
        return false;
    }
}
  1. 无需按标准返回的方法注解
/**
 * @author cuishiying
 * @date 2021-05-25
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface UnifiedIgnore {
}

参数统一校验

目前只处理了RequestBody类型的参数。 由于Filter拿不到方法相关信息, 所以只能基于 Interceptor 或者 AOP 实现, AOP 实现需要指定 Controller 切面, 需要提取配置参数, 故选择 Interceptor 拦截所有。

  1. 注入拦截器
/**
 * @author cuishiying
 * @date 2021-01-22
 */
@Configuration
public class InterceptorAutoConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new ValidateInterceptor()).addPathPatterns("/**");
    }

}
  1. 拦截器实现
/**
 * @author cuishiying
 * @date 2021-01-22
 */
@Component
public class ValidateInterceptor implements HandlerInterceptor {

    private final Logger log = LoggerFactory.getLogger(getClass());

    private static final Validator validator = Validation.byProvider(HibernateValidator.class)
            .configure()
            .failFast(true)
            .buildValidatorFactory()
            .getValidator();

    ObjectMapper mapper = new ObjectMapper();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {

        if (HandlerMethod.class.isInstance(handler)) {
            HandlerMethod handlerMethod = (HandlerMethod) handler;
            MethodParameter[] methodParameters = handlerMethod.getMethodParameters();
            for (MethodParameter arg : methodParameters){
                if (arg != null && arg.hasParameterAnnotation(RequestBody.class)) {
                    RequestWrapper requestWrapper = new RequestWrapper(request);
                    String body = getRequestBody(requestWrapper);
                    Object o = mapper.readValue(body, arg.getParameterType());
                    Set<ConstraintViolation<Object>> constraintViolations = validator.validate(o);
                    if(!constraintViolations.isEmpty()){
                        throw new ConstraintViolationException(constraintViolations);
                    }
                }
            }
        }

        return HandlerInterceptor.super.preHandle(request, response, handler);
    }

    private String getRequestBody (HttpServletRequest request) throws IOException {
        return request.getReader().lines().collect(Collectors.joining(System.lineSeparator()));
    }
}
  1. 由于流只能被消费一次, 所有此处需要处理流消费的问题。这里不能在拦截器包装 HttpServletRequestWrapper 处理。因为流在拦截器消费一次后不会再向下传递。而在 FilterHttpServletRequestWrapper 处理, 流因为中间载体可以被多次消费, 而且向下游传递的是 HttpServletRequestWrapper
/**
 * @author cuishiying
 * @date 2021-01-22
 */
@Configuration
public class FilterAutoConfig {

    @Bean
    public FilterRegistrationBean<RepeatStreamFilter> traceFilterRegistration() {
        FilterRegistrationBean<RepeatStreamFilter> registration = new FilterRegistrationBean<>();
        registration.setOrder(Ordered.HIGHEST_PRECEDENCE + 100);
        registration.addUrlPatterns("/*");
        registration.setFilter(new RepeatStreamFilter());
        return registration;
    }

}
/**
 * @author cuishiying
 * @date 2021-01-22
 */
public class RepeatStreamFilter implements Filter {

    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        ServletRequest requestWrapper=null;
        if(request instanceof HttpServletRequest) {
            requestWrapper = new RequestWrapper((HttpServletRequest)request);
        }

        if(requestWrapper==null) {
            chain.doFilter(request, response);
        }else {
            chain.doFilter(requestWrapper, response);
        }
    }
}
/**
 * @author cuishiying
 * @date 2021-01-22
 */
public class RequestWrapper extends HttpServletRequestWrapper {

    private ByteArrayOutputStream cachedBytes;

    public RequestWrapper(HttpServletRequest request) {
        super(request);
    }

    @Override
    public ServletInputStream getInputStream() throws IOException {
        if (cachedBytes == null)
            cacheInputStream();
        return new RequestWrapper.CachedServletInputStream();
    }

    @Override
    public BufferedReader getReader() throws IOException {
        return new BufferedReader(new InputStreamReader(getInputStream()));
    }

    private void cacheInputStream() throws IOException {
        cachedBytes = new ByteArrayOutputStream();
        IOUtils.copy(super.getInputStream(), cachedBytes);
    }

    /* An inputstream which reads the cached request body */
    public class CachedServletInputStream extends ServletInputStream {
        private ByteArrayInputStream input;

        public CachedServletInputStream() {
            /* create a new input stream from the cached request body */
            input = new ByteArrayInputStream(cachedBytes.toByteArray());
        }

        @Override
        public int read() throws IOException {
            return input.read();
        }

        @Override
        public boolean isFinished() {
            return false;
        }

        @Override
        public boolean isReady() {
            return false;
        }

        @Override
        public void setReadListener(ReadListener listener) {
        }
    }
}

最后

本文到此结束,感谢阅读。如果您觉得不错,请关注公众号【当我遇上你】, 您的支持是我写作的最大动力。