SpringBoot自動配置和自定義starter

1. SpringBoot做了什麼?

可以看到我們利用SpringBoot搭建好工程,開始專注業務開發,省去了所有的配置、依賴管理,非常簡單!

那麼這些省去的配置、依賴SpringBoot是什麼時候幫我們完成的呢?
在這裏插入圖片描述

1.1 依賴管理

首先來看SpringBoot如何幫我們管理依賴及其版本信息。

打開項目的pom.xml文件,可以看到我們的項目依賴比較簡單:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.6.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>cn.itcast</groupId>
    <artifactId>bank</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>bank</name>
    <description>Demo project for Spring Boot</description>

    <properties>
        <java.version>1.8</java.version>
        <skipTests>true</skipTests>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

這裏包含了4部分內容,分別是:

  • 父工程配置
  • Properties配置
  • 依賴項
  • 插件

1)父工程配置

這個項目有一個父工程配置:

在這裏插入圖片描述

可以看到使用的SpringBoot版本是最新的2.1.6RELEASE 版本。跟入這個pom查看,發現又繼承了另一個父工程:

在這裏插入圖片描述

繼續跟入,發現裏面已經管理了各種依賴及其版本了,列舉一部分大家看:

在這裏插入圖片描述

因此,我們的項目需要引入依賴時,只需要指定座標,版本都有SpringBoot管理,避免了依賴間的衝突。

2)Properties:

properties中主要定義了項目的JDK版本:

在這裏插入圖片描述

3)依賴項:

這裏我們總共引入了3個依賴項:

在這裏插入圖片描述

但是,看看項目中實際引入了哪些jar包:

在這裏插入圖片描述

當我們移除兩個spring-boot-starter命名的依賴項時,可以發現所有的jar都消失了:

在這裏插入圖片描述

也就是說,在下面的這個依賴項(starter)中:

<!--web工程起步依賴-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

定義好了一個web工程所需的所有依賴,當我們引入依賴時,無需像以前那樣一個一個尋找需要的依賴座標並且驗證版本是否衝突,只需要引入這個starter即可!

4)插件:

最後是一個插件:

<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

這個是SpringBoot工程在打包時用到的插件。

5)總結:

因爲SpringBoot對於依賴的管理,我們搭建項目時只需要引入starter,無需再依賴引入上話費更多時間,大大提高了開發效率。

1.2 xml配置去哪兒了?

在以前,我們搭建一個web工程,至少需要下列配置文件:

  • tomcat配置:web.xml(包括spring監聽器、SpringMVC的DispatcherServlet、各種過濾器)
  • SpringMVC配置:springmvc.xml(包括註解驅動、視圖解析器等)
  • Spring配置:applicationContext.xml(包括數據源、Bean掃描、事務等)
  • mybatis配置:mybatis-config.xml全局配置,mybatis與spring整合配置、mapper映射文件配置

完成所有配置需要花費大量時間,每天都淹沒在xml的海洋中,整個人生都陷入了被xml支配的恐懼、黑暗當中。

直到有一天,SpringBoot如同一頁方舟,將你從xml海洋中拯救出來。

在SpringBoot中,就用Java代碼配置代替了以前繁雜的xml配置,而且這些配置都已經放到了SpringBoot提供的jar包中,因此我們引入starter依賴的那一刻,這些配置都已經生效了!這就是springBoot的自動配置功能。

SpringBoot的自動配置原理

1.3 SpringBoot自動配置初探

回到開始的問題,SpringBoot取消了xml,並且完成了框架的自動配置,那麼是如何實現的?

其實在SpringBoot的中,已經提前利用Java配置的方式,爲Spring平臺及第三方庫做好了默認配置,並打包到了依賴中。當我們引入SpringBoot提供的starter依賴時,就獲得了這些默認配置,因此我們就無需配置,開箱即用了!

默認配置的位置如圖:
在這裏插入圖片描述
其中就包括了SpringMVC的配置:
在這裏插入圖片描述
並且在這些類中,定義了大量的Bean,包括以前我們自己需要配置的如:ViewResolver、HandlerMapping、HandlerAdapter等。

在這裏插入圖片描述

因此,我們就無需配置了。

至於,這些配置是如何被加載的?怎樣生效的,我們進行源碼分析。

2.源碼分析

接下來我們來看看SpringBoot的源碼,分析下SpringBoot的自動配置原理。

2.1.啓動類

整個項目 的入口是帶有main函數的啓動類:BankApplication

