从源码倒看Spring用法(二)
代码仓库 :Gitee 仓库链接
本文档的所有示例代码都可以在代码仓库中找到,建议结合代码一起阅读。
回顾学习历程
在上一篇文章中,我们总结了四个重要的 Spring 用法知识点:
- Spring Profile 环境隔离
- Spring FactoryBean
- Spring 抽象 Bean
- Spring 构造函数参数注入和集合类型注入
本文将继续总结在阅读 Spring 源码过程中学习到的其他重要用法知识点,作为后续复习使用。
Spring 用法知识点总结
本文档将继续总结在阅读 Spring 源码过程中学习到的其他重要用法知识点,每个知识点都包含概念、特点、使用场景、XML 配置方式、核心机制和注意事项。
1. 如何在单例 Bean 中依赖一个 Prototype Bean,保证每次调用都是新实例
概念
当单例 Bean 需要依赖 Prototype Bean 时,由于单例 Bean 只会创建一次,如果直接通过属性注入 Prototype Bean,那么单例 Bean 中的 Prototype Bean 引用也只会创建一次,无法实现每次调用都获取新实例的需求。
核心特点
- 使用
lookup-method标签实现方法级别的依赖注入 - 需要在单例 Bean 中定义一个抽象方法
- Spring 通过 CGLIB 动态代理实现该方法
- 每次调用该方法都会返回新的 Prototype Bean 实例
使用场景
- 单例 Bean 需要每次调用都获取新的 Prototype Bean 实例
- 避免在单例 Bean 中持有 Prototype Bean 的引用
- 实现延迟获取 Prototype Bean 实例
XML 配置方式
xml
<!-- 定义prototype bean -->
<bean id="prototypeBean" class="com.example.lookupmethod.PrototypeBean" scope="prototype">
<property name="name" value="原型Bean"/>
</bean>
<!-- 定义单例service,使用lookup-method依赖prototype bean -->
<bean id="singletonService" class="com.example.lookupmethod.SingletonService">
<property name="serviceName" value="单例服务"/>
<!--
lookup-method 标签:
- name: 抽象方法名
- bean: 要返回的prototype bean的id
-->
<lookup-method name="createPrototypeBean" bean="prototypeBean"/>
</bean>
核心机制
- Spring 在创建单例 Bean 时,会检查是否有
lookup-method配置 - 如果有,Spring 会使用 CGLIB 创建代理类,实现抽象方法
- 每次调用该方法时,Spring 会从容器中获取新的 Prototype Bean 实例
- 通过方法调用而非属性注入,实现了延迟获取和每次新实例的效果
注意事项
- 使用
lookup-method的类必须是抽象类或非 final 类(CGLIB 需要) - 抽象方法必须是 public 或 protected
- 抽象方法不能有参数
- 返回类型必须是 Prototype Bean 的类型或其父类/接口
2. replaced-method 和 FactoryBean 无法同时使用
概念
replaced-method(方法替换)和 FactoryBean 是 Spring 提供的两种不同的机制,但它们无法同时使用。当尝试在 FactoryBean 上配置 replaced-method 时,Spring 会在验证阶段抛出异常。
核心特点
replaced-method需要在 Bean 类上找到对应的方法FactoryBean返回的是目标对象,而不是 FactoryBean 本身- Spring 无法在 FactoryBean 类上找到目标方法,导致验证失败
使用场景
- 了解 Spring 的限制和约束
- 避免配置错误
- 理解 Spring 内部机制
XML 配置方式(错误示例)
xml
<!-- 错误配置:同时使用FactoryBean和replaced-method -->
<bean id="serviceWithFactoryBean" class="com.example.overridemethod.ServiceFactoryBean">
<property name="name" value="通过FactoryBean创建的服务"/>
<!-- replaced-method 在FactoryBean上不会生效,会抛出异常 -->
<replaced-method name="process" replacer="methodReplacer">
<arg-type>String</arg-type>
</replaced-method>
</bean>
核心机制
- Spring 在验证 BeanDefinition 时,会检查
replaced-method配置 - 验证过程会在 FactoryBean 类上查找目标方法
- 由于目标方法在目标对象上,而不是 FactoryBean 类上,验证失败
- Spring 抛出
BeanDefinitionValidationException异常
注意事项
replaced-method和FactoryBean不能同时使用- 如果需要方法替换,应该直接配置目标 Bean,而不是使用 FactoryBean
- 如果需要使用 FactoryBean,应该避免使用
replaced-method
3. 多例模式循环依赖的错误示例
概念
当两个或多个 Prototype Bean 相互依赖时,Spring 无法解决这种循环依赖,会抛出 BeanCreationException 异常。
核心特点
- Prototype Bean 不会放入 Spring 的三级缓存
- 每次获取 Prototype Bean 都是新实例
- 无法提前暴露半成品对象
- 无法解决循环依赖
使用场景
- 了解 Spring 循环依赖的解决机制
- 理解单例和 Prototype 的区别
- 避免配置错误
XML 配置方式(错误示例)
xml
<!-- ServiceA:依赖ServiceB,使用prototype作用域 -->
<bean id="serviceA" class="com.example.circulardependency.ServiceA" scope="prototype">
<property name="name" value="ServiceA"/>
<constructor-arg ref="serviceB"/>
</bean>
<!-- ServiceB:依赖ServiceA,使用prototype作用域 -->
<bean id="serviceB" class="com.example.circulardependency.ServiceB" scope="prototype">
<property name="name" value="ServiceB"/>
<constructor-arg ref="serviceA"/>
</bean>
核心机制
- Spring 通过三级缓存解决单例 Bean 的循环依赖
- 三级缓存包括:singletonObjects、earlySingletonObjects、singletonFactories
- Prototype Bean 不会放入缓存,每次都是新实例
- 无法提前暴露半成品对象,因此无法解决循环依赖
注意事项
- Prototype Bean 的循环依赖无法解决,会抛出异常
- 单例 Bean 的循环依赖可以通过三级缓存机制解决
- 避免在 Prototype Bean 之间形成循环依赖
4. 别名循环依赖的错误示例
概念
当别名形成循环依赖时(如 beanA 的别名是 beanB,beanB 的别名是 beanA),Spring 会检测到这种循环并拒绝注册,抛出 BeanDefinitionParsingException 异常。
核心特点
- Spring 在注册别名时会检测循环依赖
- 如果检测到循环依赖,会抛出异常
- 错误信息:
Cannot register alias 'beanA' for name 'beanB': Circular reference
使用场景
- 了解 Spring 别名注册机制
- 避免配置错误
- 理解 Spring 的验证机制
XML 配置方式(错误示例)
xml
<!-- 定义beanA -->
<bean id="beanA" class="java.lang.String">
<constructor-arg value="BeanA"/>
</bean>
<!-- beanA的别名是beanB -->
<alias name="beanA" alias="beanB"/>
<!-- beanB的别名是beanA(形成循环) -->
<alias name="beanB" alias="beanA"/>
核心机制
- Spring 在注册别名时会建立映射关系
- 注册过程中会检查是否形成循环依赖
- 如果检测到循环,会抛出
IllegalStateException - 异常信息会明确指出循环依赖的路径
注意事项
- Spring 不允许别名形成循环依赖
- 别名最终都指向同一个 Bean 定义,但注册过程不允许循环
- 避免配置循环的别名关系
5. 重写 preProcessXml 和 postProcessXml 方法,打印 Bean 数量变化
概念
preProcessXml 和 postProcessXml 是 DefaultBeanDefinitionDocumentReader 中的两个钩子方法,分别在解析 Bean 定义之前和之后调用。可以通过继承 DefaultBeanDefinitionDocumentReader 并重写这两个方法来观察 Bean 数量的变化。
核心特点
preProcessXml在解析 Bean 定义之前调用postProcessXml在解析 Bean 定义之后调用- 可以通过这两个方法观察 Bean 注册过程
- 支持扩展和自定义处理逻辑
使用场景
- 调试 Bean 定义加载过程
- 观察 Bean 数量变化
- 添加自定义的预处理和后处理逻辑
- 监控和日志记录
实现方式
java
public class CustomBeanDefinitionDocumentReader extends DefaultBeanDefinitionDocumentReader {
@Override
protected void preProcessXml(Element root) {
int beanCount = getRegistry().getBeanDefinitionCount();
System.out.println("preProcessXml: Bean数量(处理前): " + beanCount);
}
@Override
protected void postProcessXml(Element root) {
int beanCount = getRegistry().getBeanDefinitionCount();
System.out.println("postProcessXml: Bean数量(处理后): " + beanCount);
}
}
核心机制
preProcessXml在doRegisterBeanDefinitions方法中,解析 Bean 定义之前调用postProcessXml在doRegisterBeanDefinitions方法中,解析 Bean 定义之后调用- 可以通过
getRegistry()获取BeanDefinitionRegistry,进而获取 Bean 定义数量 - 这两个方法是模板方法模式的应用,支持扩展
注意事项
- 需要继承
DefaultBeanDefinitionDocumentReader才能重写这两个方法 - 这两个方法是 protected 方法,只能在子类中访问
- 可以通过这两个方法添加自定义逻辑,但不建议修改核心流程
6. fireComponentRegistered 方法的调用示例
概念
fireComponentRegistered 方法在注册 BeanComponentDefinition 时被调用,用于触发 ComponentRegisteredEvent 事件。这个方法主要用于 Spring 内部的事件通知机制。
核心特点
- 在
processBeanDefinition方法中调用 - 当注册
BeanComponentDefinition时触发 - 主要用于 Spring 内部的事件通知
- 支持监听器模式
使用场景
- 自定义命名空间处理器
- 需要通知组件注册事件的场景
- 理解 Spring 内部事件机制
- 扩展 Spring 功能
调用位置
java
protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
if (bdHolder != null) {
bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
// 注册BeanDefinition
BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
// 触发组件注册事件
getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
}
}
核心机制
fireComponentRegistered方法会触发ComponentRegisteredEvent事件- 事件会被注册的监听器接收和处理
- 主要用于 Spring 内部的事件通知机制
- 支持扩展和自定义处理
注意事项
- 这个方法主要在 Spring 内部使用
- 通常在使用自定义命名空间处理器时会触发
- 可以通过事件监听器来观察组件注册过程
- 不建议直接调用这个方法
代码示例
1. lookup-method 示例
配置文件位置 :code/spring-basic/src/main/resources/applicationContext-lookupmethod.xml
测试类位置 :code/spring-basic/src/test/java/com/example/lookupmethod/LookupMethodTest.java
相关类:
PrototypeBean.java- 原型 BeanSingletonService.java- 单例 Service,使用 lookup-method
关键代码示例:
xml
<bean id="prototypeBean" class="com.example.lookupmethod.PrototypeBean" scope="prototype">
<property name="name" value="原型Bean"/>
</bean>
<bean id="singletonService" class="com.example.lookupmethod.SingletonService">
<property name="serviceName" value="单例服务"/>
<lookup-method name="createPrototypeBean" bean="prototypeBean"/>
</bean>
java
@Test
public void testLookupMethodReturnsNewInstance() {
BeanFactory factory = new XmlBeanFactory(
new ClassPathResource("applicationContext-lookupmethod.xml")
);
SingletonService service = factory.getBean("singletonService", SingletonService.class);
// 第一次调用
PrototypeBean bean1 = service.createPrototypeBean();
int instanceId1 = bean1.getInstanceId();
// 第二次调用
PrototypeBean bean2 = service.createPrototypeBean();
int instanceId2 = bean2.getInstanceId();
// 验证两次调用返回不同的实例
assertNotEquals(instanceId1, instanceId2);
}
2. replaced-method 和 FactoryBean 冲突示例
配置文件位置 :code/spring-basic/src/main/resources/applicationContext-overridemethod-factorybean.xml
测试类位置 :code/spring-basic/src/test/java/com/example/overridemethod/OverrideMethodAndFactoryBeanTest.java
相关类:
ServiceWithOverrideMethod.java- 使用 replaced-method 的 ServiceServiceFactoryBean.java- FactoryBean 实现ServiceMethodReplacer.java- 方法替换器
关键代码示例:
xml
<!-- 错误配置:同时使用FactoryBean和replaced-method -->
<bean id="serviceWithFactoryBean" class="com.example.overridemethod.ServiceFactoryBean">
<property name="name" value="通过FactoryBean创建的服务"/>
<replaced-method name="process" replacer="methodReplacer">
<arg-type>String</arg-type>
</replaced-method>
</bean>
java
@Test
public void testReplacedMethodNotWorkWithFactoryBean() {
assertThrows(BeanDefinitionStoreException.class, () -> {
BeanFactory factory = new XmlBeanFactory(
new ClassPathResource("applicationContext-overridemethod-factorybean.xml")
);
factory.getBean("serviceWithFactoryBean", ServiceWithOverrideMethod.class);
});
}
3. Prototype 循环依赖错误示例
配置文件位置 :code/spring-basic/src/main/resources/applicationContext-circulardependency-prototype.xml
测试类位置 :code/spring-basic/src/test/java/com/example/circulardependency/CircularDependencyPrototypeTest.java
相关类:
ServiceA.java- 依赖 ServiceBServiceB.java- 依赖 ServiceA
关键代码示例:
xml
<bean id="serviceA" class="com.example.circulardependency.ServiceA" scope="prototype">
<constructor-arg ref="serviceB"/>
</bean>
<bean id="serviceB" class="com.example.circulardependency.ServiceB" scope="prototype">
<constructor-arg ref="serviceA"/>
</bean>
java
@Test
public void testPrototypeCircularDependencyThrowsException() {
assertThrows(BeanCreationException.class, () -> {
BeanFactory factory = new XmlBeanFactory(
new ClassPathResource("applicationContext-circulardependency-prototype.xml")
);
factory.getBean("serviceA", ServiceA.class);
});
}
4. 别名循环依赖错误示例
配置文件位置 :code/spring-basic/src/main/resources/applicationContext-alias-circular.xml
测试类位置 :code/spring-basic/src/test/java/com/example/alias/CircularAliasTest.java
关键代码示例:
xml
<bean id="beanA" class="java.lang.String">
<constructor-arg value="BeanA"/>
</bean>
<alias name="beanA" alias="beanB"/>
<alias name="beanB" alias="beanA"/>
java
@Test
public void testCircularAliasThrowsException() {
assertThrows(BeanDefinitionParsingException.class, () -> {
new XmlBeanFactory(
new ClassPathResource("applicationContext-alias-circular.xml")
);
});
}
5. preProcessXml 和 postProcessXml 示例
配置文件位置 :code/spring-basic/src/main/resources/applicationContext-prepostprocess.xml
测试类位置 :code/spring-basic/src/test/java/com/example/prepostprocess/PrePostProcessTest.java
相关类:
CustomBeanDefinitionDocumentReader.java- 自定义 DocumentReader
关键代码示例:
java
public class CustomBeanDefinitionDocumentReader extends DefaultBeanDefinitionDocumentReader {
@Override
protected void preProcessXml(Element root) {
int beanCount = getRegistry().getBeanDefinitionCount();
System.out.println("preProcessXml: Bean数量(处理前): " + beanCount);
}
@Override
protected void postProcessXml(Element root) {
int beanCount = getRegistry().getBeanDefinitionCount();
System.out.println("postProcessXml: Bean数量(处理后): " + beanCount);
}
}
6. fireComponentRegistered 示例
配置文件位置 :code/spring-basic/src/main/resources/applicationContext-component.xml
测试类位置 :code/spring-basic/src/test/java/com/example/component/ComponentRegisteredTest.java
相关类:
SimpleComponent.java- 简单组件
关键代码示例:
java
// 在DefaultBeanDefinitionDocumentReader.processBeanDefinition方法中
protected void processBeanDefinition(Element ele, BeanDefinitionParserDelegate delegate) {
BeanDefinitionHolder bdHolder = delegate.parseBeanDefinitionElement(ele);
if (bdHolder != null) {
bdHolder = delegate.decorateBeanDefinitionIfRequired(ele, bdHolder);
BeanDefinitionReaderUtils.registerBeanDefinition(bdHolder, getReaderContext().getRegistry());
// 触发组件注册事件
getReaderContext().fireComponentRegistered(new BeanComponentDefinition(bdHolder));
}
}
总结
本文档总结了在阅读 Spring 源码过程中学习到的六个重要用法知识点:
1. lookup-method 机制
核心价值 :通过 lookup-method 机制,我们可以在单例 Bean 中每次调用都获取新的 Prototype Bean 实例,解决了单例 Bean 依赖 Prototype Bean 的问题。
关键要点:
- 使用
lookup-method标签配置抽象方法 - Spring 通过 CGLIB 动态代理实现方法
- 每次调用都返回新的 Prototype Bean 实例
实际应用:适用于单例 Bean 需要每次调用都获取新实例的场景,如单例服务需要每次处理都创建新的数据访问对象。
2. replaced-method 和 FactoryBean 冲突
核心价值:了解 Spring 的限制和约束,避免配置错误。
关键要点:
replaced-method和FactoryBean不能同时使用- Spring 会在验证阶段检测并抛出异常
- 需要根据需求选择使用哪种机制
实际应用:在配置 Spring Bean 时,需要注意这些限制,避免配置错误。
3. Prototype Bean 循环依赖
核心价值:理解 Spring 循环依赖的解决机制,了解单例和 Prototype 的区别。
关键要点:
- Spring 通过三级缓存解决单例 Bean 的循环依赖
- Prototype Bean 不会放入缓存,无法解决循环依赖
- 避免在 Prototype Bean 之间形成循环依赖
实际应用:在设计 Bean 依赖关系时,需要注意 Prototype Bean 的限制。
4. 别名循环依赖
核心价值:了解 Spring 别名注册机制,避免配置错误。
关键要点:
- Spring 在注册别名时会检测循环依赖
- 如果检测到循环,会抛出异常
- 别名最终都指向同一个 Bean 定义
实际应用:在配置别名时,需要注意避免形成循环依赖。
5. preProcessXml 和 postProcessXml
核心价值:通过重写这两个方法,可以观察 Bean 定义加载过程,添加自定义处理逻辑。
关键要点:
preProcessXml在解析 Bean 定义之前调用postProcessXml在解析 Bean 定义之后调用- 可以通过这两个方法观察 Bean 数量变化
实际应用:适用于调试、监控和扩展 Spring 功能的场景。
6. fireComponentRegistered
核心价值:了解 Spring 内部的事件通知机制,理解组件注册过程。
关键要点:
- 在注册
BeanComponentDefinition时调用 - 触发
ComponentRegisteredEvent事件 - 主要用于 Spring 内部的事件通知
实际应用:在自定义命名空间处理器或扩展 Spring 功能时,可以监听这个事件。
学习心得
通过阅读 Spring 源码,我们不仅了解了这些功能的实现原理,更重要的是学会了如何在实际项目中正确使用这些特性。这些知识点虽然看似简单,但在实际开发中却非常实用,能够帮助我们更好地管理和组织 Spring 应用的配置。
参考资料
代码仓库
- Gitee 仓库 :查看完整代码
- lookup-method 示例代码:
code/spring-basic/src/main/java/com/example/lookupmethod/ - replaced-method 示例代码:
code/spring-basic/src/main/java/com/example/overridemethod/ - 循环依赖示例代码:
code/spring-basic/src/main/java/com/example/circulardependency/ - 别名循环依赖示例代码:
code/spring-basic/src/test/java/com/example/alias/ - preProcessXml/postProcessXml 示例代码:
code/spring-basic/src/main/java/com/example/prepostprocess/ - fireComponentRegistered 示例代码:
code/spring-basic/src/main/java/com/example/component/ - 配置文件:
code/spring-basic/src/main/resources/ - 测试代码:
code/spring-basic/src/test/java/com/example/
- lookup-method 示例代码:
Spring Framework 源码
- Spring Framework 源码相关类和方法
- 之前完成的相关文章