Spring系列(零) Spring Framework 文档中文翻译

Spring 框架文档(核心篇1和2)

Version 5.1.3.RELEASE


最新的, 更新的笔记, 支持的版本和其他主题,独立的发布版本等, 是在Github Wiki 项目维护的.

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

  • 核心 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创建并实例化后, 你就有了一个可执行系统或程序.
The Spring Ioc Container

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.xml classpath:/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定义的属性, 下面表格列出了这些属性:

表 1. Bean定义中的属性

属性 参看
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指定名称的动机是使用内部类和自动装配.

bean命名约定

给bean命名遵循与给实例域命名相同的约定. 也就是,使用小写字母开头的骆驼命名法. 例如: accountManager, accountService, userDao, loginController等.

坚持命名bean可以使你的配置易读易理解. 还有, 如果使用Spring AOP, 当给名字相关的一些列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"/>

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

Java 配置

如果使用Java配置, @Bean注解可以提供别名, 参看:如何使用@Bean注解.

1.3.2 实例化Bean

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

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

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

内部类名称

如果你想要为一个静态内部类配置bean, 你就必须使用内部类的双名称.

例如, 你有个类定义为SomeThing,在com.example包下. SomeThing有个静态内部类为OtherThing, 那它的class属性的值将是com.example.SomeThing$OtherThing

注意内部类和外部类之间需要使用$字母分割.

使用构造器实例化

当使用构造器创建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 依赖

从此处开始升级到5.2.0版本

典型的企业应用不是由单个对象组成的(或用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>

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

参数是从0开始的.

构造参数名称

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

<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容器实例加载.

构造器注入还是Setter注入

因为可以同时混用构造器和Setter注入, 一种比较好的原则是: 强制的依赖使用构造器注入, 可选的依赖则可以使用setter或配置方法. 注意: setter方法上使用@Required注解将使得对应属性成为必须的依赖.

Spring团队提倡构造器注入, 这会让你实现的程序组件是不可变对象并且可以保证依赖不是null. 更多的好处是, 构造器注入返回给客户端的组件总是完全被初始化的状态. 还有个边缘效应, 就是大量的构造函数参数是一种不好的代码味道, 暗示着这个类承载了太多的责任并且需要被合理的重构, 实现关注点分离.

Setter注入应该在依赖是可选的前提下优先使用, 这些依赖可以被赋以合理的默认值. 另外, 非null验证必须要在代码使用依赖的所有地方进行. 使用setter注入的一个好处是: setter方法使得对象的重配置和重注入更易于控制. 通过JMX MBeans管理就是一种setter注入的案例.

使用DI使得特定类更加有意义. 有时候, 当你使用三方类库时, 你并没有源代码, 此时你将手握DI选择权. 例如, 如果三方类没有暴露任何setter方法, 那么使用构造器注入将是唯一途径.

依赖解析过程

容器按照下面阐述的对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时.

循环依赖

如果用占主导地位的构造器注入, 就可能会导致无法解析的循环依赖.

例如: 类A需要类B,通过构造器参数注入, 相反类B也需要类A,通过构造器注入. 如果配置A,B相互注入给对方, Spring的IoC容器就会在运行时检测到循环引用, 并抛出BeanCurrentlyInCreationException.

一种处理这种情况的方法是编辑源代码, 将一个的setter注入改为构造器注入. 相应地避免使用构造器注入而仅仅使用setter. 换句话说, 虽然不提倡, 但可以使用setter注入配置循环依赖.

不同于典型的案例(即没有循环依赖), 在bean A和bean B之间的循环依赖将迫使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元素的local属性不再支持4.0版本的beans XSD, 因为它不再提供常规bean引用的值. 当升级到4.0时, 请修改现有的idref local引用, 修改为idref 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>

4.0的beans XSD 后ref元素的local属性不再支持, 因此不需要用它为一个正常的bean引用提供值了. 当升级到4.0时请注意修改现有的ref localref 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">administrator@example.org</prop>
            <prop key="support">support@example.org</prop>
            <prop key="development">development@example.org</prop>
        </props>
    </property>
    <!-- results in a setSomeList(java.util.List) call -->
    <property name="someList">
        <list>
            <value>a list element followed by a reference</value>
            <ref bean="myDataSource" />
        </list>
    </property>
    <!-- results in a setSomeMap(java.util.Map) call -->
    <property name="someMap">
        <map>
            <entry key="an entry" value="just some string"/>
            <entry key ="a ref" value-ref="myDataSource"/>
        </map>
    </property>
    <!-- results in a setSomeSet(java.util.Set) call -->
    <property name="someSet">
        <set>
            <value>just some string</value>
            <ref bean="myDataSource" />
        </set>
    </property>
</bean>

字典map的键值,或者集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">administrator@example.com</prop>
                <prop key="support">support@example.com</prop>
            </props>
        </property>
    </bean>
    <bean id="child" parent="parent">
        <property name="adminEmails">
            <!-- the merge is specified on the child collection definition -->
            <props merge="true">
                <prop key="sales">sales@example.com</prop>
                <prop key="support">support@example.co.uk</prop>
            </props>
        </property>
    </bean>
<beans>

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

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

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="someone@somewhere.com"/>
    </bean>

    <bean name="p-namespace" class="com.example.ExampleBean"
        p:email="someone@somewhere.com"/>
</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的引用.

p-命名空间不如标准xml格式灵活. 例如, 声明属性引用的这种格式与使用ref的格式冲突. 而标准的XML则不会. 我们强烈建议你细心选用合适的方式, 与团队沟通来避免产生同时使用三种格式的XML文档.

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="something@somewhere.com"/>
    </bean>

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

</beans>

c:命名空间使用与p:命名空间相同的约定来设置构造函数参数(对于引用使用-ref后缀).相似的, 需要声明在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="something@somewhere.com"/>

鉴于XML语法, 索引必须以_开头, 因为XML属性名称不能以数字开头(虽然一些IDE可以). 相应的,元素<constructor-arg>中也可使用索引数字,但不常用, 因为声明时的顺序已经足够了.

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

复合属性名称

在设置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" />

depends-on属性能够指定初始化期间的依赖. 仅在单例bean中, 指定相应的销毁期依赖. 用depends-on关系定义的依赖bean将被首先销毁, 优先于它自身修饰的bean. 因此, depends-on可以用来控制关闭顺序.

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指定自动装配模式. 自动装配功能有四种模式. 你可以任选其一. 下表描述了这四种模式:

表2. 自动装配模式

模式 说明
no (默认)不使用自动装配. bean的引用必须使用ref元素. 对于较大部署不推荐修改这个模式设置. 因为显式指定的协作者提供了各大的控制权和可读性. 某种意义上是一个系统的架构文档.
byName 通过属性名称自动装配. Spring通过名称查找需要自动装配的bean. 例如: 如果一个bean定义按名字自动装配, 并且包含了一个master属性(也就是同时有setMaster(..)方法), Spring会查找名为master的bean定义, 并且将其设置到属性.
byType 如果容器中有匹配property类型的bean就自动装配. 如果多于一个, 将抛出致命异常, 这表明可能不应该使用byType匹配. 如果没有匹配的bean, 则忽略(属性不被赋值)
constructor byType类似但是是提供给构造函数参数的. 如果容器中没有精确类型的bean, 致命错误将发生

通过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).

autowire-candidate属性被设计为仅对基于类型的装配有效. 对于通过name引用的显式引用无效, 即使指定的bean没有被标记为候选也会被解析. 结果就是当名字匹配时, 通过name自动装配仍然会注入bean.

你可以通过基于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类.

另一个访问不同作用域的方法是使用ObjectFactory/Provider连接点. 参看: 不同作用域的bean依赖.

你可以发现ServiceLocatorFactoryBean(在包org.springframework.beans.factory.config)是很有用的.

任意方法替换

一种比查找方法注入不那么有用的形式是在一个管理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.

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

表 3. Bean 作用域

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中是可用的.

从Spring3.0开始, 线程级别的作用域是可用的, 但不是默认注册的. 关于更多请参看SimpleThreadScope的文档. 关于如何注册这个作用域或者其他自定义作用域的指导, 请参看自定义作用域.

1.5.1 单例作用域

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

换个说法, 当你定义了一个bean,并且将其设置为singleton作用域时, Spring IoC容器创建了bean定义的唯一实例. 这个唯一实例是存储在此类单例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的单例作用域:

原型bean

(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请求)并且委托调用真实对象上的方法.

在单例bean之间你也可以使用<aop:scoped-proxy/>, 通过一个中间代理引用, 这个代理是可序列化的,因此能在反序列化时重新获取目标单例bean.

当在一个prototype作用域的bean上声明<aop:scoped-proxy/>时 , 每个请求共享代理对象的方法将会将其引导到新建实例上并调用.

同时, 在作用域安全的方式下, 从一个短作用域访问bean, 作用域代理并不是唯一的方法. 你也可以用ObjectFactory<MyTargetBean>声明你的注入点(也就是,构造器或setter参数或者自动装配域), 允许通过getObject()在需要对象时获取当前实例-- 而不是持有其实例或单独存储之.

作为扩展的变体, 你可以声明ObjectProvider<MyTargetBean>, 这个对象有几个访问方式的变体, 包含getIfAvailablegetIfUnique.

JSR-330 中这被叫做Provider并且用来与Provider<MyTargetBean>一起使用, 并且用get()方法获取. 关于JSR-330更多请参考这里.

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

<?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类代理对象就被创建了.

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实现, 并且像下一个例子一样注册到了容器中.

下一个例子使用SimpleThreadScope, 其包含在Spring中但没有默认注册. 这个教程与你自定义的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>

当你在FactoryBean实现中使用<aop:scoped-proxy/>时, FactoryBean自身是作用域下的bean, 不是从getObject()方法返回的对象.

1.6 自定义Bean的特性

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

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

1.6.1 生命周期回调

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

在现代Spring程序中,JSR-250 的 @PostConstruct@PreDestroy 注解被认为是获取生命周期回调的最佳实践. 使用这些注解意味着你的bean不会耦合到Spring特定的接口中. 更多信息请参看:使用@PostConstruct@PreDestroy.

如果你不想用JSR-250注解但仍然想去除耦合, 考虑元数据中使用 init-methoddestroy-method.

在内部, 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代码没有耦合.

你可以为<bean>元素分配给属性destroy-method一个专门的值(inferred(推断)), 这个值指示Spring自动去探测特定bean上的公共closeshutdown方法. (任何实现了java.lang.AutoCloseablejava.oi.Closeable的类将会匹配到) 你也可以为<beans>元素分配给属性default-destroy-method一个特定值, 用来在所有bean上应用动作(参看默认初始化和销毁方法). 注意, 这个java代码配置的默认行为.

默认初始化和销毁方法

当你不用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配置了多种生命周期机制,并且每个机制都使用了不同的方法名, 那么每个配置方法将会按本提示后面给出的顺序执行. 尽管如此, 如果为多个机制配置了相同的方法名-- 例如, 初始化方法的init()-- 那该方法将只执行一次, 就如上面所阐述的.

为同一个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的扩展. 它添加了两个方法在上下文刷新和关闭时交互.

注意, 标准的org.springframework.context.Lifecycle接口是一个明确的规约, 是为了启动和停止通知使用, 并没有在上下文刷新时应用自动启动. 对于特定bean的自动启动更细粒度的控制(包括启动阶段), 考虑实现接口org.springframework.context.SmartLifecycle代替.

同时请注意, 停止通知不能保证一定在销毁时会发生. 在标准的停止过程下,所有的Lifecycle bean在一般的销毁回调被传播前首先接收一个停止通知. 尽管这样, 在上下文生命周期热刷新时或者在抛弃刷新尝试时, 只有销毁方法会被调用.

启动和停止调用的顺序可能是比较重要的. 如果两个对象间存在依赖关系, 依赖方将在被依赖方之后启动, 并且在被依赖方之后停止. 不过有时候直接依赖是不知道的. 你可能仅仅知道一些类型的对象的优先于另一些类型的对象启动. 这种情况下, 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 基于web的ApplicationContext实现已经提供了当web程序关闭时优雅地关闭容器的代码.

如果你在一个非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接口:

表4 Aware接口

名称 注入的依赖 解释文档索引
ApplicationContextAware 声明ApplicationContext ApplicationContextAware and BeanNameAware
ApplicationEventPublisherAware 封装的ApplicationContext的事件发布 ApplicationContext的附加功能
BeanClassLoaderAware 用来加载bean的类加载器 实例化bean
BeanFactoryAware 声明BeanFactory ApplicationContextAware and BeanNameAware
BeanNameAware 声明的bean的名称 ApplicationContextAware and BeanNameAware
BootstrapContextAware 容器运行其中的资源适配器BootstrapContext, 一般仅JCA可用的ApplicationContext中可用 JCA CCI
LoadTimeWeaverAware 加载时为处理类定义的加载器 Spring框架中使用AspectJ加载时织入
MessageSourceAware 配置的解析消息的资源策略(支持参数化和国际化) ApplicationContext的附加功能
NotificationPublisherAware Spring JMX 提醒发布者 提醒
ResourceLoaderAware 底层访问资源的配置加载器 Resources
ServletConfigAware 容器运行其中的当前ServletConfig,仅在web-aware的Spring ApplicationContext中可用 Spring MVC
ServletContextAware 容器运行其中的当前ServletContext,仅在web-aware的Spring ApplicationContext中可用 Spring MVC

请再次注意, 使用这些接口将使你的代码耦合到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.

ApplicationContext默认预初始化所有的单例. 因此, 重要的一点是(至少对于单例bean)如果你有个(父)bean定义, 并且仅仅想把它作为模板使用, 并且给他指定了类名, 你就必须保证设置了abstract属性为true, 否则应用程序上下文将(尝试)预实例化这个abstractbean.

1.8 容器扩展点

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

1.8.1 使用BeanPostProcessor自定义bean

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

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

BeanPostProcessor实例作用在bean或对象上. 也就是Spring容器实例化一个bean实例后BeanPostProcessor实例开始做他们的工作.

BeanPostProcessor是容器级别的作用域. 仅仅是你使用容器层次结构的时候才是有意义的. 如果你在容器中定义了一个BeanPostProcessor,它将仅仅处理这个容器中的bean. 换句话说, 定义在一个容器中的bean不能被其他容器中的BeanPostProcessor处理, 就算这俩容器都是相同等级层次架构的一部分.

为了改变bean的定义(也就是定义bean的蓝图), 你需要使用BeanFactoryPostProcessor, 这在使用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的初始化过程中, 所以这种早期探测是至关重要的.

编程的方式注册BeanPostProcessor实例

建议注册BeanPostProcessor的方式就是前面提到的,通过ApplicationContext自动检测, 同时你也可以用编程的方式使用ConfigurableBeanFactory的方法addBeanPostProcessor()进行注册. 当你需要评估注册前的条件逻辑, 或者想要在一个层次结构中跨上下文拷贝post-processor时, 编程的方式就很有用了. 注意, 编程添加的BeanPostProcessor实例并不关心Ordered接口. 这里, 是注册的顺序决定了执行的顺序. 同时注意, 编程注册的BeanPostProcessor接口总是在那些自动检测注册的接口之前处理, 就算显式指定了顺序也是如此.

