跳至主要内容

[Spring Boot] Start from scratch

Ref: 從無到有上手你的第一個 Spring Boot 應用程式 | The Will Will Web

Spring Boot 正在 Java 界颳起一陣旋風, Spring Boot 整合了一大堆好用的, 現成的套件, 然後設計一些簡潔的程式架構, 搭配 IoC 與 AOP 大幅簡化開發的複雜度, 也減少了許多繁瑣的設定步驟.

這篇文章大量抄襲 從無到有上手你的第一個 Spring Boot 應用程式 | The Will Will Web 手刻 Spring Boot 應用程式, 從無到有解剖整個開發與啟動過程.

Maven Project

在新資料夾進行 mkdir sandbox/helloworld

pom.xml for Apache Maven

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.duotify</groupId>
<artifactId>app1</artifactId>
<version>0.0.1-SNAPSHOT</version>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.9</version>
</parent>

<!-- Additional lines to be added here... -->

</project>

建立後執行 mvn package 就會產生 target/app1-0.0.1-SNAPSHOT.jar 檔案, 不過這個檔案很小, 目前沒有什麼實質內容, 所以這個 jar 檔是沒有用的.

Spring Boot Parent POM

剛剛的 pom.xml 檔案可以看到以下 <parent> 片段, 明確的指定了一個名為 spring-boot-starter-parentParent POM 檔:

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.9</version>
</parent>

Spring Boot 框架整理了許多 Starters 套件, 可以大幅簡化在開發不同應用程式時的上手門檻. 而這份 spring-boot-starter-parent POM 檔裡面, 就定義了所有 Starters 套件的預設值. 雖然這些套件不一定會用到, 但是當需要用到的時候, 也不用花時間瞭解設定, 因為 Spring Boot 已經全部把大多數人都會設定的屬性(Properties) / 套件版本(version) / 常用的 Plugins, 全部都寫在這份 Parent POM 檔中, 並自動繼承給 Spring Boot 專案.

可以從以下路徑找到 spring-boot-starter-parent 這個 POM 檔:

~/.m2/repository/org/springframework/boot/spring-boot-starter-parent/2.7.9/spring-boot-starter-parent-2.7.9.pom

加入套件相依性

Spring Boot 就是一個 Java 應用程式, 應用程式所需參考到的那些 JARs 檔, 完全可以透過 Maven 或 Gradle 來進行管裡. 以 Maven 為例, spring-boot-starter-parent 這個 POM 檔, 透過 Dependency Management 機制, 預先定義好了會用到的相依套件, 也指定好了版本資訊. 所以在你專案下的 pom.xml 是不需要指定 <version> 版本資訊的, 直接使用 groupId<artifactId> 就可以順利的載入相依套件.

這只是 Maven 的繼承效果, 想要自己決定想採用的版本, 還是可以加上 <version> 元素來指定版本. 不過最好思考一下為什麼要這麼做? 因為如果未來想要將 Spring Boot 升級版本時, 只要調整一下 spring-boot-starter-parent 這個 Parent POM 的版本, 所有「測試過的」相依套件就會一併升級到沒問題的版本, 自己指定套件版本反而是有升級風險的.

可以利用 mvn dependency:tree 查看專案的套件相依資訊, 此時只會看到一個 com.duotify:app1:jar:0.0.1-SNAPSHOT 套件 (就是目前專案) 而已, 因為並沒有在 pom.xml 宣告使用任何相依套件:

mvn dependency:tree

[INFO] com.duotify:app1:jar:0.0.1-SNAPSHOT

要用 Spring 來開發 Web 的話 (包含 MVC 或 API 開發), 一般會使用 spring-boot-starter-web 這個 Starters 套件, 只要加入 spring-boot-starter-webpom.xml 之中即可:

<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

同前, 一般來說不需要加上 <version> 元素, 直接繼承使用 Parent POM 定義的版本才是最佳實務(Best Practices).

加完之後的 pom.xml 檔案內容如下:

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.duotify</groupId>
<artifactId>app1</artifactId>
<version>0.0.1-SNAPSHOT</version>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
</parent>

<!-- Additional lines to be added here... -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

</project>

