SpringBoot自定义starter之标准化处理

前言

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

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

实现

pom.xml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
<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 目录下

1
2
3
4
5
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
cn.idea360.unified.UnifiedResponseBodyAdvice,\
cn.idea360.unified.UnifiedExceptionHandler,\
cn.idea360.unified.filter.FilterAutoConfig,\
cn.idea360.unified.interceptor.InterceptorAutoConfig

通用异常处理

  1. 返回格式
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
/**
* @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. 异常拦截
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
/**
* @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
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
public class BsException extends RuntimeException {
private Integer code;
private String message;
private String stackTrace;
private transient Throwable throwable;

public BsException() {
}

public BsException(String message) {
super(message);
this.code = this.formatErrCode(-1);
this.message = message;
}

public BsException(int code, String message) {
super(message);
this.code = this.formatErrCode(code);
this.message = message;
}

public BsException(int code, Throwable throwable) {
super(throwable);
this.code = this.formatErrCode(code);
this.message = throwable.getMessage();
this.stackTrace = ExceptionUtils.getStackTrace(throwable);
this.throwable = throwable;
}

public BsException(int code, String message, Throwable throwable) {
super(message, throwable);
this.code = this.formatErrCode(code);
this.message = message;
this.throwable = throwable;
this.stackTrace = ExceptionUtils.getStackTrace(throwable);
}

private int formatErrCode(int errCode) {
return errCode == 0 ? -1 : errCode;
}

public Integer getCode() {
return this.code;
}

public String getMessage() {
if (!StringUtils.isEmpty(this.message)) {
return this.message;
} else {
return this.throwable != null ? this.throwable.getMessage() : "";
}
}

public String getStackTrace2String() {
return this.stackTrace == null ? "" : this.stackTrace;
}

public String toString() {
Integer var10000 = this.getCode();
return "ErrCode:" + var10000 + ", ErrMsg:" + this.getMessage();
}
}

统一包装接口格式

  1. 全局格式

首先, 我们应该允许上层配置白名单, 所以先从配置文件读取配置。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @author cuishiying
* @date 2021-06-11
*/
@ConfigurationProperties(prefix = UnifiedProperties.UNIFIED_PREFIX)
public class UnifiedProperties {

public static final String UNIFIED_PREFIX = "easyliao.framework.unified";

private final List<String> ignores = new ArrayList<>();

public List<String> getIgnores() {
return ignores;
}
}

然后配置全局返回格式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
/**
* @author cuishiying
* @date 2021-01-22
*/
@EnableConfigurationProperties(UnifiedProperties.class)
@RestControllerAdvice
public class UnifiedResponseBodyAdvice implements ResponseBodyAdvice<Object>, BeanFactoryAware {

@Resource
private UnifiedProperties unifiedProperties;

private static ObjectMapper objectMapper;

private static final String[] innerIgnores = 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 (returnType.getGenericParameterType().equals(String.class)) {
try {
response.getHeaders().set("Content-Type", "application/json;charset=utf-8");
return objectMapper.writeValueAsString(new UnifiedResult.Builder<>().data(body).build());
} catch (JsonProcessingException e) {
e.printStackTrace();
throw new RuntimeException("返回String类型错误");
}
}

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

private boolean ignoring(String uri) {
for (String string : innerIgnores) {
if (uri.contains(string)) {
return true;
}
}
for (String string : unifiedProperties.getIgnores()) {
if (uri.contains(string)) {
return true;
}
}
return false;
}

@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
objectMapper = beanFactory.getBean(ObjectMapper.class);
}
}
  1. 无需按标准返回的方法注解
1
2
3
4
5
6
7
8
9
/**
* @author cuishiying
* @date 2021-05-25
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface UnifiedIgnore {
}

参数统一校验

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

  1. 注入拦截器
1
2
3
4
5
6
7
8
9
10
11
12
13
/**
* @author cuishiying
* @date 2021-01-22
*/
@Configuration
public class InterceptorAutoConfig implements WebMvcConfigurer {

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

}
  1. 拦截器实现
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
/**
* @author cuishiying
* @date 2021-01-22
*/
@Component
public class ValidateInterceptor implements HandlerInterceptor, BeanFactoryAware {

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

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

private static ObjectMapper 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) && !arg.getParameterType().isAssignableFrom(String.class)) {
RequestWrapper requestWrapper = new RequestWrapper(request);
String body = getRequestBody(requestWrapper);
Object o = objectMapper.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()));
}

@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
objectMapper = beanFactory.getBean(ObjectMapper.class);
}
}
  1. 由于流只能被消费一次, 所有此处需要处理流消费的问题。这里不能在拦截器包装 HttpServletRequestWrapper 处理。因为流在拦截器消费一次后不会再向下传递。而在 FilterHttpServletRequestWrapper 处理, 流因为中间载体可以被多次消费, 而且向下游传递的是 HttpServletRequestWrapper
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @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;
}

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* @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);
}
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
/**
* @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) {
}
}
}

最后

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