BeanPostProcessor实例和AOP自动代理

实现了BeanPostProcessor接口的类是特殊的并且被容器区别对待. 所有BeanPostProcessor实例和他们直接饮用的bean都是在启动时实例化的, 作为ApplicationContext启动阶段的一个特殊部分. 接下来, 所有BeanPostProcessor实例将被以一种风格注册并应用到所有容器的bean上. 因为AOP自动代理是作为BeanPostProcessor自身的一个实现. 所以BeanpostProcessor实例和它们直接引用的bean都不符合自动代理的条件,因此,它们中没有织入的方面。

对于这样的bean, 你可能看到如下日志信息: Bean someBean 不符合所有BeanPostProcessor接口处理的条件(例如:不符合自动代理).

如果你有通过自动装配或@Resource注解(可能会回到自动装配)方式装配bean到BeanPostProcessor中, Spring在查找类型匹配的依赖候选时, 可能会访问到不期望的bean, 使得他们不符合自动代理或者其他bean post-processor处理. 例如, 如果你有个@Resource的依赖项, 其中字段或setter名称并不直接对应到bean的声明名称, 并且不使用name属性, 那么Spring将按类型访问其他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实例(也就是通过配置元数据创建的对象), 那你就需要使用BeanPostProcessor(之前章节讨论的通过BeanPostProcessor自定义bean). 虽然技术上可以使用BeanFactoryPostProcessor与bean实例协作(例如, 通过使用BeanFactory.getBean()), 但这样做将导致早产的bean实例, 侵犯了标准的Spring容器生命周期.这样容易导致副作用, 例如绕开bean处理.

同时BeanFactoryPostProcessor实例是容器级别的作用域. 这仅仅在使用容器层次结构的时候是有意义的. 如果你容器中定义了一个BeanFactoryPostProcessor, 那他就只在这个容器定义的bean起作用.在一个容器中的bean定义不能被另一个容器中的BeanFactoryPostProcessor处理, 就算两个容器都是相同层次结构的一部分.

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

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

与使用BeanPostProcessor一样, 你通常不想要配置BeanFactoryPostProcessor为延迟加载. 如果没有其他bean引用一个Bean(Factory)PostProcessor, 那post-processor将不会被初始化. 因此, 将其标记为延迟初始化是会被忽略的, 并且Bean(Factory)PostProcessor将会被更早的初始化, 就算你设置了<beans/>元素的default-lazy-init=true.

例子: 类的名字替换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属性:

你可以使用PropertySourcesPlaceholderConfigurer去替换类的名称, 这有时候是有用的, 当你不得不在运行时选择一个特定的类实现的时候. 下面展示了这种情况:

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

如果这个类不能在运行时被解析为一个有效的类, 解析失败发生在当它将要被创建时, 对于一个非延迟加载的bean, 也就是在ApplicationContextpreInstantiateSingletons()阶段.

示例: 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

指定的覆写的值总是字面量. 他们不会被解析为bean的引用. 当xml bean定义中指定一个bean的引用时,这个约定同样被使用.

自从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 基于注解的容器配置

Spring中是否注解配置要优于XML的配置

基于注解的配置是否优于XML, 关于注解的介绍引出了这个问题. 简短的答案是"看情况". 长一点的答案是:每种方式都有其优缺点, 经常地, 这取决于开发人员决定哪种方式更适合他们. 基于他们定义的方式, 注解提供了许多上下文, 这导致了更短而更闭联的配置. 而XML更精通于不接触代码或无需重新编译而组装组件.一些开发人员更喜欢将装配接近源码的方式, 而其他人则认为注解的类不再是POJO了, 而且配置变得零散而难以控制.

不管何种选择, Spring都能兼容两种方式甚至混用它们. 值得指出,通过java配置选项, Spring使得注解在一个非侵入方式下使用, 不需要触碰目标组件的源码, 至于工具, 所有的配置方式都被Spring Tool Suite 支持.

一种替换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. 这些注解的细节可以在相关章节找到.

注解注入在XML注入之前执行. 因此, XML配置将覆盖了通过两种方式装配的注解.

通常, 你可以像分离的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.)

<context:annotation-config/>仅仅查找相同应用上下文定义的bean上的注解. 这意味着如果你为一个DispatcherServletWebApplicationContext上添加标签<context:annotation-config/>, 它仅仅在你的Controller上检查@Autowired, 而不会在service上. 可参看:DispatcherServlet获取更多信息.

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类内部(例如, 放在初始化方法里面). 这样就强制了必填属性的引用和值, 就算你在容器外使用类也是如此.

@Required注解在Spring framework 5.1开始起被否定了, 更倾向于必填设置通过构造函数(或使用随bean属性的setter方法InitializingBean.afterPropertiesSet()的自定义实现) .

1.9.2 使用@Autowired

JSR 330 的注解@Inject 能够替换在本节例子中使用的Spring注解@Autowired. 这里(bean的标准注解)有更多细节.

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

public class MovieRecommender {

    private final CustomerPreferenceDao customerPreferenceDao;

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

    // ...
}

从Spring 框架4.3 开始, 如果目标bean只定义了一个构造函数, 那么@Autowired注解就不是必须的. 但如果有多个构造函数, 那至少需要注解其中的一个来告诉容器使用哪个.

你也可以将@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;
    }

    // ...
}

确保你的目标组件(如MovieCatalogCustomerPreferenceDao)和使用@Autowired注解的地方是类型一致的. 否则,注入可能会因为运行时没有匹配到类型而失败.

对于XML定义的bean或组件类通过classpath扫描, 容器通常可以预知类型. 但对于@Bean工厂方法, 你需要确保返回类型要足够明确. 对于那些实现了多个接口的组件, 或隐式的引用到具体实现类型的组件, 考虑在工厂方法返回值将其声明为特定类型(至少要指定为与注入点引用的bean需要的类型).

你也可以为需要一个数组的域或方法提供一组特定类型的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;
    }

    // ...
}

如果你想指定集合数组中的元素顺序, 你可以给目标bean实现org.springframework.core.Ordered接口或者使用@Order或者标准的@Priority注释. 否则他们的顺序遵循容器中定义的相关目标bean注册的顺序.

你可以在目标类级别上声明@Order注解, 以及在@Bean方法上, 这可能是单独的bean定义(在使用同一个bean类的多个定义的情况下). @Order值可能影响注入点的优先级, 但要知道, 它不影响单例的启动顺序, 而这是由依赖关系和@DependsOn声明决定的正交关系.

注意标准的javax.annotation.Priority注解不能在@Bean级别上使用, 因为它不能声明在方法上. 它的语义可以通过@Order值与每个类型的单一bean上使用@Primary相结合来建模.

甚至类型化的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;
    }

    // ...
}

每个类仅有一个构造函数可以被标记为必填, 但可以有多个注解的构造函数. 那种情况下, 每个都将被作为候选, 并且Spring使用最大参数的构造函数, 这个构造函数可以满足依赖.

建议将@Autowired所需的必填属性置于@Required注解之上. 必填属性表明属性不是为了自动装配必须要的. 属性将被忽略如果它不能被自动装配. 而@Required, 另一方面它更强, 它可以以容器支持的任何方式设置属性. 如果没有值注入, 相关的异常将会抛出.

作为替换, 你可以使用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() {
    }

    // ...
}

