SpringBoot自動配置原理


   在微服務概念興起的今天,很多公司轉型使用微服務作為架構。在技術選型上Spring Cloud 是非常好的選擇,它提供了一站式的分布式系統解決方案,而Spring Cloud中的每個組件都是基於Spring Boot構建的,Spring Boot提供J2EE一站式解決方案,具有以下優點:

  • 快速創建獨立運行的Spring項目以及與主流框架集成
  • 使用嵌入式的Servlet容器,應用無需打成WAR包
  • starters自動依賴與版本控制
  • 大量的自動配置,簡化開發,也可修改默認值
  • 無需配置XML,無代碼生成,開箱即用
  • 准生產環境的運行時應用監控
  • 與雲計算的天然集成

一、Spring Boot的核心組件模塊

模塊 java文件數
spring-boot 551
spring-boot-actuator 423
spring-boot-autoconfigure 783
spring-boot-devtools 169
spring-boot-cli 180
spring-boot-tools 355

從上面的java文件數量大致可以看出,SpringBoot技術框架的核心組成部分:
spring-boot-autoconfigure
spring-boot
spring-boot-tools

我們把SpringBoot源碼導入IntelliJ IDEA,查看artifact的全部依賴關系。
IDEA有個Maven Projects窗口,一般在右側能夠找到,如果沒有可以從菜單欄打開:View>Tool Windows>Maven Projects;
選擇要分析的maven module(idea的module相當於eclipse的project),右擊show dependencies,會出來該module的全部依賴關系圖,非常清晰細致。
例如,spring-boot-starter-freemarker的依賴圖分析如下:

在spring-boot-build 的pom中,我們可以看到:

<modules>
    <module>spring-boot-dependencies</module>
    <module>spring-boot-parent</module>
    <module>spring-boot-tools</module>
    <module>spring-boot</module>
    <module>spring-boot-test</module>
    <module>spring-boot-autoconfigure</module>
    <module>spring-boot-test-autoconfigure</module>
    <module>spring-boot-actuator</module>
    <module>spring-boot-devtools</module>
    <module>spring-boot-docs</module>
    <module>spring-boot-starters</module>
    <module>spring-boot-actuator-docs</module>
    <module>spring-boot-cli</module>
</modules>

其中,在spring-boot-dependencies中,SpringBoot項目維護了一份龐大依賴。這些依賴的版本都是經過實踐,測試通過,不會發生依賴沖突的。就這樣一個事情,就大大減少了Spring開發過程中,出現jar包沖突的概率。spring-boot-parent依賴spring-boot-dependencies。

下面我們簡要介紹一下SpringBoot子modules。

  • spring-boot:SpringBoot核心工程。
  • spring-boot-starters:是SpringBoot的啟動服務工程。
  • spring-boot-autoconfigure:是SpringBoot實現自動配置的核心工程。
  • spring-boot-actuator:提供SpringBoot應用的外圍支撐性功能。

  比如:

  1. Endpoints,SpringBoot應用狀態監控管理
  2. HealthIndicator,SpringBoot應用健康指示表
  3. 提供metrics支持
  4. 提供遠程shell支持
  • spring-boot-tools:提供了SpringBoot開發者的常用工具集。諸如,spring-boot-gradle-plugin,spring-boot-maven-plugin就是這個工程里面的。
  • spring-boot-cli:是Spring Boot命令行交互工具,可用於使用Spring進行快速原型搭建。你可以用它直接運行Groovy腳本。如果你不喜歡Maven或Gradle,Spring提供了CLI(Command Line Interface)來開發運行Spring應用程序。你可以使用它來運行Groovy腳本,甚至編寫自定義命令。

二、Spring Boot Starters

  Spring boot中的starter概念是非常重要的機制,能夠拋棄以前繁雜的配置,統一集成進starter,應用者只需要引入starter jar包,spring boot就能自動掃描到要加載的信息。
  starter讓我們擺脫了各種依賴庫的處理,需要配置各種信息的困擾。Spring Boot會自動通過classpath路徑下的類發現需要的Bean,並織入bean。
  例如,如果你想使用Spring和用JPA訪問數據庫,你只要依賴 spring-boot-starter-data-jpa 即可。
  目前,github上spring-boot項目的最新的starter列表spring-boot/spring-boot-starters如下:

spring-boot-starter
spring-boot-starter-activemq
spring-boot-starter-actuator
spring-boot-starter-amqp
spring-boot-starter-aop
spring-boot-starter-artemis
spring-boot-starter-batch
spring-boot-starter-cache
spring-boot-starter-cloud-connectors
spring-boot-starter-data-cassandra
spring-boot-starter-data-couchbase
spring-boot-starter-data-elasticsearch
spring-boot-starter-data-jpa
spring-boot-starter-data-ldap
spring-boot-starter-data-mongodb
spring-boot-starter-data-mongodb-reactive
spring-boot-starter-data-neo4j
spring-boot-starter-data-redis
spring-boot-starter-data-rest
spring-boot-starter-data-solr
spring-boot-starter-freemarker
spring-boot-starter-groovy-templates
spring-boot-starter-hateoas
spring-boot-starter-integration
spring-boot-starter-jdbc
spring-boot-starter-jersey
spring-boot-starter-jetty
spring-boot-starter-jooq
spring-boot-starter-jta-atomikos
spring-boot-starter-jta-bitronix
spring-boot-starter-jta-narayana
spring-boot-starter-log4j2
spring-boot-starter-logging
spring-boot-starter-mail
spring-boot-starter-mobile
spring-boot-starter-mustache
spring-boot-starter-parent
spring-boot-starter-reactor-netty
spring-boot-starter-security
spring-boot-starter-social-facebook
spring-boot-starter-social-linkedin
spring-boot-starter-social-twitter
spring-boot-starter-test
spring-boot-starter-thymeleaf
spring-boot-starter-tomcat
spring-boot-starter-undertow
spring-boot-starter-validation
spring-boot-starter-web
spring-boot-starter-web-services
spring-boot-starter-webflux
spring-boot-starter-websocket

這是Spring Boot的核心啟動器,包含了自動配置、日志和YAML。它的項目依賴圖如下:

 

  可以看出,這些starter只是配置,真正做自動化配置的代碼的是在spring-boot-autoconfigure里面。同時spring-boot-autoconfigure依賴spring-boot工程,這個spring-boot工程是SpringBoot的核心。
  SpringBoot會基於你的classpath中的jar包,試圖猜測和配置您可能需要的bean。
  例如,如果你的classpath中有tomcat-embedded.jar,你可能會想要一個TomcatEmbeddedServletContainerFactory Bean (SpringBoot通過獲取EmbeddedServletContainerFactory來啟動對應的web服務器。常用的兩個實現類是TomcatEmbeddedServletContainerFactory和JettyEmbeddedServletContainerFactory)。
  其他的所有基於Spring Boot的starter都依賴這個spring-boot-starter。比如說spring-boot-starter-actuator的依賴樹,如下圖:

 

三、SpringBoot 自動配置原理

SpringBoot 自動配置主要通過 @EnableAutoConfiguration, @Conditional, @EnableConfigurationProperties 或者 @ConfigurationProperties 等幾個注解來進行自動配置完成的。
@EnableAutoConfiguration 開啟自動配置,主要作用就是調用 Spring-Core 包里的 loadFactoryNames(),將 autoconfig 包里的已經寫好的自動配置加載進來。
@Conditional 條件注解,通過判斷類路徑下有沒有相應配置的 jar 包來確定是否加載和自動配置這個類。
@EnableConfigurationProperties的作用就是,給自動配置提供具體的配置參數,只需要寫在application.properties 中,就可以通過映射寫入配置類的 POJO 屬性中。

