依赖注入(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 | public class SimpleMovieLister { |
而在@Autowired
注解的帮助下,setter注入
也衍生出了最简化的形式,以上代码直接可以写作下面的形式,我们姑且叫它私有变量注入
,这种注入方式的特点是,先创建实例
,再反射修改私有变量
注入依赖,时至今日,相信很多coder还是非常乐于使用这种依赖注入方式,
1 | public class SimpleMovieLister { |
接下来再说构造注入
,这种注入方式更加直接,其特点是,创建实例
与注入依赖
同时进行,都是通过反射调用构造方法
完成,
1 | public class SimpleMovieLister { |
这里再介绍一种进阶版,采用lombok
+ private finaal
的构造注入才是最常见的写法,我们接下来讨论的构造注入
,如不特殊说明,都是指的这种方式,
1 |
|
随着Java-based Container Configuration越来越流行,构造注入
也有了广义上的版本,我们姑且叫它方法参数注入
,与普通构造注入
不同的是,依赖是作为反射调用bean创建方法
的参数(args)
注入,而对象实例化过程,是由在方法内部编程实现,而bean的依赖可能是构造注入
,也可能是setter注入
,看下面的例子,
1 |
|
特殊依赖
这里还需要举例说明两种特殊的依赖。
第一种很常见,其依赖不是其他的bean,而是一个外部的值,还以上面的代码为例,
1 | public class SimpleMovieLister { |
另外一种比较特殊,但有可能在不知不觉中还是接触到了,这个就是循环依赖
,
1 | public class 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
作为访问器,但通过反射
,仍然可以直接修改私有变量的值,在运行时改变依赖。
以上两个场景概括起来,就是在说明,存在这样或那样的可能性,在运行时(runtime)
阶段,依赖是有可能被有意或无意修改,从而导致程序的行为不正确,甚至产生灾难性后果。基于以上分析,也很容易推导出保证运行时安全性的手段,那就是构造注入且私有变量为final
,
简而言之,Java语言在
语义(semantic)
上保证final
是不可修改的,所以不会在运行时(runtime)
被修改,即使通过反射
亦不可能。
结论
技术本无对错,只看是否适用于不同的场景,而不同的技术,在不同的历史时期和时代背景下,会进化、发展、演变出不同的方式、流派、体系,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经久不衰的主要原因。而作为终端用户的我们,除了与时俱进,更要尊重技术发展的潮流,既不能用旧时代的想法和思路来处理新时代问题,也不能用新时代的视角和手段来否定旧时代技术,唯有如此,才是技术人员应有的素质和觉悟。
让我们回归本篇文章的讨论,在新时代的背景下,如何选择依赖注入方式呢?归纳起来,也就是下面几点,
- 优先采用
private final变量 + 构造注入
来注入依赖; - 在没有其他
值(依赖)
需要注入时,可以考虑用lombok的@RequiredArgsConstructor
来简化代码; - 原则上应避免
循环依赖
,如果实在无法避免,可以适度使用setter注入
,但仍应该避免使用私有变量注入
; - 在Unit Test等场景下,仍然可以使用
私有变量注入
来简化代码。