看一下 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 。
可以看到其中的自動配置。
# 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>
自動配置需要滿足兩個條件:
- 能配置 CharacterEncodingFilter 這個 Bean;
- 能配置 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 ,效果如下圖。
在application.properties中添加配置。
hello.msg=wyk
重新運行, 訪問 http://localhost:8080 ,效果如下圖。