有时候有这样的需求,接口需要返回统一格式的响应参数,如下:

1
2
3
4
5
6
{
"status":200,
"message":"请求成功!",
"result":"响应参数对象",
"timestamp":1559815994252
}

所有的响应结果对象都封装在result对象中,或许我们会在每个controller层接口都类似这样操作:

1
2
3
4
5
6
7
8
9
10
@GetMapping(value = "test")
public JSONObject test(){
Person person = new Person("张三",23,"男");
JSONObject jsonObject = new JSONObject();
jsonObject.put("status",200);
jsonObject.put("message","请求成功!");
jsonObject.put("result",person);
jsonObject.put("timestamp",System.currentTimeMillis());
return jsonObject;
}

​ 可想而知,这样造成巨大工作量且不说,而且和代码耦合度太高,不利于项目的维护,可能大家会想到使用AOP,对,也是可以解决,但是如果多个项目都使用这种统一的响应参数,那是否可以利用springboot的starter功能,做个插拔式的,可以供多个项目使用?
显而易见,使用springboot的starter更好,下面我们就开始来创建这样的一个starter,源码下载地址https://github.com/sunnymaple/rest-spring-boot-starter.git

1、项目工程的创建

①、创建rest-spring-boot-startar空的工程
②、在该工程下分别创建
startar:rest-spring-boot-startar,pom如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>cn.sunnymaple</groupId>
<artifactId>rest-spring-boot-starter</artifactId>
<version>1.0.0</version>

<dependencies>
<dependency>
<groupId>cn.sunnymaple</groupId>
<artifactId>rest-spring-boot-starter-autoconfigure</artifactId>
<version>1.0.0</version>
</dependency>
</dependencies>

</project>

autoconfigure:rest-spring-boot-starter-autoconfigure,pom如下:

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
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.1.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>cn.sunnymaple</groupId>
<artifactId>rest-spring-boot-starter-autoconfigure</artifactId>
<version>1.0.0</version>
<name>rest-spring-boot-starter-autoconfigure</name>
<description>Demo project for Spring Boot</description>

<properties>
<java.version>1.8</java.version>
</properties>

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.4</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.51</version>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-source-plugin</artifactId>
<executions>
<execution>
<id>attach-sources</id>
<goals>
<goal>jar</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>

</project>

​ 其中startar模块用于引autoconfigure模块的包,而在autoconfigure模块里实现具体的功能,创建过程请参考文章《三、SpringBoot自定义Starter》,项目结构如下:

图片

2、功能的实现

主要列入一些主要的类

2.1、创建HttpStatusEnum

定义响应状态码status,以及状态码对应的message信息:

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
public enum HttpStatusEnum {
/**
* Http请求状态码 - 请求成功 200
*/
OK(HttpStatus.OK.value(),"请求成功!"),
/**
* 404
*/
NOT_FOUND(HttpStatus.NOT_FOUND.value(), "资源未找到!"),
/**
* Http请求状态码 - 请求失败 500
*/
INTERNAL_SERVER_ERROR(HttpStatus.INTERNAL_SERVER_ERROR.value(),"系统繁忙,请稍后重试!");
//TODO 可以根据需求定义更多的状态码

HttpStatusEnum(int status, String message) {
this.status = status;
this.message = message;
}

/**
* 状态码
*/
private int status;
/**
* 状态码对应的信息
*/
private String message;

public int getStatus() {
return status;
}

public String getMessage() {
return message;
}
}

2.2、创建RestResult

用于封装接口的响应参数

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
public class RestResult {
/**
* 响应结果的状态码,默认为200,200表示请求成功,其他为异常
*/
private Integer status = HttpStatusEnum.OK.getStatus();
/**
* 响应结果信息
*/
private String message = HttpStatusEnum.OK.getMessage();
/**
* 响应结果对象,所有接口返回的对象都存放在result对象中
*/
private Object result;
/**
* 时间戳
*/
private long timestamp = System.currentTimeMillis();

public RestResult() {
}

public RestResult(Integer status, String message, Object result) {
this.status = status;
this.message = message;
this.result = result;
}

public RestResult(Integer status, String message) {
this.status = status;
this.message = message;
}

public RestResult(String message, Object result) {
this.message = message;
this.result = result;
}

public RestResult(Object result) {
this.result = result;
}

/**
* result为null
* @return
*/
public static RestResult factory(){
return new RestResult(HttpStatusEnum.OK.getMessage(),null);
}

/**
* result为null
* @return
*/
public static RestResult factory(Object result){
return new RestResult(HttpStatusEnum.OK.getMessage(),result);
}
//省略Geter/Seter
}

