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)集中管理,极大地提升了代码的可维护性、整洁性和健壮性

相关推荐
程序员鱼皮1 天前
又被 Cursor 烧了 1 万块,我麻了。。。
前端·后端·ai·程序员·大模型·编程
tonydf1 天前
动态表单之后:如何构建一个PDF 打印引擎?
后端
allbs1 天前
spring boot项目excel导出功能封装——4.导入
spring boot·后端·excel
用户69371750013841 天前
11.Kotlin 类:继承控制的关键 ——final 与 open 修饰符
android·后端·kotlin
用户69371750013841 天前
10.Kotlin 类:延迟初始化:lateinit 与 by lazy 的对决
android·后端·kotlin
稚辉君.MCA_P8_Java1 天前
通义 Go 语言实现的插入排序(Insertion Sort)
数据结构·后端·算法·架构·golang
未若君雅裁1 天前
sa-token前后端分离集成redis与jwt基础案例
后端
江小北1 天前
美团面试:MySQL为什么能够在大数据量、高并发的业务中稳定运行?
后端
zhaomy20251 天前
从ThreadLocal到ScopedValue:Java上下文管理的架构演进与实战指南
java·后端
华仔啊1 天前
10分钟搞定!SpringBoot+Vue3 整合 SSE 实现实时消息推送
java·vue.js·后端