从零开始 Spring Boot 39:循环依赖

news/2024/10/31 7:34:32/

从零开始 Spring Boot 39:循环依赖

spring boot

图源:简书 (jianshu.com)

什么是循环依赖

我们看一个例子:

@Component
public class Person {private Dog pet;public Person(Dog pet) {this.pet = pet;}
}@Component
public class Dog {private Person owner;public Dog(Person owner) {this.owner = owner;}
}

这里定义了两个 Spring Bean:persondog。这两个 bean 都包含对另一个 bean 的依赖,并且这种依赖是通过构造器来完成注入的。

如果实际运行这样的示例,就会报错:

The dependencies of some of the beans in the application context form a cycle:┌─────┐
|  dog defined in file [D:\workspace\learn_spring_boot\ch39\cycle-dep\target\classes\com\example\cycledep\Dog.class]
↑     ↓
|  person defined in file [D:\workspace\learn_spring_boot\ch39\cycle-dep\target\classes\com\example\cycledep\Person.class]
└─────┘

错误提示告诉我们这两个 bean 之间出现了循环依赖的问题,因此程序无法正常启动。

这是因为 Spring 要创建Person对象,就必须先创建Dog对象以调用Person的构造器,但要创建Dog对象,同样需要先创建一个Person对象来调用Dog的构造器,这样就陷入了“先有鸡还是先有蛋”的问题,无法进行下去。

需要说明的是,这种循环依赖仅会在构造器注入的情况下出现,属性注入或者 Setter 注入都不会导致,因为后两者并不会影响到构造器的调用和对应 bean 实例的创建,它们都是在 bean 实例创建后的合适时间被初始化/调用的。

解决循环依赖

拆分代码

通常出现这种循环依赖说明代码结构有问题,我们可能需要重新设计代码。拆分其中相互依赖的部分,自然就可以解决循环依赖的问题。

延迟初始化

可以在构造器中产生循环引用的依赖注入上使用@Lazy来解决循环引用问题:

@Component
public class Person {private Dog pet;public Person(@Lazy Dog pet) {this.pet = pet;}
}@Component
public class Dog {private Person owner;public Dog(Person owner) {this.owner = owner;}
}

此时,在 Spring 创建Person对象的时候,Spring 会使用一个Dog类型的代理,而不是真正创建一个Dog类型,真正的Dog类型会在之后需要的时候才被创建,而那时候Person类型的 bean 早已完成创建和初始化,因此再调用Dog的构造器进行注入时不会产生循环依赖的问题。

属性注入和 Setter 注入

就像之前说的,这种循环依赖仅会在使用构造器注入时出现,因此我们可以使用属性注入或 Setter 注入的方式来解决循环依赖问题:

@Component
public class Person {@Setter(onMethod = @__(@Autowired))private Dog pet;
}@Component
public class Dog {@Setter(onMethod = @__(@Autowired))private Person owner;
}

除此之外我们还需要修改一个配置选项:

spring.main.allow-circular-references=true
  • 在早期的 Spring 版本中,spring.main.allow-circular-references默认为true,因此可以直接通过这种方式规避循环依赖,但后来 Spring 官方认为循环依赖是“代码异味”,所以将该选项默认设置为false
  • 在目前的版本中,无论spring.main.allow-circular-references的值是什么,构造器注入导致的循环依赖都会报错。
  • Lombok 和依赖注入的内容可以参考我的另一篇文章。

当然属性注入也是同样的效果:

@Component
public class Person {@Autowiredprivate Dog pet;
}@Component
public class Dog {@Autowiredprivate Person owner;
}

部分依赖注入

注意,下面的解决方案都不需要将spring.main.allow-circular-references配置设置为true

循环依赖实际上是因为两个互相存在依赖关系的类型都使用依赖注入实现依赖导致的,因此只要我们不完全使用依赖注入(部分使用依赖注入),就可以解决此类问题:

@Component
public class Person {@Autowiredprivate Dog pet;@PostConstructpublic void init(){this.pet.setOwner(this);}
}@Component
public class Dog {@Setterprivate Person owner;
}

这里的关键在于,Person通过依赖注入来初始化pet属性,而Dog类中的owner属性并没有借助依赖注入进行初始化,所以这里并不存在循环依赖。但显然我们需要实现Dog类对Person类的依赖关系,这可以通过 bean 的生命周期回调来完成,比如这个示例中的@PostConstruct标记的回调方法,在这个方法中我们通过this.pet.setOwner(this)的方式创建了Dog实例对Person实例的依赖关系。

