【译】IOC容器3

1.10. Classpath扫描和管理的组件

本章中的大多数例子都使用XML来指定配置元数据,在Spring容器中产生每个 BeanDefinition。上一节(基于注解的容器配置)演示了如何通过源级注解提供大量的配置元数据。然而,即使在这些例子中,"基础" Bean定义也是在XML文件中明确定义的,而注解只驱动依赖性注入。本节描述了一个通过扫描classpath来隐式检测候选组件的选项。候选组件是与过滤器标准相匹配的类,并且有一个在容器中注册的相应的bean定义。这样就不需要使用 XML 来执行 bean 注册了。相反,你可以使用注解(例如 @Component)、AspectJ类型表达式或你自己的自定义过滤标准来选择哪些类在容器中注册了Bean定义。

你可以使用Java来定义 Bean,而不是使用XML文件。看看 @Configuration@Bean@Import@DependsOn 注解,看看如何使用这些功能的例子。
1.10.1. @Component 和进一步的 Stereotype 注解

@Repository 注解是任何满足 repository(也被称为数据访问对象或 DAO)角色或 stereotype 的类的标记。这个标记的用途包括异常的自动翻译,如 异常翻译 中所述。

Spring提供了更多的 stereotype 注解。@Component, @Service, 和 @Controller@Component 是一个通用的stereotype,适用于任何Spring管理的组件。@Repository@Service@Controller@Component 的特殊化,用于更具体的使用情况(分别在持久层、服务层和表现层)。因此,你可以用 @Component 来注解你的组件类,但是,通过用 @Repository@Service@Controller 来注解它们,你的类更适合于被工具处理或与切面关联。例如,这些stereotype注解是指向性的理想目标。在Spring框架的未来版本中,@Repository@Service@Controller 还可以携带额外的语义。因此,如果你要在服务层使用 @Component@Service 之间进行选择,@Service 显然是更好的选择。同样地,如前所述,@Repository 已经被支持作为持久层中自动异常翻译的标记。

1.10.2. 使用元注解和组合注解

Spring提供的许多注解都可以在你自己的代码中作为元注解使用。元注解是一个可以应用于另一个注解的注解。例如,前面 提到的 @Service 注解是用 @Component 进行元注解的,如下例所示。

Java

Kotlin

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component 
public @interface Service {
​
    // ...
}
@Component 使 @Service@Component 的处理方式相同。

你也可以结合元注解来创建 “composed annotations”(组合注解)。例如,Spring MVC的 @RestController 注解是由 @Controller@ResponseBody 组成。

此外,组合注解可以选择性地重新声明来自元注解的属性以允许定制。当你想只暴露元注解的一个子集的属性时,这可能特别有用。例如,Spring的 @SessionScope 注解将 scope 名称硬编码为 session,但仍然允许自定义 proxyMode。下面的列表显示了 SessionScope 注解的定义。

Java

Kotlin

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

然后,你可以使用 @SessionScope,而不用声明 proxyMode,如下所示。

Java

Kotlin

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

你也可以覆盖 proxyMode 的值,如下例所示。

Java

Kotlin

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

更多细节,请参见 Spring 注解编程模型 维基页面。

1.10.3. 自动检测类和注册Bean定义

Spring可以自动检测 stereotype 的类,并在 ApplicationContext 中注册相应的 BeanDefinition 实例。例如,以下两个类符合这种自动检测的条件。

Java

Kotlin

@Service
public class SimpleMovieLister {
​
    private MovieFinder movieFinder;
​
    public SimpleMovieLister(MovieFinder movieFinder) {
        this.movieFinder = movieFinder;
    }
}

Java

Kotlin

@Repository
public class JpaMovieFinder implements MovieFinder {
    // implementation elided for clarity
}

为了自动检测这些类并注册相应的Bean,你需要在你的 @Configuration 类中添加 @ComponentScan,其中 basePackages 属性是这两个类的共同父包。(或者,你可以指定一个用逗号或分号或空格分隔的列表,其中包括每个类的父包。)

Java

Kotlin

