spring boot快速入門

王延領發表於2021-11-09

1.什麼是SpringBoot

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

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

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

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

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

  • 簡單來說就是SpringBoot其實不是什麼新的框架,它預設配置了很多框架的使用方式,就像maven整合了所有的jar包,spring boot整合了所有的框架 。

  • Spring Boot 出生名門,從一開始就站在一個比較高的起點,又經過這幾年的發展,生態足夠完善,Spring Boot 已經當之無愧成為 Java 領域最熱門的技術。

Spring Boot的主要優點:

● 能夠快速建立基於Spring的應用程式

● 能夠直接使用java main方法啟動內嵌的Tomcat伺服器執行SpringBoot程式,不需要部署war包檔案

● 提供約定的starter POM來簡化Maven配置,讓Maven的配置變得簡單

● 自動化配置,根據專案的Maven依賴配置,Springboot自動配置Spring、Spring mvc等

● 提供了程式的健康檢查等功能

● 基本可以完全不使用XML配置檔案,採用註解配置

SpringBoot四大核心

● 自動配置

針對很多Spring應用程式和常見的應用功能,SpringBoot能自動提供相關配置

● 起步依賴

告訴SpringBoot需要什麼功能,它就能引入需要的依賴庫

● Actuator

讓你能夠深入執行中的SpringBoot應用程式,一探SpringBoot程式的內部資訊

● 命令列介面

這是SpringBoot的可選特性,主要針對Groovy語言使用;

Groovy是一種基於JVM(Java虛擬機器) 的敏捷開發語言,它結合了Python、Ruby和Smalltalk的許多強大的特性,Groovy 程式碼能夠與Java程式碼很好地結合,也能用於擴充套件現有程式碼,由於其執行在JVM上的特性,Groovy可以使用其他Java語言編寫的庫。

2.第一個spring boot程式

2.1.使用 IDEA 直接建立專案

1、建立一個新專案

2、選擇spring initalizr , 可以看到預設就是去官網的快速構建工具那裡實現

3、填寫專案資訊

4、選擇初始化的元件(初學勾選 Web 即可)

5、填寫專案路徑

6、等待專案構建成功

image-20211108104225240

image-20211108104244944

img

2.2.pom.xml 分析

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <!-- 父依賴 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.6</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.example</groupId>
    <artifactId>springbootOne</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>springbootOne</name>
    <description>Demo project for Spring Boot</description>
    <properties>
        <java.version>1.8</java.version>
    </properties>
    <dependencies>
        <!-- web場景啟動器 -->
        <!--web依賴:tomcat,dispatcherServlet,xml...-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--spring-boot-starter:所有的springboot依賴都是使用這個開頭的-->
        <!-- 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>

    <build>
        <plugins>
            <!-- 打包外掛 -->
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

  • 專案後設資料:建立時候輸入的Project Metadata部分,也就是Maven專案的基本元素,包括:groupId、artifactId、version、name、description等

  • parent:繼承spring-boot-starter-parent的依賴管理,控制版本與打包等內容

  • dependencies:專案具體依賴,這裡包含了spring-boot-starter-web用於實現HTTP介面(該依賴中包含了Spring MVC),官網對它的描述是:使用Spring MVC構建Web(包括RESTful)應用程式的入門者,使用Tomcat作為預設嵌入式容器。spring-boot-starter-test用於編寫單元測試的依賴包。更多功能模組的使用將在後面逐步展開。

  • build:構建配置部分。預設使用了spring-boot-maven-plugin,配合spring-boot-starter-parent就可以把Spring Boot應用打包成JAR來直接執行。

2.3.編寫一個http介面

1、在主程式的同級目錄下,新建一個controller包,一定要在同級目錄下,否則識別不到

![image-20211108111755801](https://gitee.com/wyl1924/cdn/raw/master/img/blog/image-20211108111755801.png

image-20211108111933347

image-20211108112414293

2.4.將專案打成jar包

點選 maven的 package,等待生成。

image-20211108112805061

如果測試用例影響到打包,可以跳過

<!--
    在工作中,很多情況下我們打包是不想執行測試用例的
    可能是測試用例不完事,或是測試用例會影響資料庫資料
    跳過測試用例執
-->
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <!--跳過專案執行測試用例-->
        <skipTests>true</skipTests>
    </configuration>
</plugin>

打成了jar包後,就可以在任何地方執行了

image-20211108113444005

2.5.埠

有沒有看到我的埠被佔用了

image-20211108113910276

2.6.生成專案模板

為方便我們初始化專案,Spring Boot給我們提供一個專案模板生成網站。

  1. 開啟瀏覽器,訪問:https://start.spring.io/

  2. 根據頁面提示,選擇構建工具,開發語言,專案資訊等。

  3. 點選 Generate the project,生成專案模板,生成之後會將壓縮包下載到本地。

image-20211108171343805

3.執行原理探究

我們之前寫的HelloSpringBoot,到底是怎麼執行的呢,我們從pom.xml檔案探究起;

3.1.父依賴

pom.xml

  • spring-boot-dependencies:核心依賴在父工程中!
  • 我們在寫或者引入一些Springboot依賴的時候,不需要指定版本,就因為有這些版本倉庫

1、其中它主要是依賴一個父專案,主要是管理專案的資源過濾及外掛!

<!-- 父依賴 -->
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.5.6</version>
        <relativePath/>
    </parent>

2、點進去,發現還有一個父依賴

image-20211108135229517

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

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

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

3.2.啟動器 spring-boot-starter

  • 依賴

    <dependency>        								 <groupId>org.springframework.boot</groupId>
         <artifactId>spring-boot-starter</artifactId>
    </dependency>
    
  • springboot-boot-starter-xxx,說白了就是Springboot的啟動場景

  • 比如spring-boot-starter-web,他就會幫我們自動匯入web的所有依賴

  • springboot會將所有的功能場景,都變成一個個的啟動器

  • 我們要使用什麼功能,就只需要找到對應的啟動器就好了start

3.3.主程式

3.3.1.預設的主啟動類

package com.example.springbootone;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
//@SpringBootApplication 來標註一個主程式類
//說明這是一個Spring Boot應用
@SpringBootApplication
public class SpringbootOneApplication {

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

}

3.3.2註解(@SpringBootApplication)

  • 作用:標註在某個類上說明這個類是SpringBoot的主配置

  • SpringBoot就應該執行這個類的main方法來啟動SpringBoot應用;

  • 進入這個註解:可以看到上面還有很多其他註解!

image-20211108135746831

@ComponentScan

  • 這個註解在Spring中很重要 ,它對應XML配置中的元素。

  • 作用:自動掃描並載入符合條件的元件或者bean , 將這個bean定義載入到IOC容器中

@SpringBootConfiguration

  • 作用:SpringBoot的配置類 ,標註在某個類上 , 表示這是一個SpringBoot的配置類;

  • 我們繼續進去這個註解檢視

image-20211108135926732

  • 這裡的 @Configuration,說明這是一個spring的配置類 ,配置類就是對應Spring的xml 配置檔案;

  • @Component 這就說明,啟動類本身也是Spring中的一個元件而已,負責啟動應用!

  • 我們回到 SpringBootApplication 註解中繼續看。

@EnableAutoConfiguration

  • 開啟自動配置功能

    • 以前我們需要自己配置的東西,而現在SpringBoot可以自動幫我們配置 ;
    • @EnableAutoConfiguration告訴SpringBoot開啟自動配置功能,這樣自動配置才能生效;

    點進註解接續檢視:

  • @AutoConfigurationPackage :自動配置包

image-20211108140054846

image-20211108140207428

    • @import :Spring底層註解@import , 給容器中匯入一個元件

    • Registrar.class 作用:自動配置包註冊,將主啟動類的所在包及包下面所有子包裡面的所有元件掃描到Spring容器 ;

    • 這個分析完了,退到上一步,繼續看

  • @Import({AutoConfigurationImportSelector.class}) :給容器匯入元件 ;

    • AutoConfigurationImportSelector自動配置匯入選擇器,那麼它會匯入哪些元件的選擇器呢?我們點選去這個類看原始碼:
// 獲取所有的配置
List<String> configurations = this.getCandidateConfigurations(annotationMetadata, attributes);
  • 獲得候選的配置

    protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
        // 和下面的方法對應
        //這裡的getSpringFactoriesLoaderFactoryClass()方法
        //返回的就是我們最開始看的啟動自動匯入配置檔案的註解類;EnableAutoConfiguration
      List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.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裡面的第一個引數對應
    //這裡的getSpringFactoriesLoaderFactoryClass()方法
    //返回的就是我們最開始看的啟動自動匯入配置檔案的註解類;EnableAutoConfiguration
     protected Class<?> getSpringFactoriesLoaderFactoryClass() {
         return EnableAutoConfiguration.class;
     }
    
  • 這個方法getCandidateConfigurations()又呼叫了 SpringFactoriesLoader 類的靜態方法!我們進入SpringFactoriesLoader類loadFactoryNames() 方法,獲取所有的載入配置

    public static List<String> loadFactoryNames(Class<?> factoryClass, @Nullable ClassLoader classLoader) {
        String factoryClassName = factoryClass.getName();
        //這裡它又呼叫了 loadSpringFactories 方法
        return (List)loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
    }
    
  • 我們繼續點選檢視 loadSpringFactories 方法

    • 專案資源:META-INF/spring.factories
    • 系統資源:META-INF/spring.factories
    • 從這些資源中配置了所有的nextElement(自動配置),分裝成properties
    //將所有的資源載入到配置類中(將下面的抽離出來分析,第15行)
    Properties properties = PropertiesLoaderUtils.loadProperties(resource);
    
    private static Map<String, List<String>> loadSpringFactories(@Nullable ClassLoader classLoader) {
        //獲得classLoader , 我們返回可以看到這裡得到的就是EnableAutoConfiguration標註的類本身
        MultiValueMap<String, String> result = (MultiValueMap)cache.get(classLoader);
        if (result != null) {
            return result;
        } else {
            try {
                //去獲取一個資源 "META-INF/spring.factories"
                Enumeration<URL> urls = classLoader != null ? classLoader.getResources("META-INF/spring.factories") : ClassLoader.getSystemResources("META-INF/spring.factories");
                LinkedMultiValueMap result = new LinkedMultiValueMap();
                //判斷有沒有更多的元素,將讀取到的資源迴圈遍歷,封裝成為一個Properties
                while(urls.hasMoreElements()) {
                  URL url = (URL)urls.nextElement();
                    UrlResource resource = new UrlResource(url);
                    Properties properties = PropertiesLoaderUtils.loadProperties(resource);
                    Iterator var6 = properties.entrySet().iterator();
                    while(var6.hasNext()) {
                        Entry<?, ?> entry = (Entry)var6.next();
                        String factoryClassName = ((String)entry.getKey()).trim();
                        String[] var9 = StringUtils.commaDelimitedListToStringArray((String)entry.getValue());
                        int var10 = var9.length;
                        for(int var11 = 0; var11 < var10; ++var11) {
                            String factoryName = var9[var11];
                            result.add(factoryClassName, factoryName.trim());
                        }
                    }
                }
                cache.put(classLoader, result);
                return result;
            } catch (IOException var13) {
                throw new IllegalArgumentException("Unable to load factories from location [META-INF/spring.factories]", var13);
            }
        }
    }
    
  • 發現一個多次出現的檔案:spring.factories

3.3.3.spring.factories

我們根據源頭開啟spring.factories , 看到了很多自動配置的檔案;這就是自動配置根源所在!

image-20211108140808447

3.3.4.WebMvcAutoConfiguration

在上面的自動配置類隨便找一個開啟看看,比如 :WebMvcAutoConfiguration

@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 })
public class WebMvcAutoConfiguration {

	public static final String DEFAULT_PREFIX = "";

	public static final String DEFAULT_SUFFIX = "";

	private static final String[] SERVLET_LOCATIONS = { "/" };

	@Bean
	@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)

可以看到這些一個個的都是JavaConfig配置類,而且都注入了一些Bean,可以找一些自己認識的類,看著熟悉一下!

所以,自動配置真正實現是從classpath中搜尋所有的META-INF/spring.factories配置檔案 ,並將其中對應的 org.springframework.boot.autoconfigure. 包下的配置項,通過反射例項化為對應標註了 @Configuration的JavaConfig形式的IOC容器配置類 , 然後將這些都彙總成為一個例項並載入到IOC容器中

3.3.5.結論:

  1. SpringBoot在啟動的時候從類路徑下的META-INF/spring.factories中獲取EnableAutoConfiguration指定的值

  2. 將這些值作為自動配置類匯入容器 , 自動配置類就生效 , 幫我們進行自動配置工作;

  3. 以前我們需要自動配置的東西,現在springboot幫我們做了

  4. 整合JavaEE,整體解決方案和自動配置的東西都在springboot-autoconfigure的jar包中;

  5. 它會把所有需要匯入的元件,以類名的方式返回,這些元件就會被新增到容器中

  6. 它會給容器中匯入非常多的自動配置類 (xxxAutoConfiguration), 就是給容器中匯入這個場景需要的所有元件 , 並自動配置,@Configuration(javaConfig) ;

  7. 有了自動配置類 , 免去了我們手動編寫配置注入功能元件等的工作;

    img

