当前位置: 首页 > news >正文

spring MVC 加载bean以及与Servlet的联系

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的源码:

publicclassContextLoaderListenerextendsContextLoaderimplementsServletContextListener{// Tomcat 启动时调用的方法@OverridepublicvoidcontextInitialized(ServletContextEventevent){// 核心:调用父类的 initWebApplicationContext 方法,并传入 ServletContextinitWebApplicationContext(event.getServletContext());}// Tomcat 关闭时调用的方法,用于销毁容器@OverridepublicvoidcontextDestroyed(ServletContextEventevent){closeWebApplicationContext(event.getServletContext());ContextCleanupListener.cleanupAttributes(event.getServletContext());}}

ContextLoaderListener将核心工作委托给了父类ContextLoader

2.initWebApplicationContext方法:创建与初始化Bean

ContextLoader.initWebApplicationContext是根容器创建和初始化的核心。

publicWebApplicationContextinitWebApplicationContext(ServletContextservletContext){// 1. 检查 ServletContext 中是否已存在根容器,若存在则抛出异常,防止重复初始化if(servletContext.getAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE)!=null){thrownewIllegalStateException("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_ATTRIBUTEservletContext.setAttribute(WebApplicationContext.ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE,this.context);returnthis.context;}

createWebApplicationContext方法会根据配置实例化容器:

protectedWebApplicationContextcreateWebApplicationContext(ServletContextsc){// 1. 获取上下文类,默认为 XmlWebApplicationContextClass<?>contextClass=determineContextClass(sc);// 2. 检查类型合法性if(!ConfigurableWebApplicationContext.class.isAssignableFrom(contextClass)){thrownewApplicationContextException("Custom context class ["+contextClass.getName()+"] is not of type ConfigurableWebApplicationContext");}// 3. 通过反射实例化容器ConfigurableWebApplicationContextwac=(ConfigurableWebApplicationContext)BeanUtils.instantiateClass(contextClass);returnwac;}

configureAndRefreshWebApplicationContext方法则负责真正的 Bean 加载:

protectedvoidconfigureAndRefreshWebApplicationContext(ConfigurableWebApplicationContextwac,ServletContextsc){// 1. 设置 ServletContext 和 ConfigLocationwac.setServletContext(sc);StringconfigLocation=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中被实现。

publicabstractclassHttpServletBeanextendsHttpServlet{@Overridepublicfinalvoidinit()throwsServletException{// ... 从 web.xml 中读取 init-param 配置,并通过 BeanWrapper 设置到当前 Servlet 实例中// 模板方法,由子类 FrameworkServlet 实现initServletBean();}}

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

initServletBeanFrameworkServlet中的实现最终会调用initWebApplicationContext,这是 Web 容器加载的核心。

protectedWebApplicationContextinitWebApplicationContext(){// 1. 获取根容器【关键步骤】// 从 ServletContext 中以 ROOT_WEB_APPLICATION_CONTEXT_ATTRIBUTE 为 Key 获取根容器WebApplicationContextrootContext=WebApplicationContextUtils.getWebApplicationContext(getServletContext());WebApplicationContextwac=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);returnwac;}

这里的核心是WebApplicationContextUtils.getWebApplicationContext(getServletContext());它正是从 Servlet 容器的全局空间中拿到了之前在ContextLoaderListener中创建并存入的根容器

3.createWebApplicationContext: 创建并建立父子关系

创建子容器时,会建立父容器关系:

protectedWebApplicationContextcreateWebApplicationContext(@NullableApplicationContextparent){// 1. 获取上下文类,默认为XmlWebApplicationContextClass<?>contextClass=getContextClass();// 2. 通过反射实例化一个新的 ConfigurableWebApplicationContextConfigurableWebApplicationContextwac=(ConfigurableWebApplicationContext)BeanUtils.instantiateClass(contextClass);// 3. 设置环境wac.setEnvironment(getEnvironment());// 4. 设置父容器【关键步骤】wac.setParent(parent);// 5. 配置位置,例如 spring-mvc.xml 或 MvcConfigStringconfigLocation=getContextConfigLocation();if(configLocation!=null){wac.setConfigLocation(configLocation);}// 6. 完成后续容器初始化与刷新操作(配置、监听器、refresh 等)configureAndRefreshWebApplicationContext(wac);returnwac;}

wac.setParent(parent);这行代码建立父子容器关系。

4.configureAndRefreshWebApplicationContextonRefresh

protectedvoidconfigureAndRefreshWebApplicationContext(ConfigurableWebApplicationContextwac){// 1. 设置ServletContext、ServletConfig...// 2. 刷新子容器(所有Web组件的创建、依赖注入发生在此)wac.refresh();}

refresh()完成后,FrameworkServlet会调用模板方法onRefresh(),而DispatcherServlet则重写了onRefresh(),用于初始化其核心策略组件。

@OverrideprotectedvoidonRefresh(ApplicationContextcontext){// 初始化Spring MVC的九大核心组件,如 HandlerMapping、HandlerAdapter、ViewResolver 等initStrategies(context);}protectedvoidinitStrategies(ApplicationContextcontext){// 每个方法都会尝试从子容器(WebApplicationContext)中查找相应组件,// 如果找不到,就使用 DispatcherServlet.properties 中的默认配置initMultipartResolver(context);initLocaleResolver(context);initThemeResolver(context);initHandlerMappings(context);initHandlerAdapters(context);initHandlerExceptionResolvers(context);initRequestToViewNameTranslator(context);initViewResolvers(context);initFlashMapManager(context);}

📊 Controller Bean 的注册与 HandlerMapping 建立

@ControllerBean 注册有两种情形:

情形一:基于 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恰巧如此:

publicclassRequestMappingHandlerMappingextendsAbstractHandlerMethodMapping<RequestMappingInfo>{@OverridepublicvoidafterPropertiesSet(){// 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.javaprotectedvoidinitStrategies(ApplicationContextcontext){initMultipartResolver(context);initLocaleResolver(context);initThemeResolver(context);initHandlerMappings(context);initHandlerAdapters(context);initHandlerExceptionResolvers(context);initRequestToViewNameTranslator(context);initViewResolvers(context);initFlashMapManager(context);}

运用时期组件总览(按请求处理顺序)

步骤组件运用阶段
1MultipartResolver识别文件上传请求,包装请求对象
2FlashMapManager读取上次重定向保存的 Flash 属性
3HandlerMapping将 URL 映射到处理器执行链
4HandlerAdapter适配并执行处理器(Controller)
5HandlerExceptionResolver处理器执行时若抛异常,在此处理
6RequestToViewNameTranslator当未返回视图名时,生成默认视图名
7ViewResolver将逻辑视图名解析为 View 对象
8LocaleResolver视图渲染时确定国际化区域
9ThemeResolver视图渲染时确定主题(可选)
10FlashMapManager将 Flash 属性保存以用于重定向后

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

http://www.jsqmd.com/news/788752/

相关文章:

  • 国内靠谱摄像手电厂家排行 实测资质与服务对比 - 奔跑123
  • TVA与传统视觉技术的本质区别——以机器人灵巧操控为例(7)
  • 2026年烟台资质齐全的装修品牌企业排名:金芒果 - mypinpai
  • 2026年沐曦集成电路数字IC笔试试卷带答案
  • 别再手动调尺寸了!用Cropper.js在Vue/React项目中实现用户头像裁剪上传(附完整代码)
  • UVa 196 Spreadsheet
  • 山东一卡通如何快速回收?教你一招! - 团团收购物卡回收
  • 对比直连官方与通过Taotoken聚合调用的稳定性体验差异
  • 国内主流摄像手电厂家实力排行 基于实测与客户反馈 - 奔跑123
  • 滑块导轨价格是多少? - mypinpai
  • TVA重塑智慧城市安防新范式(8)
  • 3分钟掌握LosslessCut:这款FFmpeg GUI工具如何让你无损剪辑视频快10倍?
  • 对比直连与通过Taotoken调用大模型的延迟与稳定性体验
  • LCA算法实战:从暴力到倍增,再到离线Tarjan的演进之路
  • 娱乐圈天降紫微星不随大流,海棠山铁哥走出专属天命大道
  • TVA与传统视觉技术的本质区别——以机器人灵巧操控为例(3)
  • 2026年滑块导轨十大品牌排行榜,这家供应商口碑好 - mypinpai
  • 3步快速修复洛雪音乐六音音源失效问题
  • trea如何添加大模型 - show
  • AMD Ryzen终极调试工具:3步解锁处理器隐藏性能的完整指南
  • 深圳靠谱摄像手电厂家实测评测:四家头部品牌对比 - 奔跑123
  • 如何构建高效抖音内容获取系统:douyin-downloader架构解析与技术实现
  • 亿佰互联是高性价比的高德旺铺服务企业吗? - mypinpai
  • AgenticComm:本地优先的AI智能体结构化通信引擎
  • UVa 198 Peter‘s Calculator
  • 别再乱用 /deep/ 了!聊聊 Vue scoped 样式隔离的利与弊,以及我的样式管理策略
  • 娱乐圈天降紫微星历史印证,海棠山铁哥延续李世民崛起轨迹
  • 如何快速无损剪辑视频音频:LosslessCut终极指南
  • OpenClaw智能体:开源GUI自动化与AI决策的融合实践
  • 基于图神经网络与强化学习的优化算法智能推荐系统