2

@Lazy 注解为啥就能破解死循环?

 1 year ago
source link: http://www.javaboy.org/2023/0719/spring_lazy.html
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client
22 天前 16 分钟 读完 (大约 2331 个字)

@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());

最终打印结果如下:

20230715231434.png

小伙伴们看到,我们从 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 到底是怎么把变量注入进来的?一文。我这里就借用该文的总结,带领小伙伴们稍微回顾一下属性注入的过程:

20230713220838.png
  1. 在创建 Bean 的时候,原始 Bean 创建出来之后,会调用 populateBean 方法进行 Bean 的属性填充。
  2. 接下来调用 postProcessAfterInstantiation 方法去判断是否需要执行后置处理器,如果不需要,就直接返回了。
  3. 调用 postProcessProperties 方法,去触发各种后置处理器的执行。

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

20230713223334.png
  1. 接下来执行 resolvedCachedArgument 方法尝试从缓存中找到需要的 Bean 对象。
  2. 如果缓存中不存在,则调用 resolveFieldValue 方法去容器中找到 Bean。
  3. 最后调用 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 容器中查找。

后续的逻辑我就不再多说了,小伙伴们参考 @Autowired 到底是怎么把变量注入进来的?一文即可。

好啦,现在小伙伴们明白了 @Lazy 注解是如何解决 Spring 循环依赖了吧~虽然解决了,但是我们在日常开发中,要是能避免循环依赖还是要去避免~

喜欢这篇文章吗?扫码关注公众号【江南一点雨】【江南一点雨】专注于 SPRING BOOT+微服务以及前后端分离技术,每天推送原创技术干货,关注后回复 JAVA,领取松哥为你精心准备的 JAVA 干货!

javaboy.jpg

About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK