前一陣子公司專案讓我有機會使用 Spring Boot 開發,但因為當時專案時間很趕,所以也一直專注在開發上當時開發所需要的技術。由於不熟 Spring Boot or Spring Core,所以用的東西都很基礎。所以我想趁現在好好的充實自己並嘗試寫一個 Spring Boot 系列的文章。

環境確認


這系列的文章,會以以下的版本為主

  • Java 11
  • Spring Boot 2.7
  • Apache Maven 3.8.2

建立第一個 Hello World


由於艾倫以前已經使用過 IDE 或 網頁版的 Spring Boot Initializr 初始化,所以這次會以還未使用過的 Spring CLI。

安裝 Spring CLI


1$ brew tap spring-io/tap
2$ brew install springboot
3
4$ spring --version
5Spring CLI v2.7.0

使用 Spring CLI 初始化專案


1$ spring init -dweb -j 11 -v 1.0.0 -a helloworld
2Using service at https://start.spring.io
3Content saved to 'helloworld.zip'

這邊對參數做一些小解釋

  • -d: 是 Dependencies,指定專案所需要的 packages,目前先選擇最基本的 web starter
  • -j: 是 Java 版本,這邊我們選擇 Java 11
  • -v: 此專案的版本初始化
  • -a: 是 artifact Id

更多的參數或者 dependencies 的選擇,可以透過 spring init -list 看到。

執行 Spring Boot


