轻量级Java EE企业应用开发实战
上QQ阅读APP看书,第一时间看更新

6.2 IoC

IoC容器是Spring框架中非常重要的核心组件,可以说,是伴随着Spring诞生和成长的。Spring通过IoC容器来管理所有Java对象(也被称为Spring Bean)及其相互间的依赖关系。本节全面讲解IoC容器的概念及用法。

6.2.1 依赖注入与控制反转

很多人都会被问及“依赖注入(Dependency Injection,DI)”与“控制反转”之间到底有哪些联系和区别。在Java应用程序中,无论是受限的嵌入式应用程序,还是多层架构的服务端企业级应用程序,它们通常由来自应用适当的对象进行组合合作。也就是说,对象在应用程序中通过彼此依赖来实现功能。

尽管Java平台提供了丰富的应用程序开发功能,但它缺乏组织基本构建块成为一个完整系统的方法。那么,组织系统这个任务最后只能留给架构师和开发人员。开发者可以使用各种设计模式(如Factory、Abstract Factory、Builder、Decorator和Service Locator)来组合各种类和对象实例构成应用程序。虽然这些模式给出了能解决什么类型的问题,但使用模式的一个最大的障碍是,除非开发者有非常丰富的经验,否则仍然无法在应用程序中正确地使用它,这就给Java开发者设定了一定的技术门槛,特别是那些普通的开发人员。

而Spring框架的IoC(Inversion of Control,控制反转)组件能够通过提供正规化的方法来组合不同的组件,使之成为一个完整可用的应用。Spring框架将规范化的设计模式作为一级的对象,这样方便开发者将之集成到自己的应用程序,这也是很多组织和机构选择使用Spring框架来开发健壮的、可维护的应用程序的原因。开发人员无须手动处理对象的依赖关系,而是交给了Spring容器去管理,这极大地提升了开发体验。

那么依赖注入与控制反转又是什么关系呢?

依赖注入是Martin Fowler在2004年提出的关于控制反转的解释(6)。Martin Fowler认为控制反转一词让人产生疑惑,无法直白地理解“到底哪方面的控制被反转了”。所以,Martin Fowler建议采用依赖注入一词来代替控制反转。

依赖注入和控制反转其实就是一个事物的两种不同的说法而已,本质上是一回事。依赖注入是一个程序设计模式和架构模型,有时候也称为控制反转,尽管从技术上来讲,依赖注入是一个控制反转的特殊实现。依赖注入是指一个对象应用另一个对象来提供一个特殊的能力。例如,把一个数据库连接以参数的形式传到一个对象的结构方法里,而不是在那个对象内部自行创建一个连接。依赖注入和控制反转的基本思想就是把类的依赖从类内部转化到外部以减少依赖。利用控制反转,对象在被创建的时候会由一个调控系统统一进行对象实例的管理,将该对象所依赖的对象的引用通过调控系统传递给它。也可以说,依赖被注入对象中。所以,控制反转是关于一个对象如何获取它所依赖的对象的引用的过程,而这个过程体现为“谁来传递依赖的引用”这个职责的反转。

控制反转一般分为两种实现类型,依赖注入和依赖查找(Dependency Lookup)。其中依赖注入应用得比较广泛。Spring是采用依赖注入这种方式来实现控制反转的。

6.2.2 IoC容器和Bean

Spring通过IoC容器来管理所有Java对象及其相互间的依赖关系。在软件开发过程中,系统的各个对象之间、各个模块之间、软件系统与硬件系统之间或多或少都会存在耦合关系,如果一个系统的耦合度过高,就会造成难以维护的问题。但是完全没有耦合的代码是不能工作的,代码需要相互协作、相互依赖来完成功能。而IoC的技术恰好解决了这类问题,各个对象之间不需要直接关联,而是在需要用到对方的时候由IoC容器来管理对象之间的依赖关系,对于开发人员来说只需要维护相对独立的各个对象代码即可。

IoC是一个过程,即对象定义其依赖关系,而其他与之配合的对象只能通过构造函数参数、工厂方法的参数或者在工厂方法构造或返回后在对象实例上设置的属性来定义其依赖关系。然后,IoC容器在创建bean时会注入这些依赖项。这个过程在职责上是反转的,就是把原先代码里需要实现的对象创建、依赖的代码反转给容器来帮忙实现和管理,所以称为“控制反转”。

IoC应用了以下设计模式:

  • 反射:在运行状态中,根据提供的类的路径或者类名,通过反射来动态地获取该类的所有属性和方法。
  • 工厂模式:把IoC容器当作一个工厂,在配置文件或者注解中给出定义,然后利用反射技术,根据给出的类名生成相应的对象。对象生成的代码及对象之间的依赖关系在配置文件中定义,这样就实现了解耦。

org.springframework.beans和org.springframework.context包是Spring IoC容器的基础。BeanFactory接口提供了能够管理任何类型的对象的高级配置机制。ApplicationContext是BeanFactory的子接口,它更容易与Spring的AOP功能集成,进行消息资源处理(用于国际化)、事件发布以及作为应用层特定的上下文(例如,用于Web应用程序的WebApplicationContext)。简而言之,BeanFactory提供了基本的配置功能,而ApplicationContext在此基础之上增加了更多的企业特定功能。

在Spring应用中,bean是由Spring IoC容器来进行实例化、组装并受其管理的对象。bean和它们之间的依赖关系反映在容器使用的配置元数据中。

6.2.3 配置元数据

配置元数据描述了Spring容器在应用程序中是如何来实例化、配置和组装对象的。

最初,Spring用XML文件格式来记录配置元数据,从而很好地实现了IoC容器本身与实际写入此配置元数据的格式完全分离。

当然,基于XML的元数据不是唯一允许的配置元数据形式。目前,比较流行的配置元数据的方式是基于注解的配置和基于Java的配置。

  • 基于注解的配置:Spring 2.5引入了支持基于注解的配置元数据。
  • 基于Java的配置:从Spring 3.0开始,Spring JavaConfig项目提供了许多功能,并成为Spring框架核心的一部分。因此,可以使用Java而不是XML文件来定义应用程序类外部的bean。这类注解比较常用的有@Configuration、@Bean、@Import和@DependsOn等。

Spring配置至少需要一个或者多个由容器管理的Bean。基于XML的配置方式,需要用<beans/>元素内的<bean/>元素来配置这些Bean;而在基于Java的配置方式中,通常在使用了@Configuration注解的类中使用@Bean注解的方法。

以下示例显示基于XML的配置元数据的基本结构。

在上面的XML文件中,id属性用于标识单个bean定义的字符串。class属性定义bean的类型,并使用完全限定的类名。id属性的值是指协作对象。

以下示例显示基于注解的配置元数据的基本结构。

6.2.4 实例化容器

Spring IoC容器需要在应用启动时进行实例化。在实例化过程中,IoC容器会从各种外部资源(如本地文件系统、Java类路径等)加载配置元数据,提供给ApplicationContext构造函数。

下面是一个从类路径中加载基于XML的配置元数据的例子。

当系统规模比较大时,通常会让bean定义分到多个XML文件。这样,每个单独的XML配置文件通常就能够表示系统结构中的逻辑层或模块。就如上面的例子所演示的那样,当某个构造函数需要多个资源位置时,可以使用一个或多个<import/>来从另一个文件加载bean的定义,例如,

6.2.5 使用容器

ApplicationContext是高级工厂的接口,能够维护不同bean及其依赖项的注册表。其提供的方法T getBean(String name, Class<T> requiredType)可以用于检索Bean的实例。

ApplicationContext读取bean定义并按如下方式访问它们:

若配置方式不是XML而是Groovy,则可以将ClassPathXmlApplicationContext改为GenericGroovyApplicationContext。GenericGroovyApplicationContext是另一个Spring框架上下文的实现:

以上是使用ApplicationContext的getBean来检索Bean的实例的方式。ApplicationContext接口还有其他一些检索Bean的方法,但理想情况下应用程序代码不应该使用它们。因为程序代码根本不需要调用getBean方法的话,就可以完全不依赖于Spring API。例如,Spring与Web框架的集成为各种Web框架组件(如控制器和JSF托管的Bean)提供了依赖注入,允许通过元数据(例如自动装配注入)声明对特定bean的依赖关系。

6.2.6 Bean的命名

每个Bean都有一个或多个标识符。这些标识符在托管Bean的容器中必须是唯一的。一个Bean通常只有一个标识符,但是如果它需要多个标识符,那么额外的可以被认为是别名。

