详解Spring @Lazy注解为什么能破解死循环

 更新时间:2023年07月20日 10:47:33   作者:江南一点雨  
这篇文章主要来和大家探讨一下Spring中的@Lazy注解为什么能破解死循环,文中的示例代码讲解详细,具有一定的参考价值,需要的可以了解一下

以下内容基于 Spring6.0.4。

上篇文章松哥和大家聊了在 Spring 中并非所有的循环依赖都可以被解决,有一些循环依赖默认情况下 Spring 也是完全无法解决的。不熟悉的小伙伴可以先看看上篇文章

上篇文章第一小节的案例为例,在构造方法中互相注入对方的 Bean,此时完全就是一个死循环呀,对于这种死循环,难道真的有办法解决?

Spring 里边提供了办法来解决,但是似乎又没有解决,为什么这么说,看完本文你就明白了。

1. @Lazy

如本文题目所示,上篇文章涉及到的三种无法自动解决的循环依赖,都可以通过添加 @Lazy 注解来解决。

如果是构造器注入,如下:

@Service
public class AService {
    BService bService;
    @Lazy
    public AService(BService bService) {
        this.bService = bService;
    }
    public BService getbService() {
        return bService;
    }
}
@Service
public class BService {
    AService aService;
    @Lazy
    public BService(AService aService) {
        this.aService = aService;
    }
    public AService getaService() {
        return aService;
    }
}

@Lazy 注解可以添加在 AService 或者 BService 的构造方法上,也可以都添加上。

添加上之后,我们再去启动项目,就不会报错了。这样看起来问题解决了,但是其实还是差点意思,小伙伴们看一下我的启动代码:

ClassPathXmlApplicationContext ctx = new ClassPathXmlApplicationContext("aop.xml");
AService aService = ctx.getBean(AService.class);
BService bService = ctx.getBean(BService.class);
System.out.println("aService.getClass() = " + aService.getClass());
System.out.println("bService.getClass() = " + bService.getClass());
System.out.println("aService.getbService().getClass() = " + aService.getbService().getClass());
System.out.println("bService.getaService().getClass() = " + bService.getaService().getClass());

最终打印结果如下:

小伙伴们看到,我们从 AService 和 BService 中获取到的 Bean 都是正常的未被代理的对象,事实上我们的原始代码确实也没有需要代理的地方。但是,AService 中的 BService 以及 BService 中的 AService 却都是代理对象,按理说 AService 中的 BService 应该和我们从 Spring 容器中获取到的 BService 一致,BService 中的 AService 也应该和 Spring 容器中获取到的 AService 一致,但实际上,两者却并不相同。

不过这样也好懂了,为什么 Spring 能把一个死结给解开,就是因为 AService 和 BService 各自注入的 Bean 都不是原始的 Bean,都是一个代理的 Bean,AService 中注入的 BService 是一个代理对象,同理,BService 中注入的 AService 也是一个代理对象。

这也是为什么我一开始说这个问题 Spring 解决了又没解决。

其实,这就是 @Lazy 这个注解的工作原理,看名字,加了该注解的对象会被延迟加载,实际上被该注解标记的对象,会自动生成一个代理对象。

上篇文章中提到的另外两个问题,也可以通过 @Lazy 注解来解决,代码如下:

@Service
@Scope("prototype")
public class AService {
    @Lazy
    @Autowired
    BService bService;
}
@Service
@Scope("prototype")
public class BService {
    @Lazy
    @Autowired
    AService aService;
}

这里 @Lazy 只要一个其实就能解决问题,也可以两个都添加。

对于含有 @Async 注解的情况,也可以通过 @Lazy 注解来解决:

@Service
public class AService {
    @Autowired
    @Lazy
    BService bService;
    @Async
    public void hello() {
        bService.hello();
    }
    public BService getbService() {
        return bService;
    }
}
@Service
public class BService {
    @Autowired
    AService aService;
    public void hello() {
        System.out.println("xxx");
    }
    public AService getaService() {
        return aService;
    }
}

如此,循环依赖可破!

总而言之一句话,@Lazy 注解是通过建立一个中间代理层,来破解循环依赖的。

2. 原理分析

接下来我们再来分析一下 @Lazy 注解处理的源码。

这块的源码分析我就不从头开始分析了,因为整个处理流程前面部分和之前文章 @Autowired 到底是怎么把变量注入进来的?所介绍的内容是一致的,不熟悉的小伙伴建议先阅读 @Autowired 到底是怎么把变量注入进来的?一文。我这里就借用该文的总结,带领小伙伴们稍微回顾一下属性注入的过程:

1.在创建 Bean 的时候,原始 Bean 创建出来之后,会调用 populateBean 方法进行 Bean 的属性填充。

2.接下来调用 postProcessAfterInstantiation 方法去判断是否需要执行后置处理器,如果不需要,就直接返回了。

3.调用 postProcessProperties 方法,去触发各种后置处理器的执行。

4.在第 3 步的方法中,调用 findAutowiringMetadata,这个方法又会进一步触发 buildAutorwiringMetadata 方法,去找到包含了 @Autowired、@Value 以及 @Inject 注解的属性或者方法,并将之封装为 InjectedElement 返回。

5.调用 InjectedElement#inject 方法进行属性注入。

6.接下来执行 resolvedCachedArgument 方法尝试从缓存中找到需要的 Bean 对象。

7.如果缓存中不存在,则调用 resolveFieldValue 方法去容器中找到 Bean。

8.最后调用 makeAccessible 和 set 方法完成属性的赋值。

在第 7 步中,调用 resolveFieldValue 方法去解析 Bean,@Lazy 注解的相关逻辑就是在这个方法中进行处理的(对应 @Autowired 到底是怎么把变量注入进来的?一文的 3.2 小节)。

resolveFieldValue 方法最终会执行到 resolveDependency 方法:

@Nullable
public Object resolveDependency(DependencyDescriptor descriptor, @Nullable String requestingBeanName,
  @Nullable Set<String> autowiredBeanNames, @Nullable TypeConverter typeConverter) throws BeansException {
 descriptor.initParameterNameDiscovery(getParameterNameDiscoverer());
 if (Optional.class == descriptor.getDependencyType()) {
  return createOptionalDependency(descriptor, requestingBeanName);
 }
 else if (ObjectFactory.class == descriptor.getDependencyType() ||
   ObjectProvider.class == descriptor.getDependencyType()) {
  return new DependencyObjectProvider(descriptor, requestingBeanName);
 }
 else if (javaxInjectProviderClass == descriptor.getDependencyType()) {
  return new Jsr330Factory().createDependencyProvider(descriptor, requestingBeanName);
 }
 else {
  Object result = getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary(
    descriptor, requestingBeanName);
  if (result == null) {
   result = doResolveDependency(descriptor, requestingBeanName, autowiredBeanNames, typeConverter);
  }
  return result;
 }
}

在这个方法中,首先会判断注入的属性类型是 Optional、ObjectFactory 还是 JSR-330 中的注解,我们这里都不是,所以走最后一个分支。

在最后一个 else 中,首先调用 getAutowireCandidateResolver().getLazyResolutionProxyIfNecessary 方法看一下是否需要延迟加载 Bean 对象,@Lazy 注解就是在这里进行处理的。如果能够延迟加载,那么该方法的返回值就不为 null,就可以直接返回了,就不需要执行 doResolveDependency 方法了。

ContextAnnotationAutowireCandidateResolver#getLazyResolutionProxyIfNecessary:

@Override
@Nullable
public Object getLazyResolutionProxyIfNecessary(DependencyDescriptor descriptor, @Nullable String beanName) {
 return (isLazy(descriptor) ? buildLazyResolutionProxy(descriptor, beanName) : null);
}

大家看一下,这个方法首先会调用 isLazy 去判断一下是否需要延迟加载,如果需要,则调用 buildLazyResolutionProxy 方法构建一个延迟加载的对象;如果不需要,则直接返回一个 null 即可。

protected boolean isLazy(DependencyDescriptor descriptor) {
 for (Annotation ann : descriptor.getAnnotations()) {
  Lazy lazy = AnnotationUtils.getAnnotation(ann, Lazy.class);
  if (lazy != null && lazy.value()) {
   return true;
  }
 }
 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;
}

这个判断方法主要是检查当前类中各种参数上是否含有 @Lazy 注解、方法、属性以及类名上是否含有 @Lazy 注解,如果有,则返回 true,否则返回 false。

再来看 buildLazyResolutionProxy 方法:

private Object buildLazyResolutionProxy(
  final DependencyDescriptor descriptor, final @Nullable String beanName, boolean classOnly) {
 BeanFactory beanFactory = getBeanFactory();
 final DefaultListableBeanFactory dlbf = (DefaultListableBeanFactory) beanFactory;
 TargetSource ts = new TargetSource() {
  @Override
  public Class<?> getTargetClass() {
   return descriptor.getDependencyType();
  }
  @Override
  public boolean isStatic() {
   return false;
  }
  @Override
  public Object getTarget() {
   Set<String> autowiredBeanNames = (beanName != null ? new LinkedHashSet<>(1) : null);
   Object target = dlbf.doResolveDependency(descriptor, beanName, autowiredBeanNames, 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");
   }
   if (autowiredBeanNames != null) {
    for (String autowiredBeanName : autowiredBeanNames) {
     if (dlbf.containsBean(autowiredBeanName)) {
      dlbf.registerDependentBean(autowiredBeanName, beanName);
     }
    }
   }
   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);
 }
 ClassLoader classLoader = dlbf.getBeanClassLoader();
 return (classOnly ? pf.getProxyClass(classLoader) : pf.getProxy(classLoader));
}