package cn.itcast.bank;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class BankApplication {

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

這裏跟SpringBoot有關聯的部分有兩個,一個是SpringApplication.run(BankApplication.class, args);,另一個就是啓動類上的註解:@SpringBootApplication

我們分別來跟蹤這兩部分源碼。

2.2.SpringApplication類的初始化和run

看下整個類的介紹:

在這裏插入圖片描述

可以看出,核心作用就是從主函數加載一個Spring的應用

其中啓動應用的是幾個run方法:

/**
	 * Static helper that can be used to run a {@link SpringApplication} from the
	 * specified source using default settings.
	 * @param primarySource the primary source to load
	 * @param args the application arguments (usually passed from a Java main method)
	 * @return the running {@link ApplicationContext}
	 */
public static ConfigurableApplicationContext run(Class<?> primarySource, String... args) {
    return run(new Class<?>[] { primarySource }, args);
}

/**
	 * Static helper that can be used to run a {@link SpringApplication} from the
	 * specified sources using default settings and user supplied arguments.
	 * @param primarySources the primary sources to load
	 * @param args the application arguments (usually passed from a Java main method)
	 * @return the running {@link ApplicationContext}
	 */
public static ConfigurableApplicationContext run(Class<?>[] primarySources, String[] args) {
    return new SpringApplication(primarySources).run(args);
}

最終走的是第二個run方法。而這個方法做兩件事情:

  • new SpringApplication(primarySources):創建本類實例
  • run(args):運行Spring應用

2.2.1.構造函數

我把跟構造函數有關的幾個變量和方法提取出來,方便查看:

// SpringApplication.java

/**
 * 資源加載器
 */
private ResourceLoader resourceLoader;
/**
 * SpringBoot核心配置類的集合,這裏只有一個元素,是我們傳入的主函數
 */
private Set<Class<?>> primarySources;
/**
 * 應用類型
 */
private WebApplicationType webApplicationType;

/**
 * ApplicationContextInitializer 數組
 */
private List<ApplicationContextInitializer<?>> initializers;
/**
 * ApplicationListener 數組
 */
private List<ApplicationListener<?>> listeners;

public SpringApplication(Class<?>... primarySources) {
    this(null, primarySources);
}

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    this.resourceLoader = resourceLoader;
    Assert.notNull(primarySources, "PrimarySources must not be null");
    // 將傳入的啓動類裝入集合
    this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
    // 判斷當前項目的類型,可以是SERVLET、REACTIVE、NONE
    this.webApplicationType = WebApplicationType.deduceFromClasspath();
    // 初始化 initializers 數組
    setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
    // 初始化 listeners 數組
    setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
    this.mainApplicationClass = deduceMainApplicationClass();
}

註解:

  • ResourceLoader resourceLoader:Spring中用來加載資源(配置文件)的加載器
  • Class<?>... primarySources:這裏是Config配置類,本例中就是BankApplication
  • WebApplicationType.deduceFromClasspath():判斷當前項目的類型,可以是SERVLET、REACTIVE、NONE,根據當前classpath中包含的class來判斷,會影響後續創建的ApplicationContext的類型
  • getSpringFactoriesInstances(ApplicationContextInitializer.class)
    獲取ApplicationContextInitializer類型的實現類對象數組
  • getSpringFactoriesInstances(ApplicationListener.class):獲取ApplicationListener類型的實現類對象數組
  • deduceMainApplicationClass():沒有實際用途,打印日誌,輸出當前啓動類名稱

2.2.1.1.deduceFromClasspath()

判斷項目類型:

static WebApplicationType deduceFromClasspath() {
    if (ClassUtils.isPresent(WEBFLUX_INDICATOR_CLASS, null) && !ClassUtils.isPresent(WEBMVC_INDICATOR_CLASS, null)
        && !ClassUtils.isPresent(JERSEY_INDICATOR_CLASS, null)) {
        return WebApplicationType.REACTIVE;
    }
    for (String className : SERVLET_INDICATOR_CLASSES) {
        if (!ClassUtils.isPresent(className, null)) {
            return WebApplicationType.NONE;
        }
    }
    return WebApplicationType.SERVLET;
}

可以看到判斷結果包含3種:

  • REACTIVE:要求classpath中包含org.springframework.web.reactive.DispatcherHandler這個是WebFlux中的核心處理器,我們並沒有。
  • SERVLET:要求classpath中包含org.springframework.web.servlet.DispatcherServlet,這是SpringMVC的核心控制器,在classpath中肯定可以找到(web項目)
  • NONE:以上都不滿足,就是NONE

2.2.1.2.getSpringFactoriesInstances

getSpringFactoriesInstances(Class<T> type) 方法,獲得指定類型的實現類對象的實例。

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type) {
    return getSpringFactoriesInstances(type, new Class<?>[] {});
}

private <T> Collection<T> getSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, Object... args) {
    ClassLoader classLoader = getClassLoader();
    // Use names and ensure unique to protect against duplicates
    // 1.先加載指定類型的實現類的名稱集合
    Set<String> names = new LinkedHashSet<>(SpringFactoriesLoader.loadFactoryNames(type, classLoader));
    // 2.根據類的名稱,創建對應實例
    List<T> instances = createSpringFactoriesInstances(type, parameterTypes, classLoader, args, names);
    // 3.排序
    AnnotationAwareOrderComparator.sort(instances);
    return instances;
}

這裏關鍵是第1步中,調用SpringFactoriesLoader.loadFactoryNames(type, classLoader)方法,跟入進去

2.2.1.3.loadFactoryNames方法

SpringFactoriesLoader.loadFactoryNames(type, classLoader)方法:

// SpringFactoriesLoader
/**
  * 使用指定的類加載器,加載{@value #FACTORIES_RESOURCE_LOCATION}中記錄的,指定factoryClass
  * 類型的實現類的全路徑名。
  * @param factoryClass 需要加載的接口或抽象類
  * @param classLoader 用來加載資源的類加載器
  */
public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
    // 獲取接口名稱
    String factoryClassName = factoryClass.getName();
    // 從loadSpringFactories(classLoader)方法返回的Map中根據factoryClass名稱獲取對應的實現類名稱數組
    return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}

private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
    // 嘗試從緩存中獲取結果
    MultiValueMap<String, String> result = cache.get(classLoader);
    if (result != null) {
        return result;
    }

    try {
        // 從默認路徑加載資源文件,地址是:"META-INF/spring.factories"
        Enumeration<URL> urls = (classLoader != null ?
                                 classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
                                 ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
        // 創建空map
        result = new LinkedMultiValueMap<>();
        // 遍歷資源路徑
        while (urls.hasMoreElements()) {
            // 獲取某個路徑
            URL url = urls.nextElement();
            UrlResource resource = new UrlResource(url);
            // 加載文件內容,文件中是properties格式,key是接口名,value是實現類的名稱以,隔開
            Properties properties = PropertiesLoaderUtils.loadProperties(resource);
            for (Map.Entry<?, ?> entry : properties.entrySet()) {
                // 獲取key的 名稱
                String factoryClassName = ((String) entry.getKey()).trim();
                // 將實現類字符串變成數組並遍歷,然後添加到結果result中
                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);
    }
}

此處,加載的資源是/META-INF/spring.factories文件中的properties:

在這裏插入圖片描述

內容類似這樣:

# PropertySource Loaders
org.springframework.boot.env.PropertySourceLoader=\
org.springframework.boot.env.PropertiesPropertySourceLoader,\
org.springframework.boot.env.YamlPropertySourceLoader

# Run Listeners
org.springframework.boot.SpringApplicationRunListener=\
org.springframework.boot.context.event.EventPublishingRunListener

# Error Reporters
org.springframework.boot.SpringBootExceptionReporter=\
org.springframework.boot.diagnostics.FailureAnalyzers

# Application Context Initializers
org.springframework.context.ApplicationContextInitializer=\
org.springframework.boot.context.ConfigurationWarningsApplicationContextInitializer,\
org.springframework.boot.context.ContextIdApplicationContextInitializer,\
org.springframework.boot.context.config.DelegatingApplicationContextInitializer,\
org.springframework.boot.web.context.ServerPortInfoApplicationContextInitializer

# Application Listeners
org.springframework.context.ApplicationListener=\
org.springframework.boot.ClearCachesApplicationListener,\
org.springframework.boot.builder.ParentContextCloserApplicationListener,\
org.springframework.boot.context.FileEncodingApplicationListener,\
org.springframework.boot.context.config.AnsiOutputApplicationListener,\
org.springframework.boot.context.config.ConfigFileApplicationListener,\
org.springframework.boot.context.config.DelegatingApplicationListener,\
org.springframework.boot.context.logging.ClasspathLoggingApplicationListener,\
org.springframework.boot.context.logging.LoggingApplicationListener,\
org.springframework.boot.liquibase.LiquibaseServiceLocatorApplicationListener

根據傳入的接口名稱,就可以尋找到對應的實現類,例如org.springframework.boot.SpringApplicationRunListener.

結束後,把得到的名字集合傳遞給createSpringFactoriesInstance方法

2.2.1.4.createSpringFactoriesInstances

然後看看#createSpringFactoriesInstances(Class<T> type, Class<?>[] parameterTypes, ClassLoader classLoader, Object[] args, Set<String> names) 方法,創建對象的代碼:

/**
 * 根據類的全名稱路徑數組,創建對應的對象的數組
 *
 * @param type 父類類型
 * @param parameterTypes 構造方法的參數類型
 * @param classLoader 類加載器
 * @param args 構造方法參數
 * @param names 類全名稱的數組
 */
private <T> List<T> createSpringFactoriesInstances(Class<T> type,
		Class<?>[] parameterTypes, ClassLoader classLoader, Object[] args,
		Set<String> names) {
    // 定義空實例集合
	List<T> instances = new ArrayList<>(names.size()); 
	// 遍歷 names 數組
	for (String name : names) {
		try {
			// 獲得類名稱 name
			Class<?> instanceClass = ClassUtils.forName(name, classLoader);
			// 判斷類是否實現自 type 類
			Assert.isAssignable(type, instanceClass);
			// 獲得構造方法
			Constructor<?> constructor = instanceClass.getDeclaredConstructor(parameterTypes);
			// 創建對象
			T instance = (T) BeanUtils.instantiateClass(constructor, args);
			instances.add(instance);
		} catch (Throwable ex) {
			throw new IllegalArgumentException("Cannot instantiate " + type + " : " + name, ex);
		}
	}
	return instances;
}

2.2.2.run方法

在完成SpringApplication對象初始化後,會調用其中的run方法,一起看下:

  1. 啓動計時器,計算啓動時間。
  2. 加載配置屬性和監聽器
  3. 開始創建ApplicationContext(根據前面的WebApplicationType進行創建容器)
  4. 準備、初始化容器prepareContext(掃描bean,完成bean的加載),同時會調用剛纔說到的Initializers的初始化方法,通過反射創建類。
  5. 加載bean和resource(資源)。
public ConfigurableApplicationContext run(String... args) {
    // 1.計時器,記錄springBoot啓動耗時
    StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    ConfigurableApplicationContext context = null;
    Collection<SpringBootExceptionReporter> exceptionReporters = new ArrayList<>();
    // 2.配置headLess屬性,這個跟AWT有關,忽略即可
    configureHeadlessProperty();
    // 3.獲取SpringApplicationRunListener實例數組,默認獲取的是EventPublishRunListener
    SpringApplicationRunListeners listeners = getRunListeners(args);
    // 啓動監聽
    listeners.starting();
    try {
        // 4.創建ApplicationArguments對象
        ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
        //5.加載屬性配置。所有的environment的屬性都會加載進來,包括 application.properties 和外部的屬性配置
        ConfigurableEnvironment environment = prepareEnvironment(listeners, applicationArguments);
        configureIgnoreBeanInfo(environment);
        // 6.打印Banner
        Banner printedBanner = printBanner(environment);
        // 7.根據WebApplicationType,創建不同的ApplicationContext
        context = createApplicationContext();
        // 8.獲取異常報告器
        exceptionReporters = getSpringFactoriesInstances(SpringBootExceptionReporter.class,
                                                         new Class[] { ConfigurableApplicationContext.class }, context);
        // 9.調用各種初始化器的initialize方法,初始化容器
        prepareContext(context, environment, listeners, applicationArguments, printedBanner);
        refreshContext(context);
        // 10.執行初始化的後置邏輯,默認爲空
        afterRefresh(context, applicationArguments);
        // 停止計時器
        stopWatch.stop();
        // 11.打印 Spring Boot 啓動的時長日誌
        if (this.logStartupInfo) {
            new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
        }
        // 12.通知監聽器,SpringBoot啓動完成
        listeners.started(context);
        // 13.調用 ApplicationRunner的運行方法
        callRunners(context, applicationArguments);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, listeners);
        throw new IllegalStateException(ex);
    }

    try {
        // 通知監聽器,SpringBoot正在運行
        listeners.running(context);
    }
    catch (Throwable ex) {
        handleRunFailure(context, ex, exceptionReporters, null);
        throw new IllegalStateException(ex);
    }
    return context;
}

