Spring Boot Overview

Spring Boot 透過「設定優先於配置」(Convention over Configuration) 及其預設的 Starter 依賴,簡化了新 Spring 應用程式的開發。


Spring Boot 3.x

Three Stages

  flowchart TB
    S[listeners.starting 啟動開始]
    pEnv[prepareEnvironment 準備環境]
    pCtx[prepareContext 準備上下文]
    rCtx[refreshContext 刷新上下文]
    s[startup.started 啟動完成]
    r[ready App 準備就緒]
    F[failed 失敗]

    S ===>|準備環境變數| pEnv ===>|準備並設定上下文| pCtx ===>|刷新 Ctx|rCtx ====>|Context 啟動完成|s ===>|執行 Runners| r

    pEnv ---->|Error|F
    pCtx ---->|Error|F
    rCtx ---->|Error|F
    s ---->|Error|F
    r ---->|Error|F

Prepared (準備階段)

I. Constructor

public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
    //...
}
  1. 判定 Application 類型
this.webApplicationType = WebApplicationType.deduceFromClasspath();
  1. 使用 class SpringFactoriesLoader (SPI 機制) 載入 (META-INF/spring.factories) Key 所有可實例化之初始器與監聽器 (SpringApplicationRunListener 的子類別)

  2. 設定所有 Application 事件監聽器

this.bootstrapRegistryInitializers = new ArrayList<>(getSpringFactoriesInstances(BootstrapRegistryInitializer.class));
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
  1. 從 StackFrame 中找出 App Main Class
this.mainApplicationClass = deduceMainApplicationClass();

II. Spring Application Instance.run

public ConfigurableApplicationContext run(String... args) {
    //...
    try {
        //...
    }
    catch (Throwable ex) {
        throw handleRunFailure(context, ex, listeners);
    }
    try {
        if (context.isRunning()) {
            listeners.ready(context, startup.ready());
        }
    }
    catch (Throwable ex) {
        throw handleRunFailure(context, ex, null);
    }
    return context;
}
  1. Startup startup = Startup.create() : 記錄啟動效能與追蹤。
  2. DefaultBootstrapContext bootstrapContext = createBootstrapContext();
    • 建立一個「引導上下文」,用於在初始階段緩存、共享或傳遞程式碼中所需的物件實例,供上下文關聯與設定。
    • 在正式 Spring Context 建立前用來共享物件。
  3. configureHeadlessProperty(); : java.awt.headless 設定, default=true
    • 獲取所有註冊於 SpringApplicationRunListener 的監聽器 (listeners)。
    • 發布 ApplicationStartingEvent,通知所有相關元件 (觀察者模式 Observer Pattern)
SpringApplicationRunListeners listeners = getRunListeners(args);
listeners.starting(bootstrapContext, this.mainApplicationClass);
    • 處理命令列參數。
    • 準備執行時的環境變數。
    • 印出 Banner。
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
Banner printedBanner = printBanner(environment);

Context Created (上下文建立)

  1. 根據 Web App 類型建立 Application Context。
protected ConfigurableApplicationContext createApplicationContext() {
    return this.applicationContextFactory.create(this.webApplicationType);
}
  1. 設定啟動過程的記錄器。
public void setApplicationStartup(ApplicationStartup applicationStartup) {
    this.applicationStartup = (applicationStartup != null) ? applicationStartup : ApplicationStartup.DEFAULT;
}
  1. 準備 Context
    • postProcessApplicationContext() : 注入環境屬性 (properties)。
    • applyInitializers() : 執行在 Prepared 階段獲取的所有初始器 (ApplicationContextInitializer)。
    • listeners.contextPrepared(context) : 通知上下文準備完成。
    • bootstrapContext.close(context) : 關閉引導上下文。
private void prepareContext(DefaultBootstrapContext bootstrapContext, ConfigurableApplicationContext context,
        ConfigurableEnvironment environment, SpringApplicationRunListeners listeners,
        ApplicationArguments applicationArguments, Banner printedBanner)

Refreshed Context (刷新上下文)

連結: SpringApplicationShutdownHook

