分析SpringBoot的自動化配置原理的時候,可以觀察下這些@Enable*注解的源碼,可以發現所有的注解都有一個@Import注解。@Import注解是用來導入配置類的,這也就是說這些自動開啟的實現其實是導入了一些自動配置的Bean。
如:freemarker的自動化配置類FreeMarkerAutoConfiguration,這個自動化配置類需要classloader中的一些類需要存在並且在其他的一些配置類之后進行加載。
但是還存在一些自動化配置類,它們需要在使用一些注解開關的情況下才會生效。比如spring-boot-starter-batch里的@EnableBatchProcessing注解、@EnableCaching等。
一、自動注入示例
在分析這些開關的原理之前,我們來看一個需求:
定義一個Annotation,讓使用了這個Annotaion的應用程序自動化地注入一些類或者做一些底層的事情。
我們會使用Spring提供的@Import注解配合一個配置類來完成。
我們以一個最簡單的例子來完成這個需求:定義一個注解EnableContentService,使用了這個注解的程序會自動注入ContentService這個bean。
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Import(ContentConfiguration.class) public @interface EnableContentService {} public interface ContentService { void doSomething(); } public class SimpleContentService implements ContentService { @Override public void doSomething() { System.out.println("do some simple things"); } }
然后在應用程序的入口加上@EnableContentService注解。
這樣的話,ContentService就被注入進來了。 SpringBoot也就是用這個完成的。只不過它用了更加高級點的ImportSelector。
二、@Import注解導入配置方式的三種類型
在一的示例中,我們用到了@Import注解,現在來看看@Import的使用方法。
第一類:直接導入配置類
例如,@EnableScheduling中直接導入配置類SchedulingConfiguration,這個類注解了@Configuration,且注冊了一個scheduledAnnotationProcessor的Bean
@Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Import({SchedulingConfiguration.class}) @Documented public @interface EnableScheduling { }
第二類:依據條件選擇配置類
例如在@EnableAsync中,通過AsyncConfigurationSelector.class的選擇配置類配置。
@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的根接口為ImportSelector,這個接口需要重寫selectImports方法,在此方法內進行事先條件判斷。
若adviceMode為PORXY,則返回ProxyAsyncConfiguration這個配置類。
若activeMode為ASPECTJ,則返回AspectJAsyncConfiguration配置類。
關鍵方法如下:
public class AsyncConfigurationSelector extends AdviceModeImportSelector<EnableAsync> { private static final String ASYNC_EXECUTION_ASPECT_CONFIGURATION_CLASS_NAME = "org.springframework.scheduling.aspectj.AspectJAsyncConfiguration"; /** * {@inheritDoc} * @return {@link ProxyAsyncConfiguration} or {@code AspectJAsyncConfiguration} for * {@code PROXY} and {@code ASPECTJ} values of {@link EnableAsync#mode()}, respectively */ @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; } } }
第三類:動態注冊Bean
spring中的EnableAspectJAutoProxy.java
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Documented @Import(AspectJAutoProxyRegistrar.class) public @interface EnableAspectJAutoProxy { boolean proxyTargetClass() default false; boolean exposeProxy() default false; }
AspectJAutoProxyRegistrar 實現了ImportBeanDefinitionRegistrar接口,ImportBeanDefinitionRegistrar的作用是在運行時自動添加Bean到已有的配置類,通過重寫方法:
class AspectJAutoProxyRegistrar implements ImportBeanDefinitionRegistrar { /** * Register, escalate, and configure the AspectJ auto proxy creator based on the value * of the @{@link EnableAspectJAutoProxy#proxyTargetClass()} attribute on the importing * {@code @Configuration} class. */ @Override 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); } } }
其中,AnnotationMetadata參數用來獲得當前配置類上的注解;
BeanDefinittionRegistry參數用來注冊Bean。
三、ImportSelector在SpringBoot中的使用
SpringBoot里的ImportSelector是通過SpringBoot提供的@EnableAutoConfiguration這個注解里完成的。
這個@EnableAutoConfiguration注解可以顯式地調用,否則它會在@SpringBootApplication注解中隱式地被調用。
@EnableAutoConfiguration注解中使用了EnableAutoConfigurationImportSelector作為ImportSelector。下面這段代碼就是EnableAutoConfigurationImportSelector中進行選擇的具體代碼:
@Override public String[] selectImports(AnnotationMetadata metadata) { try { AnnotationAttributes attributes = getAttributes(metadata); List<String> configurations = getCandidateConfigurations(metadata, attributes); configurations = removeDuplicates(configurations); // 刪除重復的配置 Set<String> exclusions = getExclusions(metadata, attributes); // 去掉需要exclude的配置 configurations.removeAll(exclusions); configurations = sort(configurations); // 排序 recordWithConditionEvaluationReport(configurations, exclusions); return configurations.toArray(new String[configurations.size()]); } catch (IOException ex) { throw new IllegalStateException(ex); } }
其中getCandidateConfigurations方法將獲取配置類:
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) { return SpringFactoriesLoader.loadFactoryNames( getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()); }
SpringFactoriesLoader.loadFactoryNames方法會根據FACTORIES_RESOURCE_LOCATION這個靜態變量從所有的jar包中讀取META-INF/spring.factories文件信息:
public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) { String factoryClassName = factoryClass.getName(); try { Enumeration<URL> urls = (classLoader != null ? classLoader.getResources(FACTORIES_RESOURCE_LOCATION) : ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION)); List<String> result = new ArrayList<String>(); while (urls.hasMoreElements()) { URL url = urls.nextElement(); Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url)); String factoryClassNames = properties.getProperty(factoryClassName); // 只會過濾出key為factoryClassNames的值 result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames))); } return result; } catch (IOException ex) { throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() + "] factories from location [" + FACTORIES_RESOURCE_LOCATION + "]", ex); } }
getCandidateConfigurations方法中的getSpringFactoriesLoaderFactoryClass方法返回的是EnableAutoConfiguration.class,所以會過濾出key為org.springframework.boot.autoconfigure.EnableAutoConfiguration的值。
下面這段配置代碼就是autoconfigure這個jar包里的spring.factories文件的一部分內容(有個key為org.springframework.boot.autoconfigure.EnableAutoConfiguration,所以會得到這些AutoConfiguration):
# Initializers org.springframework.context.ApplicationContextInitializer=\ org.springframework.boot.autoconfigure.logging.AutoConfigurationReportLoggingInitializer # Application Listeners org.springframework.context.ApplicationListener=\ org.springframework.boot.autoconfigure.BackgroundPreinitializer # Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\ org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\ org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\ org.springframework.boot.autoconfigure.MessageSourceAutoConfiguration,\
...
...
當然了,這些AutoConfiguration不是所有都會加載的,會根據AutoConfiguration上的@ConditionalOnClass等條件判斷是否加載。
上面這個例子說的讀取properties文件的時候只會過濾出key為org.springframework.boot.autoconfigure.EnableAutoConfiguration的值。
SpringBoot內部還有一些其他的key用於過濾得到需要加載的類:
-
org.springframework.test.context.TestExecutionListener
-
org.springframework.beans.BeanInfoFactory
-
org.springframework.context.ApplicationContextInitializer
-
org.springframework.context.ApplicationListener
-
org.springframework.boot.SpringApplicationRunListener
-
org.springframework.boot.env.EnvironmentPostProcessor
-
org.springframework.boot.env.PropertySourceLoader
四、spring中的@Enable*
@EnableAspectJAutoProxy
@EnableAspectJAutoProxy注解 激活Aspect自動代理,使用@EnableAspectJAutoProxy相當於<aop:aspectj-autoproxy />開啟對AspectJ自動代理的支持。
@EnableAsync
@EnableAsync注解開啟異步方法的支持。
見《@Async實現異步調用》
@EnableScheduling
@EnableScheduling注解開啟計划任務的支持。
@EnableWebMVC
@EnableWebMVC注解用來開啟Web MVC的配置支持。
也就是寫Spring MVC時的時候會用到。
@EnableConfigurationProperties
@EnableConfigurationProperties注解是用來開啟對@ConfigurationProperties注解配置Bean的支持。
@EnableJpaRepositories
@EnableJpaRepositories注解開啟對Spring Data JPA Repostory的支持。
Spring Data JPA 框架,主要針對的就是 Spring 唯一沒有簡化到的業務邏輯代碼,至此,開發者連僅剩的實現持久層業務邏輯的工作都省了,唯一要做的,就只是聲明持久層的接口,其他都交給 Spring Data JPA 來幫你完成!
簡單的說,Spring Data JPA是用來持久化數據的框架。
@EnableTransactionManagement
@EnableTransactionManagement注解開啟注解式事務的支持。
注解@EnableTransactionManagement通知Spring,@Transactional注解的類被事務的切面包圍。這樣@Transactional就可以使用了。
@EnableCaching
@EnableCaching注解開啟注解式的緩存支持
五、@EnableScheduling源碼分析
1. @Scheduled 可以將一個方法標識為可定時執行的。但必須指明cron(),fixedDelay(),或者fixedRate()屬性。
注解的方法必須是無輸入參數並返回空類型void的。
@Scheduled注解由注冊的ScheduledAnnotationBeanPostProcessor來處理,該processor可以通過手動來注冊,更方面的方式是通過<task:annotation-driven/>或者@EnableScheduling來注冊。@EnableScheduling可以注冊的原理是什么呢?先看定義:
package org.springframework.scheduling.annotation; import java.lang.annotation.Documented; import java.lang.annotation.ElementType; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.util.concurrent.Executor; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import org.springframework.scheduling.Trigger; import org.springframework.scheduling.config.ScheduledTaskRegistrar; @Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME) @Import(SchedulingConfiguration.class) @Documented public @interface EnableScheduling { }
可以看到@EnableScheduling的實現由SchedulingConfiguration來完成。
@Configuration @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public class SchedulingConfiguration { @Bean(name = TaskManagementConfigUtils.SCHEDULED_ANNOTATION_PROCESSOR_BEAN_NAME) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) public ScheduledAnnotationBeanPostProcessor scheduledAnnotationProcessor() { return new ScheduledAnnotationBeanPostProcessor(); } }
從上述代碼可以看出,SchedulingConfiguration注冊了一個ScheduledAnnotationBeanPostProcessor。
來看一下ScheduledAnnotationBeanPostProcessor來如何處理定時任務的?
protected void processScheduled(Scheduled scheduled, Method method, Object bean) { try { Assert.isTrue(method.getParameterTypes().length == 0, "Only no-arg methods may be annotated with @Scheduled"); Method invocableMethod = AopUtils.selectInvocableMethod(method, bean.getClass()); Runnable runnable = new ScheduledMethodRunnable(bean, invocableMethod); boolean processedSchedule = false; String errorMessage = "Exactly one of the 'cron', 'fixedDelay(String)', or 'fixedRate(String)' attributes is required"; Set<ScheduledTask> tasks = new LinkedHashSet<ScheduledTask>(4); // Determine initial delay long initialDelay = scheduled.initialDelay(); String initialDelayString = scheduled.initialDelayString(); if (StringUtils.hasText(initialDelayString)) { Assert.isTrue(initialDelay < 0, "Specify 'initialDelay' or 'initialDelayString', not both"); if (this.embeddedValueResolver != null) { initialDelayString = this.embeddedValueResolver.resolveStringValue(initialDelayString); } try { initialDelay = Long.parseLong(initialDelayString); } catch (NumberFormatException ex) { throw new IllegalArgumentException( "Invalid initialDelayString value \"" + initialDelayString + "\" - cannot parse into integer"); } } // Check cron expression String cron = scheduled.cron(); if (StringUtils.hasText(cron)) { Assert.isTrue(initialDelay == -1, "'initialDelay' not supported for cron triggers"); processedSchedule = true; String zone = scheduled.zone(); if (this.embeddedValueResolver != null) { cron = this.embeddedValueResolver.resolveStringValue(cron); zone = this.embeddedValueResolver.resolveStringValue(zone); } TimeZone timeZone; if (StringUtils.hasText(zone)) { timeZone = StringUtils.parseTimeZoneString(zone); } else { timeZone = TimeZone.getDefault(); } tasks.add(this.registrar.scheduleCronTask(new CronTask(runnable, new CronTrigger(cron, timeZone)))); } // At this point we don't need to differentiate between initial delay set or not anymore if (initialDelay < 0) { initialDelay = 0; } // Check fixed delay long fixedDelay = scheduled.fixedDelay(); if (fixedDelay >= 0) { Assert.isTrue(!processedSchedule, errorMessage); processedSchedule = true; tasks.add(this.registrar.scheduleFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay))); } String fixedDelayString = scheduled.fixedDelayString(); if (StringUtils.hasText(fixedDelayString)) { Assert.isTrue(!processedSchedule, errorMessage); processedSchedule = true; if (this.embeddedValueResolver != null) { fixedDelayString = this.embeddedValueResolver.resolveStringValue(fixedDelayString); } try { fixedDelay = Long.parseLong(fixedDelayString); } catch (NumberFormatException ex) { throw new IllegalArgumentException( "Invalid fixedDelayString value \"" + fixedDelayString + "\" - cannot parse into integer"); } tasks.add(this.registrar.scheduleFixedDelayTask(new IntervalTask(runnable, fixedDelay, initialDelay))); } // Check fixed rate long fixedRate = scheduled.fixedRate(); if (fixedRate >= 0) { Assert.isTrue(!processedSchedule, errorMessage); processedSchedule = true; tasks.add(this.registrar.scheduleFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay))); } String fixedRateString = scheduled.fixedRateString(); if (StringUtils.hasText(fixedRateString)) { Assert.isTrue(!processedSchedule, errorMessage); processedSchedule = true; if (this.embeddedValueResolver != null) { fixedRateString = this.embeddedValueResolver.resolveStringValue(fixedRateString); } try { fixedRate = Long.parseLong(fixedRateString); } catch (NumberFormatException ex) { throw new IllegalArgumentException( "Invalid fixedRateString value \"" + fixedRateString + "\" - cannot parse into integer"); } tasks.add(this.registrar.scheduleFixedRateTask(new IntervalTask(runnable, fixedRate, initialDelay))); } // Check whether we had any attribute set Assert.isTrue(processedSchedule, errorMessage); // Finally register the scheduled tasks synchronized (this.scheduledTasks) { Set<ScheduledTask> registeredTasks = this.scheduledTasks.get(bean); if (registeredTasks == null) { registeredTasks = new LinkedHashSet<ScheduledTask>(4); this.scheduledTasks.put(bean, registeredTasks); } registeredTasks.addAll(tasks); } } catch (IllegalArgumentException ex) { throw new IllegalStateException( "Encountered invalid @Scheduled method '" + method.getName() + "': " + ex.getMessage()); } }
從上面的代碼可以看出:@Scheduled有三個屬性,分別是:
cron expression
fixedDelay
fixedRate
根據這些屬性的不同,都加入到ScheduledTaskRegistrar來管理定時任務:
ScheduledTaskRegistrar.java
protected void scheduleTasks() { if (this.taskScheduler == null) { this.localExecutor = Executors.newSingleThreadScheduledExecutor(); this.taskScheduler = new ConcurrentTaskScheduler(this.localExecutor); } if (this.triggerTasks != null) { for (TriggerTask task : this.triggerTasks) { addScheduledTask(scheduleTriggerTask(task)); } } if (this.cronTasks != null) { for (CronTask task : this.cronTasks) { addScheduledTask(scheduleCronTask(task)); } } if (this.fixedRateTasks != null) { for (IntervalTask task : this.fixedRateTasks) { addScheduledTask(scheduleFixedRateTask(task)); } } if (this.fixedDelayTasks != null) { for (IntervalTask task : this.fixedDelayTasks) { addScheduledTask(scheduleFixedDelayTask(task)); } } }
從上面看出:
3種不同屬性的task均由quartz的taskScheduler的不同方法來完成,
scheduleWithFixedDelay,
scheduleAtFixedRate,
schedule
即最終的實現由TaskScheduler來完成定時任務。