2.3.@SpringBootApplication註解

點擊進入,查看源碼:

/**
 * Indicates a {@link Configuration configuration} class that declares one or more
 * {@link Bean @Bean} methods and also triggers {@link EnableAutoConfiguration
 * auto-configuration} and {@link ComponentScan component scanning}. This is a convenience
 * annotation that is equivalent to declaring {@code @Configuration},
 * {@code @EnableAutoConfiguration} and {@code @ComponentScan}.
 *
 * @author Phillip Webb
 * @author Stephane Nicoll
 * @since 1.2.0
 */
@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 {

	/**
	 * Exclude specific auto-configuration classes such that they will never be applied.
	 * @return the classes to exclude
	 */
	@AliasFor(annotation = EnableAutoConfiguration.class)
	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
	 */
	@AliasFor(annotation = EnableAutoConfiguration.class)
	String[] excludeName() default {};

	/**
	 * Base packages to scan for annotated components. Use {@link #scanBasePackageClasses}
	 * for a type-safe alternative to String-based package names.
	 * @return base packages to scan
	 * @since 1.3.0
	 */
	@AliasFor(annotation = ComponentScan.class, attribute = "basePackages")
	String[] scanBasePackages() default {};

	/**
	 * Type-safe alternative to {@link #scanBasePackages} for specifying the packages to
	 * scan for annotated components. The package of each class specified will be scanned.
	 * <p>
	 * Consider creating a special no-op marker class or interface in each package that
	 * serves no purpose other than being referenced by this attribute.
	 * @return base packages to scan
	 * @since 1.3.0
	 */
	@AliasFor(annotation = ComponentScan.class, attribute = "basePackageClasses")
	Class<?>[] scanBasePackageClasses() default {};

}

這裏重點的註解有3個:

  • @SpringBootConfiguration
  • @EnableAutoConfiguration
  • @ComponentScan

逐個來看。

2.3.1.@SpringBootConfiguration

org.springframework.boot.@SpringBootConfiguration 註解,標記這是一個 Spring Boot 配置類。代碼如下:

