1.4 依赖

典型的企业应用程序不会只有一个对象(或Spring术语中的bean)。即使是最简单的应用程序也有一些对象一起工作,最终呈现给用户。

下一节将解释如何从定义一些独立的bean到一个真实的应用程序,在该应用程序中通过对象协作来实现一个目标。

1.4.1. 依赖注入

依赖项注入(DI)是指:对象仅仅通过构造函数参数、工厂方法的参数或从工厂方法构造,或返回对象实例后在其上设置属性来定义其依赖项(即它们使用的其他对象)。然后,容器在创建bean时注入这些依赖项。这个过程基本上和bean通过直接类的构造器或服务定位器来定位其依赖的过程是相反的(因此称为控制反转)。

使用DI,代码更干净,当对象具有依赖关系时,去耦更有效。对象不需要查找其依赖项,甚至不知道依赖项的位置或类。 因此,你的类变得更容易测试,特别是当依赖于接口或抽象基类时。(允许在单元测试中使用stub或mock实现)

DI有两个主要的方式:基于构造函数的依赖注入和基于setter的依赖注入。

基于构造函数的依赖注入

基于构造函数的DI是通过容器调用带有多个参数的构造函数来实现的,每个参数表示一个依赖项。这个和调用带有特定参数的静态工厂方法来构造bean几乎是等效的,这里将会对构造函数和静态工厂方法的参数做相似的处理。下面的示例显示了一个只能通过构造函数注入依赖项的类:

public class SimpleMovieLister {

    // the SimpleMovieLister has a dependency on a MovieFinder
    private MovieFinder movieFinder;

    // a constructor so that the Spring container can inject a MovieFinder
    public SimpleMovieLister(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // business logic that actually uses the injected MovieFinder is omitted...
}

注意这个类没有什么特别的。它是一个POJO,不依赖于特定于容器的接口、基类或注解。

构造函数参数的解析

一般通过参数的类型来进行构造函数参数的解析匹配。如果bean定义的构造函数参数中没有潜在的歧义,那么在bean定义中定义构造函数参数的顺序就是在bean被实例化时,这些参数被提供给适当的构造函数的顺序。可以看下面的类:

package x.y;

public class ThingOne {

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

假设ThingTwo和ThingThree类与继承无关,则不存在潜在的歧义。因此,以下配置工作正常,你不需要在< constructor-arg/>元素中显式指定构造函数参数索引或类型。

<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>

当引用另一个bean时,如果类型是已知的,可以通过类型进行匹配(前面的例子就是这样)。当使用简单类型时,例如<value>true</value>,Spring无法确定值的类型,因此在没有帮助的情况下无法按类型匹配。考虑以下情况:

package examples;

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;
    }
}

构造函数类型匹配

在前面的场景中,如果使用type属性显式指定构造函数参数的类型,则容器可以使用与简单类型匹配的类型。如下示例所示:

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

构造函数索引

你也可以使用index属性来显示的指定构造函数参数的位置,如下所示:

<bean id="exampleBean" class="examples.ExampleBean">
    <constructor-arg index="0" value="7500000"/>
    <constructor-arg index="1" value="42"/>
</bean>

除了解决多个简单值的不确定性之外,指定索引还可以解决构造函数有两个相同类型参数的不确定性。

注意:index是从0开始的。

构造函数名字匹配

你也可以使用构造函数的参数名字来指定,如下所示:

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

请记住,要使用这项功能,必须在启用调试标志的情况下编译代码,以便Spring可以从构造函数中查找参数名。

如果无法或不希望使用调试标志编译代码,可以使用@ConstructorProperties JDK注解显式命名构造函数参数。然后示例类必须如下所示:

package examples;

public class ExampleBean {

    // Fields omitted

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

基于Setter的注入依赖

基于Setter的DI是由容器在调用无参数构造函数或无参数静态工厂方法实例化bean之后调用bean上的setter方法来完成的。

下面的示例显示了一个类,该类只能通过使用纯setter注入来注入依赖项。这个类是传统的Java。它是一个POJO,不依赖于特定于容器的接口、基类或注解。

public class SimpleMovieLister {

    // the SimpleMovieLister has a dependency on the MovieFinder
    private MovieFinder movieFinder;

    // a setter method so that the Spring container can inject a MovieFinder
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // business logic that actually uses the injected MovieFinder is omitted...
}

ApplicationContext支持它所管理的bean的基于构造函数和基于setter的DI。在通过构造函数方法注入了一些依赖项之后,仍然可以使用基于setter的DI。 所有的依赖都会被转化成为BeanDefinition,它会与PropertyEditor实例一起使用,将属性从一种格式转换为另一种格式。 然而,大多数Spring用户并不直接使用这些类(即代码编程方式),而是使用XML bean定义、注解的组件(即用@Component、@Controller等来注解的类),或基于Java的@Configuration注解中的@Bean方法。然后,这些定义会在内部转换为BeanDefinition实例,并被用于Spring IOC容器中。

构造函数还是setter形式的注入?

由于你可以混合使用基于构造函数和基于setter的DI,所以对于强制依赖项使用构造函数,对于可选依赖项使用setter方法或配置方法是一个很好的实践经验。 注意,可以使用setter方法上的@Required注解使属性成为必需的依赖项;但是,最好使用构造函数注入并在程序中做相应的验证。

Spring团队通常提倡构造函数注入,因为它允许你将应用程序组件实现为不可变的对象,并确保所需的依赖项不为空。 此外,构造函数注入的组件总是以完全初始化的状态返回到客户端(调用)代码。另一点需要注意的是,大量的构造函数参数是一种糟糕的代码体验,这意味着类可能做了太多的事情,应该重构以做适当的分离。

Setter注入应该主要用于可在类内分配合理默认值的可选依赖项。否则,在代码使用依赖项的任何地方都必须执行非空检查。 Setter注入的一个好处是Setter方法使该类的对象能够在以后重新配置或重新注入。因此,通过JMX MBeans进行管理是Setter注入的一个成功的用例。

对特定类使用最有意义的DI样式。有时,在处理没有源代码的第三方类时,只有一种选择。例如,如果第三方类不公开任何setter方法,那么构造函数注入可能是DI的唯一可用形式。

依赖解析流程

容器按如下流程处理Bean的依赖解析:

  • ApplicationContext被创建和实例化出来,它包含了所有bean的配置元数据。这些配置元数据可以通过XML、Java代码或注解来指定。

  • 对于每个bean,其依赖项都以属性、构造函数参数或静态工厂方法的参数的形式表示。当bean实际创建时,这些依赖项会提供给bean。

  • 每个属性或构造函数参数都是要设置的值的实际定义,或者是对容器中另一个bean的引用。

  • 作为值的每个属性或构造函数参数都将从其指定的格式转换为该属性或构造函数参数的实际类型。默认情况下,Spring可以将以字符串格式提供的值转换为所有内置类型,如int、long、String、boolean等。

Spring容器在创建容器时会验证每个bean的配置。但是,在bean实际创建之前,不会设置bean的属性。当创建容器时,单实例并设置为预实例化(默认方式)的bean会被创建。作用域被设置为bean作用域。否则,只有在请求bean时才创建它。创建bean可能会导致创建bean图,因为bean的依赖项及其依赖项的依赖项(等等)会被创建和分配。

请注意,这些依赖项之间的解析不匹配可能会很晚出现,也就是说,在第一次创建受影响的bean时。

循环依赖

如果主要使用构造函数注入,则可能创建循环依赖的场景。

例如:类A通过构造函数注入需要类B的实例,类B通过构造函数注入需要类A的实例。如果为要相互注入的类A和类B配置bean,那么SpringIOC容器在运行时检测到这个循环引用,会抛出BeanCurrentlyInCreationException。

一种可能的解决方案是编辑一些类的源代码,这些类由setter而不是构造函数配置。或者,避免构造函数注入,只使用setter注入。换句话说,尽管不建议这样做,但是可以使用setter注入配置循环依赖项。

与典型情况(没有循环依赖性)不同,bean a和bean b之间的循环依赖强制将其中一个bean注入另一个bean,然后完全初始化自己(经典的鸡和蛋场景)。

一般来说,你可以相信Spring会做正确的事。它在容器加载时检测配置问题,例如不存在的bean和循环依赖项的引用。

Spring在实际创建bean时设置属性并尽可能晚地解析依赖项。这意味着,如果在创建对象或其依赖项时出现问题,那么之前正确加载的Spring容器稍后可能在请求对象时生成异常,例如:bean由于缺少或无效的属性而引发异常。

对于这样的配置问题,这种潜在的延迟可见性是ApplicationContext实现默认情况下预实例化singleton bean的原因。

在实际需要使用这些bean之前,你需要花费一些前期时间和内存来创建它们,并在创建ApplicationContext时发现配置问题,而不是稍后。

你仍然可以重写这个默认行为,以便单例bean可以延迟初始化,而不是预先实例化。

如果不存在循环依赖关系,当一个或多个协作bean被注入依赖bean时,每个协作bean在被注入依赖bean之前都被完全配置。这意味着,如果bean a依赖于bean b,那么在bean a上调用setter方法之前,spring ioc容器会完全配置bean b。

换句话说,bean被实例化(即使它不是预实例化的singleton),它的依赖被设置,并且调用相关的生命周期方法(例如configured init方法或InitializingBean回调方法)。

依赖注入的例子

下面的示例是基于XML的配置元数据使用基于setter的DI。如下所示:

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

    <!-- setter injection using the neater ref attribute -->
    <property name="beanTwo" ref="yetAnotherBean"/>
    <property name="integerProperty" value="1"/>
</bean>

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

下面是相应的ExampleBean类:

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;
    }
}

在上面的例子中,setter声明和XML文件中指定的属性匹配。 下面是构造函数DI的例子:

<bean id="exampleBean" class="examples.ExampleBean">
    <!-- constructor injection using the nested ref element -->
    <constructor-arg>
        <ref bean="anotherExampleBean"/>
    </constructor-arg>

    <!-- constructor injection using the neater ref attribute -->
    <constructor-arg ref="yetAnotherBean"/>

    <constructor-arg type="int" value="1"/>
</bean>

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

下面是相应的ExampleBean类:

public class ExampleBean {

    private AnotherBean beanOne;

    private YetAnotherBean beanTwo;

    private int i;

    public ExampleBean(
        AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {
        this.beanOne = anotherBean;
        this.beanTwo = yetAnotherBean;
        this.i = i;
    }
}

bean定义中指定的构造函数参数用作ExampleBean的构造函数的参数。

现在考虑这个例子的一个变体,在这里,Spring不是使用构造函数,而是调用一个静态工厂方法来返回对象的一个实例:

<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"/>

下面是ExampleBean的例子:

public class ExampleBean {

    // a private constructor
    private ExampleBean(...) {
        ...
    }

    // a static factory method; the arguments to this method can be
    // considered the dependencies of the bean that is returned,
    // regardless of how those arguments are actually used.
    public static ExampleBean createInstance (
        AnotherBean anotherBean, YetAnotherBean yetAnotherBean, int i) {

        ExampleBean eb = new ExampleBean (...);
        // some other operations...
        return eb;
    }
}

静态工厂方法的参数由<constructor-arg/>元素提供,与实际使用的构造函数完全相同。工厂方法返回的类的类型不必与包含静态工厂方法的类的类型相同(尽管在本例中是这样)。实例(非静态)工厂方法可以以基本相同的方式使用(除了使用factory-bean属性而不是class属性),因此我们不在这里讨论这些细节。

1.4.2 依赖和配置的细节

如上节所示,自定义的Bean属性或者构造函数参数可以引用其他被管理的Bean,或者定义内联的值。Spring基于XML的配置元数据可以在<property/>和<constructor-arg/>元素中的配置子元素类型来实现这一功能。

直接值(基本类型,字符串或者其他)

<property/>元素的value属性将属性或构造函数参数指定为人类可读的字符串表示形式。Spring的转换服务用于将这些值从字符串转换为属性或参数的实际类型。以下示例显示了正在设置的各种值:

<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <!-- results in a setDriverClassName(String) call -->
    <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
    <property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
    <property name="username" value="root"/>
    <property name="password" value="masterkaoli"/>
</bean>

下面的例子使用了更简洁的p-namespace例子:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
    https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource"
        destroy-method="close"
        p:driverClassName="com.mysql.jdbc.Driver"
        p:url="jdbc:mysql://localhost:3306/mydb"
        p:username="root"
        p:password="masterkaoli"/>

</beans>

上面的XML更加简洁。但是,除非你使用一个IDE(如Intellij IDEA或Spring工具套件),在创建bean定义时支持自动完成属性,否则只能在运行时而不是设计时发现拼写错误。强烈建议使用这样的IDE协助开发。

你也可以配置一个java.util.Properties实例如下:

<bean id="mappings"
    class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">

