spring boot快速入門


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>


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM