@ControllerAdvice是 Spring Framework 中一个非常实用的注解,主要用于全局性地增强所有 @Controller或 @RestController控制器。下面这个表格帮你快速了解它的核心功能、应用场景和代码示例。
| 主要功能 | 核心注解 | 应用场景描述 | 简单代码示例 |
|---|---|---|---|
| 全局异常处理 | @ExceptionHandler |
集中处理控制器中抛出的异常,避免在每个控制器中重复编写try-catch。 | @ExceptionHandler(NullPointerException.class) public Result handleNullPointer(...) { ... } |
| 全局数据绑定 | @ModelAttribute |
为所有控制器的Model自动添加一些公共数据(如用户信息、系统配置)。 | @ModelAttribute("msg") public String globalMsg() { return "Welcome"; } |
| 全局数据预处理 | @InitBinder |
全局定制请求参数绑定(如字符串转日期、忽略特定字段、数据校验)。 | @InitBinder public void initBinder(WebDataBinder binder) { binder.setDisallowedFields("id"); } |
| 响应体统一处理 | (实现 ResponseBodyAdvice接口) |
在响应体被写入前,对所有标记了 @ResponseBody或返回 ResponseEntity的方法的返回值进行统一封装或修改。 |
@Override public Object beforeBodyWrite(...) { // 统一封装返回值 } |
下面我们结合具体案例,详细看看每一项功能如何实现。
💡 全局异常处理
这是 @ControllerAdvice最广泛使用的功能。它可以让你在一个地方捕获和处理整个Web应用中的异常,返回统一的JSON错误信息或自定义错误页面,提升用户体验和维护性 。
实战案例:处理自定义业务异常和空指针异常
假设你有一个自定义的业务异常 BizException,可以这样全局处理它和其他特定异常:
typescript
// 1. 自定义业务异常
public class BizException extends RuntimeException {
private String errorCode;
private String errorMsg;
// 省略构造方法和getter/setter
}
// 2. 统一的响应结果封装类
@Data
public class Result {
private String code;
private String message;
private Object data;
public static Result error(String code, String message) {
Result result = new Result();
result.setCode(code);
result.setMessage(message);
return result;
}
}
// 3. 全局异常处理器
@RestControllerAdvice // 等同于 @ControllerAdvice + @ResponseBody
public class GlobalExceptionHandler {
private static final Logger log = LoggerFactory.getLogger(GlobalExceptionHandler.class);
/**
* 专门处理业务异常
*/
@ExceptionHandler(BizException.class)
public Result handleBizException(BizException e, HttpServletRequest request) {
log.warn("业务异常发生在请求 {}: ", request.getRequestURI(), e);
// 返回给前端的JSON格式是 {"code": "500", "message": "业务操作失败", "data": null}
return Result.error(e.getErrorCode(), e.getErrorMsg());
}
/**
* 处理空指针异常
*/
@ExceptionHandler(NullPointerException.class)
public Result handleNullPointerException(NullPointerException e, HttpServletRequest request) {
log.error("空指针异常发生在请求 {}: ", request.getRequestURI(), e);
return Result.error("500", "系统内部错误:空指针异常");
}
/**
* 处理所有未精确定义的异常,作为最后的兜底方案
*/
@ExceptionHandler(Exception.class)
public Result handleException(Exception e, HttpServletRequest request) {
log.error("系统异常发生在请求 {}: ", request.getRequestURI(), e);
return Result.error("500", "系统繁忙,请稍后再试");
}
}
Controller中的使用非常简单,无需再处理异常:
less
@RestController
@RequestMapping("/api/user")
public class UserController {
@GetMapping("/{id}")
public User getUser(@PathVariable String id) {
// 如果id无效,直接抛出业务异常,会被GlobalExceptionHandler捕获处理
if (!isValid(id)) {
throw new BizException("USER_001", "用户ID不合法");
}
// 模拟可能的空指针异常
return userService.findUserById(id);
}
}
通过这种方式,所有控制器的异常处理逻辑都被集中和简化了 。
🌐 全局数据绑定
当你希望每个控制器都能获取到一些公共数据时(比如当前用户信息、站点设置),可以使用 @ModelAttribute实现全局数据绑定 。
实战案例:为所有页面添加公共信息
typescript
@ControllerAdvice
public class GlobalDataAdvice {
/**
* 该方法会在任何控制器方法执行前被调用,其返回值会自动添加到Model中。
* 键名为 "globalAttr"
*/
@ModelAttribute("globalAttr")
public Map<String, Object> globalData() {
Map<String, Object> map = new HashMap<>();
map.put("siteName", "我的应用");
map.put("currentYear", Calendar.getInstance().get(Calendar.YEAR));
// 这里可以模拟从数据库或配置中心加载数据
return map;
}
}
在控制器或视图(如JSP、Thymeleaf模板)中,可以直接使用这些数据:
kotlin
@Controller
public class HomeController {
@RequestMapping("/home")
public String home(Model model) {
// 可以从model中获取全局数据
Map<?, ?> globalAttrs = (Map<?, ?>) model.asMap().get("globalAttr");
System.out.println(globalAttrs.get("siteName")); // 输出:我的应用
return "home";
}
}
xml
<!-- 在Thymeleaf模板中直接使用 -->
<html>
<head><title>首页</title></head>
<body>
<h1>欢迎来到 <span th:text="${globalAttr.siteName}">默认站点名</span></h1>
<footer>© <span th:text="${globalAttr.currentYear}">2025</span></footer>
</body>
</html>
⚙️ 全局数据预处理
@InitBinder允许你全局定制如何将HTTP请求参数绑定到模型对象(Data Binding),例如设置日期格式、注册自定义属性编辑器、或者禁止绑定某些字段 。
实战案例:全局日期格式转换
前端提交的日期字符串格式多样(如"2023-10-26"或"26/10/2023"),我们希望统一转换为 java.util.Date类型。
typescript
@ControllerAdvice
public class GlobalDataPreAdvice {
/**
* 为所有控制器注册一个日期转换器
*/
@InitBinder
public void handleInitBinder(WebDataBinder dataBinder) {
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd");
dateFormat.setLenient(false); // 严格解析日期
// 注册一个自定义编辑器,用于处理Date类型
dataBinder.registerCustomEditor(Date.class, new CustomDateEditor(dateFormat, true)); // true表示允许空值
}
}
之后,在控制器方法中,Spring会自动将格式为"yyyy-MM-dd"的字符串参数转换为Date对象:
typescript
@RestController
public class TaskController {
@PostMapping("/task")
public String createTask(String taskName, @DateTimeFormat(pattern="yyyy-MM-dd") Date dueDate) {
// 由于配置了@InitBinder,dueDate参数会被自动转换
return "任务创建成功";
}
}
✨ 统一响应体封装
对于前后端分离的项目,通常希望所有返回JSON数据的接口都有一个统一的格式(如 {code: "0000", message: "成功", data: ...})。当使用 @ResponseBody或 @RestController时,可以通过实现 ResponseBodyAdvice接口来实现这一目标 。
实战案例:统一封装API响应
typescript
// 注意:@RestControllerAdvice 已经包含了 @ControllerAdvice
@RestControllerAdvice(basePackages = "com.yourpackage.controller")
public class GlobalResponseAdvice implements ResponseBodyAdvice<Object> {
/**
* 判断哪些方法的返回值需要进入 beforeBodyWrite 进行处理
*/
@Override
public boolean supports(MethodParameter returnType, Class converterType) {
// 本例中,只处理那些不本身就是Result类型的方法返回值,避免重复包装
return !returnType.getParameterType().equals(Result.class);
}
/**
* 在响应体写入HTTP响应流之前,对body进行修改
*/
@Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
// 如果返回值已经是Result类型(比如异常处理器返回的),则直接返回
// 如果返回值是String类型,需要特殊处理,因为String有专门的HttpMessageConverter
if (body instanceof Result) {
return body;
} else if (body instanceof String) {
// 因为String需要特殊转换,不能直接返回Result对象,这里需要额外处理(例如返回JSON字符串)
// 实际开发中可能需要借助工具类(如Jackson)将Result对象序列化成JSON字符串返回
return JSONUtil.toJsonStr(Result.success(body));
}
// 最普遍的情况:将原始数据用Result成功格式包装
return Result.success(body);
}
}
这样,你的控制器代码会非常简洁:
less
@RestController
@RequestMapping("/api/product")
public class ProductController {
@GetMapping("/list")
public List<Product> getProducts() {
// 直接返回业务数据,GlobalResponseAdvice会自动包装成 {"code": "0000", "message": "成功", "data": [...]}
return productService.findAll();
}
}
⚠️ 高级用法与注意事项
-
控制生效范围 :使用
basePackages,annotations等属性可以限制@ControllerAdvice的生效范围 。less@ControllerAdvice(basePackages = {"com.example.admin.controller"}) // 只对admin包下的控制器生效 @ControllerAdvice(annotations = RestController.class) // 只对带有@RestController注解的控制器生效 -
处理多个
@ControllerAdvice的顺序 :当有多个@ControllerAdvice类时,可以使用@Order注解或实现Ordered接口来控制它们的执行顺序。顺序会影响异常处理(匹配到的第一个异常处理器生效)和@ModelAttribute/@InitBinder的叠加效果 。 -
@RestControllerAdvice:这是一个组合注解,等价于@ControllerAdvice + @ResponseBody。如果你的全局异常处理类全部返回JSON数据,使用这个注解会更方便 。
💎 总结
@ControllerAdvice是 Spring MVC 中实现关注点分离 的强大工具。它将分散在各个控制器中的横切关注点(Cross-cutting Concerns)集中管理,极大地提升了代码的可维护性、整洁性和健壮性。