使用Spring整合框架及银行业务简介(蓝桥杯软件大赛培训教材-Java方向)
上QQ阅读APP看本书,新人免费读10天
设备和账号都新为新人

1.4 依赖注入

一个Web应用系统不可能只含有一个对象(或者说bean),即使最简单的系统也需要多个对象协同工作。接下来的内容将阐释如何定义一系列bean,并让它们一起协同工作。

1.4.1 基本依赖注入

● 依赖注入概念

依赖注入(Dependency Injection)和控制反转本质上表示同一个概念,其具体含义是:当某个角色(可能是一个Java实例,调用者)需要另一个角色(另一个Java实例,被调用者)的协助时,在传统的程序设计中,通常由调用者来创建被调用者的实例。但在Spring中,创建被调用者的工作不再由调用者来完成,而是由IOC容器来完成,所以称之为控制反转,然后注入调用者,因此也称为依赖注入。

再用一个通俗的例子来说明什么是依赖注入。在这个例子里面有一个调用者:一个人,一个被调用者:斧子。原始社会里,几乎没有社会分工,需要斧子的人(调用者)只能自己去造一把斧子(被调用者)。对应的为Java程序里的调用者自己创建被调用者。

进入工业社会,工厂出现了,斧子不再由需要斧子的人生产,而是在工厂里被生产出来了。此时需要斧子的人(调用者)找到工厂,购买斧子,并且无须关心斧子的制造过程,对应Java程序的简单工厂设计模式。

进入按需分配的社会,需要斧子的人不需要找到工厂,只要发出一个需要斧子的简单指令,斧子就会出现在他面前,对应的情形为Spring框架的依赖注入。

针对这个场景,我们进行抽象,并完成如图1.3所示的设计。

图1.3 人与斧子类图

程序具体代码如下:

    //斧子
    public interface Axe {
        //砍
        public String chop();
    }
    //石斧
    public class StoneAxe implements Axe {
        @Override
        public String chop() {
            return "石斧砍柴好慢";
        }
    }
    //钢斧
    public class SteelAxe implements Axe {
        @Override
        public String chop() {
            return "钢斧砍柴真快";
        }
    }
    //人
    public interface Person {
        //使用斧子
        public void useAxe();
    }
    //中国人
    public class Chinese implements Person{
        private Axe axe;
        public Chinese() {
            super();
            this.axe = new StoneAxe();
        }
        @Override
        public void useAxe() {
            System.out.println(axe.chop());
        }
        public Axe getAxe() {
            return axe;
        }
        public void setAxe(Axe axe) {
            this.axe = axe;
        }
    }

分析上述代码,关键点在Chinese类的构造方法中。这里的实现就像原始社会的场景一样,调用者手工打造了一把石斧。如果某一天,要将石斧替换为钢斧,或者其他类型的斧头,我们必须进入Chinese类内部来修改源代码。进入工业社会之后,相关代码替换为通过工厂获得斧子实例,这样虽然解除了调用者和斧子的耦合,但是调用者又和特定的工厂耦合在了一起。进入按需分配的社会,IOC容器会将被调用者实例(一把斧子)注入到调用者(一个人),实现了解耦。IOC容器主要有两种注入方式,即构造器注入和Setter注入。

● 构造器注入

基于构造器的依赖注入通过调用带参数的构造器来实现,每个参数代表着一个依赖。

(1)首先改写Chinese类,具体代码如下:

    public class Chinese implements Person{
        private Axe axe;
        public Chinese() {
            super();
        }
        public Chinese(Axe axe) {
            super();
            this.axe = axe;
        }
        @Override
        public void useAxe() {
            System.out.println(axe.chop());
        }

在该代码中,调用者(Chinese)不需要创建Axe实例,使用一个构造器准备接收Axe实例,虚位以待。

(2)使用配置文件描述依赖关系

    <bean id="stoneAxe" class="org.bd.spring.ch01.di.StoneAxe"></bean>
    <bean id="steelAxe" class="org.bd.spring.ch01.di.SteelAxe"></bean>
    <bean id="chinese" class="org.bd.spring.ch01.di.Chinese">
        <constructor-arg index="0" ref="stoneAxe"></constructor-arg>
    </bean>

上面的配置信息中,第3行定义了之前提到的调用者,第4行采用构造器注入的方式将标识为stoneAxe的bean实例注入给标识为Chinese调用者实例。

constructor-arg元素代表着一个构造参数,index属性指定参数的位置,0代表第一个参数,在本例中实际上可以省略,ref属性指定了被注入bean的id。

(3)运行测试

下面可以写一个简单的main()方法进行测试。

    public class Main {
        public static void main(String[] args) {
            ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
            Person person = context.getBean("chinese", Person.class);
            person.useAxe();
        }
    }

输出结果为“石斧砍柴好慢”。如果想要给调用者注入不同类型的实例,只需修改配置文件,例如:

    <bean id="chinese" class="org.bd.spring.ch01.di.Chinese">
        <constructor-arg index="0" ref="steelAxe"></constructor-arg>
    </bean>

源代码不做任何修改,输出结果为“钢斧砍柴真快”。

● Setter注入

构造器注入是在实例化对象时,将主对象依赖的对象作为参数传递给构造器。而Setter注入是在实例化完成之后调用Setter方法,将主对象依赖的对象作为参数传递给Setter方法。后者要求主对象有无参构造器,且提供对应的Setter方法。现在的Chinese类已满足这样的要求,那么需要变动的只是配置文件中的内容。

    <bean id="chinese" class="org.bd.spring.ch01.di.Chinese">
        <property name="axe" ref="stoneAxe"></property>
    </bean>

该配置文件的含义是,使用id为stoneAxe的bean给Chinese实例的axe属性赋值。在理解了注入的本质和构造器注入之后,理解Setter注入就非常简单,大家可变换被注入的bean的id,自行测试运行结果。

1.4.2 更多依赖注入的细节

上面的例子是通过构造器注入或Setter注入将一个被调用者bean实例注入到一个调用者bean实例。依赖注入不仅仅支持将一个bean实例注入到另一个bean实例,还可以将基本数据类型、String类型、集合等类型注入到调用者bean实例中,下面对这些不同的注入类型逐一进行介绍。

● 基本数据类型和String类型

在构造器注入或Setter注入中,可以通过value属性给property和constructor-arg元素指定字符串值,将字符串注入到调用者bean实例中。如果调用者bean中的数据类型为基本数据类型,则Spring会帮我们将字符串转换为基本数据类型。下面是配置DBCP(一个数据库连接池框架)的dataSource的实例。

    <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource">
        <property name="driverClassName" value="oracle.jdbc.OracleDriver"></property>
        <property name="url" value="jdbc:oracle:thin:@localhost:1521:XE"></property>
        <property name="username" value="oa"></property>
        <property name="password" value="oa123"></property>
    </bean>

测试代码如下:

    //加载beans.xml,实例化容器
    ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
    DataSource ds = context.getBean("dataSource", DataSource.class);
    try {
        System.out.println(ds.getConnection());
    } catch (SQLException e) {
        e.printStackTrace();
    }

注意:该段代码的运行依赖commons-dbcp-x.x.jar、commons-pool-x.x.jar和数据库驱动包。

● 集合类型

在配置文件中通过使用list、set、map及props元素,可以向调用者bean中注入List、Set、Map及Properties类型的对象。例如有这样一个类,它依赖各种集合类型的对象,类定义如下:

    public class ExampleBean3 {
        private Properties emails;
        private List<String> stringList;
        private List<ExampleBean> beanList;
        private Map<String, String> someMap;
        private Set<String> someSet;
        //省略getter&setter方法
        @Override
        public String toString() {
            return "ExampleBean3 :\n[emails=" + emails + "\n stringList="
                + stringList + "\n beanList=" + beanList + "\n someMap="
                + someMap + "\n someSet=" + someSet + "]";
        }
    }

在配置文件中为ExampleBean3类对象注入其依赖的集合对象。

    <bean id="expBean3" class="org.bd.spring.ch01.collectios.ExampleBean3">
        <property name="emails"><!--emails属性是Properties类型 -->
            <props>
                <prop key="administrator">administrator@example.org</prop>
                <prop key="support">support@example.org</prop>
                <prop key="development">development@example.org</prop>
            </props>
        </property>
        <property name="stringList"><!--stringList属性是List<String>类型 -->
            <list>
                <value>IOC</value>
                <value>DI</value>
                <value>Spring</value>
            </list>
        </property>
        <property name="beanList"><!--beanList属性是List<ExampleBean>类型 -->
            <list>
                <ref bean="expBean"/><!-- 第一个元素,引用bean作为List的元素 -->
                <bean class="org.bd.spring.ch01.ExampleBean"></bean><!-- 第二个元素,匿名bean-->
            </list>
        </property>
        <property name="someMap"><!--someMap属性是Map<String,String>类型 -->
            <map>
                <entry><!-- 第一个元素 -->
        <key><value>admin</value></key><!-- 键 -->
        <value>administrator@example.org</value><!-- 值 -->
                </entry>
                <entry><!-- 第二个元素 -->
        <key><value>support</value></key>
        <value>support@example.org</value>
                </entry>
            </map>
        </property>
        <property name="someSet"><!--someSet属性是Set<String>类型 -->
            <set>
                <value>JavaSE</value>
                <value>JavaEE</value>
                <value>JavaME</value>
            </set>
        </property>
    </bean>

向调用者对象中注入集合类型对象时,实际上是先往集合里添加元素,再将集合对象注入给调用者。添加元素时,应当注意集合上的泛型,除Properties较为特殊(键和值都是字符串类型的Map)外,对于元素类型是基本类型或者字符串类型的,可以用<value>标签来表示一个元素,对于元素类型是引用类型的,可以使用<ref bean="…"/>引入一个已经定义好的bean,或者使用<bean>标签创建一个bean。

测试代码如下:

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("beans.xml");
        ExampleBean3 bean = context.getBean("expBean3",ExampleBean3.class);
        System.out.println(bean);
    }

