一文带你搞懂springboot自动化配置的原理

springboot的自动化配置做了什么

在开始这篇文章前,我想问大家一个问题,那就是springboot的自动化配置到底为我们做了什么?

不知道各位朋友有没有使用SSM框架搞过开发,也就是不用springboot,需要自己手动整合spring,springMVC和mybatis这3个框架,之前我们在使用spring的时候需要写大量的xml配置或Java配置类,就像下面这个样子:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans
	http://www.springframework.org/schema/beans/spring-beans.xsd
	http://www.springframework.org/schema/context
	http://www.springframework.org/schema/context/spring-context.xsd">
	<!-- 配置整合mybatis过程 -->
	<!-- 1.配置数据库相关参数properties的属性:${url} -->
	<context:property-placeholder location="classpath:jdbc.properties" />

	<!-- 2.数据库连接池 -->
	<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
		<!-- 配置连接池属性 -->
		<property name="driverClass" value="${jdbc.driver}" />
		<property name="jdbcUrl" value="${jdbc.url}" />
		<property name="user" value="${jdbc.username}" />
		<property name="password" value="${jdbc.password}" />

		<!-- c3p0连接池的私有属性 -->
		<property name="maxPoolSize" value="30" />
		<property name="minPoolSize" value="10" />
		<!-- 关闭连接后不自动commit -->
		<property name="autoCommitOnClose" value="false" />
		<!-- 获取连接超时时间 -->
		<property name="checkoutTimeout" value="10000" />
		<!-- 当获取连接失败重试次数 -->
		<property name="acquireRetryAttempts" value="2" />
	</bean>

	<!-- 3.配置SqlSessionFactory对象 -->
	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<!-- 注入数据库连接池 -->
		<property name="dataSource" ref="dataSource" />
		<!-- 配置MyBaties全局配置文件:mybatis-config.xml -->
		<property name="configLocation" value="classpath:mybatis-config.xml" />
		<!-- 扫描entity包 使用别名 -->
		<property name="typeAliasesPackage" value="com.soecode.lyf.entity" />
		<!-- 扫描sql配置文件:mapper需要的xml文件 -->
		<property name="mapperLocations" value="classpath:mapper/*.xml" />
	</bean>

	<!-- 4.配置扫描Dao接口包,动态实现Dao接口,注入到spring容器中 -->
	<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
		<!-- 注入sqlSessionFactory -->
		<property name="sqlSessionFactoryBeanName" value="sqlSessionFactory" />
		<!-- 给出需要扫描Dao接口包 -->
		<property name="basePackage" value="com.soecode.lyf.dao" />
	</bean>
</beans>

但是自从我们使用springboot后就再也没有写过这些配置了,比如我们在引入spring-boot-starter-jdbc的依赖后,然后在application.yml中配置好数据库的信息后,就可以直接使用JdbcTemplate了,而不需要再去将JdbcTemplate注册为一个bean放到spring容器中,就像下面这样:

@SpringBootTest(classes = SpringbootAutoConfigApplication.class)
public class DataSourceTest {
	// 直接使用JdbcTemplate,而不需要手动通过@configuration配置类 + @bean的方式配置JdbcTemplate 
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Test
    public void test_query_teacher() {
        List<Teacher> teachers = jdbcTemplate.query("select * from teacher", new BeanPropertyRowMapper<>(Teacher.class));
        teachers.forEach(System.out::println);
    }

}

springboot是如何做到的呢?答案就是自动化配置,springboot通过自动化配置的方式帮我们将一些对象自动注入到spring容器中,下面我们将揭晓springboot是如何做到的。

springboot自动化配置的原理

我们在开发一个springboot项目时肯定会有一个主启动类,如下所示:

@SpringBootApplication
public class SpringbootAutoConfigApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootAutoConfigApplication.class, args);
    }

}

然后我们注意看这个@SpringBootApplication注解,它内部有一个@EnableAutoConfiguration注解,这个注解就是用来实现自动化配置的。

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
}

然后我们进入这个@EnableAutoConfiguration注解,可以看到这里面有两个关键的注解,分别是:@Import(AutoConfigurationImportSelector.class)@AutoConfigurationPackage

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {
}

这两个注解都非常关键

@Import(AutoConfigurationImportSelector.class)的作用

我们知道spring中的@Import注解是用来加载配置类的,这里的@Import(AutoConfigurationImportSelector.class)表示加载的配置类是AutoConfigurationImportSelector, 然后我们看下这个类的逻辑:

public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware,
		ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
}

可以看到这个类实现了很多个接口,其中和自动化配置有关的接口是DeferredImportSelector,然后我们看下这个接口:

public interface DeferredImportSelector extends ImportSelector {

	/**
	 * 导入分组
	 */
	@Nullable
	default Class<? extends Group> getImportGroup() {
		return null;
	}


	/**
	 * 处理不同的分组结果
	 */
	interface Group {

		/**
		 * 这个方法中用来加载自动化配置
		 */
		void process(AnnotationMetadata metadata, DeferredImportSelector selector);

}

这里面比较重要的方法是getImportGroup和Group接口中的process方法,它目前只有一个实现

	@Override
	public Class<? extends Group> getImportGroup() {
		return AutoConfigurationGroup.class;
	}

然后我们看下AutoConfigurationGroup类中process方法的实现:

@Override
public void process(AnnotationMetadata annotationMetadata, DeferredImportSelector deferredImportSelector) {
	// 这里是参数校验的代码,可以忽略
	Assert.state(deferredImportSelector instanceof AutoConfigurationImportSelector,
			() -> String.format("Only %s implementations are supported, got %s",
					AutoConfigurationImportSelector.class.getSimpleName(),
					deferredImportSelector.getClass().getName()));
	// 获取自动化配置的信息				
	AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
			.getAutoConfigurationEntry(annotationMetadata); // 获取自动化配置的信息	
	this.autoConfigurationEntries.add(autoConfigurationEntry);
	for (String importClassName : autoConfigurationEntry.getConfigurations()) {
		this.entries.putIfAbsent(importClassName, annotationMetadata);
	}
}

上面这段代码中,关键代码是getAutoConfigurationEntry(annotationMetadata); 它是用来获取springboot的自动化配置信息,我们具体看下这个方法的逻辑:

protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
	if (!isEnabled(annotationMetadata)) {
		return EMPTY_ENTRY;
	}
	// 这里是在获取注解的属性配置信息
	AnnotationAttributes attributes = getAttributes(annotationMetadata);
	// 这里开始读取所有的springboot自动化配置
	List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
	configurations = removeDuplicates(configurations);
	Set<String> exclusions = getExclusions(annotationMetadata, attributes);
	checkExcludedClasses(configurations, exclusions);
	configurations.removeAll(exclusions);
	configurations = getConfigurationClassFilter().filter(configurations);
	fireAutoConfigurationImportEvents(configurations, exclusions);
	return new AutoConfigurationEntry(configurations, exclusions);
}

上面的代码中,最重要的代码是:getCandidateConfigurations(annotationMetadata, attributes); 这里将会读取springboot的自动化配置信息,咱们具体看下:

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
	// 这里是在从spring.factories文件中读取自动化配置信息,我们在spring.factories中自定义的配置类就是在这里被解析的
	List<String> configurations = new ArrayList<>(
			SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(), getBeanClassLoader()));
	// 这里是在从META-INF/spring/%s.imports中读取springboot的自动化配置信息,是需要阅读的重点方法
	ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configurations::add);
	Assert.notEmpty(configurations,
			"No auto configuration classes found in META-INF/spring.factories nor in META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports. If you "
					+ "are using a custom packaging, make sure that file is correct.");
	return configurations;
}

我们重点看下ImportCandidates.load(AutoConfiguration.class, getBeanClassLoader()).forEach(configurations::add); 这行代码,它的作用是从META-INF/spring/%s.imports中读取springboot的自动化配置信息。

public static ImportCandidates load(Class<?> annotation, ClassLoader classLoader) {
	Assert.notNull(annotation, "'annotation' must not be null");
	ClassLoader classLoaderToUse = decideClassloader(classLoader);
	// 这里就是在读取springboot中的自动化配置文件的具体信息,LOCATION是一个常量,表示:"META-INF/spring/%s.imports"
	String location = String.format(LOCATION, annotation.getName());
	Enumeration<URL> urls = findUrlsInClasspath(classLoaderToUse, location);
	List<String> importCandidates = new ArrayList<>();
	while (urls.hasMoreElements()) {
		URL url = urls.nextElement();
		importCandidates.addAll(readCandidateConfigurations(url));
	}
	return new ImportCandidates(importCandidates);
}

我们可以从上面的方法中得知,springboot加载的就是org.springframework.boot.autoconfigure.AutoConfiguration.imports文件的内容,如下图所示:
在这里插入图片描述
然后我们看下这个文件中有什么,可以看到里面放的全是自动化配置的路径信息
在这里插入图片描述
在这里插入图片描述
相信看到这里,大家应该就明白springboot是如何加载自动化配置信息的了,但是到这里还没有完,我们先进入到其中一个自动化配置类中看看,比如我们最常用的JDBC,在jdbc这个包中,我们需要重点关注的是类名以AutoConfiguration结尾的类,这些就是自动化配置类,如下图所示:
在这里插入图片描述
然后咱们可以看到这个配置中有一个@Import({ DatabaseInitializationDependencyConfigurer.class, JdbcTemplateConfiguration.class, NamedParameterJdbcTemplateConfiguration.class })的配置表示加载DatabaseInitializationDependencyConfigurer,JdbcTemplateConfiguration,NamedParameterJdbcTemplateConfiguration这3个配置类,由于我们在开发中经常会用到JdbcTemplate,所以我们重点看下JdbcTemplateConfiguration这个配置类:

