Java微框架Spring Boot 运行原理深入解读
本文節選自《?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";@Overridepublic 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 方法可以看出,判斷條件是:
實例分析
在了解了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>自動配置要滿足兩個條件:
1.配置參數
Spring Boot 的自動配置是基于類型安全的配置實現的,這里的配置類可以在application.properties 中直接設置,源碼如下:
@ConfigurationProperties(prefix = "spring.http.encoding")//1 public class HttpEncodingProperties {public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8");//2private Charset charset = DEFAULT_CHARSET; //2private boolean force = true; //3public 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 {@Autowiredprivate HttpEncodingProperties httpEncodingProperties; //3@Bean//4@ConditionalOnMissingBean(CharacterEncodingFilter.class) //5public 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 {@Autowiredprivate 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 {@AutowiredHelloService helloService;@RequestMapping("/")public String index(){return helloService.sayHello();}public static void main(String[] args) {SpringApplication.run(Ch65Application.class, args);} }在代碼中可以直接注入HelloService 的Bean,但在項目中我們并沒有配置這個Bean,這是通過自動配置完成的。
訪問?http://localhost:8080?,效果如圖9 所示。
圖9 訪問http://local host:8080
這時在application.properties 中配置msg 的內容:
hello.msg= wangyunfei此時再次訪問?http://localhost:8080?,效果如圖10 所示。
圖10 查看效果
在application.properties 中添加debug 屬性,查看自動配置報告:
debug=true我們新增的自動配置顯示在控制臺的報告中,如圖11所示。
圖11 控制臺報告
總結
以上是生活随笔為你收集整理的Java微框架Spring Boot 运行原理深入解读的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Spring Boot属性配置文件详解
- 下一篇: Spring Boot 性能优化