@Enable**功能开关的三种实现方式详解

622 阅读7分钟

@Import注解的作用

使用场景

通常我们会使用@Enable开头的注解到配置类中,表示开启某个功能特性,比如@EnableCaching、@EnableAsync,这就需要结合@Import注解来实现,这种方式可以简化配置,实现配置的可插拔。

@Import是Spring框架提供的

@Import注解作用是导入一些特定的配置类,这些特定类包括下面三种:

  • @Configuration注解的类
  • 实现ImportSelector接口的类
  • 实现ImportBeanDefinitionRegistrar接口的类

上面三种效果都是一样,但是会有各自的优缺点:

  • @Configuration使用简单明了,但是同样的拓展性不强
  • SelectorImports可以通过实现接口,在接口方法中写复杂点的逻辑,还支持多个配置类的引用
  • ImportBeanDefinitionRegistrar最灵活但也最复杂,需要手动创建和注入Bean

就现在我们对这三种方式的优缺点可能还很模糊,接下来我们结合实际代码来加深理解。

@Import的实际使用说明

现在我们通过现成的框架为例子,再手动写一个简单示例,来分别说明上面提到的三种场景。

一:@Configuration注解的类

例子:@EnableScheduling

比如定时任务开关:@EnableScheduling注解,

  • @EnableScheduling
    • @Import({SchedulingConfiguration.class})

里面Import了SchedulingConfiguration类,这个类的代码不多:

@Configuration
@Role(2)
public class SchedulingConfiguration {
    public SchedulingConfiguration() {
    }

    @Bean(
        name = {"org.springframework.context.annotation.internalScheduledAnnotationProcessor"}
    )
    @Role(2)
    public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() {
        return new ScheduledAnnotationBeanPostProcessor();
    }
}

现在说明一下,首先必须是用@Configuration修饰,这是必要条件,然后是@Role注解,这个注解对于我们来说没有意义,是Spring框架内部对Bean的一个标识:

int ROLE_APPLICATION = 0;	// 应用程序或者业务Bean
int ROLE_SUPPORT = 1;	// 框架内部,用于逻辑区分
int ROLE_INFRASTRUCTURE = 2;	// 框架内部的Bean

简单说这个@Role不用理会,现在继续看,这个配置类里面注册了一个ScheduledAnnotationBeanPostProcessor的Bean,这个Bean初始化后,回去执行里面的代码逻辑,源码太长就不展示了,阅读完源码,就可以知道是去找到用@Scheduled注解修饰的方法,然后执行定制任务的逻辑。

所以我们这第一种方式的工作流程是这样的:

  1. 在配置类中使用@EnableScheduling注解
  2. 配置类会被Spring扫描,所以@EnableScheduling也会开始工作
  3. @EnableScheduling内的@Import({SchedulingConfiguration.class})生效,SchedulingConfiguration配置类里面的Bean被注入
  4. ScheduledAnnotationBeanPostProcessor类被初始化,开始执行里面关于定时任务的代码逻辑

实践

了解了上面的工作原理后,我们也照葫芦画瓢写一个类似的:

需求:写一个@EnableMyLog,使用了这个注解就会输出一段话,不使用就不会输出

  1. 步骤一:创建工作类

    public class MyLog {
        public MyLog() {
            System.out.println("@EnableMyLog注解生效!!");
        }
    
    
  2. 步骤二:创建配置类,将工作类注入进来

    @Configuration
    public class MyLogConfig {
        @Bean
        public MyLog myLog() {
            return new MyLog();
        }
    }
    
  3. 步骤三:声明@EnableMyLog注解

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Import({MyLogConfig.class})
    public @interface EnableMyLog {
    }
    

这个示例要注意不要和启动类在同一个包路径下,因为SpringBoot启动类的@SpringBootApplication默认会扫描该路径下的所有配置类,那么直接就生效了,我们要测试的是可开关,所以新建一个包,在这里写代码即可,也就是外部路径:

├─src
│  ├─main
│  │  ├─java
│  │  │  └─com
│  │  │      ├─cc
│  │  │      │      Application.java
│  │  │      │
│  │  │      └─ext
│  │  │          └─mylog
│  │  │                  EnableMyLog.java
│  │  │                  MyLog.java
│  │  │                  MyLogConfig.java
│  │  │
│  │  └─resources
│  │          application.yml

好了,现在我们直接启动程序,此时并没有输出,将@EnableMyLog修饰在启动类上(或者其他配置类),再次启动,就可以看到输出了:

@EnableMyLog注解生效!!

二:实现ImportSelector接口的类

例子:@EnableAsync

这个一个支持Spring异步的注解开关,

有了上面的学习经验,多余的就不赘述了,直接看这个注解Import的类:

@Import({AsyncConfigurationSelector.class})

进去看源码:

public class AsyncConfigurationSelector extends AdviceModeImportSelector<EnableAsync> {
    private static final String ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME = "org.springframework.scheduling.aspectj.AspectJAsyncConfiguration";

    public AsyncConfigurationSelector() {
    }

    @Nullable
    public String[] selectImports(AdviceMode adviceMode) {
        switch(adviceMode) {
        case PROXY:
            return new String[]{ProxyAsyncConfiguration.class.getName()};
        case ASPECTJ:
            return new String[]{"org.springframework.scheduling.aspectj.AspectJAsyncConfiguration"};
        default:
            return null;
        }
    }
}

这个类继承的父类AdviceModeImportSelector实现了接口ImportSelector,接口提供了一个方法:

String[] selectImports(AnnotationMetadata var1);

AsyncConfigurationSelector重写了这个方法,里面的内容是根据adviceMode作为条件来返回需要的配置类,相比起上面用@Import的方式,这种方式更加灵活,并且可以一次性返回多个配置类。


实践

需求:还是输出一段话,但我们输出多条不一样的,用来模拟返回多个类

  1. 加一个工作类

    public class MyLog2 {
        public MyLog2() {
            System.out.println("@EnableMyLog注解生效!!22222");
        }
    }
    
  2. 加一个配置类

    @Configuration
    public class MyLogConfig2 {
        @Bean
        public MyLog2 myLog2() {
            return new MyLog2();
        }
    }
    
  3. 实现ImportSelector写一个类

    public class MyLogConfigSelector implements ImportSelector {
        @Override
        public String[] selectImports(AnnotationMetadata annotationMetadata) {
            String[] configs = new String[2];
            configs[0] = MyLog.class.getName();
            configs[1] = MyLog2.class.getName();
            return configs;
        }
    }
    
  4. 写一个新的注解

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Import({MyLogConfigSelector.class})
    public @interface EnableMyLogMulti {
    }
    

注意路径和启动类不在一个包路径下哦,上面已经说过了。

然后直接启动程序,没有输出,使用了@EnableMyLogMulti注解后,输出:

@EnableMyLog注解生效!!
@EnableMyLog注解生效!!22222

三:实现ImportBeanDefinitionRegistrar接口的类

例子:@EnableAspectJAutoProxy

它会向容器注册一个自动代理创建器,学习过AOP机制的应该多少了解一点,不了解也没关系,这不是重点,我们只需要知道这种方式的效果就可以了。

@Import({AspectJAutoProxyRegistrar.class})

查看源码:

class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar {
    AspectJAutoProxyRegistrar() {
    }

    public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);
        AnnotationAttributes enableAspectJAutoProxy = AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
        if (enableAspectJAutoProxy != null) {
            if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
                AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
            }

            if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
                AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
            }
        }

    }
}

它实现了ImportBeanDefinitionRegistrar接口,在接口方法里面自己实现了Bean的注册逻辑,