在基于XML的配置元数据中,使用id或者name属性来指定bean标识符。id属性允许指定一个id。通常,这些标识符的名称是字母,比如myBean、userService等,但也可能包含特殊字符。如果你想向bean引入其他别名,那么可以在name属性中指定它们,用“,”“;”或空格分隔。历史原因,在Spring 3.1以前的版本中,id属性被定义为一个xsd:ID类型,所以限制了可能的字符。从Spring 3.1开始,它被定义为一个xsd:string类型。注意,虽然类型更改了,但bean id的唯一性仍由容器强制执行。

用户也可以不必为bean提供名称或标识符。如果没有显式地提供名称或标识符,容器就会为该bean自动生成一个唯一的名称。但是,如果要通过名称引用该bean,就必须提供一个名称。

在命名bean时尽量遵守使用标准Java约定。也就是说,bean的名字使用以一个小写字母开头的骆驼法命名规则,比如accountManager、accountService、userDao、loginController等。使用这样命名的bean会让应用程序的配置更易于阅读和理解。

Spring为未命名的组件生成bean名称,同样遵循以上规则。本质上,简单的命名方式就是直接采用类名称并将其初始字符变为小写。但也有特例,当前两个字符或多个字符是大写时,我们不进行处理。比如,URL类的bean名称仍然是URL。这些命名规则定义在java.beans.Introspector.decapitalize方法中。

6.2.7 实例化bean的方式

所谓bean的实例化,就是根据配置来创建对象的过程。

如果是使用基于XML的配置方式,就在<bean />元素的class属性中指定需要实例化的对象的类型(或类)。这个class属性在内部实现,通常是一个BeanDefinition实例的Class属性。但也有例外情况,比如使用工厂方法或者bean定义继承进行实例化。

使用Class属性有两种方式:

  • 通常,容器本身是通过反射机制来调用指定类的构造函数,从而创建bean。这与使用Java代码的new运算符相同。
  • 通过静态工厂方法创建,类中包含静态方法。通过调用静态方法返回对象的类型可能和Class一样,也可能完全不一样。

如果你想配置使用静态的内部类,就必须用内部类的二进制名称。例如,在com.waylau包下有一个User类,这个类里面有一个静态的内部类Account,这种情况下bean定义的class属性应该是com.waylau.User$Account。这里需要注意,使用“$”字符来分割外部类和内部类的名称。

概括起来,bean的实例化有3种方式,分别说明如下:

1.通过构造函数实例化

Spring IoC容器可以管理几乎所有你想让它管理的类,不限于管理POJO。大多数Spring用户更喜欢使用POJO(一个默认无参的构造方法和setter、getter方法)。但在容器中使用非bean形式的类也是可以的,比如遗留系统中的连接池,很显然它与JavaBean规范不符,但Spring也能管理它。

当你使用构造方法来创建bean的时候,Spring对类来说并没有什么特殊之处。也就是说,正在开发的类不需要实现任何特定的接口或者以特定的方式进行编码。但是,根据你所使用的IoC类型,可能需要一个默认(无参)的构造方法。

当使用基于XML的元数据配置文件时,可以这样来指定bean类:

2.使用静态工厂方法实例化

当采用静态工厂方法创建bean时,除了需要指定class属性外,还需要通过factory-method属性来指定创建bean实例的工厂方法,Spring将调用此方法返回实例对象。就此而言,跟通过普通构造器创建类实例没什么两样。

下面的bean定义展示了如何通过工厂方法来创建bean实例。

以下是基于XML的元数据配置文件:

以下是需要创建实例的类的定义:

注 意

在此例中,createInstance()必须是一个static方法。

3.使用工厂实例方法实例化

通过调用工厂实例的非静态方法进行实例化与通过静态工厂方法实例化类似。使用这种方式时,class属性置为空,而factory-bean属性必须指定为当前(或其祖先)容器中包含工厂方法的bean的名称,而该bean的工厂方法本身必须通过factory-method属性来设定。

以下是基于XML的元数据配置文件:

以下是需要创建实例的类的定义:

当然,一个工厂类也可以有多个工厂方法。以下是基于XML的元数据配置文件:

以下是需要创建实例的类的定义:

6.2.8 注入方式

在Spring框架中,主要有以下两种注入方式:

1.基于构造函数