3.4啟動

@SpringBootApplication
public class Springboot01HellowordApplication {

    public static void main(String[] args) {
       	//該方法返回一個ConfigurableApplicationContext物件
 		//引數一:應用入口的類; 引數二:命令列引數  
        SpringApplication.run(Springboot01HellowordApplication.class, args);
    }

}

SpringApplication.run分析

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

3.4.1.SpringApplication

image-20211108141346936

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

  1. 推斷應用的型別是普通的專案還是Web專案

  2. 查詢並載入所有可用初始化器 , 設定到initializers屬性中

  3. 找出所有的應用程式監聽器,設定到listeners屬性中

  4. 推斷並設定main方法的定義類,找到執行的主類

檢視構造器

image-20211108141446936

3.4.2.run方法流程分析

img

4.yaml語法學習

4.1.配置檔案

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

  • application.properties

    • 語法結構 :key=value
  • application.yaml

    • 語法結構 :key:空格 value

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

比如我們可以在配置檔案中修改Tomcat 預設啟動的埠號!測試一下!

server:
  port: 8081

4.2.YAML

yaml概述

  • YAML是 "YAML Ain't a Markup Language" (YAML不是一種標記語言)的遞迴縮寫。在開發的這種語言時,YAML 的意思其實是:"Yet Another Markup Language"(仍是一種標記語言)

  • 這種語言以資料作為中心,而不是以標記語言為重點!

  • 以前的配置檔案,大多數都是使用xml來配置;比如一個簡單的埠配置,我們來對比下yaml和xml

    • 傳統xml配置:

      <server>
          <port>8081<port>
      </server>
      
    • yaml配置:

      server:
        prot: 8080
      

yaml基礎語法

說明:語法要求嚴格!

  1. 空格不能省略

  2. 以縮排來控制層級關係,只要是左邊對齊的一列資料都是同一個層級的。

  3. 屬性和值的大小寫都是十分敏感的。

字面量:普通的值 [ 數字,布林值,字串 ]

  • 字面量直接寫在後面就可以 , 字串預設不用加上雙引號或者單引號;k: v

    注意:

    • “ ” 雙引號,不會轉義字串裡面的特殊字元 , 特殊字元會作為本身想表示的意思;

      比如 :name: "wang\n jingmo" 輸出 :wang換行 jingmo

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

      比如 :name: ‘wang\n jingmo’ 輸出 :wang\n jingmo

物件、Map(鍵值對)

#物件、Map格式
k: 
    v1:
    v2:

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

student:
    name: wangyanling
    age: 3

行內寫法

student: {name: wangyanling,age: 3}

陣列( List、set )

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

pets:
 - cat
 - dog
 - pig

行內寫法

pets: [cat,dog,pig]

修改SpringBoot的預設埠號

配置檔案中新增,埠號的引數,就可以切換埠;

server:
  port: 8082

4.3.注入配置檔案

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

4.3.1. yaml注入配置檔案

① 在springboot專案中的resources目錄下新建一個檔案 application.yml

② 編寫一個實體類 Dog

package com.wyl.springboot.pojo;

@Component  //註冊bean到容器中
public class Dog {
    private String name;
    private Integer age;
    
    //有參無參構造、get、set方法、toString()方法  
}

③ 試著用@Value給bean注入屬性值

@Component //註冊bean
public class Dog {
    @Value("旺財")
    private String name;
    @Value("1")
    private Integer age;
}

④ 在SpringBoot的測試類下注入並輸出

@SpringBootTest
class DemoApplicationTests {

    @Autowired //將狗狗自動注入進來
    Dog dog;

    @Test
    public void contextLoads() {
        System.out.println(dog); //列印看下狗狗物件
    }

}

結果成功輸出,@Value注入成功

Dog{name='旺財', age=1}

⑤ 再編寫一個複雜點的實體類

@Component //註冊bean到容器中
public class Person {
    private String name;
    private Integer age;
    private Boolean happy;
    private Date birth;
    private Map<String,Object> maps;
    private List<Object> lists;
    private Dog dog;
    
    //有參無參構造、get、set方法、toString()方法  
}

⑥ 使用yaml配置的方式進行注入

寫的時候注意區別和優勢,首先編寫一個yaml配置

person:
  name: wjm
  age: 3
  happy: false
  birth: 2000/01/01
  maps: {k1: v1,k2: v2}
  lists:
   - code
   - girl
   - music
  dog:
    name: 旺財
    age: 1

⑦ 把物件的所有值都寫好後,注入到類中

/*
@ConfigurationProperties作用:
將配置檔案中配置的每一個屬性的值,對映到這個元件中;
告訴SpringBoot將本類中的所有屬性和配置檔案中相關的配置進行繫結
引數 prefix = “person” : 將配置檔案中的person下面的所有屬性一一對應
*/
@Component //註冊bean
@ConfigurationProperties(prefix = "person")
public class Person {
    private String name;
    private Integer age;
    private Boolean happy;
    private Date birth;
    private Map<String,Object> maps;
    private List<Object> lists;
    private Dog dog;
}

⑧ IDEA 提示,springboot配置註解處理器沒有找到

Not Found

The requested URL /spring-boot/docs/2.3.3.RELEASE/reference/html/configuration-metadata.html was not found on this server.

檢視文件(在網址中更改版本獲得,如回到2.1.9),找到一個依賴

<!-- 匯入配置檔案處理器,配置檔案進行繫結就會有提示,需要重啟 -->
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-configuration-processor</artifactId>
  <optional>true</optional>
</dependency>

⑨ 確認以上配置都完成後,去測試類中測試

@SpringBootTest
class DemoApplicationTests {

    @Autowired
    Person person; //將person自動注入進來

    @Test
    public void contextLoads() {
        System.out.println(person); //列印person資訊
    }

}

結果:所有值全部注入成功

4.3.2. 載入指定配置檔案

@PropertySource :載入指定的配置檔案;
@configurationProperties:預設從全域性配置檔案中獲取值

  1. 在resources目錄下新建一個person.properties檔案
name=hello
  1. 在程式碼中指定載入person.properties檔案
@PropertySource(value = "classpath:person.properties")
@Component //註冊bean
public class Person {
    @Value("${name}")
    private String name;
    ......
}
  1. 再次輸出測試,指定配置檔案繫結成功

4.3.3.配置檔案佔位符

配置檔案還可以編寫佔位符生成隨機數

person:
    name: wangjingmo${random.uuid} # 隨機uuid
    age: ${random.int}  # 隨機int
    happy: false
    birth: 2000/01/01
    maps: {k1: v1,k2: v2}
    lists:
      - code
      - girl
      - music
    dog:
      name: ${person.hello:other}_旺財
      age: 1

4.3.4.回顧properties配置

上面採用的yaml方法都是最簡單的方式,也是開發中最常用的、pringboot所推薦的

接下來看看其他的實現方式,原理都是相同的,寫還是那樣寫

配置檔案除了yml還有之前常用的properties

【注意】properties配置檔案在寫中文的時候會有亂碼 , 需要去IDEA中設定編碼格式為UTF-8:settings-->FileEncodings 中配置

測試步驟

  1. 新建一個實體類User
@Component //註冊bean
public class User {
    private String name;
    private int age;
    private String sex;
}
  1. 編輯配置檔案 user.properties
user1.name=wyl
user1.age=18
user1.sex=男
  1. 在User類上使用@Value來進行注入
@Component //註冊bean
@PropertySource(value = "classpath:user.properties")
public class User {
    //直接使用@value
    @Value("${user.name}") //從配置檔案中取值
    private String name;
    @Value("#{9*2}")  // #{SPEL} Spring表示式
    private int age;
    @Value("男")  // 字面量
    private String sex;
}
  1. Springboot測試
SpringBootTest
class DemoApplicationTests {

    @Autowired
    User user;

    @Test
    public void contextLoads() {
        System.out.println(user);
    }

}

結果正常輸出

4.3.4.對比小結

@Value使用起來並不友好!我們需要為每個屬性單獨註解賦值比較麻煩
img

  1. @ConfigurationProperties只需要寫一次即可 , @Value則需要每個欄位都新增
  2. 鬆散繫結:這個什麼意思呢? 比如yml中寫的last-name,這個和lastName是一樣的,- 後面跟著的字母預設是大寫的。這就是鬆散繫結
  3. JSR303資料校驗 ,可以在欄位是增加一層過濾器驗證 , 保證資料的合法性
  4. 複雜型別封裝,yml中可以封裝物件 , 使用value就不支援

結論:

  1. 配置yml和配置properties都可以獲取到值 , 強烈推薦 yml;
  2. 如果在某個業務中,只需要獲取配置檔案中的某個值,可以使用一下 @value;
  3. 如果專門編寫了一個JavaBean來和配置檔案進行一一對映,就直接使用@configurationProperties

5.JSR303資料校驗

Springboot中可以用@validated來校驗資料,如果資料異常則會統一丟擲異常,方便異常中心統一處理。這裡來寫個註解讓name只能支援Email格式

@Component //註冊bean
@ConfigurationProperties(prefix = "person")
@Validated //資料校驗
public class Person {
    @Email(message="郵箱格式錯誤") //name必須是郵箱格式
    private String name;
}

執行結果:default message [不是一個合法的電子郵件地址]

使用資料校驗,可以保證資料的正確性; 下面列出一些常見的使用

@NotNull(message="名字不能為空")
private String userName;
@Max(value=120,message="年齡最大不能查過120")
private int age;
@Email(message="郵箱格式錯誤")
private String email;

空檢查
@Null 驗證物件是否為null
@NotNull 驗證物件是否不為null, 無法查檢長度為0的字串
@NotBlank 檢查約束字串是不是Null還有被Trim的長度是否大於0,只對字串,且會去掉前後空
格.
@NotEmpty 檢查約束元素是否為NULL或者是EMPTY.
Booelan檢查
@AssertTrue 驗證 Boolean 物件是否為 true
@AssertFalse 驗證 Boolean 物件是否為 false
長度檢查
@Size(min=, max=) 驗證物件(Array,Collection,Map,String)長度是否在給定的範圍之內
@Length(min=, max=) string is between min and max included.
日期檢查
@Past 驗證 Date 和 Calendar 物件是否在當前時間之前
@Future 驗證 Date 和 Calendar 物件是否在當前時間之後
@Pattern 驗證 String 物件是否符合正規表示式的規則
.......等等
除此以外,我們還可以自定義一些資料校驗規則

5.1.多環境切換

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

5.1.1.多配置檔案

在主配置檔案編寫的時候,檔名可以是application-{profile}.properties/yml , 用來指定多個環境版本。例如:application-test.properties 代表測試環境配置 application-dev.properties代表開發環境配置
但是Springboot並不會直接啟動這些配置檔案,它預設使用application.properties主配置檔案。但可以通過配置來選擇需要啟用的環境

#比如在配置檔案中指定使用dev環境,我們可以通過設定不同的埠號進行測試;
#我們啟動SpringBoot,就可以看到已經切換到dev下的配置了;
spring.profiles.active=dev

5.1.2.yml的多文件塊

和properties配置檔案中一樣,但使用yml去實現不需要建立多個配置檔案,更加方便

server:
	port: 8081
#選擇要啟用那個環境塊
spring:
	profiles:
		active: prod
---
server:
	port: 8083
spring:
	profiles: dev #配置環境的名稱
	
---
server:
	port: 8084
spring:
	profiles: prod #配置環境的名稱

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

5.1.3.配置檔案載入位置

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

springboot 啟動會掃描以下位置的application.properties或者application.yml檔案作為Spring boot的預設配置檔案

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

優先順序由高到底,高優先順序的配置會覆蓋低優先順序的配置;
SpringBoot會從這四個位置全部載入主配置檔案;互補配置

4.4.4.運維小技巧

指定位置載入配置檔案

我們還可以通過spring.config.location來改變預設的配置檔案位置

專案打包好以後,我們可以使用命令列引數的形式,啟動專案的時候來指定配置檔案的新位置;

這種情況,一般是後期運維做的多,相同配置,外部指定的配置檔案優先順序最高

java -jar spring-boot-config.jar --spring.config.location=F:/application.properties

6.自動配置原理

----聯絡---- spring.factories

SpringBoot官方文件中有大量的配置,我們無法全部記住,官網:https://docs.spring.io/spring-boot/docs/2.3.1.RELEASE/reference/html/appendix-application-properties.html#core-properties

1595493746481

