【Spring源码】从源码倒看Spring用法(二)

从源码倒看Spring用法(二)

代码仓库Gitee 仓库链接

本文档的所有示例代码都可以在代码仓库中找到,建议结合代码一起阅读。

回顾学习历程

上一篇文章中,我们总结了四个重要的 Spring 用法知识点:

  1. Spring Profile 环境隔离
  2. Spring FactoryBean
  3. Spring 抽象 Bean
  4. 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-methodFactoryBean 不能同时使用
  • 如果需要方法替换,应该直接配置目标 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 数量变化

概念

preProcessXmlpostProcessXmlDefaultBeanDefinitionDocumentReader 中的两个钩子方法,分别在解析 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);
    }
}
核心机制
  • preProcessXmldoRegisterBeanDefinitions 方法中,解析 Bean 定义之前调用
  • postProcessXmldoRegisterBeanDefinitions 方法中,解析 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 - 原型 Bean
  • SingletonService.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 的 Service
  • ServiceFactoryBean.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 - 依赖 ServiceB
  • ServiceB.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-methodFactoryBean 不能同时使用
  • 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/

Spring Framework 源码

  • Spring Framework 源码相关类和方法
  • 之前完成的相关文章

相关文档

相关推荐
xdpcxq10292 小时前
风控场景下超高并发频次计算服务
java·服务器·网络
想用offer打牌2 小时前
你真的懂Thread.currentThread().interrupt()吗?
java·后端·架构
橘色的狸花猫2 小时前
简历与岗位要求相似度分析系统
java·nlp
独自破碎E2 小时前
Leetcode1438绝对值不超过限制的最长连续子数组
java·开发语言·算法
用户91743965393 小时前
Elasticsearch Percolate Query使用优化案例-从2000到500ms
java·大数据·elasticsearch
程序员NEO3 小时前
LangChain4j 工具调用实战
后端
计算机毕设VX:Fegn08953 小时前
计算机毕业设计|基于springboot + vue小区人脸识别门禁系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
yaoxin5211233 小时前
279. Java Stream API - Stream 拼接的两种方式:concat() vs flatMap()
java·开发语言
坚持学习前端日记3 小时前
2025年的个人和学习年度总结以及未来期望
java·学习·程序人生·职场和发展·创业创新