private void refreshContext(ConfigurableApplicationContext context) {
    if (this.registerShutdownHook) {
        shutdownHook.registerApplicationContext(context);
    }
    refresh(context);
}

After Refreshed (刷新後處理)

afterRefresh(context, applicationArguments);
  • 停止 Startup.create() 的啟動計時。

載入並執行 ApplicationRunner, CommandLineRunner

private void callRunners(ConfigurableApplicationContext context, ApplicationArguments args)

啟動失敗處理

private RuntimeException handleRunFailure(ConfigurableApplicationContext context, Throwable exception,
			SpringApplicationRunListeners listeners)

啟動時可擴充的擴充介面

參考: 這些 16 個 SpringBoot 擴充介面讓程式碼更優雅

  • Spring 容器內 Bean 的生命週期
  flowchart TB
    ACIinit[ApplicationContextInitializer: void initialize]

    AACrf[AbstractApplicationContext: + void refresh]

    BDRPP-pPBDR[BeanDefinitionRegistryPostProcessor: void postProcessBeanDefinitionRegistry]

    BFPP[BeanFactoryPostProcessor: postProcessBeanFactory]

    IABPP-pPBI[InstantiationAwareBeanPostProcessor: postProcessBeforeInstantiation]

    SIABPP-dCC[SmartInstantiationAwareBeanPostProcessor: determineCandidateConstructors]

    MBDPP-pPMBD[MergedBeanDefinitionPostProcessor: postProcessMergedBeanDefinition]

    IABPP-pPAI[InstantiationAwareBeanPostProcessor: postProcessAfterInstantiation]

    SIABPP-gEBR[SmartInstantiationAwareBeanPostProcessor: getEarlyBeanReference]

    BFA-sBF[BeanFactoryAware: setBeanFactory]

    IABPP-pPPVs[InstantiationAwareBeanPostProcessor: postProcessProperties]

    ACAP-iAI[ApplicationContextAwareProcessor: invokeAwareInterfaces]

    BNA-sBN[BeanNameAware: setBeanName]

    IABPP-pPBInit[InstantiationAwareBeanPostProcessor: postProcessBeforeInitialization]

    PC[@PostConstruct]

    InitBean-aPpS[InitializingBean: afterPropertiesSet]

    IABPP-pPAInit[InstantiationAwareBeanPostProcessor: postProcessAfterInitialization]

    FB-gO[FactoryBean: getObject]

    SIS-aSI[SmartInitializingSingleton: afterSingletonsInstantiated]

    CLR-run[CommandLineRunner: run]

    ACIinit ---> AACrf ---> BDRPP-pPBDR ---> BFPP ---> IABPP-pPBI ---> SIABPP-dCC ---> MBDPP-pPMBD ---> IABPP-pPAI ---> SIABPP-gEBR ---> BFA-sBF ---> IABPP-pPPVs ---> ACAP-iAI ---> BNA-sBN ---> IABPP-pPBInit ---> PC --->  InitBean-aPpS ---> IABPP-pPAInit ---> FB-gO ---> SIS-aSI ---> CLR-run
  • ApplicationContextInitializer

用於在 Spring Context refresh 之前進行設定的擴充介面。
通常用於在 SpringBoot 容器中調整 Properties
或是修改 ConfigurableApplicationContext。

public interface ApplicationContextInitializer<C extends ConfigurableApplicationContext> {

	/**
	 * Initialize the given application context.
	 * @param applicationContext the application to configure
	 */
	void initialize(C applicationContext);

}

Spring Bean Lifecycle

  flowchart TB
	c[Constructor]
	a[Autowired]
	p[PostConstruct]
	i[InitializingBean]
	app[ApplicationRunner]
	clr[CommandLineRunner]
	c --> a --> p --> i --> app --> clr
  • @PostConstruct@PreDestroy 註解都是 Java EE 的一部分。
  • 由於 Java EE 在 Java 9 中被棄用,並在 Java 11 中移除。
  • Spring 替代方案:InitializingBean, DisposableBean

class & interface 的深度剖析

SpringApplicationShutdownHook

連結: Threads Related, AbstractApplicationContext

Spring Shutdown Hook 執行時會對線程池進行「強制關閉」,可能導致線程池中的數據遺失或尚未同步。

AbstractApplicationContext

負責在 destroyBeans() 之前關閉線程池與上下文資源。

  1. 發布 class ContextClosedEvent 事件,在事件監聽中優雅關閉線程池。
  2. 調用 interface Lifecycle - stop() 方法。
protected void doClose() {
    // 檢查是否需要執行關閉操作...
    if (this.active.get() && this.closed.compareAndSet(false, true)) {
        //...

        try {
            // 發布關閉事件
            publishEvent(new ContextClosedEvent(this)); // 步驟 1
        }
        catch (Throwable ex) {
            logger.warn("Exception thrown from ApplicationListener handling ContextClosedEvent", ex);
        }

        // 停止所有 Lifecycle beans,避免個別銷毀時的延遲。
        if (this.lifecycleProcessor != null) {
            try {
                this.lifecycleProcessor.onClose(); // 步驟 2
            }
            catch (Throwable ex) {
                logger.warn("Exception thrown from LifecycleProcessor on context close", ex);
            }
        }

        // 銷毀容器內緩存的所有單例 Beans。
        destroyBeans(); // 在銷毀 Beans 之前

        //...

        // 切換為不活躍狀態。
        this.active.set(false);
    }
}

ThreadPoolTaskExecutor

Spring 封裝此類別用於管理線程池。 在需要優雅關閉時,可以調用 destroy() 方法或是配置其生命週期。

@Component
public class ContextClosedHandler implements ApplicationListener<ContextClosedEvent> {

	@Autowired
	private ThreadPoolTaskExecutor threadPoolTaskExecutor;

	@Override
	public void onApplicationEvent(ContextClosedEvent event) {
		threadPoolTaskExecutor.destroy();
	}

	// ...
}

ExecutorConfigurationSupport

public void setWaitForTasksToCompleteOnShutdown(boolean waitForJobsToCompleteOnShutdown) {
    this.waitForTasksToCompleteOnShutdown = waitForJobsToCompleteOnShutdown;
}

public void setAwaitTerminationSeconds(int awaitTerminationSeconds) {
    this.awaitTerminationMillis = awaitTerminationSeconds * 1000L;
}

AbandonedRunException

Spring Boot 啟動 AOT 模式或是啟動時被異常中斷拋出的異常。

SpringBootExceptionReporter

  • FailureAnalyzers

獲取啟動失敗後的 FailureAnalysis 解析。

  • FailureAnalysisReporter

格式化列印失敗解析訊息。

Startup Steps

startup

Events

階段EventSpring Boot 2Spring Boot 3說明
1ApplicationStartingEvent應用程式啟動開始
2ApplicationEnvironmentPreparedEventEnvironment 環境準備完成
3ApplicationContextInitializedEventApplicationContext 初始完成
4ApplicationPreparedEventBean 定義載入完成
5ContextRefreshedEventApplicationContext 刷新完成
6ApplicationStartedEvent啟動完成並準備執行 Runner
7AvailabilityChangeEvent應用程式可用狀態變更 (ACCEPTING_TRAFFIC)
8ApplicationReadyEvent完全就緒,可以處理請求
-ApplicationFailedEvent啟動失敗時觸發

Spring Framework vs Spring Boot

特性(とくせい)Spring FrameworkSpring Boot
位置付(いちづ)包括的(ほうかつてき) なエンタープライズフレームワーク。Spring 基盤(きばん) の「迅速(じんそく)開発(かいはつ) 」ツール。
構成(こうせい)手動(しゅどう) で XML/Java Config を記述(きじゅつ)自動構成(じどうこうせい) (Auto-configuration)。
デプロイ外部(がいぶ) の Servlet コンテナ(Tomcat など)が必要(ひつよう)()() みコンテナ、スタンドアロン実行(じっこう) (Fat JAR)。
依存関係(いぞんかんけい)手動(しゅどう) でライブラリバージョンを管理(かんり)Starter 依存関係(いぞんかんけい) でバージョン管理(かんり)簡素化(かんそか)