建立一個新的資料夾如 demo,將剛剛 Spring CLI 所產生的 ZIP 檔案放入資料夾中,並解壓縮。 在 Terminal 中移動到那個 folder 並在裡面執行 “./mvnw spring-boot:run” ,最後可以看到以下畫面

 1  .   ____          _            __ _ _
 2 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
 3( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 4 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
 5  '  |____| .__|_| |_|_| |_\__, | / / / /
 6 =========|_|==============|___/=/_/_/_/
 7 :: Spring Boot ::                (v2.7.0)
 8
 92022-06-12 22:08:28.328  INFO 85059 --- [           main] com.example.helloworld.DemoApplication   : Starting DemoApplication using Java 17 on ChangdeMacBook-Pro.local with PID 85059 (/Users/allen/helloworld/target/classes started by allen in /Users/allen/helloworld)
102022-06-12 22:08:28.331  INFO 85059 --- [           main] com.example.helloworld.DemoApplication   : No active profile set, falling back to 1 default profile: "default"
112022-06-12 22:08:29.023  INFO 85059 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
122022-06-12 22:08:29.035  INFO 85059 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
132022-06-12 22:08:29.035  INFO 85059 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet engine: [Apache Tomcat/9.0.63]
142022-06-12 22:08:29.107  INFO 85059 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
152022-06-12 22:08:29.107  INFO 85059 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 721 ms
162022-06-12 22:08:29.395  INFO 85059 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
172022-06-12 22:08:29.404  INFO 85059 --- [           main] com.example.helloworld.DemoApplication   : Started DemoApplication in 1.627 seconds (JVM running for 1.887)

從上面的訊息來看,可以看到 Spring Application 已經用 Tomcat 起來,且正在聽 8080 port。

這時我們嘗試打 8080 port

1 ~  curl http://127.0.0.
2{"timestamp":"2022-06-12T14:10:55.321+00:00","status":404,"error":"Not Found","path":"/"}

會得到 404 Error Msg,這是正常的,因為目前我們還沒有建立任何的 endpoint ~ 但至少確認 Tomcat 有起來。

Spring CLI 產生檔案

我們可以看到 Spring CLI 已經幫我們產生了以下的檔案

 1$ tree
 2.
 3├── HELP.md
 4├── mvnw
 5├── mvnw.cmd
 6├── pom.xml
 7└── src
 8    ├── main
 9    │   ├── java
10    │   │   └── com
11    │   │       └── example
12    │   │           └── helloworld
13    │   │               └── DemoApplication.java
14    │   └── resources
15    │       ├── application.properties
16    │       ├── static
17    │       └── templates
18    └── test
19        └── java
20            └── com
21                └── example
22                    └── helloworld
23                        └── DemoApplicationTests.java

DemoApplication.java

我們可以看到 Spring Boot 的進入點 SpringApplication.run。

如果對 Spring Boot 啟動時做了什麼事情有興趣,可以看看這篇文章 從 SpringBootApplication 談談 Spring Boot 啓動時都做了哪些事?

 1package com.example.helloworld;
 2
 3import org.springframework.boot.SpringApplication;
 4import org.springframework.boot.autoconfigure.SpringBootApplication;
 5
 6@SpringBootApplication
 7public class DemoApplication {
 8
 9	public static void main(String[] args) {
10		SpringApplication.run(DemoApplication.class, args);
11	}
12
13}

Pom.xml

 1<?xml version="1.0" encoding="UTF-8"?>
 2<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
 3	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
 4	<modelVersion>4.0.0</modelVersion>
 5	<parent>
 6		<groupId>org.springframework.boot</groupId>
 7		<artifactId>spring-boot-starter-parent</artifactId>
 8		<version>2.7.0</version>
 9		<relativePath/> <!-- lookup parent from repository -->
10	</parent>
11	<groupId>com.example</groupId>
12	<artifactId>helloworld</artifactId>
13	<version>1.0.0</version>
14	<name>demo</name>
15	<description>Demo project for Spring Boot</description>
16	<properties>
17		<java.version>11</java.version>
18	</properties>
19	<dependencies>
20		<dependency>
21			<groupId>org.springframework.boot</groupId>
22			<artifactId>spring-boot-starter-web</artifactId>
23		</dependency>
24
25		<dependency>
26			<groupId>org.springframework.boot</groupId>
27			<artifactId>spring-boot-starter-test</artifactId>
28			<scope>test</scope>
29		</dependency>
30	</dependencies>
31
32	<build>
33		<plugins>
34			<plugin>
35				<groupId>org.springframework.boot</groupId>
36				<artifactId>spring-boot-maven-plugin</artifactId>
37			</plugin>
38		</plugins>
39	</build>
40
41</project>

從上面看到,我們繼承了 starter parent,且 dependencies 如預期裡面包含了 web starter 跟我們一開始初始化專案時所要求的一樣。

最後這邊可以看到,有裝 spring boot maven plugin,這是我們執行 mvn spring-boot:run 所需要的 plugin。

Maven dependency tree

當我們下 “./mvnw dependency:tree” 時,我們可以看到整個專案所需要的所包含的 package,也可以看到 package 中所拉到的其他 package 或者我們叫 transitive dependencies。

以下面的結果為範例,我們可以看到 spring-boot-starter-web 裡面有包含 spring-boot-starter-tomcat。

 1$  ./mvnw dependency:tree
 2[INFO] com.example:helloworld:jar:1.0.0
 3[INFO] +- org.springframework.boot:spring-boot-starter-web:jar:2.7.0:compile
 4[INFO] |  +- org.springframework.boot:spring-boot-starter:jar:2.7.0:compile
 5[INFO] |  |  +- org.springframework.boot:spring-boot:jar:2.7.0:compile
 6[INFO] |  |  +- org.springframework.boot:spring-boot-autoconfigure:jar:2.7.0:compile
 7[INFO] |  |  +- org.springframework.boot:spring-boot-starter-logging:jar:2.7.0:compile
 8[INFO] |  |  |  +- ch.qos.logback:logback-classic:jar:1.2.11:compile
 9[INFO] |  |  |  |  \- ch.qos.logback:logback-core:jar:1.2.11:compile
10[INFO] |  |  |  +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.17.2:compile
11[INFO] |  |  |  |  \- org.apache.logging.log4j:log4j-api:jar:2.17.2:compile
12[INFO] |  |  |  \- org.slf4j:jul-to-slf4j:jar:1.7.36:compile
13[INFO] |  |  +- jakarta.annotation:jakarta.annotation-api🫙1.3.5:compile
14[INFO] |  |  \- org.yaml:snakeyaml:jar:1.30:compile
15[INFO] |  +- org.springframework.boot:spring-boot-starter-json:jar:2.7.0:compile
16[INFO] |  |  +- com.fasterxml.jackson.core:jackson-databind:jar:2.13.3:compile
17[INFO] |  |  |  +- com.fasterxml.jackson.core:jackson-annotations:jar:2.13.3:compile
18[INFO] |  |  |  \- com.fasterxml.jackson.core:jackson-core:jar:2.13.3:compile
19[INFO] |  |  +- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar:2.13.3:compile
20[INFO] |  |  +- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.13.3:compile
21[INFO] |  |  \- com.fasterxml.jackson.module:jackson-module-parameter-names:jar:2.13.3:compile
22[INFO] |  +- org.springframework.boot:spring-boot-starter-tomcat:jar:2.7.0:compile
23[INFO] |  |  +- org.apache.tomcat.embed:tomcat-embed-core:jar:9.0.63:compile
24[INFO] |  |  +- org.apache.tomcat.embed:tomcat-embed-el:jar:9.0.63:compile
25[INFO] |  |  \- org.apache.tomcat.embed:tomcat-embed-websocket:jar:9.0.63:compile
26[INFO] |  +- org.springframework:spring-web:jar:5.3.20:compile
27[INFO] |  |  \- org.springframework:spring-beans:jar:5.3.20:compile
28[INFO] |  \- org.springframework:spring-webmvc:jar:5.3.20:compile
29[INFO] |     +- org.springframework:spring-aop:jar:5.3.20:compile
30[INFO] |     +- org.springframework:spring-context:jar:5.3.20:compile
31[INFO] |     \- org.springframework:spring-expression:jar:5.3.20:compile

將 Tomcat 換成 Jetty

從 dependency tree 我們已經看到 tomcat 是由 web start 拉入的,所以我們可以透過 exclusion 將 tomcat starter 從 web starter 中排除,並手動拉入 jetty starter。

 1<dependencies>
 2	<dependency>
 3		<groupId>org.springframework.boot</groupId>
 4		<artifactId>spring-boot-starter-web</artifactId>
 5		<exclusions>
 6			<!-- 去除Tomcat容器 -->
 7			<exclusion>
 8			<groupId>org.springframework.boot</groupId>
 9			<artifactId>spring-boot-starter-tomcat</artifactId>
10			</exclusion>
11		</exclusions>
12	</dependency>
13	<!-- 增加Jetty容器 -->
14	<dependency>
15		<groupId>org.springframework.boot</groupId>
16		<artifactId>spring-boot-starter-jetty</artifactId>
17	</dependency>
18	<dependency>
19		<groupId>org.springframework.boot</groupId>
20		<artifactId>spring-boot-starter-test</artifactId>
21		<scope>test</scope>
22	</dependency>
23</dependencies>

這時我們重新執行 ./mvnw spring-boot:run

 1  .   ____          _            __ _ _
 2 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
 3( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 4 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
 5  '  |____| .__|_| |_|_| |_\__, | / / / /
 6 =========|_|==============|___/=/_/_/_/
 7 :: Spring Boot ::                (v2.7.0)
 8
 92022-06-12 22:49:19.224  INFO 87278 --- [           main] com.example.helloworld.DemoApplication   : Starting DemoApplication using Java 17 on ChangdeMacBook-Pro.local with PID 87278 (/Users/allen/helloworld/target/classes started by allen in /Users/allen/helloworld)
102022-06-12 22:49:19.226  INFO 87278 --- [           main] com.example.helloworld.DemoApplication   : No active profile set, falling back to 1 default profile: "default"
112022-06-12 22:49:19.740  INFO 87278 --- [           main] org.eclipse.jetty.util.log               : Logging initialized @1029ms to org.eclipse.jetty.util.log.Slf4jLog
122022-06-12 22:49:19.857  INFO 87278 --- [           main] o.s.b.w.e.j.JettyServletWebServerFactory : Server initialized with port: 8080
132022-06-12 22:49:19.860  INFO 87278 --- [           main] org.eclipse.jetty.server.Server          : jetty-9.4.46.v20220331; built: 2022-03-31T16:38:08.030Z; git: bc17a0369a11ecf40bb92c839b9ef0a8ac50ea18; jvm 17+0
142022-06-12 22:49:19.896  INFO 87278 --- [           main] o.e.j.s.h.ContextHandler.application     : Initializing Spring embedded WebApplicationContext
152022-06-12 22:49:19.897  INFO 87278 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 633 ms
162022-06-12 22:49:19.958  INFO 87278 --- [           main] org.eclipse.jetty.server.session         : DefaultSessionIdManager workerName=node0
172022-06-12 22:49:19.958  INFO 87278 --- [           main] org.eclipse.jetty.server.session         : No SessionScavenger set, using defaults
182022-06-12 22:49:19.959  INFO 87278 --- [           main] org.eclipse.jetty.server.session         : node0 Scavenging every 600000ms
192022-06-12 22:49:19.966  INFO 87278 --- [           main] o.e.jetty.server.handler.ContextHandler  : Started o.s.b.w.e.j.JettyEmbeddedWebAppContext@69da0b12{application,/,[file:///private/var/folders/dx/18qvmm_55hz74r3xdx9wn6x40000gn/T/jetty-docbase.8080.12428220257398218881/],AVAILABLE}
202022-06-12 22:49:19.966  INFO 87278 --- [           main] org.eclipse.jetty.server.Server          : Started @1256ms
212022-06-12 22:49:20.187  INFO 87278 --- [           main] o.e.j.s.h.ContextHandler.application     : Initializing Spring DispatcherServlet 'dispatcherServlet'
222022-06-12 22:49:20.187  INFO 87278 --- [           main] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
232022-06-12 22:49:20.188  INFO 87278 --- [           main] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
242022-06-12 22:49:20.219  INFO 87278 --- [           main] o.e.jetty.server.AbstractConnector       : Started ServerConnector@17ca8b92{HTTP/1.1, (http/1.1)}{0.0.0.0:8080}
252022-06-12 22:49:20.220  INFO 87278 --- [           main] o.s.b.web.embedded.jetty.JettyWebServer  : Jetty started on port(s) 8080 (http/1.1) with context path '/'
262022-06-12 22:49:20.230  INFO 87278 --- [           main] com.example.helloworld.DemoApplication   : Started DemoApplication in 1.274 seconds (JVM running for 1.52)

我們可以看到 Application 現在由 Jetty 9.4 起來~