Spring Boot学习笔记(十)使用@RestControllerAdvice注解统一处理controller中的返回结果和异常
springboot中统一处理返回结果和异常
一、@ControllerAdvice和@RestControllerAdvice
@ControllerAdvice和@RestControllerAdvice注解是@Controller的一个增强版,用来增强Controller的功能。可以在加了@ControllerAdvice或@RestControllerAdvice的自定义类中定义使用了@ExceptionHandler、@InitBinder、@ModelAttribute注解的方法。分别用来全局异常处理、全局数据预处理、全局数据绑定。并应用到所有@RequestMapping、@PostMapping、@GetMapping注解中。也可以使用在ResponseBodyAdvice中,用来统一处理Controller的返回结果。
@ControllerAdvice和@RestControllerAdvice区别在于,@RestControllerAdvice相当于@ControllerAdvice+@ResponseBody的集合。表示该方法返回json数据。
@ControllerAdvice捕获异常后,如果需要页面跳转就不能加@ResponseBody,加了则该方法返回的是json数据,所以这种情况也不能使用@RestControllerAdvice。
二、统一处理返回结果
统一返回值类型无论项目前后端是否分离都是非常必要的,方便对接接口的开发人员更加清晰地知道这个接口的调用是否成功(不能仅仅简单地看返回值是否为 null 就判断成功与否,因为有些接口的设计就是如此)。
pom文件:
<?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>org.example</groupId>
<artifactId>ControllerAdviceDemo</artifactId>
<version>1.0-SNAPSHOT</version>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
</dependencies>
</project>
返回结果类:
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
private int code;
private String message;
private T data;
public static <T> Result<T> success(T data) {
return new Result<>(ResultEnum.SUCCESS.getCode(), ResultEnum.SUCCESS.getMessage(), data);
}
public static <T> Result<T> success(String message, T data) {
return new Result<>(ResultEnum.SUCCESS.getCode(), message, data);
}
public static Result<?> failed() {
return new Result<>(ResultEnum.COMMON_FAILED.getCode(), ResultEnum.COMMON_FAILED.getMessage(), null);
}
public static Result<?> failed(String message) {
return new Result<>(ResultEnum.COMMON_FAILED.getCode(), message, null);
}
public static Result<?> failed(ResultEnum enumEx){
return new Result<>(enumEx.getCode(),enumEx.getMessage(),null);
}
public static Result<?> failed(ResultEnum enumEx,String message){
return new Result<>(enumEx.getCode(),message,null);
}
public static Result<?> failed(BaseException ex){
return new Result<>(ex.getCode(),ex.getMsg(),null);
}
public static <T> Result<T> instance(Integer code, String message, T data) {
Result<T> result = new Result<>();
result.setCode(code);
result.setMessage(message);
result.setData(data);
return result;
}
}
枚举类父接口
public interface IResponseEnum {
public int getCode();
public String getMessage();
}
基础异常类:
public abstract class BaseException extends RuntimeException{
private static final long serialVersionUID = 1L;
private int code;
private String msg;
public BaseException(int code,String msg){
super(msg);
this.code = code;
this.msg = msg;
}
public BaseException(IResponseEnum exEnum){
super(exEnum.getMessage());
this.code = exEnum.getCode();
this.msg = exEnum.getMessage();
}
public BaseException(int code,String msg,Throwable cause){
super(msg,cause);
this.code = code;
this.msg = msg;
}
public BaseException(IResponseEnum exEnum,Throwable cause){
super(exEnum.getMessage(),cause);
this.code = exEnum.getCode();
this.msg = exEnum.getMessage();
}
public BaseException(int code,String msg,Throwable cause,
boolean enableSuppression,
boolean writableStackTrace){
super(msg,cause,enableSuppression,writableStackTrace);
this.code = code;
this.msg = msg;
}
public BaseException(IResponseEnum exEnum,Throwable cause,
boolean enableSuppression,
boolean writableStackTrace){
super(exEnum.getMessage(),cause,enableSuppression,writableStackTrace);
this.code = exEnum.getCode();
this.msg = exEnum.getMessage();
}
public int getCode() {
return code;
}
public void setCode(int code) {
this.code = code;
}
public String getMsg() {
return msg;
}
public void setMsg(String msg) {
this.msg = msg;
}
}
常用返回结果枚举类
public enum ResultEnum implements IResponseEnum {
SUCCESS(2001,"接口调用成功!"),
COMMON_FAILED(2001, "接口调用失败");
private int code;
private String message;
ResultEnum(int code, String message) {
this.code = code;
this.message = message;
}
public int getCode() {
return code;
}
public String getMessage() {
return message;
}
}
定义好统一的返回结果类型后,就可以在controller中使用了,但是如果controller中每个方法结尾都写一段构建返回结果的操作,这些都是很重复的工作,所以还要继续想办法进一步处理统一返回结构。
Spring 中提供了一个接口 ResponseBodyAdvice 以及@RestControllerAdvice或@ControllerAdvice注解,能帮助我们实现上述需求:
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);
}
ResponseBodyAdvice 是对 Controller 返回的内容在 HttpMessageConverter 进行类型转换之前拦截,进行相应的处理操作后,再将结果返回给客户端。但是只拦截加了@RequestMapping(GetMapping这种也可以)和@ResponseBody的方法的返回结果,这点要注意。
supports:判断是否要交给 beforeBodyWrite 方法执行,ture:需要;false:不需要
beforeBodyWrite:对 response 进行具体的处理
ResponseAdvice 示例:
@RestControllerAdvice
public class ResponseAdvice implements ResponseBodyAdvice<Object> {
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
return true;
}
@Override
public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) {
if (body instanceof Result) {
return body;
}else if(body instanceof String){
try {
return new ObjectMapper().writeValueAsString(Result.success(body));
} catch (JsonProcessingException e) {
e.printStackTrace();
}
}else if(body instanceof Throwable){
return Result.failed(ResultEnum.COMMON_FAILED,"接口调用出错,请联系管理员!");
}
return Result.success(body);
}
}
注意事项:
1、如果controller方法中返回的是String类型,但是加了@ResponseBody注解,那么在ResponseBodyAdvice中拦截到String类型并且处理完后需要最后返回一个String类型(可以转换成json字符串),否则会报错。
因为spring实际上在类型转换处理时的HttpMessageConverter分为两类:String类型为StringHttpMessageConverter,其他类型为MappingJackson2HttpMessageConverter。
实际上是在调用AbstractHttpMessageConverter的protected void addDefaultHeaders(HttpHeaders headers, T t, @Nullable MediaType contentType) throws IOException
方法时,因为StringHttpMessageConverter重写了此方法:protected void addDefaultHeaders(HttpHeaders headers, String s, @Nullable MediaType type) throws IOException
将参数类型T改为String,而ResponseBodyAdvice返回的是Result类型,所以会报错。因此在ResponseBodyAdvice中需要单独处理一下String类型。
三、统一异常处理
业务异常类:
public class BusinessException extends BaseException {
private static final long serialVersionUID = 1L;
public BusinessException(int code, String msg) {
super(code, msg);
}
public BusinessException(IResponseEnum exEnum) {
super(exEnum);
}
public BusinessException(int code, String msg, Throwable cause) {
super(code, msg, cause);
}
public BusinessException(IResponseEnum exEnum, Throwable cause) {
super(exEnum, cause);
}
public BusinessException(int code, String msg, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(code, msg, cause, enableSuppression, writableStackTrace);
}
public BusinessException(IResponseEnum exEnum, Throwable cause, boolean enableSuppression, boolean writableStackTrace) {
super(exEnum, cause, enableSuppression, writableStackTrace);
}
}
业务异常枚举类:
public enum BusinessExceptionEnum implements IResponseEnum {
VALIDATE_FAILED(3001, "参数校验失败"),
USERNOTFOUND(3002,"用户不存在");
private int code;
private String message;
BusinessExceptionEnum(int code, String message){
this.code = code;
this.message = message;
}
@Override
public int getCode() {
return this.code;
}
@Override
public String getMessage() {
return this.message;
}
}
定义好异常类以及对于的异常枚举类,以后新增的异常类型,就只需要在枚举类中新增即可,无需再新增异常类。
定义好异常类后,就可以使用了,使用@ExceptionHandler和@RestControllerAdvice就可以拦截异常了。
@RestControllerAdvice
public class ExceptionAdvice {
@ExceptionHandler(BusinessException.class)
public Result handleBusinessException(BusinessException e){
return Result.failed(e);
}
//返回错误页面
@ExceptionHandler(RuntimeException.class)
public ModelAndView handleError(RuntimeException e){
ModelAndView modelAndView = new ModelAndView();
modelAndView.setViewName("error");
modelAndView.addObject("code", 500);
modelAndView.addObject("msg", "服务器异常!");
return modelAndView;
}
@ExceptionHandler(Exception.class)
public Throwable handleException(Exception e){
return e;
}
}
controller方法:
@RequestMapping("/testException")
public String testException() throws Exception {
//throw new Exception();
throw new RuntimeException();
//throw new BusinessException(BusinessExceptionEnum.USERNOTFOUND);
}
在 @ExceptionHandler中定义好拦截的异常类型即可拦截指定的异常了。
注意事项:
1、@RestControllerAdvice是在@ControllerAdvice的基础上加上了@ResponseBody,表明加了该注解的方法返回类型是@ResponseBody。所以这里的返回结果也会被ResponseBodyAdvice拦截,所以在ResponseBodyAdvice中也需要统一处理出现异常后的返回结果,参考上文ResponseAdvice类代码。如果使用@ControllerAdvice注解,但是不加上@ResponseBody,则不会被拦截,但此时返回的就是错误页面了默认是error/500.html,或者error.html
2、在单个Controller中也可以定义@ExceptionHandler方法作为本Controller的异常处理方法。优先级是,Controller中的异常处理方法>全局的异常处理方法。定义的处理异常类型越详细优先级越高。比如:如果出现RuntimeException,那么它会被处理RuntimeException异常的方法拦截,不会被处理Exception异常的方法拦截。但是如果处理Exception异常的方法定义在它自己的Controller中,那么它只会被本Controller中的异常拦截方法拦截。
更多推荐
所有评论(0)