ARTICLE DETAIL

资讯详情

深耕网站视觉设计与运营推广的一线实战洞察。

深入解析Spring Beans机制:从BeanFactory到三级缓存与生命周期管理

深入解析Spring Beans机制:从BeanFactory到三级缓存与生命周期管理 1. 项目概述为什么我们要深入理解Spring Beans如果你是一名Java开发者那么“Spring”这个词对你来说可能熟悉得像空气一样。我们每天都在用它Autowired、Component、Service这些注解信手拈来Spring Boot的自动配置更是让我们几乎忘记了XML配置的存在。但你是否曾停下来想过当你写下Service注解的那一刻Spring到底为你做了什么那个被你注入到Controller里的Service对象它从哪里来经历了怎样的“生命历程”最终才成为你手中那个功能完备的Bean这就是“Spring Beans机制”要回答的核心问题。它远不止是“一个被Spring管理的Java对象”这么简单。Beans是Spring框架这座宏伟建筑的基石是整个控制反转IoC和依赖注入DI思想的物理承载。不理解Beans你对Spring的理解就始终停留在“会用”的层面一旦遇到复杂的循环依赖、作用域问题、AOP代理失效或者启动报BeanCreationException时就会陷入盲目试错的困境。我见过太多开发者项目做了好几年依然对BeanFactory和ApplicationContext的区别模棱两可对Bean的生命周期一知半解更别提三级缓存、早期引用这些解决循环依赖的精妙设计了。这次我们不谈那些高屋建瓴的“设计模式”也不做简单的API罗列。我们就从一个Bean的“诞生”开始像解构一个魔法一样一步步拆解Spring核心容器是如何编织这张Bean的管理之网的。你会发现那些看似神秘的“魔法”背后都是一套严谨、精巧的机制在支撑。2. 核心容器基石BeanFactory与ApplicationContext的职责边界要理解Beans必须先理解容纳它们的“容器”。在Spring中这个容器有两个核心代表BeanFactory和ApplicationContext。很多初学者会混淆它们其实它们的职责有清晰的边界。2.1 BeanFactory低调的基石提供者BeanFactory是Spring IoC容器最底层的接口定义了容器最基本的功能管理Bean。你可以把它想象成一个对象工厂的蓝图它只关心最核心的几件事实例化Bean根据配置信息无论是XML、注解还是Java Config调用构造方法或工厂方法创建对象。装配Bean解决Bean之间的依赖关系将所需的依赖注入到Bean中。管理Bean生命周期负责调用初始化回调如InitializingBean的afterPropertiesSet或PostConstruct方法和销毁回调。提供Bean的访问通过名称或类型获取Bean实例。它的设计遵循了“最小接口”原则非常轻量。一个典型的DefaultListableBeanFactory就能独立工作。但是它不提供一些企业级应用常用的“增强”功能比如国际化的消息解析、事件发布机制、更便捷的AOP集成等。注意直接使用BeanFactory时Bean默认是**懒加载Lazy Loading**的。也就是说只有在调用getBean()方法请求某个Bean时容器才会去创建它。这在某些追求极致启动速度或资源受限的场景下有用但对于现代应用我们更希望启动时就完成所有检查。2.2 ApplicationContext全功能的企业级管家ApplicationContext是BeanFactory的子接口你可以把它理解为BeanFactory的“豪华增强版”。它在继承了所有Bean管理能力的基础上增加了一系列开箱即用的企业级特性这也是为什么我们在Spring Boot应用中几乎总是使用它的原因。它额外提供的关键能力包括统一的资源访问支持以classpath:、file:、http:等前缀访问资源文件简化配置。国际化i18n支持方便处理多语言消息。事件发布与监听机制这是Spring内部模块解耦的重要方式比如ContextRefreshedEvent容器刷新完成事件就被广泛用于启动后执行某些逻辑。更强大的AOP集成简化了面向切面编程的配置和使用。默认即时加载Eager LoadingApplicationContext在启动时就会创建并配置所有的单例Bean除非显式设置为懒加载。这虽然让启动时间变长但能在启动阶段就暴露出配置错误和依赖问题而不是在运行时才突然崩溃提高了系统的可预测性。一个重要的认知ApplicationContext并非重写了Bean的创建逻辑它内部持有一个BeanFactory的实例通常是DefaultListableBeanFactory将Bean的创建、装配等核心工作委托给它自己则专注于提供那些增值服务。这就像一家公司BeanFactory是核心的生产部门而ApplicationContext是包含了生产、市场、人事、后勤的完整管理层。实操心得在99%的Spring Boot应用中你接触到的都是ApplicationContext例如AnnotationConfigApplicationContext或SpringApplication创建的上下文。理解BeanFactory的存在是为了让你明白Spring的层次化设计以及在极少数需要深度定制容器行为时知道从哪里入手。例如如果你需要实现一个高度定制化的Bean作用域最终需要操作的就是BeanFactory层面的BeanDefinition。3. Bean的定义与注册从概念到蓝图的旅程容器知道了要管理Bean但它首先得知道如何创建这个Bean。这个“创建说明书”就是BeanDefinitionBean定义。它是Spring容器中对于Bean的抽象描述是比Java Class对象更丰富的一层元数据。3.1 BeanDefinitionBean的元数据蓝图一个BeanDefinition对象包含了创建一个Bean所需的所有信息Bean的类名beanClassName指向具体的Java类。作用域scope是单例singleton还是原型prototype或者是request、session等Web作用域。是否懒加载lazyInit。初始化方法和销毁方法名。构造函数参数值用于构造器注入。属性值用于Setter注入。依赖的Bean名称。在Spring启动过程中容器会扫描所有配置源如ComponentScan扫描的包、XML文件、Bean方法为每一个需要管理的Bean生成一个对应的BeanDefinition对象并将其注册到一个中心仓库——通常是DefaultListableBeanFactory内部的beanDefinitionMap一个ConcurrentHashMap中。3.2 注册的三种主流方式XML配置传统方式bean iduserService classcom.example.service.UserServiceImpl property nameuserDao refuserDao/ /beanSpring解析这个bean标签就会生成一个包含类名、属性依赖等信息的RootBeanDefinition并注册。注解扫描现代主流 使用Component,Service,Repository,Controller等注解标记类。Spring在启动时通过ClassPathBeanDefinitionScanner扫描指定包路径识别这些注解并为每个类生成一个ScannedGenericBeanDefinition并注册。Service public class UserServiceImpl implements UserService { // ... }Java配置类灵活配置 使用Configuration标记一个类并在其中使用Bean注解方法。Spring会处理这个配置类将Bean方法的返回值注册为Bean。这种方式生成的通常是ConfigurationClassBeanDefinition。Configuration public class AppConfig { Bean public DataSource dataSource() { return new HikariDataSource(...); } }核心要点在Bean实例被创建出来之前BeanDefinition就已经存在于容器中了。它是一份静态的蓝图。后续所有的Bean生命周期管理都是基于这份蓝图来进行的。理解这一点就能明白为什么修改BeanDefinition例如通过BeanFactoryPostProcessor可以影响后续所有Bean的创建行为。4. Bean的生命周期一个Bean的奇幻漂流这是Spring Beans机制中最核心、最复杂也最迷人的部分。一个Bean从无到有再到销毁经历了十多个关键节点。我们把它拆解成几个阶段来看。4.1 阶段一实例化与属性填充实例化Instantiate容器根据BeanDefinition的信息通过反射最常见或工厂方法调用构造器创建一个原始的Java对象。此时对象内部的依赖字段如Autowired标记的字段都还是null它只是一个“空壳”。属性填充Populate PropertiesSpring根据依赖注入的方式构造器、Setter、字段将其他Bean或配置值注入到当前Bean中。这是解决依赖关系的关键步骤。踩坑记录循环依赖问题最常发生在这个阶段。Bean A创建时需要注入Bean B而Bean B创建时又需要注入Bean A。如果处理不好容器就会陷入死锁。Spring通过“三级缓存”机制巧妙地解决了单例Bean的构造器循环依赖Setter/字段注入可以构造器注入不行这个我们后面会详细讲。4.2 阶段二Aware接口回调与初始化在Bean的属性填充完毕后Spring提供了一系列Aware接口让Bean能感知到容器的存在获取一些基础设施对象。Aware接口回调如果Bean实现了相关的Aware接口容器会回调对应的方法注入相关的依赖。BeanNameAware设置Bean的名字。BeanFactoryAware设置BeanFactory。ApplicationContextAware设置ApplicationContext这是一个非常重要的接口很多需要获取容器中其他Bean的自定义逻辑会用到它。BeanPostProcessor前置处理这是Spring一个极其强大的扩展点。所有实现了BeanPostProcessor接口的Bean它们的postProcessBeforeInitialization方法会在这个阶段被调用。你可以在这里对Bean进行修改或包装比如AOP的动态代理就是在这里生成的。初始化Initialization方式一如果Bean实现了InitializingBean接口会调用其afterPropertiesSet()方法。方式二更推荐使用JSR-250的PostConstruct注解在方法上标记它会在属性注入完成后执行。方式三在XML或Bean注解中指定的init-method。这三种方式的执行顺序是PostConstruct-InitializingBean.afterPropertiesSet()- 自定义的init-method。BeanPostProcessor后置处理调用所有BeanPostProcessor的postProcessAfterInitialization方法。经过这一步Bean已经完全准备就绪。4.3 阶段三使用与销毁使用期Bean处于可用状态驻留在容器中单例或等待被获取原型。销毁Destruction当容器关闭时对于单例Bean。方式一PreDestroy注解标记的方法。方式二实现DisposableBean接口的destroy()方法。方式三在XML或Bean注解中指定的destroy-method。执行顺序与初始化类似PreDestroy-DisposableBean.destroy()- 自定义的destroy-method。生命周期流程图文字描述版开始 - 实例化Bean对象 - 填充属性/解决依赖 - Aware系列接口回调 - BeanPostProcessor.postProcessBeforeInitialization - PostConstruct - InitializingBean.afterPropertiesSet - init-method - BeanPostProcessor.postProcessAfterInitialization - Bean就绪可用 - 容器关闭 - PreDestroy - DisposableBean.destroy - destroy-method - 结束实操心得理解这个生命周期对于调试和编写扩展组件至关重要。比如如果你在PostConstruct方法里试图调用一个被AOP代理的方法可能会发现增强逻辑没有生效。这是因为PostConstruct执行时BeanPostProcessor的后置处理可能包含了创建代理的逻辑可能还未应用到当前Bean上。正确的做法是将这类逻辑移到更靠后的阶段或者通过ApplicationListener监听ContextRefreshedEvent事件。5. 依赖注入的魔法Setter、构造器与字段注入的抉择依赖注入是Spring的招牌魔法。它主要有三种方式各有优劣。5.1 三种注入方式详解Setter注入通过Setter方法注入。Service public class UserService { private UserDao userDao; Autowired public void setUserDao(UserDao userDao) { this.userDao userDao; } }优点灵活允许在Bean创建后重新注入依赖虽然极少这么做。缺点对象在构造后可能处于不完整状态因为依赖可能还没注入。无法将依赖字段标记为final。构造器注入通过构造方法注入。这是Spring官方自4.x版本以来推荐的方式。Service public class UserService { private final UserDao userDao; Autowired // Spring 4.3以后如果只有一个构造器此注解可省略 public UserService(UserDao userDao) { this.userDao userDao; } }优点不可变性依赖字段可以声明为final确保Bean在构造完成后就处于完全初始化的“不可变”状态线程安全。明确依赖强制要求所有必需依赖在构造时提供避免了不完整状态。便于测试无需Spring容器可以直接通过构造器传入Mock对象进行单元测试。缺点当依赖很多时构造器参数列表会很长影响可读性但这通常也是类职责过重的信号。字段注入直接在字段上使用Autowired。Service public class UserService { Autowired private UserDao userDao; }优点代码极其简洁。缺点不利于测试你必须使用Spring的测试框架或者反射来注入依赖。隐藏了依赖关系类对外部的依赖不再一目了然。可能导致违反单一职责原则因为添加依赖太容易容易让一个类承担过多职责。无法声明为final。5.2 抉择与最佳实践当前社区的共识和Spring团队的推荐是优先使用构造器注入来注入必需的依赖。对于可选的依赖比如某些配置项没有也能工作可以考虑使用Setter注入。字段注入应尽量避免在业务核心代码中使用它更适合在配置类、或者一些非核心的辅助类中为了简洁而使用。为什么构造器注入能解决循环依赖这里有个常见的误解。实际上构造器注入无法解决循环依赖。因为A和B都需要对方作为构造参数在实例化阶段就会卡住。Spring的“三级缓存”机制解决的是单例模式下Setter注入或字段注入的循环依赖。理解这一点能让你在设计Bean时主动避免构造器循环依赖这是一种更好的设计实践。6. 作用域与生命周期管理单例、原型与更多Bean的作用域定义了Bean实例的生命周期和可见范围。Spring默认提供了几种作用域并支持自定义。6.1 核心作用域singleton单例默认在整个Spring IoC容器中只存在一个该Bean的共享实例。所有对该Bean的请求都返回同一个实例。它的生命周期与容器相同。配置Scope(singleton)或Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)注意单例Bean不是Java单例模式静态实例它是“容器内的单例”。在多线程环境下如果单例Bean有状态即有可变的成员变量需要开发者自己处理线程安全问题。prototype原型每次请求通过getBean()或注入都会创建一个新的Bean实例。类似于new操作符。配置Scope(prototype)重要特性Spring只负责原型Bean的创建和初始化不负责其销毁。销毁的回调方法不会被调用需要客户端代码自己管理其生命周期。6.2 Web相关作用域需Web环境request每个HTTP请求创建一个新实例。session每个HTTP会话创建一个新实例。application每个ServletContext生命周期创建一个实例近似于单例但范围是Web应用。6.3 自定义作用域Spring允许你通过实现Scope接口来定义自己的作用域比如“线程作用域”、“集群作用域”等。你需要将这个自定义的作用域注册到容器中。beanFactory.registerScope(myThreadScope, new MyThreadScope());然后就可以使用Scope(myThreadScope)。实操心得原型Bean的注入陷阱。如果你将一个原型Beanscopeprototype注入到一个单例Bean中你会发现这个原型Bean“失效”了——单例Bean只会在初始化时注入一次原型Bean之后每次访问的都是同一个实例。要解决这个问题有几种方法使用方法注入Lookup注解。通过ApplicationContext.getBean()每次获取新实例但这样增加了与容器的耦合。将单例Bean的实现改为ObjectFactoryMyPrototypeBean或ProviderMyPrototypeBean这样每次调用getObject()或get()方法时容器会返回一个新的原型实例。这是更优雅的方式。7. 循环依赖与三级缓存Spring的经典解决方案循环依赖是面试高频题也是理解Spring容器内部工作机制的绝佳切入点。我们重点看Spring最经典的单例Bean循环依赖解决方案。7.1 什么是循环依赖假设有两个BeanAService和BService。Service public class AService { Autowired private BService bService; } Service public class BService { Autowired private AService aService; }这就是一个典型的Setter/字段注入的循环依赖。AService创建需要BServiceBService创建又需要AService。7.2 三级缓存机制详解Spring通过三个Map俗称三级缓存来打破这个“先有鸡还是先有蛋”的僵局。这三个缓存都位于DefaultSingletonBeanRegistry中一级缓存singletonObjects(ConcurrentHashMap)存放内容完全初始化好的单例Bean。这是最终获取Bean的地方。Key: Bean nameValue: 完整的Bean实例二级缓存earlySingletonObjects(HashMap)存放内容早期的单例对象已经实例化但尚未完成属性填充和初始化。用于解决循环依赖。Key: Bean nameValue: 早期的Bean引用可能是原始对象也可能是AOP代理对象。三级缓存singletonFactories(HashMap)存放内容单例对象工厂ObjectFactory。这是解决循环依赖和AOP代理协同工作的关键。Key: Bean nameValue:ObjectFactory调用其getObject()方法可以返回一个早期的Bean引用。7.3 解决循环依赖的流程推演我们以AService和BService的循环依赖为例假设A先开始创建开始创建A调用getBean(“aService”)。实例化A通过反射创建出A的原始对象a。将A的工厂放入三级缓存此时A还未填充属性但Spring提前将一个能产生A早期引用的ObjectFactory放入singletonFactories三级缓存。这个工厂很聪明如果A最终不需要被AOP代理它就返回原始对象a如果需要代理它就有能力返回一个早期的代理对象。这是关键一步。填充A的属性Spring发现A依赖bService于是调用getBean(“bService”)去获取B。开始创建B流程类似反射创建B的原始对象b。将B的工厂放入三级缓存。填充B的属性Spring发现B依赖aService于是调用getBean(“aService”)。再次获取A此时Spring不会从头创建A而是先去一级缓存找没有再去二级缓存找没有最后在三级缓存中找到了A的ObjectFactory。获取A的早期引用调用这个工厂的getObject()方法。无论返回的是A的原始对象还是代理对象这个引用都被放入二级缓存earlySingletonObjects并从三级缓存中移除。然后将这个早期引用注入给B。B完成属性填充和初始化B拿到了A的早期引用得以继续完成后续的初始化步骤Aware回调、PostConstruct等。完成后B被放入一级缓存singletonObjects。返回给AB创建完毕返回到A的创建流程A成功注入完整的B。A完成初始化A继续完成自己的初始化最终也被放入一级缓存。同时由于A的早期引用已经存在于二级缓存在A完全初始化后需要确保一级缓存和二级缓存中的引用是同一个对于被代理的Bean这需要特殊处理。为什么需要三级缓存二级不行吗关键在于AOP。如果只有二级缓存存放早期对象那么当A需要被代理时在B注入A的时刻就必须确定A的最终代理对象。但此时A的初始化还没完成某些AOP切面逻辑依赖于初始化后的状态可能无法正确应用。三级缓存中的ObjectFactory提供了一个“懒加载”机制它允许在真正需要早期引用的时候即B注入A时才去决定生成原始对象还是代理对象并且这个生成逻辑可以很灵活。这解耦了实例化、代理生成和依赖注入的时机。重要限制三级缓存只能解决Setter注入或字段注入的循环依赖。对于构造器注入的循环依赖Spring是无能为力的会在启动时直接抛出BeanCurrentlyInCreationException。因为构造器注入发生在实例化阶段此时Bean的原始对象都还没创建出来更无法提前暴露引用到缓存中。8. 高级话题与扩展点理解了核心机制我们可以看看Spring提供的强大扩展能力它们让你能深度介入容器的管理过程。8.1 BeanPostProcessorBean的加工车间BeanPostProcessor是一个容器级别的后处理器允许你在Bean初始化回调PostConstruct等执行前后对Bean实例进行修改或包装。这是Spring AOP实现动态代理的基石。postProcessBeforeInitialization(Object bean, String beanName)在初始化方法调用前执行。可以返回原始Bean或包装后的Bean。postProcessAfterInitialization(Object bean, String beanName)在初始化方法调用后执行。AOP的AbstractAutoProxyCreator就是在这里检查Bean是否需要被代理如果需要则用代理对象替换原始对象并返回。自定义示例一个简单的给所有Bean打印日志的处理器。Component public class MyBeanPostProcessor implements BeanPostProcessor { Override public Object postProcessBeforeInitialization(Object bean, String beanName) { System.out.println(Before初始化: beanName); return bean; // 可以返回包装后的对象 } Override public Object postProcessAfterInitialization(Object bean, String beanName) { System.out.println(After初始化: beanName); return bean; } }8.2 BeanFactoryPostProcessorBean定义的整形师BeanFactoryPostProcessor在BeanDefinition加载完成后但Bean实例化之前执行。它可以读取、修改甚至注册新的BeanDefinition。这比BeanPostProcessor更早影响力更大。postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory)参数直接就是Bean工厂你可以通过它获取和修改所有Bean的定义。典型应用PropertySourcesPlaceholderConfigurer用来解析${...}占位符它就是通过BeanFactoryPostProcessor来修改Bean定义中的属性值。自定义注解扫描与注册你可以扫描特定注解然后动态地向容器中注册新的Bean定义。8.3 FactoryBean复杂的对象工厂普通的Bean方法或bean标签创建的是方法返回类型或class指定的对象。但如果你需要创建的Bean本身很复杂或者你想隐藏Bean的真实类型就可以使用FactoryBean接口。FactoryBean是一个能生产对象的工厂Bean。当你向容器请求一个FactoryBean时默认得到的是它getObject()方法返回的对象而不是FactoryBean本身。要获取FactoryBean实例需要在Bean名称前加符号。经典例子MyBatis-Spring整合中的SqlSessionFactoryBean。它本身是一个FactoryBean我们在配置中定义它但Spring最终放到容器里的是它创建的SqlSessionFactory实例。Bean public SqlSessionFactoryBean sqlSessionFactoryBean(DataSource dataSource) { SqlSessionFactoryBean factoryBean new SqlSessionFactoryBean(); factoryBean.setDataSource(dataSource); // ... 其他配置 return factoryBean; // Spring会调用它的getObject()方法将返回的SqlSessionFactory注册为Bean }9. 常见问题排查与实战技巧理论最终要服务于实践。下面是一些实战中高频出现的问题和解决思路。9.1 启动时报BeanCreationException或BeanDefinitionStoreException可能原因1循环依赖构造器注入。错误信息通常包含Requested bean is currently in creation。解决方案检查并重构代码打破构造器循环依赖。通常可以通过将部分依赖改为Setter注入或者提取公共逻辑到第三个Bean中来解决。可能原因2Bean定义冲突。比如通过ComponentScan和Bean方法重复定义了同一个Bean。解决方案使用Bean的name属性指定不同名称或者使用Primary注解指定主候选Bean。可能原因3依赖的Bean不存在或未扫描到。检查包扫描路径、Component等注解是否添加、Bean方法是否正确。可能原因4Bean的属性注入失败。比如Value注入的配置项在配置文件中不存在。检查配置文件和Value的键名。9.2 注入的Bean为null可能原因1Bean未被Spring管理。确保类上有Component或其派生注解或者已在配置类中通过Bean声明。检查包是否在ComponentScan的扫描范围内。可能原因2在非Spring管理的对象中使用Autowired。例如在普通的new出来的对象中Autowired不会生效。依赖注入只对Spring容器管理的Bean有效。可能原因3静态字段/方法上使用Autowired。Autowired是基于对象实例的不能用于静态成员。如果必须注入静态字段可以通过PostConstruct在非静态方法中为静态字段赋值。可能原因4存在多个同类型Bean且未指定注入哪一个。Spring无法做出选择。使用Qualifier注解指定Bean名称或者将其中一个Bean标记为Primary。9.3 AOP代理失效问题现象在同一个Bean内部方法A调用方法B方法B上的事务注解Transactional或自定义切面不生效。原因Spring AOP默认使用JDK动态代理或CGLIB代理。这些代理是基于“从容器中获取Bean”这个动作生效的。当在Bean内部直接调用另一个方法时this.methodB()调用的是原始对象的方法绕过了代理对象因此切面逻辑不会被执行。解决方案自我注入将Bean自己注入到自己的一个字段中然后通过这个字段调用方法。Service public class MyService { Autowired private MyService self; // 注入自己 public void methodA() { // 通过代理对象调用 self.methodB(); } Transactional public void methodB() { // ... } }使用AspectJ的编译时或加载时织入LTW但这会引入更复杂的配置。重构代码将需要被代理的方法拆分到另一个Bean中。9.4 原型Bean注入单例Bean失效如前所述将原型Bean直接注入单例Bean会导致单例Bean始终持有同一个原型实例。解决方案使用ObjectFactory或Provider推荐Service public class SingletonService { Autowired private ObjectFactoryPrototypeBean prototypeBeanFactory; public void doSomething() { PrototypeBean bean prototypeBeanFactory.getObject(); // 每次都是新的 // ... } }使用方法注入LookupService public abstract class SingletonService { public void doSomething() { PrototypeBean bean getPrototypeBean(); // ... } Lookup protected abstract PrototypeBean getPrototypeBean(); }Spring会生成一个子类来覆盖这个抽象方法每次调用都返回新的原型Bean。理解Spring Beans机制就像掌握了Spring框架的“内功心法”。它不会让你立刻写出更炫酷的代码但会让你在面对复杂问题、进行性能调优、深度定制框架行为时拥有清晰的思路和强大的自信。从BeanDefinition的注册到三级缓存解决循环依赖的巧妙再到BeanPostProcessor提供的强大扩展性每一步都体现了Spring设计上的深思熟虑。下次当你再使用Autowired时希望你能会心一笑知道背后正有一场精密的魔法在为你运转。
返回列表