Spring 框架文档

目前只有核心篇的1.0部分, 后续后慢慢翻译出来, 英文实在太差, 有的不如机翻, 不过力求准确!

Version 5.1.3.RELEASE


  • 总览 历史, 设计哲学, 反馈, 入门.

  • 核心 IoC容器, 事件, 资源, 国际化(i18n), 验证, 数据绑定, 类型转化, Spring表达式语言(SpEL), 面向切面编程(AOP).

  • 测试 Mock对象, 测试上下文框架(TestContext framework), Spring MVC 测试, WebTestClient.

  • 数据访问 事务, DAO支持, JDBC, ORM, 编组XML.

  • Web Servlet Spring MVC, WebSocket, SockJS, STOMP 消息.

  • Web Reactive Spring WebFlux, WebClient, WebSocket.

  • 集成 Remoting, JMS, JCA, JMX, Email, Tasks, Scheduling, Cache.

  • 语言 Kotlin, Groovy, 动态语言(Dynamic languages).


第一部分 总览

内容:

  1. 我们为什么以"Spring"命名
  2. Spring及Spring框架的历史
  3. 设计哲学
  4. 反馈和贡献
  5. 入门

Spring 简化了Java企业应用的创建. 可以提供在企业应用环境下Java生态所需的一切, 同时也支持Kotlin和Groovy作为JVM的替代语言, 根据实际需要,也可以创建多种不同的架构.(architecture). 从Spring Framwork 5.0 开始, Spring需要JDK 8+ 并且已经为JDK9提供开箱即用支持

Spring提供了广泛的应用场景. 在大型企业应用中,应用往往要存在很长一段时间,并且不得不运行在一个升级周期超出开发人员控制的JDK和服务器上. 而其他的应用则使用内嵌服务器单独运行jar包,或者部署在云环境. 还有一些应用可能独立部署, 根本不需要服务器(例如批处理或集成负载).

Spring是开源的.背后有长期大量而活跃的根据实际应用案例而提交的反馈.这将帮助Spring成功长期进化.

1. 命名"Spring"的含义

"Spring"意思是在不同环境中不同的东西. 能够用来指代Spring项目本身(这是它的发端起始点). 随着时间推移, 其他建立在Spring之上的项目被创建出来. 通常我们称"Spring", 其实是指所有这些项目. 本文档主要聚焦基础: 也就是Spring框架本身.

Spring框架分模块. 可以根据情况选择需要的模块. 核心模块是核心容器, 包含配置模型和依赖注入机制. 还有更多,Spring 框架提供了对不同应用架构的基础支持. 包含消息,事务和持久化,还有Web. 它还包含基于Servlet的MVC框架, 同时提供了对应的交互式框架Web Flux.

关于模块的提醒: Spring框架jar文件允许JDK9支持的模块路径("Jigsaw"). 在此类应用中, Spring Framework 5 的jar文件带有自动模块名称清单. 它定义了独立于jar工件的语言级别的模块名称(“spring.core”,“spring.context”等). 当然, Spring在JDK8和9的类路径上都可以正常工作.

2. Spring 和 Spring Framework 的历史

Spring 是为了回应早期复杂的J2EE规范于2003年诞生. 有人认为 Java EE 和 Spring 是竞争关系,实际上,Spring是Java EE 的补充. Spring的编程模型并不是完全拥抱Java EE平台规范, 而是小心地有选择地从EE生态中集成了独立的规范:

  1. Servlet API (JSR 340)

  2. WebSocket API (JSR 356)

  3. Concurrency Utilities (JSR 236)

  4. JSON Binding API (JSR 367)

  5. Bean Validation (JSR 303)

  6. JPA (JSR 338)

  7. JMS (JSR 914)

  8. 如果需要的话,还有 JTA/JCA 做事务协调

Spring Framework 还支持依赖注入(JSR 330)和普通注解(JSR 250)规范. 这些规范的实现由开发人员可以用来替换Spring默认提供的机制.

Spring Framework 5.0 开始起, Spring要求Java EE 7以上(e.g. Servlet 3.1+, JPA 2.1+).同时使用新的Java EE 8(e.g. Servlet 4.0, JSON Binding API)以上的新api提供开箱即用.这就保证了Spring完全兼容Tomcat8和9, WebSphere9, 还有JBoss EAP 7.

慢慢的,Java EE 在开发中的角色发生了变化. 早期Java EE 和 Spring创建的程序是被部署到应用程序服务器上. 而今天, 归功于Spring Boot, 应用程序以devops或云的方式创建,使用内嵌的Servlet容器, 而且经常变化.自从Spring Framework 5 , WebFlux程序甚至都不需要直接调用Servlet Api了, 它可以运行在非Servlet规范的容器(如Netty)中.

Spring是持续革新和进化的. 超出Spring Framework, 有很多其他项目如Spring Boot, Spring Security,Spring Data,Spring Cload, Spring Batch,还有很多. 每个项目都有它自己的源码仓库, 问题跟踪和发布周期. 从spring.io/projects 可以看到所有项目的列表.

3. 设计哲学

当你学习一个框架的时候, 不仅要知道它能干什么, 更重要的是知道它所遵循的原则. 下面是Spring Framework遵循的指导原则.

  • 在所有层面提供选择权. Spring允许你尽量延迟设计选择. 例如, 你可以通过配置而不是修改代码就替换掉数据持久化的提供程序.这也同样适用于其他基础设施概念并能集成很多三方API.

  • 容纳不同的观点. Spring 拥抱伸缩性, 它并不坚持认为事情应该就这样做. 根据观点不同, 它提供了广泛的应用选择.

  • 保持强大的向后兼容性. Spring演化经过精心设计和管理, 可以防止版本之间出现破坏性改变. Spring支持一定范围版本的JDK和第三方库. 便于维护依赖于Spring的程序和库.

  • 关心API设计. Spring团队花费大量精力和时间设计API, 使其直观并且能保持多个版本和持续很多年.

  • 高质量的编码, Spring强调有意义的, 实时的,准确的javadoc. 是极少数声称代码简洁且包之间没有循环依赖的项目之一.

4. 反馈和贡献

对于如何操作或诊断或调试问题, 我们强烈建议使用StackOverflow, 并且我们有一个问题页清单, 列出了使用建议. 如果你完全确定Spring Framework有问题或者想提交特性, 请使用JIRA问题跟踪.

如果你解决了问题或者修正了建议, 你可以在GitHub上发起PR. 总之,请记住, 除了微不足道的问题,我们希望有个问题存根进行讨论并记录下来供未来参考.

更多问题请参看顶级页面"贡献"页上的指南.

5 入门

如果你刚开始使用Spring, 你可能想要通过创建一个Spring Boot的项目开始Spring之旅. Spring Boot提供了一个快速(也是固化的)方式创建生产就绪的 Spring 程序, 它基于Spring 框架, 信奉约定优于配置,并且设计为快速启动运行.

你可以使用start.spring.io来生成基础项目, 或者按照"入门"指南一步步创建, 例如"Getting Started Building a RESTful Web Service". 这些指南只关注于当前主题任务, 可以比较容易的理解, 很多都是Spring Boot项目. Spring portfolio还包含其他项目, 当你解决特定问题时你可能会考虑关注下相关的项目.


核心技术

这部分指导文档涵盖了Spring Framework不可或缺的所有技术

这些技术中最重要的,是Spring Framework的Ioc容器. 在吃透了Spring Framework 的IoC容器之后,紧接着是理解Spring的AOP技术. Spring Framework有其自身的AOP框架, 概念上很好理解并且能够满足实际应用中80%的热点需要.

Spring提供了AspectJ集成(这是目前特性最为丰富,当然也是Java企业领域最成熟的AOP实现).

1. IoC容器

本章涵盖Spring的IoC容器.

1.1 介绍Spring IoC容器和Beans

本节涵盖了Spring Framework对IoC原则的实现. DI是与其密切相关的另一个概念. IoC是一个处理过程,通过这个过程,对象只能通过构造函数参数, 工厂方法参数或在从工厂方法构造或返回的对象实例上设置的属性来定义他们的依赖关系. 当创建这些bean时, 容器去注入这些依赖. 这个过程从根本上反转了由对象自身去控制它所需依赖的方式, 通过直接类构造或类似Service Locator模式的机制.

org.springframework.beansorg.springframework.context 这两个包是IoC容器的基础. BeanFactory 接口提供了能够管理任何对象类型的高级配置机制. ApplicationContextBeanFactory 的一个子类接口. 增加以下功能:

  • 易于与Spring的AOP特性集成.
  • 消息资源处理(国际化)
  • 事件发布
  • 应用程序层次的特定上下文,例如:在Web程序中的WebApplicationContext.

简言之, BeanFactory 提供了配置框架和基本的功能, ApplicationContext 增加了诸多企业特性功能. ApplicationContextBeanFactory 的一个完整超集, 在本章中专门用于Spring IoC容器的描述. 如果想用BeanFactory代替ApplicationContext可以参看后面有关BeanFactory的内容.

Spring中,构成你程序的骨架并且被Spring IoC容器管理的对象被称为beans. bean就是一个被Spring IoC容器实例化,装配和管理的对象. bean也可以简单的是应用中诸多对象中的一个.bean和他们之间的依赖被映射到容器的配置元数据中.

1.2 容器概览

org.springframework.context.ApplicationContext 接口代表了Spring IoC容器并且负责实例化,配置,组装bean. 容器通过读取配置元数据获取指令来实例化,配置,组装bean.配置元数据使用XML,Java注解或者Java代码的方式表现.它允许您表达组成应用程序的对象以及这些对象之间丰富的依赖.

Spring提供了ApplicationContext 接口的几个实现. 在独立应用中, 通常会创建一个ClassPathXmlApplicationContextFileSystemXmlApplicationContext的实例.XML是传统的定义配置的格式, 你也可以通过一小段XML配置来启用这些支持的格式, 指定容器使用Java注解或者代码格式配置.

在很多的应用场景下, 并不需要显式的实例化一个或多个Spring的IoC容器. 例如, 在Web应用中,web.xml文件中大概八行类似的样板化的XML描述就足够了(参看Web程序中便捷的ApplicationContext实例). 如果你使用Spring Tool Suite(一种Eclipse增强开发环境), 能够很轻松地用几次点击鼠标和几个按键生成这样的样板配置.

下图从较高层次展示了Spring如何工作. 你的程序类和配置元数据时结合在一起的, 因此,当ApplicationContext创建并实例化后, 你就有了一个可执行系统或程序.

1.2.1 配置元数据

如同上图展示的, Spring IoC 容器使用配置元数据. 配置元数据表现了你作为开发者如何告知Spring容器去实例化,配置并组装程序中的对象.

配置元数据以传统而直观的XML格式提供, 这是本节大部分内容传达的关于Spring IoC容器的关键概念和特性.

XML不是唯一允许描述元数据的格式. Spring IoC 容器已经弱化了配置以何种格式书写. 当今,许多开发人员更愿意在程序中选择Java配置的方式.

如何使用其他格式的配置,可以参考下面的信息:

  • 注解配置: Spring 2.5引入了注解配置支持
  • Java配置: 从Spring3.0开始, Spring JavaConfig项目中的一些特性已经成为Spring Framework的核心. 因此,你可以使用Java而不是XML文件扩展你的应用. 要使用这些新特性, 请参看@Configuration,@Bean,@Import,@DependsOn注解.

Spring配置由至少一个,或典型的超过一个由容器管理的bean的定义. XML格式使用<bean/>元素配置这些beans, 它嵌套在顶层<beans/>元素里面. Java配置则包含在使用@Configuration注解的class中,并使用@Bean注解方法.

这些bean定义与构成你程序的对象相吻合. 例如, 你定义服务层对象,数据访问层对象,变现层对象如Struts Action 实例, 基础对象如Hibernate SessionFactories, JMS 队列等. 一般不会在容器中定义细粒度的域对象.因为这通常是DAO或业务逻辑层的任务去创建和加载这些对象. 尽管如此, 你可以使用AspectJ去配置在容器之外创建对象.参看在Spring中使用AspectJ依赖注入领域对象.

下面例子展示了XML配置的基本格式:

<?xml version="1.0" encoding="UTF-8"?>
<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="...">
        <!-- collaborators and configuration for this bean go here
        1. id是区分bean的一个字符串标识符
        2. class 定义bean的类型,使用全限定类名
         -->
    </bean>

    <bean id="..." class="...">
        <!-- collaborators and configuration for this bean go here -->
    </bean>

    <!-- more bean definitions go here -->

</beans>

id属性的值指向协作的对象. 本例中没有明确写出.可参看依赖项.

1.2.2 实例化容器

ApplicationContext 构造参数中的定位参数字符串是让容器从外部变量加载配置. 参数可以是多种资源格式, 例如本地文件系统, Java CLASSPATH等.

ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");

学习Spring容器后, 你可能想要了解下Spring的Resource抽象, 它提供了一种便捷的从URI格式的资源中读取流的机制. 尤其是,Resource路径通常用来构建程序上下文, 这点可参看"程序上下文和资源路径"

下面例子展示了服务层对象的配置文件:

<?xml version="1.0" encoding="UTF-8"?>
<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">

    <!-- services -->

    <bean id="petStore" class="org.springframework.samples.jpetstore.services.PetStoreServiceImpl">
        <property name="accountDao" ref="accountDao"/>
        <property name="itemDao" ref="itemDao"/>
        <!-- additional collaborators and configuration for this bean go here -->
    </bean>

    <!-- more bean definitions for services go here -->

</beans>

接下来的例子展示了数据访问层配置:

<?xml version="1.0" encoding="UTF-8"?>
<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">
        <!-- additional collaborators and configuration for this bean go here -->
    </bean>

    <bean id="itemDao" class="org.springframework.samples.jpetstore.dao.jpa.JpaItemDao">
        <!-- additional collaborators and configuration for this bean go here -->
    </bean>

    <!-- more bean definitions for data access objects go here -->

</beans>

前面例子中, 服务层由PetStoreServiceImpl类和两个数据读取对象JpaAccountDaoJpaItemDao(根据JPA对象关系映射标准). name指类中的属性,表示bean的名称, ref元素指向另一个bean定义. 在idref元素之间的联系表明了对象间的协作依赖关系. 关于对象依赖配置的更多细节, 参看"依赖项".

结合XML格式的配置

使用多个xml文件定义bean是有用的. 通常各自的xml文件能分别表示逻辑层或架构中的一个模块.

你可以使用程序上下文构造器从所有这些XML片段中加载bean的定义. 构造器获取多个resource资源位置, 就像我们在上节展示的那样. 或者, 使用一个或多个<import/>元素从其他文件中加载bean定义. 下面展示了如何这样做:

<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 必须是与导入文件处在相同路径目录下. 就像你看到的, '/'可以忽略. 虽然路径是相对的,但尽量不要使用'/'. 导入的这些文件的格式必须符合Spring的Schema, 需要有顶级<beans/>元素. 必须是有效的XML的bean定义.

可以但不提倡在父级目录中使用'../'引用文件. 这样会在当前应用程序外创建依赖文件.特别不提倡使用classpath:URLs(例如,classpath:../services.xml),运行时解析时会选择最近的根路径并且转到它的父目录.Classpath的配置可能会错误地引导到其他的目录下.
可以使用绝对路径替代相对路径,例如file:C:/config/services.xmlclasspath:/config/services.xml. 但这样就不灵活地将路径耦合到程序配置中了.一般的做法是可以使用一种间接方式-例如占位符"${...}", 这样系统JVM可以在运行时解析到正确路径上

命名空间本身提供了导入指令特性. 比纯bean定义更高级的配置特性Spring也有提供. 例如contextutil命名空间.

Groovy的Bean定义DSL

外部化元数据配置的更高级例子, bean定义也可以使用Spring的Groovy Bean Definition DSL, 因Gails框架而熟知. 下面演示了".groovy"文件中典型的配置的方式:

beans {
    dataSource(BasicDataSource) {
        driverClassName = "org.hsqldb.jdbcDriver"
        url = "jdbc:hsqldb:mem:grailsDB"
        username = "sa"
        password = ""
        settings = [mynew:"setting"]
    }
    sessionFactory(SessionFactory) {
        dataSource = dataSource
    }
    myService(MyService) {
        nestedBean = { AnotherBean bean ->
            dataSource = dataSource
        }
    }
}

这种配置的风格大体上与XML配置相同, 甚至支持XML命名空间. 可以通过importBeans指令从XML文件导入bean定义.

1.2.3 使用容器

ApplicationContext接口是一个能管理注册的bean以及他们之间依赖的高级工厂. 通过方法T getBean(String name, Class<T> requiredType) , 可以获取Bean的实例.

ApplicationContext允许读取bean定义并访问他们, 如下所示

// create and configure beans
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");

// retrieve configured instance
PetStoreService service = context.getBean("petStore", PetStoreService.class);

// use configured instance
List<String> userList = service.getUsernameList();

Groovy配置的启动也类似. 不过它有Groovy风格的不同上下文实现(同时也支持XML). 下面展示了Groovy配置:

ApplicationContext context = new GenericGroovyApplicationContext("services.groovy", "daos.groovy");

最灵活的变量是GenericApplicationContext, 其中包含读取器代理, 例如: 对于XML文件, 它使用XmlBeanDefinitionReader读取. 如下例:

GenericApplicationContext context = new GenericApplicationContext();
new XmlBeanDefinitionReader(context).loadBeanDefinitions("services.xml", "daos.xml");
context.refresh();

对于Groovy, 可是使用GroovyBeanDefinitionReader, 如下所示:

GenericApplicationContext context = new GenericApplicationContext();
new GroovyBeanDefinitionReader(context).loadBeanDefinitions("services.groovy", "daos.groovy");
context.refresh();

在相同的ApplicationContext中可以混合使用这些读取代理器, 从而从不同资源中读取配置.

可以使用getBean获取bean的实例. ApplicationContext接口还有一些其他的方法获取bean, 但是理想状态下, 你的程序应该永远不要使用它们. 确实, 你的程序代码压根不应该调用getBean方法,因此就一点也不会依赖Spring API. 例如, Spring为多种Web框架的组件集成提供DI功能, 例如controller和JSF管理的Bean, 允许你通过元数据声明依赖的bean(类似包装(autowiring)注解).

1.3 Bean 概览

Spring IoC 容器管理一到多个bean. 这些bean是根据你提供给容器的配置创建的. (例如, 通过XML格式的<bean />定义)

在容器内部, 这些bean定义表现为BeanDefinition对象. 其包含如下信息 :

  • 包含包名的类名: 典型地,bean定义的实际实现类;
  • Bean行为配置元素, 标记bean在容器中行为(作用域scope, 生命周期回调等);
  • bean的协同或依赖的其他bean的引用.
  • 最近创建对象的其他配置信息,例如当使用bean时连接池的池大小或链接数

元数据被解析为一系列组成bean定义的属性, 下面表格列出了这些属性:

Table 1. The bean definition
| 属性 | 参看 |
| ------ | ------ |
| Class | 初始化bean |
| Name | 命名bean |
| Scope | Bean的作用域 |
| Constructor arguments | 依赖注入 |
| Properties | 依赖注入 |
| Autowiring mode | 自动装配协作对象 |
| Lazy initialization mode | 懒加载Bean |
| Initialization method | 初始化回调 |
| Destruction method | 销毁回调 |

bean定义包含如何创建特定bean, 除此之外ApplicationContext的实现允许将容器外创建的bean注册进来. 这是通过getBeanFactory()方法访问ApplicationContext的 BeanFactory , 该方法默认返回DefaultListableBeanFactory 实现. DefaultListableBeanFactory支持通过registerSingleton(..)registerBeanDefinition(..) 方法注册. 尽管可以这样做,应用程序一般还是单纯使用规则的bean定义元数据.

bean元数据和单例的手工支持必须尽早注册, 这是为了容器能够在自动装配和其他自省阶段合理解析.重写已经存在的元数据以及已经存在的单例在某些级别上是支持的, 但运行时注册新的bean(与工厂的并发访问)没有得到正式的支持,而且可能导致并发访问异常或bean状态不一致,或两者都有.

1.3.1 命名Bean

每个bean都有一个或多个标识符. 容器内这些标识符必须是唯一的. 一般一个bean只有一个标识符. 但是也可以有多个,多出来的标识符是别名.

XML配置中,idname属性用来做标识符. id用来精确指定一个id. 按照习惯, 这些名称是字母数字组成的('myBean', 'someService', etc.), 但他们也可以包含特殊字符. 如果你想给bean指定别名,你可以将他们赋值给name属性, 用逗号,分号或者空格分割. 在Spring3.1之前, id是定义为一个xsd:ID类型, 只能是字母. 自从3.1开始将其定义为xsd:string类型. 注意id的唯一性依然是容器强制的, 而不是XML解析器.

给beannameid属性不是必须的. 如果没有指定, 容器会为bean生成一个唯一名称. 尽管这样, 如果你想通过名称引用bean, 或者通过ref元素或者是Service Locator风格的查找, 你就必须给bean指定名称. 不给bean指定名称的动机是使用内部类和自动装配.

扫描类路径时, Spring会给未命名组件生成名称, 遵循前面描述的规则: 本质上是取类名,然后将首字符小写. 当有多余一个字母并且第一个和第二个字母都是大写时将保留大小写. 这些规则定义在java.beans.Introspector.decapitalize(Spring使用)

在Bean定义之外添加别名

在bean定义内, 你可以给它指定多个名称, 可以使用给id指定一个名称, 同时也可以给name指定多个(使用分隔符).使用这些名称指定的bean都是等效的, 在某些情况下也是有用的, 例如: 让应用程序中的每个组件通过使用特定于该组件本身的bean名称来引用公共依赖项。

然而在定义bean的时候为其指定别名有时候并不够. 有时候需要将别名bean定义外的其他地方指定. 通常的例子是, 在大型系统内各个子系统分别有各自配置, 每个子系统有一组其bean的定义. XML的配置中,你可以使用<alias/>元素实现. 如下:

<alias name="fromName" alias="toName"/>

本例中, bean的名称(同一容器)被命名为fromName, 在使用别名定义后, 这个bean通过toName也可以引用.

例如, 子系统A引用了一个数据源叫subsystemA-dataSource. 子系统B引用数据源叫subsystemB-dataSource. 当主程序都使用这两个系统时, 主程序引用了数据源myApp-dataSource. 这三个数据源都指向相同的对象, 你可以将下面的别名配置添加到元数据:

<alias name="myApp-dataSource" alias="subsystemA-dataSource"/>
<alias name="myApp-dataSource" alias="subsystemB-dataSource"/>

现在, 虽然每个组件和主程序都通过一个名称引用了各自唯一的数据源, 并且能保证不会与其他定义冲突(有效创建了命名空间), 然而实际上他们引用的同一个对象.

1.3.2 实例化Bean

bean定义的本质是创建对象的配方. 当需要时容器将查看bean的配方, 并使用该bean的定义封装的配置元数据来创建(或获取)实际对象.

如果使用XML配置, 要实例化的对象的类型是通过<bean/>节点的class属性来指定的. class属性(对应到BeanDefinition实例是Class属性)通常是强制的. (例外的情况请参看:使用工厂方法实例化,和Bean定义的继承.) 有两种使用Class属性的方法:

  • 典型的, 直接指定class, 由容器通过构造器反射的形式直接创建bean. 有点等同于java代码的new操作.
  • 为包含创建对象的静态工厂方法指定对象的实际类, 少数情况下容器通过静态工厂方法创建bean. 被静态工厂方法创建出来的对象可能是相同的类型或者压根是另一个类型.
使用构造器实例化

当使用构造器创建bean时. 所有标准类都可用且都是可与Spring兼容的. 也就是开发时不需要实现任何接口或者遵循特定的编程风格. 简单定义为一个class即可. 尽管如此, 根据使用的IoC容器, 可能你需要定义一个默认构造器.

IoC容器可以管理任何你想要被托管的类. 它不仅限于管理JavaBeans. 大多数Spring用户喜欢在属性后定义getter和sertter模块. 你也可以在容器中定义非bean风格的类. 例如, 如果你想使用遗留代码中完全不遵循JavaBean规范的连接池, Spring也是可以管理的.

使用XML配置指定bean定义 如下:

<bean id="exampleBean" class="examples.ExampleBean"/>

<bean name="anotherExample" class="examples.ExampleBeanTwo"/>

更多关于构造函数参数和对象构造后属性的赋值, 参看:依赖注入.

静态工厂方法实例化

当定义使用静态工厂构建的bean时, 需要使用class属性指定包含静态工厂方法的类, 并且使用factory-method属性指定工厂方法.你可以调用该方法(带可选参数, 后面有表述)返回一个对象, 接着这个对象就可以像使用构造器创建出来的一样使用了. 一种这么使用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属性设置工厂方法的名称. 下面展示了怎么配置:

<!-- the factory bean, which contains a method called createInstance() -->
<bean id="serviceLocator" class="examples.DefaultServiceLocator">
    <!-- inject any dependencies required by this locator bean -->
</bean>

<!-- the bean to be created via the factory bean -->
<bean id="clientService"
    factory-bean="serviceLocator"
    factory-method="createClientServiceInstance"/>

下面代码展示了对应的java类:

public class DefaultServiceLocator {

    private static ClientService clientService = new ClientServiceImpl();

    public ClientService createClientServiceInstance() {
        return clientService;
    }
}

工厂类可以有多个工厂方法的定义, 如下所示:

<bean id="serviceLocator" class="examples.DefaultServiceLocator">
    <!-- inject any dependencies required by this locator bean -->
</bean>

<bean id="clientService"
    factory-bean="serviceLocator"
    factory-method="createClientServiceInstance"/>

<bean id="accountService"
    factory-bean="serviceLocator"
    factory-method="createAccountServiceInstance"/>

下面是对应的java类:

public class DefaultServiceLocator {

    private static ClientService clientService = new ClientServiceImpl();

    private static AccountService accountService = new AccountServiceImpl();

    public ClientService createClientServiceInstance() {
        return clientService;
    }

    public AccountService createAccountServiceInstance() {
        return accountService;
    }
}

关于工厂bean本身如何通过DI管理和配置, 参看:依赖和配置细节.

Spring文档中,"factory bean"(工厂bean)是指Spring容器中配置的用来通过实例或静态工厂方法创建对象的bean. 相比而言, FactoryBean(注意大小写)指Spring特有的FactoryBean

1.4 依赖

典型的企业应用不是由单个对象组成的(或用Spring语法来说的bean).就算是最简单的程序也有一些终端用户看起来相互合作的对象来呈现.接下来的这节阐述如何在一个真实的系统中定义一系列的bean, 从而让他们协作达成目标.

1.4.1 依赖注入

依赖注入是一个处理过程, 在对象被构造后或者从工厂返回后, 仅仅通过构造函数参数, 工厂方法的参数或者属性赋值的方式来定义他们的依赖(也就是与其他对象协作). 容器在创建bean后注入他们的依赖. 这个处理过程本质上是bean自己去使用类构造和服务定位模式管理初始化和定位它的依赖项的反转(因此叫控制反转).

使用DI原则的代码是清晰的, 并且做到了与提供的依赖项更有效地解耦. 对象不自己定位查找依赖项, 也不知道依赖项的位置和类型.因此, 你的类就更容易被测试, 特别是依赖于接口和抽象类的情况下, 允许你单元测试中使用桩对象或模拟实现.

DI有两个主要的变种: 构造函数依赖注入和属性Setter依赖注入.

构造函数依赖注入

构造函数注入是通过容器调用有若干参数的构造函数完成的, 每个参数代表一个依赖项. 调用带参的静态工厂方法构造bean与此十分类似, 这里讨论对待构造函数构造和静态工厂方法构造是相似的. 下例展示了构造器注入的类定义:

public class SimpleMovieLister {

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

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

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

注意这个类没啥特别之处. 它本身就是一个没有实现容器相关接口,基类或使用注解的普通POJO.

构造参数解析

构造参数是通过类型解析匹配的. 如果bean的构造参数没有潜在的二义性, 那么在bean中定义的参数顺序就是bean初始化时的参数顺序. 看下面的代码:

package x.y;

public class ThingOne {

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

假设ThingTwoThingThree没有继承关系, 没有潜在的二义性. 因此, 下面的配置能很好的起作用, 在<constructor-arg/>元素中你不需要制定构造参数的索引或明确制定其类型.

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

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

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

当另一个bean被引用时, 类型是已知的,匹配能发生(就像前面例子中的处理过程). 当使用简单类型时,例如<value>true</value>, Spring不能决定值的类型, 因此无法自动匹配. 再看下面的类:

package examples;

public class ExampleBean {

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

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

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

构造参数类型匹配

上述场景中, 如果使用type属性给参数指定了类型, 容器就能通过类型匹配. 如下所示:

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

构造参数索引

可以使用index属性指定构造函数的参数, 如下:

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

另外, 如果参数有多个简单类型, 可以使用索引解决多个简单类型参数的二义性.

构造参数名称

也可以使用指定构造参数名称消除二义性, 如下:

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

记住, 要不受限制的使用此功能, 你的代码必须要启用debug标记编译, 这样Spring才能从构造函数查找参数名称. 如果不能或不想启用debug标记, 可以使用@ConstructorPropertiesJDK注解显式添加到构造函数的参数上. 看起来如同下面例子:

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或静态工厂方法返回bean之后, 再由容器调用bean的setter方法.

下例展示了只能用纯setter方式进行注入的类定义. 这个类是传统的java. 是一个没有实现容器相关的接口,基类或添加注解的POJO.

public class SimpleMovieLister {

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

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

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

ApplicationContext支持构造函数注入和Setter注入. 也支持通过构造函数注入部分依赖后再由Setter注入. 你使用BeanDefinition类的形式配置依赖, 这个类与PropertyEditor实例协作将属性从一种格式转化为另一种格式. 尽管如此, 大多Spring用户不会直接使用这些类(在编程方面), 而是使用XML bean定义, 或者注解注释组件(也就是使用@Component,@Controller等),或者用@Configuration注解的类中使用@Bean的java代码配置. 这些资源将在内部转化为BeanDefinition实例并用于在IoC容器实例加载.

依赖解析过程

容器按照下面阐述的对bean依赖进行解析:

  • ApplicationContext被创建, 并且使用配置的bean元数据进行初始化. 配置元数据可以是XML,Java code或注解.
  • 对于每个bean, 它的依赖以属性,构造函数参数或者静态工厂的参数(如果使用代替普通的构造函数)形式表现. 这些依赖将在bean被创建后提供给bean.
  • 每个属性或参数是要被设置的值的定义, 或者是容器中另一个bean的引用.
  • 每个属性或参数的值是由特定的格式转换到实际类型的. 默认情况下, Spring能够将字符串格式转化为所有内置类型如: int, long, String, boolean等.

Spring容器在创建后对每个bean的配置进行校验. 尽管如此, 在bean被创建后bean的属性才会被赋值. 单例域的并且设置为预实例化(默认情况)的bean在容器创建后被创建. 域的定义参看bean的作用域. 除此之外, bean只有在被请求时才创建. 一个bean的创建会潜在地导致bean的整个图被创建, 也就是bean的依赖,它的依赖的依赖等都被创建和分配. 注意: 依赖解析的不匹配可能会后期表现出来, 也就是第一次创建受影响的bean时.

总体上你可以信任Spring去做正确的事情. 它在容器加载期间检测配置问题, 如不存在的bean或循环依赖.当bean被真正创建后, Spring会尽量延后设置属性和解析依赖. 这意味着在容器正确加载后, 如果你请求的bean有问题或它的依赖有问题,可能会抛出异常--例如, bean抛出缺失或无效属性的异常. 这种潜在的配置问题的延迟导致的不可见性就是为什么ApplicationContext的实现默认会预实例化单例bean. 这种会导致前期一些时间和内存消耗的代价能换来配置问题的及时发现, 就是在ApplicationContext被创建时, 而不是以后调用bean时. 你也可以覆盖这种预初始化的配置为延迟初始化.

如果没有循环依赖, 当一个或多个协作的bean被注入到依赖的bean里面, 每个协作bean其实是优先于依赖bean就被完全配置好了. 这意味着如果A依赖B, 容器会在调用A的setter方法之前完全配置好B. 换句话说,这个bean被实例化了(如果它不是预实例化的),它的依赖被设置了, 并且他的生命周期函数(如配置的初始化函数或初始化bean回调函数)也被调用了.

DI的例子

下例使用XML配置setter形式的DI. 一小段bean的定义如下:

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

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

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

下面展示了对应的ExampleBean类:

public class ExampleBean {

    private AnotherBean beanOne;

    private YetAnotherBean beanTwo;

    private int i;

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

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

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

上例中, setter声明为匹配xml文件中指定的属性, 下面例子使用构造函数注入:

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

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

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

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

下面展示了对应的ExampleBean类定义:

public class ExampleBean {

    private AnotherBean beanOne;

    private YetAnotherBean beanTwo;

    private int i;

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

在bean定义的构造函数参数用来通过类ExampleBean的构造函数参数注入.

现在改变下例子, 不用构造函数注入, 而使用静态工厂方法返回对象实例:

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

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

下面展示了对应的ExampleBean类:

public class ExampleBean {

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

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

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

给静态工厂方法的参数是<constructor-arg/>元素提供的, 就像使用构造函数一样. 工厂方法返回的实例类型不需要与包含静态工厂的类的类型一致(本例一致). 一个(非静态)实例工厂本质上使用相同方式(除了使用factory-bean而不是class属性),所以我们不讨论这些细节.

1.4.2 依赖和配置细节

在前面的章节中提到, 你可以定义bean的属性或者通过构造函数参数去引用另外的bean(协作者)或者在行内书写数据值. 为了能这样做, Spring的XML配置可以使用<property/><constructor-arg/>元素包含在bean定义元素中.

纯值数据(原始数据,String等类型)

元素<property/>的属性value可以指定为书写或构造参数的纯值数据. Spring的转化服务用来将这些值从String转化为合适的类型. 下例展示了一组这样的配置:

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

下例使用p-命名空间展示更简洁的XML配置:

<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更为精简. 类型转化是发生在运行时而不是设计时, 除非你使用能够在定义bean时支持属性自动完成的IDE(就像IntelliJ IDEA 或 Spring Tool Suite). 而这样的IDE辅助是我们提倡的.

也可以配置java.util.Properties类型的实例, 如下:

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

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

Spring 容器转化内部<value/>元素的内容为java.util.Properties的实例, 这是通过使用JavaBeans的PropertyEditor机制来实现的. 这是一个捷径, 也是几种Spring团队喜欢使用嵌套<value/>而不是value属性风格的情况之一.

idref元素

idref元素仅是一种防止错误的方法, 可以将容器中的另一个bean的id(一个字符串,不是引用)传递给属性或构造参数. 如下所示:

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

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

上面bean的定义完全等效于(在运行时)下面的片段:

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

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

第一种形式优于第二种, 因为使用idref标签能够让容器在部署期间检查引用的bean是不是真的存在. 第二种情况下, 传递给bean名为clienttargetName属性的值不会被校验. 仅仅是在client被实际实例化的时候会发生类型转化(大多数情况下是严重错误). 如果clientbean是个原型bean, 则只有在部署容器很久之后才会发现错误和由此产生的异常.

<idref/>元素带值的一个地方(至少在Spring2.0之前的版本中)是在ProxyFactoryBean定义中AOP拦截器的配置. 当你为了防止拼写错拦截器ID而指定拦截器名称时使用<idref/>元素.

引用其他bean(协作者)

ref元素是在<constructor-arg/> 或者 <property/>中的不可更改的元素. 在这里, 你将另一个由容器管理的bean(协作者)作为引用的值赋给一个bean的属性. 被引用的bean作为引用被赋值给这个bean的属性, 并且它是在被赋值前就按需初始化的. (如果这个协作者是个单例的话,它已经被容器初始化了).所有引用最终都是另一个对象的引用. 作用域和校验则取决于你是否通过bean,local,partent属性为另一个对象指定ID或名称.

通过<ref/>tag的bean属性指定目标bean是常见的方式, 并且允许其在同一个容器或父容器中创建任何bean的引用. 不管是不是配置在XML格式的文件. bean属性的值可以是目标bean的ID后者是name中的任一值. 下面展示了ref元素:

<ref bean="someBean"/>

通过parent属性创建的引用指定目标bean是在当前容器的父容器. 其值可能是目标bean的id或name的其中任一值. 目标bean必须在当前容器的父容器中. 主要使用这个属性的场景是: 当你使用了有层次的容器并且在父容器中通过代理proxy包装了一个同名的父bean. 下面是一对使用parent的例子:

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

<bean/>元素如果定义在<property/>或者<constructor-arg/>内部, 则表示定义了内部bean, 如下所示:

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

内部bean定义不需要指定ID或name, 如果指定了, 容器也不会使用它们作为bean的标识符. 容器也会在创建时忽略它们的scope标记, 因为内部bean通常都是匿名的, 并且总是跟外部bean一起创建. 一般不可能去单独的访问内部bean, 或者将他们注入到协作bean而不是包装bean中.

作为旁例, 从一个自定义的scope中获取到销毁回调是可能的, 例如对于一个单例bean中作用域为request-scope的内部bean. 内部bean的创建是与外部bean的创建是绑定的, 但是销毁回调使它特定于request生命周期. 这并不是一个普遍的场景, 内部bean一般是与包含它的bean有着相同的作用域.

集合

<list/>,<set/>,<map/><props/>元素分别
对应于Java集合类型(Collection)的List, Set, Map, 和 Properties. 下例展示其用法:

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

字典map的键值,或者集set的值, 可以是下面元素的任一:

bean | ref | idref | list | set | map | props | value | null
集合合并

Spring容器支持合并集合. 开发人员可以定义一个父的<list/>,<set/>,<map/><props/>元素,并且子元素的<list/>,<set/>,<map/><props/>可以继承和覆盖父集合的值. 也就是说, 子集合的值是父集合与子集合元素的合并, 子集合的元素覆盖了父集合中的值.

本节讨论父子bean的合并机制. 读者要是不了解父子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">
            <!-- the merge is specified on the child collection definition -->
            <props merge="true">
                <prop key="sales">[email protected]</prop>
                <prop key="support">[email protected]</prop>
            </props>
        </property>
    </bean>
<beans>

注意merge=true的使用, 它在beanchild<props>元素中名为adminEmails. 当child被容器解析和初始化后, 最终的实例将有一个adminEmailsProperties集合, 包含了合并父集合与子集合中adminEmails的全部元素. 下面展示了结果:

[email protected]
[email protected]
[email protected]

Properties集合的值继承了父<props/>, 并且子集合中support值覆盖了父集合中的值.

这种合并行为也同样类似于<list/>, <map/>, 和 <set/>等集合类型. 在<list/>元素的特定情况下, 其语义与List集合类型相关联(也就是一个一系列值的有序集合). 父集合的值优先于子集合的值. 在Map, Set, 和 Properties类型情况下, 元素间不存在顺序. 因此,容器内部使用构筑在Map,Set,Properties实现类型之上的无序集合类型.

集合合并的限制

不能合并不同的集合类型(如MapList进行合并). 如果这样做, 会抛出相应异常. merge属性必须在继承或者较低的子定义上. 指定在父集合定义上的merge是多余的,也不会产生期望的合并.

强类型集合

从Java5的泛型集合开始, 你可以使用强类型的集合了. 也就是声明一个值包含(例如)String 类型的元素集合成为可能. 如果使用Spring的DI去注入一个强类型集合, 你可以得到Spring类型转化支持, 将先前添加到Collection的元素转化为正确的类型. 下例展示了用法:

public class SomeClass {

    private Map<String, Float> accounts;

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

当beansomethingaccount属性准备注入时, 它的泛型类型信息被反射获取到. 因此, Spring的类型转化基础设施辨别出元素的值是Float, 并且将字符串值(9.99,2.75 和 3.99)转化为实际的Float类型.

Null和空字符值

Spring将properties的空参数视为空字符串. 下面的XML配置片段设置email属性为空值("").

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

上面的例子等效于下面的Java代码:

exampleBean.setEmail("");

<null/>元素处理null值. 如下所示:

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

上面代码等效于:

exampleBean.setEmail(null);
XML的p-命名空间

p-命名空间可以在bean元素的属性中使用,(而不是嵌套的<property/>元素), 可以用来描述属性值或协作bean.

Spring支持用命名空间扩展配置格式, 这基于XML架构定义. 这节讨论的bean配置格式定义在XML架构文档中. 尽管如此, 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>

这个例子展示了bean定义中有个p-命名空间叫email. 这实际是告诉Spring有个属性声明. 如前面提到的, p-命名空间没有架构定义, 因此你可以用属性的名字设置标签属性名称.

下面展示了两个bean定义中同时引用了另一个bean.

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:p="http://www.springframework.org/schema/p"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        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定义使用<property name="spouse" ref="jane"/>创建了从beanjohnjane的引用, 第二个定义则使用了p:spouse-ref="jane"来完成相同的事情. 本例中, spouse是属性名, 同时-ref表明这不是一个表值而是对另一个bean的引用.

XML的c-命名空间

类似于p-命名空间. 从Spring3.1开始, c-命名空间允许将构造参数配置在行内, 而不是单独嵌套的<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="beanTwo" class="x.y.ThingTwo"/>
    <bean id="beanThree" class="x.y.ThingThree"/>

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

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

</beans>

c:命名空间使用与p:命名空间相同的约定来设置构造函数参数(对于引用使用-ref后缀).相似的, 需要声明在XML文件中, 虽然在XSD架构中未定义(它存在于Spring的内核中).

对于少数构造函数名称不可用的情况(通常是没有debug信息的已编译二进制文件),可以使用参数索引, 如下:

<!-- c-namespace index declaration -->
<bean id="beanOne" class="x.y.ThingOne" c:_0-ref="beanTwo" c:_1-ref="beanThree"
    c:_2="[email protected]"/>

实践中, 构造函数解析机制对于匹配参数已经足够了, 所以除非你真正需要, 我们建议使用名称标记贯穿整个程序.

复合属性名称

在设置bean属性时可以使用复合或者嵌套的属性名称. 只要路径下所有组件不为null即可. 如下定义:

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

somethingbean有个fred属性, fred又有个bob属性, bob有个sammy属性, 最终sammy属性被赋值为123. 为了能使其起作用, 在bean被构建后, fred和其bob必须不为null.否则NullPointerException将被抛出.

1.4.3 使用depend-on

如果一个bean是另一个bean的依赖, 也就意味着一个bean会作为另一个bean的属性值. 在XML配置中你使用<ref/>元素来配置. 但有时候bean之间的依赖关系不是直接的. 举个例子, 一个类的静态初始化器需要被触发,比如对于数据库驱动注册. depends-on属性能够显式的迫使一个或多个bean的初始化, 这发生在使用了这个元素的bean初始化之前. 下例使用depends-on展示只有一个bean的依赖:

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

对于多个依赖, 可以为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" />

1.4.4 延迟加载的bean

默认情况下,ApplicationContext实现会立马创建和配置所有单例bean, 作为其初始化步骤的一部分. 通常,预初始化时令人满意的, 因为配置和环境错误可以被及时发现, 而不是经过几小时,几天. 当这种行为不令人满意时, 你可以通过将bean定义为延迟加载而阻止预初始化发生. 一个延迟加载的bean告知IoC容器,这个bean是在第一次请求时创建,而不是容器启动时.

XML中, 通过<bean/>元素的lazy-init属性控制这种行为. 如下:

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

当上面的配置被ApplicationContext处理时, 其启动时lazybean不会被立即初始化, 而not.lazybean将立即被初始化.

尽管如此, 当一个延迟初始化bean是一个非延迟初始化bean的依赖时, ApplicationContext在启动时创建了延迟bean, 因为它必须满足单例的依赖. 延迟初始化bean被注入到非延迟的单例bean中了.

也可以在容器级别通过<beans/>元素的default-lazy-init属性控制延迟加载行为. 如下所示:

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

1.4.5 自动装配

Spring容器能够自动装配协作bean之间的关系. 你可以让容器通过检查ApplicationContext中的内容自动解析协作bean. 自动装配有如下好处:

  • 自动装配可以显著减少指定属性和构造参数的需要. (对于本章其他地方讨论的bean模板等机制也是有价值的).

  • 自动配置可以随着对象发展而更新配置. 例如, 如果需要向类添加依赖,则可以自动满足依赖而不需手工配置. 因此,自动装配在开发阶段很有用, 不会在代码变得更稳定时拒绝切换到显式书写的选项.

当时用XML配置时(参看依赖注入), 你可以通过<bean/>元素的autowire属性为一个bean指定自动装配模式. 自动装配功能有四种模式. 你可以任选其一. 下表描述了这四种模式:

Table2. 自动装配模式

通过byTypeconstructor装配模式, 你可以装配数组和泛型集合. 这种情况下, 容器中所有类型匹配的候选对象都将提供以满足依赖. 对于Map,如果key的类型是String,你就可以自动装配. 一个自动装配的Map实例的值是由所有匹配类型的bean组成的, 这个实例的key包含对应bean的名称.

自动装配的限制和不足

自动装配使用在贯穿整个项目的过程中能工作得很好. 如果不是普遍使用, 而只是使用到一两个bean上时会搞得开发人员头晕.

参考自动装配的限制和不足:

  • propertyconstructor-arg设置的显式依赖总是会覆盖自动装配. 不能自动装配简单类型,StringClass(或者这些类型的数组). 这个限制是专门设计的.

  • 比起显式装配, 自动装配精确度较低. 虽然, 正如前面表格提到的, Spring非常小心地避免在多个期望结果下导致的二义性进行猜测. Spring管理的对象间的关系以及不是显式文档定义的了.

  • 装配信息可能是不可用的, 对于从Spring容器生成的文档的工具而言.

  • 容器内多个bean定义可能会匹配到自动装配的setter类型或构造参数类型.对于数组,或者Map实例, 这不是一个问题. 然而对于期望单一值的依赖, 这种二义性不能被随意处理, 如果没有唯一bean可用,异常将会抛出.

对于后面的几种场景, 你可能有如下几个选择:

  • 抛弃自动装配, 拥抱显式装配

  • 设置bean的autowire-candidatefalse以防止自动装配, 正像下一节描述的.

  • 通过设置<bean/>元素的primary属性为true将其定义为优先匹配对象.

  • 使用有更多细粒度控制的注解驱动配置, 如在注解配置中描述的.

从自动配置中排除bean

在单个bean级别, 你可以从自动装配中排除bean.在XML配置中, 可以通过设置bean的autowire-candidatefalse. 容器能使得特定bean排除在自动装配之外(包括注解格式的配置如@Autowired).

你可以通过基于bean名称的模式匹配去限制bean的自动装配. 根级别元素<beans/>通过属性default-autowire-candidate接收一个或多个模式.例如:限制名称以Repository结尾的bean的候选状态, 可以使用模式*Repository. 可以通过逗号分隔多个模式. bean元素上的autowire-candidate属性的值truefalse总是有优先权. 对于这些bean, 模式规则不生效.

这些技术对于从没想要通过自动装配注入的bean是有用的. 这并不意味着被排除的bean自己不能通过自动装配所配置, 而是它本身将不会作为bean的候选装配给其他bean.

1.4.6 方法注入

大多数应用场景中, 容器中的很多bean都是单例的. 当一个单例的bean需要与另一个单例bean协作, 或者一个非单例bean需要与另一个非单例bean协作时, 一般需要通过将一个bean作为另一个bean的属性来处理依赖关系. 当bean的生命周期不同时将会发生问题. 假设一个单例bean A需要一个非单例(原型)bean B, 也许每个方法都有调用. 容器只创建A一次, 因此只有一次机会设置它的属性. 一旦有用到, 容器不能总是使用B的新实例提供给A.

一种解决方案就是抛弃依赖注入. 你可以使一个Bean A 通过实现接口ApplicationContextAware被容器所感知, 并且a通过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容器更为高级的特性, 能够让你更聪明滴处理此类问题.

查找方法注入

查找方法注入是重写容器管理的bean并查找另一个容器中的命名bean将其返回的能力. 查找一般是一个原型bean, 就像在前面章节讨论的. Spring框架通过CGLib库的二进制代码动态生成子类重写方法.

  • 为了能让动态的子类工作, 需要为其生成子类的bean不能是final, 同时需要覆盖的方法也不能是final.

  • 进行单元测试时, 如果有abstract方法的类需要你自己去定义子类并且提供abstract方法的桩实现.

  • 具体方法也是需要能组件扫描的, 这就需要获取具体类.

  • 另一个关键的限制查找方法和工厂方法,特别是与配置类中的@bean注解方法不兼容. 这种情况下, 容器不再控制创建实例因此也就不能在运行时创建子类.

在前面的CommandManager类的代码片段中, Spring容器动态覆盖实现了createCommand()方法. 类CommandManager没有任何Spring依赖, 如下所示;

package fiona.apple;

// no more Spring imports!

public abstract class CommandManager {

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

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

在客户端代码中包含了需要注入的方法, 这个被注入的方法需要如下的格式:

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

如果方法是abstract修饰的, 子类将实现这个方法, 否则动态生成的子类将重写定义在源类中的实际方法. 如下所示:

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

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

id定义为commandManager的bean调用其createCommand()方法, 在其需要myCommand实例的时候. 你必须小心地部署beanmyCommand为原型bean. 如果它是一个单例, 则每次返回的都是相同的实例.

也可以使用组件注解的方式, 你可以通过@Lookup注解在方法上, 如下所示:

public abstract class CommandManager {

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

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

或者, 更规范地, 你可以信任目标bean通过返回的类型解析得到目标bean.

public abstract class CommandManager {

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

    @Lookup
    protected abstract MyCommand createCommand();
}

注意通常你需要用一个桩实现来声明这种带注释的查找方法, 这样他们才能与Spring组件扫描兼容, 默认情况下, 抽象类会被扫描忽略. 这个限制不适于显式注册或显式导入bean类.

任意方法替换

一种比查找方法注入不那么有用的形式是在一个管理bean中使用另一个方法实现去替换方法. 你可以跳过这节, 直到你需要这种机制再回来看.

使用XML配置的元数据时, 你可以使用replaced-method元素替换一个已经存在的方法. 请看下面的类定义, 它有个方法computeValue需要被重写:

public class MyValueCalculator {

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

    // some other methods...
}

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

/**
 * meant to be used to override the existing computeValue(String)
 * implementation in MyValueCalculator
 */
public class ReplacementComputeValue implements MethodReplacer {

    public Object reimplement(Object o, Method m, Object[] args) throws Throwable {
        // get the input value, work with it, and return a computed result
        String input = (String) args[0];
        ...
        return ...;
    }
}

需要部署的源bean定义和需要覆盖的方法应该按如下方式组合:

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

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

你可以为元素<replace-method/>需要覆盖的方法签名指定一个或多个<arg-type/>元素. 只有类中的方法被重载并且有多个的时候才需要参数签名. 为了方便, String类型的参数可以只是一个缩写, 例如,下面的写法都匹配java.lang.String

java.lang.String
String
Str

因为参数的个数经常能区分出可能的选择, 因此这种缩写能节省大量输入时间, 通过一个短字符串来匹配一个参数类型.

1.5 Bean 作用域(scope)

当创建bean定义的时候, 实际上你就有个通过bean定义去创建类真实实例的配方. bean定义是配方的想法是非常重要的, 因为它意味着,你可以通过一个配方去创建多个实例.

通过bean的定义, 你不仅可以控制插入到对象的依赖和配置值, 还可以控制通过bean定义的对象的作用域. 这种方式是强大而灵活, 因为你可以选择通过配置生成的对象的作用域, 而不是必须在class级别操作对象的作用域. bean可以定义为几个作用域其中的一个. Spring框架支持六种作用域, 四种仅可以用在web类型的ApplicationContext中. 你也可以自定义scope.

下面表格描述了支持的作用域:

Table 3. Bean scopes
|Scope|Description|
|------|------|
|singleton|(默认)对于每个IoC容器来说, 处理每个bean定义的对象实例仅有一个|
|prototype|处理一个bean定义可以有多个对象实例|
|request|处理对于每个HTTP请求仅有一个实例. 也就是对于每个bean定义, 每个HTTP请求都有它自己的实例. 仅仅在Web类型的Spring ApplicationContext中是可用的.|
|session|处理在一个HTTPSession范围内一个bean的定义. 仅仅在Web类型的Spring ApplicationContext中是可用的.|
|application|处理在ServletContext级别的bean的定义. 仅仅在Web类型的Spring ApplicationContext中是可用的.|
|websocket|处理在WebSocket级别的bean的定义, 仅仅在Web类型的Spring ApplicationContext中是可用的.|

1.5.1 单例作用域

容器内仅仅有一个共享的bean实例, 并且所有通过bean定义中的id或者id列表仅能匹配出唯一的特定bean的实例.

换个说法, 当你定义了一个bean,并且将其设置为singleton作用域时, Spring IoC容器创建了bean定义的唯一实例. 这个唯一实例是存储在此类单例bean的缓存中的, 并且所有子请求和引用都会返回缓存的bean. 下面的图展示了单例bean如何工作:

Spring单例bean的概念不同于GOF模式书中的单例模式. GoF单例硬编码了对象的作用域, 所以对于每个ClassLoader,有且仅有一个bean实例被创建. Spring的单例bean作用域对于每个容器每个bean来说有且仅有一个. 这意味着, 如果你在每个容器中定义一个指定的bean, 容器将为每个bean的定义生成一个且仅有一个bean的实例. 单例作用域是Spring默认的. 要用XML定义一个单例bean, 你可以参看下面定义:

<bean id="accountService" class="com.something.DefaultAccountService"/>

<!-- the following is equivalent, though redundant (singleton scope is the default) -->
<bean id="accountService" class="com.something.DefaultAccountService" scope="singleton"/>

1.5.2 原型作用域

非单例的原型作用域bean将在每次请求的时候创建一个新的实例. 也就是这个bean被注入另一个bean或者通过容器的getBean()方法调用获取. 原则上, 使用需要保持状态的bean时使用原型作用域, 使用状态无关的bean时使用单例bean.

下图说明了Spring的单例作用域:

(DAO对象不是典型的原型作用域, 因为一个DAO不保持任何会话状态. 我们重用单例的说明图是很容易的)

下面例子定义了XML格式的原型bean

<bean id="accountService" class="com.something.DefaultAccountService" scope="prototype"/>

同其他作用域相比, Spring不会管理原型bean的完整生命周期. 容器除了将其实例化,配置,另外还有组装和将其提供给client外, 不会有更多的原型实例的任何记录了. 因此, 虽然初始化回调方法由所有作用域的对象都会调用, 但在原型模式来说, 配置的销毁回调方法不会被调用. 客户端代码必须清理原型对象并释放被其占用的任何昂贵的资源. 为了使Spring容器能获取到原型bean占用的资源, 尝试使用自定义的post-处理器, 这个处理器维护这需要被清理的bean引用.

在某些方面, Spring容器对于原型bean的作用是对new操作符的代替. 过去所有的生命周期管理都是客户端来维护. (关于Spring中bean生命周期的更多信息, 参看:生命周期回调)

1.5.3 拥有原型bean依赖的单例bean

当使用有原型bean做为依赖的单例bean时, 记住依赖是在实例化的时候解析的. 因此,如果你将一个原型bean注入单例bean, 那这个原型bean是作为新的bean被初始化了, 并且注入到了单例bean中. 这个原型bean是单例bean的独占实例.

尽管如此, 假设你想要在运行期间为单例bean多次重复获取原型bean. 你不能讲原型bean注入到单例bean中, 因为注入只发生了一次, 就在容器初始化单例bean并解析他的依赖的时候. 如果你需要一个运行时的原型bean, 参看:方法注入.

1.5.4 Request,Session,Application, 还有webSocket

request,session,application,websocket作用域只有在web类型的Spring ApplicationContext(比如XmlWebApplicationContext)实现中可用.如果在标准的Spring容器中使用这些作用域, 比如ClassPathXmlApplicationContext, 则IllegalStateException将会因未知作用域而抛出.

初始化Web配置

为了支持bean的这几个作用域(request,session,application,websocket(web作用域bean)), 在定义bean时还需要做少量的配置. (对于标准的作用域singletonprototype,初始化设置是不需要的)

如何完成初始化设置取决于你特定的Servlet环境.

如果你使用Spring web MVC访问作用域bean, 实际上,请求是由Spring的DispatcherServlet处理的, 不需要其他的设置. DispatcherServlet已经暴露了所有的相关状态.

如果你使用Servlet 2.5 的Web容器, 当不使用DispatcherServlet处理时(比如使用JSF或Struts), 你需要注册org.springframework.web.context.request.RequestContextListener ServletRequestListener. 对于Servlet 3.0+, 可以通过接口WebApplicationInitializer编程完成. 或者作为替换,包括使用旧的容器的话, 在web.xml文件中添加如下声明:

<web-app>
    ...
    <listener>
        <listener-class>
            org.springframework.web.context.request.RequestContextListener
        </listener-class>
    </listener>
    ...
</web-app>

或者, 如果监听器设置有异常, 考虑使用Spring的RequestContextFilter. filter映射依赖于包含其的web程序配置, 所以你必须合理修改下. 下面列出web程序的部分配置:

<web-app>
    ...
    <filter>
        <filter-name>requestContextFilter</filter-name>
        <filter-class>org.springframework.web.filter.RequestContextFilter</filter-class>
    </filter>
    <filter-mapping>
        <filter-name>requestContextFilter</filter-name>
        <url-pattern>/*</url-pattern>
    </filter-mapping>
    ...
</web-app>

DispatcherServlet,RequestContextListener,还有RequestContextFilter都做了同样的事情, 即绑定Http请求对象到服务请求的Thread. 这使得bean在调用链中可以使用request-和session-作用域.

Request 作用域

参考如下XML配置bean:

<bean id="loginAction" class="com.something.LoginAction" scope="request"/>

Spring容器使用loginAction定义为每个HTTP请求创建bean LoginAction的实例. 也就是loginActionbean在HTTP请求级别作用域. 你可以随意修改实例的内部状态,因为被loginAction bean定义实例化出来的其他对象看不到这些修改. 他们是特定于单独的request的. 当请求完成处理后, request作用域的bean就被抛弃了.

当使用注解驱动的组件或java代码配置时, @RequestScope注解能用来分配给一个request作用域的组件. 下面例子展示了如何使用:

@RequestScope
@Component
public class LoginAction {
    // ...
}
Session 作用域

参看下面XML配置的bean定义:

<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>

Spring容器通过userPreferences bean的定义为每个HTTP Session生成UserPreferences bean的实例. 换句话说, userPreferences bean 在 HTTP Session 作用域级别. 跟request作用域bean一样, 你可以随意修改实例的内部状态, 而其他由同一个userPreferencesbean定义生成的 HTTP Session 的实例不会看到这些变化, 因为他们特定于单独的 HTTP Session. 当 HTTP Session 最终不再使用时, 对应的Session作用域的bean也就不再使用了.

当使用注解驱动组件或java代码配置时, 你可以使用@SessionScope注解到session作用域的相关组件上.

Application 作用域

考虑下面XML的bean定义:

<bean id="appPreferences" class="com.something.AppPreferences" scope="application"/>

Spring容器使用appPreferences bean的定义创建了AppPreferences bean的实例. 也就是appPreferences bean是在ServletContext级别的, 并且作为一个标准的ServletContext属性存储. 这类似于Spring单例bean, 但有两点重要的不同: 它是在每个Servlet上的单例, 不是在每个Spring'ApplcationContext'上的单例(可能在任何web程序中有多个),并且实际上它是暴露的并且因此是作为ServletContext属性可见的.

当使用注解驱动或者java代码配置时, 你可以使用@ApplicationScope注解到application组件上. 如下所示:

@ApplicationScope
@Component
public class AppPreferences {
    // ...
}
作用域bean作为依赖

Spring容器不仅管理对象(beans)的初始化, 也包含其组装协作者(依赖项). (举例来说)如果你想要将一个HTTP request作用域的bean注入到另一个更长生命周期的bean中, 你可以选择注入一个AOP代理对象代替bean. 也就是说, 你需要使用暴露了与作用域对象相同接口的代理对象注入, 这个代理对象能够从相关作用域中获取到真实对象(例如一个HTTP请求)并且委托调用真实对象上的方法.

下面例子中只有一行, 但理解其在背后的原因比怎么做更重要:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- an HTTP Session-scoped bean exposed as a proxy -->
    <bean id="userPreferences" class="com.something.UserPreferences" scope="session">
        <!-- instructs the container to proxy the surrounding bean -->
        <aop:scoped-proxy/> <!--这里定义了代理-->
    </bean>

    <!-- a singleton-scoped bean injected with a proxy to the above bean -->
    <bean id="userService" class="com.something.SimpleUserService">
        <!-- a reference to the proxied userPreferences bean -->
        <property name="userPreferences" ref="userPreferences"/>
    </bean>
</beans>

为了创建代理, 需要插入一个子元素<aop:scoped-proxy/>到作用域bean的定义中. (参看创建代理的类型选择和XML配置方式). 为什么定义在request,session或者自定义作用域上的bean需要<aop:scoped-proxy/>? 考虑下面单例bean的定义并对比上面表述的作用域(注意下面userPreferencesbean定义是不完整的):

<bean id="userPreferences" class="com.something.UserPreferences" scope="session"/>

<bean id="userManager" class="com.something.UserManager">
    <property name="userPreferences" ref="userPreferences"/>
</bean>

上面例子中, 单例bean(userManager)注入了HTTP Session-作用域的bean中(userPreferences). 这里的突出点是userManager是单例的: 它是每个容器唯一的, 并且它的依赖(在本例中是userPreferencesbean)仅仅注入了一次. 这意味着userManagerbean都是在相同的userPreferences对象上进行操作的(也就是最初被注入的那个).

这不是你想要的结果, 当将一个短生命周期的bean注入到长生命周期的bean中时(例如, 注入一个HTTP Session作用域的协作bean到单例bean中). 相反, 你需要一个单例的userManager对象, 并且对于HTTPsession生命周期, 你需要userPreferences对象并将其指定为HTTP Session. 因此, 容器创建了一个暴露了与UserPreferences类相同公共接口的对象.(理想情况下这个对象是UserPreferences实例), 这个对象能够从作用域机制中(HTTP request,session等)获取到真实的对象. 容器将代理对象注入到userManagerbean, 它并不知道UserPreferences引用是个代理. 本例中, 当UserManager实例调用注入对象UserPreferences的方法时, 实际上它是调用代理的方法. 代理然后获取从HTTP Session作用域(本例中)的真实UserPreferences对象, 并调用真实对象上的方法.

因此, 你需要下面的配置(完整而正确), 当你注入request-session-作用域的bean时, 如下:

<bean id="userPreferences" class="com.something.UserPreferences" scope="session">
    <aop:scoped-proxy/>
</bean>

<bean id="userManager" class="com.something.UserManager">
    <property name="userPreferences" ref="userPreferences"/>
</bean>
选择创建的代理类型

默认情况下, 当Spring容器用标记为<aop:scoped-proxy/>元素的bean创建代理时, 一个CGLIB类代理对象就被创建了.

或者, 你可以使用标准JDK基于接口的代理为作用域bean配置容器. 通过将元素<aop:scoped-proxy>的属性proxy-target-class设置为false. 使用JDK基于接口的代理意味着你不需要在classpath下引用多余的库. 尽管如此, 这也意味着作用域bean必须实现至少一个接口并且所有注入的的作用域bean必须通过接口引用. 下例展示了如何使用接口代理:

<!-- DefaultUserPreferences implements the UserPreferences interface -->
<bean id="userPreferences" class="com.stuff.DefaultUserPreferences" scope="session">
    <aop:scoped-proxy proxy-target-class="false"/>
</bean>

<bean id="userManager" class="com.stuff.UserManager">
    <property name="userPreferences" ref="userPreferences"/>
</bean>

关于选择基于类的还是基于接口的代理, 请参看: 代理机制.

1.5.5 自定义作用域

bean的作用域机制是可扩展的. 你可以自定义或者重新定义已经存在的作用域, 虽然稍后会知道这不是最佳实践而且你不能重写内置的singletonprototype作用域.

创建自定义作用域

为了集成自定义的作用域到容器中, 你需要实现接口org.springframework.beans.factory.config.Scope, 这个接口将会在本节描述. 对于如何实现自定义作用域的观念, 参看Spring框架实现的Scope实现还有Scopejava文档, 里面解释了更多需要实现的方法.

Scope接口有四个方法, 用来从作用域获取,移除,销毁对象.

以session作用域的实现为例, 返回session作用域bean(如果不存在,方法在这个实例将在绑定到session后以备将来引用, 然后返回一个新实例).下面的方法从潜在作用域返回对象:

Object get(String name, ObjectFactory objectFactory)

以session作用域的实现为例, 从潜在的session作用域删除bean. 对象应该被返回, 但如果指定名称的bean找不到, 你可以返回null. 下面的方法将从潜在作用域删除对象:

Object remove(String name)

下面的方法为作用域注册了回调, 将在它被销毁或者当作用域内的指定对象销毁时执行:

void registerDestructionCallback(String name, Runnable destructionCallback)

关于销毁回调, 可以参看java文档或者Spring作用域实现的代码.

下面方法包含了从潜在作用域获取会话id:

String getConversationId()

这个标识符在每个作用域都不同, 对于session作用域实现, 这个标识符可以是session标识符.

使用自定义作用域

在你编码并测试一个或多个自定义作用域实现后, 你需要使Spring容器能够感知到你的作用域. 下面方法是为Spring容器注册一个新作用域的核心方法:

void registerScope(String scopeName, Scope scope);

这个方法是在接口ConfigurableBeanFactory声明的, 在Spring的大多数对于接口ApplicationContext的实现中, 通过BeanFactory属性可以获取到.

registerScope(..)方法的第一个参数是相关作用域的唯一名称. Spring内置的此类名称是singletonprototype. 方法的第二个参数是自定义Scope实现的实例, 这个实例就是你想注册和使用的.

假设你写好了自定义的Scope实现, 并且像下一个例子一样注册到了容器中.

Scope threadScope = new SimpleThreadScope();
beanFactory.registerScope("thread", threadScope);

你可以创建bean的定义, 并将自定义的scope实现依附其中. 如下:

<bean id="..." class="..." scope="thread">

自定义的Scope实现不仅限于使用编程方式注册, 你也可以使用声明方式注册, 通过CustomScopeConfigurer类进行注册, 如下所示:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop
        http://www.springframework.org/schema/aop/spring-aop.xsd">

    <bean class="org.springframework.beans.factory.config.CustomScopeConfigurer">
        <property name="scopes">
            <map>
                <entry key="thread">
                    <bean class="org.springframework.context.support.SimpleThreadScope"/>
                </entry>
            </map>
        </property>
    </bean>

    <bean id="thing2" class="x.y.Thing2" scope="thread">
        <property name="name" value="Rick"/>
        <aop:scoped-proxy/>
    </bean>

    <bean id="thing1" class="x.y.Thing1">
        <property name="thing2" ref="thing2"/>
    </bean>

</beans>

1.6 自定义Bean的特性

Spring框架提供了一些接口供你自定义bean的特性. 本节按下面方式组织:

  • 生命周期回调
  • ApplicationContextAwareBeanNameAware
  • 其他Aware接口

1.6.1 生命周期回调

为了与容器管理的bean的生命周期交互, 你可以实现Spring InitializingBeanDisposableBean 接口. 容器会为前者调用afterPropertiesSet(),为后者调用destroy() 以使bean执行在bean初始化和销毁时应该做的操作.

在内部, Spring框架使用接口BeanPostProcessor的实现来处理他发现的任何回调接口并且调用合适的方法. 如果你需要自定义特性或者修改Spring没有默认提供的其他生命周期行为, 你可以实现BeanPostPocessor. 更多信息请参看:容器扩展点.

除了初始化和销毁回调外, Spring管理的对象也可以实现Lifecycle接口, 以便那些对象能参与容器自身生命周期驱动的启动和停止过程.

生命周期回调接口将在本节描述.

初始化回调

org.springframework.beans.factory.InitializingBean 接口能够在容器设置了bean的所有必要属性后执行初始化工作. InitializingBean 接口只有一个方法:

void afterPropertiesSet() throws Exception;

我们强烈建议不要使用InitializingBean接口,因为它不必要地耦合到了Spring中. 相反, 我们建议使用@PostConstruct 注解或者指定一个POJO 初始化方法. 如果使用XML配置, 你可以使用init-method属性指定一个没有参数的方法名称. Java代码配置的话, 你可以使用@Bean上的 initMethod 属性. 参看:获取生命周期回调. 参考下例:

<bean id="exampleInitBean" class="examples.ExampleBean" init-method="init"/>
public class ExampleBean {

    public void init() {
        // do some initialization work
    }
}

上面的例子和下面的例子(由两个清单组成)有相同的效果:

<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
public class AnotherExampleBean implements InitializingBean {

    public void afterPropertiesSet() {
        // do some initialization work
    }
}

这样,上面的头两个例子代码没有与Spring耦合.

销毁回调

实现org.springframework.beans.factory.DisposableBean接口使得一个bean能在包含它的容器销毁时回调. DisposableBean接口只有一个方法:

void destroy() throws Exception;

我们建议你不要使用DisposableBean回调接口, 因为代码不必要的耦合到Spring了. 相反, 我们建议使用@PreDestroy注解, 或者在bean定义中指定一个普通方法. 使用XML配置的话你可以使用元素<bean/>上的destroy-method属性. 使用java代码配置的话, 你可以使用@Bean注解上的destroyMethod属性. 参看获取生命周期回调. 示例如下:

<bean id="exampleInitBean" class="examples.ExampleBean" destroy-method="cleanup"/>
public class ExampleBean {

    public void cleanup() {
        // do some destruction work (like releasing pooled connections)
    }
}

上面的定义等同于下面的定义:

<bean id="exampleInitBean" class="examples.AnotherExampleBean"/>
public class AnotherExampleBean implements DisposableBean {

    public void destroy() {
        // do some destruction work (like releasing pooled connections)
    }
}

上面的两个定义与Spring代码没有耦合.

默认初始化和销毁方法

当你不用Spring特定的InitializingBeanDisposableBean 接口回调的初始化和销毁方法时, 你一般编写方法的名称类似init(),initialize(),dispose()等. 理论上, 这些生命周期回调方法是标准化的贯穿于整个项目, 这样开发人员就能使用相同的方法并保持一惯性.

你可以配置Spring容器查找每个bean上的命名初始化和销毁方法. 这意味着你作为开发人员能编写程序类并使用名为init()的初始化方法回调, 而并不需要为每个bean定义上指定init-method="init". Spring容器在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.");
        }
    }
}

你可以像下面这样使用那个类:

<beans default-init-method="init">

    <bean id="blogService" class="com.something.DefaultBlogService">
        <property name="blogDao" ref="blogDao" />
    </bean>

</beans>

顶级元素<beans/>上的属性default-init-method促使Spring容器去识别在bean上的init()方法作为初始化回调函数. 当bean被创建和组装后, 如果bean有这么个方法, 它就将在恰当的时候执行.

你也可以在顶级元素<beans/>上使用default-destroy-method, 类似地去配置(XML)销毁回调函数.

在已经存在的bean上, 已经有按约定命名的回调方法, 你可以使用<bean/>元素上指定init-methoddestroy-method重写默认的方法.

Spring容器担保在bean的依赖全部被提供后能够立即调用配置的初始化回调. 因此,初始化回调发生在原始bean引用上, 这意味着AOP注入还没有应用到bean上. 一个完整的目标bean是首先被创建然后一个AOP代理注入链被引用. 如果目标bean和代理被单独定义, 你的代码就能够绕开代理与目标bean交互. 因此, 在init方法上应用注入是不合逻辑的, 因为这样将耦合目标bean的生命周期到它的代理或注入器, 并且当你的代码与原始的目标bean直接交互时留下奇怪的语义.

组合生命周期机制

从Spring2.5开始, 控制bean生命周期行为有三种选择:

  • 回调接口 InitializingBeanDisposableBean
  • 自定义的init()destroy() 方法
  • 注解 @PostConstruct@PreDestroy. 你可以组合这些机制去控制bean.

为同一个bean配置多个生命周期机制, 初始化方法将按下面顺序执行:

  • 使用注解@PostConstruct的方法
  • 通过回调接口InitializingBean定义的afterPropertiesSet()方法
  • 自定义的init()方法

销毁方法按相同的顺序执行:

  • 使用注解@PreDestroy的方法
  • 通过回调接口DisposableBean定义的destroy()
  • 自定义的destroy()
启动和停止回调

对于拥有自己生命周期需要的任何对象, 接口Lifecycle定义了必不可少的方法(例如启动和停止某些后台进程).

public interface Lifecycle {

    void start();

    void stop();

    boolean isRunning();
}

任何Spring管理的对象都可以实现接口Lifecycle. 然后, 当ApplicationContext自身收到开始和停止信号时(例如,运行时的停止/重启场景), 它将级联调用上下文定义的所有Lifecycle实现. 这是通过委托给LifecycleProcessor完成的, 如下所示:

public interface LifecycleProcessor extends Lifecycle {

    void onRefresh();

    void onClose();
}

注意, LifecycleProcessor本身是接口Lifecycle的扩展. 它添加了两个方法在上下文刷新和关闭时交互.

启动和停止调用的顺序可能是比较重要的. 如果两个对象间存在依赖关系, 依赖方将在被依赖方之后启动, 并且在被依赖方之后停止. 不过有时候直接依赖是不知道的. 你可能仅仅知道一些类型的对象的优先于另一些类型的对象启动. 这种情况下, SmartLIfecycle接口定义了另一种选项, 在其父类接口Phased上指定getPhase()方法. 下面的代码展示了Phased接口:

public interface Phased {

    int getPhase();
}

下面展示了SmartLifecycle接口:

public interface SmartLifecycle extends Lifecycle, Phased {

    boolean isAutoStartup();

    void stop(Runnable callback);
}

当启动时, 底层阶段的对象优先启动. 当停止时, 顺序相反. 因此, 一个实现了接口SmartLifecycle并且其方法getPhase()返回Integer.MIN_VALUE将会在第一个启动的, 并是最后一个停止的. 在此范围的另一端,也就是Integer.MAX_VALUE将表明对象是最后启动的并且是最先停止的(可能因为它依赖于要运行的其他进程). 当提到阶段值的时候, 重要的一点是任何"正常"Lifecycle对象如果没有实现SmartLifecycle接口的话,这个阶段值的默认为0. 因此, 任何负值的对象将在那些标准组件之前启动(在其后停止). 如果正值则相反.

通过SmartLifecycle接收一个停止方法. 任何实现必须在对象的停止过程完成后调用回调的run()方法. 这就使程序拥有了异步停止能力, 因为接口LifecycleProcessor的默认实现DefaultLifecycleProcessor在每个阶段中等待该组对象的超时值以调用该回调. 每个阶段的超时时间是30秒. 你可以通过定义一个名为lifecycleProcessor的bean来重写默认的生命周期处理器. 如果你想仅仅是修改超时时间, 定义如下:

<bean id="lifecycleProcessor" class="org.springframework.context.support.DefaultLifecycleProcessor">
    <!-- timeout value in milliseconds -->
    <property name="timeoutPerShutdownPhase" value="10000"/>
</bean>

就像前面提到的, LifecycleProcessor接口也定义了上下文刷新和关闭的回调方法. 后者驱动关闭过程,就好像stop()方法被显式调用, 但它发生在上下文关闭时. 另一方面, 刷新回调赋予了SmartLifecyclebean的另一种特性. 当上下文被刷新(在所有对象创建和初始化后), 回调就被调用了. 此时, 默认的生命周期处理器检查每个SmartLifecycle对象的isAutoStartup()方法返回的bool值. 如果是true, 对象将立刻启动而不是等到上下文或者其自身的start()方法显式调用(与上下文刷新不同,上下文启动并不是标准上下文实现的标准实现). phase值和任何依赖关系决定了前面描述的启动顺序.

在非WEB程序中优雅地停止Spring容器

如果你在一个非web程序中(例如, 一个胖客户端桌面环境)使用Spring容器, 注册一个JVM的停止钩子. 这样做可以保证你优雅地关闭, 并调用单例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("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...
    }
}

1.6.2 ApplicationContextAwareBeanNameAware

ApplicationContext创建了一个实现了org.springframework.context.ApplicationContextAware接口的对象实例时, 这个实例就提供了对ApplicationContext的引用. 下面代码是接口ApplicationContextAware的定义:

public interface ApplicationContextAware {

    void setApplicationContext(ApplicationContext applicationContext) throws BeansException;
}

因此, bean能够通过编程方式操作创建了他们的ApplicationContext, 通过ApplicationContext接口或通过能转化为这个接口子类型的对象(例如: ConfigurableApplicationContext,这个接口暴露了更多的功能). 其中一个用途是对其他bean进行编程检索. 有时候这种能力是有用的. 但总体上你应该避免使用, 因为它会耦合你的代码到Spring并且不能遵循IoC风格, 在那里协作bean作为bean的属性. ApplicationContext的其他方法提供了访问文件,发布程序事件,还有访问MessageSource等功能. 这些附加特性参见ApplicationContext的附加功能.

自从Spring2.5开始, 自动装配是另一种能获取ApplicationContext引用的替代方式. "传统的"constructorbyType装配模型(如同在装配协作者一节描述的)能分别为构造器参数或setter方法参数提供ApplicationContext类型依赖. 更多的灵活性, 包括自动装配字段和多参数方法, 使用新的基于注解的装配特性. 如果你这么做了, 带有@Autowired注解的域, 构造函数或方法, 那ApplicationContext就自动装配到一个期望ApplicationContext类型的属性域,构造函数参数, 或者方法参数中去了. 更多信息请参见使用@Autowired.

ApplicationContext创建了实现org.springframework.beans.factory.BeanNameAware接口的类实例后, 这个类就拥有了在定义时为其指定的名字引用. 下面代码展示了接口BeanNameAware的定义:

public interface BeanNameAware {

    void setBeanName(String name) throws BeansException;
}

回调在bean的属性被布局后但是在InitializingBean, afterPropertiesSet或自定义初始化方法之前被调用.

1.6.3 其他Aware接口

除了(之前讨论的)ApplicationContextAwareBeanNameAware外, Spring提供了一系列Aware接口以使bean向容器表明他们需要特定的基础设施依赖. 作为普遍法则, 这个名称表明了依赖类型. 下面表格简要列出最重要的Aware接口:

Table 4 Aware接口

请再次注意, 使用这些接口将使你的代码耦合到Spring API, 并且也不遵循IoC风格. 因此我们建议他们当需要访问容器时使用基础设施bean.

1.7 Bean定义继承

bean的定义包含很多配置信息, 包含构造函数参数, 属性值, 还有特定于容器的信息, 例如初始化方法,静态工厂方法等. 一个bean的子类定义继承了父定义中的配置数据. 子类定义可以覆盖或者按需添加数据. 使用父子定义可以节省很多编码. 事实上, 这是一种模型.

如果你使用编程方式使用ApplicationContext接口, 子类bean的定义是通过ChildBeanDefinition类来表现的. 许多用户不在这个层面使用它. 相反, 他们通过在诸如ClassPathXmlApplicationContext的类中生命配置bean. 当你使用基于XML的配置元数据时, 你可以通过parent属性指明子类bean定义, 其值是父bean的定义名称. 下例展示了如何这么做:

<bean id="inheritedTestBean" abstract="true"
        class="org.springframework.beans.TestBean">
    <property name="name" value="parent"/>
    <property name="age" value="1"/>
</bean>

<!--注意`parent`属性-->
<bean id="inheritsWithDifferentClass"
        class="org.springframework.beans.DerivedTestBean"
        parent="inheritedTestBean" init-method="initialize">
    <property name="name" value="override"/>
    <!-- the age property value of 1 will be inherited from parent -->
</bean>

如果没有指定,子类bean定义将使用父类定义的bean类,但依然可以覆盖它. 后一种情况下, 子类必须兼容父类(也就是必须接受父类的属性值).

子类继承了父bean的作用域,构造函数参数值, 属性值还有方法重写, 同时可以添加新的值给它. 任何你指定的作用域,初始化方法,销毁方法或者static工厂方法配置都将覆盖掉父类的配置.

剩下的配置总是从子类定义获取: 依赖,装配模式,依赖检查,单例, 懒加载.

上面例子中父类定义使用abstract属性显式标记父bean是抽象的. 如果父bean不指定类型, 显式标记父bean为abstract是必须的, 如下所示:

<bean id="inheritedTestBeanWithoutClass" abstract="true">
    <property name="name" value="parent"/>
    <property name="age" value="1"/>
</bean>

<bean id="inheritsWithClass" class="org.springframework.beans.DerivedTestBean"
        parent="inheritedTestBeanWithoutClass" init-method="initialize">
    <property name="name" value="override"/>
    <!-- age will inherit the value of 1 from the parent bean definition-->
</bean>

父bean不能被实例化自己, 因为它不完整, 且显式标记为abstract了. 当一个定义是abstract的时, 它仅仅是为子定义提供的一个纯净的模板bean定义. 视图尝试单独使用这个抽象bean, 或者将它引用为另一个bean的ref属性, 或者使用父bean的id执行getBean()方法将返回错误. 相似的, 容器内部的preInstantiateSingletons()方法将忽略定义为抽象的bean.

1.8 容器扩展点

通常, 攻城狮不需要ApplicationContext的子类实现. 相反, Spring容器可以通过实现特定接口的方式扩展. 以下几节描述了这些接口.

1.8.1 使用BeanPostProcessor自定义bean

BeanPostProcessor接口定义了一些回调方法, 你可以实现提供你自己的(或者覆盖默认的)初始化逻辑, 依赖解析逻辑等. 如果你想要在容器完成初始化, 配置和实例化bean之后实现一些自己的逻辑, 你可以插入一个或多个BeanPostProcessor的实现.

你可以配置多个BeanPostProcessor实例, 并能够通过order属性的值来控制这些实例执行的顺序.如果BeanPostProcessor实现了Ordered接口, 你就可以设置这个属性了. 如果你写了自己的BeanPostProcessor, 你就也应该考虑实现Ordered接口. 更多细节可参看javadoc关于BeanPostProcessorOrdered接口的部分. 也可以参看: 编程注册BeanPostProcessor的实例.

org.springframework.beans.factory.config.BeanPostProcessor接口包含两个回调方法. 当一个post-processor的bean被注册到容器后, 对于每个本容器实例化出的bean, post-processor都将从容器获取到回调方法, 是在容器初始化方法之前(例如InitializingBean.afterPropertiesSet()或者任何声明为init的方法)将被调用, 且在bean实例化回调方法之后. post-processor可以做任何事情, 包括完全忽略回调方法. 一个bean post-processor通常检查回调接口, 或者使用代理来包装bean. 一些Spring AOP 基础设施类就是实现为post-processor, 为了提供代理包装逻辑.

ApplicationContext自动探测在配置元数据中任何实现了BeanPostProcessor接口的bean. ApplicationContext将作为post-processor注册了这些bean, 以便晚些时候在bean创建的时候调用. bean post-processor能够以和其他bean相同的方式部署到容器中.

注意当在配置类上使用@Bean注解的工厂方法声明BeanPostProcessor时, 工厂方法的返回类型必须是实现类型本身, 或者至少是接口org.springframework.beans.factory.config.BeanPostProcessor, 明确表示bean的post-processor属性. 否则, ApplicationContext不能在完全创建它时通过类型自动探测. 因为BeanPostProcessor需要被早点实例化, 从而被用于其他bean的初始化过程中, 所以这种早期探测是至关重要的.

下面的例子展示了在ApplicationContext中如何编码, 注册和使用BeanPostProcessor.

示例: BeanPostProcessor-风格的Hello World程序

第一个例子是基本用法. 展示了一个自定义的BeanPostProcessor实现调用每个容器创建的bean的toString()方法并将结果打印到控制台.

下面是代码:

package scripting;

import org.springframework.beans.factory.config.BeanPostProcessor;

public class InstantiationTracingBeanPostProcessor implements BeanPostProcessor {

    // simply return the instantiated bean as-is
    public Object postProcessBeforeInitialization(Object bean, String beanName) {
        return bean; // we could potentially return any object reference here...
    }

    public Object postProcessAfterInitialization(Object bean, String beanName) {
        System.out.println("Bean '" + beanName + "' created : " + bean.toString());
        return bean;
    }
}

下面beans元素使用了InstantiationTracingBeanPostProcessor:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:lang="http://www.springframework.org/schema/lang"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/lang
        http://www.springframework.org/schema/lang/spring-lang.xsd">

    <lang:groovy id="messenger"
            script-source="classpath:org/springframework/scripting/groovy/Messenger.groovy">
        <lang:property name="message" value="Fiona Apple Is Just So Dreamy."/>
    </lang:groovy>

    <!--
    when the above bean (messenger) is instantiated, this custom
    BeanPostProcessor implementation will output the fact to the system console
    -->
    <bean class="scripting.InstantiationTracingBeanPostProcessor"/>

</beans>

注意InstantiationTracingBeanPostProcessor仅仅是声明了. 它没有名字, 并且应该它是个bean, 它能够依赖注入给任何其他bean. (上面配置定义了个bean, 是使用Groovy脚本的形式,Spring动态语言支持的细节请看:动态语言支持)

下面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
示例: RequiredAnnotationBeanPostProcessor

使用回调接口或注解与自定义BeanPostProcessor实现协力是扩展Spring容器的常见做法. 一个例子是Spring的RequiredAnnotationBeanPostProcessor, 一个BeanPostProcessor的实现, 它是随Spring发行附带的, 能确保标记为任意注释的beans上的javabean属性是实际上配置为依赖注入的一个值.

1.8.2 使用BeanFactoryPostProcessor自定义配置元数据

下一个扩展点我们看一下org.springframework.beans.factory.config.BeanFactoryPostProcessor. 这个接口的语法类似于其他的BeanPostProcessor, 但有个主要不同: BeanFactoryPostProcessor操作bean的元数据. 也就是, Spring容器使用BeanFactoryPostProcessor读取配置元数据并可能在容器实例化任何除BeanFactoryPostProcessor之外的bean之前改变它.

你可以配置多个BeanFactoryPostProcessor实例, 并可以通过order属性控制这些实例的执行顺序. 虽然,你仅仅需要设置实现了Ordered接口的BeanFactoryPostProcessor的这个属性. 如果你编写了自己的BeanFactoryPostProcessor, 你就应该考虑实现Ordered接口. 更多细节参看:BeanFactoryPostProcessorOrdered的java文档.

一个bean工厂的post-processor, 当它在ApplicationContext中声明时自动执行, 为了给定义在容器中的元数据做出修改. Spring包含很多预定义的工厂post-processor, 例如PropertyOverrideConfigurerPropertySourcesPlaceholderConfigurer. 你也可以使用自定义的BeanFactoryPostProcessor--例如, 注册自动以的属性编辑器.

ApplicationContext自动检测那些部署到其中的, 实现接口BeanFactoryPostProcessor的bean. 在恰当的时候, 它使用这些bean为bean工厂的post-processor. 你可以象其他bean一样部署这些post-processor bean.

例子: 类的名字替换PropertySourcesPlaceholderConfigurer

通过使用标准的java Properties 格式, 你可以使用PropertySourcesPlaceholderConfigurer来扩展独立在文件中的bean定义中的属性值. 这么做能使人们部署程序去自定义的特定环境的属性, 例如数据库URLs和密码, 而不需要复杂的有风险的去修改主要的XML定义文件.

请看下面的XML格式的配置元数据片段, 里面有个使用占位符的DataSource值被定义:

<bean class="org.springframework.context.support.PropertySourcesPlaceholderConfigurer">
    <property name="locations" value="classpath:com/something/jdbc.properties"/>
</bean>

<bean id="dataSource" destroy-method="close"
        class="org.apache.commons.dbcp.BasicDataSource">
    <property name="driverClassName" value="${jdbc.driverClassName}"/>
    <property name="url" value="${jdbc.url}"/>
    <property name="username" value="${jdbc.username}"/>
    <property name="password" value="${jdbc.password}"/>
</bean>

示例展示了从一个外部Properties文件中配置的属性. 在运行时, PropertySourcesPlaceholderConfigurer被用来替换数据源中的一些属性值. 需要替换的属性是以一种${properties-name}的格式指定, 遵循Ant和log4j以及JSP EL风格.

真正的值来自另一个标准的Properties文件:

jdbc.driverClassName=org.hsqldb.jdbcDriver
jdbc.url=jdbc:hsqldb:hsql://production:9002
jdbc.username=sa
jdbc.password=root

因此, 字符串${jdbc.username}在运行时被'sa'替换, 并且其他的与键匹配的占位符值都给替换了. PropertySourcesPlaceholderConfigurer检查properties占位符和bean的定义属性. 而且, 你可以自定义占位符前缀和后缀.

随着Spring2.5版本的context命名空间, 你可以使用专门的配置元素去配置属性占位符. 你可以为location属性提供一个或多个用逗号分隔的值列表. 如下:

<context:property-placeholder location="classpath:com/something/jdbc.properties"/>

PropertySourcesPlaceholderConfigurer不仅查找指定的properties文件. 默认情况下, 如果它不能从指定的属性文件中找到, 它会继续查找SpringEnvironment属性和标准的System属性:

<bean class="org.springframework.beans.factory.config.PropertySourcesPlaceholderConfigurer">
    <property name="locations">
        <value>classpath:com/something/strategy.properties</value>
    </property>
    <property name="properties">
        <value>custom.strategy.class=com.something.DefaultStrategy</value>
    </property>
</bean>

<bean id="serviceStrategy" class="${custom.strategy.class}"/>
示例: PropertyOverrideConfigurer

PropertyOverrideConfigurer是另一个bean工厂post-processor, 类似于PropertySourcesPlaceholderConfigurer, 但又不同于后者, 对于bean的properties, 最初的定义可以没有默认值或者压根就没值. 如果一个覆盖的Properties文件没有bean的properties合适的入口, 那么默认的上下文定义将被使用.

注意, bean的定义并不知道被覆盖, 所以覆盖配置被使用的xml定义文件并不是立即就可见. 万一有多个PropertyOverrideConfigurer实例为同一属性定义了多个, 那依据覆盖机制最后一个将获胜.

properties文件配置的行格式如下:

beanName.property=value

下面是这种格式的示例:

dataSource.driverClassName=com.mysql.jdbc.Driver
dataSource.url=jdbc:mysql:mydb

示例文件能被定义了一个名字为dataSource的bean使用, 其中包含了driverurl属性.

组合属性名称也是受支持的, 只要路径path的每个被复写的部分都是非空, 最终属性除外(假设是使用构造器初始化的). 在接下来的例子中, beantomfred属性的bob属性的sammy属性被设置为123.

tom.fred.bob.sammy=123

自从Spring 2.5的context命名空间开始, 就可以使用一个明确的配置元素去配置属性了. 如下所示:

<context:property-override location="classpath:override.properties"/>

1.8.3 使用FactoryBean自定义实例化逻辑

你可以为本身是工厂的类实现接口org.springframework.beans.factory.FactoryBean.

FactoryBean接口是可插入Spring容器实例化逻辑的一个点. 如果你有比较复杂的代码, 这种代码使用java比使用一对xml文件更好的表达, 你就可以自己去创建自己的FactoryBean, 将复杂的实例化代码写进这个类, 并将这个类插入到容器中.

FactoryBean接口提供了额三个方法:

  • Object getObject(): 返回工厂创建的实例对象. 这个实例可能是共享的, 取决于工厂返回的是单例还是原型.

  • boolean isSingleton(): 如果这个FactoryBean返回单例就是true,否则是false.

  • Class getObjectType(): 返回对象的类型, 这个对象是getObject()方法返回的. 或者返回null, 如果类型不能预知.

FactoryBean的观念和接口在Spring框架中多处使用. Spring自身提供的就总有50多个实现.

当你需要问容器请求接口FactoryBean的真实实例本身, 而不是它生产的bean时, 在调用ApplicationContext的方法getBean()方法将返回在bean的id前面冠以&. 所以, 对于一个id为myBeanFactoryBean, 调用getBean("myBean")将返回FactoryBean的产品, 而调用getBean("&myBean")返回FactoryBean实例本身.

1.9 基于注解的容器配置

一种替换XML配置的方式是基于注解的配置, 后者依赖于二进制字节码原数据装配组件而不是使用尖括号的声明. 不同于XML描述装配, 开发人员将配置转移到组件类内部,通过注解在类上, 方法上或域字段上. 就像在示例:RequiredAnnotationBeanPostProcessor中一样, 使用BeanPostProcessor与注解协作是一种扩展Spring 容器的普遍方法. 例如, Spring2.0赋予了使用@Required注解强制必填属性的能力. Spring2.5使得使用相同的一般模式来驱动Spring DI 成为可能. 本质上, @Autowired注解提供了在自动装配一节的描述相同的能力, 但拥有更细粒度的控制和更广的适用性. Spring2.5同样添加了JSR-250注解的支持,例如@PostConstruct@PreDestroy. Spring 3.0 添加了对JSR-330(java的依赖注入)注解支持, 包含在javax.inject包下如@Inject@Named. 这些注解的细节可以在相关章节找到.

通常, 你可以像分离的bean定义一样注册他们, 但你也可以隐式的通过包含在XML配置中的下面标签来注册. (注意将context命名空间包含进去):

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>

</beans>

(隐式注册的post-processor包含AutowiredAnnotationBeanPostProcessor, CommonAnnotationBeanPostProcessor, PersistenceAnnotationBeanPostProcessor, 还有前面提到的RequiredAnnotationBeanPostProcessor.)

1.9.1 @Required

@Required注解引用在bean的setter方法上, 如下:

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Required
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // ...
}

这个注解表明, 受影响的bean的属性必须在配置时就确定, 不管是通过bean定义中的显式的属性值还是自动装配. 如果没有找到值,容器将抛出异常. 这允许早期和显式失败, 避免了NullPointerException等诸如此类的实例. 我们依然建议你将断言放置在bean类内部(例如, 放在初始化方法里面). 这样就强制了必填属性的引用和值, 就算你在容器外使用类也是如此.

1.9.2 使用@Autowired

你可以将@Autowired注解应用到构造函数, 如下所示:

public class MovieRecommender {

    private final CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
        this.customerPreferenceDao = customerPreferenceDao;
    }

    // ...
}

你也可以将@Autowired注解应用到传统的setter方法上, 如下:

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

    // ...
}

也可以将@Autowired应用到域并且可以和构造方法混用, 如下

public class MovieRecommender {

    private final CustomerPreferenceDao customerPreferenceDao;

    @Autowired
    private MovieCatalog movieCatalog;

    @Autowired
    public MovieRecommender(CustomerPreferenceDao customerPreferenceDao) {
        this.customerPreferenceDao = customerPreferenceDao;
    }

    // ...
}

你也可以为需要一个数组的域或方法提供一组特定类型的bean, 这些bean从ApplicationContext中获取. 如下:

public class MovieRecommender {

    @Autowired
    private MovieCatalog[] movieCatalogs;

    // ...
}

同样可以引用于集合类, 如下:

public class MovieRecommender {

    private Set<MovieCatalog> movieCatalogs;

    @Autowired
    public void setMovieCatalogs(Set<MovieCatalog> movieCatalogs) {
        this.movieCatalogs = movieCatalogs;
    }

    // ...
}

甚至类型化的Map实例也是可以自动包装的, 只要期待的key值是String类型. Map的值包含了所有期望类型的实例, 而key值包含了相关bean的名称, 如下所示:

public class MovieRecommender {

    private Map<String, MovieCatalog> movieCatalogs;

    @Autowired
    public void setMovieCatalogs(Map<String, MovieCatalog> movieCatalogs) {
        this.movieCatalogs = movieCatalogs;
    }

    // ...
}

默认情况下, 如果没有候选bean, 自动装配将会失败(也就是默认必填). 默认的行为是将注解方法, 构造函数和域看做所需的依赖. 你也可以像如下所示的那样去改变其行为:

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Autowired(required = false)
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }

    // ...
}

作为替换, 你可以使用java8的java.util.Optional来表达一个非必填属性依赖. 如下所示:

public class SimpleMovieLister {

    @Autowired
    public void setMovieFinder(Optional<MovieFinder> movieFinder) {
        ...
    }
}

从Spring 5.0 开始, 你也可以使用@Nullable注解了(任何包中任何类型的注释, 例如JSR-305的:javax.annotation.Nullable) :

public class SimpleMovieLister {

    @Autowired
    public void setMovieFinder(@Nullable MovieFinder movieFinder) {
        ...
    }
}

您也可以为众所周知的依赖项接口使用@Autowired: BeanFactory, ApplicationContext, Environment, ResourceLoader, ApplicationEventPublisher,还有MessageSource. 这些接口和他们的扩展接口, 例如:ConfigurableApplicationContext或者ResourcePatternResolver, 会被自动解析, 不需要特殊的设置步骤. 下面例子展示了自动装配一个ApplicationContext对象:

public class MovieRecommender {

    @Autowired
    private ApplicationContext context;

    public MovieRecommender() {
    }

    // ...
}

1.9.3 使用@Primary注释进行微调

因为按类型的自动装配可能会有多个候选, 所以有必要在选择过程中有更多控制. 一种达成此目标的方式是使用@Primary注解. @Primary表明当多个bean候选都符合单例依赖时, 这个特定的bean应当被自动装配给引用. 如果确实有一个主要的bean, 那它就成为了自动装配的值.

参看下面的配置, 定义了firstMovieCatalog作为主要的MovieCatalog:

@Configuration
public class MovieConfiguration {

    @Bean
    @Primary
    public MovieCatalog firstMovieCatalog() { ... }

    @Bean
    public MovieCatalog secondMovieCatalog() { ... }

    // ...
}

使用上面的配置, 下面的MovieRecommender被自动装配了firstMovieCatalog:

public class MovieRecommender {

    @Autowired
    private MovieCatalog movieCatalog;

    // ...
}

相关的bean定义如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>

    <bean class="example.SimpleMovieCatalog" primary="true">
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean id="movieRecommender" class="example.MovieRecommender"/>

</beans>

1.9.4 使用Qualifiers微调

当使用按类型自动封装时, 如果有几个不同的实例而只有一个主要的实例被确定, @Primary是一种有效的方法.当你需要在选择过程中有更多控制时, 可以使用Spring的@Qualifier注解. 你可以使用它的几个相关参数来缩小类型匹配范围, 这样对于任一参数选择其特定的bean. 在最简单的示例中, 它可以是一个普通的描述值, 如下:

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定义:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>

    <bean class="example.SimpleMovieCatalog">
        <qualifier value="main"/> <!--用main指定的值被具有相同名称的构造函数参数自动装配-->

        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <qualifier value="action"/> <!--用action指定的值被具有相同名称的构造函数参数自动装配-->

        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean id="movieRecommender" class="example.MovieRecommender"/>

</beans>

在类型匹配的候选时, 让限定值依赖bean名称选择, 不需要在注入点添加@Qualifier注解. 如果没有其他解析指令(如限定符或primary标记), 对于非单一依赖的情况, Spring通过目标bean的名称并选择相同名称的候选来匹配注入点名称(也就是,域名称或参数名称).

也就是说, 如果你想通过名称来表达注解驱动的注入, 就不需要使用@Autowired, 就算他能在类型匹配的候选中进行选择. 相反, 使用JSR-250 的 @Resource 注解, 这个注解语义上定义了特定目标bean的唯一名称, 声明的类型和匹配过程无关. @Autowired有不同的语义: 在通过类型选择bean后, 特定的String限定的值被认为仅仅是侯选中的(例如, 匹配account限定将依赖于相同的限定标签).

对于本身定义为集合的bean, 如Map或数组类型, @Resource是好的解决方法, 通过名字引用特定的集合或数组bean. 也就是说, 自从4.3版本开始, 你也可以使用@Autowired类型匹配算法来匹配Map和数组类型, 只要元素类型信息保存在@Bean的返回签名类型或集合继承中. 在这种情况下, 如上一段所述, 你可以使用限定值在相同类型中进行选择.

从4.3开始, 对于注入, @Autowired也被认为是自引用的(也就是引用返回到当前被注入的bean上). 注意,自我注入是个回退. 一般的对其他组件的依赖往往具有优先权. 从这个意义上说, 自我引用不在一般候选范围因此从来不是主要的. 相反,他们一般是最后的选择. 实际上你应该使用自我引用作为最后的手段(例如通过bean的事务代理调用同一实例的其他方法). 在这种场景下, 考虑重构受影响的方法到单独的代理bean中. 想法, 你可以使用@Resource, 这可以通过其唯一名称获取到当前bean的代理.

@Autowired被用到域,构造函数,还有多个参数的方法上, 允许通过限定注解在参数级别缩小范围. 相比之下, @Resource仅被支持在域和bean属性的setter方法上使用. 因此, 你应该在你注入目标是个构造函数或多参数方法上使用限定.

你也可以创建自定义的限定注解. 定义了一个注解, 并给其定义上添加了@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定义提供信息了. 你可以添加<qualifier/>标签作为<bean/>标签的子元素, 然后为其指定typevalue来匹配你自定义的注解. 类型通过注解类的全限定名匹配. 最为替换, 如果没有名字冲突, 你可以使用短名称. 下面例子展示了全部两种方式:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>

    <bean class="example.SimpleMovieCatalog">
        <qualifier type="Genre" value="Action"/>
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <qualifier type="example.Genre" value="Comedy"/>
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean id="movieRecommender" class="example.MovieRecommender"/>

</beans>

在类路径扫描和组件管理中, 你可以看到XML定义的基于注解的限定元数据. 特别的, 请参看使用注解提供限定元数据.

一些情况下, 使用没有值的注解就足够了. 当注解服务于更普遍的目标并且能应用于多个不同类型的依赖时更为有用. 例如, 你可能需要提供一个离线目录供搜索, 当没有网络接入时. 首先, 定义简单的注解, 如下:

@Target({ElementType.FIELD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Qualifier
public @interface Offline {

}

然后, 添加注解到域或属性上, 如下:

public class MovieRecommender {

    @Autowired
    @Offline //此处添加了注解@Offline
    private MovieCatalog offlineCatalog;

    // ...
}

现在bean定义仅仅需要一个限定type, 如下所示:

<bean class="example.SimpleMovieCatalog">
    <qualifier type="Offline"/>  <!--这个元素指定了限定-->
    <!-- inject any dependencies required by this bean -->
</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
}

自动包装的域是用自定义限定注解的, 其包含两个属性:genreformat, 如下所示:

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的元数据属性来代替<qualifier/>元素. 如果可用, <qualifier/>和他的属性优先, 但自动封装机制回退则使用<meta/>标签的值, 如果没有限定存在的话. 如下所示定义中后面两个:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <context:annotation-config/>

    <bean class="example.SimpleMovieCatalog">
        <qualifier type="MovieQualifier">
            <attribute key="format" value="VHS"/>
            <attribute key="genre" value="Action"/>
        </qualifier>
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <qualifier type="MovieQualifier">
            <attribute key="format" value="VHS"/>
            <attribute key="genre" value="Comedy"/>
        </qualifier>
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <meta key="format" value="DVD"/>
        <meta key="genre" value="Action"/>
        <!-- inject any dependencies required by this bean -->
    </bean>

    <bean class="example.SimpleMovieCatalog">
        <meta key="format" value="BLURAY"/>
        <meta key="genre" value="Comedy"/>
        <!-- inject any dependencies required by this bean -->
    </bean>

</beans>

1.9.5 使用泛型作为自动包装限定符

另外, 对于@Qualifier注解, 你可以使用java泛型类型作为限定的隐式格式. 例如, 你有如下配置:

@Configuration
public class MyConfiguration {

    @Bean
    public StringStore stringStore() {
        return new StringStore();
    }

    @Bean
    public IntegerStore integerStore() {
        return new IntegerStore();
    }
}

假设前面的bean实现了一个泛型接口(也就是Store<String>Store<Integer>), 你可以@Autowire这个Store接口, 并且泛型被用来作为限定, 如下:

@Autowired
private Store<String> s1; // <String> qualifier, injects the stringStore bean

@Autowired
private Store<Integer> s2; // <Integer> qualifier, injects the integerStore bean

泛型限定也用来自动装配集合列表,Map接口和数组. 下面例子自动装配了泛型List:

// Inject all Store beans as long as they have an <Integer> generic
// Store<String> beans will not appear in this list
@Autowired
private List<Store<Integer>> s;

1.9.6 使用CustomAutowireConfigurer

CustomAutowireConfigurer是一个BeanFactoryPostProcessor, 允许你注册你自己自定义的限定注解类型, 就算它们不是被Spring的@Qualifier注解的. 下例展示了如何使用CustomAutowireConfigurer:

<bean id="customAutowireConfigurer"
        class="org.springframework.beans.factory.annotation.CustomAutowireConfigurer">
    <property name="customQualifierTypes">
        <set>
            <value>example.CustomQualifier</value>
        </set>
    </property>
</bean>

AutowireCandidateResolver通过下面的方式决定自动装配候选:

  • 每个bean定义的autowire-candidate
  • 任何<beans/>元素上可用的default-autowire-candidates模型
  • 使用了@Qualifier的注解以及通过CustomAutowireConfigurer注册的自定义注解

当多个bean被限定为自动装配的候选, 首要项的决策遵循: 如果候选中存在primary属性且值为true, 则它被选中.

1.9.7 使用@Resource注入

Spring支持使用JSR-250规范的@Resource注解进行注入, 其(javax.annotation.Resource)能够用在域或者setter方法上. 这是JavaEE的一种普遍模式: 例如, 在JSF-管理的bean和JAX-WS端点中. Spring为Spring管理的bean也支持这种模式.

@Resource获取name属性. 默认Spring将这个值作为bean名称注入. 换句话说, 它遵循按名称语义, 如下所示:

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Resource(name="myMovieFinder")  // 这里注入了一个@Resource
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
}

如果没有明确指定名称, 默认名称就是从域的名字或者setter方法导出. 在域情况下, 它使用域名称, 在setter方法情况下, 它获取bean属性名. 下面例子展示了名为movieFinder的bean注入到setter方法:

public class SimpleMovieLister {

    private MovieFinder movieFinder;

    @Resource
    public void setMovieFinder(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
}

在没有显式的名称指定的@Resource使用场景下, 类似于@Autowired,@Resource查找主类型匹配而不是命名bean, 并且也可以解析熟知的依赖:BeanFactory, ApplicationContext, ResourceLoader, ApplicationEventPublisher, 和 MessageSource 接口.

因此, 下例中,customerPreferenceDao域首先查找一个名为"customerPreferenceDao"的bean 并且回退到主类型匹配的类型CustomerPreferenceDao:

public class MovieRecommender {

    @Resource
    private CustomerPreferenceDao customerPreferenceDao;

    @Resource
    private ApplicationContext context; // `context`域是注入了熟知的依赖类型`ApplicationContext`

    public MovieRecommender() {
    }

    // ...
}

1.9.8 使用@PostConstruct@PreDestroy

CommonAnnotationBeanPostProcessor不仅识别@Resource注解, 也可以识别JSR-250生命周期注解:javax.annotation.PostConstructjavax.annotation.PreDestroy. Spring 2.5 引用的, 对于这些注解的支持提供了一种相应的生命周期回调机制, 如之前的初始化回调和销毁回调章节有描述的. 假设ApplicationContext注册了CommonAnnotationBeanPostProcessor, 一个添加了这些注解的方法将会被调用, 调用点与相应的Spring生命周期接口方法或显式声明回调方法相同. 下面例子中, 缓存通过初始化方法预热并通过销毁方法清除:

public class CachingMovieLister {

    @PostConstruct
    public void populateMovieCache() {
        // populates the movie cache upon initialization...
    }

    @PreDestroy
    public void clearMovieCache() {
        // clears the movie cache upon destruction...
    }
}

关于组合使用不同的生命周期机制, 可以参看组合生命周期机制.

1.10 类路径扫描和托管组件

本章中大部分例子都将使用XML来指定用于Spring容器生产BeanDefinition的配置元数据. 前面章节(基于注解的容器配置)阐述如何通过代码级别的注解来生成大量配置元数据. 就算在那些示例中, 最基础的bean定义依然是显式定义在XML文件中的, 而注解仅仅是驱动了DI. 本节描述一种隐式的通过扫描类路径方法探测候选组件的方式. 候选组件是一组按一定过滤规则匹配的并被容器注册的相关bean定义. 这就移除了使用XML来完成bean注册的方式. 相反, 你可以使用注解(例如:@Component), AspectJ类型表达式, 或者你自定义的过滤规则来筛选哪些类定义能注册到容器中.

1.10.1 @Component和更多注解

@Repository注解是为很多履行一个仓储角色(等同于数据访问对象或DAO)的诸多类上的一个标记. 使用这个标记的一个用法就是自动解析异常, 如同在异常解析一节描述的.

Spring 提供了更多注解: @Component, @Service, 和 @Controller. @Component是一般的使用在Spring管理的组件上的注解. @Repository, @Service, 和 @Controller是比@Component更具针对性的用例(各自分别在持久化,服务和表现层使用). 因此, 你可以在你的组件上使用@Component, 但使用@Repository, @Service, 和 @Controller替代将使你的类在工具处理时或联合使用方面时更合理. 例如, 这些注解提供了理想的目标切入点. @Repository, @Service, 和 @Controller也能在将来Spring版本中提供更多的语义.因此, 如果你在你的服务层中选择使用@Component@Service, @Service是更明确的更好的选项. 类似的, 如前所述, @Repository在持久层已经得到异常解析方面的支持.

1.10.2 使用元注解和组合注解

许多Spring提供的注解可以作为元注解用到你的代码中. 元注解是可以应用到另一个注解上的注解. 例如, 前面提到的@Service注解就是使用@Component元注解的, 如下:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component // Component 将导致 @Service 与 @Component 一样被对待
public @interface Service {

    // ....
}

你也可以组合使用元注解用来创建组合注解. 例如, Spring MVC 的 @RestController是由@Controller@ResponseBody组合的.

更多的, 组合注解能够选择重新定义元注解的属性来自定义. 如果你想要仅暴露元注解中的一些属性的话这就是有用的. 例如, Spring的@SessionScope注解硬编码了scope的名称为session, 但仍然允许自定义proxyMode. 如下是SessionScope的定义:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Scope(WebApplicationContext.SCOPE_SESSION)
public @interface SessionScope {

    /**
     * Alias for {@link Scope#proxyMode}.
     * <p>Defaults to {@link ScopedProxyMode#TARGET_CLASS}.
     */
    @AliasFor(annotation = Scope.class)
    ScopedProxyMode proxyMode() default ScopedProxyMode.TARGET_CLASS;

}

然后你可以不声明proxyMode来使用@SessionScope:

@Service
@SessionScope
public class SessionScopedService {
    // ...
}

你也可以重写proxyMode的值, 如下所示:

@Service
@SessionScope(proxyMode = ScopedProxyMode.INTERFACES)
public class SessionScopedUserService implements UserService {
    // ...
}

更多细节请看wiki页面的Spring注解编程模型

1.10.3 自动探测类和注册bean定义

Spring能自动探测更多的类并使用ApplicationContext注册相关的BeanDefinition实例.例如下面的两个类对于这样的自动探测是合适的:

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

为了自动探测并注册相关的bean, 你需要添加@ConponentScan@Configuration类上, basePackages属性时两个类通用的包(如果多个包的话,你可以使用逗号或分号或空格分割的列表,将每个类的父包包含其中).

@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig{
    ...
}

下面是对应的XML:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xsi:schemaLocation="http://www.springframework.org/schema/beans
        http://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        http://www.springframework.org/schema/context/spring-context.xsd">

    <context:component-scan base-package="org.example"/>

</beans>

更多的, 当你使用component-scan元素时, AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor两者都是隐式包含的. 这意味着这两个组件被自动探测并一起包装了--都没有任何XML提供的bean配置元数据.

1.10.4 使用过滤器来自定义扫描

默认情况下, 用@Component, @Repository, @Service, @Controller标记的类, 或者使用@Component元注解的自定义注解标记的类是探测的候选组件. 尽管这样, 你也可以通过应用过滤器修改和扩展行为. 为注解@ComponentScan添加includeFiltersexcludeFilters参数(或者component-scan元素的include-filterexclude-filter子元素). 每个元素都需要typeexpression属性, 下面的表格描述了过滤选项:

表5. 过滤类型
|过滤类型|示例表达式|描述|
|-------|----------|----|
|annotation (default)|org.example.SomeAnnotation|目标组件上, 在类级别上出现的注释|
|assignable|org.example.SomeClass|目标组件要被其扩展或实现的类或接口|
|aspectj|org.example..Service+|与目标组件匹配的一个AspectJ表达式|
|regex|org.example.Default.
|与目标组件类名称匹配的正则表达式|
|custom|org.example.MyTypeFilter|org.springframework.core.type.TypeFilter接口的自定义实现|

下面例子展示了忽略所有@Repository注解并使用"stub"仓储代替的配置:

@Configuration
@ComponentScan(basePackages = "org.example",
        includeFilters = @Filter(type = FilterType.REGEX, pattern = ".*Stub.*Repository"),
        excludeFilters = @Filter(Repository.class))
public class AppConfig {
    ...
}

下面例子是等效的XML配置:

<beans>
    <context:component-scan base-package="org.example">
        <context:include-filter type="regex"
                expression=".*Stub.*Repository"/>
        <context:exclude-filter type="annotation"
                expression="org.springframework.stereotype.Repository"/>
    </context:component-scan>
</beans>

1.10.5 在组件中定义元数据

Spring组件也能为容器贡献bean定义元数据. 你可以通过使用@Configuration注解的类里面的@Bean注解来定义bean元数据. 下面例子展示了这种方式:

@Component
public class FactoryMethodComponent {

    @Bean
    @Qualifier("public")
    public TestBean publicInstance() {
        return new TestBean("publicInstance");
    }

    public void doWork() {
        // Component method implementation omitted
    }
}

上面的类是Spring的一个组件,work()方法中它有应用特定的代码. 它也贡献了一个bean的定义,有个引用方法publicInstance()的工厂方法. @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);
    }
}

示例自动为类型为String的参数country自动装配了另一个名为privateInstance的bean的age属性值. Spring表达式语言元素通过注解#{<expression>}定义了值. 对于@Value注解, 表达式解析器在解析表达式文本的时候查找bean的名称.

从Spring4.3开始, 你可能需要声明一个参数类型为InjectionPoint的工厂方法(或者它的特定的子类如DependencyDescriptor), 来访问请求的注入点来触发当前bean的创建. 注意这种仅适用于真实的bean实例创建, 对于已存在实例的注入不适用. 作为其结果, 这个特性对于原型bean更有意义. 对于其他作用域, 工厂方法仅看到scope内触发创建新bean实例的注入点(例如, 触发创建延迟加载单例bean的依赖项).这种情况下你可以谨慎的使用注入点元数据. 下面例子展示了如何使用InjectionPoint:

@Component
public class FactoryMethodComponent {

    @Bean @Scope("prototype")
    public TestBean prototypeInstance(InjectionPoint injectionPoint) {
        return new TestBean("prototypeInstance for " + injectionPoint.getMember());
    }
}

普通Spring组件中的@Bean方法与其他在Spring@Configuration类中的其他同辈们处理是不同的. 不同之处在于, @Component类没有用CGLIB增强用来切入方法和域的调用. CGLIB代理是一种调用@Configuration类中@Bean注解的方法和域来创建引用到相关对象的bean元数据的方法.此类方法不会被普通的java语义调用, 而是为了提供非正常的生命周期管理和Spring的Bean代理而贯穿容器, 即使是通过编程方法调用@Bean方法引用其他bean时. 相反, 调用@Component类中的@Bean方法或域时使用标准java语义, 没有其他特定CGLIB的处理或应用其他限制.

1.10.6 命名的自动探测组件

当一个组件被作为扫描过程的一部分探测到时, 它的bean名称通过扫描者知道的BeanNameGenerator策略生成. 默认情况下,任何Spring的注解(@Component, @Repository, @Service, 和 @Controller)包含一个名字value因此提供了相应bean定义的名称.

如果注解不包含value或被其他组件探测(例如自定义过滤器), 默认的bean名称生成器会返回首字母小写的不合格类名. 例如, 付过下面的组件类被探测到, 他们的名字应该是myMovieListermovieFinderImpl.

@Service("myMovieLister")
public class SimpleMovieLister {
    // ...
}
@Repository
public class MovieFinderImpl implements MovieFinder {
    // ...
}
@Configuration
@ComponentScan(basePackages = "org.example", nameGenerator = MyNameGenerator.class)
public class AppConfig {
    ...
}
<beans>
    <context:component-scan base-package="org.example"
        name-generator="org.example.MyNameGenerator" />
</beans>

作为通用规定, 考虑使用注解指定名称, 当其他组件显式引用到它时. 另一方面,当容器负责装配时自动生成的名称就足够了.

1.10.7 为自动探测组件提供作用域

Spring托管组件一样, 默认的和大多数通用的scope是singleton. 虽然如此, 有时候你需要通过@Scope指定一个不同的作用域. 你可以为其指定名字, 如下所示:

@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
    // ...
}

@Scope注解仅在具体的bean类上(注解的组件)或工厂方法上(@Bean方法)是自省的. 相比于XMLbean定义, 没有bean定义继承的概念, 并且类级别的继承与元数据不相干.

在Spring上下文中更多的关于web特定的作用域如"request","session"等,请参看Request,Session,Application和WebSocket作用域. 这些作用域有预置的注解, 你也可以使用Spring元注解的方式组合你自己的作用域注解: 例如, 使用@Scope("prototype")自定义注解,可能需要声明一个自定义的scope-proxy模式.

@Configuration
@ComponentScan(basePackages = "org.example", scopeResolver = MyScopeResolver.class)
public class AppConfig {
    ...
}
<beans>
    <context:component-scan base-package="org.example" scope-resolver="org.example.MyScopeResolver"/>
</beans>

当使用非单例作用域时, 也许必要为作用域的对象生成代理. 原因在作用域bean作为依赖项中有描述. 为了达到此目的, component-scan元素的一个scoped-proxy属性时可用的. 三种可能的值分别是: no,interfaces,和targetClass. 例如, 下面配置将生成标准的JDK动态代理:

@Configuration
@ComponentScan(basePackages = "org.example", scopedProxy = ScopedProxyMode.INTERFACES)
public class AppConfig {
    ...
}
<beans>
    <context:component-scan base-package="org.example" scoped-proxy="interfaces"/>
</beans>

1.10.8 Qualifier元数据注解

在使用修饰符来微调基于注解的装配中讨论了@Qualifier注解. 那一节中的例子展示了在解析自动装配的候选的时候, @Qualifier注解和自定义修饰注解提供的更好的控制. 因为这些例子基于XML bean定义, 修饰元数据是通过在作为候选的bean元素中使用qualifiermeta子元素来实现的. 当依赖于类路径扫描的自动探测组件时, 你可以在候选类上添加限定元数据注解. 下面三个例子展示了这种技术:

@Component
@Qualifier("Action")
public class ActionMovieCatalog implements MovieCatalog {
    // ...
}
@Component
@Genre("Action")
public class ActionMovieCatalog implements MovieCatalog {
    // ...
}
@Component
@Offline
public class CachingMovieCatalog implements MovieCatalog {
    // ...
}

1.10.9 候选组件生成索引

虽然类路径扫描是很快的, 对于大型程序, 也可以通过在编译时创建一个静态候选列表在提供启动性能. 在这种模式下,自动扫描的所有模块必须必须使用这种机制.

生成索引, 要给每个包含被扫描的目标组件的模块添加一个额外的依赖. 下例展示了Maven中的配置:

<dependencies>
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context-indexer</artifactId>
        <version>5.2.0.BUILD-SNAPSHOT</version>
        <optional>true</optional>
    </dependency>
</dependencies>

对于Gradle4.5或更早版本, 依赖需要声明在compileOnly配置中, 如下例子所示:

dependencies {
    compileOnly "org.springframework:spring-context-indexer:5.2.0.BUILD-SNAPSHOT"
}
dependencies {
    annotationProcessor "org.springframework:spring-context-indexer:5.2.0.BUILD-SNAPSHOT"
}

处理过程将生成一个META-INF/spring.components文件, 包含在jar文件.

1.11 使用JSR330标准注解

Spring3.0 开始, Spring提供了对JSR-330标准注解(依赖注入)的支持. 这些注解与Spring注解以同样的方式被扫描. 使用它们你需要在类路径下引入相关的jar.

<dependency>
    <groupId>javax.inject</groupId>
    <artifactId>javax.inject</artifactId>
    <version>1</version>
</dependency>

1.11.1 使用@Inject@Named依赖注入

代替@Autowired注解, 也可以使用@javax.inject.Inject如下:

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

    @Inject
    public void setMovieFinder(Provider<MovieFinder> movieFinder) {
        this.movieFinder = 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;
    }

    // ...
}

如同@Autowired,@Inject也可以与java.util.Optional@Nullable一同使用. 在这里这是更合适的, 因为@Inject没有required属性. 下面两个例子展示了如何使用@Inject@Nullable:

public class SimpleMovieLister {

    @Inject
    public void setMovieFinder(Optional<MovieFinder> movieFinder) {
        ...
    }
}
public class SimpleMovieLister {

    @Inject
    public void setMovieFinder(@Nullable MovieFinder movieFinder) {
        ...
    }
}

1.11.2 @Named@ManagedBean: 标准可替换@Component的注解

替换@Component, 也可以使用@javax.inject.Namedjavax.annotation.ManagedBean, 如下所示:

import javax.inject.Inject;
import javax.inject.Named;

@Named("movieListener")  // @ManagedBean("movieListener") could be used as well
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@ManagedBean时, 你可以像使用Spring注解一样使用组件扫描, 如下所示:

@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig  {
    ...
}

1.11.3 JSR-330 标准注解的限制

使用标准注解, 你应该知道一些显著的特性是不可用的, 如下表所示:

表6 Spring组件模型元素与JSR-330变体的区别
|Spring|Javax.inject.*|javax.inject限制/说明|
|------|----------------|-------------------|
|@Autowired|@Inject|@Inject没有'required'属性. 可以用Java8的Optional替代|
|@Component|@Named/@ManagedBean|JSR-330不提供组合模型,仅仅是一种标识命名模型的方式|
|@Scope("singleton")|@Singleton|JSR-330的默认作用域类似Spring的prototype. 所以为了保持与Spring默认的一致性, JSR-330 的bean声明在容器中默认是singleton的. 为了使用非singleton的其他作用域, 你需要使用Spring的@Scope注解. javax.inject也提供了@Scope注解.不过这个仅仅用来创建你自己的注解时使用.|
|@Qualifier|@Qualifier / @Named|javax.inject.Qualifier仅是为了创建自定义限定的元注解.具体的string类型的限定符(如同Spring的@Qualifier带有一个值)可以是通过javax.inject.Named关联|
|@Value|-|无等效的|
|@Required|-|无等效的|
|@Lazy|-|无等效的|
|ObjectFactory|Provider|javax.inject.Provider是Spring的ObjectFactory的替代, 但仅仅有一个短的方法get(). 它也可以用在与Spring@Autowired或无注解构造函数以及setter方法上组合使用|

1.12 基于Java的容器配置

这节内容涵盖了如何在java代码中使用注解配置容器. 包含下面主题:

  • 基本概念: @Bean@Configuration
  • 使用AnnotationConfigApplicationContext初始化Spring容器
  • 使用@Bean注解
  • 使用@Configuration注解
  • 组合基于java的配置
  • Bean定义档案
  • PropertySource抽象
  • 语句中的占位符解析

1.12.1 基本概念: @Bean@Configuration

Spring中的基于Java的配置支持和核心是@Configuration注解类和@Bean注解的方法.

@Bean注解用来表明一个方法实例化,配置,并初始化出有Spring容器管理的新对象.对于熟悉Spring的<beans/>XML配置, @Bean注解扮演了与<bean/>元素相同的角色. 你可以与Spring@Component一起使用@Bean注解方法. 即使如此, 他们总是跟@Configurationbean一起使用的.

@Configuration注解的类表明它的主要目的是作为bean定义的源. 而且@Configuration允许通过调用同一个类中的其他@bean方法来定义bean间的依赖关系. 可能最简单的@Configuration类如下:

@Configuration
public class AppConfig {

    @Bean
    public MyService myService() {
        return new MyServiceImpl();
    }
}

上面的AppConfig类等同于下面的<beans/>XML配置;

<beans>
    <bean id="myService" class="com.acme.services.MyServiceImpl"/>
</beans>

@Bean@Configuration注解在接下来的章节中会更深的讨论. 首先, 我们扫描了通过使用Java配置的方式创建容器的方式.

1.12.2 使用AnnotationConfigApplicationContext初始化Spring容器

接下来的章节记录了Spring的AnnotationConfigApplicationContext, Spring3.0引入的. 多功能的ApplicationContext实现能接收不仅仅是Configuration类作为输入, 还有@Component类以及JSR-330元数据注解的类.

@Configuration类作为输入时, @Configuration类本身被作为bean定义注册并将其内部所有的@Bean方法也被注册为bean定义.

@Component和JSR-330类作为输入时, 他们被注册为bean定义, 并且假定如@Autowired@Inject的DI元数据在这些类的内部按需使用.

简单的结构

与Spring的XML文件用来被初始化ClassPathXmlApplicationContext时的输入一样, 你可以使用@Configuration类作为初始化AnnotationConfigApplicationContext时的输入. 这允许完全不适用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,Dependency1Dependency2使用了诸如@Autowired的Spring DI注解.

通过register(Class<?> ... )编程构建容器

你可以通过无参构造实例化AnnotationConfigApplicationContext,然后通过register()方法配置它. 这种方法在编程构建AnnotationConfigApplicationContext是特别有用. 如下所示:

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

使用scan(String…​)开启组件扫描

开启组件扫描, 你可以在@Configuration类上注解如下:

@Configuration
@ComponentScan(basePackages = "com.acme")  // 这个注解启动了组件扫描
public class AppConfig  {
    ...
}

有经验的Spring用户可能更熟悉XML声明, 它是从Spring的context:命名空间, 如下例子所示:

<beans>
    <context:component-scan base-package="com.acme"/>
</beans>

在前面例子中, com.acme包被扫描以查找任何@Component注解的类, 还有那些注册在Spring容器内的bean定义.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);
}

使用AnnotationConfigWebApplicationContext支持WEB程序

一种WebApplicationContextAnnotationConfigApplicationContext变体是AnnotationConfigWebApplicationContext. 当配置ContextLoaderListenerSevlet监听器, Spring MVC DispatcherServlet等时可以使用这个实现. 下面的web.xml片段配置一个典型的Spring MVC Web程序(注意contextClass context-param 和 init-param 的使用).

<web-app>
    <!-- Configure ContextLoaderListener to use AnnotationConfigWebApplicationContext
        instead of the default XmlWebApplicationContext -->
    <context-param>
        <param-name>contextClass</param-name>
        <param-value>
            org.springframework.web.context.support.AnnotationConfigWebApplicationContext
        </param-value>
    </context-param>

    <!-- Configuration locations must consist of one or more comma- or space-delimited
        fully-qualified @Configuration classes. Fully-qualified packages may also be
        specified for component-scanning -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>com.acme.AppConfig</param-value>
    </context-param>

    <!-- Bootstrap the root application context as usual using ContextLoaderListener -->
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>

    <!-- Declare a Spring MVC DispatcherServlet as usual -->
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <!-- Configure DispatcherServlet to use AnnotationConfigWebApplicationContext
            instead of the default XmlWebApplicationContext -->
        <init-param>
            <param-name>contextClass</param-name>
            <param-value>
                org.springframework.web.context.support.AnnotationConfigWebApplicationContext
            </param-value>
        </init-param>
        <!-- Again, config locations must consist of one or more comma- or space-delimited
            and fully-qualified @Configuration classes -->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>com.acme.web.MvcConfig</param-value>
        </init-param>
    </servlet>

