SpringBoot系列课程(四)-自动化配置原理

2022-09-07 14:48:15

1.前言

在这里插入图片描述
不论在工作中,亦或是求职面试,Spring Boot已经成为我们必知必会的技能项。除了某些老旧的政府项目或金融项目持有观望态度外,如今的各行各业都在飞速的拥抱这个已经不是很新的Spring启动框架。

当然,作为Spring Boot的精髓,自动配置原理的工作过程往往只有在“面试”的时候才能用得上,但是如果在工作中你能够深入的理解Spring Boot的自动配置原理,将无往不利。

Spring Boot的出现,得益于“习惯优于配置”的理念,没有繁琐的配置、难以集成的内容(大多数流行第三方技术都被集成),这是基于Spring 4.x提供的按条件配置Bean的能力。

2.SpringBoot的入口

我们开发任何一个Spring Boot项目,都会用到如下的启动类

@SpringBootApplicationpublicclassApplication{publicstaticvoidmain(String[] args){
        SpringApplication.run(Application.class, args);}}

从上面代码可以看出,Annotation定义(@SpringBootApplication)和类定义(SpringApplication.run)最为耀眼,所以要揭开SpringBoot的神秘面纱,我们要从这两位开始就可以了。

3.SpringBootApplication背后的秘密

@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 @interfaceSpringBootApplication{...}

虽然定义使用了多个Annotation进行了原信息标注,但实际上重要的只有三个Annotation:

@Configuration@SpringBootConfiguration点开查看发现里面还是应用了@Configuration@EnableAutoConfiguration@ComponentScan

所以,如果我们使用如下的SpringBoot启动类,整个SpringBoot应用依然可以与之前的启动类功能对等:

@Configuration@EnableAutoConfiguration@ComponentScanpublicclassApplication{publicstaticvoidmain(String[] args){
        SpringApplication.run(Application.class, args);}}

每次写这3个比较累,所以写一个@SpringBootApplication方便点。接下来分别介绍这3个Annotation。

4.@Configuration

这里的@Configuration对我们来说不陌生,它就是JavaConfig形式的Spring Ioc容器的配置类使用的那个@Configuration,SpringBoot社区推荐使用基于JavaConfig的配置形式,所以,这里的启动类标注了@Configuration之后,本身其实也是一个IoC容器的配置类。
举几个简单例子回顾下,XML跟config配置方式的区别:

表达形式层面

基于XML配置的方式是这样:

<?xml version="1.0" encoding="UTF-8"?><beansxmlns="http://www.springframework.org/schema/beans"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd"default-lazy-init="true"><!--bean定义--></beans>

而基于JavaConfig的配置方式是这样:

@ConfigurationpublicclassMockConfiguration{//bean定义}

任何一个标注了@Configuration的Java类定义都是一个JavaConfig配置类。

注册bean定义层面

基于XML的配置形式是这样:

<beanid="mockService"class="..MockServiceImpl">
    ...</bean>

而基于JavaConfig的配置形式是这样的:

@ConfigurationpublicclassMockConfiguration{@Beanpublic MockServicemockService(){returnnewMockServiceImpl();}}

任何一个标注了@Bean的方法,其返回值将作为一个bean定义注册到Spring的IoC容器,方法名将默认成该bean定义的id。

表达依赖注入关系层面

为了表达bean与bean之间的依赖关系,在XML形式中一般是这样:

<beanid="mockService"class="..MockServiceImpl">
    <propery name ="dependencyService" ref="dependencyService" /></bean><beanid="dependencyService"class="DependencyServiceImpl"></bean>

而基于JavaConfig的配置形式是这样的:

@ConfigurationpublicclassMockConfiguration{@Beanpublic MockServicemockService(){returnnewMockServiceImpl(dependencyService());}@Beanpublic DependencyServicedependencyService(){returnnewDependencyServiceImpl();}}

如果一个bean的定义依赖其他bean,则直接调用对应的JavaConfig类中依赖bean的创建方法就可以了。

5.@ComponentScan扫描bean

我们原来使用spring的使用不会在xml中一个一个配置bean,我们在再类上加上@Repository@Service,@Controller,@Component,并且注入时可以使用@AutoWired的注解注入。 这一切的功能都需要我们配置包扫描<context:component-scan base-package="com.bruceliu"/>.
然而现在注解驱动开发已经没有了配置文件,不能配置。但是提供了@ComponentScan,我们可以在配置类上面加上这个注解也是一样,并且也能扫描配置包项目的相关注解,也能完成自动注入。

接下来我们先来看扫描组件,后面再看注入

package com.bruceliu.service;import com.bruceliu.bean.User;import org.springframework.stereotype.Service;@ServicepublicclassUserService{public UsergetUser(Long id){
        System.out.println("userservice...");return null;}}
package com.bruceliu.controller;import com.bruceliu.bean.User;import org.springframework.stereotype.Controller;@ControllerpublicclassUserController{//先不拷贝页面,直接打印即可public UsergetUser(Long id){
        System.out.println("usercontroller...");return null;}}
