Spring MVC 加载bean以及与Servlet的联系
Spring MVC 中 Bean 的加载,本质上是两个有层级关系的 IoC 容器(ApplicationContext) 的启动过程。它们由 ContextLoaderListener 和 DispatcherServlet 分别负责创建,并与底层的 Servlet 容器(如 Tomcat) 紧密集成。
我按照从"总体架构"到"源码细节"的顺序来梳理。
第一阶段:根容器的加载(ContextLoaderListener)
第二阶段:DispatcherServlet 的加载(Web容器)
🏛️ 总体架构:双重容器体系
Servlet 容器启动时,会创建两个独立的 Spring 容器:
- 根容器 (Root WebApplicationContext):由
ContextLoaderListener加载,通常用于管理 Service、Dao 等全局业务组件。 - Web 容器 (DispatcherServlet 持有的子容器):由每个
DispatcherServlet加载,通常用于管理 @Controller、ViewResolver 等 Web 组件。
两者的关系是:Web 容器将根容器设为父容器。子容器可以访问父容器中的 Bean,反之则不行,这形成了一种清晰的职责分离。
接下来,通过源码解析这一过程。
⚙️ 根容器(Bean工厂)加载:ContextLoaderListener 的使命
在传统的 web.xml 配置中,根容器的加载与 Servlet 容器的启动事件绑定在一起:
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>classpath:spring-config.xml</param-value>
</context-param>
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
这段配置背后,有着精密的源码逻辑。
1. 监控 Servlet 生命周期:ContextLoaderListener
ContextLoaderListener 实现了 javax.servlet.ServletContextListener 接口。当 Tomcat 容器启动时,会自动调用其 contextInitialized 方法。
下面是 ContextLoaderListener 的源码:
public class ContextLoaderListener extends ContextLoader implements ServletContextListener {
// Tomcat 启动时调用的方法
@Override
public void contextInitialized(ServletContextEvent event) {
// 核心:调用父类的 initWebApplicationContext 方法,并传入 ServletContext
initWebApplicationContext(event.getServletContext());
}
// Tomcat 关闭时调用的方法,用于销毁容器
@Override
public void contextDestroyed(ServletContextEvent event) {
closeWebApplicationContext(event.getServletContext());
ContextCleanupListener.cleanupAttributes(event.getServletContext());
}
}
ContextLoaderListener 将核心工作委托给了父类 ContextLoader。
2. initWebApplicationContext 方法:创建与初始化Bean
ContextLoader.initWebApplicationContext 是根容器创建和初始化的核心。
public WebApplicationContext initWebApplicationContext(ServletContext servletContext) {
// 1. 检查 ServletContext 中是否已存在根容器,若存在则抛出异常,防止重复初始化
if (servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE) != null) {
throw new IllegalStateException("Cannot initialize context because there is already a root application context present.");
}
// 2. 创建 WebApplicationContext 实例
if (this.context == null) {
// 通过 createWebApplicationContext 方法创建容器(如 XmlWebApplicationContext)
this.context = createWebApplicationContext(servletContext);
}
// 3. 配置并刷新容器(解析配置、创建Bean)
configureAndRefreshWebApplicationContext((ConfigurableWebApplicationContext) this.context, servletContext);
// 4. 将创建好的根容器存入 ServletContext 中,Key 为常量:WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE
servletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE, this.context);
return this.context;
}
createWebApplicationContext 方法会根据配置实例化容器:
protected WebApplicationContext createWebApplicationContext(ServletContext sc) {
// 1. 获取上下文类,默认为 XmlWebApplicationContext
Class<?> contextClass = determineContextClass(sc);
// 2. 检查类型合法性
if (!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)) {
throw new ApplicationContextException("Custom context class [" + contextClass.getName() +
"] is not of type ConfigurableWebApplicationContext");
}
// 3. 通过反射实例化容器
ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
return wac;
}
configureAndRefreshWebApplicationContext 方法则负责真正的 Bean 加载:
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac, ServletContext sc) {
// 1. 设置 ServletContext 和 ConfigLocation
wac.setServletContext(sc);
String configLocation = sc.getInitParameter(CONFIG_LOCATION_PARAM);
if (configLocation != null) {
wac.setConfigLocation(configLocation);
}
// 2. 设置环境、父容器(此时为null)
// ... 其他设置
// 3. 核心调用:刷新容器,这行代码会触发 所有Bean 的解析、注册、实例化和依赖注入!
wac.refresh();
}
refresh()是 IoC 容器初始化的真正入口,它会加载配置文件、扫描组件、创建 Bean 实例、完成依赖注入。此处不展开,但其结果是:所有在spring-config.xml或通过注解声明的 Service、Dao 等 Bean 被创建和管理。
3. 与 Servlet 容器的关联:存入 ServletContext
最关键的一行代码是 servletContext.setAttribute(...)。它将根容器存储到 ServletContext 中,这个 ServletContext 是 Servlet 容器的全局上下文。
这意味着:Spring 根容器成为 Servlet 容器的一个全局属性,任何 Servlet(包括 DispatcherServlet)都可以通过 ServletContext 访问它。
至此,根容器被加载并存放于 Servlet 容器的全局空间中,静待后续使用。
🚀 Web容器(处理请求线程)加载:DispatcherServlet 的使命
根容器加载完,接下来 DispatcherServlet 会启动,初始化它自己持有的 Web 容器(子容器)。
1. Servlet 的起点:init() 方法
作为一个 Servlet,DispatcherServlet 的入口是 init() 方法。它的继承链是:DispatcherServlet -> FrameworkServlet -> HttpServletBean -> HttpServlet。init() 方法最终在 HttpServletBean 中被实现。
public abstract class HttpServletBean extends HttpServlet {
@Override
public final void init() throws ServletException {
// ... 从 web.xml 中读取 init-param 配置,并通过 BeanWrapper 设置到当前 Servlet 实例中
// 模板方法,由子类 FrameworkServlet 实现
initServletBean();
}
}
2. 容器的创建与关联:initWebApplicationContext
initServletBean 在 FrameworkServlet 中的实现最终会调用 initWebApplicationContext,这是 Web 容器加载的核心。
protected WebApplicationContext initWebApplicationContext() {
// 1. 获取根容器【关键步骤】
// 从 ServletContext 中以 ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 为 Key 获取根容器
WebApplicationContext rootContext = WebApplicationContextUtils
.getWebApplicationContext(getServletContext());
WebApplicationContext wac = null;
// 2. 如果通过构造器传入了容器(如在 Servlet 3.0+ 环境中),则直接使用
if (this.webApplicationContext != null) {
wac = this.webApplicationContext;
// ... 设置父容器
}
if (wac == null) {
// 3. 没有传入则创建一个新的 Web 容器
wac = createWebApplicationContext(rootContext); // 创建时会将 rootContext 设为父容器
}
// 4. 刷新 Web 容器,初始化其内部的 Bean(如 Controller)
configureAndRefreshWebApplicationContext(wac);
return wac;
}
这里的核心是
WebApplicationContextUtils.getWebApplicationContext(getServletContext());它正是从 Servlet 容器的全局空间中拿到了之前在ContextLoaderListener中创建并存入的根容器。
3. createWebApplicationContext: 创建并建立父子关系
创建子容器时,会建立父容器关系:
protected WebApplicationContext createWebApplicationContext(@Nullable ApplicationContext parent) {
// 1. 获取上下文类,默认为XmlWebApplicationContext
Class<?> contextClass = getContextClass();
// 2. 通过反射实例化一个新的 ConfigurableWebApplicationContext
ConfigurableWebApplicationContext wac = (ConfigurableWebApplicationContext) BeanUtils.instantiateClass(contextClass);
// 3. 设置环境
wac.setEnvironment(getEnvironment());
// 4. 设置父容器【关键步骤】
wac.setParent(parent);
// 5. 配置位置,例如 spring-mvc.xml 或 MvcConfig
String configLocation = getContextConfigLocation();
if (configLocation != null) {
wac.setConfigLocation(configLocation);
}
// 6. 完成后续容器初始化与刷新操作(配置、监听器、refresh 等)
configureAndRefreshWebApplicationContext(wac);
return wac;
}
wac.setParent(parent); 这行代码建立父子容器关系。
4. configureAndRefreshWebApplicationContext 与 onRefresh
protected void configureAndRefreshWebApplicationContext(ConfigurableWebApplicationContext wac) {
// 1. 设置ServletContext、ServletConfig...
// 2. 刷新子容器(所有Web组件的创建、依赖注入发生在此)
wac.refresh();
}
refresh() 完成后,FrameworkServlet 会调用模板方法 onRefresh(),而 DispatcherServlet 则重写了 onRefresh(),用于初始化其核心策略组件。
@Override
protected void onRefresh(ApplicationContext context) {
// 初始化Spring MVC的九大核心组件,如 HandlerMapping、HandlerAdapter、ViewResolver 等
initStrategies(context);
}
protected void initStrategies(ApplicationContext context) {
// 每个方法都会尝试从子容器(WebApplicationContext)中查找相应组件,
// 如果找不到,就使用 DispatcherServlet.properties 中的默认配置
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
📊 Controller Bean 的注册与 HandlerMapping 建立
@Controller Bean 注册有两种情形:
情形一:基于 XML 配置
若 initWebApplicationContext 时配置了 contextConfigLocation(如 classpath:spring-mvc.xml),容器在 refresh() 时会解析该 XML,处理其中的 <context:component-scan> 或 <bean class="...Controller"/> 标签,完成 @Controller 的 Bean 定义、注册。
情形二:基于注解配置(更常见)
通过 AnnotationConfigWebApplicationContext 等注解驱动的容器,在 refresh() 时也会被 ClassPathBeanDefinitionScanner 扫描,检测到标注有 @Controller、@Service 的类,为其创建 BeanDefinition,并注册到容器中。
关键时机:afterPropertiesSet() 与 HandlerMapping 建立
在 Bean 初始化完成(refresh())后,Spring 会调用实现了 InitializingBean 接口的 Bean 的 afterPropertiesSet() 方法。RequestMappingHandlerMapping 恰巧如此:
public class RequestMappingHandlerMapping extends AbstractHandlerMethodMapping<RequestMappingInfo> {
@Override
public void afterPropertiesSet() {
// 1. 初始化配置
initConfiguration();
// 2. 核心:扫描容器中所有的 Bean,找出标注了 @Controller 和 @RequestMapping 的方法,
// 并解析为 RequestMappingInfo,最终存储到 MappingRegistry 中
super.afterPropertiesSet();
}
}
super.afterPropertiesSet() 是真正的处理入口。
至此,@Controller 注册完成,HandlerMapping 也建立了 URL 到处理器的映射,一个请求到来时,能正确找到对应方法。
🔗 总结:容器与 Servlet 的关联
整理一下从 Servlet 容器启动到 Spring MVC 就绪的关键步骤和对应源码:
- 容器启动:Tomcat 等启动,加载
web.xml或通过 SPI 发现配置。 - 根容器加载:
ContextLoaderListener的contextInitialized被 Tomcat 调用,最终调用ContextLoader.initWebApplicationContext创建WebApplicationContext并存入ServletContext。 - Web容器加载:作为 Servlet 的
DispatcherServlet,其init()被 Tomcat 调用,最终进入FrameworkServlet.initWebApplicationContext。 - 父子关联:
initWebApplicationContext中从ServletContext拿到根容器,并通过wac.setParent(parent)建立父子关联。 - Bean 注册:子容器
refresh()→ 扫描 →@Controller等 Bean 被注册。afterPropertiesSet()→HandlerMapping建立 URL 映射。 - 策略初始化:
onRefresh()→initStrategies()加载 Spring MVC 的九大组件。 - 请求接管:一切就绪,
DispatcherServlet开始处理 HTTP 请求。
在 Servlet 3.0+ 环境中,整个启动过程可以完全摆脱
web.xml,通过实现ServletContainerInitializer或继承AbstractAnnotationConfigDispatcherServletInitializer来完成。
连贯阅读顺序总结(模拟实际 debug 路径)
要一步步跟踪源码,建议按以下顺序打断点或阅读:
- Tomcat 启动 → 触发
ContextLoaderListener.contextInitialized(ServletContextEvent) - 进入
ContextLoader.initWebApplicationContext(ServletContext)- 观察
createWebApplicationContext如何实例化XmlWebApplicationContext - 进入
configureAndRefreshWebApplicationContext→wac.refresh() - 在
refresh()中观察obtainFreshBeanFactory()→loadBeanDefinitions()读取 XML/注解配置
- 观察
- 回到
initWebApplicationContext,看servletContext.setAttribute(...)存入根容器 - DispatcherServlet 初始化:Tomcat 调用
HttpServletBean.init()→FrameworkServlet.initServletBean()→initWebApplicationContext() - 在
initWebApplicationContext中:- 查看
WebApplicationContextUtils.getWebApplicationContext(servletContext)取出根容器 - 进入
createWebApplicationContext(rootContext),注意setParent(parent) - 再次调用
refresh()刷新子容器
- 查看
- 子容器
refresh()完成后,FrameworkServlet调用onRefresh(),进入DispatcherServlet.initStrategies(),查看initHandlerMappings等 - 最后,请求进来时,
DispatcherServlet.doDispatch()会使用这些已初始化的组件。
提示:如果想看
@Controller是如何被HandlerMapping识别的,可以在RequestMappingHandlerMapping.afterPropertiesSet()里打断点,它会在子容器refresh()的过程中被调用(因为实现了InitializingBean)。
高阶:无 web.xml 的启动路径(Spring Boot / Servlet 3.0+)
如果没有 web.xml,入口是 META-INF/services/javax.servlet.ServletContainerInitializer 文件中配置的 SpringServletContainerInitializer。它会调用 WebApplicationInitializer 的实现类(如 AbstractAnnotationConfigDispatcherServletInitializer),其内部最终仍然会创建 ContextLoaderListener 并注册 DispatcherServlet,逻辑与上述一致,只是不再需要手动编写 XML。
可以这样跟踪:
- 在
SpringServletContainerInitializer.onStartup(Set<Class<?>>, ServletContext)打断点 - 进入
AbstractDispatcherServletInitializer.registerDispatcherServlet(ServletContext)等
但核心的父子容器创建和关联逻辑与 web.xml 方式完全一样。
通过以上路径,就能从源码层面完整理解 Spring MVC 是如何加载 Bean,以及如何与 Servlet 容器(如 Tomcat)建立联系的了。
Spring MVC 组件
在 Spring MVC 中,DispatcherServlet 的 initStrategies 方法会初始化九大核心组件,为后续的请求处理做好准备。每个组件都有明确的职责和特定的运用时期(请求处理流程中的某个阶段)。
// DispatcherServlet.java
protected void initStrategies(ApplicationContext context) {
initMultipartResolver(context);
initLocaleResolver(context);
initThemeResolver(context);
initHandlerMappings(context);
initHandlerAdapters(context);
initHandlerExceptionResolvers(context);
initRequestToViewNameTranslator(context);
initViewResolvers(context);
initFlashMapManager(context);
}
运用时期组件总览(按请求处理顺序)
| 步骤 | 组件 | 运用阶段 |
|---|---|---|
| 1 | MultipartResolver |
识别文件上传请求,包装请求对象 |
| 2 | FlashMapManager |
读取上次重定向保存的 Flash 属性 |
| 3 | HandlerMapping |
将 URL 映射到处理器执行链 |
| 4 | HandlerAdapter |
适配并执行处理器(Controller) |
| 5 | HandlerExceptionResolver |
处理器执行时若抛异常,在此处理 |
| 6 | RequestToViewNameTranslator |
当未返回视图名时,生成默认视图名 |
| 7 | ViewResolver |
将逻辑视图名解析为 View 对象 |
| 8 | LocaleResolver |
视图渲染时确定国际化区域 |
| 9 | ThemeResolver |
视图渲染时确定主题(可选) |
| 10 | FlashMapManager |
将 Flash 属性保存以用于重定向后 |
注意:LocaleResolver 和 ThemeResolver 也可能在 Controller 方法中提前被调用,但最主要还是在视图渲染阶段。
更多推荐


所有评论(0)