@RestControllerAdvice 注解

1,318 阅读4分钟

@RestControllerAdvice 注解

官方文档的描述

@ExceptionHandler, @InitBinder, and @ModelAttribute methods apply only to the @Controller class, or class hierarchy, in which they are declared. If, instead, they are declared in an @ControllerAdvice or @RestControllerAdvice class, then they apply to any controller. Moreover, as of 5.3, @ExceptionHandler methods in @ControllerAdvice can be used to handle exceptions from any @Controller or any other handler.

@ExceptionHandler, @InitBinder, 和 @ModelAttribute 方法只适用于哪些被 @Controller 声明的类,或类的层次结构中。如果它们被声明在 @ControllerAdvice 或 @RestControllerAdvice 声明的类中,那么它们就适用于任何控制器。此外,从 5.3 版本开始,@ControllerAdvice 中的 @ExceptionHandler 方法可以用来处理来自任何 @Controller 或任何其他处理器的异常。

@ControllerAdvice is meta-annotated with @Component and therefore can be registered as a Spring bean through component scanning. @RestControllerAdvice is meta-annotated with @ControllerAdvice and @ResponseBody, and that means @ExceptionHandler methods will have their return value rendered via response body message conversion, rather than via HTML views.

@ControllerAdvice 被 @Component 注解进行了标注,因此可以通过组件扫描注册为 Spring Bean。@RestControllerAdvice 被 @ControllerAdvice 和 @ResponseBody 注解进行了标注,这意味着 @ExceptionHandler 方法的返回值将通过响应体信息转换呈现,而不是通过 HTML 视图。

On startup, RequestMappingHandlerMapping and ExceptionHandlerExceptionResolver detect controller advice beans and apply them at runtime. Global @ExceptionHandler methods, from an @ControllerAdvice, are applied after local ones, from the @Controller. By contrast, global @ModelAttribute and @InitBinder methods are applied before local ones.

在启动时,RequestMappingHandlerMapping 和 ExceptionHandlerExceptionResolver 会检测控制器的切面,并在运行时应用它们。来自 @ControllerAdvice 的全局 @ExceptionHandler 方法(也就是全局异常处理切面方法),会在控制器方法执行之后调用。相比之下,全局的 @ModelAttribute 和 @InitBinder 方法会在控制器方法执行之前调用。

应用

一共就有三个应用,因为官方文档就给配了仨注解:

  1. 全局异常处理
  2. 全局数据绑定
  3. 全局数据预处理

全局异常处理

使用 @ControllerAdvice 实现全局异常处理,只需要定义类,添加该注解即可定义方式如下:

@ControllerAdvice
public class MyGlobalExceptionHandler {
    /**
     * 处理服务异常
     *
     * @param e 异常
     * @return 接口异常信息
     */
    @ExceptionHandler(value = GlobalException.class)
    public Result<?> errorHandler(GlobalException e) {
        return Result.fail(e.getCode(), e.getMessage());
    }
    
    /**
     * 处理参数校验异常
     *
     * @param e 异常
     * @return 接口异常信息
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Result<?> errorHandler(MethodArgumentNotValidException e) {
        return Result.fail(VALID_ERROR.getCode(), Objects.requireNonNull(e.getBindingResult().getFieldError()).getDefaultMessage());
    }
​
    /**
     * 处理系统异常
     *
     * @param e 异常
     * @return 接口异常信息
     */
    @ExceptionHandler(value = Exception.class)
    public Result<?> errorHandler(Exception e) {
        e.printStackTrace();
        return Result.fail(SYSTEM_ERROR.getCode(), SYSTEM_ERROR.getDesc());
    }
}

在该类中,可以定义多个方法,不同的方法处理不同的异常,例如专门处理空指针的方法、专门处理数组越界的方法...,也可以直接向上面代码一样,在一个方法中处理所有的异常信息。

@ExceptionHandler 注解用来指明异常的处理类型,即如果这里指定为 NullpointerException,则数组越界异常就不会进到这个方法中来。

全局数据绑定

全局数据绑定功能可以用来做一些初始化的数据操作,我们可以将一些公共的数据定义在添加了 @ControllerAdvice 注解的类中,这样,在每一个 Controller 的接口中,就都能够访问导致这些数据。

@ControllerAdvice
public class MyGlobalExceptionHandler {
    @ModelAttribute(name = "md")    // 全局数据都绑定了 name 字段,每个接口都可以访问到 name
    public Map<String,Object> mydata() {
        HashMap<String, Object> map = new HashMap<>();
        map.put("age", 99);
        map.put("gender", "男");
        return map;
    }
}

使用 @ModelAttribute 注解标记该方法的返回数据是一个全局数据,默认情况下,这个全局数据的 key 就是返回的变量名,value 就是方法返回值,当然开发者可以通过 @ModelAttribute 注解的 name 属性去重新指定 key。

定义完成后,在任何一个Controller 的接口中,都可以获取到这里定义的数据:

@RestController
public class HelloController {
    @GetMapping("/hello")
    public String hello(Model model) {
        Map<String, Object> map = model.asMap();
        System.out.println(map);
        int i = 1 / 0;
        return "hello controller advice";
    }
}

全局数据预处理

考虑我有两个实体类,Book 和 Author,分别定义如下:

public class Book {
    private String name;
    private Long price;
    //getter/setter
}
public class Author {
    private String name;
    private Integer age;
    //getter/setter
}

此时,如果我定义一个数据添加接口,如下:

@PostMapping("/book")
public void addBook(Book book, Author author) {
    System.out.println(book);
    System.out.println(author);
}

这个时候,添加操作就会有问题,因为两个实体类都有一个 name 属性,从前端传递时 ,无法区分。此时,通过 @ControllerAdvice 的全局数据预处理可以解决这个问题

解决步骤如下:

  1. 给接口中的变量取别名

    @PostMapping("/book")
    public void addBook(@ModelAttribute("b") Book book, @ModelAttribute("a") Author author) {
        System.out.println(book);
        System.out.println(author);
    }
    
  2. 进行请求数据预处理 在 @ControllerAdvice 标记的类中添加如下代码:

    @InitBinder("b")
    public void b(WebDataBinder binder) {
        binder.setFieldDefaultPrefix("b.");
    }
    @InitBinder("a")
    public void a(WebDataBinder binder) {
        binder.setFieldDefaultPrefix("a.");
    }
    

    @InitBinder("b") 注解表示该方法用来处理和 Book 和相关的参数,在方法中,给参数添加一个 b 前缀,即请求参数要有 b 前缀。

  3. 发送请求

    请求发送时,通过给不同对象的参数添加不同的前缀,可以实现参数的区分

    img

juejin.cn/post/684490… ,引用了这个文章的两个例子,感谢感谢