结论

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




ElementType java 注解 java lazy注解_aop

将@Lazy注解从LazyBeab的componentBean字段上去掉,再看下执行结果。


ElementType java 注解 java lazy注解_ElementType java 注解_02

可以发现,在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的实例化。这点可以拿之前的控制台输出来进行比对,就可以发现端倪。



ElementType java 注解 java lazy注解_ElementType java 注解_03



ElementType java 注解 java lazy注解_spring_04


小结

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

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

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

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

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



ElementType java 注解 java lazy注解_java_05



ElementType java 注解 java lazy注解_aop_06


@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方法中打上断点,启动应用程序。



ElementType java 注解 java lazy注解_spring boot_07



ElementType java 注解 java lazy注解_spring boot_08

可以看到应用程序执行到了CglibAopProxy内部类DynamicAdvisedInterceptor的intercept方法中,在图片最下面调用了TargetSource的getTarget方法,这就会执行到在ContextAannotationAutowireCandidateResolver的buildLazyResolutionProxy方法中创建的TargetSource匿名内部类的getTarget方法。这时候才会通过BeanFactory实例的doResolveDependency方法真正的去解析依赖。

ElementType java 注解 java lazy注解_spring_09


ElementType java 注解 java lazy注解_java_10

总结

当将@Lazy注解添加到字段或者方法上的参数上,IoC容器将会为其创建代理对象,如果在Bean上面添加@Lazy注解,并且在每个依赖该Bean的地方都添加上@Lazy注解,那么该Bean不会在IoC容器初始化的时候就进行实例化,只有到调用该Bean的方法时,IoC容器才会真正的去实例化Bean。

如果仅在Bean上面添加@Lazy注解,其它依赖该Bean的地方不添加@Lazy注解并且每个依赖该Bean的Bean并非全部都被@Lazy注解标记,那么即使添加了@Lazy注解,也依然会在IoC容器初始化的时候进行实例化。

说起来有点绕口,如果有小伙伴未能理解,可以根据以上代码多测试几次就会明白,实在不明白,欢迎在评论区给我留言哦。