啥?Spring竟然可以创建“重复”的Bean?

743 篇文章 3 订阅
742 篇文章 2 订阅

一、项目中存在了名称重复的bean

众所周知,在Spring中时不能够创建两个名称相同的bean的,否则会在启动时报错:

image-20230322100944642

但是却在我们的spring项目中发现了两个相同名称的bean,并且项目也可以正常启动,对应的bean也可以正常使用。

因为项目原因中会用到多个redis集群,所以有配置了多个redis环境,并且在id上做了区分。

但是在配置redis环境的时候,两个环境beanid却是相同的。

 

xml

复制代码

<bean id="cacheClusterConfigProvider" class="com.xxx.rediscluster.provider.CacheClusterConfigProvider">    <property name="providers">        <list>           //创建了一个名为 ccProvider 的bean            <bean id="ccProvider" class="com.xxx.rediscluster.provider.CCProvider">                <!--# 替换为当前环境的R2M 3C配置中心地址(详见上方R2M 3C服务地址)-->                <property name="address" value="${r2m.zkConnection}"/>                <!--# 替换为R2M集群名-->                <property name="appName" value="${r2m.appName}"/>                <!--# 替换为当前环境的客户端对应配置中心token口令(参考上方token获取方式)-->                <property name="token" value="${r2m.token}"/>                <!--# 替换为集群认证密码-->                <property name="password" value="${r2m.password}"/>            </bean>        </list>    </property> </bean> ​ <bean id="tjCacheClusterConfigProvider" class="com.xxx.rediscluster.provider.CacheClusterConfigProvider">    <property name="providers">        <list>           //这里竟然也是 ccProvider            <bean id="ccProvider" class="com.xxx.rediscluster.provider.CCProvider">                <!--# 替换为当前环境的R2M 3C配置中心地址(详见上方R2M 3C服务地址)-->                <property name="address" value="${r2m.tj.zkConnection}"/>                <!--# 替换为R2M集群名-->                <property name="appName" value="${r2m.tj.appName}"/>                <!--# 替换为当前环境的客户端对应配置中心token口令(参考上方token获取方式)-->                <property name="token" value="${r2m.tj.token}"/>                <!--# 替换为集群认证密码-->                <property name="password" value="${r2m.tj.password}"/>            </bean>        </list>    </property> </bean> ​ <bean ref="ccProvider">

大家也都知道,<bean>标签可以声明一个bean,是肯定会被spring解析并且使用的,那么为什么在这里面两个相同的bean名称却不会报错呢?

image-20230322103204708

可以看到我们创建的bean是正常的,并且从功能上来说也是可以使用的。

二、问题的排查过程

2.1 尝试直接找到创建重复bean位置

首先debug尝试找到创建重复bean时的相关信息,看看有没有什么思路

image-20230322103624912

然后重启项目,选择debug模式,但是在运行之后IDEA提示断点被跳过了

image-20230322104033613

查阅了一些资料跟方式都不起作用,遂放弃此思路。

2.2 从创建其父bean开始寻找思路

放弃了上述思路后想到,可以凭借之前学习的spring源码从代码层面去排查此问题

将断点设置到创建reids bean处

image-20230322104714244

果然,断点在这里是能进来的

image-20230322104804469

那么我们的思路就很简单了。

在spring中,装配属性的步骤发生在:populateBean(beanName, mbd, instanceWrapper)的过程中,如果发现其属性也是一个bean,那么会先获取bean,如果不存在则会先创建其属性bean,然后创建完成之后将属性bean赋值给要装配的bean。

 

ini

复制代码

//循环要装配bean的所有属性 for (PropertyValue pv : original) {   if (pv.isConverted()) {      deepCopy.add(pv);   }   else {      String propertyName = pv.getName();      Object originalValue = pv.getValue();      //获取真正要装配的bean      Object resolvedValue = valueResolver.resolveValueIfNecessary(pv, originalValue);      Object convertedValue = resolvedValue;      boolean convertible = bw.isWritableProperty(propertyName) &&            !PropertyAccessorUtils.isNestedOrIndexedProperty(propertyName);   } }

从debug中也可以看出,我们bean的属性只有一个,也就是 providers,符合我们在上面xml中配置的属性

image-20230322105338830

我们从真正创建要装配的bean的地方开始找找什么时候开始创建bean的

 

typescript

复制代码

private Object resolveInnerBean(Object argName, String innerBeanName, BeanDefinition innerBd) {   RootBeanDefinition mbd = null;   try {     ...      // 真正创建bean的地方      Object innerBean = this.beanFactory.createBean(actualInnerBeanName, mbd, null);      if (innerBean instanceof FactoryBean) {         boolean synthetic = mbd.isSynthetic();         return this.beanFactory.getObjectFromFactoryBean(               (FactoryBean<?>) innerBean, actualInnerBeanName, !synthetic);     }      else {         return innerBean;     }   }   catch (BeansException ex) {      throw new BeanCreationException(            this.beanDefinition.getResourceDescription(), this.beanName,            "Cannot create inner bean '" + innerBeanName + "' " +           (mbd != null && mbd.getBeanClassName() != null ? "of type [" + mbd.getBeanClassName() + "] " : "") +            "while setting " + argName, ex);   } }

createBean(actualInnerBeanName, mbd, null)这行代码如果有小伙伴阅读过spring源码一定不陌生,通过这个方法可以获得要创建的bean对象。

image-20230322152949119

从debug中也可以看到真正要创建的beanName已经换成了我们的想要装配的属性 ccProvider

至此我们已经发现了,和我们的预期一致,<bean>标签无论在什么位置确实会创建一个bean对象。

那么为什么这里的beanName不怕重复呢?

2.3 为什么这里的bean不会出现重复的问题

回顾刚刚之前提到的spring不允许重复名称的bean,其实很好理解,因为我们在创建bean的过程中,会将创建好的bean以beanName为key放到缓存的map中,如果我们有两个相同名称的bean,那么当存在重复的bean时,第二个bean会将第一个bean给覆盖掉。

这样的话,就不存在唯一性了,别的bean需要依赖重复的bean的时候有可能返回的并不是同一个bean。

那么为什么这里两个bean并不会重复呢?

其实细心的读者已经发现了,这里变量名称是 innerBean,说明他是一个内部bean,那么 innerBean与普通的 bean有什么不同呢?为什么 innerBean并不会产生 名称重复的问题呢?

我们重新梳理下创建普通 bean的流程:

innerbean.drawio

其实答案已经很明显了:

如果我们创建的是一个普通bean,在创建完成之后会将bean放置到缓存中,如果有其他bean要使用直接从缓存中取走就可以了,而beanName不能重复也是基于此考虑。

而创建innerBean则基于createBean()原子性操作前提,只会返回创建好的bean,并不会将其加入到spring的bean缓存中,因此也就不存在beanName重复的问题了

三、总结

3.1 为什么spring可以存在”重复“名称的bean

我们这里重新梳理下bean的创建流程:

在spring注入一个普通bean的过程中,会将通过反射创建的空属性对象赋值,如果发现其依赖的属性也是一个bean,那么会首先去获取这个bean,如果获取不到的话则会转而去创建bean。

而此时要创建的bean成为innerBean,并不会被spring其他bean共享,所以可以在名称上是重复的。

3.2 innerBean的用法

还是我们刚刚的例子,我们可以将其改写成下面的这个样子:

 

xml

复制代码

<bean id="cacheClusterConfigProvider" class="com.wangyin.rediscluster.provider.CacheClusterConfigProvider">    <property name="providers">        <list>            <!--# 引用ccProviderRef-->            <ref bean="ccProviderRef"></ref>        </list>    </property> </bean> ​ <!--# 定义了一个公共的ccProviderRef--> <bean id="ccProviderRef" class="com.wangyin.rediscluster.provider.CCProvider">    <!--# 替换为当前环境的R2M 3C配置中心地址(详见上方R2M 3C服务地址)-->    <property name="address" value="${r2m.zkConnection}"/>    <!--# 替换为R2M集群名-->    <property name="appName" value="${r2m.appName}"/>    <!--# 替换为当前环境的客户端对应配置中心token口令(参考上方token获取方式)-->    <property name="token" value="${r2m.token}"/>    <!--# 替换为集群认证密码-->    <property name="password" value="${r2m.password}"/> </bean> ​ <bean id="ccProviderRef" class="com.wangyin.rediscluster.provider.CCProvider">    <!--# 替换为当前环境的R2M 3C配置中心地址(详见上方R2M 3C服务地址)-->    <property name="address" value="${r2m.zkConnection}"/>    <!--# 替换为R2M集群名-->    <property name="appName" value="${r2m.appName}"/>    <!--# 替换为当前环境的客户端对应配置中心token口令(参考上方token获取方式)-->    <property name="token" value="${r2m.token}"/>    <!--# 替换为集群认证密码-->    <property name="password" value="${r2m.password}"/> </bean>

在上面的例子中我们定义了一个普通bean,并将其引用到我们想要的属性中。

此时 ccProviderRef作为一个普通bean,是可以被其他bean引用的,但是此时bean的名称就不可重复。

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
spring 的优点? 1.降低了组件之间的耦合性 ,实现了软件各层之间的解耦 2.可以使用容易提供的众多服务,如事务管理,消息服务等 3.容器提供单例模式支持 4.容器提供了AOP技术,利用它很容易实现如权限拦截,运行期监控等功能 5.容器提供了众多的辅助类,能加快应用的开发 6.spring对于主流的应用框架提供了集成支持,如hibernate,JPA,Struts等 7.spring属于低侵入式设计,代码的污染极低 8.独立于各种应用服务器 9.spring的DI机制降低了业务对象替换的复杂性 10.Spring的高度开放性,并不强制应用完全依赖于Spring,开发者可以自由选择spring的部分或全部 什么是DI机制? 依赖注入(Dependecy Injection)和控制反转(Inversion of Control)是同一个概念,具体的讲:当某个角色 需要另外一个角色协助的时候,在传统的程序设计过程中,通常由调用者来创建被调用者的实例。但在spring创建被调用者的工作不再由调用者来完成,因此称为控制反转。创建被调用者的工作由spring来完成,然后注入调用者 因此也称为依赖注入。 spring以动态灵活的方式来管理对象 , 注入的两种方式,设置注入和构造注入。 设置注入的优点:直观,自然 构造注入的优点:可以在构造器中决定依赖关系的顺序。 什么是AOP? 面向切面编程(AOP)完善spring的依赖注入(DI),面向切面编程在spring中主要表现为两个方面 1.面向切面编程提供声明式事务管理 2.spring支持用户自定义的切面 面向切面编程(aop)是对面向对象编程(oop)的补充, 面向对象编程将程序分解成各个层次的对象,面向切面编程将程序运行过程分解成各个切面。 AOP从程序运行角度考虑程序的结构,提取业务处理过程的切面,oop是静态的抽象,aop是动态的抽象, 是对应用执行过程中的步骤进行抽象,,从而获得步骤之间的逻辑划分。 aop框架具有的两个特征: 1.各个步骤之间的良好隔离性 2.源代码无关性 Hibernate工作原理及为什么要用? 原理: 1.读取并解析配置文件 2.读取并解析映射信息,创建SessionFactory 3.打开Sesssion 4.创建事务Transation 5.持久化操作 6.提交事务 7.关闭Session 8.关闭SesstionFactory 为什么要用: 1. 对JDBC访问数据库的代码做了封装,大大简化了数据访问层繁琐的重复性代码。 2. Hibernate是一个基于JDBC的主流持久化框架,是一个优秀的ORM实现。他很大程度的简化DAO层的编码工作 3. hibernate使用Java反射机制,而不是字节码增强程序来实现透明性。 4. hibernate的性能非常好,因为它是个轻量级框架。映射的灵活性很出色。它支持各种关系数据库,从一对一到多对多的各种复杂关系。 2. Hibernate是如何延迟加载? 1. Hibernate2延迟加载实现:a)实体对象 b)集合(Collection) 2. Hibernate3 提供了属性的延迟加载功能 当Hibernate在查询数据的时候,数据并没有存在与内存中,当程序真正对数据的操作时,对象才存在与内存中,就实现了延迟加载,他节省了服务器的内存开销,从而提高了服务器的性能。 3.Hibernate中怎样实现类之间的关系?(如:一对多、多对多的关系) 类与类之间的关系主要体现在表与表之间的关系进行操作,它们都市对对象进行操作,我们程序中把所有的表与类都映射在一起,它们通过配置文件中的many-to-one、one-to-many、many-to-many、 4. 说下Hibernate的缓存机制 1. 内部缓存存在Hibernate中又叫一级缓存,属于应用事物级缓存 2. 二级缓存: a) 应用及缓存 b) 分布式缓存 条件:数据不会被第三方修改、数据大小在可接受范围、数据更新频率低、同一数据被系统频繁使用、非 关键数据 c) 第三方缓存的实现 5. Hibernate的查询方式 Sql、Criteria,object comptosition Hql: 1、 属性查询 2、 参数查询、命名参数查询 3、 关联查询 4、 分页查询 5、 统计函数 6. 如何优化Hibernate? 1.使用双向一对多关联,不使用单向一对多 2.灵活使用单向一对多关联 3.不用一对一,用多对一取代 4.配置对象缓存,不使用集合缓存 5.一对多集合使用Bag,多对多集合使用Set 6. 继承类使用显式多态 7. 表字段要少,表关联不要
Spring IOC 控制反转:把创建对象的权利交给Spring 创建对象 1.无参构造<bean class=""> 2.静态工厂<bean class="" factory-method=""> 3.实例工厂 <bean bean-factory="" factory-method=""> 管理对象 对象关系DI 构造器注入<construct-arg> set注入<property> 生命周期 scope:prototype/singleton init-method destroy-method API BeanFactory:使用这个工厂创建对象的方式都是懒加载,在调用的时候再创建 ClassPathXmlApplicationContext:使用这个工厂创建对象,他会根据scope智能判断是否懒加载,如果是单例则创建容器时就会创建里面bean的实例,如果是多例在获取使用时才会创建bean实例 FileSystemXmlApplicationContext磁盘路径 AnnotationConfigApplicationContext注解 WebApplicationContext:web环境使用的容器 注解 创建对象 Component:不分层的注解 Controller:web层 Service:service层 Repository:dao层 管理对象 注入 AutoWired Qualifier Resource Value 声明周期 Scope PostConstruct PreDestroy 新注解 Bean:写方法上,将方法的返回值 Configuration:标记配置类 ComponentScan包扫描 PropertySource:加载配置文件 Import:导入其他配置类 AOP 概念:面向切面编程,在不改变源码的情况下对方法进行增强,抽取横切关注点(日志处理,事务管理,安全检查,性能测试等等),使用AOP进行增强,使程序员只需要关注与业务逻辑编写. 专业术语 目标Target:需要增强的类 连接点JoinPoint:目标中可被增强的方法 切入点PointCut:被增强的方法 增强Advice:增强代码 切面Aspect:切点加通知 织入weaving:讲切面加载进内存形成代理对象的过程 代理Proxy 底层实现 JDK动态代理(默认) 基于接口:代理对象与目标对象是兄弟关系,目标类必须实现接口 CGLIB动态代理 基于父类:代理对象与目标对象是父子关系.目标不能被final修饰 修改默认代理方法:<aop:aspectj-autoproxy proxy-target-class="true"/> 增强种类 前置通知 后置通知 异常通知 最终通知 环绕通知 注意:使用注解的方式,最终通知和后置通知顺序换了,建议使用环绕通知 注解 配置 声明式事务管理 PlatFormTransactionManager:平台事务管理器:定义了commit/rollback Mybatis/jdbc:DataSourceTransactionManager Hibernater:HibernaterTransactionManager TransactionManagerDifinition 传播行为:A-->B,在B上声明是否一定需要事务管理 requerd:必须的(默认),如果A有事务那么就加入A的事务,如果A没有事务那么单独创建一个事务 supports,如果A有事务则加入,如果没有就算了 隔离级别 default:使用数据库默认的隔离级别(mysql:可重复读,oracle:读已提交) readuncommited:读未提交,不可以解决任何问题 readcommited:读已提交,可以解决脏读问题 repeatableRead:可重复读,可以解决脏读,不可重复读问题 Serializbler:串行化,可以解决所有问题 超时时间: 默认-1(永不超时),事务一直不提交也不回滚的时间 是否只读: 默认false TransactionManagerStatus: 事务的一些状态 整合 Spring整合Junit 1.导入依赖spring-test 2.加注解:RunWith、ContextConfiguration 3.注入对象进行测试 Spring整合web 1.导入依赖spring-web 2.配置ContextLoadListener 3.配置 <!--全局初始化参数--> <context-param> <param-name>contextConfigLocation</param-name> <param-value>classpath:applicationContext.xml</param-value> </context-param> 4.在Servlet中使用WebApplicationContextUtils获取容器对象 5.使用容器对象去获取Service对象

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值