//注解类==配置文件@Configuration//告诉spring这是一个注解类@ComponentScan("com.bruceliu")publicclassMainConfig{//相当于在xml中配置了<bean id="" class="com.bruceliu.dao.UserDao"><bean/>@Bean("userDao")//指定bean的名字public UserDaouserDao01(){returnnewUserDaoImpl();}}
publicclassMainConfigTest{@TestpublicvoidtestIoc(){
        ApplicationContext context=newAnnotationConfigApplicationContext(MainConfig.class);for(String beanName: context.getBeanDefinitionNames()){
            System.out.println(beanName);}}}

注:所以SpringBoot的启动类最好是放在root package下,因为默认不指定basePackages。

6.@EnableAutoConfiguration

@EnableAutoConfiguration作为一个复合Annotation,其自身定义关键信息如下:

@SuppressWarnings("deprecation")@Target(ElementType.TYPE)@Retention(RetentionPolicy.RUNTIME)@Documented@Inherited@AutoConfigurationPackage@Import(EnableAutoConfigurationImportSelector.class)public @interfaceEnableAutoConfiguration{...}

其中,最关键的要属@Import(EnableAutoConfigurationImportSelector.class),借助EnableAutoConfigurationImportSelector@EnableAutoConfiguration可以帮助SpringBoot应用将所有符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IoC容器。就像一只“八爪鱼”一样借助于Spring框架原有的一个工具类:SpringFactoriesLoader的支持,@EnableAutoConfiguration可以智能的自动配置功效才得以大功告成!
在这里插入图片描述
而这个注解也是一个派生注解,其中的关键功能由@Import提供,其导入的AutoConfigurationImportSelector的selectImports()方法通过SpringFactoriesLoader.loadFactoryNames()扫描所有具有META-INF/spring.factories的jar包。spring-boot-autoconfigure-x.x.x.x.jar里就有一个这样的spring.factories文件。

这个spring.factories文件也是一组一组的key=value的形式,其中一个key是EnableAutoConfiguration类的全类名,而它的value是一个xxxxAutoConfiguration的类名的列表,这些类名以逗号分隔,如下图所示:

在这里插入图片描述

这个@EnableAutoConfiguration注解通过@SpringBootApplication被间接的标记在了Spring Boot的启动类上。在SpringApplication.run(…)的内部就会执行selectImports()方法,找到所有JavaConfig自动配置类的全限定名对应的class,然后将所有自动配置类加载到Spring容器中。

7.自动配置生效

每一个XxxxAutoConfiguration自动配置类都是在某些条件之下才会生效的,这些条件的限制在Spring Boot中以注解的形式体现,常见的条件注解有如下几项:

@ConditionalOnBean:当容器里有指定的bean的条件下。
@ConditionalOnMissingBean:当容器里不存在指定bean的条件下。
@ConditionalOnClass:当类路径下有指定类的条件下。
@ConditionalOnMissingClass:当类路径下不存在指定类的条件下。

ServletWebServerFactoryAutoConfiguration配置类为例,解释一下全局配置文件中的属性如何生效,比如:server.port=8081,是如何生效的(当然不配置也会有默认值,这个默认值来自于org.apache.catalina.startup.Tomcat)。
在这里插入图片描述
ServletWebServerFactoryAutoConfiguration类上,有一个@EnableConfigurationProperties注解:开启配置属性,而它后面的参数是一个ServerProperties类,这就是习惯优于配置的最终落地点。
在这里插入图片描述
在这个类上,我们看到了一个非常熟悉的注解:@ConfigurationProperties,它的作用就是从配置文件中绑定属性到对应的bean上,而@EnableConfigurationProperties负责导入这个已经绑定了属性的bean到spring容器中(见上面截图)。那么所有其他的和这个类相关的属性都可以在全局配置文件中定义,也就是说,真正“限制”我们可以在全局配置文件中配置哪些属性的类就是这些XxxxProperties类,它与配置文件中定义的prefix关键字开头的一组属性是唯一对应的。

至此,我们大致可以了解。在全局配置的属性如:server.port等,通过@ConfigurationProperties注解,绑定到对应的XxxxProperties配置实体类上封装为一个bean,然后再通过@EnableConfigurationProperties注解导入到Spring容器中。

而诸多的XxxxAutoConfiguration自动配置类,就是Spring容器的JavaConfig形式,作用就是为Spring 容器导入bean,而所有导入的bean所需要的属性都通过xxxxProperties的bean来获得。

可能到目前为止还是有所疑惑,但面试的时候,其实远远不需要回答的这么具体,你只需要这样回答:

Spring Boot启动的时候会通过@EnableAutoConfiguration注解找到META-INF/spring.factories配置文件中的所有自动配置类,并对其进行加载,而这些自动配置类都是以AutoConfiguration结尾来命名的,它实际上就是一个JavaConfig形式的Spring容器配置类,它能通过以Properties结尾命名的类中取得在全局配置文件中配置的属性如:server.port,而XxxxProperties类是通过@ConfigurationProperties注解与全局配置文件中对应的属性进行绑定的。

EnableAutoConfigurationImportSelector,@EnableAutoConfiguration可以帮助SpringBoot应用将所有符合条件的@Configuration配置都加载到当前SpringBoot创建并使用的IoC容器。就像一只“八爪鱼”一样。
在这里插入图片描述@EnableAutoConfiguration自动配置的魔法骑士就变成了:从classpath中搜寻所有的META-INF/spring.factories配置文件,并将其中org.springframework.boot.autoconfigure.EnableutoConfiguration对应的配置项通过反射(Java Refletion)实例化为对应的标注了@Configuration的JavaConfig形式的IoC容器配置类,然后汇总为一个并加载到IoC容器。

SpringBoot的启动原理基本算是讲完了,为了方便记忆,我根据上面的分析画了张图
在这里插入图片描述

SpringBoot自动化配置关键组件关系图

mybatis-spring-boot-starter、spring-boot-starter-web等组件的META-INF文件下均含有spring.factories文件,自动配置模块中,SpringFactoriesLoader收集到文件中的类全名并返回一个类全名的数组,返回的类全名通过反射被实例化,就形成了具体的工厂实例,工厂实例来生成组件具体需要的bean。

可以发现其最终实现了ImportSelector(选择器)和BeanClassLoaderAware(bean类加载器中间件),重点关注一下AutoConfigurationImportSelector的selectImports方法。

public String[]selectImports(AnnotationMetadata annotationMetadata){if(!this.isEnabled(annotationMetadata)){return NO_IMPORTS;}else{
            AutoConfigurationMetadata autoConfigurationMetadata= AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
            AutoConfigurationImportSelector.AutoConfigurationEntry autoConfigurationEntry=this.getAutoConfigurationEntry(autoConfigurationMetadata, annotationMetadata);return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());}}

该方法在springboot启动流程——bean实例化前被执行,返回要实例化的类信息列表。我们知道,如果获取到类信息,spring自然可以通过类加载器将类加载到jvm中,现在我们已经通过spring-boot的starter依赖方式依赖了我们需要的组件,那么这些组建的类信息在select方法中也是可以被获取到的,不要急我们继续向下分析。

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;}

该方法中的getCandidateConfigurations方法,通过方法注释了解到,其返回一个自动配置类的类名列表,方法调用了loadFactoryNames方法,查看该方法

publicstatic List<String>loadFactoryNames(Class<?> factoryClass,@Nullable ClassLoader classLoader){
        String factoryClassName= factoryClass.getName();return(List)loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());}privatestatic Map<String, List<String>>loadSpringFactories(@Nullable ClassLoader classLoader){
        MultiValueMap<String, String> result=(MultiValueMap)cache.get(classLoader);if(result!= null){return result;}else{try{
                Enumeration<URL> urls= classLoader!= null? classLoader.getResources("META-INF/spring.factories"): ClassLoader.getSystemResources("META-INF/spring.factories");
                LinkedMultiValueMap result=newLinkedMultiValueMap();while(urls.hasMoreElements()){
                    URL url=(URL)urls.nextElement();
                    UrlResource resource=newUrlResource(url);
                    Properties properties= PropertiesLoaderUtils.loadProperties(resource);
                    Iterator var6= properties.entrySet().iterator();

在上面的代码可以看到自动配置器会根据传入的factoryClass.getName()到项目系统路径下所有的spring.factories文件中找到相应的key,从而加载里面的类。我们就选取这个mybatis-spring-boot-autoconfigure下的spring.factories文件
在这里插入图片描述
进入org.mybatis.spring.boot.autoconfigure.MybatisAutoConfiguration中,主要看一下类头:
在这里插入图片描述
发现Spring的@Configuration,俨然是一个通过注解标注的springBean,继续向下看,

@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class})这个注解的意思是:当存在SqlSessionFactory.class, SqlSessionFactoryBean.class这两个类时才解析MybatisAutoConfiguration配置类,否则不解析这一个配置类,make sence,我们需要mybatis为我们返回会话对象,就必须有会话工厂相关类。

@CondtionalOnBean(DataSource.class):只有处理已经被声明为bean的dataSource。

@ConditionalOnMissingBean(MapperFactoryBean.class)这个注解的意思是如果容器中不存在name指定的bean则创建bean注入,否则不执行(该类源码较长,篇幅限制不全粘贴)

以上配置可以保证sqlSessionFactory、sqlSessionTemplate、dataSource等mybatis所需的组件均可被自动配置,@Configuration注解已经提供了Spring的上下文环境,所以以上组件的配置方式与Spring启动时通过mybatis.xml文件进行配置起到一个效果。通过分析我们可以发现,只要一个基于SpringBoot项目的类路径下存在SqlSessionFactory.class, SqlSessionFactoryBean.class,并且容器中已经注册了dataSourceBean,就可以触发自动化配置,意思说我们只要在maven的项目中加入了mybatis所需要的若干依赖,就可以触发自动配置,但引入mybatis原生依赖的话,每集成一个功能都要去修改其自动化配置类,那就得不到开箱即用的效果了。所以Spring-boot为我们提供了统一的starter可以直接配置好相关的类,触发自动配置所需的依赖(mybatis)如下:
在这里插入图片描述
这里是截取的mybatis-spring-boot-starter的源码中pom.xml文件中所有依赖:
在这里插入图片描述

因为maven依赖的传递性,我们只要依赖starter就可以依赖到所有需要自动配置的类,实现开箱即用的功能。也体现出Springboot简化了Spring框架带来的大量XML配置以及复杂的依赖管理,让开发人员可以更加关注业务逻辑的开发。

  • 作者:IT-老牛
  • 原文链接:https://itxiongmao.blog.csdn.net/article/details/109158674
    更新时间:2022-09-07 14:48:15