Spring IOC容器启动原理:从AnnotationConfigApplicationContext到refresh()
很多人问过我一个有点“经典”的问题注解启动的 Spring在new AnnotationConfigApplicationContext(...)那一刻后面到底发生了什么第一次翻开 Spring 源码的人十有八九会卡在这个构造方法上因为它不像看配置文件那样直观调用链会把人绕晕。我最初读的时候也是一行一行断点走下来才把“容器初始化”这件事串成一条线。这里的核心关键词就三个AnnotationConfigApplicationContext、IOC 容器、refresh()。只要搞懂这三个词你不仅能看懂 Spring 的启动流程还能顺手解决循环依赖、Autowired 失效、BeanDefinition 被覆盖这一类线上问题。所以这篇文章我不想只是把源码翻译一遍而是按我自己踩坑后的理解把启动过程拆成几个能复现、能验证的环节。适合正在读 Spring 源码的人、准备面试的人以及被 IOC 相关 Bug 折磨过的人。1. 启动入口拆解从构造方法一路跟到 refresh()1.1 构造方法的四条分支AnnotationConfigApplicationContext的构造方法有四种常见用法代码层面对应四条路径。// 用法1直接传入配置类 AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(AppConfig.class); // 用法2直接传入包路径 AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(com.example.project); // 用法3先创建空容器再手动注册并刷新 AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(); context.register(AppConfig.class); context.refresh(); // 用法4同时传配置类和包路径 AnnotationConfigApplicationContext context new AnnotationConfigApplicationContext(); context.register(AppConfig.class); context.scan(com.example.project); context.refresh();不管哪种用法最终都绕不开两件事把“要交给容器管理的类”变成BeanDefinition然后调用refresh()把BeanDefinition变成可用的单例 Bean。看无参构造方法源码是这样public AnnotationConfigApplicationContext() { this.reader new AnnotatedBeanDefinitionReader(this); this.scanner new ClassPathBeanDefinitionScanner(this); }这里创建了两个核心组件。AnnotatedBeanDefinitionReader负责处理显式传入的配置类和组件类ClassPathBeanDefinitionScanner负责按包路径扫描。这两个组件各管一摊但最终都把信息注册到同一个DefaultListableBeanFactory里。有参构造方法里能看到清晰的调用链public AnnotationConfigApplicationContext(Class?... componentClasses) { this(); register(componentClasses); refresh(); }register和scan来自AnnotationConfigRegistry接口这个接口只有这两个方法含义很明确注解容器支持的注册方式就两种要么注册类要么扫包。无论哪种真正的容器启动动作都在后面的refresh()。1.2 register()显式注册配置类register()方法内部最终会走到AnnotatedBeanDefinitionReader.doRegisterBean。这个方法做的事情表面上是“注册类”实际上有一整套细节先判断类上的Conditional条件是否满足满足之后包装成AnnotatedGenericBeanDefinition再解析Scope、Lazy、Primary、DependsOn、Role、Description等注解把它们设置到 BeanDefinition 上最后调用DefaultListableBeanFactory.registerBeanDefinition把 BeanDefinition 放进容器。这里有一个容易被忽略的点注册配置类时Spring 并不会立刻解析配置类里的Bean方法。它只是把配置类本身当作一个普通 BeanDefinition 存起来。真正的解析发生在refresh()的invokeBeanFactoryPostProcessors阶段由ConfigurationClassPostProcessor完成。我把这个过程理解为“报名”和“考试”的关系。register()只是帮这些类报名让他们进入候选名单但谁有资格成为 Bean、哪些Bean方法要生效要等refresh()里的“考官”来判定。1.3 scan()按包路径批量注册scan()方法走的是另一条线最终会进入ClassPathBeanDefinitionScanner.doScan。这里的核心逻辑是根据传入的包路径把它转换成classpath*:com/example/project/**/*.class这样的资源路径然后扫描所有匹配的.class文件。这里有个细节值得多提一句扫描过程用的是 ASM 读取类的元数据并不会真正加载类到 JVM。所以即使某些类没有依赖 Spring 的注解也不会因为加载失败导致扫描崩溃。扫描到候选类之后再用isCandidateComponent判断是不是符合规则最后照样注册成 BeanDefinition。我在刚接触这部分时犯过一个糊涂以为scan()注册完成之后Bean 就已经创建了。实际上不是scan()只完成 BeanDefinition 的注册真正的单例实例化要等到refresh()里的finishBeanFactoryInitialization。理解这个先后顺序后面排查“为什么 Bean 没创建”“为什么静态变量没注入”之类的问题会顺很多。2. 包扫描机制ClassPathBeanDefinitionScanner 如何把 Component 变成 BeanDefinition2.1 includeFilters 与 excludeFilters 过滤规则ClassPathBeanDefinitionScanner的过滤规则是理解扫描行为的关键。构造方法里会调用registerDefaultFilters()默认注册Component的AnnotationTypeFilter。因为Service、Controller、Repository都带有Component元注解而AnnotationTypeFilter默认会考虑元注解所以这些注解标注的类也会被扫描到。另外 JSR-330 的Named、JSR-250 的ManagedBean也在默认过滤器中。实际开发中如果你想只扫描自己定义的注解可以这样设置scanner.addIncludeFilter(new AnnotationTypeFilter(MyMarker.class));但要注意addIncludeFilter是追加不是覆盖。要覆盖默认规则需要先清空scanner.resetFilters(false); scanner.addIncludeFilter(new AnnotationTypeFilter(MyMarker.class));resetFilters(true)会把 includeFilters 清空false则保留默认。这个细节经常有人在自定义扫描时踩坑以为设置了自己的 includeFilter 就只会扫自己指定的注解结果默认的Component依然生效导致扫进来一堆意料之外的 Bean。2.2 isCandidateComponent 的具体判断ClassPathScanningCandidateComponentProvider里的isCandidateComponent是过滤链的最后一环。它的判断逻辑分为两层。第一层是过滤器的匹配先检查 excludeFilters只要有一个匹配就排除再检查 includeFilters必须至少有一个匹配才进入下一层。第二层是类本身的判断不允许是注解、不允许是接口、不允许是抽象类还要是可以独立存在的类非内部类或者静态内部类。这套逻辑非常讲究。接口和抽象类不能直接实例化如果放进来后面createBean时会直接报错。内部类如果没有 static 修饰依赖外部类实例也没法独立创建。所以这一层过滤是必要的保护。还有一种情况容易被误解一个抽象类标注了Component正常情况下不会被注册。但如果抽象类里面有Lookup方法情况会变得复杂。不过这是很偏的场景一般业务代码不会遇到知道有这层机制就行真遇到再深入查。2.3 扫描到 class 之后如何变成 BeanDefinition过滤通过后scanCandidateComponents会生成一个ScannedGenericBeanDefinition里面包含了类的元数据、注解信息。接着doScan方法会做一系列补充处理。首先是解析Scope得到 scope 名称并设置到 BeanDefinition。之后用AnnotationConfigUtils.processCommonDefinitionAnnotations处理公共注解包括Lazy、Primary、DependsOn、Role、Description这些。最后通过checkCandidate检查是否有重名冲突没有冲突就注册到容器。整个过程可以用一句话总结包扫描的本质是把“类路径下的 class 文件”翻译成“容器能识别的 BeanDefinition”。它没有创建任何实例也没有执行任何初始化只是拿到了“菜的原材料”和“菜谱”。3. 内置后置处理器AnnotatedBeanDefinitionReader 注册了哪些关键组件3.1 五个处理器分别负责什么AnnotatedBeanDefinitionReader的构造方法里有一行关键调用AnnotationConfigUtils.registerAnnotationConfigProcessors(this.registry)。这一行会往容器里注册一批固定名称的“基础设施 Bean”。这些 Bean 不是通过包扫描发现的而是直接以 RootBeanDefinition 的方式硬注册进去的。它们包括ConfigurationClassPostProcessorBeanDefinitionRegistryPostProcessor负责解析Configuration、ComponentScan、Import、Bean、PropertySource等所有注解驱动配置。AutowiredAnnotationBeanPostProcessorBeanPostProcessor负责处理Autowired和Value的注入。CommonAnnotationBeanPostProcessorBeanPostProcessor负责处理Resource、PostConstruct、PreDestroy。EventListenerMethodProcessorSmartInitializingSingleton负责把EventListener方法包装成 ApplicationListener。DefaultEventListenerFactory配合上面的处理器创建事件监听器适配器。这里要注意一个关键设计ConfigurationClassPostProcessor不是普通的后置处理器它是BeanDefinitionRegistryPostProcessor并且实现了PriorityOrdered。这意味着它在refresh()的invokeBeanFactoryPostProcessors阶段会最先被执行。只有它先跑完配置类里的Bean、扫描到的组件才会被注册成 BeanDefinition后续其他处理器才有东西可处理。3.2 重复注册与自定义覆盖的判断逻辑AnnotationConfigUtils.registerAnnotationConfigProcessors在注册这些基础设施 Bean 之前会先判断容器里是否已经有同名 BeanDefinition。如果已经有了就不会重复注册。这些基础设施 Bean 的固定名称是org.springframework.context.annotation.internalConfigurationAnnotationProcessor这样的长名字。如果你好奇心强在DefaultListableBeanFactory里 dump 一下 BeanDefinitionNames能看到这些长名称。这也带来一个实际启示如果你想替换 Spring 内置的AutowiredAnnotationBeanPostProcessor不能简单地在配置类里定义一个同名 Bean 去覆盖。因为containsBeanDefinition已经为 trueSpring 会跳过注册。想要自定义需要修改BeanFactory里已有的 RootBeanDefinition或者用别的机制比如定制BeanPostProcessor的注册逻辑。我见过有人在项目里试着用Bean定义一个internalAutowiredAnnotationProcessor来替换结果发现根本不生效就是这个原因。3.3 为什么这些处理器必须先注册如果这些处理器不在会出现什么情况最直观的是Autowired完全失效。因为Autowired的处理依赖AutowiredAnnotationBeanPostProcessor如果容器里没有这个 BeanPostProcessor那么在populateBean阶段就没有人执行字段注入逻辑所有Autowired字段全是 null。同理没有CommonAnnotationBeanPostProcessorResource、PostConstruct、PreDestroy也不会生效。这也就解释了为什么在某些“自定义容器”或“半手工 Spring”环境里明明类扫描进来了但依赖注入就是不工作——大概率是没走完整套registerAnnotationConfigProcessors。从源码里挖这一层比背面试题里“Autowired 是怎么实现的”要有用得多。你一旦知道它是靠AutowiredAnnotationBeanPostProcessor处理的遇到“Autowired 不生效”的问题排查思路马上就有了查这个 BeanPostProcessor 是否注册、是否在registerBeanPostProcessors阶段被实例化、它的执行顺序是否被其他后置处理器影响。4. refresh() 源码逐段解读IOC 容器初始化的十二个关键步骤4.1 prepareRefresh 到 prepareBeanFactoryrefresh()是AbstractApplicationContext的核心模板方法整个 IOC 容器初始化最壮观的场面都集中在这里。我把调用链拆开讲。第一步prepareRefresh()负责状态准备设置启动时间、标记容器激活、初始化属性源、校验必需的属性。这一步比较轻重点是让容器进入“可刷新”状态。第二步obtainFreshBeanFactory()在AnnotationConfigApplicationContext这条继承链上比较特殊。因为GenericApplicationContext的 BeanFactory 在构造时就已经是DefaultListableBeanFactory了所以这里主要是刷新 BeanFactory 的 id并且复用原有的 BeanDefinition。如果是ClassPathXmlApplicationContext这一步会销毁旧工厂重建逻辑完全不同。这也是注解容器和 XML 容器在实现上的一个典型差异。第三步prepareBeanFactory(beanFactory)是给工厂“装基础设施”。它做了几件关键的事设置类加载器和表达式解析器。添加ApplicationContextAwareProcessor专门处理ApplicationContextAware等 Aware 接口。把BeanFactory、ResourceLoader、ApplicationEventPublisher、ApplicationContext这几个类型的依赖纳入可解析范围这样 Bean 里通过Autowired注入这些类型时可以拿到当前容器实例。添加ApplicationListenerDetector用于把容器里的普通 Bean 识别成事件监听器。这一步之后BeanFactory 才算具备了完整的能力。4.2 invokeBeanFactoryPostProcessors配置解析的黄金时机invokeBeanFactoryPostProcessors(beanFactory)是整个refresh()里信息量最大的一步。它负责触发所有BeanFactoryPostProcessor其中最重要的是BeanDefinitionRegistryPostProcessor子接口的实现。执行顺序有讲究我整理成如下规则先找出所有已注册的BeanDefinitionRegistryPostProcessor。在这些处理器内部按PriorityOrdered、Ordered、普通顺序分组执行postProcessBeanDefinitionRegistry。执行过程中新注册进来的BeanDefinitionRegistryPostProcessor会被循环取出并继续执行。所有BeanDefinitionRegistryPostProcessor的postProcessBeanDefinitionRegistry跑完后再依次调用它们的postProcessBeanFactory。最后处理普通的BeanFactoryPostProcessor同样按优先级分组。ConfigurationClassPostProcessor是PriorityOrdered的实现类所以它是第一批执行的。它的processConfigBeanDefinitions会找出所有配置类然后通过ConfigurationClassParser解析配置类上的ComponentScan、Import、Bean方法、ImportResource等注解并把解析结果转换为新的 BeanDefinition 注册到容器。这一步做完配置类里的Bean方法变成了一个个ConfigurationClassBeanDefinition扫描的路径变成了具体的ScannedGenericBeanDefinition。可以说用户写的注解配置就是在这里被翻译成 Spring 内部的 BeanDefinition 模型的。这里给一个排查思路如果你自定义了一个BeanFactoryPostProcessor希望它能修改所有 BeanDefinition但它不生效先检查它是不是BeanDefinitionRegistryPostProcessor再检查优先级。因为ConfigurationClassPostProcessor优先执行如果它还没解析配置类你的后置处理器可能只看到了极少数 BeanDefinition。4.3 registerBeanPostProcessors 到 registerListenersregisterBeanPostProcessors(beanFactory)负责注册所有BeanPostProcessor。这一阶段同样按PriorityOrdered、Ordered、普通顺序注册。BeanPostProcessor的作用时机在 Bean 实例创建阶段也就是populateBean和initializeBean时。这里要说一个很多人忽略的点BeanPostProcessor和BeanFactoryPostProcessor的注册时机不同。后者在 Bean 实例创建之前执行作用于 BeanDefinition前者在 Bean 实例创建过程中执行作用于实例。理解这条界限对排查“为什么我的后置处理器没生效”至关重要。接下来几步相对轻量。initMessageSource()初始化国际化资源没有配置就用默认的DelegatingMessageSource。initApplicationEventMulticaster()初始化事件广播器默认是SimpleApplicationEventMulticaster。onRefresh()是模板方法给子类扩展用比如 Spring MVC 在这里初始化主题解析器。registerListeners()把容器里的ApplicationListenerBean 注册到广播器并补发启动早期缓存的事件。4.4 finishBeanFactoryInitializationBean 真正实例化的开始finishBeanFactoryInitialization(beanFactory)是容器从“定义阶段”进入“运行阶段”的分水岭。注释写得很明确实例化所有剩余的非懒加载单例 Bean。这一阶段内部的关键调用链是preInstantiateSingletons-getBean-doGetBean-createBean-doCreateBean。在doGetBean中Spring 会先从三级缓存singletonObjects、earlySingletonObjects、singletonFactories中查找。如果没找到再去创建。创建过程在doCreateBean里分几步实例化构造器或工厂方法、暴露到三级缓存允许提前引用、populateBean填充属性、initializeBean执行初始化方法。populateBean会应用InstantiationAwareBeanPostProcessor其中就包括处理Autowired的AutowiredAnnotationBeanPostProcessor。initializeBean会依次执行 Aware 接口回调、BeanPostProcessor.postProcessBeforeInitialization、afterPropertiesSet/PostConstruct/init-method、BeanPostProcessor.postProcessAfterInitialization。这一步执行完毕之后业务 Bean 才算真正可用。4.5 finishRefresh发出容器启动完成事件最后一步finishRefresh()做收尾工作清理资源缓存、初始化LifecycleProcessor、调用onRefresh()触发 Lifecycle Bean 的启动、发布ContextRefreshedEvent事件。ContextRefreshedEvent是 Spring 容器启动完成的信号。我们经常用ApplicationListenerContextRefreshedEvent在容器启动后执行初始化数据、预热缓存等操作就是挂在这一步之后。从源码顺序看这个事件是在所有单例 Bean 都创建完成后才发布的所以监听器里可以放心地Autowired任何容器 Bean不会出现“Bean 还没创建好就去用”的情况。把十二步串起来看refresh()就像是容器的一张执行清单。每一步都有明确职责顺序也经过精心设计状态准备在前BeanDefinition 处理在后实例化再往后最后收尾发布事件。这张清单值得多看几遍读懂了它IOC 容器就不再有黑盒。5. Configuration 增强机制CGLIB 代理与 Full/Lite 模式详解5.1 为什么要对配置类做增强ConfigurationClassPostProcessor解析完配置类还会做一件很有意思的事对Configuration类进行 CGLIB 增强。ConfigurationClassEnhancer会给配置类生成一个子类代理拦截Bean方法的调用。为什么要拦截看一个典型例子Configuration public class AppConfig { Bean public DataSource dataSource() { return new DataSource(); } Bean public JdbcTemplate jdbcTemplate() { return new JdbcTemplate(dataSource()); } }jdbcTemplate()方法内部调用了dataSource()。如果配置类不被代理这个调用会直接执行原方法每次调用都new DataSource()产生一个全新的对象。结果就是jdbcTemplate里用的 DataSource 和容器中的dataSourceBean 不是同一个实例单例语义被破坏。有了 CGLIB 代理后对dataSource()的调用会被拦截。代理会先去容器里查dataSourceBean如果已经创建就直接返回没创建就先创建再返回。这样无论内部怎么互相调用所有Bean方法返回的都是容器管理的单例对象。5.2 proxyBeanMethodsfalse 什么时候用从 Spring 5.2 开始Configuration注解增加了proxyBeanMethods属性默认是 true也就是默认走 CGLIB 代理。如果设置为 false配置类就是“Lite 模式”不会被 CGLIB 增强。那什么时候可以关掉关键看配置类内部是否有Bean方法之间的互相调用。如果不互相调用只是各自独立创建 Bean那么代理就没有必要。关掉之后有三个好处启动阶段少了 CGLIB 增强的开销配置类不再要求能被继承子类代理必须要继承final 类也能用了Bean方法可以是 final 的。我这里给一个经验如果你只是单纯用 Java 配置类把Bean方法列出来方法里没有互相调用proxyBeanMethods false是非常安全的优化。一旦配置类里有Bean方法互相调用且依赖单例语义就必须保持默认 true。5.3 Bean 方法里互相调用的坑既然提到互相调用就多说一句踩坑记录。有人会在Component类里写Bean方法这样配置类不会被 CGLIB 增强内部调用Bean方法每次都会新建对象。这种代码跑起来不一定报错但单例会悄悄失效很难排查。还有个坑是Configuration类里注入依赖时直接注入配置类类型。由于配置类本身被 CGLIB 代理注入的类型和期望的类型对不上时会抛出BeanNotOfRequiredTypeException。不过一般场景不会注入整个配置类更常见的是注入配置类中某个Bean方法产生的对象这种没有这个问题。我记得有一次排查一个“DataSource 被创建了两次”的问题根源就是配置类被 Lite 化处理某处代码直接new了配置类再调用Bean方法。如果想避免这种问题尽量保证所有地方都从容器拿 Bean而不是手动new配置类。6. 把源码变成排查工具三个常见问题的根因分析6.1 循环依赖到底是怎么被三级缓存解决的循环依赖是面试高频题也是线上排查的重灾区。Spring 解决循环依赖的核心在DefaultSingletonBeanRegistry里的三级缓存singletonObjects一级缓存存的是完整初始化后的单例 Bean。earlySingletonObjects二级缓存存的是提前暴露的早期引用可能还没完成属性填充。singletonFactories三级缓存存的是 ObjectFactory通过它可以拿到早期引用。流程大致是这样A 创建时实例化完成后会把ObjectFactory放入三级缓存。A 在populateBean填充属性时发现依赖 B于是去创建 B。B 创建时填充属性发现依赖 A此时从三级缓存取到 A 的ObjectFactory调用getEarlyBeanReference得到 A 的早期引用放入二级缓存注入给 B。B 创建完成后A 拿到 B 的完整引用继续完成初始化。最后 A 初始化完毕从一级缓存返回。为什么需要三级缓存而不是二级关键原因和 BeanPostProcessor 有关。某些SmartInstantiationAwareBeanPostProcessor比如 AOP 相关的在getEarlyBeanReference时可能生成代理对象。如果把原始实例直接放进二级缓存后续就没法再做代理增强。三级缓存存的是一个工厂真正需要早期引用时才触发代理逻辑这样就兼顾了性能和正确性。构造器注入的循环依赖为什么解决不了因为在构造器阶段对象还没创建完成还没把ObjectFactory暴露到三级缓存。Spring 在查找依赖时发现缓存里没有会尝试继续创建从而形成死循环最终报BeanCurrentlyInCreationException。遇到构造器循环依赖可以用Lazy注入代理来拆解。6.2 Autowired 为 null 或失效时的排查思路遇到Autowired字段为 null很多人的第一反应是“是不是容器没扫到这个类”。这当然是一部分原因但从源码角度排查可以按下面的顺序来。第一查 BeanDefinition 是否注册。在DefaultListableBeanFactory里打断点看beanDefinitionNames里有没有目标类。没有就查扫描路径是否正确、ComponentScan是否覆盖到目标包、类上是否有Component族注解。第二查是否被过滤规则排除。如果自定义了 TypeFilter检查过滤规则是否误伤。第三查是否在 BeanPostProcessor 注册之后手动 new 了对象。Autowired只对容器管理的 Bean 生效自己 new 出来的对象没有经过populateBean字段当然不会注入。第四查作用域。如果是 prototype 作用域每次获取都是新对象需要确认获取时机。如果是在容器启动早期手动获取可能 Bean 还没创建完成。还有一个常见现象在static字段上加Autowired永远为 null。原因从源码看很直接AutowiredAnnotationBeanPostProcessor的postProcessProperties是面向实例的static 字段不属于某个具体实例Spring 根本没打算处理它。如果要注入静态资源建议用 setter 方法在启动时手动赋值或者重新设计一下依赖结构。6.3 进阶实战自定义 BeanDefinitionRegistryPostProcessor 注册 Bean理解了invokeBeanFactoryPostProcessors的时机之后你可以利用源码知识做一件事在容器启动早期动态注册一个新的 BeanDefinition。这个技巧在中间件集成、动态扩展场景下很实用。先写一个处理器Component public class DynamicBeanRegistryPostProcessor implements BeanDefinitionRegistryPostProcessor { Override public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry registry) throws BeansException { GenericBeanDefinition definition new GenericBeanDefinition(); definition.setBeanClassName(CacheService.class.getName()); definition.setScope(ConfigurableBeanFactory.SCOPE_SINGLETON); registry.registerBeanDefinition(cacheService, definition); System.out.println(registered cacheService: registry.containsBeanDefinition(cacheService)); } Override public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) throws BeansException { // 可以在这里修改 beanFactory 的配置比如设置 ignoreDependencyInterface } }再定义一个普通的CacheService类不加任何注解。启动容器后通过context.getBean(CacheService.class)能拿到实例因为 BeanDefinition 已经在postProcessBeanDefinitionRegistry阶段被注册后续finishBeanFactoryInitialization会正常实例化它。这个机制能做什么比如从数据库或配置中心读取一段配置动态决定要不要注册某个 Bean、注册多少个 Bean、BeanClass 是什么。这些场景下写死在ComponentScan里反而不灵活动态注册 BeanDefinition 就成了更合适的方案。核心要点只有一个把握住执行时机确保注册操作发生在 BeanDefinition 处理阶段。7. 高效阅读 Spring 源码的断点与路线图7.1 合适的入口断点哪里打读源码最怕乱翻。我的建议是第一遍先只打一个断点AbstractApplicationContext.refresh()。这一步是容器初始化的主干所有后续流程都由它驱动。跟着调用栈往下走一遍先把十二步整体过一遍。第二遍再进细节。推荐的断点位置AnnotationConfigApplicationContext构造方法看容器如何创建 reader/scanner。ConfigurationClassPostProcessor.postProcessBeanDefinitionRegistry看配置解析的起点。ConfigurationClassParser.doProcessConfigurationClass看配置类的解析步骤包括ComponentScan、Import、Bean的逐个处理。DefaultListableBeanFactory.registerBeanDefinition看 BeanDefinition 注册到容器的入口。AbstractAutowireCapableBeanFactory.doCreateBean看 Bean 实例化的全过程。DefaultSingletonBeanRegistry.getSingleton看三级缓存的查找逻辑。有了这些断点你在实际调试一个 Demo 时可以带着“当前执行到哪一步了”的意识配合Evaluate Expression查看上下文信息比干读源码效率高很多。7.2 源码阅读路线图如果按路线图来我建议分四条线走。第一条线是入口线从AnnotationConfigApplicationContext构造方法开始走到refresh()整体把握容器启动流程。第二条线是配置解析线从ConfigurationClassPostProcessor开始进入ConfigurationClassParser看它是如何逐个处理配置类注解的。这一线是注解驱动开发的核心。第三条线是实例化线从finishBeanFactoryInitialization进入preInstantiateSingletons再到getBean、doGetBean、createBean、doCreateBean完整看一遍 Bean 从无到有的过程。这条线对理解生命周期、循环依赖至关重要。第四条线是生命周期线从doCreateBean里的populateBean和initializeBean入手看 BeanPostProcessor 是如何嵌入实例化和初始化过程的。这条线真正体现了“Spring 的可扩展性是怎么设计出来的”。四条线之间有交叉但每条线都有清晰主线。按照这条路线读下来我自己的体会是比零散看源码要快得多。7.3 调试过程中容易误判的现象调试 Spring 源码有两个容易让人误判的点我提一下。第一个是getBeanNamesForType会提前触发 Bean 创建。在调试时你可能会发现在preInstantiateSingletons之前某些 Bean 已经被实例化了。原因是getBeanNamesForType内部需要确定类型可能触发getBean。所以不要看到有 Bean 创建就以为容器已经开始实例化了要确认触发时机。第二个是 BeanPostProcessor 的执行顺序很容易被忽略。多个BeanPostProcessor之间有顺序如果你的后置处理器依赖其他处理器先执行建议实现Ordered或PriorityOrdered来保证顺序。否则调试时看到的现象会非常随机因为 Bean 创建顺序本身不是严格按代码书写顺序来的。最后一个建议读源码一定要配一个最小的可执行 Demo。光看不练很容易看着看着就迷失在调用栈里。我通常会在项目里放一个空的AnnotationConfigApplicationContext启动类所有断点都挂在源码上跑一遍观察每一步的 BeanDefinitionNames 变化。把这一步做完你再回头看那些 IOC 的面试题和线上问题会发现它们变成了一条清晰的逻辑链。

相关新闻

软考架构设计师论文 —— 论AI Agent技术及其应用(5)

软考架构设计师论文 —— 论AI Agent技术及其应用(5)

接前一篇文章:软考架构设计师论文 —— 论AI Agent技术及其应用(4) 本文内容参考: 【爆肝整理】17种AI Agent架构全解析!从ReAct到多智能体系统,小白程序员也能秒懂的Agent设计指南-CSDN博客 特此致谢! 论题 随着大语言模型技术的发展,AI Agent凭借感知、规划、工具…

2026/10/15 13:41:11 阅读更多 →
程序员肩颈护理:专业按摩推拿选择指南

程序员肩颈护理:专业按摩推拿选择指南

1. 如何选择专业的身体按摩推拿服务第一次走进按摩店时,我被琳琅满目的项目名称和价格表弄得晕头转向。作为长期伏案工作的程序员,我的肩颈问题已经严重到影响睡眠质量。经过三年尝试了七家不同机构的服务后,我总结出一套鉴别专业按摩推拿机构…

2026/10/15 13:41:11 阅读更多 →
Jenkins Pipeline 中 docker: not found 报错排查与解决

Jenkins Pipeline 中 docker: not found 报错排查与解决

1. 问题现象与根因定位1.1 报错长什么样,为什么让人一头雾水第一次碰到这个报错的人,大概率会愣一下。Pipeline 脚本里明明写了docker build、docker push,本地终端跑得好好的,一放到 Jenkins 上就给你甩一句: docker …

2026/10/15 13:40:11 阅读更多 →

最新新闻

使用EasyExcel实现高效Excel数据导出与ZIP打包

使用EasyExcel实现高效Excel数据导出与ZIP打包

1. 项目背景与需求拆解最近在开发一个数据报表导出功能时,遇到了一个典型需求:用户上传的Excel文件每行数据需要转换成独立表格,最终将所有表格打包成ZIP供下载。这种场景在数据分发、报表生成等业务中很常见。传统POI方案在处理大批量数据时…

2026/10/15 14:26:35 阅读更多 →
Flutter在OpenHarmony上的跨平台开发实战

Flutter在OpenHarmony上的跨平台开发实战

1. 项目背景与核心价值Flutter作为Google推出的跨平台开发框架,近年来在移动应用开发领域获得了广泛关注。而OpenHarmony作为新兴的分布式操作系统,其生态建设正处于快速发展阶段。将Flutter应用于OpenHarmony平台开发,不仅能够复用现有的Flu…

2026/10/15 14:26:35 阅读更多 →
Magisk与EdXposed搭配指南:MIUI 12安卓10刷框架全流程

Magisk与EdXposed搭配指南:MIUI 12安卓10刷框架全流程

说实话,最近不少玩机群里又开始聊EdXposed了,尤其是手持小米老机型、又停在MIUI 12开发版和安卓10这批人。Magisk(面具)装好之后,下一步最让人手痒的就是搞一个EdXposed框架来玩各类Xposed模块。但是EdXposed和Magisk又…