6.1.分析自動配置原理

  1. SpringBoot啟動的時候載入主配置類,開啟了自動配置功能 @EnableAutoConfiguration

  2. @EnableAutoConfiguration 作用

    • 利用EnableAutoConfigurationImportSelector給容器中匯入一些元件

    • 可以檢視selectImports()方法的內容,他返回了一個autoConfigurationEnty,來自this.getAutoConfigurationEntry(autoConfigurationMetadata,annotationMetadata);這個方法我們繼續來跟蹤:

    • 這個方法有一個值:List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);叫做獲取候選的配置 ,我們點選繼續跟蹤

      • SpringFactoriesLoader.loadFactoryNames()
      • 掃描所有jar包類路徑下META-INF/spring.factories
      • 把掃描到的這些檔案的內容包裝成properties物件
      • 從properties中獲取到EnableAutoConfiguration.class類(類名)對應的值,然後把他們新增在容器中
    • 在類路徑下,META-INF/spring.factories裡面配置的所有EnableAutoConfiguration的值加入到容器中:

image-20211108144552231

    • 每一個這樣的 xxxAutoConfiguration類都是容器中的一個元件,都加入到容器中;用他們來做自動配置;
  1. 每一個自動配置類進行自動配置功能;

  2. 我們以HttpEncodingAutoConfiguration(Http編碼自動配置)為例解釋自動配置原理;

    //表示這是一個配置類,和以前編寫的配置檔案一樣,也可以給容器中新增元件;
    @Configuration 
       
    //啟動指定類的ConfigurationProperties功能;
      //進入這個HttpProperties檢視,將配置檔案中對應的值和HttpProperties繫結起來;
      //並把HttpProperties加入到ioc容器中
    @EnableConfigurationProperties({HttpProperties.class}) 
       
    //Spring底層@Conditional註解
      //根據不同的條件判斷,如果滿足指定的條件,整個配置類裡面的配置就會生效;
      //這裡的意思就是判斷當前應用是否是web應用,如果是,當前配置類生效
    @ConditionalOnWebApplication(
        type = Type.SERVLET
    )
       
    //判斷當前專案有沒有這個類CharacterEncodingFilter;SpringMVC中進行亂碼解決的過濾器;
    @ConditionalOnClass({CharacterEncodingFilter.class})
       
    //判斷配置檔案中是否存在某個配置:spring.http.encoding.enabled;
      //如果不存在,判斷也是成立的
      //即使我們配置檔案中不配置pring.http.encoding.enabled=true,也是預設生效的;
    @ConditionalOnProperty(
        prefix = "spring.http.encoding",
        value = {"enabled"},
        matchIfMissing = true
    )
       
    public class HttpEncodingAutoConfiguration {
        //他已經和SpringBoot的配置檔案對映了
        private final Encoding properties;
        //只有一個有參構造器的情況下,引數的值就會從容器中拿
        public HttpEncodingAutoConfiguration(HttpProperties properties) {
            this.properties = properties.getEncoding();
        }
       
        //給容器中新增一個元件,這個元件的某些值需要從properties中獲取
        @Bean
        @ConditionalOnMissingBean //判斷容器沒有這個元件?
        public CharacterEncodingFilter characterEncodingFilter() {
            CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
            filter.setEncoding(this.properties.getCharset().name());
            filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.REQUEST));
            filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.RESPONSE));
            return filter;
        }
        //。。。。。。。
    }
    

一句話總結 :根據當前不同的條件判斷,決定這個配置類是否生效!

  • 一但這個配置類生效;這個配置類就會給容器中新增各種元件;

  • 這些元件的屬性是從對應的properties類中獲取的,這些類裡面的每一個屬性又是和配置檔案繫結的;

  • 所有在配置檔案中能配置的屬性都是在xxxxProperties類中封裝著;

  • 配置檔案能配置什麼就可以參照某個功能對應的這個屬性類

    //從配置檔案中獲取指定的值和bean的屬性進行繫結
    @ConfigurationProperties(prefix = "spring.http") 
    public class HttpProperties {
        // .....
    }
    

我們去配置檔案裡面試試字首,看提示!

1595493884773

這就是自動裝配的原理!

6.2.總結

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

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

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

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

    xxxxAutoConfigurartion:自動配置類;給容器中新增元件

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

6.3.@Conditional

瞭解完自動裝配的原理後,我們來關注一個細節問題,自動配置類必須在一定的條件下才能生效;

@Conditional派生註解(Spring註解版原生的@Conditional作用

作用:必須是@Conditional指定的條件成立,才給容器中新增元件,配置配裡面的所有內容才生效;

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

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

6.4.自動配置類是否生效

我們可以在application.properties通過啟用 debug=true屬性;

在控制檯列印自動配置報告,這樣我們就可以很方便的知道哪些自動配置類生效;

#開啟springboot的除錯類
debug=true 
  • Positive matches:(自動配置類啟用的:正匹配)

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

  • Unconditional classes: (沒有條件的類)

6.5.自定義Starter

我們分析完畢了原始碼以及自動裝配的過程,我們可以嘗試自定義一個啟動器來玩玩!

6.5.1.說明

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

命名歸約:

官方命名:

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

自定義命名:

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

6.5.2.編寫啟動器

  1. 在IDEA中新建一個空專案 spring-boot-starter-diy

  2. 新建一個普通Maven模組:kuang-spring-boot-starter

  3. 新建一個Springboot模組:kuang-spring-boot-starter-autoconfigure

  4. 點選apply即可,基本結構

  5. 在我們的 starter 中 匯入 autoconfigure 的依賴!

    <!-- 啟動器 -->
    <dependencies>
        <!--  引入自動配置模組 -->
        <dependency>
            <groupId>com.kuang</groupId>
            <artifactId>kuang-spring-boot-starter-autoconfigure</artifactId>
            <version>0.0.1-SNAPSHOT</version>
        </dependency>
    </dependencies>
    
  6. 將 autoconfigure 專案下多餘的檔案都刪掉,Pom中只留下一個 starter,這是所有的啟動器基本配置!

  7. 我們編寫一個自己的服務

    package wyl.ss;
    
    public class HelloService {
    
        HelloProperties helloProperties;
    
        public HelloProperties getHelloProperties() {
            return helloProperties;
        }
    
        public void setHelloProperties(HelloProperties helloProperties) {
            this.helloProperties = helloProperties;
        }
    
        public String sayHello(String name){
            return helloProperties.getPrefix() + name + helloProperties.getSuffix();
        }
    
    }
    
  8. 編寫HelloProperties 配置類

    package wyl.ss;
    
    import org.springframework.boot.context.properties.ConfigurationProperties;
    
    // 字首 kuang.hello
    @ConfigurationProperties(prefix = "kuang.hello")
    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;
        }
    }
    
  9. 編寫我們的自動配置類並注入bean,測試!

    package wyl.ss;
    
    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 //web應用生效
    @EnableConfigurationProperties(HelloProperties.class)
    public class HelloServiceAutoConfiguration {
    
        @Autowired
        HelloProperties helloProperties;
    
        @Bean
        public HelloService helloService(){
            HelloService service = new HelloService();
            service.setHelloProperties(helloProperties);
            return service;
        }
    
    }
    
  10. 在resources編寫一個自己的 META-INF\spring.factories

    # Auto Configure
    org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
    wyl.ss.HelloServiceAutoConfiguration
    
  11. 編寫完成後,可以安裝到maven倉庫中!

6.5.3.新建專案測試我們自己寫的啟動器

  1. 新建一個SpringBoot 專案

  2. 匯入我們自己寫的啟動器

    <dependency>
        <groupId>wyl.ss</groupId>
        <artifactId>ss-spring-boot-starter</artifactId>
        <version>1.0-SNAPSHOT</version>
    </dependency>
    
  3. 編寫一個 HelloController 進行測試我們自己的寫的介面!

    package wyl.ss.controller;
    
    @RestController
    public class HelloController {
    
        @Autowired
        HelloService helloService;
    
        @RequestMapping("/hello")
        public String hello(){
            return helloService.sayHello("zxc");
        }
    
    }
    
  4. 編寫配置檔案 application.properties

    ss.hello.prefix="ppp"
    ss.hello.suffix="sss"
    
  5. 啟動專案進行測試,結果成功 !

7.整合JDBC

7.1.SpringData簡介

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

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

Sping Data 官網

資料庫相關的啟動器 :可以參考官方文件

7.2.建立專案

7.2.1.新建專案測試

引入相應的模組:Spring Web、SQL中的JDBC API、MySql Driver

專案建好之後,Spring Boot自動匯入了啟動器

7.2.2.編寫yaml配置檔案,連線資料庫

新建一個 application.yml

spring:
datasource:
username: root
password: 123456
#?serverTimezone=UTC解決時區的報錯
url: jdbc:mysql://localhost:3306/資料庫名稱?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver

7.2.3.測試

配置完這一些東西后就可以直接去使用了,SpringBoot已經預設進行了自動配置

@SpringBootTest
class SpringbootDataJdbcApplicationTests {

    //DI注入資料來源
    @Autowired
    DataSource dataSource;

    @Test
    public void contextLoads() throws SQLException {
        //看一下預設資料來源
        System.out.println(dataSource.getClass());
        //獲得連線
        Connection connection =   dataSource.getConnection();
        System.out.println(connection);
        //關閉連線
        connection.close();
    }
}

可以看到預設配置的資料來源為 class com.zaxxer.hikari.HikariDataSource, 我們並沒有手動配置

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

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

有了資料庫連線,就可以 CRUD 運算元據庫了。但需要先了解物件 JdbcTemplate

7.2.4.原始碼

開啟 DataSourceProperties 的原始碼,能配置的所有東西都在這

public class DataSourceProperties implements BeanClassLoaderAware, InitializingBean {

	private ClassLoader classLoader;
	private String name;
	private boolean generateUniqueName = true;
	private Class<? extends DataSource> type;
	private String driverClassName;
	private String url;
	private String username;
	private String password;
    ............

開啟 DataSourceAutoConfiguration 原始碼,資料來源的所有自動配置都在這裡

7.2.5.JDBCTemplate

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

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

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

測試

新建一個 controller 目錄,在裡面編寫一個 JdbcController

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.Date;
import java.util.List;
import java.util.Map;

@RestController
@RequestMapping("/jdbc")
public class JdbcController {
    /**
     * Spring Boot 預設提供了資料來源,預設提供了 org.springframework.jdbc.core.JdbcTemplate
     * JdbcTemplate 中會自己注入資料來源,用於簡化 JDBC操作
     * 還能避免一些常見的錯誤,使用起來也不用再自己來關閉資料庫連線
     */
    @Autowired
    JdbcTemplate jdbcTemplate;
    //查詢employee表中所有資料
    //List 中的1個 Map 對應資料庫的 1行資料
    //Map 中的 key 對應資料庫的欄位名,value 對應資料庫的欄位值
    @GetMapping("/userList")
    public List<Map<String,Object>> userList(){
        String sql = "select * from user";
        List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
        return maps;
    }
    //新增一個使用者
    @GetMapping("/add")
    public String addUser(){
        //插入語句,注意時間問題
        String sql = "insert into mybatis.user(id, name,pwd)" +
            " values (9,'Java程式設計思想','qqwweer987')";
        jdbcTemplate.update(sql);
        //查詢
        return "addOk";
    }
    //修改使用者資訊
    @GetMapping("/update/{id}")
    public String updateUser(@PathVariable("id") int id){
        //插入語句
        String sql = "update mybatis.user set name=?,pwd=? where id="+id;
        //資料封裝
        Object[] objects = new Object[2];
        objects[0] = "大威天龍";
        objects[1] = "qwert123";
        jdbcTemplate.update(sql,objects);
        //查詢
        return "update-Ok";
    }
    //刪除使用者
    @GetMapping("/delete/{id}")
    public String delUser(@PathVariable("id") int id){
        //插入語句
        String sql = "delete from mybatis.user where id=?";
        jdbcTemplate.update(sql,id);
        //查詢
        return "delete-Ok";
    }
}

8.整合 druid

8.1.druid簡介

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

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

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

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

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

  • Github地址:https://github.com/alibaba/druid/

8.2.配置