@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig  {
    // ...
}
为了简洁起见,前面的例子可以使用注解的 value 属性(即 @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
        https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/context
        https://www.springframework.org/schema/context/spring-context.xsd">
​
    <context:component-scan base-package="org.example"/>
​
</beans>
使用 就隐含地实现了 的功能。在使用 时,通常不需要包括 元素。
扫描classpath包需要classpath中存在相应的目录项。当你用Ant构建JAR时,确保你没有激活JAR任务的 files-only 开关。另外,在某些环境中,根据安全策略,classpath目录可能不会被暴露—例如,JDK 1.7.0_45及以上版本的独立应用程序(这需要在清单中设置 'Trusted-Library',见 java jre 7u45 breaks classloader.getResources()? - Stack Overflow)。在JDK 9的模块路径(Jigsaw)上,Spring的classpath扫描一般都能按预期工作。但是,请确保你的组件类在你的 module-info 描述符中被导出。如果你希望Spring调用你的类中的非public成员,请确保它们是 "开放的"(也就是说,它们在你的 module-info 描述符中使用 opens 声明而不是 exports 声明)。

此外,当你使用组件扫描元素时,AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor 都被隐含地包括在内。这意味着这两个组件被自动检测并连接在一起—所有这些都不需要在XML中提供任何bean配置元数据。

你可以通过包含值为 falseannotation-config 属性来禁用 AutowiredAnnotationBeanPostProcessorCommonAnnotationBeanPostProcessor 的注册。
1.10.4. 使用Filter来自定义扫描

默认情况下,用 @Component@Repository@Service@Controller@Configuration 注解的类,或者本身用 @Component 注解的自定义注解是唯一被检测到的候选组件。然而,你可以通过应用自定义 filter 来修改和扩展这种行为。将它们作为 @ComponentScan 注解的 includeFiltersexcludeFilters 属性(或作为XML配置中 元素的` 子元素)。每个 filter 元素都需要typeexpression` 属性。下表描述了过滤选项。

Filter Type示例表达式说明
注解 (默认)org.example.SomeAnnotation一个注解在目标组件中的类型级别是 presentmeta-present
可指定org.example.SomeClass目标组件可分配给(继承或实现)的一个类(或接口)。
aspectjorg.example..*Service+要被目标组件匹配的 AspectJ type 表达式。
regexorg\.example\.Default.*一个与目标组件的类名相匹配的 regex expression。
自定义org.example.MyTypeFilterorg.springframework.core.type.TypeFilter 接口的自定义实现。

下面的例子显示了配置忽略了所有 @Repository 注解,而使用 “stub” repository。

Java

Kotlin

@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 或者提供 use-default-filters="false" 作为 ` 元素的属性来禁用默认过滤器。这将有效地禁止对用@Component@Repository@Service@Controller@RestController@Configuration` 注解或元注解的类进行自动检测。
1.10.5. 在组件中定义Bean元数据

Spring组件也可以向容器贡献Bean定义元数据。你可以用用于在 @Configuration 注解的类中定义Bean元数据的相同的 @Bean 注解来做到这一点。下面的例子展示了如何做到这一点。

Java

Kotlin

@Component
public class FactoryMethodComponent {

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

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

前面的类是一个Spring组件,在它的 doWork() 方法里有特定的应用代码。然而,它也贡献了一个Bean定义,它有一个工厂方法,引用了 publicInstance() 方法。@Bean 注解标识了工厂方法和其他Bean定义属性,例如通过 @Qualifier 注解标识了 qualifier 值。其他可以指定的方法级注解有 @Scope@Lazy 和自定义 qualifier 注解。

除了对组件初始化的作用,你还可以将 @Lazy 注解放在标有 @Autowired@Inject 的注入点上。在这种情况下,它导致了一个延迟解析的代理的注入。然而,这种代理方法是相当有限的。对于复杂的懒加载交互,特别是与可选的依赖关系相结合,我们推荐使用 ObjectProvider 来代替。

如前所述,支持自动注入的字段和方法,并额外支持 @Bean 方法的自动注入。下面的例子展示了如何做到这一点。

Java

Kotlin

@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表达式语言元素通过符号 #{ } 定义了该属性的值。对于 @Value 注解,表达式解析器被预设为在解析表达式文本时寻找Bean名称。

从Spring Framework 4.3开始,你也可以声明一个 InjectionPoint(或其更具体的子类: DependencyDescriptor)类型的工厂方法参数来访问触发创建当前Bean的请求注入点。请注意,这只适用于Bean实例的实际创建,而不适用于现有实例的注入。因此,这个功能对 prototype scope 的Bean最有意义。对于其它scope,工厂方法只看到在给定scope中触发创建新 bean 实例的注入点(例如,触发创建 lazy singleton bean 的依赖关系)。在这种情况下,你可以在语义上注意使用所提供的注入点元数据。下面的例子显示了如何使用 InjectionPoint

Java

Kotlin

@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处理或其他约束条件适用。

你可以将 @Bean 方法声明为 static 的,这样就可以在不创建其包含的配置类实例的情况下调用它们。在定义后处理bean(例如,BeanFactoryPostProcessorBeanPostProcessor 类型)时,这一点特别有意义,因为这种bean在容器生命周期的早期被初始化,并且应该避免在这一点上触发配置的其它部分。由于技术上的限制,对静态 @Bean 方法的调用永远不会被容器拦截,甚至在 @Configuration 类中也不会(如本节前面所述)。CGLIB子类只能覆盖非静态方法。因此,对另一个 @Bean 方法的直接调用具有标准的Java语义,结果是直接从工厂方法本身返回一个独立实例。@Bean 方法的Java语言可见性对Spring容器中产生的Bean定义没有直接影响。你可以在非 @Configuration 类中自由地声明你的工厂方法,也可以在任何地方为静态方法声明。然而, @Configuration 类中的常规 @Bean 方法需要是可重写的—也就是说,它们不能被声明为 privatefinal@Bean 方法也可以在特定组件或配置类的基类上发现,也可以在组件或配置类实现的接口中声明的Java 8默认方法上发现。这使得组成复杂的配置安排有了很大的灵活性,从Spring 4.2开始,甚至可以通过Java 8默认方法进行多重继承。最后,一个类可以为同一个Bean持有多个 @Bean 方法,作为对多个工厂方法的安排,根据运行时的可用依赖关系来使用。这与在其他配置情况下选择 "最环保" 的构造函数或工厂方法的算法相同。在构造时选择具有最大数量的可满足的依赖关系的变量,类似于容器在多个 @Autowired 构造函数之间进行选择。
1.10.6. 命名自动检测的组件

当一个组件作为扫描过程的一部分被自动检测到时,它的Bean名称由该扫描器已知的 BeanNameGenerator 策略生成。默认情况下,任何包含 name value 的Spring stereotype 注解(@Component@Repository@Service@Controller)都会向相应的Bean定义提供该名称。

如果这样的注解不包含 name value,或者对于任何其他检测到的组件(比如那些由自定义过滤器发现的组件),默认的bean类名称生成器会返回未加大写的非限定类名称。例如,如果检测到以下组件类,其名称将是 myMovieListermovieFinderImpl

Java

Kotlin

@Service("myMovieLister")
public class SimpleMovieLister {
    // ...
}

Java

Kotlin

@Repository
public class MovieFinderImpl implements MovieFinder {
    // ...
}

如果你不想依赖默认的 Bean 命名策略,你可以提供一个自定义的 Bean 命名策略。首先,实现 BeanNameGenerator 接口,并确保包含一个默认的无参数构造函数。然后,在配置扫描器时提供全路径类名,正如下面的注解和 Bean 定义示例所示。

如果你由于多个自动检测的组件具有相同的非限定类名称(即具有相同名称的类,但驻留在不同的包中)而遇到命名冲突,你可能需要配置一个 BeanNameGenerator,它默认为生成的Bean名称的完全限定类名称。从Spring Framework 5.2.3开始,位于包 org.springframework.context.annotation 中的 FullyQualifiedAnnotationBeanNameGenerator 可以用于此类目的。

Java

Kotlin

@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. 为自动检测的组件提供一个Scope

与一般的Spring管理的组件一样,自动检测的组件的默认和最常见的scope是 singleton。然而,有时你需要一个不同的scope,可以通过 @Scope 注解来指定。你可以在注解中提供scope的名称,如下面的例子所示。

Java

Kotlin

@Scope("prototype")
@Repository
public class MovieFinderImpl implements MovieFinder {
    // ...
}
@Scope 注解只对具体的Bean类(对于注解的组件)或工厂方法(对于 @Bean 方法)进行内省。与XML bean定义相比,没有bean定义继承的概念,而且类级别的继承层次与元数据的目的无关。

有关Spring context 中 “request” 或 “session” 等Web特定 scope 的详细信息,请参阅 Request、 Session、 Application 和 WebSocket Scope。与这些 scope 的预制注解一样,你也可以通过使用Spring的元注解方法来组成你自己的 scope 注解:例如,用 @Scope("prototype") 元注解的自定义注解,可能还会声明一个自定义 scope 代理(scoped-proxy)模式。

为了给 scope 解析提供一个自定义的策略,而不是依赖基于注解的方法,你可以实现 ScopeMetadataResolver 接口。请确保包含一个默认的无参数构造函数。然后你可以在配置扫描器时提供全路径的类名,正如下面这个注解和 bean 定义的例子所示。

Java

Kotlin

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

当使用某些非 singleton scope 时,可能需要为 scope 对象生成代理。这在 “作为依赖的 Scope Bean”中有所描述。为了这个目的,在组件扫描元素上有一个 scoped-proxy 属性。三个可能的值是:nointerfacestargetClass。例如,以下配置的结果是标准的JDK动态代理。

Java

Kotlin

@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 注解在 “用 Qualifiers 微调基于注解的自动注入” 中讨论过。那一节中的例子演示了如何使用 @Qualifier 注解和自定义 qualifier 注解来提供细粒度的控制,当你解决自动注入候选对象时。因为这些例子是基于XML Bean定义的,qualifier 元数据是通过使用XML中 bean 元素的 qualifiermeta 子元素提供给候选Bean定义的。当依靠classpath扫描来自动检测组件时,你可以在候选类上用类型级注解来提供 qualifier 元数据。下面的三个例子演示了这种技术。

Java

Kotlin

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

Java

Kotlin

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

Java

Kotlin

@Component
@Offline
public class CachingMovieCatalog implements MovieCatalog {
    // ...
}
与大多数基于注解的替代方案一样,请记住,注解元数据是与类定义本身绑定的,而XML的使用允许同一类型的多个Bean在其限定符元数据中提供变化,因为该元数据是按实例而不是按类提供的。
1.10.9. 生成一个候选组件的索引

虽然classpath扫描非常快,但通过在编译时创建一个静态的候选列表,可以提高大型应用程序的启动性能。在这种模式下,所有作为组件扫描目标的模块都必须使用这种机制。

你现有的 @ComponentScan` 指令必须保持不变,以请求上下文扫描某些包中的候选者。当ApplicationContext` 检测到这样的索引时,它会自动使用它而不是扫描classpath。

要生成索引,请为每个包含组件的模块添加一个额外的依赖,这些组件是组件扫描指令的目标。下面的例子说明了如何用Maven做到这一点。

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

对于Gradle 4.5和更早的版本,应该在 compileOnly 配置中声明该依赖关系,如下面的例子所示。

dependencies {
    compileOnly "org.springframework:spring-context-indexer:6.0.8-SNAPSHOT"
}

在Gradle 4.6及以后的版本中,应该在 annotationProcessor 配置中声明该依赖,如以下例子所示。

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

spring-context-indexer 工件会生成一个 META-INF/spring.component 文件,并包含在jar文件中。

当在你的IDE中使用这种模式时,spring-context-indexer 必须被注册为注解处理器,以确保候选组件更新时索引是最新的。
当在classpath上发现 META-INF/spring.component 文件时,索引会自动启用。如果索引对某些库(或用例)是部分可用的,但不能为整个应用程序建立索引,你可以通过将 spring.index.ignore 设置为 true,或者通过 SpringProperties 机制,退回到常规的 classpath 安排(就像根本没有索引存在一样)。

1.11. 使用JSR 330标准注解

Spring提供对JSR-330标准注解的支持(依赖注入)。这些注解的扫描方式与Spring注解的扫描方式相同。要使用它们,你需要在你的classpath中拥有相关的jar。

如果你使用Maven,jakarta.inject 工件在标准的Maven资源库( Central Repository: jakarta/inject/jakarta.inject-api/2.0.0 )中可以找到。你可以在你的pom.xml文件中添加以下依赖。jakarta.inject jakarta.inject-api 2.0.0
1.11.1. 用 @Inject@Named 进行依赖注入

你可以使用 @Jakarta.inject.Inject 来代替 @Autowired,如下所示。

Java

Kotlin

public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

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

@Autowired 一样,你可以在字段级、方法级和构造函数-参数级使用 @Inject。此外,你可以将你的注入点声明为一个 Provider,允许按需访问较短scope的Bean,或通过 Provider.get() 调用懒加载地访问其他Bean。下面的例子提供了前述例子的一个变体。

Java

Kotlin

public class SimpleMovieLister {

    private Provider<MovieFinder> movieFinder;

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

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

如果你想为应该被注入的依赖使用一个 qualifier 的名字,你应该使用 @Named 注解,正如下面的例子所示。

Java

Kotlin

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

Java

Kotlin

public class SimpleMovieLister {

    @Inject
    public void setMovieFinder(@Nullable MovieFinder movieFinder) {
        // ...
    }
}
1.11.2. @Named@ManagedBean:与 @Component 注解的标准对等物

你可以使用 @jakarta.inject.Namedjakarta.annotation.ManagedBean 来代替 @Component,如下例所示。

Java

Kotlin

@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 可以以类似的方式使用,如下面的例子所示。

Java

Kotlin

@Named
public class SimpleMovieLister {

    private MovieFinder movieFinder;

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

    // ...
}

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

Java

Kotlin

@Configuration
@ComponentScan(basePackages = "org.example")
public class AppConfig  {
    // ...
}
@Component 相比,JSR-330的 @Named 和JSR-250的 @ManagedBean 注解是不可组合的。你应该使用Spring 的 stereotype 模型来构建自定义组件注解。
1.11.3. JSR-330 标准注解的局限性

当你使用标准注解工作时,你应该知道一些重要的功能是不可用的,如下表所示。

Springjakarta.inject.*jakarta.inject 限制 / 说明
@Autowired@Inject@Inject 没有 'required' 属性。可以用 Java 8的 Optional 来代替。
@Component@Named / @ManagedBeanJSR-330并没有提供一个可组合的模型,只是提供了一种识别命名组件的方法。
@Scope("singleton")@SingletonJSR-330的默认scope就像Spring的 prototype。然而,为了与Spring的一般默认值保持一致,在Spring容器中声明的JSR-330 Bean默认是一个 singleton。为了使用 singleton 以外的scope,你应该使用Spring的 @Scope 注解。jakarta.inject 也提供了一个 jakarta.inject.Scope 注解:不过,这个注解只用于创建自定义注解。
@Qualifier@Qualifier / @Namedjakarta.inject.Qualifier 只是一个用于构建自定义 qualifier 的元注解。具体的 String qualifier(像Spring的 @Qualifier 一样有一个值)可以通过 jakarta.inject.Named 来关联。
@Value-没有对应的
@Lazy-没有对应的
ObjectFactoryProviderjakarta.inject.Provider 是Spring的 ObjectFactory 的直接替代品,只是 get() 方法的名字比较短。它也可以与Spring的 @Autowired 结合使用,或者与非注解的构造器和setter方法结合使用。

1.12. 基于Java的容器配置

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

1.12.1. 基本概念:@Bean@Configuration

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

@Bean 注解用来表示一个方法实例化、配置和初始化了一个新的对象,由Spring IoC容器管理。对于那些熟悉Spring的 XML配置的人来说,`@Bean` 注解的作用与 元素的作用相同。你可以在任何Spring @Component 中使用 @Bean 注解的方法。然而,它们最常被用于 @Configuration Bean。

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

Java

Kotlin

@Configuration
public class AppConfig {

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

前面的 AppConfig 类等同于下面的 Spring `` XML。

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

完整的 @Configuration 与 "精简的" @Bean 模式?

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

与完整的 @Configuration 不同,精简的 @Bean 方法不能声明bean间的依赖关系。相反,它们对其包含的组件的内部状态进行操作,也可以选择对其可能声明的参数进行操作。因此,这样的 @Bean 方法不应该调用其他的 @Bean 方法。每个这样的方法从字面上看只是一个特定的Bean引用的工厂方法,没有任何特殊的运行时语义。这里的正面效果是,在运行时不需要应用CGLIB子类,所以在类的设计方面没有任何限制(也就是说,包含的类可以是 final 的等等)。

在常见的情况下,@Bean 方法要在 @Configuration 类中声明,确保始终使用 "完整" 模式,因此跨方法引用会被重定向到容器的生命周期管理。这可以防止同一个 @Bean 方法被意外地通过普通的Java调用来调用,这有助于减少在 "精简" 模式下操作时很难追踪的细微Bug。

下面几节将深入讨论 @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定义,并且假定DI元数据如 @Autowired@Inject 在必要时被用于这些类。

简单构造

与实例化 ClassPathXmlApplicationContext 时使用Spring XML文件作为输入一样,你可以在实例化 AnnotationConfigApplicationContext 时使用 @Configuration 类作为输入。这使得Spring容器的使用完全不需要XML,正如下面的例子所示。

Java

Kotlin

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注解的类都可以作为输入提供给构造函数,正如下面的例子所示。

Java

Kotlin

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 时特别有用。下面的例子展示了如何做到这一点。

Java

Kotlin

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 类添加如下注解。

Java

Kotlin

@Configuration
@ComponentScan(basePackages = "com.acme") 
public class AppConfig  {
    // ...
}
这个注解可以实现组件扫描。
有经验的Spring用户可能熟悉来自Spring的 context: namespace的XML声明等价物,如下例所示。

在前面的例子中,com.acme 包被扫描以寻找任何 @Component 注解的类,这些类被注册为容器中的Spring Bean定义。AnnotationConfigApplicationContext 暴露了 scan(String…) 方法,以实现同样的组件扫描功能,如下例所示。

Java

Kotlin

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 listener、Spring MVC DispatcherServlet 等时使用这个实现。下面的 web.xml 片段配置了一个典型的Spring MVC Web应用(注意使用 contextClasscontext-paraminit-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>
对于编程用例,GenericWebApplicationContext 可以作为 AnnotationConfigWebApplicationContext 的替代。请参阅 GenericWebApplicationContext javadoc 了解详情。
1.12.3. 使用 @Bean 注解

@Bean 是一个方法级注解,是XML 元素的直接类似物。该注解支持 所提供的一些属性,例如。

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

声明一个 Bean

为了声明一个Bean,你可以用 @Bean 注解来注解一个方法。你可以用这个方法在 ApplicationContext 中注册一个Bean定义,该类型被指定为该方法的返回值。默认情况下,Bean的名字和方法的名字是一样的。下面的例子显示了一个 @Bean 方法声明。

Java

Kotlin

@Configuration
public class AppConfig {

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

前面的配置完全等同于下面的Spring XML。

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

这两个声明使 ApplicationContext 中一个名为 transferService 的Bean可用,并与 TransferServiceImpl 类型的对象实例绑定,正如下面的文字图片所示。

transferService -> com.acme.TransferServiceImpl

你也可以使用 default 方法来定义Bean。这允许通过在默认方法上实现带有Bean定义的接口来组成Bean配置。

Java

public interface BaseConfig {

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

@Configuration
public class AppConfig implements BaseConfig {

}

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

Java

Kotlin

@Configuration
public class AppConfig {

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

然而,这将提前类型预测的可见性限制在指定的接口类型(TransferService)。然后,只有在受影响的 singleton Bean被实例化后,容器才知道完整的类型(TransferServiceImpl)。非lazy单体Bean根据它们的声明顺序被实例化,所以你可能会看到不同的类型匹配结果,这取决于另一个组件何时试图通过非声明的类型进行匹配(比如 @Autowired TransferServiceImpl,它只在 transferService Bean被实例化后才会解析)。

如果你一直通过声明的服务接口来引用你的类型,你的 @Bean 返回类型可以安全地加入这个设计决定。然而,对于实现了多个接口的组件或可能被其实现类型引用的组件来说,声明最具体的返回类型是比较安全的(至少要与引用你的Bean的注入点要求的具体类型一样)。
Bean 依赖

一个 @Bean 注解的方法可以有任意数量的参数,描述构建该Bean所需的依赖关系。例如,如果我们的 TransferService 需要一个 AccountRepository,我们可以用一个方法参数将这种依赖关系具体化,如下例所示。

Java

Kotlin

@Configuration
public class AppConfig {

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

解析机制与基于构造函数的依赖注入基本相同。更多细节见 相关章节

接收生命周期的回调

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

常规的Spring 生命周期 回调也被完全支持。如果一个bean实现了 InitializingBeanDisposableBeanLifecycle,它们各自的方法就会被容器调用。

标准的 *Aware 接口集(如 BeanFactoryAwareBeanNameAwareMessageSourceAwareApplicationContextAware 等)也被完全支持。

@Bean 注解支持指定任意的初始化和销毁回调方法,就像Spring XML在 bean 元素上的 init-methoddestroy-method 属性一样,如下例所示。

Java

Kotlin

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配置定义的具有 public 的 closeshutdown 方法的Bean会自动被列入销毁回调。如果你有一个 public 的 closeshutdown 方法,并且你不希望它在容器关闭时被调用,你可以在你的Bean定义中添加 @Bean(destroyMethod = "") 来禁用默认 (inferred) 模式。你可能想对你用JNDI获取的资源默认这样做,因为它的生命周期是在应用程序之外管理的。特别是,要确保总是对 DataSource 这样做,因为它在Jakarta EE应用服务器上已知是有问题的。下面的例子显示了如何阻止一个 DataSource 的自动销毁回调。JavaKotlin@Bean(destroyMethod = "") public DataSource dataSource() throws NamingException { return (DataSource) jndiTemplate.lookup("MyDS"); }另外,对于 @Bean 方法,你通常使用程序化的JNDI查找,要么使用Spring的 JndiTemplateJndiLocatorDelegate helper,要么直接使用JNDI InitialContext,但不使用 JndiObjectFactoryBean 变体(这将迫使你将返回类型声明为 FactoryBean 类型,而不是实际的目标类型,使得它难以用于其他 @Bean 方法中的交叉引用调用,这些方法打算引用这里提供的资源)。

就前文例子中的 BeanOne 而言,在构造过程中直接调用 init() 方法同样有效,正如下面的例子所示。

Java

Kotlin

@Configuration
public class AppConfig {

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

    // ...
}
当你直接在Java中工作时,你可以对你的对象做任何你喜欢的事情,而不一定需要依赖容器的生命周期。
指定 Bean 的 Scope

Spring包括 @Scope 注解,这样你就可以指定Bean的 scope。

使用 @Scope 注解

你可以指定你用 @Bean 注解定义的 Bean 应该有一个特定的 scope。你可以使用 Bean Scopes 部分中指定的任何一个标准 scope。

默认的scope是 singleton,但你可以用 @Scope 注解来覆盖它,如下例所示。

Java

Kotlin

@Configuration
public class MyConfiguration {

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

Spring提供了一种通过 scope 代理 来处理scope 依赖的便捷方式。使用XML配置时,创建这种代理的最简单方法是 ` 元素。在Java中用@Scope注解配置你的Bean,提供了与proxyMode属性相当的支持。默认是ScopedProxyMode.DEFAULT,这通常表示不应该创建任何 scope 代理,除非在组件扫描指令级别配置了不同的默认值。你可以指定ScopedProxyMode.TARGET_CLASSScopedProxyMode.INTERFACESScopedProxyMode.NO`。

如果你把XML参考文档中的 scope 代理例子(见 scope 代理)移植到我们使用Java的 @Bean 上,它类似于以下内容。

Java

Kotlin

// 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 属性来重写,正如下面的例子所示。

Java

Kotlin

@Configuration
public class AppConfig {

    @Bean("myThing")
    public Thing thing() {
        return new Thing();
    }
}
Bean 别名

正如在 Bean 命名 中所讨论的,有时最好给一个Bean起多个名字,也就是所谓的Bean别名。@Bean 注解的 name 属性接受一个 String 数组来实现这一目的。下面的例子展示了如何为一个Bean设置若干别名。

Java

Kotlin

@Configuration
public class AppConfig {

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

有时,为 Bean 提供更详细的文本描述是有帮助的。当Bean被暴露(也许是通过JMX)用于监控目的时,这可能特别有用。

为了给 @Bean 添加描述,你可以使用 @Description 注解,如下图所示。

Java

Kotlin

@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 注解的方法声明bean。对 @Configuration 类上的 @Bean 方法的调用也可以用来定义bean间的依赖关系。参见 “基本概念:@Bean 和 @Configuration” 的一般介绍。

注入bean间的依赖

当Bean相互之间有依赖关系时,表达这种依赖关系就像让一个Bean方法调用另一个一样简单,正如下面的例子所示。

Java

Kotlin

@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间的依赖关系。
查询方法注入

如前所述,查找方法注入 是一个高级功能,你应该很少使用。在 singleton scope 的Bean对 prototype scope 的Bean有依赖性的情况下,它是很有用的。为这种类型的配置使用Java提供了实现这种模式的自然手段。下面的例子展示了如何使用查找方法注入。

Java

Kotlin

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() 方法被重载,这样它就可以查找到一个新的(prototype) command 对象。下面的例子显示了如何做到这一点。

Java

Kotlin

@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 注解的方法被调用了两次。

Java

Kotlin

@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 scope。这就是神奇之处。所有的 @Configuration 类都是在启动时用 CGLIB 子类化的。在子类中,子方法首先检查容器中是否有任何缓存(scope)的Bean,然后再调用父方法并创建一个新实例。

根据你的Bean的scope,其行为可能是不同的。我们在这里讨论的是singleton(单例)。
没有必要将CGLIB添加到你的classpath中,因为CGLIB类被重新打包到 org.springframework.cglib 包中,并直接包含在 spring-core JAR中。
由于CGLIB在启动时动态地添加功能,所以有一些限制。特别是,配置类不能是 final 的。然而,配置类的任何构造函数都是允许的,包括使用 @Autowired 或单一的非默认构造函数声明进行默认注入。如果你想避免任何CGLIB施加的限制,可以考虑在非 @Configuration 类中声明你的 @Bean 方法(例如,在普通的 @Component 类中声明),或者用 @Configuration(proxyBeanMethods = false) 来注释你的配置类。这样,@Bean 方法之间的跨方法调用就不会被拦截,所以你必须完全依赖构造函数或方法级别的依赖注入。
1.12.5. 构建基于Java的配置

Spring基于Java的配置功能让你可以编写注解,这可以降低配置的复杂性。

使用 @Import 注解

就像 ` 元素在Spring XML文件中被用来帮助模块化配置一样,@Import注解允许从另一个配置类中加载@Bean` 定义,如下例所示。

Java

Kotlin

@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,正如下面的例子所示。

Java

Kotlin

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

从Spring框架4.2开始,@Import 也支持对常规组件类的引用,类似于 AnnotationConfigApplicationContext.register 方法。如果你想避免组件扫描,通过使用一些配置类作为入口点来明确定义你的所有组件,这就特别有用。
在导入的 @Bean 定义上注入依赖

前面的例子是可行的,但也是简单的。在大多数实际情况下,Bean在配置类之间有相互依赖的关系。当使用XML时,这不是一个问题,因为不涉及编译器,你可以声明 ref="someBean" 并相信Spring会在容器初始化过程中解决这个问题。当使用 @Configuration 类时,Java编译器会对配置模型进行约束,即对其他Bean的引用必须是有效的Java语法。

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

Java

Kotlin

@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 类在上下文的初始化过程中会被很早地处理,强行以这种方式注入依赖关系可能会导致意外的早期初始化。只要有可能,就采用基于参数的注入方式,如前面的例子。另外,对于通过 @Bean 定义的 BeanPostProcessorBeanFactoryPostProcessor 要特别小心。那些通常应该被声明为 static @Bean 方法,而不是触发其包含的配置类的实例化。否则,@Autowired@Value 可能对配置类本身不起作用,因为它有可能在 AutowiredAnnotationBeanPostProcessor 之前作为一个bean实例创建它。

下面的例子显示了一个Bean是如何被自动注入到另一个Bean的。

Java

Kotlin

@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");
}
从Spring Framework 4.3开始,@Configuration 类中的构造函数注入才被支持。还要注意的是,如果目标Bean只定义了一个构造函数,就不需要指定 @Autowired

便于浏览的全限定名称的 import bean

在前面的场景中,使用 @Autowired 效果很好,并提供了所需的模块化,但确定自动注入的Bean定义到底在哪里声明,还是有些模糊。例如,作为一个查看 ServiceConfig 的开发者,你怎么知道 @Autowired AccountRepository Bean到底是在哪里声明的?它在代码中并不明确,而这可能就很好。请记住, Spring Tools for Eclipse 提供的工具可以呈现图形,显示一切是如何注入的,这可能就是你所需要的。另外,你的Java IDE可以很容易地找到 AccountRepository 类型的所有声明和使用,并快速显示返回该类型的 @Bean 方法的位置。

在不能接受这种模糊性的情况下,你希望在你的IDE中从一个 @Configuration 类直接导航到另一个,可以考虑自动注入配置类本身。下面的例子展示了如何做到这一点。

Java

Kotlin

@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 类,这种紧密耦合可以得到一定程度的缓解。考虑一下下面的例子。

Java

Kotlin

@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的启动创建顺序,可以考虑将其中一些Bean声明为 @Lazy(在第一次访问时创建,而不是在启动时创建)或者声明为 @DependsOn 某些其他Bean(确保特定的其他Bean在当前Bean之前创建,超出后者的直接依赖关系)。
有条件地包括 @Configuration 类或 @Bean 方法

根据一些任意的系统状态,有条件地启用或禁用一个完整的 @Configuration 类,甚至是单个的 @Bean 方法,往往是很有用的。一个常见的例子是使用 @Profile 注解来激活Bean,只有在Spring Environment 中启用了特定的配置文件时(详见 Bean定义配置)。

@Profile 注解实际上是通过使用一个更灵活的注解来实现的,这个注解叫做 @Conditional@Conditional 注解指出了特定的 org.springframework.context.annotation.Condition 实现,在注册 @Bean 之前应该参考这些实现。

Condition 接口的实现提供了一个 matches(…) 方法,它返回 truefalse。例如,下面的列表显示了用于 @Profile 的实际 Condition 实现。

Java

Kotlin

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
    // 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;
}

更多细节请参见 @Conditional javadoc。

将Java和XML配置相结合

Spring的 @Configuration 类支持的目的并不是要100%完全取代Spring XML。一些设施,如Spring XML命名空间,仍然是配置容器的理想方式。在XML方便或必要的情况下,你有一个选择:要么通过使用例如 ClassPathXmlApplicationContext 以 "以XML为中心" 的方式实例化容器,要么通过使用 AnnotationConfigApplicationContext@ImportResource 注解来根据需要导入XML,以 "以Java为中心" 的方式实例化它。

以XML为中心使用 @Configuration

从XML引导Spring容器并以临时的方式包含 @Configuration 类可能是更好的做法。例如,在一个使用Spring XML的大型现有代码库中,根据需要创建 @Configuration 类并从现有的XML文件中包含它们是比较容易的。在本节后面,我们将介绍在这种 "以XML为中心" 的情况下使用 @Configuration 类的选项。

@Configuration 类声明为普通的 Spring `` 元素

记住,@Configuration 类最终是容器中的Bean定义。在这个系列的例子中,我们创建了一个名为 AppConfig@Configuration 类,并将其作为 定义包含在 `system-test-config.xml` 中。因为 被打开了,所以容器会识别 @Configuration 注解并正确处理 AppConfig 中声明的 @Bean 方法。

下面的例子显示了Java中的一个普通配置类。

Java

Kotlin

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

Java

Kotlin

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 ` 并没有声明一个id元素。虽然这样做是可以接受的,但考虑到没有其他Bean会引用它,而且它也不可能被明确地从容器中获取名字,所以这样做是不必要的。同样地,DataSourceBean只按类型自动注入,所以严格来说不需要明确的beanid`。

使用 ` 来拾取@Configuration` 类

因为 @Configuration 是用 @Component 元注解的,所以 @Configuration 注解的类自动成为组件扫描的候选对象。使用与前面例子中描述的相同场景,我们可以重新定义 system-test-config.xml 以利用组件扫描。注意,在这种情况下,我们不需要明确声明 ,因为 可以实现同样的功能。

下面的例子显示了修改后的 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>
@Configuration 以类为中心使用XML与 @ImportResource

@Configuration 类是配置容器的主要机制的应用中,仍然可能需要至少使用一些 XML。在这些情况下,你可以使用 @ImportResource 并只定义你需要的 XML。这样做实现了 "以 Java 为中心" 的配置容器的方法,并使 XML 保持在最低限度。下面的例子(包括一个配置类、一个定义 bean 的 XML 文件、一个 properties 文件和 main 类)显示了如何使用 @ImportResource 注解来实现 "以 Java 为中心" 的配置,并根据需要使用 XML。

Java

Kotlin

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

Java

Kotlin

public static void main(String[] args) {
    ApplicationContext ctx = new AnnotationConfigApplicationContext(AppConfig.class);
    TransferService transferService = ctx.getBean(TransferService.class);
    // ...
}

1.13. Environment 抽象

Environment 接口是一个集成在容器中的抽象,它对 application environment 的两个关键方面进行建模:配置文件(profiles)属性(properties)

profile是一个命名的、逻辑上的bean定义组,只有在给定的profile处于活动状态时才会在容器中注册。无论是用 XML 定义的还是用注解定义的,Bean 都可以被分配给一个profile。Environment 对象在profile方面的作用是确定哪些profile(如果有的话)是当前活动(active)的,以及哪些profile(如果有的话)应该是默认活动的。

属性(Properties)在几乎所有的应用程序中都扮演着重要的角色,它可能来自各种来源:properties 文件、JVM系统属性、系统环境变量、JNDI、Servlet上下文参数、特设的 Properties 对象、Map 对象等等。与属性有关的 Environment 对象的作用是为用户提供一个方便的服务接口,用于配置属性源并从它们那里解析属性。

1.13.1. Bean定义配置

Bean定义配置(Bean definition profiles) 在核心容器中提供了一种机制,允许在不同的环境中注册不同的bean。“环境”这个词对不同的用户来说意味着不同的东西,而这个功能可以帮助许多用例,包括。

  • 在开发中针对内存中的数据源工作,而在QA或生产中从JNDI查找相同的数据源。

  • 仅在将应用程序部署到 performance 环境中时才注册监控基础设施。

  • 为 customer A 与 customer B 的部署注册定制的bean实现。

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

Java

Kotlin

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

现在考虑如何将这个应用程序部署到QA或production环境中,假设该应用程序的数据源是在生产应用服务器的JNDI目录下注册的。我们的 dataSource bean现在看起来如下。

Java

Kotlin

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

问题是如何根据当前环境在使用这两种变化之间切换。随着时间的推移,Spring用户已经设计出了许多方法来完成这一工作,通常是依靠系统环境变量和包含 ${placeholder} 标记的XML `` 语句的组合,根据环境变量的值解析到正确的配置文件路径。Bean定义配置是一个核心的容器功能,为这个问题提供了一个解决方案。

如果我们把前面的例子中显示的环境特定的Bean定义的用例进行概括,我们最终需要在某些情况下注册某些Bean定义,但在其他情况下不需要。你可以说,你想在情况A中注册某种类型的bean定义,而在情况B中注册另一种类型。

使用 @Profile

@Profile 注解让你表明当一个或多个指定的配置文件处于活动状态时,一个组件就有资格注册。使用我们前面的例子,我们可以重写 dataSource 配置如下。

Java

Kotlin

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

Java

Kotlin

@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(destroyMethod = "") 禁用默认的销毁方法推理。
如前所述,对于 @Bean 方法,你通常会选择使用程序化的JNDI查找,通过使用Spring的 JndiTemplate/JndiLocatorDelegat helper 或前面显示的直接使用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 作为 元注解,以创建一个自定义的组成注解。下面的例子定义了一个自定义的 @Production 注解,你可以把它作为 @Profile("production") 的直接替换。

Java

Kotlin

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Profile("production")
public @interface Production {
}
如果一个 @Configuration 类被标记为 @Profile,所有与该类相关的 @Bean 方法和 @Import 注解都会被绕过,除非一个或多个指定的 profiles 处于激活状态。如果一个 @Component@Configuration 类被标记为 @Profile({"p1", "p2"}),该类不会被注册或处理,除非 profiles "p1" 或 "p2" 已经被激活。如果一个给定的profiles前缀为NOT操作符(!),那么只有在该profiles没有激活的情况下,才会注册被注解的元素。例如,给定 @Profile({"p1", "!p2"}),如果profile 'p1' 被激活或 profile 'p2' 未被激活,注册将发生。

@Profile 也可以在方法层面上声明,以便只包括一个配置类的一个特定Bean(例如,对于一个特定Bean的备选变体),正如下面的例子所示。

Java

Kotlin

@Configuration
public class AppConfig {

    @Bean("dataSource")
    @Profile("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") 
    public DataSource jndiDataSource() throws Exception {
        Context ctx = new InitialContext();
        return (DataSource) ctx.lookup("java:comp/env/jdbc/datasource");
    }
}
StandaloneDataSource 方法只在 development profile 中可用。
jndiDataSource 方法只在 production profile 中可用。
对于 @Bean 方法的 @Profile,一个特殊的情况可能适用。在同一Java方法名的重载 @Bean 方法的情况下(类似于构造函数重载),@Profile 条件需要在所有重载的方法上一致声明。如果条件不一致,那么在重载的方法中,只有第一个声明的条件才是重要的。因此,@Profile 不能被用来选择具有特定参数签名的重载方法而不是另一个。同一个Bean的所有工厂方法之间的解析遵循Spring在创建时的构造函数解析算法。如果你想定义具有不同概况条件的备选Bean,请使用不同的Java方法名,通过使用 @Bean name 属性指向同一个Bean名称,如前面的例子所示。如果参数签名都是一样的(例如,所有的变体都有无参数的工厂方法),这是首先在一个有效的Java类中表示这种安排的唯一方法(因为一个特定名称和参数签名的方法只能有一个)。
XML Bean 定义配置

XML的对应部分是 ` 元素的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 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。也可以通过嵌套 profile 来应用逻辑 “and”,如下面的例子所示。在前面的例子中,如果 productionus-east profiles都处于活动状态,那么 dataSource Bean就会被暴露。
激活一个 Profile

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

激活一个 profile 可以通过几种方式进行,但最直接的是以编程方式对环境API进行激活,该API可以通过 ApplicationContext 获得。下面的例子显示了如何做到这一点。

Java

Kotlin

AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext();
ctx.getEnvironment().setActiveProfiles("development");
ctx.register(SomeConfig.class, StandaloneDataConfig.class, JndiDataConfig.class);
ctx.refresh();

此外,你还可以通过 spring.profiles.active 属性声明性地激活profiles,它可以通过系统环境变量、JVM系统属性、web.xml 中的servlet上下文参数,甚至作为JNDI中的一个条目来指定(参见 PropertySource 抽象)。在集成测试中,可以通过使用 spring-test 模块中的 @ActiveProfiles 注解来声明活动profiles(见 environment profiles 的 context 配置)。

请注意,profiles 不是一个 "非此即彼" 的命题。你可以同时激活多个profiles。在程序上,你可以向 setActiveProfiles() 方法提供多个profiles名称,该方法接受 String… 可变参数。下面的例子激活了多个profiles。

Java

Kotlin

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

在声明上,spring.profiles.active 可以接受一个用逗号分隔的 profile 名称列表,正如下面的例子所示。

-Dspring.profiles.active="profile1,profile2"
默认 Profile

默认 profile 代表默认启用的 profile。考虑一下下面的例子。

Java

Kotlin

@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就不应用。

你可以通过在环境中使用 setDefaultProfiles() 来改变默认配置文件的名称,或者通过声明性地使用 spring.profiles.default 属性。

1.13.2. PropertySource 抽象

Spring的 Environment 抽象提供了对可配置的属性源层次结构的搜索操作。考虑一下下面的列表。

Java

Kotlin

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

这些默认的属性源存在于 StandardEnvironment 中,用于独立的应用程序中。 StandardServletEnvironment 被填充了额外的默认属性源,包括servlet config、servlet context参数,以及 JndiPropertySource(如果JNDI可用)。

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

执行的搜索是分层次的。默认情况下,系统属性(system properties)比环境变量有优先权。因此,如果在调用 env.getProperty("my-property") 时,my-property 属性恰好在两个地方都被设置了,那么系统属性值 "胜出" 并被返回。请注意,属性值不会被合并,而是被前面的条目完全覆盖。对于一个普通的 StandardServletEnvironment 来说,完整的层次结构如下,最高优先级的条目在顶部。ServletConfig 参数(如果适用 - 例如,在 DispatcherServlet 上下文的情况下)。ServletContext 参数(web.xml的context-param条目).JNDI环境变量(java:comp/env/ 条目)。JVM系统属性(-D 命令行参数)。JVM系统环境(操作系统环境变量).

最重要的是,整个机制是可配置的。也许你有一个自定义的属性源,你想集成到这个搜索中。要做到这一点,实现并实例化你自己的 PropertySource,并将其添加到当前环境的 PropertySources 集合中。下面的例子展示了如何做到这一点。

Java

Kotlin

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

在前面的代码中,MyPropertySource 在搜索中被加入了最高优先级。如果它包含 my-property 属性,该属性将被检测并返回,而不是任何其他 PropertySource 中的任何 my-property 属性。 MutablePropertySources API暴露了许多方法,允许精确地操作属性源(property sources)的集合。

1.13.3. 使用 @PropertySource

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

给定一个包含键值对 testbean.name=myTestBean 的名为 app.properties 的文件,下面的 @Configuration 类以这样一种方式使用 @PropertySource,即调用 testBean.getName() 返回 myTestBean

Java

Kotlin

@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 资源位置的 ${…} 占位符都会根据已经针对环境(environment)注册的属性源集合进行解析,如下例所示。

Java

Kotlin

@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. 声明中的占位符解析

历史上,元素中占位符的值只能通过JVM系统属性或环境变量来解析。现在的情况不再是这样了。因为 Environment 抽象被集成到整个容器中,所以很容易通过它来解决占位符的问题。这意味着你可以以任何你喜欢的方式配置解析过程。你可以改变通过系统属性和环境变量搜索的优先级,或者完全删除它们。你也可以酌情将你自己的属性源添加到组合中。

具体来说,无论 customer 属性在哪里定义,只要它在 Environment 中是可用的,下面的语句就能发挥作用。

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

1.14. 注册 LoadTimeWeaver

LoadTimeWeaver 被Spring用来在类被加载到Java虚拟机(JVM)时进行动态转换。

要启用 load-time 织入,你可以把 @EnableLoadTimeWeaving 添加到你的一个 @Configuration 类中,如下例所示。

Java

Kotlin

@Configuration
@EnableLoadTimeWeaving
public class AppConfig {
}

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

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

一旦为 ApplicationContext 配置,该 ApplicationContext 中的任何Bean都可以实现 LoadTimeWeaverAware,从而接收对 load-time 织入实例的引用。这在与 Spring的JPA支持 相结合时特别有用,因为JPA类的转换可能需要 load-time 织入。请参考 LocalContainerEntityManagerFactoryBean javadoc以了解更多细节。关于AspectJ load-time 织入的更多信息,请参见 在Spring框架中用AspectJ进行加载时织入(Load-time Weaving)

1.15. ApplicationContext 的附加功能

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

为了以更加面向框架的风格增强 BeanFactory 的功能,context包还提供了以下功能。

  • 通过 MessageSource 接口,以i18n风格访问消息。

  • 通过 ResourceLoader 接口访问资源,如URL和文件。

  • 事件发布,即通过使用 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 检索消息的基本方法。当没有找到指定地区(locale)的消息时,将使用默认的消息。任何传入的参数都成为替换值,使用标准库提供的 MessageFormat 功能。

  • String getMessage(String code, Object[] args, Locale loc): 基本上与前一个方法相同,但有一点不同。不能指定默认消息。如果找不到消息,会抛出一个 NoSuchMessageException

  • String getMessage(MessageSourceResolvable resolvable, Locale locale): 在前面的方法中使用的所有属性(properties)也被包裹在一个名为 MessageSourceResolvable 的类中,你可以使用这个方法。

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

Spring提供了三种 MessageSource 实现:ResourceBundleMessageSourceReloadableResourceBundleMessageSourceStaticMessageSource。它们都实现了 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>

这个例子假设你在classpath中定义了三个资源包,分别是 formatexceptionswindows。任何解析消息的请求都以 JDK 标准的方式处理,即通过 ResourceBundle 对象解析消息。就本例而言,假设上述两个资源包文件的内容如下。

# in format.properties
message=Alligators rock!
# in exceptions.properties
argument.required=The {0} argument is required.

下一个例子显示了一个运行 MessageSource 功能的程序。请记住,所有 ApplicationContext 的实现也是 MessageSource 的实现,所以可以强制转换为 MessageSource 接口。

Java

Kotlin

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 的文件中,它存在于你的classpath的根部。messageSource Bean定义通过它的 basenames 属性引用了一些资源包。在列表中传递给 basenames 属性的三个文件作为文件存在于你的 classpath 根部,分别被称为 format.propertiesexceptions.propertieswindows.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>

Java

Kotlin

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 的文件。

通常情况下,地区的解析(locale resolution)是由应用程序的周围环境管理的。在下面的例子中,(英国)信息所依据的地方语言是手动指定的。

# in exceptions_en_GB.properties
argument.required=Ebagum lad, the ''{0}'' argument is required, I say, required.

Java

Kotlin

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被创建和配置时都会被注入 ApplicationContextMessageSource

因为Spring的 MessageSource 是基于Java的 ResourceBundle,它不会合并具有相同基名的bundle,而是只使用找到的第一个bundle。随后的具有相同基名的消息包会被忽略。
作为 ResourceBundleMessageSource 的替代品,Spring提供了一个 ReloadableResourceBundleMessageSource 类。这个变体支持相同的bundle文件格式,但比基于JDK的标准 ResourceBundleMessageSource 实现更灵活。特别是,它允许从任何Spring资源位置(不仅仅是classpath)读取文件,并支持bundle属性文件的热重载(同时在两者之间有效地缓存它们)。详情请参见 ReloadableResourceBundleMessageSource javadoc。
1.15.2. 标准和自定义事件

ApplicationContext 中的事件处理是通过 ApplicationEvent 类和 ApplicationListener 接口提供的。如果一个实现 ApplicationListener 接口的Bean被部署到上下文中,每当 ApplicationEvent 被发布到 ApplicationContext 时,该Bean就会被通知。本质上,这是标准的观察者设计模式。

从Spring 4.2开始,事件基础架构得到了极大的改进,它提供了基于 注解的模型 以及发布任何任意事件的能力(也就是不一定从 ApplicationEvent 继承出来的对象)。当这样的对象被发布时,我们会为你把它包装成一个事件。

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

事件说明
ContextRefreshedEventApplicationContext 被初始化或刷新时发布(例如,通过使用 ConfigurableApplicationContext 接口上的 refresh() 方法)。这里,"初始化" 意味着所有的Bean都被加载,后处理器Bean被检测和激活,单例被预先实例化,并且 ApplicationContext 对象可以使用。只要上下文没有被关闭,就可以多次触发刷新,前提是所选的 ApplicationContext 实际上支持这种 "热" 刷新。例如, XmlWebApplicationContext 支持热刷新,但 GenericApplicationContext 不支持。
ContextStartedEventApplicationContext 通过使用 ConfigurableApplicationContext 接口上的 start() 方法启动时发布。在这里,"开始" 意味着所有的 Lifecycle Bean都收到一个显式的启动信号。通常,这个信号被用来在显式停止后重新启动Bean,但它也可能被用来启动那些没有被配置为自动启动的组件(例如,在初始化时还没有启动的组件)。
ContextStoppedEventApplicationContext 通过使用 ConfigurableApplicationContext 接口上的 stop() 方法而停止时发布。在这里,"停止" 意味着所有的 Lifecycle Bean收到一个明确的停止信号。停止的上下文可以通过调用 start() 重新启动。
ContextClosedEventApplicationContext 通过使用 ConfigurableApplicationContext 接口上的 close() 方法或通过 JVM shutdown hook 被关闭时发布。这里,"关闭" 意味着所有的单例Bean将被销毁。一旦上下文被关闭,它的生命就结束了,不能被刷新或重新启动。
RequestHandledEvent一个针对Web的事件,告诉所有Bean一个HTTP请求已经被处理。该事件在请求完成后被发布。这个事件只适用于使用Spring的 DispatcherServlet 的Web应用程序。
ServletRequestHandledEventRequestHandledEvent 的一个子类,增加了 Servlet 特定的上下文信息。

你也可以创建和发布你自己的自定义事件。下面的例子展示了一个简单的类,它扩展了Spring的 ApplicationEvent 基类。

Java

Kotlin

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来实现的。下面的例子展示了这样一个类。

Java

Kotlin

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 接口与 application context 进行交互。

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

Java

Kotlin

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() 方法可以保持类型安全,避免了任何强制向下转型的需要。你可以根据你的需要注册任意多的事件监听器,但是请注意,在默认情况下,事件监听器是同步接收事件的。这意味着 publishEvent() 方法会阻塞,直到所有的监听器都完成对事件的处理。这种同步和单线程的方法的一个好处是,当一个监听器收到一个事件时,如果有一个事务上下文的话,它就在发布者的事务上下文中操作。如果需要使用另一种事件发布策略,请参阅 Spring 的 ApplicationEventMulticaster 接口和 SimpleApplicationEventMulticaster 实现的 javadoc 配置选项。

下面的例子显示了用于注册和配置上述每个类的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 Integration 项目为构建轻量级、 面向模式、事件驱动的架构提供了完整的支持,这些架构建立在众所周知的Spring编程模型之上。
基于注解的事件监听器

你可以通过使用 @EventListener 注解在管理型Bean的任何方法上注册一个事件监听器。 BlockedListNotifier 可以被重写如下。

Java

Kotlin

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

方法签名再次声明了它所监听的事件类型,但是,这一次,有了一个灵活的名字,并且没有实现一个特定的监听接口。事件类型也可以通过泛型来缩小,只要实际的事件类型在其实现层次中解析你的泛型参数。

如果你的方法应该监听几个事件,或者你想在没有参数的情况下定义它,事件类型也可以在注解本身中指定。下面的例子展示了如何做到这一点。

Java

Kotlin

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

也可以通过使用定义 SpEL表达式 的注解的 condition 属性来添加额外的运行时过滤,该表达式应该匹配以实际调用特定事件的方法。

下面的例子显示了我们的 notifier 如何被改写成只有在事件的 content 属性等于 my-event 时才会被调用。

Java

Kotlin

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

每个 SpEL 表达式都针对一个专门的上下文进行评估。下表列出了提供给上下文的项目,以便你可以使用它们进行条件性事件处理。

名称位置说明示例
事件root 对象实际的 ApplicationEvent#root.event or event
参数数组root 对象用于调用方法的参数(作为一个对象数组)。#root.argsargs; args[0] 用于访问第一个参数,等等。
参数名称evaluation context任何一个方法参数的名称。如果由于某种原因,这些名称无法使用(例如,因为在编译的字节码中没有debug信息),单个参数也可以使用 #a<#arg> 语法,其中 <#arg> 代表参数索引(从0开始)。#blEvent#a0(你也可以使用 #p0#p<#arg> 参数符号作为别名)。

请注意,#root.event 让你可以访问底层事件,即使你的方法签名实际上指的是一个被发布的任意对象。

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

Java

Kotlin

@EventListener
public ListUpdateEvent handleBlockedListEvent(BlockedListEvent event) {
    // notify appropriate parties via notificationAddress and
    // then publish a ListUpdateEvent...
}
异步监听器 不支持此功能。

handleBlockedListEvent() 方法为其处理的每个 BlockedListEvent 发布一个新的 ListUpdateEvent。如果你需要发布几个事件,你可以返回一个 Collection 或者一个事件数组来代替。

异步监听器

如果你想让一个特定的监听器异步处理事件,你可以重新使用 常规的 @Async 支持。下面的例子展示了如何做到这一点。

Java

Kotlin

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

使用异步事件时要注意以下限制。

  • 如果一个异步事件监听器抛出一个异常,它不会被传播给调用者。参见 AsyncUncaughtExceptionHandler 获取更多细节。

  • 异步事件监听器方法不能通过返回一个值来发布后续的事件。如果你需要发布另一个事件作为处理的结果,请注入一个 ApplicationEventPublisher 来手动发布该事件。

监听顺序

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

Java

Kotlin

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

你也可以使用泛型来进一步定义你的事件的结构。考虑使用 EntityCreatedEvent,其中 T 是被创建的实际实体的类型。例如,你可以创建下面的监听器定义,只接收一个 PersonEntityCreatedEvent

Java

Kotlin

@EventListener
public void onPersonCreated(EntityCreatedEvent<Person> event) {
    // ...
}

由于泛型擦除,只有当被触发的事件解析了事件监听器过滤的泛型参数时,这才起作用(也就是说,像 class PersonCreatedEvent extends EntityCreatedEvent { … })。

在某些情况下,如果所有的事件都遵循相同的结构,这可能会变得相当乏味(前面的例子中的事件应该就是这样)。在这种情况下,你可以实现 ResolvableTypeProvider,以引导框架超越运行时环境提供的内容。下面的事件展示了如何做到这一点。

Java

Kotlin

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 抽象,如 资源(Resources) 所述。

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

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

提供给 ApplicationContext 构造函数的一个或多个位置路径实际上是资源字符串,以简单的形式,根据具体的上下文实现被适当地处理。例如,ClassPathXmlApplicationContext 将一个简单的位置路径视为 classpath 位置。你也可以使用带有特殊前缀的位置路径(资源字符串)来强制从 classpath 或URL加载定义,而不考虑实际的上下文类型。

1.15.4. 应用程序启动跟踪

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

用具体的指标来跟踪应用程序的启动步骤,可以帮助了解在启动阶段花费的时间,但它也可以作为一种方式来更好地了解整个上下文生命周期。

AbstractApplicationContext(及其子类)被一个 ApplicationStartup 工具化,它收集关于各种启动阶段的 StartupStep 数据。

  • application context 生命周期(基础包扫描、配置类管理)。

  • bean生命周期(实例化、智能初始化、后处理)。

  • application 事件处理。

下面是一个在 AnnotationConfigApplicationContext 中的工具化的例子。

Java

Kotlin

// create a startup step and start recording
StartupStep scanPackages = this.getApplicationStartup().start("spring.context.base-packages.scan");
// add tagging information to the current step
scanPackages.tag("packages", () -> Arrays.toString(basePackages));
// perform the actual phase we're instrumenting
this.scanner.scan(basePackages);
// end the current step
scanPackages.end();

application context 已经有了多个步骤的工具。一旦被记录下来,这些启动步骤就可以用特定的工具进行收集、显示和分析。关于现有启动步骤的完整列表,你可以查看 专门的附录部分

默认的 ApplicationStartup 实现是一个无操作的变体,以最小的开销。这意味着在应用启动过程中,默认不会收集任何指标。Spring Framework 提供了一个实现,用于跟踪 Java Flight Recorder (飞行记录)的启动步骤:FlightRecorderApplicationStartup。要使用这个变体,你必须在 ApplicationContext 创建后立即将它的一个实例配置到 ApplicationContext 中。

如果开发者提供自己的 AbstractApplicationContext 子类,或者希望收集更精确的数据,也可以使用 ApplicationStartup 基础设施。

ApplicationStartup 只用于应用程序的启动和核心容器;它绝不是 Java profilers 或 Micrometer 等指标库的替代。

为了开始收集自定义的 StartupStep,组件可以直接从 application context 中获得 ApplicationStartup 实例,使他们的组件实现 ApplicationStartupAware,或者在任何注入点上请求 ApplicationStartup 类型。

开发人员在创建自定义启动步骤时不应使用 "spring.*" 命名空间。这个命名空间是为Spring内部使用而保留的,并且会有变化。
1.15.5. 为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 作为默认值。如果参数存在,监听器会使用预定义的分隔符(逗号、分号和空格)来分离 String,并将这些值作为搜索 application context 的位置。Ant风格的路径模式也被支持。例如,/WEB-INF/*Context.xml(搜索所有名称以 Context.xml 结尾且位于 WEB-INF 目录中的文件)和 /WEB-INF/**/*Context.xml(搜索 WEB-INF 任何子目录中的所有此类文件)。

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

可以将Spring ApplicationContext 部署为RAR文件,在Jakarta EE RAR部署单元中封装上下文及其所有需要的Bean类和库JARs。这相当于启动一个独立的 ApplicationContext(只在Jakarta EE环境中托管),能够访问Jakarta EE服务器设施。RAR部署是部署无头WAR文件的一个更自然的选择—实际上,一个没有任何HTTP入口点的WAR文件,只用于在Jakarta EE环境中引导Spring ApplicationContext

RAR部署对于那些不需要HTTP入口,而只由消息端点和预定作业组成的应用环境来说是理想的。这种情况下的Bean可以使用应用服务器资源,如JTA事务管理器和JNDI绑定的JDBC DataSource 实例和JMS ConnectionFactory 实例,也可以向平台的JMX服务器注册—所有这些都是通过Spring的标准事务管理和JNDI以及JMX支持设施实现的。应用组件还可以通过Spring的 TaskExecutor 抽象与应用服务器的JCA WorkManager 互动。

参见 SpringContextResourceAdapter 类的javadoc,了解RAR部署中涉及的配置细节。

对于将Spring ApplicationContext 作为Jakarta EE RAR文件的简单部署:

  1. 将所有应用类打包成一个RAR文件(这是一个标准的JAR文件,有不同的文件扩展名)。

  2. 将所有需要的库JARs添加到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. BeanFactory 还是 ApplicationContext?

本节解释了 BeanFactoryApplicationContext 容器级别之间的差异以及对引导(bootstrap)的影响。

你应该使用 ApplicationContext,除非你有充分的理由不这样做, GenericApplicationContext 及其子类 AnnotationConfigApplicationContext 是自定义引导(bootstrap)的常见实现。这些是Spring核心容器的主要入口,用于所有常见的目的:加载配置文件、触发classpath扫描、以编程方式注册Bean定义和注解类,以及(从5.0开始)注册功能性Bean定义。

因为 ApplicationContext 包括 BeanFactory 的所有功能,所以通常推荐使用它而不是普通的 BeanFactory,除非是需要完全控制 Bean 处理的情况。在 ApplicationContext 中(如 GenericApplicationContext 的实现),通过惯例(即通过 bean 名称或 bean 类型—特别是后处理器)来检测几种 bean,而普通的 DefaultListableBeanFactory 对任何特殊的 bean 都不了解。

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

下表列出了 BeanFactoryApplicationContext 接口和实现所提供的功能。

功能BeanFactoryApplicationContext
Bean的实例化/注入YesYes
集成的生命周期管理NoYes
自动注册 BeanPostProcessorNoYes
自动注册 BeanFactoryPostProcessorNoYes
方便的 MessageSource 访问(用于国际化)NoYes
内置的 ApplicationEvent 发布机制NoYes

要在 DefaultListableBeanFactory 中显式地注册一个bean后处理器,你需要以编程方式调用 addBeanPostProcessor,如下例所示。

Java

Kotlin

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

要将 BeanFactoryPostProcessor 应用于普通的 DefaultListableBeanFactory,你需要调用其 postProcessBeanFactory 方法,如下例所示。

Java

Kotlin

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 注册了所有常见的注解后处理器,并且可以通过配置注解引入额外的处理器,例如 @EnableTransactionManagement。在Spring基于注解的配置模型的抽象层面上,bean后处理器的概念仅仅是一个内部容器的细节。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值