组合基于 Java 的配置

Infra 基于 Java 的配置功能允许您组合注解,从而降低配置的复杂性。

使用 @Import 注解

就像在 Infra XML 文件中使用 <import/> 元素来帮助模块化配置一样, @Import 注解允许从另一个配置类加载 @Bean 定义,如下例所示:

  • Java

@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

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

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

注入对导入的 @Bean 定义的依赖

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

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

  • Java

@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);
  // 一切都在配置类之间连接起来...
  TransferService transferService = ctx.getBean(TransferService.class);
  transferService.transfer(100.00, "A123", "C456");
}

还有另一种方法可以达到同样的效果。请记住,@Configuration 类最终只是容器中的另一个 bean: 这意味着它们可以像任何其他 bean 一样利用 @Autowired@Value 注入以及其他功能。

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

避免在同一配置类上的 @PostConstruct 方法中访问本地定义的 bean。 这实际上会导致循环引用,因为非静态 @Bean 方法在语义上需要调用完全初始化的配置类实例。 由于不允许循环引用(例如在 Infra App 2.6+ 中),这可能会触发 BeanCurrentlyInCreationException

此外,要特别小心通过 @Bean 定义 BeanPostProcessorBeanFactoryPostProcessor。 这些通常应声明为 static @Bean 方法,而不触发其包含配置类的实例化。 否则,@Autowired@Value 可能在配置类本身上不起作用,因为可能会早于 AutowiredAnnotationBeanPostProcessor 将其创建为 bean 实例。

以下示例显示了如何将一个 bean 自动装配到另一个 bean:

  • Java

@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);
  // 一切都在配置类之间连接起来...
  TransferService transferService = ctx.getBean(TransferService.class);
  transferService.transfer(100.00, "A123", "C456");
}
仅从 Infra Framework 4.3 开始支持 @Configuration 类中的构造函数注入。 另请注意,如果目标 bean 仅定义了一个构造函数,则无需指定 @Autowired
全限定导入的 bean 以便于导航

在前面的场景中,使用 @Autowired 效果很好并提供了所需的模块化,但确定自动装配的 bean 定义究竟在哪里声明仍然有些模棱两可。 例如,作为查看 ServiceConfig 的开发人员,您如何确切知道 @Autowired AccountRepository bean 是在哪里声明的? 它在代码中并不明确,但这可能没问题。 请记住,https://today-tech.cn/tools[Infra Tools for Eclipse] 提供的工具可以呈现显示一切如何连接的图形,这可能就是您所需要的。 此外,您的 Java IDE 可以轻松找到 AccountRepository 类型的所有声明和使用,并快速向您显示返回该类型的 @Bean 方法的位置。

如果这种歧义是不可接受的,并且您希望在 IDE 中从一个 @Configuration 类直接导航到另一个,请考虑自动装配配置类本身。 以下示例显示了如何执行此操作:

  • Java

@Configuration
public class ServiceConfig {

  @Autowired
  private RepositoryConfig repositoryConfig;

  @Bean
  public TransferService transferService() {
    // '通过'配置类导航到 @Bean 方法!
    return new TransferServiceImpl(repositoryConfig.accountRepository());
  }
}

在上述情况下,AccountRepository 的定义位置完全明确。 但是,ServiceConfig 现在紧密耦合到 RepositoryConfig。这就是权衡。 通过使用基于接口或基于抽象类的 @Configuration 类,可以在一定程度上缓解这种紧密耦合。 考虑以下示例:

  • Java

@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})  // 导入具体的配置!
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,这超出了后者直接依赖项所暗示的范围。

后台初始化

从 6.2 开始,有一个后台初始化选项:@Bean(bootstrap=BACKGROUND) 允许挑选特定的 bean 进行后台初始化,涵盖每个此类 bean 在上下文启动时的整个 bean 创建步骤。

具有非延迟注入点的依赖 bean 会自动等待 bean 实例完成。 所有常规后台初始化都强制在上下文启动结束时完成。 只有另外标记为 @Lazy 的 bean 才允许稍后完成(直到第一次实际访问)。

