恒美微站 Logo 恒美微站
  • 首页
  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心
  • 联系我们

Spring+SpringMVC父子容器初始化全解析:从web.xml到Bean加载

  • 首页
  • 资讯中心
  • /
  • Spring+SpringMVC父子容器初始化全解析:从web.xml到Bean加载

相关资讯

docker-ce 指标采集实践指南:go-metrics 统一 Prometheus 度量约定的原理与实战 2026/10/13 10:06:19
Arco Design Switch 开关组件基础用法详解:从最小示例到受控原理 2026/10/13 10:06:19
用层次分析法(AHP)科学决策就业选择 2026/10/13 10:01:18

最新资讯

Kafka生产者API深度解析:从异步机制到可靠性保障
在线UML类图工具:语义建模、代码同步与协作闭环
obsidian-weread-plugin 模板编辑器窗口:三栏布局、Nunjucks 实时预览与主题系统集成
UML试题刷题攻略:用例图与顺序图核心考点与答题模板
DR5白金高级版:Windows下PS2021的RAW处理增强方案
工业相机选型与调试实战:Dalsa相机应用经验全解析

今日推荐

ESP32舵机控制全攻略:从PWM原理到高级玩法与常见故障排查
CAN总线原理与STM32实战:抗干扰、仲裁、错误处理全解析
CAN总线错误处理机制深度解析:从位错误到Bus Off的硬件级调试

本周热门

UE动画修改实战:从资产编辑到重定向与蒙太奇驱动
统计随机数生成器攻击下的KLJN安全密钥交换协议Matlab仿真
政务API安全治理:资产测绘、低代码编排与行标对标实践

本月精选

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证
2026 大模型集体涨价:用 Python 做企业 Token 成本测算与选型避坑(附配置)

Spring+SpringMVC父子容器初始化全解析:从web.xml到Bean加载