2.3、创建AppResponseHandlerProperties

该对象是配置类,定义了开启与关闭响应统一格式的响应参数的功能,其中:
①、? 匹配单个字符,如/demo? 则/demo1、/demo2会被匹配 而/demo12则不被匹配
②、* 除/外的任意字符 如:/demo* 则 /demo1、/demo2、/demo12会被匹配 而/demo/1不会被匹配,如:/demo/* 可以匹配/demo/1 /demo/2 /demo/12
③、/** 匹配任意的多个目录 如:/demo/** 可以匹配/demo/1 /demo/2 /demo/12 /demo/1/2等

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@ConfigurationProperties(prefix = "response.handler")
public class AppResponseHandlerProperties {
/**
* 是否启用AppResponseHandler处理器
* true 启用
* false 停用
*/
private boolean enabled;
/**
* 定义不需要使用AppResponseHandler的接口
* 支持通配符:? /* /**
*/
private String[] nonResponseHandler;

}

2.4、创建NonResponseHandler

​ 它是一个注解,可以加在controller层的类和方法上,加在类上表示该controller的所有接口都不使用统一格式的响应参数,加在方法上表示该方法(接口)不使用统一格式的响应参数。

1
2
3
4
5
@Target({ElementType.TYPE,ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NonResponseHandler {
}

2.5、创建AppResponseHandler

​ 该类是响应统一格式的参数功能实现的主角,它实现ResponseBodyAdvice接口,并在该类上加上@ControllerAdvice注解。它们的作用分别是:
@ControllerAdvice:在spring 3.2中,新增了@ControllerAdvice 注解,可以用于定义@ExceptionHandler、@InitBinder、@ModelAttribute,并应用到所有@RequestMapping中,参考@ControllerAdvice 文档
** ResponseBodyAdvice:**ResponseBodyAdvice是一个接口,它的源码如下:

1
2
3
4
5
6
7
8
9
10
public interface ResponseBodyAdvice<T> {

boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType);

@Nullable
T beforeBodyWrite(@Nullable T body, MethodParameter returnType, MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request, ServerHttpResponse response);

}

​ 其中supports这个方法返回true时会调用beforeBodyWrite方法,返回false则不会执行beforeBodyWrite,所有我们可以在supports方法中定义响应统一格式的参数的规则,而在beforeBodyWrite方法中将响应参数进行封装成我们想要的格式。

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
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
@ControllerAdvice
@Slf4j
public class AppResponseHandler implements ResponseBodyAdvice {

@Autowired
private HttpServletRequest request;

@Autowired
private AppResponseHandlerProperties properties;

@Override
public boolean supports(MethodParameter methodParameter,
Class aClass) {
//判断类是否加上了NoResponseHandler注解,如果是则返回false
NonResponseHandler classNoResponseHandler =
methodParameter.getDeclaringClass()
.getAnnotation(NonResponseHandler.class);
if (!Utils.isEmpty(classNoResponseHandler)){
return false;
}
//判断方法(接口)是否加上了NoResponseHandler注解,如果是则返回false
NonResponseHandler noResponseHandler =
methodParameter.getMethodAnnotation(NonResponseHandler.class);
if (!Utils.isEmpty(noResponseHandler)){
return false;
}
//从配置文件中获取过滤的接口
String[] nrhs = properties.getNonResponseHandler();
if (!Utils.isEmpty(nrhs)){
String path = request.getServletPath();
for (String nrh : nrhs){
AntPathMatcher matcher = new AntPathMatcher();
boolean match = matcher.match(nrh, path);
if (match){
return false;
}
}
}
return true;
}

@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
Object result = null;
try {
//封装统一格式的响应参数
result = restResult(o,mediaType,serverHttpRequest,serverHttpResponse,aClass);
} catch (IOException e) {
log.info("创建统一格式的响应数据异常!",e);
}
return result;
}

//省略一些方法

private Object restResult(Object o,MediaType mediaType,ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse,Class aClass) throws IOException {
/*
* 1:判断是否为null
*/
if (o == null){
RestResult restResult = RestResult.factory();
if (isJsonResponse(mediaType) &&
StringHttpMessageConverter.class == aClass){
return JSONObject.toJSONString(restResult,
SerializerFeature.WriteMapNullValue);
}
return restResult;
}
/*
* 2、对于响应的是Resource资源对象(如:文件下载),则直接返回Object对象
* 对于响应对象本身就是RestResult对象,则不再次封装,直接返回
*/
if (o instanceof Resource || o instanceof RestResult){
return o;
}
/*
* 3、判断是否返回的是Map
* 如果是:判断请求状态码,如果状态码为非200,则说明请求失败,则获取错误信息RestResult
*/
int successCode = HttpStatusEnum.OK.getStatus();
if (o instanceof Map){
HttpServletResponse response = ((ServletServerHttpResponse) serverHttpResponse).getServletResponse();
Integer status = response.getStatus();
if (!Objects.equals(status, successCode)){
HttpServletRequest request = ((ServletServerHttpRequest) serverHttpRequest).getServletRequest();
RestResult restResult = (RestResult) request.getAttribute("result");
if (!Utils.isEmpty(restResult)){
return restResult;
}else {
Map<String,Object> error = (Map<String, Object>) o;
return Utils.map2Object(error, RestResult.class);
}
}else {
return RestResult.factory(o);
}
}
if (o instanceof String){
/* 按道理 只要API 返回值是 String / byte[]等
* 不会由MappingJackson2HttpMessageConverter处理的返回值
* 都有可能出错,抛出ClassCastException...
* 目前API 出现的比较多的是String,所以只处理String情况
* 如果 API返回的是 String,
*/
try {
/*
* String返回值 将被StringHttpMessageConverter处理,
* 所以此时应该返回RestResult的json序列化后的字符串
* 如果此时还是返回RestResult对象,会抛出ClassCastException
* 因为StringHttpMessageConverter会把RestResult对象当做String处理
*/
return writeValueAsString(RestResult.factory(o));
}catch (JsonProcessingException e) {
log.warn("json serialize error", e);
return o;
}
}
return RestResult.factory(o);
}
}