@Autowired, @Inject, @Resource, 和 @Value 是被Spring的BeanPostProcessor实现所处理. 这意味着你不能将这些注释应用到你自己的BeanPostProcessor或者BeanFactoryPostProcessor类型. 这些类型必须显式的使用XML或Spring@bean方法装配.

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的名称被认为是默认的指定值. 因此,你可以定义一个idmain的bean替代内部的指定元素, 结果是一样的. 虽然你可以使用bean名称引用特定的bean, @Autowired根本上是使用可选语义的修饰语进行类型驱动注入.这就意味着限定的值,即便有名称后备, 总是会限定在匹配类型范围内. 他们语义上不会表达为对一个beanid的引用. 好的限定值是main,MEMApersistent, 表达独立于bean的id外的一种特定角色, 这个bean的id可能是自动生成的匿名定义, 就像前面例子中一样.

限定还可被用到集合, 就像前面讨论的一样, 如:Set<MovieCatalog>. 这种情况下, 所有匹配的bean, 根据声明的限定, 是作为集合注入. 这暗示了限定符不需要是唯一的. 相反他们构成了过滤标准. 例如, 你可以使用相同的限定值"action"定义多个MovieCatalogbean, 所有他们都被注入到使用@Qualifier("action")注解的集合Set<MovieCatalog>中.

在类型匹配的候选时, 让限定值依赖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;
    }
}

注解提供的名称将会被接收CommonAnnotationBeanPostProcessor通知的ApplicationContext解析为bean的名称. 名称可以通过JNDI解析, 如果你显式配置了Spring的SimpleJndiBeanFactory. 尽管可以, 但我们建议你依赖默认的行为并使用Spring的JNDI查找来保证间接级别.

在没有显式的名称指定的@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...
    }
}

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

如同@Resource, @PostConstruct@PreDestroy注解是从JDK6到8成为标准的java类库的一部分. 尽管这样, 整个javax.annotation包从JDK9的核心java模块中独立了, 并最终在JDK11中被删除.如果要用, 则需要从Maven中心引用javax.annotation-api架构, 简单的将其添加到类路径下.

1.10 类路径扫描和托管组件

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

Spring 3.0 开始. 通过Spring的Java配置项目提供的一些特性成为了Spring核心的一部分. 这就允许你使用Java而不是传统的XML来定义bean. 可以参看@Configuration, @Bean, @Import, 和 @DependsOn注解示例来了解如何使用这些特性.

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{
    ...
}

为了简洁, 上面例子可以使用注解的值属性(也就是@ComponentScan("org.example")).

下面是对应的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>

使用<context:component-scan>隐式的启用了<context:annotation-config>的功能. 当使用<context:component-scan>时就不需要在包含<context:annotation-config>元素了.

类路径包的扫描需要相关的目录包含在类路径下. 当你使用Ant打包JARs是, 确保没有为JAR任务打开"仅文件"开关. 同时, 类路径目录可能会在一些环境下由于安全策略而没有暴露--例如, JDK1.7.0-45的独立app或更高(需要信任类库设置在你的清单中, 参看http://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources).

在JDK9的模块路径(Jigsaw)下, Spring类路径扫描通常按预期执行. 尽管如此, 请确保你的组件类导出在module-info下. 如果你期望Spring调用你类中的非公有变量, 请确保他们是'opened'(也就是,在module-info描述符下,他们使用opens声明替换exports声明).

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

你可以通过包含annotation-config属性并设置为false来关闭 AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor.

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>

你也可以通过在注解上设置useDefaultFilters=false或者给元素<component-scan/>设置use-default-filters="false"来使默认过滤器失效. 这实际上也使得使用@Component, @Repository, @Service, @Controller, 或 @Configuration 注解的类的自动探测失效了.

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以及自定义限定注解.

除了组件初始化角色, 你也可以将@Lazy注解放在由@Autowired@Inject标记的注入点上. 在这个上下文中, 它最终注入了一个延迟解析代理.

自动装配域和方法是受支持的, 如前所述, 同时对@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的处理或应用其他限制.

你可能声明@Beanstatic, 允许调用它们,而无需将其包含的配置类作为实例创建。这在定义post-processor的bean时有特殊的意义(例如, 类型BeanFactoryPostProcessorBeanPostProcessor), 因为这样的bean会被容器生命周期中更早的初始化, 并且在这个点上应该避免触发其他配置部分.

调用静态的@Bean方法从来不会被容器切入, 就算是@Configuration类(如前所述), 由于技术限制: CGLIB 子类仅仅能覆盖非静态方法. 因此, 对另一个@Bean方法的直接调用具有java语义, 结果一个独立的实例直接由工厂方法返回.

@Bean方法的Java语言的可见性不会立即对Spring容器中的bean定义产生影响. 你可以自由的在非@Configuration类中声明工厂方法, 或者在任何地方声明静态方法. 尽管可以, @Configuration类中普通的@Bean方法需要是可覆盖的-- 也就是他们不能被声明为private或者final.

在提供的组件或配置类中@Bean方法可以被发现, 同样在java8的由组件和配置类实现的接口中的默认实现方法也可以. 从Spring4.2开始, 这就允许在组合复杂配置时有更大的灵活性, 甚至通过Java8的默认办法使多继承成为可能.

最终,一个类中可能持有相同bean的多个方法, 作为多工厂方法的安排, 这些取决于于运行时可用依赖. 这与选择最佳构造函数或配置中的工厂方法是相同的算法: 在构造时会选择具有最大数量的依赖项的变量,即容器如何在多个@Autowired构造函数之间进行选择。

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 {
    // ...
}

如果你不想依赖默认的bean命名策略, 你可以提供自定义的bean命名策略. 首先,实现BeanNameGenerator接口, 并且保证包含了无参构造函数. 接着, 当配置扫描者的时候提供全限定类名, 如下例子中的注解和bean定义展示的:

@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模式.

为作用域解析提供一个自定义策略而不是依赖注解的方式, 你可以实现接口ScopeMetadataResolver. 确保包含一个无参构造函数. 接着你在配置扫描的时候提供一个全限定名, 如下例子中的注解和bean定义所示:

@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 {
    // ...
}

对大多数基于注解的变体, 记住注解元数据是绑定到类定义自身的, 使用XML允许给相同类型的多个bean提供不同的限定元数据, 因为元数据是提供给每个实例,而不是每个类型.

1.10.9 候选组件生成索引

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

@ComponentScan<context:component-scan>指令必须保持不变, 以请求上下文扫描特定包中的候选. 当ApplicationContext探测到这么个索引, 则自动使用它而不是扫描类路径.

生成索引, 要给每个包含被扫描的目标组件的模块添加一个额外的依赖. 下例展示了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"
}

对于Gradle4.6及以后版本, 依赖需要声明在annotationProcessor配置中, 如下例子所示:

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

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

当在你的IDE中使用此模式的时候, spring-context-indexer必须作为注解处理器注册, 来确保当候选组件更新时索引页同步更新.

META-INF/spring.components存在在类路径下时, 索引自动生效. 如果索引是特定对某些库(或用例)可用但不是为整个程序构建时, 你可以通过spring.index.ignore设置为true来回退到标准的类路径排列(好像根本不存在索引), 无论是作为系统属性或设置在类路径下的spring.properties文件.

1.11 使用JSR330标准注解

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

如果你使用Maven, javax.inject在标准的Maven仓储下是可用的(http://repo1.maven.org/maven2/javax/inject/javax.inject/1/). 你可以添加下面依赖到pom.xml:

<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  {
    ...
}

不同于@Component, JSR-330 的 @Named 和 JSR-250 的 ManagedBean注解不能组合使用. 你应该使用Spring场景模型或者创建自定义注解.

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>

全@Configuration vs 轻@Bean模式

@Bean方法在没有被@Configuration注解的类中声明时, 它们会作为轻型模式被处理. 在@Component中的bean方法或在普通旧的类中的bean方法会被认为是轻型的, 与包含的类有不同的主要目的, 并且@Bean方法是种额外的好处.例如, 服务组件可能通过各合适的组件类附加的@Bean方法暴露管理视图给容器. 此情景下, @Bean方法是通用的工厂方法机制.

不同于全@Configuration, 轻量@Bean方法不能声明内部bean依赖. 相反, 他们操作包含他们组件的内部状态, 以及可选择的它们声明的参数. 这样的@Bean方法因此不能调用其他@Bean方法. 每个这个的方法仅仅是对特定bean的引用的工厂方法, 不需要任何运行时的特定语义. 积极的一个影响是运行时没有CGLIB子类被应用, 因此在类设计方面没有任何限制(也就是,包含类可能是final的等等).

普遍场景下, @Bean方法在@Configuration类内部声明, 要保证full模式总是被使用并跨方法引用因此直接关联到容器生命周期管理. 这就阻止了相同的@Bean方法通过普通Java调用, 从而帮助减少lite模式下难以追踪的bug发生.

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

记住@Configuration类是使用@Component元注解的, 所以他们是扫描的候选. 在前面的例子中, 假设AppConfig声明在com.acme包内部(包括更深层次的包内), 在调用scan()期间它被选中. 依赖refresh(), 它所有的@Bean方法被处理并注册在容器内部.

使用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依赖

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

默认Java配置定义的bean有个公有的closeshutdown方法自动参与销毁回调. 如果你定义了相关的方法又不想在容器关闭时调用,你可以添加@Bean(destroyMethod="")到bean的定义来关闭默认的(inferred)推断模式.

你可能想在获取JNDI资源时默认这么做, 这种资源的生命周期是在程序外管理的. 特别的, 确保在操作DataSource时总是这么做, 因为在Java EE 程序服务器上已知是有问题的.

下面示例展示了如何阻断自动的销毁回调:

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

此外, 使用@Bean方法, 通常使用编程方式使用JNDI查找, 也使用Spring的JndiTemplateJndiLocatorDelegate帮助类或者直接的JNDIInitialContext, 但不是JndiObjectFactoryBean变量(这将迫使你声明返回类型为FactoryBean而不是真实的目标类型, 增加其他@Bean方法中跨引用调用的难度, 这些方法本来要引用提供的资源).

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

@Configuration
public class AppConfig {

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

    // ...
}

直接与java操作时, 你可以做任何你想要做的事情, 而不需依赖容器生命周期.

指定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依赖声明的方法仅适用于在@Configuration内部定义的@Bean. 适用寻常的@Component类不能声明内部bean依赖的情况.

查找方法注入

如前面提到的, 查找方法注入是应该少用的高级特性. 在一个单例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.

根据你bean作用域不同, 这种行为可能不同. 我们这里只讲singleton.

从Spring3.2开始, 不需要添加CGLIB到类路径了, 因为CGLIB类已经被打包到org.springframework.cglib并直接包含在Spring核心JAR中了.


由于在启动时CGLIB动态添加特性, 有一些限制. 特别的, 配置Configuration类必须不是final的. 虽然, 从4.3开始, 配置类中任何构造函数都是被允许的, 包含使用@Autowired或单个的默认构造函数.

如果你想避免CGLIB的限制, 考虑将你的@Bean方法声明在非@Configuration类中(例如使用@Component类代替).@Bean之间的跨方法调用就不再被拦截了, 所以你必须在构造函数和方法级别完全依赖DI.

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

从Spring4.2开始, @Import也支持普通的组件类, 类似于AnnotationConfigApplicationContext.register方法. 如果想避免组件扫描这就是有用的, 通过使用少量配置类作为入口显式定义你的所有组件.

在导入的@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注入等特性.

确保以这种方式注入的依赖关系只属于最简单的类型。@Configuration类在上下文初始化过程中处理得非常早,通过这种方式强制注入依赖项可能会导致意外的早期初始化。尽可能使用基于参数的注入,如前面的示例所示。

同时请特别小心的使用BeanPostProcessorBeanFactoryPostProcessor定义. 这些应该总是声明为static @Bean方法, 不触发容器配置类的初始化. 否则, @Autowired@Value不会在配置类上工作, 因为它过早的作为bean被创建了.

下面展示一个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");
}

@Configuration类上进行构造参数注入是从Spring4.3开始支持的. 注意如果仅有一个构造参数就不需要指定@Autowired在目标bean定义上.在前面例子中, @Autowired无需在RepositoryConfig上指定.

全限定导入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类和他们的依赖于通常的导航基于接口的代码没什么差别.

如果你想对特定bean的启动创建产生影响, 考虑声明他们为@Lazy的(对于首次访问而不是启动)或者@DependsOn的(确保其他bean在当前bean前创建, 超出后者直接依赖的含义).

条件包含@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);
    // ...
}

system-test-config.xml文件中, AppConfig <bean/>没有声明id属性. 这样是可接受的, 它不是必要的, 没有其他bean引用它, 并且也不可能显式通过名字被容器获取. 相似的, DataSourcebean仅仅是按类型装配, 所有显式的id不是严格需要的.

使用context:component-scan/来拾取@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");
    }
}

如早前提到的, @Bean方法,你通常通过使用Spring的JndiTemplate/JndiLocatorDelegate或直接是JNDI的InitialContext来编程实现JNDI查找, 但不使用JndiObjectFactoryBean, 因为这个变量会强制你声明返回值为FactoryBean类型.

profile的字符串可能包含一个简单的profile名称(如production)或者profile表达式. profile表达式允许更复杂的逻辑(如product & us-east). 下面操作符支持profile表达式:

  • !: 表示非
  • &: 表示并
  • |: 表示或

不适用圆括号的话你不能混用&|. 例如production & us-east | eu-central不是有效的表达式. 必须使用production & (us-east | eu-central).

你可以使用@Profile作为元注解来创建组合注解. 下面例子定义了一个@Production注解, 可以用来替代@Profile("production"):

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}

如果一个@Configuration类标记为@Profile, 那所有关联到这个类的@Bean方法和@Import都会被绕开, 除非一个或多个特定的profile被激活. 如果@Component@Configuration类被@Profile({"p1", "p2"})标记, 那这些类将不会被处理,除非'p1'或'p2'被激活. 如果profile被非操作符!作为前缀, 那注解的元素会在profile非激活状态下注册. 例如, 给定@Profile({"p1", "!p2"}),注册将在profile'p1'激活或者profile'p2'非激活状态下发生.

@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文件中避免混乱.

对应的XML配置不支持前面提到的profile表达式. 虽然, 可以通过使用!操作符. 也可能通过嵌套的profile用"and"来完成逻辑与, 如下:

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

上面例子中, dataSourcebean在 productionus-east都被激活情况下暴露出来.

激活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存在, 在独立程序中, StandardServletEnvironment是由更多的包含servlet配置和servlet上下文参数的属性源生成. 它可选地启动JndiPropertySource.参看java文档.

具体的, 当你使用StandardEnvironment时, env.containsProperty("my-property")将返回true, 如果运行时有my-property系统属性或my-property环境变量存在的话.

查找是按层次的. 默认系统属性优先于环境变量. 因此, 如果my-property碰巧同时出现, 在调用env.getProperty("my-property"), 系统属性胜出并返回. 注意属性值不是合并, 而是完全被优先的值覆盖.

对于普通StandardServletEnvironment, 全部层级如下, 最上面事最高优先级的:

  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将抛出.

@PropertySource注解是可重复的, 根据Java8的约定. 虽然可以, 但所有@PropertySource注解需要在相同层级声明, 直接在配置类或者作为元注解自定义注解. 混用元注解和指令注解不建议, 因为指令注解实际上覆盖了原注解.

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注入.

作为ResourceBundleMessageSource的替换, Spring提供了ReloadableResourceBundleMessageSource类. 这个变量支持文件格式化但比起基于标准JDK的ResourceBundleMessageSource实现更灵活. 特别的, 它允许从Spring资源路径读取文件(不仅是类路径)并且支持热加载property文件(将其有效缓存). 查看java文档ReloadableResourceBundleMessageSource获取更多信息.

1.15.2 标准和自定义事件

通过ApplicationEvent类和ApplicationListener接口提供了ApplicationContext的事件处理. 如果bean实现了ApplicationListener接口并部署在上下文中, 每当ApplicationEvent发布到ApplicationContext中时, 这个bean就得到通知了. 本质上, 这是标准的观察者模式.

从Spring4.2开始, 事件相关基础设施被明显增强了, 并且提供了基于注解的模型,也可以发布任意事件.(也就是说对象没必要扩展自ApplicationEvent). 当这样的对象发布后, 我们为你包装在一个事件中.

下面表格描述了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>known.spammer@example.org</value>
            <value>known.hacker@example.org</value>
            <value>john.doe@example.org</value>
        </list>
    </property>
</bean>

<bean id="blackListNotifier" class="example.BlackListNotifier">
    <property name="notificationAddress" value="blacklist@example.org"/>
</bean>

总体来看, 当emailServicesendEmail()方法被调用后, 如果有任何在黑名单邮件消息, 自定义的事件BlackListEvent就发布了. blackListNotifierbean作为ApplicationListener被注册, 并接收BlackListEvent, 这里可以通知到恰当的某些部分.

Spring的事件机制是为了在相同上下文内部bean之间的简单通信. 尽管如此, 对于大多数复杂的企业集成需求, 独立维护的Spring集成项目提供了完整的对构建轻量,面向模式的, 事件驱动的架构的支持, 可以构建在熟知的Spring编程模型之上.

基于注解的事件监听器

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

不尽是ApplicationEvent, 可以是你发送的任意对象事件.

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文件丢到应用程序服务器的部署路径下.

此种部署单元是自包含的. 他们没有暴露给外部组件, 甚至没暴露给相同程序的其他模块. 与基于RAR的ApplicationContext交互通常通过它与其他模块共享的JMS目标发生. 基于RAR的ApplicationContext也可能定时作业或与文件系统种的文件交互. 如果它需要允许从外界同步访问, 它可以(举例来说)到处RMI端点,端点是用来在相同机器上被其他模块使用的.

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实例进行扩展时.

AnnotationConfigApplicationContext有通用的post-processor注册并可能在底层通过配置注解引入更多的processor, 如@EnableTransactionManagement. 在Spring基于注解的抽象级别, bean postprocessor的概念变成了一个微小的内部容器细节.

2. 资源

本章涵盖了Spring如何处理资源以及在Spring中你如何与资源打交道的内容. 包含下面议题:

  • 简介
  • Resource 接口
  • 内置 Resource 实现
  • ResourceLoader
  • ResourceLoaderAware接口
  • 作为依赖项的Resource
  • 应用上下文和资源路径

2.1 简介

Java的标准java.net.URL类和用来处理多种URL前缀的标准处理器, 很不幸对于底层资源的访问不是足够的. 例如, 没有标准的URL实现能用来访问需要从类路径下或者相关的ServletContext下的资源. 虽然可以注册为特定的URL前缀的处理器(类似已经存在的处理http:前缀的处理器), 但一般比较复杂, 并且URL接口仍然缺乏一些急需的功能, 例如检查指向资源是否存在的方法.

2.2 资源接口

Spring的Resource接口意味着对于底层资源的抽象来说功能更丰富的接口. 下面列出了Resource接口定义:

public interface Resource extends InputStreamSource {

    boolean exists();

    boolean isOpen();

    URL getURL() throws IOException;

    File getFile() throws IOException;

    Resource createRelative(String relativePath) throws IOException;

    String getFilename();

    String getDescription();

}

如同Resource接口定义的, 它扩展了InputStreamSource接口, 如下列出这个接口的定义:

public interface InputStreamSource {

    InputStream getInputStream() throws IOException;

}

一些重要的从Resource接口而来的方法是:

  • getInputStream() : 定位和打开资源, 返回从资源读取的InputStream. 它期待每个调用返回全新的InputStream. 调用者有义务关闭流.
  • exists(): 返回boolean指示这个资源实际上是否物理上存在.
  • isOpen(): 返回boolean指示资源是否表现为处理打开的流. 如果true,InputStream不能被多次读取,而且进读取一次然后关闭以避免资源泄漏. 返回false则通常对一般资源实现, 同时可能返回异常InputStreamResource.
  • getDescription(): 返回资源的描述, 当处理资源时用于错误的输出. 通常时全限定文件名或资源的url.

其他方法允许你获取实际的表示资源的URLFile对象(如果底层实现是兼容的并支持此功能).

Spring自身使用Resource抽象扩展作为许多方法签名的参数类型. 其他相同API中(如多个ApplicationContext实现)的方法使用String或简单格式来创建Resource到上下文实现, 或者, 通过特殊的Stirng路径上的前缀, 来使调用者指定必须创建使用的特定Resource.

Resource接口用在很多Spring或与Spring协作, 它本身是很有用的工具类, 可用于你的代码中, 用来访问资源, 就算你代码不知道或在意Spring的其他部分. 这使得你的代码耦合到Spring中, 但它真的仅仅耦合了其中一小部分工具类, 这些类提供了更有用的URL的替换并能提供其他相同目的的三方库.

Resource抽象功能上不替换. 它做了封装. 例如, UrlResource封装了RUL并使用封装的URL工作.

2.3 内置资源实现类

Spring包含如下Resource实现:

  • UrlResource
  • ClassPathResource
  • FileSystemResource
  • ServletContextResource
  • InputStreamResource
  • ByteArrayResource

2.3.1 UrlResource

UrlResource封装了java.net.URL并且可以用来访问可以用URL访问的对象, 如文件,HTTP目标,FTP目标等.所有URL都有一个标准的String表现, 有标准化的前缀可以用来表明URL类型. 包含file:来访问文件路径, http:访问通过HTTP协议的资源, ftp:访问FTP资源等.

UrlResource通过显式使用UrlResource构造函数来创建, 但通常, 在你调用包含一个表示路径的String参数API方法时就隐式创建了. 对于后者, JavaBeans PropertyEditor最终决定创建Resource哪个类型.如果路径包含熟知的前缀(如:classpath:), 他就为这个前缀创建了一个特定的Resource. 那如果没有可识别的前缀, 就假定它是一个标准的URL字符串并创建UrlResource.

2.3.2 ClassPathResource

这个类表示应该从类路径包含的资源. 它使用线程上下文类加载器, 给定的类加载器,或者一个为加载资源的类.

如果类路径资源在文件系统, 不在包含在jar文件并没有被解压(被servlet引擎或环境)到文件系统中, 类路径资源Resource实现支持作为java.io.File解析. 为处理这种情况, Resource实现总是支持作为java.net.URL解析.

ClassPathResource过显式使用UrlResource构造函数来创建, 但通常, 在你调用包含一个表示路径的String参数API方法时就隐式创建了. 对于后者, JavaBeans PropertyEditor识别出特定在字符串路径上的前缀classpath:,并创建一个ClassPathResource.

2.3.3 FileSystemResource

这是一个对java.io.Filejava.nio.file.Path处理的Resource实现.支持作为FileURL解析.

2.3.4 ServletContextResource

这是一个对ServletContext资源的Resource实现, ServletContext资源解析为在相关web程序根路径内的相对路径.