    <!-- map all requests for /app/* to the dispatcher servlet -->
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/app/*</url-pattern>
    </servlet-mapping>
</web-app>

1.12.3 使用@Bean注解

@Bean是方法级别的注解, 并且与XML 的 <bean/>元素同源. 这个注解支持一些<bean/>属性,如* init-method * destroy-method * autowiring * name.

@Configuration注解的或者在@Component注解的类中都可以使用@Bean注解.

声明一个Bean

要声明bean, 你可以在一个方法上添加@Bean注解. 在ApplicationContext内部, 这个方法将被它的返回值类型注册到容器的bean定义. 默认bean的名字和方法名称是一样的. 下面示例展示了@Bean的声明:

@Configuration
public class AppConfig {

    @Bean
    public TransferServiceImpl transferService() {
        return new TransferServiceImpl();
    }
}

上面的配置是等同于下面XML的配置:

<beans>
    <bean id="transferService" class="com.acme.TransferServiceImpl"/>
</beans>

两种方式都使得名为transferService的bean在ApplicationContext可用, 绑定到一个类型为TransferServiceImpl的实例, 如下所示:

transferService -> com.acme.TransferServiceImpl

你也可以使用接口(或基类)声明@Bean方法返回类型, 如下所示;

@Configuration
public class AppConfig {

    @Bean
    public TransferService transferService() {
        return new TransferServiceImpl();
    }
}

然则, 这限制了预测类型的可见性为接口类型(TransferService). 然后, 容器仅知道一次完整类型(TransferServiceImpl),受影响的bean单例被创建了. 非延迟加载的单例bean实例化根据他们声明的顺序初始化, 所以,依赖于当其他组件尝试匹配一个没有声明的类型时, 你可能看到不同类型的匹配结果(如@Autowired TransferServiceImpl, 仅在transferService被实例化时解析).

Bean依赖

@Bean注解方法可能还有随意数量的描述创建bean所需依赖的参数. 例如, 如果我们的TransferService需要一个AccountRepository, 我们可以具象出使用带有一个参数的方法, 如下:

@Configuration
public class AppConfig {

    @Bean
    public TransferService transferService(AccountRepository accountRepository) {
        return new TransferServiceImpl(accountRepository);
    }
}

这种解析机制比较其构造函数依赖而言相当, 更多细节参看相关章节.

获取生命周期回调

任何用@Bean注解的类定义都支持一般的生命周期回调, 并且可以使用JSR-250@PostConstruct@PreDestroy注解. 参看JSR-250注解获取更多细节.

一般的Spring的生命周期回调也完全支持. 如果bean实现了InitializingBean,DisposableBean,或者Lifecycle, 他们各自的方法就会被容器调用.

标准的*Aware接口(如BeanFactoryAware,BeanNameAware,MessageSourceAware,ApplicationContextAware等)也是完全支持的.

@Bean注解支持指定任意的初始化和销毁回调方法, 如同XML中在<bean/>元素上的init-methoddestroy-method属性, 如下:

public class BeanOne {

    public void init() {
        // initialization logic
    }
}

public class BeanTwo {

    public void cleanup() {
        // destruction logic
    }
}

@Configuration
public class AppConfig {

    @Bean(initMethod = "init")
    public BeanOne beanOne() {
        return new BeanOne();
    }

    @Bean(destroyMethod = "cleanup")
    public BeanTwo beanTwo() {
        return new BeanTwo();
    }
}
@Bean(destroyMethod="")
public DataSource dataSource() throws NamingException {
    return (DataSource) jndiTemplate.lookup("MyDS");
}

在上面的BeanOne示例代码中, 构造时调用init()方法是等效的, 如下所示:

@Configuration
public class AppConfig {

    @Bean
    public BeanOne beanOne() {
        BeanOne beanOne = new BeanOne();
        beanOne.init();
        return beanOne;
    }

    // ...
}

指定Bean作用域

Spring包含@Scope注解所以你可以指定bean的作用域.

使用@Scope注解

你可以为@Bean注解的bean定义指定作用域. 可以使用在bean作用域中指定的标准作用域.

默认作用域是singleton, 但可以覆盖, 如下:

@Configuration
public class MyConfiguration {

    @Bean
    @Scope("prototype")
    public Encryptor encryptor() {
        // ...
    }
}
@Scopescoped-proxy

通过作用域代理, Spring提供了一种方便的方法与作用域依赖协作. 最简单的方式是在XML配置<aop:scoped-proxy/>添加创建的代理.在Java中配置bean使用@Scope注解的proxyMode属性提供了等效的支持. 默认没有代理(ScopedProxyMode.NO), 但可以指定ScopedProxyMode.TARGET_CLASSScopedProxyMode.INTERFACES.

如果将XML中的引用文档(参看作用域代理)移植到Java的@Bean, 它看起来如下:

// an HTTP Session-scoped bean exposed as a proxy
@Bean
@SessionScope
public UserPreferences userPreferences() {
    return new UserPreferences();
}

@Bean
public Service userService() {
    UserService service = new SimpleUserService();
    // a reference to the proxied userPreferences bean
    service.setUserPreferences(userPreferences());
    return service;
}

自定义Bean命名

默认配置的类使用@Bean方法名称作为结果Bean的名称. 这个功能可以使用name属性覆盖, 如下:

@Configuration
public class AppConfig {

    @Bean(name = "myThing")
    public Thing thing() {
        return new Thing();
    }
}

Bean别名

如在命名Bean中讨论的, 有时候迫切需要给bean指定多个名称, 也就是别名. @Beanname属性可以接收一个String类型的数组来达到这个目的. 下面例子展示了一个bean中的多个别名:

@Configuration
public class AppConfig {

    @Bean({"dataSource", "subsystemA-dataSource", "subsystemB-dataSource"})
    public DataSource dataSource() {
        // instantiate, configure and return DataSource bean...
    }
}

Bean描述

有时候为bean提供更多细节的描述是有用的. 特别是在bean暴露给监控的时候(也许通过JMX).

@Bean添加描述, 你可以使用@Description注解, 如下:

@Configuration
public class AppConfig {

    @Bean
    @Description("Provides a basic example of a bean")
    public Thing thing() {
        return new Thing();
    }
}

1.12.4 使用@Configuration注解

@Configuration是一个表明对象是bean定义源的类级注解. @Configuration通过公有的@Bean注解方法声明类. 调用@Configuration类内的@Bean方法也可以用来定义内部bean依赖. 参看基本概念:@Bean@Configuration的基本介绍.

注入内部bean依赖

当bean有对其他bean的依赖时, 表达这种依赖如同一个调用另一个bean方法这样简单, 如下所示:

@Configuration
public class AppConfig {

    @Bean
    public BeanOne beanOne() {
        return new BeanOne(beanTwo());
    }

    @Bean
    public BeanTwo beanTwo() {
        return new BeanTwo();
    }
}

前面的例子中, beanOne通过构造函数接收一个beanTwo.

查找方法注入

如前面提到的, 查找方法注入是应该少用的高级特性. 在一个单例bean拥有原型bean依赖的场景它是有用的. 使用java配置为实现这种模式提供了自然的意义. 如下展示了如何使用查找方法注入:

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

通过使用java配置, 你可以创建CommandManager的子类, 其createCommand()方法被覆盖, 这个方法用来查找一个原型指令对象. 如下所示:

@Bean
@Scope("prototype")
public AsyncCommand asyncCommand() {
    AsyncCommand command = new AsyncCommand();
    // inject dependencies here as required
    return command;
}

@Bean
public CommandManager commandManager() {
    // return new anonymous implementation of CommandManager with createCommand()
    // overridden to return a new prototype Command object
    return new CommandManager() {
        protected Command createCommand() {
            return asyncCommand();
        }
    }
}

关于基于Java配置内部工作的更多信息

下面的两个例子, 此例中一个@Bean注解方法被调用两次.

@Configuration
public class AppConfig {

    @Bean
    public ClientService clientService1() {
        ClientServiceImpl clientService = new ClientServiceImpl();
        clientService.setClientDao(clientDao());
        return clientService;
    }

    @Bean
    public ClientService clientService2() {
        ClientServiceImpl clientService = new ClientServiceImpl();
        clientService.setClientDao(clientDao());
        return clientService;
    }

    @Bean
    public ClientDao clientDao() {
        return new ClientDaoImpl();
    }
}

clientDao()clientService1()clientService2()分别调用一次. 因为这个方法生成一个新的ClientDaoImpl并返回, 你可能预测会有两个实例(每个服务一个). 这种概念是有问题的: Spring中, bean默认是singleton作用域. 这就是魔法时刻: 所有@Configuration类都在启动时由CGLIB生成子类了. 子类中, 在它调用父类方法并创建实例前, 子方法检查任何缓存(作用域内)的bean.


1.12.5 组合基于Java的配置

Spring基于Java的配置特性使得可以组合注释, 这能减少配置的复杂性.

使用@Import注解

很像XML配置中<import/>元素被用来辅助模块化配置, @Import注解允许从其他配置类加载@Bean定义, 如下:

@Configuration
public class ConfigA {

    @Bean
    public A a() {
        return new A();
    }
}

@Configuration
@Import(ConfigA.class)
public class ConfigB {

    @Bean
    public B b() {
        return new B();
    }
}

现在, 在初始化上下文时, 不需要指定ConfigA.classConfigB.class, 仅仅ConfigB需要显示提供, 如下所示:

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class);

    // now both beans A and B will be available...
    A a = ctx.getBean(A.class);
    B b = ctx.getBean(B.class);
}

这种方法简化了容器初始化, 因为仅仅一个类需要处理, 而你不需要记住构造时潜在的大量@Configuration类.

在导入的@Bean定义中注入依赖

前面的例子可以运行但是过分简化. 在大多数实际场景中, bean依赖会跨Configuration类. 当使用XML时, 这不是问题, 因为没有编译介入, 你可以声明ref="someBean"并信任Spring会在初始化时处理好它. 当使用@Configuration类时, java编译器在配置模式下设置了约束, 引用其他bean必须符合java语义.

幸运的是, 解决这个问题很简单. 我们已经讨论的, @Bean方法可以有任意的描述依赖的参数. 考虑下面的真实场景, 它有几个@Configuration类, 每个依赖描述在其他配置中:

@Configuration
public class ServiceConfig {

    @Bean
    public TransferService transferService(AccountRepository accountRepository) {
        return new TransferServiceImpl(accountRepository);
    }
}

@Configuration
public class RepositoryConfig {

    @Bean
    public AccountRepository accountRepository(DataSource dataSource) {
        return new JdbcAccountRepository(dataSource);
    }
}

@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {

    @Bean
    public DataSource dataSource() {
        // return new DataSource
    }
}

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
    // everything wires up across configuration classes...
    TransferService transferService = ctx.getBean(TransferService.class);
    transferService.transfer(100.00, "A123", "C456");
}

有其他方法可以得到相同的结果. 记住@Configuration类仅仅是容器中的另一个bean: 这意味着他们可以得到跟其他bean一样的好处, 可以使用如@Autowired@Value注入等特性.

下面展示一个bean如何被另一个bean自动装配:

@Configuration
public class ServiceConfig {

    @Autowired
    private AccountRepository accountRepository;

    @Bean
    public TransferService transferService() {
        return new TransferServiceImpl(accountRepository);
    }
}

@Configuration
public class RepositoryConfig {

    private final DataSource dataSource;

    @Autowired
    public RepositoryConfig(DataSource dataSource) {
        this.dataSource = dataSource;
    }

    @Bean
    public AccountRepository accountRepository() {
        return new JdbcAccountRepository(dataSource);
    }
}

@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {

    @Bean
    public DataSource dataSource() {
        // return new DataSource
    }
}

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(SystemTestConfig.class);
    // everything wires up across configuration classes...
    TransferService transferService = ctx.getBean(TransferService.class);
    transferService.transfer(100.00, "A123", "C456");
}
全限定导入bean,便于导航

前面的场景中, 使用@Autowired工作的很好并且提供了急需的模块化, 但明确的决定包装bean在哪里依然是模糊的. 例如, 开发人员查找ServiceConfig, 你怎么指定@Autowired AccountRepository定义在哪里? 代码里面没有明确, 并且这还好. 记住Spring Tool Suite 提供了工具化能呈现包装的图标, 这可能是你需要的全部. 你的Java IDE也能简单的找到所有的声明和AccountRepository类的使用以及很快展示@Bean方法和返回值.

万一这种模糊性不可接受并且你想从IDE直接从@Configuration类导航到另一个, 就要考虑包装配置类本身了, 如下:

@Configuration
public class ServiceConfig {

    @Autowired
    private RepositoryConfig repositoryConfig;

    @Bean
    public TransferService transferService() {
        // navigate 'through' the config class to the @Bean method!
        return new TransferServiceImpl(repositoryConfig.accountRepository());
    }
}

前面提到的这种情况, AccountRepository是完全显式定义的. 虽然, ServiceConfig现在被紧密耦合到RepositoryConfig了. 这是折中. 通过使用基于接口或抽象类的@Configuration类使用, 紧耦合能或多或少得到缓解. 如下:

@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方法

根据随机的系统状态, 条件化的启动和禁止整个@Configuration类或单独的@Bean方法是有用的. 一个普遍的例子就是,当profile在Spring的Environment来使用@Profile注解来激活bean(参看Bean定义Profiles获取更多).

@Profile注解实际上是使用更灵活的@Conditional注解来实现的. @Conditional注解表明特定的org.springframework.context.annotation.Condition实现应该在@Bean被注册前咨询到.

接口Condition的实现提供了一个matches(...)方法, 返回true,false. 例如, 下面代码展示了@Profile使用到的Condition实现:

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    if (context.getEnvironment() != null) {
        // Read the @Profile annotation attributes
        MultiValueMap<String, Object> 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;
}

查看@Conditionaljava文档获取更多细节.

组合Java和XML配置

Spring的@Configuration支持并不是为了100%达到替换XML配置的目的. 一些设施, 例如Spring的XML命名空间, 保留了配置容器更理想的方式. 万一XML配置更方便或更需要, 你有一个选择: 要么容器是XML为中心的方式,例如ClassPathXmlApplicationContext, 或者是一个使用AnnotationConfigApplicationContext@ImportResource注解导入XML的以Java配置为主的方式.

使用@Configuration类的XML为主方式

可能用XML去启动Spring容器并用ad-hoc钩子方式包含@Configuration类更好. 例如, 在大型的现有的代码中使用XML, 按需创建@Configuration类并从已有的XML文件中包含进来更为容易. 本节的后面, 我们会讲述在XML为主的程序使用@Configuration的这种选择.

声明@Configuration类为普通的<bean/>元素

记住@Configuration类是容器中的最终的bean定义. 在这个系列的例子中, 我们创建了名为AppConfig@Configuration类并作为<bean/>定义包含在system-test-config.xml中. 因为<context:annotation-config/>是开着的, 容器识别出@Configuration注解并合理处理了定义在AppConfig中的@Bean方法.

下面例子展示了Java中的一般配置:

@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文件的一部分:

<beans>
    <!-- enable processing of annotations such as @Autowired and @Configuration -->
    <context:annotation-config/>
    <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>

    <bean class="com.acme.AppConfig"/>

    <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
</beans>

下面是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

因为@Configuration是用@Component作为元注解的, 基于@Configuration的类自动作为组件扫描的候选. 使用前面描述的例子中的场景, 我们可以重新定义system-test-config.xml来获取组件扫描的好处. 注意,在本例中, 我们不需要显式声明<context:annotation-config/>因为<context:component-scan/>启动了相同的功能.

下面例子展示了修改后的system-test-config.xml文件:

<beans>
    <!-- picks up and registers AppConfig as a bean definition -->
    <context:component-scan base-package="com.acme"/>
    <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>

    <bean class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
</beans>
使用@ImportResource导入XML的@Configuration类为主的方式

程序中用了@Configuration类作为主要的配置容器的机制, 但仍然可能需要使用少量XML. 这种场景下, 你可以使用@ImportResource并仅定义你需要的XML. 这样就可以用Java为中心的方式配置容器并将XML保持到最小. 下面例子(包含一个配置类,一个定义了bean的xml文件, 一个properties文件,以及主类main)展示了如何使用@ImportResource注解完成Java配置并按需使用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);
    }
}
properties-config.xml
<beans>
    <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>
</beans>
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);
    // ...
}

1.13 环境抽象

Environment接口是集成到容器的一个抽象, 抽象出两个关键的程序环境方面: profiles 和 properties.

profile是一个命名的, bean定义的逻辑组,这些bean只有给定的profile激活才注册到容器中的. 不管是在XML还是通过注解定义, bean可能会被分配给一个profile. 关联到profile的Environment对象角色是决定哪些profile当前是激活的, 还有哪些profile应该是默认激活的.

在大多数程序中, properties扮演了一个重要的角色, 并且源于一些列初始来源: properties文件, JVM系统属性,系统环境变量, JNDI,servlet上下文参数,ad-hoc属性对象, Map对象等. 关联到properties的Environment对象提供给用户一种便捷的服务接口, 来配置属性源和解析他们.

1.13.1 Bean定义Profile

Bean定义profile提供了一种核心容器的机制, 允许不同环境中不同bean的注册. "environment"这个单词对不同的用户有不同的意义, 并且在很多用例下有用, 包含:

  • 开发时在内存数据库进行, QA或生产则查找JNDI中相同的数据库

  • 在性能环境部署程序时, 注册监控组件

  • 对于客户A和客户B部署不同的自定义实现.

考虑第一种用例, 在特定程序中需要需要一个DataSource. 在测试环境, 配置可能如下组织:

@Bean
public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
        .setType(EmbeddedDatabaseType.HSQL)
        .addScript("my-schema.sql")
        .addScript("my-test-data.sql")
        .build();
}

现在考虑在QA或生产环境部署程序, 假设程序的生产数据源注册在服务器的JNDI目录. 我们的dataSourcebean现在如下:

@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
    Context ctx = new InitialContext();
    return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}

问题是如何根据当前环境转换这两个变量. 随着时间的过去, Spring的用户想出很多方法来实现, 通常依赖于系统环境变量和XML包含了${placeholder}语句的<import/>来解决, 根据环境变量的值不同解析出正确的文件路径. Bean定义profile提供了解决此类问题的一种核心特性.

如果我们总结上面的用例, 我们以需要在不同上下文中注册不同的bean定义结束. 你可能会说你想在状况A时注册一个profile,而状况B时注册另一个profile. 我们修改你的配置以达到这一需求.

使用@Profile

@Profile注解能表明一个组件对于一种或多种激活的profile是有效的. 使用前面的例子, 我们重写dataSource配置如下:

@Configuration
@Profile("development")
public class StandaloneDataConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .addScript("classpath:com/bank/config/sql/test-data.sql")
            .build();
    }
}
@Configuration
@Profile("production")
public class JndiDataConfig {

    @Bean(destroyMethod="")
    public DataSource dataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}

profile的字符串可能包含一个简单的profile名称(如production)或者profile表达式. profile表达式允许更复杂的逻辑(如product & us-east). 下面操作符支持profile表达式:

  • !: 表示非
  • &: 表示并
  • |: 表示或

你可以使用@Profile作为元注解来创建组合注解. 下面例子定义了一个@Production注解, 可以用来替代@Profile("production"):

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}

@Profile也能在方法级别声明用来包含配置类中一个特定的bean(例如, 特定bean的替换变量), 如下:

@Configuration
public class AppConfig {

    @Bean("dataSource")
    @Profile("development") // `standaloneDataSource`方法仅在`development`激活状态可用
    public DataSource standaloneDataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .addScript("classpath:com/bank/config/sql/test-data.sql")
            .build();
    }

    @Bean("dataSource")
    @Profile("production") // `jndiDataSource`方法仅在`production`激活状态可用
    public DataSource jndiDataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}

使用@Bean方法上的@Profile, 一种特殊场景可能会应用: 对于具有相同方法名称重载的@Bean方法(类似构造函数重载), @Profile条件需要在所有重载方法上声明. 如果条件不一致, 则仅在重载方法中第一个声明处起作用. 因此@Profile不能被用在选择具有参数签名的方法. 同一bean的所有工厂方法之间的解析在创建时遵循Spring的构造函数解析算法。

如果你想用不同的profile条件定义不同的bean, 请使用不同的java方法名称, 这些名称通过使用@Bean的name属性指向同一个bean名称, 如前面的例子所示. 如果参数签名都一样(例如, 所有的变量都有无参构造方法), 这是在有效的java类中表示这种安排的唯一方法(因为只能有一个特定名称和参数签名的方法).

XML bean定义 profile

XML相对应的是<beans>元素的profile属性. 我们上面的例子可以被重写为XML文件, 如下:

<beans profile="development"
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xsi:schemaLocation="...">

    <jdbc:embedded-database id="dataSource">
        <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
        <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
    </jdbc:embedded-database>
</beans>
<beans profile="production"
    xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="...">

    <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
</beans>

也可能为了避免分开定义在不同的<beans/>元素, 所以定义在同一个文件中, 如下:

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="...">

    <!-- other bean definitions -->

    <beans profile="development">
        <jdbc:embedded-database id="dataSource">
            <jdbc:script location="classpath:com/bank/config/sql/schema.sql"/>
            <jdbc:script location="classpath:com/bank/config/sql/test-data.sql"/>
        </jdbc:embedded-database>
    </beans>

    <beans profile="production">
        <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
    </beans>
</beans>

spring-bean.xsd被限制仅仅作为文件的最后元素出现. 这可能有助于XML文件中避免混乱.

<beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:jdbc="http://www.springframework.org/schema/jdbc"
    xmlns:jee="http://www.springframework.org/schema/jee"
    xsi:schemaLocation="...">

    <!-- other bean definitions -->

    <beans profile="production">
        <beans profile="us-east">
            <jee:jndi-lookup id="dataSource" jndi-name="java:comp/env/jdbc/datasource"/>
        </beans>
    </beans>
</beans>
激活profile

现在我们升级了配置, 我们依然需要通知Spring哪个profile被激活. 如果我们启动样例程序, 我们会看到一个NoSuchBeanDefinitionException异常抛出, 因为容器找不到名叫dataSource的bean.

可以有几种方式激活profile, 但最直接的是通过使用ApplicationContextEnvironment API . 如下展示了如何这么做:

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();

更多的, 你也可以通过属性spring.profiles.active激活profile, 这是通过系统变量, JVM系统属性, web.xml中定义的servlet参数, 甚至是JNDI中的配置(查看PropertySource抽象).在集成测试中, 激活profile可以通过使用在spring-test模块上的@ActiveProfiles注解激活(参看使用环境profile配置上下文).

注意profile不是一个"是...或者..."的命题. 你可以一次启动多个profile. 编程方式来说, 你可以提供多个profile名称给setActiveProfiles()方法, 接收一个String...变量, 如下激活了多个profile:

ctx.getEnvironment().setActiveProfiles("profile1", "profile2");

spring.profiles.active也可以接受一个逗号分隔的profile名称, 如下:

-Dspring.profiles.active="profile1,profile2"
默认profile

默认profile是默认情况下表现出的profile. 考虑下面的例子:

@Configuration
@Profile("default")
public class DefaultDataConfig {

    @Bean
    public DataSource dataSource() {
        return new EmbeddedDatabaseBuilder()
            .setType(EmbeddedDatabaseType.HSQL)
            .addScript("classpath:com/bank/config/sql/schema.sql")
            .build();
    }
}

如果没有profile激活, dataSource被创建. 你可以看到这是一种对于一个或多个bean定义的默认方式. 如果任意profile启用了, 默认profile就不会被启用.

你可以通过使用Environment上的setDefaultProfiles()修改默认profile的名称. 或者,通过使用属性spring.profiles.default声明.

PropertySource抽象

Environment抽象提供了从property源通过配置架构查找操作的能力. 如下:

ApplicationContext ctx = new GenericApplicationContext();
Environment env = ctx.getEnvironment();
boolean containsMyProperty = env.containsProperty("my-property");
System.out.println("Does my environment contain the 'my-property' property? " + containsMyProperty);

上面的代码片段中, 我们看到从高层次的方式来查找Spring的my-property是否在当前环境中定义. 为回答这个问题, Environment对象从一系列PropertySource对象中查找. PropertySource是一个对key-value对的简单的抽象. Spring的StandardEnvironment使用两个PropertySource 对象进行配置--一个是JVM系统属性(System.getProperties())还有一个是系统环境变量(System.getenv()).

具体的, 当你使用StandardEnvironment时, env.containsProperty("my-property")将返回true, 如果运行时有my-property系统属性或my-property环境变量存在的话.

  1. ServletConfig 参数 (如果恰当, 例如, 对于DispatcherServlet上下文)
  2. ServletContext 参数(web.xml 上下文参数)
  3. JNDI环境变量(java:comp/env/入口)
  4. JVM 系统属性(-D命令行参数)
  5. JVM 系统环境变量(操作系统环境变量)

最重要的, 整个机制是可配置的. 也许你有自定义的propertis源想要集成到这里. 这么做的话, 继承和实例化你自己的PropertySource并为当前Environment添加PropertySources. 如下:

ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());

前面代码中, MyPropertySource被以最大优先级添加. 如果它包含my-property属性, 这个属性将被找到并返回, 优先于任何其他PropertySource中的my-property. MutablePropertySourcesAPI暴露了很多允许精确操作property源的方法.

1.13.3 使用@PropertySource

@PropertySource注解提供了方便的添加PropertySource到Spring的Environment的机制.

给定的app.properties文件包含了键值对testbean.name=myTestBean, 紧接着的@Configuration类使用@PropertySource调用testBean.getName()返回myTestBean:

@Configuration
@PropertySource("classpath:/com/myco/app.properties")
public class AppConfig {

    @Autowired
    Environment env;

    @Bean
    public TestBean testBean() {
        TestBean testBean = new TestBean();
        testBean.setName(env.getProperty("testbean.name"));
        return testBean;
    }
}

任何在@PropertySource源路径下的${…​}都将解析为一组已经注册到环境中的属性源, 如下:

@Configuration
@PropertySource("classpath:/com/${my.placeholder:default/path}/app.properties")
public class AppConfig {

    @Autowired
    Environment env;

    @Bean
    public TestBean testBean() {
        TestBean testBean = new TestBean();
        testBean.setName(env.getProperty("testbean.name"));
        return testBean;
    }
}

假设my.placeholder在其他的属性源中定义并已经注册(例如, 系统属性或环境变量), 那这个占位符将解析为相关的值. 如果没有, 那么default/path将会被使用, 如果没有默认属性指定和解析, 那IllegalArgumentException将抛出.

1.13.4 语句中的占位符解析

历史上, 元素中的占位符只能依据JVM系统属性或环境变量解析. 这已经不是问题了. 因为Environment抽象通过容器集成, 通过它解析占位符是简单的途径. 这意味着你可以用你喜欢的方式配置解析方式. 你也可以修改查找系统属性和环境变量的优先级, 或者移除他们. 适当的话你还可以添加你自己的源.

具体的, 下面的语句不管哪里定义了customer, 只要他在Environment中有效就能工作:

<beans>
    <import resource="com/bank/service/${customer}-config.xml"/>
</beans>

1.14 注册LoadTimeWeaver

LoadTimeWeaver被Spring用来在加载到JVM时动态转化类.

启用加载时织入, 你可以添加@EnableLoadTimeWeaving到一个@Configuration类上. 如下:

@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}

对应的XML配置,可以使用context:load-time-weaver元素.

<beans>
    <context:load-time-weaver/>
</beans>

一旦为ApplicationContext配置了任何在ApplicationContext内部都可以实现LoadTimeWeaverAware的bean,从而在收到在加载时织入的实例. 组合使用Spring的JPA支持时这一点很有用, 对于JPA类转化时在加载时织入的地方可能是必要的. 访问LocalContainerEntityManagerFactoryBean的文档有更详细介绍. 对AspectJ 加载时织入, 参看使用AspectJ加载时织入.

1.15 ApplicationContext的更多功能

在本章的描述中, org.springframework.beans.factory包提供了管理和操作bean的基本功能, 包括以编程的方式. org.springframework.context包添加了接口ApplicationContext, 这个接口扩展了BeanFactory, 另外扩展了其他接口的功能,用来在一个更应用程序向的风格中提供附加功能. 许多人以一种完全声明的方式使用ApplicationContext, 并不是编程的创建它, 而是依赖支持类如ContextLoader来自动实例化ApplicationContext,将其作为Java EE Web程序启动的一个过程.

在一个倾向框架的风格中加强BeanFactory功能, context包也提供了如下功能:

  • 使用接口MessageSource以i18n风格访问消息
  • 通过ResourceLoader接口访问资源,如URLs和文件
  • 通过使用ApplicationEventPublisher接口实现事件发布, 实现了ApplicationListener接口的命名bean.
  • 多(层次)上下文加载, 通过HierarchicalBeanFactory接口, 使得每个聚焦于特定的层, 如web层
1.15.1 使用MessageSource国际化

ApplicationContext接口扩展了叫MessageSource的接口,因而提供了国际化("i18n")功能. Spring也提供了HierarchicalMessageSource接口, 这个接口能层次性解析消息. 总之这些接口提供的功能依赖于Spring有效处理消息解析的能力. 这些定义在接口的方法包含:

  • String getMessage(String code, Object[] args, String default, Locale loc): 用来从MessageSource获取消息的基本方法. 当指定区域没有消息时, 默认的消息将被采用. 使用标准类库MessageFormat提供的功能, 任何传入的参数将替换值.

  • String getMessage(String code, Object[] args, Locale loc): 本质上跟上面方法一样但有个区别: 没有默认消息指定. 如果没有消息发现, NoSuchMessageException将被抛出.

  • String getMessage(MessageSourceResolvable resolvable, Locale locale): 上面方法中使用的属性被包装到一个叫MessageSourceResolvable的类里面, 这个你可用在本方法中.

ApplicationContext被加载, 它就自动加载上下文中定义的MessageSourcebean. 这个类必须有名字messageSource. 如果找到这个bean, 所有前面的方法的调用将委托给消息源. 如果没有消息源被找到, ApplicationContext尝试找到包含相同名称的父容器. 如果找到了, 这个bean就作为MessageSource使用. 如果ApplicationContext不能找到任何消息源, 则空的DelegatingMessageSource被初始化用来接受上面定义的方法的调用.

Spring提供了两个MessageSource实现, ResourceBundleMessageSourceStaticMessageSource. 二者都实现了HierarchicalMessageSource以便能处理嵌套消息. StaticMessageSource很少使用但提供了编程的方式添加消息到源的方法. 下例展示了ResourceBundleMessageSource:

<beans>
    <bean id="messageSource"
            class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basenames">
            <list>
                <value>format</value>
                <value>exceptions</value>
                <value>windows</value>
            </list>
        </property>
    </bean>
</beans>

这个示例假设你有三个源叫format,exceptionwindows在你的类路径下. 任何解析消息的请求都是以通过对象ResourceBundle解析消息的标准JDK方式处理的. 本例的目的,假设上面的两个消息文件如下:

# in format.properties
message=Alligators rock!
# in exceptions.properties
argument.required=The {0} argument is required.

下面例子展示了编程执行MessageSource功能. 记住所有的ApplicationContext实现也是MessageSource实现, 所以可以被转化为MessageSource接口.

public static void main(String[] args) {
    MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
    String message = resources.getMessage("message", null, "Default", null);
    System.out.println(message);
}

上面的程序运行结果如下:

Alligators rock!

总之, MessageSource定义在一个名叫beans.xml的文件中, 这个文件在你类路径的根目录下. messageSourcebean定义通过basenames属性引用到一系列资源bundles. 这三个文件分别叫format.properties, exceptions.properties, 还有windows.properties存在于你的类路径下, 通过basenames属性传递.

下面例子展示了传递给消息查找的参数. 这些参数转化为String对象并插入查找的占位符中.

<beans>

    <!-- this MessageSource is being used in a web application -->
    <bean id="messageSource" class="org.springframework.context.support.ResourceBundleMessageSource">
        <property name="basename" value="exceptions"/>
    </bean>

    <!-- lets inject the above MessageSource into this POJO -->
    <bean id="example" class="com.something.Example">
        <property name="messages" ref="messageSource"/>
    </bean>

</beans>
public class Example {

    private MessageSource messages;

    public void setMessages(MessageSource messages) {
        this.messages = messages;
    }

    public void execute() {
        String message = this.messages.getMessage("argument.required",
            new Object [] {"userDao"}, "Required", null);
        System.out.println(message);
    }
}

execute()方法调用的结果如下:

The userDao argument is required.

说起国际化("i18n"), Spring的变量MessageSource实现遵循与标准JDK的ResourceBundle相同的区域解析和回退规则. 总之, 继续前面定义的messageSource示例,如果您想解析针对英语消息(en-GB), 那么你需要分别创建format_en_GB.properties, exceptions_en_GB.properties, 和 windows_en_GB.properties.

一般的, 区域解析是受程序的周围环境管理. 接下来的例子中, 依赖于(英国)的消息将被手工的解析:

# in exceptions_en_GB.properties
argument.required=Ebagum lad, the {0} argument is required, I say, required.
public static void main(final String[] args) {
    MessageSource resources = new ClassPathXmlApplicationContext("beans.xml");
    String message = resources.getMessage("argument.required",
        new Object [] {"userDao"}, "Required", Locale.UK);
    System.out.println(message);
}

运行结果如下:

Ebagum lad, the 'userDao' argument is required, I say, required.

你也可以使用MessageSourceAware接口获取任何定义的MessageSource的引用. 任何在ApplicationContext中定义的MessageSourceAware接口实现将在程序上下文的bean创建和配置时使用MessageSource注入.

1.15.2 标准和自定义事件

通过ApplicationEvent类和ApplicationListener接口提供了ApplicationContext的事件处理. 如果bean实现了ApplicationListener接口并部署在上下文中, 每当ApplicationEvent发布到ApplicationContext中时, 这个bean就得到通知了. 本质上, 这是标准的观察者模式.

下面表格描述了Spring提供的标准事件:

表7 内置事件
|Event|Explanation|
|-----|-----------|
|ContextRefreshedEvent|当ApplicationContext被初始化或者刷新后发布(如通过使用ConfigurableApplicationContext接口的refresh()方法调用).这里,"initialized"意味着所有的bean被加载,post-processor被探测并激活, 单例被预初始化, ApplicationContext对象准备就绪可用. 只要上下文没有被关闭, 刷新会被多次触发, 所选的ApplicationContext事实上支持热刷新. 例如XmlWebApplicationContext支持热刷新,但GenericApplicationContext不支持|
|ContextStartedEvent|在ApplicationContext启动后, 使用接口ConfigurableApplicationContext的方法start()将发布该事件.这里, "启动"意味着所有的Lifecyclebean接收到显式的启动信号. 一般而言, 这个信号在显式停止后用来重启bean, 但它也用来启动那些没有被配置为自动启动的bean(例如, 没有被初始化启动的组件).|
|ContextStoppedEvent|当ConfigurableApplicationContext接口的方法stop()调用后ApplicationContext被停止是发布该事件. 这里"停止"意味着所有Lifecyclebean接收一个显式的停止信号. 结束的上下文可能通过start()调用重启|
|ContextClosedEvent|当ConfigurableApplicationContext接口的方法close()调用后,当ApplicationContext关闭时发布. 这里, 关闭意味着所有单例bean被销毁. 关闭的上下文生命结束. 它不可能被刷新或重启|
|RequestHandledEvent|一个WEB特定的事件,告诉所有HTTP请求被处理的bean. 这个事件在请求完成后被发布. 这个事件仅仅对于使用了DispatcherServlet的web程序有用. |

你也可以创建和发布你自定义的事件. 下面的例子展示了扩展ApplicationEvent基类的一个简单类:

public class BlackListEvent extends ApplicationEvent {

    private final String address;
    private final String content;

    public BlackListEvent(Object source, String address, String content) {
        super(source);
        this.address = address;
        this.content = content;
    }

    // accessor and other methods...
}

发布自定义的ApplicationEvent, 可以调用ApplicationEventPublisher上的publishEvent()方法. 一般这是通过创建一个实现ApplicationEventPublisherAware的类并将其注册为一个Spring bean. 如下所示:

public class EmailService implements ApplicationEventPublisherAware {

    private List<String> blackList;
    private ApplicationEventPublisher publisher;

    public void setBlackList(List<String> blackList) {
        this.blackList = blackList;
    }

    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void sendEmail(String address, String content) {
        if (blackList.contains(address)) {
            publisher.publishEvent(new BlackListEvent(this, address, content));
            return;
        }
        // send email...
    }
}

在配置时, Spring探测到实现了ApplicationEventPublisherAware接口的EmailService, 并自动调用setApplicationEventPublisher(). 实际上,传入的参数是Spring容器自身. 你通过ApplicationEventPublisher接口与之交互.

为了接收自定义的ApplicationEvent, 你可以创建实现了ApplicationListener接口的类并注册为bean. 如下所示:

public class BlackListNotifier implements ApplicationListener<BlackListEvent> {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    public void onApplicationEvent(BlackListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

注意ApplicationListener是使用自定义事件为泛型的参数的(前面例子中的BlackListEvent).这意味着onApplicationEvent()方法能保持类型安全, 防止任何类型转换. 只要愿意你可以注册任意多的监听器, 但要注意,默认情况下事件监听器是同步接收事件的. 这意味着publishEvent()方法会阻塞直到所有监听器处理完事件. 一个同步和单线程的好处是,当监听器获取到一个事件, 它就会在发布者的事务上下文可用时进行操作. 如果其他事件发布策略时必要的, 可参看Spring接口ApplicationEventMulticaster接口文档.

下面例子展示了用来注册和配置上面所述每个类的bean定义:

<bean id="emailService" class="example.EmailService">
    <property name="blackList">
        <list>
            <value>[email protected]</value>
            <value>[email protected]</value>
            <value>[email protected]</value>
        </list>
    </property>
</bean>

<bean id="blackListNotifier" class="example.BlackListNotifier">
    <property name="notificationAddress" value="[email protected]"/>
</bean>

总体来看, 当emailServicesendEmail()方法被调用后, 如果有任何在黑名单邮件消息, 自定义的事件BlackListEvent就发布了. blackListNotifierbean作为ApplicationListener被注册, 并接收BlackListEvent, 这里可以通知到恰当的某些部分.

基于注解的事件监听器

从Spring4.2开始, 你可以在任意的通过EventListener注解的bean方法上注册事件监听器. BlackListNotifier可以重写如下:

public class BlackListNotifier {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    @EventListener
    public void processBlackListEvent(BlackListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

这个方法签名再次声明了它监听的事件类型, 但这次, 没有实现特定的接口并有灵活的名称. 这个事件类型也可以通过泛型缩小范围. 只要在继承架构内真实类型可以解析泛型参数即可.

如果你的方法应该监听多个事件或者你想用无参来定义它, 这个事件类型也可以指定在注解上面. 如下所示:

@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
    ...
}

通过使用condition属性可以为运行时添加过滤, 可以定义一个SpEL表达式, 用来匹配特定事件调用该方法.

下面例子展示了我们的notifier可以重写, 仅在content属性等于my-enent的时候来调用:

@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlackListEvent(BlackListEvent blEvent) {
    // notify appropriate parties via notificationAddress...
}

每个SpEL表达式依赖于专有上下文解析. 下表列出上下文可以用的项目, 这些你可以用来条件化处理事件:

表8 事件SpEL可用的元数据
|名称|位置|描述|示例|
|----|----|----|----|
|Event|root object|实际上是ApplicationEvent|#root.event|
|Arguments array|root object|用来调用目标的参数(数组)|#root.args[0]|
|Argument name|evaluation context|方法参数名字. 如果因为某种原因,名称不可用(例如因为debug信息),参数名称也可以基于#a<#arg>使用, 其中#arg代表参数索引(从0开始)|#blEvent#a0 (你也可以使用#p0#p<#arg>记号作为别名)|

注意#root.event提供了访问底层事件的入口, 就算你的方法签名实际指向发布的任意对象.

如果你需要发布另一个事件的结果作为事件, 你可以改变方法签名, 使其返回要发布的事件, 如下:

@EventListener
public ListUpdateEvent handleBlackListEvent(BlackListEvent event) {
    // notify appropriate parties via notificationAddress and
    // then publish a ListUpdateEvent...
}

这个新方法为每个上面方法处理的BlackListEvent发布了一个ListUpdateEvent事件. 如果需要发布若干事件, 则需要返回事件的Collection.

异步监听器

如果你想要一个特定的监听器去异步处理事件, 你可以重用标准的@Async支持. 如下:

@EventListener
@Async
public void processBlackListEvent(BlackListEvent event) {
    // BlackListEvent is processed in a separate thread
}

当使用异步事件时有如下限制:

  • 如果事件监听器抛出Exception, 它将不会传递给调用者, 查看AsyncUncaughtExceptionHandler有更多细节.

  • 此类监听器不能发送回复. 如果你需要将处理结果作为事件发布, 需要手工将ApplicationEventPublisher注入.

有序监听器

如果你需要一个监听器在另一个之前调用, 你可以添加@Order注解到方法上, 如下:

@EventListener
@Order(42)
public void processBlackListEvent(BlackListEvent event) {
    // notify appropriate parties via notificationAddress...
}

泛型事件

你也可以使用泛型深化定义事件结构. 考虑使用EntityCreatedEvent<T> 其中T是创建的真实实体. 例如, 你可以仅仅通过EntityCreatedEventPerson创建事件监听器:

@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
    ...
}

因为类型擦除, 只有在触发的事件对事件侦听器筛选的泛型参数进行转述时, 此操作才有效(也就是, 形如class PersonCreatedEvent extends EntityCreatedEvent<Person> { …​ }).

在特定场景下, 如果所有事件允许相同的结构(如前面例子中的事件案例),这可能会变得相当乏味. 在类似示例中, 你可以实现ResolvableTypeProvider来指导框架跨越运行时环境所能提供的. 如下:

public class EntityCreatedEvent<T> extends ApplicationEvent implements ResolvableTypeProvider {

    public EntityCreatedEvent(T entity) {
        super(entity);
    }

    @Override
    public ResolvableType getResolvableType() {
        return ResolvableType.forClassWithGenerics(getClass(), ResolvableType.forInstance(getSource()));
    }
}

1.15.3 便捷访问底层资源

为了以最优来使用应用程序上下文的使用和理解,您应该使用Spring的Resource抽象(如Resource一章中所描述的)重新调整.

程序上下文是一个ResourceLoader,用来加载Resource对象. Resource本质上是个JDKjava.net.URL类的特性增强版本. 实际上, Resource的实现封装了java.net.URL实例. Resource能以一种透明的方式获取任何位置的底层资源, 包含类路径, 系统路径, 任何使用标准URL描述的地方, 还有其他的变量. 如果资源路径字符串是个没有特殊前缀的字符串, 那么这些资源的来源是特定的,并且适合于实际的应用程序上下文类型.

你可以配置一个实现了指定借口ResourceLoaderAware的bean部署到应用上下文, 用来自动在程序上下文初始化时作为ResourceLoader传入. 你也可以暴露类型为Resource的属性, 用来访问静态资源. 他们像其他属性一样被注入. 当bean被部署后, 你可以像简单的String路径一样指定这些Resource属性, 并依赖自动从字符串转化为真实的Resource对象.

位置路径或提供给ApplicationContext参数的路径实际上是资源字符串, 并且简单的形式下, 将根据特定的上下文实现被合适的对待. 如ClassPathXmlApplicationContext将以类路径对待一个简单的路径. 你可以使用特定的前缀迫使定义从类路径或URL加载, 不管真实的上下文是什么.

1.15.4 Web程序便捷的ApplicationContext实例化

通过使用如ContextLoader你可以创建ApplicationContext实例. 当然, 你也可以使用ApplicationContext的其中一个实现类编程方式创建ApplicationContext.

你可以通过ContextLoaderListener注册ApplicationContext, 如下:

<context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>/WEB-INF/daoContext.xml /WEB-INF/applicationContext.xml</param-value>
</context-param>

<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

监听器检查contextConfigLocation参数. 如果参数不存在, 默认使用/WEB-INF/applicationContext.xml. 当这个参数存在时, 监听器通过预定义的字符(逗号,分号,空格)分割字符串并使用它们来作为应用上下文查找的路径. Ant风格的路径模式也支持. 例如:/WEB-INF/*Context.xml(对于包含在WEB-INF目录下的所有的以Context.xml结尾的文件)和/WEB-INF/**/*Context.xml(对于所有WEB-INF目录下任何子目录的文件).

1.15.5 部署SpringApplicationContext为JavaEE RAR 文件

部署Spring的ApplicationContext为JavaEE RAR 文件是可能的, 将封装上下文和所有需要的类和库JAR文件到一个JAVA EE RAR部署单元. 这等同于启动了一个单独的ApplicationContext(仅在Java EE 环境宿主), 能够访问Java EE 服务器设施. RAR部署是一种更自然的替代方式去部署一个没有头文件的WAR文件, 实际上, 没有任何HTTP入口的WAR文件经常用在Java EE环境中, 用来启动一个ApplicationContext.

对于不需要HTTP入口但有消息端点和定时任务的程序上下文, RAR部署是理想化的. 这类上下文中的bean能使用程序服务器资源, 比如JTA事务管理和JNDI绑定JDBCDataSource实例以及JMSConnectionFactory实例, 并且也能注册平台的JMX服务器--都是通过Spring标准事务管理和JNDI还有JMX支持设施的. 通过Spring的TaskExecutor抽象, 应用组件也可以与应用服务器JCAWorkManager交互.

关于RAR部署的更多配置细节可以查看文档SpringContextResourceAdapter类.

对于一个简单的将ApplicationContext部署为Java EE RAR 文件来说:

  1. 打包所有程序类到RAR文件(标准的JAR文件, 只是后缀不同). 添加所需的库JAR到RAR架构的根下. 添加META-INF/ra.xml部署描述符(见javadocSpringContextResourceAdapter)还有相关的XML定义文件(典型的如META-INF/applicationContext.xml).

  2. 将结果RAR文件丢到应用程序服务器的部署路径下.

1.16 BeanFactory

BeanFactoryAPI提供了基本的Spring IoC容器功能. 它的专门的约定几乎是用来与Spring其他部分或相关三方框架集成, 在高级的容器GenericApplicationContext种, 它的DefaultListableBeanFactory实现是一个关键的代理.

BeanFactory和相关的接口(如BeanFactoryAware,InitializingBean,DisposableBean)是与其他组件重要的集成点. 不需要任何注解或反射, 他们允许有效的在容器和它的组件间交互. 应用级别的bean可能使用相同的回调接口但一般更喜欢声明式的DI, 不管通过注解或通过编程方式配置.

注意核心BeanFactoryAPI和它的DefaultListableBeanFactory实现不会假设配置格式或任何使用的组件注解. 所有这些调剂通过扩展(如XmlBeanDefinitionReaderAutowiredAnnotationBeanPostProcessor)并在共享的作为元数据呈现的BeanDefinition对象上操作. 这是使得Spring容器灵活可扩展的关键.

1.16.1 BeanFactory 或者 ApplicationContext?

本节解释了BeanFactoryApplicationContext容器级别的不同和启动时的含义.

不管你是否有理由, 你应该使用ApplicationContext, GenericApplicationContext和它的子类AnnotationConfigApplicationContext是为自定义启动更一般的实现. 这些是Spring核心容器一般目的而言主要的入口点: 配置文件加载, 触发类路径扫描, 编程注册bean定义和注解类, 还有(从5.0开始)注册函数式bean定义.

因为ApplicationContext包含了BeanFactory的所有功能, 它一般是更建议使用的, 除非有所有bean的处理控制的需求. 在ApplicationContext(如GenericApplicationContext实现)内, 按惯例几种bean被探测(也就是, 通过名字或类型-特别的,post-processors), 而DefaultListableBeanFactory并不知道任何特定的bean.

对许多容器扩展特性来说, 如注解处理和AOP代理, BeanPostProcessor扩展点是关键. 如果你仅仅使用普通的DefaultListableBeanFactory, post-processors默认不会被探查和激活. 这种情况可能是疑惑的, 因为你的配置没有任何错误. 当然此场景下 , 容器需要通过附加的设置完全启动.

下面列出了通过BeanFactoryApplicationContext接口和实现提供的特性.

表9 特性矩阵
|Feature|BeanFactory|ApplicationContext|
|--------|-----------|---------------------|
|bean实例化/装配|是|是|
|集成生命周期管理|否|是|
|BeanPostProcessor自动注册|否|是|
|BeanFactoryPostProcessor自动注册|否|是|
|MessageSource便捷访问(对于内部)|否|是|
|内置ApplicationEvent发布机制|否|是|

显式使用DefaultListableBeanFactory注册一个post-processor, 你需要编程方式调用addBeanPostProcessor如下:

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// populate the factory with bean definitions

// now register any needed BeanPostProcessor instances
factory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor());
factory.addBeanPostProcessor(new MyBeanPostProcessor());

// now start using the factory

为一个普通DefaultListableBeanFactory应用BeanFactoryPostProcessor, 你需要调用它的postProcessBeanFactory方法, 如下:

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(new FileSystemResource("beans.xml"));

// bring in some property values from a Properties file
PropertySourcesPlaceholderConfigurer cfg = new PropertySourcesPlaceholderConfigurer();
cfg.setLocation(new FileSystemResource("jdbc.properties"));

// now actually do the replacement
cfg.postProcessBeanFactory(factory);

两个例子中, 显式注册步骤是不必要的, 这就是在Spring背景的程序中, ApplicationContext变量比DefaultListableBeanFactory更好用. 特别是当一个典型企业应用中依赖于BeanFactoryPostProcessorBeanPostProcessor实例进行扩展时.

03-10 01:46