就像 <import/>
元素在 Spring XML 文件中被用来帮助模块化配置一样,@Import 注解允许从另一个配置类中加载 @Bean 定义,如下例所示。
@Configuration
public class ConfigA {
@Bean
public A a() {
return new A();
}
}
@Configuration
@Import(ConfigA.class)
public class ConfigB {
@Bean
public B b() {
return new B();
}
}
现在,在实例化上下文时不需要同时指定 ConfigA.class 和 ConfigB.class,而只需要明确提供 ConfigB,正如下面的例子所示:
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(ConfigB.class);
// now both beans A and B will be available...
A a = ctx.getBean(A.class);
B b = ctx.getBean(B.class);
}
这种方法简化了容器的实例化,因为只需要处理一个类,而不是要求你在构建过程中记住潜在的大量@Configuration 类。
:::tips 从 Spring 框架 4.2 开始,@Import 也支持对常规组件类的引用,类似于AnnotationConfigApplicationContext.register 方法。如果你想避免组件扫描,通过使用一些配置类作为入口点来明确定义你的所有组件,这就特别有用。 :::
在导入的 @Bean 定义上注入依赖关系
前面的例子是可行的,但也是简单的。在大多数实际情况下,Bean 在配置类之间有相互依赖的关系。当使用 XML 时,这不是一个问题,因为不涉及编译器,你可以声明 ref="someBean"
并相信 Spring 会在容器初始化过程中解决这个问题。当使用 @Configuration 类时,Java 编译器会对配置模型进行约束,即对其他 Bean 的引用必须是有效的 Java 语法。
幸运的是,解决这个问题很简单。正如我们已经讨论过的,一个 @Bean 方法可以有任意数量的参数来描述 Bean 的依赖关系。考虑下面这个更真实的场景,有几个 @Configuration 类,每个类都依赖于其他类中声明的 Bean。
@Configuration
public class ServiceConfig {
@Bean // 依赖 RepositoryConfig 中的 bean 声明
public TransferService transferService(AccountRepository accountRepository) {
return new TransferServiceImpl(accountRepository);
}
}
@Configuration
public class RepositoryConfig {
@Bean // 依赖 SystemTestConfig 中的 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 注入以及其他功能。
:::tips 请确保你用这种方式注入的依赖关系是最简单的那种。@Configuration 类在上下文的初始化过程中会被很早地处理,强行以这种方式注入依赖关系可能会导致意外的早期初始化。只要有可能,就采用基于参数的注入方式,如前面的例子。
另外,对于通过 @Bean 定义的 BeanPostProcessor 和 BeanFactoryPostProcessor 要特别小心。那些通常应该被声明为静态的 @Bean 方法,而不是触发其包含的配置类的实例化。否则,@Autowired 和 @Value 可能对配置类本身不起作用,因为有可能在 AutowiredAnnotationBeanPostProcessor 之前将其创建为一个 bean 实例。 :::
下面的例子显示了一个 Bean 是如何被自动连接到另一个 Bean 的。
@Configuration
public class ServiceConfig {
// 这里使用 @Autowired 注入 AccountRepository
@Autowired
private AccountRepository accountRepository;
@Bean
public TransferService transferService() {
return new TransferServiceImpl(accountRepository);
}
}
@Configuration
public class RepositoryConfig {
private final DataSource 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");
}
:::tips 从 Spring Framework 4.3 开始,@Configuration 类中的构造函数注入才被支持。还要注意的是,如果目标 Bean 只定义了一个构造函数,就不需要指定 @Autowired。 :::
方便导航:完全限定符导入的 Bean
在前面的场景中,使用 @Autowired 效果很好,并提供了所需的模块化,但确定自动连接的 Bean 定义到底在哪里声明,还是有些模糊。例如,作为一个查看 ServiceConfig 的开发者,你怎么知道 @Autowired AccountRepository Bean 到底是在哪里声明的?它在代码中并不明确,而这可能就很好。记住,Eclipse 的Spring 工具提供的工具可以呈现图形,显示所有东西是如何连接的,这可能就是你所需要的。另外,你的Java IDE 可以很容易地找到 AccountRepository 类型的所有声明和使用,并快速显示返回该类型的 @Bean 方法的位置。
在不能接受这种模糊性的情况下,你希望在你的 IDE 中从一个 @Configuration 类直接导航到另一个,可以考虑自动连接配置类本身。下面的例子展示了如何做到这一点:
@Configuration
public class ServiceConfig {
@Autowired
private RepositoryConfig repositoryConfig;
@Bean
public TransferService transferService() {
// 通过配置类导航到 @Bean 方法!
return new TransferServiceImpl(repositoryConfig.accountRepository());
}
}
在前面的情况下,AccountRepository 的定义是完全明确的。然而,ServiceConfig 现在与 RepositoryConfig 紧密耦合了。这就是权衡的结果。这种紧耦合可以通过使用基于接口或基于抽象类的@Configuration 类来缓解。考虑一下下面的例子:
@Configuration
public class ServiceConfig {
@Autowired
private RepositoryConfig repositoryConfig;
@Bean
public TransferService transferService() {
return new TransferServiceImpl(repositoryConfig.accountRepository());
}
}
// 声明了一个接口,并在接口中声明了 @Bean
@Configuration
public interface RepositoryConfig {
@Bean
AccountRepository accountRepository();
}
@Configuration
public class DefaultRepositoryConfig implements RepositoryConfig {
// 覆盖接口中的 Bean 方法
@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 类和它们的依赖关系就变得与浏览基于接口的代码的通常过程没有什么不同。
:::tips 如果你想影响某些 Bean 的启动创建顺序,可以考虑将其中一些 Bean 声明为 @Lazy(在第一次访问时创建,而不是在启动时创建)或声明为 @DependsOn 某些其他 Bean(确保特定的其他 Bean 在当前Bean 之前创建,超出后者的直接依赖关系)。 :::