1. 程式人生 > 實用技巧 >SpringBoot 學習筆記

SpringBoot 學習筆記

SpringBoot 學習筆記

目錄

1. SpringBoot簡介

1.1 什麼是Spring

  • Spring:中文譯為春天 --> 給 Java 程式設計師的春天
  • 2002年,首次推出了 Spring 框架的雛形: interface 21 框架
  • Spring框架即以 interface 21 框架位基礎,並不斷豐富其內涵,於2004年3月24日,釋出了1.0正式版
  • Rod Johnson , Spring Frameword 創始人,著名作者。很難想象 Rod Johnson 的學歷,他是悉尼大學的博士,然而他的專業不是計算機,而是音樂學
  • Spring 理念:使現有的技術更加容易使用,本身就是個大雜燴

Spring 是為了解決企業應用開發的複雜性而建立的,簡化開發

1.2 Spring 是如何簡化Java開發的

為了降低 Java 開發的複雜性,Spring 採用一下4種關鍵策略:

  1. 基於 POJO(老式 Java 物件)的輕量級和最小侵入性程式設計,所有東西都是一個 JavaBean
  2. 通過 IOC,依賴注入(DI)和麵向介面程式設計實現鬆耦合
  3. 基於切面(AOP)和慣例進行宣告式程式設計
  4. 通過切面和模板減少樣式程式碼,RedisTemplate,xxxTemplate等

1.3 什麼是 SpringBoot

  • SpringBoot 是由Pivotal團隊在2013年開始研發、2014年4月釋出第一個版本的全新開源的輕量級框架。它基於Spring4.0設計,不僅集成了Spring框架原有的優秀特性,而且還通過簡化配置來進一步簡化了Spring應用的整個搭建和開發過程。另外,SpringBoot 通過整合大量的框架使得依賴包的版本衝突,以及引用的不穩定性等問題得到了很好的解決。

  • SpringBoot就是一個javaWeb的開發框架,和SpringMVC類似,對比其他javaweb框架的好處,官方說是簡化開發,約定大於配置,you can "just run",能迅速地開發web應用,幾行程式碼開發一個http介面。

  • SpringBoot 是基於 Spring 開發的,SpringBoot 本身並不提供 Spring 框架的核心特性以及擴充套件功能,=只是用來快速、敏捷地開發新一代基於 Spring 框架的應用程式。也就是說,它並不是用來替代 Spring 的解決方案,而是和 Spring 框架緊密結合用於提升 Spring 開發者體驗的工具。 SpringBoot 以 約定大於配置 的核心思想,預設幫我們進行了很多配置,多數 SpringBoot 應用只需要很少的 Spring 配置。同時它集成了大量常用的第三方庫配置(例如 Redis、MongoDB、Jpa、RabbitMQ、Quartz 等等),SpringBoot 應用中這些第三方庫可以零配置地開箱即用。

1.4 SpringBoot 的主要優點

  • 為所有的 Spring 開發者更快的入門
  • 開箱即用,提供各種預設配置來簡化專案配置
  • 內嵌式容器簡化Web專案
  • 沒有冗餘程式碼生成和XML配置的要求

2. 第一個 SpringBoot 程式

2.1 環境準備

  • JDK 1.8
  • Maven-3.6.1
  • SpringBoot 2.X 最新版
  • IDEA

2.2 建立第一個工程專案

有三種建立 SpringBoot 工程專案的方式

  1. Spring 官方提供了非常方便的工具讓我們快速構建應用

    官網快速構建網址:https://start.spring.io/

  2. 使用 IDEA 快速構建應用

    IDEA 開發工具已經集成了 SpringBoot 專案的建立,可以直接使用 IDEA 工具進行快速構建應用

    • File --> Project... 或者是 create New Project,然後就彈出下面的構建先專案的框

    • 配置專案資訊

    • 新增外部依賴

      可以自行選擇新增外部依賴

    • 後面的可以一直next下去

  3. 使用 Maven 建立 SpringBoot 應用

    建立 Maven 工程後,匯入如下依賴

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.1.4.RELEASE</version>
    </parent>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
    </dependencies>
    

構建完SpringBoot工程後,專案結構如下:

2.3 編寫程式程式碼

  • 修改伺服器埠

    修改 resources 目錄下 application.properties 檔案

    # 應用服務 WEB 訪問埠
    server.port=8088
    
  • 編寫Controller

    建立個 Controller 包並建立 HelloController

    @RestController
    public class HelloController {
    
        @RequestMapping("/hello")
        public String hello(){
            return "Hello,SpringBoot!";
        }
    
    }
    
  • 啟動 SpringBoot 應用程式

    啟動後控制檯日誌輸出如下:

    若輸出如下資訊,則代表啟動成功

  • 訪問 /hello 介面

至此,我們的第一個 SpringBoot 工程專案建立完畢

注意事項

  • 使用 IDEA 建立 SpringBoot 的時候,若網路不好或者連線不上官網,可以採用阿里的 SpringBoot 源:http://start.aliyun.com

  • SpringBoot 內建 Tomcat,若需要配置 Tomcat,則需要修改 application.properties 檔案,比如上面的設定 Tomcat 伺服器埠號。Tomcat 啟動失敗,需要注意埠號是否衝突等問題,設定 Tomcat 配置即可解決

  • 所有的程式碼必須放在 SpringBoot 主啟動類同級目錄下或同級目錄的包下,因為 SpringBoot 的 @SpringBootApplication 註解中的 @ComponentScan 註解預設配置的是掃描同級包下的類以及同級包下所有包裡面的類。編寫程式碼時必須得按照這個規範來。且主啟動類必須有如下程式碼

    public static void main(String[] args) {
        // XXXX.class 中的xxx為當前主啟動類的類名,args 是 main 方法中的 args
        SpringApplication.run(Springboot01Application.class, args);
    }
    
  • 主啟動類必須有 @SpringBootApplication 註解

2.4 打包

使用 maven 的package,將專案打成jar包

打包外掛程式碼如下:

<build>
    <plugins>
        <plugin>
            <groupId>org.apache.maven.plugins</groupId>
            <artifactId>maven-compiler-plugin</artifactId>
            <configuration>
                <source>1.8</source>
                <target>1.8</target>
                <encoding>UTF-8</encoding>
            </configuration>
        </plugin>
        <plugin>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-maven-plugin</artifactId>
        </plugin>
    </plugins>
</build>

打包完成後,會在target目錄下生成一個jar包,並會在控制檯輸出如下打包日誌

小彩蛋

在 resources 目錄下建立一個 banner.txt 檔案,可以替換 SpringBoot 的啟動介面。

比如下圖設定了個佛祖的文字影象

文字如下:

////////////////////////////////////////////////////////////////////
//                          _ooOoo_                               //
//                         o8888888o                              //
//                         88" . "88                              //
//                         (| ^_^ |)                              //
//                         O\  =  /O                              //
//                      ____/`---'\____                           //
//                    .'  \\|     |//  `.                         //
//                   /  \\|||  :  |||//  \                        //
//                  /  _||||| -:- |||||-  \                       //
//                  |   | \\\  -  /// |   |                       //
//                  | \_|  ''\---/''  |   |                       //
//                  \  .-\__  `-`  ___/-. /                       //
//                ___`. .'  /--.--\  `. . ___                     //
//              ."" '<  `.___\_<|>_/___.'  >'"".                  //
//            | | :  `- \`.;`\ _ /`;.`/ - ` : | |                 //
//            \  \ `-.   \_ __\ /__ _/   .-` /  /                 //
//      ========`-.____`-.___\_____/___.-`____.-'========         //
//                           `=---='                              //
//      ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^        //
//            佛祖保佑       永不宕機     永無BUG                    //
////////////////////////////////////////////////////////////////////

3. SpringBoot 執行原理

這裡只是簡單瞭解下 SpringBoot 的執行原理,若有錯誤,可以指出,勿噴。

3.1 pom.xml

我們先從 pom.xml 進行追入

從 pom.xml 我們可以發現,SpringBoot 應用中的 pom.xml 是有個父依賴的,父依賴是 spring-boot-starter-parent,主要是管理專案的資源過濾及外掛

<parent>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-parent</artifactId>
    <version>2.3.1.RELEASE</version>
    <relativePath/> <!-- lookup parent from repository -->
</parent>

我們進入這個父依賴,會發現還有個父依賴,父依賴是:spring-boot-dependencies。

這個才是真正管理 SpringBoot 應用裡面所有依賴版本的地方,SpringBoot 的版本控制中心

以後我們匯入依賴預設是不需要寫版本,但是如果匯入的包沒有在依賴中管理著,就需要手動配置版本

<parent>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-dependencies</artifactId>
  <version>2.3.1.RELEASE</version>
</parent>

若是阿里源建立的 SpringBoot 專案,則是使用 dependencyManagement 來管理 SpringBoot 版本依賴,其引入的依賴也是 spring-boot-dependencies

<!-- 依賴版本管理 -->
<dependencyManagement>
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-dependencies</artifactId>
        <version>${spring-boot.version}</version>
        <type>pom</type>
        <scope>import</scope>
    </dependency>
    </dependencies>
</dependencyManagement>

我們匯入了 SpringBoot 的依賴後,會發現依賴名字都有一個共同的規律:artifactId 都是 spring-boot-starter-xxxxx。其中,這些 spring-boot-starter-xxxxx 就是 spring-boot 的場景啟動器

比如:

<dependency>    
    <groupId>org.springframework.boot</groupId>    
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>

我們這麼寫,maven就自動幫我們匯入了 SpringBoot web模組所需要依賴的元件。

SpringBoot 將所有的功能場景都抽取出來,做成一個個的 starter(啟動器)。我們只需要在專案中引入這些 starter ,所有相關的依賴都會匯入進來。我們需要用什麼功能,就匯入什麼樣的場景啟動器即可。我們未來也可以自己自定義 starter。

3.2 主啟動類

pom.xml 幫我們定義好了場景啟動器的版本號,接下來看看 SpringBoot 的啟動類

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

可以發現 SpringBoot 啟動類十分簡單,只有一個註解。但真的是這樣嗎?

我們點進 @SpringBootApplication 這個註解,慢慢追進去,會發現裡面大有乾坤。現在我們就開始一層一層點進去理解。

3.2.1 註解

1. @ComponentScan

中文直譯:“元件掃描”。剛點進 @SpringBootApplication 這個註解,我們最熟悉的就是@ComponentScan這個註解了,它在這裡的作用是掃描主啟動類包下的類或者是主啟動類包下其他包裡面的類,將這些類注入到 Spring 容器中。所以,我們寫的程式碼必須寫在主啟動包下或其包的子包下

這個註解引數加了 @Filter 是為了過濾不符合條件的包下的類

2. @SpringBootConfiguration

中文直譯:“SpringBoot配置”。這個註解加了 @Configuration 註解,說明這個註解是配置類,並且會被 Spring 進行接管。這個註解的作用是 宣告這個主啟動類是 SpringBoot 的配置類,並且交由 SpringBoot 託管(或按本質來說,是 Spring 的配置類,交由 Spring 託管)

3. @EnableAutoConfiguration

