一、@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中的异常拦截方法拦截。

Logo

为开发者提供学习成长、分享交流、生态实践、资源工具等服务,帮助开发者快速成长。

更多推荐