Spring Lookup注解

前言

今天我们看下Spring的Lookup注解,这个注解可以使Spring替换一个bean原有的,获取其它对象具体的方法,并自动返回在容器中的查找结果。

正文

在了解它之前,我们先来看下一个例子。

我们有一个Bean,TestClassB,它是多例的。大致如下:

1
2
3
4
5
6
7
@Component
@Scope(value = SCOPE_PROTOTYPE)
public class TestClassB {
public void printClass() {
System.out.println("This is TestClass B: " + this);
}
}

现在一个单例Bean,TestClassA,使用到了TestClassB,代码大致如下:

1
2
3
4
5
6
7
8
9
10
@Component
public class TestClassA {
@Autowired
private TestClassB testClassB;

public void printClass() {
System.out.println("This is TestClass A: " + this);
testClassB.printClass();
}
}

我们进行下测试,可以发现一些问题。

1
2
3
4
5
6
7
8
9
10
11
12
@RunWith(SpringRunner.class)
@SpringBootTest
public class LookUpTests {
@Autowired
private TestClassA testClassA;
@Test
public void test(){
for (int i=0;i<5;i++){
testClassA.printClass();
}
}
}

可以看到输出结果:

1
2
3
4
5
6
7
8
9
10
This is TestClass A: com.zwt.demo.util.TestClassA@2c768ada
This is TestClass B: com.zwt.demo.util.TestClassB@c1fca2a
This is TestClass A: com.zwt.demo.util.TestClassA@2c768ada
This is TestClass B: com.zwt.demo.util.TestClassB@c1fca2a
This is TestClass A: com.zwt.demo.util.TestClassA@2c768ada
This is TestClass B: com.zwt.demo.util.TestClassB@c1fca2a
This is TestClass A: com.zwt.demo.util.TestClassA@2c768ada
This is TestClass B: com.zwt.demo.util.TestClassB@c1fca2a
This is TestClass A: com.zwt.demo.util.TestClassA@2c768ada
This is TestClass B: com.zwt.demo.util.TestClassB@c1fca2a

对于TestClassA,它因为是单例,所以一直是一个实例,我们是可以理解的,但是对于TestClassB,我们明明设置了多例,但是我们发现它仍是一个实例,相当于单例。

对于这种情况的产生,很好理解,因为TestClassA为单例,因此TestClassB只有一次注入的机会,即在生成单例TestClassA的时候,因此导致了TestClassB的多例不体现,仍相当于个单例模式。

如果我们要求TestClassB必须为多例的,那么上面这种情况是会出现问题的。

如何解决这种问题呢?

其实我们最常用的一种方法是拿到SpringContext,然后手动获取Bean。代码大致如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class SpringUtils implements ApplicationContextAware {
private static ApplicationContext applicationContext;
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
SpringUtils.applicationContext=applicationContext;
}
public static ApplicationContext getApplicationContext(){
return applicationContext;
}
public static <T> T getBean(Class<T> clazz){
return applicationContext.getBean(clazz);
}
}

同时TestClassA里的方法如下:

1
2
3
4
5
6
7
@Component
public class TestClassA {
public void printClass(){
System.out.println("This is TestClass A: " + this);
SpringUtils.getBean(TestClassB.class).printClass();
}
}

可以看到输出结果,TestClassB已经是多例的了。

1
2
3
4
5
6
7
8
9
10
This is TestClass A: com.zwt.demo.util.TestClassA@7c447c76
This is TestClass B: com.zwt.demo.util.TestClassB@1640c151
This is TestClass A: com.zwt.demo.util.TestClassA@7c447c76
This is TestClass B: com.zwt.demo.util.TestClassB@5d5b5fa7
This is TestClass A: com.zwt.demo.util.TestClassA@7c447c76
This is TestClass B: com.zwt.demo.util.TestClassB@2a32fb6
This is TestClass A: com.zwt.demo.util.TestClassA@7c447c76
This is TestClass B: com.zwt.demo.util.TestClassB@6107165
This is TestClass A: com.zwt.demo.util.TestClassA@7c447c76
This is TestClass B: com.zwt.demo.util.TestClassB@164a62bf

当然也可以让TestClassA继承ApplicationContextAware直接拿到ApplicationContext,然后获取TestClassB。

这种方式在项目使用中还是比较多的,也是很方便的。

还有其他方法吗?

下面我们来说另一种方式,当然就是我们今天的主角,Lookup注解。那具体如何使用呢,我们来看下。