再執行一次 mvn dependency:tree 查看專案的套件相依資訊, 就會非常多了:

[INFO] com.duotify:app1:jar:0.0.1-SNAPSHOT
[INFO] \- org.springframework.boot:spring-boot-starter-web:jar:2.7.3:compile
[INFO] +- org.springframework.boot:spring-boot-starter:jar:2.7.3:compile
[INFO] | +- org.springframework.boot:spring-boot:jar:2.7.3:compile
[INFO] | +- org.springframework.boot:spring-boot-autoconfigure:jar:2.7.3:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-logging:jar:2.7.3:compile
[INFO] | | +- ch.qos.logback:logback-classic:jar:1.2.11:compile
[INFO] | | | +- ch.qos.logback:logback-core:jar:1.2.11:compile
[INFO] | | | \- org.slf4j:slf4j-api:jar:1.7.36:compile
[INFO] | | +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.17.2:compile
[INFO] | | | \- org.apache.logging.log4j:log4j-api:jar:2.17.2:compile
[INFO] | | \- org.slf4j:jul-to-slf4j:jar:1.7.36:compile
[INFO] | +- jakarta.annotation:jakarta.annotation-api:jar:1.3.5:compile
[INFO] | +- org.springframework:spring-core:jar:5.3.22:compile
[INFO] | | \- org.springframework:spring-jcl:jar:5.3.22:compile
[INFO] | \- org.yaml:snakeyaml:jar:1.30:compile
[INFO] +- org.springframework.boot:spring-boot-starter-json:jar:2.7.3:compile
[INFO] | +- com.fasterxml.jackson.core:jackson-databind:jar:2.13.3:compile
[INFO] | | +- com.fasterxml.jackson.core:jackson-annotations:jar:2.13.3:compile
[INFO] | | \- com.fasterxml.jackson.core:jackson-core:jar:2.13.3:compile
[INFO] | +- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar:2.13.3:compile
[INFO] | +- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.13.3:compile
[INFO] | \- com.fasterxml.jackson.module:jackson-module-parameter-names:jar:2.13.3:compile
[INFO] +- org.springframework.boot:spring-boot-starter-tomcat:jar:2.7.3:compile
[INFO] | +- org.apache.tomcat.embed:tomcat-embed-core:jar:9.0.65:compile
[INFO] | +- org.apache.tomcat.embed:tomcat-embed-el:jar:9.0.65:compile
[INFO] | \- org.apache.tomcat.embed:tomcat-embed-websocket:jar:9.0.65:compile
[INFO] +- org.springframework:spring-web:jar:5.3.22:compile
[INFO] | \- org.springframework:spring-beans:jar:5.3.22:compile
[INFO] \- org.springframework:spring-webmvc:jar:5.3.22:compile
[INFO] +- org.springframework:spring-aop:jar:5.3.22:compile
[INFO] +- org.springframework:spring-context:jar:5.3.22:compile
[INFO] \- org.springframework:spring-expression:jar:5.3.22:compile

Hello World

第一支 Spring Boot 程式

基本專案設定完成後, 撰寫第一支 Spring Boot Java 程式. 建立 src/main/java/com/duotify/app1/MyApplication.java 檔案.

helloworld.java
package com.duotify.app1;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@EnableAutoConfiguration
public class MyApplication {

@RequestMapping("/")
String home() {
return "Hello World!";
}

public static void main(String[] args) {
SpringApplication.run(MyApplication.class, args);
}

}

Spring Boot 框架設計上大量使用了依賴注入來減少程式耦合, 程式中透過類別標注 (Annotations) 來提高程式可讀性之外, 也讓 Spring Framework 透過 Component scanning 找到相對硬的服務.

類別上的 @RestController 標注 (Annotations), 在 Spring 裡面又被稱為是一種 Stereotype Annotations (刻板印象標注). 這可以提高程式碼的可讀性, 讓熟悉Spring 框架的開發人員看到 @RestController 標注, 就一目了然的知道這個類別其實就是一個支援 REST 功能的 Controller (控制器). 這樣的設計會讓開發人員自動形成一種刻板印象 (Stereotype), 看到這類標注就會自動識別這個類別的角色與用途, 猜想是官方所以用 Stereotype 這個單字的主因.