  加入druid相關配置(.yml配置檔案)

spring:
  #資料庫配置
  datasource:
    type: com.alibaba.druid.pool.DruidDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
    url: jdbc:mysql://localhost:3306/springboot?serverTimezone=GMT%2B8&useUnicode=true&characterEncoding=utf-8&autoReconnect=true&zeroDateTimeBehavior=convertToNull&allowMultiQueries=true&useSSL=false
    username: root
    password: root
    druid:
        # 初始連線數
        initial-size: 10
        # 最大連線池數量
        max-active: 100
        # 最小連線池數量
        min-idle: 10
        # 配置獲取連線等待超時的時間
        max-wait: 60000
        # 開啟PSCache,並且指定每個連線上PSCache的大小
        pool-prepared-statements: true
        max-pool-prepared-statement-per-connection-size: 20
        # 配置間隔多久才進行一次檢測,檢測需要關閉的空閒連線,單位是毫秒
        timeBetweenEvictionRunsMillis: 60000
        # 配置一個連線在池中最小生存的時間,單位是毫秒
        min-evictable-idle-time-millis: 300000
        validation-query: SELECT 1 FROM DUAL
        test-while-idle: true
        test-on-borrow: false
        test-on-return: false
        stat-view-servlet:
            enabled: true
            url-pattern: /druid/*
        filter:
            stat:
                log-slow-sql: true
                slow-sql-millis: 1000
                merge-sql: false
            wall:
                config:
                    multi-statement-allow: true

 com.alibaba.druid.pool.DruidDataSource 基本配置引數如下:

配置 預設值 說明
name 配置這個屬性的意義在於,如果存在多個資料來源,監控的時候可以通過名字來區分開來。 如果沒有配置,將會生成一個名字,格式是:“DataSource-” + System.identityHashCode(this)
jdbcUrl 連線資料庫的url,不同資料庫不一樣。例如: mysql : jdbc:mysql://10.20.153.104:3306/druid2 oracle : jdbc:oracle:thin:@10.20.149.85:1521:ocnauto
username 連線資料庫的使用者名稱
password 連線資料庫的密碼。如果你不希望密碼直接寫在配置檔案中,可以使用ConfigFilter。詳細看這裡:https://github.com/alibaba/druid/wiki/%E4%BD%BF%E7%94%A8ConfigFilter
driverClassName 根據url自動識別 這一項可配可不配,如果不配置druid會根據url自動識別dbType,然後選擇相應的driverClassName(建議配置下)
initialSize 0 初始化時建立物理連線的個數。初始化發生在顯示呼叫init方法,或者第一次getConnection時
maxActive 8 最大連線池數量
maxIdle 8 已經不再使用,配置了也沒效果
minIdle 最小連線池數量
maxWait 獲取連線時最大等待時間,單位毫秒。配置了maxWait之後,預設啟用公平鎖,併發效率會有所下降,如果需要可以通過配置useUnfairLock屬性為true使用非公平鎖。
poolPreparedStatements false 是否快取preparedStatement,也就是PSCache。PSCache對支援遊標的資料庫效能提升巨大,比如說oracle。在mysql下建議關閉。
maxOpenPreparedStatements -1 要啟用PSCache,必須配置大於0,當大於0時,poolPreparedStatements自動觸發修改為true。在Druid中,不會存在Oracle下PSCache佔用記憶體過多的問題,可以把這個數值配置大一些,比如說100
validationQuery 用來檢測連線是否有效的sql,要求是一個查詢語句。如果validationQuery為null,testOnBorrow、testOnReturn、testWhileIdle都不會其作用。
validationQueryTimeout 單位:秒,檢測連線是否有效的超時時間。底層呼叫jdbc
Statement物件的void setQueryTimeout(int seconds)方法
testOnBorrow true 申請連線時執行validationQuery檢測連線是否有效,做了這個配置會降低效能
testOnReturn false 歸還連線時執行validationQuery檢測連線是否有效,做了這個配置會降低效能
testWhileIdle false 建議配置為true,不影響效能,並且保證安全性。申請連線的時候檢測,如果空閒時間大於timeBetweenEvictionRunsMillis,執行validationQuery檢測連線是否有效
timeBetweenEvictionRunsMillis 1分鐘
( 1.0.14 )
有兩個含義: 1) Destroy執行緒會檢測連線的間隔時間 2) testWhileIdle的判斷依據,詳細看testWhileIdle屬性的說明
numTestsPerEvictionRun 不再使用,一個DruidDataSource只支援一個EvictionRun
minEvictableIdleTimeMillis 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,是組合關係,並非替換關係

引入druid依賴

<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid-spring-boot-starter</artifactId>
    <version>1.1.9</version>
</dependency>

專案啟動後可通過show processlist檢視mysql的全部執行緒是否和配置的initial-size一致。

8.3.druid監控測試

訪問ip:port/druid驗證即可,url中的/druid要和配置檔案中的url-pattern一致

stat-view-servlet:
    enabled: true
    url-pattern: /druid/*

效果如下:

img

9.整合mybatis

9.1.匯入 MyBatis 所需要的依賴

<dependency>
    <groupId>org.mybatis.spring.boot</groupId>
    <artifactId>mybatis-spring-boot-starter</artifactId>
    <version>2.1.3</version>
</dependency>

9.2.配置資料庫連線資訊

spring:
  datasource:
    username: root
    password: admin
    #?serverTimezone=UTC解決時區的報錯
    url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
    driver-class-name: com.mysql.cj.jdbc.Driver
    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

9.3.建立實體類

@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
    private int id;
    private String name;
    private String pwd;
}

9.4.建立mapper

UserMapper.java

// 這個註解表示了這是一個 mybatis 的 mapper 類
@Mapper
@Repository
public interface UserMapper {

    List<User> queryUserList();

    User queryUserById(int id);

    int addUser(User user);

    int updateUser(User user);

    int deleteUser(int id);
}
  1. 對應的Mapper對映檔案

    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">
    <!--namespace=繫結一個對應的Dao/Mapper介面-->
    <mapper namespace="com.wyl.mapper.UserMapper">
        
        <select id="queryUserList" resultType="User">
            select * from mybatis.user;
        </select>
    
        <select id="queryUserById" resultType="User">
            select * from mybatis.user where id = #{id};
        </select>
    
        <insert id="addUser" parameterType="User">
            insert into mybatis.user (id, name, pwd) values (#{id},#{name},#{pwd});
        </insert>
    
        <update id="updateUser" parameterType="User">
            update mybatis.user set name=#{name},pwd = #{pwd} where id = #{id};
        </update>
    
        <delete id="deleteUser" parameterType="int">
            delete from mybatis.user where id = #{id}
        </delete>
    </mapper>
    
  2. maven配置資源過濾問題

    <resources>
        <resource>
            <directory>src/main/java</directory>
            <includes>
                <include>**/*.xml</include>
            </includes>
            <filtering>true</filtering>
        </resource>
    </resources>
    
  3. 編寫部門的 UserController 進行測試!

    @RestController
    public class UserController {
        @Autowired
        private UserMapper userMapper;
    
        @GetMapping("/queryUserList")
        public List<User> queryUserList() {
            List<User> userList = userMapper.queryUserList();
    
            for (User user : userList) {
                System.out.println(user);
            }
    
            return userList;
        }
        
         //新增一個使用者
        @GetMapping("/addUser")
        public String addUser() {
            userMapper.addUser(new User(7,"wyl","123456"));
            return "ok";
        }
    
        //修改一個使用者
        @GetMapping("/updateUser")
        public String updateUser() {
            userMapper.updateUser(new User(7,"wjm","123456"));
            return "ok";
        }
    
        @GetMapping("/deleteUser")
        public String deleteUser() {
            userMapper.deleteUser(7);
    
            return "ok";
        }
    }
    

啟動專案訪問進行測試!

9.5.分頁

新增相關依賴

首先,我們需要在 pom.xml 檔案中新增分頁外掛依賴包。

pom.xml

<!-- pagehelper -->
<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.2.5</version>
</dependency>

新增相關配置

然後在 application.yml 配置檔案中新增分頁外掛有關的配置。

application.yml

# pagehelper   
pagehelper:
    helperDialect: mysql
    reasonable: true
    supportMethodsArguments: true
    params: count=countSql

編寫分頁程式碼

首先,在 DAO 層新增一個分頁查詢方法。這個查詢方法跟查詢全部資料的方法除了名稱幾乎一樣。

SysUserMapper.java

import java.util.List;
import com.louis.springboot.demo.model.SysUser;

public interface SysUserMapper {
    int deleteByPrimaryKey(Long id);

    int insert(SysUser record);

    int insertSelective(SysUser record);

    SysUser selectByPrimaryKey(Long id);

    int updateByPrimaryKeySelective(SysUser record);

    int updateByPrimaryKey(SysUser record);
    
    /**
     * 查詢全部使用者
     * @return
     */
    List<SysUser> selectAll();
    
    /**
     * 分頁查詢使用者
     * @return
     */
    List<SysUser> selectPage();
}

然後在 SysUserMapper.xml 中加入selectPage的實現,當然你也可以直接用@Select註解將查詢語句直接寫在DAO程式碼,但我們這裡選擇寫在XML對映檔案,這是一個普通的查詢全部記錄的查詢語句,並不需要寫分頁SQL,分頁外掛會攔截查詢請求,並讀取前臺傳來的分頁查詢引數重新生成分頁查詢語句。

SysUserMapper.xml

<select id="selectPage"  resultMap="BaseResultMap">
  select 
  <include refid="Base_Column_List" />
  from sys_user
</select>

服務層通過呼叫DAO層程式碼完成分頁查詢,這裡統一封裝分頁查詢的請求和結果類,從而避免因為替換ORM框架而導致服務層、控制層的分頁介面也需要變動的情況,替換ORM框架也不會影響服務層以上的分頁介面,起到了解耦的作用。

SysUserService.java

import java.util.List;
import com.louis.springboot.demo.model.SysUser;
import com.louis.springboot.demo.util.PageRequest;
import com.louis.springboot.demo.util.PageResult;

public interface SysUserService {

    /**
     * 根據使用者ID查詢使用者
     * @param userId
     * @return
     */
    SysUser findByUserId(Long userId);

    /**
     * 查詢所有使用者
     * @return
     */
    List<SysUser> findAll();

    /**
     * 分頁查詢介面
     * 這裡統一封裝了分頁請求和結果,避免直接引入具體框架的分頁物件, 如MyBatis或JPA的分頁物件
     * 從而避免因為替換ORM框架而導致服務層、控制層的分頁介面也需要變動的情況,替換ORM框架也不會
     * 影響服務層以上的分頁介面,起到了解耦的作用
     * @param pageRequest 自定義,統一分頁查詢請求
     * @return PageResult 自定義,統一分頁查詢結果
     */
    PageResult findPage(PageRequest pageRequest);
}

服務實現類通過呼叫分頁外掛完成最終的分頁查詢,關鍵程式碼是 PageHelper.startPage(pageNum, pageSize),將前臺分頁查詢引數傳入並攔截MyBtis執行實現分頁效果。

SysUserServiceImpl.java

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.github.pagehelper.PageHelper;
import com.github.pagehelper.PageInfo;
import com.louis.springboot.demo.dao.SysUserMapper;
import com.louis.springboot.demo.model.SysUser;
import com.louis.springboot.demo.service.SysUserService;
import com.louis.springboot.demo.util.PageRequest;
import com.louis.springboot.demo.util.PageResult;
import com.louis.springboot.demo.util.PageUtils;

@Service
public class SysUserServiceImpl implements SysUserService {
    
    @Autowired
    private SysUserMapper sysUserMapper;
    
    @Override
    public SysUser findByUserId(Long userId) {
        return sysUserMapper.selectByPrimaryKey(userId);
    }

    @Override
    public List<SysUser> findAll() {
        return sysUserMapper.selectAll();
    }
    
    @Override
    public PageResult findPage(PageRequest pageRequest) {
        return PageUtils.getPageResult(pageRequest, getPageInfo(pageRequest));
    }
    
    /**
     * 呼叫分頁外掛完成分頁
     * @param pageQuery
     * @return
     */
    private PageInfo<SysUser> getPageInfo(PageRequest pageRequest) {
        int pageNum = pageRequest.getPageNum();
        int pageSize = pageRequest.getPageSize();
        PageHelper.startPage(pageNum, pageSize);
        List<SysUser> sysMenus = sysUserMapper.selectPage();
        return new PageInfo<SysUser>(sysMenus);
    }
}

在控制器SysUserController中新增分頁查詢方法,並呼叫服務層的分頁查詢方法。

SysUserController.java

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.louis.springboot.demo.service.SysUserService;
import com.louis.springboot.demo.util.PageRequest;

@RestController
@RequestMapping("user")
public class SysUserController {

    @Autowired
    private SysUserService sysUserService;
    
    @GetMapping(value="/findByUserId")
    public Object findByUserId(@RequestParam Long userId) {
        return sysUserService.findByUserId(userId);
    }
    
    @GetMapping(value="/findAll")
    public Object findAll() {
        return sysUserService.findAll();
    }
    
    @PostMapping(value="/findPage")
    public Object findPage(@RequestBody PageRequest pageQuery) {
        return sysUserService.findPage(pageQuery);
    }
}

分頁查詢請求封裝類。

PageRequest.java

/**
 * 分頁請求
 */
public class PageRequest {
    /**
     * 當前頁碼
     */
    private int pageNum;
    /**
     * 每頁數量
     */
    private int pageSize;
    
    public int getPageNum() {
        return pageNum;
    }
    public void setPageNum(int pageNum) {
        this.pageNum = pageNum;
    }
    public int getPageSize() {
        return pageSize;
    }
    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
    }
}

分頁查詢結果封裝類。

PageResult.java

import java.util.List;
/**
 * 分頁返回結果
 */
public class PageResult {
    /**
     * 當前頁碼
     */
    private int pageNum;
    /**
     * 每頁數量
     */
    private int pageSize;
    /**
     * 記錄總數
     */
    private long totalSize;
    /**
     * 頁碼總數
     */
    private int totalPages;
    /**
     * 資料模型
     */
    private List<?> content;
    public int getPageNum() {
        return pageNum;
    }
    public void setPageNum(int pageNum) {
        this.pageNum = pageNum;
    }
    public int getPageSize() {
        return pageSize;
    }
    public void setPageSize(int pageSize) {
        this.pageSize = pageSize;
    }
    public long getTotalSize() {
        return totalSize;
    }
    public void setTotalSize(long totalSize) {
        this.totalSize = totalSize;
    }
    public int getTotalPages() {
        return totalPages;
    }
    public void setTotalPages(int totalPages) {
        this.totalPages = totalPages;
    }
    public List<?> getContent() {
        return content;
    }
    public void setContent(List<?> content) {
        this.content = content;
    }
}

分頁查詢相關工具類。

PageUtils.java

import com.github.pagehelper.PageInfo;
public class PageUtils {

    /**
     * 將分頁資訊封裝到統一的介面
     * @param pageRequest 
     * @param page
     * @return
     */
    public static PageResult getPageResult(PageRequest pageRequest, PageInfo<?> pageInfo) {
        PageResult pageResult = new PageResult();
        pageResult.setPageNum(pageInfo.getPageNum());
        pageResult.setPageSize(pageInfo.getPageSize());
        pageResult.setTotalSize(pageInfo.getTotal());
        pageResult.setTotalPages(pageInfo.getPages());
        pageResult.setContent(pageInfo.getList());
        return pageResult;
    }
}

編譯測試執行

啟動應用,訪問:localhost:8088/swagger-ui.html,找到對應介面,模擬測試,結果如下。

引數:pageNum: 1, pageSize: 5

10.事務管理

SpringBoot 使用事務非常簡單,底層依然採用的是Spring本身提供的事務管理

• 在入口類中使用註解 @EnableTransactionManagement 開啟事務支援

• 在訪問資料庫的Service方法上新增註解 @Transactional 即可

案例思路

通過SpringBoot +MyBatis實現對資料庫學生表的更新操作,在service層的方法中構建異常,檢視事務是否生效;

專案名稱:springboot--transacation

10.1.實現步驟

10.1.1.StudentController

@Controller
public class SpringBootController {

    @Autowired
    private StudentService studentService;
    @RequestMapping(value = "/springBoot/update")
    public @ResponseBody Object update() {
        Student student = new Student();
        student.setId(1);
        student.setName("Mark");
        student.setAge(100);
        int updateCount = studentService.update(student);
        return updateCount;
    }
}

10.1.2.StudentService介面

public interface StudentService {

    /**
     * 根據學生標識更新學生資訊
     * @param student
     * @return
     */
    int update(Student student);
}

10.1.3.StudentServiceImpl

介面實現類中對更新學生方法進行實現,並構建一個異常,同時在該方法上加@Transactional註解

@Override
@Transactional //新增此註解說明該方法新增的事務管理
public int update(Student student) {
    int updateCount = studentMapper.updateByPrimaryKeySelective(student);
    System.out.println("更新結果:" + updateCount);
    //在此構造一個除數為0的異常,測試事務是否起作用
    int a = 10/0;
    return updateCount;
}

10.1.4.Application

在類上加@EnableTransactionManagement開啟事務支援

@EnableTransactionManagement可選,但是@Service必須新增事務才生效

@SpringBootApplication
@EnableTransactionManagement //SpringBoot開啟事務的支援
public class Application {

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

}

10.1.5.啟動Application

1.檢視資料庫,並沒有修改資料,通過以上結果,說明事務起作用了。

2.註釋掉StudentServiceImpl上的@Transactional測試----資料庫發上來改變。

11.SpringMVC註解

SpringBoot下的SpringMVC和之前的SpringMVC使用是完全一樣的,主要有以下註解:

1.@Controller

Spring MVC的註解,處理http請求

2.@RestController

Spring4後新增註解,是@Controller註解功能的增強,是@Controller與@ResponseBody的組合註解;

如果一個Controller類新增了@RestController,那麼該Controller類下的所有方法都相當於新增了@ResponseBody註解;

用於返回字串或json資料。

案例:

• 建立MyUserController類,演示@RestController替代@Controller + @ResponseBody

@RestController
public class MyUserController {
    @Autowired
    private UserService userService;

    @RequestMapping("/user/getUser")
    public Object getUser(){
        return userService.getUser(1);
    }
}

3.@RequestMapping(常用)

支援Get請求,也支援Post請求

4.@GetMapping

RequestMapping和Get請求方法的組合只支援Get請求;Get請求主要用於查詢操作。

5.@PostMapping

RequestMapping和Post請求方法的組合只支援Post請求;Post請求主要使用者新增資料。

6.@PutMapping

RequestMapping和Put請求方法的組合只支援Put請求;Put通常用於修改資料。

7.@DeleteMapping

RequestMapping 和 Delete請求方法的組合只支援Delete請求;通常用於刪除資料。

12.RESTful實現

Spring boot開發RESTFul 主要是幾個註解實現:

@PathVariable:獲取url中的資料,該註解是實現RESTFul最主要的一個註解。

@PostMapping:接收和處理Post方式的請求

@DeleteMapping:接收delete方式的請求,可以使用GetMapping代替

@PutMapping:接收put方式的請求,可以用PostMapping代替

@GetMapping:接收get方式的請求

RESTful的優點

• 輕量,直接基於http,不再需要任何別的諸如訊息協議,get/post/put/delete為CRUD操作

• 面向資源,一目瞭然,具有自解釋性。

• 資料描述簡單,一般以xml,json做資料交換。

• 無狀態,在呼叫一個介面(訪問、操作資源)的時候,可以不用考慮上下文,不用考慮當前狀態,極大的降低了複雜度。

• 簡單、低耦合

12.1.案例

使用RESTful風格模擬實現對學生的增刪改查操作

該專案整合了MyBatis、spring、SpringMVC,通過模擬實現對學生的增刪改查操作。

1.建立RESTfulController,並編寫程式碼

@RestController
public class RESTfulController {

    /**
     * 新增學生
     * 請求地址:http://localhost:8080/springboot-restful/springBoot/student/wyl/23
     * 請求方式:POST
     * @param name
     * @param age
     * @return
     */
    @PostMapping(value = "/springBoot/student/{name}/{age}")
    public Object addStudent(@PathVariable("name") String name,
                             @PathVariable("age") Integer age) {
        Map retMap = new HashMap();
        retMap.put("name",name);
        retMap.put("age",age);
        return retMap;
    }
    /**
     * 刪除學生
     * 請求地址:http://localhost:8080/springboot-restful/springBoot/student/1
     * 請求方式:Delete
     * @param id
     * @return
     */
    @DeleteMapping(value = "/springBoot/student/{id}")
    public Object removeStudent(@PathVariable("id") Integer id) {

        return "刪除的學生id為:" + id;
    }

    /**
     * 修改學生資訊
     * 請求地址:http://localhost:8080/springboot-restful/springBoot/student/2
     * 請求方式:Put
     * @param id
     * @return
     */
    @PutMapping(value = "/springBoot/student/{id}")
    public Object modifyStudent(@PathVariable("id") Integer id) {

        return "修改學生的id為" + id;
    }

    @GetMapping(value = "/springBoot/student/{id}")
    public Object queryStudent(@PathVariable("id") Integer id) {

        return "查詢學生的id為" + id;
    }
}

12.2.RESTful原則

• 增post請求、刪delete請求、改put請求、查get請求

• 請求路徑不要出現動詞

例如:查詢訂單介面

/boot/order/1021/1(推薦)

/boot/queryOrder/1021/1(不推薦)

• 分頁、排序等操作,不需要使用斜槓傳引數

例如:訂單列表介面 /boot/orders?page=1&sort=desc

一般傳的引數不是資料庫表的欄位,可以不採用斜槓

13.靜態資源處理

13.1.對哪些目錄對映?

classpath:/META-INF/resources/ 
classpath:/resources/
classpath:/static/ 
classpath:/public/
/:當前專案的根路徑

