深入剖析 SpringMVC 异常处理与 Spring 父子容器
在 Spring + SpringMVC 的 Web 应用中,有两个核心概念经常被开发者提起:异常处理机制 和父子容器关系。理解它们不仅有助于写出更健壮的代码,还能避免许多隐藏的配置陷阱。本文将从源码逻辑到实际应用,结合流程图与结构图,为你系统梳理这两块知识。
一、SpringMVC 异常处理机制
1.1 核心流程:谁来解决我的异常?
当 SpringMVC 的 DispatcherServlet 在处理请求过程中抛出异常时,它并不会直接把异常抛给容器,而是委托给一组 异常处理器(HandlerExceptionResolver)。流程如下:
能
不能
全部不能处理
Controller 抛出异常
遍历所有
HandlerExceptionResolver
当前Resolver
能否处理?
调用 resolver.resolveException()
返回 ModelAndView 或错误响应
继续下一个Resolver
继续向上抛出异常
最终由容器处理
SpringMVC 默认注册了三个异常解析器,它们按固定顺序执行,一旦某个解析器返回非空的 ModelAndView,后续解析器就不再执行。
1.2 三大默认异常解析器
| 解析器 | 作用 | 触发条件 |
|---|---|---|
ExceptionHandlerExceptionResolver |
处理被 @ExceptionHandler 注解标记的方法 |
控制器类或 @ControllerAdvice 中定义了匹配的异常处理方法 |
ResponseStatusExceptionResolver |
处理带有 @ResponseStatus 注解的异常类 |
抛出的异常类上标注了 @ResponseStatus |
DefaultHandlerExceptionResolver |
处理 SpringMVC 内置的标准异常 | 如 TypeMismatchException、MissingServletRequestParameterException 等 |
1.2.1 ExceptionHandlerExceptionResolver
这是最常用、最灵活的解析器。它会在 @Controller 或 @ControllerAdvice 类中查找带有 @ExceptionHandler 注解的方法,根据异常类型进行匹配。
java
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ArithmeticException.class)
public Result handleArithmetic(ArithmeticException e) {
return Result.error("除数不能为零");
}
}
1.2.2 ResponseStatusExceptionResolver
如果自定义异常类上标注了 @ResponseStatus,当该异常被抛出时,解析器会自动设置 HTTP 状态码。
java
@ResponseStatus(HttpStatus.NOT_FOUND)
public class ResourceNotFoundException extends RuntimeException {
}
1.2.3 DefaultHandlerExceptionResolver
处理 SpringMVC 自身抛出的标准异常,比如参数类型不匹配、缺少必要参数等。它会返回一个默认的错误视图(如 400 错误页面)。
1.3 异常处理整体时序图
ExceptionResolvers Controller HandlerAdapter DispatcherServlet Client ExceptionResolvers Controller HandlerAdapter DispatcherServlet Client alt [能处理] loop [每个解析器] 请求 执行 Controller 调用方法 抛出异常 传播异常 遍历解析器 能否处理? 解析异常 → ModelAndView 返回错误响应
💡 注意 :如果所有解析器都返回
null,则异常会继续上抛,最终导致 500 错误或由 Servlet 容器处理。
1.4 实战建议
- 使用
@ControllerAdvice统一处理业务异常,避免在每个 Controller 中重复写 try-catch。 - 自定义异常时,可结合
@ResponseStatus快速指定状态码。 - 若需记录日志,可在全局异常处理方法中添加日志输出。
二、Spring 父子容器(Spring + SpringMVC)
在传统的 SSM(Spring + SpringMVC + MyBatis)项目中,应用启动时会创建两个 Spring 容器:
- 父容器 (Root WebApplicationContext):由
ContextLoaderListener启动,加载applicationContext.xml或通过@Configuration配置的 Spring 核心组件(如 Service、DAO、数据源、事务管理等)。 - 子容器 (Servlet WebApplicationContext):由
DispatcherServlet启动,加载spring-mvc.xml或 MVC 配置(如 Controller、视图解析器、拦截器等)。
2.1 父子容器结构图
子容器 SpringMVC
父容器 Spring
父容器无法访问子容器
子容器可以访问父容器
Service Bean
DAO Bean
数据源
事务管理器
Controller Bean
拦截器
视图解析器
2.2 核心区别与规则
| 特性 | 父容器(Spring) | 子容器(SpringMVC) |
|---|---|---|
| 加载时机 | 应用启动时由 ContextLoaderListener 加载 |
DispatcherServlet 初始化时加载 |
| 配置位置 | applicationContext.xml 或 @Configuration |
spring-mvc.xml 或 @Configuration + @EnableWebMvc |
| 管理的 Bean | Service、DAO、数据源、事务、AOP 等 | Controller、拦截器、视图解析器、局部异常处理器 |
| 能否访问对方 | ❌ 不能访问子容器的 Bean | ✅ 可以访问父容器的 Bean |
| Properties 隔离 | 各自加载的 *.properties 文件互不共享 |
子容器无法直接使用父容器的属性文件(除非通过 Bean 引用) |
2.3 为什么这样设计?
- 职责分离:父容器负责业务逻辑和数据层,子容器只负责 Web 层。子容器依赖父容器,但父容器不应感知 Web 层,避免循环依赖。
- 模块化 :可以在同一个父容器下挂载多个子容器(例如多个
DispatcherServlet对应不同模块)。 - 性能优化:Controller 的创建和销毁由子容器管理,父容器不需要扫描 Web 层的注解。
2.4 常见问题与注意事项
❌ 问题1:父容器中扫描了 @Controller
如果父容器配置了 <context:component-scan base-package="com.example" />(未排除 @Controller),会导致两个问题:
- Controller 被父容器和子容器各初始化一次,可能出现事务代理失效。
@ResponseBody等 Web 相关注解可能无法正常工作。
✅ 正确做法 :
父容器配置扫描时排除 @Controller:
xml
<context:component-scan base-package="com.example">
<context:exclude-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
子容器只扫描 @Controller:
xml
<context:component-scan base-package="com.example" use-default-filters="false">
<context:include-filter type="annotation" expression="org.springframework.stereotype.Controller"/>
</context:component-scan>
❌ 问题2:在父容器 Bean 中试图 @Autowired 子容器的 Bean
例如在 Service 中注入 Controller ------ 启动时会报错,因为父容器找不到对应类型的 Bean。
✅ 解决方案 :
重新审视设计,Service 不应该依赖 Controller。如果确实需要(极少见),可以通过 ApplicationContext 手动获取子容器的 Bean,但强烈不推荐。
❌ 问题3:属性文件隔离
父容器加载的 jdbc.properties 在子容器中无法通过 @Value 直接注入。如果需要共享,可以在父容器中配置 PropertySourcesPlaceholderConfigurer,子容器通过引用父容器的 Bean 间接使用。
2.5 现代 Spring Boot 环境下的变化
在 Spring Boot 中,默认只有一个容器 (基于 SpringApplication.run 创建)。传统的父子容器分离不再必须,但如果你手动创建 SpringApplicationBuilder,仍然可以实现父子容器。Spring Boot 更推荐单一容器 + 明确的分层包结构。
三、总结
| 知识点 | 关键结论 |
|---|---|
| SpringMVC 异常处理 | 按顺序遍历 HandlerExceptionResolver,优先使用 @ExceptionHandler,其次是 @ResponseStatus,最后处理标准异常。建议使用 @ControllerAdvice 统一管理。 |
| 父子容器 | 父容器(Spring)不能访问子容器(SpringMVC)的 Bean;子容器可以访问父容器的 Bean。属性文件相互隔离。传统 SSM 项目必须正确配置扫描包,避免重复加载。 |
理解这两个机制,可以让你在排查 Web 应用异常、处理依赖注入失败、设计分层架构时更加游刃有余。
如果你正在使用 Spring Boot,绝大多数默认配置已经替你做好了,但掌握原理仍然有助于应对复杂场景下的定制需求。
📌 参考资料
- Spring Framework 官方文档:HandlerExceptionResolver
- Spring MVC 官方文档:Context Hierarchy