/**
 * Indicates that a class provides Spring Boot application
 * {@link Configuration @Configuration}. Can be used as an alternative to the Spring's
 * standard {@code @Configuration} annotation so that configuration can be found
 * automatically (for example in tests).
 * <p>
 * Application should only ever include <em>one</em> {@code @SpringBootConfiguration} and
 * most idiomatic Spring Boot applications will inherit it from
 * {@code @SpringBootApplication}.
 *
 * @author Phillip Webb
 * @since 1.4.0
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
public @interface SpringBootConfiguration {

}
  • 可以看到,它上面繼承自 @Configuration 註解,所以兩者功能也一致,可以將當前類內聲明的一個或多個以 @Bean 註解標記的方法的實例納入到 Srping 容器中,並且實例名就是方法名。

2.3.2.@ComponentScan

註解的類所在的包開始,掃描包及子包

我們跟進源碼:

在這裏插入圖片描述

並沒有看到什麼特殊的地方。我們查看註釋:
在這裏插入圖片描述
大概的意思:

配置組件掃描的指令。提供了類似與<context:component-scan>標籤的作用

通過basePackageClasses或者basePackages屬性來指定要掃描的包。如果沒有指定這些屬性,那麼將從聲明這個註解的類所在的包開始,掃描包及子包

而我們的@SpringBootApplication註解聲明的類就是main函數所在的啓動類,因此掃描的包是該類所在包及其子包。因此,一般啓動類會放在一個比較前的包目錄中

在這裏插入圖片描述

2.4.@EnableAutoConfiguration(非常重要)

2.4.1.官方介紹

關於這個註解,官網上有一段說明:

The second class-level annotation is @EnableAutoConfiguration. This annotation
tells Spring Boot to “guess” how you want to configure Spring, based on the jar
dependencies that you have added. Since spring-boot-starter-web added Tomcat
and Spring MVC, the auto-configuration assumes that you are developing a web
application and sets up Spring accordingly.

簡單翻譯以下:

第二級的註解@EnableAutoConfiguration,告訴SpringBoot基於你所添加的依賴,去“猜測”你想要如何配置Spring。比如我們引入了spring-boot-starter-web,而這個啓動器中幫我們添加了tomcatSpringMVC的依賴。此時自動配置就知道你是要開發一個web應用,所以就幫你完成了web及SpringMVC的默認配置了!

總結,SpringBoot內部對大量的第三方庫或Spring內部庫進行了默認配置,這些配置是否生效,取決於我們是否引入了對應庫所需的依賴,如果有那麼默認配置就會生效。

那麼,這裏SpringBoot是如何進行判斷和猜測的呢?

我們來看源碼:

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.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 {};

}

其中,核心的就是頂部註解:

@Import(AutoConfigurationImportSelector.class)

繼續來看這兩個類

2.4.2.@Import

看源碼:

/**
 * Indicates one or more {@link Configuration @Configuration} classes to import.
 *
 * <p>Provides functionality equivalent to the {@code <import/>} element in Spring XML.
 * Allows for importing {@code @Configuration} classes, {@link ImportSelector} and
 * {@link ImportBeanDefinitionRegistrar} implementations, as well as regular component
 * classes (as of 4.2; analogous to {@link AnnotationConfigApplicationContext#register}).
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Import {

	/**
	 * {@link Configuration}, {@link ImportSelector}, {@link ImportBeanDefinitionRegistrar}
	 * or regular component classes to import.
	 */
	Class<?>[] value();

}

@Import註解的作用就是把一個或多個類導入到Spring容器中。不過,導入的方式多種多樣:

  • 可以直接通過類名導入:@Import(User.class)就是把User這個類導入
  • 可以通過ImportSelector來導入。而ImportSelector是一個接口,用來獲取需要導入的類的名稱數組,

2.4.3.ImportSelect

public interface ImportSelector {

	/**
	 * Select and return the names of which class(es) should be imported based on
	 * the {@link AnnotationMetadata} of the importing @{@link Configuration} class.
	 */
	String[] selectImports(AnnotationMetadata importingClassMetadata);

}

基於AnnotationMetadata來導入多個@Configuration類型的類的名稱數組。

而在@EnableAutoConfiguration中通過@Import(AutoConfigurationImportSelector)其中的參數:AutoConfigurationImportSelector正是這個接口的一個子類。

如圖:

在這裏插入圖片描述

其中DeferredImportSerlector有自己的特殊功能。

2.4.4.DeferredImportSerlector

部分源碼如下:

/**
 * A variation of {@link ImportSelector} that runs after all {@code @Configuration} beans
 * have been processed. This type of selector can be particularly useful when the selected
 * imports are {@code @Conditional}.
 *
 * <p>Implementations can also extend the {@link org.springframework.core.Ordered}
 * interface or use the {@link org.springframework.core.annotation.Order} annotation to
 * indicate a precedence against other {@link DeferredImportSelector DeferredImportSelectors}.
 *
 * <p>Implementations may also provide an {@link #getImportGroup() import group} which
 * can provide additional sorting and filtering logic across different selectors.
 *
 * @author Phillip Webb
 * @author Stephane Nicoll
 * @since 4.0
 */
public interface DeferredImportSelector extends ImportSelector {

