Spring等依赖注入(Dependency Injection、DI)容器则是功能强大的工具箱。某种程度上,God比Spring更优秀,因为它用最简短的代码,言简意赅地说明了God和Spring的相同的本质,即针对接口编程的使能工具。
Martin Fowler有一篇非常著名的文章——简单一个网页的PageRank为7,《IoC容器和依赖注入模式》(Inversion of Control Containers and the Dependency Injection pattern )。在该文中,Martin Fowler把一些创建对象的工具箱如PicoContainer和Spring背后的“模式”,称为DI。既然术语DI已经被业界广为接受,本文将使用Spring DI称呼Spring(的对象创建模块)。本节还将指出Martin Fowler的该文章的不妥之处。
【最后更新2021.3.1 返回目录
3.2.1 Spring DI的注入方式
1.下载Spring Framework
2.单层依赖
对于简单地单层依赖,如Demo→IMan,使用Spring DI显得大炮打蚊子,它与God相比,没有什么优势。
God使用属性配置文件my.properties保存如下键值对:
aMan=chap3.init.Son
而Spring使用XML保存配置文件,如spring1.xml如下。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd">
<bean id="aMan" class="chap3.init.Son" />
</beans>
其中<bean>的属性id表示源代码中使用的键,而属性class表示将创建对象的类全名。从例程3-5源代码中可以看出,Spring从(BlueJ项目根目录下的) spring1.xml中获得键aMan的值,并创建其对象。
Spring最引人注目的强大之处,在多层依赖场合得到体现。假定Demo→IMan<= Hubby→IWoman,而IWoman的子类有妈妈/Mom和姐妹/Sister等。
在多层依赖场合,目前的God,在每一层依赖关系中,都需要使用一次。比如说Demo的main方法中使用God创建某个IMan的对象;而Hubby类体中有成员变量IWoman,也需要使用God创建某个相关的IWoman对象。
Spring DI的强大之处,在于Demo→IMan<= Hubby→IWoman…更多的依赖层,Spring能够按照配置文件一次性地创建各层级的对象,Spring DI只需要在Deme中使用一次ApplicationContext,其他对象都按照配置文件中指定的类全名和值进行初始化。站在Demo的位置看,ApplicationContext和God的地位相当,使用两种工具没有多大的区别。但是,站在Hubby的位置看Hubby→IWoman关系,Hubby代码中既没有new某个IWoman,也不像God那样显式地创建某个IWoman,而Spring DI魔术般地就建立了Hubby依赖的某个IWoman对象。
★依赖注入指类A所依赖的对象,(从A的源代码上看)在不知不觉中被创建。
完成依赖注入工作的模块称为依赖注入容器。Spring DI的核心仍为反射+配置文件,这般魔术是如何实现的呢?由于类A不显式地调用依赖注入容器——真正体现容器的注入能力,因此该容器必须首先创建类A以及它依赖的对象如B,再将B赋值给A的某个成员变量。所以,依赖注入工具需要是一个容器以保存A和B的对象。
按照上述依赖注入定义,Spring DI在单层依赖中,与God一样被显式地调用,这种工作方式被称为服务定位器(Service Locator)。Spring DI在多层依赖中,才体现为DI容器。
对于服务定位器或DI容器的划分,是按照一个工具所起的作用来区别。目前的God只是服务定位器,在下一节我们将God升级为DI容器;而Spring DI如同手机,在拍照时看作相机(服务定位器),通常作为手机(DI容器)。
下面先看看Spring DI如何完成依赖的注入。
【2017.12.30:
1.原博客 依赖注入(Dependency Injection)模式 大概的意思有,写得较垃圾:不流畅,例子不好。
2.不再将DI称为一种模式,而仅仅视为工具箱;强调 依赖注入(Dependency Injection)和框架、控制反转IoC,一点关系都没有。
3.例子 不再是A依赖2个具体类,而改成Demo→IMan<= Hubby→IWoman
4.伸手的方式,它不属于正常编程代码,不再更多讨论。所以接口注入不讨论;升级God时讨论@的应用。
】
4.依赖的注入
Spring DI作为创建对象的工具,要求用户按照一些严格地规定,去编写源代码和相应的配置文件。这里把DI的概念形容为:伸手-等待。
- 等待,意味着所有对象的创建,等待那一次ApplicationContext的出现;
- 伸手,意味着当程序员需要使用Spring DI工具来创建某个对象时,该对象的具体类型(如IMan的子类老公/Hubby、IWoman的子类Sister等),要按照注入器Spring的“规定动作”提供“伸出手来”与Spring握手的代码。伸手代码不属于常规代码。
①Setter注入(Setter Injection)
1. Setter注入(Setter Injection)
Spring支持的一个规定动作为Setter注入,即通过setter方法完成注入。例如Spring准备创建对象的具体类型Hubby有两个成员变量IWoman和int,其源代码中就需要提供2个setter方法,必须是set+xxxx,而xxxx(忽略大小写)将在配置文件中使用。该setter方法不属于常规代码,而是“伸手”代码。
反过来推算,假定配置文件spring2.xml如下
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"
>
<bean id="aMan" class="chap3.init.Hubby">
<property name="IWoman" ref="next"/>
<property name="iiiddd" value="100" />
</bean>
<bean id="next" class="chap3.init.Mom" scope="singleton">
<property name="name" value=" queen"/>
</bean>
</beans>
则按照Spring的要求,第一个标签<bean>的属性class的值即chap3.init.Hubby,在其源代码中,必须有标签<property >的属性name所指定的设置方法,每一个name属性表示有一个set+(name值)的设置方法,即Hubby必须有setIWoman(IWoman)和setIiiddd (数字类型) 2个设置方法。<property name="Iiiddd" value="100" />中,属性value的值,为Java基本类型或String等变量进行初始化。在<property name="IWoman" ref="next"/>中,属性ref,指向本配置文件中另外一个<bean>标签,后者的 id的值匹配前者ref的值,此时为next。同样,它必须有一个setIName()方法。
现在观察例程3-6的源代码。
package chap3.init;//其他类的包语句略
public interface IWoman{
default public void setName(String name){}//参考8.1 虚域模式
abstract public void say();
}
public class Mom implements IWoman{
private String name;
@Override public void setName(String name){ //伸手方法
this.name =name;
}
@Override public void say(){
System.out.println("Mom.name="+name);
}
}
public class Hubby implements IMan{
private IWoman woman ;
public void setIWoman(IWoman woman){// 伸手方法
this.woman =woman;
}
private int id;
public void setIiiddd(int id){//伸手方法
this.id =id;
}
@Override public void foo(){
//System.out.println("Hubby");
System.out.println("Hubby.ID="+id);
woman.say();
}
}
//Demo
public static void testSpring2() throws IOException {
ApplicationContext ctx = new FileSystemXmlApplicationContext("spring2.xml");
IMan man = (IMan)ctx.getBean("aMan");
man.foo();
}
运行输出:
Hubby.ID=100
Mom.name= queen
当Demo按照配置文件spring2.xml创建ApplicationContext对象后,Demo显式地获得一个IMan对象的引用(指向Hubby),完成Demo→IMan的依赖;同时,Spring“自动地”为Hubby对象设置好成员变量,包括IWoman woman和int id。为什么setter方法不属于常规代码?因为Demo仅仅依赖IMan,IMan没有setter方法,Demo不可能去调用这些不存在的代码; IMan的子类Hubby中“伸手”代码setIWoman(IWoman)和setIiiddd (数字类型)是为Spring准备的,在阅读源代码时需要清醒地知道这一点。
②构造器注入
依赖注入容器如Spring支持的规定动作,除了Setter注入,还可以是构造器注入。例如IWoman的子类Sister有3个域String name、String address和int age。Sister的构造器有3个参数。修改配置文件spring2.xml,另存为spring3.xml,在其中添加Sister对象创建的信息如下所示并保存。
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans-2.5.xsd"
>
<bean id="aMan" class="chap3.init.Hubby">
<property name="IWoman" ref="next"/>
<property name="iiiddd" value="100" />
</bean>
<bean id="next" class="chap3.init.Sister" >
<constructor-arg value="Princess"/>
<constructor-arg type="java.lang.String" value="1101"/>
<constructor-arg value=" 18"/>
</bean>
</beans>
Demo. testSpring2()中将spring2.xml修改spring3.xml,再次运行,Spring分别用Setter注入和构造器注入创建了Hubby和Sister的对象。
5. 关于“伸手”代码
Spring所需的“伸手”代码,是按照Spring的“规定动作”提供的、与Spring握手的代码,伸手代码专供Spring使用,不属于常规代码。
既然是规定动作,“伸手”代码必须匹配相对应的配置文件,如例程3-6中Hubby的setIiiddd(int id)方法。稍微不注意程序员就可能对该无聊的命名重构为setID(int id),但是重构将导致Spring抛出运行时异常,因为Spring按照配置文件找不到setIiiddd(int id)。“伸手”代码不是作为通常的代码而出现,它干扰了其他程序员对源代码的阅读,因为这些“伸手”代码不应该作为接口的一部分,或者说不应该强迫应用程序员去了解它们。
3.2.2 增强的God
抽象依赖原则的使能工具God、Spring DI模块其实并不神秘,并不需要高大尚的术语来介绍。依赖注入所谓的在不知不觉中完成依赖关系的建立,不过是xml文件较属性配置文件强大带来的,因为xml文件可以将层级依赖关系表示出来,再通过反射机制去创建各级对象。很多人夸大DI的魔力,甚至用胡言乱语的控制反转(Inversion of Control)来渲染这种魔力。
DI的应用中,有一些细节需要讨论。①配置文件问题;②容器与注入。本节用最简单的代码增强的God,最简单的代码在于说明问题的本质,为了避免复杂,该代码将有很多的不足。
1. 配置文件的利弊
为了依赖抽象类型,通过配置文件决定创建对象的具体类型,是使能工具的基本特点。在代码不需要重新编译情况下,可以动态地修改/变换配置文件从而将抽象类型的子类型连入程序,而这些子类型可能是原有代码编译期之后加入的;配置文件也使得替换子类型十分简单,例如在不同的场合以不同的方式部署数据库系统。
但是,配置文件的缺点也比较明显:①IDE难以支持源代码和配置文件的联动,特别是在系统开发过程中,如果重构某个类的名字,就必须到相应的配置文件中手工修改对应的字符串,IDE无法验证配置项的正确性;或者在编写一些测试代码时,程序员需要在源代码和配置文件之间跑来跑去。②使软件变得复杂。开发人员不得不同时维护代码和配置文件,调试变得困难,往往配置方面的一个手误会导致莫名其妙的错误,配置文件中任何变动(错误)都可能导致整个系统无法工作;配置文件过多还会导致系统管理、部署等变得困难;③在应用程序开发时,程序员还需要注意,软件的用户可能对编程一无所知,也不知道如何编写配置文件。此时不要让应用程序的用户,如七老八十的爹爹去设置配置文件。
因此,当不适合使用配置文件时,通过源代码来配置,有两个方案,其一是使用简单工厂模式,其二是使用Java注解/标注(Annotation)以避免维护配置文件。增强God将使用Java注解取代配置文件。
2. Java注解简介
注解是为Java代码中的类、方法、变量、参数和包等添加的一种@开头的标注,如@Override等,可以通过反射获取注解的内容。JDK中除了提供前面已经多次使用的注解,例如@Override、@FunctionalInterface之外,还提供了用于程序员自定义注解的元标注(meta-annotation)。
- @Target,说明一个自定义注解的适用场合/目标,它将在源代码中用来修饰什么。Target的取值由枚举java.lang.annotation.ElementType限定,包括ANNOTATION_TYPE , CONSTRUCTOR , FIELD , LOCAL_VARIABLE , METHOD,PACKAGE , PARAMETER , TYPE和TYPE_PARAMETER。
- @Retention,说明被修饰的自定义标注何时发挥作用(或保留策略),取值由RetentionPolicy枚举。SOURCE只会保留在程序源码里,或者说仅对编译器有效,如@Override供编译器检查;默认或指定为CLASS时,指定该标注写入class文件,但是不会把这些信息加载到JVM中;RUNTIME,表示可以通过反射机制获得该标注。
仍然用前面的例子,Demo→IMan→IWoman,假定IMan的子类Son有域IWoman woman。自定义的注解@Inject("chap3.init.Mom")将用于标记域,其后的值指定要创建的对象类型;然后在Son的构造器中调用God1的create()按照@Inject的值创建IWoman对象。这一简单的方案如例程3-7所示,演示了Java注解取代配置文件。
package chap3.init.godplus;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Inject {
String value() default "";
}
package chap3.init;
import chap3.init.godplus.God1;
import chap3.init.godplus.Inject;
public class Son implements IMan{
@Inject("chap3.init.Mom") private final IWoman mom; //@Inject
public Son(){
mom = (IWoman)God1.create(Son.class) ; // God1
}
public void foo(){
System.out.println("Son");
mom.say();
}
}
//Demo
public static void testGodplus() {
IMan man = new Son();
man.foo();
}
package chap3.init.godplus;
import java.lang.annotation.*;
import java.lang.reflect.*;
public class God1 {
private God1() { }
/**
* 解析clazz 的类中的@Inject(typeName),按照typeName指定的类全名创建对象。
*
* @param clazz 含有@Inject的类
* @return Inject标记的对象
*/
public static Object create(Class<?> clazz) {
Object r = null; //不支持多个@Inject
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
Annotation[] annotations = field.getDeclaredAnnotations();
for (Annotation a : annotations) {
if (a instanceof Inject) {
Inject inj = (Inject) a;
String typeName = inj.value();
try {
r = Class.forName(typeName).newInstance();
} catch (ClassNotFoundException ex) {
} catch (InstantiationException ex) {
} catch (IllegalAccessException ex) {
}
}
}
}
return r;
}
}
God0的不足,包括
- 需要给@Inject一个值以取代配置文件中的键值对;字符串常量"chap3.init.Mom"仍然是硬编码
- 不支持Son类中使用多个@Inject; Son类中需要在构造器中直接调用God0的create()。
- Demo→Son。
2.容器与注入
<遗留文字>
Spring与God一样,仅仅是一个被调的工具库。框架是一个骨架式方案,应用程序员至少需要编写框架的@Override方法以提供代码支持。Spring不是框架,而是完整的方案。
Martin Fowler在Inversion of Control Containers and the Dependency Injection pattern中提出了依赖注入/DI,写到「控制反转是框架所共有的特征,如果仅仅因为使用了控制反转就认为这些轻量级容器与众不同,就好象在说"我的轿车是与众不同的,因为它有四个轮子"」,但他强化大众印象——DI容器是框架。
事实上,Spring DI容器作为ADP的使能工具【依赖注入的意义,在于保证Client仅仅与(通常是接口或抽象类)IServer耦合,而不与IServer的子类型耦合,使得程序符合OCP或依赖于抽象类型原则。】,仅仅是工具箱。它和框架、控制反转IoC,一点关系都没有。它完全是一个自行车,只有两个轮子。
后头看例程2-4,客户类TestSortor依赖抽象类型IntSort,但是TestSortor赖皮地不考虑创建IntSort的对象,而且有两种形式:
等待注入。
参数传递。
public class TestSortor {
public static void test(int[] array,IntSort s){//IntSort作为参数
array = s.sort(array);
pln(array);
}
//或者,依赖传入
private static IntSort s;
public static void setSortor(IntSort s){
TestSortor.s =s;
}
public static void test(int[] array){
array = s.sort(array);
pln(array);
}
}
2.2.3 依赖注入模式
对于Client依赖于IServer,Client可以赖皮地不考虑创建IServer的对象,而将IServer初始化的工作交给依赖注入容器。
站在Client的角度,依赖注入(Dependency Injection、DI) 的概念很简单,简言之:伸手-等待。
(1) Client的类体中没有初始化IServer变量的代码,它不想自己创建IServer对象,而是提供public构造器Client(IServer)或设置方法setIServer (IServer)等。是为伸手;
(2) 坐等外界为其初始化IServer对象。是为等待。
值得注意的是,“伸手”代码如setIServer (IServer),虽然看起来很平常,但这些代码不是给Client的用户常规调用的,而是为依赖注入容器如Spring准备的。
★单就学习设计模式而言,工具类God已经足够。
God能够完成Spring目前的工作(Spring作为庞大的框架,有其他用途),然而,God和Spring的软肋在于,它们在配置文件中需要一个能够绑定的子类型的名字,如果IServer的实现类是匿名类或lambda表达式——这也意味着Main不在意Client的成员变量s是否完成了初始化,Main自己将创建IServer的实现并传递给Client。
此外,对于SortTest拥有的static成员IntSort,Spring的注入,显得难看。
因此,很多时候可以选择参数传递方式。特别是Java8后,函数接口的实参用lambda表达式。
<遗留文字>
1.2.2 设计注射器
代码中,IServer对象的创建使用了tool.God的静态方法create(), create()不过是一个使用反射+属性配置文件(.properties文件)创建对象的静态工厂。
package creational.di;
import tool.God
public class App{ //Injection
public static void test(){
IServer s = (IServer) God.create("1-6"); //1-6 = creational.di.Server
Client c = new Client();
c.setIServer(s);//注入
c.show();
}
}
站在Client的角度,依赖注入模式等待外界创建并传入对象。
3. 但是,比工具类God更为强大的依赖注入容器,如Spring、PicoContainer等,它们认为使用/依赖关系是面向对象编程的最基本的程序结构,各种各样的使用关系如Client与IServer、C与S等等广泛存在,作为一个依赖注入的工具或框架,希望程序员不再编写如下代码:
各种用于依赖注入的专用框架被开发出来如Spring等,它们被称为依赖注入或控制反转容器(DI/IoC Container)。
依赖注入模式和依赖注入容器、设计依赖注入容器所使用的技术(回调机制或控制反转)是3个东西,虽然密切相关——像爸爸、妈妈和孩子一样密切。
/*请注意,至少到目前为止,我们不需要任何特别的术语——依赖倒置原则DIP和控制反转IoC,而此时,我会将依赖注入(Dependency Injection)作为一种设计模式。(也就是说,依赖注入与IoC不是同一个概念)*/
/*[吐槽]在我眼里,目前常见的两个术语依赖倒置原则DIP和控制反转IoC,基本上是没有价值的术语。(因为有回调这个术语足以),我把作为依赖注入模式和设计依赖注入容器所使用的技术区别开来。更重要的原因,设计依赖注入容器所使用的技术,或者说,设计任一框架所使用的技术就是回调。凭什么要把依赖注入容器称为回调容器或控制反转容器。既然回调是一个常用术语,控制反转作为回调机制的同义词就没有什么价值*/
下面说明spring的两个用法:
1.如同God,仅仅作为一个利用反射+配置文件来创建对象的工具类。
2.按照XML配置文件自动装配——反映出依赖注入容器比God牛x之处。
早期网络文章【Ioc容器的革命性优点】写道:“我们知道,在Java基本教程中有一个定律告诉我们:所有的对象都必须创建;或者说:使用对象之前必须创建,但是现在我们可以不必一定遵循这个定律了,我们可以从Ioc容器中直接获得一个对象然后直接使用,无需事先创建它们”。
这是什么话?使用对象之前必须创建,现在我们仍然遵循这个定律!不过使用工具。
链接:
- Martin Fowler Inversion of Control Containers and the Dependency Injection pattern 中文
- InversionOfControl
- Shivprasad koirala, 12 Jun 2010 Design pattern – Inversion of control and Dependency injection 好多图。