Spring框架5.3.27官方文档(中文翻译)—— 核心技术-IoC容器(三)

203 阅读58分钟

1.IoC容器

最近发现了一个Spring的中文网,文档翻译的很全,故后续不再继续翻译文档。

耗时约三周,约65000字,终于翻译完啦!!!o( ̄▽ ̄)ブ

文章列表

原文:docs.spring.io/spring-fram…

1.11. 使用JSR 330标准注解

从Spring 3.0开始,Spring提供了对JSR-330标准注解(依赖注入)的支持。以与Spring注解相同的方式扫描这些注解。要使用它们,需要在类路径中有相关的jar文件。

提示

如果使用Maven,则使用在标准的Maven存储库(repo1.maven.org/maven2/java…:

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

补充

  • @Inject:类似于@Autowired
  • @Named:可以用作@Component,也可以用作@Qualifier
  • @ManagedBean:该注解来自JSR-250,可以用作@Component

1.11.1. 使用@Inject@Named进行依赖注入

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

import javax.inject.Inject;

public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

    public void listMovies() {
        this.movieFinder.findMovies(...);
        // ...
    }
}

@Autowired一样,你可以在字段级方法级构造函数参数级使用@Inject。此外,你可以将注入点声明为Provider,允许按需访问较短作用域的bean,或者通过调用Provider.get()延迟访问其他bean。下面的例子是前一个例子的变体:

import javax.inject.Inject;
import javax.inject.Provider;

public class SimpleMovieLister {

    private Provider<MovieFinder> 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注解等价的标准

你可以使用@javax.inject.Namedjavax.annotation.ManagedBean代替@Component,如下示例所示:

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

@Named("movieListener")  // 同样可以使用 @ManagedBean("movieListener")
public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

    // ...
}

在不指定组件名称的情况下使用@Component是很常见的。@Named可以以类似的方式使用,如下例所示:

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

@Named
public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

    // ...
}

当你使用@Named@ManagedBean时,你可以用与使用Spring注解完全相同的方式使用组件扫描,如下面的例子所示:

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

提示

@Component相反,JSR-330 @Named和JSR-250 @ManagedBean注解是不可组合的。您应该使用Spring的构造型模型(stereotype model)来构建自定义组件注解。

1.11.3. JSR-330标准注解的限制

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

Springjavax.inject.*javax.inject restrictions / comments
@Autowired@Inject@Inject 没有required属性. 可以与Java 8的Optional一起使用。
@Component@Named / @ManagedBeanJSR-330不提供可组合的模型,只提供一种识别命名组件的方法。
@Scope("singleton")@SingletonJSR-330的默认作用域类似于Spring的prototype。但是,为了使其与Spring的一般默认值保持一致,在Spring容器中声明的JSR-330 bean默认情况下是单例的。为了使用单例以外的作用域,你应该使用Spring的@Scope注解。javax.inject还提供了@Scope注解。不过,这个工具仅用于创建您自己的注解。
@Qualifier@Qualifier / @Namedjavax.inject.Qualifier只是一个用于构建自定义限定符的元注解。具体的字符串限定符(比如Spring带值的@Qualifier)可以通过javax.inject.Named来关联。
@Value-没有等价
@Required-没有等价
@Lazy-没有等价
ObjectFactoryProviderjavax.inject.Provider是Spring的ObjectFactory直接替代品,只是有一个更短的get()方法名。它还可以与Spring的@Autowired无注解的构造函数和setter方法结合使用。

1.12. 基于Java的容器配置(Java-based Container Configuration)

本节介绍如何在Java代码中使用注解来配置Spring容器。它包括以下主题:

1.12.1. 基本概念:@Bean@Configuration

Spring新的Java配置支持中的核心构件是带@Configuration注解的类和带@Bean注解的方法。

@Bean注解用于指示方法实例化配置初始化要由Spring IoC容器管理的新对象。对于那些熟悉Spring <beans/>XML配置,@Bean注解与<beans/>元素扮演相同的角色。你可以在任何Spring @Component中使用带有@Bean注解的方法。但是,它们最常与@Configuration bean一起使用

@Configuration注解类表明它的主要目的是作为bean定义的来源。此外,@Configuration类允许通过调用同一类中的其他@Bean方法来定义bean间依赖关系。最简单的@Configuration类如下所示:

@Configuration
public class AppConfig {

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

前面的AppConfig类等价于下面的Spring<beans/>XML:

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

完整@Configuration vs. “轻量”@Bean模式?(Full @Configuration vs. "lite" @Bean mode)

@Bean方法在没有使用@Configuration注解的类中声明时,它们被称为以**“轻量”模式**处理。在@Component或甚至在普通的旧类中声明的Bean方法被认为是“轻量”,包含类的主要目的不同,而@Bean方法在那里有额外的好处。例如,服务组件可以通过每个适用组件类上的附加@Bean方法向容器公开管理视图。在这种情况下,@Bean方法是一种通用的工厂方法机制。

与完整的@Configuration不同,轻量的@Bean方法不能声明bean之间的依赖关系。相反,它们操作的是包含它们的组件的内部状态,以及它们可能声明的参数(可选)。因此,这样的@Bean方法不应该调用其他@Bean方法。每个这样的方法实际上只是特定bean引用的工厂方法,没有任何特殊的运行时语义。这里的积极副作用(positive side-effect)是,在运行时不必应用CGLIB子类,因此在类设计方面没有限制(也就是说,包含的类可能是final等)。

在常见的场景中,@Bean方法将在@Configuration类中声明,以确保始终使用“完整”模式,并且跨方法引用因此被重定向到容器的生命周期管理。这可以防止通过常规Java调用意外地调用相同的@Bean方法,这有助于减少在“轻量”模式下操作时难以跟踪的细微错误。

下面几节将深入讨论@Bean@Configuration注解。不过,首先我们将介绍使用基于Java的配置创建Spring容器的各种方法。

1.12.2. 使用AnnotationConfigApplicationContext实例化Spring容器

下面几节介绍Spring的AnnotationConfigApplicationContext,它是在Spring 3.0中引入的。这个通用的ApplicationContext实现不仅能够接受@Configuration类作为输入,还能够接受普通的@Component类和带有JSR-330元数据注解的类。

@Configuration类作为输入提供时,@Configuration类本身被注册为bean定义,类中声明的所有@Bean方法也被注册为bean定义。

当提供@Component和JSR-330类时,它们被注册为bean定义,并且假设在这些类中需要使用像@Autowired@Inject这样的DI元数据。

简单的构造函数

在实例化一个ClassPathXmlApplicationContext时,使用Spring XML文件作为输入的方式大致相同,在实例化一个AnnotationConfigApplicationContext时,您可以使用@Configuration类作为输入。这允许完全不受XML约束地使用Spring容器,如下面的示例所示:

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

前面的例子假设MyServiceImplDependency1Dependency2使用Spring依赖注入注解,比如@Autowired

使用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(); // 注意这里需要调用refresh()
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}

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

要启用组件扫描,你可以这样注解@Configuration类:

@Configuration
@ComponentScan(basePackages = "com.acme") // 启用组件扫描
public class AppConfig  {
    // ...
}

有经验的Spring用户可能对Spring的context: 命名空间中的XML声明很熟悉,如下例所示:

<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方法都被处理并注册为容器中的bean定义。

支持带有AnnotationConfigWebApplicationContext的Web应用程序

AnnotationConfigApplicationContext 的一个WebApplicationContext变体可以通过注解AnnotationConfigWebApplicationContext获得。您可以在配置Spring ContextLoaderListener servlet监听器、Spring MVC DispatcherServlet等时使用此实现。下面的web.xml代码段配置了一个典型的Spring MVC web应用程序(注意使用了contextClass context-paraminit-param):

官方的web.xml示例

<web-app>
    <!-- 使用`AnnotationConfigWebApplicationContext`配置`ContextLoaderListener` -->
    <context-param>
        <param-name>contextClass</param-name>
        <param-value>
            org.springframework.web.context.support.AnnotationConfigWebApplicationContext
        </param-value>
    </context-param>

    <!-- 指定`@Configuration`配置类 -->
    <!-- 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>
        
        <!-- 使用`AnnotationConfigWebApplicationContext`配置`DispatcherServlet` -->
        <!-- 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>
        <!-- 指定`@Configuration`配置类 -->
        <!-- 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>

提示

对于程序化的用例,GenericWebApplicationContext可以用来替代AnnotationConfigWebApplicationContext。详细信息请参见GenericWebApplicationContext javadoc。

1.12.3. 使用@Bean注解

@Bean是一个方法级注解,是XML<bean/>元素。注解支持<bean/>提供的一些属性,例如:

您可以在@Configuration注解的类或@Component注解的类中使用@Bean注解。

声明一个Bean

要声明一个bean,可以用@Bean注解一个方法。您可以使用此方法在指定为方法返回值的类型的ApplicationContext中注册bean定义。默认情况下,bean名称与方法名称相同。下面的例子展示了一个@Bean方法声明:

@Configuration
public class AppConfig {

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

上面的配置完全等同于下面的Spring XML:

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

这两个声明都使一个名为transferService的bean在ApplicationContext中可用,绑定到TransferServiceImpl类型的对象实例,如下图所示:

transferService -> com.acme.TransferServiceImpl

您还可以使用默认方法来定义bean。这允许通过在默认方法上实现带有bean定义的接口来组合bean配置

public interface BaseConfig {

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

@Configuration
public class AppConfig implements BaseConfig {

}

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

@Configuration
public class AppConfig {

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

但是,这限制了对指定接口类型(TransferService)的预先类型预测的可见性。然后,只有在受影响的单例bean实例化之后,才使用容器已知的完整类型(TransferServiceImpl)。非惰性单例bean根据其声明顺序进行实例化,因此您可能会看到不同的类型匹配结果,这取决于另一个组件何时尝试使用未声明的类型进行匹配(例如@Autowired TransferServiceImpl,它仅在transferService bean实例化后才解析)。

提示

如果您始终通过声明的服务接口引用您的类型,那么您的@Bean返回类型可以安全地加入该设计决策。然而,对于实现多个接口的组件或可能由其实现类型引用的组件,声明尽可能具体的返回类型更安全(至少与引用bean的注入点所要求的一样具体)。

Bean依赖

@Bean注解的方法可以具有任意数量的参数,这些参数描述构建该bean所需的依赖关系。例如,如果我们的TransferService需要一个AccountRepository,我们可以用一个方法参数实现这个依赖,如下面的例子所示:

@Configuration
public class AppConfig {

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

解析机制与基于构造函数的依赖注入非常相似。有关详细信息,请参阅相关部分

@Configuration
public class AppConfig {

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

接收生命周期回调

任何用@Bean注解定义的类都支持常规的生命周期回调,并且可以使用JSR-250中的@PostConstruct@PreDestroy注解。有关更多细节,请参阅JSR-250注解

它还完全支持常规的Spring生命周期(lifecycle) 回调。如果bean实现了InitializingBeanDisposableBeanLifecycle,则容器将调用它们各自的方法。

*Aware接口的标准集合(如 BeanFactoryAwareBeanNameAwareMessageSourceAwareApplicationContextAware等)也得到了完全支持。

补充

任何@Bean定义的bean接收Spring生命周期回调,包括

  • JSR-250注解:@PostConstruct@PreDestroy
  • InitializingBean,DisposableBeanLifecycle
  • *AwareBeanFactoryAware,BeanNameAware,MessageSourceAware,ApplicationContextAware

@Bean注解支持指定任意的初始化销毁回调方法,很像Spring 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();
    }
}

提示

默认情况下,使用具有公共的(public)closeshutdown方法的Java配置定义的bean将自动使用销毁回调调用。如果您有一个公共的closeshutdown方法,并且不希望它在容器关闭时被调用,那么您可以在bean定义中添加@Bean(destroyMethod="")禁用默认(推断)模式

对于使用JNDI获取的资源,默认情况下您可能希望这样做,因为它的生命周期是在应用程序外部管理的。特别是,要确保始终对DataSource执行此操作,因为众所周知,在Java EE应用程序服务器上这是有问题的。

下面的例子展示了如何防止一个DataSource自动销毁回调

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

此外,对于@Bean方法,您通常使用编程JNDI查找,要么使用Spring的JndiTemplateJndiLocatorDelegate助手,要么直接使用JNDI InitialContext,而不是使用JndiObjectFactoryBean变体(这将迫使您将返回类型声明为FactoryBean类型,而不是实际的目标类型,这使得在其他打算引用此处提供的资源的@Bean方法中使用交叉引用调用变得更加困难)。

在前面提到的例子中的BeanOne中,在构造过程中直接调用init()方法同样有效,如下面的例子所示:

@Configuration
public class AppConfig {

    @Bean
    public BeanOne beanOne() {
        BeanOne beanOne = new BeanOne();
        beanOne.init(); // 直接调用初始化方法
        return beanOne;
    }

    // ...
}

指定bean的作用域

Spring包含@Scope注解,这样您就可以指定bean的作用域。

使用@Scope注解

您可以指定使用@Bean注解定义的bean应该具有特定的作用域。您可以使用Bean作用域(Bean Scopes)部分中指定的任何标准作用域。

默认的作用域是**单例(singleton)**的,但是你可以用@Scope注解覆盖它,如下例所示:

@Configuration
public class MyConfiguration {

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

Spring提供了一种通过**作用域代理(scoped proxies)处理作用域依赖的方便方法。当使用XML配置时,创建这样一个代理最简单的方法是<aop:scoped-proxy/>元素。在Java中使用@Scope注解配置bean提供了与proxyMode属性相当的支持。默认值ScopedProxyMode.DEFAULT,这通常表示不应该创建有作用域的代理**,除非在组件扫描指令级别配置了不同的默认值。您可以指定:

