在使用Spring时,可能会遇到这种情况:一个单例的Bean依赖另一个非单例的Bean。如果简单的使用自动装配来注入依赖,就可能会出现一些问题,如下所示:
单例的Class A
@Componentpublic class ClassA { @Autowired private ClassB classB; public void printClass() { System.out.println("This is Class A: " + this); classB.printClass(); }}
非单例的Class B
@Component@Scope(value = SCOPE_PROTOTYPE)public class ClassB { public void printClass() { System.out.println("This is Class B: " + this); }}
这里Class A
采用了默认的单例scope,并依赖于Class B
, 而Class B
的scope是prototype
,因此不是单例的,这时候跑个测试就看出这样写的问题:
@RunWith(SpringRunner.class)@ContextConfiguration(classes = {ClassA.class, ClassB.class})public class MyTest { @Autowired private ClassA classA; @Test public void simpleTest() { for (int i = 0; i < 3; i++) { classA.printClass(); } }}
输出的结果是:
This is Class A: ClassA@282003e1This is Class B: ClassB@7fad8c79This is Class A: ClassA@282003e1This is Class B: ClassB@7fad8c79This is Class A: ClassA@282003e1This is Class B: ClassB@7fad8c79
可以看到,两个类的Hash Code在三次输出中都是一样。Class A
的值不变是可以理解的,因为它是单例的,但是Class B
的scope是prototype
却也保持Hash Code不变,似乎也成了单例?
产生这种的情况的原因是,Class A
的scope是默认的singleton
,因此Context
只会创建Class A
的bean一次,所以也就只有一次注入依赖的机会,容器也就无法每次给Class A
提供一个新的Class B
。
不那么好的解决方案
要解决上述问题,可以对Class A
做一些修改,让它实现ApplicationContextAware
。
@Componentpublic class ClassA implements ApplicationContextAware { private ApplicationContext applicationContext; public void printClass() { System.out.println("This is Class A: " + this); getClassB().printClass(); } public ClassB getClassB() { return applicationContext.getBean(ClassB.class); } public void setApplicationContext(ApplicationContext applicationContext) throws BeansException { this.applicationContext = applicationContext; }}
这样就能够在每次需要到Class B
的时候手动去Context
里找到新的bean。再跑一次测试后得到了以下输出:
This is Class A: com.devhao.ClassA@4df828d7This is Class B: com.devhao.ClassB@31206bebThis is Class A: com.devhao.ClassA@4df828d7This is Class B: com.devhao.ClassB@3e77a1edThis is Class A: com.devhao.ClassA@4df828d7This is Class B: com.devhao.ClassB@3ffcd140
可以看到Class A
的Hash Code
在三次输出中保持不变,而Class B
的却每次都不同,说明问题得到了解决,每次调用时用到的都是新的实例。
但是这样的写法就和Spring强耦合在一起了,Spring提供了另外两种方法来降低侵入性。
@Lookup
Spring提供了一个名为@Lookup
的注解,这是一个作用在方法上的注解,被其标注的方法会被重写,然后根据其返回值的类型,容器调用BeanFactory
的getBean()
方法来返回一个bean。
@Componentpublic class ClassA { public void printClass() { System.out.println("This is Class A: " + this); getClassB().printClass(); } @Lookup public ClassB getClassB() { return null; }}
可以发现简洁了很多,而且不再和Spring强耦合,再次运行测试依然可以得到正确的输出。
被标注的方法的返回值不再重要,因为容器会动态生成一个子类然后将这个被注解的方法重写/实现,最终调用的是子类的方法。使用的@Lookup
的方法需要符合如下的签名:
[abstract] theMethodName(no-arguments);
作用域代理
Spring还提供了另外一种方法来解决这个问题。简单来说就是如果一个bean A
对另外一个作用域更短的bean B
有依赖,那么在实例化bean A
并注入依赖时,注入的不是bean B
本身,而是一个AOP代理,这个代理可以找到实际的bean
。
@Componentpublic class ClassA { @Autowired private ClassB classB; public void printClass() { System.out.println("This is Class A: " + this); classB.printClass(); }}
@Component@Scope(value = SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)public class ClassB { public void printClass() { System.out.println("This is Class B: " + this); }}
可以看出,使用这种方法的好处是仅需对bean B
进行简单的配置,并且bean A
根本不用意识到代理的存在,将bean B
当做一个正常的bean
来装载就好。