	/**
	 * Return a specific import group.
	 * <p>The default implementations return {@code null} for no grouping required.
	 * @return the import group class, or {@code null} if none
	 * @since 5.0
	 */
	@Nullable
	default Class<? extends Group> getImportGroup() {
		return null;
	}
    
    // 其它代碼略
}

其作用也是導入一組配置類,但是當你需要對導入的配置類進行過濾時使用。

其中的Class<? extends Group> getImportGroup()方法,就是用來獲取需要導入的配置組,而其返回值Group,而後續就會利用Group內部方法去獲取需要加載的配置類。

2.4.5.DeferredImportSelector.Group

Group是DeferredImportSelector中的一個內部類。用於選擇和處理要導入的配置類。

源碼如下:

// DeferredImportSelector
/**
	 * Interface used to group results from different import selectors.
	 */
interface Group {

    /**
		 * Process the {@link AnnotationMetadata} of the importing @{@link Configuration}
		 * class using the specified {@link DeferredImportSelector}.
		 */
    void process(AnnotationMetadata metadata, DeferredImportSelector selector);

    /**
		 * Return the {@link Entry entries} of which class(es) should be imported
		 * for this group.
		 */
    Iterable<Entry> selectImports();

    // 略。。。
}

回頭看看@EnableAutoConfiguration中的註解:

在這裏插入圖片描述

其中的@Import導入的AutoConfigurationImportSelector一定是ImportSelector的實現類。

2.5.AutoConfigurationImportSelector

AutoConfigurationImportSelectorDeferredImportSelector的一個實現類,因此實現了Class<? extends Group> getImportGroup()這個方法:

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

返回的是AutoConfigurationImportSelector的內部類AutoConfigurationGroup

2.5.1.AutoConfigurationGroup

其中包含一些成員變量:

private static class AutoConfigurationGroup
			implements DeferredImportSelector.Group, BeanClassLoaderAware, BeanFactoryAware, ResourceLoaderAware {

    // key:自動配置類的名稱,AnnotationMetadata:導入配置類的元信息
    private final Map<String, AnnotationMetadata> entries = new LinkedHashMap<>();
    // 自動配置類的數組
    private final List<AutoConfigurationEntry> autoConfigurationEntries = new ArrayList<>();

    private ClassLoader beanClassLoader;
    private BeanFactory beanFactory;
    private ResourceLoader resourceLoader;

    private AutoConfigurationMetadata autoConfigurationMetadata;
    
    // ...略
}
  • Map<String, AnnotationMetadata> entries屬性: key:自動配置類的名稱,AnnotationMetadata:導入配置類的元信息

  • private final List<AutoConfigurationEntry> autoConfigurationEntries: AutoConfigurationEntry的數組

    • AutoConfigurationEntry源碼:

    • protected static class AutoConfigurationEntry {
      		// 自動配置類名稱數組
      		private final List<String> configurations;
      		// 需要排除的配置
      		private final Set<String> exclusions;
      }
      

包含2個重要的方法:

//AutoConfigurationGroup
// 加載要導入的配置類,成員變量賦值
@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(getAutoConfigurationMetadata(), annotationMetadata);
    // 放入list
    this.autoConfigurationEntries.add(autoConfigurationEntry);
    // 放入entries
    for (String importClassName : autoConfigurationEntry.getConfigurations()) {
        this.entries.putIfAbsent(importClassName, annotationMetadata);
    }
}
// 對這些配置類過濾,篩選並返回
@Override
public Iterable<Entry> selectImports() {
    if (this.autoConfigurationEntries.isEmpty()) {
        return Collections.emptyList();
    }
    // 獲取需要排除的配置類名稱
    Set<String> allExclusions = this.autoConfigurationEntries.stream()
        .map(AutoConfigurationEntry::getExclusions).flatMap(Collection::stream).collect(Collectors.toSet());
    // 獲取處理過的配置類名稱
    Set<String> processedConfigurations = this.autoConfigurationEntries.stream()
        .map(AutoConfigurationEntry::getConfigurations).flatMap(Collection::stream)
        .collect(Collectors.toCollection(LinkedHashSet::new));
    // 移除需要排除的配置類
    processedConfigurations.removeAll(allExclusions);
	// 對配置類排序,並重新封裝爲Entry返回
    return sortAutoConfigurations(processedConfigurations, getAutoConfigurationMetadata()).stream()
        .map((importClassName) -> new Entry(this.entries.get(importClassName), importClassName))
        .collect(Collectors.toList());
}
  • AutoConfigurationGroup#process(...) 方法:用於加載自動配置,並給上述兩個變量賦值
  • #selectImports()方法:從entries中篩選要引入的配置類。

其中的加載配置的代碼是這行:

AutoConfigurationEntry autoConfigurationEntry = ((AutoConfigurationImportSelector) deferredImportSelector)
        .getAutoConfigurationEntry(getAutoConfigurationMetadata(), annotationMetadata);

跟入:

// AutoConfigurationImportSelector.java

