这部分的文档覆盖了Spring完整的技术。
在这些技术中最重要的要属Spring的控制反转(IoC)容器了,紧随其后的是全面覆盖的面向切面编程(AOP)技术。Spring有它自己的AOP框架,它很容易理解,而且成功解决了Java企业编程中80%的AOP需求。
Spring也集成了AspectsJ(目前在Java领域使用最丰富最成熟的AOP实现 )。
本章主要介绍Spring关于控制反转的实现。控制反转(IoC)又被称作依赖注入(DI)。它是一个对象定义其依赖的过程,它的依赖也就是与它一起合作的其它对象,这个过程只能通过构造方法参数、工厂方法参数、或者被构造或从工厂方法返回后通过settter方法设置其属性来实现。然后容器在创建bean时注入这些依赖关系。这个过程本质上是反过来的,由bean本身控制实例化,或者直接通过类的结构或Service定位器模式定位它自己的依赖,因此得其名曰控制反转。
org.springframework.beans和org.springframework.context两个包是Spring IoC容器的基础。BeanFactory接口提供了一种先进的管理任何类型对象的配置机制。ApplicationContext是BeanFactory的子接口,它使得与Spring AOP特性集成更简单,添加了消息资源处理(用于国际化)、事件发布,还添加了应用层特定的上下文,比如用于web应用的WebApplicationContext。
简而言之,BeanFactory提供了配置框架和基础功能,ApplicationContext在此基础上添加了更多的企业级特定功能。ApplicationContext是BeanFactory的完整超集,且仅用于本章描述Spring的IoC容器。更多使用BeanFactory代替ApplicationContext的信息请参考7.16 BeanFactory“。
在Spring中,形成应用主干且被Spring IoC容器管理的对象称为beans。一个bean就是被Spring IoC容器实例化、装配和管理的对象。简单来说,一个bean就是你的应用中众多对象中一个。Beans和它们之间的依赖被容器中配置的元数据反射。
org.springframework.context.ApplicationContext代表了Spring的IoC容器,并且负责实例化、配置和装配前面提及的bean。容器通过读取配置元数据获取指令来决定哪些对象将被实例化、配置和装配。配置元数据反映在XML、Java注解或者Java代码中。它允许你表达组合成应用的对象及它们之间丰富的依赖关系。
Spring提供了几个可以直接使用的ApplicationContext接口的实现。在独立的应用中通常创建ClassPathXmlApplicationContext或者FileSystemXmlApplicationContext的实例。XML是定义配置元数据的传统形式,也可以通过添加一小段XML配置让容器声明式地支持Java注解或Java代码配置元数据的形式。
在大部分应用场景中,显式的代码不需要实例化一个或多个Spring IoC容器。例如,在web应用中,在web.xml中引用简单的八行(大约)样板XML描述符足够了(参考7.15.4 web应用中方便地实例化ApplicationContext](#context-create))。如果使用Spring工具套件Eclipse开发环境,样板配置只需要简单地点击几次鼠标或键盘就可以被创建。
下面的图表展示了Spring是怎么工作的。应用程序的类文件和配置元数据是绑定在一起的,所以在ApplicationContext被创建和初始化后,你将拥有完整的已配置好的和可执行的系统或应用。
图7.1 Spring的IoC容器
如前述图表所述,Spring IoC容器是消费配置元数据的一种形式,配置元数据代表开发者告诉Spring容器怎么去实例化、配置和装配应用中的对象。
配置元数据习惯上使用简单直观的XML形式,本章大部分地方也使用这种形式来传达Spring IoC容器的关键概念和特性。
关于使用其它形式配置元数据,参考:
Spring配置包含了至少一个必须容器管理的bean定义。基于XML的配置元数据显示为被顶级**
这些bean定义与组成应用的实际对象通信。典型地,可以定义service层对象、数据访问对象(DAO)、诸如Struts Action实例的视图层对象、基础对象如Hibernate SessionFactories、JMS Queues,等等,也不需要在容器中定义细粒度的领域对象,因为通常由DAO层和业务逻辑来创建和加载领域对象。然而,可以使用与AspectJ的集成来配置在IoC容器控制之外创建的对象。参考使用AspectJ依赖注入领域对象。
下面展示了XML配置的基础结构:
<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.xsd">
<bean id="..." class="...">
bean>
<bean id="..." class="...">
bean>
beans>
id属性是一个字符串,它唯一标识一个bean的定义,class属性定义了bean的类型并且需要使用全路径。id属性的值指向了合作的对象。XML指定合作者XML并没有显示在上述例子中,具体可参考依赖。
实例一个Spring IoC容器很简单,提供给ApplicationContext构造方法的一个或多个路径是实际的资源字符串,这使得容器可以从不同的外部文件加载配置元数据,比如本地的文件系统,Java的CLASSPATH,等等。
ApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"services.xml", "daos.xml"});
下面的例子展示了service层的对象配置文件(services.xml):
<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.xsd">
<bean id="petStore" class="org.springframework.samples.jpetstore.services.PetStoreServiceImpl">
<property name="accountDao" ref="accountDao"/>
<property name="itemDao" ref="itemDao"/>
bean>
beans>
下面的例子展示了数据访问对象daos.xml文件:
<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.xsd">
<bean id="accountDao"
class="org.springframework.samples.jpetstore.dao.jpa.JpaAccountDao">
bean>
<bean id="itemDao" class="org.springframework.samples.jpetstore.dao.jpa.JpaItemDao">
bean>
beans>
在前面的例子中,service层包含类PetStoreServiceImpl和两个数据访问对象JpaAccountDao和JpaItemDao(基于JPA 对象关系映射标准)。property元素的name属性指向了JavaBean属性的名字,ref属性指向另一个bean定义的名字。id和ref之间的链接表达了两个合作者间的依赖关系。详细的配置和对象依赖请参考依赖。
跨越多个XML文件配置bean定义是很有用的,通常一个独立的XML配置文件代表架构中的一个逻辑层或模块。
可以使用ApplicationContext的构造方法加载所有这些XML片段的bean定义。构造方法传入多个Resource路径即可,就像前面章节介绍的那样。替代方案,也可以使用一个或多个**
<beans>
<import resource="services.xml"/>
<import resource="resources/messageSource.xml"/>
<import resource="/resources/themeSource.xml"/>
<bean id="bean1" class="..."/>
<bean id="bean2" class="..."/>
beans>
上面的例子中,外部的bean定义从三个文件中加载进来:services.xml, messageSource.xml和themeSource.xml。所有的路径对于当前文件都是相对路径,所以services.xml必须在同一个目录或当前路径所在的classpath下,而messageSource.xml和themeSource.xml则必须在resources路径下,resources必须当前文件目录的下一级。如你所见,开头的斜杠被忽略了,即使给了路径也是相对的,不使用开头的斜杠格式还更好点。被导入文件的内容,包括顶级的**
ApplicationContext接口是一个先进的工厂,用于维护不同的bean及其依赖关系的注册。使用**T getBean(String name, Class requiredType)**方法可以获取bean实例。
ApplicationContext允许你可以像下面这样读取bean定义并访问它们:
// create and configure beans
ApplicationContext context = new ClassPathXmlApplicationContext(new String[] {"services.xml", "daos.xml"});
// retrieve configured instance
PetStoreService service = context.getBean("petStore", PetStoreService.class);
// use configured instance
List<String> userList = service.getUsernameList();
使用**getBean()获取bean实例。ApplicationContext接口还有几个其它方法用于获取bean,但是理想状态下应用程序代码应该从来都不会用到它们。应用程序代码的解应该不会调用getBean()**方法,而且不应该依赖于Spring的API。例如,Spring的web框架集成为不同的web框架类提供了依赖注入,比如controller和JSF管理的bean。
Spring IoC容器管理了至少一个bean,这些bean通过提供给容器的配置元数据来创建,例如,XML形式的**
在容器本身内部,这些bean定义表示为BeanDefinition对象,它包含以下元数据:
元数据转化成了一系列的属性,组成了每个bean的定义。
表 7.1. bean定义
属性 | 参考 |
---|---|
class | 7.3.2 实例化bean |
name | 7.3.1 命名bean |
scope | 7.5 bean的作用域 |
构造方法的参数 | 7.4.1 依赖注入 |
属性 | 7.4.1 依赖注入 |
自动装配的模式 | 7.4.5 自动装配合作者 |
延迟初始化的模式 | 7.4.4 延迟初始化bean |
初始化的模式 | 初始化回调 |
销毁方法 | 销毁回调 |
除了包含创建指定bean的信息的bean定义外,ApplicationContext实现也允许注册用户在容器外创建的已存在的对象。可以通过ApplicationContext的getBeanFactory()方法获取其BeanFactory的实现DefaultListableBeanFactory。DefaultListableBeanFactory通过**registerSingleton(…)和registerBeanDefinition(…)**方法支持这样的注册。然而,典型的应用都是通过元数据定义bean。
每一个bean都有一个或多个标识符。这些标识符在托管bean的容器中必须唯一。一个bean通常只有一个标识符,但如果需要更多标识符,可以通过别名来实现。
在XML中,可以使用id和/或name属性来指定bean的标识符。id属性允许显式地指定一个id。按照约定,这些名字是字母数字的(‘myBean’, ‘fooService’, 等),但是也可以包含一些特殊字符。如果你想引入其它别名,也可以在name属性中指定,用逗号(,)、分号(;)或空格分割。作为历史记录,Spring 3.1之前的版本,id属性被定义为xsd:ID,它会限制一些可能的字符。3.1开始,它被定义为xsd:string类型。注意id的唯一性是被容器强制执行的,而不再是XML解析器。
给bean指定一个名字或id并不是必需的。如果没有显式地指定名字或id,容器会为那个bean生成一个唯一的名字。但是,如果要根据名字引用那个bean,比如通过ref元素或Service定位器查找,那么必须指定一个名字。不提供名字一般用于内部bean和自动装配合作者。
在bean的定义中,可以为其提供多个名字,通过与id属性指定的名字或name属性里的其它名字组合起来。这些名字对同一个bean是等价的,在有些情况下很有用,比如,允许应用中的每个组件通过其本身指定的名字引用共同的依赖。
在bean定义的地方指定所有的别名并不足够。有时候希望在其它地方为bean指定一个别名。这在大型系统中很常见,它们的配置被很多子系统分割,每个子系统有它自己的一系列bean定义。在XML配置中,可以使用**
<alias name="fromName" alias="toName"/>
在上述这个案例中,同一个容器中这个bean叫fromName,在使用了别名定义也可以叫toName。
例如,子系统A中的配置元数据通过subsystemA-datasource引用一个数据源,子系统B中的配置元数据通过subsystemB-datasource引用这个数据源,使用这两个子系统的主应用通过myApp-datasource引用这个数据源。为了使用这三个名字引用相同的对象,可以在MyApp配置中添加如下的别名定义:
<alias name="subsystemA-dataSource" alias="subsystemB-dataSource"/>
<alias name="subsystemA-dataSource" alias="myApp-dataSource" />
现在每个组件和主应用都可以通过不同的名字引用这个数据源了,并且可以保证不会与任何其它的定义冲突(有效地创建了一个命名空间),它们还引用了同一个bean。
一个bean的定义本质上就是创建一个或多个对象的食谱。容器查看这个食谱并使用被bean定义封装的配置元数据来创建(或获取)实际的对象。
如果使用XML配置,在**
用构造方法创建bean的方式,在Spring中所有正常的类都可以使用并兼容。也就是说,被开发的类不需要实现任何特定的接口或以特定的形式编码。仅仅指定bean类就足够了。但是,依靠什么样的类型来指定bean,你可能需要默认的空构造方法。
Spring的IoC窗口几乎可以管理所有的类,并不仅限于真的JavaBean。大部分用户更喜欢带有默认(无参)构造方法和适当setter/getter方法的JavaBean。你也可以拥有不同的非bean风格的类。例如,如果你需要使用不是JavaBean格式的连接池,Spring一样可以管理它。
在XML中,可以像下面这样指定bean类:
关于如何为构造方法提供参数(如果需要的话)及在对象被构造之后为其设置属性,请参考注入依赖。
当使用静态工厂方法创建一个bean时,需要使用class属性指定那个包含静态工厂方法的类,并使用factory-method属性指定工厂方法的名字。应该可以调用这个方法(带参数的稍后讨论)并返回一个有效的对象,之后它就像用构造方法创建的对象一样对待。一种这样的bean定义的使用方法是在代码调用静态工厂。
下面的bean定义指定了通过调用工厂方法创建这个bean。这个定义没有指定返回类型,仅仅指定了包含工厂方法的类。在这个例子中,**createInstance()**方法必须是静态的。
<bean id="clientService"
class="examples.ClientService"
factory-method="createInstance"/>
public class ClientService {
private static ClientService clientService = new ClientService();
private ClientService() {}
public static ClientService createInstance() {
return clientService;
}
}
关于如何为工厂方法提供参数及在对象从工厂返回之后为其设置属性,请参考依赖与配置详解。
与使用静态工厂方法实例化类似,使用实例的工厂方法实例化是调用一个已存在的bean的非静态方法来创建一个新的bean。为了使用这种机制,请把class属性置空,在factory-bean属性中指定被调用的用来创建对象的包含工厂方法的那个bean的名字,并factory-method属性中设置工厂方法的名字。
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
bean>
<bean id="clientService"
factory-bean="serviceLocator"
factory-method="createClientServiceInstance"/>
public class DefaultServiceLocator {
private static ClientService clientService = new ClientServiceImpl();
private DefaultServiceLocator() {}
public ClientService createClientServiceInstance() {
return clientService;
}
}
一个工厂类可以拥有多个工厂方法,如下:
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
bean>
<bean id="clientService"
factory-bean="serviceLocator"
factory-method="createClientServiceInstance"/>
<bean id="accountService"
factory-bean="serviceLocator"
factory-method="createAccountServiceInstance"/>
public class DefaultServiceLocator {
private static ClientService clientService = new ClientServiceImpl();
private static AccountService accountService = new AccountServiceImpl();
private DefaultServiceLocator() {}
public ClientService createClientServiceInstance() {
return clientService;
}
public AccountService createAccountServiceInstance() {
return accountService;
}
}
工厂bean本身也可以通过依赖注入管理和配置,参考依赖与配置详解。
一个典型的企业级应用包含了不止一个对象(或者用Spring的说法叫bean)。即使是最简单的应用也需要几个对象一起工作以展现给最终用户看到的连贯应用。下一节介绍怎么样定义一系列独立的bean使它们相互合作实现一个共同的目标。
依赖注入是一个对象定义其依赖的过程,它的依赖也就是与它一起合作的其它对象,这个过程只能通过构造方法参数、工厂方法参数、或者被构造或从工厂方法返回后通过setter方法设置其属性来实现。然后容器在创建这个bean时注入这些依赖。这个过程本质上是反过来的,由bean本身控制实例化,或者直接通过类的结构或Service定位器模式定位它自己的依赖,因此得其名曰控制反转。
使用依赖注入原则代码更干净,并且当对象提供了它们的依赖时解耦更有效。对象不查找它的依赖,且不知道依赖的位置或类。比如,类变得更容易测试,尤其是当依赖是基于接口或抽象基类时,这允许在单元测试时模拟实现。
依赖注入有两种主要的方式,基于构造方法的依赖注入和基于setter方法的依赖注入。
基于构造方法的依赖注入,由容器调用带有参数的构造方法来完成,每个参数代表一个依赖。调用带有特定参数的静态工厂方法创建bean几乎是一样的,这里把构造方法的参数与静态工厂方法的参数同等对待。下面的例子展示了一个只能通过构造方法注入依赖的类。注意,这个类并没有什么特殊的地方,它仅仅只是一个没有依赖容器的特定接口、基类或注解的POJO。
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...
}
构造方法参数的解决匹配时使用参数的类型。如果没有潜在的歧义存在于bean定义的构造方法参数中,那么在bean实例化时bean定义中参数的顺序与构造方法中的顺序保持一致。例如,下面这个类:
package x.y;
public class Foo {
public Foo(Bar bar, Baz baz) {
// ...
}
}
没有潜在的歧义,假设Bar和Baz类没有继承关系。然后下面这样配置就可以了,不需要在**
<beans>
<bean id="foo" class="x.y.Foo">
<constructor-arg ref="bar"/>
<constructor-arg ref="baz"/>
bean>
<bean id="bar" class="x.y.Bar"/>
<bean id="baz" class="x.y.Baz"/>
beans>
当另一个bean被引用时,类型是已知的并且匹配可能出现(像上例一样)。
当使用简单类型时,比如**
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属性指定了参数的类型则容器会使用类型匹配。例如:
(译者注:如果不指定类型,spring分不清把7500000赋给year还是42赋给year,因为constructor-arg并不是按它们出现的顺序与参数匹配)
<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>
除了解决多个简单值的起义,使用索引还可以解决构造方法中存在多个相同类型的参数的问题。注意索引从0开始。
也可以使用参数的名字来消除起义:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg name="years" value="7500000"/>
<constructor-arg name="ultimateAnswer" value="42"/>
bean>
注意,为了使其可以立即使用,代码必须开启调试标记来编译,这样Spring才能从构造方法中找到参数的名字。如果没有开启调试标记(或不想)编译,可以使用JDK的注解@ConstructorProperties显式地指定参数的名字。类看起来不得不像下面这样:
package examples;
public class ExampleBean {
// Fields omitted
@ConstructorProperties({"years", "ultimateAnswer"})
public ExampleBean(int years, String ultimateAnswer) {
this.years = years;
this.ultimateAnswer = ultimateAnswer;
}
}
基于setter方法的依赖注入,由容器在调用无参构造方法或无参静态工厂方法之后调用setter方法来实例化bean。
下面的例子展示了一个只能通过纯净的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方法的依赖注入,也支持在使用构造方法注入依赖之后再使用setter方法注入依赖。以BeanDefinition的形式配置依赖,可以与PropertyEditor实例一起把属性从一种形式转化为另一种形式。但是,大多数Spring用户不直接(编程式地)使用这些类,而是使用XML bean定义、注解的组件(例如,以**@Component**、@Controller注解的类)或基于Java的**@Configuration类的@Bean方法。这些资源然后都被内部转化为了BeanDefinition**的实例,并用于加载完整的Spring IoC容器的实例。
容器按如下方式处理依赖注入:
Spring容器在创建时会验证每个bean的配置。但是,bean的属性本身直到bean实际被创建时才会设置。单例作用域的或被设置为预先实例化(默认)的bean会在容器创建时被创建。作用域的定义请参考7.5 bean的作用域。否则,bean只在它被请求的时候才会被创建。创建一个bean会潜在地引起一系列的bean被创建,因为bean的依赖及其依赖的依赖(等等)会被创建并赋值。注意,那些不匹配的依赖可能稍后创建,比如,受影响的bean的首次创建(译者注:这句可能翻译的不到位,有兴趣的自己翻译下,原文为 Note that resolution mismatches among those dependencies may show up late, i.e. on first creation of the affected bean)。
你通常可以相信Spring做正确的事。它会在容器加载时检测配置问题,比如引用不存在的bean和循环依赖。Spring尽可能晚地设置属性和解决依赖,在bean实际被创建之后。这意味着Spring正确加载了,之后如果在创建对象或它的依赖时出现问题那么Spring又会产生异常。例如,因为一个缺少或失效的属性导致bean抛出了异常。这潜在地会推迟一些配置问题的可视性,这就是为什么ApplicationContext的实现类默认预先实例化单例bean。在bean实际需要之前会花费一些时间和内存成本,因此在ApplicationContext创建时会发现配置问题,而不是之后。你也可以重写默认的行为让单例bean延迟初始化,而不是预先实例化。
如果不存在循环依赖,当一个或多个合作的bean被注入依赖的bean时,每个合作的bean都将在注入依赖的bean之前被完全实例化。这意味着如果A依赖于B,那么在调用A的setter方法注入B之前会完全实例化B。换句话说,bean被实例化(如果不是预先实例化的单例),它的依赖被设置,然后相关的生命周期方法被调用(比如,配置的初始化方法或初始化bean的回调方法)。
下面的例子使用XML配置setter方法注入。XML的部分配置如下:
<bean id="exampleBean" class="examples.ExampleBean">
<property name="beanOne">
<ref bean="anotherExampleBean"/>
property>
<property name="beanTwo" ref="yetAnotherBean"/>
<property name="integerProperty" value="1"/>
bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
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;
}
}
上面的例子中,XML中setter方法与属性匹配,下面的例子使用构造方法注入:
<bean id="exampleBean" class="examples.ExampleBean">
<constructor-arg>
<ref bean="anotherExampleBean"/>
constructor-arg>
<constructor-arg ref="yetAnotherBean"/>
<constructor-arg type="int" value="1"/>
bean>
<bean id="anotherExampleBean" class="examples.AnotherBean"/>
<bean id="yetAnotherBean" class="examples.YetAnotherBean"/>
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的构造方法参数。
现在考虑使用另外一种方式,调用静态工厂方法代替构造方法返回这个对象的实例:
<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"/>
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;
}
}
通过**
如前所述,可以定义bean的属性和构造方法的参数引用其它的bean(合作者),或设置值。在XML配置中,可以使用
<bean id="myDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<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命名空间可以更简洁的表示:
<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
http://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更简洁了,但是错误只能在运行时被发现,而不是设计的时候,除非使用类似IntelliJ IDEA或Spring工具套件(STS)的开发工具,它们可以在创建bean定义时自动完成属性的拼写。强烈推荐使用这类IDE。
也可以像下面这样配置java.util.Properties:
<bean id="mappings"
class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="properties">
<value>
jdbc.driver.className=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/mydb
value>
property>
bean>
Spring容器会使用JavaBean的PropertyEditor机制把
idref元素是一种很简单的错误检查方式,可以把容器中另一个bean的id(字符串值-不是引用)传递给
<bean id="theTargetBean" class="..."/>
<bean id="theClientBean" class="...">
<property name="targetName">
<idref bean="theTargetBean" />
property>
bean>
上面的片段在运行时与下面的片段完成一样:
<bean id="theTargetBean" class="..." />
<bean id="client" class="...">
<property name="targetName" value="theTargetBean" />
bean>
第一种形式比第二种形式更好,因为使用idref标签时容器会在部署时验证引用的命名的bean是否实际存在。第二种形式不会对client bean的targetName属性的值执行任何验证。错误只能在client bean实际实例化时才能发现(可能导致致命的结果)。如果client bean是一个prototype类型的bean。那么错误很可能会在容器启动很久之后才能发现。
ref是
通过bean的标签指定目标bean是最常用的形式,并且可以引用同一个或父容器中的任何bean,不论是否在同一个XML文件中。引用的值可能是目标bean的id属性,或name属性中的一个(译者注:因为name属性可以指定多个name)。
<ref bean="someBean"/>
通过parent属性可以指定对当前容器的父容器中的bean的引用。parent属性的值可能是目标bean的id属性或name属性中的一个,而且目标bean必须在当前容器的父容器中。这种引用形式主要出现在有容器继承并且想把父容器中存在的bean包装成同样名字的代理用于子容器的时候。
<bean id="accountService" class="com.foo.SimpleAccountService">
bean>
<bean id="accountService"
property>
bean>
在
<bean id="outer" class="...">
<property name="target">
<bean class="com.example.Person">
<property name="name" value="Fiona Apple"/>
<property name="age" value="25"/>
bean>
property>
bean>
内部bean不需要定义id或name,即使指定了,容器也不会使用它作为标识符。容器在创建内部bean时也会忽略其作用域标志,内部bean总是匿名的且总是随着外部bean一起创建。不可能把内部bean注入到除封闭bean以外的合作bean,也不可能单独访问它们。
有一种边界情况,可以从自定义作用域中接收销毁方法的回调,例如,对于包含在一个单例bean中的request作用域的内部bean,它的创建依赖于包含它的bean,但是销毁方法的回调允许它参与到request作用域的生命周期中。这并不是一种常见的情况,内部bean一般共享包含它的bean的作用域。
在,
<bean id="moreComplexObject" class="example.ComplexObject">
<property name="adminEmails">
<props>
<prop key="administrator">[email protected]prop>
<prop key="support">[email protected]prop>
<prop key="development">[email protected]prop>
props>
property>
<property name="someList">
<list>
<value>a list element followed by a referencevalue>
<ref bean="myDataSource" />
list>
property>
<property name="someMap">
<map>
<entry key="an entry" value="just some string"/>
map>
property>
<property name="someSet">
<set>
<value>just some stringvalue>
<ref bean="myDataSource" />
set>
property>
bean>
map的键值或set的值也可以是以下任何元素:
bean | ref | idref | list | set | map | props | value | null
Spring容器同样支持集合的合并。开发者可以定义一个父类型的,
这节主要讨论父子集合的合并机制,读者如果对父子bean的定义不太熟悉,可以先读相关章节再继续。
下面的例子展示了集合的合并:
<beans>
<bean id="parent" abstract="true" class="example.ComplexObject">
<property name="adminEmails">
<props>
<prop key="administrator">[email protected]prop>
<prop key="support">[email protected]prop>
props>
property>
bean>
<bean id="child" parent="parent">
<property name="adminEmails">
<props merge="true">
<prop key="sales">[email protected]prop>
<prop key="support">[email protected]prop>
props>
property>
bean>
<beans>
注意子bean的adminEmails属性上的props元素的merge=true。当child被实例化时,它拥有一个adminEmails Properties集合,这个集合包含了父子两个集合adminEmails合并的结果。
[email protected]
[email protected]
[email protected]
子集合Properties的值集继承了所有父集合的元素,并且子集合中的support值重写了父集合的值。
, 和
元素合并的时候,因为List集合维护的是有序的值,所以父集合的值在子集合值的前面。在Map, Set和Properties中的值则不存在顺序。没有顺序的集合类型更有效,因此容器内部更倾向于使用Map, Set和Properties。
不能合并不同的集合类型(比如,Map和List),如果试图这么做将会抛出异常。merge属性必须指定在子集合的定义上,在父集合上指定merge属性将是多余的且不会合并集合。
Java5引入了泛型类型,所以可以使用强类型的集合。也就是说,例如,可以声明一个只能包含String元素的集合类型。如果使用Spring把强类型的集合注入到一个bean,可以利用Spring的类型转换以便元素在被添加到集合之前转换成合适的类型。
public class Foo {
private Map<String, Float> accounts;
public void setAccounts(Map<String, Float> accounts) {
this.accounts = accounts;
}
}
<beans>
<bean id="foo" class="x.y.Foo">
<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>
当foo的accounts属性准备注入的时候,会通过反射获得强类型Map
Spring把对属性的空参数作为空字符串。下面的XML片段把email属性的值设置成了空字符串值("")。
<bean class="ExampleBean">
<property name="email" value=""/>
bean>
上面的例子与下面的Java代码是等价的:
exampleBean.setEmail("")
<bean class="ExampleBean">
<property name="email">
<null/>
property>
bean>
上面的配置与下面的Java代码等价:
exampleBean.setEmail(null)
p命名空间使你可以使用bean元素的属性,而不是内置的
Spring支持使用命名空间扩展配置,这是基于XML的schema定义的。本章讨论的bean的配置形式是定义在XML schema文档中的。然而,p命名空间不是定义在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
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean name="classic" class="com.example.ExampleBean">
<property name="email" value="[email protected]"/>
bean>
<bean name="p-namespace" class="com.example.ExampleBean"
p:email="[email protected]"/>
beans>
这个例子展示了p命名空间中一个叫做email的属性。这会告诉Spring包含了一个属性声明。如前所述,p命名空间没有schema定义,所以可以把xml的属性名设置成java的属性名。
下面的例子包含两个引用了其它对象的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
http://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命名空间定义属性值,还使用特定的形式声明了属性的引用。第一个bean定义使用
与使用p命名空间简写XML类似,c命名空间是Spring3.1新引入的,允许使用内联属性配置构造方法的参数,而不用嵌套constructor-arg元素。
让我们一起回顾下基于构造方法的依赖注入这节,使用c: 命名空间如下:
<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
http://www.springframework.org/schema/beans/spring-beans.xsd">
<bean id="bar" class="x.y.Bar"/>
<bean id="baz" class="x.y.Baz"/>
<bean id="foo" class="x.y.Foo">
<constructor-arg ref="bar"/>
<constructor-arg ref="baz"/>
<constructor-arg value="[email protected]"/>
bean>
<bean id="foo" class="x.y.Foo" c:bar-ref="bar" c:baz-ref="baz" c:email="[email protected]"/>
beans>
c: 命名空间与p: 命名空间使用一样的转换(后面的-ref用于bean引用),通过名字设置构造方法的参数。同样地,c命名空间也没有定义在XSD文件中(但是存在于Spring核心包中)。
对于极少数的情况,不能获得构造方法参数名时(通常不使用调试信息编译字节码),可以退而求其次使用参数的索引。
<bean id="foo" class="x.y.Foo" c:_0-ref="bar" c:_1-ref="baz"/>
经过实践,构造方法的解决机制在匹配参数方面很有效率,所以除非真的需要,我们推荐在所有配置的地方都使用名字符号。
可以使用合成或嵌套的属性名设置bean的属性,只要路径中除了最后的属性值的所有的组件都不为null,考虑使用如下bean定义:
<bean id="foo" class="foo.Bar">
<property name="fred.bob.sammy" value="123" />
bean>
foo有一个属性叫fred,fred有一个属性叫bob,bob有一个属性叫sammy,并且最后的sammy属性的值被设置为123。其中,foo的fred属性和fred的bob属性在bean构造后必须不为null,否则将会抛出空指针异常。
如果一个bean是另一个bean的依赖,那通常意味着这个bean被设置为另一个bean的属性。典型地,在XML配置中使用元素来完成这件事。但是,有时bean之间的依赖并不是直接的,例如,一个静态的初始化器需要被触发,比如在数据库驱动注册时。depends-on属性可以明确地强制一个或多个bean在使用它(们)的bean初始化之前被初始化。
下面的例子使用depends-on属性表示对一个单例bean的依赖:
<bean id="beanOne" class="ExampleBean" depends-on="manager"/>
<bean id="manager" class="ManagerBean" />
要表示对多个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销毁之前被销毁。因此,depends-on也能够控制关闭的顺序。(译者注,初始化的时候依赖的对象先初始化才能注入,销毁时需要依赖的对象先销毁才能解绑)
默认地,ApplicationContext的实现创建和配置所有单例bean作为初始化过程的一部分。通常这种预初始化是令人满意的,因为配置或环境中的错误可以立即被发现,相反则可能需要数小时甚至数天才能发现错误。如果不需要预初始化,可以把bean定义为延迟初始化来阻止预初始化。延迟初始化的bean会告诉IoC容器在第一次请求到这个bean时才初始化,而不是启动的时候。
在XML中,这种行为是通过
<bean id="lazy" class="com.foo.ExpensiveToCreateBean" lazy-init="true"/>
<bean name="not.lazy" class="com.foo.AnotherBean"/>
当ApplicationContext使用上面的配置启动时时,名为lazy的bean并不急着预初始化,而not.lazy则会预初始化。
但是,当一个延迟初始化的bean是另一个非延迟初始化的单例bean的依赖时,ApplicationContext会在启动时创建这个延迟初始化的bean,因为它必须用来满足那个单例的依赖。这个延迟初始化的bean被注入到非延迟初始化的单例bean中。
也可以在
<beans default-lazy-init="true">
beans>
Spring容器可以相互合作的bean间自动装配其关系。你可让让Spring通过检查ApplicationContext的内容自动为你解决bean之间的依赖。自动装配有以下优点:
使用XML配置时,可以为带有autowire属性的bean定义指定自动装配的模式。自动装配的功能有四种模式。可以为每个自动装配的bean指定一种模式。
表7.2 自动装配的模式
模式 | 解释 |
---|---|
no | 默认地没有自动自动装配。bean的引用必须通过ref元素定义。对于大型部署,不推荐更改默认设置,因为显式地指定合作者能够更好地控制且更清晰。在一定程度上,这也记录了系统的结构。 |
byName | 按属性名称自动装配。Spring为待装配的属性寻找同名的bean。例如,如果一个bean被设置为按属性名称自动装配,且它包含一个属性叫master(亦即,它有setMaster(…)方法),Spring会找到一个名叫master的bean并把它设置到这个属性中。 |
byType | 按属性的类型自动装配,如果这个属性的类型在容器中只存在一个bean。如果多于一个,则会抛出异常,这意味着不能为那个bean使用按类型装配。如果一个都没有,则什么事都不会发生,这个属性不会被装配。 |
constructor | 与按类型装配类似,只不过用于构造方法的参数。如果这个构造方法的参数类型在容器中不存在明确的一个bean,将会抛出异常。 |
使用按类型装配或构造方法自动装配模式,可以装配数组和集合类型。在这个情况下,容器中所有匹配期望类型的候选者都将被提供用来满足此依赖。你可以自动装配强类型的Map如果其键的类型是String。自动装配Map的值将包含所有匹配期望类型的bean实例,并且Map的键将包含对应的bean的名字。
可以联合使用自动装配行为和依赖检查,其中依赖检查会在自动装配完成后执行。
如果一个项目一直使用自动装配,它会运行得很好。如果只是用在一两个bean上,则会出现混乱。
自动装配有如下局限性和缺点:
在上述场景中,有如下几种选择:
在单个bean上,可以避免自动装配。在xml形式中,设置**
也可以通过定义bean名称的匹配模式避免自动装配。顶级元素**
这项技术对于那些你不想它们被自动注入到其它bean的bean非常有用。这并不意味着它自己不能使用自动装配,而是,它不是其它bean自动装配的候选者。
在大部分应用场景中,容器中的大多数bean都是单例的。当一个单例bean需要与另一个单例bean合作,或者一个非单例bean需要与另外一个非单例bean合作的时候,仅仅定义一个bean作为另一个的属性就能解决它们的依赖关系。如果bean的生命周期不一致就会出现问题。假如,一个单例bean A 需要使用一个非单例bean B,也许在每次调用A的方法时都需要。容器仅仅创建这个单例bean A 一次,所以只有一次机会设置其属性。容器不能在 A 每次调用的时候都提供一个新的 B 的实例。
一种解决方法是放弃控制反转。你可以通过实现ApplicationContextAware接口使 A 能连接到容器,并在每次 A 需要 B 的实例的时候调用容器的**getBean(“B”)**方法取得新的 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容器的高级特性,允许以一种干净的方式处理这种用例。
查找方法注入是容器的一项能力,它可以重写容器管理的bean的方法,并返回另一个bean的查找结果。查找往往涉及到前面描述的那种原型(prototype)bean。spring利用CGLIB库动态地生成字节码子类,从而重写方法以实现查找方法注入。
查看前面关于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);
如果方法是抽象的,动态生成的子类会实现这个方法。另外,动态生成的子类也会重写原类中的具体方法。例如:
<bean id="command" class="fiona.apple.AsyncCommand" scope="prototype">
bean>
<bean id="commandManager" class="fiona.apple.CommandManager">
<lookup-method name="createCommand" bean="command"/>
bean>
commandManager这个bean会在任何它需要command实例的时候调用其createCommand()方法。如果实际需要,则必须把command声明为原型(prototype)。如果被声明为单例(singleton),则每次返回的都是同一个command实例。
方法注入的一种很少使用的形式,它是使用另外的方法实现替换目标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">
<replaced-method name="computeValue" replacer="replacementComputeValue">
<arg-type>Stringarg-type>
replaced-method>
bean>
<bean id="replacementComputeValue" class="a.b.c.ReplacementComputeValue"/>
可以使用一个或多个**
java.lang.String
String
Str
因为参数的个数往往就能够区分每一种可能了,这种简写方法可以少打几个字,所以你可以只输入最短的字符就能够匹配参数类型。
当创建一个bean定义时,就给了一份创建那个类的实例的食谱。bean定义是一份食谱,这个想法很重要,因为那意味着,可以根据一份食谱创建很多实例。
不仅可以控制bean实例的各种各样的依赖关系和配置值,还可以控制这些实例的作用域(scope)。这种方式很强大且是弹性的,因为可以通过配置选择实例的作用域,而不需要在类级别控制其作用域。bean可以被部署成多个作用域之一,spring支持很多作用域,对于web应用可以使用5种。
下面的作用域都是可以直接使用的,也可以创建自定义作用域。
表 7.3. bean的作用域
作用域 | 描述 |
---|---|
singleton(单例) | 默认值,每个spring的IoC容器中只保留一份bean定义对应的实例。 |
prototype(原型) | 一份bean定义对应多个实例。 |
request(请求) | 依赖于Http请求的生命周期,也就是说,每个Http请求都有它自己实例。这只在web应用上下文中有效。 |
session(会话) | 依赖于Http会话的生命周期。这只在web应用上下文中有效。 |
globalSession(全局会话) | 依赖于全局Http会话的生命周期。典型地仅当使用在Portlet上下文中有效。这只在web应用上下文中有效。 |
application(应用) | 依赖于ServletContext的生命周期。这只在web应用上下文中有效。 |
websocket | 依赖于WebSocket的生命周期。这只在web应用上下文中有效。 |
一个单例bean仅共享同一个实例,并且所有的请求都只返回同一个实例。
换句话说,当把一个bean定义为单例时,spring容器严格地只创建那个bean定义的一个实例。这个单独的实例被存储在单例bean的缓存中,并且所有后来的请求及引用都会返回这个缓存的对象。
spring中单例bean的概念不同于GoF设计模式中关于单例模式的定义。GoF的单例模式硬编码了对象的作用域,所以一个特定的类有且只有一个实例在一个类加载器中被创建。spring单例的作用域被更好地描述 为每个容器及每个bean。这意味着如果在一个spring容器中定义了一个特定类的bean,那么spring容器会且仅会创建一个那个bean定义对应的类的实例。*单例作用域是spring默认的作用域。*在xml形式中,按如下方式定义一个单例bean:
<bean id="accountService" class="com.foo.DefaultAccountService"/>
<bean id="accountService" class="com.foo.DefaultAccountService" scope="singleton"/>
非单例的原型作用域的bean将会导致每次请求时都创建一个新的实例。也就是说,在这个bean被注入到另一个bean时,或通过容器的**getBean()**方法请求它时都会创建一个新的实例。通常来说,对于所有有状态的bean使用原型作用域,对于无状态的bean使用单例作用域。
下面的图表描绘了spring的原型作用域。一个DAO对象无需配置成原型的,因为它不具有状态,这里仅仅是作者为了重用单例作用域那节的图表。
在xml形式中,按如下方式定义一个原型bean:
与其它作用域相比,spring不会管理一个原型bean的完整生命周期:容器实例化、配置、装配原型对象,然后把它交给客户端,没有更多原型实例的记录了。因此,尽管初始化的回调方法在所有对象上被调用,但是对于原型对象,销毁的回调方法并未被调用。客户端代码必须自己清除原型作用域的对象并释放其占用的资源。为了让spring容器释放原型对象占用的资源,试着使用自定义的bean后置处理器,它拥有将要被清除的bean的引用。
在某些方面,spring容器关于原型对象的角色就相当于Java中的new操作。从传递点之后所有的生命周期都有客户端自己处理(关于spring容器中bean的生命周期的详细信息,请参考7.6.1 生命周期回调)。
当使用依赖于原型bean的单例bean的时候,注意依赖关系是在实例化的时候被解决的。因此,如果依赖注入一个原型bean到单例bean,一个新的原型bean会被实例化,然后注入到单例bean中。这个原型实例是唯一一个提供给这个单例bean的实例。
但是,假设需要在运行时单例bean每次都获得一个新的原型bean,没有办法做到注入一个新的原型bean到单例bean,因为注入仅仅发生一次,当容器实例化单例bean的时候已经解决了它的依赖的注入。如果需要在运行时获取新的原型实例,请参考7.4.6 方法注入。
请求、会话、全局会话、应用及WebSocket作用域仅仅当使用在web上下文中才有效(比如XmlWebApplicationContext)。如果在其它spring容器中使用这些作用域,比如ClassPathXmlApplicationContext,将会抛出IllegalStateException,因为未知的作用域。
为了支持请求、会话、全局会话、应用及WebSocket作用域,一些小小的初始化配置是必须的(对于单例和原型作用域,初始化配置并不是必须的)。
怎么完成这个初始化配置依赖于特定的Servlet环境。
如果使用Spring Web MVC,使用Spring的DispatcherServlet或DispatcherPortlet处理请求,那么就需要什么配置了,因为DispatcherServlet和DispatcherPortlet已经帮你配置好了。
如果使用Servlet 2.5 Web容器,不使用Spring的DispatcherServlet处理请求(例如,使用JSF或Struts时),则需要注册org.springframework.web.context.request.RequestContextListener ServletRequestListener。对于Servlet 3.0+,这项配置可以通过WebApplicationInitializer接口编程式处理。对于更老的容器,只能在web.xml文件中添加如下声明了:
...
org.springframework.web.context.request.RequestContextListener
...
如果使用上述配置,考虑使用Spring的RequestContextFilter。过滤器映射依赖于其web应用配置,所以应该合适地调整之。
...
requestContextFilter
org.springframework.web.filter.RequestContextFilter
requestContextFilter
/*
...
DispatcherServlet, RequestContextListener和RequestContextFilter其实是做了同一件事,也就是绑定Http请求对象到服务于那个请求的线程。这样才能使得请求或会话作用域的bean能够更深入到调用链中。
考虑如下xml形式的bean定义:
spring容器为每次Http请求创建了一个新的LoginAction的实例。也就是说,loginAction的作用域是Http请求级别的。可以尽情地改变这个实例的内部状态,因为从同一份bean定义创建的其它实例不会发现这些状态的改变,它们只与特定的请求相关。当请求处理完成了,它绑定的bean就会被丢弃。
当使用注解驱动的组件或Java配置时,@RequestScope注解可以被用来赋值给一个组件的请求作用域。
@RequestScope
@Component
public class LoginAction {
// ...
}
考虑如下xml形式的bean定义:
spring容器为一个单独的Htpp会话创建一个新的UserPreferences实例。也就是说userPreferences的作用域是Http会话级别的。与请求作用域的bean一样,可以尽情地改变这个实例的内部状态,因为从同一份bean定义创建的其它实例不会发现这些状态的改变,它们只与特定的Http会话相关。当这个Http会话被丢弃的时候,它绑定的bean也就被丢弃了。
当使用注解驱动的组件或Java配置时,**@SessionScope **注解可以被用来赋值给一个组件的会话作用域。
@SessionScope
@Component
public class UserPreferences {
// ...
}
考虑下面的定义:
全局会话作用域与标准的Http会话作用域类似,不过它只用在基于portlet的web应用上下文中。portlet规范中定义了全局会话的概念,那就是共享了同一个portlet应用中所有的组件的全局的会话。定义为全局会话作用域的bean的生命周期与全局的portlet会话相关。
如果在标准的Servlet应用中定义一个或多个拥有全局会话作用域的bean,那么标准的Http会话作用域将被使用,并不会报错。
考虑如下xml形式的bean定义:
对于一个完整的web应用spring容器会创建一个新的AppPreferences实例。也就是说,appPreferences的作用域是ServletContext级别的,作为一个ServletContext普通属性被存储。这与spring的单例bean很类似,但有两种非常重要的区别:它在每个ServletContext中是单例,而不是spring的“ApplicationContext”(ApplicationContext在任何给定的web应用中可能存在多个)。另外,它实际上是作为ServletContext的属性被暴露并可见。
当使用注解驱动的组件或Java配置时,**@ApplicationScope **注解可以被用来赋值给一个组件的应用作用域。
@ApplicationScope
@Component
public class AppPreferences {
// ...
}
spring容器不仅管理对象(bean)的实例化,也连接合作者(依赖)。例如,如果注入一个Http请求作用域的bean到另一个更长周期的bean,可能会在原bean的位置选择使用AOP代理。也就是说,需要注入一个代理对象,这个代理对象暴露了与原对象相同的公共接口,但是它能从相关的作用域获取真实的目标对象,并以委托的方式调用真实对象的方法。
下面的配置中仅仅只有一行,便更重要地是要理解“为什么”和“怎么样”。
为了创建那么个代理,在bean定义中插入子元素**
在这个例子中,单例userManager被注入了一个Http会话作用域的对象userPreferences。这里的显著点是userManager是一个单例:它将在每个容器中严格地实例化一次,并且它的依赖(此例中仅仅userPreferences一个)也只会注入一次。这就意味着userManager将只会操作同一个userPreferences对象,也就是一开始注入的那个。
这并不是我们把短周期的bean注入长周期的bean想要的结果,例如,注入一个Http会话bean到一个单例bean中。不如说,我们只需要一个userManager对象,但是需要与特定会话关联的userPreferences对象。这样,容器创建一个与UserPreferences类相同的公共接口的对象(理想状态是UserPreferences的一个实例),它可以从作用域机制(Http请求、会话等)中获取真实的UserPreferences对象。然后把这个代理对象注入到userManager中,且userManager不会意识到这是一个代理。在这个案例中,当UserManager实例调用依赖注入的UserPreferences对象的方法时,实际上是调用的这个代理的方法。这个代理然后从Http会话中获取真实的UserPreferences对象,并在这个真实的UserPreferences对象上委托方法调用。
因此,当注入请求、会话和全局会话作用域的bean到合作对象时,需要下面这样正确和完整的配置:
默认地,当spring容器创建一个标记了**
替代方案,可以配置spring容器创建标准的JDK基于接口的代理,通过指定**
更多关于选择基于类的代理还是选择基于接口的代理的详细信息,请参考[11.6 代理机制](#proxying mechanisms)。
作用域机制是可以扩展的,我们可以定义自己的作用域,甚至可以重新定义已存在的作用域,不过后者被认为是坏的实践,并且不能重写内置的单例和原型作用域。
为了让自定义的作用域集成到spring容器中,需要实现org.springframework.beans.factory.config.Scope接口,这个接口会在接下来讲到。关于怎么实现自己的作用域,请参考spring自己提供的关于Scope的实现和Scope的javadocs,这里更详细地解释了需要实现的方法。
Scope接口有四个方法,从作用域中获取对象、移除对象和允许它们被销毁。
下面的方法用来从作用域中返回对象。例如,会话作用域的实现返回会话作用域的bean(如果不存在,则会返回一个新的实例,并绑定到会话中,为了将来使用)。
Object get(String name, ObjectFactory objectFactory)
下面的方法用来从作用域中移除对象。例如,会话作用域的实现从会话中移除会话作用域的bean。对象应该被返回,但是如果指定名字的对象未找到可以返回null。
Object remove(String name)
下面的方法用来注册作用域在销毁时或其中对象被销毁时应该执行的回调。参考javadocspring的作用域的一个实现获取关于销毁回调的更多信息。
void registerDestructionCallback(String name, Runnable destructionCallback)
下面的方法用来从作用域中获取会话(conversation,非session)标识。每个作用域的标识都不一样。对于会话作用域的实现,这个标识可以是会话id(session id)。
String getConversationId()
在写完或测试完一个或多个自定义作用域的实现后,需要让spring容器识别到新的作用域。下面的方法是注册一个新的作用域到spring容器的核心方法。
void registerScope(String scopeName, Scope scope);
这个方法定义在ConfigurableBeanFactory接口中,它允许大部分ApplicationContext的具体实现,这些实现通过BeanFactory属性与Spring联系在一起。
registerScope(…)方法的第一个参数是唯一与一个作用域关联的名字,例如,spring容器自己的singleto和prototype。它的第二个参数是具体的自定义的Scope的实现的一个实例,也就是希望注册并使用的。
假设你写了自定义的Scope实现,并像下面这样注册了它。
Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);
然后就可以按照作用域的规则使用自定义的作用域了:
使用自定义的Scope实现,并不一定要编程式地注册,也可以像下面这样声明,使用CustomScopeConfigurer类:
为了与容器中bean的生命周期管理交互,可以实现Spring的InitializingBean和DisposableBean接口。容器会在初始化和销毁bean时调用前者的**afterPropertiesSet()和后者destroy()**方法执行相应的动作。
spring内部使用BeanPostProcessor的实例处理它能找到的任何回调接口并调用合适的方法。如果需要定制一些spring未提供的又可以立即使用的功能或生命周期行为,可以直接实现自己的**BeanPostProcessor **。更多信息请参考7.8 容器扩展点。
除了初始化和销毁回调,spring管理的对象也可以实现Lifecycle接口,从而参与到容器本身的生命周期的启动和关闭中。
生命周期回调接口将在下面描述。
org.springframework.beans.factory.InitializingBean接口允许bean的所有必要属性都被容器设置好后执行初始化操作。InitializingBean接口只有一个方法:
void afterPropertiesSet() throws Exception;
其实并不建议使用InitializingBean 接口因为没有必要把代码与spring耦合起来。可以使用@PostConstruct注解或在bean定义中指定初始化方法作为替代方案。在xml配置中,可以使用init-method属性指定一个无参的方法。在Java配置中,可以使用**@Bean的initMethod**属性,参考接收生命周期回调。例如,下面这种方式不会与spring耦合:
public class ExampleBean {
public void init() {
// do some initialization work
}
}
上面这种方式严格来说与下面的方式是一样的:
public class AnotherExampleBean implements InitializingBean {
public void afterPropertiesSet() {
// do some initialization work
}
}
实现了org.springframework.beans.factory.DisposableBean接口的bean允许在包含这的容器被销毁时获取一个回调。DisposableBean接口只有一个方法:
void destroy() throws Exception;
其实并不建议使用DisposableBean回调接口,因为没必要把代码与spring耦合。可以使用**@PreDestroy注解或在bean定义中指定销毁方法作为替代方案。在xml配置中,可以使用
public class ExampleBean {
public void cleanup() {
// do some destruction work (like releasing pooled connections)
}
}
上面这种方式严格来说与下面是一样的:
public class AnotherExampleBean implements DisposableBean {
public void destroy() {
// do some destruction work (like releasing pooled connections)
}
}
当不使用spring指定的InitializingBean和DisposableBean回调接口编写初始化和销毁回调方法时,我们一般会把这些方法命名为**init(), initialize(), dispose()**等等。理想状态下,在整个项目中这些方法的名字都是标准化的,以便所有的开发者都使用相同的名字并保证一致性。
可以配置spring容器在每个bean上自动寻找初始化和销毁的回调方法。这意味着,开发者可以直接使用**init()作为初始化方法而不用为每一个bean配置init-method=“init”**了。容器会在bean创建后调用这个方法(而且,这与标准的生命周期回调是一致的)。这个特性也需要为初始化和销毁方法定义统一的命名约定。
假设你的初始化方法叫init(),销毁方法叫destroy(),那么你的类将与下面的类很相似:
public class DefaultBlogService implements BlogService {
private BlogDao blogDao;
public void setBlogDao(BlogDao blogDao) {
this.blogDao = blogDao;
}
// this is (unsurprisingly) the initialization callback method
public void init() {
if (this.blogDao == null) {
throw new IllegalStateException("The [blogDao] property must be set.");
}
}
}
顶级元素**
同样地,可以配置顶级元素**
当bean存在与上述约定不一样的回调方法名称时,可以使用**
spring容器保证在bean的所有依赖都注入完毕时立马调用初始化回调方法。因此,初始化方法是调用在原生(非原生即代理)的bean引用上,这意味着AOP的拦截器还没开始生效。目标bean首先被完全创建,然后AOP代理及其拦截器链才被应用。如果目标bean与代理是分开定义的,我们甚至可以绕过代理直接与原生bean进行交互。因此,在初始化方法上应用拦截器是矛盾的,因为,这样做会把目标bean的生命周期与它的代理或拦截器耦合在一起,在直接使用原生bean时就会有很奇怪的语法。
从spring 2.5 开始,有三种方式可以控制bean的生命周期行为:InitializingBean和DisposableBean回调接口;自定义的init()和destroy()方法;@PostConstruct和**@PreDestroy**注解。可以混合使用这些方式来控制给定的bean。
同一个bean配置了多种方式,对于初始化方法将按以下顺序调用:
销毁方法也是同样的顺序:
Lifecycle接口为任何有自己生命周期需求的对象定义了一些基本的方法(例如,启动和停止一些后台进程)。
public interface Lifecycle {
void start();
void stop();
boolean isRunning();
}
所有spring管理的对象都可以实现这个接口。然后,当ApplicationContext接收到启动和停止的信号时,例如,运行时的stop/restart场景,它会级联调用此上下文内所有的Lifecycle实现。它是通过委托LifecycleProcessor实现的:
public interface LifecycleProcessor extends Lifecycle {
void onRefresh();
void onClose();
}
注意,LifecycleProcessor本身扩展了Lifecycle接口,同时添加了两个另外的方法用于在上下文刷新和关闭时做出响应。
启动和停止的调用顺序可能是很重要的。如果两个对象存在依赖关系,那么依赖方将在其依赖启动之后启动,并在其依赖停止之前停止。但是,有些时候依赖并不是直接的。你可能只知道一些特定类型的对象将在另外一些类型的对象之前启动。在这种情形中,SmartLifecycle接口有另外一种选择,它的父接口Phased中定义了一个**getPhase()**方法。
public interface Phased {
int getPhase();
}
public interface SmartLifecycle extends Lifecycle, Phased {
boolean isAutoStartup();
void stop(Runnable callback);
}
启动时,低相位(phase)的对象先启动;停止时,顺序反过来。因此,实现了SmartLifecycle接口并且getPahse()返回Integer.MIN_VALUE的对象将先启动后停止。反过来,带有Integer.MAX_VALUE相位的对象将会后启动先停止(可能是因为它依赖于其它进程运行)。对于那些正常的未实现SmartLifecycle接口的Lifecycle对象,它们的相位默认值为0。因此,负相位的对象将在这些正常对象启动之前启动,停止之后停止,反之亦然。
可以发现SmartLifecycle中的stop方法可以接受一个回调。任何实现必须在其停止线程完成以后调用回调(Runnable)的run()方法。在必须的时候可以异步停止,因为LifecycleProcessor接口默认的实现DefaultLifecycleProcessor会等到超时直到每个相位的对象都调用了那个回调。默认每个相位的超时时间是30秒。可以在上下文中定义一个名叫“lifecycleProcessor”的bean重写默认的生命周期处理器。如果仅仅只修改超时时间,那么下面的定义就足够了:
如前所述,LifecycleProcessor接口也定义了上下文刷新(onRefresh())和关闭(onClose())的方法。后者会简单地驱动关闭进程即使stop()方法被显式地调用了,但是它会发生在上下文关闭的时候。另一方面,刷新方法会带来SmartLifecycle bean的另一个功能。在上下文被刷新时(在所有对象都被实例化和初始化之后),这个方法会被调用,并且那时默认的生命周期处理器会检查每个SmartLifecycle对象的**isAutoStartup()方法返回的布尔值。如果为true,那么这个对象会在那时被启动而不会等待上下文显式地调用或它自己的start()**方法(不像上下文的刷新,标准的上下文实现并不会自动启动)。相位的值及依赖关系将会按与上面描述相同的方式检查启动顺序。
如果在非web应用环境中使用spring的IoC容器,例如,在富客户端桌面环境中,注册一个JVM的关闭钩子(shutdown hook)。这样做可以保证优雅地关闭并调用相关单例bean的销毁方法从而使得所有的资源都被释放。当然,你必须要正确地配置并实现这些销毁方法。
为了注册一个关闭钩子,调用ConfigurableApplicationContext接口的**registerShutdownHook()**方法。
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public final class Boot {
public static void main(final String[] args) throws Exception {
ConfigurableApplicationContext ctx = new ClassPathXmlApplicationContext(
new String []{"beans.xml"});
// add a shutdown hook for the above context...
ctx.registerShutdownHook();
// app runs here...
// main method exits, hook is called prior to the app shutting down...
}
}
当ApplicationContext创建了一个实现了org.springframework.context.ApplicationContextAware接口的对象实例时,这个实例就拥有了ApplicationContext的引用。
public interface ApplicationContextAware {
void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}
因此,bean可以编程式地操作创建了它的ApplicationContext,可以通过ApplicationContext接口,也可以把它强制转换成它的子类,比如ConfigurableApplicationContext,这样可以暴露更多的功能。一种使用方式是提取其它bean。有时这个能力很有用,但是,通常来说应该避免这样操作,因为这会使得代码与spring耦合,并且不符合控制反转的原则,而控制反转中合作者是以属性的方式提供的。ApplicationContext还提供了一些诸如访问文件资源、发布应用事件、访问消息源的方法。关于附加功能的描述请参考7.15 ApplicationContext的附加功能。
从spring 2.5 开始,自动装配就是另外一种获取ApplicationContext引用的方式。传统的constructor和byType装配模式(参考7.4.5 自动装配合作者)能够分别为构造方法参数或setter方法参数提供一个ApplicationContext类型的依赖。另外,也可以使用注解方式自动装配字段或方法参数。如果这样做了,ApplicationContext就会被注入到一个字段、构造方法参数或者其它需要ApplicationContext类型的方法参数,当然这些字段或参数得带有**@Autowired**注解。更多信息请参考7.9.2 @Autowired。
当ApplicationContext创建了一个实现了org.springframework.beans.factory.BeanNameAware接口的类时,这个类就拥有了一个定义在它关联的对象定义中的名字的引用。
public interface BeanNameAware {
void setBeanName(string name) throws BeansException;
}
这个方法会在操作了正常的属性之后但是在初始化方法之前运行,不管是InitializingBean的**afterPropertiesSet()方法还是自定义的init()**方法
除了上面讨论的ApplicationContextAware和BeanNameAware接口,spring还提供了一系列的Aware接口使得bean可以从容器获取需要的资源(依赖)。大部分重要的Aware接口汇总在下面了——通常,它们的名字已经可以很好地解释它们的依赖类型了。
表7.4 Aware接口
名称 | 注入的依赖 | 描述 |
---|---|---|
ApplicationContextAware | 声明ApplicationContext | 7.6.2 ApplicationContextAware和BeanNameAware |
ApplicationEventPublisherAware | 封闭的ApplicationContext的事件发布者 | 7.15 ApplicatinContext的附加功能 |
BeanClassLoaderAware | 用于加载bean的类加载器 | 7.3.2 实例化bean |
BeanFactoryAware | 声明BeanFactory | 7.6.2 ApplicationContextAware和BeanNameAware |
BeanNameAware | 定义的bean的名字 | 7.6.2 ApplicationContextAware和BeanNameAware |
BootstrapContextAware | 容器运行于其中的资源适配器BootstrapContext。一般只用于JCA所在的上下文中。 | 32 JCA CCI |
LoadTimeWeaverAware | 加载时处理类定义的织入者 | 11.8.4 加载时使用AspectJ织入 |
MessageSourceAware | 配置的用于解决消息的策略(用于支持参数化及国际化) | 7.15 ApplicatinContext的附加功能 |
NotificationPublisherAware | spring中JMX通知发布者 | 31.7 通知 |
PortletConfigAware | 当前容器运行于其中的PortletConfig。仅用于web上下文中。 | 25 Portlet MVC 框架 |
PortletContextAware | 当前容器运行于其中的PortletContext。仅用于web上下文中。 | 25 Portlet MVC 框架 |
ResourceLoaderAware | 配置的用于低级别访问资源的加载器 | 8 资源 |
ServletConfigAware | 当前的容器运行于其中的ServletConfig。仅用于web上下文中。 | 22 Web MVC 框架 |
ServletContextAware | 当前的容器运行于其中的ServletContext。仅用于web上下文中。 | 22 Web MVC 框架 |
再次注意,使用这些接口将使你的代码与spring的API耦合,并且不会遵循控制反转的原则。因此,他们被推荐用于需要编程式访问容器的基础设施bean。
bean的定义中可以包含大量的配置信息,包括构造方法参数、属性值以及容器指定的信息,比如初始化方法、静态工厂方法名等等。子bean定义可以从父定义中继承配置信息。子定义可以重写这些值,也可以在需要的时候添加新的。使用父子定义可以少码几个字,这是模板的一种形式。
如果编程式地使用ApplicationContext,子定义可以使用ChildBeanDefinition类。不过大部分用户并不这样使用,而是以类似ClassPathXmlApplicationContext的形式声明式地配置。在xml中,可以使用子定义的parent属性指定父bean。
如果没有指定子定义的class,则会使用父定义的class,不过也可以重写它。后者必须保证子定义中的class与父定义中的class兼容,即它可以接受父定义中的属性值。
子定义可以从父定义继承作用域、构造方法参数值、属性值以及方法,也可以为它们添加新值。任何在子定义中定义的作用域、初始化方法、销毁方法或者静态工厂方法都会覆盖父定义中的配置。
其余的配置总是取决于子定义:依赖、自动装配模式、依赖检查、单例、延迟初始化。
上面的例子中在父定义中显式地使用了abstract属性。如果父定义不指定类,那么必须显式地标记abstract属性,如下:
父bean不能被实例化,因为它本身并不完整,并且被显式地标记为abstract了。当一个bean被定义为abstract,则它只能用做定义子bean的模板。如果试着使用abstract的父bean,比如通过另一个bean引用它或调用getBean()方法获取它,都会报错。同样,容器内部的preInstantiateSingletons()方法也会忽略定义为abstract的bean。
通常,开发者不需要自己实现ApplicationContext。然而,Spring提供了一些特殊的集成接口,实现这些接口可以扩展Spring。下面几个章节将介绍这些集成接口。
可以实现BeanPostProcessor接口定义的回调方法,从而提供自己的实例化逻辑、依赖注入逻辑等。如果想要在spring容器完成了实例化、配置及初始化bean前后实现自定义的逻辑,那么可以插入一个或多个BeanPostProcessor的实现。
可以配置多重BeanPostProcessor实例,并且可以设置它们的order属性控制它们的执行顺序,不过只有当这些实例也实现了Ordered接口才可以。更多信息请参考BeanPostProcessor和Ordered接口的javadoc,也可以参考下面关于BeanPostProcessor的编程式注册的说明。
org.springframework.beans.factory.config.BeanPostProcessor接口包含两个回调方法。如果容器中一个类被注册为后处理器,那么对于容器创建的每一个bean实例,在它们初始化前后都会获得这个后处理器的一个回调。这个后处理器可以对bean实例采取任何行动,也可以完全忽略回调。一个bean后处理器一般用于检查回调接口或用代理包装bean。一些Spring的AOP基础类为了提供代理包装的逻辑都被实现为后处理器。
(译者注:此处要理解实例化(instantiation)和初始化(initialization)的区别,bean是先实例化再初始化的,后(置)处理器是针对实例化来说的,它是在bean实例化后起作用,但是它内部的两个方法分别在bean初始化前后执行拦截的作用)
ApplicationContext会自动检测所有实现了BeanPostProcessor接口的所有bean,接着把它们注册为后处理器,以便在其它bean创建的时候调用它们。后处理器bean可以像容器中其它bean一样部署。
注意,当使用**@Bean工厂方法配置一个BeanPostProcessor实现类时,这个工厂方法的返回值类型必须是这个实现类本身或至少得是org.springframework.beans.factory.config.BeanPostProcessor接口,清晰地表明这个bean是后处理器的本性。否则,ApplicationContext在完全创建它之前不能通过类型检测到它。因为BeanPostProcessor**需要早点被实例化以便应用到其它bean的初始化中,所以早期的类型检测是必要的。
虽然我们推荐使用ApplicationContext自动检测BeanPostProcessor,但是也可以使用ConfigurableBeanFactory的addBeanPostProcessor方法编程式地注册。这在注册之前处理条件逻辑或者在同一继承体系上下文之间复制后处理器很有用。注意,编程式注册的BeanPostProcessor并不遵从Ordered接口的顺序,那是因为它们注册的顺序就是它们执行的顺序。同样地,编程式注册的BeanPostProcessor总是在通过自动检测注册的BeanPostProcessor之前执行。
实现了BeanPostProcessor接口的类是很特殊的,并且会被容器特殊对待。所有BeanPostProcessor和它们直接引用的bean会在启动时实例化,且作为ApplicationContext启动的特殊部分。然后,所有的BeanPostProcessor按顺序注册并应用到所有后来的bean上。因为AOP的自动代理也被实现为BeanPostProcessor本身,所以无论是BeanPostProcessor还是它们直接引用的bean都有获得自动代理的资格,且不会有切面织入它们。对于这些bean,你应该会看到一条信息日志消息:“Bean foo is not eligible for getting processed by all BeanPostProcessor interfaces (for example: not eligible for auto-proxying)”。注意,如果有bean通过自动装配或**@Resource**(可能会变成自动装配)被装配到了BeanPostProcessor,Spring在通过类型匹配查找依赖时可能会访问非预期的bean,从而使它们具有自动代理或其它后处理的资格。例如,如果通过不带name属性的**@Resource**注解在一个字段或setter方法上声明了一个依赖,并且这个字段或setter方法的名字并不直接与一个bean的名字通信,然后Spring将通过类型去匹配它们。
下面的例子展示了如何在ApplicationContext中编写、注册并使用BeanPostProcessor。
第一个例子展示了基本用法,它显示了一个自定义的BeanPostProcessor实现在bean在被创建时调用它们的**toString()**方法并把结果打印到控制台。
请看下面自定义的BeanPostProcessor实现类的定义:
package scripting;
import org.springframework.beans.factory.config.BeanPostProcessor;
import org.springframework.beans.BeansException;
public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {
// simply return the instantiated bean as-is
public Object postProcessBeforeInitialization(Object bean,
String beanName) throws BeansException {
return bean; // we could potentially return any object reference here...
}
public Object postProcessAfterInitialization(Object bean,
String beanName) throws BeansException {
System.out.println("Bean '" + beanName + "' created : " + bean.toString());
return bean;
}
}
从上面可以看到,InstantiationTracingBeanPostProcessor是如此简单的定义。它甚至不需要一个名字,而且因为它是一个bean,它也可以像其它bean一样被依赖注入。(上面的例子也展示了如何通过Groovy脚本语言定义一个bean。关于Spring的动态语言支持的详细描述请参考35 动态语言支持。)
下面简单展示了Java应用如何执行前面的代码和配置:
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
import org.springframework.scripting.Messenger;
public final class Boot {
public static void main(final String[] args) throws Exception {
ApplicationContext ctx = new ClassPathXmlApplicationContext("scripting/beans.xml");
Messenger messenger = (Messenger) ctx.getBean("messenger");
System.out.println(messenger);
}
}
上面应用的输出看起来像下面这样
Bean 'messenger' created : org.springframework.scripting.groovy.GroovyMessenger@272961
org.springframework.scripting.groovy.GroovyMessenger@272961
使用回调接口、注解或自定义的BeanPostProcessor实现类是一种扩展Spring容器的常用方式。Spring的RequiredAnnotationBeanPostProcessor是其中一个例子——它可以保证bean中被注解@Required标记的属性能被依赖注入一个值。
接下来我们看的一个扩展点是org.springframework.beans.factory.config.BeanFactoryPostProcessor。这个接口的语法与BeanPostProcessor类型,主要区别是:BeanFactoryPostProcessor操作的是配置元数据,也就是说,Spring容器允许BeanFactoryPostProcessor读取配置元数据并可能在容器实例化这些bean之前改变它们,当然,BeanFactoryPostProcessor除外。
可以配置多重BeanFactoryPostProcessor,也可以通过设置它们的order属性控制它们的执行顺序,当然,只有实现了Ordered接口才能设置顺序。更多BeanFactoryPostProcessor和Ordered接口的详细信息请参考javadoc。
ApplicationContext中定义的BeanFactoryPostProcessor是自动执行的,以便作用于这个容器中定义的元数据。Spring包含很多预先定义好的BeanFactoryPostProcessor,比如PropertyOverrideConfigure和PropertyPlaceholderConfigure。也可以使用自定义的BeanFactoryPostProcessor,比如,用于注册自定义的属性编辑器。
ApplicationContext会自动检测所有实现了BeanFactoryPostProcessor接口的bean,并在适当的时候使用这些bean作为bean工厂后置处理器。可以像部署其它bean一样部署这些后置处理器。
如果使用标准的Java Properties形式从外部文件中加载属性值,那么需要使用PropertyPlaceholderConfigurer。这样做可以定制环境相关的属性值,比如数据库URL和密码,而不需要冒很大的风险去修改容器中XML的定义。
查看下面的XML配置片段,配置DataSource时使用了占位符。这个例子展示了如何从外部文件配置属性。在运行时,PropertyPlaceholderConfigurer会替代其中的一些属性。这些被替代的占位符以**${property-name}**的形式定义,这与Ant/log4j/JSP EL的风格是一致的。
实际的值是以标准Java Properties的形式存在于另一个文件中的:
jdbc.driverClassName=org.hsqldb.jdbcDriver
jdbc.url=jdbc:hsqldb:hsql://production:9002
jdbc.username=sa
jdbc.password=root
因此,在运行时字符串**${jdbc.username}**会被替换为值’sa’,其它的占位符也是按一样的规则替换。PropertyPlaceholderConfigurer会检查一个bean定义的大部分属性。另外,占位符的前缀和后缀是可以自定义的。
Spring 2.5中引入的context命名空间中有专门配置占位符的元素。一个或多个位置可以使用逗号分割开。
PropertyPlaceholderConfigurer不仅查找我们指定的Properties文件中的属性值。默认地,如果在指定的文件中没有找到它还会检查Java的系统属性。设置它的systemPropertiesMode属性可以改变它的行为,包含以下三个值:
更多信息请参考PropertyPlaceholderConfigurer的javadoc。
<bean class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
<property name="locations">
<value>classpath:com/foo/strategy.propertiesvalue>
property>
<property name="properties">
<value>custom.strategy.class=com.foo.DefaultStrategyvalue>
property>
```
如果在运行时这个类不能被解析为正确的类,那么这个bean会创建失败,对于非延迟初始化的bean,这种失败发生在ApplicationContext的**preInstantiateSingletons()**期间。
PropertyOverrideConfigurer,另外一个bean工厂后置处理器,与PropertyPlaceholderConfigurer类似,但又不像后者,原始的bean定义的属性可以有默认值或没有值。如果重写的Properties文件没有合适的值,那就使用默认值。
注意,bean定义是不会意识到被重写的,所以从XML定义是看不出重写配置器被使用的。如果有多个PropertyOverrideConfigurer实例定义了同一个属性的不同的值,那么最后一个会起作用,因为重写机制的存在。
属性文件按下面的形式编写:
beanName.property=value
例如:
dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql:mydb
这个例子适用于包含了一个dataSource的bean的定义,且它包含driver和url属性。
也支持复合的属性名,只要除了最后一个属性之前的组件不为空即可。例如:
foo.fred.bob.sammy=123
它表示foo有一个fred属性,fred有一个bob属性,bob有一个属性sammy,这个sammy属性会被设置成123。
Spring 2.5中引入的context命名空间中有专门配置属性重写的元素。
(译者注:关于BeanPostProcessor和BeanFactoryPostProcessor的使用请参考本人另一篇博文:http://blog.csdn.net/tangtong1/article/details/51916679)
为对象实现org.springframework.beans.factory.FactoryBean接口,这些对象是他们自己的工厂。
FactoryBean接口是Spring窗口实例化逻辑的插入点。如果有一个非常复杂的初始化代码,它用Java能比XML更好地表述,那么就可以创建一个自己的FactoryBean,编写复杂的初始化逻辑在那个类中,并插入到窗口中。
FactoryBean接口提供了三个方法:
FactoryBean的概念和接口在Spring框架中被大量使用,Spring自己至少使用了50个FactoryBean的实现。
当需要从容器获得实际的FactoryBean的实例时,调用ApplicationContext的getBean()方法时在bean的id前面加上&符号即可。所以对于一个id为myBean的FactoryBean,调用getBean(“myBean”)会返回FactoryBean创建的实例,然而,调用getBean("&myBean")则会返回FactoryBean的实例本身。
(译者注:关于FactoryBean的使用请参考本人另一篇博文:http://blog.csdn.net/tangtong1/article/details/51920069)
注解形式比XML形式更好吗?
注解形式的引入引起了一个话题,它是否比XML形式更好。简单的回答是视情况而定。详细的回答是每一种方式都有它的优缺点,通常由开发者决定哪种方式更适合他们。由于他们定义的方式,注解提供了更多的上下文声明,导致了更短更简明的配置。然而,XML形式装配组件不会涉及到它们的源码或者重新编译它们。一些开发者更喜欢亲近源码,但另一些则认为注解类不是POJO,且配置很分散,难以控制。
不管做出什么选择,Spring都支持两种风格且可以混用它们。另外,通过Java配置的方式,Spring可以让注解变得非侵入式,不会触碰到目标组件的源码。而且,所有的配置方式Spring Tool Suite都支持。
一种XML形式的替代方案是使用基于注解的配置,它依赖于字节码元数据,用于装配组件并可取代尖括号式的声明。不同于使用XML描述一个bean,开发者需要把配置移动到组件类本身,并给相关的类、方法及字段声明加上注解。像例子:RequiredAnnotationBeanPostProcessor中提及的一样,联合使用BeanPostProcessor和注解是扩展Spring容器的一种常用方法。例如,Spring 2.0引入了**@Required注解,它强制属性必须获取一个值。Spring 2.5遵循同样的方式驱动依赖注入。本质上,@Autowired注解提供了相同的能力,如7.4.5 自动装配合作者中描述的一样,不过它提供了更细粒度的控制和更广泛的适用性。Spring 2.5也支持JSR-250的注解,比如@PostConstruct和@PreDestroy**。Spring 3.0支持JSR-330的注解,它们位于javax.inject包下,比如**@Inject和@Named**。更详细的信息可以在相关章节中找到。
可以一个一个地注册这些bean,也可以隐式地注册它们,使用下面的配置即可(注意,请包含context命名空间)。
(隐式注册的后置处理器包括AutowiredAnnotationBeanPostProcessor, CommonAnnotationBeanPostProcessor, PersistenceAnnotationBeanPostProcessor以及前面提到的RequiredAnnotationBeanPostProcessor。)
可以在构造方法上使用**@Autowired**:
public class MovieRecommender {
private final CustomerPreferenceDao customerPreferenceDao;
@Autowired
public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
this.customerPreferenceDao = customerPreferenceDao;
}
// ...
}
也可以在setter方法上使用**@Autowired**:
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Autowired
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
也可以应用在具有任意名字和多个参数的方法上:
public class MovieRecommender {
private MovieCatalog movieCatalog;
private CustomerPreferenceDao customerPreferenceDao;
@Autowired
public void prepare(MovieCatalog movieCatalog,
CustomerPreferenceDao customerPreferenceDao) {
this.movieCatalog = movieCatalog;
this.customerPreferenceDao = customerPreferenceDao;
}
// ...
}
也可以应用在字段上,甚至可以与构造方法上混用:
public class MovieRecommender {
private final CustomerPreferenceDao customerPreferenceDao;
@Autowired
private MovieCatalog movieCatalog;
@Autowired
public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
this.customerPreferenceDao = customerPreferenceDao;
}
// ...
}
也可以从ApplicationContext中提供特定类型的所有bean,只要添加这个注解在一个那种类型的数组字段或方法上即可:
public class MovieRecommender {
@Autowired
private MovieCatalog[] movieCatalogs;
// ...
}
同样适用于集合类型:
public class MovieRecommender {
private Set movieCatalogs;
@Autowired
public void setMovieCatalogs(Set movieCatalogs) {
this.movieCatalogs = movieCatalogs;
}
// ...
}
甚至Map也可以被自动装配,只要key的类型是String就可以。Map的value将包含所有的特定类型的bean,并且key会包含这些bean的名字。
public class MovieRecommender {
private Map movieCatalogs;
@Autowired
public void setMovieCatalogs(Map movieCatalogs) {
this.movieCatalogs = movieCatalogs;
}
// ...
}
默认地,如果没有候选的bean则自动装配会失败。这种默认的行为表示被注解的方法、构造方法及字段必须(required)有相应的依赖。也可按下面的方法改变这种行为。
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Autowired(required=false)
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
也可以把**@Autowired用在那些著名的可解析的依赖的接口上:BeanFactory, ApplicationContext, Environment, ResourceLoader, ApplicationEventPublisher, 以及MessageSource**。这些接口和它们扩展的接口,比如ConfigurableApplicationContext或ResourcePatternResolver,会自动解析,不需要特殊设置。
public class MovieRecommender {
@Autowired
private ApplicationContext context;
public MovieRecommender() {
}
// ...
}
@Autowired, @Inject, @Resource和**@Value注解都是被Spring的BeanPostProcessor处理的,这反过来意味着我们不能使用自己的BeanPostProcessor或BeanFactoryPostProcessor类型来处理这些注解。这些类型必须通过XML或使用Spring的@Bean**方法显式地装配。
因为基于类型的自动装配可能会导致多个候选者,所以对这种过程通常需要更多的控制。一种方式是使用Spring的**@Primary注解。它表示如果存在多个候选者且另一个bean只需要一个特定类型的bean依赖时,就使用标记了@Primary**注解的那个依赖。如果只有一个候选者那就直接使用那个候选者即可。
我们假设下面的配置,定义firstMovieCatalog作为主要的(primary)MovieCatalog。
@Configuration
public class MovieConfiguration {
@Bean
@Primary
public MovieCatalog firstMovieCatalog() { ... }
@Bean
public MovieCatalog secondMovieCatalog() { ... }
// ...
}
使用这种配置,下面的MovieRecommender将装配firstMovieCatalog。
public class MovieRecommender {
@Autowired
private MovieCatalog movieCatalog;
// ...
}
通信的bean定义看起来像下面这样:
当一个类型有几个实例时使用**@Primary是一种有效的方式。当需要对选择过程做更多的控制时,那就需要用到Spring的@Qualifier**注解了。为指定的参数绑定一个限定的值,可以缩小类型匹配的范围,使用这种方式,这个值可以是一个很普通的描述性的值:
public class MovieRecommender {
@Autowired
@Qualifier("main")
private MovieCatalog movieCatalog;
// ...
}
@Qualifier注解还能用在构造方法参数或方法参数上:
public class MovieRecommender {
private MovieCatalog movieCatalog;
private CustomerPreferenceDao customerPreferenceDao;
@Autowired
public void prepare(@Qualifier("main")MovieCatalog movieCatalog,
CustomerPreferenceDao customerPreferenceDao) {
this.movieCatalog = movieCatalog;
this.customerPreferenceDao = customerPreferenceDao;
}
// ...
}
通信的bean定义如下所示。带有限定符“main”的bean会被装配到拥有同样值的构造方法参数上。
bean的名字被认为是默认的限定符,它是一种后备的匹配。因此,可以定义一个bean的id为“main”,而不是内嵌qualifier元素,这会有一样的匹配结果。然而,尽管可以使用这种转换通过名字引用指定的bean,但是**@Autowired**默认是使用带有限定符的类型驱动的注入的。这意味着,带有bean名字的限定符总是会缩小类型匹配的语义,它们从语义上无法表达引用的是一个独立的bean的id。好的限定符是“main”、“EMEA”或者“persistent”等,它们表达了指定的组件是不同于bean的id的,它们可能会在匿名的bean定义中自动形成,就像前面的例子一样。
限定符也可以用于集合类型上,如前所述,比如Set
也可创建自定义的限定符注解,只要定义一个注解并在其定义上添加**@Qualifier**即可:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Genre {
String value();
}
然后就可以在字段或参数上使用自定义的限定符了:
public class MovieRecommender {
@Autowired
@Genre("Action")
private MovieCatalog actionCatalog;
private MovieCatalog comedyCatalog;
@Autowired
public void setComedyCatalog(@Genre("Comedy") MovieCatalog comedyCatalog) {
this.comedyCatalog = comedyCatalog;
}
// ...
}
接着,提供候选的bean定义的信息。可以添加**
在7.10 类路径扫描及管理的组件中,我们将会看到一种在XML中配置限定符元数据的替代方案。特别地,请参考7.10.8 通过注解提供注解元数据。
在一些情况下,使用没有值的注解可能就足够了。这非常有用当注解提供了一种更通用的目的,并且可以运用到不同的依赖上。例如,当没有网络时可能会需要一种offline的类别。第一步定义这个简单的注解:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Offline {
}
然后,添加这个注解到将被自动装配的字段或属性上:
public class MovieRecommender {
@Autowired
@Offline
private MovieCatalog offlineCatalog;
// ...
}
然后,这个bean定义就只需要限定符类型:
也可以自定义限定符注解,使它们带有命名的属性或者替代简单的value属性。如果多个属性值被指定在一个将被装配的字段或参数上,那么bean的定义必须匹配所有的属性值。例如,请看下面的注解定义:
@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface MovieQualifier {
String genre();
Format format();
}
这种情况下Format是一个枚举:
public enum Format {
VHS, DVD, BLURAY
}
被装配的字段使用这个自定义的限定符注解,它包含两个属性:genre和format。
public class MovieRecommender {
@Autowired
@MovieQualifier(format=Format.VHS, genre="Action")
private MovieCatalog actionVhsCatalog;
@Autowired
@MovieQualifier(format=Format.VHS, genre="Comedy")
private MovieCatalog comedyVhsCatalog;
@Autowired
@MovieQualifier(format=Format.DVD, genre="Action")
private MovieCatalog actionDvdCatalog;
@Autowired
@MovieQualifier(format=Format.BLURAY, genre="Comedy")
private MovieCatalog comedyBluRayCatalog;
// ...
}
最后,这些bean的定义需要包含这些限定符。这个例子也展示了bean的meta属性可以使用**
除了**@Qualifier**注解,也可以使用Java的泛型类型作为一种显式的限定。例如,假设有如下配置:
@Configuration
public class MyConfiguration {
@Bean
public StringStore stringStore() {
return new StringStore();
}
@Bean
public IntegerStore integerStore() {
return new IntegerStore();
}
}
假设,上述bean实现了泛型接口,例如Store
@Autowired
private Store s1; // qualifier, injects the stringStore bean
@Autowired
private Store s2; // qualifier, injects the integerStore bean
泛型限定符也可以用于自动装配的List、Map或数组:
// Inject all Store beans as long as they have an generic
// Store beans will not appear in this list
@Autowired
private List> s;
CustomAutowireConfigurer是一个BeanFactoryPostProcessor,它可以注册自定义的限定符注解类型,甚至它们没有被Spring的**@Qualifier**注解所注解。
example.CustomQualifier
AutowireCandidateResolver通过以下方式决定了自动装配的候选者:
当多个bean被限定为候选者时,主要决定因素如下:如果这些候选者中有一个bean定义上明确地设置了primay属性为true,那么它将被选择。
Spring也支持使用JSR-250的**@Resource**注解在字段和setter方法上进行注入。这在Java EE 5和6中是一种通用的模式,例如,在JSF 1.2管理的bean或JAX-WS 2.0终端。Spring也同样支持这种模式来管理对象。
@Resource拥有一个name属性,默认地,Spring会把这个name属性的值解释为将要注入的bean的名字。换句话说,它按照名字的语法进行注入,如下例所示:
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Resource(name="myMovieFinder")
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
如果没有提供名字,默认的名字从字段名或setter方法名派生而来。对于一个字段,它会取字段的名字,对于stter方法,它会取bean的属性名。所以,下面的方法会使用名字为“movieFinder”的bean注入到setter方法中。
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Resource
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
如果**@Resource没有显式地指定名字,与@Autowired**类似,它会寻找主要的(primary)类型匹配如果没有找到默认的名字,并解决众所周知的依赖:BeanFactory, ApplicationContext, ResourceLoader, ApplicationEventPublisher, 和MessageSource接口。
因此,下面的例子中customerPreferenceDao字段首先会寻找名字为customerPreferenceDao的bean,然后才会寻找接口CustomerPreferenceDao的主要的类型匹配。同样地,“context”字段会寻找ApplicationContext类型的已知的可解析的依赖。
public class MovieRecommender {
@Resource
private CustomerPreferenceDao customerPreferenceDao;
@Resource
private ApplicationContext context;
public MovieRecommender() {
}
// ...
}
CommonAnnotationBeanPostProcessor不仅能够识别到**@Resource注解,还能识别到JSR-250的生命周期注解。这是在Spring 2.5引入的,这项支持为初始化回调及销毁回调又提供了一种替代方案。CommonAnnotationBeanPostProcessor是在ApplicationContext**中注册的,因此带有这些注解的方法会与Spring自身的生命周期接口方法或显式声明的回调方法在同样调用。下面的例子中,缓存会在初始化的时候设置并在销毁时清除。
public class CachingMovieLister {
@PostConstruct
public void populateMovieCache() {
// populates the movie cache upon initialization...
}
@PreDestroy
public void clearMovieCache() {
// clears the movie cache upon destruction...
}
}
关于组合使用不同的生命周期机制的详细信息,请参考组合使用生命周期机制。
本章的大部分例子都将采用XML的形式配置元数据。上一节(7.9 基于注解的容器配置)描述了怎么在源码级别提供配置。即便如此,基本的配置还是得通过XML来配置,注解仅仅用于驱动依赖注入。本节提供了一种隐式地通过扫描类路径检测候选组件的方式。候选组件是那些符合相应过滤规则并与容器通信的类。这种方式可以让我们不再通过XML的形式执行bean的注册,而是采用注解(比如**@Component**)、AspectJ表达式或自定义的过滤规则来选择哪些类将被注册到容器中。
@Repository注解是一种用于标识存储类(也被称为数据访问对象或者DAO)的标记。异常的自动翻译是这个标记的用法之一,参考20.2.2 异常翻译。
Spring提供了一些扩展注解:@Component, @Service和**@Controller**。@Component可用于管理任何Spring的组件。@Repository, @Service和**@Controller是@Component用于指定用例的特殊形式,比如,在持久层、服务层和表现层。使用@Service或@Controller能够让你的类更易于被合适的工具处理或与切面(aspect)关联。比如,这些注解可以使目标组件成为切入点。当然,@Repository, @Service和@Controller也能携带更多的语义。因此,如果你还在考虑使用@Component还是@Service用于注解service层,那么就选@Service吧,它更清晰。同样地,如前面所述,@Repository**还能够用于在持久层标记自动异常翻译。
Spring提供了很多注解可用于元注解。元注解即一种可用于别的注解之上的注解。例如,@Service就是一种被元注解**@Component**注解的注解:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component // Spring will see this and treat @Service in the same way as @Component
public @interface Service {
// ....
}
元注解也可以组合起来形成组合注解。例如,@RestController注解是一种**@Controller与@ResponseBody**组合的注解。
另外,组合注解也可以重新定义来自元注解的属性。这在只想暴露元注解的部分属性值的时候非常有用。例如,Spring的**@SessionScope注解把它的作用域硬编码为session**,但是仍然允许自定义proxyMode。
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope(WebApplicationContext.SCOPE_SESSION)
public @interface SessionScope {
/**
* Alias for {@link Scope#proxyMode}.
* Defaults to {@link ScopedProxyMode#TARGET_CLASS}.
*/
@AliasFor(annotation = Scope.class)
ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;
}
@SessionScope然后就可以使用了,而且不需要提供proxyMode,如下:
@Service
@SessionScope
public class SessionScopedService {
// ...
}
或者重写proxyMode的值,如下:
@Service
@SessionScope(proxyMode = ScopedProxyMode.INTERFACES)
public class SessionScopedUserService implements UserService {
// ...
}
更多信息请参考Spring注解编程模型。
Spring能够自动检测被注解的类,并把它们注册到ApplicationContext中。例如,下面的两个会被自动检测到:
@Service
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Autowired
public SimpleMovieLister(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
}
@Repository
public class JpaMovieFinder implements MovieFinder {
// implementation elided for clarity
}
为了能够自动检测到这些类并注册它们,需要为**@Configuration类添加@ComponentScan注解,并设置它的basePackage**属性为这两个类所在的父包(替代方案,也可以使用逗号、分号、空格分割这两个类所在的包)。
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig {
...
}
也可以使用XML形式的配置:
使用**
类路径扫描的包必须保证这些包出现在classpath中。当使用Ant构建JAR包时,请确定不要激活仅仅使用文件的开关。同样地,类路径目录可能在某些环境下基于安全考虑不允许暴露,基于JDK 1.7.0_45及更高版本的app(需要在清单中设置信任库,参考http://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources)。
另外,使用component-scan元素时默认也启用了AutowiredAnnotationBeanPostProcessor和CommonAnnotationBeanPostProcessor。这意味着这两个组件被自动检测到了且不需要在XML中配置任何元数据。
默认地,只有使用注解**@Component, @Repository, @Service, @Controller或自定义注解注解的类才能被检测为候选组件。然而,我们可以使用自定义的过滤器修改并扩展这种行为。添加这些过滤器到@ComponentScan注解的includeFilters或excludeFilters参数即可(或component-scan元素的子元素include-filter或exclude-filter)。每个过滤器元素都需要type和expression**属性。下表描述了相关的选项:
表7.5. 过滤器类型
过滤器类型 | 表达式例子 | 描述 |
---|---|---|
annotation(默认) | org.example.SomeAnnotation | 目标组件类级别的注解 |
assignable | org.example.SomeClass | 目标组件继承或实现的类或接口 |
aspectj | org.example…*Service+ | 用于匹配目标组件的AspecJ类型表达式 |
regex | org.example.Default.* | 用于匹配目标组件类名的正则表达式 |
custom | org.example.MyTypeFilter | org.springframework.core.type.TypeFilter接口的自定义实现 |
下面的例子展示了如何忽略掉所有的**@Repository**注解,并使用带有“stub”的Repository代替:
@Configuration
@ComponentScan(basePackages = "org.example",
includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"),
excludeFilters = @Filter(Repository.class))
public class AppConfig {
...
}
或者使用XML形式配置:
Spring的组件也可以为容器贡献bean的定义元数据,只要在**@Component注解的类内部使用@Bean**注解即可。下面是一个简单的例子:
@Component
public class FactoryMethodComponent {
@Bean
@Qualifier("public")
public TestBean publicInstance() {
return new TestBean("publicInstance");
}
public void doWork() {
// Component method implementation omitted
}
}
这个类是Spring的一个组件,它包含一个应用相关的方法doWork()。但是,它也通过方法publicInstance()贡献了一个bean定义。@Bean注解标识了这个工厂方法和其它的bean属性,比如**@Qualifier注解的限定符。其它可用于此处的方法级别注解还有@Scope, @Lazy**及自定义注解等。
自动装配的字段和方法也可以像前面讨论的一样被支持,也可以支持**@Bean**方法的自动装配:
@Component
public class FactoryMethodComponent {
private static int i;
@Bean
@Qualifier("public")
public TestBean publicInstance() {
return new TestBean("publicInstance");
}
// use of a custom qualifier and autowiring of method parameters
@Bean
protected TestBean protectedInstance(
@Qualifier("public") TestBean spouse,
@Value("#{privateInstance.age}") String country) {
TestBean tb = new TestBean("protectedInstance", 1);
tb.setSpouse(spouse);
tb.setCountry(country);
return tb;
}
@Bean
private TestBean privateInstance() {
return new TestBean("privateInstance", i++);
}
@Bean
@RequestScope
public TestBean requestScopedInstance() {
return new TestBean("requestScopedInstance", 3);
}
}
上面的例子使用了另一个叫作privateInstance的bean的Age属性自动装配了String类型的参数country。Spring的表达式语言使用**#{}的记法定义了这个属性的值。对于@Value**注解,提前配置的表达式解析器会在需要解析表达式文本的时候寻找bean的名字。
在Spring组件内部的**@Bean方法的处理不同于使用@Configuration注解的类内部的@Bean方法。不同之处是@Component类不会使用CGLIB拦截调用的方法和字段从而进行增强。CGLIB代理在调用@Configuration类中的@Bean方法时会创建对合作对象的引用,这种方法的调用不会通过正常的Java语法调用,而是通过容器以便提供生命周期管理,甚至在通过编程地方式调用@Bean方法时也会形成对其它bean的引用。相反,调用普通的@Component类中的@Bean**方法只会形成标准的Java语法调用,不会有特殊的CGLIB处理过程及其它的限制条件。
当一个组件被扫描过程自动检测到时,它的名字由BeanNameGenerator定义的策略生成。默认地,Spring的扩展注解(@Component, @Repository, @Service和**@Controller**)都包含一个value属性,这个value值会提供一个名字以便通信。
如果这样的注解没有明确地提供value值,或者另外一些检测到的组件(比如自定义过滤器扫描到的组件),那么默认生成器会返回一个首字母小写的短路径的类名。比如,下面两个组件,它们的名字分别为myMovieLister和movieFinderImpl:
@Service("myMovieLister")
public class SimpleMovieLister {
// ...
}
@Repository
public class MovieFinderImpl implements MovieFinder {
// ...
}
如果不想遵循默认的名字生成策略,也可以提供自定义的策略。首先,需要实现BeanNameGenerator接口,并且要包含一个无参构造方法。然后,配置扫描器时为其指定这个自定义生成器的全路径:
@Configuration
@ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class)
public class AppConfig {
...
}
一般地,当其它的组件可能会明确地引用这个组件时为其注解提供一个名字是个很好地方式。另外,当容器装配时自动生成的名字足够用了。
一般Spring管理的组件的作用域默认为singleton。但是,有时可能会需要不同的作用域,这时可以通过**@Scope**注解来声明:
@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
// ...
}
更详细的信息请参考7.5.4 Request, session, global session, application和WebSocket作用域。
@Configuration
@ComponentScan(basePackages = "org.example", scopeResolver = MyScopeResolver.class)
public class AppConfig {
...
}
当使用非单例作用域时,有必要为作用域内的对象生成代理。原因如有作用域的bean作为依赖项中描述。因此,component-scan元素需要指明scoped-proxy属性。有三种可选值:无,接口和目标类。例如,下面的配置将使用标准的JDK动态代理:
@Configuration
@ComponentScan(basePackages = "org.example", scopedProxy = ScopedProxyMode.INTERFACES)
public class AppConfig {
...
}
@Qualifier注解在7.9.4 使用限定符微调基于注解的自动装配中被讨论过。那节的例子中展示了如何使用**@Qualifier注解,并展示了如何使用自定义的限定符注解提供更细粒度的控制。那些例子都是基于XML形式的,使用qualifier或meta**子元素为bean提供限定符。同样地,也可以在类级别提供注解达到同样的效果。下面的三个例子展示了用法:
@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
// ...
}
@Component
@Genre("Action")
public class ActionMovieCatalog implements MovieCatalog {
// ...
}
@Component
@Offline
public class CachingMovieCatalog implements MovieCatalog {
// ...
}
从Spring 3.0开始,Spring开始支持JSR-330的标准注解用于依赖注入。这些注解与Spring自带的注解一样被扫描。仅仅只需要引入相关的jar包即可。
如果使用Maven,javax.inject也可以在标准Maven仓库中找到,添加如下配置到pom.xml即可。
javax.inject
javax.inject
1
可以像下面这样使用**@javax.inject.Inject代替@Autowired**:
import javax.inject.Inject;
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
public void listMovies() {
this.movieFinder.findMovies(...);
...
}
}
与**@Autowired一样,可以在字段级别、方法级别或构造参数级别使用@Inject**。另外,也可以定义注入点为Provider,以便按需访问短作用域的bean或通过调用**Provider.get()**延迟访问其它的bean。上面例子的一种变体如下:
import javax.inject.Inject;
import javax.inject.Provider;
public class SimpleMovieLister {
private Provider movieFinder;
public void listMovies() {
this.movieFinder.get().findMovies(...);
...
}
}
如果你喜欢为依赖添加一个限定符,也可以像下面这样使用**@Named**注解:
import javax.inject.Inject;
import javax.inject.Named;
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(@Named("main") MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
可以像下面这样使用**@javax.inject.Named代替@Component**:
import javax.inject.Inject;
import javax.inject.Named;
@Named("movieListener")
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
通常使用**@Component都不指定名字,同样地@Named**也可以这么用:
import javax.inject.Inject;
import javax.inject.Named;
@Named
public class SimpleMovieLister {
private MovieFinder movieFinder;
@Inject
public void setMovieFinder(MovieFinder movieFinder) {
this.movieFinder = movieFinder;
}
// ...
}
使用**@Named**时,也可以像使用Spring注解一样使用组件扫描:
@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig {
...
}
使用标准注解时,应该要了解以下不支持的特性:
表 7.6. Spring组件模型与JSR-330变种的对比
Spring | javax.inject.* | javax.inject的局限性 |
---|---|---|
@Autowired | @Inject | @Inject没有require属性,可以使用Java 8的Optional代替。 |
@Component | @Named | JSR-330没有提供组合模型,仅仅只是一种标识组件的方式 |
@Scope(“singleton”) | @Singleton | JSR-330默认的作用域类似于Spring的prototype。然而,为了与Spring一般的配置的默认值保持一致,JSR-330配置的bean在Spring中默认为singleton。为了使用singleton以外的作用域,必须使用Spring的@Scope注解。javax.inject也提供了一个@Scope注解,不过这仅仅被用于创建自己的注解。 |
@Qualifier | @Qualifier/@Named | javax.inject.Qualifier仅使用创建自定义的限定符。可以通过javax.inject.Named创建与Spring中@Qualifier一样的限定符 |
@Value | - | 无 |
@Required | - | 无 |
@Lazy | - | 无 |
ObjectFactory | Provider | javax.inject.Provider是对Spring的ObjectFactory的直接替代,仅仅使用简短的get()方法即可。它也可以与Spring的@Autowired或无注解的构造方法和setter方法一起使用。 |
Spring中基于Java的配置的核心内容是**@Configuration注解的类和@Bean**注解的方法。
@Bean注解表示一个方法将会实例化、配置并初始化一个对象,且这个对象会被Spring容器管理。这就像在XML中**
注解了**@Configuration的类表示这个类的目的就是作为bean定义的地方。另外,@Configuration**类内部的bean可以调用本类中定义的其它bean作为依赖。最简单的配置大致如下:
@Configuration
public class AppConfig {
@Bean
public MyService myService() {
return new MyServiceImpl();
}
}
上面的AppConfig类与下面的XML形式是等价的:
全量的@Configuration和简化的@Bean模式?
当@Bean方法不定义在@Configuration的类中时,它们会被一种简化的模式处理。例如,定义在@Component类或普通类中的@Bean方法。
不同于全量的@Configuration模式,简化的@Bean方法不能轻易地使用别的依赖。通常在简化械下一个@Bean方法不会调用另一个@Bean方法。
推荐在@Configuration类中使用@Bean方法,从而保证全量模式总是起作用。这样可以防止同一个@Bean方法被无意中调用多次,并减少一些狡猾的bug。
@Bean和**@Configuration**注解将会在下面的章节中详细讨论。首先,我们来看看基于Java配置以不同的方式创建Spring的容器。
下面的章节介绍Spring 3.0中引入的AnnotationConfigApplicationContext。这个ApplicationContext的实现不仅可以把**@Configuration类作为输入,同样普通的@Component**类和使用JSR-330注解的类也可以作为输入。
当使用**@Configuration类作为输入时,这个类本身及其下面的所有@Bean**方法都会被注册为bean。
当**@Component和JSR-330类作为输入时,它们会被注册为bean,并且假设在必要的时候使用了@Autowired或@Inject**。
与使用ClassPathXmlApplicationContext注入XML文件一样,可以使用AnnotationConfigApplicationContext注入**@Configuration**类。这样就完全不用在Spring容器中使用XML了:
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
如前面所述,AnnotationConfigApplicationContext不限于只注入**@Configuration类,任何@Component**或JSR-330注解的类都能被提供给这个构造方法。例如:
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(MyServiceImpl.class, Dependency1.class, Dependency2.class);
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
上面假设了MyServiceImpl, Dependency1, Dependency2使用了Spring的依赖注入注解比如**@Autowired**。
一个AnnotationConfigApplicationContext可能会使用无参构造方法实例化,然后使用**register()**方法进行配置。这种方式通常用于编程式构建容器:
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.register(AppConfig.class, OtherConfig.class);
ctx.register(AdditionalConfig.class);
ctx.refresh();
MyService myService = ctx.getBean(MyService.class);
myService.doStuff();
}
为了扫描组件,只要像下面这样配置**@Configuration**类即可:
@Configuration
@ComponentScan(basePackages = "com.acme")
public class AppConfig {
...
}
上面的例子中,com.acme包会被扫描,只要是使用了**@Component注解的类,都会被注册进容器中。同样地,AnnotationConfigApplicationContext也暴露了scan(String…)**方法用于扫描组件:
public static void main(String[] args) {
AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.scan("com.acme");
ctx.refresh();
MyService myService = ctx.getBean(MyService.class);
}
一个WebApplicationContext与AnnotationConfigApplicationContext的变种是AnnotationConfigWebApplicationContext。这个实现可以用于配置Spring的ContextLoaderListener的servlet监听器、Spring MVC的DispatcherServlet等。下面是一个典型的配置Spring MVC web应用的片段。注意包含contextClass的context-param和init-param的用法:
contextClass
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
contextConfigLocation
com.acme.AppConfig
org.springframework.web.context.ContextLoaderListener
dispatcher
org.springframework.web.servlet.DispatcherServlet
contextClass
org.springframework.web.context.support.AnnotationConfigWebApplicationContext
contextConfigLocation
com.acme.web.MvcConfig
dispatcher
/app/*
在上面的场景下,AccountRepository的定义就很明确了。然而,ServiceConfig与RepositoryConfig耦合了;这是一种折衷的方法。这种耦合某种程度上可以通过接口或抽象解决,如下:
@Configuration
public class ServiceConfig {
@Autowired
private RepositoryConfig repositoryConfig;
@Bean
public TransferService transferService() {
return new TransferServiceImpl(repositoryConfig.accountRepository());
}
}
@Configuration
public interface RepositoryConfig {
@Bean
AccountRepository accountRepository();
}
@Configuration
public class DefaultRepositoryConfig implements RepositoryConfig {
@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(...);
}
}
@Configuration
@Import({ServiceConfig.class, DefaultRepositoryConfig.class}) // import the concrete config!
public class SystemTestConfig {
@Bean
public DataSource dataSource() {
// return DataSource
}
}
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
TransferService transferService = ctx.getBean(TransferService.class);
transferService.transfer(100.00, "A123", "C456");
}
现在ServiceConfig就与具体的DefaultRepositoryConfig松耦合了,并且内置的IDE工具也可以生效:对于 开发者可以很容易地获得RepositoryConfig实现类的继承体系。使用这种方式,操纵@Configuration类和它们的依赖与基于接口的代码没有区别。
有时候有条件地包含或不包含一个@Configuration类或@Bean方法很有用,这基于特定的系统状态。一种通用的方法是使用@Profile注解去激活bean,仅当指定的配置文件包含在了Spring的环境中才有效(参考7.13.1 bean定义配置文件)。
@Profile注解实际是实现了一个更灵活的注解@Conditional。@Condition注解表明一个@Bean被注册之前会先询问@Condition。
Condition接口的实现只要简单地提供matches(…)方法,并返回true或false即可。例如,下面是一个实际的Condition实现用于@Profile:
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
if (context.getEnvironment() != null) {
// Read the @Profile annotation attributes
MultiValueMap attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
if (attrs != null) {
for (Object value : attrs.get("value")) {
if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
return true;
}
}
return false;
}
}
return true;
}
更多信息请参考@Conditional的javadoc。
Spring的@Configuration类并不能100%地替代XML配置。一些情况下使用XML的命名空间仍然是最理想的方式来配置容器。在某些场景下,XML是很方便或必要的,你可以选择以XML为主,比如ClassPathXmlApplicationContext,或者以Java为主使用AnnotationConfigApplicationContext并在需要的时候使用@ImportResource注解导入XML配置。
更受人喜爱的方法是从XML启动容器并包含@Configuration类。例如,在大型的已存在的系统中,以前是使用XML配置的,所以很容易地创建@Configuration类,并包含他们到XML文件中,下面我们会讲解以XML为主的案例。
记住@Configuration类仅仅用于bean的定义。在这个例子中,我们创建了一个叫做AppConfig的配置类,并把它包含到system-test-config.xml中。因为**
@Configuration
public class AppConfig {
@Autowired
private DataSource dataSource;
@Bean
public AccountRepository accountRepository() {
return new JdbcAccountRepository(dataSource);
}
@Bean
public TransferService transferService() {
return new TransferService(accountRepository());
}
}
system-test-config.xml:
jdbc.properties:
jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml");
TransferService transferService = ctx.getBean(TransferService.class);
// ...
}
因为@Configuration是被元注解@Component注解的,所以@Configuration注解的类也可以被自动扫描。同样使用上面的例子,我们可以重新定义system-test-config.xml来使用组件扫描。注意,这种情况下,我们就没必要明确地声明 system-test-config.xml: 在一些应用中,@Configuration类是主要的配置方式,也需要使用一些XML配置。在这些场景下,简单地使用@ImportResource并按需要定义XML文件即可。这种方式可以以Java为主,并保持少量的XML配置。 properties-config.xml 欢迎关注我的公众号“彤哥读源码”,查看更多“源码&架构&算法”系列文章, 与彤哥一起畅游源码的海洋。以@Configuration类为主使用@ImportResource引入XML
@Configuration
@ImportResource("classpath:/com/acme/properties-config.xml")
public class AppConfig {
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;
@Bean
public DataSource dataSource() {
return new DriverManagerDataSource(url, username, password);
}
}
jdbc.properties
jdbc.url=jdbc:hsqldb:hsql://localhost/xdb
jdbc.username=sa
jdbc.password=
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
TransferService transferService = ctx.getBean(TransferService.class);
// ...
}