就我們在上面五個目錄下放靜態資源(比如:a.png等),可以直接訪問(http://localhost:8080/a.png),類似於以前web專案的webapp下;放到其他目錄下無法被訪問。

優先順序:resources > static(預設) > public

13.2.原始碼分析

SpringBoot自動配置的WebMvcAutoConfiguration.java

  • SpringBoot中,SpringMVC的web配置都在 WebMvcAutoConfiguration 這個配置類裡面;

  • 我們可以去看看 WebMvcAutoConfigurationAdapter 中有很多配置方法;

  • 有一個方法:addResourceHandlers 新增資源處理

    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        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 配置
        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/** , 都需要去 classpath:/META-INF/resources/webjars/ 找對應的資源;

13.2.1.什麼是webjars 呢?

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

13.2.2.第一種靜態資源對映規則

使用SpringBoot需要使用Webjars,我們可以去搜尋一下:

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

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

<dependency>
    <groupId>org.webjars</groupId>
    <artifactId>jquery</artifactId>
    <version>3.4.1</version>
</dependency>

匯入完畢,檢視webjars目錄結構,並訪問Jquery.js檔案!

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

1595506019658

13.2.3.第二種靜態資源對映規則

1、那我們專案中要是使用自己的靜態資源該怎麼匯入呢?我們看下一行程式碼;

1595516976999

2、我們去找staticPathPattern發現第二種對映規則 :/** , 訪問當前的專案任意資源,它會去找 resourceProperties 這個類,我們可以點進去看一下分析:

// 進入方法
public String[] getStaticLocations() {
    return this.staticLocations;
}
// 找到對應的值
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
// 找到路徑
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { 
    "classpath:/META-INF/resources/",
  	"classpath:/resources/", 
    "classpath:/static/", 
    "classpath:/public/" 
};

3、ResourceProperties 可以設定和我們靜態資源有關的引數;這裡面指向了它會去尋找資源的資料夾,即上面陣列的內容。

4、所以得出結論,以下四個目錄存放的靜態資源可以被我們識別:

"classpath:/META-INF/resources/"
"classpath:/resources/"
"classpath:/static/"
"classpath:/public/"

5、我們可以在resources根目錄下新建對應的資料夾,都可以存放我們的靜態檔案;

1595517831392

6、比如我們訪問 http://localhost:8080/1.js , 他就會去這些資料夾中尋找對應的靜態資原始檔;

1595517869049

13.2.4.引用

不用寫static 路徑

 <link rel="stylesheet" href="/layui/css/layui.css">
 <link rel="stylesheet" href="/easyui/default/easyui.css">
 <script src="/layui/jquery-1.10.2.min.js" type="text/javascript"></script>
 <script src="/easyui/jquery.easyui.min.js" type="text/javascript"></script>

13.3.自定義靜態資源路徑

首先,自定義會覆蓋預設!所以沒十足把握的情況下,不建議覆蓋,但可以新增。
兩種方式

13.3.1.配置類程式碼實現

@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {         
    	registry.addResourceHandler("/**").addResourceLocations("file:F:/AppFiles/");
    }
}

13.3.2.配置檔案

spring.resources.static-locations=classpath:/META-INF/resources/,classpath:/resources/,\
  classpath:/static/,classpath:/public/,file:c:/appfiles/

13.3.3.相對路徑配置

以上的情況均是絕對路徑,受限於環境,開發生產LinuxWin等。這種變化情況多,建議理清思路,再決定是否適用。

解決方案

String gitPath = path.getParentFile().getParentFile().getParent() 
	+ File.separator + "logistics" + File.separator + "uploads" 
	+ File.separator;

工具類

spring框架自帶的ResourceUtils,或者結合第三方工具

13.3.4. 歡迎頁與圖示

image-20211108162103407

14.國際化

國際化(Internationalization 簡稱 I18n,其中“I”和“n”分別為首末字元,18 則為中間的字元數)是指軟體開發時應該具備支援多種語言和地區的功能。換句話說就是,開發的軟體需要能同時應對不同國家和地區的使用者訪問,並根據使用者地區和語言習慣,提供相應的、符合用具閱讀習慣的頁面和資料,例如,為中國使用者提供漢語介面顯示,為美國使用者提供提供英語介面顯示。

在 Spring 專案中實現國際化,通常需要以下 3 步:

  1. 編寫國際化資源(配置)檔案;
  2. 使用 ResourceBundleMessageSource 管理國際化資原始檔;
  3. 在頁面獲取國際化內容。

14.1. 編寫國際化資原始檔

在 Spring Boot 的類路徑下建立國際化資原始檔,檔名格式為:基本名_語言程式碼_國家或地區程式碼,例如 login_en_US.properties、login_zh_CN.properties。

以 spring-boot-springmvc-demo1為例,在 src/main/resources 下建立一個 i18n 的目錄,並在該目錄中按照國際化資原始檔命名格式分別建立以下三個檔案,

  • login.properties:無語言設定時生效
  • login_en_US.properties :英語時生效
  • login_zh_CN.properties:中文時生效
    以上國際化資原始檔建立完成後,IDEA 會自動識別它們,並轉換成如下的模式:

img

開啟任意一個國際化資原始檔,並切換為 Resource Bundle 模式,然後點選“+”號,建立所需的國際化屬性,如下圖。

國際化配置檔案

14.2.配置檔案生效探究

Spring Boot 已經對 ResourceBundleMessageSource 提供了預設的自動配置。

Spring Boot 通過 MessageSourceAutoConfiguration 對 ResourceBundleMessageSource 提供了預設配置,其部分原始碼如下。

@Configuration(proxyBeanMethods = false)
@ConditionalOnMissingBean(name = AbstractApplicationContext.MESSAGE_SOURCE_BEAN_NAME, search = SearchStrategy.CURRENT)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE)
@Conditional(org.springframework.boot.autoconfigure.context.MessageSourceAutoConfiguration.ResourceBundleCondition.class)
@EnableConfigurationProperties
public class MessageSourceAutoConfiguration {

    private static final Resource[] NO_RESOURCES = {};

    // 將 MessageSourceProperties 以元件的形式新增到容器中
    // MessageSourceProperties 下的每個屬性都與以 spring.messages 開頭的屬性對應
    @Bean
    @ConfigurationProperties(prefix = "spring.messages")
    public MessageSourceProperties messageSourceProperties() {
        return new MessageSourceProperties();
    }

    //Spring Boot 會從容器中獲取 MessageSourceProperties
    // 讀取國際化資原始檔的 basename(基本名)、encoding(編碼)等資訊
    // 並封裝到 ResourceBundleMessageSource 中
    @Bean
    public MessageSource messageSource(MessageSourceProperties properties) {
        ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
        //讀取國際化資原始檔的 basename (基本名),並封裝到 ResourceBundleMessageSource 中
        if (StringUtils.hasText(properties.getBasename())) {
            messageSource.setBasenames(StringUtils
                    .commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));
        }
        //讀取國際化資原始檔的 encoding (編碼),並封裝到 ResourceBundleMessageSource 中
        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;
    }
    ...
}

從以上原始碼可知:
Spring Boot 將 MessageSourceProperties 以元件的形式新增到容器中;
MessageSourceProperties 的屬性與配置檔案中以“spring.messages”開頭的配置進行了繫結;
Spring Boot 從容器中獲取 MessageSourceProperties 元件,並從中讀取國際化資原始檔的 basename(檔案基本名)、encoding(編碼)等資訊,將它們封裝到 ResourceBundleMessageSource 中;
Spring Boot 將 ResourceBundleMessageSource 以元件的形式新增到容器中,進而實現對國際化資原始檔的管理。

檢視 MessageSourceProperties 類,其程式碼如下。

public class MessageSourceProperties {
    private String basename = "messages";
    private Charset encoding;
    @DurationUnit(ChronoUnit.SECONDS)
    private Duration cacheDuration;
    private boolean fallbackToSystemLocale;
    private boolean alwaysUseMessageFormat;
    private boolean useCodeAsDefaultMessage;

    public MessageSourceProperties() {
        this.encoding = StandardCharsets.UTF_8;
        this.fallbackToSystemLocale = true;
        this.alwaysUseMessageFormat = false;
        this.useCodeAsDefaultMessage = false;
    }
    ...
}

通過以上程式碼,我們可以得到以下 3 點資訊:

  • MessageSourceProperties 為 basename、encoding 等屬性提供了預設值;
  • basename 表示國際化資原始檔的基本名,其預設取值為“message”,即 Spring Boot 預設會獲取類路徑下的 message.properties 以及 message_XXX.properties 作為國際化資原始檔;
  • 在 application.porperties/yml 等配置檔案中,使用配置引數“spring.messages.basename”即可重新指定國際化資原始檔的基本名。

通過以上原始碼分析可知,Spring Boot 已經對國際化資原始檔的管理提供了預設自動配置,我們這裡只需要在 Spring Boot 全域性配置檔案中,使用配置引數“spring.messages.basename”指定我們自定義的國際資原始檔的基本名即可,程式碼如下(當指定多個資原始檔時,用逗號分隔)。

spring.messages.basename=i18n.login

14.3. 獲取國際化內容

由於頁面使用的是 Tymeleaf 模板引擎,因此我們可以通過表示式 #{...} 獲取國際化內容。

以 spring-boot-adminex 為例,在 login.html 中獲取國際化內容,程式碼如下。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0">
    <meta name="description" content="">
    <meta name="author" content="ThemeBucket">
    <link rel="shortcut icon" href="#" type="image/png">
    <title>Login</title>
    <!--將js css 等靜態資源的引用修改為 絕對路徑-->
    <link href="css/style.css" th:href="@{/css/style.css}" rel="stylesheet">
    <link href="css/style-responsive.css" th:href="@{/css/style-responsive.css}" rel="stylesheet">

    <!-- HTML5 shim and Respond.js IE8 support of HTML5 elements and media queries -->
    <!--[if lt IE 9]>

    <script src="js/html5shiv.js" th:src="@{/js/html5shiv.js}"></script>
    <script src="js/respond.min.js" th:src="@{/js/respond.min.js}"></script>
    <![endif]-->
</head>

<body class="login-body">
<div class="container">

    <form class="form-signin" th:action="@{/user/login}" method="post">
        <div class="form-signin-heading text-center">
            <h1 class="sign-title" th:text="#{login.btn}">Sign In</h1>
            <img src="/images/login-logo.png" th:src="@{/images/login-logo.png}" alt=""/>
        </div>
        <div class="login-wrap">
            <p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>
            <input type="text" class="form-control" name="username" placeholder="User ID" autofocus
                   th:placeholder="#{login.username}"/>
            <input type="password" class="form-control" name="password" placeholder="Password"
                   th:placeholder="#{login.password}"/>
            <label class="checkbox">
                <input type="checkbox" value="remember-me" th:text="#{login.remember}">
                <span class="pull-right">
                    <a data-toggle="modal" href="#myModal" th:text="#{login.forgot}"> </a>
                </span>
            </label>
            <button class="btn btn-lg btn-login btn-block" type="submit">
                <i class="fa fa-check"></i>
            </button>

            <div class="registration">
                <!--Thymeleaf 行內寫法-->
                [[#{login.not-a-member}]]
                <a class="" href="/registration.html" th:href="@{/registration.html}">
                    [[#{login.signup}]]
                </a>
                <!--thymeleaf 模板引擎的引數用()代替 ?-->
                <br/>
                <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>
            </div>
        </div>

        <!-- Modal -->
        <div aria-hidden="true" aria-labelledby="myModalLabel" role="dialog" tabindex="-1" id="myModal"
             class="modal fade">
            <div class="modal-dialog">
                <div class="modal-content">
                    <div class="modal-header">
                        <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
                        <h4 class="modal-title">Forgot Password ?</h4>
                    </div>
                    <div class="modal-body">
                        <p>Enter your e-mail address below to reset your password.</p>
                        <input type="text" name="email" placeholder="Email" autocomplete="off"
                               class="form-control placeholder-no-fix">

                    </div>
                    <div class="modal-footer">
                        <button data-dismiss="modal" class="btn btn-default" type="button">Cancel</button>
                        <button class="btn btn-primary" type="button">Submit</button>
                    </div>
                </div>
            </div>
        </div>
        <!-- modal -->
    </form>
</div>
<!-- Placed js at the end of the document so the pages load faster -->
<!-- Placed js at the end of the document so the pages load faster -->
<script src="js/jquery-1.10.2.min.js" th:src="@{/js/jquery-1.10.2.min.js}"></script>
<script src="js/bootstrap.min.js" th:src="@{/js/bootstrap.min.js}"></script>
<script src="js/modernizr.min.js" th:src="@{/js/modernizr.min.js}"></script>
</body>
</html>

14.4.區域資訊解析器自動配置

我們知道,Spring MVC 進行國際化時有 2 個十分重要的物件:

  • Locale:區域資訊物件
  • LocaleResolver:區域資訊解析器,容器中的元件,負責獲取區域資訊物件

我們可以通過以上兩個物件對區域資訊的切換,以達到切換語言的目的。

Spring Boot 在 WebMvcAutoConfiguration 中為區域資訊解析器(LocaleResolver)進行了自動配置,原始碼如下。

    @Bean
    @ConditionalOnMissingBean(name = DispatcherServlet.LOCALE_RESOLVER_BEAN_NAME)
    @SuppressWarnings("deprecation")
    public LocaleResolver localeResolver() {
        if (this.webProperties.getLocaleResolver() == WebProperties.LocaleResolver.FIXED) {
            return new FixedLocaleResolver(this.webProperties.getLocale());
        }
        if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
            return new FixedLocaleResolver(this.mvcProperties.getLocale());
        }
        AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
        Locale locale = (this.webProperties.getLocale() != null) ? this.webProperties.getLocale()
                : this.mvcProperties.getLocale();
        localeResolver.setDefaultLocale(locale);
        return localeResolver;
    }

從以上原始碼可知:

  • 該方法預設向容器中新增了一個區域資訊解析器(LocaleResolver)元件,它會根據請求頭中攜帶的“Accept-Language”引數,獲取相應區域資訊(Locale)物件。
  • 該方法上使用了 @ConditionalOnMissingBean 註解,其引數 name 的取值為 localeResolver(與該方法注入到容器中的元件名稱一致),該註解的含義為:當容器中不存在名稱為 localResolver 元件時,該方法才會生效。換句話說,當我們手動向容器中新增一個名為“localeResolver”的元件時,Spring Boot 自動配置的區域資訊解析器會失效,而我們定義的區域資訊解析器則會生效。

手動切換語言

  1. 修改 login.html 切換語言連結,在請求中攜帶國際化區域資訊,程式碼如下。
<!--thymeleaf 模板引擎的引數用()代替 ?-->
<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>
  1. 建立一個 component 包,並在該包中建立一個區域資訊解析器 MyLocalResolver,程式碼如下。
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 MyLocalResolver implements LocaleResolver {
    @Override
    public Locale resolveLocale(HttpServletRequest request) {
        //獲取請求中引數
        String l = request.getParameter("l");
        //獲取預設的區域資訊解析器
        Locale locale = Locale.getDefault();
        //根據請求中的引數重新構造區域資訊物件
        if (StringUtils.hasText(l)) {
            String[] s = l.split("_");
            locale = new Locale(s[0], s[1]);
        }
        return locale;
    }
    @Override
    public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
    }
}

為了讓我們的區域化資訊能夠生效,我們需要再配置一下這個元件!在我們自己的MvcConofig下新增bean

@Bean
public LocaleResolver localeResolver(){
    return new MyLocalResolver();
}

15.swagger

15.1.新增依賴

<!-- swagger -->
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger2</artifactId>
    <version>2.9.2</version>
</dependency>
<dependency>
    <groupId>io.springfox</groupId>
    <artifactId>springfox-swagger-ui</artifactId>
    <version>2.9.2</version>
</dependency>

15.2.新增配置類

新增一個swagger 配置類,在工程下新建 config 包並新增一個 SwaggerConfig 配置類。

SwaggerConfig.java

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;

@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket createRestApi(){
        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
                .select()
                .apis(RequestHandlerSelectors.any())
                .paths(PathSelectors.any()).build();
    }

    private ApiInfo apiInfo(){
        return new ApiInfoBuilder()
                .title("Kitty API Doc")
                .description("This is a restful api document of Kitty.")
                .version("1.0")
                .build();
    }

}

15.3.新增控制器

新增一個控制器,在工程下新建 controller包並新增一個 HelloController控制器。

HelloController.java

import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;

/* 類註解 */
@Api(value = "desc of class")
@RestController
public class HelloController {

    /* 方法註解 */
    @ApiOperation(value = "desc of method", notes = "")
    @GetMapping(value="/hello")
    public Object hello( /* 引數註解 */ @ApiParam(value = "desc of param" , required=true ) @RequestParam String name) {
        return "Hello " + name + "!";
    }
}

執行即可

15.4.常用註解說明

swagger 通過註解介面生成文件,包括介面名,請求方法,引數,返回資訊等。

@Api: 修飾整個類,用於controller類上

@ApiOperation: 描述一個介面,使用者controller方法上

@ApiParam: 單個引數描述

@ApiModel: 用來物件接收引數,即返回物件

@ApiModelProperty: 物件接收引數時,描述物件的欄位

@ApiResponse: Http響應其中的描述,在ApiResonse中

@ApiResponses: Http響應所有的描述,用在

@ApiIgnore: 忽略這個API

@ApiError: 發生錯誤的返回資訊

@ApiImplicitParam: 一個請求引數

@ApiImplicitParam: 多個請求引數

更多使用說明,參考 Swagger 使用手冊

15.5.新增請求引數

在很多時候,我們需要在呼叫我們每一個介面的時候都攜帶上一些通用引數,比如採取token驗證邏輯的往往在介面請求時需要把token也一起傳入後臺,接下來,我們就來講解一下如何給Swagger新增固定的請求引數。

修改SwaggerConfig配置類,替換成如下內容,利用ParameterBuilder構成請求引數。

SwaggerConfig.java

@Configuration
@EnableSwagger2
public class SwaggerConfig {

    @Bean
    public Docket createRestApi(){
        // 新增請求引數,我們這裡把token作為請求頭部引數傳入後端
        ParameterBuilder parameterBuilder = new ParameterBuilder();  
        List<Parameter> parameters = new ArrayList<Parameter>();  
        parameterBuilder.name("token").description("令牌")
            .modelRef(new ModelRef("string")).parameterType("header").required(false).build();  
        parameters.add(parameterBuilder.build());  
        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()).select()
                .apis(RequestHandlerSelectors.any()).paths(PathSelectors.any())
                .build().globalOperationParameters(parameters);
//        return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
//                .select()
//                .apis(RequestHandlerSelectors.any())
//                .paths(PathSelectors.any()).build();
    }

    private ApiInfo apiInfo(){
        return new ApiInfoBuilder()
                .title("Swagger API Doc")
                .description("This is a restful api document of Swagger.")
                .version("1.0")
                .build();
    }

}