除此之外, Stereotype Annotations 還可以賦予「類別」一個角色, 讓 Spring Framework 可以透過 Component scanning 快速的找到相對應的服務. Spring 內建的 Stereotype Annotations 可以從 org.springframework.stereotype 查閱, 基本上所有的 Stereotype Annotations 都會繼承自 org.springframework.stereotype.Component (@Component) 型別.

類別上的 @EnableAutoConfiguration 標注 (Annotations) 則會讓 Spring Boot 自動找出所有相依套件中 JAR 檔的類別, 並自動建立與註冊成 Spring Beans 元件, 讓 Spring Boot 可以在需要的時候使用這些可重複利用這些的 Spring Beans 元件.

  • home() 方法上的 @RequestMapping("/") 是定義控制器的路由, 決定網址的結構.

啟動 Spring Boot 網站

mvn spring-boot:run

透過瀏覽器開啟 http://localhost:8080/ 即可看到網站

Package & Deploy

打包應用程式 *.jar 檔

現在執行 mvn package 封裝 target/app1-0.0.1-SNAPSHOT.jar 檔案的話, 檔案依然只有 2.4KB 而已. 因為目前只有打包 MyApplication, 並不包含 Tomcat 之類的套件, 還無法成為一個可以獨立運作的執行檔.

Java 並沒有提供一種稱為 Nested JAR 的封裝方式, 也就是在一個 JAR 檔裡面包含其他需要用到的 JAR 檔. 所以如果要部署一個包含相依套件的應用程式, 就會需要部署好幾個檔案, 使用上較為不便. 想要發佈一個__自我包含所有 JAR 檔的 JAR 可執行檔(self-contained executable jar file)__, 通常會把應用程式打包成俗稱 ÜBER JARFAT JAR 的格式. 詳見 The Executable Jar Format 文件說明.

Spring Boot 亦可打包成 Tomcat 所支援的 WAR 格式, 另篇討論.

若應用程式封裝打包的需求, 可透過 spring-boot-maven-plugin plugin 達成目的. 只要在專案的 pom.xml 加入以下設定即可:

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

加完之後的 pom.xml 檔案內容如下:

pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>com.duotify</groupId>
<artifactId>app1</artifactId>
<version>0.0.1-SNAPSHOT</version>

<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.3</version>
</parent>

<!-- Additional lines to be added here... -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>

</project>

理論上 spring-boot-maven-plugin plugin 還有 <executions><configuration> 需要設定才對. 但是 Spring Boot 提供的 Parent POM 已經設定了這些內容, 所以才會看起來這麼簡單!

  • spring-boot-maven-plugin configuration

    spring-boot-maven-plugin plugin 完整的設定內容如下 (從 spring-boot-starter-parent PQM 取出):

    <plugin>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-maven-plugin</artifactId>
    <executions>
    <execution>
    <id>repackage</id>
    <goals>
    <goal>repackage</goal>
    </goals>
    </execution>
    </executions>
    <configuration>
    <mainClass>${start-class}</mainClass>
    </configuration>
    </plugin>

加入 spring-boot-maven-plugin 敘述後執行 mvn package 封裝 target/app1-0.0.1-SNAPSHOT.jar, 這個時候檔案大小就有 17MB 了, 本身包含了 Tomcat 套件而且可以獨立運作執行.

圖中的 app1-0.0.1-SNAPSHOT.jar.original 是應用程式原始的 JAR 檔, 因為被 spring-boot-maven-plugin 執行過 repackage 目標(Goal), 所以在執行__重新封裝__(repackage)時加入了 Tomcat 進去.

執行 java -jar target/app1-0.0.1-SNAPSHOT.jar, Spring Boot 應用程式可以順利執行並啟動了! 👍

Docker

See: Containerizing Spring Boot Application

Summary

Spring Boot 提供一套簡潔的架構, 可以快速完成任務. 但是神奇的架構背後, 其實有很多值得探討的地方. 越是抽絲剝繭釐清了背後的原理之後, 才有辦法舉一反三. 思考並在正確的時間點做出正確的技術決策.

See Also