    <!-- typed as a java.util.Properties -->
    <property name="properties">
        <value>
        jdbc.driver.className=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mydb
        </value>
    </property>
</bean>

Spring容器使用JavaBeans属性编辑器机制将<value/>元素中的文本转换为java.util.properties实例。这是一个很好的快捷方式,也是Spring团队支持使用嵌套的<value/>元素而不是value属性样式的少数地方之一。

idref元素

idref元素是将容器中另一个bean的id(字符串值,而不是引用)传递给<constructor-arg/>或<property/>元素的一种简单的防止错误方法。下面的示例演示如何使用它:

<bean id="theTargetBean" class="..."/>

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

前面的bean定义片段(在运行时)与以下片段完全等效:

<bean id="theTargetBean" class="..." />

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

第一个表单比第二个表单更好,因为使用idref标记可以让容器在部署时验证引用的、bean是否实际存在。在第二个例子中,不会对传递给客户机bean的targetName属性的值执行任何验证。只有当客户端bean实际被实例化时,才会发现拼写错误(最有可能是致命的结果)。如果客户端bean是prototype类型的bean,那么只有在部署容器很长时间之后,才会发现这种类型和产生的异常。

4.0 beans xsd不再支持idref元素的local属性,因为它不再提供常规bean引用的值。在升级到4.0模式时,请将idref local修改对idref bean。

<idref/>元素的一个常用的作用(至少在Spring2.0之前的版本中)是在用在ProxyFactoryBean中定义的AOP拦截器里。指定拦截器名称时使用<idref/>元素可防止你拼写错误拦截器ID。

引用其他的Beans

ref元素是<constructor-arg/>或<property/>定义元素中的最后一个元素。在这里,你将bean的指定属性的值设置为对容器管理的另一个bean(合作者)的引用。引用的bean是要设置其属性的bean的依赖项,在设置该属性之前,需要对其进行初始化。(如果合作者是单例bean,那么它可能已经由容器初始化。)所有引用最终都是对另一个对象的引用。作用域和验证会根据你通过bean、local或parent属性来指定另一个对象的ID或name而做相应的变动。

通过<ref/>标记的bean属性指定目标bean是最通用的形式,允许在同一容器或父容器中创建对任何bean的引用,而不管它是否在同一XML文件中。bean属性的值可以与目标bean的id属性相同,也可以与目标bean的name属性中的一个值相同。以下示例显示如何使用ref元素:

<ref bean="someBean"/>

通过parent属性指定目标bean将创建对当前容器的父容器中的bean的引用。父属性的值可以与目标bean的id属性或目标bean的name属性中的一个值相同。目标bean必须位于当前bean的父容器中。 当你使用层级的容器结构时,并且希望用与父bean同名的代理将现有bean包装在父容器中时,就可以使用parent属性。下面展示了如何使用父属性:

<!-- in the parent context -->
<bean id="accountService" class="com.something.SimpleAccountService">
    <!-- insert dependencies as required as here -->
</bean>
<!-- in the child (descendant) context -->
<bean id="accountService" <!-- bean name is the same as the parent bean -->
    class="org.springframework.aop.framework.ProxyFactoryBean">
    <property name="target">
        <ref parent="accountService"/> <!-- notice how we refer to the parent bean -->
    </property>
    <!-- insert other configuration and dependencies as required here -->
</bean>

内部Beans

在<property/> 或者 <constructor-arg/>元素内部的<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定义不需要ID或名称。如果指定,容器也不会使用这个值作为标识符。容器在创建时也忽略范围标志,因为内部bean总是匿名的,并且总是用外部bean创建的。不可能单独访问内部bean,也不可能将它们注入到除封闭bean之外的协作bean中。

可以接收来自自定义范围的destruction回调,例如,对于包含在单例bean的请求范围内的内部bean。内部bean实例的创建与它的包含bean绑定在一起,但是destruction回调让它可以接入请求范围的生命周期。这不是一个常见的场景。内部bean通常只共享其包含bean的范围。

集合

<list/>, <set/>, <map/>,和 <props/> 分别被用来设置Java Collection类型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">administrator@example.org</prop>
            <prop key="support">support@example.org</prop>
            <prop key="development">development@example.org</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>

下面的属性可以作为map的key或者value, 也可以作为set的value:

bean | ref | idref | list | set | map | props | value | null

集合合并

Spring容器还支持合并集合。应用程序开发人员可以定义父级< list/>、< map/>、< set/>或< props/>元素,并让子级< list/>、< map/>、< set/>或< props/>元素继承和重写父集合中的值。也就是说,子集合的值是合并父集合和子集合的元素的结果,而子集合元素覆盖父集合中指定的值。

本节讨论合并的父子bean机制。不熟悉父bean和子bean定义的读者可能希望在继续之前阅读相关部分。

下面的示例演示集合合并:

<beans>
    <bean id="parent" abstract="true" class="example.ComplexObject">
        <property name="adminEmails">
            <props>
                <prop key="administrator">administrator@example.com</prop>
                <prop key="support">support@example.com</prop>
            </props>
        </property>
    </bean>
    <bean id="child" parent="parent">
        <property name="adminEmails">
            <!-- the merge is specified on the child collection definition -->
            <props merge="true">
                <prop key="sales">sales@example.com</prop>
                <prop key="support">support@example.co.uk</prop>
            </props>
        </property>
    </bean>
<beans>

注意在子bean定义的adminEmails属性的<props/>元素上使用了merge=true属性。当子bean由容器解析和实例化时,生成的实例具有一个adminEmails属性集合,该集合包含将子级的adminEmails集合与父级的adminEmails集合合并的结果。以下列表显示了结果:

administrator=administrator@example.com
sales=sales@example.com
support=support@example.co.uk

子属性集合的值集继承父集合中的所有属性元素,子集合的支持值覆盖父集合中的值。

这种合并行为同样适用于< list/>、< map/>和< set/>集合类型。< list/>元素比较特殊(即值是有序集合),父列表的值会位于子列表的所有值之前。对于映射、集合和属性集合类型,不存在排序。因此,对于容器内部使用的关联映射、集合和属性实现类型的基础集合类型,没有有效的排序语义。

集合合并的限制

不能合并不同的集合类型(如映射和列表)。如果你尝试这样做,就会抛出一个异常。合并属性必须在继承子对象上定义。在父集合定义上指定合并属性是多余的,不会导致所需的合并。

强类型集合

通过在Java 5中引入泛型类型,可以使用强类型集合。也就是说,可以声明集合类型,使其只能包含(例如)字符串元素。如果使用Spring将强类型集合注入bean,则可以利用Spring的类型转换支持,以便在将强类型集合实例的元素添加到集合之前将其转换为适当的类型。下面的Java类和bean定义显示了如何这样做:

public class SomeClass {

    private Map<String, Float> accounts;

    public void setAccounts(Map<String, Float> accounts) {
        this.accounts = accounts;
    }
}
<beans>
    <bean id="something" class="x.y.SomeClass">
        <property name="accounts">
            <map>
                <entry key="one" value="9.99"/>
                <entry key="two" value="2.75"/>
                <entry key="six" value="3.99"/>
            </map>
        </property>
    </bean>
</beans>

当something bean的accounts属性准备好注入时,关于强类型map的元素类型的泛型信息可以通过反射获得。因此,Spring的类型转换基础结构将各种值元素识别为float类型,并将字符串值(9.99、2.75和3.99)转换为实际的float类型。

Null和Empty字符串值

Spring将属性和参数的空值视为空Strings。以下基于XML的配置元数据片段将email属性设置为空字符串值(“”)。

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

上面的例子相当于下面的例子:

exampleBean.setEmail("");

< null/> 元素表示null值看如下所示:

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

上面例子相当于:

exampleBean.setEmail(null);

p-namespace的XML快捷方式

p-namespace允许你使用bean元素的属性(而不是嵌套的< property/>元素)来描述协作bean或属性值。 Spring支持基于XML模式定义的带有命名空间的可扩展配置格式。本章讨论的bean配置格式在XML模式文档中定义。但是,p-namespace没有在xsd文件中定义,并且只存在于spring的核心中。

下面的示例显示了两个解析为相同结果的XML片段(第一个使用标准XML格式,第二个使用P命名空间):

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean name="classic" class="com.example.ExampleBean">
        <property name="email" value="someone@somewhere.com"/>
    </bean>

    <bean name="p-namespace" class="com.example.ExampleBean"
        p:email="someone@somewhere.com"/>
</beans>

该示例在bean定义中显示了p-namespace中名为email的属性。这告诉Spring这是一个属性申明。如前所述,p-namespace没有模式定义,因此可以将property name设置为属性名。

下一个示例包括另外两个bean定义,它们都引用了另一个bean:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean name="john-classic" class="com.example.Person">
        <property name="name" value="John Doe"/>
        <property name="spouse" ref="jane"/>
    </bean>

    <bean name="john-modern"
        class="com.example.Person"
        p:name="John Doe"
        p:spouse-ref="jane"/>

    <bean name="jane" class="com.example.Person">
        <property name="name" value="Jane Doe"/>
    </bean>
</beans>

此示例不仅包括使用p-namespace的属性值,还使用特殊格式声明属性引用。第一个bean定义使用< property name="spouse" ref="jane"/>创建从bean john到bean jane的引用,但是第二个bean定义使用p:spouse ref=“jane”作为属性来执行完全相同的操作。在这种情况下,spouse是属性名,而-ref部分表明这不是一个直接值,而是对另一个bean的引用。

p-namespace不如标准XML格式灵活。例如,声明属性引用的格式与以ref结尾的属性冲突,而标准XML格式则不冲突。我们建议你仔细选择方法,并将其传达给你的团队成员,以避免生成同时使用这三种方法的XML文档。

c-namespace的XML快捷方式

和前面的p-namespace一样,Spring 3.1引入的c-namespace允许内联属性配置构造函数参数,而不是嵌套的constructor-arg元素。下面是例子:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:c="http://www.springframework.org/schema/c"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        https://www.springframework.org/schema/beans/spring-beans.xsd">

    <bean id="beanTwo" class="x.y.ThingTwo"/>
    <bean id="beanThree" class="x.y.ThingThree"/>

    <!-- traditional declaration with optional argument names -->
    <bean id="beanOne" class="x.y.ThingOne">
        <constructor-arg name="thingTwo" ref="beanTwo"/>
        <constructor-arg name="thingThree" ref="beanThree"/>
        <constructor-arg name="email" value="something@somewhere.com"/>
    </bean>