这个方法就是用来生成代理的对象的,这里构建了代理对象 TargetSource,在其 getTarget 方法中,会去执行 doResolveDependency 获取到被代理的对象(doResolveDependency 的获取逻辑可以参考@Autowired 到底是怎么把变量注入进来的?一文),而 getTarget 方法只有在需要的时候才会被调用。所以,@Lazy 注解所做的事情,就是在给 Bean 中的各个属性注入值的时候,原本需要去 Spring 容器中找注入的对象,现在不找了,先给一个代理对象顶着,需要的时候再去 Spring 容器中查找。

以上就是详解Spring @Lazy注解为什么能破解死循环的详细内容,更多关于Spring @Lazy的资料请关注脚本之家其它相关文章!

相关文章

  • Java多线程案例之定时器详解

    Java多线程案例之定时器详解

    定时器也是软件开发中的一个重要组件. 类似于一个 “闹钟”. 达到一个设定的时间之后, 就执行某个指定好的代码。本文就来和大家详细聊聊定时器的原理与使用,感兴趣的可以了解一下
    2022-10-10
  • SpringBoot定义过滤器、监听器、拦截器的方法

    SpringBoot定义过滤器、监听器、拦截器的方法

    本篇文章主要介绍了SpringBoot定义过滤器、监听器、拦截器的方法,具有一定的参考价值,有兴趣的可以了解一下。
    2017-04-04
  • springboot整合ehcache 实现支付超时限制的方法

    springboot整合ehcache 实现支付超时限制的方法

    在线支付系统需要极高的稳定性,在有限的系统资源下,稳定性优先级要高于系统并发以及用户体验,因此需要合理的控制用户的支付请求。下面通过本文给大家介绍springboot整合ehcache 实现支付超时限制的方法,一起看看吧
    2018-01-01
  • IDEA中的Run/Debug Configurations各项解读

    IDEA中的Run/Debug Configurations各项解读

    这篇文章主要介绍了IDEA中的Run/Debug Configurations各项解读,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2023-09-09
  • Springboot编写CRUD时访问对应数据函数返回null的问题及解决方法

    Springboot编写CRUD时访问对应数据函数返回null的问题及解决方法

    我在学习springboot,其中在编写CRUD时发现访问数据的函数执行下去返回值是null但是其它部分正常,这篇文章主要介绍了Springboot在编写CRUD时,访问对应数据函数返回null,需要的朋友可以参考下
    2024-02-02
  • Java函数式编程(十):收集器

    Java函数式编程(十):收集器

    这篇文章主要介绍了Java函数式编程(十):收集器,本文是系列文章的第10篇,其它文章请参阅本文底部的相关文章,需要的朋友可以参考下
    2014-09-09
  • 基于Graphics2D drawImage图片失真的解决方案

    基于Graphics2D drawImage图片失真的解决方案

    这篇文章主要介绍了基于Graphics2D drawImage图片失真的解决方案,具有很好的参考价值,希望对大家有所帮助。如有错误或未考虑完全的地方,望不吝赐教
    2021-11-11
  • idea在某天打开后一直在updating index的问题及解决

    idea在某天打开后一直在updating index的问题及解决

    idea长时间updatingindexs无法加载项目,原因是缓存文件异常或被安全软件清理,解决方法是点击File>InvalidateCaches...>InvalidateandRestart重新加载,或等待idea自动装配完毕
    2026-05-05
  • Spring Boot 实现程序的优雅退出(详细步骤)

    Spring Boot 实现程序的优雅退出(详细步骤)

    Spring Boot 为我们提供了优雅退出的功能,使应用程序能够在关闭时正常处理完所有当前请求,避免请求被中断导致数据丢失或不一致等问题,本文将全面介绍如何在 Spring Boot 应用程序中实现优雅退出,感兴趣的朋友跟随小编一起看看吧
    2024-03-03
  • Java读取文件方法汇总

    Java读取文件方法汇总

    这篇文章主要为大家详细介绍了Java读取文件方法,按字节读取文件内容、按字符读取文件内容、随机读取文件内容等,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2016-12-12

最新评论