在 SpringBoot 封装统一响应体(一) 中介绍了使用统一结果类 ServerResponse
来封装统一响应体对象,需要用类似 ServerResponse.ok(data)
的形式进行响应。
在 Spring 3.2 中,新增了 @ControllerAdvice
,是一个 Controller 增强器,可对 Controller 中被 @RequestMapping
注解的方法加一些逻辑处理,最常用的就是异常处理。这篇文章介绍一种通过 @ControllerAdvice
和基于 AOP 实现的统一响应体方案。
1. 创建统一结果类
为了方便返回结果,这里还是创建一个统一结果类,其中 lombok 的 @AllArgsConstructor
注解用来向类中添加全参构造方法:
@Data
@AllArgsConstructor
public class ServerResponse {
private Boolean success;
private Integer code;
private String message;
private Object data;
}
2. 创建统一响应注解
统一响应注解是一个标记是否开启统一响应增强的注解:
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.METHOD, ElementType.TYPE})
public @interface BaseResponse {
}
3. 创建结果枚举类
结果枚举类还是复用之前的 ResultEnum
:
@Getter
@AllArgsConstructor
public enum ResultEnum {
OK(true, 200, "成功"),
BAD_REQUEST(false, 400, "参数错误"),
UNAUTHORIZED(false, 401, "未授权,请登录"),
NOT_FOUND(false, 404, "找不到请求的资源");
private Boolean success;
private Integer code;
private String message;
}
这边使用了
@AllArgsConstructor
创建全参构造方法
4. 创建业务异常类
业务异常类是用于识别业务相关的异常,使用 ResponseCode
作为入参可以通过捕获异常获得返回的状态码信息:
@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;
}
}
这里有几个注意点:
- 自定义的异常可以继承
RuntimeException
,也可以继承Exception
。Spring 对于RuntimeException
类的异常才会进行事务回滚,所以我们一般自定义异常都继承该异常类; @EqualsAndHashCode
注解会生成equals(Object other)
和hashCode()
方法。有时候我们需要重写一个类的@equals
和@hashcode
方法,就可以使用@EqualsAndHashCode
注解;@Data
相当于@Getter
、@Setter
、@RequiredArgsConstructor
、@ToString
、@EqualsAndHashCode
这5个注解的合集;@EqualsAndHashCode(callSuper = false)
不调用父类的属性,如果子类属性相同,那么两个对象的 hashCode 值就相等;
在实际开发的时候,不同的异常除了通过状态码区分,还可以自定义不同的异常类,方便后面进行异常处理:
public class IllegalArgumentsException extends RuntimeException {
// ...
}
public class UnauthorizedException extends RuntimeException{
// ...
}
5. 创建异常处理类
在 Controller 中抛出异常,这里进行异常处理:
@ControllerAdvice(annotations = BaseResponse.class)
@Slf4j
public class GlobalExceptionHandler {
/**
* 处理 400 错误
* @return
*/
@ResponseBody // 不加 @ResponseBody 返回的是框架默认的响应体
@ExceptionHandler(value = IllegalArgumentsException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ServerResponse handleIllegalArgumentsException() {
log.info("参数错误");
return new ServerResponse(
ResultEnum.BAD_REQUEST.getSuccess(),
ResultEnum.BAD_REQUEST.getCode(),
ResultEnum.BAD_REQUEST.getMessage(),
null
);
}
/**
* 处理 401 错误
* @return
*/
@ResponseBody
@ExceptionHandler(value = UnauthorizedException.class)
@ResponseStatus(HttpStatus.UNAUTHORIZED)
public ServerResponse handleUnauthorizedException() {
log.info("未授权,请登录");
return new ServerResponse(
ResultEnum.UNAUTHORIZED.getSuccess(),
ResultEnum.UNAUTHORIZED.getCode(),
ResultEnum.UNAUTHORIZED.getMessage(),
null
);
}
}
这里需要注意 @ExceptionHandler
注解传入的参数可以是一个数组,但是传入的参数不能相同,也就是不能使用两个@ExceptionHandler去处理同一个异常。
还有一个问题,在自定义类上面使用 @ControllerAdvice
注解,为了能返回自己封装的响应体,需要给成员方法都加上 @ResponseBody
。如果不加 @ResponseBody
注解,例如下面的代码:
@ControllerAdvice(annotations = BaseResponse.class)
public class GlobalExceptionHandler {
@ExceptionHandler(value = IllegalArgumentsException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ServerResponse handleIllegalArgumentsException() {
return new ServerResponse(
ResultEnum.BAD_REQUEST.getSuccess(),
ResultEnum.BAD_REQUEST.getCode(),
ResultEnum.BAD_REQUEST.getMessage(),
null
);
}
}
响应的结果如下,可以看到是框架默认的响应体:
加上 @ResponseBody
之后就可以正常响应了:
顺便再解释一下 @ControllerAdvice
和 @RestControllerAdvice
的区别。简单地说,@RestControllerAdvice
注解包含了 @ControllerAdvice
注解和 @ResponseBody
注解:
@RestController
=@Controller
+@ResponseBody
;@RestControllerAdvice
=@ControllerAdvice
+@ResponseBody
;
如果自定义类上面添加了 @RestControllerAdvice
注解,返回自己封装的响应体就无需再用 @ResponseBody
:
@RestControllerAdvice(annotations = BaseResponse.class)
public class GlobalExceptionHandler {
@ExceptionHandler(value = IllegalArgumentsException.class)
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ServerResponse handleIllegalArgumentsException() {
return new ServerResponse(
ResultEnum.BAD_REQUEST.getSuccess(),
ResultEnum.BAD_REQUEST.getCode(),
ResultEnum.BAD_REQUEST.getMessage(),
null
);
}
}
6. 创建响应增强类
创建一个响应增强类 ServerResponseAdvice
实现 ResponseBodyAdvice
接口:
@ControllerAdvice(annotations = BaseResponse.class)
@Slf4j
public class ServerResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
log.info("returnType:" + returnType);
log.info("converterType:" + converterType);
return true;
}
@Override
public ServerResponse beforeBodyWrite(
Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response
) {
return new ServerResponse(
ResultEnum.OK.getSuccess(),
ResultEnum.OK.getCode(),
ResultEnum.OK.getMessage(), // 成功展示默认提示信息
body // 传给前端的参数
);
}
}
这里需要实现 supports
和 beforeBodyWrite
两个方法:
supports
返回布尔值,用来判断哪些方法需要拦截(例如需要鉴权的请求),returnType
参数可以拿到调用的方法名;beforeBodyWrite
就是在响应前的拦截操作,可以对响应头和响应体的内容进行修改,body
参数就是 Controller 方法中返回的内容;
如果不想在每一个接口都写一遍 ServerResponse.ok(data)
这样的代码,就可以使用 beforeBodyWrite
方法进行统一封装:
@RestControllerAdvice
public class ServerResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@Override
public ServerResponse beforeBodyWrite(
Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response
) {
// 如果已经是 Result 类型,就直接返回
if (body instanceof Result) {
return body;
}
// 不是 Result 类型,就用 Result 包装后返回
return Result.success(body);
}
}
7. 在 Controller 中使用
这边写了一个简单的 Controller 方法,在类上面加上了封装的统一响应体注解:
@BaseResponse // 使用统一响应体注解
@RestController
@RequestMapping("ann-response")
public class AnnResponseController {
@PostMapping("{userId}")
public Integer pathValidate(@PathVariable Integer userId) {
if(userId.equals(0)) {
throw new IllegalArgumentsException();
}
if(userId.equals(1)) {
throw new UnauthorizedException();
}
return userId;
}
}
下面对接口进行测试:
http://localhost:8080/ann-response/0
http://localhost:8080/ann-response/1
http://localhost:8080/ann-response/2
8. ControllerAdvice 注解说明
上面的代码中,我们看到在异常处理类和响应增强类中都用到了如下代码:
@ControllerAdvice(annotations = BaseResponse.class)
或者
@RestControllerAdvice(annotations = BaseResponse.class)
我们看到注解后面带了一个 annotations
的参数,这边解释一下注解的用法。
在一个类上面加 @ControllerAdvice
或者 @RestControllerAdvice
注解,就是定义该类为异常处理类。如果不带任何参数,那么这个类就是全局异常处理类,例如在 SpringBoot 参数校验 中我们创建的 GlobalExceptionHandler
。有时候如果不希望捕获全局异常,可以通过给注解传递参数,指定处理的 Controller 范围:
1. basePackages
:指定一个或多个包,这些包及其子包下的所有 Controller 都被该 @ControllerAdvice 管理
@ControllerAdvice(basePackages = {"com.tfjybj.intern.provider.controller"})
public class FaultBarrier{}
2. basePackageClasses
:是 basePackages 的一种变形,指定一个或多个 Controller 类,这些类所属的包及其子包下的所有 Controller 都被该 @ControllerAdvice 管理
@ControllerAdvice(basePackageClasses = {Test.class})
public class FaultBarrier{}
3. assignableTypes
:指定一个或多个 Controller 类,这些类被该 @ControllerAdvice 管理
@ControllerAdvice(assignableTypes= {Test.class})
public class FaultBarrier{}
4. annotations
:指定一个或多个注解,被这些注解所标记的 Controller 会被该 @ControllerAdvice 管理
@ControllerAdvice(annotations = {RestController.class})
public class FaultBarrier{}
可以看到,我们使用的是第四种方式。另外还要注意下,{}
在 Java 中表示数组,例如我们想指定多个注解,可以这样用:
@ControllerAdvice(annotations = {RestController1.class, RestController2.class})
public class FaultBarrier{}
在异常处理类中,我们可以使用 @ExceptionHandler
注解定义异常处理方法,其中 value
为需要处理的异常类的 class 。
当然,@ControllerAdvice
的缺点也很明显,只能处理 Controller 层抛出的异常,对例如 Interceptor(拦截器)层的异常、定时任务中的异常、异步方法中的异常,不会进行处理。
9. 总结
通过上面的代码可以看出,这种封装方法是通过抛出特定的异常或者传特定的状态码,然后对异常进行处理,实际上跟 ServerResponse.ok
、ServerResponse.badRequest
用法异曲同工,但是可以对异常处理更精细。
然后在 beforeBodyWrite
中对响应内容进行了封装,因此无需在 Controller 方法写类似 ServerResponse.ok
之类的代码,但是成功的响应通常不止 200
请求,常见的还有 201
、204
、206
等等,因此上面封装的方法很难对成功的请求进行细分。
参考
SpringBoot统一响应体解决方案
spring-web-unified-response-demo
SpringBoot - @ControllerAdvice 处理异常
Lombok 的 @EqualsAndHashCode(callSuper = false) 的使用
SpringBoot ResponseBodyAdvice 接口实现自定义返回数据类型(响应头)
SpringBoot 使用 beforeBodyWrite 实现统一的接口返回类型