SpringMVC 异常处理?Spring 父子容器?

深入剖析 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 内置的标准异常 TypeMismatchExceptionMissingServletRequestParameterException
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,绝大多数默认配置已经替你做好了,但掌握原理仍然有助于应对复杂场景下的定制需求。

📌 参考资料

相关推荐
qq_422152572 分钟前
PDF 解密工具怎么选?2026 年文档密码移除方案与注意事项
java·前端·pdf
布朗克16812 分钟前
38 Spring Boot入门——自动配置、核心注解与Starter机制
java·spring boot·后端
沪漂阿龙20 分钟前
LangChain 系列:Structured Output结构化输出与源码解析
java·人工智能·架构·langchain
半夜燃烧的香烟23 分钟前
springboot3.0 集成minio上传文件,支持多个桶名
java·开发语言·spring boot
J2虾虾35 分钟前
Android支持Java语言的标准
android·java·开发语言
Oo_行者_oO1 小时前
Spring Schedule + ShedLock + RabbitMQ 生产级落地方案 - 云楼(中国)
java·后端
AI人工智能+电脑小能手1 小时前
【大白话说Java面试题 第113题】【并发篇】第13题:说一下乐观锁的优点和缺点?
java·开发语言·面试
Mahir081 小时前
HashMap 底层原理深度解密:从数据结构到 JDK1.7/1.8 演进全解
java·后端·面试·hashmap
小马爱打代码1 小时前
Spring Boot 自动装配流程
java·spring boot·后端