﻿# 認識 Spring Boot 應用程式的啟動生命週期

我在學習一個全新框架時，很喜歡去看那些初學者不太愛看或看不太懂的內容。例如我在學 Angular 的時候，明明 `ng new` 就可以建立新專案，就可以開始寫程式，但我就會深入研究啟動的完整過程。而我在學 Spring Boot 的時候也一樣，雖然 [Spring Initializr](https://start.spring.io/) 真的很好用，相依套件選一選就可以開始開發應用程式，但我就會想瞭解這些神奇設計的背後做了什麼事，藉此瞭解一個框架的核心原理。這個過程看似沒效率，但事實上此舉可以學習到非常廣泛的知識，而且可以很好的連結不同技術細節。今天這篇文章我們就回歸基礎，看看 Spring Boot 應用程式的啟動生命週期。

![Spring Boot](https://stwillblogassets.blob.core.windows.net/files/images/external/user-images.githubusercontent.com/c7c584c486c466dfc045-191159099-cb5c8253-e35a-4e87-a070-7dcc9cd5984d.png)

### 建立範例應用程式

1.  使用 Spring Boot CLI 快速建立專案 (也可以用 [Spring Initializr](https://start.spring.io/) 建立)
    
    ```bash
    spring init --dependencies=web,lombok --groupId=com.duotify starter1
    ```
    
2.  使用 Visual Studio Code 開啟該專案
    
    ```bash
    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()` 方法的類別，就是我們的進入點：

```java
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` 即可查看原始碼：

```java
@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 {
```

由此上述程式碼，我們可以發現另外三個重要的標注：

1.  `@SpringBootConfiguration`
    
    這個 `@SpringBootConfiguration` 標注，主要用來將**目前類別**(`DemoApplication`)套上 `@Configuration` 標注，你可以看一下 `@SpringBootConfiguration` 的原始碼部分，他有套用一個 `@Configuration` 標注：
    
    ```java
    @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](https://www.baeldung.com/spring-bean) 元件的過程。
    
2.  `@EnableAutoConfiguration`
    
    這個 `@EnableAutoConfiguration` 標注，會先讓 Spring Boot 透過 `@ComponentScan` 的定義，自動找出所有相依套件中 JAR 檔案中有套用 `@Component` 標注的類別，並自動**建立**與**註冊**成 [Spring Bean](https://www.baeldung.com/spring-bean) 元件。這個過程就如同自動幫你在這些類別加上 `@Configuration` 標注的意思。
    
    > 例如 `Tomcat` 或 `Spring MVC` 等相依套件，都有類似的設計，所以使用 `@EnableAutoConfiguration` 可以大幅簡化程式碼數量。
    
3.  `@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% 的情境下都可以忽略底層的技術細節。

### 示範幾種不同的套用情境

1.  整個應用程式只有一個類別，不需要 `@ComponentScan` 自動掃描 `sub-packages`
    
    ```java
    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](https://www.baeldung.com/spring-bean)，但我們並沒有套用 `@ComponentScan`，所以他並不會自動去找 `com.duotify.starter1` 與 `com.duotify.starter1.*` 的類別。
    
    這裡的 `@RestController` 標注，其原始碼長這樣，他包含了 `@Controller` 與 `@ResponseBody` 標注：
    
    ```java
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Controller
    @ResponseBody
    public @interface RestController {
    ```
    
    而 `@Controller` 標注的原始碼長這樣：
    
    ```java
    @Target(ElementType.TYPE)
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Component
    public @interface Controller {
    ```
    
    所以其實他有套用 `@Component`，因此會被 `@EnableAutoConfiguration` 標注自動設定，所以 `DemoApplication` 類別本身就會被註冊成 [Spring Bean](https://www.baeldung.com/spring-bean) 元件，因此可以直接當 Controller 來執行。
    
2.  整個應用程式不只有一個類別，我們希望把 Controller 類別拆開來撰寫，因此需要 `@ComponentScan` 自動掃描 `sub-packages` 中的類別
    
    我先把 `DemoApplication.java` 改成這樣：
    
    ```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` 類別：
    
    ```java
    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](https://www.baeldung.com/spring-bean) 元件。
    
    ```bash
    $ curl localhost:8080
    {"timestamp":"2022-09-19T08:02:19.812+00:00","status":404,"error":"Not Found","path":"/"}
    ```
    
    我們在 `DemoApplication.java` 的 `DemoApplication` 類別加上 `@ComponentScan` 標注：
    
    ```java
    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 了！
    
    ```bash
    $ curl localhost:8080
    Hello World!
    ```
    
3.  我們有個共用的物件，透過 `@Bean` 註冊，並讓其他也註冊在 **Spring IoC container** 的元件使用
    
    因為所有使用 `@Component` 標注的類別，最終都會被註冊到 **Spring IoC container** 裡面。而所有註冊到 **Spring IoC container** 的元件都可以注入到其他元件中。所以我們可以在 `DemoApplication` 類別加入一個 Spring Bean 方法：
    
    ```java
    @Bean
    public String appName() {
      return "Starter1";
    }
    ```
    
    加入之後，這個有套用 `@Bean` 標注的 `appName()` 方法，其實並不會在 Spring Boot 應用程式啟動時自動執行，除非你在 `DemoApplication` 類別上套用 `@Configuration` 標注。結果如下：
    
    ```java
    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` 標注：
    
    ```java
    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 來用：
    
    ```java
    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` 即可：
    
    ```java
    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 + "!";
        }
    }
    ```
    
    測試執行：
    
    ```bash
    $ curl localhost:8080/
    Hello Starter1!
    ```
    

### 總結

其實這篇文章已經牽涉許多基礎知識，其中包含：

-   [Spring Initializr](https://start.spring.io/)
-   [Spring Boot CLI](https://docs.spring.io/spring-boot/docs/current/reference/html/cli.html)
-   [Using the @SpringBootApplication Annotation](https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.using-the-springbootapplication-annotation)
    -   `@SpringBootApplication`
        -   `@EnableAutoConfiguration`
        -   `@ComponentScan` ([Spring Component Scanning](https://www.baeldung.com/spring-component-scanning))
        -   `@SpringBootConfiguration`
            -   `@Configuration`
-   [Spring Stereotype Annotations](https://www.geeksforgeeks.org/spring-stereotype-annotations/)
    -   `@Component`
        -   `@Controller`
            -   `@RestController`
        -   `@Service`
        -   `@Compoment`
        -   `@Repository`
        -   `@Configuration`
-   [Spring IoC Container](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#beans-introduction)
    -   [`@Bean`](https://www.baeldung.com/spring-bean)
    -   [`@Autowired`](https://www.baeldung.com/spring-autowire)

因為這些都是 Spring 的核心機制，只要能完整理解上述概念，你就可以很好的理解其他部分！👍

### 相關連結

-   [Difference Between @ComponentScan and @EnableAutoConfiguration in Spring Boot | Baeldung](https://www.baeldung.com/spring-componentscan-vs-enableautoconfiguration)
-   [Spring Component Scanning | Baeldung](https://www.baeldung.com/spring-component-scanning)
-   [What is a Spring Bean? | Baeldung](https://www.baeldung.com/spring-bean)
-   [Spring Boot Change Context Path | Baeldung](https://www.baeldung.com/spring-boot-context-path)