完成之後重新啟動應用,再次檢視hello介面,可以看到已經支援傳送token請求引數了。

15.6.配置API分組

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

@Bean
public Docket docket(Environment environment) {
   return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo())
      .groupName("group1") // 配置分組
       ....
}

如何配置多個分組?配置多個分組只需要配置多個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");
}

16.攔截器

在 Spring Boot 專案中,使用攔截器功能通常需要以下 3 步:

  1. 定義攔截器;

  2. 註冊攔截器;

  3. 指定攔截規則(如果是攔截所有,靜態資源也會被攔截)。

16.1.定義攔截器

在 Spring Boot 中定義攔截器十分的簡單,只需要建立一個攔截器類,並實現 HandlerInterceptor 介面即可。

HandlerInterceptor 介面中定義以下 3 個方法,如下表。

返回值型別 方法宣告 描述
boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) 該方法在控制器處理請求方法前執行,其返回值表示是否中斷後續操作,返回 true 表示繼續向下執行,返回 false 表示中斷後續操作。
void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) 該方法在控制器處理請求方法呼叫之後、解析檢視之前執行,可以通過此方法對請求域中的模型和檢視做進一步修改。
void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) 該方法在檢視渲染結束後執行,可以通過此方法實現資源清理、記錄日誌資訊等工作。
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
    /**
     * 目標方法執行前
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        Object loginUser = request.getSession().getAttribute("loginUser");
        if (loginUser == null) {
            //未登入,返回登陸頁
            request.setAttribute("msg", "您沒有許可權進行此操作,請先登陸!");
            request.getRequestDispatcher("/index.html").forward(request, response);
            return false;
        } else {
            //放行
            return true;
        }
    }

    /**
     * 目標方法執行後
     *
     * @param request
     * @param response
     * @param handler
     * @param modelAndView
     * @throws Exception
     */
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        log.info("postHandle執行{}", modelAndView);
    }

    /**
     * 頁面渲染後
     *
     * @param request
     * @param response
     * @param handler
     * @param ex
     * @throws Exception
     */
    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        log.info("afterCompletion執行異常{}", ex);
    }
}

16.2.註冊攔截器

建立一個實現了 WebMvcConfigurer 介面的配置類(使用了 @Configuration 註解的類),重寫 addInterceptors() 方法,並在該方法中呼叫 registry.addInterceptor() 方法將自定義的攔截器註冊到容器中。

在配置類 MyMvcConfig 中,新增以下方法註冊攔截器,程式碼如下。

@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
    ......
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new LoginInterceptor());
    }
}

16.3.指定攔截規則

在使用 registry.addInterceptor() 方法將攔截器註冊到容器中後,我們便可以繼續指定攔截器的攔截規則了,程式碼如下

@Slf4j
@Configuration
public class MyConfig implements WebMvcConfigurer {
    ......
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        log.info("註冊攔截器");
        registry.addInterceptor(new LoginInterceptor()).addPathPatterns("/**") //攔截所有請求,包括靜態資原始檔
                .excludePathPatterns("/", "/login", "/index.html", "/user/login", "/css/**", "/images/**", "/js/**", "/fonts/**"); //放行登入頁,登陸操作,靜態資源
    }
}

在指定攔截器攔截規則時,呼叫了兩個方法,這兩個方法的說明如下:

  • addPathPatterns:該方法用於指定攔截路徑,例如攔截路徑為“/**”,表示攔截所有請求,包括對靜態資源的請求。
  • excludePathPatterns:該方法用於排除攔截路徑,即指定不需要被攔截器攔截的請求。

至此,攔截器的基本功能已經完成,接下來,我們先實現 spring-boot-adminex 的登陸功能,為驗證登陸攔截做準備。

17.異常處理

Spring Boot 提供了一套預設的異常處理機制,一旦程式中出現了異常,Spring Boot 會自動識別客戶端的型別(瀏覽器客戶端或機器客戶端),並根據客戶端的不同,以不同的形式展示異常資訊。

  1. 對於瀏覽器客戶端而言,Spring Boot 會響應一個“ whitelabel”錯誤檢視,以 HTML 格式呈現錯誤資訊

image-20211108174302407

  1. 對於機器客戶端而言,Spring Boot 將生成 JSON 響應,來展示異常訊息。
{
    "timestamp": "2021-07-12T07:05:29.885+00:00",
    "status": 404,
    "error": "Not Found",
    "message": "No message available",
    "path": "/m1ain.html"
}

Spring Boot 異常處理自動配置原理

Spring Boot 通過配置類 ErrorMvcAutoConfiguration 對異常處理提供了自動配置,該配置類向容器中注入了以下 4 個元件。

  • ErrorPageCustomizer:該元件會在在系統發生異常後,預設將請求轉發到“/error”上。

  • BasicErrorController:處理預設的“/error”請求。

  • DefaultErrorViewResolver:預設的錯誤檢視解析器,將異常資訊解析到相應的錯誤檢視上。

  • DefaultErrorAttributes:用於頁面上共享異常資訊。

下面,我們依次對這四個元件進行詳細的介紹。

ErrorPageCustomizer

ErrorMvcAutoConfiguration 向容器中注入了一個名為 ErrorPageCustomizer 的元件,它主要用於定製錯誤頁面的響應規則。

@Bean
public ErrorPageCustomizer errorPageCustomizer(DispatcherServletPath dispatcherServletPath) {
    return new ErrorPageCustomizer(this.serverProperties, dispatcherServletPath);
}

ErrorPageCustomizer 通過 registerErrorPages() 方法來註冊錯誤頁面的響應規則。當系統中發生異常後,ErrorPageCustomizer 元件會自動生效,並將請求轉發到 “/error”上,交給 BasicErrorController 進行處理,其部分程式碼如下。

@Override
public void registerErrorPages(ErrorPageRegistry errorPageRegistry) {
    //將請求轉發到 /errror(this.properties.getError().getPath())上
    ErrorPage errorPage = new ErrorPage(this.dispatcherServletPath.getRelativePath(this.properties.getError().getPath()));
    // 註冊錯誤頁面
    errorPageRegistry.addErrorPages(errorPage);
}

BasicErrorController

ErrorMvcAutoConfiguration 還向容器中注入了一個錯誤控制器元件 BasicErrorController,程式碼如下。

@Bean
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
                                                 ObjectProvider<ErrorViewResolver> errorViewResolvers) {
    return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
            errorViewResolvers.orderedStream().collect(Collectors.toList()));
}

BasicErrorController 的定義如下。

//BasicErrorController 用於處理 “/error” 請求
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}")
public class BasicErrorController extends AbstractErrorController {
    ......
    /**
     * 該方法用於處理瀏覽器客戶端的請求發生的異常
     * 生成 html 頁面來展示異常資訊
     * @param request
     * @param response
     * @return
     */
    @RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
    public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
        //獲取錯誤狀態碼
        HttpStatus status = getStatus(request);
        //getErrorAttributes 根據錯誤資訊來封裝一些 model 資料,用於頁面顯示
        Map<String, Object> model = Collections
                .unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
        //為響應物件設定錯誤狀態碼
        response.setStatus(status.value());
        //呼叫 resolveErrorView() 方法,使用錯誤檢視解析器生成 ModelAndView 物件(包含錯誤頁面地址和頁面內容)
        ModelAndView modelAndView = resolveErrorView(request, response, status, model);
        return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);
    }

    /**
     * 該方法用於處理機器客戶端的請求發生的錯誤
     * 產生 JSON 格式的資料展示錯誤資訊
     * @param request
     * @return
     */
    @RequestMapping
    public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
        HttpStatus status = getStatus(request);
        if (status == HttpStatus.NO_CONTENT) {
            return new ResponseEntity<>(status);
        }
        Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
        return new ResponseEntity<>(body, status);
    }
    ......
}

Spring Boot 通過 BasicErrorController 進行統一的錯誤處理(例如預設的“/error”請求)。Spring Boot 會自動識別發出請求的客戶端的型別(瀏覽器客戶端或機器客戶端),並根據客戶端型別,將請求分別交給 errorHtml() 和 error() 方法進行處理。

返回值型別 方法宣告 客戶端型別 錯誤資訊返型別
ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) 瀏覽器客戶端 text/html(錯誤頁面)
ResponseEntity<Map<String, Object>> error(HttpServletRequest request) 機器客戶端(例如安卓、IOS、Postman 等等) JSON

換句話說,當使用瀏覽器訪問出現異常時,會進入 BasicErrorController 控制器中的 errorHtml() 方法進行處理,當使用安卓、IOS、Postman 等機器客戶端訪問出現異常時,就進入error() 方法處理。

在 errorHtml() 方法中會呼叫父類(AbstractErrorController)的 resolveErrorView() 方法,程式碼如下。

protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
                                        Map<String, Object> model) {
    //獲取容器中的所有的錯誤檢視解析器來處理該異常資訊
    for (ErrorViewResolver resolver : this.errorViewResolvers) {
        //呼叫錯誤檢視解析器的 resolveErrorView 解析到錯誤檢視頁面
        ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
        if (modelAndView != null) {
            return modelAndView;
        }
    }
    return null;
}

從上述原始碼可以看出,在響應頁面的時候,會在父類的 resolveErrorView 方法中獲取容器中所有的 ErrorViewResolver 物件(錯誤檢視解析器,包括 DefaultErrorViewResolver 在內),一起來解析異常資訊。

DefaultErrorViewResolver

ErrorMvcAutoConfiguration 還向容器中注入了一個預設的錯誤檢視解析器元件 DefaultErrorViewResolver,程式碼如下。

@Bean
@ConditionalOnBean(DispatcherServlet.class)
@ConditionalOnMissingBean(ErrorViewResolver.class)
DefaultErrorViewResolver conventionErrorViewResolver() {
    return new DefaultErrorViewResolver(this.applicationContext, this.resources);
}

當發出請求的客戶端為瀏覽器時,Spring Boot 會獲取容器中所有的 ErrorViewResolver 物件(錯誤檢視解析器),並分別呼叫它們的 resolveErrorView() 方法對異常資訊進行解析,其中自然也包括 DefaultErrorViewResolver(預設錯誤資訊解析器)。

DefaultErrorViewResolver 的部分程式碼如下。

public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {

    private static final Map<HttpStatus.Series, String> SERIES_VIEWS;

    static {
        Map<HttpStatus.Series, String> views = new EnumMap<>(HttpStatus.Series.class);
        views.put(Series.CLIENT_ERROR, "4xx");
        views.put(Series.SERVER_ERROR, "5xx");
        SERIES_VIEWS = Collections.unmodifiableMap(views);
    }

    ......

    @Override
    public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
        //嘗試以錯誤狀態碼作為錯誤頁面名進行解析
        ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
        if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
            //嘗試以 4xx 或 5xx 作為錯誤頁面頁面進行解析
            modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
        }
        return modelAndView;
    }

    private ModelAndView resolve(String viewName, Map<String, Object> model) {
        //錯誤模板頁面,例如 error/404、error/4xx、error/500、error/5xx
        String errorViewName = "error/" + viewName;
        //當模板引擎可以解析這些模板頁面時,就用模板引擎解析
        TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
                this.applicationContext);
        if (provider != null) {
            //在模板能夠解析到模板頁面的情況下,返回 errorViewName 指定的檢視
            return new ModelAndView(errorViewName, model);
        }
        //若模板引擎不能解析,則去靜態資原始檔夾下查詢 errorViewName 對應的頁面
        return resolveResource(errorViewName, model);
    }

    private ModelAndView resolveResource(String viewName, Map<String, Object> model) {
        //遍歷所有靜態資原始檔夾
        for (String location : this.resources.getStaticLocations()) {
            try {
                Resource resource = this.applicationContext.getResource(location);
                //靜態資原始檔夾下的錯誤頁面,例如error/404.html、error/4xx.html、error/500.html、error/5xx.html
                resource = resource.createRelative(viewName + ".html");
                //若靜態資原始檔夾下存在以上錯誤頁面,則直接返回
                if (resource.exists()) {
                    return new ModelAndView(new DefaultErrorViewResolver.HtmlResourceView(resource), model);
                }
            } catch (Exception ex) {
            }
        }
        return null;
    }
    ......
}

