Spring中Enable*功能的使用

@Enable** 註解,通常用於開啓某一類功能。相似於一種開關,只有加了這個註解,才能使用某些功能。html

spring boot 中常常遇到這樣的場景,老大讓你寫一個定時任務腳本、開啓一個spring緩存,或者讓你提供spring 異步支持。你的作法確定是 @EnableScheduling+@Scheduled,@EnableCaching+@Cache,@EnableAsync+@Async 立馬開始寫邏輯了,但你是否真正瞭解其中的原理呢?以前有寫過一個項目,是日誌系統,其中要提供spring 註解支持,簡化配置,當時就是參考以上源碼的技巧實現的。java

1 原理

先來看@EnableScheduling源碼spring

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(SchedulingConfiguration.class)
@Documented
public @interface EnableScheduling {

}
複製代碼

能夠看到這個註解是一個混合註解,和其餘註解的惟一區別就是多了一個@Import註解api

經過查詢spring api文檔緩存

Indicates one or more @Configuration classes to import. Provides functionality equivalent to the element in Spring XML. Allows for importing @Configuration classes, ImportSelector and ImportBeanDefinitionRegistrar implementations, as well as regular component classes (as of 4.2; analogous to AnnotationConfigApplicationContext.register(java.lang.Class<?>...)). 表示要導入的一個或多個@Configuration類。 提供與Spring XML中的元素等效的功能。 容許導入@Configuration類,ImportSelector和ImportBeanDefinitionRegistrar實現,以及常規組件類(從4.2開始;相似於AnnotationConfigApplicationContext.register(java.lang.Class <?> ...))。app

能夠看出,經過這個註解的做用是導入一些特定的配置類,這些特定類包括三種框架

  • @Configuration 註解的類
  • 實現ImportSelector接口的類
  • 實現ImportBeanDefinitionRegistrar接口的類

1.1 @Configuration註解類

先來看看導入@Configuration註解的例子,打開SchedulingConfiguration類異步

發現他是屬於第一種,直接註冊了一個ScheduledAnnotationBeanPostProcessor 的 Beanide

簡單介紹一下ScheduledAnnotationBeanPostProcessor這個類幹了什麼事,他實現了BeanPostProcessor類。這個類能夠在bean初始化後,容器接管前實現本身的邏輯。在bean 初始化以後,經過AnnotatedElementUtils.getMergedRepeatableAnnotations()方法去拿到當前bean有@Scheduled和@Schedules註解的方法。若是有的話,將其註冊到內部ScheduledTaskRegistrar變量中,開啓定時任務並執行。順便說一下,BeanPostProcessor接口對全部bean適用,每一個要註冊的bean都會走一遍postProcessAfterInitialization方法。post

能夠看出,這種方法適用於初始化時便獲取到所有想要的信息,如@Scheduled的元數據等。同時須要注意:被註解方法不能有參數,不能有返回值。

1.2 實現ImportSelector接口的類

再來看看第二種實現方式,打開EnableAsync類

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AsyncConfigurationSelector.class)
public @interface EnableAsync {

    Class<? extends Annotation> annotation() default Annotation.class;

    boolean proxyTargetClass() default false;

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

}
複製代碼

能夠看到他經過導入AsyncConfigurationSelector類來開啓異步支持,打開AsyncConfigurationSelector類

public class AsyncConfigurationSelector extends AdviceModeImportSelector<EnableAsync> {

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

}
複製代碼

AdviceModeImportSelector是一個抽象類,他實現了ImportSelector類的selectImports方法,先來看一下selectImports的api 文檔

Interface to be implemented by types that determine which @Configuration class(es) should be imported based on a given selection criteria, usually one or more annotation attributes. An ImportSelector may implement any of the following Aware interfaces, and their respective methods will be called prior to selectImports(org.springframework.core.type.AnnotationMetadata):

ImportSelectors are usually processed in the same way as regular @Import annotations, however, it is also possible to defer selection of imports until all @Configuration classes have been processed (see DeferredImportSelector for details). 經過一個給定選擇標準的類型來肯定導入哪些@Configuration,他和@Import的處理方式相似,只不過這個導入Configuration能夠延遲到全部Configuration都加載完

總結起來有一下幾點:

  • selectImports 接口和@Configuration相似,用於導入類。
  • 和@Configuration不一樣,selectImports 接口能夠根據條件(通常是註解的屬性)指定須要導入的類
  • AdviceModeImportSelector 抽象類實現了SelectImports接口,並限定選擇條件只能是AdviceMode枚舉類,也就是說你自定義的註解必須包含AdviceMode屬性。

1.3實現ImportBeanDefinitionRegistrar接口的類

查看@EnableAspectJAutoProxy

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import(AspectJAutoProxyRegistrar.class)
public @interface EnableAspectJAutoProxy {

    boolean proxyTargetClass() default false;

    boolean exposeProxy() default false;

}
複製代碼

這個註解導入的時AspectJAutoProxyRegistrar類,AspectJAutoProxyRegistrar實現了

ImportBeanDefinitionRegistrar接口,實現類