中文直譯:“自動匯入配置”。這一個註解才是 @SpringBootApplication 這個註解的核心,或者是說 SpringBoot 自動配置的核心。這裡面有兩個註解:@AutoConfigurationPackage@Import

  • @AutoConfigurationPackage

    中文直譯:“自動配置包”。這個註解是 SpringBoot 自動匯入包的註解。它這個註解裡面還有個 @Import 註解。使用 @Import 註解的原始碼部分如下:

    @Import(AutoConfigurationPackages.Registrar.class)
    

    通過這行程式碼,我們可以知道它是用來註冊被掃描的包下的類到 Spring 容器中的。

    如果有興趣,可以點進 AutoConfigurationPackages 這個類去繼續追原始碼。它裡面有兩個靜態修飾的類 RegistrarBasePackages, 還有一個final 定義的內部類 PackageImportsBasePackages 類是用來獲取被掃描的所有的包下的類,Registrar 類是用來將這些類註冊到 Spring 容器中的。PackageImports 類是匯入包的包裝類,用 String 陣列儲存待註冊到 Spring 容器的包名。

  • @Import(AutoConfigurationImportSelector.class)

    這行註解是匯入 AutoConfigurationImportSelector 這個類,這個類直譯就是:“自動匯入配置選擇器” 。

    如果有興趣,可以自己點進 AutoConfigurationImportSelector 這個類去追,由於能力有限,只能給個追的路線。

    • 方法 getCandidateConfigurations()

      獲取所有的配置

    /**
     * Return the auto-configuration class names that should be considered. By default
     * this method will load candidates using {@link SpringFactoriesLoader} with
     * {@link #getSpringFactoriesLoaderFactoryClass()}.
     * @param metadata the source metadata
     * @param attributes the {@link #getAttributes(AnnotationMetadata) annotation
     * attributes}
     * @return a list of candidate configurations
     */
    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
       List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
             getBeanClassLoader());
       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()

      方法位置如圖所示,然後我們點進這個方法看程式碼

    /**
      * Load the fully qualified class names of factory implementations of the
      * given type from {@value #FACTORIES_RESOURCE_LOCATION}, using the given
      * class loader.
      * @param factoryType the interface or abstract class representing the factory
      * @param classLoader the ClassLoader to use for loading resources; can be
      * {@code null} to use the default
      * @throws IllegalArgumentException if an error occurs while loading factory names
      * @see #loadFactories
      */
    public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
        String factoryTypeName = factoryType.getName();
        return (List)loadSpringFactories(classLoader).getOrDefault(factoryTypeName, Collections.emptyList());
    }
    

    根據註釋第二行 given type from {@value #FACTORIES_RESOURCE_LOCATION}, using the given

    上圖中的 META-INF/spring.factories 在下圖中 這個位 置

    點進 spring.factories ,可以看到很多 Spring 已經幫我們定義好的 jar 包

3.2.2 run方法

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

分析該方法主要分兩部分,一部分是 SpringApplication 的例項化,二是 run 方法的執行;

SpringApplication 這個類主要做了以下四件事情:

  1. 推斷應用的型別是普通的專案還是 Web 專案
  2. 查詢並載入所有可用初始化器,設定到 initializers 屬性中
  3. 找出所有的應用程式監聽器,設定到 listeners 屬性中
  4. 推斷並設定 main 方法的定義類,找到執行的主類

檢視構造器

public SpringApplication(ResourceLoader resourceLoader, Class... primarySources) {    
 this.webApplicationType = WebApplicationType.deduceFromClasspath();    this.setInitializers(this.getSpringFactoriesInstances();    this.setListeners(this.getSpringFactoriesInstances(ApplicationListener.class));    this.mainApplicationClass = this.deduceMainApplicationClass();                        }

run方法流程分析

(圖引自狂神說公眾號)

4. yaml 配置注入

SpringBoot 官方提供了兩種配置檔案:properties 和 yaml(推薦)

SpringBoot使用一個全域性的配置檔案,配置檔名稱是固定的:

  • application.properties

    語法結構:key=value (中間不能有空格)

  • application.yml

    語法結構:key:空格 value (yml檔案的配置必須有空格,且yml對空格十分敏感)

配置檔案的作用:修改 SpringBoot 自動配置的預設值,因為 SpringBoot 在底層都給我們自動配置好了。

比如我們使用配置檔案修改 SpringBoot 內建的 Tomcat 伺服器預設啟動的埠號

application.properties:

# 應用服務 WEB 訪問埠
server.port=8088

application.yml:

# port:後面有一個空格
server:
  port: 8088

4.1 yaml 簡介

YAML 是一個可讀性高,用來表達資料庫序列化的格式。YAML參考了其他多種語言,包括:C語言、Python、Perl,並從XML、電子右鍵的資料格式(RFC 2822)中獲得靈感。Clark Evans 在2001年首次發表了這種語言。

YAML是“YAML Ain't a Markuo Language”(YAML 不是一種標記語言)的遞迴縮寫。在開發這種語言時,YAML的意思其實是“Yet Another Markup Language”(仍然時一種標記語言),但為了強調這種語言以資料作為中心,而不是以標記語言為重點,而用反向縮略語重新命名。

4.2 yaml 基礎語法

說明:yaml 語法要求嚴格

  1. 空格不能省略。
  2. 以縮排來控制層級關係
  3. 屬性和值的大小寫都是十分敏感的。
  4. 縮排長度沒有限制,只要元素左邊對齊就能表示同一層級
  5. 使用#表示註釋
  6. 字串可以不用引號標註
  • 字面量:普通的值 [數字,布林值,字串]

    直面兩直接寫在後面就可以,字串預設不用加上雙引號或者單引號

    # 注意中間一定要有空格
    key: val
    

    注意:

    1. "" 雙引號,不會轉義字串裡面的特殊字元,特殊字元會作為本身想表示的意思

      比如:name: "xp \n com" 輸出 xp 換行 com

    2. '' 單引號,會轉義特殊字元,特殊字元最終會變成和普通字元一樣輸出

      比如:name: 'xp \n com' 輸出: xp \n com

  • 物件、Map(鍵值對)

    # 物件、Map格式
    key:
    	val1:
    	val2:
    

    在下一行來寫物件的屬性和值的關係,注意縮排,比如:

    student: 
    	name: xp
    	age: 18
    

    行內寫法:

    student: {name: xp,age: 18}
    
  • 陣列(List、Set)

    用 - 值表示陣列中的一個元素,比如:

    pets:
    	- cat
    	- dog
    	- pig
    

    行內寫法:

    pets: [cat,dog,pig]
    

4.3 注入配置檔案

yaml 檔案更強大的地方在於,它可以給我們的實體類直接注入匹配值

4.3.1 兩種配置bean的方式

以前配置bean的方式配置:

  1. 建立一個實體類 Cat,並使用 @value 注入屬性值

    @Component
    public class Cat {
    
        @Value("miao")
        private String name;
        @Value("18")
        private Integer age;
    
        public Cat() {
        }
    
        public Cat(String name, Integer age) {
            this.name = name;
            this.age = age;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public Integer getAge() {
            return age;
        }
    
        public void setAge(Integer age) {
            this.age = age;
        }
    
        @Override
        public String toString() {
            return "Cat{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
    
  2. 在 SpringBoot 的測試類中編寫測試程式碼

    @SpringBootTest
    class Springboot01ApplicationTests {
    
        @Autowired
        private Cat cat;
    
        @Test
        void contextLoads() {
            System.out.println(cat);
        }
    
    }
    
  3. 點選執行測試

使用yaml配置bean

  1. 建立實體類,在實體類上加上 @ConfigurationProperties 註解,並配置prefix。其中prefix的值就是yaml檔案中配置的bean的名

    @Component
    @ConfigurationProperties(prefix = "cat")
    public class Cat {
    
    //    @Value("miao")
        private String name;
    //    @Value("18")
        private Integer age;
    
        public Cat() {
        }
    
        public Cat(String name, Integer age) {
            this.name = name;
            this.age = age;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public Integer getAge() {
            return age;
        }
    
        public void setAge(Integer age) {
            this.age = age;
        }
    
        @Override
        public String toString() {
            return "Cat{" +
                    "name='" + name + '\'' +
                    ", age=" + age +
                    '}';
        }
    }
    
  2. 在 resource 目錄下建立 application.yml 檔案,並編寫 bean

    cat:
      name: miao1
      age: 17
    
  3. 點選執行測試

如果idea 報瞭如下錯誤

Spring Boot Configuration Annotation Processor not found in classpath

只需在 pom.xml 中引入如下依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-configuration-processor</artifactId>
    <optional>true</optional>
</dependency>

看到這裡可能發現好像yaml來注入bean好像並沒有什麼優勢,那麼我們再接下來看看 @value 和 @ConfigurationProperties 兩個的區別

4.3.2 @ConfigurationProperties 和 @value

回顧properties配置

我們上面採用的yaml方法都是最簡單的方式,開發中最常用的,也是SpringBoot所推薦的!

配置檔案中,除了yaml檔案,還有properties檔案。

先建立配置檔案 cat.properties

cat.name=miao
cat.age=3

在原來的實體類中修改註解

@Component
@PropertySource(value = "classpath:cat.properties")
public class Cat {
    @Value("${cat.name}")
    private String name;
    @Value("${cat.age}")
    private Integer age;
}

使用@ConfigurationProperties

鬆散繫結

我們在yml中寫 cat-name 時,效果和catName是一樣的

封裝物件

像之前在yml中定義的cat,就是一個物件,而 @value無法封裝物件

對比小結

@ConfigurationProperties @Value
功能 批量注入配置檔案中的屬性 一個個指定
鬆散繫結(鬆散語法) 支援 不支援
SpEL 不支援 支援
JSR303資料校驗 支援 不支援
複雜型別封裝 支援 不支援
  1. @ConfigurationProperties 只需要寫一次即可,@Value 則需要每個欄位都新增
  2. 鬆散繫結,在yml中寫 cat-name 時,效果和catName是一樣的。- 後面跟著的字母預設是大寫的。這就是鬆散繫結。
  3. JSR303資料校驗,這個就是我們在欄位裡增加一層過濾器,可以保證資料的合法性。
  4. 複雜型別封裝,yml可以封裝物件,使用@Value不支援

結論

配置yml和配置properties都可以獲取到值,推薦yml。

如果我們在某個業務中,只需要獲取配置檔案中某個值,可以使用以下@Value

如果說我們專門編寫了一個JavaBean來和配置檔案一一對映,就直接使用@ConfigurationProperties

5. JSR303 資料校驗及多環境切換

5.1 JSR303 簡介

JSR 是 Java Specification Requests 的縮寫,意思是 Java 規範提案。是指向 JCP(Java Community Process)提出新增一個標準化技術規範的正式請求。任何人都可以提交 JSR,以向 Java 平臺增添新的API和服務。JSR 已成為 Java 界的一個重要的標準。

JSR-303 是 JAVA EE 6 中的一項子規範,叫做 Bean Validation,Hibernate Validator 是 Bean Validation 的參考實現, Hibernate Validator 提供了 JSR 303 規範中所有內建 constraint 的實現,除此之外還有一些附加的 constraint。

5.2 常見引數

空檢查

註解 作用
@Null 驗證物件是否為null。
@NotNull 驗證物件是否不為null, 無法查檢長度為0的字串。
@NotBlank 檢查約束字串是不是Null還有被Trim的長度是否大於0,只對字串,且會去掉前後空格.
@NotEmpty 檢查約束元素是否為NULL或者是EMPTY.

Boolean 檢查

註解 作用
@AssertTrue 驗證 Boolean 物件是否為 true
@AssertFalse 驗證 Boolean 物件是否為 false

長度檢查

註解 作用
@Size(min=, max=) 驗證物件(Array,Collection,Map,String)長度是否在給定的範圍之內。
@Length(min=,max=) 驗證 String 的長度是否在 min 和 max 之間

日期檢查

註解 作用
@Past 驗證 Date 和 Calendar 物件是否在當前時間之前
@Future 驗證 Date 和 Calendar 物件是否在當前時間之後
@Pattern 驗證 String 物件是否符合正則表示式的規則

數值檢查

建議使用在 String,Integer 型別,不建議使用在 int 型別上,因為表單值為 "" 時無法轉換為 int,但可以轉換為 String 為 "", Integer 為 null

註解 作用
@Min 驗證 Number 和 String 物件是否大於等於指定的值。
@Max 驗證 Number 和 String 物件是否小於等於指定的值。
@DecimalMax 被標註的值必須不大於約束中指定的最大值,這個約束的引數時一個通過BigDecimal 定義的最大值的字串表示,小輸存在精度。
@DecimalMin 被標註的值必須不大於約束中指定的最大值,這個約束的引數時一個通過BigDecimal 定義的最小值的字串表示,小輸存在精度。
@Digits 驗證 Number 和 String 的狗成是否合法。
@Digits(integer=,fraction=) 驗證字串是否符合指定格式的數字,interger指定整數,fraction 指定小輸精度。
@Range(min=,max=) 檢查數字是否介於 min 和 max 之間

其他檢查

註解 作用
@Valid 遞迴的對關聯物件進行校驗。如果關聯物件是個集合或者陣列,那麼對其中的元素進行遞迴校驗,如果時一個map,則對其中的值部分進行校驗。(是否進行遞迴驗證)
@CreditCardNumber 信用卡驗證
@Email 驗證是否是郵件地址,如果為 null,不進行驗證,算通過驗證。
@ScriptAssert(lang=,script=,alias=)
@URL(protocol=,host=,port=,regexp=,flags=) 驗證URL 是否正確

5.3 簡單使用

  1. 匯入 SpringBoot validation 啟動器

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
    
  2. 在原來 Cat 的實體類上加上校驗註解

    注:這裡必須加上 @Validated 註解才能讓 jsr303 校驗的註解生效

    @Component
    @ConfigurationProperties(prefix = "cat")
    @Validated  // 校驗註解,只有加上這個註解才能進行 jsr303 校驗
    public class Cat {
    	// @NotBlank 檢查約束字串是不是Null還有被Trim的長度是否大於0,只對字串,且會去掉前後空格.
        @NotBlank(message = "名字不能為空")
        private String name;
        // @Range(min=,max=) 檢查數值是否在min和max之間
        @Range(min = 0,max = 150,message = "年齡必須在0-150之間")
        private Integer age;
    }
    
  3. 修改 yml 中配置的bean

    cat:
      name:
      age: -1
    
  4. 執行測試類

    @SpringBootTest
    class Springboot01ApplicationTests {
        @Autowired
        private Cat cat;
    
        @Test
        void contextLoads() {
            System.out.println(cat);
        }
    }
    
  5. 執行結果如下

5.4 注意和小結

  • 使用 JSR303 校驗,必須加上 @Validated 註解,不然將不會進行校驗
  • 需要匯入 SpringBoot validation 啟動器 的依賴,不然將不能使用這些註解進行校驗,且會爆紅
  • 合理使用 JSR303 校驗,可以簡化開發,減少 if else 的使用,更加輕便,使程式碼更加簡潔。

6. 多環境切換

6.1 多配置檔案

profile 是 Spring 對不同環境提供不同配置功能的支援,可以通過啟用不同的環境半年本,實現快速切換環境。

我們在主配置檔案編寫的時候,檔名可以是 application-{profile}.properties/yml ,用來指定多個環境版本。

例如:

application-test.properties 代表測試環境配置

application-dev.properties 代表開發環境配置

但是 SpringBoot 並不回直接啟動這些配置檔案,它預設使用 application.properties/yml 主配置檔案

啟用環境

我們需要通過一個配置來選擇需要啟用的環境:

這裡我們測試 Tomcat 伺服器埠號的配置

application.yml

spring:
  profiles:
    active: dev # 配置環境的名稱 
server:
  port: 8082

然後我們建立一個 application-dev.yml

application-dev.yml

server:
  port: 8083

再然後我們啟動 SpringBoot 應用程式檢視 SpringBoot 內建Tomcat啟動的埠號

發現 Tomcat 是從8083埠啟動,說明我們切換環境成功

注意:如果yml和properties同時都配置了埠,並且沒有啟用其他環境 , 預設會使用properties配置檔案的!

6.2 配置檔案載入位置

6.2.1 配置檔案載入位置和載入順序

外部載入配置檔案的方式十分多,我們選擇最常用的即可,在開發的資原始檔中進行配置

在官方外部配置檔案說明參考文件 https://docs.spring.io/spring-boot/docs/2.1.3.RELEASE/reference/htmlsingle/#boot-features-external-config-typesafe-configuration-properties 24.3 Application Property Files 中有明確說明配置檔案的載入位置和載入順序,具體如下圖:

配置位置以相反的順序進行搜尋,配置位置是:classpath:/,classpath:/config/,file:./,file:./config/ 。搜尋的順序如下:

  1. file:./config/
  2. file:./
  3. classpath:/config/
  4. classpath:/

SpringBoot 啟動會掃描以下位置的 application.properties 或者是 application.yml 檔案作為 SpringBoot 的預設配置檔案:

優先順序1:專案路徑下的 config 包下的配置檔案
優先順序2:專案路徑下的配置檔案
優先順序3:資源路徑下的 config 包下的配置檔案
優先順序4:資源路徑下的配置檔案

優先順序由高到低,高優先順序的配置會覆蓋低優先順序的配置;

SpringBoot 會從這四個位置全部載入主配置檔案,互補配置

6.2.2 測試

我們分別在專案路徑下建立config包並在該目錄下建立 application.yml,專案根目錄下建立 application.yml,resource 目錄下建立 application.yml,reource 目錄下建立 config 包並該目錄下建立 application.yml

建立後目錄及其配置檔案位置如下:

在這4個 application.yml 中,都配置 Tomcat 啟動埠,且每個埠號都不同,測試SpringBoot 的 Tomcat 啟動哪個埠來確定這些包下的配置檔案 application.yml 的優先順序

server:
  port: 8081
server:
  port: 8082
server:
  port: 8083
server:
  port: 8084

7. 自動配置原理

SpringBoot 官方文件中由大量的配置,我們無法全部記住。官方文件配置:https://docs.spring.io/spring-boot/docs/2.4.0-SNAPSHOT/reference/html/appendix-application-properties.html#common-application-properties

7.1 分析自動配置原理

我們就以 HttpEncodingAutoConfiguration 這個類來分析自動配置原理

// @Configuration 表明這是一個配置類
@Configuration(proxyBeanMethods = false)
// 啟動指定類 ServerProperties 的功能
// 點進這個類,我們會發現裡面的成員變數對應的是我們配置檔案中的server下的配置,比如 server.port=8080,
// 裡面有個註解 @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true),它的作用是將配置檔案中"server"字首對應的值和 ServerProperties 的成員變數繫結起來,並加入到 iOC 容器中
@EnableConfigurationProperties(ServerProperties.class)
// 判斷當前專案是不是web應用程式,如果是,則當前配置類生效,否則不生效
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)
// 判斷當前專案是否有 CharacterEncodingFilter(SpringMVC進行亂碼 解讀的過濾器) 這個類,如果有,則當前配置類生效,否則不生效
@ConditionalOnClass(CharacterEncodingFilter.class)
// 判斷是否手動配置了server.servlet.encoding (設定編碼),若手動配置了,則使用手動配置的值,否則使用預設值
@ConditionalOnProperty(prefix = "server.servlet.encoding", value = "enabled", matchIfMissing = true)
public class HttpEncodingAutoConfiguration {
	// 它已經在 SpringBoot 的配置檔案映射了
   private final Encoding properties;
	// 只有一個有參構造器的情況下,引數的值就會從容器中拿
   public HttpEncodingAutoConfiguration(ServerProperties properties) {
      this.properties = properties.getServlet().getEncoding();
   }

    // 給容器中新增一個元件,這個元件的某些值需要從上面定義的 final Encoding properties中獲取
   @Bean
    // 如果當前IOC容器中已經有這個 Bean 了,則不注入,否則將這個Bean注入到IOC容器中
   @ConditionalOnMissingBean
   public CharacterEncodingFilter characterEncodingFilter() {
      CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
      filter.setEncoding(this.properties.getCharset().name());
      filter.setForceRequestEncoding(this.properties.shouldForce(Encoding.Type.REQUEST));
      filter.setForceResponseEncoding(this.properties.shouldForce(Encoding.Type.RESPONSE));
      return filter;
   }
}
  • @Configuration(proxyBeanMethods = false)

    標明這個類是配置類

  • @EnableConfigurationProperties(ServerProperties.class)

    啟動指定類 ServerProperties 的功能

    如果我們點進 ServerProperties 這個類,我們會發現裡面的成員變數對應的是我們配置檔案中的server下的配置,比如 server.port=8080,或

    server:
      port: 8083
    

    ServerProperties 類如下

    @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
    public class ServerProperties {
       /**
        * Server HTTP port.
        */
       private Integer port;
    }
    

    它有一個註解 @ConfigurationProperties(prefix = "server", ignoreUnknownFields = true) ,它的作用是將配置檔案中"server"字首對應的值和 ServerProperties 的成員變數繫結起來,並加入到 IOC 容器中。

    這裡的 port 就是我們配置檔案中可以配置的屬性上面已經有示例和解釋了。

    server:
      port: 8083
    
  • @ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET)

    判斷當前專案是不是web應用程式,如果是,則當前配置類生效,否則不生效

  • @ConditionalOnClass(CharacterEncodingFilter.class)

    判斷當前專案是否有 CharacterEncodingFilter (SpringMVC進行亂碼解讀的過濾器) 這個類,如果有,則當前配置類生效,否則不生效

  • @ConditionalOnProperty(prefix = "server.servlet.encoding", value = "enabled", matchIfMissing = true)

    判斷是否手動配置了server.servlet.encoding (設定編碼),若手動配置了,則使用手動配置的值,否則使用預設值

  • @ConditionalOnMissingBean

    如果當前IOC容器中已經有這個 Bean 了,則不注入,否則將這個Bean注入到 IOC 容器中

也就是說,根據當前不同的條件判斷,決定這個配置類是否生效

  • 一旦這個配置類生效,這個配置類就會給容器中新增各種元件。
  • 這些元件的屬性是從對應的 properties 類中獲取的,這些類裡面的每一個屬性又是和配置檔案繫結的。
  • 所有的配置檔案中能配置的屬性都是 xxxxProperties 類中封裝著。
  • 配置檔案能配置什麼就可以參照某個功能對應的這個屬性類

這就是自動裝配的原理

7.2 使用自動裝配的注意事項

  1. SpringBoot 啟動會載入大量的自動裝配類。

  2. 我們看我們需要的功能有沒有在 SpringBoot 預設寫好的自動裝配類當中。

  3. 我們再來看這個自動配置類中到底配置了哪些元件。(只要我們要用的元件存在其中,我們就不需要再手動配置了)

  4. 給容器中自動配置類新增元件的時候,會從 properties 類中獲取某些屬性。我們只需要再配置檔案中指定這些屬性的值即可。

    xxxxxAutoConfigurartion:自動配置類,給容器中新增元件

    xxxxxProperties:封裝配置檔案中相關屬性

7.3 @Conditional

我們剛剛在分析 HttpEncodingAutoConfiguration 這個類的時候,我們會發發現,很多註解的底層都有 @Conditional 及以這個開頭的註解

@Conditional 是Spring4新提供的註解,它的作用是按照一定的條件進行判斷,滿足條件給容器註冊bean。

我們看其原始碼:

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Conditional {
    Class<? extends Condition>[] value();
}

分析其原始碼,發現這個註解可以標註在類和方法上,並且有個 value 屬性,可以傳入 Class 物件陣列,這些 Class 物件,必須繼承 Condition 介面。

我們來看看 Condition 的原始碼

@FunctionalInterface
public interface Condition {
    boolean matches(ConditionContext var1, AnnotatedTypeMetadata var2);
}

實現 Condition 介面,就必須實現 matches 方法。這個方法返回 true,則將 bean 注入到 IOC 容器中,返回 false,則不注入

到這裡,我們就更加能夠理解它自動裝配的原理了:**@Conditional 的派生註解,實現了Condition 介面,重寫了 matches 方法,重寫這個方法的時候,規定了新增這個註解後類或方法需要達到的條件。SpringBoot 的自動裝配類,都添加了 @Conditional 註解,SpringBoot 自動裝配時,根據該自動裝配類的註解裡 matches 方法裡的條件判斷,決定這個配置類是否生效 **。

用我們比較好聽懂的話來說,就是 只有達到自動裝配類中註解裡的 matches 方法的條件,該自動裝配類才會自動裝配

現在我們再來看看 @Conditional 的派生註解

@Conditional 擴充套件註解 作用(判斷是否滿足當前指定條件)
@ConditionalOnJava 系統的 Java 版本是否符合條件
@ConditionalOnBean 容器中存在指定 Bean
@ConditionalOnMissingBean 容器中不存在指定 Bean
@ConditionalOnExpression 滿足 SpEL 表示式指定
@ConditionalOnClass 系統中有指定的類
@ConditionalOnMissingClass 系統中沒有指定的類
@ConditionalOnSingleCandidate 容器中只有一個指定的Bean,或者這個 Bean是首選 Bean
@ConditionalOnProperty 系統中指定的屬性是否有指定的值
@ConditionalOnResource 類路徑下是否存在指定資原始檔
@ConditionalOnWebApplication 當前是 web 環境
@ConditionalOnNotWebApplication 當前不是 web 環境
@ConditionalOnJndi JNDI 存在指定項

那麼多的配置類,必須在一定的條件下才能生效。也就是說,我們載入了這麼多的配置類,但不是所有的都生效了

那我們怎麼知道哪些自動配置類生效呢?

我們只需要在配置檔案中啟用 debug=true 屬性,來讓控制檯列印自動配置報告,這樣我們就可以很方便的知道哪些自動配置類生效

debug:
  true

Positive matches:(自動配置類啟用的:正匹配)

Negative matches:(沒有啟動,沒有匹配成功的自動配置類:負匹配)

Unconditional classes: (沒有條件的類)

7.4 小結

SpringBoot 所有自動裝配都是在啟動的時候掃描並載入的: spring.factories 所有的自動裝配類都在這裡,但是不一定生效,要判斷條件是否成立,只要匯入對應的 starter ,就有對應的啟動器了,有了啟動器,我們自動裝配就會生效。

  1. SpringBoot 在啟動的時候,從類路徑下 /META-INF/spring.factories 獲取指定的值
  2. 將這些自動配置的類匯入容器,自動配置就會生效,幫我們進行自動裝配
  3. 以前我們需要自動配置的東西,限制 SpringBoot 幫我們做了
  4. 整合 JavaEE,解決方法和自動配置的東西都在 spring-boot-autoconfigure-2,2,0,RELEASE.jar 這個包下
  5. 它會把所有需要匯入的元件,以類命的方式返回,這些元件就會被新增到容器
  6. 容器中也會存在非常多的 xxxAutoConfiguration 的檔案,就是這些類給容器中匯入了這個場景需要的所有元件

8. 自定義 Starter

我們知道了 SpringBoot 的自動裝配原理,那麼我們也可以嘗試自定義一個啟動器來玩玩!

8.1 說明

啟動器是一個空 jar 檔案,僅提供輔助性依賴管理,這些依賴有可能用於自動裝配或者其他類庫。

命名規約

官方命名:

  • 字首:spring-boot-starter-xxx
  • 比如:spring-boot-starter-web

自定義命名:

  • 字尾:xxx-spring-boot-starter-xxx
  • 比如:mybatis-spring-boot-starter

8.2 編寫啟動器

  1. 首先我們先建立兩個工程專案,一個是我們編寫自己diy的啟動類的專案和一個測試的專案,專案目錄如下

  1. 建立 HelloProperties

    根據我們的原始碼,可以知道 xxxProperties 是存放配置資訊的,所以我們先建立一個 HelloProperties

    package com.xp;
    
    import org.springframework.boot.context.properties.ConfigurationProperties;
    
    @ConfigurationProperties(prefix = "hello.msg")
    public class HelloProperties {
    
        private String prefix;
    
        private String suffix;
    
        public String getPrefix() {
            return prefix;
        }
    
        public void setPrefix(String prefix) {
            this.prefix = prefix;
        }
    
        public String getSuffix() {
            return suffix;
        }
    
        public void setSuffix(String suffix) {
            this.suffix = suffix;
        }
    }
    
  2. 建立一個 HelloService

    建立一個 HelloService,用來儲存我們啟動器類的方法

    public class HelloService {
    
        HelloProperties helloProperties;
    
        public HelloService(HelloProperties helloProperties) {
            this.helloProperties = helloProperties;
        }
    
        public HelloProperties getHelloProperties() {
            return helloProperties;
        }
    
        public void setHelloProperties(HelloProperties helloProperties) {
            this.helloProperties = helloProperties;
        }
    
        public String sayHello(String msg){
            return helloProperties.getPrefix()+msg+helloProperties.getSuffix();
        }
    
    }
    
  3. 編寫自動配置類 HelloAutoConfiguration

    SpringBoot 啟動器的核心就是自動配置類,它將判斷這個啟動類是否生效以及幫我們自動配置

    package com.xp;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
    import org.springframework.boot.context.properties.EnableConfigurationProperties;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    
    @Configuration
    @ConditionalOnWebApplication
    @EnableConfigurationProperties(HelloProperties.class)
    public class HelloAutoConfiguration {
    
        @Autowired
        private HelloProperties helloProperties;
    
        @Bean
        public HelloService helloService(){
            return new HelloService(helloProperties);
        }
    
    }
    
  4. 配置 spring.factories

    之前我們分析 SpringBoot 執行原理的時候發現,SpringBoot 應用程式啟動的時候會自動掃描 **META-INF\ ** 目錄下的 spring.factories 。所以我們需要在 resource 目錄下建立個 META-INF 目錄,並在該目錄下建立 spring.factories 並配置我們這個啟動類

    # 開啟我們自己diy的自動配置類
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    com.xp.HelloAutoConfiguration
    
  5. 將我們自己 diy 的啟動器打成jar包,安裝到 Maven 中

  6. 在我們的測試類中引入剛剛我們自定義的啟動器的依賴

    <!-- 引入我們自己diy的啟動器包 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-autoconfigure</artifactId>
    </dependency>
    
  7. 編寫 Controller

    啟動器已經配置好了,編寫 Controller 介面來測試我們的啟動器是否編寫成功

    @RestController
    public class HelloController {
    
        @Autowired
        private HelloService helloService;
    
        @RequestMapping("/hello")
        public String hello() {
            return  helloService.sayHello("hello");
        }
    
    }
    
  8. 在 application.yml/properties 中配置我們剛剛的啟動類

    # 伺服器啟動埠
    server:
      port: 8080
    # 自定義啟動器的設定
    hello:
      msg:
        prefix: 這是字首
        suffix: 這是字尾
    
  9. 測試

    啟動 SpringBoot 專案,在瀏覽器輸入 URL 進行訪問剛剛寫好的介面

到這裡,我們自定義的 Starter 就建立好了

9. 整合 JDBC

9.1 SpringData 簡介

對於資料訪問層,無論是 SQL(關係型資料庫)還是NOSQL(非關係型資料庫),SpringBoot 底層都是採用 Spring Data 的方式進行統一處理。

SpringBoot 底層都是採用 Spring Data 的方式進行統一處理各種資料庫,Spring Data 也是 Spring 中與SpringBoot、SpringCloud 等齊名的知名專案。

SpringData 官網:https://spring.io/projects/spring-data#learn

資料庫相關的啟動器,可以參考官方文件:https://docs.spring.io/spring-boot/docs/2.4.0-SNAPSHOT/reference/htmlsingle/#using-boot-starter

9.2 整合 JDBC

9.2.1 JDBC 簡單測試

  1. 建立 SpringBoot 專案,引入 JDBC API 模組

    若沒有在建立專案時引入,則需要自己手動引入 spring-boot-starter-jdbc (SpringBoot JDBC啟動器)

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-jdbc</artifactId>
    </dependency>
    
  2. 在 application.yml/properties 中配置資料來源

    spring:
      datasource:
        driver-class-name: com.mysql.jdbc.Driver
        url: jdbc:mysql://localhost:3306/study?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
        password: root
        username: root
    
  3. 編寫測試類

    @SpringBootTest
    class Springboot01ApplicationTests {
        @Autowired
        DataSource dataSource;
    
        @Test
        void contextLoads() {
            try {
                // 獲取資料庫連線
                Connection connection = dataSource.getConnection();
                // 編譯SQL語句
                PreparedStatement ppstm = connection.prepareStatement("select * from user");
                // 獲得結果集並迴圈輸出
                ResultSet rs = ppstm.executeQuery();
                while (rs.next()){
                    System.out.println(rs.getString("name"));
                }
                System.out.println(connection);
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    
    }
    
  4. 分析 JDBC 自動配置原理

    在控制檯中,connection 物件輸出的資訊是 HikariProxyConnection@215638041 wrapping com.mysql.cj.jdbc.ConnectionImpl@797c3c3b 。但我們並沒有手動配置這個類。

    那我們就去全域性搜尋 DataSourceAutoConfiguration 這個類去檢視資料來源是如何自動配置的。下面是關於資料庫連線池的配置

    @Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
          DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.Generic.class,
          DataSourceJmxConfiguration.class })
    protected static class PooledDataSourceConfiguration {
    
    }
    

    這裡匯入的類都在 DataSourceConfiguration配置類下,可以看出 SpringBoot 2.3.0預設使用 HikanDataSource 資料來源,而以前的版本,如:SpringBoot 1.5預設使用 org.apache.tomcat.jdbc.pool.DataSource 作為資料來源。

    HikariDataSource 號稱 JavaWeb 當前速度最快的資料來源,相比於傳統的 C3P0、DBCP、Tomcat jdbc 等連線池更加優秀

    可以使用 spring.datasource.type 指定自定義的資料來源型別,值為 要使用的連線池實現的完全限定名。

9.2.2 JdbcTemplate

  • 有了資料來源(com.zaxxer.hikari.HikariDataSource),然後可以拿到資料庫連線(java.sql.Connection),有了連線,就可以使用原生的 JDBC 語句來操作資料庫。
  • 即使不使用第三方資料庫操作框架,如 MyBatis 等, Spring 本身也對原生的 JDBC 做了輕量級的封裝,即 JdbcTemplate。
  • 資料庫操作的所有 CRUD 方法都在 JdbcTemplate中。
  • SpringBoot 不僅提供了預設的資料來源,同時預設已經配置好了 JdbcTemplate 放在了容器中,程式設計師只需自己注入即可使用。
  • JdbcTemplate 的自動配置依賴 org.springframework.boot.autoconfigure.jdbc 包下的 JdbcTemplateConfiguration 類

JdbcTemplate 主要提供了以下幾類方法:

  • execute 方法:可以用於執行任何 SQL 語句,一般用於執行 DDL 語句。
  • update 方法及 batchUpdate 方法:update方法用於執行新增、修改、刪除等語句;batchUpdate方法用於執行批處理相關語句。
  • query方法及queryForxxx方法:用於執行查詢相關語句。
  • call 方法:用於執行儲存過程、函式相關語句。

測試

@RestController
public class JdbcTemplateController {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @RequestMapping("/query")
    public List<Map<String, Object>> query(){
        String sql = "select * from user";
        List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
        return maps;
    }

    @RequestMapping("/update")
    public String update(){
        String sql = "update user set name=?,password=? where id = ?";
        jdbcTemplate.update(sql,"123","123","6");
        return "update ok!";
    }


    @RequestMapping("/insert")
    public String insert(){
        String sql = "insert into user (name,password,hobby) values(?,?,?)";
        jdbcTemplate.update(sql,"xp","xp","敲程式碼");
        return "insert ok!";
    }

    @RequestMapping("/delete")
    public String delete(){
        String sql = "delete from user where id = ?";
        jdbcTemplate.update(sql,6);
        return "delete ok!";
    }
}

到這裡 JdbcTemplate 就實現了。免去了我們自己寫工具類的麻煩。

10. 整合 Druid

10.1 Druid 簡介

Java 程式很大一部分要操作資料庫,為了提高效能操作資料庫的時候,又不得不使用資料庫連線池。

Druid 是阿里巴巴開源平臺上一個資料庫連線池實現,結合了 C3P0、DBCP 等 DB 池的優點,同時加入了日誌監控。

Druid 可以很好的監控 DB 池連線和 SQL 的執行情況,天生就是針對監控而生的 DB 連線池。

Druid 已經在阿里巴巴部署了超過600個應用,經過一年多生產環境大規模部署的嚴苛考研。

SpringBoot 2.0 以上預設使用 Hikari 資料來源,可以說 Hikari 和 Druid 都是當前 Java Web 上最優秀的資料來源,我們來重點介紹 SpringBoot 如何整合 Druid 資料來源,如何實現資料庫監控。

Druid GitHub官網: https://github.com/alibaba/druid/

配置 預設值 說明
name 配置這個屬性的意義在於,如果存在多歌資料來源,監控的時候可以通過名字來取分開來。如果沒有配置,將會生成一個名字,格式是:“DataSource-” +System.identityHashCode(this)。
url 連線資料庫的url,不同資料庫不一樣,例如:mysql:jdbc:mysql://10.20.153.104:3306/druid2 oracle:jdbc:oracle:thin:@10.20.149.85:1521:ocnauto
username 連線資料庫的使用者名稱
password 連線資料庫的密碼/如果你不希望密碼直接寫在配置檔案中,可以使用 ConfigFilter
driverClassName 根據 url自動識別 這一項可配可不配,如果不配置 druid 會根據 url 自動識別 dbType ,然後選擇響應的 driverClassName
initialSize 0 初始化時簡歷物理連線的個數。初始化發生在顯示呼叫init方法,或者第一次getConnection時
maxActive 8 最大連線池數量
maxldle 8 已經不再使用,配置了也沒效果
minldle 最小連線池數量
maxWait 獲取連線時最大等待時間,單位毫秒。配置了maxWait之後,預設啟用公平鎖,併發效率會有所下降,如果需要可以通過配置 userUnfairLock 屬性為 true 使用非公平鎖。
poolPreparedStatements false 是否快取 preparedStatement ,就就是說 PSCache。PSCache 對支援遊標的資料庫效能提升巨大,比如說 oracle。在 mysql 下建議關閉。
maxOpenPreparedStatements -1 要啟用PSCache,必須配置大於0,當大於0時,poolPreparedStatements自動觸發修改為 true。在Druid 中不回存在 Oracle 下 PSCache 佔用記憶體過多的問題,可以把這個數值配置大一些,比如說100
vvalidationQuery 用來檢測連線是否有效的sql,要求是一個查詢語句。如果 validationQuery 為 null,testOnBorrow、testOnReturn、testWhileldle 都不回起做用
validationQueryTimeout 單位:秒,檢測連線是否有效的超時時間。底層呼叫jdbc Statement 物件的 void setQueryTimeout(int seconds) 方法
testOnBorrow true 申請連線時執行 validationQuery 檢測連線是否有效,做了這個配置會降低效能。
testOnRetrun false 歸還連線時執行 validationQuery 檢測連線是否有效,做了這個配置會降低效能
testWhileldle false 建議配置為 true,不影響效能,並且保證安全性。申請連線的時候檢測,如果空閒時間大於 timeBetweenEvictionRunsMillis,執行 validationQuery 檢測連線是否有效。
timeBetweenEvictionRunsMillis 一分鐘(1.0.14) 有兩個含義:1、Destroy執行緒會檢測連線的間隔時間,如果連線空閒時間大於等於minEvictableldleTimeMillis 則關閉物理連線。2、testWhileldle 的判斷依據,詳細看 testWhileldle 屬性的說明
numTestsPerEvictionRun 不再使用,一個 DruidDataSource 只支援一個EvictionRun
minEvictableldleTimeMillis 30分鐘(1.0.14) 連線保持空閒而不被驅逐的最長時間
connectionInitSqls 物理連線初始化的時候執行的sql
exceptionSorter 根據dbType自動識別 當資料庫丟擲一些不可恢復的異常時,拋棄連線
filters 屬性型別是字串,通過別名的方式配置擴充套件外掛,常用的外掛有:監控統計用的filter:stat 日誌用的 filter:log4j 防禦 sql 注入的filter:wall
proxyFilters 型別是List<com.alibaba.druid.filter.Filter>,如果同時配置了 filters 和 proxyFilters,是組合關係,並非替換關係

10.2 配置資料來源

  1. 引入依賴

    <dependency>    
        <groupId>com.alibaba</groupId>    
        <artifactId>druid</artifactId>    
        <version>1.1.21</version>
    </dependency>
    
  2. 切換資料來源

    SpringBoot 2.0 以上預設使用 com.zaxxer.hikari.HikariDataSource 資料來源,但可以通過 spring.datasource.type 指定資料來源

    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/study?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
        password: root
        username: root
        # 切換成 Druid 資料來源
        type: com.alibaba.druid.pool.DruidDataSource
    
  3. 測試資料來源是否已經成功切換

    使用之前的測試類進行測試

    @SpringBootTest
    class Springboot01ApplicationTests {
        @Autowired
        DataSource dataSource;
    
        @Test
        void contextLoads() {
            try {
                Connection connection = dataSource.getConnection();
                PreparedStatement ppstm = connection.prepareStatement("select * from user");
                ResultSet rs = ppstm.executeQuery();
                while (rs.next()){
                    System.out.println(rs.getString("name"));
                }
                System.out.println(connection);
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    
    }
    

    在控制檯中檢視 connection 中,是否已經切換成 Druid 資料來源

  4. 配置 Druid 資料來源引數

    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/study?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
        password: root
        username: root
        # 切換成 Druid 資料來源
        type: com.alibaba.druid.pool.DruidDataSource
        #Spring Boot 預設是不注入這些屬性值的,需要自己繫結
        #druid 資料來源專有配置
        initialSize: 5
        minIdle: 5
        maxActive: 20
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
    
        #配置監控統計攔截的filters,stat:監控統計、log4j:日誌記錄、wall:防禦sql注入
        #如果允許時報錯  java.lang.ClassNotFoundException: org.apache.log4j.Priority
        #則匯入 log4j 依賴即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
        filters: stat,wall,log4j
        maxPoolPreparedStatementPerConnectionSize: 20
        useGlobalDataSourceStat: true
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
    
  5. 匯入 Log4j 依賴

    <!-- log4j -->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    
  6. 建立 Druid 自動配置類

    @Configuration
    public class DruidConfig {
        
        /*
           將自定義的 Druid資料來源新增到容器中,不再讓 Spring Boot 自動建立
           繫結全域性配置檔案中的 druid 資料來源屬性到 com.alibaba.druid.pool.DruidDataSource從而讓它們生效
           @ConfigurationProperties(prefix = "spring.datasource"):作用就是將 全域性配置檔案中
           字首為 spring.datasource的屬性值注入到 com.alibaba.druid.pool.DruidDataSource 的同名引數中
         */
        @ConfigurationProperties(prefix = "spring.datasource")
        @Bean
        public DataSource druidDataSource(){
            return new DruidDataSource();
        }
    
    }
    
  7. 測試

    在剛剛測試類的基礎上,加上輸出我們剛剛配置後 druid 資訊的程式碼

    @SpringBootTest
    class Springboot01ApplicationTests {
    
        @Autowired
        private Cat cat;
    
        @Autowired
        DataSource dataSource;
    
        @Test
        void contextLoads() {
            try {
                Connection connection = dataSource.getConnection();
                PreparedStatement ppstm = connection.prepareStatement("select * from user");
                ResultSet rs = ppstm.executeQuery();
                while (rs.next()){
                    System.out.println(rs.getString("name"));
                }
                System.out.println(connection);
                DruidDataSource druidDataSource = (DruidDataSource) dataSource;
                System.out.println("druidDataSource 資料來源最大連線數:" + druidDataSource.getMaxActive());
                System.out.println("druidDataSource 資料來源初始化連線數:" + druidDataSource.getInitialSize());
    
                //關閉連線
                connection.close();
            } catch (SQLException e) {
                e.printStackTrace();
            }
        }
    
    }
    

    至此,我們就配置好了 Druid 資料來源

10.3 配置 Druid 資料來源監控

Druid 資料來源具有監控的功能,並提供了一個 web 介面方便使用者檢視,類似安裝路由器時,人家也提供了一個預設的 web 頁面。

  1. 設定 Druid 的後臺管理頁面,比如登入賬號、密碼等。配置後臺管理

    在我們剛剛的 Druid 配置類中,配置如下的bean

    /**
     * 配置druid後臺監控的servlet
     */
    @Bean
    public ServletRegistrationBean statViewServlet(){
        ServletRegistrationBean bean = new ServletRegistrationBean(new StatViewServlet(),"/druid/*");
    
        // 後臺需要有人登入,賬號密碼配置
        HashMap<String, String> initParams  = new HashMap<>();
    
        initParams .put("loginUsername","admin");
        initParams .put("loginPassword","12345");
    
        // 允許誰可以訪問
        initParams .put("allow","");
    
        bean.setInitParameters(initParams);
        return bean;
    }
    
  2. 訪問 http://localhost:8084/druid/login.html ,進入 druid 後臺監控

  3. 然後我們訪問我們之前的寫好的介面,然後在 druid 監控後臺檢視具體監控資訊

    檢視 druid 後臺監控資訊

  4. 配置 Druid web 監控 filter 過濾器

    /**
     * Druid 後臺監控過濾器
     */
    @Bean
    public FilterRegistrationBean webStatFilter(){
        FilterRegistrationBean<WebStatFilter> bean = new FilterRegistrationBean<>(new WebStatFilter());
    
        // exclusions : 設定哪些請求進行過濾排除掉,從而不進行統計
        HashMap<String, String> initParams = new HashMap<>();
        initParams.put("exclusions","*.js,*.css./druid/*,/jdbc/*");
        bean.setInitParameters(initParams);
    
        // /* 代表過濾所有請求
        bean.setUrlPatterns(Collections.singletonList("/*"));
        return bean;
    }
    

到這裡,我們就成功在 SpringBoot 中集成了 Druid

11. 整合MyBatis

11.1 MyBatis 簡介

  • MyBatis 是一款優秀的持久層框架,它支援自定義 SQL 儲存過程以及高階對映, MyBatis 面出了幾乎所有的 JDBC 程式碼以及設定和獲取結果集的工作。MyBatis 可以通過簡單XML註解來配置和對映原始型別介面和 Java POJO (Plain Old Java Objects,普通老師 Java 物件)為資料庫中的記錄。
  • Mybatis 本是 apache 的一個開源專案 iBatis,2010年這個專案由 apache software foundation 遷移到了 google code,並且改名為 MyBatis
  • 2013年11月遷移到 GitHub
  • 官網:https://mybatis.org/mybatis-3/zh/index.html
  • GitHub:https://github.com/mybatis/mybatis-3

11.2 整合 MyBtis

  1. 匯入 MyBatis 所需要的依賴

    <!-- SpringBoot 整合 mybatis -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.3</version>
    </dependency>
    
    <build>
        <!-- 外掛 -->
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    	<!-- 靜態資源解析 -->
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>
    
  2. 配置資料庫連線資訊(不變)

    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/study?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
        password: root
        username: root
        # 切換成 Druid 資料來源
        type: com.alibaba.druid.pool.DruidDataSource
        #Spring Boot 預設是不注入這些屬性值的,需要自己繫結
        #druid 資料來源專有配置
        initialSize: 5
        minIdle: 5
        maxActive: 20
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
    
        #配置監控統計攔截的filters,stat:監控統計、log4j:日誌記錄、wall:防禦sql注入
        #如果允許時報錯  java.lang.ClassNotFoundException: org.apache.log4j.Priority
        #則匯入 log4j 依賴即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
        filters: stat,wall,log4j
        maxPoolPreparedStatementPerConnectionSize: 20
        useGlobalDataSourceStat: true
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
    
  3. 在測試類中測試資料庫是否連線成功

  4. 建立實體類

    @Component
    public class User {
    
        private Integer id;
    
        private String name;
    
        private String password;
    
        private String hobby;
    
        public User() {
        }
    
        public User(Integer id, String name, String password, String hobby) {
            this.id = id;
            this.name = name;
            this.password = password;
            this.hobby = hobby;
        }
    
        public Integer getId() {
            return id;
        }
    
        public void setId(Integer id) {
            this.id = id;
        }
    
        public String getName() {
            return name;
        }
    
        public void setName(String name) {
            this.name = name;
        }
    
        public String getPassword() {
            return password;
        }
    
        public void setPassword(String password) {
            this.password = password;
        }
    
        public String getHobby() {
            return hobby;
        }
    
        public void setHobby(String hobby) {
            this.hobby = hobby;
        }
    
        @Override
        public String toString() {
            return "User{" +
                    "id=" + id +
                    ", name='" + name + '\'' +
                    ", password='" + password + '\'' +
                    ", hobby='" + hobby + '\'' +
                    '}';
        }
    }
    
  5. 配置 Mybaits

    在 properties.yml 檔案中配置 MyBatis

    mybatis:
      # 給實體類起別名
      type-aliases-package: com.xp.model
      # 將 resource 目錄下的所有 mapper.xml 註冊到 MyBatis 中
      mapper-locations: classpath:mapper/*.xml
    
  6. 編寫 Mapper 介面 以及對應的 mapper.xml

    UserMapper

    @Mapper // 這個註解表示這個介面是MyBatis的mapper介面
    @Component  // 將mapper注入到Spring容器中
    public interface UserMapper {
    
        List<User> queryAllUser();
    
        int updateUser(@Param("user") User user);
    
        int deleteUser(Integer id);
    
        int addUser(@Param("user") User user);
    
    }
    

    UserMapper.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.xp.mapper.UserMapper">
        <select id="queryAllUser" resultType="User">
            select * from user;
        </select>
    
        <update id="updateUser" parameterType="User">
            update user
            set name = #{user.name},password=#{user.password},hobby=#{user.hobby}
            where id=#{user.id};
        </update>
    
        <insert id="addUser" parameterType="User">
            insert into user (name,password,hobby) values (#{user.name},#{user.password},#{user.hobby})
        </insert>
    
        <delete id="deleteUser">
            delete from user where id = #{id}
        </delete>
    
    </mapper>
    
  7. 編寫 Controller 介面測試

    @RestController
    @RequestMapping("/mybatis")
    public class MyBtisController {
    
        @Autowired
        UserMapper userMapper;
    
        @RequestMapping("/query")
        public List<User> query() {
            return userMapper.queryAllUser();
        }
    
        @RequestMapping("/update")
        public String update() {
            int row = userMapper.updateUser(new User(5, "zhangsan", "zs", "敲程式碼"));
            return row > 0 ? "update OK!" : "update Fail!";
        }
    
        @RequestMapping("/delete")
        public String delete() {
            int row = userMapper.deleteUser(2);
            return row > 0 ? "delete OK!" : "delete Fail";
        }
    
        @RequestMapping("/add")
        public String add() {
            int row = userMapper.addUser(new User(null, "lisi", "ls", "打籃球"));
            return row > 0 ? "add OK!" : "add Fail!";
        }
    }
    
    
  8. 啟動 SpringBoot 專案,通過 URL 訪問介面測試

到這裡,我們就成功在 SpringBoot 中整合了 MyBatis

12. Web 開發靜態資源處理

12.1 靜態資源對映規則

首先,我們搭建一個普通SpringBoot專案,回顧一下Helloword程式
寫請求非常簡單。那我們要引入我們前端資源,比如 css、js等檔案,這個 SpringBoot 怎麼處理呢?
如果我們是一個web應用,我們的main下會有一個webapp,哦我們以前都是將所有的頁面導在這裡面的。但是我們現在的 pom 呢,打包方式是 jar 的方式,那麼這種方式 SpringBoot 能布恩那個來給我們寫頁面呢?當然也是可以的,但是 SpringBoot 對於靜態資源放置的位置,是有規定的

我們先來聊聊這個靜態資源對映規則
在 SpringBoot 中, SpringMVC 的 web 配置都在 WebMvcAutoConfiguration 這個配置類裡面。我們可以去看看 WebMvcAutoConfigurationAdapter 中的配置方法。

SpringBoot 中有一個 addResourceHandlers 方法,它的作用是新增資源處理

@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
   // 判斷使用者是否已經手動配置了靜態資源處理,isAddMappings 中的 addMappings 預設是true
    if (!this.resourceProperties.isAddMappings()) {
      logger.debug("Default resource handling disabled");
      return;
   }
    // 快取控制
   Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
   CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
   // webjars 配置,所有的 /webjars/** 都需要去 /META-INF/resources/webjars/ 找對應的資源
   if (!registry.hasMappingForPattern("/webjars/**")) {
      customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
            .addResourceLocations("classpath:/META-INF/resources/webjars/")
            .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
   }
   // 靜態資源配置,第二種載入靜態資源的方式
   String staticPathPattern = this.mvcProperties.getStaticPathPattern();
   if (!registry.hasMappingForPattern(staticPathPattern)) {
      customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
            .addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
            .setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
   }
}

讀原始碼,我們可以知道,如果我們使用者沒有手動配置姿態資源處理,它會走預設的靜態資源處理。預設的靜態資源處理,是所有的 /webjars/** 都需要去 /META-INF/resources/webjars/ 找對應的資源

12.2 什麼是 webjars

webjars 的本質是以 jar 包的方式引入我們的靜態資源,我們以前要匯入一個靜態資原始檔,直接匯入即可。

webjars 官網:https://www.webjars.org

要使用 jQuery,我們只需要引入 jQuery 對應版本的 pom 依賴即可

<!-- webjars 引入 jQuery -->
<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.4.1</version>
</dependency>

成功匯入後我們去檢視 webjars 目錄結構,會發現如下: 它的包路徑是 /META-INF/resources/webkars/jquery/3.4.1/jquery.js ,WebMvcAutoConfigurationAdapter 這個類中靜態資源預設載入的路徑和這個符合。

訪問:只要是靜態資源,SpringBoot 就會去對應的路徑尋找資源。我們這裡訪問:http://localhost:8080/webjars/jquery/3.4.1/jquery.js

12.3 第二種靜態資源對映規則

那我們專案中要是使用自己的靜態資源該怎麼匯入呢?

在剛剛的原始碼中,我們發現第二種靜態資源的方式是staticPathPattern中配置的,我們點進 ResourceProperties 這個類,會發現 CLASSPATH_RESOURCE_LOCATIONS 規定了第二種對映規則:

// 預設靜態資源路徑
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { 
	"classpath:/META-INF/resources/",
    "classpath:/resources/", 
    "classpath:/static/", 
    "classpath:/public/" 
};

/**
  * Locations of static resources. Defaults to classpath:[/META-INF/resources/,
  * /resources/, /static/, /public/].
  */
// 儲存所有靜態資源路徑的String陣列
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
// 獲取靜態資源路徑
public String[] getStaticLocations() {
    return this.staticLocations;
}
// 設定靜態資源路徑
public void setStaticLocations(String[] staticLocations) {
    this.staticLocations = appendSlashIfNecessary(staticLocations);
}
// 遍歷 /META-INF/resources/, /resources/, /static/, /public/ 下的靜態資源,並新增斜槓
private String[] appendSlashIfNecessary(String[] staticLocations) {
    String[] normalized = new String[staticLocations.length];
    for (int i = 0; i < staticLocations.length; i++) {
        String location = staticLocations[i];
        normalized[i] = location.endsWith("/") ? location : location + "/";
    }
    return normalized;
}

我們會發現,當我們將靜態資源放在 /META-INF/resources/, /resources/, /static/, /public/ 這些靜態資源目錄下時,ResourceProperties 這個類會自動幫我們掃描並載入。

測試:在 resources 目錄下建立三個資料夾,分別是 resources,static,public,並在這些目錄下建立我們自己定義的 js ,my.js。目錄和檔案結構如下:

然後,這三個 js 檔案,都自定義寫入不同的內容,訪問 http://localhost:8080/my.js 測試:

經測試,這三個路徑的優先順序為:resources > static > public 。

12.4 自定義靜態資源路徑

我們也可以自己通過配置檔案來指定防止靜態資原始檔的目錄。

在 application.yml 檔案中配置 resources的static-locations:

spring:
  resources:
    static-locations: classpath:/my/

然後在 resources 包下建立 my 目錄,並在該目錄下建立 diy.js

啟動我們的 SpringBoot 應用,訪問 http://localhost:8080/diy.js 測試:

注意一旦自己配置了靜態檔案存放的目錄,原來的自動配置就都會失效

12.5 首頁處理

靜態資原始檔夾說完後,我們繼續想下看原始碼!可以看到一個歡迎頁的對映,就是我們首頁!

@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
      FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
   WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
         new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
         this.mvcProperties.getStaticPathPattern());
   welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
   welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
   return welcomePageHandlerMapping;
}

private Optional<Resource> getWelcomePage() {
    String[] locations = getResourceLocations(this.resourceProperties.getStaticLocations());
    return Arrays.stream(locations).map(this::getIndexHtml).filter(this::isReadable).findFirst();
}

private Resource getIndexHtml(String location) {
    return this.resourceLoader.getResource(location + "index.html");
}

通過原始碼,我們可以得知,靜態資原始檔夾下的所有 index.html 頁面;被/** 對映。

也就是說,當我們訪問 http://localhost:8080/ 時,會自動趙大鵬靜態資原始檔夾下的index.html 檔案。

13. Thymeleaf 模板引擎

13.1 模板引擎

前端交給我們的頁面,是 html 頁面。如果我們以前開發,我們需要把它們轉成 jsp 頁面,jsp 好處就是當我們查出一些資料轉發到 JSP 頁面以後,我們可以用 jsp 輕鬆實現頁面資料的顯示及互動等。

將商品支援非常強大的功能,包括能寫 Java 程式碼,但是呢,我們現在的這種情況,SpringBoot 這個專案首先是以 jar 的方式,不是 war 。我們的還是嵌入的 T歐美cat,所以呢, SpringBoot 預設是不支援 JSP 的

那不支援 jsp ,如果我們直接用純靜態頁面的方式,那給我們的開發會帶來非常大的麻煩。那怎麼辦呢?

SpringBoot 支援使用模板引擎,預設是 Thymeleaf

那麼什麼是模板引擎呢?

其實我們以前用的 jsp ,它就是一個模板引擎,還有用得比較多的 FreeMaker,包括 SpringBoot 預設的 Thymeleaf 。模板引擎,它們的思想都是一樣的。具體的思想如下圖:

模板引擎的作用就是我們來寫一個頁面模板,比如有些值,是動態的,我們寫一些表示式。而這些值,就是從我們後臺封裝的一些資料。然後把這個模板和這個資料交給我們的模板引擎。模板引擎會根據我們後臺封裝的資料根據模板中寫的表示式解析,填充到我們指定的位置。然後把這個資料最終生成一個我們想要的內容給我們寫出去,這就是模板引擎。不管是 jsp 還是其他模板引擎,都是這個思想,只不過,就是不同模板引擎之間,它們的語法可能有點不同。

13.2 Thymeleaf 簡介

Thymeleaf 是一款用於渲染 XCML/XHTML/HTML5 內容的模板引擎,類似 JSP,Velocity,FreeMaker。它可以輕易餘 SpringMVC 等 Web 框架進行整合作為 Web 應用的模板引擎,是 SpringBoot 官方使用的模板引擎。

官網:https://www.thymeleaf.org/

13.3 特點

  • 動靜結合: Thymeleaf 在有網路和無網路的環境下皆可執行。
  • 開箱即用:它提供標準和 Spring 標準兩種方言,可以直接套用模板實現 JSTL、OGNL 表示式效果
  • 多方言支援: Thymeleaf 提供 Spring 標準方言和一個與 SpringMVC 完美整合的可選模板,可以快速地實現表單繫結、屬性編輯器、國際化等功能。
  • 與 SpringBoot 完美整合,SpringBoot 提供了 Thymeleaf 的預設配置,並且為 Thymelaef 配置了檢視解析器,可以像以前操作 jsp 一樣操作 Thymeleaf ,程式碼幾乎沒有任何區別,就是在模板語法上有區別。

13.4 常用語法

Thymeleaf 的主要作用是把 model 中的資料渲染到html 中,因此其語法主要是如何解析 model 中的資料。從以下方面來學習:

  • 變數、方法、條件判斷、迴圈、運算【邏輯運算、布林運算、比較運算、條件運算】
  • 其它
  1. Thymeleaf 通過 ${...} 來獲取 model 中的變數,語法和 el 表示式差不多,但它是 ognl 表示式

    <!--/*@thymesVar id="thymeleaf" type="java.lang.String"*/-->
    <div th:text="${thymeleaf}"></div>
    
  2. Themeleaf 通過 th:object 自定義變數,可以通過 *{...} 取出對應的屬性

    <!--/*@thymesVar id="user" type="com.xp.entity.User"*/-->
    <div th:object="${user}">
        <h2 th:text="*{name}"></h2>
        <h2 th:text="*{age}"></h2>
        <!--/*@thymesVar id="friend" type="com.xp.entity.Friend"*/-->
        <h2 th:text="*{friend.name}"></h2>
    </div>
    
  3. ognl 表示式本身就支援方法呼叫,但需要注意的是必須使用註釋指明該變數是哪個類的

    <!--/*@thymesVar id="user" type="com.xp.entity.User"*/-->
    <!--/*@thymesVar id="name" type="java.lang.String"*/-->
    <!--/*@thymesVar id="age" type="java.lang.Integer"*/-->
    <div th:object="${user}">
        <h2 th:text="*{name.hashCode()}"></h2>
        <h2 th:text="*{age.hashCode()}"></h2>
        <!--/*@thymesVar id="friend" type="com.xp.entity.Friend"*/-->
        <h2 th:text="*{friend.name.hashCode()}"></h2>
    </div>
    

    Thymeleaf 中提供了一些內建物件,並且這些物件中提供了一些方法,方便我們呼叫、獲取這些物件,需要使用 #物件名 來呼叫

    • 一些環境相關的物件

      物件 作用
      #ctx 獲取 Thymeleaf 自己的 Context 物件
      #request 如果是 web 程式,可以獲取 HttpServletRequest 物件
      #respone 如果是 web 程式,可以獲取 HttpServletResponse 物件
      #session 如果是 web 程式,可以獲取 HttpSession 物件
      #servletContext 如果是web 程式,可以獲取 HttpServletContext 物件
    • Thymeleaf 提供的全域性物件

      物件 作用
      #datas 處理 java.util.date 的工具物件
      #calendars 處理 java.util.calendar 的工具物件
      #numbers 用來對數字格式的方法
      #strings 用來處理字串的方法
      #bools 用來判斷布林值的方法
      #arrays 用來護理陣列的方法
      #lists 用來處理 List 集合的方法
      #sets 用來處理 Set 集合的方法
      #maps 用來處理 Map 集合的方法

      例如:

      <div th:text="${#dates.format(data,'yyyy-MM-dd HH:mm:ss')}"></div>
      
      <div th:Object="${#session.getAttribute('user')}">
          <h1 th:text="*{name}"></h1>
          <h1 th:text="*{age}"></h1>
          <h1 th:text="*{friend.name}"></h1>
      </div>
      
  4. 字面值

    • 字串字面值:使用一對 '' (單引號)引用的內容就是字串的字面值了

      <div th:text="'字串字面值'"></div>
      
    • 數字字面值:不需要任何特殊語法,寫的是是什麼就是什麼,可以進行算術運算

      <div th:text="2020"></div>
      <div th:text="2018+2"></div>
      
    • 布林字面值:只有 true 或 false

      <div th:if="true">布林值:true</div>
      
  5. 字串拼接

    • 我們經常使用得普通字串拼接方法

      <div th:text="'歡迎 '+${user.name}+‘ !’"></div>
      
    • Thymeleaf 使用一對 | 拼接

      <div th:text="|歡迎 +${user.name} !|"></div>
      
  6. 運算

    • 算術運算

      支援的運算子: + - * / %

      <div th:text="${user.age}%2"></div>
      
    • 比較運算運算

      支援的比較運算: >,<,>=,<=,但是 >,< 不能直接使用,因為 html 會解析為標籤,要使用別名

      注意 == 和 != 不僅可以比較數值,類似於 equals 的功能

      可以使用的別名:gt(>), lt(<), ge(>=) , le(<=), not(!), eq(==), neq/ne(!=)

    • 條件運算

      • 三元運算

        <div th:text="${user.isAdmin}?'管理員':'普通會員'"></div>
        
      • 預設值

        有的時候,我們取一個值可能為空,這個時候需要做非空判斷,可以使用表示式 ?: 預設值簡寫

        <span th:text="${user.name} ?: '二狗'"></span>
        
  7. Thymeleaf 通過 th:each 實現迴圈

    <div th:each="list:${lists}">
        <h1 th:text="${list}"></h1>
    </div>
    

    遍歷的結合可以是以下型別

    • Iterable,實現了Iterable介面的類
    • Enumeration,列舉
    • Interator,迭代器
    • Map,遍歷得到的是Map.Entry
    • Array,陣列及其它一切符合陣列結果的物件
  8. Thymeleaf 使用 th:if 或者 if:unless 來進行邏輯判斷

    <div th:if="${user.age} >= 18">
        <h1>成年人</h1>
    </div>
    

    如果表示式的值為 true,則標籤會渲染到頁面,否則不進行渲染。

    以下情況會被認為 true

    • 表示式值為 true
    • 表示式值為非0數值
    • 表示式值為非0字元
    • 表示式值為字串,但不是“false”、“no”,“off”
    • 表示式不是布林、字串、數字、字元中的任何一種

    其它情況包括 null 都被認定為 false

  9. Thymeleaf 使用 th:switchth:case 來進行分支控制

    <div th:switch="${user.role}">
      <p th:case="'admin'">使用者是管理員</p>
      <p th:case="'manager'">使用者是經理</p>
      <p th:case="*">使用者是別的玩意</p>
    </div>
    

    需要注意的是,一旦有一個 th:case 成立,其它的則不再判斷。與 java 中的 switch 是一樣的

    另外 th:case="*" 表示預設,放在最後

  10. Thymeleaf 使用 th:inline="javascript" 來宣告該 script 標籤的指令碼是需要特殊處理的 js 指令碼

    <script th:inline="javascript">
        var user = /*[[${user}]]*/ {};
        var age = /*[[${user.age}]]*/ 20;
        console.log(user);
        console.log(age)
    </script>
    
    var user = /*[[Thymeleaf表示式]]*/
    

    因為 Thymeleaf 被註釋起來,因此即便是靜態環境下,js 程式碼也不會報錯,而是採用表示式後面跟著的預設值。且 User 物件會直接處理為 json 格式

13.5 簡單使用

  1. 引入 Thymeleaf

    <!-- thymeleaf -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    
  2. Thymeleaf 分析

    從 SpringBoot 的配置原理進行分析我們這個 Thymeleaf 的自動配置規則。

    我們先去找到 ThymeleafProperties 這個類

    @ConfigurationProperties(prefix = "spring.thymeleaf")
    public class ThymeleafProperties {
    
       private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
    
       public static final String DEFAULT_PREFIX = "classpath:/templates/";
    
       public static final String DEFAULT_SUFFIX = ".html";
    
       /**
        * Whether to check that the template exists before rendering it.
        */
       private boolean checkTemplate = true;
    
       /**
        * Whether to check that the templates location exists.
        */
       private boolean checkTemplateLocation = true;
    
       /**
        * Prefix that gets prepended to view names when building a URL.
        */
       private String prefix = DEFAULT_PREFIX;
    
       /**
        * Suffix that gets appended to view names when building a URL.
        */
       private String suffix = DEFAULT_SUFFIX;
    
       /**
        * Template mode to be applied to templates. See also Thymeleaf's TemplateMode enum.
        */
       private String mode = "HTML";
    
       /**
        * Template files encoding.
        */
       private Charset encoding = DEFAULT_ENCODING;
    
       /**
        * Whether to enable template caching.
        */
       private boolean cache = true;
    }
    

    在這個配置類裡面,我們看到很多熟悉的東西,比如 prefix 和 suffix 。

    我們只需要把我們的 html 頁面放在類路徑下的 templates 下,thymeleaf 就可以幫我們自動選軟了。

  3. 在 resources 的 templates 目錄下建立一個 text.html 頁面

    使用前,我們需要引入 xmlns:th="http://www.thymeleaf.org",來讓idea給我們增加提示

    ${test}:這個和我們之前 jsp 使用的 EL 表示式差不多,後端通過 model 設定值,然後在前端頁面使用 ${XXX} 來獲取 model 中設定的 XXX 值

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>Thymeleaf 測試</title>
    </head>
    <body>
    
        <h1>Thymeleaf 測試</h1>
        <div th:text="${test}"></div>
    
    </body>
    </html>
    
  4. 編寫 Controller

    ThymeleafController

    @Controller
    public class ThymeleafController {
    
        @RequestMapping("/test")
        public String test(Model model){
            // 通過 model 向前端傳值
            model.addAttribute("test","通過Thymeleaf模板引擎傳值");
            return "test";
        }
    
    }
    
  5. 測試

    輸入 URL 訪問我們的 Controller 介面

    到這裡,我們 SpringBoot 中 Thymeleaf 的簡單使用就完成了

14. SpringMVC 自動裝配原理

在進行專案編寫前嗎,我們還需知道一個東西,就是 SpringBoot 對我們的SpringMVC 還做了哪些配置,包括如何擴充套件,如何定製。

只有把這些都搞清楚了,我們在之後使用才會更加得心應手。

途徑一:原始碼分析。途徑二:官方文件

官方文件地址:https://docs.spring.io/spring-boot/docs/2.2.5.RELEASE/reference/htmlsingle/#boot-features-spring-mvc-auto-configuration

官方文件關於 SpringMVC 的自動配置描述如下:

Spring MVC Auto-configuration
// Spring Boot 為 Spring MVC 提供了自動配置,它可以很好地與大多數應用程式一起工作
Spring Boot provides auto-configuration for Spring MVC that works well with most applications .
// 自動配置在 Spring 預設設定的基礎上添加了一下功能:
The auto -configuration adds the following features on top of Spring's defaults:
// 包含內容協商檢視解析器和Bean名字檢視解析器
Inclusion of ContentNegotiatingViewResolver and BeanNameViewResolver beans.
// 支援靜態資原始檔夾的路徑,以及Webjars
Support for serving static resources ,including support for WebJars
// 自動註冊了 Converter(轉換器,這就是我們網頁提交資料到後臺自動封裝成物件的東西,比如把“1”字串自動轉換為int型別) 
// Formatter(格式化器,比如一個網頁給我們了一個 2020-7-12,它會給我們自動格式化為Date物件)
Automatic registration of Converter, GenericConverter, and Formatter beans.
// HttpMessageConverters (SpringMVC 用來轉換 Http 請求和響應的,比如我們要把一個User物件轉換為JSON字串,可以去看官網文件解釋)
Support for HttpMessageConverters (covered later in this document)
// 定義錯誤程式碼生成規則的
Automatic registration of MessageCodesResolver (covered later in this document).
// 首頁定製
Static index.html support.
// 圖示定製
Custon support (covered later in this document).
// 初始化資料繫結器(幫我們把請求資料繫結到JavaBean中)
Automatic use of a ConfigurableWebBindingInitializer bean (covered later in this document).
/*
如果你希望保留Spring Boot MVC 功能,並且希望新增其他MVC配置(攔截器、格式化程式、檢視控制器和其他功能)
則可以新增自己的 @Configuration 類,型別為 WebMvcConfigurer ,但不新增 @EnableWebMvc。
如果希望提供 RequestMappingHandlerMapping RequestMappingHandlerAdaptor 或者 ExceptionHandlerExceptionResolver 的自定義示例,
可以宣告 WebMvcRegistrationsAdapter 示例來提供此類元件
*/
If you want to keep Spring Boot MVC features and you want to add additional MVC configuration(interceptors, formatters, view controllers, and other features), you can add your own @Configuration class of the type WebMvcConfigurer but without @EnableWebMvc. If you wish to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdaptor, or ExceptionHandlerExceptionResolver, you can declare a WebMvcRegistrationsAdapter instance to provide such components.
// 如果你向完全控制 Spring MVC ,可以新增自己的 @Configuration,並用 @EnableWebMvc 進行註釋
If you want to take complete control of Spring MVC, you can add your own @Configuration annotated with @EnableWebMvc.

上面已經告訴了我們 Spring MVC 自動配置了什麼,那我們開始追下原始碼來深入瞭解把

14.1 ContentNegotiatingViewResovler 內容協商檢視解析器

ContentNegotiatingViewResovler 自動配置了了 ViewResolver ,就是我們之前學習的 SpringMVC 的檢視解析器。

即根據方法的返回值取得檢視物件(View),然後由檢視物件決定如何渲染(轉發,重定向)。

SpringBoot 中關於 SpringMVC 的自動配置都在 WebMvcConfiguration,然後搜尋 ContentNegotiatingViewResovler 找到如下方法

@Bean
@ConditionalOnBean(ViewResolver.class)
@ConditionalOnMissingBean(name = "viewResolver", value = ContentNegotiatingViewResolver.class)
public ContentNegotiatingViewResolver viewResolver(BeanFactory beanFactory) {
   ContentNegotiatingViewResolver resolver = new ContentNegotiatingViewResolver();
   resolver.setContentNegotiationManager(beanFactory.getBean(ContentNegotiationManager.class));
   // ContentNegotiatingViewResolver uses all the other view resolvers to locate
   // a view so it should have a high precedence
   resolver.setOrder(Ordered.HIGHEST_PRECEDENCE);
   return resolver;
}

點進 ContentNegotiatingViewResovler 這個類,我們會發現它實現了 ViewResolver 介面,然後我們再點進 ViewResolver,會發現它只定義了一個 resolveViewName() 方法

public class ContentNegotiatingViewResolver extends WebApplicationObjectSupport
      implements ViewResolver, Ordered, InitializingBean {
}

public interface ViewResolver {
	@Nullable
	View resolveViewName(String viewName, Locale locale) throws Exception;
}

我們回到 ContentNegotiatingViewResovler 這個類去檢視從 ViewResolver 介面實現的 resolveViewName() 方法

@Override
@Nullable
public View resolveViewName(String viewName, Locale locale) throws Exception {
   RequestAttributes attrs = RequestContextHolder.getRequestAttributes();
    // 斷言attrs 是 ServletRequestAttributes 型別的,也就是說斷言非空
   Assert.state(attrs instanceof ServletRequestAttributes, "No current ServletRequestAttributes");
   List<MediaType> requestedMediaTypes = getMediaTypes(((ServletRequestAttributes) attrs).getRequest());
   if (requestedMediaTypes != null) {
       // 獲取所有候選檢視
      List<View> candidateViews = getCandidateViews(viewName, locale, requestedMediaTypes);
       // 從所有的候選檢視中獲取最優的檢視
      View bestView = getBestView(candidateViews, requestedMediaTypes, attrs);
      if (bestView != null) {
         return bestView;
      }
   }
   // 下面就是列印日誌的方法,與檢視解析無關
   String mediaTypeInfo = logger.isDebugEnabled() && requestedMediaTypes != null ?
         " given " + requestedMediaTypes.toString() : "";

   if (this.useNotAcceptableStatusCode) {
      if (logger.isDebugEnabled()) {
         logger.debug("Using 406 NOT_ACCEPTABLE" + mediaTypeInfo);
      }
      return NOT_ACCEPTABLE_VIEW;
   }
   else {
      logger.debug("View remains unresolved" + mediaTypeInfo);
      return null;
   }
}

看完原始碼後,我們可以得出結論:ContentNegotiatingViewResovler 這個檢視解析器就是用來組合所有的檢視解析器的,它會篩選出最優的檢視解析器

我們再去研究下他的組合邏輯,看到有個屬性 ViewResolvers,看看它是在哪裡進行賦值的!

protected void initServletContext(ServletContext servletContext) {
    // 這裡它是從beanFactory工具中獲取容器中的所有檢視解析器
    // ViewRescolver.class 把所有的檢視解析器來組合的
    Collection<ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(this.obtainApplicationContext(), ViewResolver.class).values();
    ViewResolver viewResolver;
    if (this.viewResolvers == null) {
        this.viewResolvers = new ArrayList(matchingBeans.size());
    }
    // ...............
}

既然它是在容器中去找檢視解析器,我們是否可以猜想,我們自己也可以去實現一個檢視解析器呢?

我們可以自己容器中去新增一個檢視解析器;這個類就會幫我們自動的將它組合進來。

  1. 先自定義我們自己的檢視解析器 ViewResolver

    MyViewResolver

    public class MyViewResolver implements ViewResolver {
        @Override
        public View resolveViewName(String viewName, Locale locale) throws Exception {
            return null;
        }
    }
    
  2. 配置 SpringMVC

    建立一個配置類叫 WebMvcConfig,並將我們自己定義的檢視解析器 MyViewResolver 註冊到 Spring 容器中

    @Configuration
    public class WebMvcConfig implements WebMvcConfigurer {
    
        @Bean
        public ViewResolver myViewResolver(){
            return new MyViewResolver();
        }
    
    }
    
  3. 在 DispatcherServlet 這個類的 doDispatch() 這個方法上打上斷點

    因為 SpringMVC 的所有請求,都會進入 DispatcherServlet 的 doDispatch() 這個方法進行處理,

    這裡的圖是已經啟動除錯進入斷點了

  4. DEBUG 啟動 SpringBoot 應用程式

    隨便訪問一個我們以前寫好的 Controller 介面,檢視 引數資訊

    我們可以發現,我們自己寫的檢視解析器 MyViewResolver 已經註冊到 Spring 容器中了

到這裡,我們自定義的解析器就成功生效了。如果我們想要使用自己定製化的東西,我們只需要給容器中加入這個元件就好了。剩下的事情 SpringBoot 會自動幫我們做。

14.2 轉換器和格式化器

搜尋 formattingConversionService 這個方法:

@Bean
@Override
public FormattingConversionService mvcConversionService() {
    // 從配置檔案中獲取格式化規則
    Format format = this.mvcProperties.getFormat();
    // 將取出的配置資訊放進 DateTimeFormatters 這個類中封裝
   WebConversionService conversionService = new WebConversionService(new DateTimeFormatters()
         .dateFormat(format.getDate()).timeFormat(format.getTime()).dateTimeFormat(format.getDateTime()));
    // 將日期格式的格式化器註冊進 Spring 容器中
   addFormatters(conversionService);
   return conversionService;
}

我們點進配置檔案 MvcProperties 這個類,發現這個格式化的規則是 new 了一個 Format 物件

private final Format format = new Format();

我們繼續點進 Format 這個類

public static class Format {

   /**
    * Date format to use, for example `dd/MM/yyyy`.
    */
   private String date;

   /**
    * Time format to use, for example `HH:mm:ss`.
    */
   private String time;

   /**
    * Date-time format to use, for example `yyyy-MM-dd HH:mm:ss`.
    */
   private String dateTime;
}

發現這些日期格式在註釋中已經說明了預設值。

如果配置了我們自己的格式化方式,就會註冊到 Bean 中生效,我們可以在配置檔案中配置日期格式化的規則。

@Deprecated
@DeprecatedConfigurationProperty(replacement = "spring.mvc.format.date")
public String getDateFormat() {
   return this.format.getDate();
}
spring:
    mvc:
      format:
        date: yyyy-MM-dd

14.3 修改 SpringBoot 的預設配置

這麼多的自動配置,原理都是一樣的,通過這個 WebMVC 的自動配置原理分析,我們要學會一種學習方式,通過原始碼探究,得出結論。這個結論一定是屬於自己的,而且一通百通。

SpringBoot 的底層,大量用到了這些設計思想細節,所以,沒事需要多閱讀原始碼!得出結論。

SpringBoot 在自動配置很多元件的時候,先看容器中有沒有使用者自己配置的(如果使用者自己配置 @Bean ),如果有就用使用者配置的,如果沒有就用自動配置的。

如果有些元件可以存在多個,比如我們的檢視解析器,就將使用者配置的和自己預設的組合起來!

拓展使用 Spring MVC 官方文件如下:

If you want to keep Spring Boot MVC features and you want to add additional MVC configuration (interceptors,formatters,view controllers,and other features),you can add your own @Configuration class of type WebMvcConfigurer but without @EnableWebMvc .If you wish to provide custom instances of RequestMappingHandlerMapping, RequestMappingHandlerAdapter,or ExceptionHandlerExceptionResolver, you can declare a WebMvcRegistrationsAdapter instance to provide such components.

我們要做的就是編寫一個 @Configuration 註解類,並且型別要為 WebMvcConfigurer,而且還不能表主 @EnableWebMvc 註解

那我們現在再自己寫一個配置

在我們剛剛寫的 WebMvcConfig 中重寫 addViewControllers() 方法

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    @Bean
    public ViewResolver myViewResolver(){
        return new MyViewResolver();
    }

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/test1").setViewName("test");
    }
}

然後啟動 SpringBoot 應用程式,進入 /test1 這個路徑測試是否進入我們之前寫的 test 頁面

的確是可以跳轉過來,同時,我們發現 Thymeleaf 沒有接收到引數時,會隱藏內容。

我們要擴充套件 SpringMVC ,官方就推薦我們這麼去使用,即保留 SpringBoot 所有的自動配置,也能用我們擴充套件的配置

我們可以去分析一下原理:

  1. WebMvcAutoConfiguration 是 SpringMVC 的自動配置類,裡面有一個類 WebMvcAutoConfigurationAdapter

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnWebApplication(type = Type.SERVLET)
    @ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
    @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
    @AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
    @AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
          ValidationAutoConfiguration.class })
    // WebMvcAutoConfiguration webmvc 自動配置類
    public class WebMvcAutoConfiguration {
    	
        @Configuration(proxyBeanMethods = false)
    	@Import(EnableWebMvcConfiguration.class)
        // @Import(EnableWebMvcConfiguration.class) 引入 EnableWebMvcConfiguration 類
    	@EnableConfigurationProperties({ WebMvcProperties.class, ResourceProperties.class })
    	@Order(0)
        // WebMvcAutoConfigurationAdapter類
    	public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer {
        }
    
    }
    
  2. 這個類上有一個註解,在做其他自動配置時會匯入: @Import(EnableWebMvcConfiguration.class)

  3. 我們點進 EnableWebMvcConfiguration 這個類看一下,它繼承了一個父類: DelegatingWebMvcConfiguration

    @Configuration(proxyBeanMethods = false)
    // 繼承 DelegatingWebMvcConfiguration
    public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration implements ResourceLoaderAware {
        	private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
    
    	// 從容器中獲取所有的 webmvcConfigurer
    	@Autowired(required = false)
    	public void setConfigurers(List<WebMvcConfigurer> configurers) {
    		if (!CollectionUtils.isEmpty(configurers)) {
    			this.configurers.addWebMvcConfigurers(configurers);
    		}
    	}
    
    }
    
  4. 我們可以在這個類中尋找一個我們剛才設定的 viewController 當作參考,發現它呼叫了一個

    @Override
    protected void addViewControllers(ViewControllerRegistry registry) {
       this.configurers.addViewControllers(registry);
    }
    
  5. 我們點進去看一下

    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
       // 將所有的 webMvcConfigurer 相關配置一起呼叫!包括我們自己配置的 和 Spring 給我們配置的
        for (WebMvcConfigurer delegate : this.delegates) {
          delegate.addViewControllers(registry);
       }
    }
    