2.6、创建AppResponseHandlerAutoConfiguration

该类是一个自定配置类,

1
2
3
4
5
6
7
8
9
10
@Configuration
//在web条件下才自定配置
@ConditionalOnWebApplication
//且response.handler.enabled=true时,默认也是true
@ConditionalOnProperty(name = "response.handler.enabled",havingValue = "true",matchIfMissing = true)
@EnableConfigurationProperties({AppResponseHandlerProperties.class})
//导入2.5中的类AppResponseHandler,并注入到springAOP中
@Import(AppResponseHandler.class)
public class AppResponseHandlerAutoConfiguration {
}

2.7、配置spring.factories以及元数据

spring.factories:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=
cn.sunnymaple.rest.response.AppResponseHandlerAutoConfiguration

spring-configuration-metadata.json:

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
{
"hints":[{
"name":"response.handler.enabled",
"values":[{
"value":true,
"description":"启用,默认值"
},{
"value":false,
"description":"禁用"
}]
}],
"groups":[{
"sourceType": "cn.sunnymaple.rest.response.AppResponseHandlerProperties",
"name": "response.handler",
"type": "cn.sunnymaple.rest.response.AppResponseHandlerProperties"
}],
"properties":[
{
"sourceType": "cn.sunnymaple.rest.response.AppResponseHandlerProperties",
"name": "response.handler.enabled",
"type": "java.lang.Boolean",
"description": "是否启用统一格式的响应参数",
"defaultValue": true
},{
"sourceType": "cn.sunnymaple.rest.response.AppResponseHandlerProperties",
"name": "response.handler.non-response-handler",
"type": "java.lang.String[]",
"description": "定义响应参数不封装成固定格式的接口"
}]
}

​ 到目前为止,响应统一格式的自定义Startar就创建成功了,接下来就是使用install进行打包,之后就可以在其他项目中使用。

3、自定义starter的使用

新建一个项目,在pom中引入包:

1
2
3
4
5
<dependency>
<groupId>cn.sunnymaple</groupId>
<artifactId>rest-spring-boot-starter</artifactId>
<version>1.0.0</version>
</dependency>

①、可以使用response.handler.enabled直接关闭该功能;
②、在properties或者yml文件中使用response.handler.non-response-handler指定那些接口不使用该功能;
③、可以使用@NonResponseHandler 注解指定哪些Controller层接口不使用该功能。

× 请我吃糖~
打赏二维码