protected AutoConfigurationEntry getAutoConfigurationEntry(AutoConfigurationMetadata autoConfigurationMetadata, AnnotationMetadata annotationMetadata) {
    // 1 判斷是否開啓。如未開啓,返回空數組。
    if (!isEnabled(annotationMetadata)) {
        return EMPTY_ENTRY;
    }
    // 2 獲得註解的屬性(主要獲取exclude排除的自動配置項)
    AnnotationAttributes attributes = getAttributes(annotationMetadata);
    // 3 從 /META-INF/spring.factories讀取@EnableAutoConfiguration相關的配置類的名稱數組
    List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
    // 3.1 移除重複的配置類
    configurations = removeDuplicates(configurations);
    // 4 獲得需要排除的配置類
    Set<String> exclusions = getExclusions(annotationMetadata, attributes);
    // 4.1 校驗排除的配置類是否合法,沒有則跳過檢查
    checkExcludedClasses(configurations, exclusions);
    // 4.2 從 configurations 中,移除需要排除的配置類
    configurations.removeAll(exclusions);
    // 5 根據條件(@ConditionalOn註解),過濾掉不符合條件的配置類
    configurations = filter(configurations, autoConfigurationMetadata);
    // 6 觸發自動配置類引入完成的事件
    fireAutoConfigurationImportEvents(configurations, exclusions);
    // 7 創建 AutoConfigurationEntry 對象
    return new AutoConfigurationEntry(configurations, exclusions);
}

其中,最關鍵的是第三步:

List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);

2.5.2.getCandidateConfigurations

繼續跟入,源碼如下:

protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
    // 利用SpringFactoriesLoader加載指定類型對應的類的全路徑
    List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
                                                                         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;
}

這裏通過getSpringFactoriesLoaderFactoryClass(),獲取到的是@EnableAutoConfiguration:

在這裏插入圖片描述

SpringFactoriesLoader.loadFactoryNames()方法我們已經見過一次了,它會去classpath下的/META-INF/spring.factories中尋找以EnableAutoConfiguration爲key的配置類的名稱:

在這裏插入圖片描述

到此,所有的自動配置類加載完畢,大致流程如圖:
在這裏插入圖片描述

2.6.自動配置類中有什麼

2.6.1.autoconfigure包

剛纔加載的所有自動配置類,都可以再spring-boot-autoconfigure包中找到這些自動配置類:

非常多,幾乎涵蓋了現在主流的開源框架,例如:

  • redis
  • jms
  • amqp
  • jdbc
  • jackson
  • mongodb
  • jpa
  • solr
  • elasticsearch

… 等等

4.6.2.默認配置

我們來看一個我們熟悉的,例如SpringMVC,查看mvc 的自動配置類:

打開WebMvcAutoConfiguration:

我們看到這個類上的4個註解:

  • @Configuration:聲明這個類是一個配置類

  • @ConditionalOnWebApplication(type = Type.SERVLET)

    ConditionalOn,翻譯就是在某個條件下,此處就是滿足項目的類是是Type.SERVLET類型,也就是一個普通web工程,顯然我們就是

  • @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })

    這裏的條件是OnClass,也就是滿足以下類存在:Servlet、DispatcherServlet、WebMvcConfigurer,其中Servlet只要引入了tomcat依賴自然會有,後兩個需要引入SpringMVC纔會有。這裏就是判斷你是否引入了相關依賴,引入依賴後該條件成立,當前類的配置纔會生效!

  • @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)

    這個條件與上面不同,OnMissingBean,是說環境中沒有指定的Bean這個才生效。其實這就是自定義配置的入口,也就是說,如果我們自己配置了一個WebMVCConfigurationSupport的類,那麼這個默認配置就會失效!

接着,我們查看該類中定義了什麼:

視圖解析器:

處理器適配器(HandlerAdapter):

還有很多,這裏就不一一截圖了。

2.6.3.默認配置的屬性覆蓋

另外,這些默認配置的屬性來自哪裏呢?

我們看到,這裏通過@EnableAutoConfiguration註解引入了兩個屬性:WebMvcProperties和ResourceProperties。這不正是SpringBoot的屬性注入玩法嘛。

我們查看這兩個屬性類:

找到了內部資源視圖解析器的prefix和suffix屬性。

ResourceProperties中主要定義了靜態資源(.js,.html,.css等)的路徑:

如果我們要覆蓋這些默認屬性,只需要在application.properties中定義與其前綴prefix和字段名一致的屬性即可。

2.7.總結(非常重要)

SpringBoot爲我們提供了默認配置,而默認配置生效的步驟:

  • @EnableAutoConfiguration註解會去尋找META-INF/spring.factories文件,讀取其中以EnableAutoConfiguration爲key的所有類的名稱,這些類就是提前寫好的自動配置類
  • 這些類都聲明瞭@Configuration註解,並且通過@Bean註解提前配置了我們所需要的一切實例。完成自動配置
  • 但是,這些配置不一定生效,因爲有@ConditionalOn註解,滿足一定條件纔會生效。比如條件之一:是一些相關的類要存在
  • 類要存在,我們只需要引入了相關依賴(啓動器),依賴有了條件成立,自動配置生效。
  • 如果我們自己配置了相關Bean,那麼會覆蓋默認的自動配置的Bean
  • 我們還可以通過配置application.yml文件,來覆蓋自動配置中的屬性