支持流访问或URL访问, 但仅在web程序结构解压并且资源以及存在于物理文件系统时允许java.io.File访问. 不管是否解压到文件系统或直接从Jar或其他的类似数据库的地方(这是可能的)访问, 都依赖于Servlet容器.

2.3.5 InputStreamResource

InputStreamResource是对于一个给定InputStreamResource实现. 仅用于没有合适的Resource实现时. 特别的, 更应该用ByteArrayResource或其他基于文件的Resource实现.

相比其他的Resource实现, 这是一个对以及打开资源的描述. 因此, isOpen()返回true. 如果你需要保持资源描述符或者你需要多次读取流就不要使用它.

2.3.6 ByteArrayResource

这是对给定字节数组的Resource实现. 它为给定数组创建一个ByteArrayInputStream.

对于从任何给定的byte数组而不需要从单独使用InputStreamResource时是很有用的.

2.4 ResourceLoader

ResourceLoader接口被实现为能够返回(加载)Resource的对象. 下面列出ResourceLoader接口定义:

public interface ResourceLoader {

    Resource getResource(String location);

}

所有程序上下文均实现了ResourceLoader接口. 所以, 所有程序上下文可以包含Resource实例.

当你在特定的程序上下文调用getResource()方法时, 并且指定的位置路径没有特殊的前缀, 你就得到一个特定于上下文的Resource类型. 例如, 假设下面代码依赖于ClassPathXmlApplicationContext执行.

Resource template = ctx.getResource("some/resource/path/myTemplate.txt");

依赖于ClassPathXmlApplicationContext, 代码返回一个ClassPathResource. 如果相同的方法依据FileSystemXmlApplicationContext实例执行, 他将返回FileSystemResource. 对于一个WebApplicationContext, 他将返回ServletContextResource. 每种上下文对应每种特定返回的对象.

结果, 你可以使用一种恰当的特定上下文的风格加载资源.

另一方面, 你可能也需要强制使用ClassPathResource, 不管上下文类型是什么, 通过指定特定的classpath:前缀, 如下:

Resource template = ctx.getResource("classpath:some/resource/path/myTemplate.txt");

相似的, 你可以强制通过指定任意标准的java.net.URL前缀来使用UrlResource. 下面一对实例使用filehttp前缀:

Resource template = ctx.getResource("file:///some/resource/path/myTemplate.txt");
Resource template = ctx.getResource("http://myhost.com/resource/path/myTemplate.txt");

下面表格简要列出从String对象到Resource对象的转化策略:

表10 资源字符串

前缀 示例 解释
classpath: classpath:com/myapp/config.xml 从类路径加载
file: file:///data/config.xml 以url格式从文件系统加载. 查看FileSystemResource附加说明
http: http://myserver/logo.png 从url加载
(none) /data/config.xml 依赖于底层ApplicationContext类型

2.5 ResourceLoaderAware接口

ResourceLoaderAware接口是特定的回调接口, 用来标识期望有ResourceLoader引用提供的组件. 下面展示了接口定义:

public interface ResourceLoaderAware {
    void setResourceLoader(ResourceLoader resourceLoader);
}

当一个类实现ResourceLoaderAware并且部署到应用上下文中(受Spring管理)时, 它就被上下文认作ResourceLoaderAware. 引用上下文接着调用setResourceLoader(ResourceLoader), 并将自身作为参数传入(记住, Spring所有应用上下文都实现了ResourceLoader接口).

因为ApplicationContext时一个ResourceLoader, bean也可以实现ApplicationContextAware并使用现有的上下文直接加载资源. 尽管如此, 一般而言,如果这就是你想达到的目的, 最好使用特定的ResourceLoader接口(被认为是个工具接口). 代码将仅耦合到资源加载接口而不是整个SpringApplicationContext接口.

在程序组件中, 你也可以依赖ResourceLoader自动装配, 作为ResourceLoaderAware实现的替代. 传统的constructorbyType自动装配模式(如在自动装配协作者中描述的)能为构造函数参数或setter方法提供ResourceLoader. 问了更多的灵活性(包括自动装配域和多参数方法), 考虑使用基于注解的自动装配特性. 这种情况下,ResourceLoader自动装配给 使用@Autowired注解的任何域,构造参数或方法参数. 更多信息参看:使用@Autowired.

2.6 Resource作为依赖

如果一个bean将要通过某种动态方式判定和提供资源路径, 可能对于这个bean使用ResourceLoader接口加载资源就是有意义的. 例如, 考虑加载某类型的模版, 这种模版资源需要依赖于用户角色. 如果资源是固定的, 那就可以完全排除ResourceLoader接口的使用, 暴露bean需要的的Resource属性, 并期望他们被注入.

然后注入这些属性的简单之处在于,所有应用程序上下文都注册并使用一个特殊的JavaBeans PropertyEditor,它可以将字符串路径转换为资源对象. 所以, 如果myBean有个类型为Resource的模版属性, 它可以被配置为一个简单的字符串, 如下:

<bean id="myBean" class="...">
    <property name="template" value="some/resource/path/myTemplate.txt"/>
</bean>

注意资源路径没有前缀. 所以, 因为程序上下文将会被作为ResourceLoader, 资源将通过ClassPathResource,FileSystemResource,ServletContextResource, 依赖于具体的上下文类型.

如果你需要强制使用特定的Resource类型,你可以使用前缀. 下面两个例子展示了如何强制一个ClassPathResourceUrlResource(后者被用来访问文件系统文件):

<property name="template" value="classpath:some/resource/path/myTemplate.txt">
<property name="template" value="file:///some/resource/path/myTemplate.txt"/>

2.7 应用上下文和资源路径

本节涵盖了如何创建有资源的上下文, 包含使用XML的捷径, 如何使用通配符, 和其他细节.

2.7.1 构建应用上下文

一个程序上下文构造器(对于一个特定的应用上下文类型)一般用一个字符串或字符串数组作为资源的位置路径, 如组成上下文的定义的XML文件.

当这个位置路径不含有前缀时, 特定的Resource类型从这个路径构建并加载bean定义依赖,而且时特定于上下文的. 例如, 考虑下面的例子, 创建了一个ClassPathXmlApplicationContext:

ApplicationContext ctx = new ClassPathXmlApplicationContext("conf/appContext.xml");

bean的定义从类路径加载, 因为使用了ClassPathResource. 再考虑下面的例子, 创建了FileSystemXmlApplicationContext:

ApplicationContext ctx = new FileSystemXmlApplicationContext("conf/appContext.xml");

现在bean定义从文件系统路径加载(本例中, 相对于当前工作目录).

注意特定的类路径前缀或URL前缀覆盖了默认的加载bean定义的Resource类型, 考虑下面例子:

ApplicationContext ctx = new FileSystemXmlApplicationContext("classpath:conf/appContext.xml");

使用FileSystemXmlApplicationContext从类路径加载定义. 虽然如此, 仍然是一个FileSystemXmlApplicationContext. 如果因此作为ResourceLoader来使用, 任何没有前缀的路径都将被作为文件系统路径对待.

ClassPathXmlApplicationContext构建实例--捷径

ClassPathXmlApplicationContext类暴露了很多构造函数来方便地初始化. 基本概念是, 你可以仅仅提供一个包含XML文件名本身的字符串数组(没有引导路径信息),或者提供一个Class. ClassPathXmlApplicationContext接着从给定class获取路径信息.

有如下目录结构:

com/
  foo/
    services.xml
    daos.xml
    MessengerService.class

下面例子展示了ClassPathXmlApplicationContext实例组合从名字为services.xmldaos.xml 的文件中(在类路径下)整合bean的定义初始化.