2026/10/15 14:26:35 阅读更多 →
MIUI12安卓10安装EdXposed教程:Magisk与Riru模块搭配指南

MIUI12安卓10安装EdXposed教程:Magisk与Riru模块搭配指南

1. 写在前面:这个教程到底适合谁先把这个折腾帖的定位说清楚。标题里写的是MIUI12、安卓10、红米K20,但实际这套流程在所有安卓10左右的机型上基本都能跑通,哪怕你不是小米用户,只要手机解锁了BL、能装Magisk,思路完全…

2026/10/15 14:26:35 阅读更多 →
Java+SSM与Flask混合架构酒店管理系统开发实践

Java+SSM与Flask混合架构酒店管理系统开发实践

1. 项目概述这个酒店客房管理系统是一个典型的B/S架构企业级应用,采用JavaSSM(SpringSpringMVCMyBatis)作为后端核心框架,同时创新性地引入Flask作为辅助服务框架。系统实现了从客房预订、入住办理到结算退房的全流程数字化管理&a…

2026/10/15 14:26:35 阅读更多 →
只读数据库+结构化日志驱动的代码行为验证

只读数据库+结构化日志驱动的代码行为验证

1. 这不是“加个日志”那么简单:为什么只读数据库日志验证是代码推断的临门一脚你写完一段业务逻辑,跑通了单元测试,接口返回200,前端页面也渲染正常——但你真的敢说这段代码“行为确定、结果可信”吗?很多开发者卡在…

