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

相关推荐
间彧2 小时前
@ControllerAdvice与AOP切面编程在处理异常时有什么区别和各自的优势?
后端
间彧3 小时前
什么是Region多副本容灾
后端
爱敲代码的北3 小时前
WPF容器控件布局与应用学习笔记
后端
爱敲代码的北3 小时前
XAML语法与静态资源应用
后端
清空mega3 小时前
从零开始搭建 flask 博客实验(5)
后端·python·flask
爱敲代码的北3 小时前
UniformGrid 均匀网格布局学习笔记
后端
一只叫煤球的猫3 小时前
从1996到2025——细说Java锁的30年进化史
java·后端·性能优化
喵个咪3 小时前
开箱即用的GO后台管理系统 Kratos Admin - 数据脱敏和隐私保护
后端·go·protobuf
我是天龙_绍4 小时前
Java Object equal重写
后端