DefaultErrorViewResolver 解析異常資訊的步驟如下:

  1. 根據錯誤狀態碼(例如 404、500、400 等),生成一個錯誤檢視 error/status,例如 error/404、error/500、error/400。
  2. 嘗試使用模板引擎解析 error/status 檢視,即嘗試從 classpath 類路徑下的 templates 目錄下,查詢 error/status.html,例如 error/404.html、error/500.html、error/400.html。
  3. 若模板引擎能夠解析到 error/status 檢視,則將檢視和資料封裝成 ModelAndView 返回並結束整個解析流程,否則跳轉到第 4 步。
  4. 依次從各個靜態資原始檔夾中查詢 error/status.html,若在靜態資料夾中找到了該錯誤頁面,則返回並結束整個解析流程,否則跳轉到第 5 步。
  5. 將錯誤狀態碼(例如 404、500、400 等)轉換為 4xx 或 5xx,然後重複前 4 個步驟,若解析成功則返回並結束整個解析流程,否則跳轉第 6 步。
  6. 處理預設的 “/error ”請求,使用 Spring Boot 預設的錯誤頁面(Whitelabel Error Page)。

DefaultErrorAttributes

ErrorMvcAutoConfiguration 還向容器中注入了一個元件預設錯誤屬性處理工具 DefaultErrorAttributes,程式碼如下。

@Bean
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
    return new DefaultErrorAttributes();
}

DefaultErrorAttributes 是 Spring Boot 的預設錯誤屬性處理工具,它可以從請求中獲取異常或錯誤資訊,並將其封裝為一個 Map 物件返回,其部分程式碼如下。

public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver, Ordered {
    ......
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
        Map<String, Object> errorAttributes = getErrorAttributes(webRequest, options.isIncluded(Include.STACK_TRACE));
        if (!options.isIncluded(Include.EXCEPTION)) {
            errorAttributes.remove("exception");
        }
        if (!options.isIncluded(Include.STACK_TRACE)) {
            errorAttributes.remove("trace");
        }
        if (!options.isIncluded(Include.MESSAGE) && errorAttributes.get("message") != null) {
            errorAttributes.remove("message");
        }
        if (!options.isIncluded(Include.BINDING_ERRORS)) {
            errorAttributes.remove("errors");
        }
        return errorAttributes;
    }

    private Map<String, Object> getErrorAttributes(WebRequest webRequest, boolean includeStackTrace) {
        Map<String, Object> errorAttributes = new LinkedHashMap<>();
        errorAttributes.put("timestamp", new Date());
        addStatus(errorAttributes, webRequest);
        addErrorDetails(errorAttributes, webRequest, includeStackTrace);
        addPath(errorAttributes, webRequest);
        return errorAttributes;
    }
    ......
}

在 Spring Boot 預設的 Error 控制器(BasicErrorController)處理錯誤時,會呼叫 DefaultErrorAttributes 的 getErrorAttributes() 方法獲取錯誤或異常資訊,並封裝成 model 資料(Map 物件),返回到頁面或 JSON 資料中。該 model 資料主要包含以下屬性:

  • timestamp:時間戳;
  • status:錯誤狀態碼
  • error:錯誤的提示
  • exception:導致請求處理失敗的異常物件
  • message:錯誤/異常訊息
  • trace: 錯誤/異常棧資訊
  • path:錯誤/異常丟擲時所請求的URL路徑

18.全域性異常處理

我們知道 Spring Boot 已經提供了一套預設的異常處理機制,但是 Spring Boot 提供的預設異常處理機制卻並不一定適合我們實際的業務場景,因此,我們通常會根據自身的需要對 Spring Boot 全域性異常進行統一定製,例如定製錯誤頁面,定製錯誤資料等。

定製錯誤頁面

我們可以通過以下 3 種方式定製 Spring Boot 錯誤頁面:

  • 自定義 error.html
  • 自定義動態錯誤頁面
  • 自定義靜態錯誤頁面

自定義 error.html

我們可以直接在模板引擎資料夾(/resources/templates)下建立 error.html ,覆蓋 Spring Boot 預設的錯誤檢視頁面(Whitelabel Error Page)。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>自定義 error.html</title>
</head>
<body>
<h1>自定義 error.html</h1>
<p>status:<span th:text="${status}"></span></p>
<p>error:<span th:text="${error}"></span></p>
<p>timestamp:<span th:text="${timestamp}"></span></p>
<p>message:<span th:text="${message}"></span></p>
<p>path:<span th:text="${path}"></span></p>
</body>
</html>

如果 Sprng Boot 專案使用了模板引擎,當程式發生異常時,Spring Boot 的預設錯誤檢視解析器(DefaultErrorViewResolver)就會解析模板引擎資料夾(resources/templates/)下 error 目錄中的錯誤檢視頁面。

精確匹配

我們可以根據錯誤狀態碼(例如 404、500、400 等等)的不同,分別建立不同的動態錯誤頁面(例如 404.html、500.html、400.html 等等),並將它們存放在模板引擎資料夾下的 error 目錄中。當發生異常時,Spring Boot 會根據其錯誤狀態碼精確匹配到對應的錯誤頁面上。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<h1>自定義動態錯誤頁面 404.html</h1>
<p>status:<span th:text="${status}"></span></p>
<p>error:<span th:text="${error}"></span></p>
<p>timestamp:<span th:text="${timestamp}"></span></p>
<p>message:<span th:text="${message}"></span></p>
<p>path:<span th:text="${path}"></span></p>
</body>
</html>

匹配

我們還可以使用 4xx.html 和 5xx.html 作為動態錯誤頁面的檔名,並將它們存放在模板引擎資料夾下的 error 目錄中,來模糊匹配對應型別的所有錯誤,例如 404、400 等錯誤狀態碼以“4”開頭的所有異常,都會解析到動態錯誤頁面 4xx.html 上。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<h1>自定義動態錯誤頁面 4xx.html</h1>
<p>status:<span th:text="${status}"></span></p>
<p>error:<span th:text="${error}"></span></p>
<p>timestamp:<span th:text="${timestamp}"></span></p>
<p>message:<span th:text="${message}"></span></p>
<p>path:<span th:text="${path}"></span></p>
</body>
</html>

自定義靜態錯誤頁面

若 Sprng Boot 專案沒有使用模板引擎,當程式發生異常時,Spring Boot 的預設錯誤檢視解析器(DefaultErrorViewResolver)則會解析靜態資原始檔夾下 error 目錄中的靜態錯誤頁面。

精確匹配

我們可以根據錯誤狀態碼(例如 404、500、400 等等)的不同,分別建立不同的靜態錯誤頁面(例如 404.html、500.html、400.html 等等),並將它們存放在靜態資原始檔夾下的 error 目錄中。當發生異常時,Spring Boot 會根據錯誤狀態碼精確匹配到對應的錯誤頁面上。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<h1>自定義靜態錯誤頁面 404.html</h1>
<p>status:<span th:text="${status}"></span></p>
<p>error:<span th:text="${error}"></span></p>
<p>timestamp:<span th:text="${timestamp}"></span></p>
<p>message:<span th:text="${message}"></span></p>
<p>path:<span th:text="${path}"></span></p>
</body>
</html>

模糊匹配

我們還可以使用 4xx.html 和 5xx.html 作為靜態錯誤頁面的檔名,並將它們存放在靜態資原始檔夾下的 error 目錄中,來模糊匹配對應型別的所有錯誤,例如 404、400 等錯誤狀態碼以“4”開頭的所有錯誤,都會解析到靜態錯誤頁面 4xx.html 上。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title></title>
</head>
<body>
<h1>自定義靜態錯誤頁面 4xx.html</h1>
<p>status:<span th:text="${status}"></span></p>
<p>error:<span th:text="${error}"></span></p>
<p>timestamp:<span th:text="${timestamp}"></span></p>
<p>message:<span th:text="${message}"></span></p>
<p>path:<span th:text="${path}"></span></p>
</body>
</html>

錯誤頁面優先順序

以上 5 種方式均可以定製 Spring Boot 錯誤頁面,且它們的優先順序順序為:自定義動態錯誤頁面(精確匹配)>自定義靜態錯誤頁面(精確匹配)>自定義動態錯誤頁面(模糊匹配)>自定義靜態錯誤頁面(模糊匹配)>自定義 error.html。

當遇到錯誤時,Spring Boot 會按照優先順序由高到低,依次查詢解析錯誤頁,一旦找到可用的錯誤頁面,則直接返回客戶端展示。

定製錯誤資料

我們知道,Spring Boot 提供了一套預設的異常處理機制,其主要流程如下:

  1. 發生異常時,將請求轉發到“/error”,交由 BasicErrorController(Spring Boot 預設的 Error 控制器) 進行處理;
  2. BasicErrorController 根據客戶端的不同,自動適配返回的響應形式,瀏覽器客戶端返回錯誤頁面,機器客戶端返回 JSON 資料。
  3. BasicErrorController 處理異常時,會呼叫 DefaultErrorAttributes(預設的錯誤屬性處理工具) 的 getErrorAttributes() 方法獲取錯誤資料。

我們還可以定製 Spring Boot 的錯誤資料,具體步驟如下。

  1. 自定義異常處理類,將請求轉發到 “/error”,交由 Spring Boot 底層(BasicErrorController)進行處理,自動適配瀏覽器客戶端和機器客戶端。
  2. 通過繼承 DefaultErrorAttributes 來定義一個錯誤屬性處理工具,並在原來的基礎上新增自定義的錯誤資料。

1. 自定義異常處理類

被 @ControllerAdvice 註解的類可以用來實現全域性異常處理,這是 Spring MVC 中提供的功能,在 Spring Boot 中可以直接使用。

建立一個名為 UserNotExistException 的異常類,程式碼如下

/**
* 自定義異常
*/
public class UserNotExistException extends RuntimeException {
    public UserNotExistException() {
        super("使用者不存在!");
    }
}

在 IndexController 新增以下方法,觸發 UserNotExistException 異常,程式碼如下

@Controller
public class IndexController {
    ......
    @GetMapping(value = {"/testException"})
    public String testException(String user) {
        if ("user".equals(user)) {
            throw new UserNotExistException();
        }
        //跳轉到登入頁 login.html
        return "login";
    }
}

建立一個名為 MyExceptionHandler 異常處理類,程式碼如下

import com.wyl.Exception.UserNotExistException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;

import javax.servlet.http.HttpServletRequest;
import java.util.HashMap;
import java.util.Map;

@ControllerAdvice
public class MyExceptionHandler {
    @ExceptionHandler(UserNotExistException.class)
    public String handleException(Exception e, HttpServletRequest request) {
        Map<String, Object> map = new HashMap<>();
        //向 request 物件傳入錯誤狀態碼
        request.setAttribute("javax.servlet.error.status_code",500);
        //根據當前處理的異常,自定義的錯誤資料
        map.put("code", "user.notexist");
        map.put("message", e.getMessage());
        //將自定的錯誤資料傳入 request 域中
        request.setAttribute("ext",map);
        return "forward:/error";
    }
}

2. 自定義錯誤屬性處理工具

1)在 net.biancheng.www.componet 包內,建立一個錯誤屬性處理工具類 MyErrorAttributes(繼承 DefaultErrorAttributes ),通過該類我們便可以新增自定義的錯誤資料,程式碼如下。

import org.springframework.boot.web.error.ErrorAttributeOptions;
import org.springframework.boot.web.servlet.error.DefaultErrorAttributes;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.WebRequest;

import java.util.Map;
//向容器中新增自定義的儲物屬性處理工具
@Component
public class MyErrorAttributes extends DefaultErrorAttributes {
    @Override
    public Map<String, Object> getErrorAttributes(WebRequest webRequest, ErrorAttributeOptions options) {
        Map<String, Object> errorAttributes = super.getErrorAttributes(webRequest, options);
        //新增自定義的錯誤資料
        errorAttributes.put("company", "www.biancheng.net");
        //獲取 MyExceptionHandler 傳入 request 域中的錯誤資料
        Map ext = (Map) webRequest.getAttribute("ext", 0);
        errorAttributes.put("ext", ext);
        return errorAttributes;
    }
}

在 templates/error 目錄下,建立動態錯誤頁面 5xx.html,程式碼如下。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>自定義 error.html</title>
</head>
<body>
<p>status:<span th:text="${status}"></span></p>
<p>error:<span th:text="${error}"></span></p>
<p>timestamp:<span th:text="${timestamp}"></span></p>
<p>message:<span th:text="${message}"></span></p>
<p>path:<span th:text="${path}"></span></p>
<!--取出定製的錯誤資訊-->
<h3>以下為定製錯誤資料:</h3>
<p>company:<span th:text="${company}"></span></p>
<p>code:<span th:text="${ext.code}"></span></p>
<p>path:<span th:text="${ext.message}"></span></p>
</body>
</html>

相關文章