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、等待項目構建成功
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包,一定要在同級目錄下,否則識別不到
 {
SpringApplication.run(SpringbootOneApplication.class, args);
}
}
3.3.2注解(@SpringBootApplication)
-
作用:標注在某個類上說明這個類是SpringBoot的主配置
-
SpringBoot就應該運行這個類的main方法來啟動SpringBoot應用;
-
進入這個注解:可以看到上面還有很多其他注解!
@ComponentScan
-
這個注解在Spring中很重要 ,它對應XML配置中的元素。
-
作用:自動掃描並加載符合條件的組件或者bean , 將這個bean定義加載到IOC容器中
@SpringBootConfiguration
-
作用:SpringBoot的配置類 ,標注在某個類上 , 表示這是一個SpringBoot的配置類;
-
我們繼續進去這個注解查看
-
這里的 @Configuration,說明這是一個spring的配置類 ,配置類就是對應Spring的xml 配置文件;
-
@Component 這就說明,啟動類本身也是Spring中的一個組件而已,負責啟動應用!
-
我們回到 SpringBootApplication 注解中繼續看。
@EnableAutoConfiguration
-
開啟自動配置功能
- 以前我們需要自己配置的東西,而現在SpringBoot可以自動幫我們配置 ;
- @EnableAutoConfiguration告訴SpringBoot開啟自動配置功能,這樣自動配置才能生效;
點進注解接續查看:
-
@AutoConfigurationPackage :自動配置包
-
-
@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 , 看到了很多自動配置的文件;這就是自動配置根源所在!
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.結論:
-
SpringBoot在啟動的時候從類路徑下的
META-INF/spring.factories
中獲取EnableAutoConfiguration
指定的值 -
將這些值作為自動配置類導入容器 , 自動配置類就生效 , 幫我們進行自動配置工作;
-
以前我們需要自動配置的東西,現在springboot幫我們做了
-
整合JavaEE,整體解決方案和自動配置的東西都在
springboot-autoconfigure
的jar包中; -
它會把所有需要導入的組件,以類名的方式返回,這些組件就會被添加到容器中
-
它會給容器中導入非常多的自動配置類 (xxxAutoConfiguration), 就是給容器中導入這個場景需要的所有組件 , 並自動配置,@Configuration(javaConfig) ;
-
有了自動配置類 , 免去了我們手動編寫配置注入功能組件等的工作;
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
這個類主要做了以下四件事情:
-
推斷應用的類型是普通的項目還是Web項目
-
查找並加載所有可用初始化器 , 設置到initializers屬性中
-
找出所有的應用程序監聽器,設置到listeners屬性中
-
推斷並設置main方法的定義類,找到運行的主類
查看構造器:
3.4.2.run方法流程分析
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基礎語法
說明:語法要求嚴格!
-
空格不能省略
-
以縮進來控制層級關系,只要是左邊對齊的一列數據都是同一個層級的。
-
屬性和值的大小寫都是十分敏感的。
字面量:普通的值 [ 數字,布爾值,字符串 ]
-
字面量直接寫在后面就可以 , 字符串默認不用加上雙引號或者單引號;
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:默認從全局配置文件中獲取值
- 在resources目錄下新建一個person.properties文件
name=hello
- 在代碼中指定加載person.properties文件
@PropertySource(value = "classpath:person.properties")
@Component //注冊bean
public class Person {
@Value("${name}")
private String name;
......
}
- 再次輸出測試,指定配置文件綁定成功
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 中配置
測試步驟
- 新建一個實體類User
@Component //注冊bean
public class User {
private String name;
private int age;
private String sex;
}
- 編輯配置文件 user.properties
user1.name=wyl
user1.age=18
user1.sex=男
- 在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;
}
- Springboot測試
SpringBootTest
class DemoApplicationTests {
@Autowired
User user;
@Test
public void contextLoads() {
System.out.println(user);
}
}
結果正常輸出
4.3.4.對比小結
@Value使用起來並不友好!我們需要為每個屬性單獨注解賦值比較麻煩
- @ConfigurationProperties只需要寫一次即可 , @Value則需要每個字段都添加
- 松散綁定:這個什么意思呢? 比如yml中寫的last-name,這個和lastName是一樣的,
-
后面跟着的字母默認是大寫的。這就是松散綁定 - JSR303數據校驗 ,可以在字段是增加一層過濾器驗證 , 保證數據的合法性
- 復雜類型封裝,yml中可以封裝對象 , 使用value就不支持
結論:
- 配置yml和配置properties都可以獲取到值 , 強烈推薦 yml;
- 如果在某個業務中,只需要獲取配置文件中的某個值,可以使用一下 @value;
- 如果專門編寫了一個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
6.1.分析自動配置原理
-
SpringBoot啟動的時候加載主配置類,開啟了自動配置功能 @EnableAutoConfiguration
-
@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的值加入到容器中:
-
-
- 每一個這樣的 xxxAutoConfiguration類都是容器中的一個組件,都加入到容器中;用他們來做自動配置;
-
每一個自動配置類進行自動配置功能;
-
我們以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 { // ..... }
我們去配置文件里面試試前綴,看提示!
這就是自動裝配的原理!
6.2.總結
-
SpringBoot啟動會加載大量的自動配置類
-
我們看我們需要的功能有沒有在SpringBoot默認寫好的自動配置類當中;
-
我們再來看這個自動配置類中到底配置了哪些組件;(只要我們要用的組件存在在其中,我們就不需要再手動配置了)
-
給容器中自動配置類添加組件的時候,會從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.編寫啟動器
-
在IDEA中新建一個空項目 spring-boot-starter-diy
-
新建一個普通Maven模塊:kuang-spring-boot-starter
-
新建一個Springboot模塊:kuang-spring-boot-starter-autoconfigure
-
點擊apply即可,基本結構
-
在我們的 starter 中 導入 autoconfigure 的依賴!
<!-- 啟動器 --> <dependencies> <!-- 引入自動配置模塊 --> <dependency> <groupId>com.kuang</groupId> <artifactId>kuang-spring-boot-starter-autoconfigure</artifactId> <version>0.0.1-SNAPSHOT</version> </dependency> </dependencies>
-
將 autoconfigure 項目下多余的文件都刪掉,Pom中只留下一個 starter,這是所有的啟動器基本配置!
-
我們編寫一個自己的服務
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(); } }
-
編寫
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; } }
-
編寫我們的自動配置類並注入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; } }
-
在resources編寫一個自己的
META-INF\spring.factories
# Auto Configure org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ wyl.ss.HelloServiceAutoConfiguration
-
編寫完成后,可以安裝到maven倉庫中!
6.5.3.新建項目測試我們自己寫的啟動器
-
新建一個SpringBoot 項目
-
導入我們自己寫的啟動器
<dependency> <groupId>wyl.ss</groupId> <artifactId>ss-spring-boot-starter</artifactId> <version>1.0-SNAPSHOT</version> </dependency>
-
編寫一個
HelloController
進行測試我們自己的寫的接口!package wyl.ss.controller; @RestController public class HelloController { @Autowired HelloService helloService; @RequestMapping("/hello") public String hello(){ return helloService.sayHello("zxc"); } }
-
編寫配置文件
application.properties
ss.hello.prefix="ppp" ss.hello.suffix="sss"
-
啟動項目進行測試,結果成功 !
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/*
效果如下:
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);
}
-
對應的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>
-
maven配置資源過濾問題
<resources> <resource> <directory>src/main/java</directory> <includes> <include>**/*.xml</include> </includes> <filtering>true</filtering> </resource> </resources>
-
編寫部門的 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,我們可以去搜索一下:
要使用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
13.2.3.第二種靜態資源映射規則
1、那我們項目中要是使用自己的靜態資源該怎么導入呢?我們看下一行代碼;
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根目錄下新建對應的文件夾,都可以存放我們的靜態文件;
6、比如我們訪問 http://localhost:8080/1.js , 他就會去這些文件夾中尋找對應的靜態資源文件;
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. 歡迎頁與圖標
14.國際化
國際化(Internationalization 簡稱 I18n,其中“I”和“n”分別為首末字符,18 則為中間的字符數)是指軟件開發時應該具備支持多種語言和地區的功能。換句話說就是,開發的軟件需要能同時應對不同國家和地區的用戶訪問,並根據用戶地區和語言習慣,提供相應的、符合用具閱讀習慣的頁面和數據,例如,為中國用戶提供漢語界面顯示,為美國用戶提供提供英語界面顯示。
在 Spring 項目中實現國際化,通常需要以下 3 步:
- 編寫國際化資源(配置)文件;
- 使用 ResourceBundleMessageSource 管理國際化資源文件;
- 在頁面獲取國際化內容。
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 會自動識別它們,並轉換成如下的模式:
打開任意一個國際化資源文件,並切換為 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">×</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 自動配置的區域信息解析器會失效,而我們定義的區域信息解析器則會生效。
手動切換語言
- 修改 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>
- 創建一個 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 步:
-
定義攔截器;
-
注冊攔截器;
-
指定攔截規則(如果是攔截所有,靜態資源也會被攔截)。
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 會自動識別客戶端的類型(瀏覽器客戶端或機器客戶端),並根據客戶端的不同,以不同的形式展示異常信息。
- 對於瀏覽器客戶端而言,Spring Boot 會響應一個“ whitelabel”錯誤視圖,以 HTML 格式呈現錯誤信息
- 對於機器客戶端而言,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 解析異常信息的步驟如下:
- 根據錯誤狀態碼(例如 404、500、400 等),生成一個錯誤視圖 error/status,例如 error/404、error/500、error/400。
- 嘗試使用模板引擎解析 error/status 視圖,即嘗試從 classpath 類路徑下的 templates 目錄下,查找 error/status.html,例如 error/404.html、error/500.html、error/400.html。
- 若模板引擎能夠解析到 error/status 視圖,則將視圖和數據封裝成 ModelAndView 返回並結束整個解析流程,否則跳轉到第 4 步。
- 依次從各個靜態資源文件夾中查找 error/status.html,若在靜態文件夾中找到了該錯誤頁面,則返回並結束整個解析流程,否則跳轉到第 5 步。
- 將錯誤狀態碼(例如 404、500、400 等)轉換為 4xx 或 5xx,然后重復前 4 個步驟,若解析成功則返回並結束整個解析流程,否則跳轉第 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 提供了一套默認的異常處理機制,其主要流程如下:
- 發生異常時,將請求轉發到“/error”,交由 BasicErrorController(Spring Boot 默認的 Error 控制器) 進行處理;
- BasicErrorController 根據客戶端的不同,自動適配返回的響應形式,瀏覽器客戶端返回錯誤頁面,機器客戶端返回 JSON 數據。
- BasicErrorController 處理異常時,會調用 DefaultErrorAttributes(默認的錯誤屬性處理工具) 的 getErrorAttributes() 方法獲取錯誤數據。
我們還可以定制 Spring Boot 的錯誤數據,具體步驟如下。
- 自定義異常處理類,將請求轉發到 “/error”,交由 Spring Boot 底層(BasicErrorController)進行處理,自動適配瀏覽器客戶端和機器客戶端。
- 通過繼承 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>