1 整体说明
在前后端分离的Web开发中,为便于开发,有必要统一后端接口的返回结果格式以及统一处理全局异常。常见的统一格式如下:
{
"status":"100",
"message":"操作成功",
"data":"hello,world"
}
为了避免各个微服务都自行实现返回结果的封装和全局异常的处理,可将上述功能抽取为公共服务,其他服务引入公共服务直接使用。
通过@RestControllerAdvice
来拦截所有的@RestController
,@RestControllerAdvice
注解搭配ResponseBodyAdvice
接口可以实现在接口调用正常时的封装返回数据,@RestControllerAdvice
注解搭配@ExceptionHandler
注解可以实现在接口调用异常时封装指定异常类型的结果。本文就逐步说明如何搭建公共服务,并在其他服务中引用该公共服务。
2 公共服务搭建
2.1 服务自建自用
参考资料:SpringBoot 如何统一后端返回格式?老鸟们都是这样玩的!
2.1.1 定义返回结果格式
@Data
public class ResultData<T> {
private int status;
private String message;
private T data;
private long timestamp ;
public ResultData (){
this.timestamp = System.currentTimeMillis();
}
public static <T> ResultData<T> success(T data) {
ResultData<T> resultData = new ResultData<>();
resultData.setStatus(ReturnCode.RC100.getCode());
resultData.setMessage(ReturnCode.RC100.getMessage());
resultData.setData(data);
return resultData;
}
public static <T> ResultData<T> fail(int code, String message) {
ResultData<T> resultData = new ResultData<>();
resultData.setStatus(code);
resultData.setMessage(message);
return resultData;
}
}
/* 定义状态码 */
public enum ReturnCode {
/**操作成功**/
RC100(100,"操作成功"),
/**操作失败**/
RC999(999,"操作失败"),
/**服务限流**/
RC200(200,"服务开启限流保护,请稍后再试!"),
/**服务降级**/
RC201(201,"服务开启降级保护,请稍后再试!"),
/**热点参数限流**/
RC202(202,"热点参数限流,请稍后再试!"),
/**系统规则不满足**/
RC203(203,"系统规则不满足要求,请稍后再试!"),
/**授权规则不通过**/
RC204(204,"授权规则不通过,请稍后再试!"),
/**access_denied**/
RC403(403,"无访问权限,请联系管理员授予权限"),
/**access_denied**/
RC401(401,"匿名用户访问无权限资源时的异常"),
/**服务异常**/
RC500(500,"系统异常,请稍后重试"),
INVALID_TOKEN(2001,"访问令牌不合法"),
ACCESS_DENIED(2003,"没有权限访问该资源"),
CLIENT_AUTHENTICATION_FAILED(1001,"客户端认证失败"),
USERNAME_OR_PASSWORD_ERROR(1002,"用户名或密码错误"),
UNSUPPORTED_GRANT_TYPE(1003, "不支持的认证模式");
/**自定义状态码**/
private final int code;
/**自定义描述**/
private final String message;
ReturnCode(int code, String message){
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
2.1.2 封装接口异常的返回结果
@Slf4j
@RestControllerAdvice
public class RestExceptionHandler {
/**
* 默认全局异常处理。
*/
@ExceptionHandler(Exception.class)
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
public ResultData<String> exception(Exception e) {
log.error("全局异常信息 ex={}", e.getMessage(), e);
return ResultData.fail(ReturnCode.RC500.getCode(),e.getMessage());
}
}
2.1.3 封装接口正常的返回结果
借助@RestControllerAdvice
注解和ResponseBodyAdvice
接口实现。
@RestControllerAdvice
是@RestController
注解的增强,可以实现三个方面的功能:全局异常处理、全局数据绑定、全局数据预处理。
ResponseBodyAdvice
接口的作用:拦截Controller
方法的返回值,统一处理返回值/响应体,一般用来统一返回格式,加解密,签名等等。
@RestControllerAdvice //拦截Controller方法的返回值, 统一处理返回值/响应体, 一般用于统一返回格式、加解密、签名等等
public class RestResponseAdvice implements ResponseBodyAdvice<Object> {
@Autowired
private ObjectMapper objectMapper;
/*
* 是否支持advice功能
* true支持, false不支持
*/
@Override
public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
return true;
}
/*
* 处理返回的结果数据
*/
@SneakyThrows
@Override
public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType,
Class<? extends HttpMessageConverter<?>> aClass,
ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
if(o instanceof String){
return objectMapper.writeValueAsString(ResultData.success(o));
}
if (o instanceof ResultData) { // 对于已经是ResultData类型的结果不再封装, 直接返回
return o;
}
return ResultData.success(o);
}
}
2.1.4 在当前服务中验证
经过2.1.1~2.1.3节的处理,已经完成统一接口的返回数据格式和封装全局异常,这里直接在当前服务中创建2个接口进行验证。
@RestController
@RequestMapping("/api/demo")
public class RestDemo {
@GetMapping("/hello")
public String Hello() {
return "hello, world!";
}
@GetMapping("/wrong")
public int Wrong() {
return 3/0;
}
}
项目结构:
接口调用正常的结果:
接口调用异常的结果:
2.2 公共服务安装
参考资料:
为了在其他服务中能以maven依赖项的方式引用2.1节搭建的公共服务,需要把公共服务安装到本地maven仓库或者部署到远程仓库。这里采用安装到本地仓库的方式进行演示。
Spring Boot打包的是Spring Boot特有格式的jar包,即可以运行的fat jar(生成jar包中源码对应的class文件在BOOT-INF目录),并不是传统的maven的JAR包,为此需要修改公共服务的pom.xml
配置文件,修改如下:
<build>
<plugins>
<!-- <plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>-->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<source>1.8</source>
<target>1.8</target>
</configuration>
</plugin>
</plugins>
</build>
在IDEA的Terminal中执行maven项目安装命令:mvn clean install -Dmaven.test.skip=true
。
3 公共服务引用
搭建另一个SpringBoot Web业务服务,在这个服务中引用第2节搭建的公共服务以实现统一接口的返回结果格式的目的。
3.1 引入公共服务依赖项
公共服务的pom.xml
部分配置如下:
<groupId>com.example</groupId>
<artifactId>common-advice</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>common-advice</name>
<description>Public service that wraps interface results</description>
<properties>
<java.version>1.8</java.version>
</properties>
在业务服务的pom.xml
中添加依赖项:
<dependency>
<groupId>com.example</groupId>
<artifactId>common-advice</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
3.2 组件扫描路径添加公共服务
在业务服务的组件扫描路径(在服务入口增加@ComponentScan
配置)中增加公共服务的包路径,否则无法使@RestControllerAdvice
注解修饰的配置类生效。
查看@RestControllerAdvic
源码可知,它是封装了@ControllerAdvice
注解,而@ControllerAdvice
封装了@Component
注解,根据SpringBoot自动装配的规定,通过@ComponentScan
配置可以将@RestControllerAdvic
注解标记的组件加载。
@ComponentScan({"com.example.demorest", "com.example.common.advice"})
@SpringBootApplication
public class DemorestApplication {
public static void main(String[] args) {
SpringApplication.run(DemorestApplication.class, args);
}
}
3.3 编写接口验证
@RestController
@RequestMapping("/api/original")
public class RestOriginal {
@GetMapping("/success")
public String GetSuccess() {
return "hello, don!";
}
@GetMapping("/error")
public int GetError() {
return 6/0;
}
}
项目结构:
接口访问正常的结果:
接口访问异常的结果:
4 补充说明
通过调试我们可以发现,@RestControllerAdvice
注解搭配的ResponseBodyAdvice
接口和ExceptionHandler
注解都是在controller接口方法体已经执行完成后在开始应用。