Spring Boot 自動配置理解 以及實現自定義Starter
本文節選自《 JavaEE開發的顛覆者——Spring Boot實戰 》一書。本書從Spring 基礎、Spring MVC 基礎講起,從而無難度地引入Spring Boot 的學習。涵蓋使用Spring Boot 進行Java EE 開發的絕大數應用場景,包含:Web 開發、資料訪問、安全控制、批處理、非同步訊息、系統整合、開發與部署、應用監控、分散式系統開發等。
本文先通過分析Spring Boot 的執行原理後,根據已掌握的知識自定義一個starter pom。
我們知道Spring 4.x 提供了基於條件來配置Bean 的能力,其實Spring Boot的神奇的實現也是基於這一原理的。
本文內容是理解Spring Boot 運作原理的關鍵。我們可以藉助這一特性來理解Spring Boot 執行自動配置的原理,並實現自己的自動配置。
Spring Boot 關於自動配置的原始碼在spring-boot-autoconfigure-1.3.0.x.jar 內,主要包含了如圖1 所示的配置。
圖1 包含的配置
若想知道Spring Boot 為我們做了哪些自動配置,可以檢視這裡的原始碼。
可以通過下面三種方式檢視當前專案中已啟用和未啟用的自動配置的報告。
(1)執行jar 時增加–debug 引數:
java -jar xx.jar –debug
(2)在application.properties 中設定屬性:
debug=true
(3)在STS 中設定,如圖2 所示。
圖2 在STS 中設定
此時啟動,可在控制檯輸出。已啟用的自動配置為:
未啟用的自動配置為:
運作原理
關於Spring Boot 的運作原理,我們還是迴歸到@SpringBootApplication 註解上來,這個註解是一個組合註解,它的核心功能是由@EnableAutoConfiguration 註解提供的。
下面我們來看下@EnableAutoConfiguration 註解的原始碼:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@Import({ EnableAutoConfigurationImportSelector.class,
AutoConfigurationPackages.Registrar.class })
public @interface EnableAutoConfiguration {
Class<?>[] exclude() default {};
String[] excludeName() default {};
}
這裡的關鍵功能是@Import 註解匯入的配置功能,EnableAutoConfigurationImportSelector 使用SpringFactoriesLoader.loadFactoryNames 方法來掃描具有META-INF/spring.factories 檔案的jar包,而我們的spring-boot-autoconfigure-1.3.0.x.jar 裡就有一個spring.factories 檔案,此檔案中聲明瞭有哪些自動配置,如圖3 所示。
圖3 自動配置
核心註解
開啟上面任意一個AutoConfiguration 檔案, 一般都有下面的條件註解, 在
spring-boot-autoconfigure-1.3.0.x.jar 的org.springframwork.boot.autoconfigure.condition 包下,條件註解如下。
@ConditionalOnBean:當容器裡有指定的Bean 的條件下。
@ConditionalOnClass:當類路徑下有指定的類的條件下。
@ConditionalOnExpression:基於SpEL 表示式作為判斷條件。
@ConditionalOnJava:基於JVM 版本作為判斷條件。
@ConditionalOnJndi:在JNDI 存在的條件下查詢指定的位置。
@ConditionalOnMissingBean:當容器裡沒有指定Bean 的情況下。
@ConditionalOnMissingClass:當類路徑下沒有指定的類的條件下。
@ConditionalOnNotWebApplication:當前專案不是Web 專案的條件下。
@ConditionalOnProperty:指定的屬性是否有指定的值。
@ConditionalOnResource:類路徑是否有指定的值。
@ConditionalOnSingleCandidate:當指定Bean 在容器中只有一個,或者雖然有多個但是指定首選的Bean。
@ConditionalOnWebApplication:當前專案是Web 專案的條件下。
這些註解都是組合了@Conditional 元註解,只是使用了不同的條件(Condition)。
下面我們簡單分析一下@ConditionalOnWebApplication 註解。
package org.springframework.boot.autoconfigure.condition;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Conditional;
@Target({ ElementType.TYPE, ElementType.METHOD })
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Conditional(OnWebApplicationCondition.class)
public @interface ConditionalOnWebApplication {
}
從原始碼可以看出,此註解使用的條件是OnWebApplicationCondition,下面我們看看這個條件是如何構造的:
package org.springframework.boot.autoconfigure.condition;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.type.AnnotatedTypeMetadata;
import org.springframework.util.ClassUtils;
import org.springframework.util.ObjectUtils;
import org.springframework.web.context.WebApplicationContext;
import org.springframework.web.context.support.StandardServletEnvironment;
@Order(Ordered.HIGHEST_PRECEDENCE + 20)
class OnWebApplicationCondition extends SpringBootCondition {
private static final String WEB_CONTEXT_CLASS =
“org.springframework.web.context.” + “support.GenericWebApplicationContext”;
@Override
public ConditionOutcome getMatchOutcome(ConditionContext context,
AnnotatedTypeMetadata metadata) {
boolean webApplicationRequired = metadata.isAnnotated(ConditionalOnWebApplication.class.getName());
ConditionOutcome webApplication = isWebApplication(context, metadata);
if (webApplicationRequired && !webApplication.isMatch()) {
return ConditionOutcome.noMatch(webApplication.getMessage());
}
if (!webApplicationRequired && webApplication.isMatch()) {
return ConditionOutcome.noMatch(webApplication.getMessage());
}
return ConditionOutcome.match(webApplication.getMessage());
}
private ConditionOutcome isWebApplication(ConditionContext context,
AnnotatedTypeMetadata metadata) {
if (!ClassUtils.isPresent(WEB_CONTEXT_CLASS, context.getClassLoader()))
{
return ConditionOutcome.noMatch("web application classes not found");
}
if (context.getBeanFactory() != null) {
String[] scopes =
context.getBeanFactory().getRegisteredScopeNames();
if (ObjectUtils.containsElement(scopes, “session”)) {
return ConditionOutcome.match(“found web application ‘session’
scope”);
}
}
if (context.getEnvironment() instanceof StandardServletEnvironment) {
return ConditionOutcome.match("found web application StandardServletEnvironment");
}
if (context.getResourceLoader() instanceof WebApplicationContext) {
return ConditionOutcome.match("found web application WebApplicationContext");
}
return ConditionOutcome.noMatch("not a web application");
}
}
從isWebApplication 方法可以看出,判斷條件是:
<li>GenericWebApplicationContext 是否在類路徑中;</li>
<li>容器裡是否有名為session 的scope;</li>
<li>當前容器的Enviroment 是否為StandardServletEnvironment;</li>
<li>當前的ResourceLoader 是否為WebApplicationContext ( ResourceLoader 是ApplicationContext 的頂級介面之一);</li>
<li>我們需要構造ConditionOutcome 類的物件來幫助我們, 最終通過ConditionOutcome.isMatch 方法返回布林值來確定條件。</li>
例項分析
在瞭解了Spring Boot 的運作原理和主要的條件註解後,現在來分析一個簡單的Spring Boot 內建的自動配置功能:http 的編碼配置。
我們在常規專案中配置http 編碼的時候是在web.xml 裡配置一個filter,如:
<filter>
<filter-name>encodingFilter</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>
自動配置要滿足兩個條件:
<li>能配置CharacterEncodingFilter 這個Bean;</li>
<li>能配置encoding 和forceEncoding 這兩個引數。</li>
1.配置引數
Spring Boot 的自動配置是基於型別安全的配置實現的,這裡的配置類可以在application.properties 中直接設定,原始碼如下:
@ConfigurationProperties(prefix = “spring.http.encoding”)//1
public class HttpEncodingProperties {
public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");//2
private Charset charset = DEFAULT_CHARSET; //2
private boolean force = true; //3
public Charset getCharset() {
return this.charset;
}
public void setCharset(Charset charset) {
this.charset = charset;
}
public boolean isForce() {
return this.force;
}
public void setForce(boolean force) {
this.force = force;
}
}
程式碼解釋
① 在application.properties 配置的時候字首是spring.http.encoding;
② 預設編碼方式為UTF-8,若修改可使用spring.http.encoding.charset=編碼;
③ 設定forceEncoding,預設為true,若修改可使用spring.http.encoding.force=false。
2.配置Bean
通過呼叫上述配置,並根據條件配置CharacterEncodingFilter 的Bean,我們來看看原始碼:
@Configuration
@EnableConfigurationProperties(HttpEncodingProperties.class) //1
@ConditionalOnClass(CharacterEncodingFilter.class) //2
@ConditionalOnProperty(prefix = “spring.http.encoding”, value = “enabled”,
matchIfMissing = true) //3
public class HttpEncodingAutoConfiguration {
@Autowired
private HttpEncodingProperties httpEncodingProperties; //3
@Bean//4
@ConditionalOnMissingBean(CharacterEncodingFilter.class) //5
public CharacterEncodingFilter characterEncodingFilter() {
CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
filter.setEncoding(this.httpEncodingProperties.getCharset().name());
filter.setForceEncoding(this.httpEncodingProperties.isForce());
return filter;
}
}
程式碼解釋
① 開啟屬性注入,通過@EnableConfigurationProperties 宣告,使用@Autowired 注入;
② 當CharacterEncodingFilter 在類路徑的條件下;
③ 當設定spring.http.encoding=enabled 的情況下,如果沒有設定則預設為true,即條件符合;
④ 像使用Java 配置的方式配置CharacterEncodingFilter 這個Bean;
⑤ 當容器中沒有這個Bean 的時候新建Bean。
實戰
看完前面幾節的講述,是不是覺得Spring Boot 的自動配置其實很簡單,是不是躍躍欲試地想讓自己的專案也具備這樣的功能。其實我們完全可以仿照上面http 編碼配置的例子自己寫一個自動配置,不過這裡再做的徹底點,我們自己寫一個starter pom,這意味著我們不僅有自動配置的功能,而且具有更通用的耦合度更低的配置。
為了方便理解,在這裡舉一個簡單的實戰例子,包含當某個類存在的時候,自動配置這個類的Bean,並可將Bean 的屬性在application.properties 中配置。
(1)新建starter 的Maven 專案,如圖4 所示。
圖4 新建starter 的Maven 專案
在pom.xml 中修改程式碼如下:
<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>com.wisely</groupId>
<artifactId>spring-boot-starter-hello</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-boot-starter-hello</name>
<url>http://maven.apache.org</url>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>1.3.0.M1</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>3.8.1</version>
<scope>test</scope>
</dependency>
</dependencies>
<!– 使用Spring Boot 正式版時,無須下列配置 –>
<repositories>
<repository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
<repository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</repository>
</repositories>
<pluginRepositories>
<pluginRepository>
<id>spring-snapshots</id>
<name>Spring Snapshots</name>
<url>https://repo.spring.io/snapshot</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</pluginRepository>
<pluginRepository>
<id>spring-milestones</id>
<name>Spring Milestones</name>
<url>https://repo.spring.io/milestone</url>
<snapshots>
<enabled>false</enabled>
</snapshots>
</pluginRepository>
</pluginRepositories>
</project>
程式碼解釋
在此處增加Spring Boot 自身的自動配置作為依賴。
(2)屬性配置,程式碼如下:
package com.wisely.spring_boot_starter_hello;
import org.springframework.boot.context.properties.ConfigurationProperties;
@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;
}
}
程式碼解釋
這裡配置是型別安全的屬性獲取。在application.properties 中通過hello.msg= 來設定,若不設定,預設為hello.msg=world。
(3)判斷依據類,程式碼如下:
package com.wisely.spring_boot_starter_hello;
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;
}
}
程式碼解釋
本例根據此類的存在與否來建立這個類的Bean,這個類可以是第三方類庫的類。
(4)自動配置類,程式碼如下:
package com.wisely.spring_boot_starter_hello;
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(HelloService.class)
public HelloService helloService(){
HelloService helloService = new HelloService();
helloService.setMsg(helloServiceProperties.getMsg());
return helloService;
}
}
程式碼解釋
根據HelloServiceProperties 提供的引數,並通過@ConditionalOnClass 判斷HelloService 這個類在類路徑中是否存在,且當容器中沒有這個Bean 的情況下自動配置這個Bean。
(5)註冊配置。若想自動配置生效,需要註冊自動配置類。在src/main/resources 下新建META-INF/spring.factories,結構如圖5所示。
在spring.factories 中填寫如下內容註冊:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.wisely.spring_boot_starter_hello.HelloServiceAutoConfiguration
若有多個自動配置,則用“,”隔開,此處“\”是為了換行後仍然能讀到屬性。
另外,若在此例新建的專案中無src/main/resources 資料夾,需執行如圖6所示操作。
圖6 調出src/maln/resources 資料夾
(5)使用starter。新建Spring Boot 專案,並將我們的starter 作為依賴,如圖7 所示。
圖7 新建Spring Boot 專案
在pom.xml 中新增spring-boot-starter-hello 的依賴,程式碼如下:
<dependency>
<groupId>com.wisely</groupId>
<artifactId>spring-boot-starter-hello</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
我們可以在Maven 的依賴裡檢視spring-boot-starter-hello,如圖8所示。
圖8 檢視spring-Doot-starter-hello
在開發階段,我們引入的依賴是spring-boot-starter-hello 這個專案。在starter 穩定之後,
我們可以將spring-boot-starter-hello 通過“mvn install”安裝到本地庫,或者將這個jar 包釋出到Maven 私服上。
簡單的執行類程式碼如下:
package com.wisely.ch6_5;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import com.wisely.spring_boot_starter_hello.HelloService;
@RestController
@SpringBootApplication
public class Ch65Application {
@Autowired
HelloService helloService;
@RequestMapping("/")
public String index(){
return helloService.sayHello();
}
public static void main(String[] args) {
SpringApplication.run(Ch65Application.class, args);
}
}
在程式碼中可以直接注入HelloService 的Bean,但在專案中我們並沒有配置這個Bean,這是通過自動配置完成的。
這時在application.properties 中配置msg 的內容:
hello.msg= wangyunfei
圖10 檢視效果
在application.properties 中新增debug 屬性,檢視自動配置報告:
debug=true
我們新增的自動配置顯示在控制檯的報告中,如圖11所示。
圖11 控制檯報告