ApplicationContext ctx = new ClassPathXmlApplicationContext(new String[] {"services.xml", "daos.xml"}, MessengerService.class);

查看ClassPathXmlApplicationContext文档获取更多构造函数的明细.

2.7.2 程序上下文构造函数资源路径中的通配符

程序上下文构造函数中资源路径可以是简单的路径(如前面的例子), 每一个都一对一映射到目标Resource, 或者相反, 可能存在指定的"classpath*:"前缀, 或者内部Ant风格的正则表达式(通过使用SpringPathMatcher工具匹配). 后面两个都是有效的通配符.

这种机制的一种用处就是, 当你需要在一个组合风格的应用集中使用. 所有组件都能'publish'应用上下文定义片段到熟知的路径中, 并且当最终上下文使用相同的路径前缀classpath*:创建时, 所有组合的片段都自动被获取.

注意, 构造函数中的通配符是特定于资源路径并在构造时被解析. 它跟Resource类型本身没有关系, 你不能使用classpath*:前缀去构建实际的Resource , 一个资源在某一时刻仅对应一个资源.

Ant风格的模式

路径可以包含Ant风格的模式, 如下:

/WEB-INF/*-context.xml
com/mycompany/**/applicationContext.xml
file:C:/some/path/*-context.xml
classpath:com/mycompany/**/applicationContext.xml

如果路径包含Ant风格的模式, 解析器遵循更为复杂的处理过程去试图解析通配符. 它为路径生成一个Resource资源到最近的非通配符片段并包含了一个URL. 如果URL不是jar:URL或者容器特定的变体(如WebLogic中的zip:, WebSphere中的wsjar等), 一个java.io.File包含进来并用于通过文件系统遍历解析通配符. 如果是jar URL, 解析器也可以获得到一个java.net.JarURLConnection并解析这个jar URL,然后遍历jar文件的内容.

可移植的含义

如果特定的路径已经是一个文件URL(因基类ResourceLoader是文件系统而隐式的或显式的), 通配符确保在一个完全可移植的风格下工作.

如果指定的是一个类路径, 解析器必须包含最近的非通配符路径片段URL, 这是通过调用Classloader.getResource()完成的. 因为这仅仅是一个路径的节点(不是最终文件), 它实际上是没有明确定义的一种URL. 实践中, java.io.File通常表示目录(类路径资源解析为文件路径资源)或某jar URL(类路径解析为jar路径). 同样, 其中有种可移植的概念.

如果jar URL包含最近的非通配片段, 解析器必须能获取到java.net.JarURLConnection或者手工转化为jar URL, 从而能访问jar的内容并解析通配符. 这在一些环境下会失败, 我们强烈建议特定环境下的jar资源通配符解析在特定环境下测试后再最终依赖它.

classpath*:前缀

当构建一个XML的应用上下文时, 位置字符串可能使用classpath*:前缀, 如下:

ApplicationContext ctx =
    new ClassPathXmlApplicationContext("classpath*:conf/appContext.xml");

特定的前缀指定了所有匹配到的类路径资源必须被包含进来(内部, 这通过调用ClassLoader.getResources(…​)实现), 并最终合并到上下文定义中.

通配符类路径依赖于底层类加载器的getResource()方法. 现在很多程序服务器提供了他们自身的加载器实现, 所以这种行为可能有所差别, 特别是处理jar文件时. classpath*是否工作的一个简单测试是使用加载器去加载类路径下的一个jar文件:getClass().getClassLoader().getResources("<someFileInsideTheJar>"). 尝试这类测试, 当文件有相同的名称但处于不同路径下时. 万一返回的结果不对, 检查服务器文档的设置, 这些设置可能会影响类加载器行为.

你也可以在路径其他部分中使用PathMatcher模式来组合classpath*:(例如, classpath*:META-INF/*-beans.xml). 这种情况下, 解析策略相当简单: ClassLoader.getResources()用来解析最后的非通配路径片段来获取所有类加载器继承中匹配的所有资源, 然后对于每种资源外, 对通配符子路径使用前面描述的路径匹配器解析策略。

通配符相关的其他注意事项

注意classpath*:当组合使用Ant格式模式时, 只有在模式开始之前,至少一个根目录才能可靠地工作,除非实际的目标文件驻留在文件系统中。这意味着类似classpath*:*.xml之类的模式可能不会从JAR文件的根检索文件,而可能只从扩展目录的根检索文件。

Spring检索类路径条目的能力源于JDK的ClassLoader.getResources()方法,该方法只返回空字符串的文件系统位置(指示搜索的潜在根路径)。Spring也在JAR文件中评估 URLClassLoader 加载器运行时配置和java.class.path清单,但这并不一定会导致可移植行为。

对类路径包的扫描要求在类路径中存在相应的目录条目。当您用Ant构建JAR时,不要激活JAR任务的仅文件开关。此外,类路径目录在某些环境(例如,JDK1.7.0_45及更高版本上的独立应用程序)中可能不会根据安全策略公开(这需要在清单中设置“受信任的库”)。参见http://stackoverflow.com/questions/19394570/java-jre-7u45-breaks-classloader-getresources)。

在JDK 9的模块路径(jigsaw)上,Spring的类路径扫描一般按预期工作。将资源放在专用目录中也是非常值得推荐的,避免了搜索JAR文件根级别的上述可移植性问题。

具有classpath:的Ant模式:如果要搜索的根包在多个类路径位置中可用,则不能保证资源找到匹配的资源。考虑以下资源位置示例:

com/mycompany/package1/service-context.xml

现在考虑可能有人想要Ant模式中找到文件:

classpath:com/mycompany/**/service-context.xml

这样的资源可能只位于一个位置,但是当使用前面的示例这样的路径试图解析它时,解析器将关闭getResource("com/mycompany");返回的(第一个)url;。如果此基本包节点存在于多个类加载器位置,则可能不存在实际的结束资源。因此,在这种情况下,您应该更喜欢使用具有相同Ant样式的ClassPath*:模式,它搜索包含根包的所有类路径位置。

FileSystemResource 附加说明

FileSystemResource没有附加到FileSystemApplicationContext(也就是,当FileSystemApplicationContext不是真正的ResourceLoader)时, 它对待绝对路径和相对路径能按预期处理. 相对路径相对于当前工作目录,而绝对路径相对于文件系统的根。

FileSystemApplicationContextResourceLoader时, 因向后兼容而有所变化. FileSystemApplicationContext强制所有依附的FileSystemResource实例以相对路径方式对待路径, 不管他们是否以前缀"/"开头. 实践中, 下面两种方式是等同的:

ApplicationContext ctx =
    new FileSystemXmlApplicationContext("conf/context.xml");
ApplicationContext ctx =
    new FileSystemXmlApplicationContext("/conf/context.xml");

下面两种方式也是等同的(就算区别开他们有意义, 因为一个是相对的一个是绝对的):

FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("some/resource/path/myTemplate.txt");
FileSystemXmlApplicationContext ctx = ...;
ctx.getResource("/some/resource/path/myTemplate.txt");

实践中, 如果你需要绝对路径, 你应该避免使用FileSystemResourceFileSystemXmlApplicationContext. 并通过file:前缀强制使用UrlResource. 如下展示了如何这么做:

// actual context type doesn't matter, the Resource will always be UrlResource
ctx.getResource("file:///some/resource/path/myTemplate.txt");
// force this FileSystemXmlApplicationContext to load its definition via a UrlResource
ApplicationContext ctx =
    new FileSystemXmlApplicationContext("file:///conf/context.xml");
posted @ 2019-03-09 18:36  罪恶斯巴克  阅读(1557)  评论(1编辑  收藏  举报