2026/10/15 14:25:34 阅读更多 →

日新闻

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

Docker Compose 容器化部署 ChirpStack LoRaWAN 网络服务器实战

1. 为什么选择容器化方式部署 LoRa 网络服务LoRa 物联网项目落地时,网络服务器是整个链路的核心枢纽。它负责接收网关上报的数据包、完成去重与解密、再把有效载荷分发给上层应用。早期大家习惯把网络服务器直接装在物理机上,依赖一堆系统库和特定版本的…

2026/10/15 0:00:46 阅读更多 →
共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

共享LLM服务下的跨模型自动扩缩容:Kubernetes实战与避坑指南

1. 共享推理服务里,扩缩容到底难在哪做过大模型推理服务部署的人,大概都经历过这样的场景:一个团队共用一套推理集群,A 业务跑的是 7B 的小模型,B 业务跑的是 72B 的大模型,C 业务白天流量高、晚上几乎归零…

2026/10/15 0:00:46 阅读更多 →
Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

Systemd 启动阶段根文件系统只读之谜:fstab 与 remount-fs 服务深度解析

1. 引子:一次内核恐慌引发的追查先从一个真实场景说起。某次我给一台老旧的嵌入式设备折腾系统裁减,为了把启动时间压进两秒以内,我手贱地把一串自认为“没用”的 systemd 服务给 disable 了,其中就包括systemd-remount-fs.servic…