1、SpringBoot啟動主程序類

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

每次我們直接直接啟動這個啟動類,SpringBoot就啟動成功了,並且幫我們配置了好多自動配置類。其中最重要是 @SpringBootApplication 這個注解,我們點進去看一下。

2、@SpringBootApplication注解

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

@SpringBootApplication聲明時,引用了三個重要的注解:

  • @SpringBootConfiguration : SpringBoot的配置類,標注在某個類上,表示這是一個Spring Boot的配置類
  • @EnableAutoConfiguration: 開啟自動配置類,SpringBoot的精華所在。
  • @ComponentScan包掃描

以前我們需要配置的東西,Spring Boot幫我們自動配置;@EnableAutoConfiguration告訴SpringBoot開啟自動配置功能;這樣自動配置才能生效;

  • @EnableAutoConfiguration注解

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

@EnableAutoConfiguration聲明時,引用了兩個重要的注解:

  • @AutoConfigurationPackage:自動配置包
  • @Import: 導入自動配置的組件
  • @AutoConfigurationPackage注解
     static class Registrar implements ImportBeanDefinitionRegistrar, DeterminableImports { @Override public void registerBeanDefinitions(AnnotationMetadata metadata,BeanDefinitionRegistry registry) { register(registry, new PackageImport(metadata).getPackageName()); }

它其實是注冊了一個Bean的定義。
new PackageImport(metadata).getPackageName(),它其實返回了當前主程序類的同級以及子級的包組件。

以上圖為例,DemoApplication是和demo包同級,但是demo2這個類是DemoApplication的父級,和example包同級
也就是說,DemoApplication啟動加載的Bean中,並不會加載demo2,這也就是為什么,我們要把DemoApplication放在項目的最高級中。

  • @Import(AutoConfigurationImportSelector.class)注解

可以從圖中看出AutoConfigurationImportSelector 繼承了 DeferredImportSelector繼承了ImportSelector,ImportSelector有一個方法為:selectImports。

  @Override public String[] selectImports(AnnotationMetadata annotationMetadata) { if (!isEnabled(annotationMetadata)) { return NO_IMPORTS; } AutoConfigurationMetadata autoConfigurationMetadata = AutoConfigurationMetadataLoader.loadMetadata(this.beanClassLoader); AnnotationAttributes attributes = getAttributes(annotationMetadata); List<String> configurations = getCandidateConfigurations(annotationMetadata,attributes); configurations = removeDuplicates(configurations); Set<String> exclusions = getExclusions(annotationMetadata, attributes); checkExcludedClasses(configurations, exclusions); configurations.removeAll(exclusions); configurations = filter(configurations, autoConfigurationMetadata); fireAutoConfigurationImportEvents(configurations, exclusions); return StringUtils.toStringArray(configurations); }

可以看到第九行,它其實是去加載public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";外部文件。這個外部文件,有很多自動配置的

類。如下:

3、如何自定義自己的Bean

我們以RedisTemplate為例:

@Configuration @ConditionalOnClass(RedisOperations.class) @EnableConfigurationProperties(RedisProperties.class) @Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class }) public class RedisAutoConfiguration { @Bean @ConditionalOnMissingBean(name = "redisTemplate") public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { RedisTemplate<Object, Object> template = new RedisTemplate<>(); template.setConnectionFactory(redisConnectionFactory); return template; } @Bean @ConditionalOnMissingBean public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException { StringRedisTemplate template = new StringRedisTemplate(); template.setConnectionFactory(redisConnectionFactory); return template; } }

我們每次在Spring中使用Redis,都會使用到RedisTemplate這個工具類,但是他默認給我們返回的這個工具類,可能不是很符合我們的要求。比如:我們想要開啟事務,或者想要改變它默認的序列化。

  • 這時候該如何去做呢?

