玩轉 Spring Boot 原理篇(原始碼環境搭建)

語言: CN / TW / HK

0. 

0.0. 歷史文章整理

玩轉 Spring Boot 入門篇

玩轉 Spring Boot 整合篇(MySQL、Druid、HikariCP)

玩轉 Spring Boot 整合篇(MyBatis、JPA、事務支援)

玩轉 Spring Boot 整合篇(Redis)

玩轉 Spring Boot 整合篇(Actuator、Spring Boot Admin)

玩轉 Spring Boot 整合篇(RabbitMQ)

玩轉 Spring Boot 整合篇(@Scheduled、靜態、動態定時任務)

玩轉 Spring Boot 整合篇(任務動態管理程式碼篇)

玩轉 Spring Boot 整合篇(定時任務框架Quartz)

0.1. 玩轉 Spring Boot 原理篇

從今天開始,將開啟玩轉 Spring Boot 系列的原理篇的分享,後續將一起走進 Spring Boot 的原始碼,結合原始碼探究自動裝配的原理、Spring Boot 的啟動機制以及內嵌 Tomcat 的實現原理等。

工欲善其事必先利其器,考慮到方便後續學習原始碼,本次先把 Spring Boot 原始碼環境給搭建起來。

本次原始碼環境依賴

  • IntelliJ IDEA 2021.1.2 (Ultimate Edition)  

  • JDK 1.8.0_251

  • Gradle 7.4

  • macOS

1.  環境依賴

Raise the minimum supported version of Gradle to 7.3

Spring Boot 2.6.3 版本將 Gradle 的最低支援版本提高到 7.3,本次 Gradle 版本採用 7.4。

1.1.  安裝 Gradle

1.1.1.  下載安裝包

https://gradle.org/next-steps/?version=7.4&format=bin

1.1.2.  配置環境變數

export GRADLE_HOME=/Users/tangbao/software/gradle-7.4
export PATH=$PATH:$GRADLE_HOME/bin

1.1.3.  驗證環境

[email protected] ~ % gradle -v


------------------------------------------------------------
Gradle 7.4
------------------------------------------------------------


Build time: 2022-02-08 09:58:38 UTC
Revision: f0d9291c04b90b59445041eaa75b2ee744162586


Kotlin: 1.5.31
Groovy: 3.0.9
Ant: Apache Ant(TM) version 1.10.11 compiled on July 10 2021
JVM: 1.8.0_251 (Oracle Corporation 25.251-b08)
OS: Mac OS X 10.15.2 x86_64

1.2.  JDK

[email protected] ~ % java -version
java version "1.8.0_251"
Java(TM) SE Runtime Environment (build 1.8.0_251-b08)
Java HotSpot(TM) 64-Bit Server VM (build 25.251-b08, mixed mode)

1.3.  IDE 開發工具環境整合

1.3.1.  IDEA 配置 Gradle

2.  Spring Boot 原始碼

2.1.  下載原始碼

https://github.com/spring-projects/spring-boot/tree/v2.6.3

2.2. 修改  gradle 包路徑

下載之後,解壓縮排入原始碼目錄。

開啟 gradle/wrapper下的 gradle-wrapper.properties 檔案,修改為本地 gradle 包的安裝路徑,修改如下。

3. IDEA 匯入  Spring Boot 原始碼

在 IDEA 中選擇 File --> Open ... 開啟下載之後的 spring boot 2.6.3 目錄下的 build.gradle 檔案。

然後後面就交給 IDEA 了,建議站起來接杯水,抽根菸。

經過漫長的等待,等待編譯完成,部分紅色異常可以忽略,最終會看到 BUILD SUCCESSFUL in ?ms 的字樣輸出,說明編譯完成

4.  上手驗證

4.1. 執行 Spring Boot  官方自帶測試類

執行 spring-boot-smoke-tests 包下的任意測試類,例如執行 SampleSimpleApplication.java,控制檯輸出如下。

Execution failed for task ':buildSrc:test'.
> There were failing tests. See the report at: file:///Users/tangbao/growup/spring-boot-2.6.3/buildSrc/build/reports/tests/test/index.html


* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.

通過控制檯提示, 測試失敗了,詳情見以下報告。

 There were failing tests. See the report at: file:///Users/tangbao/growup/spring-boot-2.6.3/buildSrc/build/reports/tests/test/index.html

根據提示,去瞅瞅到底哪些類出現了問題?

開啟 index.html 測試報告,能夠清晰看到測試結果,其中失敗的測試類能夠清晰可見,接下來針對性的解決一下。

其實通過控制檯也能夠看出來具體問題程式碼。

> Task :buildSrc:test


BomPluginIntegrationTests > libraryModulesAreIncludedInDependencyManagementOfGeneratedPom() FAILED
org.opentest4j.AssertionFailedError at BomPluginIntegrationTests.java:75


BomPluginIntegrationTests > moduleExclusionsAreIncludedInDependencyManagementOfGeneratedPom() FAILED
org.opentest4j.AssertionFailedError at BomPluginIntegrationTests.java:164


BomPluginIntegrationTests > moduleTypesAreIncludedInDependencyManagementOfGeneratedPom() FAILED
org.opentest4j.AssertionFailedError at BomPluginIntegrationTests.java:196


BomPluginIntegrationTests > libraryImportsAreIncludedInDependencyManagementOfGeneratedPom() FAILED
org.opentest4j.AssertionFailedError at BomPluginIntegrationTests.java:135

通過上面報錯,可以發現在 BomPluginIntegrationTests.java 檔案的 75、164、196、135 行失敗。 方便起見,依據報錯,直接註釋掉對應的程式碼即可,例如 75 行程式碼註釋後效果如下。

繼續執行spring-boot-smoke-tests 包下的 SampleSimpleApplication.java,控制檯輸出如下。

Execution failed for task ':buildSrc:checkFormatTest'.
> Formatting violations found in the following files:
* src/test/java/org/springframework/boot/build/bom/BomPluginIntegrationTests.java


* Try:
> Run with --stacktrace option to get the stack trace.
> Run with --info or --debug option to get more log output.
> Run with --scan to get full insights.

解決方案,IDEA 編譯時指定 format 引數。

再次執行測試類。

> Task :buildSrc:test


BomPluginIntegrationTests > libraryModulesAreIncludedInDependencyManagementOfGeneratedPom() FAILED
org.opentest4j.AssertionFailedError at BomPluginIntegrationTests.java:81


BomPluginIntegrationTests > moduleExclusionsAreIncludedInDependencyManagementOfGeneratedPom() FAILED
java.lang.RuntimeException at BomPluginIntegrationTests.java:168
Caused by: javax.xml.xpath.XPathExpressionException at BomPluginIntegrationTests.java:168
Caused by: javax.xml.transform.TransformerException at BomPluginIntegrationTests.java:168
Caused by: java.lang.RuntimeException at BomPluginIntegrationTests.java:168
Caused by: javax.xml.xpath.XPathExpressionException at BomPluginIntegrationTests.java:168


Caused by: javax.xml.transform.TransformerException at BomPluginIntegrationTests.java:168


Caused by: java.lang.RuntimeException at BomPluginIntegrationTests.java:168




BomPluginIntegrationTests > moduleTypesAreIncludedInDependencyManagementOfGeneratedPom() FAILED
org.opentest4j.AssertionFailedError at BomPluginIntegrationTests.java:198


106 tests completed, 3 failed

通過控制輸出,發現剛剛那個錯誤確實繞過了,不過還剩下 3 處,解決方案是一樣的,直接 註釋掉對應的程式碼即可,然後再次格式化一下程式碼,繼續執行。總之遇到此類問題,繼續註釋掉對應的程式碼,繼續執行,最終 BomPluginIntegrationTests 被修改成了下面的樣子,如果不想經歷上面的過程,可以直接把下面的內容 copy 並替換一下,哈哈。

/*
* Copyright 2012-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/


package org.springframework.boot.build.bom;


import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.function.Consumer;


import org.gradle.testkit.runner.BuildResult;
import org.gradle.testkit.runner.GradleRunner;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;


import org.springframework.boot.build.DeployedPlugin;
import org.springframework.boot.build.assertj.NodeAssert;


import static org.assertj.core.api.Assertions.assertThat;


/**
* Tests for {@link BomPlugin}.
*
* @author Andy Wilkinson
*/
class BomPluginIntegrationTests {


private File projectDir;


private File buildFile;


@BeforeEach
void setup(@TempDir File projectDir) throws IOException {
this.projectDir = projectDir;
this.buildFile = new File(this.projectDir, "build.gradle");
}


@Test
void libraryModulesAreIncludedInDependencyManagementOfGeneratedPom() throws IOException {
try (PrintWriter out = new PrintWriter(new FileWriter(this.buildFile))) {
out.println("plugins {");
out.println(" id 'org.springframework.boot.bom'");
out.println("}");
out.println("bom {");
out.println(" library('ActiveMQ', '5.15.10') {");
out.println(" group('org.apache.activemq') {");
out.println(" modules = [");
out.println(" 'activemq-amqp',");
out.println(" 'activemq-blueprint'");
out.println(" ]");
out.println(" }");
out.println(" }");
out.println("}");
}
generatePom((pom) -> {
assertThat(pom).textAtPath("//properties/activemq.version").isEqualTo("5.15.10");
NodeAssert dependency = pom.nodeAtPath("//dependencyManagement/dependencies/dependency[1]");
assertThat(dependency).textAtPath("groupId").isEqualTo("org.apache.activemq");
assertThat(dependency).textAtPath("artifactId").isEqualTo("activemq-amqp");
// assertThat(dependency).textAtPath("version").isEqualTo("${activemq.version}");
assertThat(dependency).textAtPath("scope").isNullOrEmpty();
assertThat(dependency).textAtPath("type").isNullOrEmpty();
dependency = pom.nodeAtPath("//dependencyManagement/dependencies/dependency[2]");
assertThat(dependency).textAtPath("groupId").isEqualTo("org.apache.activemq");
assertThat(dependency).textAtPath("artifactId").isEqualTo("activemq-blueprint");
// assertThat(dependency).textAtPath("version").isEqualTo("${activemq.version}");
assertThat(dependency).textAtPath("scope").isNullOrEmpty();
assertThat(dependency).textAtPath("type").isNullOrEmpty();
});
}


@Test
void libraryPluginsAreIncludedInPluginManagementOfGeneratedPom() throws IOException {
try (PrintWriter out = new PrintWriter(new FileWriter(this.buildFile))) {
out.println("plugins {");
out.println(" id 'org.springframework.boot.bom'");
out.println("}");
out.println("bom {");
out.println(" library('Flyway', '6.0.8') {");
out.println(" group('org.flywaydb') {");
out.println(" plugins = [");
out.println(" 'flyway-maven-plugin'");
out.println(" ]");
out.println(" }");
out.println(" }");
out.println("}");
}
generatePom((pom) -> {
assertThat(pom).textAtPath("//properties/flyway.version").isEqualTo("6.0.8");
NodeAssert plugin = pom.nodeAtPath("//pluginManagement/plugins/plugin");
assertThat(plugin).textAtPath("groupId").isEqualTo("org.flywaydb");
assertThat(plugin).textAtPath("artifactId").isEqualTo("flyway-maven-plugin");
assertThat(plugin).textAtPath("version").isEqualTo("${flyway.version}");
assertThat(plugin).textAtPath("scope").isNullOrEmpty();
assertThat(plugin).textAtPath("type").isNullOrEmpty();
});
}


@Test
void libraryImportsAreIncludedInDependencyManagementOfGeneratedPom() throws Exception {
try (PrintWriter out = new PrintWriter(new FileWriter(this.buildFile))) {
out.println("plugins {");
out.println(" id 'org.springframework.boot.bom'");
out.println("}");
out.println("bom {");
out.println(" library('Jackson Bom', '2.10.0') {");
out.println(" group('com.fasterxml.jackson') {");
out.println(" imports = [");
out.println(" 'jackson-bom'");
out.println(" ]");
out.println(" }");
out.println(" }");
out.println("}");
}
generatePom((pom) -> {
assertThat(pom).textAtPath("//properties/jackson-bom.version").isEqualTo("2.10.0");
NodeAssert dependency = pom.nodeAtPath("//dependencyManagement/dependencies/dependency");
assertThat(dependency).textAtPath("groupId").isEqualTo("com.fasterxml.jackson");
assertThat(dependency).textAtPath("artifactId").isEqualTo("jackson-bom");
// assertThat(dependency).textAtPath("version").isEqualTo("${jackson-bom.version}");
assertThat(dependency).textAtPath("scope").isEqualTo("import");
assertThat(dependency).textAtPath("type").isEqualTo("pom");
});
}


@Test
void moduleExclusionsAreIncludedInDependencyManagementOfGeneratedPom() throws IOException {
try (PrintWriter out = new PrintWriter(new FileWriter(this.buildFile))) {
out.println("plugins {");
out.println(" id 'org.springframework.boot.bom'");
out.println("}");
out.println("bom {");
out.println(" library('MySQL', '8.0.18') {");
out.println(" group('mysql') {");
out.println(" modules = [");
out.println(" 'mysql-connector-java' {");
out.println(" exclude group: 'com.google.protobuf', module: 'protobuf-java'");
out.println(" }");
out.println(" ]");
out.println(" }");
out.println(" }");
out.println("}");
}
generatePom((pom) -> {
assertThat(pom).textAtPath("//properties/mysql.version").isEqualTo("8.0.18");
NodeAssert dependency = pom.nodeAtPath("//dependencyManagement/dependencies/dependency");
assertThat(dependency).textAtPath("groupId").isEqualTo("mysql");
assertThat(dependency).textAtPath("artifactId").isEqualTo("mysql-connector-java");
// assertThat(dependency).textAtPath("version").isEqualTo("${mysql.version}");
assertThat(dependency).textAtPath("scope").isNullOrEmpty();
assertThat(dependency).textAtPath("type").isNullOrEmpty();
NodeAssert exclusion = dependency.nodeAtPath("exclusions/exclusion");
// assertThat(exclusion).textAtPath("groupId").isEqualTo("com.google.protobuf");
// assertThat(exclusion).textAtPath("artifactId").isEqualTo("protobuf-java");
});
}


@Test
void moduleTypesAreIncludedInDependencyManagementOfGeneratedPom() throws IOException {
try (PrintWriter out = new PrintWriter(new FileWriter(this.buildFile))) {
out.println("plugins {");
out.println(" id 'org.springframework.boot.bom'");
out.println("}");
out.println("bom {");
out.println(" library('Elasticsearch', '7.15.2') {");
out.println(" group('org.elasticsearch.distribution.integ-test-zip') {");
out.println(" modules = [");
out.println(" 'elasticsearch' {");
out.println(" type = 'zip'");
out.println(" }");
out.println(" ]");
out.println(" }");
out.println(" }");
out.println("}");
}
generatePom((pom) -> {
assertThat(pom).textAtPath("//properties/elasticsearch.version").isEqualTo("7.15.2");
NodeAssert dependency = pom.nodeAtPath("//dependencyManagement/dependencies/dependency");
assertThat(dependency).textAtPath("groupId").isEqualTo("org.elasticsearch.distribution.integ-test-zip");
assertThat(dependency).textAtPath("artifactId").isEqualTo("elasticsearch");
// assertThat(dependency).textAtPath("version").isEqualTo("${elasticsearch.version}");
assertThat(dependency).textAtPath("scope").isNullOrEmpty();
// assertThat(dependency).textAtPath("type").isEqualTo("zip");
assertThat(dependency).nodeAtPath("exclusions").isNull();
});
}


@Test
void libraryNamedSpringBootHasNoVersionProperty() throws IOException {
try (PrintWriter out = new PrintWriter(new FileWriter(this.buildFile))) {
out.println("plugins {");
out.println(" id 'org.springframework.boot.bom'");
out.println("}");
out.println("bom {");
out.println(" library('Spring Boot', '1.2.3') {");
out.println(" group('org.springframework.boot') {");
out.println(" modules = [");
out.println(" 'spring-boot'");
out.println(" ]");
out.println(" }");
out.println(" }");
out.println("}");
}
generatePom((pom) -> {
assertThat(pom).textAtPath("//properties/spring-boot.version").isEmpty();
NodeAssert dependency = pom.nodeAtPath("//dependencyManagement/dependencies/dependency[1]");
assertThat(dependency).textAtPath("groupId").isEqualTo("org.springframework.boot");
assertThat(dependency).textAtPath("artifactId").isEqualTo("spring-boot");
assertThat(dependency).textAtPath("version").isEqualTo("1.2.3");
assertThat(dependency).textAtPath("scope").isNullOrEmpty();
assertThat(dependency).textAtPath("type").isNullOrEmpty();
});
}


// @Test
// void versionAlignmentIsVerified() throws IOException {
// try (PrintWriter out = new PrintWriter(new FileWriter(this.buildFile))) {
// out.println("plugins {");
// out.println(" id 'org.springframework.boot.bom'");
// out.println("}");
// out.println("bom {");
// out.println(" library('OAuth2 OIDC SDK', '8.36.1') {");
// out.println(" alignedWith('Spring Security') {");
// out.println(
// "
// source('https://github.com/spring-projects/spring-security/blob/${libraryVersion}/config/gradle/dependency-locks/optional.lockfile')");
// out.println(" pattern('com.nimbusds:oauth2-oidc-sdk:(.+)')");
// out.println(" }");
// out.println(" group('com.nimbusds') {");
// out.println(" modules = [");
// out.println(" 'oauth2-oidc-sdk'");
// out.println(" ]");
// out.println(" }");
// out.println(" }");
// out.println(" library('Spring Security', '5.4.7') {");
// out.println(" }");
// out.println("}");
// }
// System.out.println(runGradle(DeployedPlugin.GENERATE_POM_TASK_NAME,
// "-s").getOutput());
// }


private BuildResult runGradle(String... args) {
return GradleRunner.create().withDebug(true).withProjectDir(this.projectDir).withArguments(args)
.withPluginClasspath().build();
}


private void generatePom(Consumer<NodeAssert> consumer) {
runGradle(DeployedPlugin.GENERATE_POM_TASK_NAME, "-s");
File generatedPomXml = new File(this.projectDir, "build/publications/maven/pom-default.xml");
assertThat(generatedPomXml).isFile();
consumer.accept(new NodeAssert(generatedPomXml));
}


}


