Spring中Enable*功能的使用

栏目: Java · 发布时间: 5年前

内容简介:@Enable** 注解,一般用于开启某一类功能。类似于一种开关,只有加了这个注解,才能使用某些功能。spring boot 中经常遇到这样的场景,老大让你写一个定时任务脚本、开启一个spring缓存,或者让你提供spring 异步支持。你的做法肯定是 @EnableScheduling+@Scheduled,@EnableCaching+@Cache,@EnableAsync+@Async 立马开始写逻辑了,但你是否真正了解其中的原理呢?之前有写过一个项目,是日志系统,其中要提供spring 注解支持,简

@Enable** 注解,一般用于开启某一类功能。类似于一种开关,只有加了这个注解,才能使用某些功能。

spring boot 中经常遇到这样的场景,老大让你写一个定时任务脚本、开启一个spring缓存,或者让你提供spring 异步支持。你的做法肯定是 @EnableScheduling+@Scheduled,@EnableCaching+@Cache,@EnableAsync+@Async 立马开始写逻辑了,但你是否真正了解其中的原理呢?之前有写过一个项目,是日志系统,其中要提供spring 注解支持,简化配置,当时就是参考以上源码的技巧实现的。

1 原理

先来看@EnableScheduling源码

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

}
复制代码

可以看到这个注解是一个混合注解,和其他注解的唯一区别就是多了一个@Import注解

通过查询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 <?> ...))。

可以看出,通过这个注解的作用是导入一些特定的配置类,这些特定类包括三种

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

1.1 @Configuration注解类

先来看看导入@Configuration注解的例子,打开SchedulingConfiguration类

发现他是属于第一种,直接注册了一个ScheduledAnnotationBeanPostProcessor 的 Bean

简单介绍一下ScheduledAnnotationBeanPostProcessor这个类干了什么事,他实现了BeanPostProcessor类。这个类可以在bean初始化后,容器接管前实现自己的逻辑。在bean 初始化之后,通过AnnotatedElementUtils.getMergedRepeatableAnnotations()方法去拿到当前bean有@Scheduled和@Schedules注解的方法。如果有的话,将其注册到内部ScheduledTaskRegistrar变量中,开启定时任务并执行。顺便说一下,BeanPostProcessor接口对所有bean适用,每个要注册的bean都会走一遍postProcessAfterInitialization方法。

可以看出,这种方法适用于初始化时便获取到全部想要的信息,如@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. AnImportSelector may implement any of the followingAware 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 andImportSelector, 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);
    }
}
复制代码

以上就是本文的全部内容,希望本文的内容对大家的学习或者工作能带来一定的帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

ActionScript 3.0 Cookbook

ActionScript 3.0 Cookbook

Joey Lott、Darron Schall、Keith Peters / Adobe Dev Library / 2006-10-11 / GBP 28.50

Well before Ajax and Microsoft's Windows Presentation Foundation hit the scene, Macromedia offered the first method for building web pages with the responsiveness and functionality of desktop programs......一起来看看 《ActionScript 3.0 Cookbook》 这本书的介绍吧!

CSS 压缩/解压工具
CSS 压缩/解压工具

在线压缩/解压 CSS 代码

Base64 编码/解码
Base64 编码/解码

Base64 编码/解码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换