---
# System prepended metadata

title: SpringBoot3 學習紀錄
tags: [JDK17, SpringBoot, Java]

---

---
title: 'SpringBoot3 學習紀錄'
disqus: hackmd
---

* 尚硅谷筆記：https://blog.csdn.net/qq_40734758/article/details/132910708
# 1. 概念
* **特性**
    * 快速創建Spring應用
    * 直接嵌入Tomcat、Jetty or Undertow(`無需要部署war包`)
    * **重點**：提供可選的`starter`，簡化應用整合
        * **場景啟動器**(`starter`)
        * 導包一堆，控制好版本。
        * 為每一種場景準備了一個依賴：web-starter、mybatis-starter、...
    * 按需自動配置spring與第三方庫
        * 如果這些場景我要使用（生效）。 這個場景的所有配置都會自動配置好
        * **約定大於配置**：每個場景有很多默認配置
        * 自定義：配置文件中修改幾項即可
    * 提供**生產級特性**：
        * 監控指標
        * 健康檢查
        * 外部化配置(`環境變數`、`指定讀取某個位置的application.properties`)
    * 無代碼生成、**無xml** 


# 2. 常用註解
## 2.1 組件註冊
* @Configuration：通用配置類
* @SpringBootConfiguration：標示為SpringBoot配置類(`與@Configuration相同`)
* @Bean、@Scope
* @Controller、@Service、@Repository、@Component
* @Import
* @ComponentScan

## 2.2 條件註解
* 如果條件成立，就**觸發指定行為**(`註冊Bean`)
    ```java=
    public class AppConfig {

        //有Student類，就觸發創建Student這個Bean
        @ConditionalOnClass(name = "com.nicolas.springboot3_test1.entity.Student")
        @Bean
        public Student getStudent() {
            return new Student();
        }
    }
    ```
* @**ConditionalOnXXX**：
    * @ConditionalOnClass：如果類路徑中**存在**這個類，則觸發指定行為
    * @ConditionalOnMissingClass:如果類路徑中**不存在**這個類，則觸發指定行為
    * @ConditionalOnBean：如果容器中**存在**這個Bean，則觸發指定行為
    * @ConditionalOnMissingBean:如果容器中不存在這個Bean，則觸發指定行為

## 2.3 屬性綁定
* @ConfigurationProperties：配置屬性類
* @EnableConfigurationProperties：**啟用**配置屬性類