运行结果为:

    ExampleBean3 :
    [emails={support=support@example.org, administrator=administrator@example.org,
    development=development@example.org}
    stringList=[IOC, DI, Spring]
    beanList=[org.bd.spring.ch01.ExampleBean@10a3b24, org.bd.spring.ch01.ExampleBean@b0ce8f]
    someMap={admin=administrator@example.org, support=support@example.org}
    someSet=[JavaSE, JavaEE, JavaME]]

● 空字符串和Null

也可以为bean的属性赋空字符串和null,具体配置如下:

    <bean id="exampleBean"class="ExampleBean">
        <property name="email"><value/></property>
    </bean>

功能相同的Java代码为:exampleBean.setEmail("")。

    <bean id="exampleBean"class="ExampleBean">
        <property name="email"><null/></property>
    </bean>

功能相同的Java代码为exampleBean.setEmail(null)。

1.4.3 自动装配

Spring容器可以自动装配协作者之间的关系。我们可以让Spring通过检查整个ApplicationContext的内容,自动为bean解析协作者(另外的bean)。自动装配有以下好处:(1)自动装配能显著减少通过属性或者构造参数进行注入的配置数量。

(2)自动装配可以使配置与Java代码同步更新。例如,如果需要给一个Java类增加一个依赖,那么该依赖将被自动实现而不需要修改配置。

因此在开发过程中采用自动装配将会非常有用,在系统趋于稳定的时候可以改为显式装配的方式。

在基于XML的配置中,为bean元素指定autowire属性可以用来控制自动装配的模式。自动装配功能有5种模式,表1.1列出了这5种模式及对应的描述。

表1.1 自动装配模式

byName配置的示例如下:

    <bean id="axe" class="org.bd.spring.ch01.di.StoneAxe"></bean>
    <bean id="chinese" class="org.bd.spring.ch01.di.Chinese" autowire="byName"></bean>

自动装配可以减少对依赖注入的显式配置,减少了配置文件的行数,但其也存在一些缺点,需要大家有所了解。

(1)尽管自动装配比显式装配显得更加神奇,但使用Spring应尽量避免在装配不明确的时候进行猜测,因为装配不明确可能出现难以预料的结果。而且使用自动装配,Spring所管理的对象之间的关联关系也不能清晰地进行文档化。

(2)对于那些根据Spring配置文件生成文档的工具来说,自动装配将会使这些工具没法生成依赖信息。