DTO パターン

データ転送(てんそう) オブジェクトパターン。(こと) なるレイヤー(Controller と Service など)(かん) でデータを転送(てんそう) するために使用(しよう) され、データベースエンティティの直接公開(ちょくせつこうかい)回避(かいひ) します。


MultipartFile vs File

種類(しゅるい)説明(せつめい)
MultipartFileSpring 固有(こゆう) のインターフェース。アップロードされたファイルストリームを(あらわ) す。
FileJava 標準(ひょうじゅん) クラス。ファイルシステム(ない)物理(ぶつり) パスを(あらわ) す。保存(ほぞん) のため前者(ぜんしゃ) から後者(こうしゃ) への変換(へんかん)通常(つうじょう) 必要(ひつよう)

Glossary (術語表)


A

Actuator

Micrometer

Micrometer 作為各種監控系統的門面, 提供了一種與工具無關的介面來收集指標, 並將指標發布到我們的目標指標收集器。

  flowchart LR
	App[Application]
	M[Micrometer]
	subgraph Monitoring-System
	At[Atlas]
	CW[CloudWatch]
	Dg[Datagog]
	Pmt[Prometheus]
	end

	App --> M --> Monitoring-System

MeterRegistry & Meter

  classDiagram
	class MeterRegistry
	class CloudWatchMeterRegistry
	class PrometheusMeterRegistry

	class Meter
	<<interface>> Meter

	MeterRegistry ..>  Meter
	CloudWatchMeterRegistry --|> MeterRegistry
	PrometheusMeterRegistry --|> MeterRegistry
  • Timer 用於測量短時間延遲以及此類事件的頻率。所有 Timer 的實作至少將總時間和事件計數作為獨立的時間序列報告。
  • Counter 用於測量僅增加的數值。它們可用於計算服務的請求數、完成的任務數、發生的錯誤等。
  • Gauge 表示一個既可以增加也可以減少的數值。Gauge 用於測量目前的 CPU 使用率、緩存大小、隊列中的消息數等值。
  • DistributionSummary
  • LongTaskTimer
  • FunctionCounter
  • FunctionTimer
  • TimeGauge

Auto Configuration

exclude (排除自動配置)

@SpringBootApplication(exclude = {
       RedisAutoConfiguration.class,
       DataSourceAutoConfiguration.class,
       DataSourceTransactionManagerAutoConfiguration.class,
       HibernateJpaAutoConfiguration.class
})

B

C

ClassPathResource

  • 靜態載入 Resource 資料夾中的資源
ClassPathResource resource = new ClassPathResource(file_location);

try (InputStream is = resource.getInputStream();
     InputStreamReader reader = new InputStreamReader(is)) {
     // ...
}

D

Deploy Externally Tomcat

  • 修改 pom.xml
<packaging>war</packaging>
  • 加入依賴 / 排除依賴 (擇一)
<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-web</artifactId>
	<exclusions>
		<exclusion>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-tomcat</artifactId>
		</exclusion>
	</exclusions>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-tomcat</artifactId>
    <scope>provided</scope>
</dependency>
  • public abstract class SpringBootServletInitializer implements WebApplicationInitializer
  • 方式一
@SpringBootApplication
public class RSApplication extends SpringBootServletInitializer {
    public static void main(String[] args) {
        SpringApplication.run(RSApplication.class, args);
    }
}
  • 方式二
public class ServletInitializer extends SpringBootServletInitializer {
    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        // Application.class 是標有 @SpringBootApplication 註解的啟動類別
        return builder.sources(Application.class);
    }
}

E

Embedded Tomcat

連結:

Redirect HTTP To HTTPS

No Spring Security

Way 1
  • Use Both (同時支援)
Way 2
  • HTTP to HTTPS (強制跳轉)
Way 3
  • 指定路徑從 HTTP 跳轉至 HTTPS
  • 使用 Interceptor

F

G

GraalVM

  • GraalVM CLI
  • GraalVM Glossary

Native image