2026/10/15 0:00:46 阅读更多 →

周新闻

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

流感时间序列预测实战:ARIMA/LSTM全流程拆解与避坑指南

简介:基于 ARIMA、LSTM、Transformer 等模型的流感时间序列预测 Python 源码,面向计算机相关专业课程设计与期末大作业学生,以及项目实战学习者。内容覆盖预处理、平稳性检验、定阶、残差分析、多模型对比预测的完整时序建模流程,…

2026/10/14 3:30:28 阅读更多 →
影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别

影刀RPA新手教程:键盘模拟输入实战——输入文本与模拟按键的区别 做影刀RPA自动化,十个新手有八个栽在"往输入框里填东西"这件事上:要么填不进去,要么填了一半,要么直接把原来内容追加在后面。这背后的根因&…

2026/10/14 16:30:29 阅读更多 →
影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容

影刀RPA新手教程:阅文起点小说数据采集实战——书籍信息与章节内容 1. 认识影刀:什么场景该用RPA采小说数据 起点中文网的页面结构相对稳定——分类榜单、书籍详情、章节内容三块独立页面,跳转链路清晰。这种场景非常适合影刀自动化&#x…

2026/10/15 13:48:15 阅读更多 →

月新闻

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

我发现了一个新思路:用 Remotion + Claude Code 像写代码一样自动化生成短视频

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 0:14:36 阅读更多 →
Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

Windows下 Codex 中 Chrome 和 Computer Use 插件不可用问题排查及解决参考方式:TaoToken 统一 Key 配置与验证

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/14 22:45:47 阅读更多 →
黑夜航拍船只数据集训练YOLOV5模型全流程解析

黑夜航拍船只数据集训练YOLOV5模型全流程解析

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

2026/10/15 9:49:14 阅读更多 →