一.前言: IOC(控制反转)与DI(依赖注入)
Spring框架对Java开发的重要性不言而喻,其核心特性就是IOC(Inversion of Control, 控制反转)和AOP,平时使用最多的就是其中的IOC,我们通过将组件交由Spring的IOC容器管理,将对象的依赖关系由Spring控制,避免硬编码所造成的过度程序耦合。
在讲依赖注入之前,我觉得有必要了解一下IOC(控制反转)与DI(依赖注入)的关系,在这篇文章中有详细的介绍:spring IOC 与 DI。
二.DI的三种常见注入方式
DI的三种常见注入方式为:setter注入、构造器注入和基于注解的注入(也叫field注入),下面来分别讲讲他们的特点。
2.1 基于注解注入
首先来看一下它的实现:
@RestController
@RequestMapping("/annotation")
public class AnnotationController {
@Autowired
private DiService diService;
@GetMapping("/test001")
public String test001() {
return diService.test001("annotation");
}
}
这种方式应该是目前最常见的注入方式了,原因很简单:
- 注入方式非常简单:加上
@Autowired注解
,加入要注入的字段,即可完成。 - 使得整体代码简洁明了,看起来美观大方。
在介绍注解注入的方式前,先简单了解bean的一个属性autowire,autowire主要有三个属性值:constructor,byName,byType。
- constructor:通过构造方法进行自动注入,spring会匹配与构造方法参数类型一致的bean进行注入,如果有一个多参数的构造方法,一个只有一个参数的构造方法,在容器中查找到多个匹配多参数构造方法的bean,那么spring会优先将bean注入到多参数的构造方法中。
- byName:被注入bean的id名必须与set方法后半截匹配,并且id名称的第一个单词首字母必须小写,这一点与手动set注入有点不同。
- byType:查找所有的set方法,将符合符合参数类型的bean注入。
下面进入正题:
注解方式注册bean:
在以前的开发中,我们主要使用四种注解注册bean,每种注解可以任意使用,只是语义上有所差异:
- @Component:可以用于注册所有bean
- @Repository:主要用于注册dao层的bean
- @Controller:主要用于注册控制层的bean
- @Service:主要用于注册服务层的bean
随着springboot的流行,@Bean注解也逐渐的被我们使用起来。Spring的@Bean注解用于告诉方法,产生一个Bean对象,然后这个Bean对象交给Spring管理。产生这个Bean对象的方法Spring只会调用一次,随后这个Spring将会将这个Bean对象放在自己的IOC容器中。
注解方式注入依赖(主要有两种):
- @Resource :java的注解,默认以byName的方式去匹配与属性名相同的bean的id,如果没有找到就会以byType的方式查找,如果byType查找到多个的话,使用@Qualifier注解(spring注解)指定某个具体名称的bean。
- @Autowired :spring注解,默认是以byType的方式去匹配类型相同的bean,可以结合@Qualifier 注解根据byName方式匹配。
关于他们的具体用法与区别,因为内容比较多,所以写在另一篇博客中,请见:@Autowired 和 @Resource 详解
2.2 构造器注入
老规矩,先上代码示例:
@RestController
@RequestMapping("/constructor")
public class ConstructorController {
private final DiService diService;
private final String result;
public ConstructorController(DiService diService) {
this.diService = diService;
this.result = diService.test001("constructor");
}
@GetMapping("/test001")
public String test001() {
return diService.test001(this.result);
}
}
这里有一个问题,如果只有一个有参数的构造方法并且参数类型与注入的bean的类型匹配,那就会注入到该构造方法中。如果有多个有参数的构造方法并且每个构造方法的参数列表里面都有要注入的属性,那userDaoJdbc会注入到哪里呢?
在Spring4.x版本中推荐的注入方式就是这种,相较于上面的field注入方式而言,就显得有点难看,特别是当注入的依赖很多(5个以上)的时候,就会明显的发现代码显得很臃肿。对于从field注入转过来+有强迫症的同学来说,简直可以说是石乐志 ,但是为啥spring官方还会这么推荐呢?官方文档里是这么说的:
The Spring team generally advocates constructor injection as it enables one to implement application components as immutable objects and to ensure that required dependencies are not
null
. Furthermore constructor-injected components are always returned to client (calling) code in a fully initialized state.
翻译一下就是:Spring团队通常提倡构造函数注入,因为它允许将应用程序组件实现为不可变的对象,并确保所需的依赖不为空。此外,注入构造函数的组件总是以完全初始化的状态返回给客户机(调用)代码。
简单解释一下:
- 不可变的对象:其实说的就是final关键字,这里不再多解释了。
- 依赖不为空:省去了我们对其检查。当要实例化ConstructorController的时候,由于自己实现了有参数的构造函数,所以不会调用默认构造函数,那么就需要Spring容器传入所需要的参数,所以就两种情况:1、有该类型的参数->传入,OK 。2:无该类型的参数->报错。这样就可以保证不会为空。
- 完全初始化的状态:这个可以跟上面的依赖不为空结合起来,向构造器传参之前,要确保注入的内容不为空,那么肯定要调用依赖组件的构造方法完成实例化。而在Java类加载实例化的过程中,构造方法是最后一步(之前如果有父类先初始化父类,然后自己的成员变量,最后才是构造方法,这里不详细展开。)。所以返回来的都是初始化之后的状态。
与注解方式注入相比,构造器注入可复用性高,如果使用field注入,缺点显而易见,对于IOC容器以外的环境,除了使用反射来提供它需要的依赖之外,无法复用该实现类。而且将一直是个潜在的隐患,因为你不调用将一直无法发现NPE的存在。
相对于注解注入,构造器注入可以防止循环依赖的问题,若如下代码:
public class A {
@Autowired
private B b;
}
public class B {
@Autowired
private A a;
}
如果使用构造器注入,在spring项目启动的时候,就会抛出:BeanCurrentlyInCreationException:Requested bean is currently in creation: Is there an unresolvable circular reference?从而提醒你避免循环依赖,如果是注解注入的话,启动的时候不会报错,在使用那个bean的时候才会报错。
2.3 setter注入
这是在spring3.x出来的时候,官方推荐的注入方式,但是在spring4.x以后就没有见它推荐了,而且在实际开发中已经很少能见到这种注入方式了。下面来看一下它的实现:
@RestController
@RequestMapping("/setter")
public class SetterController {
private DiService diService;
@Autowired
public void setDiService(DiService diService) {
this.diService = diService;
}
@GetMapping("/test001")
public String test001() {
return diService.test001("setter");
}
}
试想一下,一旦需要注入的组件很多,那我们会累死的,所以大家都不喜欢用它也是情理之中的事情。
这里有一点需要注意:如果通过set方法注入属性,那么spring会通过默认的空参构造方法来实例化对象,所以如果在类中写了一个带有参数的构造方法,一定要把空参数的构造方法写上,否则spring没有办法实例化对象,导致报错。
总结
这么多的依赖注入方式,我们应该怎么选择呢?那种方式最好呢?
其实,有句古话说的很对,合适自己的才是最好的,我们需要看情况来选择使用哪种注入方式。
使用构造器注入的好处:
- 保证依赖不可变(final关键字)
- 保证依赖不为空(省去了我们对其检查)
- 保证返回客户端(调用)的代码的时候是完全初始化的状态
- 避免了循环依赖
- 提升了代码的可复用性
另外,当有一个依赖有多个实现的使用,推荐使用注解方式注入的方式来指定注入的类型或name,使用setter注入指定类型。这是spring官方博客对setter注入方式和构造器注入的比较。谢谢大家看完了,如果有描述不对的地方欢迎指正,与大家共同进步!