Spring Boot學習筆記(二):Spring Boot 運行原理

看一下 Spring 實現自動配置的原理~~

全部章節傳送門:
Spring Boot學習筆記(一):Spring Boot 入門基礎
Spring Boot學習筆記(二):Spring Boot 運行原理
Spring Boot學習筆記(三):Spring Boot Web開發
Spring Boot學習筆記(四):Spring Boot 數據訪問
Spring Boot學習筆記(五):Spring Boot 企業級開發
Spring Boot學習筆記(六):Spring Boot 應用監控

Spring Boot 啟動原理

任何一個 Spring Boot 項目都會有一個啟動類,其中有一個 @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 {
    ...
}

其中,比較關鍵的注解:

  • @SpringBootConfiguration:標記當前類為配置類
  • @EnableAutoConfiguration:開啟自動配置
  • @ComponentScan:掃描主類所在的同級包以及下級包里的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 {};
}

其中的關鍵部分是 @Import 注解導入的配置功能,AutoConfigurationImportSelector 使用 getCandidateConfigurations 方法得到待配置的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;
}

SpringFactoriesLoader.loadFactoryNames 方法會掃描 META-INF/spring.factories 文件。

查看 spring-boot-autoconfigure-2.1.3.RELEASE.jar 中的 spring.factories 。


spring-factories.png

可以看到其中的自動配置。

# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\
org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\
org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\
...

核心注解

打開任意的 AutoConfiguration 文件(spring-boot-autoconfigure-2.1.3.RELEASE.jar 中),可以看到很多的條件注解,這些注解包含在 org.springframework.boot.autoconfigure.condition 包中,如下所示:

  • @ConditionalOnBean:當容器里有指定Bean的條件下
  • @ConditionalOnClass:當類路徑下有指定的類的條件下
  • @ConditionalOnExpression:基于SpEL表達式作為判斷條件
  • @ConditionalOnJava:基于JVM版本作為判斷條件
  • @ConditionalOnJndi:在JNDI存在的條件下查找指定的位置
  • @ConditionalOnMissingBean:當容器里沒有指定Bean的情況下
  • @ConditionalOnMissingClass:當容器里沒有指定類的情況下
  • @ConditionalOnWebApplication:當前項目時Web項目的條件下
  • @ConditionalOnNotWebApplication:當前項目不是Web項目的條件下
  • @ConditionalOnProperty:指定的屬性是否有指定的值
  • @ConditionalOnResource:類路徑是否有指定的值
  • @ConditionalOnOnSingleCandidate:當指定Bean在容器中只有一個,或者有多個但是指定首選的Bean
  • @ConditionalOnWebApplication: 當前項目是在 Web 項目的條件下。

這些注解都組合了@Conditional注解,只是使用了不同的條件。

實例分析

下面通過一個簡單的 Spring Boot 內置的自動配置: http的編碼配置來講解一下配置流程。

在常規項目中配置 Http 編碼的時候是在 web.xml 中配置一個 filter 。

<filter> 
    <filter-name>CharacterEncodingFilter</filter-name>
    <filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
    <init-param> 
        <param-name>encoding</param-name>
        <param-value>utf-8</param-value> 
    </init-param> 
    <init-param> 
        <param-name>forceEncoding</param-name>
        <param-value>true</param-value> 
    </init-param> 
</filter> 

自動配置需要滿足兩個條件:

  1. 能配置 CharacterEncodingFilter 這個 Bean;
  2. 能配置 encoding 和 forceEncoding 兩個參數。

配置參數的時候使用了在SpringBoot基礎章節中講述的類型安全配置,Spring Boot 也是基于這一點實現的。雙擊shift全局搜索 HttpProperties(這里需要注意,不是老版本中的 HttpEncodingProperties)。

@ConfigurationProperties(
    prefix = "spring.http"
) // 配置前綴
public class HttpProperties {
    private boolean logRequestDetails;
    private final HttpProperties.Encoding encoding = new HttpProperties.Encoding();

    public HttpProperties() {
    }

    public boolean isLogRequestDetails() {
        return this.logRequestDetails;
    }

    public void setLogRequestDetails(boolean logRequestDetails) {
        this.logRequestDetails = logRequestDetails;
    }

    public HttpProperties.Encoding getEncoding() {
        return this.encoding;
    }

    public static class Encoding {
        public static final Charset DEFAULT_CHARSET;
        private Charset charset;
        private Boolean force;
        private Boolean forceRequest;
        private Boolean forceResponse;
        private Map<Locale, Charset> mapping;

        public Encoding() {
            this.charset = DEFAULT_CHARSET;
        }

        public Charset getCharset() {
            return this.charset;
        }

        public void setCharset(Charset charset) {
            this.charset = charset;
        }

        public boolean isForce() {
            return Boolean.TRUE.equals(this.force);
        }

        public void setForce(boolean force) {
            this.force = force;
        }

        public boolean isForceRequest() {
            return Boolean.TRUE.equals(this.forceRequest);
        }

        public void setForceRequest(boolean forceRequest) {
            this.forceRequest = forceRequest;
        }

        public boolean isForceResponse() {
            return Boolean.TRUE.equals(this.forceResponse);
        }

        public void setForceResponse(boolean forceResponse) {
            this.forceResponse = forceResponse;
        }

        public Map<Locale, Charset> getMapping() {
            return this.mapping;
        }

        public void setMapping(Map<Locale, Charset> mapping) {
            this.mapping = mapping;
        }

        public boolean shouldForce(HttpProperties.Encoding.Type type) {
            Boolean force = type != HttpProperties.Encoding.Type.REQUEST ? this.forceResponse : this.forceRequest;
            if (force == null) {
                force = this.force;
            }

            if (force == null) {
                force = type == HttpProperties.Encoding.Type.REQUEST;
            }

            return force;
        }

        static {
            DEFAULT_CHARSET = StandardCharsets.UTF_8;//默認編碼是UTF8
        }

        public static enum Type {
            REQUEST,
            RESPONSE;

            private Type() {
            }
        }
    }
}

通過調用上述配置,然后根據條件配置 CharacterEncodingFilter 的 Bean 。

@Configuration 
@EnableConfigurationProperties({HttpProperties.class}) 
@ConditionalOnWebApplication(
    type = Type.SERVLET
)
@ConditionalOnClass({CharacterEncodingFilter.class})
@ConditionalOnProperty(
    prefix = "spring.http.encoding",
    value = {"enabled"},
    matchIfMissing = true
)
public class HttpEncodingAutoConfiguration {
    private final Encoding properties;

    public HttpEncodingAutoConfiguration(HttpProperties properties) {
        this.properties = properties.getEncoding();
    }

    @Bean
    @ConditionalOnMissingBean
    public CharacterEncodingFilter characterEncodingFilter() {
        CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
        filter.setEncoding(this.properties.getCharset().name());
        filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.REQUEST));
        filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.RESPONSE));
        return filter;
    }
    ...
}

看上面的注解:

  • @Configuration:標明為配置類
  • @EnableConfigurationProperties(HttpEncodingProperties.class)聲明開啟屬性注入
  • @ConditionalOnClass(CharacterEncodingFilter.class)當CharacterEncodingFilter在類路徑的條件下
  • @ConditionalOnProperty(prefix = “spring.http.encoding”, value = “enabled”, matchIfMissing = true)當spring.http.encoding=enabled的情況下,如果沒有設置則默認為true,即條件符合
  • @ConditionalOnMissingBean當容器中沒有這個Bean時新建Bean 。

實現 starter pom

使用 idea 創建 Maven 的 quickstart 項目,項目信息如下。

<groupId>com.wyk</groupId>
<artifactId>spring-boot-starter-hello</artifactId>
<version>1.0-SNAPSHOT</version>

在 pom.xml 中添加 spring-boot-autoconfigure 依賴。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-autoconfigure</artifactId>
    <version>2.1.2.RELEASE</version>
</dependency>

創建屬性配置類,用來獲取類型安全的屬性。

package com.wyk.springbootstarterhello;

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

/**
 * hello屬性配置
 */
@ConfigurationProperties(prefix="hello")
public class HelloServiceProperties {
    private static final String MSG = "world";
    //添加默認值
    private String msg = MSG;

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}

創建判斷依據類。

package com.wyk.springbootstarterhello;

/**
 * 判斷依據類
 */
public class HelloService {
    private String msg;

    public String sayHello() {
        return "Hello " + msg;
    }

    public String getMsg() {
        return msg;
    }

    public void setMsg(String msg) {
        this.msg = msg;
    }
}

創建自動配置類。

package com.wyk.springbootstarterhello;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@EnableConfigurationProperties(HelloServiceProperties.class)
@ConditionalOnClass(HelloService.class)
@ConditionalOnProperty(prefix="hello", value="enabled", matchIfMissing=true)
public class HelloServiceAutoConfiguration {
    @Autowired
    private HelloServiceProperties helloServiceProperties;

    @Bean
    @ConditionalOnMissingBean
    public HelloService helloService() {
        HelloService helloService = new HelloService();
        helloService.setMsg(helloServiceProperties.getMsg());
        return helloService;
    }
}

若要配置類生效,還需要注冊自動配置類,在src/main/resources 下新建 META-INF/spring.factories,并在其中填寫如下內容:

org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.wyk.springbootstarterhello.HelloServiceAutoConfiguration

多個配置的話需要使用逗號隔開。

另外,在idea中默認沒有resources目錄,需要手工創建,然后點擊 File->Project Structure...,在彈出的窗口中選擇 Modules,然后右擊resources目錄將其設置為資源。

[圖片上傳失敗...(image-5b5c5b-1553269214786)]

然后需要將項目添加到 Maven 中方便引用。

點擊 View->Tool Window->Maven Projects ,會在右側彈出Maven的工具欄。

[圖片上傳失敗...(image-a6ee64-1553269214786)]

點擊Lifecycle中的install即可構建Maven工程并安裝到本地倉庫,需要清除的話需點擊clean 。

接下來創建一個 Spring Boot 的Web項目用來使用前面的 starter 。創建好之后需要添加starter依賴。

<dependency>
    <groupId>com.wyk</groupId>
    <artifactId>spring-boot-starter-hello</artifactId>
    <version>1.0-SNAPSHOT</version>
</dependency>

然后簡單的修改一下運行類。

@RestController
@SpringBootApplication
public class StatertestdemoApplication {

    @Autowired
    HelloService helloService;

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

    @RequestMapping("/")
    public String index() {
        return helloService.sayHello();
    }
}

運行程序,訪問 http://localhost:8080 ,效果如下圖。

starter-hello-world.png

在application.properties中添加配置。

hello.msg=wyk

重新運行, 訪問 http://localhost:8080 ,效果如下圖。

starter-hello-wyk.png
最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。