所以得出結論:所有的 WebMbcConfiguration 都會被作用,不止 Spring 自己的配置類,我們自己的配置類當然也會被呼叫

14.4 @EnableWebMvc 註解讓 SpringBoot MVC 自動配置失效的原因

在官網中寫道,如果我們需要 SpringBoot 進行自動配置和擴充套件,則在 SpringMVC 的配置類上不能加上 @EnableWebMvc 註解,這是為什麼呢?

我們都知道,SpingBoot MVC自動配置類是 WebMvcAutoConfiguration ,我們點進這個類,會發現上面有個註解是 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)

@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
// @ConditionalOnMissingBean(WebMvcConfigurationSupport.class),這個註解的意思是:
// 如果 Spring 容器中沒有 WebMvcConfigurationSupport 這個類,那麼 SpringBoot 自動配置類 WebMvcAutoConfiguration 才會生效。
// 也就是說,如果存在 WebMvcConfigurationSupport 這個類,SpringBoot MVC 的自動配置全部失效
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
      ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {
}
  1. @ConditionalOnMissingBean(WebMvcConfigurationSupport.class)

    這個註解的意思是:如果 Spring 容器中沒有 WebMvcConfigurationSupport 這個類,那麼 SpringBoot 自動配置類 WebMvcAutoConfiguration 才會生效。

    也就是說:如果存在 WebMvcConfigurationSupport 這個類, SpringBoot MVC 的自動配置全部失效

    @ConditionalOnMissingBean(XXX.class):前面提到過這個註解,當沒有XXX這個類的時候,被註解的類生效

  2. @EnableWebMvc

    我們點進 WebMvcConfigurationSupport 這個類,發現類上的註釋中有個 @see EnableWebMvc

    / * @author Rossen Stoyanchev
     * @author Brian Clozel
     * @author Sebastien Deleuze
     * @since 3.1
     * 這個 @see EnableWebMvc ctrl+右鍵點進去   
     * @see EnableWebMvc
     * @see WebMvcConfigurer
     */
    public class WebMvcConfigurationSupport implements ApplicationContextAware, ServletContextAware {
    }
    

    點進去後,原始碼如下:

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Documented
    // @Import(DelegatingWebMvcConfiguration.class) 匯入 DelegatingWebMvcConfiguration 類
    @Import(DelegatingWebMvcConfiguration.class)
    public @interface EnableWebMvc {
    }
    

    我們可以看到,這個註解的作用,就是引入了 DelegatingWebMvcConfiguration 這個類。

    那為什麼引入了這個類就會讓 SpringBoot MVC 的自動配置全部失效呢?

    我們再點進 DelegatingWebMvcConfiguration 這個類

    @Configuration(proxyBeanMethods = false)
    public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
    }
    

    會發現,它繼承了 WebMvcConfigurationSupport 這個類。

    我們剛剛在 SpringBoot MVC 自動配置類 WebMvcAutoConfiguration 中看到 @ConditionalOnMissingBean(WebMvcConfigurationSupport.class) 這個註解,而 @EnableWebMvc 這個註解就是匯入了 WebMvcConfigurationSupport 的子類 DelegatingWebMvcConfiguration

