面试题: SpringBoot 的自动配置原理及定制starter

Posted wushaopei

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面试题: SpringBoot 的自动配置原理及定制starter相关的知识,希望对你有一定的参考价值。

3、Spring Boot 的自动配置原理

package com.mmall;

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;在前面的SpringBoot 的自启动配置一文中,我们可以得知,在主启动类中,main方法中的 SpringApplication.run(DemoApplication.class,args);是对主启动类进行初始化并启动IOC容器进行配置bean的管理。

在这里,我们可以发现,SpringBoot 中比 Spring 要优秀的一点 自动配置 的特性并没有表现在main方法中,从代码中,我们可以看出,也许 @SpringBootApplication 这一个注解会与SpringBoot 的自动配置有关,本文将从这方面下手,根据源码进行分析。

使用过Spring Boot 的我们都知道,SpringBoot 中有一个全局配置文件: application.properties application.yml

我们在项目开发过程中对程序配置的 端口、数据库等属性都可以在这个文件中进行配置,最常见的配置有: server.port 、logging.level.* 等等,然而我们实际用到的往往只是很少的一部分,那么这些属性是否有据可依呢?答案是肯定的,这些属性都可以在官方文档中查找到:

https://docs.spring.io/spring-boot/docs/2.1.0.RELEASE/reference/htmlsingle/#common-application-properties

Appendix A. Common application properties小节下:

# ===================================================================
# COMMON SPRING BOOT PROPERTIES
#
# This sample file is provided as a guideline. Do NOT copy it in its
# entirety to your own application.               ^^^
# ===================================================================


# ----------------------------------------
# CORE PROPERTIES
# ----------------------------------------
debug=false # Enable debug logs.
trace=false # Enable trace logs.

# LOGGING
logging.config= # Location of the logging configuration file. For instance, `classpath:logback.xml` for Logback.
logging.exception-conversion-word=%wEx # Conversion word used when logging exceptions.
logging.file= # Log file name (for instance, `myapp.log`). Names can be an exact location or relative to the current directory.
logging.file.max-history=0 # Maximum of archive log files to keep. Only supported with the default logback setup.
logging.file.max-size=10MB # Maximum log file size. Only supported with the default logback setup.
logging.group.*= # Log groups to quickly change multiple loggers at the same time. For instance, `logging.level.db=org.hibernate,org.springframework.jdbc`.
logging.level.*= # Log levels severity mapping. For instance, `logging.level.org.springframework=DEBUG`.
logging.path= # Location of the log file. For instance, `/var/log`.
logging.pattern.console= # Appender pattern for output to the console. Supported only with the default Logback setup.
logging.pattern.dateformat=yyyy-MM-dd HH:mm:ss.SSS # Appender pattern for log date format. Supported only with the default Logback setup.
logging.pattern.file= # Appender pattern for output to a file. Supported only with the default Logback setup.
logging.pattern.level=%5p # Appender pattern for log level. Supported only with the default Logback setup.
logging.register-shutdown-hook=false # Register a shutdown hook for the logging system when it is initialized.

# AOP
spring.aop.auto=true # Add @EnableAspectJAutoProxy.
spring.aop.proxy-target-class=true # Whether subclass-based (CGLIB) proxies are to be created (true), as opposed to standard Java interface-based proxies (false).
技术图片

我们对自动配置大胆做个猜测,在Spring Boot 中有个机制,它将通过pom.xml 依赖加载到的诸如 mybatismysqlactivemq等的jar 包创建对应的 bean,然后将bean放到容器中,并通过在 application.properties application.yml 文件中进行配置属性参数映射到对应的 bean 中的属性上,从而实现如mysql 的数据库链接地址、密码、用户名的配置等操作。

那么,在这里我们针对猜测的自动配置过程提出三个关键性问题,分别是:

  1. SpringBoot 中怎么将引入的依赖包的类创建bean并加入到容器中的?
  2. springboot 中怎么获取到配置文件 application.properties 或application.yml 中的属性参数?
  3. springboot 怎么将获取到的配置文件参数映射到bean中的属性上?