是 GraalVM 中的一種技術,它可以將應用程式與其依賴的 Jar 檔預編譯成一個單獨的可執行二進制檔案。 此二進制檔案已針對特定的平台進行優化,不再依賴 Java 虛擬機運行。

與 JVM 部署的關鍵差異

  • 應用程式的類別路徑 (classpath) 在建置時就已固定,無法更改。
  • 沒有延遲類別載入 (lazy class loading),所有編入執行檔的內容都會在啟動時載入記憶體。

Spring AOT Processing

Restrictions (限制)

  • 類別路徑在建置時固定且完全定義。
  • 定義於應用程式中的 Beans 不能在運行時更改:
    • 不支援 Spring @Profile 註解及其特定設定。
    • 不支援動態創建 Bean 的屬性 (例如 @ConditionalOnProperty)。
  • 不支援 @Profile 註解與特定屬性配置。

處理程序將會產生

  • Java 源代碼
  • 字節碼 (用於動態代理等)
  • GraalVM JSON 提示檔案:
    • 資源提示 (resource-config.json)
    • 反射提示 (reflect-config.json)
    • 序列化提示 (serialization-config.json)
    • Java 代理提示 (proxy-config.json)
    • JNI 提示 (jni-config.json)

H

I

J

Jackson

ObjectMapper

  • SerializationFeature.WRITE_DATES_AS_TIMESTAMPS : 序列化時日期是否使用時間戳記;預設為 true (使用時間戳記);設定為 false 可以將日期格式化為字串。
  • SerializationFeature.FAIL_ON_EMPTY_BEANS: 序列化空物件時是否拋出異常;預設為 true。
  • SerializationFeature.INDENT_OUTPUT : 是否格式化輸出;預設為 false。
  • SerializationFeature.WRITE_NULL_MAP_VALUES : 是否序列化為 null 的鍵值對;預設為 true。
  • DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES : 反序列化時遇到未知屬性時,是否拋出異常;預設為 true。
  • DeserializationFeature.READ_UNKNOWN_ENUM_VALUES_AS_NULL : 是否將未知列舉值反序列化為 null;預設為 false。
  • DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT : 是否將空字串反序列化為 null;預設為 false。
  • JsonParser.Feature.ALLOW_COMMENTS : 是否允許 JSON 中帶有註解;預設為 false。
  • JsonParser.Feature.ALLOW_UNQUOTED_FIELD_NAMES : 是否允許 JSON 中的欄位名稱不使用雙引號;預設為 false。
  • setSerializationInclusion
    • JsonInclude.Include.NON_DEFAULT 屬性為預設值時不序列化。
    • JsonInclude.Include.ALWAYS 始終序列化。
    • JsonInclude.Include.NON_EMPTY 屬性為 空(或 “”) 或為 NULL 時不序列化。
    • JsonInclude.Include.NON_NULL 屬性為 NULL 時不序列化。
mapper.setSerializationInclusion(JsonInclude.Include.ALWAYS);
  • readValue()
mapper.readValue(reader, new TypeReference<T>() {});
  • Enable Pretty Print Json
DefaultPrettyPrinter dpp = new DefaultPrettyPrinter();
mapper.writerWithDefaultPrettyPrinter().writeValueAsString(data);

issues

Java 8 date/time type java.time.LocalDateTime not supported by default

Java 8 date/time type java.time.LocalDateTime not supported by default: add Module “com.fasterxml.jackson.datatype:jackson-datatype-jsr310” to enable handling (through reference chain: net.arplanets.nmns.model.raw.RawNmnsData[“dataDate”])
  • 解決方案
<dependency>
    <groupId>com.fasterxml.jackson.datatype</groupId>
    <artifactId>jackson-datatype-jsr310</artifactId>
    <version>2.13.4</version>
</dependency>
ObjectMapper mapper = new ObjectMapper().registerModule(new JavaTimeModule())

K

L

M

MultivaluedMap

  • 鍵值對 Map。
  • 每個鍵可以擁有 零個或多個值

Multi-threading

  • @EanbleAsync => 配置類別
  • @Async => 方法

N

Executable JARs

O

P