基于构造函数的DI是通过调用具有多个参数的构造函数的容器来完成的,每个参数表示依赖关系,这个与调用具有特定参数的静态工厂方法来构造bean几乎是等效的。以下示例演示一个只能使用构造函数注入的依赖注入的类,该类是一个POJO,并不依赖于容器特定的接口、基类或注解。

基于构造函数的DI通常需要处理传参。构造函数的参数解析是通过参数的类型来匹配的。如果bean的构造函数参数不存在歧义,那么构造器参数的顺序就是这些参数实例化以及装载的顺序。参考如下代码:

假设Bar和Baz在继承层次上不相关,也没有什么歧义,下面的配置完全可以工作正常,开发者不需要再去<constructor-arg>元素中指定构造函数参数的索引或类型信息。

当引用另一个bean的时候,如果类型确定,匹配就会工作正常(如上面的例子)。

当使用简单的类型的时候,比如<value>true</value>,Spring IoC容器是无法判断值的类型的,所以是无法匹配的。考虑代码如下:

那么,在上面的代码这种情况下,容器可以通过使用构造函数参数的type属性来实现简单类型的匹配,比如:

或者使用index属性来指定构造参数的位置,比如:

这个索引同时是为了解决构造函数中有多个相同类型的参数无法精确匹配的问题。需要注意的是,索引是从0开始的。

开发者可以通过参数的名称来去除二义性。

需要注意的是,做这项工作的代码必须启用了调试标记编译,这样Spring才可以从构造函数查找参数名称。开发者也可以使用@ConstructorProperties注解来显式声明构造函数的名称,比如如下代码:

2.基于setter方法

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

以下示例演示一个只能使用setter来将依赖进行注入的类。该类是一个POJO,并不依赖于容器特定的接口、基类或注解。

6.2.9 实战:依赖注入的例子

我们创建一个名为dependency-injection的应用来演示依赖注入的用法。

dependency-injection应用是基于XML的配置方式。我们将会演示基于构造函数的依赖注入,同时会演示如何来解析构造函数的参数。

1.定义服务类

定义了消息服务接口MessageService,该接口的主要职责是打印消息,代码如下:

消息服务类接口的实现是MessageServiceImpl,返回我们真实想要的业务消息,代码如下:

其中,MessageServiceImpl是具有带参的构造函数,username和age是构造函数的参数。这两个参数最终会在getMessage方法中返回。

2.定义打印器

定义了打印器MessagePrinter,用于打印消息,代码如下:

我们期望,在执行printMessage方法之后就能将消息内容打印出来。而消息内容是依赖于MessageService提供的。稍后,我们会通过XML配置的方式,来将MessageService的实现进行注入。

3.定义应用主类

Application是应用的入口类,代码如下:

由于我们的应用是基于XML的配置,因此这里需要ClassPathXmlApplicationContext类。这个类是Spring上下文的其中一种实现,可以实现基于XML的配置加载。按照约定,Spring应用的配置文件spring.xml放置在应用的resources目录下。

4.创建配置文件

在应用的resources目录下创建了一个Spring应用的配置文件spring.xml:

在该spring.xml文件中,我们可以清楚地看到bean之间的依赖关系。messageServiceImpl有两个构造函数的参数:username和age,其参数值在实例化的时候就解析了。messagePrinter引用了messageServiceImpl作为其构造函数的参数。

5.运行

运行Application类就能在控制台看到“Hello World! Way Lau, age is 30”字样的信息。

6.2.10 依赖注入的详细配置

在上面的示例中,我们展示了依赖注入的大部分配置。开发者可以通过定义bean的依赖来引用其他bean或者一些值。Spring基于XML的配置元数据通过支持一些子元素<property/>以及<constructor-arg/>来达到这一目的。这些配置可以满足应用开发的大部分场景。

下面就这些配置内容进行详细的讲解。

1.直接赋值

直接赋值支持字符串、原始类型的数据。

元素<property/>有value属性,通过对人友好易读的形式来配置属性值或者构造参数。Spring的便利之处就是用来将这些字符串的值转换成指定的类型。

下面的例子使用的p命名空间是更为简洁的XML配置。

虽然上面的XML更为简洁,但是因为属性的类型是在运行时确定的,而非设计时确定的,所以可能需要IDE特定的支持才能够自动完成属性配置。