3.1 配置加载依赖并创建bean加入到容器中:

(1)引入 jar

	        <!--mybatis 开发包-->
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>1.3.2</version>
		</dependency>
		<!--springboot web模块支持-->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>
		<!--druid 的数据源-->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>druid</artifactId>
			<version>1.0.31</version>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>
技术图片

 spring-boot-starter-web          包自动帮我们引入了web模块开发需要的相关jar包,

 mybatis-spring-boot-starter   帮我们引入了dao开发相关的jar包。

 spring-boot-starter-xxx           是官方提供的starter,xxx-spring-boot-starter是第三方提供的starter。

如下图中:

技术图片技术图片?

从以下的截图可以看出在这个mybatis-spring-boot-starter 中,并没有任何源码,只有一个pom文件,它的作用就是帮我们引入了相关jar包。

技术图片技术图片?

在这里我们看一下SpringBoot 中数据源的配置方案:

spring:
  datasource:
     driver-class-name: com.mysql.cj.jdbc.Driver
     url: jdbc:mysql://localhost:3333/aiicp-gxzx?useUnicode=true&characterEncoding=utf-8&useSSL=false&autoReconnect=true&serverTimezone=UTC
     username: root
     password: root
     type: com.alibaba.druid.pool.DruidDataSource
     dbcp2:
       min-idle: 5
       initial-size: 5
       max-total: 5
       max-wait-millis: 200
技术图片

在这里,starter机制帮我们完成了项目起步所需要的相关jar包。我们知道,传统的spring应用中需要在 application.xml 中配置很多bean的,比如 dataSource 的配置,transactionManager的配置...... springboot是如何帮我们完成这些bean的配置的呢?

自动配置

(2)基于java代码的bean的配置

以mybatis为例,引入的mybatis 包不只有一个pom.xml文件, 还有一个关键性的包:mybatis-spring-boot-autoconfigure这个包

          技术图片技术图片?

从mybatis-spring-boot-autoconfigure包下的文件名我们可以知道MybatisAutoConfiguration类是自动配置的核心文件;我们进入该文件看看,

技术图片技术图片?

从截图中我们可以看到,类名和方法名前分别添加了@Configuration 、@Bean两个注解,了解Spring的都知道,这两个注解一起使用就可以创建一个基于java代码的配置类,可以用来替代相应的xml配置文件。

@Configuration注解的类可以看作是能生产让Spring IoC容器管理的Bean实例的工厂。

@Bean注解告诉Spring,一个带有@Bean的注解方法将返回一个对象,该对象应该被注册到spring容器中。

传统的基于xmlbean配置方法如下:

<beans>  
    <bean id = "car" class="com.mmall.Car">  
        <property name="wheel" ref = "wheel"></property>  
    </bean>  
    <bean id = "wheel" class="com.mmall.Wheel"></bean>  
</beans>
技术图片

相当于用基于java代码的配置方式:

@Configuration  
public class Conf {  
    @Bean  
    public Car car() {  
        Car car = new Car();  
        car.setWheel(wheel());  
        return car;  
    }  
    @Bean   
    public Wheel wheel() {  
        return new Wheel();  
    }  
}
技术图片

由此,我们可以知道,上面的 MybatisAutoConfiguration这个类,自动帮我们生成了SqlSessionFactory 这些 Mybatis 的重要实例并叫个spring容器管理,从而完成bean的自动注册。

(3)springboot 特有的常见条件依赖注解:

@ConditionalOnBean,仅在当前上下文中存在某个bean时,才会实例化这个Bean。

@ConditionalOnClass,某个class位于类路径上,才会实例化这个Bean。

@ConditionalOnExpression,当表达式为true的时候,才会实例化这个Bean。

@ConditionalOnMissingBean,仅在当前上下文中不存在某个bean时,才会实例化这个Bean。

@ConditionalOnMissingClass,某个class在类路径上不存在的时候,才会实例化这个Bean。

@ConditionalOnNotWebApplication,不是web应用时才会实例化这个Bean。

@AutoConfigureAfter,在某个bean完成自动配置后实例化这个bean。

@AutoConfigureBefore,在某个bean完成自动配置前实例化这个bean。
技术图片

到这里,我们知道了,要想完成Mybatis的自动配置,需要在类路径中存在 SqlSessionFactory.class、SqlSessionFactoryBean.class 这两个类,同时也需要有DataSource这个bean存在且已经自动注册到容器中。

根据MybatisAutoConfiguration的名称我们可以得知,在Springboot 中,引入自动配置的DataSource的包的名称应该是 DataSourceAutoConfiguration;

通过查看该包所在目录我们知道这个包又属于spring-boot-autoconfigure-2.0.4.RELEASE.jar这个包,自动配置这个包帮们引入了jdbc、kafka、logging、mongoquartz等包。很多包需要我们引入相应jar后自动配置才生效。

技术图片技术图片?

3.2 获取到配置文件 application.properties 或application.yml 中的参数

Bean 参数的获取

在这里,我们要解决的是怎么从 .properties或yml文件中获取参数,让springboot知道配置文件中有哪些参数?

DataSourceAutoConfiguration类里面,我们注意到使用了EnableConfigurationProperties这个注解。在该注解中引入了 DataSourceProperties.class 这个字节码文件。

@Configuration
@ConditionalOnClass({DataSource.class, EmbeddedDatabaseType.class})
@EnableConfigurationProperties({DataSourceProperties.class})
@Import({DataSourcePoolMetadataProvidersConfiguration.class, DataSourceInitializationConfiguration.class})
public class DataSourceAutoConfiguration {
...
}
技术图片

点击进入查看,DataSourceProperties中封装了数据源的各个属性,且使用了注解ConfigurationProperties指定了配置文件的前缀。

@ConfigurationProperties(
    prefix = "spring.datasource"
)
public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {
    private ClassLoader classLoader;
    private String name;
    private boolean generateUniqueName;
    private Class<? extends DataSource> type;
    private String driverClassName;
    private String url;
    private String username;
    private String password;
    private String jndiName;
    ...
}
技术图片

@EnableConfigurationProperties与@ConfigurationProperties这两个注解有什么用呢?我们先看一个例子:

package com.example.demo_started;

import org.springframework.beans.factory.BeanClassLoaderAware;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;

/**
 * @ClassName PropertiesBean
 * @Description TODO
 * @Author wushaopei
 * @Date 2020/1/31 20:29
 * @Version 1.0
 */
@Component
@ConfigurationProperties(prefix="spring.datasource")
public class PropertiesBean {
    private String url;
    private String username;
    private String password;
    //省略getter、setter...


    public String getUrl() {
        return url;
    }

    public void setUrl(String url) {
        this.url = url;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    @Override
    public String toString() {
        return "PropertiesBean{" +
                "url=‘" + url + ‘‘‘ +
                ", username=‘" + username + ‘‘‘ +
                ", password=‘" + password + ‘‘‘ +
                ‘}‘;
    }
}
技术图片
package com.example.demo_started;

import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.ConfigurableApplicationContext;

/**
 * @ClassName SpringbootMybatisDemoApplication
 * @Description TODO
 * @Author wushaopei
 * @Date 2020/1/31 20:28
 * @Version 1.0
 */
@SpringBootApplication
@MapperScan("com.example")
@EnableConfigurationProperties
public class SpringbootMybatisDemoApplication {
    public static void main(String[] args) {
        //SpringApplication.run(SpringbootMybatisDemoApplication.class, args);
        ConfigurableApplicationContext context = SpringApplication.run(SpringbootMybatisDemoApplication.class, args);
        //获取yml配置转换后的bean
        System.out.println("----------------------"+context.getBean(PropertiesBean.class));
        context.close();
    }
}
技术图片

运行结果:

技术图片技术图片?

从运行结果可以看出@ConfigurationProperties与@EnableConfigurationPropertie的作用就是:

@ConfigurationProperties注解的作用是把yml或者properties配置文件转化为bean。

@EnableConfigurationProperties注解的作用是使@ConfigurationProperties注解生效。如果只配置@ConfigurationProperties注解,在spring容器中是获取不到yml或者properties配置文件转化的bean的。

通过这种方式,把yml或者properties配置参数转化为bean,这些bean又是如何被发现与加载的?

3.3 发现bean 并进行加载

到了这里,我们解决了两个问题,知道了通过pom.xml加载到程序中的jar包是怎么被Springboot进行自动配置,也知道了Springboot中是怎么读取并使用 .properties或 .yml 文件的参数的。接下来我们需要将两者进行关联,参数Bean是怎么被发现,并加载到容器中自动配置bean进行关联映射的?

Bean的发现

从前面Springboot的自启动机制中可以知道,Springboot 默认扫描启动类所在的包下的主类与子类的所有组件,但并没有包括依赖包中的类,那么依赖包中的bean是如何被发现和加载的?

package com.mmall;

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

}
技术图片

我们在前面说过,SpringBoot 的自动配置是由@SpringBootApplication这个注解起得作用,具体的实现原理我们点进去看一下:

//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by Fernflower decompiler)
//

package org.springframework.boot.autoconfigure;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.context.TypeExcludeFilter;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.FilterType;
import org.springframework.context.annotation.ComponentScan.Filter;
import org.springframework.core.annotation.AliasFor;

@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 {
 ........   
}
技术图片

在SpringBootApplication注解类中,实际上重要的只有三个Annotation

@Configuration(@SpringBootConfiguration里面还是应用了@Configuration)

@EnableAutoConfiguration

@ComponentScan

 

@Configuration的作用上面我们已经知道了,被注解的类将成为一个bean配置类。

@ComponentScan的作用就是自动扫描并加载符合条件的组件,比如@Component和@Repository等,最终将这些bean定义加载到spring容器中。

@EnableAutoConfiguration 这个注解的功能很重要,借助@Import的支持,收集和注册依赖包中相关的bean定义。

这里进入到 @EnableAutoConfiguration 注解中看看:

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    Class<?>[] exclude() default {};

    String[] excludeName() default {};
}
技术图片

如上源码,@EnableAutoConfiguration注解引入了@AutoConfigurationPackage和@Import这两个注解。@AutoConfigurationPackage的作用就是自动配置的包,@Import导入需要自动配置的组件。

 

进入@AutoConfigurationPackage,发现也是引入了@Import注解

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

点进去 @Import 引用的 Registrar.classRegistrar类去看看:

    @Order(-2147483648)
    static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports {
        Registrar() {
        }

        public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
            AutoConfigurationPackages.register(registry, (new AutoConfigurationPackages.PackageImport(metadata)).getPackageName());
        }

        public Set<Object> determineImports(AnnotationMetadata metadata) {
            return Collections.singleton(new AutoConfigurationPackages.PackageImport(metadata));
        }
    }
}
技术图片

从上述源码我们可以解决一个小困惑,如下:

new AutoConfigurationPackages.PackageImport(metadata)).getPackageName()

new AutoConfigurationPackages.PackageImport(metadata)
技术图片
AutoConfigurationPackages翻译成人话,就是 自动配置的包。PackageImport()方法代表配置的原数据
技术图片

这两句代码的作用是用来加载启动类所在的包下的主类与子类的所有组件注册到spring 容器,这就是前文所说的springboot默认扫描启动类所在的包下的主类与子类的所有组件。

这里进入AutoConfigurationPackages 包去看看,

public interface AnnotationMetadata extends ClassMetadata, AnnotatedTypeMetadata {
    Set<String> getAnnotationTypes();

    Set<String> getMetaAnnotationTypes(String var1);

    boolean hasAnnotation(String var1);

    boolean hasMetaAnnotation(String var1);

    boolean hasAnnotatedMethods(String var1);

