Spring Boot使用多个@RestControllerAdvice时的拦截顺序
前言
我们在项目开发中经常会使用到公司的公共模块,例如我的项目book-service,就依赖了公司基础的核心core模块,此时core模块已经集成了@RestControllerAdvice的类,负责拦截所有的controller异常。
但是呢?他的异常处理不符合我们book-service项目的要求,我们需要定制自己的异常处理逻辑。这就导致我们book-service项目要重写自己的controller异常拦截。
@RestControllerAdvice介绍
@RestControllerAdvice 是 Spring Framework 中的一个注解,用于全局处理控制器(@Controller 或 @RestController)中的异常和增强功能。它是 @ControllerAdvice 和 @ResponseBody 的组合注解,主要用于简化 restful API 的异常处理和数据返回。
主要功能
全局异常处理:捕获控制器中抛出的异常,并返回统一的错误响应。
数据绑定增强:对请求参数进行全局处理(如校验、格式化等)。
模型数据增强:向所有控制器方法添加全局的模型数据。
核心特性
异常处理:通过 @ExceptionHandler 注解方法处理特定异常。
数据绑定:使用 @InitBinder 初始化数据绑定器。
模型数据:通过 @ModelAttribute 添加全局模型属性
RestControllerAdvice类定义
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@ControllerAdvice
@ResponseBody
public @interface RestControllerAdvice {
@AliasFor(
annotation = ControllerAdvice.class
)
String[] value() default {};
@AliasFor(
annotation = ControllerAdvice.class
)
String[] basePackages() default {};
@AliasFor(
annotation = ControllerAdvice.class
)
Class<?>[] basePackageClasses() default {};
@AliasFor(
annotation = ControllerAdvice.class
)
Class<?>[] assignableTypes() default {};
@AliasFor(
annotation = ControllerAdvice.class
)
Class<? extends Annotation>[] annotations() default {};
}
代码示例
1. 基本用法
import org.springframework.web.bind.annotation.*;
import org.springframework.http.*;
@RestControllerAdvice
public class GlobalExceptionHandler {
// 处理特定异常(如资源未找到)
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFound(ResourceNotFoundException ex) {
ErrorResponse error = new ErrorResponse("NOT_FOUND", ex.getMessage());
return new ResponseEntity<>(error, HttpStatus.NOT_FOUND);
}
// 处理其他未捕获的异常
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGeneralException(Exception ex) {
ErrorResponse error = new ErrorResponse("INTERNAL_ERROR", "服务器内部错误");
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
// 自定义错误响应类
class ErrorResponse {
private String errorCode;
private String message;
// 构造方法、Getter和Setter省略
}
2. 结合@ModelAttribute添加全局数据
@RestControllerAdvice
public class GlobalDataHandler {
@ModelAttribute
public void addCommonData(Model model) {
model.addAttribute("appName", "My REST API");
}
}
3. 结合@InitBinder自定义数据绑定
@RestControllerAdvice
public class CustomBinder {
@InitBinder
public void initBinder(WebDataBinder binder) {
// 注册自定义编辑器或格式化器
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false);
binder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, false));
}
}
常见问题
- 与 @ControllerAdvice 的区别:
@RestControllerAdvice 是 @ControllerAdvice + @ResponseBody 的组合,直接返回 JSON/XML 数据,适合 REST API。
@ControllerAdvice 默认返回视图名称,适合传统 MVC。
- 作用范围:
默认扫描所有控制器,可通过 basePackages 或 annotations 限定范围:
@RestControllerAdvice(basePackages = "com.example.api")
- 优先级:
控制器内的方法级异常处理优先于全局处理。
解决异常的方法
1、使用aop进行切面拦截异常
2、controller每个方法都用try-catch捕获异常
3、增加一个@RestControllerAdvice标注的类,负责处理我们项目的controller异常。
我选用第三种方法,但是当我写了个
GlobalExceptionHandlerAdvice类,指定basePackages为我自己的项目包,依旧还是被core模块的全局异常处理类拦截了。
查询相关资料发现如果有多个加了@RestControllerAdvice的类,他们会依次加载,遇到异常时,按照类加载顺序进行判断,如果前面的类有能力处理这个异常的方法,就给前面的类处理。
我的项目中有两个标注了@RestControllerAdvice的类,core模块的类被先加载,且core模块的异常处理类有个方法专门处理Exception类型的异常,所以我定义的GlobalExceptionHandlerAdvice异常处理类始终不执行。
解决方法:
@Order(Ordered.HIGHEST_PRECEDENCE) 使用@Order注解,提高自己的局部异常处理类的加载顺序就行了
/**
* 自定义全局异常处理
*
* @author yangyanping
* @date 2025-06-17
*/
@Slf4j
@RestControllerAdvice
@Order(Ordered.HIGHEST_PRECEDENCE)
public class GlobalExceptionHandlerAdvice {
/**
* 参数验证异常
*/
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleValidException(HttpServletRequest request, HttpServletResponse response, MethodArgumentNotValidException e) {
log.error("handleValidException#Exception,url={}", request.getRequestURI(), e);
String message = "";
BindingResult bindingResult = e.getBindingResult();
List<FieldError> fieldErrors = bindingResult.getFieldErrors();
for (FieldError item : fieldErrors) {
message = item.getDefaultMessage();
break;
}
ErrorResponse error = new ErrorResponse("Param_Valid_Error", "方法参数验证异常:" + message);
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
/**
* 参数验证异常
*/
@ExceptionHandler(ConstraintViolationException.class)
public ResponseEntity<ErrorResponse> handleConstraintViolationException(HttpServletRequest request, HttpServletResponse response, ConstraintViolationException e) {
log.error("handleValidException#Exception,url={}", request.getRequestURI(), e);
String message = "";
Set<ConstraintViolation<?>> set = e.getConstraintViolations();
for (ConstraintViolation<?> item : set) {
message = item.getMessage();
break;
}
ErrorResponse error = new ErrorResponse("Param_Valid_Error", "参数验证异常:" + message);
return new ResponseEntity<>(error, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
@Order、@Priority、@Primary 三个注解和 Orderd 接口 说明
- orderd接口,实现Oderd接口的话要实现int getOrder();这个方法,返回一个整数值,值越小优先级越高。
- @Order里面存储了一个值,默认为Integer的最大值,同样值越小优先级越高。要注意@Order只能控制组件的加载顺序,不能控制注入的优先级。但是能控制List 里面存放的XXX的顺序,原因是当通过构造函数或者方法参数注入进某个List时,Spring的DefaultListableBeanFactory类会在注入时调用AnnotationAwareOrderComparator.sort(listA)帮我们去完成根据@Order或者Ordered接口序值排序。@Order更加适用于集合注入的排序。
- @Priority与@Order类似,@Order是Spring提供的注解,@Priority是JSR 250标准,同样是值越小优先级越高。但是两者还是有一定区别,@Priority能够控制组件的加载顺序,因此@Priority侧重于单个注入的优先级排序。此外@Priority优先级比@Order更高,两者共存时优先加载@Priority。
- @Primary是优先级最高的,如果同时有@Primary以及其他几个的话,@Primary注解的Bean会优先加载。
最佳实践
- 统一错误响应格式:定义标准的错误响应结构(如 ErrorResponse)。
- 日志记录:在异常处理方法中记录日志,便于排查问题。
- 精细化异常处理:针对不同异常返回不同的 HTTP 状态码和错误信息。
相关文章
- MyBatis如何实现分页查询?_mybatis collection分页查询
- 通过Mybatis Plus实现代码生成器,常见接口实现讲解
- MyBatis-Plus 日常使用指南_mybatis-plus用法
- 聊聊:Mybatis-Plus 新增获取自增列id,这一次帮你总结好
- MyBatis-Plus码之重器 lambda 表达式使用指南,开发效率瞬间提升80%
- Spring Boot整合MybatisPlus和Druid
- mybatis 代码生成插件free-idea-mybatis、mybatisX
- mybatis-plus 团队新作 mybatis-mate 轻松搞定企业级数据处理
- Maven 依赖范围(scope) 和 可选依赖(optional)
- Trace Sql:打通全链路日志最后一里路