一.概括
我们用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容器中,这样当项目需要配置信息的时候可以直接从容器中去取。