发布时间:2026/10/13 10:06:19
Spring+SpringMVC父子容器初始化全解析:从web.xml到Bean加载 每当我把一个传统的SpringSpringMVC项目扔进Tomcat最熟悉不过的画面就是控制台里那两行日志先是ContextLoaderListener加载root WebApplicationContext紧接着DispatcherServlet开始初始化SpringMVC的子容器。很多人写代码年复一年却说不清这两行日志背后发生了什么两个容器到底有什么血缘关系为什么你配了扫描注解某个Bean却莫名其妙拿不到。这篇就来把SpringSpringMVC项目中的容器初始化过程彻底捋一遍从web.xml的入口开始到父子容器建立、BeanDefinition加载、单例实例化、循环依赖兜底一路讲到实操中踩过的坑。不管你是刚入行的Java开发还是被SpringBoot惯坏了想回头补补底子的老手这篇文章都能让你对自己项目启动时发生的事有一个完整的、能落地的认知。1. 项目整体设计与容器模型拆解1.1 先搞清楚一个SpringMVC项目里到底有几个容器很多人会误以为一个SpringMVC项目只有一个Spring容器实际上传统架构里至少存在两个容器一个是由ContextLoaderListener创建的根容器也就是常说的Root WebApplicationContext另一个是由DispatcherServlet创建的SpringMVC子容器也就是Servlet WebApplicationContext。这两个容器是典型的父子关系。根容器负责装载数据源、事务、Service、DAO这些业务基础组件子容器负责装载Controller、HandlerMapping、ViewResolver这些Web层组件。子容器可以访问父容器里的Bean但反过来不行。这个设计不是拍脑袋定的它有明确的分工逻辑Web层是依赖业务层的但业务层不应该反向感知Web层。所以控制器的Bean放子容器服务的Bean放父容器一目了然。判断项目里到底有几个容器还有一个很直接的观察点。项目如果没有配置ContextLoaderListener只有DispatcherServlet那整个项目就只有一个SpringMVC子容器所有Bean都在这个容器里。如果两个都配了那就是标准父子容器。很多老项目的隐患恰恰就是从“两个都配了但扫描包没分清楚”开始的。1.2 为什么要把容器拆成父子两层这个拆分的核心原因我在前面已经提到了职责隔离。根容器管业务子容器管表现层。但实际中还有另一个重要考量——一个Web应用技术上可以配置多个DispatcherServlet比如一个管/ api路径一个管/ web页面路径。如果每个DispatcherServlet都自己加载一遍Service和DAO那这些公共Bean就会被重复创建内存浪费不说事务代理、缓存切面全都混乱。有父子容器之后多个DispatcherServlet就可以共享同一个父容器。每个子容器里只需要放各自私有的Web配置公共部分全部交给父容器。这就像公司里公共基础设施由一个部门统一管理各业务线只维护自己专属的东西谁也不会覆盖谁。Spring Boot出现后这种父子容器被简化掉了内置容器直接就一个上下文。但理解传统模型仍然有价值因为很多存量项目、老系统、企业内部框架还在跑这套结构。你接手这类系统时如果脑子里没有父子容器这张图遇到Bean找不到或者被覆盖的问题基本无从下手。1.3 容器初始化的整体启动时序从部署角度看整个容器初始化始于web.xml的解析。Tomcat启动应用时会先创建ServletContext然后按web.xml中的配置顺序加载监听器、过滤器、Servlet。这个顺序至关重要因为ContextLoaderListener要在DispatcherServlet初始化之前完成根容器的创建。整体时序大致是这样Tomcat解析web.xml发现ContextLoaderListener于是执行它的contextInitialized方法在这里加载Spring根配置创建根容器接着容器继续解析遇到DispatcherServlet的声明按load-on-startup的配置立即实例化并调用其init方法在这个方法里创建SpringMVC子容器子容器建立后再完成SpringMVC九大组件的初始化比如HandlerMapping、HandlerAdapter、ViewResolver等到这一步应用才具备处理HTTP请求的能力。这个时序里有一个极其隐蔽但非常实用的细节根容器初始化在先子容器在后所以子容器初始化的过程中是可以直接引用父容器中的Bean的。但父容器创建时并不知道子容器的存在如果你的配置让父容器去扫描Controller它虽然能加载但因为还没建立ServletContext相关的Web环境很多Web专属组件在父容器里是跑不起来的这就是典型的配置错位。2. 核心细节解析加载次序与配置分流2.1 ContextLoaderListener到底做了什么ContextLoaderListener实现了ServletContextListener接口它的核心任务就一句话在ServletContext创建时初始化Spring的根容器。具体逻辑是contextInitialized方法里调用ContextLoader.initWebApplicationContext。它会读取context-param中配置的contextConfigLocation这个参数指向Spring根容器的XML配置文件列表。如果没配默认去/WEB-INF/applicationContext.xml找。读取到配置后创建XmlWebApplicationContext设置ServletContext然后调用refresh方法这条方法就是整个容器初始化的引擎后面我会专门讲。需要特别注意的是根容器持有的是ServletContext的引用而不是ServletConfig。因为根容器运行时ServletContext级别的它不依赖某个具体Servlet。而DispatcherServlet创建的子容器持有的是ServletConfig是Servlet级别的。这个差异直接决定了两个容器能拿到什么样的Web环境信息。我在实际项目中见过有人在根容器里直接注入HttpServletRequest结果发现拿不到或者拿到的是代理对象。就是因为根容器对应的Web环境是ServletContext级别按作用域取Request对象时会在上下文环境不完整的情况下报错。这类问题不会每次启动都出现排查起来特别费劲理解了根容器的定位就能避开。2.2 DispatcherServlet初始化时发生了什么DispatcherServlet本质上是一个Servlet它的init方法最终会走到FrameworkServlet.initWebApplicationContext。在这里SpringMVC先尝试从ServletContext中找到根容器如果存在就把根容器设置为自己容器的一个属性建立父子关系。接下来DispatcherServlet创建自己的WebApplicationContext。配置文件通过init-param中的contextConfigLocation指定通常指向springmvc.xml之类。如果没指定它会默认去寻找一个名为[servlet-name]-servlet.xml的配置文件比如DispatcherServlet就会找dispatcherServlet-servlet.xml很多人不知道这个默认规则导致新建文件起错名字配置一直没生效。容器创建完后DispatcherServlet进入onRefresh阶段这里会初始化SpringMVC的九大组件HandlerMapping、HandlerAdapter、HandlerExceptionResolver、ViewResolver、LocaleResolver、ThemeResolver、MultipartResolver、FlashMapManager、RequestToViewNameTranslator。这些组件决定了一个请求进来后如何找到处理器、如何处理异常、如何解析视图。平时我们用Spring Boot时这些都自动配好了但在传统XML项目里你得靠mvc:annotation-driven /来注册默认的那一套HandlerMapping和HandlerAdapter。这里有个实际经验如果启动过程打印了DispatcherServlet初始化完成但请求进来全部404优先排查HandlerMapping是否被正确注册。mvc:annotation-driven /缺失是经典问题它内部注册的RequestMappingHandlerMapping和RequestMappingHandlerAdapter是Controller方法映射的核心缺了它你的Controller注解方法根本不会映射到URL。2.3 配置分流的黄金法则父子容器分开之后最要紧的就是扫描包绝对不能乱。父容器只扫描Service、DAO、数据源等。如果父容器把Controller也扫进来了通常不会立刻报错但会出现方法映射失效或Bean被重复创建。因为Controller本来应该在子容器中由RequestMappingHandlerMapping处理结果它先被父容器加载了子容器里的DispatcherServlet扫描时又扫了一遍于是同一个Controller类在两个容器里各有一个实例注解映射混乱。子容器只扫描Controller。如果子容器把Service也扫了结果是父容器和子容器各有一个Service实例。更麻烦的是事务切面通常声明在父容器中子容器里那个Service没有经过事务代理它调用的方法事务根本不生效。这类Bug最难查因为它不报错只是数据一致性出问题。正确做法是在springmvc.xml的context:component-scan里用use-default-filters配合include-filter只保留Controller相关的Controller和ControllerAdvice在applicationContext.xml中排除这两个注解。实际配置可以参考这样拆!-- applicationContext.xml -- context:component-scan base-packagecom.demo context:exclude-filter typeannotation expressionorg.springframework.stereotype.Controller/ context:exclude-filter typeannotation expressionorg.springframework.web.bind.annotation.ControllerAdvice/ /context:component-scan!-- springmvc.xml -- context:component-scan base-packagecom.demo use-default-filtersfalse context:include-filter typeannotation expressionorg.springframework.stereotype.Controller/ context:include-filter typeannotation expressionorg.springframework.web.bind.annotation.ControllerAdvice/ /context:component-scan这两段配置看着简单但能把绝大多数由扫描混乱引起的诡异问题挡在门外。老项目我一般建议找个时间统一整改一次成本不高收益很大。3. 底层流程从配置文件到可用Bean的完整链条3.1 资源定位与BeanDefinition的加载注册容器初始化的第一步是把XML里写的bean标签或者context:component-scan扫描到的类转换成Spring内部统一的BeanDefinition。BeanDefinition可以理解为一个“Bean的生产说明书”里面记录了类名、作用域、懒加载标记、依赖关系、初始化方法等元信息。Spring不会一上来就创建Bean实例而是先把这些说明书收集齐等刷新流程推进到实例化阶段才按需生产。在refresh方法里这一步对应obtainFreshBeanFactory。XmlBeanDefinitionReader会读取XML配置文件解析每一个元素。如果是注解扫描ClassPathBeanDefinitionScanner会把指定包下所有符合条件的类找出来通过ASM读取类的元数据检查类上有没有Controller、Service、Repository、Component这些注解有的话就注册生成BeanDefinition。这个过程不会真正加载类到JVM效率很高。这里有一个很多人容易忽略的知识点同一个包被根容器和子容器同时扫描不一定会立刻产生冲突但会生成两个独立的BeanDefinition。它们存在于不同的容器中实例也是不同的。这种重复扫描是前面那些配置问题的底层来源。理解了BeanDefinition的注册机制你就会知道“扫重了”意味着什么它不只是多一个实例而是在两个容器各自维护了一套认知。3.2 单例Bean实例化与依赖注入BeanDefinition注册完成后进入到finishBeanFactoryInitialization这一步会实例化所有非懒加载的单例Bean。Spring默认Bean是单例且非懒加载的也就是说容器启动时就会把项目里大多数Bean实例化好而不是等到第一次使用时才创建。实例化过程比大多数人想象的要复杂。Spring底层通过getBean方法拿到BeanName先查看缓存没有就去创建。创建时根据BeanDefinition里的信息用构造器或工厂方法生成实例然后处理属性填充也就是依赖注入。这个阶段如果发现某个属性引用了另一个Bean会递归地调用getBean去获取那个依赖。接口注入、Setter注入、构造器注入其实都是在属性填充环节被处理的。平时大家用Autowired背后是AutowiredAnnotationBeanPostProcessor在起作用它扫描Bean实例里的注入点找到依赖后调用getBean把依赖塞进来。初始化完成后还有BeanPostProcessor的后置处理PostConstruct注解、InitializingBean接口、AOP代理对象的生成都发生在这个阶段。容器初始化完成后项目里的Bean就已经处于完整的、可用的状态。如果某个Bean初始化时抛异常启动就会失败。这也是为什么我们常说的“启动失败要优先看Caused by”因为很多时候根本不是配置格式的问题而是某个Bean依赖的组件没就绪。3.3 三级缓存与循环依赖兜底机制一讲到单例Bean的创建就会绕不开Spring的三级缓存。很多面试题会直接问你三级缓存是什么但放到容器初始化的上下文里它其实是实例化阶段解决循环依赖的一套兜底机制。三级缓存由三个Map组成。singletonObjects是最终的单例池存放完整创建好的BeanearlySingletonObjects提前暴露的半成品BeansingletonFactories存放的是ObjectFactory用于生成早期引用的代理对象。当A依赖B、B又依赖A时创建A的过程中发现需要B于是去创建BB的创建又需要A此时A还没完成属性填充但Spring会把A通过singletonFactories提前暴露出来让B先拿到这个早期的A引用B创建完成后A再从缓存中获取B完成自己的属性填充。这个机制能成立的关键前提是A和B都必须是单例而且通过Setter或字段注入相互依赖。如果走构造器注入循环依赖就无法解决因为实例化阶段就要拿到对方的完整实例此时还没有提前暴露的机会。Spring Boot 2.6之后默认禁止了循环依赖但传统SpringMVC项目里这个机制依然存在。我建议老项目不要依赖它能重构就重构因为它掩盖了设计问题而且代理对象在第三级缓存里生成时容易产生意料之外的嵌套情况。3.4 refresh方法中容易被忽略的扩展点前面多次提到refresh方法这个方法在AbstractApplicationContext中定义是整个容器初始化的总指挥。它的每一步设计都有明确含义值得单独拆开看。prepareRefresh做状态标记和属性源准备obtainFreshBeanFactory创建或刷新BeanFactoryprepareBeanFactory给BeanFactory配置类加载器和一些标准BeanPostProcessorpostProcessBeanFactory留给子类扩展。后面还有invokeBeanFactoryPostProcessors这是BeanFactoryPostProcessor的干活阶段像PropertySourcesPlaceholderConfigurer也就是${}占位符的替换就在这里执行。再往下registerBeanPostProcessors把BeanPostProcessor提前注册好为后续的Bean实例化做准备。initMessageSource、initApplicationEventMulticaster、onRefresh、registerListeners等步骤则分别处理国际化、事件广播、SpringMVC组件初始化、事件监听器注册。最后是finishBeanFactoryInitialization和finishRefresh。如果你在项目启动过程中需要插入自定义逻辑这些扩展点就是最佳切入点。比如动态调整某些Bean属性可以自定义一个BeanFactoryPostProcessor想在Bean实例化前后做手脚就实现BeanPostProcessor想在容器刷新完成后执行一段初始化逻辑会用到SmartInitializingSingleton或者监听ContextRefreshedEvent事件。4. 常见问题与排查技巧实录4.1 配置了Controller但请求全部404这个问题的排查链条其实不复杂但没有地图的人会绕很久。先确认DispatcherServlet的contextConfigLocation有没有生效最简单的方法是在配置文件中随便加一个错误的Bean定义如果启动报错说明文件确实被加载了如果启动一切正常大概率配置文件路径没对上。再检查mvc:annotation-driven /有没有配置然后看子容器扫描包路径有没有把Controller所在的包包括进去。我在这里还遇到过一种特殊的404场景父容器里把Controller也扫进去了导致RequestMappingHandlerMapping在初始化时发现映射已经被父容器处理子容器里找不到任何映射。这种问题修改扫描配置就能解决但排查时如果只看子容器日志很容易陷入迷茫。4.2 父容器拿不到子容器的Bean这个问题的典型报错是NoSuchBeanDefinitionException或注入时提示找不到某个Bean。原因就是父子容器的单向可见性子能看到父父看不到子。如果业务层里注入了Controller或HttpServletRequest根容器里是找不到的。遇到这种报错先判断一下注入的发生位置。如果是Service里注入Controller少见的写法但一旦出现就会触发这个问题。另一种变形场景是事务失效。当Service实例同时存在于父容器和子容器时子容器里的Service没有事务代理如果在Controller里注入了子容器的Service调用Transactional方法时事务不会生效。解决思路就是把Service的扫描彻底从子容器排除让它只存在于父容器这样事务切面才能可靠地增强它。4.3 Bean初始化顺序依赖导致的诡异空指针某些老项目会依赖Bean的初始化顺序。比如A在初始化时需要调用B的某个静态方法但B还没初始化完成结果A抛空指针。定位这类问题有一个技巧在getBean的调用栈中看是谁触发了A的创建是谁在A的属性填充中递归触发了B的创建。多数情况下这不是真正的顺序问题而是设计缺陷说明Bean之间存在不该有的隐式依赖。还有一类情况是PostConstruct里调用了别人还没准备好的Bean。Spring的PostConstruct是在属性填充完成后执行的但它无法保证其他容器中的Bean已经就绪。如果根容器和子容器都有PostConstruct逻辑就必须理清楚哪个容器先刷新。因为根容器永远先完成子容器的初始化后于父容器所以子容器里的PostConstruct可以安全依赖于父容器中已完成初始化的Bean但反过来不行。4.4 排查工具与日志分析思路遇到容器初始化问题我的习惯是先把日志级别调成DEBUG。重点看AbstractApplicationContext.refresh里打印的Starting to load bean definitions和Finished loading of bean definitions这两行之间的内容那里控制BeanDefinition的扫描注册细节。再往下看Creating shared instance of singleton bean它会逐个打印实例化的BeanName如果一个Bean创建过程耗时异常或者直接卡住基本上能顺着这个日志找到元凶。另外一个实用工具是Spring的BeanFactory报告当容器初始化失败时Spring会打印一条类似Bean xxx is a shared singleton的消息同时附带依赖解析失败的原因。把这个报告完整贴到IDE里看Caused by那一层千万别只看最前面的Exception。很多人在清理堆栈的时候只留第一行结果把真正的原因丢掉了。4.5 一个典型的传统项目踩坑记录我去年帮一个团队排查过一个老系统启动正常但用户反馈保存数据时常丢事务。刚开始怀疑数据库配置后来发现Controller里注入的Service根本没有代理直接绕过了事务。打开配置一看根容器和子容器都扫描了整个业务包而tx:annotation-driven /只配在了根容器里。子容器中扫描出来的Service全是没有事务代理的原生实例。整改方式就是前面说的在springmvc.xml里排除Service和Repository在applicationContext.xml里排除Controller和ControllerAdvice。改完重启后从DispatcherServlet的日志里可以看到Controllers被扫描到子容器Service只出现在根容器日志中事务恢复正常。这类问题在传统项目里特别普遍不是因为Spring设计有问题而是配置没有遵守父子容器的规则。5. 从传统容器到Spring Boot的演变与个人经验5.1 Spring Boot如何简化容器初始化Spring Boot出现之后传统web.xml和XML配置的可见性大幅降低。Spring Boot的核心是SpringApplication它会创建一个AnnotationConfigServletWebServerApplicationContext这是一个同时具备注解配置和Web能力的容器不再区分父子两层。你写一个SpringBootApplication的main方法它内部会把ComponentScan指向启动类所在的包自动扫描所有组件。这种简化对开发效率是巨大的提升但也带来一个问题很多人缺少对容器初始化底层过程的感知。一旦遇到启动异常、Bean循环依赖、自动配置不生效这些问题完全没有排查方向。我见过不少号称多年的Java开发遇到Spring Boot启动失败的报错连Failed to configure a DataSource这种提示都看不明白。所以即使你平时只用Spring Boot把传统SpringMVC容器初始化的过程搞清楚不是屠龙之术而是基本功。5.2 Servlet 3.0无XML配置下的初始化方式传统SpringMVC项目还演变出了一种脱离web.xml的Servlet 3.0方式核心是WebApplicationInitializer接口。Tomcat启动时会通过ServletContainerInitializer机制扫描实现了这个接口的类调用它的onStartup方法。在这个方法里你可以手动创建根容器、注册ContextLoaderListener、创建DispatcherServlet的子容器然后把Servlet添加到ServletContext中。比如这一段代码public class MyWebAppInitializer implements WebApplicationInitializer { public void onStartup(ServletContext container) { AnnotationConfigWebApplicationContext rootContext new AnnotationConfigWebApplicationContext(); rootContext.register(ServiceConfig.class); container.addListener(new ContextLoaderListener(rootContext)); AnnotationConfigWebApplicationContext mvcContext new AnnotationConfigWebApplicationContext(); mvcContext.register(WebConfig.class); ServletRegistration.Dynamic registration container.addServlet(dispatcher, new DispatcherServlet(mvcContext)); registration.setLoadOnStartup(1); registration.addMapping(/); } }这种方式保留了父子容器模型但不需要XML文件。如果你在公司里维护的既有项目在用这种模式调整配置时不要只找XML很可能会扑空。判断方法是看有没有WebApplicationInitializer实现类或者有没有AbstractAnnotationConfigDispatcherServletInitializer的子类。后者是Spring提供的一种模板覆写三个方法就能分别指定根容器配置和子容器配置。5.3 评估一个项目初始化过程是否健康的简单方法结合我多年经验一个SpringMVC项目的初始化是否健康从启动日志就能判断。健康的项目启动过程中Bean的创建顺序清晰日志里没有重复扫描警告不会出现Bean named xxx is already defined之类的提示。Spring在扫描时如果发现同名Bean会用默认规则覆盖并且打印一段警告这类日志很容易被忽略但往往是后续问题的重要线索。判断扫描是否重复还有一个办法在根容器和子容器中分别注入一个ApplicationContext打印它的BeanDefinition数量。如果两个容器里的Bean类型高度重叠说明扫描配置需要整改。我在审计老项目时经常这么做几秒就能评估出一个系统是否需要对容器配置动刀。5.4 我对容器初始化过程的理解小结做了这么多年Java开发我的体会是容器初始化过程是Spring这座大厦的地基。你不会天天看到它但它决定了一切上层组件能否正常工作。很多人写Controller、调接口非常熟练到了排查启动失败、Bean冲突、事务失效时就手足无措根子上的原因就是没有把这条初始化链路吃透。这篇文章从web.xml入口一路讲到了三级缓存和常见坑篇幅不短但如果你能把这套逻辑真正在项目里验证一遍之后再面对那些报错心态会很不一样。最后再分享一个小技巧当你对某个Spring加载细节有疑问与其各种搜索不如直接在项目里注册一个自定义的BeanPostProcessor在postProcessBeforeInitialization里打断点。运行时你能看到每一个Bean实例化、依赖注入、初始化的完整路径这种“亲眼所见”比读十篇源码分析都管用。从传统SpringMVC的父子容器到Spring Boot的自动化装配底层机制一脉相承把地基打牢了不定哪天排查线上问题的时候你就知道那点经验换回的时间有多值。

关于恒美微站

恒美微站专注于为个体商户、工作室提供极简自助建站服务,让每个人都能轻松拥有专业网站。

快速链接

  • 关于我们
  • 建站服务
  • 主题模板
  • 案例展示
  • 资讯中心

服务项目

  • 可视化建站
  • 拖拽编辑
  • 主题定制
  • SEO 优化
  • 网站托管

联系方式

  • 📍 地址:北京市朝阳区建国路 88 号
  • 📞 电话:400-888-8888
  • ✉️ 邮箱:info@hmyw.cn
  • 🕐 时间:周一至周日 9:00-18:00

© 2024 恒美微站 hmyw.cn 版权所有 | 京 ICP 备 12345678 号