接口实现方法里面关键代码是:enableAspectJAutoProxy.getBoolean("xxx")判断,来区分是使用JDK还是CGLIB来实现动态代理。

想了解Java动态代理可以看我这篇文章Java中的两种动态代理

相比起上面第二种方式,这种方式最灵活,但也最麻烦,一般前两种就足够使用了。


实践

需求:自定义Bean的注入,手动设置属性值

  1. 修改MyLog工作类,添加一个属性name

    public class MyLog {
        private String name;
    
        public MyLog() {
            System.out.println("@EnableMyLog注解生效!!");
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    }
    
  2. 实现ImportBeanDefinitionRegistrar接口,在接口方法里面写自定义的逻辑

    public class MyLogRegistrar implements ImportBeanDefinitionRegistrar {
        @Override
        public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
            // 已经被注入了就跳过
            if (registry.containsBeanDefinition(MyLog.class.getName())) {
                return ;
            }
    
            // 已经被注入了就跳过
            String[] candidates = registry.getBeanDefinitionNames();
            for (String candidate : candidates) {
                BeanDefinition beanDefinition = registry.getBeanDefinition(candidate);
                if (Objects.equals(beanDefinition.getBeanClassName(), MyLog.class.getName())) {
                    return ;
                }
            }
    
            // 创建MyLog类的Bean对象,并赋予值,然后注入
            BeanDefinition beanDefinition = BeanDefinitionBuilder.genericBeanDefinition(MyLog.class).getBeanDefinition();
            beanDefinition.getPropertyValues().add("name", "cc");
            registry.registerBeanDefinition(MyLog.class.getName(), beanDefinition);
        }
    }
    
  3. 写一个新的注解

    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Import({MyLogRegistrar.class})
    public @interface EnableMyLogFlexible {
    }
    

还是老步骤,不用不输出,用了就输出。

但是我们还要测试一下name属性是否成功设置进去了,写一个测试类:

@SpringBootTest
@RunWith(SpringRunner.class)
public class ApplicationTest {

    @Autowired
    private ApplicationContext context;

    @Test
    public void test() {
        MyLog bean = context.getBean(MyLog.class);
        System.out.println("bean.getName() = " + bean.getName());
    }
}
bean.getName() = cc

ok,大功告成。

Spring Boot的自动配置

学习了@Import之后,我们可以聊一聊Spring Boot的自动配置原理了,Spring Boot的启动类需要用注解@SpringBootApplication修饰,该注解是一个复合注解,里面有一个@EnableAutoConfiguration,该注解可以理解成我们上面说的@EnableMyLog注解,就是说用这个注解开启自动配置。

然后我们继续看,这个注解里面有@Import({AutoConfigurationImportSelector.class}),对应我们上面说的第二种方法,现在可以理解了吧,和实践例子中@EnableMyLog的工作原理是一样的:

  1. @EnableAutoConfiguration开启自动配置,即开关生效

  2. @Import({AutoConfigurationImportSelector.class})生效,执行AutoConfigurationImportSelector里面的代码

  3. AutoConfigurationImportSelector代码的作用是:

    1. 扫描所有引入的starter里面的spring.factories文件,将所有的自动配置类扫描出来
    2. 将扫描到的自动配置类进行一些去重、排除
    3. 得到最终的结果并注入

    自动配置类中会有一些默认配置,比如内嵌的Tomcat有一个additional-spring-configuration-metadata.json文件,里面写了关于端口的配置:

    {
        "name": "server.port",
        "defaultValue": 8080
    },
    

    在配置类中它将这个默认配置文件加载进来了,所以开发者引入这个starter的时候,自动配置也就完成了。

这样看来,自动配置的原理并不复杂,只要深入一点进行研究就可以理解。

总结

总的来说,学习这个对阅读源码、手撸功能开关有帮助,所以还是有点用的。

参考资料

www.cnblogs.com/xfeiyun/p/1…

www.jianshu.com/p/ac22c39a8…