    Set<MethodMetadata> getAnnotatedMethods(String var1);
}
技术图片

 简单介绍一下,AnnotationMetadata 有两种重要的实现方案,一种基于 Java 反射,另一种基于 ASM 框架。

两种实现方案适用于不同场景。StandardAnnotationMetadata基于 Java 反射,需要加载类文件。而 AnnotationMetadataReadingVisitor 基于 ASM 框架无需提前加载类,所以适用于 Spring 应用扫描指定范围内模式注解时使用。

下面是metadata 的相关接口和类的关系图:

  技术图片技术图片?

获取并返回包名,PackageImport

private static final class PackageImport {
        private final String packageName;

        PackageImport(AnnotationMetadata metadata) {
            this.packageName = ClassUtils.getPackageName(metadata.getClassName());
        }

        public int hashCode() {
            return this.packageName.hashCode();
        }

        public boolean equals(Object obj) {
            return obj != null && this.getClass() == obj.getClass() ? this.packageName.equals(((AutoConfigurationPackages.PackageImport)obj).packageName) : false;
        }

        public String getPackageName() {
            return this.packageName;
        }

        public String toString() {
            return "Package Import " + this.packageName;
        }
    }
技术图片

根据元数据中返回的类名获取其所在的包目录,将该包目录返回给容器,用于自定义扫描包或默认配置扫描。

 

回到原来的问题上,搜集并注册到spring 容器的那些bean来自哪里?

进入@EnableAutoConfiguration注解的EnableAutoConfigurationImportSelector 类中去看看,该类继承了AutoConfigurationImportSelector 类,进入该类进行看下:

package org.springframework.boot.autoconfigure;

import org.springframework.core.type.AnnotationMetadata;

/** @deprecated */
@Deprecated
public class EnableAutoConfigurationImportSelector extends AutoConfigurationImportSelector {
    public EnableAutoConfigurationImportSelector() {
    }

    protected boolean isEnabled(AnnotationMetadata metadata) {
        return this.getClass().equals(EnableAutoConfigurationImportSelector.class) ? ((Boolean)this.getEnvironment().getProperty("spring.boot.enableautoconfiguration", Boolean.class, true)).booleanValue() : true;
    }
}
技术图片
public class AutoConfigurationImportSelector implements DeferredImportSelector, BeanClassLoaderAware, ResourceLoaderAware, BeanFactoryAware, EnvironmentAware, Ordered {
    private static final String[] NO_IMPORTS = new String[0];
    private static final Log logger = LogFactory.getLog(AutoConfigurationImportSelector.class);
    ...............

    public AutoConfigurationImportSelector() {
    }

    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        if (!this.isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        } else {
            try {
                AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
                AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
                List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
                configurations = this.removeDuplicates(configurations);
                configurations = this.sort(configurations, autoConfigurationMetadata);
                Set<String> exclusions = this.getExclusions(annotationMetadata, attributes);
                this.checkExcludedClasses(configurations, exclusions);
                configurations.removeAll(exclusions);
                configurations = this.filter(configurations, autoConfigurationMetadata);
                this.fireAutoConfigurationImportEvents(configurations, exclusions);
                return (String[])configurations.toArray(new String[configurations.size()]);
            } catch (IOException var6) {
                throw new IllegalStateException(var6);
            }
        }
    }

..............


    protected AnnotationAttributes getAttributes(AnnotationMetadata metadata) {
        String name = this.getAnnotationClass().getName();
        AnnotationAttributes attributes = AnnotationAttributes.fromMap(metadata.getAnnotationAttributes(name, true));
        Assert.notNull(attributes, "No auto-configuration attributes found. Is " + metadata.getClassName() + " annotated with " + ClassUtils.getShortName(name) + "?");
        return attributes;
    }

    protected Class<?> getAnnotationClass() {
        return EnableAutoConfiguration.class;
    }

    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;
    }
.........
技术图片

selectImports 方法中,SpringFactoriesLoader.loadFactoryNames 方法扫描所有具有META-INF/spring.factories的jar包。spring-boot-autoconfigure-x.x.x.x.jar里就有一个这样的spring.factories文件。