@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(JdbcOperations.class)
class JdbcTemplateConfiguration {

	@Bean
	@Primary
	JdbcTemplate jdbcTemplate(DataSource dataSource, JdbcProperties properties) {
		JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
		JdbcProperties.Template template = properties.getTemplate();
		jdbcTemplate.setFetchSize(template.getFetchSize());
		jdbcTemplate.setMaxRows(template.getMaxRows());
		if (template.getQueryTimeout() != null) {
			jdbcTemplate.setQueryTimeout((int) template.getQueryTimeout().getSeconds());
		}
		return jdbcTemplate;
	}
}

我们可以到这个配置类中,springboot帮我们自动注册了JdbcTemplate这个bean,这也是为什么我们在引入spring-boot-starter-jdbc的依赖后就可以直接使用JdbcTemplate,而不需要写一个配置类专门注册JdbcTemplate,那是因为springboot框架通过自动化配置的方式已经帮我们注册好了。然后我们再注意看JdbcTemplateAutoConfiguration配置类的上面有一个@ConditionalOnClass({ DataSource.class, JdbcTemplate.class }), 它表示只有当项目中存在DataSource和JdbcTemplate这两个类时,这个自动化配置类才会生效。我们将@ConditionalXXX这样的注解称为条件注解。
在这里插入图片描述
总而言之,springboot的自动化配置的过程是:

  1. 读取自动化配置的文件(具体的文件是org.springframework.boot.autoconfigure.AutoConfiguration.imports),从里面拿到所有的自动化配置类的路径信息。
  2. 然后根据路径找到这些自动化配置类进行处理,具体的处理方式是:如果当前项目符合自动配置类上的条件注解中的条件,就加载这个配置类,否则就过滤掉这个自动化配置类。
  3. 加载配置类后,根据配置类的bean配置将bean注册到spring容器中。

总而言之,springboot在实现自动化配置的过程中,@conditionalXXX条件注解功不可没。

讲完了@Import(AutoConfigurationImportSelector.class)的作用,下面我们来看下剩下的@AutoConfigurationPackage注解的作用。

@AutoConfigurationPackage`注解的作用

咱们看下这个注解:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
}

可以看到里面有一个@Import(AutoConfigurationPackages.Registrar.class)表示加载AutoConfigurationPackages.Registrar这个配置类,然后咱们看下这个配置类:

static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {

	@Override
	public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
		register(registry, new PackageImports(metadata).getPackageNames().toArray(new String[0]));
	}

	@Override
	public Set<Object> determineImports(AnnotationMetadata metadata) {
		return Collections.singleton(new PackageImports(metadata));
	}

}

这里面的关键方法是:registerBeanDefinitions,咱们具体看下这个方法的逻辑:
在这里插入图片描述
可以看到这里其实就是在将主启动类所在的包下的所有class作为包扫描的起点。
在这里插入图片描述
然后再看下register方法,这里其实就是在将符合条件的bean的class信息放到beanDefinition中,用于实现后续的批量注册bean。

public static void register(BeanDefinitionRegistry registry, String... packageNames) {
	if (registry.containsBeanDefinition(BEAN)) {
		BasePackagesBeanDefinition beanDefinition = (BasePackagesBeanDefinition) registry.getBeanDefinition(BEAN);
		beanDefinition.addBasePackages(packageNames);
	}
	else {
		registry.registerBeanDefinition(BEAN, new BasePackagesBeanDefinition(packageNames));
	}
}

到目前为止,springboot是如何实现自动化配置的原理就带大家搞清楚了

排除自动化配置

为了检测上面的结论是否正确,咱们可以排除JdbcTemplate的自动化配置,看看排除这个配置后,是否还能正常使用JdbcTemplate

@SpringBootApplication(exclude = JdbcTemplateAutoConfiguration.class)
public class SpringbootAutoConfigApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootAutoConfigApplication.class, args);
    }
}

可以看到报错了,日志提示说spring容器中没有JdbcTemplate这个bean
在这里插入图片描述
所以这里也验证了一点:那就是自动化配置类确实帮我们将JdbcTemplate注册到了spring容器中,这样就不需要我们手动再去写一个配置类注册JdbcTemplate了。

总而言之,springboot的自动化配置做的只有一件事:bean的自动注册,也就是springboot在启动的时候就帮我们将开发中会用到的一些bean放到spring的对象缓存池中。

文章中完整示例代码的地址

文章中完整示例代码的地址

在这里插入图片描述

最后的总结

这篇文章中我们带大家从头到尾分析了springboot的自动化配置是如何实现的,并且还带大家通过排除JdbcTemplate的自动化配置类验证了自动配置类自动帮我们注册bean的结论。

觉得有收获的朋友可以点个赞,您的鼓励就是我最大的动力!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值