一.概括
我們用Springboot很方便的能將一個框架給搭建起來,是因爲它將以前我們需要手動配置的地方都利用自動配置來代替,利用約定大於配置的思想簡化了我們開發工作量。例如:在沒有springboot之前,我們要在工程裏面連接數據庫的時候,我們需要在applicationContext.xml文件裏面配置:
<bean name="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/test" />
<property name="username" value="root" />
<property name="password" value="123456" />
</bean>
但引入springboot後不需要聲明DriverManagerDataSource,只需要在applicationContext.properties裏面配置連接數據庫的所需要的URL,用戶名和密碼就可以了,然後在pom文件裏面引入相應的jar包就可以了,至於爲什麼可以這樣,下面會講解到
spring.datasource.druid.url=jdbc:mysql://127.0.01:3306/test?useUnicode=true&characterEncoding=utf-8&allowMultiQueries=true
spring.datasource.druid.username=root
spring.datasource.druid.password=123456
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
所以自動加載對於springboot來說很重要。
springboot啓動類
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
@SpringBootApplication註解,@SpringBootApplication是一個複合型註解,裏面包含了三個很重要的註解:
@SpringBootConfiguration:說明DemoApplication這個類是一個ioc容器配置類,可以在DemoApplication類中聲明一些bean,然後通過@ComponentScan加載到ioc容器中,相當於我們配置文件裏面的
<beans>
<bean id="**" class="******">
</bean>
.......
</beans>
@ComponentScan:掃描當前包下的所有類和當前包下面子包所包含的類,將帶有註解的類加載到ioc容器,@ComponentScan在springboot中的作用就是將代碼中帶有@Controller,@Service,@Repority等這些註解的類加載到ioc容器中。相當於我們在配置文件中寫的這樣一段代碼
<context:component-scan base-package="***.***.***"/>
@EnableAutoConfiguration:基於你配置的依賴項,也就是引入的jar包,掃描所有jar包下面的META-INF/spring.factories,spring.factories中都是這個jar的配置類,配置類裏面就有我們所需要的工具類。將所有複合自動配置條件的bean定義加載到ioc容器中,記住@EnableAutoConfiguration自動加載的是一些不需要我們自己去定義但是需要用到的“工具類”,例如上面提到的DriverManagerDataSource類。
@EnableAutoConfiguration能自動加載我們項目中所需要的“工具類”是用到了SpringFactoriesLoader,SpringFactoriesLoader能將指定的配置文件META-INF/spring.factories加載配置。
@Target({ElementType.TYPE})// 註解的適用範圍,其中TYPE用於描述類、接口(包括包註解類型)或enum聲明
@Retention(RetentionPolicy.RUNTIME)// 註解的生命週期,保留到class文件中
@Documented// 表明這個註解應該被javadoc記錄
@Inherited// 子類可以繼承該註解
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
@AliasFor(
annotation = EnableAutoConfiguration.class
)
Class<?>[] exclude() default {};
@AliasFor(
annotation = EnableAutoConfiguration.class
)
String[] excludeName() default {};
@AliasFor(
annotation = ComponentScan.class,
attribute = "basePackages"
)
String[] scanBasePackages() default {};
@AliasFor(
annotation = ComponentScan.class,
attribute = "basePackageClasses"
)
Class<?>[] scanBasePackageClasses() default {};
}
二.@EnableAutoConfiguration的基本原理
來看看@EnableAutoConfiguration這個註解是如何實現
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(EnableAutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";
/**
* Exclude specific auto-configuration classes such that they will never be applied.
* @return the classes to exclude
*/
Class<?>[] exclude() default {};
/**
* Exclude specific auto-configuration class names such that they will never be
* applied.
* @return the class names to exclude
* @since 1.3.0
*/
String[] excludeName() default {};
}
在@EnableAutoConfiguration中有兩個比較重要的註解,一個是@AutoConfigurationPackage,另外一個是@Import(EnableAutoConfigurationImportSelector.class)
@AutoConfigurationPackage的作用就是將當前啓動類所在的包以bean的形式註冊到ioc容器中,至於在後面起什麼作用,我在網上查找了半天,也沒有一個說清楚的,有誰知道的,請在下面留言告訴我。
@Import(EnableAutoConfigurationImportSelector.class):藉助EnableAutoConfigurationImportSelector父類AutoConfigurationImportSelector的selectImports方法來讀取所有依賴的jar包下面META-INF/spring.factories文件,並且根據加載條件來加載項目所需要的類,這樣就完成了springboot的自動加載。
EnableAutoConfigurationImportSelector繼承了AutoConfigurationImportSelector,看看AutoConfigurationImportSelector的selectImports方法
/**
* 最主要的方法
* annotationMetadata
* [@org.springframework.boot.autoconfigure.SpringBootApplication
* (scanBasePackageClasses=[], excludeName=[], exclude=[], scanBasePackages=[])]
* @param annotationMetadata
* @return
*/
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
/**
*
*/
AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader
.loadMetadata(this.beanClassLoader);
/**
* 得到註解中的所有屬性信息{excludeName=[], exclude=[]}
*/
AnnotationAttributes attributes = getAttributes(annotationMetadata);
/**
*加載META-INF/spring-autoconfigure-metadata.properties,獲取所有支持自動配置的信息
* 獲取所有支持EnableAutoConfiguration的組件信息,這部分信息配置在spring-boot-autoconfig包下的spring.factories下
*
* 使用了內部工具使用SpringFactoriesLoader,查找classpath上所有jar包中的
* META-INF\spring.factories,找出其中key爲
* org.springframework.boot.autoconfigure.EnableAutoConfiguration
* 的屬性定義的工廠類名稱。
*/
List<String> configurations = getCandidateConfigurations(annotationMetadata,
attributes);
configurations = removeDuplicates(configurations);
/**
* 去除不需要的
* @EnableAutoConfiguration(exclude = { DataSourceAutoConfiguration.class, RedisAutoConfiguration.class,
DataSourceTransactionManagerAutoConfiguration.class, })
*/
Set<String> exclusions = getExclusions(annotationMetadata, attributes);
checkExcludedClasses(configurations, exclusions);
configurations.removeAll(exclusions);
/**
* 然後使用AutoConfigurationImportFilter進行過濾,過濾的方式基本上是判斷現有系統是否引入了某個組件,(系統是否使用哪個組件是在pom定義的時候就確定了的)
* ,如果有的話則進行相關配置。比如ServletWebServerFactoryAutoConfiguration
* ,會在ServletRequest.class等條件存在的情況下進行配置,
* 而EmbeddedTomcat會在Servlet.class, Tomcat.class存在的情況下創建TomcatServletWebServerFactory
*
* org.springframework.boot.autoconfigure.condition.OnClassCondition
* 總而言之,此過濾器會檢查候選配置類的註解@ConditionalOnClass,如果要求的類在classpath 中不存在,則這個候選配置類會被排除掉
*/
configurations = filter(configurations, autoConfigurationMetadata);
/**
* 現在已經找到所有需要被應用的候選配置類
* 廣播事件AutoConfigurationImportEvent
*/
fireAutoConfigurationImportEvents(configurations, exclusions);
return StringUtils.toStringArray(configurations);
}
private void fireAutoConfigurationImportEvents(List<String> configurations,
Set<String> exclusions) {
List<AutoConfigurationImportListener> listeners = getAutoConfigurationImportListeners();
if (!listeners.isEmpty()) {
AutoConfigurationImportEvent event = new AutoConfigurationImportEvent(this,
configurations, exclusions);
for (AutoConfigurationImportListener listener : listeners) {
invokeAwareMethods(listener);
listener.onAutoConfigurationImportEvent(event);
}
}
}
getCandidateConfigurations方法會讀取到所有依賴jar包下面的META-INF/spring.factories,並將spring.factories中的配置類的全名稱獲取到。
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you are using a custom packaging, make sure that file is correct.");
return configurations;
}
重點來了,springboot之所以能拿到spring.factories就是通過SpringFactoriesLoader來讀取的,SpringFactoriesLoader會將依賴包所有的spring.factories讀取出來,並用一個map來封裝讀取出來的vaule。SpringFactoriesLoader是spring提供的一種擴張方案,其主要功能就是從指定的配置文件META-INF/spring.factories加載配置。
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}
/**
*在springboot啓動的時候會將依賴包所有的spring.factories讀取出來,spring.factories
*中不僅包括配置類,還有監聽器,初始化器等
*/
private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
//第一次就加載了所有的,後面每次需要的時候都是從緩存裏面取的
MultiValueMap<String, String> result = cache.get(classLoader);
if (result != null) {
return result;
}
try {
Enumeration<URL> urls = (classLoader != null ?
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
UrlResource resource = new UrlResource(url);
//將文件路徑轉變成Properties
Properties properties = PropertiesLoaderUtils.loadProperties(resource);
for (Map.Entry<?, ?> entry : properties.entrySet()) {
String factoryClassName = ((String) entry.getKey()).trim();
for (String factoryName : StringUtils.commaDelimitedListToStringArray((String) entry.getValue())) {
result.add(factoryClassName, factoryName.trim());
}
}
}
cache.put(classLoader, result);
return result;
}
catch (IOException ex) {
throw new IllegalArgumentException("Unable to load factories from location [" +
FACTORIES_RESOURCE_LOCATION + "]", ex);
}
}
拿例如引入的mybatis舉例,引入baomidou的mybaitis包後,springboot啓動會去讀取這個jar包下面的配置文件
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.baomidou.mybatisplus.spring.boot.starter.MybatisPlusAutoConfiguration
我們再看看MybatisPlusAutoConfiguration這個配置類裏面的內容,可以看到MybatisPlusAutoConfiguration裏面產生的bean都是以前需要我們去xml文件裏面配置的類,當有了MybatisPlusAutoConfiguration後,我們不需要去xml文件利聲明這些bean,springboot在啓動的時候就將這些ban加載到了容器裏。
@Configuration
@ConditionalOnClass({SqlSessionFactory.class, MybatisSqlSessionFactoryBean.class})
@ConditionalOnBean({DataSource.class})
@EnableConfigurationProperties({MybatisPlusProperties.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class})
public class MybatisPlusAutoConfiguration {
private static final Log logger = LogFactory.getLog(MybatisPlusAutoConfiguration.class);
private final MybatisPlusProperties properties;
private final Interceptor[] interceptors;
private final ResourceLoader resourceLoader;
private final DatabaseIdProvider databaseIdProvider;
private final List<ConfigurationCustomizer> configurationCustomizers;
public MybatisPlusAutoConfiguration(MybatisPlusProperties properties, ObjectProvider<Interceptor[]> interceptorsProvider, ResourceLoader resourceLoader, ObjectProvider<DatabaseIdProvider> databaseIdProvider, ObjectProvider<List<ConfigurationCustomizer>> configurationCustomizersProvider) {
this.properties = properties;
this.interceptors = (Interceptor[])interceptorsProvider.getIfAvailable();
this.resourceLoader = resourceLoader;
this.databaseIdProvider = (DatabaseIdProvider)databaseIdProvider.getIfAvailable();
this.configurationCustomizers = (List)configurationCustomizersProvider.getIfAvailable();
}
@PostConstruct
public void checkConfigFileExists() {
if (this.properties.isCheckConfigLocation() && StringUtils.hasText(this.properties.getConfigLocation())) {
Resource resource = this.resourceLoader.getResource(this.properties.getConfigLocation());
Assert.state(resource.exists(), "Cannot find config location: " + resource + " (please add config file or check your Mybatis configuration)");
}
}
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
MybatisSqlSessionFactoryBean factory = new MybatisSqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setVfs(SpringBootVFS.class);
if (StringUtils.hasText(this.properties.getConfigLocation())) {
factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
}
MybatisConfiguration configuration = this.properties.getConfiguration();
if (configuration == null && !StringUtils.hasText(this.properties.getConfigLocation())) {
configuration = new MybatisConfiguration();
}
if (configuration != null && !CollectionUtils.isEmpty(this.configurationCustomizers)) {
Iterator i$ = this.configurationCustomizers.iterator();
while(i$.hasNext()) {
ConfigurationCustomizer customizer = (ConfigurationCustomizer)i$.next();
customizer.customize(configuration);
}
}
configuration.setDefaultScriptingLanguage(MybatisXMLLanguageDriver.class);
factory.setConfiguration(configuration);
if (this.properties.getConfigurationProperties() != null) {
factory.setConfigurationProperties(this.properties.getConfigurationProperties());
}
if (!ObjectUtils.isEmpty(this.interceptors)) {
factory.setPlugins(this.interceptors);
}
if (this.databaseIdProvider != null) {
factory.setDatabaseIdProvider(this.databaseIdProvider);
}
if (StringUtils.hasLength(this.properties.getTypeAliasesPackage())) {
factory.setTypeAliasesPackage(this.properties.getTypeAliasesPackage());
}
if (StringUtils.hasLength(this.properties.getTypeEnumsPackage())) {
factory.setTypeEnumsPackage(this.properties.getTypeEnumsPackage());
}
if (StringUtils.hasLength(this.properties.getTypeHandlersPackage())) {
factory.setTypeHandlersPackage(this.properties.getTypeHandlersPackage());
}
if (!ObjectUtils.isEmpty(this.properties.resolveMapperLocations())) {
factory.setMapperLocations(this.properties.resolveMapperLocations());
}
if (!ObjectUtils.isEmpty(this.properties.getGlobalConfig())) {
factory.setGlobalConfig(this.properties.getGlobalConfig().convertGlobalConfiguration());
}
return factory.getObject();
}
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
ExecutorType executorType = this.properties.getExecutorType();
return executorType != null ? new SqlSessionTemplate(sqlSessionFactory, executorType) : new SqlSessionTemplate(sqlSessionFactory);
}
@Configuration
@Import({MybatisPlusAutoConfiguration.AutoConfiguredMapperScannerRegistrar.class})
@ConditionalOnMissingBean({MapperFactoryBean.class})
public static class MapperScannerRegistrarNotFoundConfiguration {
public MapperScannerRegistrarNotFoundConfiguration() {
}
@PostConstruct
public void afterPropertiesSet() {
MybatisPlusAutoConfiguration.logger.debug("No " + MapperFactoryBean.class.getName() + " found.");
}
}
public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar, ResourceLoaderAware {
private BeanFactory beanFactory;
private ResourceLoader resourceLoader;
public AutoConfiguredMapperScannerRegistrar() {
}
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
MybatisPlusAutoConfiguration.logger.debug("Searching for mappers annotated with @Mapper");
ClassPathMapperScanner scanner = new ClassPathMapperScanner(registry);
try {
if (this.resourceLoader != null) {
scanner.setResourceLoader(this.resourceLoader);
}
List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
if (MybatisPlusAutoConfiguration.logger.isDebugEnabled()) {
Iterator i$ = packages.iterator();
while(i$.hasNext()) {
String pkg = (String)i$.next();
MybatisPlusAutoConfiguration.logger.debug("Using auto-configuration base package '" + pkg + "'");
}
}
scanner.setAnnotationClass(Mapper.class);
scanner.registerFilters();
scanner.doScan(StringUtils.toStringArray(packages));
} catch (IllegalStateException var7) {
MybatisPlusAutoConfiguration.logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled." + var7);
}
}
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}
public void setResourceLoader(ResourceLoader resourceLoader) {
this.resourceLoader = resourceLoader;
}
}
}
最後當springboot拿到這些配置文件裏面的全性定類名的時候,會通過反射(Java Refletion)實例化爲對應的標註了@Configuration的JavaConfig形式的IoC容器配置類,然後彙總爲一個並加載到IoC容器。
當收集到所有在spring.factories中指定的bean的類路徑,在processGroupImports方法中會以處理@Import註解一樣的邏輯將其導入進容器。
public void processGroupImports() {
for (DeferredImportSelectorGrouping grouping : this.groupings.values()) {
// getImports即上面得到的所有類路徑的封裝
grouping.getImports().forEach(entry -> {
ConfigurationClass configurationClass = this.configurationClasses.get(
entry.getMetadata());
try {
// 和處理@Import註解一樣
processImports(configurationClass, asSourceClass(configurationClass),
asSourceClasses(entry.getImportClassName()), false);
}
catch (BeanDefinitionStoreException ex) {
throw ex;
}
catch (Throwable ex) {
throw new BeanDefinitionStoreException(
"Failed to process import candidates for configuration class [" +
configurationClass.getMetadata().getClassName() + "]", ex);
}
});
}
}
private void processImports(ConfigurationClass configClass, SourceClass currentSourceClass,
Collection<SourceClass> importCandidates, boolean checkForCircularImports) {
...
// 遍歷收集到的類路徑
for (SourceClass candidate : importCandidates) {
...
//如果candidate是ImportSelector或ImportBeanDefinitionRegistrar類型其處理邏輯會不一樣,這裏不關注
// Candidate class not an ImportSelector or ImportBeanDefinitionRegistrar ->
// process it as an @Configuration class
this.importStack.registerImport(
currentSourceClass.getMetadata(), candidate.getMetadata().getClassName());
// 當作 @Configuration 處理
processConfigurationClass(candidate.asConfigClass(configClass));
...
}
...
}
springboot是如何讓applicationcon.properties裏面的配置生效的呢
以MybatisPlusAutoConfiguration舉例,在MybatisPlusAutoConfiguration類上面有一個EnableConfigurationProperties註解,這個註解的作用就是讓使用 @ConfigurationProperties 的類進行ioc注入,也就是將MybatisPlusProperties這個屬性類生效,
@Configuration
@ConditionalOnClass({SqlSessionFactory.class, MybatisSqlSessionFactoryBean.class})
@ConditionalOnBean({DataSource.class})
@EnableConfigurationProperties({MybatisPlusProperties.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class})
public class MybatisPlusAutoConfiguration {
我們再看看MybatisPlusProperties這類。在這裏看到了註解@ConfigurationProperties,這個註解的作用就是讀取配置文件裏面屬性,將讀到的值綁定到被註解的類中的屬性。
@ConfigurationProperties(
prefix = "mybatis-plus"
)
public class MybatisPlusProperties {
public static final String MYBATIS_PLUS_PREFIX = "mybatis-plus";
private String configLocation;
private String[] mapperLocations;
private String typeAliasesPackage;
private String typeEnumsPackage;
private String typeHandlersPackage;
private boolean checkConfigLocation = false;
private ExecutorType executorType;
private Properties configurationProperties;
@NestedConfigurationProperty
private GlobalConfig globalConfig;
@NestedConfigurationProperty
private MybatisConfiguration configuration;
再看看項目中的配置文件application.yml,發現對mybatis的配置中是能和MybatisPlusProperties這個類一一對於對應的。
mybatis-plus:
# xml文件路徑在package下面的 classpath:/com/yourpackage/*/mapper/*Mapper.xml
# xml文件路徑在resources下面 classpath:/mapper/*Mapper.xml
mapper-locations: classpath:/mappers/*Mapper.xml
#掃描的pojo對象所在包
type-aliases-package: com.example.demo.bean
global-config:
#\u9A7C\u5CF0\u4E0B\u5212\u7EBF\u8F6C\u6362
db-column-underline: true
#\u903B\u8F91\u5220\u9664\u914D\u7F6E\uFF08\u4E0B\u97623\u4E2A\u914D\u7F6E\uFF09
logic-delete-value: 1
logic-not-delete-value: 0
sql-injector: com.baomidou.mybatisplus.mapper.LogicSqlInjector
通過@ConfigurationProperties註解將配置文件裏面的值讀到配置bean裏面,然後@EnableConfigurationProperties將註解bean注入到ioc容器中,這樣當項目需要配置信息的時候可以直接從容器中去取。