共计 6947 个字符,预计需要花费 18 分钟才能阅读完成。
前言
最近在优化自己之前基于 Spring AOP
的统一响应体的实现方案。
什么是统一响应体呢?在目前的前后端分离架构下,后端主要是一个 RESTful API
的数据接口。
但是 HTTP
的状态码数量有限,而随着业务的增长,HTTP
状态码无法很好地表示业务中遇到的异常情况。
那么可以通过修改响应返回的 JSON
数据,让其带上一些固有的字段,例如以下这样的
{ | |
"code": 10000, | |
"msg": "success", | |
"data": { | |
"id": 2, | |
"name": "test" | |
} | |
} |
其中关键属性的用途如下:
-
code
为返回结果的状态码 -
msg
为返回结果的消息 -
data
为返回的业务数据
这 3
个属性为固有属性,每次响应结果都会有带有它们。
需求
希望实现一个能够代替基于 AOP
的实现方案,需要满足以下几点:
- 原有的基于
AOP
的实现方案需要Controller
的返回类型为Object
,需要新方案不限制返回类型 - 原有的基于
AOP
的实现方案需要通过切面表达式 + 注解控制切点的Controller
(注解的包名修改会导致切面表达式的修改,即需要修改两处地方),需要新方案能够基于注解,而不需要修改切面表达式
方案思路
基于上述的需求,选择使用 Spring
的Controller
增强机制,其中关键的类为以下 3
个:
-
@ControllerAdvice
:类注解,用于指定Controller
增强处理器类。 -
ResponseBodyAdvice
:接口,实现后beforeBodyWrite()
方法后可以对响应的body
进行修改,需要结合@ControllerAdvice
使用。 -
@ExceptionHandler
:方法注解,用于指定异常处理方法,需要结合@ControllerAdvice
和@ResponseBody
使用。
示例关键代码
本示例使用的 Spring Boot
版本为 2.1.6.RELEASE
,同时需要开发工具安装lombok
插件
引入依赖
<dependencies> | |
<!--web-starter--> | |
<dependency> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-starter-web</artifactId> | |
</dependency> | |
<!--lombok--> | |
<dependency> | |
<groupId>org.projectlombok</groupId> | |
<artifactId>lombok</artifactId> | |
<optional>true</optional> | |
</dependency> | |
<!--test-starter--> | |
<dependency> | |
<groupId>org.springframework.boot</groupId> | |
<artifactId>spring-boot-starter-test</artifactId> | |
<scope>test</scope> | |
</dependency> | |
</dependencies> |
统一响应体
Controller
增强后统一响应体对应的对象
import lombok.AllArgsConstructor; | |
import lombok.Data; | |
import java.io.Serializable; | |
/** | |
* 统一的公共响应体 | |
* @author NULL | |
* @date 2019-07-16 | |
*/ | |
@Data | |
@AllArgsConstructor | |
public class ResponseResult implements Serializable { | |
/** | |
* 返回状态码 | |
*/ | |
private Integer code; | |
/** | |
* 返回信息 | |
*/ | |
private String msg; | |
/** | |
* 数据 | |
*/ | |
private Object data; | |
} |
统一响应注解
统一响应注解是一个标记是否开启统一响应增强的注解
import java.lang.annotation.ElementType; | |
import java.lang.annotation.Retention; | |
import java.lang.annotation.RetentionPolicy; | |
import java.lang.annotation.Target; | |
/** | |
* 统一响应注解 <br/> | |
* 添加注解后,统一响应体才能生效 | |
* @author NULL | |
* @date 2019-07-16 | |
*/ | |
@Retention(RetentionPolicy.RUNTIME) | |
@Target({ElementType.METHOD, ElementType.TYPE}) | |
public @interface BaseResponse {} |
状态码枚举
统一响应体中返回的状态码 code
和状态信息 msg
对应的枚举类
/** | |
* 返回状态码 | |
* | |
* @author NULL | |
* @date 2019-07-16 | |
*/ | |
public enum ResponseCode { | |
/** | |
* 成功返回的状态码 | |
*/ | |
SUCCESS(10000, "success"), | |
/** | |
* 资源不存在的状态码 | |
*/ | |
RESOURCES_NOT_EXIST(10001, "资源不存在"), | |
/** | |
* 所有无法识别的异常默认的返回状态码 | |
*/ | |
SERVICE_ERROR(50000, "服务器异常"); | |
/** | |
* 状态码 | |
*/ | |
private int code; | |
/** | |
* 返回信息 | |
*/ | |
private String msg; | |
ResponseCode(int code, String msg) { | |
this.code = code; | |
this.msg = msg; | |
} | |
public int getCode() {return code;} | |
public String getMsg() {return msg;} | |
} |
业务异常类
业务异常类是用于识别业务相关的异常,需要注意这个异常类强制需要以 ResponseCode
作为构造方法入参,这样可以通过捕获异常获得返回的状态码信息
import com.rjh.web.response.ResponseCode; | |
import lombok.Data; | |
import lombok.EqualsAndHashCode; | |
/** | |
* 业务异常类,继承运行时异常,确保事务正常回滚 | |
* | |
* @author NULL | |
* @since 2019-07-16 | |
*/ | |
@Data | |
@EqualsAndHashCode(callSuper = false) | |
public class BaseException extends RuntimeException{ | |
private ResponseCode code; | |
public BaseException(ResponseCode code) {this.code = code;} | |
public BaseException(Throwable cause, ResponseCode code) {super(cause); | |
this.code = code; | |
} | |
} |
异常处理类
用于处理 Controller
运行时未捕获的异常的处理类。
import com.rjh.web.exception.BaseException; | |
import lombok.extern.slf4j.Slf4j; | |
import org.springframework.web.bind.annotation.ControllerAdvice; | |
import org.springframework.web.bind.annotation.ExceptionHandler; | |
import org.springframework.web.bind.annotation.ResponseBody; | |
/** | |
* 异常处理器 | |
* | |
* @author NULL | |
* @since 2019-07-16 | |
*/ | |
@ControllerAdvice(annotations = BaseResponse.class) | |
@ResponseBody | |
@Slf4j | |
public class ExceptionHandlerAdvice { | |
/** | |
* 处理未捕获的 Exception | |
* @param e 异常 | |
* @return 统一响应体 | |
*/ | |
@ExceptionHandler(Exception.class) | |
public ResponseResult handleException(Exception e){log.error(e.getMessage(),e); | |
return new ResponseResult(ResponseCode.SERVICE_ERROR.getCode(),ResponseCode.SERVICE_ERROR.getMsg(),null); | |
} | |
/** | |
* 处理未捕获的 RuntimeException | |
* @param e 运行时异常 | |
* @return 统一响应体 | |
*/ | |
@ExceptionHandler(RuntimeException.class) | |
public ResponseResult handleRuntimeException(RuntimeException e){log.error(e.getMessage(),e); | |
return new ResponseResult(ResponseCode.SERVICE_ERROR.getCode(),ResponseCode.SERVICE_ERROR.getMsg(),null); | |
} | |
/** | |
* 处理业务异常 BaseException | |
* @param e 业务异常 | |
* @return 统一响应体 | |
*/ | |
@ExceptionHandler(BaseException.class) | |
public ResponseResult handleBaseException(BaseException e){log.error(e.getMessage(),e); | |
ResponseCode code=e.getCode(); | |
return new ResponseResult(code.getCode(),code.getMsg(),null); | |
} | |
} |
响应增强类
Conrtoller
增强的统一响应体处理类,需要注意异常处理类已经进行了增强,所以需要判断一下返回的对象是否为统一响应体对象。
import lombok.extern.slf4j.Slf4j; | |
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.ControllerAdvice; | |
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice; | |
/** | |
* 统一响应体处理器 | |
* @author NULL | |
* @date 2019-07-16 | |
*/ | |
@ControllerAdvice(annotations = BaseResponse.class) | |
@Slf4j | |
public class ResponseResultHandlerAdvice implements ResponseBodyAdvice { | |
@Override | |
public boolean supports(MethodParameter returnType, Class converterType) {log.info("returnType:"+returnType); | |
log.info("converterType:"+converterType); | |
return true; | |
} | |
@Override | |
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {if(MediaType.APPLICATION_JSON.equals(selectedContentType) || MediaType.APPLICATION_JSON_UTF8.equals(selectedContentType)){ // 判断响应的 Content-Type 为 JSON 格式的 body | |
if(body instanceof ResponseResult){ // 如果响应返回的对象为统一响应体,则直接返回 body | |
return body; | |
}else{ | |
// 只有正常返回的结果才会进入这个判断流程,所以返回正常成功的状态码 | |
ResponseResult responseResult =new ResponseResult(ResponseCode.SUCCESS.getCode(),ResponseCode.SUCCESS.getMsg(),body); | |
return responseResult; | |
} | |
} | |
// 非 JSON 格式 body 直接返回即可 | |
return body; | |
} | |
} |
使用示例
首先准备一个 User
对象
import lombok.Data; | |
import lombok.EqualsAndHashCode; | |
import java.io.Serializable; | |
/** | |
* 用户类 | |
* @author NULL | |
* @date 2019-07-16 | |
*/ | |
@Data | |
@EqualsAndHashCode | |
public class User implements Serializable { | |
private Integer id; | |
private String name; | |
} |
然后是准备一个简单的 UserController
即可
import com.rjh.web.entity.User; | |
import com.rjh.web.exception.BaseException; | |
import com.rjh.web.response.BaseResponse; | |
import com.rjh.web.response.ResponseCode; | |
import org.springframework.web.bind.annotation.GetMapping; | |
import org.springframework.web.bind.annotation.PathVariable; | |
import org.springframework.web.bind.annotation.RequestMapping; | |
import org.springframework.web.bind.annotation.RestController; | |
/** | |
* 测试用的 Controller | |
* | |
* @author NULL | |
* @date 2019-07-16 | |
*/ | |
@BaseResponse | |
@RestController | |
@RequestMapping("users") | |
public class UserController {@GetMapping("/{userId}") | |
public User getUserById(@PathVariable Integer userId){if(userId.equals(0)){throw new BaseException(ResponseCode.RESOURCES_NOT_EXIST); | |
} | |
if(userId.equals(1)){throw new RuntimeException(); | |
} | |
User user=new User(); | |
user.setId(userId); | |
user.setName("test"); | |
return user; | |
} | |
} |
运行结果
-
在浏览器直接访问
http://127.0.0.1:8080/users/0
,则返回结果如下(结果经过格式化处理):{ "code": 10001, "msg": "资源不存在", "data": null } -
在浏览器直接访问
http://127.0.0.1:8080/users/1
,则返回结果如下(结果经过格式化处理):{ "code": 50000, "msg": "服务器异常", "data": null } -
在浏览器直接访问
http://127.0.0.1:8080/users/2
,则返回结果如下(结果经过格式化处理):{ "code": 10000, "msg": "success", "data": { "id": 2, "name": "test" } }
由运行结果可以得知统一响应增强其实已经生效了,而且能够很好的处理异常。
示例代码地址
下面是这个示例的代码地址,如果觉得不错或者帮助到你,希望大家给个Star
:
https://github.com/spring-bas…
参考资料
- https://docs.spring.io/spring…
- https://docs.spring.io/spring…