当然,类似的你可以使用任意方式的 bean 初始化回调,比如:

@Component
public class Person implements InitializingBean {// ...@Overridepublic void afterPropertiesSet() throws Exception {this.pet.setOwner(this);}
}

效果是完全相同的。

关于更多 bean 生命周期回调的更多说明,可以参考我的这篇文章。

不使用依赖注入

可以更激进一些,完全不使用依赖注入,自然也就不存在循环依赖的问题,比如:

@Component
public class Person implements InitializingBean, ApplicationContextAware {private Dog pet;private ApplicationContext ctx;@Overridepublic void afterPropertiesSet() throws Exception {this.pet = ctx.getBean(Dog.class);this.pet.setOwner(this);}@Overridepublic void setApplicationContext(ApplicationContext applicationContext) throws BeansException {this.ctx = applicationContext;}
}@Component
public class Dog {@Setterprivate Person owner;
}

在不使用依赖注入的情况下,我们就需要用ApplicationContext.getBean获取 bean 实例,因此需要一个ApplicationContext的引用,这里可以通过ApplicationContextAware接口实现。

当然,实际上我们并不需要这么“极端”,只需要不使用依赖注入处理存在循环依赖的属性即可,对于ApplicationContext可以通过依赖注入获取:

@Component
public class Person implements InitializingBean {private Dog pet;@Autowiredprivate ApplicationContext ctx;@Overridepublic void afterPropertiesSet() throws Exception {this.pet = ctx.getBean(Dog.class);this.pet.setOwner(this);}
}

总结

如果代码中存在依赖注入,在可能的情况下,最好进行重构,因为依赖注入往往说明存在“代码异味”。如果因为成本之类的原因无法重构,可以通过本文说明的几种方式进行处理。

The End,谢谢阅读。

本文的完整示例代码可以从这里获取。

参考资料

  • 痛快!SpringBoot终于禁掉了循环依赖! - 掘金 (juejin.cn)
  • Circular Dependencies in Spring | Baeldung
  • Dependency Injection :: Spring Framework
  • 从零开始 Spring Boot 27:IoC - 红茶的个人站点 (icexmoon.cn)

http://www.ppmy.cn/news/368627.html

相关文章

QQ大盗(加强免杀)

首先先去申请一个163的邮箱(这里本人建议使用163的邮箱) 如图 比如我申请的 163邮箱是:sf791163.com 我的QQ邮箱是 544250107qq.com 申请地址:mail.163.com申请好了登陆一下 …

网站盗号事件

不知道为什么,最近好多网站都在被盗号,各位有一样的情况吗?

通过QQ号查询对方QQ绑定手机号

前言: 某日,同事打王者和队友产生分歧,游戏里互相问候对方双亲,本以为游戏结束就结束了。没想到他两加了游戏好友互骂,对方说这是小号让同事加他QQ大号。结果,同事加了,然后又被对方骂了一顿&a…

解密QQ号

解密QQ号 一、原题 【 题目描述 】 YSM 在年级里人缘特别好,大家都找他要 QQ 号,数学特别好的它有时也小卖弄一下,他把 QQ 号加密后告诉同学们,所以同学们要得到他的 QQ 号还得先解密。解密规则是这样的:首先将第一个…

QQ被盗

各位认识我和我认识的朋友,前几天我的QQ被盗了,给你们带来不解,有时向朋友发一些乱七八糟的信息,有时居然把我朋友都拉黑名单了,请谅解.现在盗QQ密码越来越高明了.请各位…

爬虫爬取QQ号

这个爬虫是拿来练手的,可以爬取网络中的QQ号,然后存储到本地。 import urllib.request import ssl import re import os from collections import deque #导入队列库def writeFileBytes(htmlBytes,topath):with open(topath,wb) as f:f.write(htmlBytes…

qqkey获取原理_【逆向】QQkey盗号木马原理分析

一、简介 QQkey是一段字符串,通过这段字符串在没有QQ登录密码的前提下你依然能够在浏览器中对别人QQ空间、邮箱等应用进行随意访问和操作。现在市面上已经有很多使用易语言编写的盗号木马,专门盗取别人的QQkey,通过QQkey改绑关联了该邮箱的St…

你的QQ号又被盗了?关于网络安全你所不知道的事情

不知道大家是否有这样一个经历: 就是大家的qq好像被别人登录了一样,群发给朋友,向朋友借钱的消息,有些身边的朋友还被骗取了大量的金额,别人又是如何知道自己的密码的呢? 现在都0202年了,想说的是QQ在密码防盗这块做得已经相当不错了,所以…