3.2依赖注入容器

本文深入探讨了Spring的依赖注入(DI)容器,解释了DI的定义和核心思想。通过对比God工具和Spring,展示了DI在多层依赖中的优势。Spring DI通过setter注入和构造器注入来实现依赖注入,而配置文件和注解在其中起到了关键作用。作者还批评了过度渲染DI和IoC概念的现象,并强调DI是一种设计模式,而非框架。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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的值,并创建其对象。

3.多层依赖

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容器中直接获得一个对象然后直接使用,无需事先创建它们”。

这是什么话?使用对象之前必须创建,现在我们仍然遵循这个定律!不过使用工具。

链接:

  1. Martin Fowler Inversion of Control Containers and the Dependency Injection pattern  中文
  2. InversionOfControl
  3. Shivprasad koirala, 12 Jun 2010 Design pattern – Inversion of control and Dependency injection 好多图。

 

评论 5
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值