    <!-- c-namespace declaration with argument names -->
    <bean id="beanOne" class="x.y.ThingOne" c:thingTwo-ref="beanTwo"
        c:thingThree-ref="beanThree" c:email="something@somewhere.com"/>

</beans>

c命名空间和p命名空间有一样的约定,比如通过名字设置构造函数时(-ref 代表着引用),同样的,虽然他没有在XSD规范中定义(存在Spring Core中),但是必须在XML文件中声明。

对于构造函数参数名不可用的少数情况(通常如果编译字节码时没有调试信息),可以回退到参数索引,如下所示:

<!-- c-namespace index declaration -->
<bean id="beanOne" class="x.y.ThingOne" c:_0-ref="beanTwo" c:_1-ref="beanThree"
    c:_2="something@somewhere.com"/>

由于XML语法的原因,index需要有前导字符_,因为XML属性名不能以数字开头(即使某些IDE允许这样做)。相应的索引表示也可用于< constructor-arg>元素,但不常用,因为简单顺序通常就足够了。

在实践中,构造函数解析机制在匹配参数方面非常有效,因此,除非确实需要,否则我们建议通过配置使用name。

复合属性名称

设置bean properties时,可以使用复合或嵌套的属性名,只要路径的所有组件(最终属性名除外)不为null。考虑以下bean定义:

<bean id="something" class="things.ThingOne">
    <property name="fred.bob.sammy" value="123" />
</bean>

something bean有一个fred属性,fred有个bob属性,bob有个sammy属性,最终sammy被设置为123,在构造bean之后,something的fred属性和fred的bob属性不能为空。否则,将引发NullPointerException。

1.4.3 使用depends-on

如果一个bean是另一个bean的依赖项,这通常意味着一个bean被设置为另一个bean的属性。通常,你可以使用基于XML的配置元数据中的< ref/>元素来实现这一点。然而,有时bean之间的依赖关系并不直接。例如,当类中的静态初始值设定项需要触发时,例如数据库驱动程序注册。depends-on属性可以显式强制一个或多个bean在使用此元素的bean初始化之前进行初始化。以下示例使用depends-on属性表示对单个bean的依赖:

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

要表示对多个bean的依赖关系,请提供一个bean名称列表作为depends-on属性的值(逗号、空白和分号都是有效的分隔符):

<bean id="beanOne" class="ExampleBean" depends-on="manager,accountDao">
    <property name="manager" ref="manager" />
</bean>

<bean id="manager" class="ManagerBean" />
<bean id="accountDao" class="x.y.jdbc.JdbcAccountDao" />

depends-on属性既可以指定初始化时间依赖项,也可以指定相应的销毁时间依赖项(仅在单例bean的情况下)。定义与给定bean的依赖关系的依赖bean在给定bean本身被销毁之前首先被销毁。因此,depends-on还可以控制关机顺序。

1.4.4 惰性初始化bean

默认情况下,ApplicationContext作为初始化过程的一部分,以预实例化的方式建和配置所有的单例bean。一般来说,我们提倡这种预实例化方式,因为如果配置或周围环境发送错误会立即被发现,而不是几小时甚至几天之后。如果不想使用这种方式,可以通过将bean定义标记为lazy-initialized来防止单例bean的预实例化。一个lazy-initialized bean告诉IOC容器在第一次请求bean实例时创建它,而不是在启动时。

在XML中,此行为由< bean/>元素上的lazy-init 属性控制,如下示例所示:

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

当ApplicationContext使用前面的配置时,当ApplicationContext启动时,lazy bean不会被预先实例化,而not.lazy bean则会被预先实例化。

但是,当一个惰性初始化bean是一个非惰性初始化的singleton bean的依赖项时,ApplicationContext会在启动时创建惰性初始化bean,因为它必须满足singleton的依赖项。惰性初始化bean被注入到其他非惰性初始化的单例bean中。

你还可以通过在< beans/>元素上使用默认的lazy init属性在容器级别控制lazy初始化,下面的示例显示:

<beans default-lazy-init="true">
    <!-- no beans will be pre-instantiated... -->
</beans>

1.4.5.自动装载合作者

Spring容器可以自动连接协作bean之间的关系。通过检查ApplicationContext的内容,可以让Spring自动解析bean的合作者(其他bean)。自动装载具有以下优点:

  • 自动装载可以显著减少指定属性或构造函数参数。(本章其他地方讨论的bean模板等其他机制在这方面也很有价值。)

  • 自动装载可以随着对象的发展更新配置。例如,如果需要向类添加依赖项,则可以自动满足该依赖项,而无需修改配置。因此,自动装载在开发期间尤其有用,也不需要在代码库变得更稳定时切换到显式装配。

使用基于XML的配置元数据(请参见依赖注入)时,可以使用< bean/>元素的autowire属性为bean定义指定autowire模式。autowiring功能有四种模式。你可以为每个bean指定autowiring,从而可以选择要autowiring的对象。下表介绍了四种autowiring模式:

Table 2. Autowiring modes

模式说明

no

(默认)无自动装载。bean必须引用由ref定义的元素。对于较大的部署,不建议更改默认设置,因为显式指定合作者可以提供更大的控制度和清晰性。在某种程度上,它记录了系统的结构。

byName

按属性名称自动装载。Spring寻找与需要自动装载的属性同名的bean。例如,如果bean定义被设置为按名称自动装载,并且它包含一个master属性(即,它有一个setMaster(..)方法),那么spring将查找名为master的bean定义并使用它来设置该属性。

byType

如果容器中只有一个属性类型的bean,则允许自动装载属性。如果存在多个,则会引发一个致命的异常,这表示你不能为该bean使用byType自动装载。如果没有匹配的bean,则不会发生任何事情(未设置属性)。

constructor

类似于byType,但适用于构造函数参数。如果容器中不只有一个构造函数参数类型的bean,则会引发致命错误。

使用byType或构造函数自动装载模式,可以装载数组和类型化集合。在这种情况下,将提供容器中与预期类型匹配的所有满足自动装载的候选者,以满足依赖关系。如果所需的键类型为字符串,则可以自动装载强类型Map实例。自动装载的Map实例的值由匹配预期类型的所有bean实例组成,并且Map实例的键是相应的bean名称。

自动装载的限制和缺陷

自动装载在一个项目中持续使用时效果最好。如果一般不使用自动装载,或者只使用它来装载一个或两个bean定义,开发人员会很困惑。

自动装载的局限性和缺点:

  • property和constructor-arg设置中的显式依赖项始终会覆盖自动装载。不能自动装载简单属性,如基元、Strings和Classes(以及此类简单属性的数组)。这是设计上的限制。

  • 自动装载不如显式装载精确。尽管如前表所述,Spring谨慎地避免猜测,以防出现可能产生意外的歧义结果。但是你的Spring管理对象之间的关系将不会在文档中明确标出。

  • 有些工具可能无法为自动装载生成相应的文档

  • 可以与setter方法或要自动注入的构造函数参数类型可能与容器中的多个bean定义相匹配。如果是数组、集合或映射实例,这没有问题。但是,对于期望单个值的依赖项,这种模糊性会引发异常。

在最后一种情况中,你有几个选项:

  • 放弃自动注入,取而代之的是显式注入。

  • 通过将bean定义的autowire候选属性设置为false,避免自动注入bean定义,如下一节所述。

  • 通过将< bean/>元素的primary属性设置为true,将单个bean指定为主要候选对象.

  • 使用基于注解的配置中可用的更细粒度的控件

从自动装载中排除Bean

你可以从自动装载中排除bean。在Spring的XML格式中,将< bean/>元素的autowire-candidate属性设置为false。则这个bean在自动装载时将会不可用(包括注解样式的配置,如@Autowired)。

autowire-candidate属性设计为只影响type-based的自动注解。它不会影响显式的按名字的引用,即使指定的bean没有标记为autowire候选对象,这些引用也会得到解析。因此,如果名称匹配,则按name自动装载仍会注入bean。

你还可以根据模式匹配来限制自动装载的候选对象。顶级的< beans/>元素在其默认的default-autowire-candidates属性中接受一个或多个模式。例如,要将autowire候选状态限制为名称以Repository结尾的任何bean,请提供值*Repository。要提供多个模式,请在逗号分隔的列表中定义它们。

如果显示的设定bean的autowire-candidate属性为true或false那么模式匹配规则将失效。

这些技术对于你不希望通过自动注解注入其他bean的bean很有用。这并不意味着被排除的bean本身不能通过使用自动注解进行配置。只是说明这个bean并不是自动注解其他bean的候选者。

1.4.6. 方法注入

在大多数应用程序场景中,容器中的大多数bean都是单例的。当一个单例bean需要与另一个单例bean协作,或者一个非单例bean需要与另一个非单例bean协作时,通常通过将一个bean定义为另一个bean的属性来处理依赖关系。当bean的生命周期不同时,就会出现一个问题。假设singleton bean a需要使用非singleton(原型)bean b,可能在a上的每个方法调用上都需要使用b。容器只创建一次singleton bean a,因此只获得一次设置属性的机会。容器不能在每次需要bean B时向bean A提供一个新的bean B实例。

解决办法是放弃一些控制反转。你可以通过实现ApplicationContextAware接口使bean aware容器,每次bean a需要b时,通过对容器进行getbean(“b”)调用来请求(通常是新的)bean b实例。下面的示例显示了这种方法:

// a class that uses a stateful Command-style class to perform some processing
package fiona.apple;

// Spring-API imports
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;

public class CommandManager implements ApplicationContextAware {

    private ApplicationContext applicationContext;

    public Object process(Map commandState) {
        // grab a new instance of the appropriate Command
        Command command = createCommand();
        // set the state on the (hopefully brand new) Command instance
        command.setState(commandState);
        return command.execute();
    }

    protected Command createCommand() {
        // notice the Spring API dependency!
        return this.applicationContext.getBean("command", Command.class);
    }

