--- 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`進行設定`BufferingApplicationStartup` | | 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`