## 2.4 自動配置原理
* **流程**：
    1. **導入start-web**：導入web開發環境
        * 場景啟動器匯入了相關場景的所有依賴：starter-json、starter-tomcat、spring-mvc
        * 每個**場景啟動器**都引入一個s**pring-boot-starter**(`核心場景啟動器`)
        * **核心場景啟動器**引入`spring-boot-autoconfigure`包
        * `spring-boot-autoconfigure`裡面囊括了**SpringBoot所有支持場景下的所有配置**
        * 只要這個包下的所有類別都能生效，那麼相當於**SpringBoot官方寫好的整合功能**
        * **SpringBoot**默認掃描不到`spring-boot-autoconfigure`下寫好的所有**配致類**。(`這些配置類給我們做了整合操作`）
    2. **主程序**：`@SpringBootApplication`
        1. `@SpringBootApplication`由**三個註解**組成
            * `@SpringBootConfiguration`：聲明是配置類
            * `@ComponentScan`：掃描包
            * `@EnableAutoConfiguration`
        2. **SpringBoot**預設`只能掃描自己主程式所在的包及其下面的子包`，**掃描不到**`sprine-boot-autoconfigure`包中官方寫好的**配置類**
        3.  `@EnableAutoConfiguration`：SpringBoot**開啟自動配置**的核心
            *  **@Import(AutoConfigurationImportSelector.class)**：**批量**給容器導入組件
                * ` AutoConfigurationImportSelector.selectImports`()方法
            * **SpringBoot**啟動會預設載入**142個配置類**。
            * 這**142個配置類**來自於 `/META-INF/spring/org.springframework.boot.autoconfigure.Autoconfiguration.imports`文件
            * 項目啟動時，利用`@Import`批量導入組件機制把`sprine-boot-autoconfigure`包下的**142**個`xxxAutoConfiguration`類導入近來(**自動配置類**)
            * 雖然導入142個
        4. 按需生效
            *  並不是142個配置類都會生效
            *  每一個自動配置類，都會有**條件註解**`@ConditionalOnXXX`，只有條件生效，才會建立
    5. `xxxAutoConfiguration`**自動配置類**
        1. 給容器中使用`@Bean`放一堆組件
        2. 每個**自動配置類**，都可能會有`@EnableConfigurationProperties`，用來把設定檔中配的**指定前綴的屬性值**封裝到`xxxProperties`類中
        3. 以Tomcat為例：把服務器的所有配置都是以server開頭。配置都封裝到屬性類中
        4. 給容器中放的所有組件的一些核心參數，都來自于`xxxProperties`。`xxxProperties`都是和**配置文件**綁定
            * 只需要修改配置文件的值，核心組件的底層參數都能修改
    7. **寫業務，無須關心各種整合**


# 3. 自定事件驅動
* **同一線程運行**：Spring Boot **預設事件**是`同步發送`的
* **訂閱者\監聽者**：**實現ApplicationListener<監聽事件>類** 或 **@EventListener在方法上**
    * 使用：
        * **@Bean**、**@EventListener**
        * SpringApplication.addListeners(...) 或 SpringApplicationBuilder.listeners(...)。
        * META-INF/spring.factories。
    * @Order：指定訂閱者的處理順序(多個訂閱者的情況) 
* **自定發布器**：可以給多個事件用來，發布事件
    * 方式ㄧ：實現 **ApplicationEventPublisherAware**
    * 方式二：注入 **ApplicationEventMulticaster**
* **自定事件**：繼承 **ApplicationEvent**，實現一個構造器

## 3.1 發布器
```java=
import org.springframework.context.ApplicationEvent;
import org.springframework.context.ApplicationEventPublisher;
import org.springframework.context.ApplicationEventPublisherAware;
import org.springframework.stereotype.Service;

//發布器
@Service
public class EventPublisher implements ApplicationEventPublisherAware {
    //底層發送事件用的元件，SpringBoot 會透過 ApplicationEventPublisherAware 介面自動注入
    private ApplicationEventPublisher applicationEventPublisher;

    //所有事件都可以發送
    public void sendEvent(ApplicationEvent event) {
        //調用底層API發送事件
        applicationEventPublisher.publishEvent(event);
    }

    //會被自動調用，把真正發送事件的底層給注入
    @Override
    public void setApplicationEventPublisher(ApplicationEventPublisher applicationEventPublisher) {
        this.applicationEventPublisher = applicationEventPublisher;
    }
}
```
## 3.2 事件
* **自定事件**：**繼承**`ApplicationEvent`，實現一個構造器
    ```java=
    //登陸成功事件
    public class LoginSuccessEvent extends ApplicationEvent {
        public LoginSuccessEvent(User source) {
            super(source);
        }
    }
    ```
    * ApplicationEvent.**getSource**()：獲取**傳入值**(`User類`)
## 3.3 訂閱者\監聽者
### 3.3.1 該事件發生的位置沒有事務
* 方式ㄧ：實現**ApplicationListener**，並註冊**LoginSuccessEvent事件**
    ```java=
    //訂閱事件(LoginSuccessEvent)
    //方式一：實現ApplicationListener，並註冊LoginSuccessEvent事件
   
    @Component
    public class AccountService implements ApplicationListener<LoginSuccessEvent> {
        private final static Logger LOGGER= LoggerFactory.getLogger(AccountService.class);
        @Transactional//如果下列方法體內有事務操作，需要添加
        @Override
        public void onApplicationEvent(LoginSuccessEvent event) {
            LOGGER.info("ApplicationListener get Event:" + event);
            //獲取事件中傳入的參數值
            User source = (User) event.getSource();
            System.out.println(source);
            throw new RuntimeException("AAAA");//測試是否在同一條線程上
        }
    }
    ```
* 方式二：**@EventListener**
    ```java=
    import com.nicolas.springboot3_test1.entity.Student;
    import com.nicolas.springboot3_test1.entity.User;
    import com.nicolas.springboot3_test1.event.LoginSuccessEvent;
    import org.slf4j.Logger;
    import org.slf4j.LoggerFactory;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.event.EventListener;

    @Configuration
    public class AppConfig {
        //訂閱事件：方式二：@EventListener
        //訂閱LoginSuccessEvent
        @Transactional//如果下列方法體內有事務操作，需要添加
        @EventListener
        public void getLoginSuccessEvent(LoginSuccessEvent event) {
            LOGGER.info("@EventListener get Event: " + event);
            //獲取事件中傳入的參數值
            User source = (User) event.getSource();
            System.out.println(source);
        }
    }
    ```
### 3.3.2 該事件發生的位置有事務
* @EventListener 改用 @TransactionalEventListener



## 3.4 使用

```java=
import com.nicolas.springboot3_test1.entity.User;
import com.nicolas.springboot3_test1.event.EventPublisher;
import com.nicolas.springboot3_test1.event.LoginSuccessEvent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class LoginController {
    @Autowired
    EventPublisher eventPublisher;

    @GetMapping("/login")
    public void login(){
        //事件發送
        //1. 準備事件
        LoginSuccessEvent loginSuccessEvent = new LoginSuccessEvent(new User("A1", "PWD"));
        //2. 發送事件
        eventPublisher.sendEvent(loginSuccessEvent);
    }
}
```
# X. SpringBoot生命週期
* **SpringApplicationRunListener**：**定義了多個回調方法**，可以在`SpringBoot生命週期`各個階段插入各自的邏輯
## X.1 SpringBoot生命週期監聽
* **圖**：
    ![SpringBoot生命週期流程](https://hackmd.io/_uploads/H1_3S5sUA.png)
1. **引導**
    1. **starting**：應用開始，SpringApplication的run方法一調用，只要有了Bootstrapcontext就開始執行
    2. **environmentPrepared**：**環境準備完成**（`把啟動參數等綁定到環境變數中`），但**IOC容器**(`ApplicationContext`)還沒創建
3. **啟動**
    1. **contextPrepared**：**IOC容器**(`ApplicationContext`)創建、準備完成，`主配置類未加載`
    3. **contextLoaded**：**IOC容器**(`ApplicationContext`)**加載**(`加載主配置類`)完成，`IOC容器未刷新`
        * **加載主配置類**：**知道目前有哪些Bean**
    5. **started**：**IOC容器**(`ApplicationContext`)**刷新**(`創建Bean`)完成，`Runner未調用`
        * Runner接口：ApplicationRunner、CommandLineRunner
    7. **ready**：runner調用完成
5. **運行**：**context.isRunning()**
### X.1.1 SpringApplicationRunListener範例(生命週期監聽)
* **代碼**：
    ```java=
    package com.nicolas.springboot3_test1.listener;

    import org.springframework.boot.ConfigurableBootstrapContext;
    import org.springframework.boot.SpringApplicationRunListener;
    import org.springframework.context.ConfigurableApplicationContext;
    import org.springframework.core.env.ConfigurableEnvironment;

    import java.time.Duration;

    /**
     * SpringApplicationRunListener 都調用一次
     * 1. 引導：利用BootstrapContext 引導整個項目啟動
     *          starting：應用開始，SpringApplication的run方法一調用，只要有了Bootstrapcontext就執行
     *          environmentPrepared：環境準備好（把啟動參數等綁定到環境變數中），但ioc還沒創建
     * 2. 啟動：
     *          contextPrepared：ioc容器建立、準備完成，但是sources（主配置類別）沒載入。(關閉引導啟動器(BootstrapContext))
     *          contextLoaded：ioc容器加載，但ioc容器並未刷新
     *                  加載 => 主配置類別加載
     *                  未刷新 => bean未創建
     *          ====IOC容器中並未創建bean=======
     *          started：ioc容器刷新(bean創建完成)，未調用Runner接口的實現類
     *              Runner接口：ApplicationRunner、CommandLineRunner
     *          ready：調用Runner接口的實現類
     * 3. 運行
     *      以前步驟都正確執行，代表容器running。
     */

    public class CoreListener implements SpringApplicationRunListener {
        @Override
        public void starting(ConfigurableBootstrapContext bootstrapContext) {
            //  listeners.starting(bootstrapContext, this.mainApplicationClass);
            System.out.println("=====1. start====正在啟動");
        }

        @Override
        public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
            // listeners.environmentPrepared(bootstrapContext, environment);
            System.out.println("=====2. environmentPrepared====環境準備完成");
        }

        //ApplicationContext >> IOC容器：管理Bean的創建、加載、生命週期
        //this.prepareContext中會調用contextPrepared、contextLoaded
        @Override
        public void contextPrepared(ConfigurableApplicationContext context) {
            System.out.println("=====3. contextPrepared====IOC容器準備完成");
        }


        @Override
        public void contextLoaded(ConfigurableApplicationContext context) {
            System.out.println("=====4. contextLoaded====IOC容器 加載完成");
        }

        @Override
        public void started(ConfigurableApplicationContext context, Duration timeTaken) {
            System.out.println("=====5. started====啟動完成");
        }

        @Override
        public void ready(ConfigurableApplicationContext context, Duration timeTaken) {
            System.out.println("=====ready====準備就緒");
        }

        @Override
        public void failed(ConfigurableApplicationContext context, Throwable exception) {
            System.out.println("=====failed====啟動失敗");

        }
    }

    ```
* 設定在`META-INF/spring.factories`：
    * **org.springframework.boot.SpringApplicationRunListener**=`com.nicolas.springboot3_test1.listener.CoreListener`

## X.2 事件觸發機制
* **getSpringFactoriesInstances方法**：表示透過`SPI機制`來獲取對象，需要在`META-INF/spring.factories`中設定
    * 讀取`META-INF/spring.factories`
    * `spring.factories`的**SPI機制**採用此種**設定方式**：
        * **全路徑類名**=該實現類**全路徑類名**
### X.2.1 各種回調監聽器
#### X.2.1.1 BootstrapRegistryInitializer
* **說明**：**感知引導初始化**
    * **starting階段前會觸發**
* **SpringFactoriesLoader類**來獲取對象
    * **設定位置**：`META-INF/spring.factories`
* **場景**：進行密鑰校對授權 
* 創建**引導上下文**(`bootstrapContext`)觸發
#### X.2.1.2 ApplicationContextInitializer
* **說明**：**感知IOC容器初始化**
    * **contextPrepared階段前會觸發**
* **設定位置**(`resource目錄下`)：META-INF/spring.factories
#### X.2.1.3 **SpringApplicationRunListener**
* **說明**：**感知SpringBoot全階段生命周期** + SpringBoot各種階段都能夠自定義操作
* **設定位置**：META-INF/spring.factories
    * **內容**：全路徑類名=該實現類全路徑類名
        * `org.springframework.boot.ApplicationContextInitializer`=com.nicolas.springboot3_test1.listener.**CoreListener**
#### X.2.1.4 Runner 接口
* **ApplicationRunner**：**started階段後會觸發**，卡死就不會就緒
    * @Bean
* **CommandLineRunner**：**started階段後會觸發**，卡死就不會就緒
    *  @Bean
#### X.2.1.5 ApplicationListener
* **說明**：**感知全階段**，基於**事件機制**(一旦到了哪個階段可以做別的事)
* 設定方式：
    * `@Bean`或`@EventListener`：**事件驅動開發**
    * `SpringApplication.addListeners(）`或 `SpringApplicationBuilder.listeners()`
    * META-INF/spring.factories

## X.3 ApplicationEvent 抽象類
* 於實現**事件驅動**的程式設計模型。它是 **Spring 事件機制的基礎** ，透過事件發布和監聽來實現鬆散耦合的元件之間的交互
* 事件驅動開發中的**事件**，需要`繼承ApplicationEvent`



### X.3.1 SpringBoot生命週期各個事件(9個)
* **事件順序和時機**：
    1. **ApplicationStartingEvent**：應用啟動但未做任何事項，除非註冊過listeners and intializers
    2. **ApplicationEnvironmentPreparedEvent**：Environment準備OK，但IOC並未
    3. **ApplicationContextInitializedEvent**：IOC準備完成，ApplicationContextInitialized調用，但是任何bean未加載
    4. **ApplicationPreparedEvent**：容器刷新之前，bean定義信息加載
    5. **ContextRefreshedEvent**：**容器準備刷新**
    6. **ApplicationStartedEvent**：容器刷新完成，runner未調用
    7. `AvailabilityChangeEvent`：**對接K8s**，`LivenessState.CORRECT` 應用**存活**
        * **存活探針**
    8. **ApplicationReadyEvent**：任何Runner被調用
    9.  `AvailabilityChangeEvent`：**對接K8s**，`ReadinessState.ACCEPTING_TRAFFIC` 應用**就緒**，可以接受API請求
        * **就緒探針**
* **ApplicationFailedEvent**：啟動錯誤事件
* **使用SpringBoot生命週期各個事件**：**監聽SpringBoot生命週期事件**
    * **方式一**：使用 `@EventListener`
    * **方式二**：實作 `ApplicationListener\<SpringBoot生命週期各個事件>` 接

## X.4 SpringApplicationRunListener各SpringBoot生命週期回調方法 + SpringBoot生命週期各個事件 + 回調監聽器
![SpringBoot生命週期流程_事件](https://hackmd.io/_uploads/BJEAeni8R.png)


# 4. 自動配置原理
## 4.1 入門
* 應用關注的三大核心：場景、配置、組件(`Bean`)

### 4.1.1 自動配置流程
![SpringBoot3_自動配置流程](https://hackmd.io/_uploads/BkQm8qoLR.png)

### 4.1.2 SPI機制
* **自動配置流程**採用**SPI機制**
* **作用**：應用程序中**動態的發現**和**加載組件**
* SpringBoot是使用**SPI機制**的思想
* **思想**：定義一個**介面**或**抽象類**，然後透過在 classpath 中定義實作該介面的類別來實現對元件的動態發現和載入。 
* Java**默認的SPI機制**是搜尋`META-INF/services`下的文件
* **SpringBoot**建議參考：
    * `spring-boot-autoconfigure`包下的`META-INF/spring`
### 4.1.3 功能開關
* 自動配置：全部配置好，不需要管理
    * 項目一啟動，SPI中指定的所有就都加載
* **@EnableXXX**：手動控制哪些功能的開啟，手動匯入
    * 開啟 xxx 功能。
    * 利用 **@Import** 把此功能要用的組件導入進去。
* 常見功能開關
    * @EnableScheduling：開啟排成器
    * @EnableAsync：開啟異步機制

## 4.2 進階
### 4.2.1 @SpringBootApplication
* **@SpringBootConfiguration**：是 **@Configuration**，容器中的元件，`標示為配置類`
    * Spring IOC 啟動就會載入建立這個類別物件。
* **@EnableAutoConfiguration**：**開起自動配置功能**
    * **@AutoConfigurationPackage**：**掃描主程式中的包及其子包**
        * 利用 **@Import({AutoConfigurationImportSelector.class})** 想要給容器導入組件
        * **AutoConfigurationImportSelector** 把**主程式所在的pacakge**的所有組件導入進來(`解釋 為什麼 SpringBoot 預設只掃描主程式所在的套件及其子包`)
    * **@Import(AutoConfigurationImportSelector.class)**：**載入所有自動配置類**，載入 starter 導入的組件。
        * **AutoConfigurationImportSelector**.**getCandidateConfigurations()** 方法中，掃描要載入的組件(`SPI機制`)
            * **掃描 SPI 檔案**：`META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`
* **@ComponentScan**：組件掃描
    * **源碼**：
        ```java=
        @ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
                @Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
        ```
        * 排除組件，排除前面已經掃描進來的**配置類**、和**自動配置類**

### 4.2.2 生命週期啟動載入機制
![SpringBoot3_生命週期啟動載入機制](https://hackmd.io/_uploads/rkCwU9oI0.png)

# 5. 自定義 start
* **場景**：抽取聊天機器人場景，它可以打招呼。
* **效果**：任何專案匯入此 starter 都具有打招呼功能，且問候語中的人名需要可以在設定檔中修改。
* **步驟**：
    1. 建立自訂 starter 項目，引入 spring-boot-starter 基礎依賴。
    2. 編寫模組功能，引入模組所有需要的依賴。
    3. 編寫 **xxxAutoConfiguration** 自動配置類，幫其他項目導入這個模組所需的所有元件。
    5. 使用**自定註解**(`@EnableXxx`)或編寫**SPI設定檔** `META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports` 指定啟動需要載入的自動設定。
        ```java=
        import org.springframework.context.annotation.Import;

        import java.lang.annotation.*;

        @Retention(RetentionPolicy.RUNTIME)
        @Target(ElementType.TYPE)
        @Documented
        @Import(SourceAutoConfiguration.class)
        public @interface EnableSource {
        }
        ```
    7. 其他項目引進即可使用
        * 項目如果採用**自定註解**方式，該項目需要添加該`@EnableXxx`
* 非SpringBoot官方的**starter**，導入後發現`沒有上述的SPI 檔案`，**為SpringBoot2**(`該依賴無法被自動Import`)
    * 可再確認是否有無`@EnableXxx`

## 5.1 依賴
```xml=
 <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- properties配置處理器，提示自定義properties-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-autoconfigure</artifactId>
        </dependency>
    </dependencies>
```

## 5.2 代碼
* XxxProperties類
    ```java=
    import lombok.AllArgsConstructor;
    import lombok.Data;
    import lombok.NoArgsConstructor;
    import org.springframework.boot.context.properties.ConfigurationProperties;

    @ConfigurationProperties(prefix = "roboot")
    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public class RobootProperties {
        private String name;
        private Integer age;
        private String email;
    }
    ```
* Server類(`業務邏輯`)
    ```java=
    import com.nicolas.roboot.properties.RobootProperties;
    import org.springframework.stereotype.Service;

    @Service
    public class RobootService {

        private final RobootProperties robootProperties;

        public RobootService(RobootProperties robootProperties) {
            this.robootProperties = robootProperties;
        }

        public String sayHello() {
            return String.format("name: %s, age: %d, email: %s", robootProperties.getName(), robootProperties.getAge(), robootProperties.getEmail());
        }
    }
    ```
* XxxAutoConfiguration類
    ```java=
    import com.nicolas.roboot.properties.RobootProperties;
    import com.nicolas.roboot.service.RobootService;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;


    //導入Roboot功能需要用的組件
    @EnableConfigurationProperties(RobootProperties.class)
    @ConditionalOnClass(RobootService.class)
    @Configuration
    public class RobootAutoConfiguration {
        private RobootProperties robootProperties;

        // 自動注入配置類
        public RobootAutoConfiguration(RobootProperties robootProperties) {
            this.robootProperties = robootProperties;
        }

        @Bean
        @ConditionalOnBean(RobootProperties.class)
        public RobootService getRobootService() {
            return new RobootService(robootProperties);
        }
    }
    ```
## 5.3 功能生效
### 5.3.1 方式ㄧ：@EnableXxx機制
* @EnableXxx
    ```java=
    @Target({ElementType.TYPE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    @Import(RobootAutoConfiguration.class)
    public @interface EnableRoboot {
    }
    ```
    * 將**RobootAutoConfiguratio類**給導入`IOC容器`
* **使用**：
    1. 依賴該pacakage
    2. 將 **@EnableRoboot** 註解，貼在該項目的啟動類上
### 5.3.2 方式二：完全自動配置
* **依賴 SpringBoot 的 SPI 機制**
* **步驟**：
    * **創建檔案**(`resources目錄下`)：`META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports`
    * 編寫**自動配置類**的**全路徑類名**(`與spring.factories的設定方式不同`)
        ```properties=
        com.nicolas.roboot.RobootAutoConfiguration
        ```
* **使用**：
    1. 依賴該pacakage後，啟動時，就會**自動加載**(`掃描每個依賴package下的META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports，如果有該檔案的話`)


# 6. Swagger文檔
* **OpenAPI 3 架構**
    ![OpenAPI3_架構](https://hackmd.io/_uploads/rJ6N2BqPC.png)
* springdoc-openapi-starter-webmvc-**ui**：**UI層**
    * 導入`swagger-ui`，**可視化的展現**接口文檔
* springdoc-openapi-starter-webmvc-**api**：**API層**
    * 適配**OpenAPI**規範 

## 6.1 整合
* **依賴**
    ```xml=
     <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>2.5.0</version>
    </dependency>
    ```
* **可選配置**
    ```properties=
    # /api-docs endpoint custom path # /v3/api-docs
    springdoc.api-docs.path=/api-docs
    
    ＃swagger相關配置在springdoc.swagger-ui
    # swagger-ui custom path
    springdoc.swagger-ui.path=/swagger-ui.html
    springdoc.show-actuator=true
    ```
    
## 6.2 使用
* 該版本默認**自動導入Swagger**，不需要添加`註解`、`配置類`
    * **SPI機制**(`org.springframework.boot.autoconfigure.AutoConfiguration.imports`)
### 6.2.1 常用註解
| 註解 | 標示位置 | 作用|
| -------- | -------- | -------- |
| @Tag     | controller類     | 標示Controller的作用     |
| @Parameter     | 參數     | 標示參數作用     |
| @Parameters     | 參數     | 參數多重說明     |
| @Schema     | model層的JavaBean     | 描述模型作用及每個屬性     |
| @Operation     | 方法     | 描述方法作用     |
| @ApiResponse     | 方法     | 描述響應狀態等     |
| @Hidden     | 方法\類型\屬性     | 隱藏     |

### 6.2.2 Docket設置
* **GroupOpenApi**類：**分組設置**，區分哪些路徑到哪一個文檔
    * **舊版本**：**Docket類**
* **整個文檔說明設定**：**兩種方式**
    * **註解**：`@OpenAPIDefinition`
    * **類**：`OpenAPI類`
* **配置**：
    ```java=
    import io.swagger.v3.oas.annotations.OpenAPIDefinition;
    import io.swagger.v3.oas.annotations.enums.SecuritySchemeType;
    import io.swagger.v3.oas.annotations.info.Info;
    import io.swagger.v3.oas.annotations.security.SecurityRequirement;
    import io.swagger.v3.oas.annotations.security.SecurityScheme;
    import org.springframework.beans.factory.annotation.Value;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.core.env.Environment;

    @Configuration("SwaggerConfig")
    @OpenAPIDefinition(info = @Info(title = "EunoAI APIi", version = "v1.0.0"),
            security = {@SecurityRequirement(name = "api_key")})
    @SecurityScheme(type = SecuritySchemeType.HTTP, name = "api_key", scheme = "bearer")
    public class SwaggerConfig {
        
        @Bean
        public GroupedOpenApi payApi() {
            return GroupedOpenApi.builder()
                    .group("Pay Group")
                    .pathsToMatch("/pay/**")
                    .build();
        }
    }
    ```
    * **@SecurityScheme**：設定Security
        *  `scheme = "bearer"`：token 前面才會帶Bearer
    * **@Info**：之前的**ApiInfo**類
        * 設定整個頁面的信息
* **配置API不需要 Security**：該方法添加 **@SecurityRequirements()**

### 6.2.3 application.properties配置
* 使用 Spring 框架的支援來**處理轉送的請求頭**(`使用Nginx`)
    ```properties=
    server.forward-headers-strategy=framework
    ```
* **控制是否啟用Swagger**，默認啟用
    * 源碼中**SwaggerConfig**的設定
        * `@ConditionalOnProperty(name = "springdoc.swagger-ui.enabled", matchIfMissing = true)`
    * **屬性**：`springdoc.swagger-ui.enabled`(`唯一控制啟動方式`)


# 7. 遠程調用
* SpringBoot整合提供了很多方式進行遠端調用
    * 輕量級客戶端方式
        * RestTemplate：普通開發
        * WebClient：響應式程式設計開發
        * HttpInterface：聲明式編程＋響應式
    * SpringCloud分散式解決方案方式
        * Spring Cloud OpenFeign
    * 第三方框架
        * Dubbo
        * gRPC
* API/SDK的區別
    * API：接口
        * 遠程提供功能
    * SDK：工具包
        * 導入jar包，直接調用功能
## 7.1 WebClient
* 非阻塞式，響應式的客戶端
* **依賴**：spring-boot-starter-webflux(`響應式編程`)

## 7.2 HttpInterface
1. **依賴**：spring-boot-starter-webflux(`響應式編程`)
2. 定義接口：
    ```java=
    import org.springframework.http.MediaType;
    import org.springframework.web.bind.annotation.RequestHeader;
    import org.springframework.web.bind.annotation.RequestParam;
    import org.springframework.web.service.annotation.GetExchange;
    import reactor.core.publisher.Mono;

    public interface WeatherInterface {
        //發送get請求，設定URL全路徑
        @GetExchange(url = "http://localhost:8080/area",accept = MediaType.APPLICATION_JSON_VALUE)//接受json數據
        Mono<String> getWeather(@RequestParam("city") String city,//參數
                        @RequestHeader("Authorization") String token//請求頭
        );
        
         //發送get請求，設定URL路徑，IP/域名的部分 => 設定在WebClient中的baseUrl(http://localhost:8080/)
        @GetExchange("/test/task")
        Task getTask(@RequestBody Task task);
    }
    ```
4. 定義代理：默認為響應模式
    ```java=
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.http.MediaType;
    import org.springframework.web.reactive.function.client.WebClient;
    import org.springframework.web.reactive.function.client.support.WebClientAdapter;
    import org.springframework.web.service.invoker.HttpServiceProxyFactory;

    @Configuration
    public class WeatherConfig {
        
        @Bean
        public HttpServiceProxyFactory getHttpServiceProxyFactory(){
            //1.創建客戶端
            WebClient webClient = WebClient.builder()
                    .defaultHeader("Content-Type", MediaType.APPLICATION_JSON_VALUE)
                    //響應資料量太大有可能會超出Buffersize，所以這裡設定的大一點
                    .codecs(clientCodecConfigurer -> clientCodecConfigurer.defaultCodecs().maxInMemorySize(256 * 1024 * 1024))
                    //錯誤處理
                    .defaultStatusHandler(
                        HttpStatusCode::isError,
                        clientResponse -> clientResponse.bodyToMono(Map.class).map(e -> new RuntimeException((String) e.get("error")))
                    )
                    .build();

            //2. 創建工廠
            HttpServiceProxyFactory factory = HttpServiceProxyFactory.builder()
                    .exchangeAdapter(WebClientAdapter.create(webClient))
                    .build();
            return factory;
        }
        
        @Bean
        public WeatherInterface getWeatherInterface(HttpServiceProxyFactory factory) {
            //3. 獲取代理對象
            return factory.createClient(WeatherInterface.class);
        }
    }
    ```
6. 使用
    ```java=
    import org.springframework.stereotype.Service;
    import reactor.core.publisher.Mono;

    @Service
    public class WeatherService {
        private final WeatherInterface weatherInterface;

        public WeatherService(WeatherInterface weatherInterface) {
            this.weatherInterface = weatherInterface;
        }

        public Mono<String> getWeather(String city) {
            return weatherInterface.getWeather(city, "header");
        }
    }
    ```
    
# 8. 可觀測性 
* SpringBoot Actuator
    ```xml=
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-actuator</artifactId>
        <version>3.3.0</version>
    </dependency>
    ```
* 對線上應用，進行觀測、監控、預警
    * 健康狀況
        * 組件狀態
        * 存活狀態
    * 運行指標
        * CPU
        * 內存
        * GC
        * 吞吐量
        * 響應成功機率
    * 鏈路追蹤
        
## 8.1 實戰
* 暴露指標
    ```properties=
    # 通過web方式暴露所有監控端點信息
    management.endpoints.web.exposure.include=*
    ```
* 訪問數據：訪問`http://localhost:8080/actuator` 展示所有可用的監控端點

## 8.2 Endpoint
### 8.2.1 常用端點
| ID | 描述 | 
| -------- | -------- | 
| auditevents     | 揭露當前應用程式的審核事件資訊。需要一個`AuditEventRepository`組件|
| beans     |顯示應用程式中所有Spring Bean的完整清單。     | 
| caches     | 暴露可用的緩存     | 
| conditions     | 顯示自動配置的所有條件信息，包括匹配或不匹配的原因。     | 
| configprops     | 顯示所有`@ConfigurationProperties`    | 
| env     | 暴霱Sprinq的屬性`ConfigurableEnvironment`     | 
| flyway     | 顯示已套用的所有**Flyway**資料庫遷移。<br>需要一個或多個`Flyway`組件。     | 
| health     | 顯示應用程式運行狀況資訊     | 
| httptrace     | 顯示HTTP追蹤訊息（預設情況下，最近100個HTTP請求-回應）。<br>需要一個HttpTraceRepository元件。     | 
| info     | 顯示應用程式信息     | 
| integrationgraph     | 顯示Spring **integrationgraph**。需要依赖`spring-integration-core`  | 
| loggers     | 顯示和修改應用程式中日誌的配置     | 
| liquibase     | 顯示已套用的所有**Liquibase**資料庫遷移。需要一個或多個Liquibase組件。| 
| metrics     | 顯示目前應用程式的 **「指標」** 資訊     | 
| mappings     | 顯示所有`@RequestMapping`路徑清單   | 
| scheduledtasks     | 顯示應用程式中的**排程任務**     | 
| sessions     | 允許從**Spring Session**支援的會話儲存中檢素和刪除使用者會話。需要使用**Spring Session**的基於**Servlet**的**Web**應用程式。     | 
| shutdown     | 使應用程式正常關閉。預設禁用     | 
| startup     | 顯示由`ApplicationStartup`收集的啟動步驟資料。需要使用`SpringApplication`進行設定`BufferingApplicationＳtartup`     | 
| threaddump     | 執行線程轉儲。 | 
| headdump     | 返回`hprof`堆內存文檔     | 
| jolokia     | 透過**HTTP**暴露JMX bean（需入**Jookia**，不適用於**WebFlux**）。需要引入依賴`jolokia-core `   | 
| logfile     | 返回日誌檔案的內容（如果已設定`1ogging.file.name`或`logging.file.path`屬性）。支援使用**HTTP** `Range`標頭來檢索部分日誌文件的內容     | 

* **重要Endpoint**：`threaddump`、`headdump`、`metrics`


### 8.2.2 定製端點
* **健康監控**(`Health`)：返回組件存活、死亡
* **指標監控**(`Metrics`)：次數(`API調用`)、速率

#### 8.2.2.1 健康監控
* 設定暴露所有健康信息
    ```properties=
    # 顯示健康端點的詳細信息
    management.endpoint.health.show-details=always
    ```
* 被監控類：
    ```java=
    import org.springframework.stereotype.Component;

    @Component
    public class MyHealthComponent {

        public Integer check(){
            //業務代碼判斷這個組件是否該是存活狀態
            return 1;
        }
    }
    ```
* 監控類：繼承**AbstractHealthIndicator**抽象類
    ```java=
    import com.nicolas.springboot3_test1.actuator.component.MyHealthComponent;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.actuate.health.AbstractHealthIndicator;
    import org.springframework.boot.actuate.health.Health;
    import org.springframework.stereotype.Component;

    //實現HealthIndicator接口，定製組件健康狀態返回
    @Component
    public class MyHealthIndicator extends AbstractHealthIndicator {
        @Autowired
        private MyHealthComponent myHealthComponent;

        //健康檢查
        @Override
        protected void doHealthCheck(Health.Builder builder) throws Exception {
            //自定義檢查方法
            Integer check = myHealthComponent.check();
            if (check == 1) {
                //存活
                builder.up()
                        //配置健康信息
                        .withDetail("code","1000")
                        .withDetail("msg","health")
                        .build();
            } else {
                //下線
                builder.down()
                        .withDetail("code","1001")
                        .withDetail("msg","dead").build();
            }
        }
    }
    ```
* **信息返回**：`http://localhost:8080/actuator/health`
    ```josn=
    {
        "status": "UP",
        "components": {
            "diskSpace": {
                "status": "UP",
                "details": {
                    "total": 1995218165760,
                    "free": 1888140931072,
                    "threshold": 10485760,
                    "path": "/Users/nicolas/SpringBoot3/springboot3_test1/.",
                    "exists": true
                }
            },
            "my": {
                "status": "UP",
                "details": {
                    "code": "1000",
                    "msg": "health"
                }
            },
            "ping": {
                "status": "UP"
            }
        }
    }
    ```
    
#### 8.2.2.2 指標監控
* **目標**：**計算方法調用次數**
    * 使用**MeterRegistry類**(`注入到被監控類`)來進行各種指標統計
* **被監控類**：
    ```java=
    import io.micrometer.core.instrument.Counter;
    import io.micrometer.core.instrument.MeterRegistry;
    import org.springframework.stereotype.Component;

    @Component
    public class MyMetricsComponent {
        //Spring規定，如果有參構造器只有一個參數，該參數會從IOC容器中獲取後，注入到MyMetricsComponent中
        Counter counter = null;

        public MyMetricsComponent(MeterRegistry meterRegistry) {
            //counter()：方法計數器
            counter = meterRegistry.counter("mymetrics.sayhello");
        }


        public void sayHello() {
            System.out.println("hello");
            counter.increment();
        }
    }
    ```
    * **指標名**：`mymetrics.sayhello`
* **使用**：`http://localhost:8080/actuator/metrics/{指標名}`
    * `http://localhost:8080/actuator/metrics/mymetrics.sayhello`
    * **返回**：
        ```json=
        {
            "name": "mymetrics.sayhello",
            "measurements": [
                {
                    "statistic": "COUNT",
                    "value": 2.0
                }
            ],
            "availableTags": []
        }
        ```

###### tags: `SpringBoot` `Java17`