2018.8.12 本博客调整为 《第3章·3.2依赖注入容器》
God是简单工具类,而Spring等依赖注入(Dependency Injection、DI)容器则是功能强大的工具箱。某种程度上,God比Spring更优秀,因为它用最简短的代码,言简意赅地说明了God和Spring的相同的本质——针对接口编程的使能工具。
Martin Fowler有一篇非常著名的文章——简单一个网页的PageRank为7,《IoC容器和依赖注入模式》(Inversion of Control Containers and the Dependency Injection pattern[1] )。在该文中,Martin Fowler把一些创建对象的工具箱如PicoContainer和Spring背后的“模式”,称为依赖注入(Dependency Injection、DI)。既然依赖注入已经被业界广为接受,本文也就使用依赖注入容器称呼Spring(的对象创建模块)。必须事先说明的是,笔者不想按照Martin Fowler的文章,逐字逐句地解释该文的哪些是正确的,哪些是错误的。下面将介绍“正确”的知识,在必要的时候指出Martin Fowler文章的错误。
3.2.1 God Vs. Spring
3.2.2注入的方式
3.2.3什么是依赖注入
【2017.12.30:
1.原博客 依赖注入(Dependency Injection)模式 大概的意思有,写得较垃圾:不流畅,例子不好。
2.不再将DI称为一种模式,而仅仅视为工具箱;强调 依赖注入(Dependency Injection)和框架、控制反转IoC,一点关系都没有。
3.例子 不再是A依赖2个具体类,而改成Client→IA→IB;
4.伸手的方式,它不属于正常编程代码,不再更多讨论。所以构造器注入、接口注入不讨论;也不讨论@的应用。
】
God是简单工具类,而Spring等依赖注入(Dependency Injection、DI)容器则是功能强大的工具箱,其核心仍为反射+配置文件。
1. 单层依赖
对于简单地单层依赖,如Client→IServer,Spring显得强大而繁琐。
God使用属性配置文件my.properties保存如下键值对
IIServer =commons.S1
Spring使用XML保存配置文件。
其中
package chap1.init.springDemo;
import commons.*;//IServer
import java.io.IOException;
import org.springframework.context.support.FileSystemXmlApplicationContext;
import org.springframework.beans.factory.BeanFactory;
public class Client {
public static void test() throws IOException {
BeanFactory bf = new FileSystemXmlApplicationContext("spring1.xml");
IServer h = (IServer)bf.getBean("IIServer");
h.doSth();
//比较
h = (IServer)yqj2065.util.God.create("IIServer");
h.doSth();
}
}
2.多层依赖
对于多层依赖场合,例如Client→IA→IB,这里假定Client不需要知道IB。
采用God工具时,通常Client在某个方法中使用God创建IA的对象,调用该对象的setter方法和模板方法;而IA也需要使用God创建相关的IB对象。由于God不支持有参数构造器,各用户需要调用下级的setter方法。
package chap1.init.springDemo;//其他类的包语句略
public interface IB{
public void setName(String name);//
public void b();
}
public class B1 implements IB{
private String name;
@Override public void setName(String name){
this.name =name;
}
public void b(){
System.out.println("B1.name="+name);
}
}
public abstract class IA{
private IB b =(IB)yqj2065.util.God.create("IB");
public final void foo(){//模板方法模式
a();
b.setName("yqj2065");
b.b();
}
public abstract void setId(int id);
public abstract void a();
}
public class A1 extends IA{
private int id;
public void setId(int id){
this.id =id;
}
public void a(){
System.out.println("A1.ID="+id);
}
}
// Client
public static void test2() throws IOException {
ApplicationContext ctx = new FileSystemXmlApplicationContext("spring2.xml");
IAA a0 = (IAA)ctx.getBean("IAA");
a0.a();
//比较
pln("God");
IA a = (IA)yqj2065.util.God.create("IA");
a.setId(5);
a.foo();
}
Spring最引人注目的强大之处,在此得到体现。只需要在Client中使用一次ApplicationContext,其他对象都按照配置文件中指定的类全名和值进行初始化。Client→IAA→IB→IC…更多的依赖层,也全部按照配置文件创建对象。
Spring的核心仍为反射+配置文件。配置文件将指导反射机制来处理相应的类名、setter方法名或有参数构造器。
Spring DI作为创建对象的工具,要求用户按照一些严格地规定,去编写源代码和相应的配置文件。可以把DI的概念形容为:伸手-等待。
1. Setter注入(Setter Injection)
Spring支持的一个规定动作——Setter注入,就是每个被注入的对象由其setter方法加以配置。按照Client→IAA→IB顺序,IAA的、需要被注入的子类如AA1,有两个成员变量IB和int,就需要提供2个设置方法,源代码中设置方法的名字必须是set+xxxx,而xxxx【忽略大小写】将在配置文件中使用。
反过来说,假定配置文件spring2.xml如下
则可以预料,chap1.init.springDemo.AA1源代码中有setIB(IB )和setIiiddd(数字类型)。因为标签
package chap1.init.springDemo;
public interface IAA{
public abstract void a();
}
package chap1.init.springDemo;
public class AA1 implements IAA{
private IB bbb ;
public void setIB(IB b){
bbb =b;
}
private int id;
public void setIiiddd(int id){
this.id =id;
}
public void a(){
System.out.println("AA1.ID="+id);
bbb.b();
}
}
// IB、Client等不变,略
既然Client→IAA→IB,而IAA、IB的对象“自动”创建,设置方法是Setter注入需要的“伸手”行为【值得注意的是,“伸手”代码如setIServer (IServer),虽然看起来很平常,但这些代码不是给Client的用户常规调用的,而是为依赖注入容器如Spring准备的。】。Clien知道IAA对象,但无法调用其设置方法;Clien不知道IB对象,即使IB在源代码中定义设置方法,Clien无法(不应该)获得IB对象地引用。【注意比较Client→IA→IB和Client→IAA→IB代码的不同】
3. “伸手”代码问题
Spring所需的“伸手”代码,在不使用Spring时,应该删除。换言之,“伸手”代码不是作为通常的代码而出现,它干扰了通常的代码。如例程1-9的AA1的setIiiddd(int id),稍微不注意就可能对该无聊的命名重构为setID(int id),但是重构将导致Spring抛出运行时异常,因为Spring按照配置文件找不到setIiiddd(int id)。
使用配置文件最主要的缺陷,在于源代码的重构难以与配置文件同步。
可以使用 @Autowired 注解,提醒程序员这是“伸手”代码,也使得Spring以更多的方式完成注入。
4. Spring等DI容器是工具箱
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);
}
}
对于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,假设Main不操心Client依赖对象的初始化,则
例程 2 9 Spring Vs. God
package init;
public class Client {
private IServer s;
public static void testGod(){
s = (IServer) yqj2065.util.God.create("IServer");
s.doSth();
}
}
package init;
public class Main {
public static void main(String[] args) {
Client c1 = new Client();
c1.testGod();
}
}
testGod(){
s = (IServer) yqj2065.util.God.create("IServer");
s.doSth();
}
}
package init;
public class Main {
public static void main(String[] args) {
Client c1 = new Client();
c1.testGod();
}
}
Main不操心Client所依赖对象的初始化的情况下,Spring Vs. God
(1)Main的代码,使用Spring时,import Spring相关包,调用相关方法获得Client对象,而这个过程就是Client所"等待"的;使用God时,Main不需要God参与。
(2) Client的代码,使用Spring时,需要伸手方法——可以写成setIiiServer(IServers);使用God时,需要赋值语句。
(3) 使用Spring时,反射获得(Client);使用God时,反射获得(IServer);
现在说明使用Spring的细节,Spring可以按照XML配置文件或源代码中的Annotation自动装配。例程2-8中Main显式调用Spring,而Spring的自动装配按照XML配置文件。本例程依赖的配置文件为netbeans 中<项目文件夹>下的client.xml。
其中,
当使用"iiiServer"为name时,Spring要求init.Client中有一个“伸手”方法setIiiServer(IServer s),也就是说,忽略大小写,iiiServer前面加set的方法。
★单就学习设计模式而言,工具类God已经足够。
God能够完成Spring目前的工作(Spring作为庞大的框架,有其他用途),然而,God和Spring的软肋在于,它们在配置文件中需要一个能够绑定的子类型的名字,如果IServer的实现类是匿名类或lambda表达式——这也意味着Main不在意Client的成员变量s是否完成了初始化,Main自己将创建IServer的实现并传递给Client。
此外,对于SortTest拥有的static成员IntSort,Spring的注入,显得难看。
因此,很多时候可以选择参数传递方式。特别是Java8后,函数接口的实参用lambda表达式。
<遗留文字>
站在Client的角度,Client接受注入有3种的方式。
package principle;
public class Client{
private IServer s;
public Client(IServer s){ this.s = s; }
public void set IServer(IServer s){ this.s = s; }
}
1. 构造器注入(Constructor Injection):Client提供构造器public Client (IServer s),等待外界创建IServer的(实现类的)对象后将其引用传递进来/注入。
2. Setter注入(Setter Injection):Client提供设置方法如setIServer (IServers),等待注入。
3.接口注入,相当于将Setter注入的setIServer (IServer s) 方法封装到一个专用的接口如InjectIServer中,而Client实现InjectIServer并给出如下的方法体。
@Override public void setIServer (IServer s){
this.s=s;
}
接口注入针对的场景是,有大量Client、Client1等都需要依赖于IServer。
public interface InjectIServer{
public void setIServer (IServer s);
}
代码中使用了[4.1虚域模式]。然而这一方式并不一定被各种依赖注入容器所支持。
1.2.2 设计注射器
Client伸手-等待,注入器如何设计呢?
注入器可以设计为下层包中的一个工具类如tool.God,或者更为强大的依赖注入容器,如Spring、PicoContainer等。
任何应用程序App都可以用God作为注射器。因而,依赖注入通常意味着某注射器使用反射机制创建对象。换言之,注射器通常使用反射机制创建对象,以作为通用工具。
代码中,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容器中直接获得一个对象然后直接使用,无需事先创建它们”。
这是什么话?使用对象之前必须创建,现在我们仍然遵循这个定律!
*/
链接: