天天看點

SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

SpringBoot筆記

文章目錄

  • SpringBoot筆記
  • 一、回顧Spring
    • 1.1、概念
    • 1.2、IOC、DI的透徹了解
      • IoC(控制反轉)
      • DI(依賴注入)
    • 1.3、自己的了解
  • 二、微服務
    • 2.1、概念
    • 2.2、 單體應用時代
    • 2.3、 微服務時代
  • 三、SpringBoot
    • 3.1、概念
    • 3.2、優缺點
    • 3.3、為什麼要使用springboot?
    • 3.4、第一個SpringBoot程式
      • 1、建立應用
        • 1.1、官網建立
        • 1.2、**idea工具**(推薦使用)
      • 2、檢視結構
      • 3、編寫controller
      • 4、啟動項目
      • 5、通路項目
    • 3.5、分析SpringBoot應用
      • 1、pom.xml
      • 2、application.yml
      • 3、maven
    • 3.6、自定義Application
    • 3.7、配置檔案
      • 1、概念
      • 2、兩種配置檔案的格式
      • 3、yml基本文法
      • 4、配置檔案的加載順序
    • 3.8、自動配置原理分析(重點)
      • `@EnableAutoConfiguration`
      • `getAutoConfigurationEntry(annotationMetadata)`
      • `loadSpringFactories(classLoader)`
      • 總結
    • 3.9、熱部署
      • 1、概念
      • 2、步驟
        • 2.1、導入依賴
        • 2.2、idea設定
    • 4.0、日志架構
      • 1、概念
      • 2、日志架構發展史
      • 3、日志架構
      • 4、日志門面
        • 1、 概念
    • 4.1、異步架構
      • 1、概念
      • 2、大緻業務需求
      • 3、差別
      • 4、實作異步
        • 4.1、啟動類添加支援
        • 4.2、編寫Controller層
        • 4.3、編寫業務層
        • 4.4、運作結果
        • 4.5、小結
      • 5、異步線程池的優化
        • 5.1、分析
        • 5.2、問題
        • 5.2、線程池的簡單了解
        • 5.3、@Async 注解支援的線程機制
        • 5.4、小結
  • 四、Web開發
    • 4.1、說明
    • 4.2、統一結果傳回 R類
      • 1、概念
      • 2、R類結構
      • 3、解決方案
        • 實作
        • 測試
    • 4.3、全局異常處理
      • 1、概念
      • 2、實作
        • 2.1、編寫Error和ExceptionCodeEnum
        • 2.2、編寫配置類
        • 2.3、測試
      • 3、自定義異常
        • 3.1、概念
        • 3.2、實作
          • 1、編寫自定義異常類
          • 2、編寫異常枚舉類
          • 3、編寫全局異常類
          • 4、測試
          • 5、結果
        • 4、小結
    • 4.4、校驗器(Validator)
      • 1、概念
      • 2、常用的校驗注解
      • 3、整合validator
        • 3.1、核心步驟概述
        • 3.2、實作
          • 1、pom.xml添加依賴
          • 2、建立使用者實體
          • 3、編寫controller
          • 4、測試
          • 5、結果
          • 6、問題
        • 3.3、優化異常資訊
          • 1、分析
          • 2、實作
          • 3、測試
          • 4、debug分析
        • 3.4、小結
      • 4、驗證普通參數(使用Assert)
        • 4.1、概念
        • 4.2、實作
          • 1、編寫異常捕捉方法
          • 2、編寫controller
          • 3、測試
          • 4、結果
      • 5、自定義校驗器
        • 5.1、概念
        • 5.2、實作
          • 1、定義驗證異常注解
          • 2、定義自定義校驗器
          • 3、測試
      • 6、驗證工具包

一、回顧Spring

1.1、概念

spring是一個輕量級的Java開發架構并且開源 為了解決企業級應用開發的複雜性而誕生的,簡化開發

  • 如何簡化的呢?

為了降低Java開發的複雜性,Spring采用了以下4種關鍵政策:

  • 所有的類都可以交給Spring來接管建立 Spring中的所有對象都是bean (大雜燴)
  • 通過IOC(控制反轉),DI(依賴注入),面向接口程式設計 将程式實作低耦合
  • 通過AOP程式設計實作功能的橫切 (再不動用原有的代碼上動态的增加功能)
  • 通過切面和模版減少樣式代碼

1.2、IOC、DI的透徹了解

我之前對這一塊的了解算是很模糊 不清晰的 因為一開始學Spring的時候本來就沒把着重點放在這個上面 但是學完之後 又去看了Springboot 然後又回來看了Spring 發現确實了解加深了一點 下面内容是我在網上看到的一個我自認為很能達到我G點的一個

blog

IoC(控制反轉)

  • 先想說說IoC(Inversion of Control,

    控制反轉

    )。這是spring的核心,貫穿始終。所謂IoC,對于spring架構來說,就是由

    spring來負責控制對象的生命周期和對象間的關系

    這是什麼意思呢,舉個簡單的例子,我們是如何找女朋友的?常見的情況是,我們到處去看哪裡有長得漂亮身材又好的mm,然後打聽她們的興趣愛好、qq号、電話号、微信号………,想辦法認識她們,投其所好送其所要,然後嘿嘿……這個過程是複雜深奧的,我們必須自己設計和面對每個環節。傳統的程式開發也是如此,在一個對象中,如果要使用另外的對象,就必須得到它(自己new一個,或者從JNDI中查詢一個),使用完之後還要将對象銷毀(比如Connection等),對象始終會和其他的接口或類耦合起來。
  • 那麼IoC是如何做的呢?有點像通過婚介找女朋友,在我和女朋友之間引入了一個第三者:婚姻介紹所。婚介管理了很多男男女女的資料,我可以向婚介提出一個清單,告訴它我想找個什麼樣的女朋友,比如長得像李嘉欣,身材像林熙雷,唱歌像周傑倫,速度像卡洛斯,技術像齊達内之類的,然後婚介就會按照我們的要求,提供一個mm,我們隻需要去和她談戀愛、結婚就行了。簡單明了,如果婚介給我們的人選不符合要求,我們就會抛出異常。整個過程不再由我自己控制,而是有婚介這樣一個類似容器的機構來控制。Spring所倡導的開發方式就是如此,所有的類都會在spring容器中登記,告訴spring你是個什麼東西,你需要什麼東西,然後spring會在系統運作到适當的時候,把你要的東西主動給你,同時也把你交給其他需要你的東西。所有的類的建立、銷毀都由 spring來控制,也就是說控制對象生存周期的不再是引用它的對象,而是spring。對于某個具體的對象而言,以前是它控制其他對象,現在是所有對象都被spring控制,是以這叫控制反轉。

DI(依賴注入)

  • IoC的一個重點是在系統運作中,動态的向某個對象提供它所需要的其他對象。這一點是通過DI(Dependency Injection,依賴注入)來實作的。比如對象A需要操作資料庫,以前我們總是要在A中自己編寫代碼來獲得一個Connection對象,有了 spring我們就隻需要告訴spring,A中需要一個Connection,至于這個Connection怎麼構造,何時構造,A不需要知道。在系統運作時,spring會在适當的時候制造一個Connection,然後像打針一樣,注射到A當中,這樣就完成了對各個對象之間關系的控制。A需要依賴 Connection才能正常運作,而這個Connection是由spring注入到A中的,依賴注入的名字就這麼來的。那麼DI是如何實作的呢? Java 1.3之後一個重要特征是反射(reflection),它允許程式在運作的時候動态的生成對象、執行對象的方法、改變對象的屬性,spring就是通過反射來實作注入的。

了解了IoC和DI的概念後,一切都将變得簡單明了,剩下的工作隻是在spring的架構中堆積木而已。

1.3、自己的了解

  • IOC 控制反轉 我覺得就是在很傳統的情況下 我們通常會在自己建立的這個對象裡面去顯式的得到另一個對象 然後使用另一個對象的所有功能或者一些資源 這樣的壞處是什麼? 就是我與另一個對象的耦合度極高 因為是緊緊相貼的 我改變了什麼還得想着另一個對象會不會跟着被改變一些 這樣子明顯不符合我們的程式設計思想 是以在這個基礎上 有大佬提出來了

    控制反轉

    這麼一個概念 這個概念我是這麼了解的 就是所有的對象關系 對象建立 對象銷毀等等都交給Spring裡面的容器去做 一個對象需要什麼資源 需要另外一個對象的什麼資源 容器會在相應的時機自動給你配置設定 你隻要等着資源到手就行 有了這個第三方的介入 我們所有後備工作都不需要去做 全全交給容器去管理 就好比上面文章所講:我想泡妹子 我就得去了解她的姓名 qq号 家庭住址等等相關資訊 這些事情都是要我親曆親為的去做這些事情 還不一定能成功!!而

    IOC思想

    就是提供了一個婚介所 所有想找女朋友的男孩子 或者想找男朋友的女孩子隻要來到這裡注冊提供相關資訊 資訊包括:想要找什麼類型的 家在哪裡 工資多高 或者長得好不好看等等…隻要提供給婚介所 婚介所會通過這個請求在你需要的時機提供給你資源 如果你不滿意 婚介所也會幫你做好事後處理
  • DI依賴注入的了解上面已經寫的很詳細了

二、微服務

2.1、概念

微服務其實是一種

架構風格

服務微化

每一個應用應該是一組小型服務 每個服務之間使用HTTP互通

  • 描述關系
SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

2.2、 單體應用時代

SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

先來打個比方:一開始的淘寶架構也如上所示 都是一個單體應用 所有的功能子產品都放在一起 比如說訂單子產品 商品子產品 使用者子產品等等一系列子產品 全組合在一起 這樣子确實也挺好 并發量大時 水準拓展同份應用 建構負載均衡 也能承受的住 但是現在問題來了 如果想要新增一個功能 是需要把這個功能在所有伺服器上同時上線 這是一個問題 如果某塊功能不夠完善 需要精益求精 同理越需要改動全身 這種 牽一發而動全身 的服務架構明顯不是我們需要的 耦合性太高 是以進而誕生了微服務這種架構風格

2.3、 微服務時代

SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

微服務就是把每個服務應用拆開來 把應用全部細化 讓服務自己組裝成一個應用

缺點:

  • 運維困難
  • 每個服務部署困難

三、SpringBoot

3.1、概念

  • 言歸正傳,什麼是SpringBoot呢,就是一個javaweb的開發架構,和SpringMVC類似,對比其他javaweb架構的好處,官方說是簡化開發,約定大于配置, you can “just run”,能迅速的開發web應用,幾行代碼開發一個http接口。

所有的技術架構的發展似乎都遵循了一條主線規律:從一個複雜應用場景 衍生 一種規範架構,人們隻需要進行各種配置而不需要自己去實作它,這時候強大的配置功能成了優點;發展到一定程度之後,人們根據實際生産應用情況,選取其中實用功能和設計精華,重構出一些輕量級的架構;之後為了提高開發效率,嫌棄原先的各類配置過于麻煩,于是開始提倡

“約定大于配置”

,進而衍生出一些一站式的解決方案。

是的這就是Java企業級應用->J2EE->spring->springboot的過程。

随着 Spring 不斷的發展,涉及的領域越來越多,項目整合開發需要配合各種各樣的檔案,慢慢變得不那麼易用簡單,違背了最初的理念,甚至人稱配置地獄。Spring Boot 正是在這樣的一個背景下被抽象出來的開發架構,目的為了讓大家更容易的使用 Spring 、更容易的內建各種常用的中間件、開源軟體;

  • Spring Boot 基于 Spring 開發,Spirng Boot 本身并不提供 Spring 架構的核心特性以及擴充功能,隻是用于快速、靈活地開發新一代基于 Spring 架構的應用程式。也就是說,它并不是用來替代 Spring 的解決方案,而是和 Spring 架構緊密結合用于提升 Spring 開發者體驗的工具。Spring Boot 以約定大于配置的核心思想,預設幫我們進行了很多設定,多數 Spring Boot 應用隻需要很少的 Spring 配置。同時它內建了大量常用的第三方庫配置(例如

    Redis

    MongoDB

    Jpa

    RabbitMQ

    Quartz

    等等),Spring Boot 應用中這些第三方庫幾乎可以零配置的開箱即用。

簡單來說就是SpringBoot其實不是什麼新的架構,它預設配置了很多架構的使用方式,就像maven整合了所有的jar包,spring boot整合了所有的架構 。Spring Boot 出生名門,從一開始就站在一個比較高的起點,又經過這幾年的發展,生态足夠完善,Spring Boot 已經當之無愧成為 Java 領域最熱門的技術。

3.2、優缺點

  • Spring Boot的主要優點:
    • 為所有Spring開發者更快的入門
    • 開箱即用,提供各種預設配置來簡化項目配置
    • 内嵌式容器簡化Web項目
    • 沒有備援代碼生成和XML配置的要求

缺點:

  • 最大的缺點就是精通難 因為springboot是基于spring架構再深入或者說再精簡 是以說如果沒精通spring的api是無法真正掌握springboot的

3.3、為什麼要使用springboot?

  • 如上面所示 一是開發一個應用十分簡單 每個功能子產品 開箱即用 幾乎都是0配置 但是最主要的原因是什麼?是它推動了微服務的發展 微服務應用建構少不了springboot 我們也了解 微服務這種架構風格每個服務應用建構的話 是個很大的工程量 如果是以前內建SSM架構的技術來建構一個應用 這開發成本是不計其數的 而springboot恰巧內建了SSM架構 輕巧簡便 而且內建了大量架構 其中就有微服務相關的

    Netflix

    它為微服務架構提供了很好的解決方案 這樣子一來優勢就很明顯了 boot為微服務架構提供了很多便利之處:一些優秀的解決方案,應用開發簡單,內建了大量的有用架構!!

3.4、第一個SpringBoot程式

1、建立應用

1.1、官網建立

官網:

https://spring.io/projects/spring-ws

SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發
SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

這麼一來 大緻的項目工程已經建立完畢

1.2、idea工具(推薦使用)

idea內建了springboot工程的建立方式:本質還是從官網上建立的

SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發
SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發
SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發
SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

IDE都支援使用Spring的項目建立向導快速建立一個Spring Boot項目;

選擇我們需要的子產品;向導會聯網建立Spring Boot項目;

預設生成的Spring Boot項目;

  • 主程式已經生成好了,我們隻需要我們自己的邏輯
  • resources檔案夾中目錄結構

    static:儲存所有的靜态資源; js css images;

    templates:儲存所有的模闆頁面;(Spring Boot預設jar包使用嵌入式的Tomcat,預設不支援JSP頁

    面);可以使用模闆引擎(freemarker、thymeleaf)

    application.properties:Spring Boot應用的配置檔案;可以修改一些預設設定;

2、檢視結構

  • 項目結構
SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

3、編寫controller

建立完畢後 先寫個controller跑一下項目 測試一下項目是否能跑起來

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

/**
 * @author chill
 * @date 2021/5/13 22:59
 */
@Controller
public class HelloController {
    //将字元串寫在頁面上
    @ResponseBody
    @RequestMapping("/hello")
    public String hello(){
        return "hello";
    }
}
           

4、啟動項目

啟動成功是這樣子的

SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

web環境下 項目是一直在運作的 接下來進行通路

5、通路項目

SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

通過路徑進行通路 項目預設端口是8080 boot初始頁面是報錯頁面 也就是目前所看到的

SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

這是我們之前寫的controller 通過路徑成功通路

項目跑成功了 接下來分析一下整個項目結構是什麼樣子的~~

  • 注意
    • springboot已經內建了大量常用架構…SSM、Secrity等等 内嵌了web容器 自帶Tomcat環境 隻要開啟web支援(導入start啟動器 boot則會自動配置好web環境)

3.5、分析SpringBoot應用

1、pom.xml

  • parent
<!--工程被建立後自帶一個父工程  可以點選父工程分析一下-->
<parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.4.5</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
 點選去檢視到還有一個父工程  點進該父工程檢視
<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-dependencies</artifactId>
    <version>2.4.5</version>
  </parent>

           

分析配置

SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發
SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

由此我們得知 :

​ 該配置的作用就是整個項目的版本仲裁中心 幾乎所有的依賴都不需要導入版本 會被自動仲裁版本

  • gav

建立時的資訊 一些jdk版本說明 工程名 還有IP位址

<!--建立時的資訊 一些jdk版本說明	工程名  還有IP位址 gav-->
    <groupId>com.chill</groupId>
    <artifactId>stringboot_study</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>stringboot_study</name>
    <description>chill first springboot project</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>

           
  • dependence
<!--
    starter:啟動器
    spring-boot-starter:往springboot裡添加依賴幾乎都是這個開頭 後面的字尾見明思意即可
    添加了web支援後 就内嵌了web容器
    -->
   <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
   </dependency>

	<!--springboot自帶測試單元-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
           

項目裡需要使用什麼功能 就往pom檔案中導入相應的啟動器

starter

導入完畢後所有自動配置會被boot

自動裝配

隻要環境到位 該功能就可以實作 友善了我們maven的書寫 以前需要導入大量的jar包才能使用某個功能

  • plugin
<!-- 打包插件-->
<build>
    <plugins>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>
           

自帶maven插件 該插件在項目打包生成jar包時起了關鍵作用 jar包中的啟動類辨別了哪個應用被開啟 可以通路

2、application.yml

檔案字尾可以是yml 或者yaml 都會被boot識别 一般檔案名預設為

application

如果想要實作環境隔離 區分環境時 則:

  • 環境隔離
    • 準備三個檔案

      appliction.yml

      appliction-dev.yml

      appliction-prod.yml

      SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發
    • 三個檔案的服務端口都不一緻 假設為8001,8002,8003
    • 通過在主檔案

      appliction.yml

      中設定激活哪個配置檔案 程式啟動後就會預設執行該檔案配置 比如激活的是

      dev

      服務端口就會使用

      dev

server:
  port: 8001
spring:
  profiles:
    active: dev
           
  • 檔案說明
    SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

這個檔案是自己需要修改一些自動配置類的

預設屬性

時 可以到這個檔案裡聲明 需要聲明一些公共對象也可以在此定義!!!

如果想要在此有提示 的敲代碼 必須存在相應的配置類

3、maven

隻要安裝了maven插件 功能都能使用

boot項目打包特别簡單 隻需要點選一個按鈕即可 傻瓜式打包

SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

打包成功後會生成

SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

該jar包包含tomcat 但是不包括整個項目的靜态資源 比如說建立的images檔案夾 或者一些js html等等

maven工程被編譯後都會被解析出一個target目錄 該目錄包含了大量的工程資訊

通過指令

java -jar jar包名

執行該jar包獲得運作結果 結果與在idea跑項目一緻

SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

3.6、自定義Application

官方文檔上有講過 主啟動類是可以自定義的 如果我們需要使用一些其他功能 可以自定義 僅作休閑 稍微了解即可

  • 官方文檔

    https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features

SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

3.7、配置檔案

1、概念

SpringBoot使用一個全局的配置檔案 核心配置檔案,配置檔案名在約定的情況下 名字是固定的; 配置檔案的作用:修改SpringBoot自動配置的預設值;SpringBoot在底層都給我們自動配置好;

  • application.properties

  • application.yml

  • application.yaml

2、兩種配置檔案的格式

在springboot架構中,resource檔案夾裡可以存放配置的檔案有兩種:properties和yml。

1、application.properties的用法

扁平的k/v格式

server.port=8081 
  server.servlet.context‐path=/chill
           
2、application.yml的用法

樹型結構

server: 
 	port: 8088 
  	servlet: 
    	context‐path: /chill
           

兩種 前者是,而後者是yml的,建議使用後者,因為它的可讀性更強。 可以看到要轉換成YML我們隻需把properies裡按. 去拆分即可。

3、yml基本文法

  • k:(空格)v:表示一對鍵值對(空格必須有);
  • 以空格的縮進來控制層級關系;隻要是左對齊的一列資料,都是同一個層級的
  • 屬性和值也是大小寫敏感;
  • 如果有特殊字元% & 記得用單引号(‘)包起來

4、配置檔案的加載順序

<includes> 
	<include>**/application*.yml</include>
	<include>**/application*.yaml</include> 
	<include>**/application*.properties</include> 
</includes>
           

如果同時存在不同字尾的檔案按照這個順序加載主配置檔案;互補配置;

3.8、自動配置原理分析(重點)

  • 分析

所有環境都是主啟動類開始被一一啟用 那麼我們來觀察一下主啟動類

public class SpringbootOneHelloworldApplication {

    public static void main(String[] args) {
      //這裡就是告訴這個boot應用 我們的啟動類是哪個類 相當于整個程式的入口 
        SpringApplication.run(SpringbootOneHelloworldApplication.class, args);
    }

}
           

是以自動配置肯定不在這裡 這裡是設定整個boot應用的一些基本配置

那麼隻有一個注解了

@SpringBootApplication
           

點進該注解進行分析一波 發現這是一個組合注解

@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) })
           

慢慢分析:這都是些

元注解

學過注解的大概都知道這是些什麼意思 是以簡單講講可以略過

@Target(ElementType.TYPE)	---->該注解能作用在哪些位置   這個隻能使用在類上
@Retention(RetentionPolicy.RUNTIME) ---->該類被編譯過後 會被記錄在類檔案中 在運作時也會被虛拟機保留 是以可以通過反射 擷取該類的所有資訊
@Documented	----> 生成javadoc會被攜帶上該注解  沒什麼實際用
@Inherited	---->繼承該類 會不會将注解衍生給子類
           