到這裡,我們就知道了為什麼加了 @EnableWebMvc 這個註解,SpringBoot MVC 的自動配置會全部失效了。

14.5 全面接管 SpringMVC

官方文件:

If you want to take complete control of Spring MVC
you can add your own @Configuration annotated with @EnableWebMvc.

翻譯過來就是,如果你想完全接管 Spring MVC,你可以在你擁有 @Configuration 註解的類上新增 @EnableWebMvc 註解

全面接管即: SpringBoot 對 SpringMVC 的自動配置不需要了,所有的配置都是由我們自己去配置!

如果我們要全面接管 Spring MVC,SpringBoot 給我們配置的靜態資源對映就一定會無效。我們現在可以去測試一下:

  1. 在 templates 包下建立 index.html

    SpringBoot 中的 MVC 自動配置會將靜態資源路徑下的 index.html 作為首頁

    <!DOCTYPE html>
    <html lang="en" >
    <head>
        <meta charset="UTF-8">
        <title>首頁</title>
    </head>
    <body>
        <h1>首頁</h1>
    </body>
    </html>
    
  2. 啟動 SpringBoot 應用程式,訪問URL根目錄

    我們可以發現首頁可以正常訪問

  3. 在我們之前的 SpringBoot MVC 配置類中增加 @EnableWebMvc註解

    @Configuration
    @EnableWebMvc
    public class WebMvcConfig implements WebMvcConfigurer {
    
        @Bean
        public ViewResolver myViewResolver(){
            return new MyViewResolver();
        }
    
        @Override
        public void addViewControllers(ViewControllerRegistry registry) {
            registry.addViewController("/test1").setViewName("test");
        }
    }
    
  4. 重啟 SpringBoot 應用程式,訪問URL根目錄

    我們會發現這個首頁已經進不去了,這說明了我們在 @Configuration 的類上新增 @EnableWebMvc 註解後,SpringBoot MVC 的自動配置全部失效,包括靜態資源對映

