4.12 基于 Java 的容器配置

4.12.1 基本概念:@Configuration 和@Bean

Spring 中新的 Java 配置支持的核心就是@Configuration 注解的类。这些类主要包括 @Bean 注解的方法来为 Spring 的 IoC 容器管理的对象定义实例,配置和初始化逻辑。

使用@Configuration 来注解类表示类可以被 Spring 的 IoC 容器所使用,作为 bean 定义的资源。最简单的@Configuration 类可以是这样的:

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

这和 Spring 的 XML 文件中的<beans/>非常类似,上面的 AppConfig 类和下面的代码是等同的:

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

正如你看到的,@Bean 注解扮演了和<bean/>元素相同的角色。@Bean 注解会在下面 的章节中详细地讨论。首先,我们要来看看使用基于 Java 的配置创建 Spring 容器的各种方 式。

4.12.2 使用 AnnotationConfigApplicationContext 实例化 Spring 容器

下面的章节说明了 Spring 的 AnnotationConfigApplicationContext,在 Spring 3.0 中 是 新加 入 的 。这 个 全 能的 ApplicationContext 实 现 类 可 以 接 受不 仅 仅 是 @Configuration 类作为输入,也可以是普通的@Component 类,还有使用 JSR-330 元数 据注解的类。

当@Configuration 类作为输入时,@Configuration 类本身作为 bean 被注册了, 并且类内所有声明的@Bean 方法也被作为 bean 注册了。

当@Component 和 JSR-330 类 作为输 入时, 它们 被注册 为 bea n,并 且被 假设如 @Autowired 或@Inject 的 DI 元数据在类中需要的地方使用。

4.12.2.1 简单构造

当实例化 ClassPathXmlApplicationContext 时,以大致相同的方式,当实例化 AnnotationConfigApplicationContext 时,@Configuration 类可能被作为输入。这 就允许在 Spring 容器中完全可以不使用 XML:

public static void main(String[] args) { 
    ApplicationContext ctx
        = new AnnotationConfigApplicationContext(AppConfig. class); 
    MyService myService = ctx.getBean(MyService.class); 
    myService.doStuff();
}

正如上面所提到的,AnnotationConfigApplicationContext 不仅仅局限于和 @Configuration 类合作。任意@Component 或 JSR-330 注解的类都可以作为构造方法的输入。比如:

public static void main(String[] args) {
    ApplicationContext ctx = new
        AnnotationConfigApplicationContext(MyServiceImpl.class,
    Dependency1.class, Dependency2.MyService myService =
    ctx.getBean(MyService.class);
    myService.doStuff();
}

上面假设 MyServiceImpl,Dependency1 和 Dependency2 使用了 Spring 依赖注 入注解,比如@Autowired。

4.12.2.2 使用 register(Class<?>...)来编程构建容器

AnnotationConfigApplicationContext 可以使用无参构造方法来实例化,之后使 用 register() 方 法 来 配 置 。 这 个 方 法 当 编 程 构 建 AnnotationConfigApplicationContext 时尤其有用。

public static void main(String[] args) {
    AnnotationConfigApplicationContext ctx = new
        AnnotationConfigApplicationContext();
    ctx.register(AppConfig.class, OtherConfig.class);
    ctx.register(AdditionalConfig.class);
    ctx.refresh();
    MyService myService = ctx.getBean(MyService.class);
    myService.doStuff();
}

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

有经验的 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。

4.12.2.4 支 持 Web 应 用 的 AnnotationConfigWebApplicationContext

WebApplicationContext 是 AnnotationConfigApplicationContext 的变种,

适用于 AnnotationConfigWebApplicationContext。当配置 Spring 的 Servlet 监听器 ContextLoaderListener,Spring MVC 的 DispatcherServlet 等时,这个实现类就 可能被用到了。下面的代码是在 web.xml 中的片段,配置了典型的 Spring MVC 的 Web 应用 程序。注意 contextClass 上下文参数和初始化参数的使用:

<web-app>
    <!-- 配置ContextLoaderListener使用
    AnnotationConfigWebApplicationContext来代替默认的 XmlWebApplicationContext -->
    <context-param>
        <param-name>contextClass</param-name>
        <param-value> org.springframework.web.context.support.AnnotationCon
        figWebApplicationContext
        </param-value>
    </context-param>
    <!-- 配置位置必须包含一个或多个逗号或空格分隔的完全限定
    @Configuration类。完全限定包也可以指定于组件扫描 -->
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>com.acme.AppConfig</param-value>
    </context-param>
    <!-- 普通方式启动根应用上下文的ContextLoaderListener -->
    <listener>
        <listener-class> org.springframework.web.context.ContextLoaderListener
        </listener-class>
    </listener>
    <!-- 普通方式声明 Spring MVC 的 DispatcherServlet -->
    <servlet>
        <servlet-name>dispatcher</servlet-name>
            <servlet-class> org.springframework.web.servlet.DispatcherServlet
        </servlet-class>
        <!-- 配置DispatcherServlet使用
        AnnotationConfigWebApplicationContext来代替默认的 XmlWebApplicationContext -->
        <init-param>
            <param-name>contextClass</param-name>
            <param-value> org.springframework.web.context.support.Annotation
                ConfigWebApplicationContext
            </param-value>
        </init-param>
        <!--配置位置必须包含一个或多个逗号或空格分隔的完全限定
        @Configuration类-->
        <init-param>
            <param-name>contextConfigLocation</param-name>
            <param-value>com.acme.web.MvcConfig</param-value>
        </init-param>
    </servlet>
    <!-- 映射所有的请求到/main/*中来派发servlet -->
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/main/*</url-pattern>
    </servlet-mapping>
</web-app>

4.12.3 构成基于 Java 的配置

4.12.3.1 使用@Import 注解

就像 Spring 的 XML 文件中使用的<import/>元素帮助模块化配置一样,@Import 注 解允许从其它配置类中加载@Bean 的配置:

@Configuration
public class ConfigA {
    public @Bean A a() { return new A(); }
}
@Configuration
@Import(ConfigA.class)
public class ConfigB {
    public @Bean B b() { return new B(); }
}

现在,当实例化上下文时,不需要指定 ConfigA.class 和 ConfigB.class 了,仅 仅 ConfigB 需要被显式提供:

public static void main(String[] args) {
    ApplicationContext ctx = new
        AnnotationConfigApplicationContext(ConfigB.class);
    // 现在bean A 和bean B都会是可用的...
    A a = ctx.getBean(A.class);
    B b = ctx.getBean(B.class);
}

这种方式简化了容器的实例化,仅仅是一个类需要被处理,而不是需要开发人员在构造 时记住很多大量的@Configuration 类。

在引入的@Bean 定义中注入依赖 上面的示例是可行的,但是很简单。在很多实际场景中,bean 会有依赖其它配置的类的依赖。当使用 XML 时,这本身不是什么问题,因为没有调用编译器,而且我们可以仅仅 声明 ref="someBean" 并且相信 Spring 在 容 器 初 始化 时 可以 完 成。 当 然 ,当 使 用 @Configuration 的类时,Java 编译器在配置模型上放置约束,对其它 bean 的引用必须 是符合 Java 语法的。

幸运的是,解决这个问题分层简单。记得@Configuration 类最终是容器中的 bean- 这就是说它们可以像其它 bean 那样利用@Autowired 注入元数据! 我们来看一个更加真实的语义,有几个@Configuration 类,每个都依赖声明在其它类中的 bean:

@Configuration
public class ServiceConfig {
    private @Autowired AccountRepository accountRepository;
    public @Bean TransferService transferService() {
        return new TransferServiceImpl(accountRepository);
    }
}
@Configuration
public class RepositoryConfig {
    private @Autowired DataSource dataSource;
    public @Bean AccountRepository accountRepository() {
        return new JdbcAccountRepository(dataSource);
    }
}
@Configuration
@Import({ServiceConfig.class, RepositoryConfig.class})
public class SystemTestConfig {
    public @Bean DataSource 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");
}

完全限定引入的 bean 便于导航

在上面的场景中,使用@Autowired 工作正常,提供所需的模块化,但是准确地决定 在哪儿声明自动装配的 bean 还是有些含糊。比如,作为开发者来看待 ServiceConfig, 你如何准确知道@Autowired AccountRepository 在哪里声明的?它没有显式地出现 在代码中,这可能很不错。要记得 SpringSource Tool Suite 提供工具可以生成展示所有对象是 如何装配起来的图片-那可能就是你所需要的。而且,你的 Java IDE 可以很容器发现所有的声明,还有使用的 AccountRepository 类型,也会很快地给你展示出@Bean 方法的位置 和返回的类型。

在这种歧义不被接受和你想有直接从 IDE 中从一个@Configuration 类到另一个导航 的情景中,要考虑自动装配配置类的本身:

@Configuration
public class ServiceConfig {
    private @Autowired RepositoryConfig repositoryConfig;
    public @Bean TransferService transferService() {
        // 通过配置类到@Bean方法的导航!
        return new
            TransferServiceImpl(repositoryConfig.accountRepository());
    }
}

在上面的情形中,定义 AccountRepository 是完全明确的。而 ServiceConfig 却紧紧耦合到 RepositoryConfig 中了;这就需要我们来权衡了。这种紧耦合可以使用 基于接口或抽象基类的@Configuration 类来减轻。考虑下面的代码:

@Configuration
public class ServiceConfig {
private @Autowired RepositoryConfig repositoryConfig;
public @Bean TransferService transferService() {
    return new
        TransferServiceImpl(repositoryConfig.accountRepository());
}
}
@Configuration
public interface RepositoryConfig {
    @Bean AccountRepository accountRepository();
}
@Configuration
public class DefaultRepositoryConfig implements RepositoryConfig
{
    public @Bean AccountRepository accountRepository() {
        return new JdbcAccountRepository(...);
    }
}
@Configuration
@Import({ServiceConfig.class, DefaultRepositoryConfig.class})
// 导入具体的配置!
public class SystemTestConfig {
    public @Bean DataSource 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 类和它们的依赖就和普通的基于接口 代码的导航过程没有任何区别了。

4.12.3.2 结合 Java 和 XML 配置

Spring 的@Configuration 类并不是完全 100%地支持 Spring XML 替换的。一些基本特 性,比如 Spring XML 的命名空间会保持在一个理想的方式下去配置容器。在 XML 便于使用 或是必须要使用的情况下,你也有另外一个选择:以“XML 为中心”的方式来实例化容器, 比如,ClassPathXmlApplicationContext,或者以“ Java 为中心”的方式,使用 AnnotationConfigurationApplicationContext 和@ImportResource 注解来引 入需要的 XML。

以“XML 为中心”使用@Configuration 类从一种特定的方式的包含@Configuration 类的 XML 文件启动 Spring 容器是不错的。 比如,在使用了 Spring XML 的大型的代码库中,根据需要并从已有的 XML 文件中创建 @Configuration 类是很简单的。在下面,你会发现在这种“XML 为中心”情形下,使用 @Configuration 类的选择。

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

要记得@Configuration 的类最终仅仅是容器中的 bean。在这个示例中,我们创建了 名为 AppConfig 的@Configuration 类,并且将它包含在 system-test-config.xml 文件中作为<bean/>的定义。因为开启了<context:annotation-config/>配置,容器 会识别@Configuration 注解,以合适的方式处理声明在 AppConfig 中@Bean 方法。

@Configuration
public class AppConfig {
    private @Autowired DataSource dataSource;
    public @Bean AccountRepository accountRepository() {
        return new JdbcAccountRepository(dataSource);
    }
    public @Bean TransferService transferService() {
        return new TransferService(accountRepository());
    }
}

system-test-config.xml
<beans>
    <!-- 开启处理注解功能,比如@Autowired和@Configuration -->
    <context:annotation-config/>
    <context:property-placeholder
        location="classpath:/com/acme/jdbc.properties"/>
    <bean class="com.acme.AppConfig"/>
    <bean class="org.springframework.jdbc.datasource.DriverManagerData Source">
        <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-t est-config.xml");
    TransferService transferService = ctx.getBean(TransferService.class);
    // ...
}

注意

在上面的 system-test-config.xml 文件中,AppConfig 的<bean/>定义没有声明 id 元素。这么做也是可以接受的,就不必让其它 bean 去引用它了。同时也就不可能从 容器中通过明确的名称来获取它了。同样地,DataSource bean,通过类型自动装配,那么明确的 bean id 就不严格要求了。

使用<context:component-scan/>来检索@Configuration 类

因为@Configuration 是使用@Component 来元数据注解的,被@Configuration 注解的类是自动作为组件扫描的候选者的。使用上面相同的语义,我们可以重新来定义 system-test-config.xml 文件来利用组件扫描的优点。注意这种情况下,我们不需要 明确地声明<context:annotation-config/>,因为<context:component-scan/> 开启了相同的功能。

system-test-config.xml
<beans>
    <!-- 选择并注册AppConfig作为bean -->
    <context:component-scan base-package="com.acme"/>
    <context:property-placeholder
        location="classpath:/com/acme/jdbc.properties"/>
    <bean class="org.springframework.jdbc.datasource.DriverManagerData Source">
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
    </bean>
</beans>

使用了@ImportResource 导入 XML 的@Configuration 类为中心在@Configuration 类作为配置容器主要机制的应用程序中,使用一些 XML 还是必要的。 在这些情况中,仅仅使用@ImportResource 来定义 XML 就可以了。这么来做就实现了“Java 为中心”的方式来配置容器并保持 XML 在最低限度。

@Configuration
@ImportResource("classpath:/com/acme/properties-config.xml")
public class AppConfig {
    private @Value("${jdbc.url}") String url;
    private @Value("${jdbc.username}") String username; 
    private @Value("${jdbc.password}") String password; 
    public @Bean 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);
    // ...
}