0%

Spring-依赖注入

常用的三种注入方式:

  • field注入
  • setter注入
  • 构造器注入

field注入

直接在需要注入的字段上加上@Autowired或者@Resource

@Autowired

@Autowired为Spring提供的注解,按照类型(byType)装配依赖对象,默认情况下它要求依赖对象必须存在,如果允许null值,可以设置它的required属性为false。如果我们想使用按照名称(byName)来装配,可以结合@Qualifier注解一起使用,如下:

1
2
3
4
5
6
@Autowired
@Qualifier("filed1Service")
private Filed1Service filed1Service;

@Service("filed1Service")
public class Filed1Service {

实现原理

通过反射进行注入,完成自动装配

后置处理器:AutowiredAnnotationBeanPostProcessor

关键:AutowiredFieldElementinject方法

参考资料:

  1. @Autowired注解的实现原理

@Resource

@Resource默认按照ByName自动注入,由J2EE提供,需要导入包javax.annotation.Resource。@Resource有两个重要的属性:name和type,而Spring将@Resource注解的name属性解析为bean的名字,而type属性则解析为bean的类型。所以,如果使用name属性,则使用byName的自动注入策略,而使用type属性时则使用byType自动注入策略。如果既不制定name也不制定type属性,这时将通过反射机制使用byName自动注入策略。

注:最好是将@Resource放在setter方法上,因为这样更符合面向对象的思想,通过set、get去操作属性,而不是直接去操作属性。

1
2
3
4
5
6
private Filed1Service filed1Service;

@Resource(name = "filed1Service")
public void setFiled1Service(Filed1Service filed1Service) {
this.filed1Service = filed1Service;
}

@Resource装配顺序

  1. 如果同时指定了name和type,则从Spring上下文中找到唯一匹配的bean进行装配,找不到则抛出异常。
  2. 如果指定了name,则从上下文中查找名称(id)匹配的bean进行装配,找不到则抛出异常。
  3. 如果指定了type,则从上下文中找到类似匹配的唯一bean进行装配,找不到或是找到多个,都会抛出异常。
  4. 如果既没有指定name,又没有指定type,则自动按照byName方式进行装配;如果没有匹配,则回退为一个原始类型进行匹配,如果匹配则自动装配。

实现原理

通过反射进行注入,完成自动装配

后置处理器:CommonAnnotationBeanPostProcessor

关键:ResourceElementgetResourceToInject方法

总结

优点

  1. 注入方式简单
  2. 整体代码简洁明了

缺点

  1. 对于IOC容器以外的环境,除了使用反射来提供它需要的依赖之外,无法复用该实现类。而且将一直是个潜在的隐患,因为你不调用将一直无法发现NullPointException的存在。
  2. 使用field注入可能会导致循环依赖,不过Spring能解决。

setter注入

直接在需要注入的字段的setter方法上加上@Autowired或者@Resource

在Spring3.x刚推出的时候,推荐使用注入的就是这种,不过现在基本看不到过这种注解方式,写起来麻烦,当初推荐Spring也有他的道理

The Spring team generally advocates setter injection, because large numbers of constructor arguments can get unwieldy, especially when properties are optional. Setter methods also make objects of that class amenable to reconfiguration or re-injection later. Management through JMX MBeans is a compelling use case.

Some purists favor constructor-based injection. Supplying all object dependencies means that the object is always returned to client (calling) code in a totally initialized state. The disadvantage is that the object becomes less amenable to reconfiguration and re-injection.

简单的翻译一下就是:构造器注入参数太多了,显得很笨重,另外setter的方式能用让类在之后重新配置或者重新注入

总结

优点

  1. 相比构造器注入,当注入参数太多或存在非必须注入的参数时,不会显得太笨重
  2. 允许在类构造完成后重新注入

缺点

  1. 代码臃肿
  2. 使用Setter注入可能会导致循环依赖,不过Spring能解决。

构造器注入

先来看看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.

简单的翻译一下:构造器注入的方式能够保证注入的组件不可变,并且确保需要的依赖不为空。此外,构造器注入的依赖总是能够在返回客户端(组件)代码的时候保证完全初始化的状态。

总结

优点

  1. 依赖不可变 components as immutable objects ,即注入对象为final
  2. 依赖不可为空required dependencies are not null,省去对注入参数的检查。当要实例化FooController的时候,由于只有带参数的构造函数,spring注入时需要传入所需的参数,所以有两种情况:1) 有该类型的参数传入 => ok; 2) 无该类型参数传入,报错
  3. 提升了代码的可复用性:非IOC容器环境可使用new实例化该类的对象。
  4. 避免循环依赖:如果使用构造器注入,在spring项目启动的时候,就会抛出:BeanCurrentlyInCreationException:Requested bean is currently in creation: Is there an unresolvable circular reference?从而提醒你避免循环依赖,如果是field注入的话,启动的时候不会报错,在使用那个bean的时候才会报错。

缺点

  1. 当注入参数较多时,代码臃肿。(对于这个问题,也要好好想一想是不是违反了类的单一性职责原则,从而导致有这么多的依赖要注入。)

循环依赖

field注入

使用FiledControllerFiled1ServiceFiled2Service模拟循环依赖,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
@Slf4j
@RestController
public class FiledController {

@Autowired
@Qualifier("filed1Service")
private Filed1Service filed1Service;
@Autowired
private Filed2Service filed2Service;

@GetMapping("/field1")
public String testFiled1(){
return filed1Service.test();
}
@GetMapping("/field2")
public String testFiled2(){
return filed2Service.test();
}
}

@Service("filed1Service")
public class Filed1Service {

@Autowired
private Filed2Service filed2Service;

public String test(){
return getClass().getName() + " call " + filed2Service.name();
}

public String name(){
return getClass().getName();
}
}

@Service
public class Filed2Service {

@Autowired
private Filed1Service filed1Service;

public String test(){
return getClass().getName() + " call " + filed1Service.name();
}

public String name(){
return getClass().getName();
}
}

启动程序发现一切正常

访问接口后一切正常

说明Spring可以解决field属性注入导致的循环依赖

但是如果在Filed1Service、Filed2Service上加上@Scope(“prototype”)后(让单例变成多例,两个类必须都加上),则会启动失败,提示存在循环依赖

setter注入

使用SetterControllerSetter1ServiceSetter2Service模拟循环依赖,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
@Slf4j
@RestController
public class SetterController {
private Setter1Service setter1Service;
private Setter2Service setter2Service;

@Autowired
public void setSetter1Service(Setter1Service setter1Service) {
this.setter1Service = setter1Service;
}
@Autowired
public void setSetter2Service(Setter2Service setter2Service) {
this.setter2Service = setter2Service;
}

@GetMapping("/setter1")
public String testFiled1(){
return setter1Service.test();
}

@GetMapping("/setter2")
public String testFiled2(){
return setter2Service.test();
}
}

@Service
public class Setter1Service {

private Setter2Service setter2Service;

@Autowired
public void setSetter2Service(Setter2Service setter2Service) {
this.setter2Service = setter2Service;
}

public String test() {
return getClass().getName() + " call " + setter2Service.name();
}

public String name() {
return getClass().getName();
}
}

@Service
public class Setter2Service {

private Setter1Service setter1Service;

@Autowired
public void setSetter1Service(Setter1Service setter1Service) {
this.setter1Service = setter1Service;
}

public String test() {
return getClass().getName() + " call " + setter1Service.name();
}

public String name() {
return getClass().getName();
}
}

启动程序发现一切正常

访问接口后一切正常

说明Spring也可以解决Setter注入导致的循环依赖

但是如果在Filed1Service、Filed2Service上加上@Scope(“prototype”)后(让单例变成多例,两个类必须都加上),则会启动失败,提示存在循环依赖:

构造器注入

使用ConstructorControllerConstructor1ServiceConstructor2Service模拟循环依赖,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
@Slf4j
@RestController
public class ConstructorController {

private final Constructor1Service constructor1Service;
private final Constructor2Service constructor2Service;

public ConstructorController(Constructor1Service constructor1Service, Constructor2Service constructor2Service) {
this.constructor1Service = constructor1Service;
this.constructor2Service = constructor2Service;
}

@GetMapping("/constructor1")
public String testConstructor1() {
return constructor1Service.test();
}

@GetMapping("/constructor2")
public String testConstructor2() {
return constructor2Service.test();
}
}

@Service
public class Constructor1Service {

private final Constructor2Service constructor2Service;

public Constructor1Service(Constructor2Service constructor2Service) {
this.constructor2Service = constructor2Service;
}

public String test() {
return getClass().getName() + " call " + constructor2Service.name();
}

public String name() {
return getClass().getName();
}
}

@Service
public class Constructor2Service {

private final Constructor1Service constructor1Service;

public Constructor2Service(Constructor1Service constructor1Service) {
this.constructor1Service = constructor1Service;
}

public String test() {
return getClass().getName() + " call " + constructor1Service.name();
}

public String name() {
return getClass().getName();
}
}

启动项目报错,如下:

解决:在出现循环依赖中的类上使用@Lazy注解(加一个就行),如下:

1
2
3
public Constructor1Service(@Lazy Constructor2Service constructor2Service) {
this.constructor2Service = constructor2Service;
}

再启动项目后一切正常

访问接口后一切正常

在Constructor1Service、Constructor2Service上加上@Scope(“prototype”)后(让单例变成多例,两个类必须都加上),依然可以成功启动,访问接口正常。

总结

  1. Spring能够解决field注入和setter注入导致的循环依赖,无法解决构造器解决的循环依赖。
  2. 使用构造器发生循环依赖,可以使用@Lazy注解解决。

Spring如何解决循环依赖

Spring是先将Bean对象实例化(依赖无参构造函数),然后再设置对象属性。

Spring的循环依赖的理论依据是基于Java的引用传递,当我们获取到对象的引用时,对象的field属性是可以延后设置的(但是构造器必须是在获取引用之前)。

以上面的Filed1ServiceFiled2Service举例,大致步骤如下:

  1. 创建Filed1Service: Filed1Service filed1Service=new Filed1Service ()
  2. 属性注入:发现需要Filed2Service
  3. 创建Filed2Service:Filed2Service filed2Service=new Filed2Service()
  4. 属性注入: 发现需要Filed1Service,此时Filed1Service 已经创建了.只是还没经过各种后置处理器处理,所以Filed1Service 是可以完成属性注入的,只是一个半成品
  5. 之后回到第2步,就可以给A赋值了,循环依赖到此解决

参考资料:

  1. Spring 如何解决循环依赖?
  2. Spring 如何解决循环依赖的问题

@lazy注解如何解决循环依赖

以上面的Constructor1ServiceConstructor2Service举例,大致步骤如下:

  1. 创建Constructor1Service,通过带参构造器创建
  2. 发现构造器中需要Constructor2Service,查询字段constructor2Service的所有注解,如果有@lazy注解,那么就不直接创建Constructor2Service了,而是使用动态代理创建一个代理类Constructor2ServiceProxy
  3. 此时Constructor1ServiceConstructor2Service就不是相互依赖了,变成了Constructor1Service依赖一个代理类Constructor2ServiceProxyConstructor2Service依赖Constructor1Service

可以通过打印类的名字验证这一点:

坚持原创技术分享,您的支持将鼓励我继续创作!