开发者也可以定义一个java.util.Properties实例,比如:

Spring的容器会将<value/>里面的文本通过使用JavaBean的PropertyEditor机制转换成一个java.util.Properties实例。这是一个捷径,也是一些Spring团队更喜欢使用嵌套的<value/>元素而不是value属性风格的原因。

2.引用其他bean

如果bean之间有协作的关系,就可以引用其他bean。

ref元素是<constructor-arg/>或者<property/>中的一个终极标签。开发者可以通过这个标签配置一个bean来引用另一个bean。当需要引用一个bean的时候,被引用的bean会先实例化,然后配置属性,也就是引用的依赖。如果该bean是单例的话,那么该bean会由容器初始化。所有引用最终都是对另一个对象的引用。bean的范围以及校验取决于开发者是否通过bean、local、parent这些属性来指定对象的id或者name属性。

通过指定bean属性中的<ref/>来指定依赖是常见的一种方式,可以引用容器或者父容器中的bean,无论是否在同一个XML文件定义都可以引用。其中bean属性中的值可以和其他引用bean中的id属性一致,或者和其中的一个name属性一致。

<ref bean="someBean"/>

通过指定bean的parent属性会创建一个引用到当前容器的父容器中。parent属性的值可以跟目标bean的id属性一致,或者和目标bean的name属性中的一个一致,且目标bean必须是当前引用目标bean容器的父容器。开发者一般只有在存在层次化容器关系,并且希望通过代理来包裹父容器中一个存在的bean的时候才会用到这个属性。

我们来看这个例子。这个accountService是父容器的bean:

<bean id="accountService" class="com.waylau.SimpleAccountService">
</bean>

在子容器中同样有一个名为accountService的bean:

由于两个容器有相同id属性的bean,因此为了避免歧义,需要加parent属性的值。

3.内部bean

定义在<bean/>元素的<property/>或者<constructor-arg/>元素之内的bean叫作内部bean。

内部bean的定义是不需要指定id或者名字的。如果指定了,容器就不会用之作为区分bean的标识符。容器同时也会无视内部bean的scope标签。所以,内部bean总是匿名的,而且总是随着外部bean同时来创建的。开发者是无法将内部bean注入外部bean以外的其他bean的。

4.集合

在<list/>、<set/>、<map/>和<props/>元素中,开发者可以配置Java集合类型List、Set、Map以及Properties的属性和参数。示例如下:

当然,map的key、value或者集合的value都可以配置为下列元素:

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

5.Null及空字符的值

Spring会将属性的空参数直接当成空字符串来处理。下面的基于XML的配置会将email属性配置为String的"":

上面的例子和以下Java代码的效果是一致的。

    exampleBean.setEmail("");

而<null/>元素则用来处理Null值,代码如下:

上面的代码和下面的Java代码效果是一样的:

    exampleBean.setEmail(null);

6.XML短域名空间

p命名空间令开发者可以使用bean的属性,而不用使用嵌套的<property/>元素就能描述开发者想要注入的依赖。以下是使用了p命名空间的例子:

与p命名空间类似,c命名空间允许内联的属性来配置构造参数而不用使用constructor-arg元素。c命名空间是在Spring 3.1首次引入的。

下面是一个使用了c命名空间的例子:

7.复合属性名称

开发者可以在配置属性的时候配置复合属性名称,只要确保除了最后一个属性外,其余的属性值都不能为Null即可。

考虑以下的例子:

foo有一个fred属性,而其中fred属性有一个bob属性,而bob属性中有一个sammy属性,最后这个sammy属性会配置为123。想要上述配置能够生效,就需要确保foo的fred属性和fred的bob属性在构造bean之后不能为Null,否则抛出NullPointerException异常。

6.2.11 使用depends-on

如果一个bean是另一个bean的依赖,那么通常这个bean也是另一个bean的属性之一。多数情况下,开发者可以在配置XML元数据的时候使用<ref/>标签。然而,有时bean之间的依赖关系不是直接关联的。比如需要调用类的静态实例化器来触发,类似数据库驱动注册。depends-on属性会使明确的强迫依赖的bean在引用之前就会初始化。下面的例子使用depends-on属性来表示单例bean上的依赖。

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

如果想要依赖多个bean,就可以提供多个名字作为depends-on的值,以逗号、空格或者分号分割,代码如下:

6.2.12 延迟加载bean

默认情况下,ApplicationContext会在实例化的过程中创建和配置所有的单例bean。总的来说,这个预初始化是很不错的。因为这样能及时发现环境上的一些配置错误,而不是系统运行了很久之后才发现。如果这个行为不是迫切需要的,开发者就可以通过将bean标记为延迟加载阻止这个预初始化。延迟初始化的bean会通知IoC不要让bean预初始化,而是在被引用的时候才会实例化。

在XML中,可以通过<bean/>元素的lazy-init属性来控制这个行为,代码如下:

当将bean配置为上面的XML的时候,ApplicationContext中的延迟加载bean是不会随着ApplicationContext的启动而进入预初始化状态的,而那些非延迟加载的bean是处于预初始化状态的。

然而,如果一个延迟加载的bean作为另一个非延迟加载的单例bean的依赖而存在,那么延迟加载的bean仍然会在ApplicationContext启动的时候加载,因为作为单例bean的依赖会随着单例bean的实例化而实例化。

开发者可以通过使用<beans/>的default-lazy-init属性在容器层次控制bean是否延迟初始化,比如:

<beans default-lazy-init="true">
</beans>

6.2.13 自动装配

Spring Boot通常使用基于Java的配置,建议主配置是单个@Configuration类。通常定义main方法的类作为主要的@Configuration类。

Spring Boot应用了很多Spring框架中的自动配置功能。自动配置会尝试根据添加的jar依赖关系自动配置Spring应用程序。例如,如果HSQLDB或者H2在类路径上,并且没有手动配置任何数据库连接bean,那么Spring Boot会自动配置为内存数据库。

要启用自动配置功能,需要将@EnableAutoConfiguration或@SpringBootApplication注解添加到一个@Configuration类中。

1.自动配置

在Spring应用中,可以自由使用任何标准的Spring框架技术来定义bean及其注入的依赖关系。为了简化程序的开发,通常使用@ComponentScan来找到bean,并结合@Autowired构造函数来将bean进行自动装配注入。这些bean涵盖了所有应用程序组件,如@Component、@Service、@Repository、@Controller等。下面是一个实际的例子。

如果一个bean只有一个构造函数,就可以省略@Autowired。

2.使用@SpringBootApplication注解

@SpringBootApplication注解是Spring Boot中的配置类注解。由于Spring Boot开发人员总是频繁使用@Configuration、@EnableAutoConfiguration和@ComponentScan来注解它们的主类,并且这些注解经常被一起使用,因此Spring Boot提供了一种方便的@SpringBootApplication注解来替代。

@SpringBootApplication注解相当于使用@Configuration、@EnableAutoConfiguration和@ComponentScan及其默认属性。

6.2.14 方法注入

在大多数应用场景下,大多数的bean都是单例的。当这个单例的bean需要和非单例的bean联合使用的时候,有可能会因为不同的bean生命周期的不同而产生问题。假设单例的bean A在每个方法调用中使用了非单例的bean B,由于容器只会创建bean A一次,而只有一个机会来配置属性,因此容器无法给bean A每次都提供一个新的bean B的实例。

一个解决方案就是放弃一些IoC。开发者可以通过实现ApplicationContextAware接口,调用ApplicationContext的getBean("B")方法来在bean A需要新的实例的时候来获取到新的B实例。参考下面的例子:

当然,这种方式有一些弊端,就是需要依赖于Spring的API。这在一定程度上对Spring框架存在耦合。

那么是否有其他方案来避免这些弊端呢?答案是肯定的。

Spring框架提供了<lookup-method/>和<replaced-method/>来解决上述问题。

1.lookup-method注入

lookup-method注入是Spring动态改变bean里方法的实现。其实现原理是利用CGLIB库,将bean方法执行返回的对象,重新生成子类和重写配置,从而达到动态改变的效果。

下面来看一个例子:

XML配置如下:

当然,如果是基于注解的配置方式,就可以添加@Lookup注解到相应的方法上:

下面的方式是等效的:

注 意

由于采用CGLIB生成之类的方式,因此需要用来动态注入的类不能是final修饰的,需要动态注入的方法也不能是final修饰的。

同时,还得注意myCommand的scope的配置,如果scope配置为singleton,那么每次调用方法createCommand返回的对象都是相同的;如果scope配置为prototype,那么每次调用返回的对象都不同。

