Spring当中注入原型Bean的方式汇总
1. 了解单例和原型Bean
1.1 什么是单例Bean?什么是原型Bean?
- 1.单例
Bean
,相信各位朋友都不陌生,Spring
当中的Bean
默认就是单例的,也就是无论从什么地方去使用@Autowired
或者@Resource
等方式去进行注入,拿到的都是同一个对象,这个对象维护在Spring容器当中,每次使用都是直接从Spring
容器当中直接进行获取。 - 2.原型
Bean
,也就是说你每次使用到该Bean
,都是Spring
框架它去重新帮你去进行创建的,也就是说你任意的两次获取该Bean
,永远不可能获取到相同的对象。
1.2 如何去定义一个单例Bean?如何去定义一个原型Bean?
使用@Component
、@Bean
、@Configuration
等注解往容器中注册的Bean
,都是单例Bean
,要想实现原型Bean,可以通过@Scope
注解等方式去配置为Bean
的作用域为prototype
。
下面是使用@Component
注解去定义普通的单例Bean
的一种方式
@Component public class User { private int id; public void setId(int id) { this.id = id; } public int getId() { return id; } public void test() { System.out.println(this.hashCode()); } }
下面是去定义一个原型Bean
的方式:
@Component @Scope(value = BeanDefinition.SCOPE_PROTOTYPE) public class User { private int id; public void setId(int id) { this.id = id; } public int getId() { return id; } public void test() { System.out.println(this.hashCode()); } }
1.3 了解Autowired注入原型Bean存在的问题
我们已经使用@Scope
注解去配置该Bean
为原型的,按道理,我们使用如下的代码注入的就是一个原型Bean
。
@Component public class App implements BeanFactoryAware { @Autowired User user; }
按道理,如果我们使用如下的代码对user
对象进行输出时,每次都是得到不同的对象,也就是说它们的hash
值应该是不同的,但是实际上呢?
for (int i = 0; i < 5; i++) { System.out.println(user); }
得到如下的结果,你也许会好奇,不是说是原型Bean
吗,怎么获取到的还是同一个对象呢?
com.wanna.User@49fdbe2b com.wanna.User@49fdbe2b com.wanna.User@49fdbe2b com.wanna.User@49fdbe2b com.wanna.User@49fdbe2b
实际上是因为,在Spring
的IOC
容器的启动过程中,已经将App
这个Bean
完成了初始化操作,在初始化过程中,已经对App
当中的属性值完成了设置,因此App
对象当中的user
对象已经被固定死了,无论我们怎么去获取,都会获取到同一个Bean
。
既然Autowired
注入的是同一个User
,那么,我们有办法去注入一个原型的Bean
吗?肯定是有的!
2. 解决Autowired
无法注入原型Bean的解决方法
2.1 使用@Scope
注解的proxyMode
属性去解决
proxyMode
可以有两种配置方式,TARGET_CLASS
和INTERFACES
,顾名思义,第一种方式代表去代理目标类,也就是使用CGLIB
去完成动态代理;第二种方式代表去代理目标类的所有接口,也就是使用JDK
动态代理去完成代理。
@Scope(value = BeanDefinition.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)
再去执行原来的代码,得到如下的运行结果,我们可以发现,得到的结果已经完全不同了!
com.wanna.User@35cec305 com.wanna.User@237add com.wanna.User@491cafec com.wanna.User@cbd40c1 com.wanna.User@4fa86cb8
如何进行实现的?我们都知道不管是CGLIB
还是JDK
的动态代理,都是拦截目标方法的,为什么我们直接输出就能拿到不同的对象?因为输出流进行输出时调用了toString
方法,调用了这个方法,自然可以触发代理方法的执行,可以打印出来原型的效果。
2.2 使用@Lookup
注解去进行解决
使用@Lookup
注解需要配置在返回类型为具体类型的方法上(一般是配置在getter
上,但是并不是一定的,也可以配置在其它方法上)。
@Lookup
注解如何使用?
- 1.如果不配置
@Lookup
注解的value
属性,那么默认是按照方法的返回类型去返回对象。 - 2.如果配置了
@Lookup
注解的value
属性,那么将会按照beanName
去返回对象。 - 3.因为这个方法并不会被调用到,调用方法的目标逻辑是在拦截目标方法的切面方法当中去进行执行的,因此就算
return null
也不影响对象的获取。 - 4.
@Lookup
注解并不一定要标注在实例方法上,可以标注在抽象方法(包括接口的方法)当中。
@Lookup public User getUser() { return this.user; }
要获取Bean
,就得使用标注了@Lookup
的方法去进行获取对象。
for (int i = 0; i < 5; i++) { System.out.println(getUser()); }
最终得到如下的内容:
com.wanna.User@2eda2062 com.wanna.User@1a9ec80e com.wanna.User@7fd4e815 com.wanna.User@5f6b53ed com.wanna.User@20cdb152
2.3 通过配置TargetSource
去进行配置
TargetSource
,从这个名字当中我们可以知道,它是一个用来获取Target
的Source
,也就是一个用来获取"目标对象"的"源"。对于如何获取Target
的Source
,这当然是可以自定义的,这也是Spring
留给我们的扩展点。
使用TargetSource
需要配置Spring
容器中默认的执行SpringAOP
的Abstract***Creator
组件的相关内容,我们如何去进行配置?我们可以手写一个BeanPostProcessor
去拦截下来Abstract***Creator
的执行,然后去进行配置。
需要注意的是:
- 我们既然想对
Abstract***Creator
进行拦截,那么我们肯定得优先级比它还高从而去保证比这个组件在创建时,Abstract***Creator
这个组件还没完成创建!因此我们这里采用了实现PriorityOrdered
接口,去保证优先级比较高。
我们类似编写如下的一个配置类:
@Configuration public class Config implements BeanPostProcessor, PriorityOrdered, BeanFactoryAware { BeanFactory beanFactory; @Override public void setBeanFactory(BeanFactory beanFactory) throws BeansException { this.beanFactory = beanFactory; } @Override public int getOrder() { return Ordered.HIGHEST_PRECEDENCE; } @Override public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException { AbstractBeanFactoryBasedTargetSource targetSource = new AbstractBeanFactoryBasedTargetSource() { @Override public Object getTarget() throws Exception { return getBeanFactory().getBean(getTargetBeanName()); } }; if (bean instanceof Abstract***Creator) { AbstractBeanFactoryBasedTargetSourceCreator creator = new AbstractBeanFactoryBasedTargetSourceCreator() { @Override protected AbstractBeanFactoryBasedTargetSource createBeanFactoryBasedTargetSource(Class<?> beanClass, String beanName) { if (User.class.isAssignableFrom(beanClass)) { return targetSource; } return null; } }; creator.setBeanFactory(beanFactory); ((Abstract***Creator) bean).setCustomTargetSourceCreators(creator); } return bean; } }
我们主要通过setCustomTargetSourceCreators
方法往Abstract***Creator
组件当中添加一个自定义的TargetSourceCreator
。
这个TargetSourceCreator
组件的作用是根据自定义相关的逻辑,最终返回一个AbstractBeanFactoryBasedTargetSource
类型的TargetSource
对象,它是一个典型的工厂模式!
对于我们想要实现的AbstractBeanFactoryBasedTargetSource
组件,我们只需要实现它的getTarget
方法即可,这个getTarget
方法主要体现之前所讲述的那个词,"目标对象"的"源",“源”是从什么地方体现的呢?就是在这里,我们可以自定义逻辑获取Bean
!
但是这里的getBeanFactory().getBean(getTargetBeanName())
,其实比较有意思,这个getBeanFactory
获取到的并不是我们Spring
的IOC
容器,而是一个克隆出来的新的BeanFactory
。
我们使用如下的代码去进行测试
for (int i = 0; i < 5; i++) { System.out.println(user); }
得到如下的结果:
com.wanna.User@1f536481 com.wanna.User@5234b61a com.wanna.User@22a260ff com.wanna.User@54c425b1 com.wanna.User@50b734c4
其实这个TargetSource
很有意思,我们来看看Spring
当中对于它的实现就知道了!获取目标对象的来源可以是ThreadLocal
,可以是Prototype
,也可以是对象池(XXXPool
),更多的实现我们还可以进行自定义。
也就是说基于TargetSource
可以实现很多好玩的功能,并非是只能被我们去用来获取原型Bean
,获取原型Bean
只是它的其中一个功能,它还有很多功能(比如ThreadLocal
)是值得我们去探索的!
还有个值得注意的问题:
- 就算我们的
Bean
是单例的,如果我们给它配置了AbstractBeanFactoryBasedTargetSource
,那么获取到这个Bean
仍旧是会变成原型的,这是这个组件内部实现的。
2.4 手动getBean
去实现获取原型Bean
既然我们都可以标注了@Lookup
注解了,那么我们当然也可以实现类似的功能对吧!
public User getUser() { return beanFactory.getBean(User.class); }
我们直接实现BeanFactoryAware
或者是ApplicationContextAware
接口去注入BeanFactory
,手动在getter
当中去进行getBean
不就行了?自己手动通过getBean
去获取到的肯定是Spring
帮我们去进行新创建的对象,而且Spring
底层肯定最终也要调用getBean
去从容器中调用。