面試題: SpringBoot 的自動配置原理


個人博客網:https://wushaopei.github.io/    (你想要這里多有)

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 ?


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM