欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

@Lazy注解加在类上、字段上有什么作用?如何正确理解@Lazy注解?

程序员文章站 2022-06-23 11:06:15
结论当将@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容器初始化的时候就会被实例化 *...

结论

当将@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注解。

@Lazy注解加在类上、字段上有什么作用?如何正确理解@Lazy注解?
将@Lazy注解从LazyBeab的componentBean字段上去掉,再看下执行结果。
@Lazy注解加在类上、字段上有什么作用?如何正确理解@Lazy注解?
可以发现,在ComponentBean上添加的@Lazy注解仿佛失去了效果,ComponentBean依然在IoC容器初始化的时候就进行了实例化。
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注解?
@Lazy注解加在类上、字段上有什么作用?如何正确理解@Lazy注解?

小结

如果只在某个类上添加@Lazy注解,如果工程中有其它地方依赖了该类,那么即使添加了@Lazy注解,也依然会在IoC容器初始化的时候就去实例化该类。如果想在使用的时候才去实例化,可以在每个依赖该类的地方上添加@Lazy注解,具体原理我们接下来就开始分析。

@Lazy注解添加到字段底层处理逻辑分析

IoC容器中由谁来解析@Lazy注解

和@Qualifier注解一样,IoC容器并不会提前解析好@Lazy注解,而是在处理Bean中添加@Au-towired或@Resource或JSR-330规范中规定的依赖注入注解的字段/方法时才会去解析。和处理@Qualifier注解一样,都是由ContextAnnotationAutowireCandidateResolver来解析。

但@Qualifier注解和@Value注解是由其父类QualifierAnnotationAutowireCandidateResolver来解析,只有@Lazy注解由ContextAnnotationAutowireCandidateResolver自己来解析。关于这一点我们也可以在这两个类的定义中看到。

@Lazy注解加在类上、字段上有什么作用?如何正确理解@Lazy注解?
@Lazy注解加在类上、字段上有什么作用?如何正确理解@Lazy注解?

@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注解加在类上、字段上有什么作用?如何正确理解@Lazy注解?
@Lazy注解加在类上、字段上有什么作用?如何正确理解@Lazy注解?
可以看到应用程序执行到了CglibAopProxy内部类DynamicAdvisedInterceptor的intercept方法中,在图片最下面调用了TargetSource的getTarget方法,这就会执行到在ContextAannotationAutowireCandidateResolver的buildLazyResolutionProxy方法中创建的TargetSource匿名内部类的getTarget方法。这时候才会通过BeanFactory实例的doResolveDependency方法真正的去解析依赖。

@Lazy注解加在类上、字段上有什么作用?如何正确理解@Lazy注解?
@Lazy注解加在类上、字段上有什么作用?如何正确理解@Lazy注解?

总结

当将@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