Spring中Enable*功能的使用
@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 <import/> 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中的<import />元素等效的功能。 允许导入@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. 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; }
这里有两点需要说明
- 如果你的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); } }