注意:在启动loadFactoryNames()方法前,SpringFactoriesLoader.loadFactoryNames方法调用loadSpringFactories方法从所有的jar包中读取META-INF/spring.factories文件信息。

public static <T> List<T> loadFactories(Class<T> factoryClass, ClassLoader classLoader) {
        Assert.notNull(factoryClass, "‘factoryClass‘ must not be null");
        ClassLoader classLoaderToUse = classLoader;
        if (classLoader == null) {
            classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
        }

        List<String> factoryNames = loadFactoryNames(factoryClass, classLoaderToUse);
        if (logger.isTraceEnabled()) {
            logger.trace("Loaded [" + factoryClass.getName() + "] names: " + factoryNames);
        }

        List<T> result = new ArrayList(factoryNames.size());
        Iterator var5 = factoryNames.iterator();

        while(var5.hasNext()) {
            String factoryName = (String)var5.next();
            result.add(instantiateFactory(factoryName, factoryClass, classLoaderToUse));
        }

        AnnotationAwareOrderComparator.sort(result);
        return result;
    }
技术图片

loadFactories会将加载到的jar中的类加载并保存,以提供loadFactoryNames中进行配置添加到执行序列

public static List<String> loadFactoryNames(Class<?> factoryClass, ClassLoader classLoader) {
        String factoryClassName = factoryClass.getName();

        try {
            Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
            ArrayList result = new ArrayList();

            while(urls.hasMoreElements()) {
                URL url = (URL)urls.nextElement();
                Properties properties = PropertiesLoaderUtils.loadProperties(new UrlResource(url));
                String factoryClassNames = properties.getProperty(factoryClassName);
                result.addAll(Arrays.asList(StringUtils.commaDelimitedListToStringArray(factoryClassNames)));
            }

            return result;
        } catch (IOException var8) {
            throw new IllegalArgumentException("Unable to load [" + factoryClass.getName() + "] factories from location [" + "META-INF/spring.factories" + "]", var8);
        }
    }
技术图片

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

技术图片技术图片?

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

其中有一个keyorg.springframework.boot.autoconfigure.EnableAutoConfiguration的值定义了需要自动配置的bean,通过读取这个配置获取一组@Configuration类。

注意:每个xxxAutoConfiguration都是一个基于javabean配置类。实际上,这些xxxAutoConfiguratio不是所有都会被加载,会根据xxxAutoConfiguration上的@ConditionalOnClass等条件判断是否加载。

private static <T> T instantiateFactory(String instanceClassName, Class<T> factoryClass, ClassLoader classLoader) {
        try {
            Class<?> instanceClass = ClassUtils.forName(instanceClassName, classLoader);
            if (!factoryClass.isAssignableFrom(instanceClass)) {
                throw new IllegalArgumentException("Class [" + instanceClassName + "] is not assignable to [" + factoryClass.getName() + "]");
            } else {
                Constructor<?> constructor = instanceClass.getDeclaredConstructor();
                ReflectionUtils.makeAccessible(constructor);
                return constructor.newInstance();
            }
        } catch (Throwable var5) {
            throw new IllegalArgumentException("Unable to instantiate factory class: " + factoryClass.getName(), var5);
        }
    }
技术图片

 如上代码段,通过反射机制将spring.factories@Configuration类实例化为对应的java实列。到此我们已经知道怎么发现要自动配置的bean了,最后一步就是怎么样将这些bean加载到spring容器。

 

bean的加载

如果要让一个普通类交给Spring容器管理,通常有以下方法:

  1. 使用 @Configuration与@Bean 注解
  2. 使用@Controller @Service @Repository @Component 注解标注该类,然后启用@ComponentScan自动扫描
  3. 使用@Import 方法

springboot中使用了@Import 方法

@EnableAutoConfiguration注解中使用了@Import({AutoConfigurationImportSelector.class})注解,AutoConfigurationImportSelector实现了DeferredImportSelector接口,