Q

R

Reactive Web

S

Scheduling Tasks

Cron 表達式語法

  • e.g. 秒 分 時 日 月 星期 年
  • 每天 0:00 執行一次定時任務
 @Scheduled(cron = "0 0 0 * * ?")
 public void testCron() {
	 logger.info("===cron: 時間:{}", dateFormat.format(new Date()));
}
  • 每星期一 0:00 執行一次定時任務
 @Scheduled(cron = "0 0 0 ? * MON")
 public void testCron() {
	 logger.info("===cron: 時間:{}", dateFormat.format(new Date()));
}
  • 每月1號 0:00 執行一次定時任務
 @Scheduled(cron = "0 0 0 1 * ?")
 public void testCron() {
	 logger.info("===cron: 時間:{}", dateFormat.format(new Date()));
}

T

Testing

Scope Dependencies

JUnit 5

  • JUnit Platform + JUnit Jupiter + JUnit Vintage
  • Vintage
    • 支援在 JUnit 5 平台上執行基於 JUnit 3JUnit 4 的測試。
  flowchart BT
	subgraph TOP
	YT[Your Test]
	end
	subgraph junit5
	jja[junit-jupiter-api]
	jje[junit-jupiter-engine]
	end
	subgraph junit4
	j4[junit-4.12]
	jve[junit-vintage-engine]
	end
	subgraph totally_made-up_example
	ta[testing-api]
	te[testing-engine]
	end
	jpe[junit-platform-engine]
	jpl[junit-platform-launcher]
	IDE[Intellij IDEA, Eclipse, Apache, Gradle, etc.]

	YT ---->|Written against| jja
	YT ----> j4
	YT ----> ta

	jve ----> j4
	jje ----> jja
	te ----> ta

	jve ----> jpe
	jje ---->|implements| jpe
	te ----> jpe

	jpl ---->|Discovers implements via ServiceLoader, Orchestrates execution| jpe

	IDE ---->|Use exclusively| jpl

JUnit 4

Spring Test

DataJpaTest

  • 使用嵌入式記憶體資料庫 (H2)
  • 自動配置 Transaction, Auto Rollback。
    • 若想提交至 in-memory database, 標註 @Commit

WebMvcTest

  • MockMvcRequestBuilders
  • MockMvcResultHandlers
單元測試中是否包含以下項目?
  1. 監聽 HTTP 請求? -> 否,因為單元測試不會評估 @PostMapping 及其相關屬性。
  2. 反序列化輸入? -> 否,因為 @RequestParam@PathVariable 等註解不會被評估。
  3. 驗證輸入? -> 否,如果不依賴 Bean Validation,@Valid 註解將不被評估。
  4. 調用業務邏輯? -> 是,我們可以驗證 Mock 的業務邏輯是否以預期的參數被調用。
  5. 序列化輸出? -> 否,因為我們只能驗證輸出的 Java 物件,而非生成 HTTP 回應。
  6. 轉換異常? -> 我們可以檢查是否拋出了特定異常,但無法確認它是否被轉換為特定 JSON 或 HTTP 狀態碼。
單純的單元測試不會涵蓋 HTTP 層

AssertJ

  • A fluent assertion library (流暢的斷言庫)。

Hamcrest

Mockito

JSONassert

JsonPath

Version Naming Scheme (版本命名慣例)

  • BUILD-SNAPSHOT : 目前開發中的快照版本。
  • M<number> : Milestone (里程碑) 版本,表示發布過程中的重大階段。
  • RELEASE : 正式發布版。
  • GA : General Availability,正式發布穩定商用版,代表該版本經過廣泛測試並建議在生產環境中使用。
  • SNAPSHOT : 開發版,可以隨時被修改,不建議在生產環境中使用。
  • RC<number> :
    • Release Candidate (發布候選版),通常是正式發布前的最後一步。

Umbrella Projects (雨傘專案)

如 Spring Cloud 與 Spring Data,由多個獨立但相關的子專案組成。 每個 Release Train (發布列車) 都有一個特殊的名稱,而非單一版本號。

U

V

W

X

Y

Z