@RestControllerAdvice
@RestControllerAdvice 是 Spring Framework 提供的一个组合注解,专门用于实现 RESTful API 的全局异常处理、数据绑定和数据预处理。它是 @ControllerAdvice 和 @ResponseBody 的组合,简化了 REST 异常处理的实现。
作用
- 全局异常处理:集中处理控制器抛出的异常
- 统一响应格式:确保所有错误响应格式一致
- 减少重复代码:避免在每个控制器中重复异常处理逻辑
- REST 专用:专为 RESTful API 设计,自动将返回值转为 JSON/XML
基本使用
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Data @AllArgsConstructor public class ErrorResponse { private int status; private String error; private String message; private String path; private LocalDateTime timestamp = LocalDateTime.now(); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| @RestControllerAdvice @Slf4j public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class) @ResponseStatus(HttpStatus.NOT_FOUND) public ErrorResponse handleResourceNotFound(ResourceNotFoundException ex, HttpServletRequest request) { log.error("Resource not found: {}", ex.getMessage()); return new ErrorResponse( HttpStatus.NOT_FOUND.value(), "Not Found", ex.getMessage(), request.getRequestURI()); }
@ExceptionHandler(MethodArgumentNotValidException.class) @ResponseStatus(HttpStatus.BAD_REQUEST) public ErrorResponse handleValidationErrors(MethodArgumentNotValidException ex, HttpServletRequest request) { List<String> errors = ex.getBindingResult() .getFieldErrors() .stream() .map(FieldError::getDefaultMessage) .collect(Collectors.toList()); log.error("Validation errors: {}", errors); return new ErrorResponse( HttpStatus.BAD_REQUEST.value(), "Validation Error", errors.toString(), request.getRequestURI()); }
@ExceptionHandler(Exception.class) @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) public ErrorResponse handleAllExceptions(Exception ex, HttpServletRequest request) { log.error("Internal server error: {}", ex.getMessage(), ex); return new ErrorResponse( HttpStatus.INTERNAL_SERVER_ERROR.value(), "Internal Server Error", "An unexpected error occurred", request.getRequestURI()); } }
|
其他
- 限定控制器的生效范围
1 2 3 4 5 6 7 8
| @RestControllerAdvice("com.example.controllers")
@RestControllerAdvice(annotations = RestController.class)
@RestControllerAdvice(assignableTypes = {UserController.class, ProductController.class})
|
- 处理数据绑定异常
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
| @ExceptionHandler(MethodArgumentNotValidException.class) public ResponseEntity<ErrorResponse> handleValidationExceptions(MethodArgumentNotValidException ex) { List<String> errors = ex.getBindingResult() .getFieldErrors() .stream() .map(error -> error.getField() + ": " + error.getDefaultMessage()) .collect(Collectors.toList()); ErrorResponse error = new ErrorResponse( HttpStatus.BAD_REQUEST.value(), "Validation failed", errors.toString()); return new ResponseEntity<>(error, HttpStatus.BAD_REQUEST); }
|
- 处理自定义业务异常
1 2 3 4 5 6 7 8 9
| @ExceptionHandler(BusinessException.class) public ResponseEntity<ErrorResponse> handleBusinessException(BusinessException ex) { ErrorResponse error = new ErrorResponse( ex.getErrorCode(), ex.getErrorType(), ex.getMessage()); return new ResponseEntity<>(error, ex.getHttpStatus()); }
|
ResponseBodyAdvice
作用
ResponseBodyAdvice 是 spring mvc 提供的一个增强接口,用于在返回对象被 HttpMessageConverter 执行序列化之前对其进行一些自定义的操作。
1 2 3 4 5 6 7 8 9 10 11
| 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); }
|
- supports:判断是否要执行 beforeBodyWrite 方法,true 为执行,false 不执行。通过该方法可以选择哪些类或哪些方法的 response 要进行处理,其他的不进行处理
- beforeBodyWrite:对 response 方法进行具体操作处理
基本使用
1 2 3 4 5 6 7 8 9 10
| @Data @NoArgsConstructor @AllArgsConstructor public class BaseResponse <T> implements Serializable { private int code; private String msg; private T data; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48
| @Component @Slf4j @RestControllerAdvice(annotations = {RestController.class})
public class ResponseAdvice implements ResponseBodyAdvice {
@Override public boolean supports(MethodParameter returnType, Class converterType) { log.info(returnType.getMethod().getDeclaringClass().getName()); log.info(converterType.toString()); return true; }
@Override public Object beforeBodyWrite(Object body, MethodParameter returnType, MediaType selectedContentType, Class selectedConverterType, ServerHttpRequest request, ServerHttpResponse response) { log.info(selectedContentType.getType()); ObjectMapper objectMapper = new ObjectMapper(); if (body instanceof String) { try { return objectMapper.writeValueAsString(new BaseResponse(0, "操作成功", body)); } catch (JsonProcessingException e) { e.printStackTrace(); } } if (body instanceof Page) { HashMap map = new LinkedHashMap(); map.put("data", ((Page<?>) body).getRecords()); map.put("current", ((Page<?>) body).getCurrent()); map.put("size", ((Page<?>) body).getSize()); map.put("total", ((Page<?>) body).getTotal()); return new BaseResponse(0, "操作成功", map); } return new BaseResponse(0, "操作成功", body); } }
|
存在问题
返回 String 类型的 ContentType 是 “text/plain”,对应的转换器是 StringHttpMessageConverter,由于我们将返回结果都封装成 BaseResponse 导致由其转换成 String 类型会引发类型错误
解决方法有两个:
- 设置接口的返回类型
1 2 3 4
| @GetMapping(value = "/ok", produces = "application/json;charset=utf-8") public String ok() { return "ok"; }
|
- 通过 SpringBoot 内置提供 Jackson 序列化 ObjectMapper 实现实体信息的序列化
1 2 3 4 5 6 7 8
| ObjectMapper objectMapper = new ObjectMapper(); if (body instanceof String) { try { return objectMapper.writeValueAsString(new Response(0, "操作成功", body)); } catch (JsonProcessingException e) { e.printStackTrace(); } }
|
转换器
使用枚举为请求参数
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Getter public enum GenderEnum {
MALE(0),
FEMALE(1);
private Integer code; GenderEnum(int code) { this.code = code; } }
|
1 2 3 4
| @Data public class QueryRequest { private GenderEnum gender; }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| @Slf4j @RestController @RequestMapping("/enum") public class EnumTestController { @GetMapping("/get") public Dict testGet(QueryRequest request) { log.info("【get-request】= {}", JSONUtil.toJsonStr(request)); return Dict.create().set("get-request", request); } @PostMapping("/post") public Dict testPost(@RequestBody QueryRequest request) { log.info("【post-request】= {}", JSONUtil.toJsonStr(request)); return Dict.create().set("post-request", request); } }
|
转换器
gender 只能接收到 MALE 、FEMALE 这样的参数,除此以外,均会报类型不匹配的错误信息,此时是无法处理 0 、1 这样的参数的
需求:
- 接收到 MALE 、FEMALE 这样的参数,可以自动转为对应的枚举值;
- 接收到 0 、1 这样的参数,也可以自动转为对应的枚举值
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| public class IntegerCodeToGenderEnumConverter implements Converter<Integer, GenderEnum> { private Map<Integer, GenderEnum> enumMap = Maps.newHashMap(); public IntegerCodeToGenderEnumConverter() { for (GenderEnum genderEnum : GenderEnum.values()) { enumMap.put(genderEnum.getCode(), genderEnum); } } @Override public GenderEnum convert(Integer source) { GenderEnum genderEnum = enumMap.get(source); if (ObjectUtil.isNull(genderEnum)) { throw new IllegalArgumentException("无法匹配对应的枚举类型"); } return genderEnum; } }
|
1 2 3 4 5 6 7 8 9 10 11
| @Configuration public class WebMvcConfig implements WebMvcConfigurer {
@Override public void addFormatters(FormatterRegistry registry) { registry.addConverter(new IntegerCodeToGenderEnumConverter()); } }
|
转换器工厂
1 2 3 4 5 6 7 8
| public interface BaseEnum {
Integer getCode(); }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| @Getter public enum GenderEnum implements BaseEnum {
MALE(0),
FEMALE(1);
private Integer code; GenderEnum(int code) { this.code = code; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class IntegerToEnumConverter<T extends BaseEnum> implements Converter<Integer, T> { private Map<Integer, T> enumMap = Maps.newHashMap(); public IntegerToEnumConverter(Class<T> enumType) { T[] enums = enumType.getEnumConstants(); for (T e : enums) { enumMap.put(e.getCode(), e); } } @Override public T convert(Integer source) { T t = enumMap.get(source); if (Objects.isNull(t)) { throw new IllegalArgumentException("无法匹配对应的枚举类型"); } return t; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class IntegerCodeToEnumConverterFactory implements ConverterFactory<Integer, BaseEnum> { private static final Map<Class, Converter> CONVERTERS = Maps.newHashMap();
@Override public <T extends BaseEnum> Converter<Integer, T> getConverter(Class<T> targetType) { Converter<Integer, T> converter = CONVERTERS.get(targetType); if (converter == null) { converter = new IntegerToEnumConverter<>(targetType); CONVERTERS.put(targetType, converter); } return converter; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class StringToEnumConverter<T extends BaseEnum> implements Converter<String, T> { private Map<String, T> enumMap = Maps.newHashMap(); public StringToEnumConverter(Class<T> enumType) { T[] enums = enumType.getEnumConstants(); for (T e : enums) { enumMap.put(e.getCode().toString(), e); } } @Override public T convert(String source) { T t = enumMap.get(source); if (Objects.isNull(t)) { throw new IllegalArgumentException("无法匹配对应的枚举类型"); } return t; } }
|
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| public class StringCodeToEnumConverterFactory implements ConverterFactory<String, BaseEnum> { private static final Map<Class, Converter> CONVERTERS = Maps.newHashMap();
@Override public <T extends BaseEnum> Converter<String, T> getConverter(Class<T> targetType) { Converter<String, T> converter = CONVERTERS.get(targetType); if (converter == null) { converter = new StringToEnumConverter<>(targetType); CONVERTERS.put(targetType, converter); } return converter; } }
|
1 2 3 4 5 6 7 8 9 10 11 12
| @Configuration public class WebMvcConfig implements WebMvcConfigurer {
@Override public void addFormatters(FormatterRegistry registry) { registry.addConverterFactory(new IntegerCodeToEnumConverterFactory()); registry.addConverterFactory(new StringCodeToEnumConverterFactory()); } }
|