2.replaced-method注入

replaced-method注入是Spring动态改变bean里方法的实现。需要改变的方法使用Spring内原有的其他类(需要继承接口org.springframework.beans.factory.support.MethodReplacer)的逻辑替换这个方法。通过改变方法执行逻辑来动态改变方法。内部实现为使用CGLIB方法,重新生成子类,重写配置的方法和返回对象,达到动态改变的效果。

下面来看一个例子:

另一个类则实现了org.springframework.beans.factory.support.MethodReplacer接口。

XML配置如下:

注 意

由于采用CGLIB生成之类的方式,因此需要用来动态注入的类不能是final修饰的,需要动态注入的方法也不能是final修饰的。

6.2.15 bean scope

默认情况下,所有Spring bean都是单例的,意味着整个Spring应用中,bean的实例只有一个。可以在bean中添加scope属性来修改这个默认值。scope属性可用的值如表6-1所示。

表6-1 Spring bean scope属性值

下面详细讨论singleton bean与prototype bean在用法上的差异。

6.2.16 singleton bean与prototype bean

对于singleton bean来说,IoC容器只管理一个singleton bean的共享实例,所有对该bean的请求都会导致Spring容器返回一个特定的bean实例。

换句话说,当定义一个bean并将其定义为singleton时,Spring IoC容器将仅创建一个由该bean定义的对象实例。该单个实例存储在缓存中,对该bean所有后续请求和引用都将返回缓存中的对象实例。

在Spring IoC容器中,singleton bean是默认的创建bean的方式,可以更好地重用对象,节省了重复创建对象的开销。

图6-1所示为singleton bean使用示意图。

对于prototype bean来说,IoC容器导致在每次对该特定bean进行请求时创建一个新的bean实例。

图6-1 singleton bean使用示意图

从某种意义上来说,Spring容器在prototype bean上的作用等同于Java的new操作符,所有过去的生命周期管理都必须由客户端处理。

图6-2 prototype bean使用示意图

使用singleton bean还是prototype bean需要注意业务场景。一般情况下,singleton bean适用于大多数场景,但某些场景(如多线程)需要每次调用都生成一个实例,此时scope就应该设为prototype。

需要注意singleton bean引用prototype bean时的陷阱(7)。你不能依赖注入一个prototype范围的bean到你的singleton bean中,因为这个注入只发生一次,就是当Spring容器正在实例化singleton bean并解析和注入它的依赖时。如果你不止一次在运行时需要一个prototype bean的新实例,就可以采用方法注入的方式。

6.2.17 理解生命周期机制

在Spring 2.5之后,开发者有3种选择来控制bean的生命周期行为:

  • InitializingBean和DisposableBean回调接口。
  • 自定义的init()以及destroy方法。
  • 使用@PostConstruct以及@PreDestroy注解。

开发者也可以在Bean上联合这些机制一起使用。如果一个bean配置了多个生命周期机制,并且含有不同的方法名,执行的顺序如下:

  • 包含@PostConstruct注解的方法。
  • 在InitializingBean接口中的afterPropertiesSet()方法。
  • 自定义的init()方法。

销毁方法的执行顺序和初始化的执行顺序相同:

  • 包含@PreDestroy注解的方法。
  • 在DisposableBean接口中的destroy()方法。
  • 自定义的destroy()方法。

6.2.18 基于注解的配置

Spring应用支持多种配置方式。除了XML配置之外,开发人员更加流行使用基于注解的配置。基于注解的配置方式允许开发人员将配置信息移入组件类本身中,在相关的类、方法或字段上声明使用注解。

Spring提供了非常多的注解,比如Spring 2.0引入的用@Required注解来强制所需属性不能为空。在Spring 2.5中,可以使用相同的处理方法来驱动Spring的依赖注入。从本质上来说,@Autowired注解提供了更细粒度的控制和更广泛的适用性。Spring 2.5添加了对JSR-250注解的支持,比如@Resource、@PostConstruct和@PreDestroy。Spring 3.0添加了对JSR-330注解的支持,包含在javax.inject包下,比如有@Inject、@Qualifier、@Named和@Provider等。使用这些注解需要在Spring容器中注册特定的BeanPostProcessor。

注 意