    public void setApplicationContext(
            ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
}

这种方法并不可取的,因为业务代码和Spring框架产生了耦合。方法注入是Spring IoC 容器的一个高级特性,它可以很好的处理这种情况。

查找方法注入

查找方法注入是指容器重写container-managed bean上的方法,并返回容器中另一个命名bean。查找通常涉及一个原型bean,如前一节中描述的场景中所示。Spring框架通过使用cglib库中的字节码动态生成覆盖该方法的子类来实现该方法注入。

  • 为了使这个动态子类工作,SpringBean容器子类不能是final类,要重写的方法也不能是final类。

  • 单元测试具有抽象方法的类需要你自己对类进行子类化,并提供抽象方法的stub实现。

  • 组件扫描还需要具体的方法,这会提供给具体的类来使用。

  • 另一个关键限制是查找方法不适用于工厂方法,尤其不适用于配置类中的@Bean方法,因为在这种情况下,容器不负责创建实例,因此无法动态创建运行时生成的子类。

对于前面代码段中的CommandManager类,Spring容器会动态重写createCommand()方法的实现。CommandManager类没有任何Spring依赖项,正如重新编写的示例所示:

package fiona.apple;

// no more Spring imports!

public abstract class CommandManager {

    public Object process(Object commandState) {
        // grab a new instance of the appropriate Command interface
        Command command = createCommand();
        // set the state on the (hopefully brand new) Command instance
        command.setState(commandState);
        return command.execute();
    }

    // okay... but where is the implementation of this method?
    protected abstract Command createCommand();
}

在包含要注入的方法的客户类(本例中为CommandManager)中,要注入的方法需要以下形式的签名:

<public|protected> [abstract] <return-type> theMethodName(no-arguments);

如果方法是抽象的,动态生成的子类会自动实现该方法。否则会将其覆盖。下面是例子:

<!-- a stateful bean deployed as a prototype (non-singleton) -->
<bean id="myCommand" class="fiona.apple.AsyncCommand" scope="prototype">
    <!-- inject dependencies here as required -->
</bean>

<!-- commandProcessor uses statefulCommandHelper -->
<bean id="commandManager" class="fiona.apple.CommandManager">
    <lookup-method name="createCommand" bean="myCommand"/>
</bean>

标识为commandManager的bean在需要myCommand bean的新实例时调用自己的createCommand()方法。如果实际上需要这样做,那么必须将myCommand bean部署为原型。如果它是单例的,则每次返回相同的myCommand bean实例。

或者,在基于注解的组件模型中,可以通过@Lookup annotation声明查找方法,如下例所示:

public abstract class CommandManager {

    public Object process(Object commandState) {
        Command command = createCommand();
        command.setState(commandState);
        return command.execute();
    }

    @Lookup("myCommand")
    protected abstract Command createCommand();
}

或者,你可以依赖目标bean根据lookup方法声明的返回类型进行解析:

public abstract class CommandManager {

    public Object process(Object commandState) {
        MyCommand command = createCommand();
        command.setState(commandState);
        return command.execute();
    }

    @Lookup
    protected abstract MyCommand createCommand();
}

注意,通常应该用具体的存根实现声明这种带注解的查找方法,以便它们与Spring的组件扫描规则兼容,默认情况下抽象类会被忽略。此限制不适用于显式注册或显式导入的bean类。

访问不同作用域目标bean的另一种方法是ObjectFactory/Provider注入点。

你还可能发现ServiceLocatorFactoryBean(在org.springframework.beans.factory.config包中)非常有用。

任意方法替换

与查找方法注入相比,方法注入的一种不太有用的形式是能够用另一个方法替换托管bean中的任意方法。你可以跳过本节的其余部分,直到实际需要此功能为止。

对于基于XML的配置元数据,可以使用replaced-method的方法元素为部署的bean替换现有的方法实现。考虑下面的类,它有一个我们想要重写的名为computeValue的方法:

public class MyValueCalculator {

    public String computeValue(String input) {
        // some real code...
    }

    // some other methods...
}

一个实现了org.springframework.beans.factory.support.MethodReplacer接口的类提供了新的方法,如下所示:

/**
 * meant to be used to override the existing computeValue(String)
 * implementation in MyValueCalculator
 */
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定义如下所示:

<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"/>

你可以在< replaced-method/>元素中使用一个或多个< arg-type/>元素来指示要重写的方法的方法签名。只有当方法被重载并且类中存在多个变量时,参数的签名才是必需的。为了方便起见,参数的类型字符串可以是完全限定类型名的子字符串。例如,以下所有内容都匹配java.lang.String:

java.lang.String
String
Str

因为参数的数量经常足以区分每个可能的选项,所以通过允许输入与参数类型匹配的最短字符串,可以节省大量的输入内容。

最后更新于