Java SpringBoot
在 Spring Boot开发过程中,经常会看到使用 @EnableXXX来激活某一个功能性的模块,通过类注解激活后就能使用所激活的配置带来的功能。
来探究一下这个 @EnableXXX做了哪些工作,或者应该怎么通过自定义的方式开发自己的功能模块。

演示环境

  • IntelliJ IDEA 2020.2.1 (Community Edition)
  • Maven 3.5.4
  • Spring Boot 2.1.1.RELEASE

    走进源码

    在 SpringBoot中 @Enable的实现方式用两种。
    一种是注解驱动的方式,以 @EnableWebMvc为例进行探究;
    另外一种是接口编程的方式,以 @EnableCaching为例进行探究。

    1、注解驱动方式(@EnableWebMvc

    在 Spring Boot项目中,可以使用 @EnableWebMvc注解用来激活 Spring MVC相关的配置,接下来进入源码一探究竟。

    1. @Retention(RetentionPolicy.RUNTIME)
    2. @Target(ElementType.TYPE)
    3. @Documented
    4. @Import(DelegatingWebMvcConfiguration.class)
    5. public @interface EnableWebMvc {
    6. }

    通过观察上面的源码,就可以大胆的猜测,其实使用 @EnableWebMvc注解的作用就是导入 DelegatingWebMvcConfiguration.class这个类,接下来就进入这个类看看。

    1. @Configuration
    2. public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
    3. ........
    4. @Override
    5. protected void addInterceptors(InterceptorRegistry registry) {
    6. this.configurers.addInterceptors(registry);
    7. }
    8. @Override
    9. protected void addResourceHandlers(ResourceHandlerRegistry registry) {
    10. this.configurers.addResourceHandlers(registry);
    11. }
    12. ........
    13. }

    进入到这个类中发现了这个 @Configuration注解,到这儿好像明白了写什么。首先这个 DelegatingWebMvcConfiguration类继承了 WebMvcConfigurationSupport类,重写了里面的关于 WebMvc的相关配置,然后作为一个配置类加载到 Spring容器中。至此来实现启动(激活)WebMvc模块。

    2、接口编程的方式(@EnableCaching

    在 Spring Boot项目中,可以使用 @EnableCaching注解用来激活缓存相关的配置,接着进入源码看看到底做了什么。 ```java @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Import(CachingConfigurationSelector.class)
    public @interface EnableCaching {

    boolean proxyTargetClass() default false;
    AdviceMode mode() default AdviceMode.PROXY;
    int order() default Ordered.LOWEST_PRECEDENCE;

}

  1. 这里 `@EnableCaching`同样是使用 `@Import`导入了一个配置类,而它导入的是 `CachingConfigurationSelector`,接着进入这个类看一看。
  2. ```java
  3. public class CachingConfigurationSelector extends AdviceModeImportSelector<EnableCaching> {
  4. .....
  5. @Override
  6. public String[] selectImports(AdviceMode adviceMode) {
  7. switch (adviceMode) {
  8. case PROXY:
  9. return getProxyImports();
  10. case ASPECTJ:
  11. return getAspectJImports();
  12. default:
  13. return null;
  14. }
  15. }
  16. .....
  17. }

发现其实这个类没有被注解标注,但是它继承了 AdviceModeImportSelector<enablecaching>,而这个类又继承了 ImportSelector,并且可以看看 ImportSelector的代码:

  1. public interface ImportSelector {
  2. /**
  3. * Select and return the names of which class(es) should be imported based on
  4. * the {@link AnnotationMetadata} of the importing @{@link Configuration} class.
  5. */
  6. String[] selectImports(AnnotationMetadata importingClassMetadata);
  7. }

这个类中只用一个方法,那就是 selectImports。也就是说当重写了这个方法之后,可以在方法中添加自己的逻辑判断,来决定最后导入哪些配置类。这样就可以实现灵活的加载配置。这个方法的返回值 String[]里面存放的是所有复合条件的配置类的全路径信息。

自定义实现

通过上面的分析已经完成了对 @EnableXXX套路的了解,接下来自己动手实现下。首先需要准备一个 Spirng Boot的项目,这里已经准备好了。

1、注解驱动方式的自定义实现

根据分析源码的步骤首先需要准备一个配置类。接下来在 configuration包下,创建一个 HelloConfiguration。代码如下:

  1. /**
  2. * Hello模块的配置
  3. */
  4. @Configuration
  5. public class HelloConfiguration {
  6. @Bean
  7. public String hello() { // method name is bean name
  8. System.out.println("Bean : hello is loading.");
  9. return "hello word !";
  10. }
  11. }

这里被 @Bean标注的方法,方法名会作为 bean对象的名称。而且当该 bean被加载的时候还会在控制台输出一段话 Bean : hello is loading.。
配置类已经准备好了,接下来在 annotation包中编写激活配置的注解。

  1. /**
  2. * 激活Hello模块配置
  3. */
  4. @Retention(RetentionPolicy.RUNTIME)
  5. @Target(ElementType.TYPE)
  6. @Documented
  7. @Import(HelloConfiguration.class) // 模块装配:注解驱动实现
  8. public @interface EnableHello {
  9. }

最后在 bootstrap包中编写验证 @EnableHello模块的启动类。

  1. /**
  2. * 验证自定义 {@link EnableHello} 模块化装配
  3. */
  4. @EnableHello
  5. public class EnableHelloBootstrap {
  6. public static void main(String[] args) {
  7. ConfigurableApplicationContext context = new SpringApplicationBuilder(EnableHelloBootstrap.class)
  8. .web(WebApplicationType.NONE)
  9. .run(args);
  10. String helloBean = context.getBean("hello", String.class);
  11. System.out.println("hello Bean: " + helloBean);
  12. context.close();
  13. }
  14. }

在启动类上标注了 @EnableHello来激活 Hello模块,并且在 Spring Boot项目启动后,获取到了应用的上下文 ConfigurableApplicationContext。然后根据注入的 bean的名字 hello来获取 bean,接着打印 bean的内容,最后关闭上下文。
启动程序后,可以在控制台中看到这样两行输出:

  1. Bean : hello is loading.
  2. hello Bean: hello word !

这刚好就是配置的内容,到这儿就完成了基于注解驱动方式激活(启动)配置。

2、接口编程方式自定义实现

根据读过源码后,首先最重要的是有一个继承 ImportSelector的实现类。在 annotation包中创建一个 HelloImportSelector配置类。

  1. /**
  2. * Hello {@link ImportSelector} 的实现
  3. */
  4. public class HelloImportSelector implements ImportSelector {
  5. private final String key = "jerome";
  6. @Override
  7. public String[] selectImports(AnnotationMetadata importingClassMetadata) {
  8. if ("jerome".equals(key)) {
  9. return new String[]{HelloJeromeConfiguration.class.getName()};
  10. }
  11. return new String[]{HelloConfiguration.class.getName()};
  12. }
  13. }

这里为了体现出 selectImports方法的作用,在本类中添加一个 key字段。当 key的值为 jerome的时候去加载 HelloJeromeConfiguration这个配置类。首先看一个这个配置类的内容。

  1. /**
  2. * Hello模块的配置
  3. */
  4. @Configuration
  5. public class HelloJeromeConfiguration {
  6. @Bean
  7. public String hello() { // method name is bean name
  8. System.out.println("Bean : hello is loading.");
  9. return "hello jerome !";
  10. }
  11. }

加载这个配置类后 bean的名字还是 hello但是内容变成了 hello jerome !。接着对 @EnableHello注解进行改造改造成 @EnableHellos

  1. /**
  2. * 激活Hellos模块配置
  3. */
  4. @Retention(RetentionPolicy.RUNTIME)
  5. @Target(ElementType.TYPE)
  6. @Documented
  7. @Import(HelloImportSelector.class) // 模块装配:接口驱动实现 优点可选bean
  8. public @interface EnableHellos {
  9. }

这里使用了 HelloImportSelector来实现模块装配。
这里 @EnableHellos可能不符合命名规范,这里只做演示使用。
最后在 bootstrap包中编写验证 @EnableHello模块的启动类。

  1. /**
  2. * 验证自定义 {@link EnableHellos} 模块化装配
  3. */
  4. @EnableHellos
  5. public class EnableHellosBootstrap {
  6. public static void main(String[] args) {
  7. ConfigurableApplicationContext context = new SpringApplicationBuilder(EnableHellosBootstrap.class)
  8. .web(WebApplicationType.NONE)
  9. .run(args);
  10. String helloBean = context.getBean("hello", String.class);
  11. System.out.println("hello Bean: " + helloBean);
  12. context.close();
  13. }
  14. }

这里和基于注解驱动方式激活(启动)配置的验证方法一样,就是直接拷贝的,主要修改了 @EnableHellos这个激活注解。
启动程序后,可以在控制台中看到这样两行输出:

  1. Bean : hello is loading.
  2. hello Bean: hello jerome !

这刚好就是配置的内容,到这儿就完成了基于接口编程方式激活(启动)配置。

总结

通过上面了解了 @EnableXXX的两种实现方式。其中基于注解驱动方式激活(启动)配置的方式是相对方便的一个实现,需要激活什么就直接导入该配置类即可;而基于接口编程方式激活(启动)配置的方式相对更加灵活,可以根据自定义的规则来选择激活哪些配置类,不激活哪些配置类,这是一种比较灵活的方式,方便对其进行扩展。
总之这两种方式都是手动激活的方式,换言之需要在启动类上添加 @Enable***的注解,来手动激活配置,这种方式在某些场景并不是很方便。