2025/08/04

Spring Boot 3 打包

可用 jar, war, docker, GraalVM image 方式打包

jar

用 jar application 方式打包

<packaging>jar</packaging>

同時要加上

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

使用兩種指令打包,二選一

  • mvn package

    打包到 target 目錄

  • mvn install

    打包後,安裝到本地的 MAVEN repository


打包後,會產生兩個

  • test-0.0.1-SNAPSHOT.jar

    這個可透過 java -jar test-0.0.1-SNAPSHOT.jar 啟動

  • test-0.0.1-SNAPSHOT.jar.original


META-INF/MANIFEST.MF 內容為

Manifest-Version: 1.0
Created-By: Maven JAR Plugin 3.4.2
Build-Jdk-Spec: 17
Implementation-Title: test
Implementation-Version: 0.0.1-SNAPSHOT
Main-Class: org.springframework.boot.loader.launch.JarLauncher
Start-Class: com.test.test.TestApplication
Spring-Boot-Version: 3.4.0
Spring-Boot-Classes: BOOT-INF/classes/
Spring-Boot-Lib: BOOT-INF/lib/
Spring-Boot-Classpath-Index: BOOT-INF/classpath.idx
Spring-Boot-Layers-Index: BOOT-INF/layers.idx

另一個 test-0.0.1-SNAPSHOT.jar.original,無法直接啟動,只是一個 jar


war

要先產生一個繼承 SpringBootServletInitializer 抽象類別

package com.test.test;

import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

public class ServletInitializer extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder application) {
        return application.sources(TestApplication.class);
    }

}

修改打包方式

<packaging>war</packaging>

排除 embedded tomcat

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-tomcat</artifactId>
            <scope>provided</scope>
        </dependency>

可同時支援 jar 跟 war

package com.test.test;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.web.servlet.support.SpringBootServletInitializer;

@SpringBootApplication
public class TestApplication extends SpringBootServletInitializer {

    @Override
    protected SpringApplicationBuilder configure(SpringApplicationBuilder builder) {
        return startBuilder(builder);
    }

    public static void main(String[] args) {
//        SpringApplication.run(TestApplication.class, args);
        startBuilder(new SpringApplicationBuilder()).run(args);
    }

    private static SpringApplicationBuilder startBuilder(SpringApplicationBuilder builder) {
        return builder.sources(TestApplication.class);
    }

}

build 會產生 test-0.0.1-SNAPSHOT.war,可以放在 tomcat 或是用 java -jar 啟動


啟動 application

使用 java 指令

java -jar test-0.0.1-SNAPSHOT-exec.war

直接啟動

在 pom.xml 要修改設定

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

然侯就能直接運作

./test-0.0.1-SNAPSHOT-exec.war

note: 在不支援的 OS 要修改 pom.xml

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <classifier>exec</classifier>
                    <executable>true</executable>
                    <embeddedLaunchScript>...</embeddedLaunchScript>
                </configuration>
            </plugin>
        </plugins>
    </build>

系統 service

可用 init.d 或 systemd service 啟動

init.d

在 /etc/init.d 建立一個 link

sudo ln -s test-0.0.1-SNAPSHOT.war /etc/init.d/test

然後就能用 service 啟動

sudo service test start

systemd

在 /etc/systemd/system 目錄中,建立檔案 test.service

[Unit]
Description-test
After=syslog.target

[Service]
User=test
ExecStart=/home/test/test-0.0.1-SNAPSHOT.war
# process receive SIGTERM signal will return 128+15=143 exit status code
SuccessExitStatus=143

[Install]
WantedBy=multi-user.target

然後

sudo systemctl start test
sudo systemctl enable test

Docker

docker -v
docker version

以 Dockerfile 產生 image

