Spring 中 @EnableXXX 注解的套路

cover.jpg

前言

在 Spring 框架中有很多实用的功能,不需要写大量的配置代码,只需添加几个注解即可开启。 其中一个重要原因是那些 @EnableXXX 注解,它可以让你通过在配置类加上简单的注解来快速地开启诸如事务管理(@EnableTransactionManagement)、Spring MVC(@EnableWebMvc)或定时任务(@EnableScheduling)等功能。这些看起来简单的注解语句提供了很多功能,但它们的内部机制从表面上看却不太明显。 一方面,对于使用者来说用这么少的代码获得这么多实用的功能是很好的,但另一方面,如果你不了解某个东西的内部是如何工作的,就会使调试和解决问题更加困难。

设计目标

Spring 框架中那些 @EnableXXX 注解的设计目标是允许用户用最少的代码来开启复杂使用的功能。 此外,用户必须能够使用简单的默认值,或者允许手动配置该代码。最后,代码的复杂性要向框架使用者隐藏掉。 简而言之,让使用者设置大量的 Bean,并选择性地配置它们,而不必知道这些 Bean 的细节(或真正被设置的内容)。下面来看看具体的几个例子:

@EnableScheduling (导入一个 @Configuration 类)

首先要知道的是,@EnableXXX 注解并不神奇。实际上在 BeanFactory 中并不知道这些注解的具体内容,而且在 BeanFactory 类中,核心功能和特定注解(如 @EnableWebMvc)或它们所存放的 jar 包(如 spring-web)之间没有任何依赖关系。 让我们看一下 @EnableScheduling,下面看看它是如何工作的。 定义一个 SchedulingConfig 配置类,如下所示:

1
2
3
4
5
@Configuration
@EnableScheduling
public class SchedulingConfig {
// some bean in here
}

上面的内容没有什么特别之处。只是一个用 @EnableScheduling 注释的标准 Java 配置。@EnableScheduling 让你以设定的频率执行某些方法。例如,你可以每 10 分钟运行 BankService.transferMoneyToMghio()。 @EnableScheduling 注解源码如下:

1
2
3
4
5
6
7
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {

}

上面的 EnableScheduling 注解,我们可以看到它只是一个标准的类级注解(@Target/@Retention),应该包含在 JavaDocs 中(@Documented),但是它有一个 Spring 特有的注解(@Import)。 @Import 是将一切联系起来的关键。 在这种情况下,由于我们的 SchedulingConfig 被注解为 @EnableScheduling,当 BeanFactory 解析文件时(内部是ConfigurationClassPostProcessor 在解析它),它也会发现 @Import(SchedulingConfiguration.class) 注解,它将导入该值中定义的类。 在这个注解中,就是 SchedulingConfiguration。

这里导入是什么意思呢?在这种情况下,它只是被当作另一个 Spring Bean。 SchedulingConfiguration 实际上被注解为@Configuration,所以 BeanFactory 会把它看作是另一个配置类,所有在该类中定义的 Bean 都会被拉入你的应用上下文,就像你自己定义了另一个 @Configuration 类一样。 如果我们检查 SchedulingConfiguration,我们可以看到它只定义了一个Bean(一个Post Processor),它负责我们上面描述的调度工作,源码如下:

1
2
3
4
5
6
7
8
9
10
11
@Configuration
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public class SchedulingConfiguration {

@Bean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME)
@Role(BeanDefinition.ROLE_INFRASTRUCTURE)
public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {
return new ScheduledAnnotationBeanPostProcessor();
}

}

也许你会问,如果想配置 SchedulingConfiguration 中定义的 bean 呢? 这里也只是在处理普通的Bean。 所以你对其它 Bean 所使用的机制也适用于此。 在这种情况下,ScheduledAnnotationBeanPostProcessor 使用一个标准的 Spring Bean 生命周期(postProcessAfterInitialization)来发现应用程序上下文何时被刷新。 当符合条件时,它会检查是否有任何 Bean 实现了 SchedulingConfigurer,如果有,就使用这些 Bean 来配置自己。 其实这一点并不明细(在 IDE 中也不太容易找到),但它与 BeanFactory 是完全分离的,而且是一个相当常见的模式,一个 Bean 被用来配置另一个 Bean。 而现在我们可以把所有的点连接起来,它(在某种程度上)很容易找到(你可以 Google 一下文档或阅读一下 JavaDocs)。

@EnableTransactionManagement(导入一个 ImportSelector)

在上一个示例中,我们讨论了像 @EnableScheduling 这样的注解如何使用 @Import 来导入另一个 @Configuration 类并使其所有的 Bean 对你的应用程序可用(和可配置)。但是如果你想根据某些配置加载不同的 Bean 集,会发生什么呢? @EnableTransactionManagement 就是一个很好的例子。TransactioConfig 定义如下:

1
2
3
4
5
@Configuration
@EnableTransactionManagement(mode = AdviceMode.ASPECTJ)
public class TransactioConfig {
// some bean in here
}

再一次,上面没有什么特别之处。只是一个用@EnableTransactionManagement注释的标准Java配置。唯一与之前的例子有些不同的是,用户为注释指定了一个参数(mode=AdviceMode.ASPECTJ)。 @EnableTransactionManagement注解本身看起来像这样。

1
2
3
4
5
6
7
8
9
10
11
12
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(TransactionManagementConfigurationSelector.class)
public @interface EnableTransactionManagement {

boolean proxyTargetClass() default false;

AdviceMode mode() default AdviceMode.PROXY;

int order() default Ordered.LOWEST_PRECEDENCE;
}

和前面一样,一个相当标准的注解,尽管这次它有一些参数。 然而,正如前文提到,@Import 注解是将一切联系在一起的关键,这一点再次得到证实。 但区别在于,这次我们导入的是 TransactionManagementConfigurationSelector 这个类,通过源码可以发现,其实它不是一个被 @Configuration 注解的类。 TransactionManagementConfigurationSelector 是一个实现ImportSelector 的类。 ImportSelector 的目的是让你的代码选择在运行时加载哪些配置类。 它有一个方法,接收关于注解的一些元数据,并返回一个类名数组。 在这种情况下,TransactionManagementConfigurationSelector 会查看模式并根据模式返回一些类。其中的 selectImports 方法源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected String[] selectImports(AdviceMode adviceMode) {
switch (adviceMode) {
case PROXY:
return new String[] {AutoProxyRegistrar.class.getName(),
ProxyTransactionManagementConfiguration.class.getName()};
case ASPECTJ:
return new String[] {determineTransactionAspectClass()};
default:
return null;
}
}

这些类中的大多数是 @Configuration(例如 ProxyTransactionManagementConfiguration),通过前文介绍我们知道它们会像前面一样工作。 对于 @Configuration 类,它们被加载和配置的方式与我们之前看到的完全一样。 所以简而言之,我们可以使用 @Import 和 @Configuration 类来加载一套标准的 Bean,或者使用 @Import 和 ImportSelector 来加载一套在运行时决定的 Bean。

@EnableAspectJAutoProxy (在 Bean 定义层导入)

@Import 支持的最后一种情况,即当你想直接处理 BeanRegistry(工厂)时。如果你需要操作Bean Factory或者在Bean定义层处理Bean,那么这种情况就适合你,它与上面的情况非常相似。 你的 AspectJProxyConfig 可能看起来像。

1
2
3
4
5
@Configuration
@EnableAspectJAutoProxy
public class AspectJProxyConfig {
// some bean in here
}

再一次,上面定义没有什么特别的东西。只是一个用 @EnableAspectJAutoProxy 注释的标准 Java 配置。 下面是@EnableAspectJAutoProxy 的源代码。

1
2
3
4
5
6
7
8
9
10
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {

boolean proxyTargetClass() default false;

boolean exposeProxy() default false;
}

和前面一样,@Import 是关键,但这次它指向 AspectJAutoProxyRegistrar,它既没有 @Configuration 注解,也没有实现 ImportSelector 接口。 这次使用的是实现了 ImportBeanDefinitionRegistrar。 这个接口提供了对 Bean 注册中心(Bean Registry)和注解元数据的访问,因此我们可以在运行时根据注解中的参数来操作 Bean 注册表。 如果你仔细看过前面的示例,你可以看到我们忽略的类也是 ImportBeanDefinitionRegistrar。 在 @Configuration 类不够用的时候,这些类会直接操作 BeanFactory。

所以现在我们已经涵盖了 @EnableXXX 注解使用 @Import 将各种 Bean 引入你的应用上下文的所有不同方式。 它们要么直接引入一组 @Configuration 类,这些类中的所有 Bean 都被导入到你的应用上下文中。 或者它们引入一个 ImportSelector 接口实现类,在运行时选择一组 @Configuration 类并将这些 Bean 导入到你的应用上下文中。 最后,他们引入一个ImportBeanDefinitionRegistrars,可以直接与 BeanFactory 在 BeanDefinition 级别上合作。

结论

总的来说,个人认为这种将 Bean 导入应用上下文的方法很好,因为它使框架使用者的使用某个功能非常容易。不幸的是,它模糊了如何找到可用的选项以及如何配置它们。 此外,它没有直接利用 IDE 的优势,所以很难知道哪些 Bean 正在被创建(以及为什么)。 然而,现在我们知道了 @Import 注解,我们可以使用 IDE 来挖掘一下每个注解及其相关的配置类,并了解哪些 Bean 正在被创建,它们如何被添加到你的应用上下文中,以及如何配置它们。 希望对你有帮助~

-------------本文结束感谢您的阅读-------------
mghio wechat
微信公众号「mghio」
请我吃🍗