Spring MVC 中 Bean 的加载,本质上是两个有层级关系的 IoC 容器(ApplicationContext) 的启动过程。它们由 ContextLoaderListenerDispatcherServlet 分别负责创建,并与底层的 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 -> HttpServletinit() 方法最终在 HttpServletBean 中被实现。

public abstract class HttpServletBean extends HttpServlet {
    @Override
    public final void init() throws ServletException {
        // ... 从 web.xml 中读取 init-param 配置,并通过 BeanWrapper 设置到当前 Servlet 实例中
        
        // 模板方法,由子类 FrameworkServlet 实现
        initServletBean();
    }
}

2. 容器的创建与关联:initWebApplicationContext

initServletBeanFrameworkServlet 中的实现最终会调用 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. configureAndRefreshWebApplicationContextonRefresh

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 就绪的关键步骤和对应源码:

  1. 容器启动:Tomcat 等启动,加载 web.xml 或通过 SPI 发现配置。
  2. 根容器加载ContextLoaderListenercontextInitialized 被 Tomcat 调用,最终调用 ContextLoader.initWebApplicationContext 创建 WebApplicationContext 并存入 ServletContext
  3. Web容器加载:作为 Servlet 的 DispatcherServlet,其 init() 被 Tomcat 调用,最终进入 FrameworkServlet.initWebApplicationContext
  4. 父子关联initWebApplicationContext 中从 ServletContext 拿到根容器,并通过 wac.setParent(parent) 建立父子关联。
  5. Bean 注册:子容器 refresh() → 扫描 → @Controller 等 Bean 被注册。afterPropertiesSet()HandlerMapping 建立 URL 映射。
  6. 策略初始化onRefresh()initStrategies() 加载 Spring MVC 的九大组件。
  7. 请求接管:一切就绪,DispatcherServlet 开始处理 HTTP 请求。

在 Servlet 3.0+ 环境中,整个启动过程可以完全摆脱 web.xml,通过实现 ServletContainerInitializer 或继承 AbstractAnnotationConfigDispatcherServletInitializer 来完成。


连贯阅读顺序总结(模拟实际 debug 路径)

要一步步跟踪源码,建议按以下顺序打断点或阅读:

  1. Tomcat 启动 → 触发 ContextLoaderListener.contextInitialized(ServletContextEvent)
  2. 进入 ContextLoader.initWebApplicationContext(ServletContext)
    • 观察 createWebApplicationContext 如何实例化 XmlWebApplicationContext
    • 进入 configureAndRefreshWebApplicationContextwac.refresh()
    • refresh() 中观察 obtainFreshBeanFactory()loadBeanDefinitions() 读取 XML/注解配置
  3. 回到 initWebApplicationContext,看 servletContext.setAttribute(...) 存入根容器
  4. DispatcherServlet 初始化:Tomcat 调用 HttpServletBean.init()FrameworkServlet.initServletBean()initWebApplicationContext()
  5. initWebApplicationContext 中:
    • 查看 WebApplicationContextUtils.getWebApplicationContext(servletContext) 取出根容器
    • 进入 createWebApplicationContext(rootContext),注意 setParent(parent)
    • 再次调用 refresh() 刷新子容器
  6. 子容器 refresh() 完成后,FrameworkServlet 调用 onRefresh(),进入 DispatcherServlet.initStrategies(),查看 initHandlerMappings
  7. 最后,请求进来时,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 中,DispatcherServletinitStrategies 方法会初始化九大核心组件,为后续的请求处理做好准备。每个组件都有明确的职责和特定的运用时期(请求处理流程中的某个阶段)。

// 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 属性保存以用于重定向后

注意:LocaleResolverThemeResolver 也可能在 Controller 方法中提前被调用,但最主要还是在视图渲染阶段。

Logo

汇聚全球AI编程工具,助力开发者即刻编程。

更多推荐