  • ScopedProxyMode.NO
  • ScopedProxyMode.TARGET_CLASS
  • ScopedProxyMode.INTERFACES

如果使用Java将有作用域的代理示例从XML参考文档(参见有作用域代理(scoped proxies))移植到我们的@Bean,它类似于以下内容:

//暴露一个支持HTTP Session作用域的bean为代理
@Bean
@SessionScope //等价于@Scope(proxyMode=ScopedProxyMode.TARGET_CLASS)
public UserPreferences userPreferences(){
    return new UserPreferences();
}

@Bean
public Service userService(){
    UserService service=new SimpleUserService();
    //引用代理的userPrefences
    service.setUserPreference(userPreferences());
    return service;
}

定制Bean名称(Customizing Bean Naming)

默认情况下,配置类使用@Bean方法的名称作为结果bean的名称。但是,可以使用name属性重写此功能,如下例所示:

@Configuration
public class AppConfig {

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

Bean别名(Bean Aliasing)

正如命名bean中所讨论的,有时需要为单个bean提供多个名称,否则称为bean别名。@Bean注解的name属性为此目的接受一个String数组。下面的例子展示了如何为一个bean设置多个别名

@Configuration
public class AppConfig {

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

Bean描述(Bean Description)

有时,提供bean的更详细的文本描述是有帮助的。当**为了监视目的而公开bean(可能通过JMX)**时,这一点特别有用。

要向@Bean添加描述,可以使用@Description注解,如下例所示:

@Configuration
public class AppConfig {

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

补充

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Description {}

1.12.4. 使用@Configuration注解

@Configuration类级别的注解,表示对象是bean定义的源。@Configuration类通过@Bean注解的方法声明bean。对@Configuration类上的@Bean方法的调用也可用于定义bean间依赖关系。请参阅 基本概念:@Bean@Configuration(Basic Concepts: @Bean and @Configuration以获得介绍。

注入bean间依赖

当bean彼此依赖时,表达依赖关系就像让一个bean方法调用另一个bean方法一样简单,如下例所示:

@Configuration
public class AppConfig{
    @Bean
    public BeanOne beanOne(){
        return new BeanOne(beanTwo()); // 调用beanTwo()引入BeanTwo依赖
    }
    
    @Bean
    public BeanTwo beanTwo(){
        return new BeanTwo();
    }
}

在前面的示例中,beanOne通过构造函数注入接收到beanTwo的引用。

提示

这种声明bean间依赖的方法只有在@Configuration类中声明@Bean方法时才有效。你不能通过使用普通的@Component类来声明bean间依赖。

查找方法注入(Lookup Method Injection)

如前所述,查找方法注入(lookup method injection)是一种高级特性,您应该很少使用。在单例作用域bean依赖于原型(prototype)作用域bean的情况下,它很有用。使用Java进行这种类型的配置为实现这种模式提供了一种自然的方法。下面的例子展示了如何使用查找方法注入:

public abstract class CommandManager{
    //抽象方法,返回一个Command对象
    protected abstract Command createCommand();
    
    public Object process(Object commandState){
        //调用createCommand()得到一个Command对象
        Command command=createCommand();
        command.setState(commandState);
        return command.execute();
    }
}

通过使用Java配置,您可以创建CommandManager的子类,其中抽象的createCommand()方法以查找新(prototype)命令对象的方式被重写。下面的例子展示了如何这样做:

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

@Bean
public CommandManager commandManager() {
    //通过CommandManager的匿名实现,返回一个为`prototype`的Command对象
    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()); // 获取ClientDao并设置
        return clientService;
    }

    @Bean
    public ClientService clientService2() {
        ClientServiceImpl clientService = new ClientServiceImpl();
        clientService.setClientDao(clientDao()); // 获取ClientDao并设置
        return clientService;
    }

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

clientDao()clientService1()clientService2()中分别被调用一次。由于该方法创建了一个ClientDaoImpl的新实例并返回它,因此您通常期望有两个实例(每个服务一个)。这肯定会有问题:在Spring中,实例化的bean默认具有单例(singleton)作用域。这就是神奇之处:**所有的@Configuration类都在启动时用CGLIB子类化。**在子类中,子方法在调用父方法并创建新实例之前首先检查容器中是否存在任何缓存的(限定作用域的)bean

提示

根据bean的作用域,行为可能会有所不同。我们在这里讨论的是单例

提示

从Spring 3.2开始,不再需要将CGLIB添加到类路径中,因为CGLIB类已被重新打包在org.springframework.cglib下,并直接包含在spring-core的JAR中。

由于CGLIB在启动时动态添加特性,因此存在一些限制。特别是,配置类不能是final的。然而,从4.3开始,配置类上允许使用任何构造函数,包括使用@Autowired或默认注入的单个非默认构造函数声明。

如果您希望避免任何CGLIB强加的限制,请考虑在非@Configuration类上声明@Bean方法(例如,在普通的@Component类上)。然后,@Bean方法之间的跨方法调用不会被拦截,因此您必须完全依赖于构造函数或方法级别的依赖注入。

1.12.5. 组合基于Java的配置(Composing Java-based Configurations)

Spring基于Java的配置特性允许您编写注解,这可以降低配置的复杂性。

使用@Import注解

就像Spring XML中<import/>元素帮助模块化配置一样,@Import注解允许从另一个配置类加载@Bean定义,如下面的例子所示:

@Configuration
public class ConfigA {

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

@Configuration
@Import(ConfigA.class) // 引入ConfigA配置类
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); // 仅指定了ConfigB配置类

    // Bean A和B都可以访问
    A a = ctx.getBean(A.class);
    B b = ctx.getBean(B.class);
}

这种方法简化了容器实例化,因为只需要处理一个类,而不需要在构造过程中记住可能大量的@Configuration类。

从Spring Framework 4.2开始,@Import也支持对常规组件类的引用,类似于AnnotationConfigApplicationContext.register方法。如果您想避免组件扫描,通过使用几个配置类作为入口点来显式定义所有组件,这是特别有用的。

在导入的@Bean定义中注入依赖(Injecting Dependencies on Imported @Bean Definitions)

前面的例子可以工作,但是过于简单。在大多数实际场景中,bean跨配置类彼此依赖。在使用XML时,这不是问题,因为不涉及编译器,您可以声明ref="someBean",并信任Spring在容器初始化期间解决这个问题。当使用@Configuration类时,Java编译器会对配置模型施加约束,因为对其他bean的引用必须是有效的Java语法。

幸运的是,解决这个问题很简单。正如我们已经讨论过的,@Bean方法可以有任意数量的参数来描述bean依赖关系。考虑以下更真实的场景,其中有几个@Configuration类,每个类都依赖于其他类中声明的bean

@Configuration
public class ServiceConfig {

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

@Configuration
public class RepositoryConfig {