因此,使用SpringBoot自動配置的關鍵有兩點:

1)啓動器starter

要想自動配置生效,只需要引入依賴即可,而依賴版本我們也不用操心,因爲只要引入了SpringBoot提供的stater(啓動器),就會自動管理依賴及版本了。

因此,玩SpringBoot的第一件事情,就是找啓動器,SpringBoot提供了大量的默認啓動器

2)全局配置

另外,SpringBoot的默認配置,都會讀取默認屬性,而這些屬性可以通過自定義application.properties文件來進行覆蓋。這樣雖然使用的還是默認配置,但是配置中的值改成了我們自定義的。

因此,玩SpringBoot的第二件事情,就是通過application.properties來覆蓋默認屬性值,形成自定義配置。

3.實現自定義starter

明白了SpringBoot自動配置原理,我們甚至可以自己定義starter,完成自己的自動配置。

3.1.自定義starter的步驟

再來回顧一下SpringBoot自動配置的掃描過程:

  • 通過@EnableAutoConfiguration上的註解,掃描到/META-INF/spring.factories文件
  • 文件中記錄了需要導入的自動配置類
  • 在自動配置類中通過@Configuration、@Bean等等方式定義了需要完成的配置
  • 配置類中通過@ConfigurationProperties讀取application.yml文件中的屬性,允許用戶覆蓋默認屬性

因此,我們要做的是:

  • 定義Properties類,通過@ConfigurationProperties讀取application.yml文件中的屬性
  • 定義@Configuration配置類,使用Properties中讀取到的屬性完成默認配置
  • 定義/META-INF/spring.factories文件,加載定義好的配置類,使其可以被SpringBoot加載

3.2.模擬DataSource的starter

我們來完成一個DataSource數據源的starter。

3.2.1.創建項目

首先創建一個新的項目,注意創建空maven項目:

在這裏插入圖片描述

填寫項目名稱:

在這裏插入圖片描述

3.2.2.引入依賴

需要引入一些基本的SpringBoot依賴:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>cn.itcast.starter</groupId>
    <artifactId>itcast-jdbc-spring-boot-starter</artifactId>
    <version>1.0-SNAPSHOT</version>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
            <version>2.1.6.RELEASE</version>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <version>2.1.6.RELEASE</version>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
            <version>2.1.6.RELEASE</version>
        </dependency>
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.10</version>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

3.2.3.編寫屬性讀取類

我們假設用戶會在配置文件中配置jdbc連接參數,配置格式如下:

itcast:
  jdbc:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/bank?characterEncoding=utf-8&serverTimezone=UTC
    username: root
    password: 123

爲了與SpringBoot的自帶啓動器區分,我們故意設置不一樣的配置。

然後編寫一個類來加載:

package cn.itcast.jdbc.config;

import org.springframework.boot.context.properties.ConfigurationProperties;

/**
 * @author 虎哥
 */
@ConfigurationProperties("itcast.jdbc")
public class ItcastJdbcProperties {
    private String driverClassName;
    private String url;
    private String username;
    private String password;

    public String getDriverClassName() {
        return driverClassName;
    }

    public void setDriverClassName(String driverClassName) {
        this.driverClassName = driverClassName;
    }

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

3.2.4.編寫配置類

編寫配置類,初始化DataSource對象:

package cn.itcast.jdbc.config;

import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;

import javax.sql.DataSource;

/**
 * @author 虎哥
 */
@Configuration
@ConditionalOnClass(DruidDataSource.class)
@EnableConfigurationProperties(ItcastJdbcProperties.class)
public class ItcastDataSourceAutoConfiguration {

    @Bean
    @Primary
    public DataSource dataSource(ItcastJdbcProperties prop){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(prop.getDriverClassName());
        dataSource.setUrl(prop.getUrl());
        dataSource.setUsername(prop.getUsername());
        dataSource.setPassword(prop.getPassword());
        return dataSource;
    }
}

3.2.5.編寫Spring.factories

最後,編寫Spring.factories,註冊這個配置類:

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
  cn.itcast.jdbc.config.ItcastDataSourceAutoConfiguration

運行mvn install 命令,把項目代碼安裝到倉庫中:

在這裏插入圖片描述

然後在本地倉庫目錄可以看到:

在這裏插入圖片描述

3.3.測試

在我們的 bank項目中,引入自己編寫的starter:

<dependency>
    <groupId>cn.itcast.starter</groupId>
    <artifactId>itcast-jdbc-spring-boot-starter</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

移除原來的jdbc連接池配置,添加自定義配置:

itcast:
  jdbc:
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/bank?characterEncoding=utf-8&serverTimezone=UTC
    username: root
    password: 123

在AccountController中注入DataSource,斷點查看注入的情況:

在這裏插入圖片描述
可以看到連接池變了:
在這裏插入圖片描述
說明我們的自定義starter生效了!

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章