Spring基础二:依赖注入

一个应用系统必然包含大量的bean,这些bean之间存在依赖关系。 依赖注入(Dependency injection)是Spring容器的核心功能。Bean可以几种方式来声明自己的依赖: 构造方法参数、工厂方法参数、Setter属性;容器在构造、初始化bean的过程中,将适当的bean引用注入进去。

本章的内容大体位于Spring官方文档的这个位置

构造参数注入

假设我们有一个bean类声明如下:

public class ThingOne {

    public ThingOne(ThingTwo thingTwo, ThingThree thingThree) {
        // ...
    }
}

我们可以用以下方式定义这个bean:

<beans>
    <bean id="beanOne" class="x.y.ThingOne">
        <constructor-arg ref="beanTwo"/>
        <constructor-arg ref="beanThree"/>
    </bean>
    <bean id="beanTwo" class="x.y.ThingTwo"/>
    <bean id="beanThree" class="x.y.ThingThree"/>
</beans>

只要类型ThingTwo和ThingThree之间没有继承关系,容器就能推测出这注入的两个构造函数参数的顺序。
否则的话,我们可能需要显示指明顺序:

<bean id="beanOne" class="x.y.ThingOne">
    <constructor-arg index="0" ref="beanTwo"/>
    <constructor-arg  index="1" ref="beanThree"/>
</bean>

我们也可以注入基本类型字面量,此时需要通过type属性来指明字面量类型,以告知容器如何匹配参数:

public class ExampleBean {

    // Number of years to calculate the Ultimate Answer
    private int years;

    // The Answer to Life, the Universe, and Everything
    private String ultimateAnswer;

    public ExampleBean(int years, String ultimateAnswer) {
        this.years = years;
        this.ultimateAnswer = ultimateAnswer;
    }
}

<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg type="int" value="7500000"/>
    <constructor-arg type="java.lang.String" value="42"/>
</bean>

还有一种方式是通过参数名字来匹配,如下:

<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg name="years" value="7500000"/>
    <constructor-arg name="ultimateAnswer" value="42"/>
</bean>

这种方式能工作的前提是,要么java代码以debug模式编译(否则参数的名字会被抹去),要么通过注解@ConstructorProperties声明了参数名字:

public class ExampleBean {

    @ConstructorProperties({"years", "ultimateAnswer"})
    public ExampleBean(int years, String ultimateAnswer) {
        this.years = years;
        this.ultimateAnswer = ultimateAnswer;
    }
}

工厂方法参数注入

工厂方法参数的注入的方式和构造方法参数注入的方式是非常类似的,直接举例:

public class ExampleBean {

    private ExampleBean(...) {
        ...
    }
    public static ExampleBean createInstance (
        AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
        ExampleBean eb = new ExampleBean (...);
        // some other operations...
        return eb;
    }
}

对应的bean声明如下:

<bean id="exampleBean" class="examples.ExampleBean" factory-method="createInstance">
    <constructor-arg ref="anotherExampleBean"/>
    <constructor-arg ref="yetAnotherBean"/>
    <constructor-arg value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>

基于属性Setter方法的注入

顾名思义,代码示例如下:

public class ExampleBean {

    private AnotherBean beanOne;

    private YetAnotherBean beanTwo;

    private int i;

    public void setBeanOne(AnotherBean beanOne) {
        this.beanOne = beanOne;
    }

    public void setBeanTwo(YetAnotherBean beanTwo) {
        this.beanTwo = beanTwo;
    }

    public void setIntegerProperty(int i) {
        this.i = i;
    }
}

对应的bean声明:

<bean id="exampleBean" class="examples.ExampleBean">
    <!-- setter injection using the nested ref element -->
    <property name="beanOne" ref="anotherExampleBean"/>
    <property name="beanTwo" ref="yetAnotherBean"/>
    <property name="integerProperty" value="1"/>
</bean>

<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>

依赖注入的几个问题

一个bean的依赖注入发生在bean的创建初始化过程当中,如果bean A依赖于B,那么容器会完成B的初始化,如果B依赖于其他bean,那么以此类推;容器会给A一个完全初始化好的B实例,换句话说,A的初始化会引发它所依赖的bean都被初始化。

构造参数依赖 VS setter属性依赖

这两种方式到底哪种更好呢?构造方法的好处是,确保bean对象一旦被创建,就是完整的,因为如果它的依赖不被满足的话,构造方法就不会成功。 而setter属性依赖没有这个保证,因此存在这样的风险:使用者拿到的bean引用尚未完成依赖注入。

因此Spring官方的建议是,对比必需的依赖,采用构造参数方式;可选的依赖采用setter属性,并且在使用过程中做null检查。

循环依赖问题

如果A和B相互依赖会怎么办呢?答案是,如果A、B都以构造参数来依赖对方,那么二者的初始化过程是无法完成的,容器会抛出一个异常。这种情况下,只能使用setter属性依赖。

idref标签

当我们想用用某个bean的标识符本身,而不是对象引用时,可以使用这个标签:

<bean id="theClientBean" class="...">
    <property name="targetName">
        <idref bean="theTargetBean"/>
    </property>
</bean>

它和下面的配置是等价的:

<bean id="client" class="...">
    <property name="targetName" value="theTargetBean"/>
</bean>

但是前者更安全,容器会检查idref指向的bean是否存在。

引用bean的范围

通常<ref bean="someBean"/>引用的bean是该容器或者父容器中存在的bean。
通过<ref parent="accountService"/>可以指定引用父容器的bean。

如果你想在子容器中创建父容器某个bean的proxy,并且使用相同的bean名字,那么上面的技术就派上用场。

内部bean

<bean id="outer" class="...">
    <!-- instead of using a reference to a target bean, simply define the target bean inline -->
    <property name="target">
        <bean class="com.example.Person"> <!-- this is the inner bean -->
            <property name="name" value="Fiona Apple"/>
            <property name="age" value="25"/>
        </bean>
    </property>
</bean>

如上面的定义所示,这bean直接定义在引用它的地方。这种bean没有名字,它的scope与外层bean一致,其地方也无法引用它(即使定义了名字和scope也会被忽略)。

Depends On依赖

通常bean之间的依赖是直接引用了目标bean,但有时候存在一些非直接的依赖,需要声明如下:

<bean id="beanOne" class="ExampleBean" depends-on="manager"/>
<bean id="manager" class="ManagerBean" />

一种可能的用途是,beanOne虽然不直接使用manager,但是manager初始化过程中执行的某些操作是beanOne正确工作的前提。

“depends on”不但指明了初始化顺序,如果这两个bean都是单例bean,也规定了bean销毁的顺序,即manager在beanOne之前。

延迟初始化

对于singleton作用域的bean(作用域的概念在后面,这章示例的所有bean都是singleton的),容器加载配置之后,就会立即初始化它们,除非加上lazy-init属性。

<bean id="lazy" class="com.something.ExpensiveToCreateBean" lazy-init="true"/>

这样,这个叫lazy的bean只有在有人尝试获取它的时候,才会初始化。因此如果有另外一个非lazy-init的bean依赖该bean,那么lazy-init=true的设置就没太大意义了。

自动注入(Autowire)

在bean的定义中,不显式指定所依赖的bean,而是让容器来查找合适的bean。
通过给标签添加autowire=mode属性开启,mode的取值为:

  1. no 不允许自动注入;
  2. byName 通过查找和属性同名的bean来注入;
  3. byType 通过查找和属性同类型的bean来注入;
  4. constructor 同byType,适用于构造参数;

官方文档没有提供示例,我尝试了一下,大概如下:

//类定义
public class MailSystem {
    private MailMaker mailMaker;
    private MailSender sender;

    public MailSystem(MailMaker mailMaker) {
        this.mailMaker = mailMaker;
    }

    public void setSender(MailSender sender) {
        this.sender = sender;
    }

    public void work() {
        sender.sendMail(mailMaker.makeMail());
    }
}

//bean定义
<bean id="mailSender" class="beans.MailSender"/>
<bean id="mailMaker" class="beans.MailMaker"/>

<bean id="mailSystem1" class="beans.MailSystem" autowire="byType">
    <constructor-arg index="0" ref="mailMaker"/>
</bean>

<bean id="mailSystem2" class="beans.MailSystem" autowire="constructor">
    <property name="sender" ref="mailSender"/>
</bean>

一旦选择autowire,所有的setter方法会被尝试自动注入。而且属性和构造参数不能同时启用autowire。

Autowire的优点是配置更精简,缺点是配置不够明确,易产生模糊性。比如按byType方式,如果有多个匹配的bean怎么办?

  1. 如果某个bean的autowire-candidate属性设置成false,那么autowire会忽略它;
  2. 如果某个bean的primary属性设置成true,那么autowire会优先选择它;
  3. <beans>可以添加一个叫做default-autowire-candidates属性,比如为"*Repository",规定只有名字以Repository结尾的bean才能被autowire;
  4. 如果仍然无法消除不确定性,容器会抛出异常。

注:这种autowire的功能太多,反而容易产生混淆,不如后面要讲的@Autowire注解。

方法注入(Method Injection)

假设Bean A依赖Bean B,A是singleTon作用域,B是prototype作用域。 那么如何实现A需要使用B的时候,获取一个新的实例呢?

假设A的定义声明对B的依赖,那么无论是属性依赖和构造参数依赖,都只会获得B的单一实例,因为容器只会在初始化是做一次依赖注入,无法达到我们的目的。一种解决方案是A不声明对B的依赖,而是每次需要B的时候从容器中去获取。

class A {
	protected B getB() {
		return context.getB("bname",B.class)。
	}
}

这种方式导致A直接耦合于spring容器,不是一种好方式,第二种方式就是依赖方法注入(LookUp Method Injection)。

依赖方法注入

abstact class A {
	protected abstract B getB();
}

<bean id="b" class="B" scope="prototype">
</bean>

<!-- commandProcessor uses statefulCommandHelper -->
<bean id="a" class="A">
    <lookup-method name="getB" bean="b"/>
</bean>

类A的getB方法被声明为abstract,然后在bean的声明中,通过lookup-method标签注入了getB方法的实现:“查找名为b的bean实例”。

能够使用此种注入方式的方法签名必须符合:<public|protected> [abstract] <return-type> theMethodName(no-arguments);

通过java注解实现查找方法注解的方式是:

abstact class A {
	@Lookup("b")
	protected abstract B getB();
}

或者通过返回类型来自动匹配:

abstact class A {
	@Lookup
	protected abstract B getB();
}

任意方法注入

我们可以实现对任意方法的注入或覆盖。首先要实现MethodReplacer接口,然后使用replaced-method标签。

public class MyValueCalculator {
    public String computeValue(String input) {
        // some real code...
    }
}
public class ReplacementComputeValue implements MethodReplacer {
    public Object reimplement(Object o, Method m, Object[] args) throws Throwable {
        // get the input value, work with it, and return a computed result
        String input = (String) args[0];
        ...
        return ...;
    }
}


<bean id="myValueCalculator" class="x.y.z.MyValueCalculator">
    <!-- arbitrary method replacement -->
    <replaced-method name="computeValue" replacer="replacementComputeValue">
        <arg-type>String</arg-type>
    </replaced-method>
</bean>

<bean id="replacementComputeValue" class="a.b.c.ReplacementComputeValue"/>

replacementComputeValue实现了MethodReplacer接口,提供了方法体的实现。标签通过name和arg-type限定了要替换方法的签名。

属性值配置

我们可以在bean的定义中,对属性的字面值进行配置,基本类型的属性值配置就不讲了,这里过一下集合类型的属性值配置;

标签<list/>, <set/>, <map/>和 分别可以配置List, Set, Map, 以及Properties类型的属性。
请看示例:

<bean id="moreComplexObject" class="example.ComplexObject">
    <!-- results in a setAdminEmails(java.util.Properties) call -->
    <property name="adminEmails">
        <props>
            <prop key="administrator">[email protected]</prop>
            <prop key="support">[email protected]</prop>
            <prop key="development">[email protected]</prop>
        </props>
    </property>
    <!-- results in a setSomeList(java.util.List) call -->
    <property name="someList">
        <list>
            <value>a list element followed by a reference</value>
            <ref bean="myDataSource" />
        </list>
    </property>
    <!-- results in a setSomeMap(java.util.Map) call -->
    <property name="someMap">
        <map>
            <entry key="an entry" value="just some string"/>
            <entry key ="a ref" value-ref="myDataSource"/>
        </map>
    </property>
    <!-- results in a setSomeSet(java.util.Set) call -->
    <property name="someSet">
        <set>
            <value>just some string</value>
            <ref bean="myDataSource" />
        </set>
    </property>
</bean>

集合属性的合并、覆盖

bean之间可以有父子关系(与容器的父子关系无关),子bean的定义可以继承parent bean的定义,自然也可以继承集合类型的属性定义。 此时子bean可以覆盖、合并某个属性的定义。

<bean id="moreComplexObject" class="example.ComplexObject">
    <!-- results in a setAdminEmails(java.util.Properties) call -->
    <property name="adminEmails">
        <props>
            <prop key="administrator">[email protected]</prop>
            <prop key="support">[email protected]</prop>
            <prop key="development">[email protected]</prop>
        </props>
    </property>
    <!-- results in a setSomeList(java.util.List) call -->
    <property name="someList">
        <list>
            <value>a list element followed by a reference</value>
            <ref bean="myDataSource" />
        </list>
    </property>
    <!-- results in a setSomeMap(java.util.Map) call -->
    <property name="someMap">
        <map>
            <entry key="an entry" value="just some string"/>
            <entry key ="a ref" value-ref="myDataSource"/>
        </map>
    </property>
    <!-- results in a setSomeSet(java.util.Set) call -->
    <property name="someSet">
        <set>
            <value>just some string</value>
            <ref bean="myDataSource" />
        </set>
    </property>
</bean>

总结

DI(依赖注入)是Spring容器的核心功能,帮助我们管理bean之间的关系,降低系统模块之间的耦合性。值得注意的是,Spring的DI是一种完全无侵入的机制,Bean的编写者,就像写普通java代码一样,在构造参数或setter方法里来声明对其他Bean接口的依赖;然后系统的构建者在Spring配置文件里面配置依赖关系。

本章描述了很多xml格式的依赖注入配置方法;在java注解方式占主流的今天,我们已无需在花功夫记住这些配置方法,通过示例了解原理即可。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章