@Lazy注解加在类上、字段上有什么作用?如何正确理解@Lazy注解?
结论
当将@Lazy注解加在字段时,Spring应用上下文会为目标类型创建一个代理对象,
Talk is cheap. Show me the code
第一步:编写一个类交由IoC容器管理。
package com.xxx.hyl.lazy;
import org.springframework.context.annotation.Lazy;
/**
* 演示当前Bean 被延迟加载,需注意的是必须在当前类上添加{@link Lazy}注解,否则当前类在IoC容器初始化的时候就会被实例化
*
* @author 君战 *
*/
@Lazy
public class ComponentBean {
public ComponentBean() {
System.out.println(this.getClass().getSimpleName() + "初始化");
}
public void say() {
System.out.println(this.getClass().getName());
}
}
第二步:编写一个类,通过字段的方式注入上一步编写类的实例。
package com.xxx.hyl.lazy;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Lazy;
public class LazyBean {
@Autowired @Lazy private ComponentBean componentBean;
public void testLazy() {
System.out.println("===========开始执行componentBean的say方法=========");
componentBean.say();
}
}
第三步:编写启动类
package com.xxx.hyl.lazy;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Lazy;
import java.io.IOException;
/**
* 演示{@link Lazy} 注解添加到字段上的作用
*
* @author 君战
* <p>*
*/
public class LazyAnnotationDemo {
public static void main(String[] args) throws IOException {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
context.register(ComponentBean.class, LazyBean.class);
context.refresh();
System.out.println("============IoC容器初始化完毕==============");
context.getBean(LazyBean.class).testLazy();
}
}
第四步:查看控制台
LazyBean初始化
============IoC容器初始化完毕==============
========开始执行componentBean的say方法=========
ComponentBean初始化
com.xxx.hyl.lazy.ComponentBean
Process finished with exit code 0
可以看到ComponentBean是在执行LazyBeab的testLazy方法时才会被初始化。接下来我们调整下代码,将@Lazy注解从ComponentBean类上去掉,再看下控制台的打印结果。
ComponentBean初始化
LazyBean初始化
============IoC容器初始化完毕==============
===========开始执行componentBean的say方法=========
com.xxx.hyl.lazy.ComponentBean
Process finished with exit code 0
ComponentBean在IoC容器初始化的时候就已经实例化。那么我们再调整下代码,ComponentBean类上保留@Lazy注解。
LazyBean初始化
ComponentBean初始化
============IoC容器初始化完毕==============
===========开始执行componentBean的say方法=========
com.xxx.hyl.lazy.ComponentBean
Process finished with exit code 0
这种情况是正确的,因为虽然在ComponentBean上添加了@Lazy注解,IoC容器不会在初始化的时候就去实例化ComponentBean,但是由于在LazyBean(该Bean并未懒加载,因此在IoC容器初始化的时候就会实例化)中依赖了ComponentBean,因此IoC容器不得不处理该依赖,从而触发ComponentBean的实例化。这点可以拿之前的控制台输出来进行比对,就可以发现端倪。
小结
如果只在某个类上添加@Lazy注解,如果工程中有其它地方依赖了该类,那么即使添加了@Lazy注解,也依然会在IoC容器初始化的时候就去实例化该类。如果想在使用的时候才去实例化,可以在每个依赖该类的地方上添加@Lazy注解,具体原理我们接下来就开始分析。
@Lazy注解添加到字段底层处理逻辑分析
IoC容器中由谁来解析@Lazy注解
和@Qualifier注解一样,IoC容器并不会提前解析好@Lazy注解,而是在处理Bean中添加@Au-towired或@Resource或JSR-330规范中规定的依赖注入注解的字段/方法时才会去解析。和处理@Qualifier注解一样,都是由ContextAnnotationAutowireCandidateResolver来解析。
但@Qualifier注解和@Value注解是由其父类QualifierAnnotationAutowireCandidateResolver来解析,只有@Lazy注解由ContextAnnotationAutowireCandidateResolver自己来解析。关于这一点我们也可以在这两个类的定义中看到。
@Lazy注解如何解析?
接下来就开始分析ContextAnnotationAutowireCandidateResolver的getLazyResolutionIfNeces-sary的方法,可以看到其实现是很简单,就是通过isLazy方法来判断当前依赖项是否是一个懒加载的,如果该方法返回true,则调用buildLazyResolutionProxy方法,否则直接返回null。
// ContextAnnotationAutowireCandidateResolver#getLazyResolutionProxyIfNecessary
public Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor, @Nullable String beanName) {
return (isLazy(descriptor) ? buildLazyResolutionProxy(descriptor, beanName) : null);
}
接下来分析下isLazy方法的实现,首先获取传入依赖描述符中的所有注解,然后遍历这些注解信息,通过AnnotationUtils的getAnnotation方法来尝试从当前注解中获取@Lazy注解数据,如果获取的数据不为null,再判断其value属性是否为true,该属性默认为true。
如果判断成立,直接返回true。如果判断不成立,接下来从方法上查找@Lazy注解数据。这里很有意思的一点是首先是从依赖描述符中获取方法参数,如果方法参数不为null,才会去查找方法上的@Lazy注解数据。这意味着如果我们在一个没有任何参数的方法上添加@Lazy注解是无效的。
// ContextAnnotationAutowireCandidateResolver#isLazy
protected boolean isLazy(DependencyDescriptor descriptor) {
for (Annotation ann : descriptor.getAnnotations()) {
// 查找依赖描述符中@Lazy注解->该依赖描述符是字段
Lazy lazy = AnnotationUtils.getAnnotation(ann, Lazy.class);
if (lazy != null && lazy.value()) {
return true;
}
}
// 查找依赖描述符中@Lazy注解->该依赖描述符是方法
MethodParameter methodParam = descriptor.getMethodParameter();
if (methodParam != null) {
Method method = methodParam.getMethod();
if (method == null || void.class == method.getReturnType()) {
Lazy lazy = AnnotationUtils.getAnnotation(methodParam.getAnnotatedElement(), Lazy.class);
if (lazy != null && lazy.value()) {
return true;
}
}
}
return false;
}
buildLazyResolutionProxy方法更简单,直接创建了TargetSource对象,该对象持有了依赖描述符以及beanName和DefaultListableBeanFactory引用,然后通过ProxyFactory来为该对象创建一个代理对象,返回该代理对象。
这里提一点题外话,ProxyFactory是Spring Framework中用来创建代理对象的一个工厂类,大名鼎鼎的Spring AOP也是使用该类来创建代理对象。需注意的这不是一个线程安全的类,因此不能将其作为实例变量或静态变量。
另外也可以看到在getTarget方法中,是直接调用DefaultListableBeanFactory的doResolveDep-endency方法来解析依赖,而不是调用更上层的resolveDependency方法来解析依赖,这是因为对@Lazy注解的解析就是在resolveDependency方法中完成,如果在这里还是调用resolveDependency方法,那么将会陷入死循环。
// ContextAnnotationAutowireCandidateResolver#buildLazyResolutionProxy
protected Object buildLazyResolutionProxy(final DependencyDescriptor descriptor, final @Nullable String beanName) {
Assert.state(getBeanFactory() instanceof DefaultListableBeanFactory,
"BeanFactory needs to be a DefaultListableBeanFactory");
final DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) getBeanFactory();
TargetSource ts = new TargetSource() {
@Override
public Class<?> getTargetClass() {
return descriptor.getDependencyType();
}
@Override
public boolean isStatic() {
return false;
}
@Override
public Object getTarget() {
Object target = beanFactory.doResolveDependency(descriptor, beanName, null, null);
if (target == null) {
Class<?> type = getTargetClass();
if (Map.class == type) {
return Collections.emptyMap();
} else if (List.class == type) {
return Collections.emptyList();
} else if (Set.class == type || Collection.class == type) {
return Collections.emptySet();
}
throw new NoSuchBeanDefinitionException(descriptor.getResolvableType(),
"Optional dependency not present for lazy injection point");
}
return target;
}
@Override
public void releaseTarget(Object target) {
}
};
ProxyFactory pf = new ProxyFactory();
pf.setTargetSource(ts);
Class<?> dependencyType = descriptor.getDependencyType();
if (dependencyType.isInterface()) {
pf.addInterface(dependencyType);
}
return pf.getProxy(beanFactory.getBeanClassLoader());
}
经过以上处理后,当Bean的依赖注入完成后,其属性注入的实际上是一个使用JDK动态代理或者CGLIB创建出来的代理对象,只有用户去调用目标对象的方法时,才会触发去真正完成依赖解析。
Debug验证
关于代理对象这一点,我们可以通过Debug方式来验证,首先在testLazy方法中打上断点,然后在ContextAannotationAutowireCandidateResolver的buildLazyResolutionProxy方法中打上断点,启动应用程序。
总结
当将@Lazy注解添加到字段或者方法上的参数上,IoC容器将会为其创建代理对象,如果在Bean上面添加@Lazy注解,并且在每个依赖该Bean的地方都添加上@Lazy注解,那么该Bean不会在IoC容器初始化的时候就进行实例化,只有到调用该Bean的方法时,IoC容器才会真正的去实例化Bean。
如果仅在Bean上面添加@Lazy注解,其它依赖该Bean的地方不添加@Lazy注解并且每个依赖该Bean的Bean并非全部都被@Lazy注解标记,那么即使添加了@Lazy注解,也依然会在IoC容器初始化的时候进行实例化。
说起来有点绕口,如果有小伙伴未能理解,可以根据以上代码多测试几次就会明白,实在不明白,欢迎在评论区给我留言哦。
本文地址:https://blog.csdn.net/m0_43448868/article/details/112005140