Spring IoC是Spring最经典设计,自动装配是IoC注入“自动化”的一个简化配置操作。尽管IoC注入帮我们管理了对象之间的依赖关系,但是仍可能发生设计不当而导致了循环依赖问题。强大Spring也提供了一些优雅的解决方案。
目录
Spring IoC容器负责对象的生命周期和对象之间的(依赖)关系。
在创建新的Bean时,IoC容器会自动注入新Bean的所依赖的其他Bean,而无须自己手动创建。
优点:
不会对业务代码构成很强的侵入性,对象具有更好的可测试性,可重用性和可拓展性。
优点看起来很抽象,可以结合实例来理解:参见王福强的《Spring揭秘》2.3节IoC的附加值
IoC 全称为 InversionofControl,翻译为 “控制反转”.
普遍认为(误,下面会讲),IoC还有一个别名为 DI( DependencyInjection),即依赖注入。“依赖注入”明确描述了“被注入对象依赖IoC容器配置依赖对象”。
理解DI的关键是:“谁依赖谁,为什么需要依赖,谁注入谁,注入了什么”,那我们来深入分析一下:
二者的关系
控制反转(Inversion of Control) 就是依赖倒置原则的一种代码设计的思路。具体采用的方法就是所谓的依赖注入(Dependency Injection)。
我们自己手动创建一个车instance时候,是从底层往上层new的:
每一个类的构造函数都直接调用了底层代码的构造函数。这个过程中,我们需要了解整个Car/Framework/Bottom/Tire类构造函数是怎么定义的,才能一步一步new/注入。底层的改动都会牵动所有上层的改动。如果每次修改一个类,我们都要修改所有以它作为依赖的类,那软件的维护成本就太高了。
而IoC Container在进行这个工作的时候是反过来的,它先从最上层开始往下找依赖关系,到达最底层之后再往上一步一步new(有点像深度优先遍历)。所谓依赖注入,就是把底层类作为参数传入上层类,实现上层类对下层类的“控制”。
这里IoC Container可以直接隐藏具体的创建实例的细节,在我们来看它就像一个工厂:
我们就像是工厂的客户。我们只需要向工厂请求一个Car实例,然后它就给我们按照Config创建了一个Car实例。我们完全不用管这个Car实例是怎么一步一步被创建出来。
private DependencyA dependencyA;
private DependencyB dependencyB;
private DependencyC dependencyC;
@Autowired
public DI(DependencyA dependencyA, DependencyB dependencyB, DependencyC dependencyC) {
this.dependencyA = dependencyA;
this.dependencyB = dependencyB;
this.dependencyC = dependencyC;
}
Spring4.3+之后,constructor注入支持非显示注入方式。
private DependencyA dependencyA;
private DependencyB dependencyB;
private DependencyC dependencyC;
// @Autowired
public DI(DependencyA dependencyA, DependencyB dependencyB, DependencyC dependencyC) {
this.dependencyA = dependencyA;
this.dependencyB = dependencyB;
this.dependencyC = dependencyC;
}
setter()
方法的参数列表声明依赖对象private DependencyA dependencyA;
private DependencyB dependencyB;
private DependencyC dependencyC;
@Autowired
public void setDependencyA(DependencyA dependencyA) {
this.dependencyA = dependencyA;
}
@Autowired
public void setDependencyB(DependencyB dependencyB) {
this.dependencyB = dependencyB;
}
@Autowired
public void setDependencyC(DependencyC dependencyC) {
this.dependencyC = dependencyC;
}
@Autowired
private DependencyA dependencyA;
@Autowired
private DependencyB dependencyB;
@Autowired
private DependencyC dependencyC;
注入方式 | 优点 | 缺点 | 实用场景 |
---|---|---|---|
字段注入 | 简单,便于添加新的dependency | 可能会出现注入失败而出现NullPointedException;在Test和其他Module不可用;不可用于final字段,从而无法保证字段的不变性。 | Spring官方不推荐这种写法。 |
setter注入 | 灵活性高,便于修改依赖对象 | 对于仅使用setter注入的依赖对象需要进行非空检查;对象无法在构造完成后马上进入就绪状态 | 重新注入非默认值(构造器注入)的依赖对象;对于非必需的依赖,建议使用setter注入。 |
constructor注入 | 对象在构造完成之后,即已进入就绪状态,可以马上使用。 | 当依赖对象比较多的时候,构造方法的参数列表会比较长,维护和使用也比较麻烦,根据单一职责原则,此时应该考虑重构了。使用不慎还有可能出现循环依赖。 |
Spring4.x之后,注入方式应该按需选择setter或constructor注入方式。
官方为什么不推荐字段注入
- 单一职责侵入
添加依赖是很简单的,可能过于简单了。添加六个、十个甚至一堆依赖一点也没有困难,使得我们不易察觉违反单一职责原则的程序。
而使用构造器注入方法时,发现违反单一职责原则的程序则相对容易了。在使用构造器方式注入时,到了某个特定的点,构造器中的参数变得太多以至于很明显地发现something is wrong。拥有太多的依赖通常意味着你的类要承担更多的责任。明显违背了单一职责原则(SRP:Single responsibility principle)。- 无法声明不变的字段。
字段注人无法注入final字段,只有构造器注入才能注入final字段- 隐藏了依赖关系
使用依赖注入容器意味着类不再对依赖对象负责,获取依赖对象的职责从类中抽离出来,IoC容器会帮你装配。当类不再为依赖对象负责,它应该更明确的使用公有的接口方法或构造器,使用这种方式能很清晰的了解类需要什么,也能明确它是可选的(setter注入)还是强制的(构造器注入)。- 依赖注入容器紧耦合
依赖注入框架的核心思想之一就是受容器管理的类不应该去依赖容器所使用的依赖对象。换句话说,这个类应该是一个简单的POJO(Plain Ordinary Java Object)能够被单独实例化并且你也能为它提供它所需的依赖。只有这样,你才能在单元测试中实例化这个类而不必去启动依赖注入容器,实现测试分离(启动容器更多是集成测试)。
然而,当使用变量直接注入时,没有一种方式能直接地实例化这个类并且满足其所有的依赖。这意味着需要手动new出来依赖对象或者只能在IoC Container范围使用。
自动装配是为了将依赖注入“自动化”的一个简化配置的操作。
当一个对象的属性是另一个对象时,实例化时,需要为这个对象属性进行实例化。这就是装配。
如果一个对象只通过接口来表明依赖关系,那么这种依赖就能够在对象本身毫不知情的情况下,用不同的具体实现进行切换。但是这样会存在一个问题,在传统的依赖注入配置中,我们必须要明确要给属性装配哪一个bean的引用,一旦bean很多,就不好维护了。基于这样的场景,spring使用注解来进行自动装配,解决这个问题。自动装配就是开发人员不必知道具体要装配哪个bean的引用,这个识别的工作会由spring来完成。与自动装配配合的还有“自动检测”,这 个动作会自动识别哪些类需要被配置成bean,进而来进行装配。这样我们就明白了,自动装配是为了将依赖注入“自动化”的一个简化配置的操作。
具体选择哪一种装配方式,需要配置标签的autowire属性,如果没有配置,默认是byName类型,就是会根据属性的名字来进行自动装配。上面最常用的还是byName和byType。自动装配时,装配的bean必须是唯一与属性进行吻合的,不能多也不能少,有且只有一个可以进行装配的bean,才能自动装配成功。否则会抛出异常。如果要统一所有bean的自动装配类型,可以在标签中配置default-autowire属性。当然如果配置了autowire属性,我们依然可以手动装配属性,手动装配会覆盖自动装配。
spring2.5之后提供了注解方式的自动装配。但是要使用这些注解,需要在配置文件中配置<context:annotation-config />
。只有加上这一配置,才可以使用注解进行自动装配,默认情况下基于注解的装配是被禁用的。
常用的自动装配注解有以下几种:@Autowired,@Qualifier(@Resource, @Inject, @Named为JavaEE的标准,不建议使用)。
@Autowired注解是byType类型的,这个注解可以用在属性上面,setter方面上面以及构造器上面。使用这个注解时,就不需要在类中为属性添加setter方法了。但是这个属性是强制性的,也就是说必须得装配上,如果没有找到合适的bean能够装配上,就会抛出异常:`NoSuchBeanDefinitionException
,如果required=false时,则不会抛出异常。另一种情况是同时有多个bean是一个类型的,也会抛出这个异常。此时需要进一步明确要装配哪一个Bean,这时可以组合使用@Qualifier注解,值为Bean的名字即可。@Qualifier注解使用byName进行装配,这样可以在多个类型一样的bean中,明确使用哪一个名字的bean来进行装配。@Qualifier注解起到了缩小自动装配候选bean的范围的作用。
自动检测配置,也是springmvc中最牛的一项功能。只要一个配置<context:component-scan base-package="">
或者注解@ComponentScan("")
,base-package属性指定要自动检测扫描的包。
该配置会自动扫描指定的包及其子包下面被构造型注解标注的类,并将这些类注册为spring bean,这样就不用在配置文件一个一个地配置成bean标签。构造型注解包括:@Controller,@Components,@Service,@Repository和使用@Component标注的自定义注解。生成的bean的ID默认为类的非限定名,也就是把类的名字的首字母换成小写。可以在这些注解的值中写名bean id的值,如@Controller(“helloworld”)。如果你想细化包被扫描的范围,可以使用<context:include-filter>
和<context:exclude-filter>
。具体使用方法这里不再详说。注意,没有被扫描到的类是不能注册为bean,也就不能被用来装配其他类。所以这个配置的base-package的范围非常重要。
依赖注入稍不注意就会出现循环依赖:
Bean之间的依赖顺序: BeanA -> BeanB -> BeanA
举个例子:
@Component
public class CircularDependencyA {
private CircularDependencyB circB;
@Autowired
public CircularDependencyA(CircularDependencyB circB) {
this.circB = circB;
}
}
@Component
public class CircularDependencyB {
private CircularDependencyA circA;
@Autowired
public CircularDependencyB(CircularDependencyA circA) {
this.circA = circA;
}
}
运行SpringBootApplication:
@SpringBootApplication
@ComponentScan("com.example.circulardependency.constructor")
public class CirculardependencyApplication {
public static void main(String[] args) {
SpringApplication.run(CirculardependencyApplication.class, args);
}
}
会报以下的错:
BeanCurrentlyInCreationException: Error creating bean with name 'circularDependencyA': Requested bean is currently in creation: Is there an unresolvable circular reference?
还会画出dependency circle:
出现循环依赖是因为设计问题,最佳处理方法是重新设计。
https://zhuanlan.zhihu.com/p/84267654
在实际开发中,推倒重来往往是不允许的,所以会有以下几种补救方法。
与constructor注入不同,setter
是按需注入的,并且允许依赖对象为null;
@Component
public class CircularDependencyA {
private CircularDependencyB circB;
@Autowired
public void setCircB(CircularDependencyB circB) {
this.circB = circB;
}
public CircularDependencyB getCircB() {
return circB;
}
}
@Component
public class CircularDependencyB {
private CircularDependencyA circA;
private String message = "Hi!";
@Autowired
public void setCircA(CircularDependencyA circA) {
this.circA = circA;
}
public String getMessage() {
return message;
}
}
加单元测试:
@RunWith(SpringRunner.class)
@SpringBootTest
@ComponentScan("com.example.circulardependency.setter")
public class CirculardependencyApplicationTests {
@Bean
public CircularDependencyB getDependencyB() {
return new CircularDependencyB();
}
@Bean
public CircularDependencyA getDependencyA() {
CircularDependencyA circularDependencyA = new CircularDependencyA();
circularDependencyA.setCircularDependencyB(getDependencyB());
return circularDependencyA;
}
@Test
public void contextLoads() {
System.out.println("Hello world.");
CircularDependencyA circularDependencyA = getDependencyA();
System.out.println(circularDependencyA.getCircularDependencyB().getMessage());
}
}
使用字段注解也可以解决循环依赖,但是字段注解为非官方推荐做法,所以在这里也就不给出实例了。
@Lazy
延迟初始化。在本例中,会先构建 CircularDependencyA完成后, 再构建CircularDependencyB,打破dependency circle。
@Component
public class CircularDependencyA {
private CircularDependencyB circB;
@Autowired
public CircularDependencyA(@Lazy CircularDependencyB circB) {
this.circB = circB;
}
}
3. 使用ApplicationContextAware, InitializingBean
ApplicationContextAware获取SpringContext,用于加载bean;InitializingBean定义了设置Bean的property之后的动作。
@Component
public class CircularDependencyA implements InitializingBean, ApplicationContextAware {
private CircularDependencyB circB;
private ApplicationContext context;
@Override
public void afterPropertiesSet() throws Exception {
this.circB = context.getBean(CircularDependencyB.class);
}
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
this.context = applicationContext;
}
public CircularDependencyB getCircularDependencyB() {
return circB;
}
}
原理:跟Spring Bean的Life cycle有关:先实例化,再调用setApplicationContext(), afterPropertiesSet(); 所以,CircularDependencyB已经加载完毕后,再加载CircularDependencyA。
倘若,CircularDependencyA先实例化, 调用afterPropertiesSet()时发现CircularDependencyB尚未加载,先加载CircularDependencyB(调用构造函数,由于此时CircularDependencyA已经实例化了,所以能够顺利加载),设置属性circB,CircularDependencyA加载完成。
原文:https://blog.csdn.net/programmer_at/article/details/82389221
[1]IoC注入方式: https://www.vojtechruzicka.com/field-dependency-injection-considered-harmful/
[2] 循环依赖: https://www.baeldung.com/circular-dependencies-in-spring
[3] 字段注入: https://blog.marcnuri.com/field-injection-is-not-recommended/
[4] IoC与DI: 概念精准解释版:http://sishuok.com/forum/blogPost/list/2427.html
通俗易懂版:https://www.zhihu.com/question/23277575/answer/169698662
●阿里巴巴为什么能抗住90秒100亿?--服务端高并发分布式架构演进之路
●SpringCloud电商秒杀微服务-Redisson分布式锁方案
查看更多好文,进入公众号--撩我--往期精彩
一只 有深度 有灵魂 的公众号0.0
Pytorch1.7.0—GPU安装教程
起因: 安装VS2017过程中,出现未找到 vc_runtimeMinimum_x86.msi 的错误,查看日记,如下: 未能安装包“Microsoft.VisualCpp.Redist.14.Latest,version=14.16.27033.4,chip=x86”。 搜索 URL https://aka.ms/VSSetupErrorReports?q=Pac...
Leap Motion的官网文档已经有不少的热心网友参与了翻译,但没有覆盖官网文档的全部。为迎合Unity VR的热潮与大家的学习需要,推出的针对Unity方向的官方文档翻译系列。第三篇 《手型资源》 介绍了Leap Motion为Unity提供的资源包种的手型预设体资源。
Android的标准广播(Normal broadcasts)标准广播一种完全异步执行的广播,在广播发出后,所有的广播接收器几乎都会在同一个时刻接收到这条广播,它们之间并没有任何先后顺序。Android的有序广播(Ordered broadcasts)一种同步执行的广播,在广播发出后,同一个时刻只会有一个广播接收器接收到这条广播,当这个广播接收器的逻辑执行完毕后,广播才会继续传
CAD看图软件哪个好?CAD作为设计行业当中最常使用的图纸文件,几乎每天都会被我们使用到,如果我们想要将CAD文件打开,那么我们必须要结束CAD看图软件才能够打开,那么CAD看图软件哪个好?通过这篇文章,我就将个人认为最好用的看图软件推荐给大家。 个人认为最好用的CAD看图软件应该是迅捷CAD看图软件,这款软件功能强大,而且操作非常简单,用户利用这款软件可以轻松的对CAD图纸文件进行查看,
关系型数据库 - Oracle / MySQL / MariaDB - ⼆维表非关系型数据库 - MongoDB / Redis / ElasticSearch - ⽂档数据库/KV数据库systemctl start mariadbmysqld - MySQL Daemon - 守护进程use mysql;update user set pa...
Linux/ubuntu 配置闲置用户自动退出前言全局用户生效特定用户生效针对SSH做配置前言配置结果为:一段时间不操作系统后自动锁屏、自动关闭屏幕、自动注销当前登录。注意,该配置对于图像化界面操作系统,配置生效后,只会对终端操作连接生效;如,打开终端,然后不操作,等超过配置时间,终端会被关闭,但操作界面不会退出。全局用户生效通过修改/etc/profile实现,在内容中添加TMOUT变量,变量值以s(秒)为单位;[sudo] vim /etc/profile# 尾部添加TMOUT=9
美国著名图书频道Book Pool集结最权威的62位作者评选出了最近10年计算机专业图书中的50强[原文]光这62位作者阵营就非常强大,我们熟悉的就有: Francesco Balena(Microsoft.NET框架程序设计,Visual Basic.NET语言描述作者) Bert Bates(Head First Design Patterns作者)
一、string类型转换成Json对象方法 1、Javascript支持的转换方式:eval eval('(' + jsonstr + ')'); 注意:需要在json字符外包裹一对小括号,ie8(兼容模式),ie7和ie6也可以使用eval()将字符串转为JSON对象,但不推荐这些方式;缺点是不安全,eval会执行json串中的表达式。 2、浏览器支持的转换方式(Firefox,chrome,opera,safari,ie9,ie8)等浏览器: ...
先看看困难户按照网上统一的标准解决方案依旧未解决的方法:标准的右击文件,选择“属性”--“安全”--“高级”;(注意,下图解决不了的记得看到最后,恶心了我几个小时,最后试到了)如果按传统方法依旧不行的话,那么就将上图中的所有者替换成EveryOne如果没有的可以先点编辑添加好再把EveryOne替换为所有者,终于,世界平静了,想砸电脑的心安定了下来,内心的MMP憋回去了,呼...
1. 创建ROS功能包使用catkin_create_pkg命令来创建一个新的catkin程序包。 首先切换到之前通过创建catkin工作空间教程创建的catkin工作空间中的src目录下:~/ros_workspace$ cd src接着使用catkin_create_pkg命令来创建一个名为’beginner_tutorials’的新程序包,这个程序包依赖于std_msgs、ro...
文章目录html基础html文件的基本结构标签这些都很简单body标签p标签hx标签em和strong标签span标签q标签blockquote标签br标签和\ hr标签address标签code标签pre标签列表/表格ul ol标签div标签table标签和caption标签进阶a标签malito标签img标签表单文本/密码输入框多行文本输入单选框/复选框下拉列表框使用下拉列表框进行多选提交/重置按钮label标签html基础html文件的基本结构<!DOCTYPE HTML>