根據前面的分析,只要我們在容器中放入一個RedisTemplate Bean即可。

 1     @Bean("redisTemplate")  2     public RedisTemplate<Object, Object> myRedisTemplate(RedisConnectionFactory redisConnectionFactory) {  3         RedisTemplate<Object, Object> template = new RedisTemplate<>();  4  template.setConnectionFactory(redisConnectionFactory);  5         // 修改序列化為Jackson
 6         template.setDefaultSerializer(new GenericJackson2JsonRedisSerializer());  7         // 開啟事務
 8         template.setEnableTransactionSupport(true);  9         return template; 10     }

我們自己定義我們的RedisTemplate模板,修改序列化,開啟事務等操作。
我們將我們自己的Bean加入到IoC容器中以后,他就會默認的覆蓋掉原來的RedisTemplate,達到定制的效果。
我們在以Kafka為例:

假設我們想要消費的對象不是字符串,而是一個對象呢?比如Person對象,或者其他Object類呢?

  1. 我們首先去查找KafkaAutoConfiguration(xxxAutoConfiguration),看看是否有關於Serializer屬性的配置
  2. 假設沒有我們就去KafkaProperties文件查找是否有Serializer的配置

然后直接在application.properties修改默認序列化就好,連Bean都不需要自己重寫。
類似這種,可以使用Spring提供的Json序列化,也可以自動使用第三方框架提供的序列化,比如Avro, Protobuff等

spring.kafka.producer.key-serializer=org.springframework.kafka.support.serializer.JsonSerializer spring.kafka.producer.value-serializer=org.springframework.kafka.support.serializer.JsonSerializer spring.kafka.consumer.key-deserializer=com.example.common.MyJson spring.kafka.consumer.value-deserializer=com.example.common.MyJson

 

  • HttpEncodingAutoConfiguration

在這么多xxxxAutoConfiguration中,我們以HttpEncodingAutoConfiguration(Http自動編碼)為例

 

 

 1 @Configuration    
 2 //表示這是一個配置類,以前編寫的配置文件一樣,也可以給容器中添加組件
 3 
 4 @EnableConfigurationProperties(HttpEncodingProperties.class)
 5 //啟動指定類的ConfigurationProperties功能;將配置文件中對應的值和HttpEncodingProperties綁定起來;並把HttpEncodingProperties加入到ioc容器中
 6 
 7 @ConditionalOnWebApplication
 8 //Spring底層@Conditional注解(Spring注解版),根據不同的條件,如果滿足指定的條件,整個配置類里面的配置就會生效;這里是判斷當前應用是否是web應用,如果是,當前配置類生效
 9 
10 @ConditionalOnClass(CharacterEncodingFilter.class)
11 //判斷當前項目有沒有這個類CharacterEncodingFilter;SpringMVC中進行亂碼解決的過濾器;
12 
13 @ConditionalOnProperty(prefix = "spring.http.encoding", value = "enabled", matchIfMissing = true)
14 //判斷配置文件中是否存在某個配置 spring.http.encoding.enabled;如果不存在,判斷也是成立的
15 //即使我們配置文件中不配置pring.http.encoding.enabled=true,也是默認生效的;
16 
17 public class HttpEncodingAutoConfiguration {
18 
19     //他已經和SpringBoot的配置文件映射了
20     private final HttpEncodingProperties properties;
21 
22     //只有一個有參構造器的情況下,參數的值就會從容器中拿
23     public HttpEncodingAutoConfiguration(HttpEncodingProperties properties) {
24         this.properties = properties;
25     }
26 
27     @Bean
28     //給容器中添加一個組件,這個組件的某些值需要從properties中獲取
29     @ConditionalOnMissingBean(CharacterEncodingFilter.class)
30     //判斷容器沒有這個組件
31     public CharacterEncodingFilter characterEncodingFilter() {
32         CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
33         filter.setEncoding(this.properties.getCharset().name());
34         filter.setForceRequestEncoding(this.properties.shouldForce(Type.REQUEST));
35         filter.setForceResponseEncoding(this.properties.shouldForce(Type.RESPONSE));
36         return filter;
37     }
38     }
39     .......
40 }

 

  通過上面的類的注解可以看到,通過使用@EnableConfigurationProperties,可以把配置文件中的屬性與HttpEncodingProperties類綁定起來並且加入到IOC容器中,進入HttpEncodingProperties類,可以看到他是通過@ConfigurationProperties 注解把配置文件中的spring.http.encoding值與該類的屬性綁定起來的。

