依赖注入(Dependency Injection)Spring Framework最核心的概念之一,通常来说,依赖注入主要分成构造注入(Constructor-based Dependency Injection)setter注入(Setter-based Dependency Injection)两种类型,本文会结合Spring Framkework的发展以及项目中的具体应用,在封装性(Encapsulation)不可变性(Immutability)安全性(Security)以及循环依赖性(Circular Dependencies)等方面来重新探讨这两种依赖注入方式,进而总结出在“新时代”的背景下,如何适当地选择。

谨以此文献给在code review中备受打击的小G同学。。。

篇首语

细说分类

首先说setter注入,这是最常见也是最老牌的一种依赖注入方式,套用Spring官方文档的例子,这种注入方式的特点是先创建实例,再反射调用setter方法注入依赖,

1
2
3
4
5
6
7
8
9
10
11
12
public class SimpleMovieLister {

// the SimpleMovieLister has a dependency on the MovieFinder
private MovieFinder movieFinder;

// a setter method so that the Spring container can inject a MovieFinder
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}

// business logic that actually uses the injected MovieFinder is omitted...
}

而在@Autowired注解的帮助下,setter注入也衍生出了最简化的形式,以上代码直接可以写作下面的形式,我们姑且叫它私有变量注入,这种注入方式的特点是,先创建实例,再反射修改私有变量注入依赖,时至今日,相信很多coder还是非常乐于使用这种依赖注入方式,

1
2
3
4
5
6
public class SimpleMovieLister {

@Autowired
private MovieFinder movieFinder;

}

接下来再说构造注入,这种注入方式更加直接,其特点是,创建实例注入依赖同时进行,都是通过反射调用构造方法完成,

1
2
3
4
5
6
7
8
9
10
11
12
public class SimpleMovieLister {

// the SimpleMovieLister has a dependency on a MovieFinder
private MovieFinder movieFinder;

// a constructor so that the Spring container can inject a MovieFinder
public SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}

// business logic that actually uses the injected MovieFinder is omitted...
}

这里再介绍一种进阶版,采用lombok + private finaal的构造注入才是最常见的写法,我们接下来讨论的构造注入,如不特殊说明,都是指的这种方式,

1
2
3
4
5
6
7
8
9
10
11
12
13
@RequiredArgsConstructor
public class SimpleMovieLister {

// the SimpleMovieLister has a dependency on a MovieFinder
private final MovieFinder movieFinder;

// lombok will help generate this method during compiling time
//public SimpleMovieLister(MovieFinder movieFinder) {
//this.movieFinder = movieFinder;
//}

// business logic that actually uses the injected MovieFinder is omitted...
}

随着Java-based Container Configuration越来越流行,构造注入也有了广义上的版本,我们姑且叫它方法参数注入,与普通构造注入不同的是,依赖是作为反射调用bean创建方法参数(args)注入,而对象实例化过程,是由在方法内部编程实现,而bean的依赖可能是构造注入,也可能是setter注入,看下面的例子,

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Configuration
public class MovieListerConfig {

@Bean
public SimpleMovieLister simpleMovieLister(MovieFinder movieFinder) {
// implemented as constructor-based injection
return new SimpleMovieLister(movieFinder);

// it can be also implemented as setter-based injection
// SimpleMovieLister lister = new SimpleMovieLister();
// lister.setMovieFinder(movieFinder);
// return lister;
}
}

特殊依赖

这里还需要举例说明两种特殊的依赖。

第一种很常见,其依赖不是其他的bean,而是一个外部的值,还以上面的代码为例,

1
2
3
4
5
6
7
8
9
public class SimpleMovieLister {

@Autowired
private MovieFinder movieFinder;

@Value("${app.movie_lister.max_size:100}")
private Integer maxSize;

}

另外一种比较特殊,但有可能在不知不觉中还是接触到了,这个就是循环依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
public class ServiceA {

@Autowired
private ServiceB serviceB;

}

public class ServiceB {

@Autowired
private ServiceA serviceA;

}

这个列子只是最简单的循环依赖,还有更复杂一些的情况,例如ServiceA -> ServiceB -> ServiceC -> ServiceA,这里不再赘述。

对比

我们用一个表格来对比这些注入方式,

对比项 setter注入 私有变量注入 构造注入(private final)
保证封装性? Yes No Yes
保证不可变性? No No Yes
保证安全性? No No Yes
允许循环依赖? Yes Yes No

下面详细解释一下表格内容。

封装性(Encapsulation)是OOP的三大特性之一,这里我们不做展开说明,而setter注入构造注入为何没有破坏封装性,也非常容易理解,那就是Spring是通过反射私有变量访问器(setter方法或构造方法)来完成依赖注入的,完全遵守了封装性的准则。那么问题来了,为什么私有变量注入破坏了封装性?

私有变量注入本质上是通过反射修改私有(private)变量值来完成依赖注入的,完全没有通过私有变量访问器,换句话说,一个私有的变量平白无故地就被修改了值。

为什么“私有变量注入”破坏了封装性?

乍一看,即使封装性被破坏,也没有什么大不了的,从结果上看,未经私有变量访问器修改的值也是符合预期的,但事实上一旦封装性被破坏,不可变性(Immutability)亦不能保证。不可变性(Immutability)也是软件OOP里一个非常重要设计理念,这里我们仍不做展开。但依照上面的表格所示,即使封装性可以保证,仍不能保证一个对象的不可变性。

啰嗦了这么多,无论封装性也好,不可变性也罢,其实都是为了保重代码在处理依赖注入时的安全性(Security)。这里的安全性(Security)是一个相对狭义的概念,其含义可以概括为一点,不可变的依赖,才可能是无害的依赖。说起来可能很抽象,下面用两个逆向场景具体说明,

setter作为访问器,在开放给Spring的同时,也开发给了其他代码。无论善意或恶意,只需要重新调用一下setXXX方法,哪怕传入一个null值,其影响也足够致命。

为什么“setter注入”不安全?

setter注入类似,虽然私有变量注入并没有提供一个setter作为访问器,但通过反射,仍然可以直接修改私有变量的值,在运行时改变依赖。

为什么“私有变量注入”不安全?

以上两个场景概括起来,就是在说明,存在这样或那样的可能性,在运行时(runtime)阶段,依赖是有可能被有意或无意修改,从而导致程序的行为不正确,甚至产生灾难性后果。基于以上分析,也很容易推导出保证运行时安全性的手段,那就是构造注入且私有变量为final,

简而言之,Java语言在语义(semantic)上保证final是不可修改的,所以不会在运行时(runtime)被修改,即使通过反射亦不可能。

为什么“private final”安全?

结论

技术本无对错,只看是否适用于不同的场景,而不同的技术,在不同的历史时期和时代背景下,会进化、发展、演变出不同的方式、流派、体系,Spring Framework亦是如此。

在Spring 2.5.x时代,依赖注入控制反转方兴未艾,广大Java EE的开发人员,还在被EJB折腾的七窍生烟之时,Spring所倡导的这些理念,让从业者有久旱逢甘霖之快感,而受限于当时的技术理念与潮流,仍使用XML作为bean相互依赖的组织媒介,使用setter注入便是“多快好省”的不二法门。

随着时间的推移,人们对依赖注入控制反转理解和实践的逐渐深入,Spring 3.x也呼之欲出。开发者也越发体会到,bean与bean之间的依赖关系,在绝大多数场景下是内联的,是自然的,甚至是与生俱来的,无论你是否用XML等配置方式去描述它,这些bean之间的依赖关系,都在那里,且相对稳定,几乎不会被修改。于是,在这个理念的指导下,才有了bean的自动装配(Autowired),才有了后来Annotation-based Container Configuration,曾经漫山遍野的XML也淹没于扑面而来的@Autowired注解之中,而私有变量注入注入更是让人高潮迭起,如痴如醉,被传统Java EE思想钳制多年的思想也随之解放,以少量的XML加以大量的私有变量注入也蔚然成风,仿佛构造方法和setter都成了明日黄花,不合时宜。

一个好的技术框架,其生命力往往取决于设计者的思想境界。当劳苦大众们在为使用私有变量注入大幅提高生产力而弹冠相庆时,Spring团队却没有停下审视和思考的脚步。于是更加极至的Java-based Container Configuration也随之诞生,并在Spring 5.x后受到Spring官方推崇。有人说Java-based Container是历史的倒退,是Java EE糟粕的复辟,现在看来,不得不佩服Spring设计者独到的眼光和正派的价值观,其终极目标,仍然是以不可变性安全性在程序设计领域重要的作用为导向的,尤其在以线程模型为程序基本运行模型的JVM语言里更是如此,所以依赖注入方式在经过无数的私有变量注入滥用,在肯定bean之间的自然依赖关系的条件下,回归到了private final变量 + 构造注入的方式,继而出现了使用lombok来简化唯一构造方法的终极写法。

拥抱变化,拥抱发展,这才是Spring Framework经久不衰的主要原因。而作为终端用户的我们,除了与时俱进,更要尊重技术发展的潮流,既不能用旧时代的想法和思路来处理新时代问题,也不能用新时代的视角和手段来否定旧时代技术,唯有如此,才是技术人员应有的素质和觉悟。

让我们回归本篇文章的讨论,在新时代的背景下,如何选择依赖注入方式呢?归纳起来,也就是下面几点,

  1. 优先采用private final变量 + 构造注入来注入依赖;
  2. 在没有其他值(依赖)需要注入时,可以考虑用lombok的@RequiredArgsConstructor来简化代码;
  3. 原则上应避免循环依赖,如果实在无法避免,可以适度使用setter注入,但仍应该避免使用私有变量注入
  4. 在Unit Test等场景下,仍然可以使用私有变量注入来简化代码。