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