基于注解的配置注入会在基于XML的配置注入之前执行,因此同时使用两种方式,后面的配置会覆盖前面装配的属性。

1.@Required

@Required注解应用于bean属性的setter方法,就像下面这个示例:

这个注解只是表明受影响的bean的属性必须在bean的定义中或者自动装配中通过明确的属性值在配置时来填充。如果受影响的bean属性没有被填充,那么容器就会抛出异常。这就是通过快速失败的机制来避免NullPointerException。

2.@Autowired

可以使用@Autowired注解到“传统的”setter方法中:

JSR-330的@Inject注解可以代替上面示例中的Spring的@Autowired注解。

也可以将注解应用于任意名称和(或)多个参数的方法:

也可以将它用于构造方法和字段:

也可以提供ApplicationContext中特定类型的所有bean,通过添加注解到期望哪种类型的数组的字段或者方法上:

同样,也可以用于特定类型的集合:

默认情况下,当出现零个候选bean的时候,自动装配就会失败。默认的行为是将被注解的方法、构造方法和字段作为需要的依赖关系。这种行为也可以通过下面这样的做法来改变。

推荐使用@Autowired的required属性而不是@Required注解。required属性表示属性对于自动装配的目的不是必需的,如果它不能被自动装配,那么属性就会忽略。另一方面,@Required更健壮一些,它强制由容器支持的各种方式的属性设置。如果没有注入任何值,就会抛出对应的异常。

3.@Primary

因为通过类型的自动装配可能有多个候选者,所以在选择过程中通常需要更多控制。达成这个目的的一种做法是使用Spring的@Primary注解。当一个依赖有多个候选者bean时,@Primary指定了一个优先提供的特殊bean。当多个候选者bean中存在一个确切的指定了@Primary的bean时,它将会自动装载这个bean。

下面来看一个例子:

对于上面的配置,下面的MovieRecommender将会使用firstMovieCatalog自动注解。

4.@Qualifier

因为通过类型的自动装配可能有多个候选者,所以在选择过程中通常需要更多控制。达成这个目的的一种做法是使用Spring的@Qualifier注解。你可以用特定的参数来关联限定符的值,缩小类型的集合匹配,那么通过参数就能选择特定的bean。用法如下:

@Qualifier注解也可以在独立的构造方法参数或方法参数中来指定:

5.@Resource

Spring支持使用JSR-250的@Resource注解在字段或bean属性的setter方法上的注入。这在Java EE 5和Java EE 6中是一个通用的模式,比如在JSF 1.2中管理的bean或JAX-WS 2.0端点。Spring也为其所管理的对象支持这种模式。

@Resource使用name属性,默认情况下Spring解析这个值作为要注入的bean的名称。换句话说,如果遵循by-name语义,就如在这个示例所展示的:

如果没有明确地指定name值,那么默认的名称就从字段名称或setter方法中派生出来。如果是字段,它就会选用字段名称;如果是setter方法,它就会选用bean的属性名称。所以下面的示例中名为movieFinder的bean通过setter方法来注入:

6.@PostConstruct和@PreDestroy

CommonAnnotationBeanPostProcessor不但能识别@Resource注解,而且还能识别JSR-250生命周期注解。在下面的示例中,在初始化后缓存会预先填充,并在销毁后会清理。

6.2.19 基于注解的配置与基于XML的配置

毫无疑问,最早的Spring配置是基于XML的配置。随着JDK 1.5发布,Java开始支持注解,同时,Spring也开始支持基于注解的配置方式。

基于注解的配置方式一定要比基于XML的配置方式更好吗?答案是具体问题具体分析。

实际上,无论是基于注解的配置方式,还是基于XML的配置方式,每种方式都有它的利与弊,通常是让开发人员来决定使用哪种策略更适合。由于定义它们的方式,注解在声明中提供了大量的上下文,使得配置更加简洁。然而,XML更擅长装配组件,而不需要触碰它们的源代码或重新编译。一些开发人员更喜欢装配源码,因为添加了注解的类会被有些人认为不再是POJO了,而且基于注解的配置会让配置变得分散并且难以控制。

无论怎么选择,Spring都可以容纳两种方式,甚至是它们的混合体。值得指出的是通过JavaConfig方式,Spring允许以非侵入式的方式来使用注解,而不需要触碰目标组件的源代码和工具。