讓我們來看看下一個注解

@SpringBootConfiguration
           

點進去發現是個組合注解

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Configuration
           

這些注解最終的作用就是标注該類為一個配置類

@Configuration

學過spring的同學都知道 該注解标注哪個類 該類就是配置類 且該類會被注冊到

IOC容器

再下一個注解

@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
		@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
           

該注解很正常 就是掃描那些全限定包名底下 帶有一些能注冊成

bean

的注解 比如

@Controller

@service

…等等

excludeFilters

就是排除一些過濾器 裡面的值表示的意思就是 帶有

Configuration

或者

AutoConfiguration

會被掃描到

SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

@EnableAutoConfiguration

接下來就是最重要的一個注解 該注解分析時需要設定斷點進行分析 見名思意 : 啟動自動配置

該注解點進去 去掉一些無用的注解

@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
           

@AutoConfigurationPackage

将主啟動類所待的包下面的所有類都注冊為

bean

@Import(AutoConfigurationImportSelector.class)

分析一下

AutoConfigurationImportSelector

該類是個什麼東西

SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

該類繼承了個

DeferredImportSelector

翻譯:延期導入選擇器 該接口是spring 4.0版本開始出現的,要比它的父接口

ImportSelector

晚了幾個版本。從文檔中我們得知,這是一個變種的

ImportSelector

接口,它在所有被

@Configuration

注解修飾的類處理完成後才運作。

DeferredImportSelector

用在處理@Conditional相關的導入時特别有用

  • ImportSelector

    :該接口通常被子類實作,用以判斷被

    @Configuration

    注解修飾的類是否應該被導入;而判斷的條件通常是基于注解的一些屬性!!
  • DeferredImportSelector

    ImportSelector

    的子類 該注解會在所有的@Configuration處理完在導入時才會生效 也就是說該注解有條件所限制 如果該接口的實作類同時實作EnvironmentAware, BeanFactoryAware ,BeanClassLoaderAware或者ResourceLoaderAware,那麼在調用其selectImports方法之前先調用上述接口中對應的方法!!!
  • ResourceLoaderAware
SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發
  • BeanFactoryAware
SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發
  • EnvironmentAware
    SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發
  • ResourceLoaderAware
SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

這些方法都會在調用其

selectImports

方法時都會被先調用執行

  • selectImports
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
  //
   if (!isEnabled(annotationMetadata)) {
      return NO_IMPORTS;
   }
   AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
   return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
           

Spring

的底層注入bean 其實都是依賴

selectImports

方法

!isEnabled(annotationMetadata)

如果自動配置被啟動時 可以重寫目前環境屬性 如果被重寫成功 則傳回true 重新配置的新環境就是一個自動配置環境 隻有在這個環境下 所有配置才會自動

getAutoConfigurationEntry(annotationMetadata)

:擷取自動配置條目 我們點進去看一下

getAutoConfigurationEntry(annotationMetadata)

protected AutoConfigurationEntry getAutoConfigurationEntry(AnnotationMetadata annotationMetadata) {
  //老規矩   自動環境到位才能進行如下
		if (!isEnabled(annotationMetadata)) {
			return EMPTY_ENTRY;
		}
  //擷取注解的所有屬性 :  exclude	excludeName
		AnnotationAttributes attributes = getAttributes(annotationMetadata);
  //擷取一些符合條件的所有配置  這個好像有用  點進去看一下
		List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
  //删除該配置裡面的複制本
		configurations = removeDuplicates(configurations);
  //通過這些屬性進行排除  
		Set<String> exclusions = getExclusions(annotationMetadata, attributes);
		checkExcludedClasses(configurations, exclusions);
		configurations.removeAll(exclusions);
  //通過過濾器再進行篩選一遍  而這裡的過濾器選項就是配置類上的 所有帶有@Condition 字眼的所有注解都生效  不生效的會被賦為null  過濾掉
		configurations = getConfigurationClassFilter().filter(configurations);
  //一些監聽的事件
		fireAutoConfigurationImportEvents(configurations, exclusions);
		return new AutoConfigurationEntry(configurations, exclusions);
	}
           

這個方法的作用就是 篩選和過濾出所有符合條件的配置類 所有類 會被

selectImports

方法注入到IOC容器中 并進行自動配置

  • getCandidateConfigurations()
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
  //重頭戲	這裡通過SpringFactoriesLoader加載器來加載一些工廠化的名字 而這個方法需要兩個參數 一個是擷取這個類是誰	等下點進去源碼分析	第二個是擷取類加載器 而這個加載器是一開始就被配置過的 
   List<String> configurations = 	SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
         getBeanClassLoader());
  //就是一些簡單的斷言	不為null就不報錯  為null就報錯
   Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
         + "are using a custom packaging, make sure that file is correct.");
   return configurations;
}
           

主要用于擷取候選的一些配置 這裡已經開始進行第一層篩選 通過類加載器進行篩選

  • loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),getBeanClassLoader())
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
   String factoryTypeName = factoryType.getName();
   return loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
}
           

用于擷取 由

SpringFactoriesLoader

類加載器加載的類 該類的名稱被作為map的key 将該key對應的所有在 spring.factories中的類全部一并傳回

  • getSpringFactoriesLoaderFactoryClass()
protected Class<?> getSpringFactoriesLoaderFactoryClass() {
   return EnableAutoConfiguration.class;
}
           

這個擷取所有使用

SpringFactoriesLoader

該加載器加載的類

  • getBeanClassLoader()

擷取bean 類加載器 用于加載 bean 如果該bean被初始化時已有執行個體 便不會再建立 否則将建立

SpringFactoriesLoader

類的類加載器

loadSpringFactories(classLoader)

大緻的流程已經分析完畢~~ 接下來畫個圖 然後進行一段總結!!

SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

總結

隻要符合自動配置類上的所有條件注解@Condition… 該自動配置類就會被自動裝配 而這些條件滿足的情況 大多數都是關于

starter

是否被導入 導入了該

starter

将條件都滿足 則就會自動配置 并且支援自定義配置 而可以自定義配置的内容 屬性 都被配置類所聲明 yml檔案中 可以将預設的屬性值更改

3.9、熱部署

1、概念

熱部署,就是在應用正在運作的時候更新軟體,卻不需要重新啟動應用

2、步驟

2.1、導入依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-devtools</artifactId>
    <scope>runtime</scope>
</dependency>
           

2.2、idea設定

SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

按鍵 ctrl+shift+alt +/ 選中 Registry

SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

找到如下這個選項 給他勾選上

SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

到此為止 大緻的熱部署配置已經完成 現在隻需要測試一下

每次更新項目代碼時 更新完畢後隻需等待幾秒鐘 再次通路即可 大部分情況都不需要重新開機伺服器

4.0、日志架構

1、概念

日志記錄在我們編寫代碼的途中 會顯得十分重要 在我們進行調試時 或者伺服器釋出了之後 使用者體驗出現了異常需要進行修改代碼時 因為釋出過後的應用代碼量是十分龐大的 如果使用原生的

sout

這種調試方式開發成本會非常大 有可能找個輸出語句 都需要需要半天 這個時候

log

日志就被開發出來了

2、日志架構發展史

  • 小故事

    這是一個故事,故事有一個主角,名字比較特色,很多人叫他ceki,全名是Ceki Gülcü,我們姑且叫他小明吧。

    很久以前,java是沒有日志的,調試也隻是用system.out.print()去列印日志,這有很大的問題,沒有日志怎麼查詢問題呢,一堆system.out.print有些是部署後想看到的,可是還有很多隻是在開發階段要看到的,如果沒有删除就是多餘的代碼了,怎麼辦呢?

這時候一個叫小明的主角出現了,他寫了一個日志架構,就是鼎鼎大名的log4j,這個實作了日志的展示,日志的級别,報錯發送郵件等等功能,這個架構一經出現,便吸引大量粉絲,太友善了!

但也會有各種各樣的問題,個性化的,比如我想加個發短信的功能怎麼辦呢?

這時候小明也看到了這個情況,個人的力量是有限的,小明便開源了log4j代碼,讓更多的人來積極參與到完善這個架構。

看到這種情況,有個角色出現了,就是阿帕奇,Apache看到這個優秀的架構,行業裡面還是空白,便說服了小明加入阿帕奇開源基金會,參與對log4j的維護,小明覺得也不錯啊,靠着大boss,很好,便在Apache安心駐紮下來。

Apache收服了小明及他的log4j,覺得很有發展前途,便去遊說sun公司,希望sun公司能在java中加入log4j為預設的log日志系統,想法是美好的,結局是殘酷的,sun公司看不上Apache這個小公司,他便自己開發一個日志系統,叫做jul,但這個并不是很友好,使用的人也有限吧。

自從log4j之後,各種日志系統也都出現了,各有優缺點,這樣出現了一種混亂的現象,就是在整合各系統時,比如spring引用各jar的時候,會發現有不同的日志系統,這怎麼辦呢,這時候Apache出面了,他做了一個門面的日志系統jcl(之前叫Jakarta Commons Logging,後更名為Commons Logging),這個日志系統不去實作具體的如何列印日志,而是去相容各種日志系統,統一兼顧,去擺平這種混亂的現象。

小明在Apache幹的并不是很開心,特别是看到Apache寫的那個jcl太low了,根本不好用,是以他就從Apache出來自己單幹,自己寫了一個門面,就是大家熟悉的SLF4J,這個出來之後,又擁有了大量粉絲,小明幹的不錯!

日志系統繼續發展,Apache也沒閑着,他開發了新一代日志系統,那就是log4j2 ,在log4j的基礎上繼續發力,比較log4j給了Apache,人家有權利這麼做,無可厚非嘛。

而小明看到了log4j2,覺得這個并不好用,還是覺得low,他呢就自己又寫了一套日志系統,那就是Logback,這個性能提升了,使用者也是越來越多,很完美!

未來的路還很多,日志系統可能還是繼續發展,到底誰與争鋒,我們拭目以待。

3、日志架構

  • 傳統的日志架構

Java Util Logging

(簡稱JUL) : 這個是jdk 也就是sun公司自己推行的 項目中使用時不需要依賴任何

jar包

使用起來十分簡單

import java.util.logging.Logger;
public class JulLog {
    public static void main(String[] args) {
        Logger logger = Logger.getLogger("JulLog");
        logger.info("JulLog");
    }
}
           

Log4J

:作者在開發完這個日志架構後實施了開源 受到了廣大開源使用者的喜好 迅速被人們廣泛應用了起來 該日志架構使用起來也非常簡單 隻需要導入一個maven依賴 使用方式與

Java Util Logging

極其相似

maven依賴:

<dependencies>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
        </dependency>
    </dependencies>
           
import org.apache.log4j.Logger;
public class Log4J {
	    public static void main(String[] args) {
	        Logger logger = Logger.getLogger("Log4J");
	        logger.info("Log4J");
	    }
	}

           

注意 :這兩個日志導入的包名得好好區分一下 别搞混了

這兩個日志已經很久沒被更新過了 有點過時了 被淘汰的原因也很清晰明了 就是性能跟不上時代 還有已經滿足不了人們現在的需求量

  • 現在流行的日志架構

Log4j2

: 這個是apache基金會仿照着

Log4j

進行再次開發 将性能提升了n倍

Logback

: 開發

Log4j

的作者閑不住了 也開發出一個性能成本效益很高的日志架構 該架構是 springboot預設的日志是實作類

由于目前

微服務

架構迅速走紅 如果每個子產品都是不同的人進行開發 每個人開發習慣都不一樣 是以會有可能導緻一個現象就是 日志架構使用的不同 可最後需要整合在一塊時可能會出現異常 是以誕生了

日志門面

這種東西

4、日志門面

1、 概念

這不是為了實作日志功能而誕生的一門技術 而是為了整合所有的日志

slf4j

: 這是由

Log4j

的開發者研究出來的日志門面 該架構是springboot預設的日志門面 !!!

早年,你工作的時候,在日志裡使用了log4j架構來輸出,于是你代碼是這麼寫的

import org.apache.log4j.Logger; 
\\省略 
  Logger logger = Logger.getLogger(Test.class); 4
  logger.trace("trace");
\\省略
           

但是,歲月流逝,sun公司對于log4j的出現内心隐隐表示嫉妒。于是在jdk1.4版本後,增加了一個包為java.util.logging,簡稱 為jul,用以對抗log4j。于是,你的上司要你把日志架構改為jul,這時候你隻能一行行的将log4j的api改為jul的api,如下所示

import java.util.logging.Logger; 
 \\省略 
  Logger loggger = Logger.getLogger(Test.class.getName());
  logger.finest("finest");
\\省略
           

可以看出,api完全是不同的。那有沒有辦法,将這些api抽象出接口,這樣以後調用的時候,就調用這些接口就好了呢? 這個時候

jcl

(Jakarta Commons Logging)出現了,說jcl可能大家有點陌生,講

commons-logging-xx.jar

元件,大家總有印象 吧。JCL 隻提供 log 接口,具體的實作則在運作時動态尋找。這樣一來元件開發者隻需要針對 JCL 接口開發,而調用元件的應用

程式則可以在運作時搭配自己喜好的日志實踐工具。JCL可以實作的內建方案如下圖所示

SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

jcl預設的配置:如果能找到Log4j 則預設使用log4j 實作,如果沒有則使用jul(jdk自帶的) 實作,再沒有則使用jcl内部提供的 SimpleLog 實作。

于是,你在代碼裡變成這麼寫了

import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; 
\\省略  
  Log log =LogFactory.getLog(Test.class);
	log.trace('trace');
\\省略
           

至于這個Log具體的實作類,JCL會在ClassLoader中進行查找。這麼做,有三個缺點,缺點一是效率較低,二是容易引發混亂, 三是在使用了自定義ClassLoader的程式中,使用JCL會引發記憶體洩露。

JCL動态查找機制進行日志執行個體化,執行順序為:

commons­logging.properties­­­­

>

系統環境變量

­­­­­­­>

log4j

>

jul­­­

>

simplelog

­­­­>

nooplog

于是log4j的作者覺得jcl不好用,自己又寫了一個新的接口api,那麼就是slf4j。關于slf4j的內建圖如下所示

SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

了解slf4j日志門面了嗎,它跟jcl機制不一樣。 它就相當于這個遊戲機, 我本身沒有遊戲, 隻提供一個運作遊戲的平台(門面) 要運作哪個遊戲我不管, 你給我放哪塊CD光牒我就運作哪個遊戲。 JCL是自己去找,先找到哪個運作哪個

SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

Slf4j與其他各種日志元件的橋接說明

jar包名 說明
slf4j-log4j12-1.7.13.jar Log4j1.2版本的橋接器,你需要将Log4j.jar加入Classpath。
log4j-slf4j-impl.jar Log4j2版本的橋接器,還需要

log4j­api.jar

log4j­core.jar

slf4j-jdk14-1.7.13.jar java.util.logging的橋接器,Jdk原生日志架構。
slf4j-nop-1.7.13.jar NOP橋接器,默默丢棄一切日志。
slf4j-simple-1.7.13.jar 個簡單實作的橋接器,該實作輸出所有事件到System.err. 隻有Info以及高于該級别的消息被列印,在小 型應用中它也許是有用的。
slf4j-jcl-1.7.13.jar

Jakarta Commons Loggin

的橋接器. 這個橋接器将Slf4j所有日志委派給Jcl。
logback-classic-1.0.13.jar(requires logback-core-1.0.13.jar) Slf4j的原生實作,Logback直接實作了Slf4j的接口,是以使用Slf4j與Logback的結合使用也意味更小的記憶體與計算開銷

如圖所示,應用調了sl4j-api,即日志門面接口。日志門面接口本身通常并沒有實際的日志輸出能力,它底層還是需要去調用具體 的日志架構API的,也就是實際上它需要跟具體的日志架構結合使用。由于具體日志架構比較多,而且互相也大都不相容,日志 門面接口要想實作與任意日志架構結合可能需要對應的橋接器,上圖紅框中的元件即是對應的各種橋接器!

我們在代碼中需要寫日志,變成下面這麼寫

import org.slf4j.Logger; 
import org.slf4j.LoggerFactory; 
	//省略 
  Logger logger = LoggerFactory.getLogger(Test.class); 
  // 省略
  logger.info("info");
           

在代碼中,并不會出現具體日志架構的api。程式根據classpath中的橋接器類型,和日志架構類型,判斷出

logger.info

應該以什 麼架構輸出!注意了,如果classpath中不小心引了兩個橋接器,那會直接報錯的!

是以,在阿裡的開發手冊上才有這麼一條

SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

ok,至此,基礎知識完畢,下面是實戰!

4.1、異步架構

1、概念

  • 在某些特殊的業務場景下 我們

    注冊完使用者

    通常會有這樣的需求 将使用者注冊成功的資訊 以

    短信

    的形式發送給使用者 一是為了驗證使用者手機号 是否合格 或者 是否能正确接收資訊 二是将使用者體驗提升 還有可能會有

    積分加持

    那麼情況就來了!!!如果程式執行期間

    積分加持

    這塊業務功能出錯了 那麼在 串行執行 這種模式中 可能造成我們使用者注冊失敗 但是因為一個附屬的功能 導緻流失掉一個使用者 這代價明顯是很大的 作為解決方案 是以異步架構的好處就顯而易見了!!!

2、大緻業務需求

SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發
  • 最主要需要實作的是 注冊成功 其他的附屬功能隻是将使用者體驗提升到最大

3、差別

SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發
  • 在雙方執行時 并行執行是讓使用者更好的體驗

    使用者注冊

    功能
  • 而在真是場景中 一定要注意并不是什麼時候都能用

    異步

    來處理 千萬不要

    濫用

    一定根據合适的業務場景來使用 就比如一些附屬的功能 都能通過

    異步

    來完成

4、實作異步

4.1、啟動類添加支援

package com.chill;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.scheduling.annotation.EnableAsync;

@SpringBootApplication
@EnableAsync
public class SpringbootCliApplication {

    public static void main(String[] args) {
        SpringApplication.run(SpringbootCliApplication.class, args);
    }
}
           

4.2、編寫Controller層

package com.chill.controller;

import com.chill.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * @tips : go go go~~ ^O^
 * @Author : chill
 * @Date : 2021-06-26
 * @Version : v1.0
 */
@RestController
@Slf4j
public class UserController {

    @Autowired
    private UserService userService;

    /**
     * 采用異步執行
     *
     * @return
     */
    @RequestMapping("/saveUser")
    public String saveUser() {
        //串行執行
        /*log.info("使用者注冊!!!");
        log.info("發送短信!!!");
        log.info("增加積分!!!");*/


        //改進後


        //并行執行
        log.info("使用者注冊!!!");
        userService.sendMessage();
        userService.addPoint();
        return "success";
    }
}
           
  • 這裡都是模拟真實業務場景 是以寫的比較簡潔

4.3、編寫業務層

package com.chill.service;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;

/**
 * @tips : go go go~~ ^O^
 * @Author : chill
 * @Date : 2021-06-26
 * @Version : v1.0
 */
@Service
@Slf4j
public class UserService {


    @Async
    public void sendMessage() {
        try {
            Thread.sleep(5000);
            log.info("發送短信!!!");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    @Async
    public void addPoint() {
        try {
            Thread.sleep(5000);
            log.info("增加積分!!!");
        } catch (Exception e) {
            e.printStackTrace();
        }
    }
}
           

4.4、運作結果

  • 通過ip位址:端口

    localhost:8002

    通路程式
    SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發
  • 通路成功 而且我在業務層加了 線程延時的 但是在我通路時 結果響應的很快 前後不超過1-2s
  • console
    SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

4.5、小結

在SpringBoot的日常開發中,一般都是同步調用的,但經常有特殊業務需要做異步來處理。比如:注冊使用者、需要送積分、發短信和郵件、或者下單成功、發送消息等等。

  • 第一個原因:

    容錯問題

    ,如果送積分出現異常,不能因為送積分而導緻使用者注冊失敗。
  • 第二個原因:

    提升性能

    ,比如注冊使用者花了30毫秒,送積分劃分50毫秒,如果同步的話一共耗時:70毫秒,用異步的話,無需等待積分,故耗時是:30毫秒就完成了業務。

5、異步線程池的優化

5.1、分析

通過上面的日志分析獲得結論:【task-1】,【task-2】,【task-3】….遞增。

SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

5.2、問題

  • 其實基于上面的開發 如果在小并發情況下去使用 是沒有什麼問題的 而我們的伺服器 肯定不是基于小規模去通路的
  • @Async的預設情況下使用的是

    SimpleAsyncTaskExecutor

    線程池 它并不是一個實際意義上的線程池 事實上 它通常會給每個

    異步方法

    新開一個線程 而這個線程被使用過後不會被回收
  • 如果接下來會有新的

    異步方法

    它會新開線程去執行 并不會重用線程 這樣子并發量一大 伺服器根本承受不住的

5.2、線程池的簡單了解

  • 線程池會有預設的一些核心線程 用來執行一些基本的方法
  • 線程池一般會設定 線程數的上限 如果線程數達到上限 會将方法先阻塞線上程池外面 等待線程池裡面的線程 一旦有空閑線程就會去執行這些被阻塞的方法
  • 線程池會有一個類似于

    哨兵

    一樣的東西(可以簡單了解為

    哨兵

    ) 它負責監督線程池裡面的狀況 如果線程池裡面空餘線程剩餘過多 且在規定時間内(一般是3-5分鐘)沒有使用 則會被銷毀

5.3、@Async 注解支援的線程機制

  • SimpleAsyncTaskExecutor

    :簡單的線程池,這個類不重用線程,每次調用都會建立一個新的線程。
  • SyncTaskExecutor

    :這個類沒實作異步調用,隻是一個同步操作,隻适合用于不需要多線程的地方。
  • ConcurrentTaskExecutor

    :Executor的适配類,不推薦使用.。
  • ThreadPoolTaskScheduler

    :可以和cron表達式使用。
  • ThreadPoolTaskExecutor

    最常用

    ,推薦,其本質就是:java.util.concurrent.ThreadPoolExecutor的包裝

如果想要更改

@Async

預設線程池 隻需要在此基礎上進行覆寫就好了~~

package com.chill.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

import java.util.concurrent.ThreadPoolExecutor;

/**
 * @Author chill
 */
@Configuration
public class SyncThreadPoolConfiguration {

    /**
     * 把springboot中的預設的異步線程線程池給覆寫掉。用ThreadPoolTaskExecutor來進行處理
     **/

    @Bean(name = "threadPoolTaskExecutor")
    public ThreadPoolTaskExecutor getThreadPoolTaskExecutor() {
        ThreadPoolTaskExecutor threadPoolTaskExecutor = new ThreadPoolTaskExecutor();
        // 1: 建立核心線程數 cpu核數 -- 50  
        threadPoolTaskExecutor.setCorePoolSize(10);
        // 2:線程池維護線程的最大數量,隻有在緩存隊列滿了之後才會申請超過核心線程數的線程
        threadPoolTaskExecutor.setMaxPoolSize(100);
        // 3:緩存隊列 可以寫大一點無非就浪費一點記憶體空間 也就是目前可以執行最大的線程數量
        threadPoolTaskExecutor.setQueueCapacity(200);
        // 4:線程的空閑時間,當超過了核心線程數之外的線程在達到指定的空閑時間會被銷毀 200s
        threadPoolTaskExecutor.setKeepAliveSeconds(200);
        // 5:異步方法内部線的名稱 自定義 chill-thread-
        threadPoolTaskExecutor.setThreadNamePrefix("chill-thread-");
        // 6:緩存隊列的政策
        /* 當線程的任務緩存隊列已滿并且線程池中的線程數量已經達到了最大連接配接數,如果還有任務來就會采取拒絕政策,
         * 通常有四種政策:
         *ThreadPoolExecutor.AbortPolicy:丢棄任務并抛出異常:RejectedExcutionException異常
         *ThreadPoolExecutor.DiscardPolicy:丢棄任務,但是不抛出異常
         *ThreadPoolExecutor.DiscardOldestPolicy: 丢棄隊列最前面的任務,然後重新嘗試執行任務(重複此過程)
         *ThreadPoolExecutor.CallerRunsPolicy:重試添加目前的任務,自動重複調用execute()方法,直到成功。
         *ThreadPoolExecutor. 擴充 重試3次,如果3次都不充公在移除。
         *jmeter 壓力測試 1s=500
         * */
        threadPoolTaskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        threadPoolTaskExecutor.initialize();
        return threadPoolTaskExecutor;
    }
}
           

5.4、小結

  • 異步不要泛濫使用 一般使用于的場景:
    • 一些核心功能的附屬業務
    • 需要開辟多個線程執行任務

異步程式設計的架構:消息中間件(ActiveMQ、RabbitMQ)

四、Web開發

4.1、說明

  • 在進行web開發時 我們通常會采用

    get/post

    方式(最常用 當然還有RestFul風格的DELETE PUT等等)進行通路

    controller接口

  • 那麼我們在實際開發中到底如何使用 如何去衡量呢?
  • 這就需要我們得搞清

    Get

    Post

    請求的差別了
  • 差別
    • 後端取值方式不同

      • get 會将請求參數包含在

        url

        中 可以通過

        @RequestParam()

        将參數值取出 而post 請求得資料都存在于 請求體中 也就是我們的

        RequestBody

        是以我們需要添加

        @RequestBody

        來将資料取出
    • get和post的緩存機制

      • get

        方式如果使用于請求後端的靜态資源 會将 靜态資源 緩存一份在浏覽器中 是以這就可以說明 為什麼在請求後端的靜态資源時一般使用的是

        get

        因為第二次進行請求時 直接從緩存中取出 快捷 效率高 是以我們思考一下 為什麼在更新靜态資源過後 有時需要清理浏覽器的緩存?
      • post

        請求每次都需要去後端擷取 每次的時間幾乎都一緻!! 網上有人證明過啦 可以去翻閱一下
    • get比post請求更快

      • 首先

        post

        請求會将大量的資料包含在請求頭内 而這些請求頭需要先通過服務端的确認 這個确認也很直白 就是

        requestBody

        的接收結果 如果成功才将資料一一發送過來 這個過程是有一點

        耗時

        消耗性能

        • post

          請求的過程:
        1. 浏覽器請求tcp連接配接(第一次握手)
        2. 伺服器答應進行tcp連接配接(第二次握手)
        3. 浏覽器确認,并發送post請求頭(第三次握手,這個封包比較小,是以http會在此時進行第一次資料發送)
        4. 伺服器傳回100 Continue響應
        5. 浏覽器發送資料
        6. 伺服器傳回200 OK響應
        • get

          請求的過程:
        1. 浏覽器請求tcp連接配接(第一次握手)
        2. 伺服器答應進行tcp連接配接(第二次握手)
        3. 浏覽器确認,并發送get請求頭和資料(第三次握手,這個封包比較小,是以http會在此時進行第一次資料發送)
        4. 伺服器傳回200 OK響應

也就是說,目測get的總耗是post的2/3左右,這個口說無憑,網上已經有網友進行過測試。

4.2、統一結果傳回 R類

1、概念

  • 在目前這個時代 項目基本都是前後端分離的 是以在我們前後端進行資料互動時 應該要存在一個标準 使得我們開發更為簡便 是以提出了

    R類

2、R類結構

  • Result

  • 基于springMvc和MybatisPlus裡的源碼總結了一部分經驗 事實上我們為什麼不用已經定義好的呢?
  • 答案是:無法滿足我們的需求 大多數消息設定 或者 狀态碼設定都需要我們自定義
package com.chill.common.base;
import lombok.Data;
import lombok.ToString;
import java.io.Serializable;

/**
 * 統一傳回結果類
 */
@Data
@ToString
public class Result implements Serializable {
    private static final long serialVersionUID = 986823857621547280L;
    private Boolean success; //是否成功
    private Integer code;   //狀态碼
    private String message; //具體消息  成功失敗與否的消息
    private Object data;    //具體資料
    private Result() {
    }

    /**
     * 請求成功 沒有資料 隻傳回狀态碼
     * @return
     */
    public static Result ok() {
        Result r = new Result();
        r.setSuccess(ResultCodeEnum.SUCCESS.getSuccess());
        r.setCode(ResultCodeEnum.SUCCESS.getCode());
        r.setMessage(ResultCodeEnum.SUCCESS.getMessage());
        return r;
    }

    /**
     * 請求成功 有資料
     * @param data
     * @return
     */
    public static Result ok(Object data) {
        Result r = new Result();
        r.setSuccess(ResultCodeEnum.SUCCESS.getSuccess());
        r.setCode(ResultCodeEnum.SUCCESS.getCode());
        r.setMessage(ResultCodeEnum.SUCCESS.getMessage());
        r.setData(data);
        return r;
    }
    /**
     * 錯誤請求 傳回錯誤消息
     * @return
     */
    public static Result error() {
        Result r = new Result();
        r.setSuccess(ResultCodeEnum.UNKNOWN_REASON.getSuccess());
        r.setCode(ResultCodeEnum.UNKNOWN_REASON.getCode());
        r.setMessage(ResultCodeEnum.UNKNOWN_REASON.getMessage());
        return r;
    }

    /**
     * 最終版 參數設定為枚舉 設定結果集的内容
     * @param resultCodeEnum
     * @return
     */
    public static Result setResult(ResultCodeEnum resultCodeEnum) {
        Result r = new Result();
        r.setSuccess(resultCodeEnum.getSuccess());
        r.setCode(resultCodeEnum.getCode());
        r.setMessage(resultCodeEnum.getMessage());
        return r;
    }
    public Result success(Boolean success) {
        this.setSuccess(success);
        return this;
    }
    public Result message(String message) {
        this.setMessage(message);
        return this;
    }
    public Result code(Integer code) {
        this.setCode(code);
        return this;
    }
    public Result data(Object o) {
        this.setData(o);
        return this;
    }
}
           
  • ResultCodeEnum

  • 傳回結果的枚舉 包括 狀态 消息等等
package com.chill.common.base;

import lombok.Getter;

/**
 * 結果狀态碼枚舉
 */
@Getter
public enum ResultCodeEnum {
    SUCCESS(true, 20000, "成功"),
    UNKNOWN_REASON(false, 20001, "未知錯誤"),
    BAD_SQL_GRAMMAR(false, 21001, "sql文法錯誤"),
    JSON_PARSE_ERROR(false, 21002, "json解析異常"),
    PARAM_ERROR(false, 21003, "參數不正确");
    private Boolean success;
    private Integer code;
    private String message;

    private ResultCodeEnum(Boolean success, Integer code, String message) {
        this.success = success;
        this.code = code;
        this.message = message;
    }
}
           

至此 我們在傳回結果時隻需要傳回

R類

便行

  • 示例
@GetMapping("/success")
    public Result success() {
        return Result.ok();
    }
           

但是這樣真的沒有問題嗎?

思考:

  • 如果我們是一個比較大的開發團隊 不可能所有人都會聽從安排 以

    R類

    去傳回結果 事實上 大多數開發人都有自己的代碼風格 是以我們需要另辟蹊徑 再想個法子 可以自己還是安裝自己的風格去傳回資料 但是都會被包裹一層 R類出去 這樣子是不是就能解決我們的問題呢

3、解決方案

  • springmvc給我們提供了這樣的支援 隻需要實作

    ResponseBodyAdvice

    接口 重寫方法 便能滿足我們的需求
    • ResponseBodyAdvice

      • 它是屬于springAOP機制的一種實作 屬于後置通知 在我們mvc将結果傳回給前端時 會被它攔截下來進行處理
      • 通常我們會對結果進行增強處理 比如 加密、簽名、字元串特殊處理等等

實作

package com.chill.handle;

import com.chill.common.base.Error;
import com.chill.common.base.Result;
import com.chill.utils.JsonUtil;
import org.springframework.core.MethodParameter;
import org.springframework.http.MediaType;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.server.ServerHttpRequest;
import org.springframework.http.server.ServerHttpResponse;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.servlet.mvc.method.annotation.ResponseBodyAdvice;

/**
 * ResponseBodyAdvice: 所有傳回值都會經過它檢查 它會在mvc層傳回資料給前端之前執行 一般對傳回值進行加密 簽名等等
 * 解決的問題:并不是非要所有傳回類型都要是 R 類     局限性沒那麼高
 */
@ControllerAdvice(basePackages = "com.chill")
public class ResultResponseHandler implements ResponseBodyAdvice<Object> {
    /**
     * 是否支援advice功能,true是支援 false是不支援
     *
     * @param methodParameter
     * @param aClass
     * @return
     */
    @Override
    public boolean supports(MethodParameter methodParameter, Class<? extends HttpMessageConverter<?>> aClass) {
        return true;
    }

    /**
     * 這個方法是否被執行 由上面的supports()的傳回值決定
     * @param o controller方法傳回的結果會被映射到該對象中
     * @param methodParameter   方法的參數 也就是目前這個controller的方法
     * @param mediaType
     * @param aClass    除開字元串   一般的類型都會被HttpMessageConverter處理過後再交給前端   字元串需要特殊處理
     * @param serverHttpRequest 請求頭
     * @param serverHttpResponse 響應頭
     * @return
     */
    @Override
    public Object beforeBodyWrite(Object o, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
        //判斷一下 該方法是否異常  因為如果出現異常的話 這個對象會被包裝成Error
        if (o instanceof Error) {
            Error error = (Error) o;
            //将error對象再裹一層 --->Result
            return Result.error().code(error.getStatus()).message(error.getMessage());
        } else if (o instanceof String) {
            //因為如果傳回是string的話預設會調用string的處理器會直接傳回,是以要進行處理
            return JSONUtil.toJsonStr(Result.ok(o));
        }
        //如果都沒問題 則直接包裝成 Result
        return Result.ok(o);
    }
}
           
  • 說明
    • 在執行順序中 如果controller方法出現異常 會先被捕捉到全局異常中 然後再進入這個 後置通知

      ResponseBodyAdvice

    • 如果controller方法 傳回值是

      String

      的話 要進行 特殊處理 使用市面上的一些Json工具包 将字元串轉為Json即可 我使用的是 hutool-all
    <dependency>
         <groupId>cn.hutool</groupId>
         <artifactId>hutool-all</artifactId>
         <version>5.7.2</version>
    </dependency>
               
    • 需要加上@ControllerAdvice(basePackages = “com.chill”) 因為如果不加的話,可能給整個系統的産生沖突影響比如:如果你使用了swagger時會出現空白異常

測試

  • 如果不處理

    String

    的後果
    SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發
    • 原因是什麼呢?
      • 因為在

        Result.ok(o)

        這句代碼時 此時 o 已經确定好是

        String

        它會調用

        StringHttpMessageConverter

        對它進行轉換 它是 springmvc 将字元串傳入前端時預設的轉換器 作用于轉換字元串為Json 然而這裡已經被包裝成

        Result

        對象了 而

        StringHttpMessageConverter

        隻能轉換

        String

        是以便報了錯
  • 正确結果
    • controller
    @GetMapping("/ok")
        public String ok() {
            return "success";
        }
               
    • 響應結果
      SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

4.3、全局異常處理

1、概念

  • 在我們開發中 不可能會使用大量的

    try/catch

    來處理異常 這樣一來顯得代碼十分臃腫 二來處理程式十分繁瑣 是以通常我們會采用

    springmvc

    内部提供的的統一異常處理 它是為了解決一系列錯誤的方案

2、實作

2.1、編寫Error和ExceptionCodeEnum

  • Error

  • Result

    類相似 主要不想影響

    Result

    是以單獨抽出來作為一個異常結果傳回類
package com.chill.common.base;

import lombok.*;


/**
 * 統一傳回異常類
 */
@Builder
@AllArgsConstructor
@NoArgsConstructor
@Data
@ToString
public class Error {
    // 異常的狀态碼,從枚舉中獲得
    private Integer status;
    // 異常的消息,寫使用者看得懂的異常,從枚舉中得到
    private String message;
    // 異常的名字
    private String exception;

    /**
     * 對異常處理進行統一封裝
     *
     * @param exceptionCodeEnum : 異常狀态枚舉
     * @param throwable         所有異常或者錯誤的頂級父類
     * @param message           錯誤資訊
     * @return
     */
    public static Error fail(ExceptionCodeEnum exceptionCodeEnum, Throwable throwable, String message) {
        Error error = Error.fail(exceptionCodeEnum, throwable);
        error.setMessage(message);
        return error;
    }

    /**
     * 對異常枚舉進行封裝
     *
     * @param resultCodeEnum
     * @param throwable
     * @return
     */
    public static Error fail(ExceptionCodeEnum resultCodeEnum, Throwable throwable) {
        Error error = new Error();
        error.setMessage(resultCodeEnum.getMessage());
        error.setStatus(resultCodeEnum.getCode());
        error.setException(throwable.getClass().getName());
        return error;
    }
}
           
  • ExceptionCodeEnum

  • 異常錯誤資訊的枚舉 包括

    狀态碼

    還有

    異常資訊

    異常類

package com.chill.common.base;

import lombok.Getter;


/**
 * 異常狀态碼枚舉
 */
@Getter
public enum ExceptionCodeEnum {
    UNKNOWN_REASON(false, 20001, "未知錯誤"),
    SERVER_ERROR(false, 500, "伺服器忙,請稍後在試"),
    ORDER_CREATE_FAIL(false, 601, "訂單下單失敗");
    private Boolean success;
    private Integer code;
    private String message;

    private ExceptionCodeEnum(Boolean success, Integer code, String message) {
        this.success = success;
        this.code = code;
        this.message = message;
    }
}
           

将常量類更新使用枚舉?

  • 如果有常量類裡面 成批 成對的一些常量 可以考慮使用枚舉 枚舉具有面向對象的特征 代碼也十厘清晰明了 不臃腫

2.2、編寫配置類

  • 建立一個包 handle 建立一個類

    GlobalExceptionHandler

package com.chill.handle;

import com.chill.common.base.Error;
import com.chill.common.base.ExceptionCodeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 全局異常處理器
 *
 * @tips : go go go~~ ^O^
 * @Author : chill
 * @Date : 2021-06-28
 * @Version : v1.0
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {


    /**
     * @param throwable
     * @return
     * @ExceptionHandler : 用于捕捉在程式中出現異常的代碼
     */
    @ResponseStatus(code = HttpStatus.INTERNAL_SERVER_ERROR) //指定該方法處理哪個狀态碼的異常  比如INTERNAL_SERVER_ERROR(内部伺服器錯誤) 就是 500
    @ExceptionHandler(Throwable.class)
    public Error processException(Throwable throwable) {
        log.error("{}", throwable);  //記錄錯誤資訊
        return Error.fail(ExceptionCodeEnum.SERVER_ERROR, throwable);   //出現錯誤後傳回自己的錯誤資訊
    }
}
           

注解說明:

  • RestControllerAdvice

    ControllerAdvice

    的差別
    • RestControllerAdvice

      注解的類裡面的方法在捕捉異常後是不能進行頁面跳轉的 而

      ControllerAdvice

      是可以的 也就是說和

      Controller

      層的那兩個注解意思有一點類似

      RestController

      Controller

  • ResponseStatus

    • 指定該方法處理哪個狀态碼的異常 比如

      INTERNAL_SERVER_ERROR

      (内部伺服器錯誤) 就是 500
  • ExceptionHandler

    • 被該注解辨別的方法 在捕捉到異常後 會進入該方法

2.3、測試

  • controller
@GetMapping("/error")
    public String error() {
        int num=1/0;
        return null;
    }
           
  • 發送請求
    SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

報完錯以後進入 我們的全局異常裡

SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發
  • 響應結果
    SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

符合我們的正确結果 但是問題來了 思考一下:如果所有controller方法出現了異常 那麼都走全局異常 然後報

500

錯誤 這樣子異常錯誤清晰嗎?根據不清晰 對于我們開發這是很難的事情!!是以我們需要優化它。

3、自定義異常

3.1、概念

  • 為了我們能夠更快的定位異常資訊 和 記錄精确的日志 我們需要對全局異常類進行優化 我們可以在訂單下單失敗時 傳回給前端明确的資訊 : 訂單下單失敗 這樣子一來 我們能夠快速的定位錯誤和排查

3.2、實作

1、編寫自定義異常類
  • 這是與訂單相關的異常類 但凡在訂單生成時出錯 都會報出該錯
  • 一般的異常錯誤幾乎都是運作時異常 是以我們隻需要繼承

    RuntimeException

    就行了
package com.chill.custom;

import com.chill.common.base.ExceptionCodeEnum;
import lombok.Data;

/**
 * 自定義異常類
 */
@Data
public class OrderException extends RuntimeException {
    private Integer code;   //訂單錯誤狀态碼
    private String message; //訂單錯誤資訊

    //ExceptionCodeEnum 包含了所有具體異常的枚舉
    public OrderException(ExceptionCodeEnum exceptionCodeEnum) {
        this.code = exceptionCodeEnum.getCode();
        this.message = exceptionCodeEnum.getMessage();
    }
}
           
2、編寫異常枚舉類
  • 它包含了所有異常狀态碼 資訊等等
  • 枚舉 友善擴充 對外修改關閉 對内修改打開
package com.chill.common.base;

import lombok.Getter;


/**
 * 異常狀态碼枚舉
 */
@Getter
public enum ExceptionCodeEnum {
    UNKNOWN_REASON(false, 20001, "未知錯誤"),
    SERVER_ERROR(false, 500, "伺服器忙,請稍後在試"),
    ORDER_CREATE_FAIL(false, 601, "訂單下單失敗");
    private Boolean success;
    private Integer code;
    private String message;

    private ExceptionCodeEnum(Boolean success, Integer code, String message) {
        this.success = success;
        this.code = code;
        this.message = message;
    }
}
           
3、編寫全局異常類
  • 新增一個異常方法 該異常用來專門處理訂單
  • 在捕捉異常時 springmvc的機制是 預設從小到大 也就是說

    Throwable

    是頂級父類 在子類解決不了問題時 才會把問題丢給這個父類 有一點像JS的冒泡 或者JVM的

    雙親委派機制

/**
     * 捕捉自定義異常
     * @param orderException    精确到 訂單異常
     * @return
     */
    @ExceptionHandler(OrderException.class)
    public Error processOrderException(OrderException orderException) {
        log.error("{}", orderException.getMessage());
        return Error.builder().
                status(orderException.getCode()).
                message(orderException.getMessage()).
                exception(orderException.getClass().getName()).
                build();
    }
           
4、測試
//測試自定義異常
    @GetMapping("/order")
    public User order() {
        throw new OrderException(ExceptionCodeEnum.ORDER_CREATE_FAIL);
    }
           
5、結果

通過

swagger

頁面進行通路

  • console
    SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發
  • swagger
    SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

結果正是我們想要的

4、小結

  • 在我們需求增加時 我們想要擷取異常的精确資訊 和 快速定位異常 我們需要對全局異常進行優化 這個優化

    springmvc

    也提供了支援 就是

    自定義異常

  • 通過自定義異常 我們可以将精确異常資訊記錄到日志中 進而快速排查bug
  • 我們應該對于一些常見的異常錯誤 我們可以采用自定義異常 專門捕捉

4.4、校驗器(Validator)

1、概念

  • 在日常開發中 我們需要對資料格式進行嚴格的排查後 才能寫入資料庫 然而在如今前後端分離的情況下 在前端表單校驗資料過後 其實後端也需要保證萬無一失 是以也要再校驗一遍 校驗的參數一般會有

    pojo

    普通的參數

    等等 那麼對于這些我們如何去校驗呢 ?
  • 因為在日常的開發中,服務端對象的校驗是非常重要的一個環節,比如:注冊的時候:校驗使用者名,密碼,身份證,郵箱等資訊是否為空,以及格式是否正确,但是這種在日常的開發中進行校驗太繁瑣了,代碼繁瑣而且很多。
  • Validator架構應運而生,它的出現就是為了解決開發人員在開發的時候減少代碼的,提升開發效率。它專門用來做接口的參數校驗,比如:密碼長度、是否為空等等。

spring的

validator校驗架構

遵守的是

JSR-303

的驗證規範(參數校驗規範),JSR全稱:Java Specification Requests縮寫。

在預設情況下:SpringBoot會引入

Hibernate Validation

機制來支援JSR-303驗證規範。

  • SpringBoot的validator校驗架構支援如下特征:
    • JSR303特征

      :JSR303是一項标準,隻提供規範不提供實作。規定一些校驗規範即校驗注解。比如:@Null、@NotNull、@Pattern。這些類都位于:javax.validation.constraints包下。
    • hibernate validation特征

      :hibernate validation是對JSR303規範的實作并且進行了增強和擴充。并增加了注解:@Email、@Length、@Range等等。
    • spring Validation

      :Spring Validation是對Hibernate Validation的二次封裝。在SpringMvc子產品中添加了自動校驗。并将校驗資訊封裝到特定的類中。
SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

2、常用的校驗注解

  • 2/3 的注解都在這了 意思也寫的很明白
JSR提供的校驗注解:         
@Null   被注釋的元素必須為 null    
@NotNull    被注釋的元素必須不為 null    
@AssertTrue     被注釋的元素必須為 true    
@AssertFalse    被注釋的元素必須為 false    
@Min(value)     被注釋的元素必須是一個數字,其值必須大于等于指定的最小值    
@Max(value)     被注釋的元素必須是一個數字,其值必須小于等于指定的最大值    
@DecimalMin(value)  被注釋的元素必須是一個數字,其值必須大于等于指定的最小值    
@DecimalMax(value)  被注釋的元素必須是一個數字,其值必須小于等于指定的最大值    
@Size(max=, min=)   被注釋的元素的大小必須在指定的範圍内    
@Digits (integer, fraction)     被注釋的元素必須是一個數字,其值必須在可接受的範圍内    
@Past   被注釋的元素必須是一個過去的日期    
@Future     被注釋的元素必須是一個将來的日期    
@Pattern(regex=,flag=)  被注釋的元素必須符合指定的正規表達式    
Hibernate Validator提供的校驗注解:  
@NotBlank(message =)   驗證字元串非null,且trim後長度必須大于0    
@Email  被注釋的元素必須是電子郵箱位址    
@Length(min=,max=)  被注釋的字元串的大小必須在指定的範圍内    
@NotEmpty   被注釋的字元串的必須非空    
@Range(min=,max=,message=)  被注釋的元素必須在合适的範圍内
           

3、整合validator

3.1、核心步驟概述

在實際開發中,隻需要三個步驟:

  • 在需要校驗的pojo,vo等等中的屬性上增加對應注解,比如

    @NotBlank

  • controller方法

    參數中的 pojo,vo中加

    @Validated

    的注解即可。(普通參數有另外的處理方法)
  • 使用全局統一異常處理捕獲的驗證失敗的提示資訊

3.2、實作

1、pom.xml添加依賴
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-validation</artifactId>
</dependency>
           
2、建立使用者實體
  • 結合校驗注解實作該實體
  • 在實際開發中 有些需要使用自定義校驗器來完成特殊的需求
package com.chill.vo;

import lombok.Data;
import org.hibernate.validator.constraints.Length;
import javax.validation.constraints.*;
import java.util.Date;

/**
 * @author chill
 * @Description: vo類
 * @date 2021/6/29 14:29
 */
@Data
public class UserVo {
    @NotNull(message = "使用者id不能為空")
    private Long userId;
    @NotBlank(message = "使用者名不能為空")
    @Length(max = 20, message = "使用者名不能超過20個字元")
    @Pattern(regexp = "^[\\u4E00-\\u9FA5A-Za-z0-9\\*]*$", message = "使用者昵稱限制:最多20字元,包含文字、字母和數字")
    private String username;
    @NotBlank(message = "手機号不能為空")
    @Pattern(regexp = "^[1][3,4,5,6,7,8,9][0-9]{9}$", message = "手機号格式有誤")
    private String mobile;
    @NotBlank(message = "聯系郵箱不能為空")
    @Email(message = "郵箱格式不對")
    private String email;
    @Future(message = "時間必須是将來時間")
    private Date createTime;
}
           
3、編寫controller
//測試校驗器
    @PostMapping("/validate")
    public UserVo validate(@RequestBody @Validated UserVo user) {
        log.info("{}",user);
        return user;
    }
           
4、測試
  • swagger工具進行調試 或者

    postman

    等等
    SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發
5、結果
  • console
    SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發
  • swagger
    SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發
6、問題
  • 确實是報錯了 但是問題是這個錯誤資訊都列印在控制台 而且錯誤資訊十分複雜 并不精确 是以我們得在全局異常類哪裡動點手腳 新增一個捕捉異常方法

3.3、優化異常資訊

1、分析
  • 如果你校驗失敗,springmvc的validator内部會以異常的方式進行傳回。報錯異常:

    MethodArgumentNotValidException

    而這個異常裡面,包含所有的校驗的提示資訊。
  • 那麼

    MethodArgumentNotValidException

    我們可以通過異常的api 取出重要的資訊
2、實作
  • 對于異常 我們可以取出異常裡的一些重要資訊 而這些重要資訊我們需要記錄日志 和 傳回給前端
/**
     * 對驗證的統一異常進行統一處理
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public Error processValidationException(MethodArgumentNotValidException e, HttpServletRequest request) {
        //擷取與字段相關的所有錯誤
        List<FieldError> fieldErrors = e.getBindingResult().getFieldErrors();
        //該方法将fieldErrors裡面的資訊 簡化  取出了最重要的一部分  key:字段名  value:錯誤資訊
        List<Map<String, String>> mapList = processFieldErrors(fieldErrors);
        Error error = Error.fail(ExceptionCodeEnum.PARAMS_VALIDATION_ERROR, e, JSONUtil.toJsonStr(mapList));
        return error;
    }
           
/**
     * 将錯誤驗證資訊取出重要部分進行傳回
     * @param fieldErrorList
     * @return
     */
    private List<Map<String, String>> processFieldErrors(List<FieldError> fieldErrorList) {
        List<Map<String, String>> mapList = new ArrayList<>();
        for (FieldError fieldError : fieldErrorList) {
            Map<String, String> map = new HashMap<>();
            map.put("field", fieldError.getField());
            map.put("msg", fieldError.getDefaultMessage());
            mapList.add(map);
        }
        return mapList;
    }
           
3、測試
  • swagger
    SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發
4、debug分析
  • 通過異常的api取出所有異常字段對象 它是一個集合
    SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發
  • 将集合中所有的異常字段對象 最重要的資訊取出 并存入map 使用json工具轉化為字元串傳入前端
    SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發

3.4、小結

  • 這是将全局異常、參數校驗和統計傳回類 做了一個總結性的使用 可以看出我們如果想要精确化的資料 或者 資訊 都需要我們自己自定義一些東西來實作需求
  • 上面有個問題 發現了嗎?
    • 參數都是實體類或者都是一個對象 那麼問題來了 如果是一個普通參數 我們可以使用

      @Validated

      來驗證嗎? 并不能

4、驗證普通參數(使用Assert)

4.1、概念

  • Assert

    是 斷言 的意思
  • Web 應用在接受表單送出的資料後都需要對其進行合法性檢查,如果表單資料不合法,請求将被駁回。類似的,當我們在編寫類的方法時,也常常需要對方法入參進行合法性檢查,如果入參不符合要求,方法将通過抛出異常的方式拒絕後續處理。
  • 一般使用:通過斷言過後 如果參數不符合要求 則直接報異常

    IllegalArgumentException

  • 是以我們需要針對

    IllegalArgumentException

    也得寫一個自定義異常捕捉方法

4.2、實作

1、編寫異常捕捉方法
  • 非法參數異常 全部會被該方法捕捉到
/**
     * 普通參數校驗統一處理
     */
    @ExceptionHandler(IllegalArgumentException.class)
    public Error handlerIllegalArgumentException(IllegalArgumentException e, HttpServletRequest request) {
        Error error = Error.builder()
                .status(4000)
                .message(e.getMessage())
                .exception(e.getClass().getName())
                .build();
        log.error("請求的位址是:{},IllegalArgumentException出現異常:{}", request.getRequestURL(), e);
        return error;
    }
           
2、編寫controller
//測試校驗器
    @PostMapping("/validate2")
    public String validate2(@RequestBody String name) {
        Assert.isNull(name,"使用者名不能為空");
        log.info("{}",name);
        return name;
    }
           
3、測試
  • swagger發送請求
    SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發
4、結果
  • console
    SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發
  • swagger
    SpringBoot整體開發的詳細過程(待完結)SpringBoot筆記一、回顧Spring二、微服務三、SpringBoot四、Web開發
    就這麼簡單 ~~

5、自定義校驗器

5.1、概念

  • 很多時候 由

    Validator 架構

    給我們提供的 一些驗證注解 并不夠用 我們通常還有一些其他的需求 是以我們就需要自定義校驗器來實作我們的需求

5.2、實作

1、定義驗證異常注解
  • 随便copy一個驗證注解 稍微修改一下就好了 比如說:

    @Email

  • 核心在 @Constraint(validatedBy = PhoneValidator.class) 需要給指定一個限制 該限制由自己定義

    PhoneValidator

package com.chill.custom.validate;

import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;

@Documented
@Constraint(validatedBy = PhoneValidator.class)
@Target({ElementType.METHOD, ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Phone {
    String message() default "手機格式不正确!";

    Class<?>[] groups() default {};

    Class<? extends Payload>[] payload() default {};

    @Target({ElementType.METHOD, ElementType.FIELD, ElementType.ANNOTATION_TYPE, ElementType.CONSTRUCTOR, ElementType.PARAMETER, ElementType.TYPE_USE})
    @Retention(RetentionPolicy.RUNTIME)
    @Documented
    public @interface List {
        Phone[] value();
    }
}
           
2、定義自定義校驗器
  • 需要實作

    ConstraintValidator

    接口 該接口的作用就是 (源碼得知:) 給

    T

    這個對象添加

    A

    的限制
  • 也就是說根據具體傳入的對象

    T

    : String

    A

    @Phone

    注解 給該String增加一個限制 該限制就是隻能為

    @Phone

    裡面定義的規則 而

    Phone

    依賴于

    PhoneValidator

    PhoneValidator

    裡有一個

    isValid

    方法 它就定義了具體規則 如果規則驗證通過則傳回

    true

    驗證失敗則傳回

    false

package com.chill.custom.validate;

import com.chill.tools.ValidateUtil;
import org.springframework.util.StringUtils;

import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

/**
 * @description: 給該 Phone注解對象定義一個限制  該限制的具體實作都在isValid 裡  是否驗證通過
 * @author: chill
 * @time: 2021/6/29 20:57
 */
public class PhoneValidator implements ConstraintValidator<Phone, String> {

    @Override
    public boolean isValid(String phone, ConstraintValidatorContext constraintValidatorContext) {
        // 1: 如果使用者沒輸入直接傳回不校驗,因為空的判斷應該交給@NotNull去做就行了
        if (StringUtils.isEmpty(phone)) {
            return true;
        }
        return ValidateUtil.validateMobile(phone);
    }

	 //可以在執行isValid之前 先初始化該注解  
    @Override
    public void initialize(Phone constraintAnnotation) {
    }
}
           
3、測試
  • 在需要加的屬性上 添加即可
  • 比較簡單 就不示範了

6、驗證工具包

  • 在多數情況下 我們需要自己定義一些工具包 來幫助我們快速開發
package com.chill.tools;

import java.util.regex.Pattern;

import org.apache.commons.lang3.StringUtils;

/**
 * 常用的一些驗證,如手機、移動号碼、聯通号碼、電信号碼、密碼、座機、 郵政編碼、郵箱、年齡、身份證、URL、QQ、漢字、字母、數字等
 */
public class ValidateUtil {
    /**
     * 手機号規則
     */
    public static final String MOBILE_PATTERN = "^((13[0-9])|(14[0-9])|(15[0-9])|(17[0-9])|(18[0-9]))(\\d{8})$";
    /**
     * 中國電信号碼格式驗證 手機段: 133,153,180,181,189,177,1700,173
     **/
    private static final String CHINA_TELECOM_PATTERN = "(?:^(?:\\+86)?1(?:33|53|7[37]|8[019])\\d{8}$)|(?:^(?:\\+86)?1700\\d{7}$)";
    /**
     * 中國聯通号碼格式驗證 手機段:130,131,132,155,156,185,186,145,176,1707,1708,1709,175
     **/
    private static final String CHINA_UNICOM_PATTERN = "(?:^(?:\\+86)?1(?:3[0-2]|4[5]|5[56]|7[56]|8[56])\\d{8}$)|(?:^(?:\\+86)?170[7-9]\\d{7}$)";
    /**
     * 中國移動号碼格式驗證 手機段:134,135,136,137,138,139,150,151,152,157,158,159,182,183,184,187,188,147,178,1705
     **/
    private static final String CHINA_MOVE_PATTERN = "(?:^(?:\\+86)?1(?:3[4-9]|4[7]|5[0-27-9]|7[8]|8[2-478])\\d{8}$)|(?:^(?:\\+86)?1705\\d{7}$)";
    /**
     * 密碼規則(6-16位字母、數字)
     */
    public static final String PASSWORD_PATTERN = "^[0-9A-Za-z]{6,16}$";
    /**
     * 固号(座機)規則
     */
    public static final String LANDLINE_PATTERN = "^(?:\\(\\d{3,4}\\)|\\d{3,4}-)?\\d{7,8}(?:-\\d{1,4})?$";
    /**
     * 郵政編碼規則
     */
    public static final String POSTCODE_PATTERN = "[1-9]\\d{5}";
    /**
     * 郵箱規則
     */
    public static final String EMAIL_PATTERN = "^([a-z0-9A-Z]+[-|_|\\.]?)+[a-z0-9A-Z]@([a-z0-9A-Z]+(-[a-z0-9A-Z]+)?\\.)+[a-zA-Z]{2,}$";
    /**
     * 年齡規則 1-120之間
     */
    public static final String AGE_PATTERN = "^(?:[1-9][0-9]?|1[01][0-9]|120)$";
    /**
     * 身份證規則
     */
    public static final String IDCARD_PATTERN = "^\\d{15}|\\d{18}$";
    /**
     * URL規則,http、www、ftp
     */
    public static final String URL_PATTERN = "http(s)?://([\\w-]+\\.)+[\\w-]+(/[\\w- ./?%&=]*)?";
    /**
     * QQ規則
     */
    public static final String QQ_PATTERN = "^[1-9][0-9]{4,13}$";
    /**
     * 全漢字規則
     */
    public static final String CHINESE_PATTERN = "^[\u4E00-\u9FA5]+$";
    /**
     * 全字母規則
     */
    public static final String STR_ENG_PATTERN = "^[A-Za-z]+$";
    /**
     * 整數規則
     */
    public static final String INTEGER_PATTERN = "^-?[0-9]+$";
    /**
     * 正整數規則
     */
    public static final String POSITIVE_INTEGER_PATTERN = "^\\+?[1-9][0-9]*$";

    /**
     * @param mobile 手機号碼
     * @return boolean
     * @Description: 驗證手機号碼格式
     */
    public static boolean validateMobile(String mobile) {
        if (StringUtils.isEmpty(mobile)) {
            return Boolean.FALSE;
        }
        return mobile.matches(MOBILE_PATTERN);
    }

    /**
     * 驗證是否是電信手機号,133、153、180、189、177
     *
     * @param mobile 手機号
     * @return boolean
     */
    public static boolean validateTelecom(String mobile) {
        if (StringUtils.isEmpty(mobile)) {
            return Boolean.FALSE;
        }
        return mobile.matches(CHINA_TELECOM_PATTERN);
    }

    /**
     * 驗證是否是聯通手機号 130,131,132,155,156,185,186,145,176,1707,1708,1709,175
     *
     * @param mobile 電話号碼
     * @return boolean
     */
    public static boolean validateUnionMobile(String mobile) {
        if (StringUtils.isEmpty(mobile)) {
            return Boolean.FALSE;
        }
        return mobile.matches(CHINA_UNICOM_PATTERN);
    }

    /**
     * 驗證是否是移動手機号
     *
     * @param mobile 手機号 134,135,136,137,138,139,150,151,152,157,158,159,182,183,184,187,188,147,178,1705
     * @return boolean
     */
    public static boolean validateMoveMobile(String mobile) {
        if (StringUtils.isEmpty(mobile)) {
            return Boolean.FALSE;
        }
        return mobile.matches(CHINA_MOVE_PATTERN);
    }

    /**
     * @param pwd 密碼
     * @return boolean
     * @Description: 驗證密碼格式  6-16 位字母、數字
     */
    public static boolean validatePwd(String pwd) {
        if (StringUtils.isEmpty(pwd)) {
            return Boolean.FALSE;
        }
        return Pattern.matches(PASSWORD_PATTERN, pwd);
    }

    /**
     * 驗證座機号碼,格式如:58654567,023-58654567
     *
     * @param landline 固話、座機
     * @return boolean
     */
    public static boolean validateLandLine(final String landline) {
        if (StringUtils.isEmpty(landline)) {
            return Boolean.FALSE;
        }
        return landline.matches(LANDLINE_PATTERN);
    }

    /**
     * 驗證郵政編碼
     *
     * @param postCode 郵政編碼
     * @return boolean
     */
    public static boolean validatePostCode(final String postCode) {
        if (StringUtils.isEmpty(postCode)) {
            return Boolean.FALSE;
        }
        return postCode.matches(POSTCODE_PATTERN);
    }

    /**
     * 驗證郵箱(電子郵件)
     *
     * @param email 郵箱(電子郵件)
     * @return boolean
     */
    public static boolean validateEamil(final String email) {
        if (StringUtils.isEmpty(email)) {
            return Boolean.FALSE;
        }
        return email.matches(EMAIL_PATTERN);
    }

    /**
     * 判斷年齡,1-120之間
     *
     * @param age 年齡
     * @return boolean
     */
    public static boolean validateAge(final String age) {
        if (StringUtils.isEmpty(age)) {
            return Boolean.FALSE;
        }
        return age.matches(AGE_PATTERN);
    }

    /**
     * 身份證驗證
     *
     * @param idCard 身份證
     * @return boolean
     */
    public static boolean validateIDCard(final String idCard) {
        if (StringUtils.isEmpty(idCard)) {
            return Boolean.FALSE;
        }
        return idCard.matches(IDCARD_PATTERN);
    }

    /**
     * URL位址驗證
     *
     * @param url URL位址
     * @return boolean
     */
    public static boolean validateUrl(final String url) {
        if (StringUtils.isEmpty(url)) {
            return Boolean.FALSE;
        }
        return url.matches(URL_PATTERN);
    }

    /**
     * 驗證QQ号
     *
     * @param qq QQ号
     * @return boolean
     */
    public static boolean validateQq(final String qq) {
        if (StringUtils.isEmpty(qq)) {
            return Boolean.FALSE;
        }
        return qq.matches(QQ_PATTERN);
    }

    /**
     * 驗證字元串是否全是漢字
     *
     * @param str 字元串
     * @return boolean
     */
    public static boolean validateChinese(final String str) {
        if (StringUtils.isEmpty(str)) {
            return Boolean.FALSE;
        }
        return str.matches(CHINESE_PATTERN);
    }

    /**
     * 判斷字元串是否全字母
     *
     * @param str 字元串
     * @return boolean
     */
    public static boolean validateStrEnglish(final String str) {
        if (StringUtils.isEmpty(str)) {
            return Boolean.FALSE;
        }
        return str.matches(STR_ENG_PATTERN);
    }

    /**
     * 判斷是否是整數,包括負數
     *
     * @param str 字元串
     * @return boolean
     */
    public static boolean validateInteger(final String str) {
        if (StringUtils.isEmpty(str)) {
            return Boolean.FALSE;
        }
        return str.matches(INTEGER_PATTERN);
    }

    /**
     * 判斷是否是大于0的正整數
     *
     * @param str 字元串
     * @return boolean
     */
    public static boolean validatePositiveInt(final String str) {
        if (StringUtils.isEmpty(str)) {
            return Boolean.FALSE;
        }
        return str.matches(POSITIVE_INTEGER_PATTERN);
    }
}
           

繼續閱讀