public void registerBeanDefinitions( AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {

        AopConfigUtils.registerAspectJAnnotationAutoProxyCreatorIfNecessary(registry);

        AnnotationAttributes enableAspectJAutoProxy =
                AnnotationConfigUtils.attributesFor(importingClassMetadata, EnableAspectJAutoProxy.class);
        if (enableAspectJAutoProxy.getBoolean("proxyTargetClass")) {
            AopConfigUtils.forceAutoProxyCreatorToUseClassProxying(registry);
        }
        if (enableAspectJAutoProxy.getBoolean("exposeProxy")) {
            AopConfigUtils.forceAutoProxyCreatorToExposeProxy(registry);
        }
    }
複製代碼

咱們來看一下ImportBeanDefinitionRegistrar官方api文檔

Interface to be implemented by types that register additional bean definitions when processing @Configuration classes. Useful when operating at the bean definition level (as opposed to @Bean method/instance level) is desired or necessary. Along with @Configuration and ImportSelector, classes of this type may be provided to the @Import annotation (or may also be returned from an ImportSelector).

這個接口的兩個參數,AnnotationMetadata 表示當前類的註解,BeanDefinitionRegistry 註冊bean。

能夠看出和前兩種方式比,這種方式更加精細,須要你本身去實現bean的註冊邏輯。第二種方式只傳入了一個AnnotationMetadata,返回類全限定名,框架自動幫你註冊。而第三種方式,還傳入了一個BeanDefinitionRegistry讓你本身去註冊。

其實三種方式都能很好的實現導入邏輯。他們的優缺點以下:

  • @Configuration 須要手動判斷如何導入。
  • SelectImports 封裝較好,可根據選擇導入,尤爲當你選擇的條件是AdviceMode,還能夠選擇AdviceModeSelector,幾行代碼搞定。
  • ImportBeanDefinitionRegistrar 最幸苦也最靈活,一些邏輯本身寫。

2 實踐

最後,咱們須要來寫一個自實現的@EnableDisconfig功能。disconfig是一種配置中心,咱們通常的用法是寫兩個bean

@Bean(destroyMethod="destroy")
public DisconfMgrBean disconfMgrBean(){
.....
}
@Bean(initMethod="int", destroyMethod="destroy")
public DisconfMgrBeanSecond disconfMgrBeanSecond(){
......
}
複製代碼

每次搭框架這麼寫確實挺費事的,即便你記在筆記上了,複製粘貼也還須要改scan路徑。下面咱們用優雅的代碼來實現一下。

2.1 @EnableDisconf

首先定義一個註解類@EnableDisconf

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
@Import({DisconfConfig.class})
public @interface EnableDisconf{
    String scanPackages() default "";
}
複製代碼

接着實現DisconfConfig類

@Configuration
public class DisconfConfig implements ApplicationContextAware {
    private ApplicationContext applicationContext;

    public DisconfConfig() {
    }

    @Bean(
        destroyMethod = "destroy"
    )
    @ConditionalOnMissingBean
    public DisconfMgrBean disconfMgrBean() {
        DisconfMgrBean bean = new DisconfMgrBean();
        Map<String, Object> bootBeans = this.applicationContext.getBeansWithAnnotation(EnableDisconf.class);
        Set<String> scanPackagesList = new HashSet();
        if (!CollectionUtils.isEmpty(bootBeans)) {
            Iterator var4 = bootBeans.entrySet().iterator();

            while(var4.hasNext()) {
                Entry<String, Object> configBean = (Entry)var4.next();
                Class<?> bootClass = configBean.getValue().getClass();
                if (bootClass.isAnnotationPresent(EnableDisconf.class)) {
                    EnableDisconf enableDisconf = (EnableDisconf)bootClass.getAnnotation(EnableDisconf.class);
                    String scanPackages = enableDisconf.scanPackages();
                    if (StringUtils.isEmpty(scanPackages)) {
                        scanPackages = bootClass.getPackage().getName();
                    }

                    scanPackagesList.add(scanPackages);
                }
            }
        }

        if (CollectionUtils.isEmpty(scanPackagesList)) {
            bean.setScanPackage(System.getProperty("scanPackages"));
        } else {
            bean.setScanPackage(StringUtils.join(scanPackagesList, ","));
        }

        return bean;
    }

    @Bean(
        initMethod = "init",
        destroyMethod = "destroy"
    )
    @ConditionalOnMissingBean
    public DisconfMgrBeanSecond disconfMgrBeanSecond() {
        return new DisconfMgrBeanSecond();
    }

    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        this.applicationContext = applicationContext;
    }
複製代碼

這裏有兩點須要說明

  1. 若是你的jar包是須要給其餘人使用,必定要加上@ConditionalOnMissingBean,確保Bean只會被建立一次。
  2. ApplicationContextAware 這個類是咱們程序感知spring容器上下文的類,簡單來講就是經過相似**Aware這樣的類去拿容器中的信息。感興趣的同窗能夠看一下spring中關於**Aware類的使用。

最後你只須要將項目打成jar包,上傳私服,而後就能夠很輕鬆的使用@Enable帶來的便捷了。

@SpringBootApplication
@EnableDisconf(scanPackages="com.demo")
public class Application{
    public static void main(String[] args){
        SpringApplication.run(Application.class,args);
    }
}
複製代碼
相關文章
相關標籤/搜索