    @Bean
    public AccountRepository accountRepository(DataSource 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:这意味着它们可以利用@Autowired@Value注入以及与任何其他bean相同的其他特性。

确保以这种方式注入的依赖项都是最简单的类型@Configuration类是在上下文初始化的早期处理的,强制以这种方式注入依赖项可能会导致意外的早期初始化。只要有可能,就采用基于参数的注入,如前面的示例所示。

另外,要特别小心通过@Bean定义BeanPostProcessorBeanFactoryPostProcessor。通常应该将这些方法声明为静态@Bean方法,而不是触发包含它们的配置类的实例化。否则,@Autowired@Value可能无法在配置类本身上工作,因为可以在AutowiredAnnotationBeanPostProcessor之前将其创建为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;

    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类中的构造函数注入仅在Spring Framework 4.3中得到支持。还要注意,如果目标bean只定义了一个构造函数,则不需要指定@Autowired

为了方便导航,完全限定了导入的bean(Fully-qualifying imported beans for ease of navigation)

在前面的场景中,使用@Autowired工作得很好,并提供了所需的模块化,但是确定装配的bean定义的确切声明位置仍然有些模糊。例如,作为一个查看ServiceConfig的开发人员,您如何确切地知道@Autowired AccountRepository bean是在哪里声明的?它在代码中不是显式的,这可能很好。请记住,Spring Tools for Eclipse提供了一些工具,可以呈现显示所有内容如何装配的图形,这可能就是您所需要的。此外,您的Java IDE可以轻松地找到AccountRepository类型的所有声明和使用,并快速显示返回该类型的@Bean方法的位置。

如果这种模糊性是不可接受的,并且您希望在IDE中从一个@Configuration直接导航到另一个@Configuration,请考虑自动装配配置类本身。下面的例子展示了如何这样做:

@Configuration
public class ServiceConfig {

    @Autowired
    private RepositoryConfig repositoryConfig;

    @Bean
    public TransferService transferService() {
        // 通过配置类的@Bean方法导航
        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(用于在第一次访问时创建,而不是在启动时创建)或将某些其他bean声明为@DependsOn(确保在当前bean之前创建特定的其他bean,而不是后者的直接依赖所暗示的)。

有条件地包含@Configuration类或@Bean方法(Conditionally Include @Configuration Classes or @Bean Methods)

根据一些任意的系统状态,有条件地启用禁用完整的@Configuration类,甚至单个的@Bean方法,通常是很有用的。一个常见的例子是,只有在Spring环境中启用了特定的profile时,才使用@Profile注解来激活Bean(有关详细信息,请参阅 Bean Definition Profiles)。

@Profile注解实际上是通过使用更灵活的@Conditional注解实现的@Conditional注解指示了在注册@Bean之前应该咨询的特定的org.springframework.context.annotation.Condition实现。

Condition接口的实现提供了一个返回truefalsematches(…)方法。例如,下面的清单显示了用于@Profile的实际Condition实现:

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    // 读取@Profile注解属性
    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;
}

更多细节请参见@Conditional javadoc。

结合Java和XML配置(Combining Java and XML Configuration)

Spring的@Configuration类支持并不打算100%完全替代Spring XML。一些工具(如Spring XML名称空间)仍然是配置容器的理想方法。在XML方便或必要的情况下,您可以选择:以“以XML为中心(XML-centric)”的方式实例化容器,例如使用ClassPathXmlApplicationContext,或者以“以java为中心(Java-centric)”的方式实例化容器,例如使用AnnotationConfigApplicationContext@ImportResource注解来根据需要导入XML。

@Configuration类以XML为中心的使用(XML-centric Use of @Configuration Classes)

最好是从XML引导Spring容器,并以特别的方式包含@Configuration类。例如,在使用Spring XML的大型现有代码库中,更容易根据需要创建@Configuration类,并从现有XML文件中包含它们。在本节后面的部分中,我们将介绍在这种“以XML为中心”的情况下使用@Configuration类的选项。

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

请记住,@Configuration类最终是容器中的bean定义。在本系列示例中,我们创建了一个名为AppConfig@Configuration类,并将其作为<bean/>定义。因为<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引用它,并且不太可能通过名称显式地从容器中获取它。类似地,DataSource bean只按类型自动装配,因此不严格要求显式bean id

使用<context:component-scan/>拾取@Configuration类(Using context:component-scan/ to pick up @Configuration classes)

因为@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>
以类为中心使用XML和@ImportResource

在使用@Configuration类作为配置容器的主要机制的应用程序中,可能仍然需要至少使用一些XML。在这些场景中,您可以使用@ImportResource并只定义所需的XML。这样做实现了一种“以java为中心”的方法来配置容器,并将XML保持在最低限度。下面的示例(其中包括一个配置类、一个定义bean的XML文件、一个属性文件和主类)展示了如何使用@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抽象(Environment Abstraction)

Environment接口是集成在容器中的抽象,它对应用程序环境的两个关键方面建模:profilesproperties

一个profile是一个命名的、逻辑的bean定义组,只有在给定的profile处于活动状态时才向容器注册Bean定义。可以将bean分配给配置文件,无论是用XML定义的还是用注解定义的。与profile相关的Environment对象的作用是确定哪些profile(如果有的话)当前是活动的,以及哪些profile(如果有的话)在默认情况下应该是活动的

属性在几乎所有应用程序中都扮演着重要的角色,并且可能来自各种来源:属性文件(properties files)JVM系统属性(JVM system properties)系统环境变量(system environment variables)JNDIservlet上下文参数专门的(ad-hoc)Properties对象Map对象等等。与属性相关的Environment对象的作用是为用户提供一个方便的服务接口,用于配置属性源并从中解析属性。

1.13.1. Bean定义Profile(Bean Definition Profiles)

Bean定义profile在核心容器中提供了一种机制,允许在不同的环境中注册不同的Bean。“环境(environment)”这个词对不同的用户有不同的含义,这个特性可以帮助处理许多用例,包括:

  • 在开发中使用内存中的数据源,而在QA或生产中从JNDI中查找相同的数据源
  • 只有在将应用程序部署到性能环境中时才注册监视基础设施。
  • 为客户A和客户B的部署注册自定义的bean实现。

考虑需要DataSource的实际应用程序中的第一个用例。在测试环境中,配置可能类似于以下内容:

@Bean
public DataSource dataSource() {
    return new EmbeddedDatabaseBuilder()
        .setType(EmbeddedDatabaseType.HSQL)
        .addScript("my-schema.sql")
        .addScript("my-test-data.sql")
        .build();
}

现在考虑如何将此应用程序部署到QA或生产环境中,假设应用程序的数据源已注册到生产应用程序服务器的JNDI目录。我们的dataSource bean现在看起来像下面的清单:

@Bean(destroyMethod="")
public DataSource dataSource() throws Exception {
    Context ctx = new InitialContext();
    return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
}

问题是如何根据当前环境在使用这两种变体之间进行切换。随着时间的推移,Spring用户已经设计了许多方法来完成这项工作,通常依赖于系统环境变量和**XML <import/>**的组合。包含${placeholder}令牌的语句,根据环境变量的值解析到正确的配置文件路径。Bean定义profile是为这个问题提供解决方案的核心容器特性。

如果我们推广前面特定于环境的bean定义示例中显示的用例,我们最终需要在某些上下文中注册某些bean定义,而不是在其他上下文中注册。您可以说,您希望在情况a中注册bean定义的某个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方法,您通常选择使用编程的JNDI查找,方法是使用Spring的JndiTemplate/JndiLocatorDelegate帮助程序,或者使用前面所示的直接JNDI InitialContext使用方法,但不使用JndiObjectFactoryBean变体,这将迫使您将返回类型声明为FactoryBean类型。

profile字符串可以包含一个简单的profile名称(例如,production)或一个profile表达式。profile表达式允许表达更复杂的profile逻辑(例如,production & us-east)。profile表达式中支持以下操作符:

  • !:profile的逻辑“not”
  • &:profile的逻辑“and”
  • |:profile的逻辑“or”

提示

如果不使用括号,就不能混合使用&|操作符。例如,production & us-east | eu-central不是一个有效的表达式。它必须表示为production & (us-east | eu-central)

您可以使用@Profile作为元注解(meta-annotation) 来创建自定义组合注解。下面的例子定义了一个自定义的@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以NOT操作符(!)为前缀,则只有当profile不活动时,才会注册带注解的元素。例如,给定@Profile({"p1","!p2"}),如果配置文件'p1'处于活动状态或配置文件'p2'未处于活动状态,则将发生注册。

@Profile也可以在方法级别声明,只包含配置类的一个特定bean(例如,对于特定bean的可选变体),如下例所示:

@Configuration
public class AppConfig {

    @Bean("dataSource")
    @Profile("development")  // standaloneDataSource方法仅在`development` profile生效
    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` profile生效
    public DataSource jndiDataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}

提示

对于@Bean方法上的@Profile,可能会出现一种特殊的情况:对于具有相同Java方法名的重载@Bean方法(类似于构造函数重载),需要在所有重载方法上一致地声明@Profile条件。如果条件不一致,则只有重载方法中第一个声明的条件有关系。因此,@Profile不能用于选择具有特定参数签名的重载方法。同一bean的所有工厂方法之间的解析遵循Spring在创建时的构造函数解析算法。

如果希望定义具有不同profile条件的备选bean,请使用不同的Java方法名,这些方法名通过使用@Beanname属性指向相同的bean名称,如前面的示例所示。如果参数签名都是相同的(例如,所有的变体都有无参数工厂方法),这是首先在有效的Java类中表示这种安排的唯一方法(因为只能有一个特定名称和参数签名的方法)。

XML Bean定义Profile(XML Bean Definition Profiles)

XML对应物是<beans>元素的profile属性。我们前面的示例配置可以重写为两个XML文件,如下所示:

<!-- 指定development profile -->
<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>
<!-- 指定production profile -->
<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>

在前面的示例中,如果productionus-east配置文件都处于活动状态,则公开dataSource bean。

激活一个Profile(Activating a Profile)

现在我们已经更新了配置,我们仍然需要指示Spring哪个profile是活动的。如果我们现在启动我们的示例应用程序,我们将看到抛出NoSuchBeanDefinitionException,因为容器找不到名为dataSource的Spring bean。

激活profile可以通过几种方式完成,但最直接的是通过编程方式针对通过ApplicationContext提供的Environment 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 Abstraction)。在集成测试中,活动profile可以通过在spring-test模块中使用@ActiveProfiles注解来声明(参见环境profile的上下文配置(context configuration with environment profiles))。

注意,profile不是一个“非此即彼”(“either-or” )的命题。您可以一次激活多个配置文件。通过编程方式,您可以向setActiveProfiles()方法提供多个profile名称,该方法接受String…下面的示例激活多个配置文件:

ctx.getEnvironment().setActiveProfiles("profile1", "profile2");

声明性地,spring.profiles.active可以接受以逗号分隔的配置文件名列表,如下例所示:

    -Dspring.profiles.active="profile1,profile2"

默认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提供默认定义的一种方式。如果启用了任何配置文件,则不应用默认配置文件

您可以通过在环境中使用setDefaultProfiles()或使用spring.profiles.default属性来更改默认配置文件的名称。

1.13.2. PropertySource 抽象(PropertySource Abstraction)

Spring的Environment抽象在属性源的可配置层次结构上提供搜索操作。考虑下面的清单:

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是对任何键值对源的简单抽象,Spring的StandardEnvironment配置了两个PropertySource对象——一个表示JVM系统属性集System.getProperties()),另一个表示系统环境变量集System.getenv())。

提示

这些默认属性源是为StandardEnvironment提供的,用于独立应用程序。StandardServletEnvironment使用额外的默认属性源填充,包括servlet配置、servlet上下文参数和JndiPropertySource(如果JNDI可用)。

具体地说,当您使用StandardEnvironment时,如果运行时存在my-property系统属性my-property环境变量,则调用env.containsProperty("my-property")返回true

执行的搜索是分层的默认情况下,系统属性优先于环境变量。因此,如果在调用envy.getproperty(“my-property”)期间碰巧在两个地方都设置了my-property属性,则系统属性值“胜出”并被返回。注意,属性值不是合并的,而是被前面的条目完全覆盖

对于一个通用的StandardServletEnvironment完整的层次结构如下所示,最高优先级的条目位于顶部:

  1. ServletConfig参数(如果适用——例如,DispatcherServlet上下文)
  2. ServletContext参数(web.xml上下文参数条目)
  3. JNDI环境变量(java:comp/env/ entries)
  4. JVM系统属性(-D命令行参数)
  5. JVM系统环境(操作系统环境变量)

最重要的是,整个机制是可配置的。也许您希望将自定义的属性源集成到此搜索中。为此,实现并实例化您自己的PropertySource,并将其添加到当前EnvironmentPropertySources集合中。下面的例子展示了如何这样做:

ConfigurableApplicationContext ctx = new GenericApplicationContext();
MutablePropertySources sources = ctx.getEnvironment().getPropertySources();
sources.addFirst(new MyPropertySource());

在前面的代码中,MyPropertySource在搜索中以最高优先级被添加。如果它包含my-property属性,则检测并返回该属性,以支持任何其他PropertySource中的任何my-property属性。MutablePropertySources API公开了许多方法,这些方法允许对属性源集进行精确操作

1.13.3. 使用@PropertySource

@PropertySource注解为向Spring Environment添加PropertySource提供了一种方便的声明性机制。

给定一个名为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

提示

根据Java 8约定,@PropertySource注解是可重复的。然而,所有这样的@PropertySource注解都需要在同一层声明,要么直接在配置类上声明,要么作为同一自定义注解中的元注解声明。不建议混合使用直接注解和元注解,因为直接注解会有效地覆盖元注解

1.13.4. 语句中的占位符解析(Placeholder Resolution in Statements)

过去,元素中占位符的值只能根据JVM系统属性环境变量来解析。现在情况已经不同了。因为Environment抽象集成在整个容器中,所以很容易通过它路由占位符的解析。这意味着您可以以任何喜欢的方式配置解析过程。您可以更改搜索系统属性和环境变量的优先级,或者完全删除它们。您还可以酌情将自己的属性源添加到混合中。

具体地说,只要customer属性在环境中可用,无论customer属性在哪里定义,下面的语句都有效:

<beans>
    <import resource="com/bank/service/${customer}-config.xml"/>
</beans>

1.14 注册一个LoadTimeWeaver

Spring使用LoadTimeWeaver类加载到Java虚拟机(JVM)时对它们进行动态转换

启用加载时编织,你可以将@EnableLoadTimeWeaving添加到你的@Configuration类中,如下面的例子所示:

@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}

另外,对于XML配置,你可以使用<context:load-time-weaver/>元素:

<beans>
    <context:load-time-weaver/>
</beans>

一旦配置好ApplicationContext,该ApplicationContext中的任何bean都可以实现LoadTimeWeaverAware,从而接收到对加载时编织实例的引用。这在与Spring的JPA支持(Spring’s JPA support)结合使用时特别有用,因为加载时编织可能是JPA类转换所必需的。有关更多细节,请参阅LocalContainerEntityManagerFactoryBean javadoc。有关AspectJ加载时编织的更多信息,请参阅Spring框架中使用AspectJ进行加载时编织(Load-time Weaving with AspectJ in the Spring Framework)

1.15. ApplicationContext的附加功能(Additional Capabilities of the ApplicationContext

正如本章介绍中所讨论的,org.springframework.beans.factory包提供了管理操作bean的基本功能,包括以编程的方式。org.springframework.context包添加了ApplicationContext接口,它扩展了BeanFactory接口,此外还扩展了其他接口,以更面向应用程序框架的风格提供额外的功能。许多人以完全声明式的方式使用ApplicationContext,甚至不以编程方式创建它,而是依靠诸如ContextLoader之类的支持类来自动实例化ApplicationContext,作为Java EE web应用程序正常启动过程的一部分。

为了以更面向框架的风格增强BeanFactory功能,上下文包还提供了以下功能:

  • 通过MessageSource接口以i18n样式访问消息。
  • 通过ResourceLoader接口访问资源,如URL和文件。
  • 事件发布,即通过使用ApplicationEventPublisher接口实现ApplicationListener接口的bean。
  • 通过HierarchicalBeanFactory接口加载多个(分层)上下文,让每个上下文都专注于一个特定的层,例如应用程序的web层。

补充

i18n:是Internationalization 单词的缩写

1.15.1. 使用MessageSource进行国际化(Internationalization using 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
  • 上述方法中使用的所有属性也都包装在一个名为MessageSourceResolvable类中,您可以与此方法一起使用该类。

当加载ApplicationContext时,它会自动搜索上下文中定义的MessageSource bean。bean的名称必须为messageSource。如果找到了这样的bean,那么对上述方法的所有调用都将委托给消息源。如果没有找到消息源,ApplicationContext将尝试在父容器查找包含同名bean。如果是,它将使用该bean作为MessageSource。如果ApplicationContext找不到任何消息源,则实例化一个空的DelegatingMessageSource,以便能够接受对上面定义的方法的调用。

Spring提供了三个MessageSource实现,ResourceBundleMessageSource, ReloadableResourceBundleMessageSourceStaticMessageSource。它们都实现了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>

该示例假设在类路径中定义了三个资源包,分别是formatexceptionswindows。任何解析消息的请求都是以通过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", Locale.ENGLISH);
    System.out.println(message);
}

上述程序的输出结果如下:

Alligators rock!

总之,MessageSource是在一个名为beans.xml的文件中定义的,该文件存在于类路径的根目录中。messageSource bean定义通过其basenames属性引用许多资源包。在列表中传递给basenames属性的三个文件作为文件存在于类路径的根目录中,它们被称为

  • format.properties
  • exceptions.properties
  • windows.properties

下一个示例显示传递给消息查找的参数。这些参数被转换为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", Locale.ENGLISH);
        System.out.println(message);
    }
}

调用execute()方法的结果输出如下:

The userDao argument is required.

关于国际化(“i18n”),Spring的各种MessageSource实现遵循与标准JDK ResourceBundle相同的语言环境解析和回退规则。简而言之,继续前面定义的messageSource示例,如果希望根据英国(en-GB)语言环境解析消息,则需要分别创建名为format_en_GB.propertiesexceptions_en_GB.propertieswindows_en_GB.properties

通常,区域设置解析由应用程序的周围环境管理。在下面的示例中,(British)消息解析的区域设置是手动指定的:

# 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的引用。在实现MessageSourceAware接口的ApplicationContext中定义的任何bean在创建和配置bean时都会被注入应用程序上下文的MessageSource

提示

因为Spring的MessageSource基于Java的ResourceBundle,所以它不会合并具有相同基名的bundle,而只会使用找到的第一个bundle。具有相同基名的后续消息包将被忽略。

提示

作为ResourceBundleMessageSource的替代方案,Spring提供了一个ReloadableResourceBundleMessageSource类。这个变体支持相同的包文件格式,但比基于标准JDK的ResourceBundleMessageSource实现更灵活。特别是,它允许从任何Spring资源位置(不仅仅是从类路径)读取文件,并支持bundle属性文件的热重新加载(同时在两者之间有效地缓存它们)。详情请参见ReloadableResourceBundleMessageSourcejavadoc。

1.15.2. 标准事件和自定义事件(Standard and Custom Events)

ApplicationContext中的事件处理是通过ApplicationEvent类和ApplicationListener接口提供的。如果将实现ApplicationListener接口的bean部署到上下文中,则每次将ApplicationEvent发布到ApplicationContext时,都会通知该bean。本质上,这是标准的观察者设计模式(Observer design pattern)。

从Spring 4.2开始,事件基础设施得到了显著改进,并提供了基于注解的模型(annotation-based model) 以及发布任意事件的能力(也就是说,不一定是从ApplicationEvent扩展的对象)。当这样的对象被发布时,我们将它包装在一个事件中。

下表描述了Spring提供的标准事件:

事件(Event)解释
ContextRefreshedEventApplicationContext被初始化或刷新时发布(例如,通过在ConfigurableApplicationContext接口上使用refresh()方法)。这里,“初始化”意味着加载所有bean,检测并激活后置处理器bean,预实例化单例,并准备好使用ApplicationContext对象。只要上下文没有关闭,只要所选的ApplicationContext实际上支持这种**“热”刷新**,刷新就可以被触发多次。例如,XmlWebApplicationContext支持热刷新,但GenericApplicationContext不支持。
ContextStartedEventConfigurableApplicationContext接口上使用start()方法启动ApplicationContext发布。这里,“已启动”意味着所有生命周期bean都接收显式启动信号。通常,该信号用于在显式停止后重新启动bean,但它也可以用于启动未配置为自动启动的组件(例如,在初始化时尚未启动的组件)。
ContextStoppedEventConfigurableApplicationContext接口上使用stop()方法停止ApplicationContext时发布。这里,“已停止”意味着所有生命周期bean都接收到显式停止信号。停止的上下文可以通过start()调用重新启动。
ContextClosedEvent通过在ConfigurableApplicationContext接口上使用close()方法或通过JVM关闭钩子关闭ApplicationContext时发布。这里,“关闭”意味着所有的单例bean都将被销毁。一旦上下文关闭,它的生命周期就结束了,不能刷新或重新启动。
RequestHandledEvent一个特定于web的事件,告诉所有bean一个HTTP请求已经得到了服务。此事件在请求完成后发布。这个事件只适用于使用Spring的DispatcherServlet的web应用程序。
ServletRequestHandledEventRequestHandledEvent的子类,用于添加servlet特定的上下文信息。

您还可以创建发布自己的自定义事件。下面的例子展示了一个扩展Spring的ApplicationEvent基类的简单类:

public class BlockedListEvent extends ApplicationEvent {

    private final String address;
    private final String content;

    public BlockedListEvent(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> blockedList;
    private ApplicationEventPublisher publisher;

    public void setBlockedList(List<String> blockedList) {
        this.blockedList = blockedList;
    }

    public void setApplicationEventPublisher(ApplicationEventPublisher publisher) {
        this.publisher = publisher;
    }

    public void sendEmail(String address, String content) {
        if (blockedList.contains(address)) {
            publisher.publishEvent(new BlockedListEvent(this, address, content));
            return;
        }
        // send email...
    }
}

在配置时,Spring容器检测到EmailService实现了ApplicationEventPublisherAware并自动调用setApplicationEventPublisher()。实际上,传入的参数是Spring容器本身。您通过应用程序的ApplicationEventPublisher接口与应用程序上下文进行交互。

接收定制的ApplicationEvent,您可以创建一个实现ApplicationListener的类,并将其注册为Spring bean。下面的例子展示了这样一个类:

public class BlockedListNotifier implements ApplicationListener<BlockedListEvent> {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    public void onApplicationEvent(BlockedListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

注意,ApplicationListener一般是用自定义事件的类型参数化的(在前面的例子中是BlockedListEvent)。这意味着onApplicationEvent()方法可以保持类型安全,避免任何向下转换的需要。您可以注册任意数量的事件监听器,但请注意,默认情况下事件监听器以同步方式接收事件。这意味着publishhevent()方法将阻塞直到所有监听器完成对事件的处理。这种同步和单线程方法的一个优点是,当监听器接收到事件时,如果事务上下文可用,它将在发布者的事务上下文中操作。如果需要另一种事件发布策略,请参阅javadoc中Spring的ApplicationEventMulticaster接口和SimpleApplicationEventMulticaster实现的配置选项。

下面的示例显示了用于注册和配置上面每个类的bean定义:

<bean id="emailService" class="example.EmailService">
    <property name="blockedList">
        <list>
            <value>known.spammer@example.org</value>
            <value>known.hacker@example.org</value>
            <value>john.doe@example.org</value>
        </list>
    </property>
</bean>

<bean id="blockedListNotifier" class="example.BlockedListNotifier">
    <property name="notificationAddress" value="blockedlist@example.org"/>
</bean>

综上所述,当调用emailService bean的sendEmail()方法时,如果有任何应该被阻止的电子邮件消息,则会发布一个BlockedListEvent类型的自定义事件。blockedListNotifier bean注册为ApplicationListener并接收BlockedListEvent,此时它可以通知适当的各方。

提示

Spring的事件机制是为相同应用程序上下文中的Spring bean之间的简单通信而设计的。然而,对于更复杂的企业集成需求,单独维护的Spring整合(Spring Integration) 项目为构建轻量级的、面向模式的(pattern-oriented)、事件驱动的体系结构提供了完整的支持,这些体系结构构建在众所周知的Spring编程模型之上。

基于注解的事件监听器

您可以使用@EventListener注解在托管bean的任何方法上注册事件监听器。BlockedListNotifier可以重写如下:

public class BlockedListNotifier {

    private String notificationAddress;

    public void setNotificationAddress(String notificationAddress) {
        this.notificationAddress = notificationAddress;
    }

    @EventListener
    public void processBlockedListEvent(BlockedListEvent event) {
        // notify appropriate parties via notificationAddress...
    }
}

方法签名再次声明它要侦听的事件类型,但这一次使用了一个灵活的名称,并且没有实现特定的监听器接口。事件类型也可以通过泛型缩小范围,只要实际事件类型在其实现层次结构中解析泛型参数即可。

如果您的方法应该侦听多个事件,或者您想在没有任何参数的情况下定义它,那么也可以在注解本身上指定事件类型。下面的例子展示了如何这样做:

@EventListener({ContextStartedEvent.class, ContextRefreshedEvent.class})
public void handleContextStart() {
    // ...
}

还可以通过使用定义SpEL表达式(SpEL expression)的注解的条件属性添加额外的运行时过滤,该注解应该与实际调用特定事件的方法相匹配。

下面的例子展示了我们的通知器如何被重写为只有当事件的content属性等于my-event时才会被调用:

@EventListener(condition = "#blEvent.content == 'my-event'")
public void processBlockedListEvent(BlockedListEvent blEvent) {
    // notify appropriate parties via notificationAddress...
}

每个SpEL表达式针对一个专用上下文求值。下表列出了上下文可用的项,以便您可以将它们用于条件事件处理:

名称位置描述示例
事件(Event)根对象(root object)实际的ApplicationEvent.#root.eventevent
参数数组(Arguments array)根对象(root object)用于调用该方法的参数(作为对象数组)。#root.argsargs; args[0] 用于访问第一个参数,等等.
参数名称(Argument name)评估上下文(evaluation context)任何方法参数的名称。如果由于某种原因,名称不可用(例如,因为编译的字节码中没有调试信息),也可以使用#a<#arg>语法,这里<#arg>表示参数index(从0开始)。#blEvent#a0 (你也可以使用#p0#p<#arg> 作为别名的参数表示法)

注意#root.event使您能够访问底层事件,即使您的方法签名实际上引用了已发布的任意对象。

如果你需要发布一个事件作为处理另一个事件的结果,你可以改变方法签名来返回应该发布的事件,如下面的例子所示:

@EventListener
public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) {
    // notify appropriate parties via notificationAddress and
    // then publish a ListUpdateEvent...
}

提示

asynchronous listeners不支持此特性。

handleBlockedListEvent()方法为它处理的每个BlockedListEvent发布一个新的ListUpdateEvent。如果需要发布多个事件,则可以返回一个Collection或事件数组。

异步监听器(Asynchronous Listeners)

如果希望特定的监听器异步处理事件,可以重用常规的@Async支持(regular @Async support)。下面的例子展示了如何这样做:

@EventListener
@Async
public void processBlockedListEvent(BlockedListEvent event) {
    // BlockedListEvent is processed in a separate thread
}

在使用异步事件时,请注意以下限制:

  • 如果异步事件监听器抛出异常,则不会将其传播给调用者。参见AsyncUncaughtExceptionHandler了解更多细节。
  • 异步事件监听器方法不能通过返回值来发布后续事件。如果您需要发布另一个事件作为处理的结果,则注入ApplicationEventPublisher来手动发布事件。

排序监听器(Ordering Listeners)

如果你需要一个监听器在另一个监听器之前被调用,你可以在方法声明中添加@Order注解,如下例所示:

@EventListener
@Order(42)
public void processBlockedListEvent(BlockedListEvent event) {
    // notify appropriate parties via notificationAddress...
}

通用的事件(Generic Events)

还可以使用泛型进一步定义事件的结构。考虑使用EntityCreatedEvent<T>其中T是被创建实体的类型。例如,您可以创建以下监听器定义来仅接收PersonEntityCreatedEvent

@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. 方便地访问低级资源(Convenient Access to Low-level Resources)

为了最佳地使用和理解应用程序上下文,您应该熟悉Spring的资源抽象,如Resources中所述。

应用程序上下文是一个ResourceLoader,它可以用来加载Resource对象Resource本质上是JDK java.net.URL类的一个功能更丰富的版本。事实上,Resource的实现在适当的地方包装了一个java.net.URL实例。Resource可以以透明的方式从几乎任何位置获取低级资源,包括从类路径文件系统位置、任何可以用标准URL描述的位置,以及其他一些变体。如果资源位置字符串是一个没有任何特殊前缀的简单路径,那么这些资源的来源是特定的,并且适合于实际的应用程序上下文类型。

您可以配置部署到应用程序上下文中的bean,以实现特殊的回调接口ResourceLoaderAware,以便在初始化时自动回调,同时将应用程序上下文本身作为ResourceLoader传入。您还可以公开Resource类型的属性,以用于访问静态资源。它们像其他属性一样被注入其中。您可以将这些Resource属性指定为简单的String路径,并依赖于部署bean时从这些文本字符串到实际Resource对象的自动转换。

提供给ApplicationContext构造函数的位置路径实际上是资源字符串,以简单的形式,根据特定的上下文实现进行适当的处理。例如,ClassPathXmlApplicationContext将简单的位置路径视为类路径位置。您还可以使用带有特殊前缀的位置路径(资源字符串)来强制从类路径或URL加载定义,而不管实际的上下文类型是什么。

1.15.4. 应用程序启动跟踪(Application Startup Tracking)

ApplicationContext管理Spring应用程序的生命周期,并围绕组件提供丰富的编程模型。因此,复杂的应用程序可以具有同样复杂的组件图和启动阶段

使用特定的指标跟踪应用程序启动步骤可以帮助理解在启动阶段时间花在哪里,但它也可以作为一种更好地理解整个上下文生命周期的方法。

AbstractApplicationContext(和它的子类)被一个ApplicationStartup工具化,它收集关于不同启动阶段的StartupStep数据:

  • 应用程序上下文生命周期(基包扫描、配置类管理)
  • bean生命周期(实例化、智能初始化、后期处理)
  • 应用程序事件处理

下面是一个在AnnotationConfigApplicationContext中插装的例子:

// 创建一个启动步骤并且开始记录
StartupStep scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan");
// 在当前步骤添加标记信息
scanPackages.tag("packages", () -> Arrays.toString(basePackages));
// 执行我们正在测试的实际阶段
this.scanner.scan(basePackages);
// 结束当前步骤
scanPackages.end();

应用程序上下文已经通过多个步骤进行了检测。一旦记录下来,这些启动步骤就可以用特定的工具收集、显示和分析。要获得现有启动步骤的完整列表,您可以查看专门的附录部分

默认的ApplicationStartup实现是一个无操作的变体,以最小化开销。这意味着默认情况下,在应用程序启动期间不会收集任何指标。Spring框架附带了一个用Java Flight Recorder跟踪启动步骤的实现:FlightRecorderApplicationStartup。要使用此变体,必须在创建ApplicationContext后立即将其实例配置到ApplicationContext

如果开发人员正在提供他们自己的AbstractApplicationContext子类,或者如果他们希望收集更精确的数据,他们也可以使用ApplicationStartup基础结构。

ApplicationStartup只用于应用程序启动和核心容器;这绝不是Java分析器或度量库(如 Micrometer)的替代品。

要开始收集自定义的StartupStep,组件可以直接从应用程序上下文中获取ApplicationStartup实例,让它们的组件实现ApplicationStartupAware,或者在任何注入点请求ApplicationStartup类型。

提示

当创建自定义启动步骤时,开发人员不应该使用"spring.*"命名空间。这个名称空间保留给Spring内部使用,可能会有变化。

1.15.5. 方便的ApplicationContext实例化Web应用程序(Convenient ApplicationContext Instantiation for Web Applications)

您可以通过使用(例如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>

<!-- 指定ContextLoaderListener用于初始化ApplicationContext -->
<listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>

监听器检查contextConfigLocation参数。如果该参数不存在,监听器将使用/WEB-INF/applicationContext.xml作为默认值。当参数存在时,监听器使用预定义的分隔符(逗号、分号和空格)分隔String,并使用这些值作为搜索应用程序上下文的位置。还支持ant风格的路径模式。例如/WEB-INF/*Context.xml(适用于所有以Context.xml结尾并且位于WEB-INF目录中的文件)和/WEB-INF/**/*Context.xml(适用于WEB-INF的任何子目录中的所有此类文件)。

1.15.6. 将Spring ApplicationContext部署为Java EE RAR文件

可以将Spring ApplicationContext作为RAR文件部署,将上下文及其所需的所有bean类和库jar封装在Java EE RAR部署单元中。这相当于引导一个能够访问Java EE服务器设施的独立ApplicationContext(仅托管在Java EE环境中)RAR部署是部署无头WAR文件场景的一种更自然的替代方案——实际上,没有任何HTTP入口点的WAR文件仅用于在Java EE环境中引导Spring ApplicationContext

RAR部署非常适合不需要HTTP入口点,而只包含消息端点和计划作业的应用程序上下文。这种上下文中的bean可以使用应用服务器资源,比如JTA事务管理器和JNDI绑定的JDBC DataSource实例和JMS ConnectionFactory实例,还可以向平台的JMX服务器注册——所有这些都是通过Spring的标准事务管理以及JNDI和JMX支持设施完成的。应用组件还可以通过Spring的TaskExecutor抽象与应用服务器的JCA WorkManager进行交互。

有关RAR部署中涉及的配置细节,请参阅SpringContextResourceAdapter类的javadoc。

简单地将Spring ApplicationContext部署为Java EE RAR文件:

  1. 将所有应用程序类打包到一个RAR文件中(这是一个具有不同文件扩展名的标准JAR文件)。
  2. 将所有必需的库JAR添加到RAR存档的根目录中。
  3. 添加一个META-INF/ra.xml部署描述符(如SpringContextResourceAdapter的javadoc所示)和相应的Spring XML bean定义文件(通常是META-INF/applicationContext.xml)。
  4. 将生成的RAR文件放入应用服务器的部署目录中。

提示

这种RAR部署单元通常是独立的。它们不向外界公开组件,甚至不向同一应用程序的其他模块公开组件。与基于rar的ApplicationContext的交互通常通过与其他模块共享的JMS目的地进行。例如,基于RAR的ApplicationContext还可以调度一些作业或对文件系统中的新文件做出反应(或类似的)。如果它需要允许来自外部的同步访问,它可以(例如)导出RMI端点,这些端点可以被同一机器上的其他应用程序模块使用。

1.16. BeanFactory API

BeanFactory API为Spring的IoC功能提供了底层基础。它的特定约定主要用于与Spring的其他部分和相关的第三方框架集成,它的DefaultListableBeanFactory实现是高级GenericApplicationContext容器中的关键委托。

BeanFactory和相关接口(如BeanFactoryAwareInitializingBeanDisposableBean)是其他框架组件的重要集成点。由于不需要任何注解甚至反射,它们允许容器与其组件之间进行非常有效的交互。应用程序级bean可以使用相同的回调接口,但通常更喜欢通过注解编程配置进行声明性依赖注入。

请注意,核心BeanFactory API级别及其DefaultListableBeanFactory实现没有对要使用的配置格式任何组件注解做出假设。所有这些风格都是通过扩展(如XmlBeanDefinitionReaderAutowiredAnnotationBeanPostProcessor)实现的,并将共享BeanDefinition对象作为核心元数据表示进行操作。这是使Spring的容器如此灵活和可扩展的本质

1.16.1. BeanFactoryApplicationContext

本节将解释BeanFactoryApplicationContext容器级别之间的区别,以及它们对引导的影响。

你应该使用ApplicationContext,除非你有很好的理由不这样做,使用GenericApplicationContext和它的子类AnnotationConfigApplicationContext作为自定义引导的通用实现。这些是Spring核心容器的主要入口点,用于所有常见目的:加载配置文件触发类路径扫描以编程方式注册bean定义和带注解的类,以及(从5.0开始)注册功能bean定义。

因为**ApplicationContext包含了BeanFactory的所有功能**,所以除了需要完全控制bean处理的场景外,通常建议使用它而不是普通的BeanFactory。在ApplicationContext(例如GenericApplicationContext实现)中,按照约定(即按bean名称或bean类型—特别是后置处理器)检测几种bean,而普通的DefaultListableBeanFactory不知道任何特殊的bean。

对于许多扩展的容器特性,比如注解处理AOP代理BeanPostProcessor扩展点(BeanPostProcessor extension point)是必不可少的。如果只使用普通的DefaultListableBeanFactory默认情况不会检测和激活此类后置处理器。这种情况可能令人困惑,因为您的bean配置实际上没有任何问题。相反,在这种情况下,需要通过额外的设置来完全引导容器。

下表列出了BeanFactoryApplicationContext接口和实现提供的特性。

特征BeanFactoryApplicationContext
Bean实例化/装配(Bean instantiation/wiring)YesYes
整合的生命周期管理(Integrated lifecycle management)NoYes
自动BeanPostProcessor注册(Automatic BeanPostProcessor registration)NoYes
自动BeanFactoryPostProcessor注册(Automatic BeanFactoryPostProcessor registration)NoYes
方便的MessageSource访问(用于国际化)(Convenient MessageSource access (for internationalization))NoYes
内置ApplicationEvent发布机制(Built-in ApplicationEvent publication mechanism)NoYes

要显式地用DefaultListableBeanFactory注册一个bean后置处理器,你需要通过编程方式调用addBeanPostProcessor,如下面的例子所示:

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
// 将Bean定义填充到Bean工厂

// 注册任何BeanPostProcessor实例
factory.addBeanPostProcessor(new AutowiredAnnotationBeanPostProcessor());
factory.addBeanPostProcessor(new MyBeanPostProcessor());

// 开始使用Bean工厂

要将BeanFactoryPostProcessor应用到一个普通的DefaultListableBeanFactory,你需要调用它的postProcessBeanFactory方法,如下面的例子所示:

DefaultListableBeanFactory factory = new DefaultListableBeanFactory();
XmlBeanDefinitionReader reader = new XmlBeanDefinitionReader(factory);
reader.loadBeanDefinitions(new FileSystemResource("beans.xml"));

// 从Properties文件引入一些属性值
PropertySourcesPlaceholderConfigurer cfg = new PropertySourcesPlaceholderConfigurer();
cfg.setLocation(new FileSystemResource("jdbc.properties"));

// 现在做替换
cfg.postProcessBeanFactory(factory);

在这两种情况下,显式注册步骤都不方便,这就是为什么在Spring支持的应用程序中,各种ApplicationContext变体比普通的DefaultListableBeanFactory更受欢迎,特别是在典型的企业设置中依赖BeanFactoryPostProcessorBeanPostProcessor实例来扩展容器功能时。

提示

一个AnnotationConfigApplicationContext已经注册了所有通用的注解后置处理器,并且可以通过配置注解(如@EnableTransactionManagement)在底层引入额外的处理器。在Spring基于注解的配置模型的抽象层,bean后置处理器的概念变成了仅仅是内部容器细节。