我在學習一個全新框架時,很喜歡去看那些初學者不太愛看或看不太懂的內容。例如我在學 Angular 的時候,明明 ng new
就可以建立新專案,就可以開始寫程式,但我就會深入研究啟動的完整過程。而我在學 Spring Boot 的時候也一樣,雖然 Spring Initializr 真的很好用,相依套件選一選就可以開始開發應用程式,但我就會想瞭解這些神奇設計的背後做了什麼事,藉此瞭解一個框架的核心原理。這個過程看似沒效率,但事實上此舉可以學習到非常廣泛的知識,而且可以很好的連結不同技術細節。今天這篇文章我們就回歸基礎,看看 Spring Boot 應用程式的啟動生命週期。
建立範例應用程式
-
使用 Spring Boot CLI 快速建立專案 (也可以用 Spring Initializr 建立)
spring init --dependencies=web,lombok --groupId=com.duotify starter1
-
使用 Visual Studio Code 開啟該專案
code starter1/
目前專案的資料夾與檔案結構如下:
.
├── HELP.md
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
├── main
│ ├── java
│ │ └── com
│ │ └── duotify
│ │ └── starter1
│ │ └── DemoApplication.java
│ └── resources
│ ├── application.properties
│ ├── static
│ └── templates
└── test
└── java
└── com
└── duotify
└── starter1
└── DemoApplicationTests.java
14 directories, 7 files
應用程式進入點
所有 Java 應用程式都需要一個進入點才能執行,因此你只要找到包含 main()
方法的類別,就是我們的進入點:
package com.example.myapp;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
在這個類別上有個 @SpringBootApplication
標注(Annotation),用來宣告這是一個 Spring Boot 應用程式。但這個 @SpringBootApplication
標注本身又套用了好幾個標注,我們按下 F12
即可查看原始碼:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
由此上述程式碼,我們可以發現另外三個重要的標注:
-
@SpringBootConfiguration
這個 @SpringBootConfiguration
標注,主要用來將目前類別(DemoApplication
)套上 @Configuration
標注,你可以看一下 @SpringBootConfiguration
的原始碼部分,他有套用一個 @Configuration
標注:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
@Indexed
public @interface SpringBootConfiguration {
你其實也可以直接在 DemoApplication
套用 @Configuration
標注,但是套用 @SpringBootConfiguration
的語意更加清晰,可讀性更高!👍
基本上,套用 @Configuration
標注會讓 DemoApplication
類別加入到 Spring IoC container 之中,若類別中有方法(Method)套用 @Bean
標注,該方法就可以在應用程式啟動時被 Spring Boot 呼叫,並將執行結果也加入到 Spring IoC container 之中,讓其他元件(@Component
)可以透過建構式注入、屬性注入(@Autowired
)或方法注入(@Qualifier
)使用。
這個行為算是一種手動建立與註冊 Spring Bean 元件的過程。
-
@EnableAutoConfiguration
這個 @EnableAutoConfiguration
標注,會先讓 Spring Boot 透過 @ComponentScan
的定義,自動找出所有相依套件中 JAR 檔案中有套用 @Component
標注的類別,並自動建立與註冊成 Spring Bean 元件。這個過程就如同自動幫你在這些類別加上 @Configuration
標注的意思。
例如 Tomcat
或 Spring MVC
等相依套件,都有類似的設計,所以使用 @EnableAutoConfiguration
可以大幅簡化程式碼數量。
-
@ComponentScan
這個 @ComponentScan
標注,主要用來將目前 package
與所有 sub-package
下有標注 @Component
標注的類別都自動掃描出來,並提供給 @EnableAutoConfiguration
進行自動設定。這個過程又稱 Component scanning (元件掃描)。
這三個標注的用途不同,又彼此相關,因此非常重要。一般來說,@EnableAutoConfiguration
與 @SpringBootConfiguration
只會套用一次在根類別(root class),而 @ComponentScan
通常也只會套用在 根類別 上,但如果你有其他函式庫,也可以自己決定要不要套用 @ComponentScan
將當前類別 package
與 sub-packages
下找出所有套用 @Component
的類別。
注意: 一般來說,比較少人會直接在類別上套用 @Component
標注,而是用繼承 @Component
的標注為主,這些繼承 @Component
的標注又被稱為 Stereotype Annotations
(刻板印象標注),像是 @Controller
, @Service
, @Repository
, @Configuration
都屬於這類標注。因為 @RestController
繼承自 @Controller
,所以這也算是一種 Stereotype Annotations
(刻板印象標注)。
正常的情況下,你在根類別上使用 @SpringBootApplication
標注即可,應該 99% 的情境下都可以忽略底層的技術細節。
示範幾種不同的套用情境
-
整個應用程式只有一個類別,不需要 @ComponentScan
自動掃描 sub-packages
package com.duotify.starter1;
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 DemoApplication {
@RequestMapping("/")
String home() {
return "Hello World!123";
}
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
這種例子是比較少見,大多出現在剛學習 Spring Boot 框架時會看到的程式碼範例中。
這裡的 @EnableAutoConfiguration
標注會自動設定所有 Spring Bean,但我們並沒有套用 @ComponentScan
,所以他並不會自動去找 com.duotify.starter1
與 com.duotify.starter1.*
的類別。
這裡的 @RestController
標注,其原始碼長這樣,他包含了 @Controller
與 @ResponseBody
標注:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Controller
@ResponseBody
public @interface RestController {
而 @Controller
標注的原始碼長這樣:
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Component
public @interface Controller {
所以其實他有套用 @Component
,因此會被 @EnableAutoConfiguration
標注自動設定,所以 DemoApplication
類別本身就會被註冊成 Spring Bean 元件,因此可以直接當 Controller 來執行。
-
整個應用程式不只有一個類別,我們希望把 Controller 類別拆開來撰寫,因此需要 @ComponentScan
自動掃描 sub-packages
中的類別
我先把 DemoApplication.java
改成這樣:
package com.duotify.starter1;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
@EnableAutoConfiguration
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
然後加入一個 com.duotify.starter1.controllers.HomeController
類別:
package com.duotify.starter1.controllers;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HomeController {
@RequestMapping("/")
String home() {
return "Hello World!";
}
}
啟動網站後,將無法找到 HomeController
控制器,因為他並沒有被註冊成 Spring Bean 元件。
$ curl localhost:8080
{"timestamp":"2022-09-19T08:02:19.812+00:00","status":404,"error":"Not Found","path":"/"}
我們在 DemoApplication.java
的 DemoApplication
類別加上 @ComponentScan
標注:
package com.duotify.starter1;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
@EnableAutoConfiguration
@ComponentScan
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
重新啟動應用程式就可以順利呼叫 API 了!
$ curl localhost:8080
Hello World!
-
我們有個共用的物件,透過 @Bean
註冊,並讓其他也註冊在 Spring IoC container 的元件使用
因為所有使用 @Component
標注的類別,最終都會被註冊到 Spring IoC container 裡面。而所有註冊到 Spring IoC container 的元件都可以注入到其他元件中。所以我們可以在 DemoApplication
類別加入一個 Spring Bean 方法:
@Bean
public String appName() {
return "Starter1";
}
加入之後,這個有套用 @Bean
標注的 appName()
方法,其實並不會在 Spring Boot 應用程式啟動時自動執行,除非你在 DemoApplication
類別上套用 @Configuration
標注。結果如下:
package com.duotify.starter1;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@EnableAutoConfiguration
@ComponentScan
@Configuration
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean(name = "appName")
public String appName() {
return "Starter1";
}
}
由於我們已經收集到 @EnableAutoConfiguration
, @ComponentScan
, @Configuration
這三個標注,因此現在可以直接合併成 @SpringBootApplication
標注:
package com.duotify.starter1;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@SpringBootApplication
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
@Bean(name = "appName")
public String appName() {
return "Starter1";
}
}
最後,我們修改 HomeController
類別,使用建構式注入 String
這個 Spring Bean 來用:
package com.duotify.starter1.controllers;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HomeController {
private String appName;
public HomeController(String appName) {
this.appName = appName;
}
@RequestMapping("/")
String home() {
return "Hello " + this.appName + "!";
}
}
若你想用「屬性注入」也可以,程式碼也更簡潔,只要在欄位套用 @Autowired
即可:
package com.duotify.starter1.controllers;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@RestController
public class HomeController {
@Autowired
private String appName;
@RequestMapping("/")
String home() {
return "Hello " + this.appName + "!";
}
}
測試執行:
$ curl localhost:8080/
Hello Starter1!
總結
其實這篇文章已經牽涉許多基礎知識,其中包含:
因為這些都是 Spring 的核心機制,只要能完整理解上述概念,你就可以很好的理解其他部分!👍
相關連結