1
2
3
4
5
6
7
8
9
10
11
@Component
public class TestClassA {
@Lookup
public TestClassB getTestClassB() {
return null;
}
public void printClass() {
System.out.println("This is TestClass A: " + this);
getTestClassB().printClass();
}
}

我们运行测试类,可以看到输出结果:

1
2
3
4
5
6
7
8
9
10
This is TestClass A: com.zwt.demo.util.TestClassA$$EnhancerBySpringCGLIB$$a967ae38@5a00eb1e
This is TestClass B: com.zwt.demo.util.TestClassB@1aac188d
This is TestClass A: com.zwt.demo.util.TestClassA$$EnhancerBySpringCGLIB$$a967ae38@5a00eb1e
This is TestClass B: com.zwt.demo.util.TestClassB@7026b7ee
This is TestClass A: com.zwt.demo.util.TestClassA$$EnhancerBySpringCGLIB$$a967ae38@5a00eb1e
This is TestClass B: com.zwt.demo.util.TestClassB@2d23faef
This is TestClass A: com.zwt.demo.util.TestClassA$$EnhancerBySpringCGLIB$$a967ae38@5a00eb1e
This is TestClass B: com.zwt.demo.util.TestClassB@7cb8437d
This is TestClass A: com.zwt.demo.util.TestClassA$$EnhancerBySpringCGLIB$$a967ae38@5a00eb1e
This is TestClass B: com.zwt.demo.util.TestClassB@62a4417

发现TestClassB是多例的,可是我们根据上面的代码,感觉TestClassB返回应该不是null么。

当然,这就要来了解下Lookup注解了,我们分析一下它的源码:

1
2
3
4
5
6
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Lookup {
String value() default "";
}

可以看到该注解作用于方法上,有一个参数value,这个值可以指定要look up的Bean的名字。如果不指定,就会默认方法返回的类型寻找Bean并进行Look up。

我们在Spring源码中寻找下该注解 @Lookup,会发现只有一个地方使用到了该注解。

在AutowiredAnnotationBeanPostProcessor类的determineCandidateConstructors方法里。该方法部分内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
public Constructor<?>[] determineCandidateConstructors(Class<?> beanClass, final String beanName)
throws BeanCreationException {

// Let's check for lookup methods here..
if (!this.lookupMethodsChecked.contains(beanName)) {
try {
ReflectionUtils.doWithMethods(beanClass, new ReflectionUtils.MethodCallback() {
@Override
public void doWith(Method method) throws IllegalArgumentException, IllegalAccessException {
Lookup lookup = method.getAnnotation(Lookup.class);
if (lookup != null) {
LookupOverride override = new LookupOverride(method, lookup.value());
try {
RootBeanDefinition mbd = (RootBeanDefinition) beanFactory.getMergedBeanDefinition(beanName);
mbd.getMethodOverrides().addOverride(override);
}
catch (NoSuchBeanDefinitionException ex) {
throw new BeanCreationException(beanName,
"Cannot apply @Lookup to beans without corresponding bean definition");
}
}
}
});
}
catch (IllegalStateException ex) {
throw new BeanCreationException(beanName, "Lookup method resolution failed", ex);
}
catch (NoClassDefFoundError err) {
throw new BeanCreationException(beanName, "Failed to introspect bean class [" + beanClass.getName() +
"] for lookup method metadata: could not find class that it depends on", err);
}
this.lookupMethodsChecked.add(beanName);
}
//部分代码略......

可以看到Spring会首先判断该Bean是否有Lookup注解的方法,现在缓存里看,缓存没有的话会尝试获取方法上的Lookup注解,如果存在,拿到需要重写(覆盖)的方法信息放入LookupOverride,最后为RootBeanDefinition添加LookupOverride的属性。这个方法最终会被AbstractAutowireCapableBeanFactory类中的createBeanInstance方法调用,去生成新的Bean并重写,实现改变Bean的效果。

因此原理大致为:方法执行返回的对象,使用 Spring 内原有的这类对象替换,通过改变方法返回值来动态改变方法。内部实现为使用 cglib 方法,重新生成子类,重写配置的方法和返回对象,达到动态改变的效果。因此Bean的多列特性也被体现了。

总结

通过解决一个单例Bean(无状态Bean)调用多例Bean(有状态Bean)的问题,我们了解了Lookup注解的一些简单用法,对Spring也有了一些深入的认识。




-------------文章结束啦 ~\(≧▽≦)/~ 感谢您的阅读-------------

您的支持就是我创作的动力!

欢迎关注我的其它发布渠道