當然,在我們開發中,不推薦使用全面接管 SpringMVC

15. 頁面國際化

有時候,我們的網站會涉及中英文甚至多語言的切換,這時我們就需要學習國際化了!

15.1 準備工作

在 IDEA 中統一設定 properties 的編碼問題!

編寫國際化配置檔案,抽取頁面需要顯示的國際化頁面訊息。我們可以去登入頁面檢視一下,哪些內容我們需要編寫國際化的配置

登入頁面程式碼:

<!DOCTYPE html>
<html lang="zh_CN" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="description" content="">
    <meta name="author" content="">
    <title>Signin Template for Bootstrap</title>
    <!-- Bootstrap core CSS -->
    <link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
    <!-- Custom styles for this template -->
    <link th:href="@{/css/signin.css}" rel="stylesheet">
</head>

<body class="text-center">
<form class="form-signin" th:action="@{/login}">
    <img class="mb-4" src="img/bootstrap-solid.png" alt="" width="72" height="72">
    <h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>
    <span style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></span>
    <label class="sr-only" th:text="#{login.username}">Username</label>
    <input type="text" class="form-control" th:placeholder="#{login.username}" name="username" required="" autofocus="">
    <label class="sr-only" th:text="#{login.password}">Password</label>
    <input type="password" name="password" class="form-control" th:placeholder="#{login.password}" required="">
    <div class="checkbox mb-3">
        <label>
            <input type="checkbox" value="remember-me">[[#{login.remember}]]
        </label>
    </div>
    <button class="btn btn-lg btn-primary btn-block" type="submit">[[#{login.btn}]]</button>
    <p class="mt-5 mb-3 text-muted">© 2017-2018</p>
    <a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a>
    <a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>
</form>

</body>

</html>

15.2 配置檔案編寫

  1. 在 resources 目錄下建立一個 i18n 目錄,用來存放國際化配置檔案

  2. 在 i18n 目錄下建立 login.properties、login_en_US.properties、login_zh_CN.properties 國際化配置檔案。建立時,我們會發現 IDEA 自動幫我們做了整合

  3. 配置國際化配置檔案

    在我們點選配置檔案後,我們會發現下面多了個東西

    點選 Resource Bundle,然後就能同時編輯三個國際化配置檔案了

    然後將我們需要編寫的內容編寫完成

15.3 建立 LocaleResolver

MessageSourceAutoConfiguration 中有一個核心方法 messageSource()

@Bean
public MessageSource messageSource(MessageSourceProperties properties) {
   ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
   if (StringUtils.hasText(properties.getBasename())) {
       // 設定國際化檔案的基礎名(去掉語言國家程式碼的
       messageSource.setBasenames(StringUtils
            .commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));
   }
   if (properties.getEncoding() != null) {
      messageSource.setDefaultEncoding(properties.getEncoding().name());
   }
   messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
   Duration cacheDuration = properties.getCacheDuration();
   if (cacheDuration != null) {
      messageSource.setCacheMillis(cacheDuration.toMillis());
   }
   messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
   messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
   return messageSource;
}

根據這個原始碼,我們需要配置 basename:

spring:
  messages:
    basename: i18n.login

配置完後在我們的前端頁面使用 thymeleaf 取值。:取值格式是 #{xxx} ,而不是${xxx}

檢視我們剛剛配置的這些是否生效。

如果我們的配置生效,則會顯示如下的頁面,若配置出錯,則可能顯示 ??login.username??

這時候,我們就得從頭開始檢查是否有寫錯

  1. 建立 properties 檔案,這裡它會自動生成,不要手動去修改

  2. 前端獲取的單詞拼寫是否和 properties 檔案中設定的一致

  3. 檢查 application.yml/properties 的配置,注意 yml/properties 檔案的語法書寫規範

  4. 注意細節,不要 ‘.’ 打成了 ‘,’ 或其他符號

按照上述步驟排錯, ??login.username?? 這種型別的錯誤就能解決了

15.4 配置國際化解析

上面是成功的將我們自己定義的國際化資訊展現出來了,但我們真正想要的,是點選按鈕切換國際語言,這個得怎麼做呢?

在 Spring 中有一個國際化得 Locale (區域資訊物件);裡面有一個叫做 LocaleResolver (獲取區域資訊物件)得解析器。

我們進入 WebMvcAutoConfiguration 這個類,尋找 localeResolver 這個方法

@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
public LocaleResolver localeResolver() {
    // 容器中沒有就自己配,有的話就用使用者配置的
   if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
      return new FixedLocaleResolver(this.mvcProperties.getLocale());
   }
    // 接收頭國際化分解
   AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
   localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
   return localeResolver;
}

其中 AcceptHeaderLocaleResolver 實現了 LocaleResolver 介面,並且裡面重寫了接口裡的 resolveLocale() 方法。

@Override
public Locale resolveLocale(HttpServletRequest request) {
   // 獲取預設的 Locale
    Locale defaultLocale = getDefaultLocale();
   // 預設的就是根據請求頭帶來的區域資訊獲取 Locale 進行國際化
    if (defaultLocale != null && request.getHeader("Accept-Language") == null) {
      return defaultLocale;
   }
   Locale requestLocale = request.getLocale();
   List<Locale> supportedLocales = getSupportedLocales();
   if (supportedLocales.isEmpty() || supportedLocales.contains(requestLocale)) {
      return requestLocale;
   }
   Locale supportedLocale = findSupportedLocale(request, supportedLocales);
   if (supportedLocale != null) {
      return supportedLocale;
   }
   return (defaultLocale != null ? defaultLocale : requestLocale);
}

也就是說,我們尋找向點選連線讓我們的國際化資源生效,就需要讓我們自己的 Locale 生效!

我們去自己寫一個自己的 LocaleResolver ,可以在連結上攜帶區域資訊!

根據我們前端定義好的引數,寫我們自己的 LocaleResolver

<a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>

自定義我們自己的 LocaleResolver :LocaleResolver 介面是 org.springframework.web.servlet 包下的

package com.xp.config;

import org.springframework.util.StringUtils;
import org.springframework.web.servlet.LocaleResolver;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Locale;

public class MyLocaleResolver implements LocaleResolver {
    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        //獲取請求的語言引數
        String language = request.getParameter("l");
        Locale locale = Locale.getDefault();    //如果沒有就使用預設的
        //如果請求攜帶了國際化的引數
        if(!StringUtils.isEmpty(language)){
            //zh_CN
            String [] spilt= language.split("_");
            //國家,地區
            locale = new Locale(spilt[0],spilt[1]);
        }
        return locale;
    }

    @Override
    public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {

    }
}

然後將我們寫好的區域化資訊注入到 Spring 容器中

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {

    // 進行重定向
    @Override
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/").setViewName("index");
        registry.addViewController("/index.html").setViewName("index");
    }


    //使自定義的國際化元件生效
    @Bean
    public LocaleResolver localeResolver(){
        return new MyLocaleResolver();
    }

}

如果直接注入 Spring 容器,可能會不生效。我網上查的是需要進行重定向。配置個檢視控制器進行重定向就可以了。

16. 整合 Swagger

相信無論是前端還是後端開發,都或多或少地被介面文件折磨過。前端經常抱怨後端給的介面文件與實際情況不一致。後端又覺得編寫及維護介面文件會耗費不少精力,經常來不及更新。其實無論是前端呼叫後端,還是 後端呼叫後端,都期望有一個好的介面文件。但是這個介面文件對於程式設計師來說,就跟註釋一樣,經常會抱怨別人寫得程式碼沒有寫註釋。然而自己寫起程式碼來,最討厭的,也是寫註釋。所以僅僅通過強制來規範大家是不夠的。隨著時間推移,版本迭代,介面文件往往很容易跟不上程式碼了。

16.1 Swagger 簡介

前後端分離

  • 前端 -> 前端控制層、檢視層
  • 後端 -> 後端控制層、服務層、資料訪問層
  • 前後端通過 API 進行互動
  • 前後端相對獨立且鬆耦合

產生的問題

  • 前後端整合,前端或者後端無法做到 “及時協商,儘早解決” ,最終導致問題集中爆發

解決方案

  • 首先定義 schema [計劃的提綱],並實時跟蹤最新的 API,降低整合風險

Swagger

  • 號稱世界上最流行的 API 框架

  • Restful API 文件線上自動生成器 => API 文件與 API 定義同步更新

  • 直接執行,線上測試 API

  • 支援多種語言(如:Java、PHP等)

  • 官網:https://swagger.io/

16.2 SpringBoot 整合 Swagger

使用 Swagger ,JDK 必須 1.8 以上,否則 swagger2 無法執行

  1. 新增 Maven 依賴

    <!-- swagger-ui -->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.9.2</version>
    </dependency>
    
    <!-- swagger2 -->
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.9.2</version>
    </dependency>
    
  2. 建立一個 Swagger 配置類

    SwaggerConfig

    @Configuration
    @EnableSwagger2
    public class SwaggerConfig {
    }
    
  3. 訪問測試:http://localhost:8080/swagger-ui.html

    訪問該 URL 後看到的 swagger 的介面:

16.3 配置 Swagger

  1. Swagger 例項 Bean 是 Docket,所以通過配置 Docket 例項來配置 Swagger。

    在我們剛剛的 Swagger 配置類中註冊 Docket 的 Bean

    // 配置 Docket 以配置 Swagger 具體引數
    @Bean
    public Docket docket(){
        return new Docket(DocumentationType.SWAGGER_2);
    }
    
  2. 可以通過 apliInfo() 屬性配置文件資訊

    private ApiInfo apiInfo(){
        Contact contact = new Contact("XP","https://www.cnblogs.com/windowsxpxp/","[email protected]");
        return new ApiInfo(
                "Swagger學習",// 標題
                "學習演示如何配置Swagger", // 描述
                "v1.0", // 版本
                "http://terms.service.url/組織連結", // 組織連結
                contact, // 聯絡人資訊
                "Apach 2.0 許可", // 許可
                "許可連結", // 許可連線
                new ArrayList<>()// 擴充套件
        );
    }
    
  3. Docket 例項關聯上 apiInfo()

    @Bean
    public Docket docket(){
        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo());
    }
    
  4. 重啟 SpringBoot 應用,並重新訪問 Swagger 後臺頁面

    發現部分資訊已經改變了

16.4 配置掃描介面

  1. 構建 Docket 時通過 select() 方法配置掃描介面以及如何掃描

    @Bean
    public Docket docket(){
        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
            .select() // 通過 .select() 方法,去配置掃描介面 .RequestHandlerSelectors 配置掃描介面以及如何掃描
            .apis(RequestHandlerSelectors.basePackage("com.xp.controller")).build()
                .groupName("xp");
    }
    
  2. 除了通過包路徑掃描介面外,還可以通過配置其他方式掃描介面,這裡註釋一下所有的配置方式:

    any() // 掃描所有,專案中的所有介面都會被掃描到
    none() // 不掃描介面
    // 通過方法上的註解掃描,如 withMethodAnnotation(GetMapping.class) 只掃描 get 請求
    withMethodAnnotation(final Class<? extends Annotation> annotation)
    // 通過類上的註解掃描,如 withClassAnnotation(Controller.class) 只掃描有 @Controller 註解的類中的介面
    withClassAnnotation(final Class<? extends Annotation> annotation)
    basePackage(final String basePackage) // 根據包路徑掃描介面
    
  3. 除此之外,我們還可以配置介面掃描過濾

    @Bean
    public Docket docket(){
        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select().apis(RequestHandlerSelectors.basePackage("com.xp.controller"))
            .path(PathSelectors.ant("/xp/**")) // 配置如何通過 path 過濾,即這裡只掃描請求以 /xp 開頭的介面
            .build();
    }
    
  4. 這裡可選值還有

    any() // 任何請求都掃描
    none() // 任何請求都不掃描
    regex(final String pathRegex) // 通過正則表示式控制
    ant(final String antPatten) // 通過 ant() 控制
    

