资讯专栏INFORMATION COLUMN

Spring中Enable*功能的使用

dinfer / 3282人阅读

摘要:和不同,接口可以根据条件一般是注解的属性指定需要导入的类抽象类实现了接口,并限定选择条件只能是枚举类,也就是说你自定义的注解必须包含属性。

@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 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 {

    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):

EnvironmentAware

BeanFactoryAware

BeanClassLoaderAware

ResourceLoaderAware

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 bootBeans = this.applicationContext.getBeansWithAnnotation(EnableDisconf.class);
        Set scanPackagesList = new HashSet();
        if (!CollectionUtils.isEmpty(bootBeans)) {
            Iterator var4 = bootBeans.entrySet().iterator();

            while(var4.hasNext()) {
                Entry 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;
    }

这里有两点需要说明

如果你的jar包是需要给其他人使用,一定要加上@ConditionalOnMissingBean,确保Bean只会被创建一次。

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);
    }
}

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/74460.html

相关文章

  • Spring Cloud 注册心高可用搭建

    摘要:配置应用名称,在注册中心中显示的服务注册名称。配置为为喜欢,即连接注册中心使用地址形式,也可以使用,但生产环境不推荐。配置注册中心清理无效节点的时间间隔,默认毫秒,即秒。 Spring Cloud的注册中心可以由Eureka、Consul、Zookeeper、ETCD等来实现,这里推荐使用Spring Cloud Eureka来实现注册中心,它基于Netfilix的Eureka做了二次...

    wthee 评论0 收藏0
  • ★推荐一款适用于SpringBoot项目轻量级HTTP客户端框架

    摘要:请求重试拦截器错误解码器在发生请求错误包括发生异常或者响应数据不符合预期的时候,错误解码器可将相关信息解码到自定义异常中。 在SpringBoot项目直接使用okhttp、httpClient或者RestTemplate发起HTTP请求,既繁琐又不方便统一管理。因此,在这里推荐一个适...

    不知名网友 评论0 收藏0
  • 深度剖析Spring Boot自动装配机制实现原理

    摘要:所以,所谓的自动装配,实际上就是如何自动将装载到容器中来。实际上在版本中,模块驱动注解的出现,已经有了一定的自动装配的雏形,而真正能够实现这一机制,还是在版本中,条件注解的出现。,我们来看一下的自动装配是怎么一回事。在前面的分析中,Spring Framework一直在致力于解决一个问题,就是如何让bean的管理变得更简单,如何让开发者尽可能的少关注一些基础化的bean的配置,从而实现自动装...

    不知名网友 评论0 收藏0
  • 2.走向自动装配

    摘要:走向自动装配模式注解装配走向自动装配课程介绍手动装配自动装配自动装配是以手动装配为基础实现的手动装配模式注解模式注解是一种用于声明在应用中扮演组件角色的注解。 2.走向自动装配 Spring 模式注解装配 2-1 走向自动装配 课程介绍 spring framework手动装配 spring boot自动装配 spring boot自动装配是以spring framework手动装...

    rose 评论0 收藏0
  • 领域驱动设计战术模式--领域事件

    摘要:将领域中所发生的活动建模成一系列离散事件。领域事件是领域模型的组成部分,表示领域中所发生的事情。创建领域事件事件命名在建模领域事件时,我们应该根据限界上下文中的通用语言来命名事件。 使用领域事件来捕获发生在领域中的一些事情。 领域驱动实践者发现他们可以通过了解更多发生在问题域中的事件,来更好的理解问题域。这些事件,就是领域事件,主要是与领域专家一起进行知识提炼环节中获得。 领域事件,可...

    wzyplus 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<