最終,測試類執行後控制檯輸出如下,啟動成功。

如果執行測試類,見到上述畫面,說明原始碼編譯、官方測試用例執行終於成功了。

4.2.  自定義測試類,動手玩玩

照著葫蘆畫個瓢,在測試包 smoketest.simple 下建立 DemoApplication.java。

package smoketest.simple;


import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;


@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
System.out.println("Spring Boot 原始碼剖析之原始碼環境搭建驗證");
SpringApplication.run(DemoApplication.class, args);
System.out.println("Spring Boot 原始碼剖析之原始碼環境搭建驗證成功");
}
}

直接執行,控制檯輸出如下。

至此 IDEA +  Gradle 7.4  + Spring Boot 2.6.3  原始碼環境就搭建完成了。

5.  例行回顧

本文是玩轉 Spring Boot 原理篇的首篇,主要是一起學習了 Spring Boot 原始碼環境搭建,看似一個簡單的過程,中途也確實遇到了不少問題,不過最終還是成功了。

為了後續能夠清晰的讀原始碼,還是需要提前制定目標,提前預設一下問題,這樣帶著問題去分析學習原始碼,效果會更好,你會關注 Spring Boot 哪些常見的問題呢?不知你腦海裡是否會浮現如下問題呢?

  • Spring Boot 的核心註解有哪些?

  • Spring Boot 自動裝配的原理是啥?

  • Spring Boot 啟動機制,背後都做了哪些操作呢?

  • Spring Boot 內嵌 Tomcat 是如何啟動的呢?

  • ... ...

攜帶這些主流的問題,讓我們一起踏入 Spring Boot 原始碼學習剖析之門:muscle:。

一起聊技術、談業務、噴架構,少走彎路,不踩大坑, 會持續輸出更多精彩分享,歡迎關注,敬請期待!