@ControllerAdice

小龙 1,024 2021-10-23

1、简介

@ControllerAdvice是spring Mvc中提供的一个注解

org.springframework.web.bind.annotation.ControllerAdvice

这是一个非常有用的注解,顾名思义它是一个增强的Controller,可以轻松的实现全局处理,使用它可实现三种功能:

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

2、示例

2.1、全局异常处理

使用@ControllerAdvice和@ExceptionHandler实现全局的异常处理,在@ExceptionHandler中可以指定需要处理的异常,可以指定多个

@ControllerAdvice
public class GlobalExceptionAdvice {
    // 可以捕获的异常类型,可以自定义异常
    @ExceptionHandler({Exception.class,RuntimeException.class})
    @ResponseBody // 以 JSON 格式返回数据
    public Object advice(Exception e) {
        Map<String, Object> map = new HashMap<>();
        map.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value());
        map.put("message", e.getMessage());
        map.put("exception", e.getClass().getName()); // 异常类型
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        map.put("path", request.getRequestURI());
        return map;
    }
}

除了在一个 @ExceptionHandler注解中定义多个异常,也可以定义多个方法,每个方法处理不同的异常;只有在@ExceptionHandler中指定的异常才会进入到方法中,其它异常是不会进入到方法中的

@ControllerAdvice
public class GlobalExceptionAdvice {
    // 可以捕获的异常类型,可以自定义异常
    @ExceptionHandler(Exception.class)
    @ResponseBody // 以 JSON 格式返回数据
    public Object exception(Exception e) {
        Map<String, Object> map = new HashMap<>();
        map.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value());
        map.put("message", e.getMessage());
        map.put("exception", e.getClass().getName()); // 异常类型
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        map.put("path", request.getRequestURI());
        return map;
    }

    // 可以捕获的异常类型,可以自定义异常
    @ExceptionHandler(RuntimeException.class)
    @ResponseBody // 以 JSON 格式返回数据
    public Object runtimeException(Exception e) {
        Map<String, Object> map = new HashMap<>();
        map.put("code", HttpStatus.INTERNAL_SERVER_ERROR.value());
        map.put("message", e.getMessage());
        map.put("exception", e.getClass().getName()); // 异常类型
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        map.put("path", request.getRequestURI());
        return map;
    }
}

2.2、全局数据绑定

全局数据绑定可以用来做一些数据初始化的操作,我们可以将一些公共的数据定义在@ControllerAdvice注解的类中,然后可以在每一个Controller接口中轻松的访问这些数据;全局数据绑定需要使用到@ControllerAdice和@ModelAttribute;
@ModelAttribute中需要指定绑定的key

@ControllerAdvice
public class GlobalDataBindAdvice {
    // 绑定key
    @ModelAttribute(value = "requestModel")
    public Object dataBind() {
        Map<String, Object> map = new HashMap<>();
        map.put("age", 18);
        map.put("sex", "男");
        return map;
    }
}

Controller接口

@RestController
public class HelloController {
    @GetMapping("/hello")
    public Objecthello(Model model) {
	HashMap map = (HashMap) model.getAttribute("requestModel");
        System.out.println(map);
        map.put("name","RsThe");
        return map ;
    }
}

使用@ModelAttribute注解标记的返回数据是一个全局数据,默认情况下,这个全局数据的key就是返回的变量名,value就是返回的具体内容

@RestController
public class HelloController {
    @GetMapping("/hello")
    public Objecthello(Model model) {
	Map<String, Object> map = model.asMap(); // 这个map就是变量的名称
        System.out.println(map);
        map.put("name","RsThe");
        return map ;
    }
}

2.3、全局数据预处理

全局数据预处理的使用场景就是在一个Controller中使用了多个实体,这些实体中可能存在有相同名称的属性,那么我们在请求的时候springMvc会将这些名称相同的属性对应的内容全部放在一个数组中,然后再赋值给这些属性。SpringMvc无法区分。通过@ControllerAdvice和@InitBinder就可以解决

实体

@Data
public class Book {
    private String name;
    private Long price;
}

@Data
public class Author {
    private String name;
    private Integer age;
}

@ControllerAdvice

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

@InitBinder("author")注解表示该方法用来处理Author实体类和相关的参数,该参数添加一个author的前缀,既在请求中参数需要有author前缀,SpringMvc可以以识别出这个参数属于哪一个实体

Controller接口

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

请求URL:

http://localhost/book?book.name=小猪佩奇&book.price=19.8&author.name=RsThe&author.age=18

3、RestControllerAdvice

在SpringMvc中还有一个注解@RestControllerAdvice,这个注解等于@ControllerAdvice + @ResponseBody,其使用场景与ControllerAdvice相同

4、ResponseBodyAdivce

ResponseBodyAdvice接口是在Controller执行了return之后,在Response返回响应内容给客户端之前,对Response执行一些处理,例如:统一封装或对响应数据进行加密等操作。

ResponseBodyAdvice接口一个有两个方法:

  1. supports:判断是否要执行beforeBodyWrite方法,true为执行,false为不执行;可以通过supports方法选择性的对某些Response进行处理,不用处理的可以直接放行
  2. boforeBodyWrite:对Resposne进行具体的处理

在使用这个接口时也需要使用到@ControllerAdvice注解,需要使用该注解标注出具体需要拦截的包,当然也可以直接使用RestControllerAdvice,可以避免在使用@ResponseBody注解

@ControllerAdvice(basePackages = {"com.rsthe.web.controller"})
@RestControllerAdvice(basePackages = {"com.rsthe.web.controller"})
//声明该类要处理的包路径
@RestControllerAdvice(basePackages = {"com.rsthe.web.controller"})
@Slf4j
public class GlobalReturnHandler implements ResponseBodyAdvice<Object> {
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return methodParameter.hasMethodAnnotation(ResponseBody.class);
    }

    @SneakyThrows
    @Override
    public Object beforeBodyWrite(Object body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
	log.info("加密处理")
        if (null == body) {
            try {
                Map map = objectMapper.readValue(objectMapper.writeValueAsString(body), Map.class);
                map.forEach((key, value) -> map.put(key, value + "-encrypt"));
                return map;
            } catch (Exception e) {
                log.error("加密失败,{}",e.getMessage())
            }
        }
        return body;
    }
}

#5 RequestBodyAdvice
上面的是对响应的进行拦截,当然也或有对请求的进行拦截,RequestBodyAdvice接口就是对请求进行处理的。它们的功能也是完全一样的,只是使用场景不一样,一个用于响应,一个用于请求。可以使用RequestBodyAdvice进行请求数据的解密操作,数据拦截验证操作

@Slf4j
@RestControllerAdvice(basePackages = "com.rsthe.web.controller")
public class ParamEncryptRequestBodyAdvice implements RequestBodyAdvice {

    @Override
    public boolean supports(MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    @Override
    public Object handleEmptyBody(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return o;
    }

    @Override
    public HttpInputMessage beforeBodyRead(HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) throws IOException {
        return new HttpInputMessage() {
            @Override
            public InputStream getBody() throws IOException {
                log.info("此处进行解密数据");
                return new ByteArrayInputStream(IOUtils.toString(httpInputMessage.getBody()).replace("-encrypt", "").getBytes(StandardCharsets.UTF_8));
            }

            @Override
            public HttpHeaders getHeaders() {
                return httpInputMessage.getHeaders();
            }
        };
    }

    @Override
    public Object afterBodyRead(Object o, HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) {
        return o;
    }
}