Spring @ControllerAdvice详解与应用实战

@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();
    }
}

⚠️ 高级用法与注意事项

  1. 控制生效范围 :使用 basePackages, annotations等属性可以限制 @ControllerAdvice的生效范围 。

    less 复制代码
    @ControllerAdvice(basePackages = {"com.example.admin.controller"}) // 只对admin包下的控制器生效
    @ControllerAdvice(annotations = RestController.class) // 只对带有@RestController注解的控制器生效
  2. 处理多个@ControllerAdvice的顺序 :当有多个 @ControllerAdvice类时,可以使用 @Order注解或实现 Ordered接口来控制它们的执行顺序。顺序会影响异常处理(匹配到的第一个异常处理器生效)和 @ModelAttribute/@InitBinder的叠加效果 。

  3. @RestControllerAdvice :这是一个组合注解,等价于 @ControllerAdvice + @ResponseBody。如果你的全局异常处理类全部返回JSON数据,使用这个注解会更方便 。

💎 总结

@ControllerAdvice是 Spring MVC 中实现关注点分离 的强大工具。它将分散在各个控制器中的横切关注点(Cross-cutting Concerns)集中管理,极大地提升了代码的可维护性、整洁性和健壮性

相关推荐
uzong1 天前
后端线上发布计划模板
后端
uzong1 天前
软件工程师应该关注的几种 UML 图
后端
上进小菜猪1 天前
基于 YOLOv8 的 100 类中药材智能识别实战 [目标检测完整源码]
后端
码事漫谈1 天前
AI 技能工程入门:从独立能力到协作生态
后端
码事漫谈1 天前
构建高并发AI服务网关:C++与gRPC的工程实践
后端
颜酱1 天前
前端必备动态规划的10道经典题目
前端·后端·算法
半夏知半秋1 天前
rust学习-闭包
开发语言·笔记·后端·学习·rust
LucianaiB1 天前
【保姆级教程】10分钟把手机变成AI Agent:自动刷课、回消息,学不会我“退网”!
后端
Mr -老鬼1 天前
功能需求对前后端技术选型的横向建议
开发语言·前端·后端·前端框架
IT=>小脑虎1 天前
Go语言零基础小白学习知识点【基础版详解】
开发语言·后端·学习·golang