FROM eclipse-temurin:17-jre
ARG JAR_FILE=target/*.jar
COPY ${JAR_FILE} application.jar
ENTRYPOINT ["java", "-Djava.security.egd=file:/dev/./urandom","-jar","/application.jar"]
EXPOSE 8080

eclipse-temurin 是 Eclipse 提供的 open JDK image

ARG 定義參數

用指令產生 image

docker build -t test/docker-all:1.0 .

-t tags,image 的 name:tags

啟動

docker run -dp 8080:8080 test/docker-all:1.0 

分層 image

定義分層的是 laryers.idx 檔案,內容為

- "dependencies":
  - "BOOT-INF/lib/"
- "spring-boot-loader":
  - "org/"
- "snapshot-dependencies":
- "application":
  - "BOOT-INF/classes/"
  - "BOOT-INF/classpath.idx"
  - "BOOT-INF/layers.idx"
  - "META-INF/"

一般會異動的只有 classes

建立新的 Dockerfile

FROM eclipse-temurin:17-jre as builder
WORKDIR application
ARG JAR_FILE=target/*-exec.jar
COPY ${JAR_FILE} application.jar
RUN java -Djarmode=layertools -jar application.jar extract

FROM eclipse-temurin:17-jre as builder
WORKDIR application
COPY --from-builder application/dependencies/ ./
COPY --from-builder application/spring-boot-loader/ ./
COPY --from-builder application/snapshot-dependencies/ ./
COPY --from-builder application/application/ ./
ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher"]
EXPOSE 8081

注意 -Djarmode=layertools

layertools list

> java -Djarmode=layertools -jar test-0.0.1-SNAPSHOT.war list
dependencies
spring-boot-loader
snapshot-dependencies
application

產生 image

docker build -t test/docker-layer:1.0 .

分層建構不支援 executable jar,要修改 executable 設定

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

Cloud Native Buildpacks

可透過 Maven/Gradle 產生 Cloud Native Buildpacks image

Cloud Native Buildpacks 是 2018 由 Pivotal, Heroku 發起,於10月加入Cloud Native沙盒,目標是要統一Buildpacks生態系。

修改 pom.xml

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <executions>
                    <execution>
                        <goals>
                            <goal>build-image</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <executable>false</executable>
                    <image>
                        <name>test/docker-cnb:${project.version}</name>
                    </image>
                </configuration>
            </plugin>
        </plugins>
    </build>

以指令產生

mvn clean package

也可以用 build-image target

mvn spring-boot:build-image

        <configuration>
          <executable>false</executable>
          <image>
            <name>test/docker-cnb:${project.version}</name>
            <pullPolicy>IF_NOT_PRESENT</pullPolicy>
          </image>
        </configuration>

可加上 pullPolicy

  • ALWAYS

  • IF_NOT_PRESENT: 不存在基礎 image 時才拉取

  • NEVER


GraalVM image

Spring Boot 3.0 支援 GraalVM 22.3+, Native Build Tools Plugin 0.9.17+

GraalVM 是 2019 年 Oracle 發表的高性能,跨語言通用 VM,可提升 Java, Scala, Grooby, Kotlin 等基於 JVM 的 application。

GraalVM 有一個 Java 撰寫的高級 JIT compiler,借助 Truffle,可執行 Javascript, Ruby, Python

# Getting Started with Oracle GraalVM

產生 native-image

javac HelloWorld.java
native-image HelloWorld
./helloworld
Hello, World!

GraalVM applicaiton 跟傳統 application 的差異

對於 Java applicaiton,GraalVM 提供兩種運作方法

  • 在 HotSpot VM 上使用 Graal 的 JIT compiler,一邊編譯,一邊運作

  • 以 AOT 編譯的 GraalVM 原生 image 可執行檔案運作,是靜態預先編譯

GraalVM applcation 的差別

  • GraalVM image 會進行靜態分析

  • 產生 image 時,無法執行到的 code 會被刪除,不會成為可執行檔案的一部分

  • GraalVM 不能直接偵測 code 的動態元素,ex: reflection, resource, 序列化, 動態代理,必須提前宣告設定

  • 應用的 classpath 路徑是固定的,不能修改

  • 不支援 bean 的延遲載入

  • 某些地方不完全相容傳統的 java app


spring boot 產生 image 時

  • classpath 固定,無法變更

  • bean 不能被修改,不支援 @Profile@ConditionalOnProperty

Spring AOT 在產生 image 時,同時會產生

  • Java source code

  • byte code

  • GraalVM 相關 JSON 描述檔案,包含

    • resource-config.json Resource hints

    • reflection.config.json Reflection hints

    • serialization-config.json Serialization hints

    • proxy-config.json Java Proxy hints

    • jni-config.json JNI hints


https://start.spring.io/ 產生的 project,增加 Graal VM Nartive Support,maven pom.xml 裡面就包含了 native, native test 兩個 profiles

        <plugins>
            <plugin>
                <groupId>org.graalvm.buildtools</groupId>
                <artifactId>native-maven-plugin</artifactId>
            </plugin>
        </plugins>

使用 maven 產生 image

mvn -Pnative spring-boot:build-image

可用 docker 執行

docker run --rm -p 8080:8080 docker.io/library/spring-boot-graalvm:1.0

也可以安裝 GraalVM Native Build Tools

sdk install java 23.0.1-graal
sdk use java 23.0.1-graal

用 maven

mvn -Pnative native:compile

會產生

  • spring-boot-graalvm: app 可執行檔案

  • spring-boot-graalvm.build_artifacts.txt: 說明 txt

2025/07/28

Spring Boot 3 unit test

libary 有兩個

  • spring-boot-test: 測試核心功能

  • spring-boot-test-autoconfigure: 測試的自動設定

spring-boot-starter-test 是集合

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>

測試還包含其他 library

  • JUnit 5

  • AssertJ

  • Hamcrest

  • Mockito

  • JSONassert

  • JsonPath


spring 提供一個 @SpringBootTest annotation,裡面有一個 webEnvironment 環境變數,支援以下設定

  • MOCK (default)

    載入一個 WebApplicationContext 並提供 Mock Web Environment。不會啟動內嵌的 web server,並可以結合 @AutoConfigureMockMvcor @AutoConfigureWebTestClient 進行 MOCK 測試

  • RANDOM_PORT

    載入一個 WebApplicationContext,提供一個真實的 web environment,隨機 port

  • DEFINED_PORT

    載入一個 WebApplicationContext,提供一個真實的 web environment,指定 port,預設 8080

  • NONE

    載入一個 WebApplicationContext,不提供 web environment


Spring MVC 測試

MvcTests.java

package com.test.test;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;

import java.util.HashMap;
import java.util.Map;

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

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class MvcTests {

    @Test
    public void getUserTest(@Autowired TestRestTemplate testRestTemplate) {
        Map<String, String> multiValueMap = new HashMap<>();
        multiValueMap.put("username", "test1");
        Result result = testRestTemplate.getForObject("/user/get?username={username}",
                Result.class, multiValueMap);
        assertThat(result.getCode()).isEqualTo(0);
        assertThat(result.getMsg()).isEqualTo("ok");
    }

}

呼叫 /user/test,檢查回傳的資料是否正確

從 log 可發現 tomcat 運作在另一個 port

Tomcat started on port 53594 (http) with context path '/'

MOCK 測試

package com.test.test;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@SpringBootTest
@AutoConfigureMockMvc
class MockMvcTests {

    @Test
    public void getUserTest(@Autowired MockMvc mvc) throws Exception {
        mvc.perform(MockMvcRequestBuilders.get("/user/get?username={username}", "test"))
                .andExpect(status().isOk())
                .andExpect(content().string("{\"code\":0,\"msg\":\"ok\",\"data\":\"test\"}"));
    }

}

在class 上面增加 @AutoConfigureMockMvc ,然後在 method 上加上 @Autowired MockMvc mvc 就可進行 MOCK 測試。

從 log 發現並沒有真的啟動一個 web server


MOCK 元件測試

package com.test.test;

import com.test.test.service.UserService;
import org.junit.jupiter.api.Test;
import org.mockito.BDDMockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.bean.override.mockito.MockitoBean;

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

@SpringBootTest
class MockBeanTests {

//    @Autowired
//    private UserService userService;

    @MockitoBean
    private UserService userService;

    @Test
    public void countAllUsers() {
        BDDMockito.given(this.userService.countAllUsers()).willReturn(88);
        assertThat(this.userService.countAllUsers()).isEqualTo(88);
    }

}

@MockitoBean 代表該變數是被 Mock 覆蓋

這邊透過 BDDMockito 模擬 countAllUsers 回傳結果


JSON 測試

spring 提供各種技術元件的單元測試

想要測試 JSON,可使用 JsonTestersAutoConfiguration,只需要在測試類別加上 @JsonTest

先新增一個檔案 test/resources/jack.json

{"id":10001, "name":"Jack", "birthday": "2000-10-08 21:00:00"}

User POJO

package com.test.test;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.AllArgsConstructor;
import lombok.Data;

import java.time.LocalDateTime;

@Data
@AllArgsConstructor
public class User {
    private long id;
    private String name;
    @JsonFormat(timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime birthday;

}

JsonTests.java

package com.test.test;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.json.JsonTest;
import org.springframework.boot.test.json.JacksonTester;

import java.time.LocalDateTime;

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

@JsonTest
class JsonTests {

    @Autowired
    private JacksonTester<User> json;

    @Test
    void serialize() throws Exception {
        User user = new User(10001L, "Jack",
                LocalDateTime.of(2000, 10, 8, 21, 0, 0));
        assertThat(this.json.write(user)).isEqualToJson("/jack.json");
        assertThat(this.json.write(user)).hasJsonPathStringValue("@.name");
        assertThat(this.json.write(user)).
                extractingJsonPathStringValue("@.name").isEqualTo("Jack");
    }

    @Test
    void deserialize() throws Exception {
        String content = "{\"id\":10002, \"name\":\"Petty\", \"birthday\": \"2025-01-01 01:01:00\"}";
        assertThat(this.json.parse(content))
                .isEqualTo(new User(10002L, "Petty",
                        LocalDateTime.of(2025, 1, 1, 1, 1, 0)));
        assertThat(this.json.parseObject(content).getName()).isEqualTo("Petty");
    }

}

2025/07/21

Spring Boot 3 test

斷點測試

用 main 直接啟動 Debug mode

在啟動 application 時,用 Debug mode 方式啟動。當程式到達斷點時,會自動暫停

用 maven plugin

使用 main 啟動 debug mode 的缺點是,無法使用 maven plugins。

使用 spring boot maven plugin 啟動 application 會 fork process 執行,故無法像 main 的方式一樣,直接 debug。需要用遠端測試的方法。

首先是需要在 spring boot maven plugin 中,設定 jvmArguments 測試參數,並啟動 application,然後新增一個遠端測試

啟動測試模式

有兩種方法

  • 在 plugin 設定中指定測試模式的 jvmArguments

        <build>
            <plugins>
                <plugin>
                    <groupId>org.springframework.boot</groupId>
                    <artifactId>spring-boot-maven-plugin</artifactId>
                    <configuration>
                        <jvmArguments>
                            -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005
                        </jvmArguments>
                    </configuration>
                </plugin>
            </plugins>
        </build>
  • 使用 mvn 啟動時,指定測試參數

    mvn spring-boot:run -Dspring-boot.run.jvmArguments="-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=5005"

啟動後,會在 console 最後面看到

> mvn spring-boot:run
[INFO] --- spring-boot-maven-plugin:3.4.0:run (default-cli) @ test ---
[INFO] Attaching agents: []
Listening for transport dt_socket at address: 5005

遠端測試

透過 IDE 建立 Remote test application

連接後,剛剛的 mvn 才會完成啟動,出現 application 的 log

Connected to the target VM, address: 'localhost:5005', transport: 'socket'

JVM remote debug 要處理斷點會比較麻煩


開發者工具

spring boot 提供 devtools,功能可自動禁用 cache,支援 application 自動重啟,即時 reload

只要引用 libary 即可

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-devtools</artifactId>
            <optional>true</optional>
        </dependency>

optional 代表只會在本專案/子專案中使用,不會傳遞到引用該專案的project

devtools 是在開發時使用,打包時 spring boot 預設不會把 devtools 包裝進去。可修改此設定

        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <configuration>
                    <excludeDevtools>false</excludeDevtools>
                </configuration>
            </plugin>
        </plugins>

並在啟動時,加上 -Dspring.devtools.restart.enabled=true 啟用

但不建議將 devtools 打包進去


預設值

Name Default Value
server.error.include-binding-errors always
server.error.include-message always
server.error.include-stacktrace always
server.servlet.jsp.init-parameters.development true
server.servlet.session.persistent true
spring.docker.compose.readiness.wait only-if-started
spring.freemarker.cache false
spring.graphql.graphiql.enabled true
spring.groovy.template.cache false
spring.h2.console.enabled true
spring.mustache.servlet.cache false
spring.mvc.log-resolved-exception true
spring.reactor.netty.shutdown-quiet-period 0s
spring.template.provider.cache false
spring.thymeleaf.cache false
spring.web.resources.cache.period 0
spring.web.resources.chain.cache false

devtools 預設禁用 cache,因為開發階段禁用 cache,較容易排除問題

可透過設定修改

spring:
  devtools:
    add-properties: false

自動 restart

devtools 會監控檔案,發生變更就會快速自動 restart。這邊使用兩個 class loader

  • base classloader

    基本不會變更的檔案,ex: 3rd party jar

  • restart classloader

    application 相關檔案

restart 時,base classloader 不需要異動,只需要重建一個 restart classloader

但要注意:AspectJ AOP programming 不支援 auto restart


觸發機制

  • Eclipse: 修改 save 後就會觸發

  • IntelliJ IDEA: 要重新 build 才會觸發

  • build tool: 在 IDE 使用 mvn compile/grdle build 會觸發


修改時頻繁 restart,會消耗很多資源

可將 IDE 改為手動編譯,或是指定一個觸發檔案

spring:
  devtools:
    restart:
      trigger-file: .reloadtrigger

只有在 .reloadtrigger 檔案被修改時,才會自動 restart


IDE 有支援自動產生 trigger file

然後在 application 有個 update 按鈕

此方法只適用於用 main 啟動的 spring application,不適用 mvn


排除資源

預設狀況下以下資源目錄不會觸發 restart,但會觸發自動 reload

  • /META-INF/maven

  • /META-INF/resources

  • /resources

  • /static

  • /public

  • /templates

可修改此設定

spring:
  devtools:
    restart:
      exclude: static/**,public/**
      # 保留預設,增加其他目錄
      # additional-exclude:

禁用 log

application 自動 restart 都會列印 log,可用以下方式關閉

spring:
  devtools:
    restart:
      log-confition-evaluation-delta: false

禁用 auto restart

spring:
  devtools:
    restart:
      enabled: false

auto reload

browser 要安裝 LiveReload plugin,並打開 devtool 的 auto reload 功能

devtools 內建一個 LiveReload server 在 port 35729

預設狀況下以下資源目錄不會觸發 restart,但會觸發自動 reload

  • /META-INF/maven

  • /META-INF/resources

  • /resources

  • /static

  • /public

  • /templates

可關閉 auto reload

spring:
  devtools:
    livereload:
      enabled: false

global 設定

$HOME/.config/spring-boot 目錄中,設定檔名為 spring-boot-devtools。支援 properties 或 yaml

spring-boot-devtools.properties

spring-boot-devtools.yaml

spring-boot-devtools.yml

$HOME 預設是 user 主目錄,可用以下方式修改

  • 設定 SPRING_DEVTOOLS_HOME 環境變數

  • 設定 spring-devtools.home 系統參數

example: spring-boot-devtools.yml

spring:
  devtools:
    restart:
      log-condition-evaluation-delta: false
      trigger-file: ".reloadtrigger"