@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. 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);
}
}
複製程式碼