后台初始化通常与依赖 bean 中的 @Lazy(或 ObjectProvider)注入点一起使用。 否则,当需要尽早注入实际的后台初始化 bean 实例时,主引导线程将被阻塞。

这种形式的并发启动适用于单个 bean:如果这样一个 bean 依赖于其他 bean,它们需要已经初始化,或者通过更早声明,或者通过 @DependsOn 强制在触发受影响 bean 的后台初始化之前在主引导线程中初始化。

必须声明类型为 ExecutorbootstrapExecutor bean 才能使后台引导实际处于活动状态。 否则,后台标记将在运行时被忽略。

引导执行器可以是仅用于启动目的的有界执行器,也可以是也用于其他目的的共享线程池。

有条件地包含 @Configuration 类或 @Bean 方法

根据某些任意系统状态有条件地启用或禁用完整的 @Configuration 类甚至单个 @Bean 方法通常很有用。 这方面的一个常见示例是使用 @Profile 注解仅在 Infra Environment 中启用了特定配置文件时才激活 bean(有关详细信息,请参阅 Bean 定义配置文件)。

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

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

  • Java

@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
  // 读取 @Profile 注解属性
  MultiValueMap<String, Object> attrs = metadata.getAllAnnotationAttributes(Profile.class.getName());
  if (attrs != null) {
    for (Object value : attrs.get("value")) {
      if (context.getEnvironment().acceptsProfiles(((String[]) value))) {
        return true;
      }
    }
    return false;
  }
  return true;
}

有关更多详细信息,请参阅 @Conditional javadoc。

结合 Java 和 XML 配置

Infra @Configuration 类支持并不旨在成为 Infra XML 的 100% 完全替代品。 某些设施,例如 Infra XML 命名空间,仍然是配置容器的理想方式。 在 XML 方便或必要的情况下,您可以选择:要么以“以 XML 为中心”的方式实例化容器(例如,使用 ClassPathXmlApplicationContext),要么以“以 Java 为中心”的方式实例化它(使用 AnnotationConfigApplicationContext@ImportResource 注解根据需要导入 XML)。

以 XML 为中心的 @Configuration 类使用

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

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

请记住,@Configuration 类最终是容器中的 bean 定义。 在这个系列示例中,我们创建一个名为 AppConfig@Configuration 类,并将其作为 <bean/> 定义包含在 system-test-config.xml 中。 因为 <context:annotation-config/> 已打开,容器会识别 @Configuration 注解并正确处理 AppConfig 中声明的 @Bean 方法。

以下示例显示了 Java 中的普通配置类:

  • Java

@Configuration
public class AppConfig {

  @Autowired
  private DataSource dataSource;

  @Bean
  public AccountRepository accountRepository() {
    return new JdbcAccountRepository(dataSource);
  }

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

以下示例显示了示例 system-test-config.xml 文件的一部分:

<beans>
  <!-- 启用处理注解,如 @Autowired 和 @Configuration -->
  <context:annotation-config/>
  <context:property-placeholder location="classpath:/com/acme/jdbc.properties"/>

  <bean class="com.acme.AppConfig"/>

  <bean class="infra.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

public static void main(String[] args) {
  ApplicationContext ctx = new ClassPathXmlApplicationContext("classpath:/com/acme/system-test-config.xml");
  TransferService transferService = ctx.getBean(TransferService.class);
  // ...
}
system-test-config.xml 文件中,AppConfig <bean/> 没有声明 id 元素。 虽然这样做是可以接受的,但这是不必要的,因为没有其他 bean 引用它,并且不太可能按名称从容器中显式获取它。 同样,DataSource bean 仅按类型自动装配,因此并不严格要求显式 bean id
使用 <context:component-scan/> 拾取 @Configuration

因为 @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="infra.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 文件、属性文件和 main 类)显示了如何使用 @ImportResource 注解来实现按需使用 XML 的“以 Java 为中心”的配置:

  • Java

@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

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