DeferredImportSelector接口继承了ImportSelector接口,ImportSelector接口只有一个selectImports方法。

public class AutoConfigurationImportSelector implements DeferredImportSelector{
...
    public String[] selectImports(AnnotationMetadata annotationMetadata) {
        if(!this.isEnabled(annotationMetadata)) {
            return NO_IMPORTS;
        } else {
            AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader);
            AnnotationAttributes attributes = this.getAttributes(annotationMetadata);
            List configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
            configurations = this.removeDuplicates(configurations);
            Set exclusions = this.getExclusions(annotationMetadata, attributes);
            this.checkExcludedClasses(configurations, exclusions);
            configurations.removeAll(exclusions);
            configurations = this.filter(configurations, autoConfigurationMetadata);
            this.fireAutoConfigurationImportEvents(configurations, exclusions);
            return StringUtils.toStringArray(configurations);
        }
}
...
}
技术图片
public interface DeferredImportSelector extends ImportSelector {
    @Nullable
    default Class<? extends DeferredImportSelector.Group> getImportGroup() {
        return null;
}
public interface Group {...}
}
技术图片
public interface ImportSelector {
    String[] selectImports(AnnotationMetadata var1);
}
技术图片

 我们先通过一个简单例子看看@Import注解是如何将bean导入到spring容器的。

1、新建一个bean

public class User {

    private Long id;
    private String name;
    private String password;
    private String phone;
...............
}
技术图片

2、创建一个UserSelecter类继承ImportSelector接口并实现selectImports方法

public class UserSelecter implements ImportSelector {
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        return new String[]{"com.mmall.demo.model.User"};
    }

}
技术图片

3、创建ImportConfig类,使用@Configuration、@Import(ItpscSelector.class)注解。

@Configuration
@Import(UserSelecter.class)
public class ImportConfig {
}
技术图片

4、从容器获取bean

	@Test
	public void testSelectImport() {
		ApplicationContext ctx = new AnnotationConfigApplicationContext(ImportConfig.class);
		String[] beanDefinitionNames = ctx.getBeanDefinitionNames();
		for (String name : beanDefinitionNames) {
			System.out.println(name);
		}
	}
技术图片

运行结果:

2020-02-01 12:31:59.283  INFO 17128 --- [           main] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@6240651f: startup date [Sat Feb 01 12:31:59 CST 2020]; root of context hierarchy
org.springframework.context.annotation.internalConfigurationAnnotationProcessor
org.springframework.context.annotation.internalAutowiredAnnotationProcessor
org.springframework.context.annotation.internalRequiredAnnotationProcessor
org.springframework.context.annotation.internalCommonAnnotationProcessor
org.springframework.context.event.internalEventListenerProcessor
org.springframework.context.event.internalEventListenerFactory
importConfig
com.mmall.demo.model.User
2020-02-01 12:31:59.309  INFO 17128 --- [       Thread-2] o.s.w.c.s.GenericWebApplicationContext   : Closing org.springframework.web.context.support.GenericWebApplicationContext@53ce1329: startup date [Sat Feb 01 12:31:55 CST 2020]; root of context hierarchy
技术图片

从结果可以看出:selectImports方法返回一组bean@EnableAutoConfiguration注解借助@Import注解将这组bean注入到spring容器中,springboot正式通过这种机制来完成bean的注入的。

总结:

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

技术图片技术图片?

图片来自于王福强老师的博客:https://afoo.me/posts/2015-07-09-how-spring-boot-works.html 

4、根据自动配置原理自定义一个 个性的 started ?

以上是关于面试题: SpringBoot 的自动配置原理及定制starter的主要内容,如果未能解决你的问题,请参考以下文章

面试高频题:springBoot自动装配的原理你能说出来吗?

笑死,面试官又问我SpringBoot自动配置原理

2022最新超详细Spring全家桶面试题(待更新ing)

面试题打卡——第十天

面试题: SpringBoot 的自启动原理

SpringBoot面试杀手锏——自动配置原理