@ConfigurationProperties(prefix = "spring.http.encoding")
public class HttpEncodingProperties 

同時我們可以注意到上面的類中使用了@ConditionalOnClass@ConditionalOnWebApplication注解,這兩個都是@Conditional的派生注解,作用是必須是@Conditional指定的條件成立,才給容器中添加組件,配置里的內容才會生效

  • Conditional注解

下面我們以@ConditionalOnClass為例,來分析一下他的源代碼。

@Conditional(OnClassCondition.class)
public @interface ConditionalOnClass {

進入OnClassCondition類,查看他的類繼承信息,可以看到他繼承SpringBootCondition類,SpringBootCondition又實現了Condition接口

OnClassCondition又override了SpringBootCondition的getMatchOutcome方法,該方法會返回條件匹配結果。
getMatchOutcome方法源代碼如下:

public ConditionOutcome getMatchOutcome(ConditionContext context,
         .....
List<String> missing = getMatches(onClasses, MatchType.MISSING, classLoader);
       ......
List<String> present = getMatches(onMissingClasses, MatchType.PRESENT, classLoader);
    }

可以看到getMatchOutcome中主要調用了getMatches方法
進入getMatches方法

private List<String> getMatches(Collection<String> candidates, MatchType matchType,
            ClassLoader classLoader) {
        List<String> matches = new ArrayList<String>(candidates.size());
        for (String candidate : candidates) {
            if (matchType.matches(candidate, classLoader)) {
                matches.add(candidate);
            }
        }
        return matches;
    }

getMatches又調用了MatchType的matches方法。

 private enum MatchType {

        PRESENT {

            @Override
            public boolean matches(String className, ClassLoader classLoader) {
                return isPresent(className, classLoader);
            }

        },

        MISSING {

            @Override
            public boolean matches(String className, ClassLoader classLoader) {
                return !isPresent(className, classLoader);
            }

        };

        private static boolean isPresent(String className, ClassLoader classLoader) {
            if (classLoader == null) {
                classLoader = ClassUtils.getDefaultClassLoader();
            }
            try {
                forName(className, classLoader);
                return true;
            }
            catch (Throwable ex) {
                return false;
            }
        }

        private static Class<?> forName(String className, ClassLoader classLoader)
                throws ClassNotFoundException {
            if (classLoader != null) {
                return classLoader.loadClass(className);
            }
            return Class.forName(className);
        }

        public abstract boolean matches(String className, ClassLoader classLoader);

    }

進入MatchType類中,可以看到他有兩個枚舉類,進一步看枚舉類中的matches的源代碼可以發現最終是利用loadClass以及forName 方法,判斷類路徑下有沒有這個指定的類。
下面列舉出的是Spring Boot對@Conditional的擴展注解。

用好SpringBoot只要把握這幾點:

  • SpringBoot啟動會加載大量的自動配置類
  • 所要做的就是我們需要的功能SpringBoot有沒有幫我們寫好的自動配置類:
  • 如果有就再來看這個自動配置類中到底配置了哪些組件,Springboot自動配置類里邊只要我們要用的組件有,我們就不需要再來配置了,但是如果說沒有我們所需要的組件,那么我們就需要自己來寫一個配置類來把我們相應的組件配置起來。
  • 給容器中自動配置類添加組件的時候,會從properties類中獲取某些屬性,而這些屬性我們就可以在配置文件指定這些屬性


免責聲明!

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



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