16.5 配置 Swagger 開關

  1. 通過 enable() 方法配置是否啟用 swagger,如果是 false,swagger 將不能在瀏覽器中訪問了

    @Bean
    public Docket docket(){
        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select().apis(RequestHandlerSelectors.basePackage("com.xp.controller")).build()
                .enable("xp");
    }
    
  2. 如何動態配置當專案處於 test、dev 環境時顯示 swagger,處於 prod 時不顯示?

    // 配置 Docket 以配置 Swagger 具體引數
    @Bean
    public Docket docket(Environment environment) {
        // 設定要顯示swagger的環境
        Profiles of = Profiles.of("dev", "test");
        // 判斷當前是否處於該環境
        // 通過 enable() 接收此引數判斷是否要顯示
        boolean b = environment.acceptsProfiles(of);
    
        return new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo())
                .enable(b) //配置是否啟用Swagger,如果是false,在瀏覽器將無法訪問
                .select()// 通過.select()方法,去配置掃描介面,RequestHandlerSelectors配置如何掃描介面
                .apis(RequestHandlerSelectors.basePackage("com.xp.controller"))
                // 配置如何通過path過濾,即這裡只掃描請求以/xp開頭的介面
                .paths(PathSelectors.ant("/xp/**"))
                .build();
    }
    
  3. 測試

16.6 配置 API 分組

  1. 如果沒有配置分組,預設是 default。通過 groupName() 方法即可配置分組:

    @Bean
    public Docket docket(Environment environment) {
       return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
          .groupName("hello") // 配置分組
           // 省略配置....
    }
    
  2. 重啟專案檢視分組

  3. 設定多個分組

    我們要配置多個分組,就只需配置多個 docket 即可:

    @Bean
    public Docket docket1(){
       return new Docket(DocumentationType.SWAGGER_2).groupName("group1");
    }
    @Bean
    public Docket docket2(){
       return new Docket(DocumentationType.SWAGGER_2).groupName("group2");
    }
    @Bean
    public Docket docket3(){
       return new Docket(DocumentationType.SWAGGER_2).groupName("group3");
    }
    
  4. 重啟專案檢視

16.7 實體配置

  1. 新建一個實體類

    @ApiModel("使用者實體類")
    public class User {
        @ApiModelProperty("使用者id")
        private Integer id;
        @ApiModelProperty("使用者姓名")
        private String name;
        @ApiModelProperty("使用者密碼")
        private String password;
        @ApiModelProperty("使用者愛好")
        private String hobby;
    }
    
  2. 只要這個實體在請求介面的返回值上(即使是泛型),否能對映到實體項中:

    @RequestMapping("/getUser")
    public User getUser(){
       return new User();
    }
    
  3. 重啟檢視測試

    :並不是因為 @ApiModel 這個註解讓實體顯示在這裡,而是隻要出現在介面方法的返回值上的實體都會顯示在這裡,而 @ApiModel 和 @ApiModelProperties 這兩個註解只是為實體添加註釋的

    @ApiModel 為類添加註釋

    @ApiModelProperties 為類屬性添加註釋

16.8 常用註解

Swagger 的所有註解定義在 io.swagger.annotations 包下

下面列一些經常用到的,未列舉出來的可以另行查閱說明:

Swagger 註解 簡單使用
@Api(tags = "xxx模組說明") 作用在模組類上
@ApiOperation("xxx介面說明") 作用在介面方法上
@ApiModel("xxxPOJO說明") 做那個用在模型類上,如VO、DTO
@ApiModelProperties(value = "xxx屬性說明",hidden = true) 作用在類方法和屬性上,hidden 設定為 true 可以隱藏該屬性
@ApiParam("xxx引數說明") 作用在引數、方法和欄位上,類似 @ApiModelProperties
@ApiOperation("xp的介面")
@PostMapping("/xp")
@ResponseBody
public String xp(@ApiParam("這個名字會被返回")String username){
   return username;
}

這樣的話,可以給一些比較難理解的屬性或者介面,增加一些配置資訊,讓人更容易閱讀!

相較於傳統的 Postman 或 Curl 方式測試介面,使用 swagger 簡直就是傻瓜式操作,不需要額外說明文件(寫得好本身就是文件)而且更不容易出錯,只需要錄入資料然後點選 Execute。如果再配合自動化框架,可以說基本就不需要人為操作了。

Swagger 是個優秀的工具,現在國內已經有很多中小心網際網路公司都在使用它。相較於傳統的要先出 Word 介面文件再測試的方式,顯然這樣也更符合現在的快速迭代開發行情。當然了,提醒下大家再正式環境要記得關閉 Swagger ,一來處於安全考慮,而來也可以節省執行時記憶體。

17. 非同步、定時、郵件任務

在我們的工作中,常常會用到非同步處理任務,比如我們在網站上傳送郵件,後臺會去傳送右鍵,此時前臺會照成響應不動。知道郵件傳送完畢,響應才會成功。所以我們一般會採用多執行緒的方式去處理這些任務。還有一些定時任務,比如需要在每天凌晨的時候,分析一次前一天的日誌資訊。還有就是郵件的傳送,微信的前生也是郵件服務呢,這些東西是怎麼實現的呢?其實 SpringBoot 都給我們提供了對應的支援,我們上手使用十分的簡單,只需要開啟一些註解支援,配置一些配置檔案即可!那我們來看看吧。 ——引自狂神說

17.1 非同步任務

  1. 建立一個 AsyncService

    先建立一個 Service 包,然後在這個包下建立 AsynService 類。

    非同步處理還是非常常用的,比如我們在網站上傳送郵件,後臺回去傳送郵件。此時前臺會造成響應不動,直到郵件傳送完畢,響應才會成功。所以我們一般會採用多執行緒的方式去處理這些任務。

    編寫方法,假裝正在處理資料,使用執行緒設定一些延時,模擬同步等待的情況。

    @Service
    public class AsyncService {
    
        public void hello(){
            try {
                Thread.sleep(3000);
            } catch (InterruptedException e) {
                e.printStackTrace();
            }
            System.out.println("業務進行中。。。。");
        }
    
    }
    
  2. 建立 AsyncController

    在 controller 包下建立 AsyncController 類,編寫介面,模擬發郵件的延遲

    @RestController
    public class AsyncController {
    
        @Autowired
        AsyncService asyncService;
    
        @RequestMapping("/hello")
        public String hello() {
            asyncService.hello();
            return "success";
        }
    
    }
    
  3. 啟動 SpringBoot 應用程式,訪問介面

    我們可以發現,我們請求 /hello 介面後,頁面並沒有馬上跳轉,也沒有馬上重新整理,瀏覽器頁面阻塞。得等到後臺響應後才會進行跳轉和重新整理。這樣使用者體驗就非常不好了!

  4. 給 hello 方法新增 @Async 註解

    我們如果想讓使用者直接得到訊息,就在後臺使用多執行緒得方式進行處理即可,但是每次都需要自己手動去編寫多執行緒得實現的話,太麻煩了。我們只需要用一個簡單的辦法,在我們的方法上加上一個簡單的註解即可,如下:

    @Async // 高數 Spring 這是一個非同步方法
    @RequestMapping("/hello")
    public String hello() {
        asyncService.hello();
        return "success";
    }
    

    SpringBoot 就會自己開一個執行緒池,進行呼叫!但是要讓這個註解生效,我們還需要再主程式類上新增一個註解 @EnableAsync ,開啟非同步註解功能

    @EnableAsync // 開啟非同步註解功能
    @SpringBootApplication
    public class Springboot01Application {
        public static void main(String[] args) {
            SpringApplication.run(Springboot01Application.class, args);
        }
    }
    
  5. 重啟 SpringBoot 應用程式,發現網頁是瞬間響應,後臺程式碼一就執行!

17.2 定時任務

專案開發中經常需要執行一些定時任務,比如需要再每天凌晨的時候,分析一次前一天的日誌資訊,Spring 為我們提供了非同步執行任務排程的方式,提供了兩個介面。

  • TaskExecutor 介面
  • TaskScheduler 介面

兩個註解

  • @EnableScheduling
  • @Scheduled

cron 表示式:

欄位 允許值 允許的特殊字元
0-59 ,-*/
0-59 ,-*/
小時 0-23 ,-*/
日期 1-31 ,-*?/L W C
月份 1-12 ,-*/
星期 0-7或SUN-SAT 0,7是SUN ,-*?/L W C
特殊字元 代表含義
, 列舉
- 區間
* 任意
/ 步長
? 日/星期衝突匹配
L 最後
W 工作日
C 和 calendar 聯絡後計算過的值
# 星期,4#2,第2個星期三

測試步驟:

  1. 建立 ScheduledService

    在 service 包下,建立一個 ScheduledService 類,類裡面有一個 hello() 方法,並設定什麼時候定時執行

    @Service
    public class ScheduledService {
    
        // 秒 分 時 日 月 周幾
        // 0  *  * *  *  0-7
        // 注意 cron 表示式的用法
        @Scheduled(cron = "0 50 19 * * 0-7")
        public void hello() {
            // 定義格式化規則
            SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
            // 格式化現在的時間並輸出
            System.out.println(sdf.format(System.currentTimeMillis()) + ",hello!");
        }
    
    }
    
  2. 開啟定時任務功能

    我們寫完定時任務後,需要在主程式類上增加 @EnableScheduling 註解開啟定時任務功能

    @EnableScheduling // 開啟定時任務功能
    @EnableAsync // 開啟非同步註解功能
    @SpringBootApplication
    public class Springboot01Application {
        public static void main(String[] args) {
            SpringApplication.run(Springboot01Application.class, args);
        }
    }
    
  3. 啟動 SpringBoot 主程式類,測試

  4. 瞭解 cron 表示式

    http://www.bejson.com/othertools/cron/

  5. 常用的 cron 表示式

    (1)0/2 * * * * ?   表示每2秒 執行任務
    (1)0 0/2 * * * ?   表示每2分鐘 執行任務
    (1)0 0 2 1 * ?   表示在每月的1日的凌晨2點調整任務
    (2)0 15 10 ? * MON-FRI   表示週一到週五每天上午10:15執行作業
    (3)0 15 10 ? 6L 2002-2006   表示2002-2006年的每個月的最後一個星期五上午10:15執行作
    (4)0 0 10,14,16 * * ?   每天上午10點,下午2點,4點
    (5)0 0/30 9-17 * * ?   朝九晚五工作時間內每半小時
    (6)0 0 12 ? * WED   表示每個星期三中午12點
    (7)0 0 12 * * ?   每天中午12點觸發
    (8)0 15 10 ? * *   每天上午10:15觸發
    (9)0 15 10 * * ?     每天上午10:15觸發
    (10)0 15 10 * * ?   每天上午10:15觸發
    (11)0 15 10 * * ? 2005   2005年的每天上午10:15觸發
    (12)0 * 14 * * ?     在每天下午2點到下午2:59期間的每1分鐘觸發
    (13)0 0/5 14 * * ?   在每天下午2點到下午2:55期間的每5分鐘觸發
    (14)0 0/5 14,18 * * ?     在每天下午2點到2:55期間和下午6點到6:55期間的每5分鐘觸發
    (15)0 0-5 14 * * ?   在每天下午2點到下午2:05期間的每1分鐘觸發
    (16)0 10,44 14 ? 3 WED   每年三月的星期三的下午2:10和2:44觸發
    (17)0 15 10 ? * MON-FRI   週一至週五的上午10:15觸發
    (18)0 15 10 15 * ?   每月15日上午10:15觸發
    (19)0 15 10 L * ?   每月最後一日的上午10:15觸發
    (20)0 15 10 ? * 6L   每月的最後一個星期五上午10:15觸發
    (21)0 15 10 ? * 6L 2002-2005   2002年至2005年的每月的最後一個星期五上午10:15觸發
    (22)0 15 10 ? * 6#3   每月的第三個星期五上午10:15觸發
    

17.3 郵件任務

郵件傳送,在我們的日常開發中,也非常的多,SpringBoot 也幫我們做了支援

  • 郵件傳送需要引入 spring-boot-start-email
  • SpringBoot 自動配置 MailSenderAutoConfiguration
  • 自定義 MailProperties 內容,配置在 application.yml 中
  • 自動配置 JavaMailSender
  • 測試郵件傳送

測試

  1. 引入 pom 依賴

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-mail</artifactId>
    </dependency>
    

    看它引入的依賴,可以看到 jakarta.mail 包

    <dependency>
      <groupId>com.sun.mail</groupId>
      <artifactId>jakarta.mail</artifactId>
      <scope>compile</scope>
    </dependency>
    
  2. 檢視自動配置類:MailSenderJndiConfiguration

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass({ MimeMessage.class, MimeType.class, MailSender.class })
    @ConditionalOnMissingBean(MailSender.class)
    @Conditional(MailSenderCondition.class)
    @EnableConfigurationProperties(MailProperties.class)
    // 引入 MailSenderJndiConfiguration 類,這個類並沒有註冊bean,看一下它匯入 的其他類
    @Import({ MailSenderJndiConfiguration.class, MailSenderPropertiesConfiguration.class })
    public class MailSenderAutoConfiguration {
    }
    

    這個類中存在bean,JavaMailSenderImpl

    @Bean
    JavaMailSenderImpl mailSender(Session session) {
       JavaMailSenderImpl sender = new JavaMailSenderImpl();
       sender.setDefaultEncoding(this.properties.getDefaultEncoding().name());
       sender.setSession(session);
       return sender;
    }
    

    然後我們去看下配置檔案

    @ConfigurationProperties(prefix = "spring.mail")
    public class MailProperties {
    
       private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
    
       /**
        * SMTP server host. For instance, `smtp.example.com`.
        */
       private String host;
    
       /**
        * SMTP server port.
        */
       private Integer port;
    
       /**
        * Login user of the SMTP server.
        */
       private String username;
    
       /**
        * Login password of the SMTP server.
        */
       private String password;
    
       /**
        * Protocol used by the SMTP server.
        */
       private String protocol = "smtp";
    
       /**
        * Default MimeMessage encoding.
        */
       private Charset defaultEncoding = DEFAULT_CHARSET;
    
       /**
        * Additional JavaMail Session properties.
        */
       private Map<String, String> properties = new HashMap<>();
    
       /**
        * Session JNDI name. When set, takes precedence over other Session settings.
        */
       private String jndiName;
    
  3. 配置檔案

    mail:
      username: [email protected]
      password: 你的qq授權碼
      host: smtp.qq.com
      # qq需要配置ssl
      properties:
        mail:
          smtp:
            ssl:
              enable: true
    

    獲取授權碼:在 QQ 郵箱中的設定 -> 賬戶 -> 開啟 pop3和smtp 服務

  4. 測試

    在 SpringBoot 測試類中編寫如下測試程式碼

    @SpringBootTest
    class Springboot01ApplicationTests {
    
        @Autowired
        JavaMailSenderImpl javaMailSender;
    
        // 郵件設定1:一個簡單的郵件
        @Test
        void test1(){
            // 設定傳送內容
            SimpleMailMessage message = new SimpleMailMessage();
            message.setSubject("標題:這裡是傳送郵件的標題");
            message.setText("內容:這是傳送郵件的內容");
            // 設定傳送郵箱和接收郵箱
            message.setTo("[email protected]");
            message.setFrom("[email protected]");
            javaMailSender.send(message);
        }
    
        //郵件設定2:一個複雜的郵件
        @Test
        void test2() throws MessagingException {
            MimeMessage mimeMessage = javaMailSender.createMimeMessage();
            MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true);
    
            helper.setSubject("標題:這裡是傳送郵件的標題");
            helper.setText("<p style='color:red'>內容:這是傳送郵件的內容</p>",true);
    
            // 傳送附件
            helper.addAttachment("1.jpg",new File("1.jpg"));
            helper.addAttachment("2.jpg",new File("1.jpg"));
    
            helper.setTo("[email protected]");
            helper.setFrom("[email protected]");
    
            javaMailSender.send(mimeMessage);
        }
    
    }
    

    點選執行,然後在郵箱中檢視是否傳送成功

18. Dubbo 和 ZooKeeper 整合

18.1 ZooKeeper 下載與安裝

1. 進入官網下載 ZooKeeper

ZooKeeper 官網地址: https://zookeeper.apache.org/

點選上面官網連結進入官網

跳轉頁面後,點選下載

2. Zookeeper 安裝

  1. 下載完後直接解壓,解壓後目錄如下:

  2. 建立一個 data 資料夾和 log 資料夾

  3. 進入 config 資料夾,將 zoo_sample.cfg 複製一份,並重命名 zoo.cfg

  4. 修改 zoo.cfg 內的配置資訊

    dataDir 和 dataLogDir 根據自己的真實路徑填寫。就是我們剛剛建立的 data 和 log 資料夾的位置

    # The number of milliseconds of each tick
    tickTime=2000
    # The number of ticks that the initial 
    # synchronization phase can take
    initLimit=10
    # The number of ticks that can pass between 
    # sending a request and getting an acknowledgement
    syncLimit=5
    # the directory where the snapshot is stored.
    # do not use /tmp for storage, /tmp here is just 
    # example sakes.
    dataDir=D:\\Code\\tool\\java\\zookeeper-3.4.14\\zookeeper-3.4.14\\data
    dataLogDir=D:\\Code\\tool\\java\\zookeeper-3.4.14\\zookeeper-3.4.14\\log
    # the port at which the clients will connect
    clientPort=2181
    # the maximum number of client connections.
    # increase this if you need to handle more clients
    #maxClientCnxns=60
    #
    # Be sure to read the maintenance section of the 
    # administrator guide before turning on autopurge.
    #
    # http://zookeeper.apache.org/doc/current/zookeeperAdmin.html#sc_maintenance
    #
    # The number of snapshots to retain in dataDir
    #autopurge.snapRetainCount=3
    # Purge task interval in hours
    # Set to "0" to disable auto purge feature
    #autopurge.purgeInterval=1
    

3. 執行 ZooKeeper

  1. 進入 bin 目錄,啟動 zkServer.cmd

    如果閃退的話,就代表啟動失敗,可能是我們剛剛配置錯誤了。

    編輯 zkServer.cmd 檔案,增加 @pause 讓它報錯時停下來,然後根據報錯百度查詢解決方案。

    java.io.IOException: Unable to create data directory XXX 代表是檔案路徑錯誤了,檢查 dataDir 和 dataLogDir 的路徑

    @echo off
    REM Licensed to the Apache Software Foundation (ASF) under one or more
    REM contributor license agreements.  See the NOTICE file distributed with
    REM this work for additional information regarding copyright ownership.
    REM The ASF licenses this file to You under the Apache License, Version 2.0
    REM (the "License"); you may not use this file except in compliance with
    REM the License.  You may obtain a copy of the License at
    REM
    REM     http://www.apache.org/licenses/LICENSE-2.0
    REM
    REM Unless required by applicable law or agreed to in writing, software
    REM distributed under the License is distributed on an "AS IS" BASIS,
    REM WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    REM See the License for the specific language governing permissions and
    REM limitations under the License.
    
    setlocal
    call "%~dp0zkEnv.cmd"
    
    set ZOOMAIN=org.apache.zookeeper.server.quorum.QuorumPeerMain
    echo on
    call %JAVA% "-Dzookeeper.log.dir=%ZOO_LOG_DIR%" "-Dzookeeper.root.logger=%ZOO_LOG4J_PROP%" -cp "%CLASSPATH%" %ZOOMAIN% "%ZOOCFG%" %*
    @pause
    endlocal
    
  2. 執行成功

4. 測試

  1. 雙擊執行 ZooKeeper 客戶端 zkCli.cmd

  2. 檢視所有節點

    注意 ls 後面有個空格

    ls /
    

  3. 建立一個節點

    建立一個我們自己的節點,然後放入存入的值

    create -e /xp test
    

    然後我們再用 ls / 命令檢視所有的節點

    可以發現這裡多了我們剛剛自己註冊的節點

  4. 獲取節點的值

    get /xp
    

    可以發現,我們在建立節點時給的值獲取出來了

18.2 dubbo-admin

dubbo 本身並不是一個服務軟體。它其實就是一個 jar 包,能夠幫你的 java 程式連線到 ZooKeeper,並利用 ZooKeeper 進行消費、提供服務。

但是為了讓使用者更好地管理監控眾多的 dubbo 服務,官方提供了一個視覺化的監控程式 dubbo-admin,不過這個監控即使不裝也不影響使用。

我們這裡來安裝一下:

1. 下載 dubbo-admin 並解壓

官網下載地址:https://github.com/apache/dubbo-admin/tree/master

2. 指定 ZooKeeper 地址

修改 dubbo-admin\src\main\resources \application.properties 指定zookeeper地址。

一般預設已經指向 ZooKepper 地址了。

這裡可以自定義設定一些內容,比如 dubbo-admin 登入密碼等。

server.port=7001
spring.velocity.cache=false
spring.velocity.charset=UTF-8
spring.velocity.layout-url=/templates/default.vm
spring.messages.fallback-to-system-locale=false
spring.messages.basename=i18n/message
spring.root.password=root
spring.guest.password=guest
# 指向 ZooKeeper 地址
dubbo.registry.address=zookeeper://127.0.0.1:2181

3. 使用 maven 打成 jar 包

使用 cmd 進入剛剛解壓的根目錄,並使用 maven 打成 jar 包

mvn clean package -Dmaven.test.skip=true

第一次打包時間比較長,耐心等待即可

也可將 Maven 切換成阿里源,或者使用 IDEA 進行打包。

4. 執行 jar 包

打完 jar 包後,先開啟 ZooKeeper ,然後使用 cmd 進入 D:\Code\tool\java\dubbo-admin-master\dubbo-admin-master\dubbo-admin\target (根據自己的實際目錄進入,dubbo-admin的 target包下)

輸入 java -jar dubbo-admin-0.0.1-SNAPSHOT.jar 命令,啟動 jar 包

java -jar dubbo-admin-0.0.1-SNAPSHOT.jar

然後我們就可以看到 dubbo-admin SpringBoot 專案啟動了

5. 開啟 ZooKeeper 客戶端,檢視節點資訊

我們雙擊開啟 ZooKeeper 客戶端 zkCli.cmd ,使用 ls / 命令檢視 ZooKeeper 中的節點資訊

如果沒有顯示,可能時延遲問題,就在這三個 dos 視窗中敲回車

6. 訪問 dubbo-admin

我們在瀏覽器中輸入 http://localhost:7001/ 來訪問 dubbo-admin

這裡的賬號密碼,是我們剛剛 application.properties 中設定的。預設賬號是 root,密碼是 root

輸入賬號和密碼後,成功進入到 dubbo-admin 頁面

18.3 SpringBoot + Dubbo + Zookeeper

18.3.1 環境搭建

  1. 建立兩個 SpringBoot 工程,分別是 consumer-server(服務消費者)、 provider-server(服務提供者),建立時都新增 web 依賴

  2. 引入依賴

    兩個工程專案引入以下依賴

    <!-- dubbo -->
    <dependency>
        <groupId>org.apache.dubbo</groupId>
        <artifactId>dubbo-spring-boot-starter</artifactId>
        <version>2.7.6</version>
    </dependency>
    <!-- zkclient ZooKeeper -->
    <dependency>
        <groupId>com.101tec</groupId>
        <artifactId>zkclient</artifactId>
        <version>0.10</version>
    </dependency>
    <!-- 引入zookeeper -->
    <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-framework</artifactId>
        <version>2.12.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.curator</groupId>
        <artifactId>curator-recipes</artifactId>
        <version>2.12.0</version>
    </dependency>
    <dependency>
        <groupId>org.apache.zookeeper</groupId>
        <artifactId>zookeeper</artifactId>
        <version>3.4.14</version>
        <!--排除這個slf4j-log4j12-->
        <exclusions>
            <exclusion>
                <groupId>org.slf4j</groupId>
                <artifactId>slf4j-log4j12</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    

18.3.2 服務提供者

1. 提供服務介面

在 provider-server (服務提供者)這個工程專案中建立 service 包,並在包下建立 TicketService 介面以及其實現類 TicketServiceImpl

TicketService

public interface TicketService {
	// 獲取票的方法
    String getTicket();

}

TicketServiceImpl

:這裡的 @Service 是 org.apache.dubbo.config.annotation 包下的註解,不要導錯了!

由於 @Service 註解同名了,所以我們使用 @Component 將這個服務註冊到 Spring 容器中

邏輯理解:應用啟動起來,dubbo就會掃描指定包下帶有 @Component 註解的服務,將它釋出在指定的註冊中心!

package com.xp.service;

import org.apache.dubbo.config.annotation.Service;
import org.springframework.stereotype.Component;

@Service    // 將服務釋出出去
@Component  // 註冊到 Spring 容器中
public class TicketServiceImpl implements TicketService {
    @Override
    public String getTicket() {
        return "買到票了";
    }
}
2. 修改 SpringBoot 配置檔案

application.properties

將服務註冊到 ZooKeeper 中

:如果顯示無法連線 dubbo 的,可能是連線超時,試試將超時時間延長。如果沒有設定連線超時時間,預設是 3 秒鐘。

# 配置 Tomcat 伺服器啟動埠
server.port=8080
# 當前應用名字
dubbo.application.name=provider-server
# 註冊中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
# 哪些服務需要被註冊
dubbo.scan.base-packages=com.xp.service
# 設定連線超時時間
dubbo.config-center.timeout=10000

18.3.3 服務消費者

1. 本來正常步驟是將服務提供者的介面打包,然後用 pom 檔案匯入,我們這裡使用簡單的方式,直接將服務的介面拿過來,路徑必須保證正確,即和服務提供者相同

TicketService 介面必須和服務提供者的包路徑一致!

2. 建立消費服務類

建立消費服務類 UserService 來消費服務提供者提供的服務

UserService

:這裡的 @Service 是 Spring 的註解

@Service    // 註冊到 Spring 容器鍾
public class UserService {

    @Reference  // 遠端引用指定的服務,他會按照全類名進行匹配,看誰給註冊中心註冊了這個全類名
    TicketService ticketService;

    public void buyTicket(){
        String ticket = ticketService.getTicket();
        System.out.println("在註冊中心買到" + ticket);
    }

}
3. 修改 SpringBoot 配置檔案

application.properties

將服務註冊到 ZooKeeper 中

:如果顯示無法連線 dubbo 的,可能是連線超時,試試將超時時間延長。如果沒有設定連線超時時間,預設是 3 秒鐘。

# 配置 Tomcat 伺服器啟動埠
server.port=8081
# 當前應用名字
dubbo.application.name=consumer-server
# 註冊中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
# 設定超時時間
dubbo.config-center.timeout=10000
4. 編寫測試類
@SpringBootTest
class ConsumerServerApplicationTests {

    @Autowired
    UserService userService;

    @Test
    void contextLoads() {
        userService.buyTicket();
    }

}

16.3.4 測試

1. 啟動 ZooKeeper 註冊中心

雙擊執行 zkServer.cmd ,啟動 ZooKeeper 註冊中心服務

2. 啟動 dubbo(也可以不啟動)

使用 cmd 執行 dubbo-admin 並登入到 dubbo-admin 視覺化監控介面

java -jar dubbo-admin-0.0.1-SNAPSHOT.jar
3. 啟動 服務提供者 SpringBoot 應用程式

4. 點選 dubbo-admin 服務 檢視服務是否註冊

5. 執行 服務消費者 的測試類

成功消費了 服務提供者 的服務

這就是 SpringBoot + dubbo + ZooKeeper 實現分散式開發的應用,其實就是一個服務拆分的思想

19. 整合 SpringSecurity

19.1 安全簡介

在 Web 開發中,安全一直是非常重要的一個方面。安全雖然屬於應用的非功能性需求,但是應該在應用開發的初期就考慮進來。如果在應用開發的後期才考慮安全問題,就可能陷入一個兩難的境地:一方面,應用存在嚴重的安全漏洞,無法滿足使用者的要求,並可能造成使用者的隱私資料被攻擊者竊取;另一方面,應用的基本架構已經確定,要修負安全漏洞,可能需要對系統的架構做出比較重大的調整,因而需要更多的開發時間,影響應用釋出程序。因此,從應用開發的第一天就應該把安全相關的因素考慮進來,並在整個應用的開發過程中。

市面上存在比較有名的安全框架:ShiroSpring Security

這裡需要闡述以下的是,每一個框架的出現都是為了解決某一問題而產生的,那麼 Spring Security 框架出現是為了解決什麼問題呢?

首先,我們看下它的官網介紹:

Spring Security 官網地址: https://spring.io/projects/spring-security

Spring Security is a powerful and highly customizable authentication and access-control framework.It is the de-facto standard for securing Spring-based application.

Spring Security 是一個功能強大且高度可定製的身份驗證和訪問控制框架。它實際是保護基於 Spring 的應用程式的標準。

Spring Security is a framework that focuses on providing both authentication and authorization to Java applications. Like all Spring projects, the real power of Spring Security is found in how easily it can be extended to meet custom requirements.

Spring Security 是一個框架,側重於為 Java 應用程式提供身份驗證和授權。與所有 Spring 專案一樣,Spring 安全性真正強大之處在於它可以輕鬆地擴充套件以滿足定製需求。

從官網的我介紹中可以知道這是一個許可權框架。像我們之前做專案是沒有使用框架是怎麼控制權限的?對於許可權,一般會細分為功能許可權,訪問許可權和選單許可權。程式碼會寫得非常的繁瑣,冗餘。

怎麼解決之前寫許可權程式碼繁瑣,冗餘的問題,一些主流框架就應運而生。Spring Security 就是其中的一種。

Spring 是一個非常流行和成功的 Java 應用開發框架。Spring Security 基於 Spring 框架,提供了一套 Web 應用安全性的完整解決方案。一般來說,Web 應用的安全性包括使用者認證(Authentication)和使用者授權(Authorization)兩個部分。使用者認證一般要求使用者提供使用者名稱許可權執行某個操作。在一個系統中,不同使用者所具有的許可權是不同的。比如對一個檔案來說,有的使用者只能進行讀取,而有的使用者可以進行修改。一般來說,系統會為不同的使用者分配不同的角色,而每個角色則對應一系列的許可權。

對於上面提到的兩種應用情景,Spring Security 框架都有很好的支援。在使用者認證方面, Spring Security 框架支援主流的認證方式,包括 HTTP 基本認證、HTTP 表單驗證、HTTP 摘要認證、OpenID 和 LDAP 等。在使用者授權方面, Spring Security 提供了基於角色的訪問控制和訪問控制列表(Access Control List ACL),可以對應用中的領域物件進行細粒度的控制。

19.2 認識 SpringSecurity

Spring Security 是針對 Spring 專案的安全框架,也是 SpringBoot 底層安全模組預設的技術選型,他可以實現強大的 Web 安全檢測,對於安全控制,我們僅需要引入 spring-boot-starter-security 模組,進行少量的配置,即可實現強大的安全管理!

記住幾個類:

  • WebSecurityConfigurerAdapter:自定義 Security 策略
  • AuthenticationManagerBuilder:自定義認證策略
  • @EnableWebSecurity:開啟 WebSecurity 模式

Spring Security 的兩個主要目的是 “認證” 和 “授權” (訪問控制)

“認證”(Authentication)

身份驗證是關於驗證您的憑據,如使用者名稱/使用者ID和密碼,以驗證您的身份

“授權”(Authorization)

授權發生在系統成功驗證您的身份猴,最終會授權您訪問資源(如資訊,檔案,資料庫,資金,位置,幾乎任何內容)的完全許可權

這個概念是通用的,而不是隻在 Spring Security 中存在。

19.3 實戰測試

19.3.1 環境搭建

  1. 新建一個 SpringBoot 專案,選擇 Web 模組和 Thymeleaf 模組。

  2. 編寫靜態頁面

    已經放在百度網盤中了,需要的可以自行下載(來源:b站 狂神說以及其 SpringBoot 視訊底下的評論區)

    連結:https://pan.baidu.com/s/1XlISo7vQpiO_DisMkrcbpw
    提取碼:q7bd

  3. 編寫 Controller

    建立一個 Controller JumpController 進行頁面跳轉

    @Controller
    public class JumpController {
    
        @RequestMapping({"/","/index"})
        public String index(){
            return "index";
        }
    
        @RequestMapping("/level1/{id}")
        public String level1(@PathVariable("id") Integer id){
            return "views/level1/"+id;
        }
    
        @RequestMapping("/level2/{id}")
        public String level2(@PathVariable("id") Integer id){
            return "views/level2/"+id;
        }
    
        @RequestMapping("/level3/{id}")
        public String level3(@PathVariable("id") Integer id){
            return "views/level3/"+id;
        }
    
    }
    
  4. 測試

啟動 SpringBoot 應用程式,測試是否能夠成功跳轉

19.3.2 認證和授權

  1. 引入依賴
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
</dependency>
  1. 編寫 Spring Security 配置類

    官方參考文件:https://docs.spring.io/spring-security/site/docs/5.3.3.BUILD-SNAPSHOT/reference/html5/#servlet-applications

    點進 WebSecurityConfigurerAdapter 這個類,可以看到有關於配置的方法 configure(),他有幾個過載的方法,都是用來配置 Spring Security 的

    /**
     * Override this method to configure the {@link HttpSecurity}. Typically subclasses
     * should not invoke this method by calling super as it may override their
     * configuration. The default configuration is:
     *
     * <pre>
     * http.authorizeRequests().anyRequest().authenticated().and().formLogin().and().httpBasic();
     * </pre>
     *
     * @param http the {@link HttpSecurity} to modify
     * @throws Exception if an error occurs
     */
    // @formatter:off
    protected void configure(HttpSecurity http) throws Exception {
       logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
    
       http
          .authorizeRequests()
             .anyRequest().authenticated()
             .and()
          .formLogin().and()
          .httpBasic();
    }
    // @formatter:on
    

    根據原始碼中的配置,我們自定義我們自己的訪問規則

    建立我們自定義的 Spring Security 的配置類 SecurityConfig ,讓這個類繼承 WebSecurityConfigurerAdapter

    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
    import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
    
    @EnableWebSecurity
    public class SecurityConfig extends WebSecurityConfigurerAdapter {
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            // 自定義請求的授權規則
            // 首頁所有人都可以訪問
            http.authorizeRequests().antMatchers("/").permitAll()
                    .antMatchers("/level1/**").hasRole("vip1")
                    .antMatchers("/level2/**").hasRole("vip2")
                    .antMatchers("/level3/**").hasRole("vip3");
            // 設定登入頁和登入請求的頁面
            http.formLogin().loginProcessingUrl("/user/login").loginPage("/toLogin");
            // 開啟登出功能,並設定登出後跳轉的路徑
            http.logout().logoutSuccessUrl("/");
        }
    }
    
  2. 啟動測試:發現除了首頁和登入頁,其他的都跳去了登入頁。因為我們目前沒有登入的角色。其他頁面的訪問請求需要登入的角色擁有對應的許可權才可以

    此時我們是無法登入的,因為我們並沒有設定登入的賬號密碼和對應的角色全新啊

  3. 模擬資料庫登入認證規則

    根據配置類的註釋資訊,我們模擬資料庫的資料

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 模擬資料庫中的資料
        // 現在這些資料是在記憶體中的,實際開發中我們頁可以設定在jdbc中拿
        auth.inMemoryAuthentication().withUser("admin").password"admin").roles("vip1","vip2","vip3");
        auth.inMemoryAuthentication().withUser("xp").password("xp").roles("vip1","vip2");
        auth.inMemoryAuthentication().withUser("guest").password("12345").roles("vip1");
    }
    
  4. 重啟 SpringBoot 應用程式

    此時我們會發現我們還是不能登入認證,並且頁面是 404

    這個的原因是因為我們要將前端傳來的密碼進行某種方式加密,否則就無法登入。

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        // 模擬資料庫中的資料
        // 現在這些資料是在記憶體中的
        // Spring Security 5.0 中新增了多種加密方式,官方推薦使用 bcrypt 加密方式
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("admin").password(new BCryptPasswordEncoder().encode("admin")).roles("vip1","vip2","vip3");
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("xp").password(new BCryptPasswordEncoder().encode("xp")).roles("vip1","vip2");
        auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder()).withUser("guest").password(new BCryptPasswordEncoder().encode("12345")).roles("vip1");
    }
    
  5. 增加密碼加密後,重啟 SpringBoot 應用程式。登入測試是否能夠根據角色訪問對應的頁面資源

19.3.3 許可權控制和登出

  1. 開啟自動配置的登出的功能
//定製請求的授權規則
@Override
protected void configure(HttpSecurity http) throws Exception {
  //....
  //開啟自動配置的登出的功能
  // /logout 登出請求
  http.logout();
}
  1. 我們在前端,index.html 增加一個登出的按鈕
<a class="item" th:href="@{/logout}">
	<i class="address card icon"></i> 登出
</a>
  1. 啟動 SpringBoot 應用程式測試

    在前端頁面點選登出按鈕測試是否成功跳轉了,預設是請求 `` 跳轉到登入頁面

    如果沒有成功跳轉而是跳到了 404 頁面,則可能是 csrf 阻止了我們使用 get 方式提交(a標籤的 href 屬性的跳轉是 get 請求的跳轉)

    因為 SpringSecurity 預設防止 csrf 跨站請求偽造,因為會產生安全問題。我們可以將請求改為 post 表單提交,或者在 SpringSecurity 中關閉 csrf 功能。關閉 csrf 的程式碼如下:

    // 關閉 SpringSecurity 的csrf 功能
    http.csrf().disable();
    
  2. 我們現在又來一個需求:使用者沒有登入的時候,導航欄只顯示登入按鈕,使用者登入之後,導航欄可以顯示登入的使用者資訊及登出按鈕!還有就是,比如 某個使用者,只有部分許可權,那麼登入時則只顯示這幾個功能,其他沒有許可權的功能不顯示。這個就是真實的網站情況了。那我們該如何做呢?

    我們需要結合 thymeleaf 中的一些功能

    sec:authorize = "isAuthenticated()"是否認證登入,來顯示不同頁面

    需引入 thymeleaf-extras-springsecurity4 依賴

    <!-- thymeleaf-extras-springsecurity4 -->
    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity4</artifactId>
        <version>3.0.4.RELEASE</version>
    </dependency>
    

    降級 SpringBoot 的版本,thymeleaf-extras-springsecurity4 SpringBoot 2.1.X 以上的版本不支援。

    或者使用 thymeleaf-extras-springsecurity5

    <!-- thymeleaf-extras-springsecurity5 -->
    <dependency>
        <groupId>org.thymeleaf.extras</groupId>
        <artifactId>thymeleaf-extras-springsecurity5</artifactId>
        <version>3.0.4.RELEASE</version>
    </dependency>
    

    :使用 thymeleaf-extras-springsecurity5 時,匯入的名稱空間是 xmlns:sec="http://www.thymeleaf.org/extras/spring-security" 。使用 thymeleaf-extras-springsecurity4 時為 xmlns:th="http://www.thymeleaf.org/thymeleaf-extras-springsecurity4"

  3. 修改前端頁面,測試是否達到需求的效果

    登入區域的許可權認證

     <!--登入登出-->
    <div class="right menu" sec:authorize="!isAuthenticated()">
        <!--未登入-->
        <a class="item" th:href="@{/toLogin}" >
            <i class="address card icon"></i> 登入
        </a>
        <a class="item" th:href="@{/register}">
            <i class="address card icon"></i> 註冊
        </a>
    </div>
    <div class="right menu" sec:authorize="isAuthenticated()">
        <!--未登入-->
        <a class="item" >
            <i class="address card icon"></i>
            使用者名稱:<span sec:authentication="principal.username"></span>
            角色:<span sec:authentication="principal.authorities"></span>
        </a>
        <a class="item" th:href="@{/logout}">
            <i class="address card icon"></i> 登出
        </a>
    </div>
    

    頁面的許可權認證

Level 1

Level-1-1 Level-1-2 Level-1-3
           <div class="column" sec:authorize="hasRole('vip2')">
               <div class="ui raised segment">
                   <div class="ui">
                       <div class="content">
                           <h5 class="content">Level 2</h5>
                           <hr>
                           <div><a th:href="@{/level2/1}"><i class="bullhorn icon"></i> Level-2-1</a></div>
                           <div><a th:href="@{/level2/2}"><i class="bullhorn icon"></i> Level-2-2</a></div>
                           <div><a th:href="@{/level2/3}"><i class="bullhorn icon"></i> Level-2-3</a></div>
                       </div>
                   </div>
               </div>
           </div>

           <div class="column" sec:authorize="hasRole('vip3')">
               <div class="ui raised segment">
                   <div class="ui">
                       <div class="content">
                           <h5 class="content">Level 3</h5>
                           <hr>
                           <div><a th:href="@{/level3/1}"><i class="bullhorn icon"></i> Level-3-1</a></div>
                           <div><a th:href="@{/level3/2}"><i class="bullhorn icon"></i> Level-3-2</a></div>
                           <div><a th:href="@{/level3/3}"><i class="bullhorn icon"></i> Level-3-3</a></div>
                       </div>
                   </div>
               </div>
           </div>

       </div>


**注**:若是沒有效果,則降低 SpringBoot 的版本,`sec:authorize="isAuthenticated()"` 在 SpringBoot 2.1.X 以上版本不再支援了

**注:** 使用 SpringSecurity 和 Thymeleaf 的包容易出現版本不相容的問題,所以如果出現不能達到預期效果的時候,先檢查自己引入的整合包的網址是否出錯了(雖然這個一般不影響),其次是檢查自己寫的標籤是否有問題,比如是否單詞拼寫錯誤,是否少了個單詞,這些都是我們自以為是正確的,往往可能就是這個出問題了。最後再檢查版本的問題,如果使用的整合包是 4 的,則需要降 SpringBoot 的版本,如果是 5 的版本,我之前測試有時候也有問題,但也不知道怎麼的就又好了。



## 20. Shiro

### 20.1 簡介

#### 1. 什麼是許可權管理

許可權管理屬於系統安全的範疇,許可權管理實現對使用者訪問系統的控制,按照安全規則,按照安全規則或者安全策略控制使用者可以訪問而且只能訪問自己被授權的資源。

許可權管理包括使用者身份認證和授權兩部分。對於需要訪問控制的資源使用者首先經過身份認證,認證通過後使用者具有該資源的訪問許可權方可訪問。



#### 2. 常見的安全框架

- Apache Shiro

官網:http://shiro.apache.org/

Apache Shiro是一個功能強大且易於使用的Java安全框架,可執行身份驗證、授權、加密和繪畫管理。較輕量級,入門簡單,不依賴於 Spring 框架,傳統的 SSM 專案使用較多。可能沒有 Spring Security 做的功能強大,但是在實際工作時可能並不需要那麼複雜的東西,所以使用小而簡單的 Shiro 就足夠了。

- Spring Security

官網:https://spring.io/projects/spring-security

Spring Security 是一個能夠為基於 Spring 的企業應用系統提供宣告式的安全訪問控制解決方案的安全框架。較複雜,入門較難,功能較強,屬於 Spring 技術模組,多用於 SpringBoot + SpringCloud 微服務分散式開發。

**Shiro 和 Spring Security 比較**

- Shiro 比 Spring Security 更容易使用,實現。
- Shiro 在 Spring Security 處理密碼學方面有一個額外的模組
- Shiro 不跟任何的框架或者容器繫結,可以獨立執行。
- Spring Security 是 Spring 的明星框架之一,與 Spring 結合較好。
- Spring Security 有更好的社群支援。



### 20.2 Shiro 特性

Apache Shiro 是一個具有很多特性的綜合性安全框架:

![image-20200726125939785](https://gitee.com/windows_xp_xp/picture_bed/raw/master/img/image-20200726125939785.png)

Shiro 的目表是 Shiro 開發團隊所稱的 “應用程式安全的四個基石” ——身份驗證、授權、會話管理和加密。

- **Authentication**:身份驗證、登入,驗證使用者是不是擁有響應的身份
- **Authorization**:授權,即許可權驗證。驗證某個已認證的使用者是否擁有某個許可權,即判斷使用者能否進行什麼操作。如:驗證某個使用者是否擁有某個角色,後者細粒度的驗證某個使用者對某個資源是否具有某個許可權。
- **SessionManager**:會話管理,即使用者登入猴就是第一次會話,在沒有退出之前,它的所有資訊都在會話中;會話可以是普通的 JavaSE化境,也可以時 Web 環境。
- **Cryptography**:加密,保護資料的安全性,如密碼加密儲存到資料庫中,而不是明文儲存

還有一些額外的特性可以在不同的應用程式環境中支援和加強這些關注點,特別是:

- Web Support:Shiro 的web 支援 API 可以非常容易地繼承到 Web 環境
- Caching:快取,比如使用者登入猴,其使用者資訊,擁有地角色,許可權不必每次去查,這樣可以提高效率
- concurrency:Shrio 支援多執行緒應用的併發驗證,即,如在一個執行緒中開啟另一個執行緒,能把許可權自動地傳播過去
- Testing:提供測試支援
- Run As:允許一個使用者假裝為另一個使用者(如果他們允許)地身份進行訪問
- Remenber Me:記住我,這個是非常常見的功能,即一次登入後,下次再來的話就不用登入了



### 20.3 Shiro 框架的結構

Authentication(認證),Authorization(授權),Session Management(會話管理),Cryptography(加密)被 Shiro 框架的開發團隊稱之為應用安全的四大基石。

- **Subject 認證的主體**

**Subject:即 “當前操作使用者”。但是,在 Shiro 中, Subject 這一概念並不僅僅指人,也可以是第三方程序,後臺賬戶(Daemon Account)或其他類似事務,它僅僅意味著 “當前跟軟體互動的東西”。但考慮到大多數目和用途,你可以把它認為是 Shiro 的 “使用者” 概念。Subject 代表了當前使用者的安全操作,SecurityManager 則管理所有使用者的安全操作**。

- **Security Manager 安全管理器**

**Security Manager:它是 Shiro 框架的核心,典型的 Facade 模式,Shiro 通過 Security Manager 來管理的內部元件例項,並通過它來提供安全管理的各種服務(相當於我們 SpringMVC 中的 DispatcherServlet)**。

- **Authenticator 認證器**

Authentication:使用者身份識別,通常被稱為使用者 “登入”。

- **Authorizer 授權管理器**

Authorizer:訪問控制。比如某個使用者是否具有某個操作的使用許可權。

- **SessionManager 會話管理器** 

SessionManager:託管 web 容器的繪畫,對C/S桌面應用的繪畫管理

- **Session DAO 操作會話資料**

- **CacheManager 快取管理器**

CacheManager:用於管理選單的資料

- **Cryptography 密碼加密技術 MD5 MD5鹽值**

Cryptography:在對資料來源使用加密演算法加密的同時,保證易於使用。

- **Realm 領域**

**Realm :“領域”的意思。用於實現使用者認證和授權的主要元件。可以自定義 Realm**

從內部來看:

![image-20200724201149059](https://gitee.com/windows_xp_xp/picture_bed/raw/master/img/image-20200724201149059.png)

從外部看:

![image-20200726134036538](https://gitee.com/windows_xp_xp/picture_bed/raw/master/img/image-20200726134036538.png) 



### 20.4 環境搭建

1. 匯入依賴

```xml
<dependencies>
    <!-- thymeleaf -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>
    <!-- web 依賴 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- mybatis -->
    <dependency>
        <groupId>org.mybatis.spring.boot</groupId>
        <artifactId>mybatis-spring-boot-starter</artifactId>
        <version>2.1.2</version>
    </dependency>

    <!-- SpringBoot 熱部署 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-devtools</artifactId>
        <scope>runtime</scope>
        <optional>true</optional>
    </dependency>
    <!-- mysql 驅動 -->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <!-- lombok -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>

    <!-- SpringBoot 整合 Shiro -->
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-spring-boot-web-starter</artifactId>
        <version>1.5.3</version>
    </dependency>
    <!-- 日誌門面 -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>jcl-over-slf4j</artifactId>
        <version>2.0.0-alpha1</version>
    </dependency>
    <!-- log4j日誌門面 -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-log4j12</artifactId>
        <version>2.0.0-alpha1</version>
    </dependency>
    <!-- log4j -->
    <dependency>
        <groupId>log4j</groupId>
        <artifactId>log4j</artifactId>
        <version>1.2.17</version>
    </dependency>
    <!-- druid 資料來源 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.23</version>
    </dependency>

    <!-- SpringBoot 測試類 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
</dependencies>
  1. 靜態資源過濾

    <build>
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <source>1.8</source>
                    <target>1.8</target>
                    <encoding>UTF-8</encoding>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
        <!-- 靜態資源解析 -->
        <resources>
            <resource>
                <directory>src/main/java</directory>
                <includes>
                    <include>**/*.xml</include>
                </includes>
                <filtering>true</filtering>
            </resource>
        </resources>
    </build>
    
  2. 編寫前端頁面和Controller,測試 Thymeleaf 是否成功生效

    index.html

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org">
    <head>
        <meta charset="UTF-8">
        <title>首頁</title>
    </head>
    <body>
    <h1>首頁</h1>
    <div th:text="${msg}"></div>
    <a th:href="@{/user/add}">add</a>|
    <a th:href="@{/user/update}">update</a>
    </body>
    </html>
    

    ShiroController

    @Controller
    public class ShiroController {
    
        @RequestMapping({"/", "/index"})
        public String toIndex(Model model) {
            model.addAttribute("msg", "hello,Shiro");
            return "index";
        }
    
        @RequestMapping("/user/add")
        public String add() {
            return "user/add";
        }
    
        @RequestMapping("/user/update")
        public String update() {
            return "user/update";
        }
    
        @RequestMapping("/toLogin")
        public String toLogin() {
            return "login";
        }
    
        @RequestMapping("/login")
        public String login(String username, String password, Model model) {
            return "login";
        }
    
        @ResponseBody
        @RequestMapping("/noauth")
        public String noAuth(){
            return "未經授權,不能訪問";
        }
    
    }
    

    訪問 /toLogin ,測試 Thymeleaf 是否成功匯入

  3. 配置資料來源和 MyBatis

    這裡使用 Druid資料來源

    # Spring 配置
    spring:
      # 資料來源
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/springboot?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
        password: root
        username: root
        # 切換成 Druid 資料來源
        type: com.alibaba.druid.pool.DruidDataSource
        #Spring Boot 預設是不注入這些屬性值的,需要自己繫結
        #druid 資料來源專有配置
        initialSize: 5
        minIdle: 5
        maxActive: 20
        maxWait: 60000
        timeBetweenEvictionRunsMillis: 60000
        minEvictableIdleTimeMillis: 300000
        validationQuery: SELECT 1 FROM DUAL
        testWhileIdle: true
        testOnBorrow: false
        testOnReturn: false
        poolPreparedStatements: true
    
        #配置監控統計攔截的filters,stat:監控統計、log4j:日誌記錄、wall:防禦sql注入
        #如果允許時報錯  java.lang.ClassNotFoundException: org.apache.log4j.Priority
        #則匯入 log4j 依賴即可,Maven 地址:https://mvnrepository.com/artifact/log4j/log4j
        filters: stat,wall,log4j
        maxPoolPreparedStatementPerConnectionSize: 20
        useGlobalDataSourceStat: true
        connectionProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
    
    # mybatis 配置
    mybatis:
      # 設定mapper的位置
      mapper-locations: classpath:mapper/*.xml
      # 取別名
      type-aliases-package: com.xp.model
    
  4. 建立 User 表和 User 實體類

    建立 User 表的 SQL語句

    DROP TABLE IF EXISTS `user`;
    CREATE TABLE if NOT EXISTS `user`(
    	`id` INT(5) PRIMARY KEY NOT NULL auto_increment COMMENT '使用者id',
    	`name` VARCHAR(30) NOT NULL COMMENT '使用者名稱',
    	`password` VARCHAR(30) NOT NULL COMMENT '密碼',
    	`perms` VARCHAR(20) COMMENT '許可權'
    );
    
    INSERT INTO `user` (`name`,`password`,`perms`) VALUES
    ('root','root','user:add'),
    ('xp','xp','user:add'),
    ('test','test','user:update'),
    ('123','123',null)
    

    User

    這裡使用了 Lombok 外掛

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    @Accessors(chain = true)
    public class User {
    
        private Integer id;
    
        private String name;
    
        private String password;
    
        private String perms;
    
    }
    
  5. 編寫 UserMapper 和 UserMapper.xml

    UserMapper

    // @Repository 將 mapper 註冊到 Spring 容器中
    @Repository
    // @Mapper 將 Mapper 註冊到 MyBatis 中
    @Mapper
    public interface UserMapper {
    
        User queryUserByUserName(@Param("userName") String userName);
    
    }
    

    UserMapper.xml

    <?xml version="1.0" encoding="UTF-8" ?>
    <!DOCTYPE mapper
            PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
            "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
    <mapper namespace="com.xp.mapper.UserMapper">
        
        <select id="queryUserByUserName" resultType="User">
            select * from user where name=#{userName};
        </select>
        
    </mapper>
    

    寫完 UserMapper 後,我們先測試是否能成功獲取資料

    @SpringBootTest
    class SpringbootShiroApplicationTests {
    
        @Autowired
        private UserMapper userMapper;
    
        @Test
        void contextLoads() {
            User user = UserMapper.queryUserByUserName("test");
            System.out.println(user);
        }
    
    }
    

    如果測試沒有問題,則編寫 service 層

    UserService

    public interface UserService {
    
        User queryUserByUserName(String userName);
    
    }
    

    UserServiceImpl

    @Service
    public class UserServiceImpl implements UserService {
    
        @Autowired
        private UserMapper userMapper;
    
        @Override
        public User queryUserByUserName(String userName) {
            return userMapper.queryUserByUserName(userName);
        }
    }
    
  6. 自定義 Realm

    在 config 包下建立我們自定義的 Realm ——MyRealm,並讓其繼承 AuthorizingRealm,實現繼承的方法

    /**
     * 自定義 realm 繼承 AuthorizingRealm
     *
     * @author xp
     */
    public class MyRealm extends AuthorizingRealm {
    
        // 自動注入我們剛剛的寫好的 service
        @Autowired
        private UserService userService;
    
        // 授權
        @Override
        protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
            System.out.println("執行了=>授權");
            return null;
        }
    
        // 認證
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            System.out.println("執行了=>認證");
            return null;
        }
    }
    
  7. 配置 ShiroConfig

    我們先建立一個類,名字叫做 ShiroConfig ,並新增 @AutoConfiguration 註解標明這個是一個 Spring 的配置類

    @Configuration
    public class ShiroConfig {
    
        // ShiroFilterFactoryBean
        @Bean
        public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
            ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
            // 設定安全管理器
            filterFactoryBean.setSecurityManager(securityManager);
    
            // 新增 shiro 的內建過濾器
            /*
                anno:無許認證就可以訪問
                authc:必須認證了才能訪問
                user:必須擁有記住我功能才能訪問
                perms:擁有對某個資源的許可權才能訪問
                role:擁有某個角色許可權才能訪問
             */
            Map<String, String> filterChainMap = new LinkedHashMap<>();
            filterChainMap.put("/user/add","authc");
            filterChainMap.put("/user/update","authc");
            filterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
    
            // 未授權跳轉到指定頁面
            filterFactoryBean.setUnauthorizedUrl("/noauth");
    
            // 設定攔截後跳轉到登入頁面
            filterFactoryBean.setLoginUrl("/toLogin");
    
            return filterFactoryBean;
        }
    
        // DefaultWebSecurityManager
        @Bean(name = "securityManager")
        public DefaultWebSecurityManager defaultWebSecurityManager(@Qualifier("myRealm") MyRealm myRealm){
            DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
            // 關聯 Realm
            securityManager.setRealm(myRealm);
            return securityManager;
        }
    
        // 建立 realm 物件,需要自定義類
        @Bean
        public MyRealm myRealm(){
            return new MyRealm();
        }
    
    }
    
  8. 啟動 SpringBoot 程式進行測試

    我們會發現,我們訪問首頁時跳轉到了登入頁面,那就說明 Shiro 的安全攔截已經生效了。

    而我們如何才能夠進入首頁呢?

20.5 認證

我們配置 Shiro 後,想要進入首頁,那麼就必須通過 Shiro 的認證。

  1. 在 Controller 中獲取我們登入時的賬號和密碼

    修改我們的 /login 請求的方法

    在 Shiro 中,Subject 物件代表當前操作使用者(Subject 不僅僅指人,也可以是第三方程序,後臺賬戶(Daemon Account)或其他類似事務,我們這裡是需要拿到當前操作的使用者資訊)。

    我們要拿到 Subject 物件,則需要從 SecurityUtils 這個工具類中獲取(因為 Subject 的構造方法是私有的)。

    token 的意思是令牌,我們這裡將使用者輸入的賬號密碼封裝進令牌裡,然後進行認證。

    @RequestMapping("/login")
    public String login(String username, String password, Model model) {
        // 獲取當前使用者
        Subject subject = SecurityUtils.getSubject();
        // 封裝使用者資料
        UsernamePasswordToken token = new UsernamePasswordToken(username, password);
        // 執行登入操作
        try {
            subject.login(token);
            return "index";
            // UnknownAccountException 使用者名稱錯誤的異常
        } catch (UnknownAccountException e) {
            model.addAttribute("msg", "使用者名稱錯誤");
            e.printStackTrace();
            return "login";
            // IncorrectCredentialsException 密碼錯誤的異常
        } catch (IncorrectCredentialsException e) {
            model.addAttribute("msg", "密碼錯誤");
            e.printStackTrace();
            return "login";
        } catch (AuthenticationException e) {
            e.printStackTrace();
            return "login";
        }
    }
    
  2. 認證

    我們自己定義的 Realm 是實現了 AuthorizingRealm 的方法。其中 doGetAuthenticationInfo() 方法就是用來認證的(:授權和認證的方法不要認錯了,因為授權和認證的英語單詞非常像,認證是 Authentication)。

        // 認證
        @Override
        protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
            System.out.println("執行了=》認證");
    
            // 獲取我們前面封裝好的令牌
            UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
            // 資料庫中查詢使用者資訊
            User user = userService.queryUserByUserName(token.getUsername());
            // 若使用者名稱不存在於資料庫中
            if (user == null) {
                // return null 丟擲 UnknownAccountException 異常
                return null;
            }
    
            // 密碼認證交給 Shiro 做, shiro 會對密碼進行加密
            return new SimpleAuthenticationInfo("", user.getPassword(), "");
        }
    
  3. 啟動 SpringBoot 應用程式進行測試

    我們可以發現,登入完後,可以訪問自己想訪問的頁面了。

    那麼又有一個問題了,我們真的可以讓使用者想訪問什麼頁面就訪問什麼頁面嗎?

20.6 授權

答案當然是否定的,對於一些特殊的頁面,我們是不希望使用者能夠訪問的。

那麼我們該如何給使用者增加限制呢?

在以前,我們可以自己使用 Filter 進行使用者許可權資源的攔截,但是我們寫得並不是太好,而且也很複雜。

Shiro 就提供了比較方便的使用者許可權資源攔截的方法。

  1. 給頁面資源增加許可權限制

    修改我們之前寫的 Shrio 配置類 ShrioConfig 中的 shiroFilterFactoryBean() 方法

    // ShiroFilterFactoryBean
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager){
        ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();
        // 設定安全管理器
        filterFactoryBean.setSecurityManager(securityManager);
    
        // 新增 shiro 的內建過濾器
        /*
            anno:無須認證就可以訪問
            authc:必須認證了才能訪問
            user:必須擁有記住我功能才能訪問
            perms:擁有對某個資源的許可權才能訪問
            role:擁有某個角色許可權才能訪問
         */
        Map<String, String> filterChainMap = new LinkedHashMap<>();
        filterChainMap.put("/user/add","perms[user:add]");
        filterChainMap.put("/user/update","perms[user:update]");
        filterFactoryBean.setFilterChainDefinitionMap(filterChainMap);
    
        // 未授權跳轉到指定頁面
        filterFactoryBean.setUnauthorizedUrl("/noauth");
    
        // 設定攔截後跳轉到登入頁面
        filterFactoryBean.setLoginUrl("/toLogin");
    
    
        return filterFactoryBean;
    }
    

    ShrioFilterFactoryBean 中提供讓我們自定義資源攔截的方式。

    anno:無須認證就可以可以訪問

    authc:必須認證了才能訪問

    user:必須擁有記住我功能才能訪問

    perms:擁有某個資源的許可權才能訪問

    role:擁有某個角色許可權才能訪問

    在這裡,我們定義這些資源的訪問方式為 perms,即必須擁有特定字串才允許訪問我們的資源

  2. 授權

    使用者登入後,使用者物件是在認證方法中的,所以我們需要將認證方法中的登入物件進行授權。

    Shiro 也提供給了我們傳遞這個使用者物件的方法。

    在剛剛的認證中,我們把密碼交給 Shrio 進行驗證,此時也可以傳遞使用者物件過去。

    // 密碼認證交給 Shiro 做, shiro 進行密碼加密
    // 這裡的 user 就是剛剛查詢資料庫後封裝的 user
    return new SimpleAuthenticationInfo(user, user.getPassword(), "");
    

    然後,我們將我們自定義的 Realm 實現的 doGetAuthorizationInfo() 方法進行修改。

    // 授權
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        System.out.println("執行了=》授權");
    
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        // 獲取當前使用者,也就是剛剛我們認證後通過 SimpleAuthenticationInfo 傳過來的user
        Subject subject = SecurityUtils.getSubject();
        User user = (User) subject.getPrincipal();
    
        // 給使用者新增訪問資源的標誌字串
        info.addStringPermission(user.getPerms());
    
        return info;
    }
    
  3. 啟動 SpringBoot 應用程式,測試

    發現我們擁有 user:update 的使用者可以訪問 update 頁面,擁有 user:add 的使用者可以訪問 add 頁面

20.7 整合 Thymeleaf

  1. 引入依賴

    跟 SpringSecurity 一樣,由於 SpringBoot 對JSP的支援不太好,而沒有了 JSP 作為模板引擎,我們需要使用新的模板引擎代替。這裡我們採用 Thymeleaf 模板引擎代替。

    <!-- thymeleaf-extras-shiro -->
    <dependency>
        <groupId>com.github.theborakompanioni</groupId>
        <artifactId>thymeleaf-extras-shiro</artifactId>
        <version>2.0.0</version>
    </dependency>
    
  2. 注入 ShiroDialect

    在 Shiro 配置類 ShiroConfig 中注入 ShiroDialect,使 Thymeleaf 和我們的 Shiro 進行整合。

    // 整合 ThymeleafDialect Thymeleaf 方言
    @Bean
    public ShiroDialect shiroDialect(){
        return new ShiroDialect();
    }
    
  3. 修改前端頁面

    在我們的實際場景中,我們還應該讓沒有該資源訪問許可權的使用者不能檢視到對應資源的跳轉連結,所以我們使用 Thymeleaf 作為模板引擎,根據使用者的資源許可權進行顯示頁面。

    修改我們之前寫得前端頁面

    <!DOCTYPE html>
    <html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:shiro="http://www.thymeleaf.org/thymeleaf-extras-shiro">
    <head>
        <meta charset="UTF-8">
        <title>首頁</title>
    </head>
    <body>
    <h1>首頁</h1>
    <div th:text="${msg}"></div>
    <shiro:guest>
        <a th:href="@{/toLogin}">登入</a>
    </shiro:guest>
    <shiro:authenticated>
        <!-- 這裡的 principal 標籤,poerperty="name" 相當於 ((User)Subject.getPrincipals()).getName,如果時 useName則相當於 後面是 .getUserName() -->
        使用者名稱:<shiro:principal property="name"/>
    </shiro:authenticated>
    
    <div shiro:hasPermission="user:add">
        <a th:href="@{/user/add}">add</a>
    </div>
    <div shiro:hasPermission="user:update">
        <a th:href="@{/user/update}">update</a>
    </div>
    </body>
    </html>
    

    到這裡,我們的 SpringBoot 整合 Shiro 就完成了,接下來再補充一點 Shiro 和 Thymeleaf 標籤

Thymeleaf Shiro 標籤

  • guest 標籤

    <!-- 使用者沒有身份驗證時顯示對應資訊,即遊客訪問資訊 -->
    <shiro:guset></shiro:guest>
    
  • user 標籤

    <!-- 使用者已經身份驗證/記住我登入後顯示響應的資訊 -->
    <shiro:user></shiro:user>
    
  • authenticated 標籤

    <!-- 使用者已經身份驗證通過,即 Subject.login 登入成功,不是記住我登入的 -->
    <shiro:authenticated></shiro:authenticated>
    
  • notAuthenticated 標籤

    <!-- 使用者已經身份驗證通過,即沒有呼叫 Subject.login 進行登入,包括記住我自動登入的也屬於未進行身份驗證,也就是說不在登入狀態時 -->
    <shiro:notAuthenticated></shiro:notAuthenticated>
    
  • principal 標籤

    <!-- 顯示使用者登入資訊 這裡的property="name" 相當於 ((User)Subject.getPrincipals()).getName,如果porperty="userName" 則相當於剛剛後面的變成 .getUserName() -->
    <shiro:principal property="name"></shiro:principal>
    
  • hasPermission 標籤

    <!-- 如果當前 Subject 有許可權將顯示 body 體內容,這裡是如果有 user:add 這個許可權,將不顯示這個標籤裡的內容 -->
    <shiro:hasPermission name="user:add"></shiro:hasPermission>
    
  • lacksPermission 標籤

    <!-- 如果當前 Subject 沒有許可權將顯示 body 體內容,這裡是如果沒有 user:add 這個許可權,將不顯示這個標籤裡的內容 -->
    <shiro:lacksPermission name="user:add"></shiro:lacksPermission>
    
  • hasRole 標籤

    <!-- 如果當前 Subjecct 有角色將顯示 body 體內容,這裡是如果有admin角色,則顯示標籤裡的內容 -->
    <shiro:hasRole name="admin"></shiro:hasRole>
    
  • hasAnyRole 標籤

    <!-- 如果當前 Subject 有任意一個角色,將顯示 body 體內容,這裡是只要有admin或user角色中的其中一個角色,則顯示 body 體的內容 -->
    <shiro:hasAnyRole name="admin,user"></shiro:hasAnyRole>
    
  • lacksRole 標籤

    <!-- 如果當前 Subject 沒有角色將顯示 body 體內容,這裡表示只要沒有admin這個角色,則顯示標籤體裡面的內容 -->
    <shiro:lacksRole name="admin"></shiro:lacksRole>