- 一、SpringBoot 是什么
- 二、基礎入門
- 三、容器功能
- 四、日志
- 五、Web 開發
- 六、Docker
- 七、數據訪問
- 八、單元測試
- 九、指標監控
- 十、原理解析
一、SpringBoot 是什么
1、為什么要用 SpringBoot
-
Spring Boot makes it easy to create stand-alone, production-grade Spring based Applications that you can "just run".
- 能快速創建出生產級別的 Spring 應用。
2、SpringBoot 優點
-
Create stand-alone Spring applications
- 創建獨立 Spring 應用
-
Embed Tomcat, Jetty or Undertow directly (no need to deploy WAR files)
- 內嵌 Web 服務器(默認使用 Tomcat)
-
Provide opinionated 'starter' dependencies to simplify your build configuration
- 自動starter依賴,簡化構建配置
-
Automatically configure Spring and 3rd party libraries whenever possible
- 自動配置 Spring 以及第三方功能
-
Provide production-ready features such as metrics, health checks, and externalized configuration
- 提供生產級別的監控、健康檢查及外部化配置
-
Absolutely no code generation and no requirement for XML configuration
- 無代碼生成、無需編寫 XML
SpringBoot 是整合 Spring 技術棧的一站式框架。
SpringBoot 是簡化 Spring 技術棧的快速開發腳手架。
3、SpringBoot 缺點
- 迭代快,需要時刻關注版本變化。
- 封裝太深,內部原理復雜,不容易精通。
4、微服務
James Lewis and Martin Fowler (2014) 提出微服務完整概念。
In short, the microservice architectural style is an approach to developing a single application as a suite of small services, each running in its own process and communicating with lightweight mechanisms, often an HTTP resource API. These services are built around business capabilities and independently deployable by fully automated deployment machinery. There is a bare minimum of centralized management of these services, which may be written in different programming languages and use different data storage technologies.-- James Lewis and Martin Fowler (2014)
- 微服務是一種架構風格
- 一個應用拆分為一組小型服務
- 每個服務運行在自己的進程內,也就是可獨立部署和升級
- 服務之間使用輕量級 HTTP 交互
- 服務圍繞業務功能拆分
- 可以由全自動部署機制獨立部署
- 去中心化,服務自治。服務可以使用不同的語言、不同的存儲技術
5、分布式
分布式的困難
- 配置管理
- 服務發現
- 遠程調用
- 負載均衡
- 服務容錯
- 服務監控
- 鏈路追蹤
- 日志管理
- 任務調度
- ......
分布式的解決
- SpringBoot + SpringCloud
6、雲原生(Cloud Native)
原生應用如何上雲。
上雲的困難
- 服務自愈
- 彈性伸縮
- 服務隔離
- 自動化部署
- 灰度發布
- 流量治理
- ......
上雲的解決
- Docker 容器化技術
- Kubernetes 容器編排,簡稱 k8s
- DevOps,企業 CI/CD,構建企業雲平台
- 擁抱新一代架構 Service Mesh 與 Serverless
二、基礎入門
1、環境准備
參照官方幫助文檔配置:
-
Java 8 及以上。
-
Maven 3.3+:項目管理工具,可以對 Java 項目進行構建、依賴管理。
- pom.xml 按照官方幫助文檔設定。
-
Gradle 6.x:基於 Apache Ant 和 Apache Maven 概念的項目自動化構建開源工具。它使用一種基於 Groovy 的特定領域語言(DSL)來聲明項目設置,目前也增加了基於 Kotlin 語言的 kotlin-based DSL,拋棄了基於 XML 的各種繁瑣配置。
-
SpringBoot CLI:命令行工具,用於使用 Spring 進行快速原型搭建。它允許你運行 Groovy 腳本,這意味着你可以使用類 Java 的語法,並且沒有那么多的模板代碼。
-
IntellIJ IDEA: 微雲上下載。
1.1 Maven 設置
給 Maven 的 settings.xml 中添加默認 JDK 版本以及默認 UTF-8 編碼。
<profiles>
<profile>
<id>JDK1.8</id>
<activation>
<activeByDefault>true</activeByDefault>
<jdk>1.8</jdk>
</activation>
<properties>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.compilerVersion>1.8</maven.compiler.compilerVersion>
<!--編譯編碼-->
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
</profile>
</profiles>
1.2 IDEA 設置
將 Maven 整合進來,IDEA 中 設置 和 新項目設置 中的 Maven 都要設定。
2、HelloWorld
功能:瀏覽器發送/hello
請求,服務器接受請求並處理,響應 Hello World 字符串。
2.1 創建一個 Maven 工程
新建一個名為 spring-boot-01-helloworld 的 Maven 工程。
2.2 導入 SpringBoot 相關的依賴
pom.xml 中加入 SpringBoot 依賴。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
<!-- 一般依賴最新版本 -->
</parent>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<!-- Web 應用開發場景 -->
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
2.3 編寫主程序 MainApplication
src\main\java 下新建一個 com.atguigu.HelloWorldMainApplication
類。
/**
* SpringBootApplication 標注一個主程序類,說明這是一個SpringBoot應用
*/
@SpringBootApplication
public class HelloWorldMainApplication {
public static void main(String[] args) {
// 啟動Spring應用
SpringApplication.run(HelloWorldMainApplication.class,args);
}
}
2.4 編寫 Controller、Service
src\main\java 下新建一個 controller.HelloController
類。
@Controller // 作為控制器組件注冊到容器中
public class HelloController {
@ResponseBody // 返回結果(json或xml)直接寫入 HTTP response body 中,一般在Ajax異步獲取數據時使用
@RequestMapping("/hello") // 映射/hello請求,即該方法處理/hello請求
public String hello(){
return "Hello World!";
}
}
2.5 運行主程序測試
Ctrl+F5
運行 HelloWorldMainApplication
。
2.6 簡化部署
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<!-- Maven 插件,可以將應用打包成一個可執行的 jar 包 -->
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<!-- 打包時跳過測試 -->
<skipTests>true</skipTests>
</configuration>
</plugin>
</plugins>
</build>
將這個應用打成 jar 包,直接使用java -jar
的命令進行執行。
3、Hello World探究
3.1 POM文件
3.1.1 父項目
項目下的 pom.xml:
<parent>
<groupId>org.springframework.boot</groupId>
<!-- SpringBoot 的版本仲裁中心 -->
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.6.RELEASE</version>
</parent>
spring-boot-starter-parent 包下的 spring-boot-starter-parent-a.b.c.pom(a.b.c 就是上面 pom.xml 中的版本號,本文是 2.2.6):
<!-- 管理SpringBoot應用里面的所有默認依賴版本 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>2.2.6.RELEASE</version>
<relativePath>../../spring-boot-dependencies</relativePath>
</parent>
以后我們導入默認依賴是不需要寫版本號的(沒有在 dependencies 里面管理的依賴需要聲明版本號)。
3.1.2 啟動器
<dependency>
<groupId>org.springframework.boot</groupId>
<!-- Web 應用開發場景 -->
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
spring-boot-starter-*:SpringBoot 官方場景啟動器,會導入相關開發場景所需的所有依賴。
*-spring-boot-starter:自定義的場景啟動器。
SpringBoot 將所有的功能場景都抽取出來,做成一個個的 starter(啟動器),只需要在項目里面引入這些 starter,相關場景的所有依賴都會導入進來。要用什么功能就導入什么場景的啟動器,引入之后記得點擊編輯區右上角的 m 圖標,來下載這些依賴。
SpringBoot 支持的所有場景。
3.2 主程序類
/**
* SpringBootApplication 標注一個主程序類,說明這是一個SpringBoot應用
*/
@SpringBootApplication
public class HelloWorldMainApplication {
public static void main(String[] args) {
// 啟動Spring應用
SpringApplication.run(HelloWorldMainApplication.class, args);
}
}
@SpringBootApplication
:標注在某個類上說明這個類是 SpringBoot 的主配置類,SpringBoot 就應該運行這個類的 main()
方法來啟動 SpringBoot 應用。
4、快速創建 SpringBoot 項目
4.1 IDEA:使用 Spring Initializer 快速創建項目
項目類型選擇 Spring Initializr,修改完組(公司名)、工件(項目名)和Java版本后,選擇我們需要的模塊(如 Spring Web),向導會聯網創建 SpringBoot 項目並下載到本地。
- 主程序已經生成好了,只需編寫自己的業務代碼。
- resources文件夾中目錄結構
- static:靜態資源, js、css、images。
- templates:模板頁面(SpringBoot 默認默認不支持 jsp 頁面),可以使用模板引擎(FreeMarker、Thymeleaf)。
- application.properties:配置文件,可以修改一些默認設置。
4.2 Eclipse:使用 STS 快速創建項目
三、容器功能
1、配置文件
SpringBoot 使用一個全局的配置文件,配置文件名是固定的。
- application.properties
- application.yml 或 application.yaml(建議使用)
配置文件的作用:修改 SpringBoot 自動配置的默認值。
YAML(YAML Ain't Markup Language)
YAML A Markup Language:是一種標記語言
YAML isn't Markup Language:不是一種標記語言
Yet another Markup Language:仍是一種標記語言
優點:沒有額外的定界符,更輕量,更易讀。
2、YAML語法
2.1 基本語法
key: value
-> value 前面一定要有空格- 大小寫敏感
- 使用縮進表示層級關系
- 縮進不允許使用tab,只允許空格
- 縮進的空格數不重要,只要相同層級的元素左對齊即可
#
表示注釋
server:
port: 8081
path: /hello
2.2 值的寫法
2.2.1 字面量
普通的值(數字,字符串,布爾值)
k: v
字符串默認不用加上單引號或者雙引號。
"":雙引號。不會轉義字符串里面的特殊字符,特殊字符會作為本身想表示的意思。
name: "zhangsan \n lisi"
輸出:zhangsan 換行 lisi
'':單引號。會轉義特殊字符,特殊字符最終只是一個普通的字符串數據。
name: 'zhangsan \n lisi'
輸出:zhangsan \n lisi
2.2.2 對象、Map
(屬性和值、鍵值對)
k: v
在下一行來寫對象的屬性和值的關系,注意縮進。
對象還是 k: v
的方式:
friends:
lastName: zhangsan
age: 20
行內寫法:
friends: {lastName: zhangsan,age: 18}
2.2.3 數組(List、Set)
用 - 值
表示數組中的一個元素:
pets: - cat - dog - pig
行內寫法:
pets: [cat, dog, pig]yaml
3、配置文件值注入
配置文件 application.yml:
person: userName: zhangsan boss: false birth: 2019/12/12 20:12:33 age: 18 pet: name: tomcat weight: 23.4 interests: [籃球,游泳] animal: - jerry - mario score: english: first: 30 second: 40 third: 50 math: [131,140,148] chinese: {first: 128,second: 136} salarys: [3999,4999.98,5999.99] allPets: sick: - {name: tom} - {name: jerry,weight: 47} health: [{name: mario,weight: 47}]
JavaBean:
/**
* @ConfigurationProperties:告訴SpringBoot將本類中的所有屬性和配置文件中相關的配置進行綁定。
*/
@Component // 只有將組件注冊到容器中,才能使用容器提供的ConfigurationProperties功能。
@ConfigurationProperties(prefix = "person") // 映射到配置文件中的person屬性。
@Data
public class Person {
private String userName;
private Boolean boss;
private Date birth;
private Integer age;
private Pet pet;
private String[] interests;
private List<String> animal;
private Map<String, Object> score;
private Set<Double> salarys;
private Map<String, List<Pet>> allPets;
}
@Data
public class Pet {
private String name;
private Double weight;
}
自定義的類和配置文件綁定一般沒有提示,我們可以導入配置文件處理器。
<!-- 導入配置文件處理器,配置文件進行綁定就會有提示 --><dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-configuration-processor</artifactId> <optional>true</optional></dependency>
3.1 配置文件的坑
3.1.1 中文亂碼
application.properties 配置中文值的時候,讀取出來的屬性值會出現亂碼問題。但是 application.yml 不會出現亂碼問題。原因是,Spring Boot 是以 ISO-8859-1 的編碼方式讀取 application.properties 配置文件。
3.1.2 加載順序
Java 的 Properties
加載屬性文件后是無法保證輸出的順序與文件中一致的,因為 Properties
是繼承自 Hashtable
的, key/value 都是直接存在 Hashtable
中的,而 Hashtable
是不保證進出順序的。
所以如果需要屬性保持有序,請使用 application.yml。
3.1.3 用戶名
如果定義一個鍵值對 user.name=xxx
, 這里會讀取不到對應寫的屬性值。為什么呢?SpringBoot 的默認 StandardEnvironment
首先將會加載 systemEnvironment 作為首個 PropertySource。而 source 即為 System.getProperties()
,按照讀取順序,返回 systemEnvironment 的值即 System.getProperty("user.name")
,Mac 電腦會讀取自己的登錄賬號。
3.3 @Value
和@ConfigurationProperties
比較
項目 | @ConfigurationProperties | @Value |
---|---|---|
功能 | 批量注入配置文件中的屬性 | 分別指定 |
松散綁定(松散語法) | 支持 | 不支持 |
SpEL | 不支持 | 支持 |
JSR-303數據校驗 | 支持 | 不支持 |
復雜類型封裝 | 支持 | 不支持 |
無論配置文件是 yml 還是 properties 他們都能獲取到值。
如果說,我們只是在某個業務邏輯中需要獲取一下配置文件中的某項值,使用 @Value
。
如果說,我們專門編寫了一個 JavaBean 來和配置文件進行映射,我們就直接使用@ConfigurationProperties
。
4、輸入數據校驗
@Component
@ConfigurationProperties(prefix = "person")
@Validated // 啟用數據校驗
public class Person {
/**
* <bean class="Person">
* <property name="lastName" value="字面量/${key}從環境變量、配置文件中獲取值/#{SpEL}"></property>
* <bean/>
*/
// lastName必須是郵箱格式
@Email
// @Value("${person.last-name}")
private String lastName;
// @Value("#{11*2}")
private Integer age;
// @Value("true")
private Boolean boss;
private Date birth;
private Map<String,Object> maps;
private List<Object> lists;
private Dog dog;
5、加載配置文件
5.1 @PropertySource
加載指定的 properties 配置文件。
/**
* @ConfigurationProperties:告訴SpringBoot將本類中的所有屬性和配置文件中相關的配置進行綁定。
*/
@Component // 只有將組件注冊到容器中,才能使用容器提供的ConfigurationProperties功能。
@PropertySource(value = {"classpath:person.properties"}) // 指定加載配置文件person.properties。
@ConfigurationProperties(prefix = "person") // 映射到配置文件中的person屬性。
//@Validated
public class Person {
/**
* <bean class="Person">
* <property name="lastName" value="字面量/${key}從環境變量、配置文件中獲取值/#{SpEL}"></property>
* <bean/>
*/
//lastName必須是郵箱格式
// @Email
//@Value("${person.last-name}")
private String lastName;
//@Value("#{11*2}")
private Integer age;
//@Value("true")
private Boolean boss;
5.2 @ImportResource
為了兼容 Spring 應用,導入 Spring 的 xml 配置文件,讓配置文件里面的內容生效。
@ImportResource(locations = {"classpath:beans.xml"}) // 導入Spring的配置文件@SpringBootApplicationpublic class SpringBoot02ConfigApplication {
beans.xml:
<?xml version="1.0" encoding="UTF-8"?><beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <bean id="helloService" class="com.atguigu.springboot.service.HelloService"></bean></beans>
SpringBoot 推薦給容器中添加組件的方式。推薦使用全注解的方式:
1、配置類 @Configuration
Spring 配置文件
2、使用 @Bean
給容器中添加組件
3、組件注解:@Component
、@Controller
、@Service
、@Repository
/**
* @Configuration指明當前類是一個配置類,替代Spring配置文件。
* 在配置文件中是使用<bean></bean>標簽添加組件。
*/
@Configuration
public class MyAppConfig {
// 將方法的返回值添加到容器中,容器中這個組件默認的ID就就是方法名
@Bean
public HelloService helloService() {
System.out.println("配置類使用@Bean給容器中添加組件");
return new HelloService();
}
}
6、配置文件占位符
6.1 隨機數
${random.uuid}、${random.value}、${random.int}、${random.long}${random.int(10)}、${random.int[1024,65536]}
6.2 使用占位符
占位符獲取之前配置的值,可以使用 :
指定默認值。
person.last-name=王五${random.uuid}person.age=${random.int}person.birth=2020/04/18person.boss=falseperson.maps.k11=v11person.maps.k22=v22person.lists=a,b,cperson.dog.name=${person.hello:hello}_小黑person.dog.age=2
7、Profile
7.1 多 Profile 文件
我們在主配置文件編寫的時候,文件名可以是 application-{profile}.properties/yml/yaml。
默認使用 application.properties 的配置。
7.2 yml 支持多文檔塊方式
server: port: 8081spring: profiles: active: dev---# 文檔塊2server: port: 8083spring: profiles: dev---# 文檔塊3server: port: 8084spring: profiles: prod
7.3 激活指定 Profile
1、在 application.properties 配置文件中指定:
spring.profiles.active=dev
2、命令行:
java -jar spring-boot-02-config-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev
可以直接在測試的時候,配置傳入命令行參數。
3、虛擬機參數:
-Dspring.profiles.active=dev
8、配置文件加載位置
SpringBoot 啟動會掃描以下位置的 application.properties 或者 application.yml 文件作為 SpringBoot 的默認配置文件。
當前工程根目錄和 resources 文件夾下:
/config/
/
優先級由高到底,高優先級的配置會覆蓋低優先級的配置。
SpringBoot 會從這四個位置全部加載主配置文件,共同起作用形成互補配置。
我們還可以通過 spring.config.location
來改變默認的配置文件位置。
項目打包好以后,我們可以使用命令行參數的形式,啟動項目的時候來指定配置文件的新位置。
指定配置文件和默認加載的這些配置文件共同起作用形成互補配置。
java -jar spring-boot-02-config-02-0.0.1-SNAPSHOT.jar --spring.config.location=D:/application.properties
多個配置用空格分開,--配置項=值。
SpringBoot 也可以從外部加載配置。
9、自動配置原理
9.1 引導加載自動配置類
SpringBoot 啟動的時候加載主配置類,主配置類上的 @SpringBootApplication
注解開啟了自動配置功能 @EnableAutoConfiguration
。
@SpringBootConfiguration
@EnableAutoConfiguration // 開啟自動配置
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
9.1.1 @SpringBootConfiguration
標注在某個類上,表示這是一個 SpringBoot 的配置類。
@Configuration
:配置類上使用這個注解。
配置類 → 配置文件,配置類也是容器中的一個組件(@Component
)。
9.1.2 @ComponentScan
指定掃描哪些包,默認掃描主程序 MainApplication
所在的包及其子包。
9.1.3@EnableAutoConfiguration
開啟自動配置功能。
以前我們需要配置的東西,SpringBoot 幫我們自動配置。
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
@AutoConfigurationPackage
:自動配置包。
@Import({Registrar.class})
public @interface AutoConfigurationPackage {
Spring 的底層注解 @Import
,給容器中導入一個 Registrar.class
類型的組件。
然后利用 Registrar
將主程序 MainApplication
所在的包及其子包里面的所有組件掃描並配置到 Spring 容器。
-
@Import({AutoConfigurationImportSelector.class})
:導入組件。-
利用
AutoConfigurationImportSelector.getAutoConfigurationEntry(AnnotationMetadata annotationMetadata)
給容器中導入一些組件; -
可以查看
AutoConfigurationImportSelector.selectImports(AnnotationMetadata annotationMetadata)
方法的內容; -
獲取候選的配置項。
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);
-
加載配置
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(this.getSpringFactoriesLoaderFactoryClass(), this.getBeanClassLoader());
-
掃描所有 jar 包類路徑下 META-INF/spring.factories,把掃描到的這些文件的內容包裝成 properties 對象。
從 properties 中獲取到 EnableAutoConfiguration
屬性下的類全限定名列表,然后把他們添加到容器中。
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\
org.springframework.boot.autoconfigure.aop.AopAutoConfiguration,\
...
每一個這樣的 xxxAutoConfiguration
類都是容器中的一個組件,都加入到容器中,用他們來做自動配置。
9.2 按需開啟自動配置項
9.2.1 @Conditional
作用:必須是 @Conditional
指定的條件成立,才給容器中添加組件,配置里面的所有內容才生效。
@Conditional擴展注解 | 作用(判斷是否滿足當前指定條件) |
---|---|
@ConditionalOnJava | 系統的java版本是否符合要求 |
@ConditionalOnBean | 容器中存在指定Bean; |
@ConditionalOnMissingBean | 容器中不存在指定Bean; |
@ConditionalOnExpression | 滿足SpEL表達式指定 |
@ConditionalOnClass | 系統中有指定的類 |
@ConditionalOnMissingClass | 系統中沒有指定的類 |
@ConditionalOnSingleCandidate | 容器中只有一個指定的Bean,或者這個Bean是首選Bean |
@ConditionalOnProperty | 系統中指定的屬性是否有指定的值 |
@ConditionalOnResource | 類路徑下是否存在指定資源文件 |
@ConditionalOnWebApplication | 當前是web環境 |
@ConditionalOnNotWebApplication | 當前不是web環境 |
@ConditionalOnJndi | JNDI存在指定項 |
自動配置類必須在一定的條件下才能生效。
我們怎么知道哪些自動配置類生效?
我們可以通過啟用 debug=true
屬性,來讓控制台打印自動配置報告,這樣我們就可以很方便地知道哪些自動配置類生效。
=========================
AUTO-CONFIGURATION REPORT
=========================
Positive matches: // 自動配置類啟用的
-----------------
DispatcherServletAutoConfiguration matched:
- @ConditionalOnClass found required class 'org.springframework.web.servlet.DispatcherServlet'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition)
- @ConditionalOnWebApplication (required) found StandardServletEnvironment (OnWebApplicationCondition)
Negative matches: // 沒有啟動,沒有匹配成功的自動配置類
-----------------
ActiveMQAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required classes 'javax.jms.ConnectionFactory', 'org.apache.activemq.ActiveMQConnectionFactory' (OnClassCondition)
AopAutoConfiguration:
Did not match:
- @ConditionalOnClass did not find required classes 'org.aspectj.lang.annotation.Aspect', 'org.aspectj.lang.reflect.Advice' (OnClassCondition)
9.3 Spring MVC 自動配置
以下是 SpringBoot 對 SpringMVC 的默認配置(WebMvcAutoConfiguration
):
-
Inclusion of
ContentNegotiatingViewResolver
andBeanNameViewResolver
beans.- 自動配置了
ViewResolver
(視圖解析器):根據方法的返回值得到視圖對象(View
),視圖對象決定如何渲染(轉發?重定向?). ContentNegotiatingViewResolver
:組合所有的視圖解析器的。- 可以自己給容器中添加一個視圖解析器(
@Bean
,@Component
),SpringBoot 會自動地將其組合進來。
- 自動配置了
-
Support for serving static resources, including support for WebJars (see below).
- 靜態資源文件夾路徑,包括
webjars
。
- 靜態資源文件夾路徑,包括
-
Static
index.html
support.- 靜態首頁訪問(歡迎頁)。
-
Custom
Favicon
support (see below).- 網站圖標支持,即 favicon.ico。
-
自動注冊了 of
Converter
,GenericConverter
,Formatter
beans.-
Converter<S, T>
:轉換器接口,將源類型S
轉換為目標類型T
。 -
GenericConverter
:通用轉換器接口,用於兩個以上類型之間的轉換。 -
Formatter<T>
格式化器接口,格式化成目標類型T
。@Bean @ConditionalOnProperty(prefix = "spring.mvc", name = "date-format") // 在文件中配置日期格式化的規則 public Formatter<Date> dateFormatter() { return new DateFormatter(this.mvcProperties.getDateFormat()); // 日期格式化組件 }
可以自己給容器中添加一個格式化器轉換器。
-
-
Support for
HttpMessageConverters
(see below).HttpMessageConverter
:SpringMVC 用來轉換HTTP
請求和響應的。HttpMessageConverters
是從容器中確定的,獲取所有的HttpMessageConverter
,然后看哪一個轉換器能處理。- 可以自己給容器中添加一個
HttpMessageConverter
。
-
Automatic registration of
MessageCodesResolver
(see below).- 定義錯誤代碼生成規則。
-
Automatic use of a
ConfigurableWebBindingInitializer
bean (see below).- 可以自己給容器中添加一個
ConfigurableWebBindingInitializer
來替換默認的。
- 可以自己給容器中添加一個
9.4 擴展SpringMVC
編寫一個配置類(@Configuration
),是 WebMvcConfigurer
類型。不能標注@EnableWebMvc
。
既保留了所有的自動配置,也能用我們擴展的配置。
// 使用 WebMvcConfigurer 可以擴展 SpringMVC 的功能
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 瀏覽器發送 /atguigu 請求來到 success 頁面
registry.addViewController("/atguigu").setViewName("success");
}
}
原理:
WebMvcConfigurer
是 SpringMVC 的自動配置類;- 在做其他自動配置時會導入;
@Import(EnableWebMvcConfiguration.class)
@Configuration
public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration {
父類 DelegatingWebMvcConfiguration
:
@Configuration(proxyBeanMethods = false)
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite();
// 從容器中獲取所有的 WebMvcConfigurer。
@Autowired(required = false)
public void setConfigurers(List<WebMvcConfigurer> configurers) {
if (!CollectionUtils.isEmpty(configurers)) {
this.configurers.addWebMvcConfigurers(configurers);
// 一個參考實現:將所有 WebMvcConfigurer 相關配置一起調用。
// @Override
// protected void addViewControllers(ViewControllerRegistry registry) {
// this.configurers.addViewControllers(registry);
// }
}
}
- 容器中所有的
WebMvcConfigurer
都會同時起作用; - 我們的配置類也會被調用。
效果:SpringMVC 的自動配置和我們的擴展配置都會起作用。
9.5 全面接管 SpringMVC
不使用 SpringBoot 對 SpringMVC 的自動配置,而是使用自定義配置。
只要配置類中添加 @EnableWebMvc
即可:
// 使用 WebMvcConfigurerAdapter 可以來擴展 SpringMVC 的功能
@EnableWebMvc
@Configuration
public class MyMvcConfig extends WebMvcConfigurerAdapter {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// super.addViewControllers(registry);
//瀏覽器發送 /atguigu 請求來到 success 頁面
registry.addViewController("/atguigu").setViewName("success");
}
}
原理:
為什么使用 @EnableWebMvc
自動配置就失效了。
@EnableWebMvc
導入了DelegatingWebMvcConfiguration
組件;
@Import(DelegatingWebMvcConfiguration.class)
public @interface EnableWebMvc {
DelegatingWebMvcConfiguration
繼承了WebMvcConfigurationSupport
類;- 把所有系統中的
WebMvcConfigurer
拿過來,所有功能的定制都是這些WebMvcConfigurer
合起來一起生效的; - 自動配置了一些非常底層的組件,
RequestMappingHandlerMapping
、這些組件依賴的組件都是從容器中獲取。
- 把所有系統中的
@Configuration
public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport {
WebMvcAutoConfiguration
只有當容器中沒有WebMvcConfigurationSupport
組件的時候才生效;
@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 {
-
而第 2 步中,
@EnableWebMvc
已經將WebMvcConfigurationSupport
組件導入進來了。 -
導入的
WebMvcConfigurationSupport
只是 SpringMVC 最基本的功能。
9.6 修改默認配置
可配置的屬性列表。
- SpringBoot 在自動配置很多組件的時候,先看容器中有沒有用戶自己配置的(
@Bean
、@Component
)如果有就用用戶配置的,如果沒有,才自動配置。如果有些組件可以有多個(ViewResolver
)將用戶配置的和自己默認的組合起來。 - 在 SpringBoot 中會有非常多的
xxxConfigurer
幫助我們進行擴展配置; - 在 SpringBoot 中會有非常多的
xxxCustomizer
幫助我們進行定制配置。
9.7 總結
-
SpringBoot 先加載所有的自動配置類
xxxAutoConfiguration
。 -
每個自動配置類按照條件生效,默認都會綁定
xxxProperties
類,xxxProperties
類和對應配置文件進行了綁定。 -
定制化配置:
- 用戶直接使用
@Bean
、@Component
替換底層的組件; - 用戶去看這個組件是獲取的配置文件什么值就去修改。
- 用戶直接使用
場景 starter -> xxxAutoConfiguration
-> 導入 xxx
組件 -> 綁定 xxxProperties
-> 綁定配置文件項(application.properties)
9.8 最佳實踐
-
引入場景依賴;
-
查看自動配置了哪些(選做);
- 自己分析,引入場景對應的自動配置一般都生效了;
- 配置文件中
debug=true
開啟自動配置報告:Negative(不生效) \ Positive(生效)。
-
是否需要修改配置項;
- 參照文檔修改配置項。
- Common Application Properties;
- 自己分析,
xxxProperties
配置類綁定了哪個配置文件;
- 參照文檔修改配置項。
-
自定義加入或者替換組件;
-
編寫自定義的配置類
xxxConfiguration
+@Bean
、@Component
替換、增加容器中默認組件。- Web應用:編寫一個配置類實現
WebMvcConfigurer
即可定制化 Web 功能 +@Bean
給容器中再擴展一些組件。
@Configuration public class AdminWebConfig implements WebMvcConfigurer { @Bean public MybatisPlusInterceptor mybatisPlusInterceptor() { MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor(); // 分頁攔截器 PaginationInnerInterceptor paginationInnerInterceptor = new PaginationInnerInterceptor(DbType.MYSQL); // 跳回首頁 paginationInnerInterceptor.setOverflow(true); // 每頁不受限制 paginationInnerInterceptor.setMaxLimit(-1L); interceptor.addInnerInterceptor(paginationInnerInterceptor); return interceptor; } }
- Web應用:編寫一個配置類實現
-
-
自定義器
xxxCustomizer
; -
......
9.9 示例
以 HttpEncodingAutoConfiguration
(Http 編碼自動配置)為例解釋自動配置原理。
@Configuration(proxyBeanMethods = false) // 表示這是一個配置類,關閉 bean 代理(每次獲取一個新的 bean,相當於 prototype)
@EnableConfigurationProperties(HttpProperties.class) // 將配置文件中對應的值和HttpProperties綁定起來,並把HttpProperties加入到容器中。
@ConditionalOnWebApplication(type = ConditionalOnWebApplication.Type.SERVLET) // 僅對 Web 應用生效。
@ConditionalOnClass(CharacterEncodingFilter.class) // CharacterEncodingFilter 類存在時生效。SpringMVC中進行亂碼解決的過濾器。
@ConditionalOnProperty(prefix = "spring.http.encoding", value = "enabled", matchIfMissing = true)
// 判斷配置文件中是否存在某個配置 spring.http.encoding=enabled。
// 即使我們不配置,pring.http.encoding.enabled=true,也是默認生效的。
public class HttpEncodingAutoConfiguration {
// 它已經和SpringBoot的配置文件映射了。
private final HttpProperties.Encoding properties;
// 只有一個有參構造器的情況下,參數的值就會從容器中拿。
public HttpEncodingAutoConfiguration(HttpProperties properties) {
this.properties = properties.getEncoding();
}
@Bean // 給容器中添加一個組件,這個組件的某些值需要從 properties 中獲取。
@ConditionalOnMissingBean // 容器中沒有這個組件時才生效。
public CharacterEncodingFilter characterEncodingFilter() {
CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
filter.setEncoding(this.properties.getCharset().name());
filter.setForceRequestEncoding(this.properties.shouldForce(Type.REQUEST));
filter.setForceResponseEncoding(this.properties.shouldForce(Type.RESPONSE));
return filter;
}
四、日志
1、日志框架
小張要開發一個大型系統。
-
System.out.println("")。將關鍵數據打印在控制台。去掉?寫在一個文件?
-
框架來記錄系統的一些運行時信息。日志框架 。zhanglogging.jar。
-
高大上的幾個功能?異步模式?自動歸檔?xxx?zhanglogging-good.jar。
-
將以前框架卸下來?換上新的框架,重新修改之前相關的API。zhanglogging-prefect.jar。
-
JDBC---數據庫驅動。
寫了一個統一的接口層,日志門面(日志的一個抽象層):logging-abstract.jar。
給項目中導入具體的日志實現就行了。我們之前的日志框架都是實現的抽象層。
市面上的日志框架。
JUL、JCL、Jboss-logging、logback、log4j、log4j2、SLF4j...
日志門面 (日志的抽象層) | 日志實現 |
---|---|
JCL(Jakarta Commons Logging) 、SLF4j(Simple Logging Facade for Java)、Jboss-logging | Log4j、JUL(java.util.logging)、Log4j2、Logback |
左邊選一個門面(抽象層)、右邊來選一個實現。
Spring 框架默認是用 JCL。
SpringBoot 選用 SLF4j 和 logback。
2、SLF4j 使用
2.1 如何在系統中使用 SLF4j
以后開發的時候,日志記錄方法的調用,不應該來直接調用日志的實現類,而是調用日志抽象層里面的方法。
給系統里面導入 slf4j 的抽象層 jar 和 logback 的實現層 jar。
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
public class HelloWorld {
public static void main(String[] args) {
Logger logger = LoggerFactory.getLogger(HelloWorld.class);
logger.info("Hello World");
}
}
圖示:
每一個日志的實現框架都有自己的配置文件。
2.2 遺留問題
A系統(slf4j + logback): Spring(commons-logging)、Hibernate(jboss-logging)、MyBatis...
統一日志記錄,即使是別的框架和我一起統一使用 slf4j 進行輸出?
如何讓系統中所有的日志都統一到 slf4j?
- 將系統中其他日志框架先排除出去。
- 用中間包來替換原有的日志框架。
- 我們導入 slf4j 其他的實現。
3、SpringBoot 日志關系
SpringBoot 使用它來做日志功能。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-logging</artifactId>
</dependency>
底層依賴關系:
總結:
1. SpringBoot 底層也是使用 slf4j + logback 的方式進行日志記錄。
2. SpringBoot 也把其他的日志都替換成了slf4j。
3. 中間轉換包?
@SuppressWarnings("rawtypes")
public abstract class LogFactory {
static String UNSUPPORTED_OPERATION_IN_JCL_OVER_SLF4J = "http://www.slf4j.org/codes.html#unsupported_operation_in_jcl_over_slf4j";
static LogFactory logFactory = new SLF4JLogFactory();
4. 如果我們要引入其他框架,一定要把這個框架的默認日志依賴移除掉。
Spring 框架用的是 commons-logging(JCL)。
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-core</artifactId>
<exclusions>
<exclusion>
<groupId>commons-logging</groupId>
<artifactId>commons-logging</artifactId>
</exclusion>
</exclusions>
</dependency>
SpringBoot 能自動適配所有的日志,而且底層使用 slf4j + logback 的方式記錄日志,引入其他框架的時候,只需要把這個框架依賴的日志框架排除掉即可。
4、日志使用
4.1 默認配置
SpringBoot 默認幫我們配置好了日志。
// 記錄器
Logger logger = LoggerFactory.getLogger(getClass());
@Test
public void contextLoads() {
// System.out.println();
// 日志的級別。
// 由低到高:trace < debug < info < warn < error
// 可以調整輸出的日志級別,日志就只會在這個級別以以后的高級別生效
logger.trace("這是trace日志...");
logger.debug("這是debug日志...");
// SpringBoot 默認給我們使用的是 info 級別的
logger.info("這是info日志...");
logger.warn("這是warn日志...");
logger.error("這是error日志...");
}
日志輸出格式:
%d表示日期時間,
%thread表示線程名,
%-5level:級別從左顯示5個字符寬度
%logger{50} 表示logger名字最長50個字符,否則按照句點分割。
%msg:日志消息,
%n是換行符
-->
%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n
SpringBoot 修改日志的默認配置:
logging.level.com.atguigu=trace
# 不指定路徑,在當前項目下生成 springboot.log 日志。
#logging.file.path=
# 可以指定完整的路徑。
#logging.file.name=D:/springboot.log
# 在當前磁盤的根路徑下創建 /spring/log 文件夾,使用 spring.log 作為日志文件。
logging.file.path=/spring/log
# 在控制台輸出的日志格式
logging.pattern.console=%d{yyyy-MM-dd} [%thread] %-5level %logger{50} - %msg%n
# 指定文件中輸出的日志格式
logging.pattern.file=%d{yyyy-MM-dd} === [%thread] === %-5level === %logger{50} === %msg%n
logging.file | logging.path | Example | Description |
---|---|---|---|
(none) | (none) | 只在控制台輸出 | |
指定文件名 | (none) | my.log | 輸出日志到 my.log 文件 |
(none) | 指定目錄 | /var/log | 輸出到指定目錄的 spring.log 文件中 |
4.2 指定配置
給類路徑下放上每個日志框架自己的配置文件即可,SpringBoot 就不使用它默認配置的了。
Logging System | Customization |
---|---|
Logback | logback-spring.xml , logback-spring.groovy , logback.xml or logback.groovy |
Log4j2 | log4j2-spring.xml or log4j2.xml |
JDK (Java Util Logging) | logging.properties |
logback.xml:直接就被日志框架識別了。
logback-spring.xml:日志框架就不直接加載日志的配置項,由 SpringBoot 解析日志配置,可以使用 SpringBoot 的高級 profile 功能。
<springProfile name="dev">
<!-- 可以指定某段配置只在某個環境下生效 -->
</springProfile>
如:
<appender name="stdout" class="ch.qos.logback.core.ConsoleAppender">
<!--
日志輸出格式:
%d表示日期時間,
%thread表示線程名,
%-5level:級別從左顯示5個字符寬度
%logger{50} 表示logger名字最長50個字符,否則按照句點分割。
%msg:日志消息,
%n是換行符
-->
<layout class="ch.qos.logback.classic.PatternLayout">
<springProfile name="dev">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} ----> [%thread] ---> %-5level %logger{50} - %msg%n</pattern>
</springProfile>
<springProfile name="!dev">
<pattern>%d{yyyy-MM-dd HH:mm:ss.SSS} ==== [%thread] ==== %-5level %logger{50} - %msg%n</pattern>
</springProfile>
</layout>
</appender>
如果使用 logback.xml 作為日志配置文件,還要使用 Profile 功能,會有以下錯誤
no applicable action for [springProfile]
5、切換日志框架
可以按照 slf4j 的日志適配圖,進行相關的切換。
slf4j + log4j 的方式:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>logback-classic</artifactId>
<groupId>ch.qos.logback</groupId>
</exclusion>
<exclusion>
<artifactId>log4j-over-slf4j</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-log4j12</artifactId>
</dependency>
切換為 log4j2:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<artifactId>spring-boot-starter-logging</artifactId>
<groupId>org.springframework.boot</groupId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-log4j2</artifactId>
</dependency>
五、Web 開發
1、簡介
-
創建 SpringBoot 應用,選中我們需要的 starter 模塊。
-
SpringBoot 已經默認將這些場景配置好了,只需要在配置文件中指定少量配置就可以運行起來
-
自己編寫業務代碼。
2、簡單功能分析
2.1 靜態資源訪問
靜態資源:
css
、jss
、html
、jpg
等資源文件,用戶多次訪問都是獲取同樣的資源,可以被瀏覽器緩存。
2.1.1 靜態資源訪問目錄
只要靜態資源放在類路徑下: called /static
(or /public
or /resources
or /META-INF/resources
"classpath:/META-INF/resources/",
"classpath:/resources/",
"classpath:/static/",
"classpath:/public/"
"/":當前項目的根路徑
訪問當前項目根路徑 /
+ 靜態資源名
,就可以獲取到對應的靜態資源。
訪問地址:http://localhost:8080/hello.jpg,去靜態資源目錄下找 hello.jpg。
原理: 靜態映射/**。
請求進來,先去找 Controller 看能不能處理。不能處理的所有請求又都交給靜態資源處理器。靜態資源也找不到則響應 404 頁面。
2.1.2 靜態資源訪問前綴
默認無前綴。
spring:
mvc:
static-path-pattern: /res/**
訪問當前項目根路徑 /
+ static-path-pattern
+ 靜態資源名
,就可以獲取到對應的靜態資源。
訪問地址:http://localhost:8080/res/hello.jpg,去靜態資源文件夾里面找 hello.jpg。
2.1.3 webjar 映射
所有 /webjars/**
請求,都會去 classpath:/META-INF/resources/webjars/
下找資源。
webjars:以 jar 包的方式引入靜態資源。
<!-- 引入jquery-webjar -->
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.5.0</version>
</dependency>
訪問地址:http://localhost:8080/webjars/jquery/3.5.0/jquery.js,后面地址要按照依賴里面的包路徑,就可以獲取到對應的靜態資源。
2.1.3 修改靜態資源路徑
改變默認的靜態資源前綴和目錄:
spring:
mvc:
static-path-pattern: /res/** # 修改前綴
resources:
static-locations: [classpath:/haha/] # 修改目錄
訪問地址:http://localhost:8080/res/haha/hello.jpg,去靜態資源目錄下找 hello.jpg。
2.2 歡迎頁
靜態資源路徑下的所有 index.html 頁面,被 /**
映射。
- 可以配置靜態資源訪問目錄;
- 但是不能配置靜態資源訪問前綴,否則導致 index.html 不能被默認訪問;
spring:
# mvc:
# static-path-pattern: /res/** # 這個會導致 Welcome Page 功能失效
訪問地址:http://localhost:8080/,就會去靜態資源目錄下找 index.html 頁面。
Controller 能處理 /index
請求。
2.3 自定義 Favicon
將 favicon.ico
放在靜態資源目錄下即可。
跟「歡迎頁」一樣,不能配置靜態資源訪問前綴,否則不能訪問自定義圖標。
2.4 靜態資源配置原理
- SpringBoot 啟動默認加載
xxxAutoConfiguration
類(自動配置類)。 - SpringMVC 功能的自動配置類
WebMvcAutoConfiguration
生效。
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class})
@ConditionalOnMissingBean({WebMvcConfigurationSupport.class})
@AutoConfigureOrder(-2147483638)
@AutoConfigureAfter({DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class, ValidationAutoConfiguration.class})
public class WebMvcAutoConfiguration {
-
查看
WebMvcConfigurer
接口的實現類WebMvcAutoConfigurationAdapter
,看看給容器中配置了什么@Configuration(proxyBeanMethods = false) @Import({WebMvcAutoConfiguration.EnableWebMvcConfiguration.class}) @EnableConfigurationProperties({WebMvcProperties.class, ResourceProperties.class, WebProperties.class}) @Order(0) public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {
-
配置類和配置文件的相關屬性
spring.xxx
進行了綁定。-
WebMvcProperties
綁定spring.mvc
-
ResourceProperties
(已過時) 綁定spring.resources
@Deprecated @ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false) public class ResourceProperties extends Resources {
-
WebProperties
綁定spring.web
@ConfigurationProperties("spring.web") public class WebProperties { public static class Resources { private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/", "classpath:/resources/", "classpath:/static/", "classpath:/public/" }; /** * Locations of static resources. Defaults to classpath:[/META-INF/resources/, * /resources/, /static/, /public/]. */ private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
-
2.4.1 配置唯一的有參類構造函數
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.WebMvcAutoConfigurationAdapter#WebMvcAutoConfigurationAdapter()
// 有參構造器所有參數的值都會從容器中獲取
// ResourceProperties:獲取和 spring.resources 綁定的所有的值的對象
// WebMvcProperties:獲取和 spring.mvc 綁定的所有的值的對象
// ListableBeanFactory:Spring 的 beanFactory
// HttpMessageConverters:找到所有的 HttpMessageConverters
// ResourceHandlerRegistrationCustomizer:找到資源處理器的自定義器。
// DispatcherServletPath:配置訪問路徑
// ServletRegistrationBean:給應用注冊 Listener -> Fliter -> Servlet
public WebMvcAutoConfigurationAdapter (ResourceProperties resourceProperties, WebMvcProperties mvcProperties,
ListableBeanFactory beanFactory, ObjectProvider<HttpMessageConverters> messageConvertersProvider,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
ObjectProvider<DispatcherServletPath> dispatcherServletPath,
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
this.resourceProperties = resourceProperties;
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable ();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
}
2.4.2 靜態資源和 webjars 的處理規則
org.springframework.boot.autoconfigure.web.servlet.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));
}
}
2.4.3 歡迎頁的處理規則
org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration.EnableWebMvcConfiguration#welcomePageHandlerMapping()
// 配置歡迎頁映射
@Bean
public WelcomePageHandlerMapping welcomePageHandlerMapping(ApplicationContext applicationContext,
FormattingConversionService mvcConversionService, ResourceUrlProvider mvcResourceUrlProvider) {
WelcomePageHandlerMapping welcomePageHandlerMapping = new WelcomePageHandlerMapping(
new TemplateAvailabilityProviders(applicationContext), applicationContext, getWelcomePage(),
this.mvcProperties.getStaticPathPattern());
welcomePageHandlerMapping.setInterceptors(getInterceptors(mvcConversionService, mvcResourceUrlProvider));
welcomePageHandlerMapping.setCorsConfigurations(getCorsConfigurations());
return welcomePageHandlerMapping;
}
// WelcomePageHandlerMapping 類的唯一有參構造函數
WelcomePageHandlerMapping(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) {
if (welcomePage != null && "/**".equals(staticPathPattern)) {
// 要用歡迎頁,必須使用默認的靜態資源目錄 /**
logger.info("Adding welcome page: " + welcomePage);
setRootViewName("forward:index.html");
}
else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
// 調用 Controller 處理 /index 請求
logger.info("Adding welcome page template: index");
setRootViewName("index");
}
}
2.4.4 Favicon 的處理規則
// 配置喜歡的圖標
@Configuration
@ConditionalOnProperty(value = "spring.mvc.favicon.enabled", matchIfMissing = true)
public static class FaviconConfiguration {
private final ResourceProperties resourceProperties;
public FaviconConfiguration(ResourceProperties resourceProperties) {
this.resourceProperties = resourceProperties;
}
@Bean
public SimpleUrlHandlerMapping faviconHandlerMapping() {
SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping();
mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1);
// 映射所有 **/favicon.ico
mapping.setUrlMap(Collections.singletonMap("**/favicon.ico",
faviconRequestHandler()));
return mapping;
}
@Bean
public ResourceHttpRequestHandler faviconRequestHandler() {
ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler();
requestHandler
.setLocations(this.resourceProperties.getFaviconLocations());
return requestHandler;
}
}
新版本沒有這個類了,找到了下面這個類:
public enum StaticResourceLocation {
CSS("/css/**"),
JAVA_SCRIPT("/js/**"),
IMAGES("/images/**"),
WEB_JARS("/webjars/**"),
FAVICON("/favicon.*", "/*/icon-*");
private final String[] patterns;
StaticResourceLocation(String... patterns) {
this.patterns = patterns;
}
public Stream<String> getPatterns() {
return Arrays.stream(this.patterns);
}
}
3、請求響應參數處理
3.1 請求映射
3.1.1 REST 使用
-
@xxxMapping
(@GetMapping
、@PostMapping
、@PutMapping
、@DeleteMapping
,@PatchMapping
是對 Put 的補充,區別是 Patch 是部分更新,Put 是全部更新,這些注解都是 Spring4.3 引入的); -
REST 風格支持(使用 HTTP 請求方式動詞來表示對資源的操作)
操作 | 以前 | 現在(REST 風格) |
---|---|---|
獲取用戶 | /getUser 的 GET 請求 | /user/id 的 GET 請求 |
保存用戶 | /saveUser 的 POST 請求 | /user/id 的 POST 請求 |
修改用戶 | /editUser 的 POST 請求 | /user/id 的 PUT 請求 |
刪除用戶 | /deleteUser 的 POST 請求 | /user/id 的 DELETE 請求 |
- 核心過濾器:瀏覽器 form 表單只支持 GET 與 POST 請求,而 DELETE、PUT 等 method 並不支持,Spring 3.0 添加了一個過濾器
HiddenHttpMethodFilter
,可以將這些請求轉換為標准的 HTTP 方法,使得支持 GET、POST、PUT 與 DELETE 請求。
用法: SpringBoot 中手動開啟 REST 支持,表單 method=post
,隱藏域 _method=put
。
擴展:如何把 _method
這個名字換成我們自己喜歡的。
@RequestMapping(value = "/user",method = RequestMethod.GET)
public String getUser(){
return "GET-張三";
}
@RequestMapping(value = "/user",method = RequestMethod.POST)
public String saveUser(){
return "POST-張三";
}
@RequestMapping(value = "/user",method = RequestMethod.PUT)
public String putUser(){
return "PUT-張三";
}
@RequestMapping(value = "/user",method = RequestMethod.DELETE)
public String deleteUser(){
return "DELETE-張三";
}
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled", matchIfMissing = false)
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
// 自定義 HiddenHttpMethodFilter,將隱藏域方法名換成自己喜歡的 _m
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter(){
HiddenHttpMethodFilter methodFilter = new HiddenHttpMethodFilter();
methodFilter.setMethodParam("_m");
return methodFilter;
}
3.1.2 REST 原理
原理(表單提交要使用 REST 的時候)
-
表單提交會帶上
_method=使用的方法
。 -
請求過來被
HiddenHttpMethodFilter
攔截。-
請求是否正常,並且是 POST。
-
獲取到
_method
的值,統一轉換成大寫。 -
兼容以下請求:PUT、DELETE、PATCH。
-
原生
request(post)
,包裝模式requesWrapper
重寫了getMethod
方法,返回的是傳入的值。 -
過濾器鏈放行的時候用
wrapper
,以后的方法調用getMethod
是調用requesWrapper
的。
-
-
使用客戶端工具發送 REST 請求
- 如 PostMan 直接發送 Put、Delete 等方式請求,無需 Filter。
spring:
mvc:
hiddenmethod:
filter:
enabled: true #開啟頁面表單的 REST 功能
3.1.3 請求映射原理
SpringMVC 功能分析都從 org.springframework.web.servlet.DispatcherServlet.doDispatch()
方法開始。
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
try {
ModelAndView mv = null;
Object dispatchException = null;
try {
processedRequest = this.checkMultipart(request);
multipartRequestParsed = processedRequest != request;
// 找到當前請求使用哪個 Handler(Controller 的方法)處理
mappedHandler = this.getHandler(processedRequest);
@Nullable
protected HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
if (this.handlerMappings != null) {
Iterator var2 = this.handlerMappings.iterator();
while(var2.hasNext()) {
HandlerMapping mapping = (HandlerMapping)var2.next();
HandlerExecutionChain handler = mapping.getHandler(request);
if (handler != null) {
return handler;
}
}
}
return null;
}
RequestMappingHandlerMapping
:保存了所有 @RequestMapping
(請求路徑) 和 Handlerg
(Controller
的方法) 的映射規則。
所有的請求映射都在 HandlerMapping
中。
-
SpringBoot 自動配置歡迎頁的
WelcomePageHandlerMapping
,訪問/
能訪問到index.html; -
SpringBoot 自動配置了默認的
RequestMappingHandlerMapping
: -
請求進來,挨個嘗試所有的
HandlerMapping
看是否有請求信息; -
如果有,就找到這個請求對應的
Handler
; -
如果沒有,就遍歷下一個
HandlerMapping
;
如果我們需要一些自定義的映射處理,可以通過給容器中放 HandlerMapping 來自定義 HandlerMapping
有的時候,比如說同一組 API 有不同的版本如 v1,v2,我們可以在 Controller
中寫兩組mapping(比如 v1/user
,v2/user
)。但同時我們也可以放在兩個包下,都是 /user
,這個時候我們就可以自定義 HandlerMapping
,把 v1/user
映射到一個包下的 /user
,把 v2/user
映射到另外一個包下的 /user
。
3.2 普通參數與基本注解
3.2.1 注解
@PathVariable
:請求路徑中的值@RequestParam
:請求參數中的值@RequestHeader
:請求 Header 中的值@RequestBody
:請求 Body 中的值@ModelAttribute
:綁定請求參數到實體對象@CookieValue
:獲取瀏覽器中的 Cookie 值@MatrixVariable
:以分號分隔的矩陣變量鍵值對,形如 ;name1=value1;name2=value2,value3
@RestControllerpublic class ParameterTestController { // 請求路徑:car/2/owner/zhangsan @GetMapping("/car/{id}/owner/{username}") public Map<String,Object> getCar(@PathVariable("id") Integer id, @PathVariable("username") String name, @PathVariable Map<String,String> pv, @RequestHeader("User-Agent") String userAgent, @RequestHeader Map<String,String> header, @RequestParam("age") Integer age, @RequestParam("inters") List<String> inters, @RequestParam Map<String,String> params, @CookieValue("_ga") String _ga, @CookieValue("_ga") Cookie cookie){ Map<String,Object> map = new HashMap<>();// map.put("id",id);// map.put("name",name);// map.put("pv",pv);// map.put("userAgent",userAgent);// map.put("headers",header); map.put("age",age); map.put("inters",inters); map.put("params",params); map.put("_ga",_ga); System.out.println(cookie.getName()+"===>"+cookie.getValue()); return map; } @PostMapping("/save") public Map postMethod(@RequestBody String content){ Map<String,Object> map = new HashMap<>(); map.put("content",content); return map; } //1、請求路徑:/cars/sell;low=34;brand=byd,audi,yd //2、SpringBoot 默認禁用了矩陣變量的功能 // 手動開啟的原理:URL 路徑使用 UrlPathHelper 進行解析。 // 實現 WebMvcConfigurer 接口,重寫 configurePathMatch() // 將 removeSemicolonContent 設置為 false 即可開啟。 //3、矩陣變量必須有 URL 路徑變量才能被解析 @GetMapping("/cars/{path}") public Map carsSell(@MatrixVariable("low") Integer low, @MatrixVariable("brand") List<String> brand, @PathVariable("path") String path){ Map<String,Object> map = new HashMap<>(); map.put("low",low); map.put("brand",brand); map.put("path",path); return map; } // 請求路徑:/boss/1;age=20/2;age=10 @GetMapping("/boss/{bossId}/{empId}") public Map boss(@MatrixVariable(value = "age",pathVar = "bossId") Integer bossAge, @MatrixVariable(value = "age",pathVar = "empId") Integer empAge){ Map<String,Object> map = new HashMap<>(); map.put("bossAge",bossAge); map.put("empAge",empAge); return map; }}
3.2.2 Servlet API
WebRequest
、ServletRequest
、MultipartRequest
、 HttpSession
、PushBuilder
、Principal
、InputStream
、Reader
、HttpMethod
、Locale
、TimeZone
、ZoneId
以上參數都是在 ServletRequestMethodArgumentResolver.supportsParameter()
里判斷的:
然后獲取請求:
3.2.3 復雜參數
Map
、Model
、Errors
/BindingResult
、RedirectAttributes
( 重定向攜帶數據)、ServletResponse
(response)、SessionStatus
、UriComponentsBuilder
、ServletUriComponentsBuilder
Map<String, Object> map
、Model model
、HttpServletRequest request
都可以給 request 域中放數據 request.setAttribute()
。
Map
、Model
類型的參數,會返回 mavContainer.getModel()
// ModelAndViewContainer:模型和視圖的容器private final ModelMap defaultModel = new BindingAwareModelMap();public ModelMap getModel() { if (this.useDefaultModel()) { return this.defaultModel; // 默認返回 BindingAwareModelMap,既是 Model 又是 Map } else { if (this.redirectModel == null) { this.redirectModel = new ModelMap(); // 重定向為空,返回一個空的 LinkedHashMap } return this.redirectModel; // 返回 RedirectAttributesModelMap }}
3.2.4 自定義對象參數
可以自動類型轉換與格式化,還可以級聯封裝。
/**
* html 中引用對象參數
* 姓名: <input name="userName"/> <br/>
* 年齡: <input name="age"/> <br/>
* 生日: <input name="birth"/> <br/>
* 寵物姓名:<input name="pet.name"/><br/>
* 寵物年齡:<input name="pet.age"/>
*/
@Data
public class Person {
private String userName;
private Integer age;
private Date birth;
private Pet pet;
}
@Data
public class Pet {
private String name;
private String age;
}
3.3 自定義類型參數:POJO 封裝
RequestParamMethodArgumentResolver
支持簡單類型的請求方法參數,ServletModelAttributeMethodProcessor
支持非簡單類型的請求方法參數。
所以對於請求 http://localhost:8080/testCustomObj?name=張三&age=18,由於請求方法參數Employee
不是簡單類型,所以會調用 ServletModelAttributeMethodProcessor
將請求參數封裝為 Employee
對象並返回。
@RequestMapping("/testCustomObj")
@ResponseBody
public Employee testCustomObj(Employee e) {
return e;
}
3.4 參數處理原理
HandlerMapping
中找到能處理請求的Handler
(Controller
中請求對應的方法);- 為當前
Handler
找一個適配器HandlerAdapter
,對於請求來說就是RequestMappingHandlerAdapter
; - 適配器執行目標方法並確定方法參數的每一個值。
拓展:
SpringMVC 處理請求大致是這樣的:
- 首先被
DispatcherServlet
截獲,DispatcherServlet
通過handlerMapping
獲得HandlerExecutionChain
,然后獲得HandlerAdapter
。 HandlerAdapter
在內部對於每個請求,都會實例化一個ServletInvocableHandlerMethod
進行處理,ServletInvocableHandlerMethod
在進行處理的時候,會分兩部分別對請求跟響應進行處理。- 處理請求的時候,會根據
ServletInvocableHandlerMethod
的屬性argumentResolvers
(這個屬性是它的父類InvocableHandlerMethod
中定義的)進行處理,其中argumentResolvers
屬性是一個HandlerMethodArgumentResolverComposite
類 (這里使用了組合模式的一種變形),這個類是實現了HandlerMethodArgumentResolver
接口的類,里面有各種實現了HandlerMethodArgumentResolver
的 List 集合。 - 處理響應的時候,會根據
ServletInvocableHandlerMethod
的屬性returnValueHandlers
(自身屬性) 進行處理,returnValueHandlers
屬性是一個HandlerMethodReturnValueHandlerComposite
類(這里使用了組合模式的一種變形),這個類是實現了HandlerMethodReturnValueHandler
接口的類,里面有各種實現了HandlerMethodReturnValueHandler
的 List 集合。
- 處理請求的時候,會根據
- 之后
HandlerAdapter
得到ModelAndView
,然后做相應的處理。
以 Resolver 結尾的類是實現了 HandlerMethodArgumentResolver
接口的類,以 Processor 結尾的類是實現了 HandlerMethodArgumentResolver
和 HandlerMethodReturnValueHandler
的類。
HandlerMethodArgumentResolver
:處理請求方法的參數HandlerMethodReturnValueHandler
:處理請求方法的返回值
3.4.1 處理器的適配器HandlerAdapter
// DispatcherServlet#getHandlerAdapterprotected HandlerAdapter getHandlerAdapter(Object handler) throws ServletException { if (this.handlerAdapters != null) { for (HandlerAdapter adapter : this.handlerAdapters) { if (adapter.supports(handler)) { return adapter; } } } throw new ServletException("No adapter for handler [" + handler + "]: The DispatcherServlet configuration needs to include a HandlerAdapter that supports this handler");}
RequestMappingHandlerAdapter
:支持方法上標注@RequestMapping
的適配器。HandlerFunctionAdapter
:支持函數式編程的適配器。HttpRequestHandlerAdapter
:無返回值,用於處理靜態資源。SimpleControllerHandlerAdapter
:是Controller
實現類的適配器類,其本質是執行Controller
中的handleRequest
方法。
3.4.2 執行目標方法
// DispatcherServlet#doDispatch// Actually invoke the handler.mv = ha.handle(processedRequest, response, mappedHandler.getHandler());// RequestMappingHandlerAdapter#handleInternalmav = invokeHandlerMethod(request, response, handlerMethod); //執行目標方法// RequestMappingHandlerAdapter#invokeHandlerMethodinvocableMethod.invokeAndHandle(webRequest, mavContainer);// ServletInvocableHandlerMethod#invokeAndHandleObject returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);// InvocableHandlerMethod#invokeForRequestObject[] args = getMethodArgumentValues(request, mavContainer, providedArgs);return this.doInvoke(args); // 通過反射調用目標方法
3.4.3 參數解析器 HandlerMethodArgumentResolver
確定將要執行的目標方法的每一個參數的值是什么。
SpringMVC 目標方法能寫多少種參數類型,取決於參數解析器。
// InvocableHandlerMethod#invokeForRequestObject[] args = getMethodArgumentValues(request, mavContainer, providedArgs);// InvocableHandlerMethod#getMethodArgumentValuesif (!this.resolvers.supportsParameter(parameter)) { // 判斷是否支持當前參數類型// 解析支持的參數,並放入參數列表args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);// HandlerMethodArgumentResolverComposite#resolveArgumentHandlerMethodArgumentResolver resolver = this.getArgumentResolver(parameter);// HandlerMethodArgumentResolverComposite#getArgumentResolver// 先從參數解析器緩存中獲取,若獲取不到再從參數解析器中獲取Iterator var3 = this.argumentResolvers.iterator();// 判斷解析器是否支持當前參數類型,若支持放入參數解析器緩存中,然后返回參數解析器
3.4.4 返回值處理
3.4.5 自定義類型參數:POJO 封裝
ServletModelAttributeMethodProcessor
負責處理自定義的參數類型(非簡單類型)。
簡單類型如下:
// BeanUtils#isSimpleValueTypepublic static boolean isSimpleValueType(Class<?> type) { return (Void.class != type && void.class != type && (ClassUtils.isPrimitiveOrWrapper(type) || Enum.class.isAssignableFrom(type) || CharSequence.class.isAssignableFrom(type) || Number.class.isAssignableFrom(type) || Date.class.isAssignableFrom(type) || Temporal.class.isAssignableFrom(type) || URI.class == type || URL.class == type || Locale.class == type || Class.class == type));}
解析參數:
// ModelAttributeMethodProcessor#resolveArgumentWebDataBinder binder = binderFactory.createBinder(webRequest, attribute, name);
WebDataBinder
:Web 數據綁定器,利用它里面的 Converters
將請求數據轉成指定的數據類型,然后封裝到指定的 POJO 對象里面。
GenericConversionService
:在設置每一個值的時候,找它里面所有的 Converters
,哪個可以將這個數據類型(request 帶來參數的字符串)轉換到指定的類型。
@FunctionalInterfacepublic interface Converter<S, T>
可以給WebDataBinder里面放自己的Converter;
private static final class StringToNumber<T extends Number> implements Converter<String, T>
自定義 Converter:
// 1、WebMvcConfigurer 定制化 SpringMVC 的功能@Beanpublic WebMvcConfigurer webMvcConfigurer (){ return new WebMvcConfigurer () { @Override public void configurePathMatch (PathMatchConfigurer configurer) { UrlPathHelper urlPathHelper = new UrlPathHelper (); // 不移除分號后面的內容,矩陣變量功能就可以生效 urlPathHelper.setRemoveSemicolonContent (false); configurer.setUrlPathHelper (urlPathHelper); } @Override public void addFormatters (FormatterRegistry registry) { registry.addConverter (new Converter<String, Pet>() { @Override public Pet convert (String source) { // 阿貓,3 if (!StringUtils.isEmpty (source)){ Pet pet = new Pet (); String [] split = source.split (","); pet.setName (split [0]); pet.setAge (Integer.parseInt (split [1])); return pet; } return null; } }); } };}
3.4.6 目標方法執行完成
將所有返回的數據都放在 ModelAndViewContainer
,包含要去的頁面地址 View
,還包含 Model
數據。
3.4.7 處理派發結果
4、數據響應與內容協商
4.1 響應 JSON
4.1.1 jackson.jar + @ResponseBody
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- Web 場景自動引入了 json 場景,下面的依賴不需要顯示聲明 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-json</artifactId>
<version>2.3.4.RELEASE</version>
<scope>compile</scope>
</dependency>
自動給前端返回 json 數據。
1、返回值解析器
// ServletInvocableHandlerMethod#invokeAndHandle
this.returnValueHandlers.handleReturnValue(
returnValue, getReturnValueType(returnValue), mavContainer, webRequest);
// HandlerMethodReturnValueHandlerComposite#handleReturnValue
handler.handleReturnValue(returnValue, returnType, mavContainer, webRequest);
// RequestResponseBodyMethodProcessor#handleReturnValue
// 使用對應的消息轉換器進行寫出操作
writeWithMessageConverters(returnValue, returnType, inputMessage, outputMessage);
2、返回值解析器原理
-
返回值處理器判斷是否支持這種類型的返回值
supportsReturnType
; -
返回值處理器調用
handleReturnValue
進行處理; -
RequestResponseBodyMethodProcessor
可以處理有 @ModelAttribute 且為對象類型的 @ResponseBody 注解。-
利用
MessageConverters
進行處理將數據寫為 json。-
內容協商:瀏覽器默認會以請求頭(Accept 字段)的方式告訴服務器他能接受什么樣的內容類型;
-
服務器根據自己的能力,決定能生產什么內容類型的數據;
-
SpringMVC 會遍歷所有容器底層的
HttpMessageConverter
,看誰能處理。- 得到
MappingJackson2HttpMessageConverter
,它可以將對象寫為 json; - 利用
MappingJackson2HttpMessageConverter
將對象轉為 json 再寫出去。
- 得到
-
-
4.1.2 支持的返回類型
-
ModelAndView
:包含Model
和View
對象,可以通過它訪問@ModelAttribute
注解的對象。 -
Model
:僅包含數據訪問,通過RequestToViewNameTranslator
來隱蔽地決定此請求返回的View
視圖對象。 -
Map
:和Model
相似。 -
View
:僅包含視圖數據,而Model
數據隱含在@ModelAttribute
注解標注的對象中、或者 -
String
:表示View
視圖的名稱。數據信息的保存同上。 -
void
:當開發者直接操作ServletResponse
/HttpServletResponse
進行請求跳轉,或者View
由RequestToViewNameTranslator
隱蔽地決定時,可使用此返回值。 -
任意對象:如果方法被
@ResponseBody
注解,可采用此值。Spring 會使用HttpMessageConverters
將對象轉化成文本輸出。 -
HttpEntity<?>
或ResponseEntity<?>
:使用此值,Spring 也會使用HttpMessageConverters
將對象轉化成文本輸出。 -
Callable
:異步請求時使用。 -
DeferredResult
:當 Spring 決定使用選擇的某個線程產生值時可以使用此對象。 -
WebAsyncTask
:帶有超時時間的 Callable 異步任務。
4.1.3 HTTPMessageConverter 原理
1、MessageConverter 規范
HttpMessageConverter
: 調用 canRead()
或 canWrite()
方法判斷是否支持將 此 Class 類型的對象,轉為 MediaType
類型的數據。
例子: json 轉為 Person 對象,或者Person 對象轉為 json。
2、默認的MessageConverter
// AbstractMessageConverterMethodArgumentResolver#readWithMessageConverters
for (HttpMessageConverter<?> converter : this.messageConverters) {
ByteArrayHttpMessageConverter
:只支持字節數組類型的。StringHttpMessageConverter
:UTF-8 類型的字符串。StringHttpMessageConverter
:ISO-8859-1 類型的字符串。ResourceHttpMessageConverter
:Resource
類型。ResourceRegionHttpMessageConverter
:ResourceHttpMessageConverter
的缺省設置,用於支持 HTTP Range 頭部使用時,將靜態資源的部分寫入到響應對象。SourceHttpMessageConverter
:DOMSource.class
、SAXSource.class
、StAXSource.class
、StreamSource.class
、Source.class
。AllEncompassingFormHttpMessageConverter
:對FormHttpMessageConverter
(表單與MultiValueMap
的相互轉換)的擴展,提供了對 xml 和 json 的支持。MappingJackson2HttpMessageConverter
:使用 Jackson 的 ObjectMapper 轉換 Json 數據MappingJackson2HttpMessageConverter
:Jaxb2RootElementHttpMessageConverter
:支持注解方式將對象轉換為 xml。
最終 MappingJackson2HttpMessageConverter
把對象轉為 json(利用底層 jackson 的objectMapper
轉換的)。
【小家 Spring】Spring MVC 容器的 web 九大組件之 ---HandlerAdapter 源碼詳解 ---HttpMessageConverter 消息轉換器詳解
4.2 內容協商
4.2.1 引入 xml 依賴
根據客戶端接收能力不同,返回不同媒體類型的數據。
若客戶端無法解析服務端返回的內容,即媒體類型未匹配,那么響應 406。
<dependency>
<groupId>com.fasterxml.jackson.dataformat</groupId>
<artifactId>jackson-dataformat-xml</artifactId>
</dependency>
4.2.2 返回 json 和 xml
Postman 分別測試,只需要改變請求頭中 Accept 字段。HTTP 協議中規定的,告訴服務器本客戶端可以接收的數據類型。
4.2.3 開啟瀏覽器參數方式內容協商功能
為了方便內容協商,開啟基於請求參數的內容協商功能。
spring:
contentnegotiation:
favor-parameter: true #開啟請求參數內容協商模式
優缺點:
- 優點:不受瀏覽器約束。
- 缺點:需要額外的傳遞 format 參數,URL 變得冗余繁瑣,缺少了 REST 的簡潔風范。還有個缺點便是:需要手動顯示開啟。
發請求:
http://localhost:8080/test/person?format=json
http://localhost:8080/test/person?format=xml
確定客戶端接收什么樣的內容類型:
-
Parameter 策略優先確定是要返回 json 數據(獲取請求頭中的 format 的值)
-
最終進行內容協商返回給客戶端 json 即可。
4.2.4 內容協商原理
-
判斷當前響應頭中是否已經有確定的媒體類型
MediaType
; -
獲取客戶端(PostMan、瀏覽器)支持接收的內容類型,即獲取 Accept 請求頭字段,如application/xml;
-
contentNegotiationManager
內容協商管理器,默認使用基於請求頭的策略; -
HeaderContentNegotiationStrategy
確定客戶端可以接收的內容類型 。
-
-
遍歷循環所有當前系統的
MessageConverter
,看誰支持操作這個Person
對象;上面這四個並不完全一樣:
-
找到支持操作
Person
對象的MessageConverter
,把MessageConverter
支持的媒體類型統計出來; -
客戶端需要 application/xml,在服務端能力列表中;
6、進行內容協商,選出最佳匹配媒體類型;
7、用 MessageConverter
將對象轉為最佳匹配媒體類型 。
導入了 jackson 處理 xml 的包,xml 的 MessageConverter
就會自動配置進來。
// WebMvcConfigurationSupport
jackson2XmlPresent = ClassUtils.isPresent("com.fasterxml.jackson.dataformat.xml.XmlMapper", classLoader);
// WebMvcConfigurationSupport#addDefaultHttpMessageConverters
if (jackson2XmlPresent) {
Jackson2ObjectMapperBuilder builder = Jackson2ObjectMapperBuilder.xml();
if (this.applicationContext != null) {
builder.applicationContext(this.applicationContext);
}
messageConverters.add(new MappingJackson2XmlHttpMessageConverter(builder.build()));
}
4.2.5 自定義 MessageConverter
實現多協議數據兼容:json、xml、x-guigu。
@ResponseBody
響應數據出去 調用RequestResponseBodyMethodProcessor
處理;- Processor 處理方法返回值,通過
MessageConverter
處理; - 所有
MessageConverter
合起來可以支持各種媒體類型數據的操作(讀、寫); - 內容協商找到最終的
MessageConverter
。
spring:
mvc:
contentnegotiation:
media-types:
gg: application/x-guigu
favor-parameter: true
配置 SpringMVC 的什么功能,只需要給容器中添加一個 WebMvcConfigurer
並配置即可。
@Bean
public WebMvcConfigurer webMvcConfigurer(){
return new WebMvcConfigurer() {
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
}
}
}
有可能我們添加的自定義的功能會覆蓋默認很多功能,導致一些默認的功能失效。
5、視圖解析與模板引擎
視圖解析:SpringBoot默認不支持 JSP,需要引入第三方模板引擎技術實現頁面渲染。
模板引擎:是為了使用戶界面與業務數據(內容)分離而產生的,它可以生成特定格式的文檔,用於網站的模板引擎就會生成一個標准的文檔。
- Velocity:一個基於 Java 的模板引擎,其提供了一個
Context
容器,在 Java 代碼里面我們可以往容器中存值,然后在 vm 文件中使用特定的語法獲取。 - [Freemarker](FreeMarker 教程網):一個基於模板生成文本輸出的通用工具,使用純 Java 編寫,模板中沒有業務邏輯,外部 Java 程序通過數據庫操作等生成數據傳入模板(template)中,然后輸出頁面。它能夠生成各種文本:HTML、XML、RTF、Java 源代碼等等,而且不需要 Servlet 環境,並且可以從任何源載入模板,如本地文件、數據庫等等。
- Thymeleaf:是適用於 Web 和獨立環境的現代服務器端 Java 模板引擎,能夠處理 HTML,XML,JavaScript,CSS 甚至純文本。有兩種標記模板模式
HTML
(常用)和XML
,三種文本模板模式TEXT
,JAVASCRIPT
和CSS
和無操作模板模式RAW
。
SpringBoot 推薦使用 Thymeleaf,因為與 Velocity、FreeMarker 等傳統的 Java 模板引擎不同,Thymeleaf 支持 HTML
模板模式,模板后綴為 .html
,可以直接被瀏覽器打開,因此,預覽時非常方便。
5.1 視圖解析原理
-
目標方法處理的過程中,所有數據都會被放在
ModelAndViewContainer
里面,包括數據和視圖地址; -
方法的參數是一個自定義類型對象(從請求參數中確定的),把它重新放在
ModelAndViewContainer
; -
任何目標方法執行完成以后都會返回
ModelAndView
(數據和視圖地址); -
processDispatchResult
處理派發結果(頁面該如何響應)。-
render(mv, request, response);
進行頁面渲染邏輯。-
根據方法的
String
返回值得到View
對象(定義了頁面的渲染邏輯)-
所有的視圖解析器嘗試是否能根據當前返回值得到 View 對象;
-
得到了 redirect:/main.html --> Thymeleaf 使用
new RedirectView()
創建視圖; -
ContentNegotiationViewResolver
里面包含了下面所有的視圖解析器,內部還是利用上面所有視圖解析器得到視圖對象; -
view.render(mv.getModelInternal(), request, response);
視圖對象調用自定義的render
進行頁面渲染工作。-
RedirectView
如何渲染(重定向到一個頁面)。-
獲取目標
URL
地址; -
調用原生的
response.sendRedirect(encodedURL);
發送重定向。
-
-
-
-
-
Controller 方法的返回值如下
5.1.1 返回 ModelAndView
-
ModelAndView
存放數據,addObject()
,往model
(request
域)添加數據; -
ModelAndView
添加邏輯視圖名,setViewName()
,經過視圖解析器,得到物理視圖,轉發到物理視圖。
@RequestMapping("/getUser.action")
public ModelAndView getUser(@RequestParam(name="userId",required = true) Integer id) throws Exception{
System.out.println("id=" + id);
ModelAndView modelAndView = new ModelAndView();
User user = userService.queryOne(id);
modelAndView.addObject("user", user);
modelAndView.setViewName("userinfo");
return modelAndView;
}
5.1.2 返回 String
-
邏輯視圖名: 經過視圖解析器,得到物理視圖,轉發到物理視圖;
@RequestMapping("/index.action") public String toIndex() { return "index"; }
-
redirect:資源路徑
:不經過視圖解析器,要求這個資源路徑以完整的路徑/
開頭,重定向到資源;new RedirectView()
->response.sendRedirect(encodedURL);
@RequestMapping("/index.action") public String toIndex() { return "redirect:/jsp/index.jsp"; }
-
forward:資源路徑
: 不經過視圖解析器,要求這個資源路徑以完整的路徑/
開頭,轉發到資源;new InternalResourceView(forwardUrl);
->request.getRequestDispatcher(path).forward(request, response);
@RequestMapping("/index.action") public String toIndex() { return "forward:/jsp/index.jsp"; }
-
普通字符串、對象:
new ThymeleafView()
-> 調用模板引擎的process
方法進行頁面渲染(用writer
輸出)// 將 user 對象以 json 的格式響應給前端頁面 @RequestMapping("/queryUserByCondition.action") @ResponseBody // 需要結合該注解,表示響應的不是視圖 public User queryUserByCondition(User user) throws Exception{ return user; }
5.2 Thymeleaf 語法
5.2.1 表達式
表達式名字 | 語法 | 用途 |
---|---|---|
變量取值 | ${...} | 獲取請求域、session 域、對象等值 |
選擇變量 | *{...} | 獲取上下文對象值 |
消息 | #{...} | 獲取國際化等值 |
鏈接 | @{...} | 生成鏈接 |
片段表達式 | ~{...} | 類似於 jsp:include ,引入公共頁面片段 |
5.2.2 字面量
文本值:'one text'
、'Another one!'
、…
數字:0
、34
、3.0
、12.3
、…
布爾值:true
、false
空值:null
變量:one
、two
、.... 變量不能有空格
5.2.3 文本操作
字符串拼接:+
變量替換:|The name is ${name}|
5.2.4 數學運算
運算符:+
、-
、*
、/
、%
5.2.5布爾運算
運算符:and
、or
一元運算:!
、not
5.2.6比較運算
比較:>
、<
、>=
、<=
(gt
、lt
、ge
、le
)
等式:==
、!=
(eq
、ne
)
5.2.7 條件運算
If-then:(if) ? (then)
If-then-else:(if) ? (then) : (else)
Default:(value) ?: (defaultvalue)
5.2.8 特殊操作
無操作:_
5.2.9 設置屬性值 th:attr
設置單個值:
<form action="subscribe.html" th:attr="action=@{/subscribe}">
<fieldset>
<input type="text" name="email" />
<input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
</fieldset>
</form>
設置多個值:
<img src="../../images/gtvglogo.png" th:attr="src=@{/images/gtvglogo.png},title=#{logo},alt=#{logo}" />
以上兩個的代替寫法 th:xxx
<input type="submit" value="Subscribe!" th:value="#{subscribe.submit}"/>
<form action="subscribe.html" th:action="@{/subscribe}">
所有 HTML5
兼容的標簽寫法:
https://www.thymeleaf.org/doc/tutorials/3.0/usingthymeleaf.html#setting-value-to-specific-attributes
5.2.10 迭代
<tr th:each="prod : ${prods}">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
<tr th:each="prod,iterStat : ${prods}" th:class="${iterStat.odd}? 'odd'">
<td th:text="${prod.name}">Onions</td>
<td th:text="${prod.price}">2.41</td>
<td th:text="${prod.inStock}? #{true} : #{false}">yes</td>
</tr>
5.2.11 條件運算
<a href="comments.html"
th:href="@{/product/comments(prodId=${prod.id})}"
th:if="${not #lists.isEmpty(prod.comments)}">view</a>
<div th:switch="${user.role}">
<p th:case="'admin'">User is an administrator</p>
<p th:case="#{roles.manager}">User is a manager</p>
<p th:case="*">User is some other thing</p>
</div>
5.2.12屬性優先級
5.3 Thymeleaf 使用
5.3.1 引入 starter
<properties>
<java.version>1.8</java.version>
<thymeleaf.version>3.0.11.RELEASE</thymeleaf.version>
<!-- 布局功能的支持程序 thymeleaf3 主程序 layout2 以上版本 -->
<thymeleaf-layout-dialect.version>2.4.1</thymeleaf-layout-dialect.version>
</properties>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
5.3.2 Thymeleaf 自動配置
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration { }
自動配好的策略
-
所有 Thymeleaf 的配置值都在
ThymeleafProperties
。 -
配置好了
SpringTemplateEngine
。 -
配置好了
ThymeleafViewResolver
。 -
我們只需要直接開發頁面。
@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {
private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";
只要我們把 HTML 頁面放在 classpath:/templates/,Thymeleaf 就能自動渲染。
Thymeleaf 頁面開發
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1 th:text="${msg}"> 哈哈 </h1>
<h2>
<a href="www.atguigu.com" th:href="${link}"> 去百度 </a><br/>
<a href="www.atguigu.com" th:href="@{link}"> 去百度 2</a>
</h2>
</body>
</html>
6、構建后台管理系統
6.1 創建項目
pom.xml 需要引入以下依賴:thymeleaf、web-starter、devtools、lombok。
6.2靜態資源處理
已經自動配置好,只需把所有靜態資源放到 static 文件夾下,模板頁面放到 templates 文件夾下。
6.3 路徑構建
th:action="@{/login}"
6.4 頁面跳轉
Controller
:
@PostMapping("/login")
public String main(User user, HttpSession session, Model model){
if (StringUtils.hasLength (user.getUserName()) && "123456".equals (user.getPassword())){
// 把登陸成功的用戶保存起來
session.setAttribute("loginUser",user);
// 登錄成功重定向到 main.html; 重定向防止表單重復提交
return "redirect:/main.html";
} else {
model.addAttribute("msg","賬號密碼錯誤");
// 回到登錄頁面
return "login";
}
}
登陸錯誤消息的顯示:
<p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty(msg)}"></p>
6.5 數據渲染
Controller
:
@GetMapping("/dynamic_table")
public String dynamic_table(Model model){
// 表格內容的遍歷
List<User> users = Arrays.asList(new User("zhangsan", "123456"),
new User("lisi", "654321"),
new User("haha", "aaa"));
model.addAttribute("users", users);
return "table/dynamic_table";
}
6.6 模板頁面
table/dynamic_table.html:
<table class="display table table-bordered" id="hidden-table-info">
<thead>
<tr>
<th>#</th>
<th>用戶名</th>
<th>密碼</th>
</tr>
</thead>
<tbody>
<tr class="userClass" th:each="user,stats:${users}">
<td th:text="${stats.count}"></td>
<td th:text="${user.userName}"></td>
<td>[[${user.password}]]</td> <!-- 行內寫法 -->
</tr>
</tbody>
</table>
開發期間模板引擎頁面修改以后,要實時生效,需要禁用模板引擎的緩存,頁面修改完成以后Ctrl + F9:重新編譯(無需重新啟動)。
# 禁用緩存
spring.thymeleaf.cache=false
6.7 國際化
6.7.1 簡介
- 編寫國際化配置文件;
- 使用
ResourceBundleMessageSource
管理國際化資源文件; - 在頁面使用
fmt:message
取出國際化內容。
6.7.2 步驟
- 編寫國際化配置文件,抽取頁面需要顯示的國際化消息;
- SpringBoot 自動配置好了管理國際化資源文件的組件;
public class MessageSourceAutoConfiguration {
private static final Resource[] NO_RESOURCES = {};
@Bean
// 我們的配置文件可以直接放在類路徑下叫messages.properties。
@ConfigurationProperties(prefix = "spring.messages")
public MessageSourceProperties messageSourceProperties() {
return new MessageSourceProperties();
}
@Bean
public MessageSource messageSource(MessageSourceProperties properties) {
ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();
if (StringUtils.hasText(properties.getBasename())) {
// 設置國際化資源文件的基礎名(去掉語言國家代碼的)。
messageSource.setBasenames(StringUtils
.commaDelimitedListToStringArray(StringUtils.trimAllWhitespace(properties.getBasename())));
}
if (properties.getEncoding() != null) {
messageSource.setDefaultEncoding(properties.getEncoding().name());
}
messageSource.setFallbackToSystemLocale(properties.isFallbackToSystemLocale());
Duration cacheDuration = properties.getCacheDuration();
if (cacheDuration != null) {
messageSource.setCacheMillis(cacheDuration.toMillis());
}
messageSource.setAlwaysUseMessageFormat(properties.isAlwaysUseMessageFormat());
messageSource.setUseCodeAsDefaultMessage(properties.isUseCodeAsDefaultMessage());
return messageSource;
}
- 去頁面獲取國際化的內容。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Signin Template for Bootstrap</title>
<!-- Bootstrap core CSS -->
<link href="asserts/css/bootstrap.min.css" th:href="@{/webjars/bootstrap/4.4.1/css/bootstrap.css}" rel="stylesheet">
<!-- Custom styles for this template -->
<link href="asserts/css/signin.css" th:href="@{/asserts/css/signin.css}" rel="stylesheet">
</head>
<body class="text-center">
<form class="form-signin" action="dashboard.html">
<img class="mb-4" th:src="@{/asserts/img/bootstrap-solid.svg}" src="asserts/img/bootstrap-solid.svg" alt="" width="72" height="72">
<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>
<label class="sr-only" th:text="#{login.username}">Username</label>
<input type="text" class="form-control" placeholder="Username" th:placeholder="#{login.username}" required="" autofocus="">
<label class="sr-only" th:text="#{login.password}">Password</label>
<input type="password" class="form-control" placeholder="Password" th:placeholder="#{login.password}" required="">
<div class="checkbox mb-3">
<label>
<input type="checkbox" value="remember-me"/> [[#{login.remember}]]
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.btn}">Sign in</button>
<p class="mt-5 mb-3 text-muted">© 2020-2021</p>
<a class="btn btn-sm">中文</a>
<a class="btn btn-sm">English</a>
</form>
</body>
</html>
效果:根據瀏覽器設置的語言信息切換了國際化。
6.7.3 原理
Locale
:區域信息對象;LocaleResolver
:獲取區域信息對象。
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty(prefix = "spring.mvc", name = "locale")
public LocaleResolver localeResolver() {
if (this.mvcProperties.getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) {
return new FixedLocaleResolver(this.mvcProperties.getLocale());
}
AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver();
localeResolver.setDefaultLocale(this.mvcProperties.getLocale());
return localeResolver;
}
默認的就是根據請求頭帶來的區域信息獲取 Locale
進行國際化。
- 點擊鏈接切換國際化
// 可以在鏈接上攜帶區域信息
public class MyLocaleResolver implements LocaleResolver {
@Override
public Locale resolveLocale(HttpServletRequest request) {
String l = request.getParameter("l");
Locale locale = Locale.getDefault();
if(!StringUtils.isEmpty(l)){
String[] split = l.split("_");
locale = new Locale(split[0],split[1]);
}
return locale;
}
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
}
@Bean
public LocaleResolver localeResolver(){
return new MyLocaleResolver();
}
}
7、RESTful 的 CRUD
7.1 實驗要求
- CRUD 滿足 REST 風格。
URI:/資源名稱/資源標識,以 HTTP 請求方式區分對資源的 CRUD 操作。
普通的 CRUD(URI 來區分操作) | RESTful 的 CRUD | |
---|---|---|
查詢 | getEmp | emp---GET |
添加 | addEmp?xxx | emp---POST |
修改 | updateEmp?id=xxx&xxx=xx | emp/{id}---PUT |
刪除 | deleteEmp?id=1 | emp/{id}---DELETE |
- 請求架構
實驗功能 | 請求 URI | 請求方式 |
---|---|---|
查詢所有員工 | emps | GET |
查詢某個員工(來到修改頁面) | emp/1 | GET |
來到添加頁面 | emp | GET |
添加員工 | emp | POST |
來到修改頁面(查出員工信息並進行回顯) | emp/1 | GET |
修改員工 | emp | PUT |
刪除員工 | emp/1 | DELETE |
7.2 CRUD-員工列表
Thymeleaf 公共頁面元素抽取:
-
抽取公共片段
<div th:fragment="copy">© 2021 The Good Thymes Virtual Grocery</div>
-
引入公共片段
<div th:insert="~{footer :: copy}"></div>
~{templatename::selector}
-> 模板名 :: 選擇器~{templatename::fragmentname}
-> 模板名 :: 片段名
-
默認效果
insert
的公共片段在div
標簽中;- 如果使用
th:insert
等屬性進行引入,可以不用寫~{}
; - 行內寫法:
[[~{}]]
或[(~{})]
。
三種引入公共片段的 th
屬性:
th:insert
:將公共片段整個插入到聲明引入的元素中;th:replace
:將聲明引入的元素替換為公共片段;th:include
:將被引入的片段的內容包含進這個標簽中。
<footer th:fragment="copy">© 2011 The Good Thymes Virtual Grocery</footer>
<!-- 三種引入方式 -->
<div th:insert="footer :: copy"></div>
<div th:replace="footer :: copy"></div>
<div th:include="footer :: copy"></div>
<!-- 對應的效果 -->
<div><footer>© 2011 The Good Thymes Virtual Grocery</footer></div>
<footer>© 2011 The Good Thymes Virtual Grocery</footer>
<div>© 2011 The Good Thymes Virtual Grocery</div>
引入片段的時候傳入參數:
<nav class="col-md-2 d-none d-md-block bg-light sidebar" id="sidebar">
<div class="sidebar-sticky">
<ul class="nav flex-column">
<li class="nav-item">
<a class="nav-link active"
th:class="${activeUri=='main.html' ? 'nav-link active' : 'nav-link'}"
href="#" th:href="@{/main.html}">
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round" class="feather feather-home">
<path d="M3 9l9-7 9 7v11a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2z"></path>
<polyline points="9 22 9 12 15 12 15 22"></polyline>
</svg>
Dashboard <span class="sr-only">(current)</span>
</a>
</li>
<!-- 引入側邊欄並傳入參數 -->
<div th:replace="commons/bar :: #sidebar(activeUri='emps')"></div>
7.3 CRUD-員工添加
添加頁面:
<form th:action="@{/emp}" method="post">
<div class="form-group">
<label>LastName</label>
<input name="LastName" type="text" class="form-control" placeholder="zhangsan">
</div>
<div class="form-group">
<label>Email</label>
<input name="email" type="email" class="form-control" placeholder="zhangsan@atguigu.com">
</div>
<div class="form-group">
<label>Gender</label><br/>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="gender" value="1">
<label class="form-check-label">男</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="gender" value="0">
<label class="form-check-label">女</label>
</div>
</div>
<div class="form-group">
<label>department</label>
<select class="form-control" name="department.id">
<!-- 提交的是部門的 id -->
<option th:value="${dept.id}" th:each="dept:${depts}" th:text="${dept.departmentName}">1</option>
</select>
</div>
<div class="form-group">
<label>Birth</label>
<input name="birth" type="text" class="form-control" placeholder="zhangsan">
</div>
<button type="submit" class="btn btn-primary">添加</button>
</form>
7.4 CRUD-員工修改
修改添加二合一表單:
<!-- 需要區分是員工修改還是添加 -->
<form th:action="@{/emp}" method="post">
<!-- 發送 PUT 請求修改員工數據 -->
<!-- REST 使用方式:
1、SpringMVC 中配置 HiddenHttpMethodFilter(Springboot 自動配置);
2、頁面創建一個 POST 表單;
3、創建一個 input 項,name="_method",值就是我們指定的請求方式。
-->
<input type="hidden" name="_method" value="put" th:if="${emp!=null}"/>
<input type="hidden" name="id" th:if="${emp!=null}" th:value="${emp.id}"/>
<div class="form-group">
<label>LastName</label>
<input name="lastName" type="text" class="form-control" placeholder="zhangsan" th:value="${emp!=null}?${emp.lastName}">
</div>
<div class="form-group">
<label>Email</label>
<input name="email" type="email" class="form-control" placeholder="zhangsan@atguigu.com" th:value="${emp!=null}?${emp.email}">
</div>
<div class="form-group">
<label>Gender</label><br/>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="gender" value="1" th:checked="${emp!=null}?${emp.gender==1}">
<label class="form-check-label">男</label>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="gender" value="0" th:checked="${emp!=null}?${emp.gender==0}">
<label class="form-check-label">女</label>
</div>
</div>
<div class="form-group">
<label>department</label>
<!-- 提交的是部門的 id -->
<select class="form-control" name="department.id">
<option th:selected="${emp!=null}?${dept.id}==${emp.department.id}" th:value="${dept.id}" th:each="dept:${depts}" th:text="${dept.departmentName}">1</option>
</select>
</div>
<div class="form-group">
<label>Birth</label>
<input name="birth" type="text" class="form-control" placeholder="zhangsan" th:value="${emp!=null}?${#dates.format(emp.birth, 'yyyy-MM-dd HH:mm')}">
</div>
<button type="submit" class="btn btn-primary" th:text="${emp!=null}?'修改':'添加'">添加</button>
</form>
7.5 CRUD-員工刪除
如果報錯:
org.springframework.web.HttpRequestMethodNotSupportedException: Request method 'POST' not supported
那么需要在 application.properties 中配置:
# 啟用 hiddenMethod 過濾器
spring.mvc.hiddenmethod.filter.enabled=true
<tr th:each="emp:${emps}">
<td th:text="${emp.id}"></td>
<td>[[${emp.lastName}]]</td>
<td th:text="${emp.email}"></td>
<td th:text="${emp.gender}==0?'女':'男'"></td>
<td th:text="${emp.department.departmentName}"></td>
<td th:text="${#dates.format(emp.birth, 'yyyy-MM-dd HH:mm')}"></td>
<td>
<a class="btn btn-sm btn-primary" th:href="@{/emp/}+${emp.id}">編輯</a>
<button th:attr="del_uri=@{/emp/}+${emp.id}" class="btn btn-sm btn-danger deleteBtn">刪除</button>
</td>
</tr>
<script>
$(".deleteBtn").click(function(){
// 刪除當前員工
$("#deleteEmpForm").attr("action", $(this).attr("del_uri")).submit();
return false;
});
</script>
8、攔截器
8.1 HandlerInterceptor
接口
/**
* 登錄檢查:
* 1、配置好攔截器要攔截哪些請求;
* 2、把這些配置放在容器中。
*/
@Slf4j
public class LoginInterceptor implements HandlerInterceptor {
// 目標方法執行之前
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
log.info("preHandle 攔截的請求路徑是 {}",requestURI);
// 登錄檢查邏輯
HttpSession session = request.getSession();
Object loginUser = session.getAttribute ("loginUser");
if (loginUser != null){
// 放行
return true;
}
// 未登錄:攔截住請求,然后跳轉到登錄頁
request.setAttribute("msg","請先登錄");
// request.sendRedirect("/");
request.getRequestDispatcher("/").forward(request,response);
return false;
}
// 目標方法執行完成以后
@Override
public void postHandle (HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
log.info ("postHandle 執行 {}", modelAndView);
}
// 頁面渲染完成以后
@Override
public void afterCompletion (HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
log.info ("afterCompletion 執行異常 {}",ex);
}
}
8.2 配置攔截器
/**
* 1、編寫一個攔截器實現 HandlerInterceptor 接口
* 2、攔截器注冊到容器中(實現 WebMvcConfigurer 的 addInterceptors 方法)
* 3、指定攔截規則(如果是攔截所有,靜態資源也會被攔截)
*/
@Configuration
public class AdminWebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors (InterceptorRegistry registry) {
registry.addInterceptor (new LoginInterceptor ())
.addPathPatterns ("/**") // 所有請求都被攔截包括靜態資源
.excludePathPatterns ("/","/login","/css/**","/fonts/**","/images/**","/js/**"); // 放行的請求
}
}
8.3 攔截器原理
-
根據當前請求,找到
HandlerExecutionChain
(可以處理請求的 handler 以及 handler 的所有攔截器); -
先來順序執行所有攔截器的
preHandle
方法;
- 如果當前攔截器
prehandler
返回true
,則執行下一個攔截器的preHandle
; - 如果當前攔截器
prehandler
返回false
,直接倒序觸發所有已經執行了的攔截器的afterCompletion
;
-
如果任何一個攔截器返回
false
,直接跳出,不執行目標方法; -
如果所有攔截器都返回
true
,執行目標方法; -
倒序執行所有攔截器的
postHandle
方法; -
前面的步驟有任何異常,都會直接倒序觸發
afterCompletion
; -
頁面成功渲染完成以后,也會倒序觸發
afterCompletion
。
9、文件上傳
一個文件上傳的過程如下圖所示:
- 瀏覽器發起 HTTP POST 請求,指定請求頭:
Content-Type: multipart/form-data - 服務端解析請求內容,執行文件保存處理,返回成功消息。
參考:文件上傳原理
9.1 頁面表單
<form method="post" action="/upload" enctype="multipart/form-data">
<input type="file" name="file"><br>
<input type="submit" value="提交">
</form>
9.2 限制上傳文件大小
spring:
servlet:
multipart:
max-file-size: 10MB # 單個上傳文件大小上限
max-request-size: 100MB # 一次請求上傳所有文件大小上限
9.3 文件上傳代碼
// MultipartFile 自動封裝上傳過來的文件
@PostMapping("/upload")
public String upload(@RequestParam("email") String email,
@RequestParam("username") String username,
@RequestPart("headerImg") MultipartFile headerImg,
@RequestPart("photos") MultipartFile[] photos) throws IOException {
log.info("上傳的信息:email={},username={},headerImg={},photos={}",
email, username, headerImg.getSize(), photos.length);
if(!headerImg.isEmpty()){
// 保存到文件服務器,OSS服務器
String originalFilename = headerImg.getOriginalFilename();
headerImg.transferTo(new File("D:\\cache\\"+originalFilename));
}
if(photos.length > 0){
for (MultipartFile photo : photos) {
if(!photo.isEmpty()){
String originalFilename = photo.getOriginalFilename();
photo.transferTo(new File("D:\\cache\\" + originalFilename));
}
}
}
return "main";
}
9.4 自動配置原理
文件上傳自動配置類 MultipartAutoConfiguration
自動配置好了文件上傳解析器 StandardServletMultipartResolver
(使用 Servlet 所提供的功能支持,不需要依賴任何其他的項目) 。
原理步驟:
-
request
請求進來使用文件上傳解析器判斷(isMultipart(request
)並封裝(resolveMultipart(request)
),返回(MultipartHttpServletRequest
)文件上傳請求; -
參數解析器
RequestPartMethodArgumentResolver
來解析請求中的文件內容並封裝成MultipartFile
(上傳文件的詳細信息,如原始文件名、內容類型、大小等等); -
將
request
中的文件信息封裝為一個MultiValueMap<String, MultipartFile>
; -
遍歷
MultiValueMap
對於每一個MultipartFile
調用FileCopyUtils.copy()
實現文件流的拷貝。
10、異常處理
10.1 默認規則
默認情況下,SpringBoot 提供 /error
處理所有錯誤的映射。
-
對於瀏覽器客戶端,響應一個 Whitelabel 錯誤頁面(因為瀏覽器請求頭的 Accept 字段默認以 text/html 開頭),中包含錯誤,HTTP 狀態和異常消息的詳細信息;
-
對於其他客戶端(如 Postman),它將生成 json 響應。
要對其進行自定義,添加 View
解析為 error
要完全替換默認行為,可以實現 ErrorController
並注冊該類型的 Bean
定義,或添加 ErrorAttributes
類型的組件以使用現有機制但替換其內容。
10.2 定制異常處理邏輯
error/404.html、error/5xx.html:有精確的錯誤狀態碼頁面就匹配精確,沒有就找 4xx.html,如果都沒有就觸發白頁。
自定義錯誤頁:
-
@ControllerAdvice
+@ExceptionHandler
處理全局異常,底層是ExceptionHandlerExceptionResolver
支持的。 -
自定義實現
HandlerExceptionResolver
處理異常,可以作為默認的全局異常處理規則。 -
ErrorViewResolver
實現自定義異常處理。
- 自己調用
response.sendError(statusCode, resolvedReason)
,error 請求就會轉給BasicErrorController
。 - 如果自己沒有調用,並且異常沒有處理器能處理,Tomcat 底層調用
response.sendError(statusCode, resolvedReason)
,error 請求就會轉給BasicErrorController
。 BasicErrorController
要去的頁面地址是ErrorViewResolver
。
-
@ResponseStatus
+ 自定義異常底層是
ResponseStatusExceptionResolver
,被ResponseStatus
注解的元素會使Tomcat 底層調用response.sendError(statusCode, resolvedReason)
。response.sendError(HttpServletResponse.SC_BAD_REQUEST, ex.getMessage());
10.3 異常處理自動配置原理
-
ErrorMvcAutoConfiguration
自動配置異常處理規則。-
定義錯誤頁面屬性:組件類型為
DefaultErrorAttributes
,組件 ID 為errorAttributes
。public class DefaultErrorAttributes implements ErrorAttributes, HandlerExceptionResolver
。
-
定義頁面跳轉邏輯:組件類型為
BasicErrorController
,組件 ID 為basicErrorController
(json + 白頁 適配響應)。-
處理默認 /error 路徑的請求,頁面響應
new ModelAndView("error", model)
。@Controller @RequestMapping("${server.error.path:${error.path:/error}}") public class BasicErrorController extends AbstractErrorController { // 產生 html 類型的數據,瀏覽器發送的請求來到這個方法處理 @RequestMapping(produces = "text/html") public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) { HttpStatus status = getStatus(request); Map<String, Object> model = Collections.unmodifiableMap(getErrorAttributes( request, isIncludeStackTrace(request, MediaType.TEXT_HTML))); response.setStatus(status.value()); // 去哪個頁面作為錯誤頁面,包含頁面地址和頁面內容 ModelAndView modelAndView = resolveErrorView(request, response, status, model); return (modelAndView == null ? new ModelAndView("error", model) : modelAndView); } // 產生 json 數據,其他客戶端來到這個方法處理 @RequestMapping @ResponseBody public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) { Map<String, Object> body = getErrorAttributes(request, isIncludeStackTrace(request, MediaType.ALL)); HttpStatus status = getStatus(request); return new ResponseEntity<Map<String, Object>>(body, status); }
-
容器中有組件
View
,組件 ID 為error
(響應默認錯誤頁) -
容器中放組件
BeanNameViewResolver
(視圖解析器),按照返回的視圖名作為組件 ID 去容器中找View
對象。
-
-
定義錯誤頁面 html 路徑:類型為
DefaultErrorViewResolver
,組件 ID 為conventionErrorViewResolver
。- 如果發生錯誤,會以 HTTP 狀態碼作為視圖頁地址(
viewName
),找到真正的頁面。 - error/404.html、error/5xx.html。
- 如果發生錯誤,會以 HTTP 狀態碼作為視圖頁地址(
-
10.4 異常處理流程
-
執行目標方法,目標方法運行期間有任何異常都會被
catch
捕捉,而且標志當前請求結束,並且將異常賦值給dispatchException
。 -
進入視圖解析流程:
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
-
mv = processHandlerException
處理 handler 發生的異常,處理完成返回ModelAndView
。-
遍歷所有的
handlerExceptionResolvers
處理器異常解析器,看誰能處理當前異常。 -
系統默認的異常解析器:
DefaultErrorAttributes
先來處理異常,把異常信息保存到 request 域,並且返回null
;- 然后交由
HandlerExceptionResolverComposite
,遍歷其中的異常處理器,默認沒有處理器能處理異常,所以異常會被拋出。- 如果異常沒有處理器能處理,最終 Tomcat 底層 就會發送
/error
請求,會被底層的BasicErrorController
處理。 - 解析錯誤視圖:遍歷所有的
ErrorViewResolver
看誰能解析。 - 默認的
DefaultErrorViewResolver
,作用是把響應狀態碼作為錯誤頁的地址,error/500.html 。 - 模板引擎最終響應這個頁面 error/500.html。
- 如果異常沒有處理器能處理,最終 Tomcat 底層 就會發送
-
11、Web 原生組件注入(Servlet
、Filter
、Listener
)
11.1 使用 Servlet API
@ServletComponentScan(basePackages = "com.atguigu.admin")
:指定原生Servlet
組件都放在哪里。@WebServlet(urlPatterns = "/my")
:效果,直接響應,沒有經過 Spring 的攔截器?@WebFilter(urlPatterns={"/css/*", "/images/*"})
:過濾 css、images 靜態資源。@WebListener
:監聽器。
擴展:DispatchServlet
如何注冊進來?
-
容器中自動配置了
DispatcherServlet
屬性綁定到WebMvcProperties
,對應的配置文件配置項是spring.mvc
。 -
通過
ServletRegistrationBean<DispatcherServlet>
把DispatcherServlet
配置進來。 -
默認映射的是
/
路徑。
多個 Servlet
都能處理到同一層路徑,精確優選原則:
A: /my/
B: /my/1 優先
11.2 使用 RegistrationBean
ServletRegistrationBean
, FilterRegistrationBean
, and ServletListenerRegistrationBean
@Configuration
public class MyRegistConfig {
@Bean
public ServletRegistrationBean myServlet(){
MyServlet myServlet = new MyServlet();
return new ServletRegistrationBean(myServlet,"/my","/my02");
}
@Bean
public FilterRegistrationBean myFilter(){
MyFilter myFilter = new MyFilter();
// return new FilterRegistrationBean(myFilter,myServlet());
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/my","/css/*"));
return filterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean myListener(){
MySwervletContextListener mySwervletContextListener = new MySwervletContextListener();
return new ServletListenerRegistrationBean(mySwervletContextListener);
}
}
12、配置 Servlet
容器
12.1 切換嵌入式Servlet容器
SpringBoot 內置的 WebServer:Tomcat
(默認), Jetty
, or Undertow
。
ServletWebServerApplicationContext
容器啟動尋找 ServletWebServerFactory
並引導創建服務器。
切換服務器:在 pom.xml 中排除 Tomcat 依賴,再將目標服務器的 starter 導入即可。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
<exclusions>
<exclusion>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-tomcat</artifactId>
</exclusion>
</exclusions>
</dependency>
原理:
SpringBoot 應用啟動發現當前是 Web 應用,web-starter
自動導入Tomcat:
-
Web 應用會創建一個 Web 版的
IoC
容器ServletWebServerApplicationContext
-
ServletWebServerApplicationContext
啟動的時候尋找ServletWebServerFactory
(Servlet
的 Web 服務器工廠 --->Servlet
的 Web 服務器); -
SpringBoot 底層默認有很多的 WebServer 工廠:
TomcatServletWebServerFactory
,JettyServletWebServerFactory
, orUndertowServletWebServerFactory
; -
底層直接會有一個 WebServer 工廠自動配置類:
ServletWebServerFactoryAutoConfiguration
; -
WebServer 工廠自動配置類
ServletWebServerFactoryAutoConfiguration
導入了ServletWebServerFactoryConfiguration
配置類; -
ServletWebServerFactoryConfiguration
配置類動態判斷系統中到底導入了哪個 Web 服務器的包,web-starter
默認是導入 Tomcat 包,容器中就有TomcatServletWebServerFactory
; -
TomcatServletWebServerFactory
創建 Tomcat 對象,然后將 Tomcat 對象傳遞給TomcatWebServer
,TomcatWebServer
的構造器擁有初始化方法initialize()
,使用this.tomcat.start();
啟動 Tomcat(只要 Tomcat 核心 jar 包存在,內嵌服務器就是手動把啟動服務器的代碼調用)。
12.2 定制 Servlet
容器
- 在 application.properties 中修改和 server 有關的配置(
ServerProperties
提供);
server.port=8081
server.context-path=/crud
server.tomcat.uri-encoding=UTF‐8
# 通用的 Servlet 容器設置
server.xxx
# Tomcat 的設置
server.tomcat.xxx
-
實現
WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>
-
把配置文件的值和
ServletWebServerFactory
進行綁定;@Component public class CustomizationBean implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> { @Override public void customize(ConfigurableServletWebServerFactory server) { server.setPort(9000); } }
-
-
直接自定義
ConfigurableServletWebServerFactory
@Bean public WebServerFactoryCustomizer configWebServer(){ WebServerFactoryCustomizer webServerFactoryCustomizer = new WebServerFactoryCustomizer<ConfigurableServletWebServerFactory>() { @Override public void customize(ConfigurableServletWebServerFactory factory) { factory.setPort(9000); } }; return webServerFactoryCustomizer; }
六、Docker
1、簡介
Docker 是一個開源的應用容器引擎,是一個輕量級容器技術。
Docker 支持將軟件編譯成一個鏡像,然后在鏡像中各種軟件做好配置,將鏡像發布出去,其他使用者可以直接使用這個鏡像。
運行中的這個鏡像稱為容器,容器啟動是非常快速的。
Windows 系統鏡像與 Docker 鏡像:
2、核心概念
-
主機(Host):安裝了 Docker 程序的機器(Docker 直接安裝在 Linux 或 Windows 操作系統之上)。
-
客戶端(Client):連接 Docker 主機進行操作。
-
倉庫(Registry):用來保存各種打包好的軟件鏡像。
-
鏡像(Images):軟件打包好的鏡像,放在 Docker 倉庫中。
-
容器(Container):鏡像啟動后的實例稱為一個容器,容器是獨立運行的一個或一組應用。
使用 Docker 的步驟:
-
安裝 Docker;
-
去 Docker 倉庫獲取軟件對應的鏡像;
-
使用 Docker 運行這個鏡像,這個鏡像就會生成一個 Docker 容器;
-
停止 Docker 容器就是停止軟件。
3、安裝Docker
3.1 安裝 WSL2 版 Linux
3.2 Linux 上安裝 Docker
4、Docker 常用命令
4.1 鏡像操作
操作 | 命令 | 說明 |
---|---|---|
檢索 | docker search 鏡像名 |
我們經常去 Docker Hub 上檢索鏡像的詳細信息,如鏡像的 tag 標簽。 |
拉取 | docker pull 鏡像名:tag |
:tag 是可選的,tag 多為軟件的版本,默認是 latest 最新版。 |
列表 | docker images |
查看所有本地鏡像。 |
刪除 | docker rmi 容器ID |
刪除指定的本地鏡像。 |
4.2 容器操作
軟件鏡像(QQ 安裝程序)-> 運行鏡像(雙擊 QQ 圖標) -> 產生一個容器(正在運行的軟件,如運行的 QQ)。
步驟:
-
搜索鏡像
[root@localhost ~]# docker search tomcat
-
拉取鏡像
[root@localhost ~]# docker pull tomcat
-
根據鏡像啟動容器
[root@localhost ~]# docker run --name myTomcat -d tomcat:latest
-
查看運行中的容器
[root@localhost ~]# docker ps
-
停止運行中的容器
[root@localhost ~]# docker stop 容器ID
-
查看所有的容器
[root@localhost ~]# docker ps -a
-
啟動容器
[root@localhost ~]# docker start 容器ID
-
刪除一個容器
[root@localhost ~]# docker rm 容器ID
-
啟動一個做了端口映射的 Tomcat
[root@localhost ~]# docker run -d -p 8888:8080 tomcat
-d
:后台運行-p
:將主機的端口映射到容器的端口,主機的端口:容器的端口
-
為了演示簡單關閉了
Linux
的防火牆- 查看防火牆狀態:
[root@localhost ~]# service firewalld status
- 關閉防火牆
[root@localhost ~]# service firewalld stop
-
查看容器的日志
[root@localhost ~]# docker logs container-name/container-id
更多命令參考:https://docs.docker.com/engine/reference/commandline/docker/
還可以參考每一個鏡像的文檔。
4.3 安裝 MySQL
拉取鏡像
[root@localhost ~]# docker pull mysql
錯誤的啟動:
[root@localhost ~]# docker run --name mysql01 -d mysql
42f09819908bb72dd99ae19e792e0a5d03c48638421fa64cce5f8ba0f40f5846
# MySQL 退出了
[root@localhost ~]# docker ps -a
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
42f09819908b mysql "docker-entrypoint.sh" 34 seconds ago Exited (1) 33 seconds ago mysql01
538bde63e500 tomcat "catalina.sh run" About an hour ago Exited (143) About an hour ago compassionate_
goldstine
c4f1ac60b3fc tomcat "catalina.sh run" About an hour ago Exited (143) About an hour ago lonely_fermi
81ec743a5271 tomcat "catalina.sh run" About an hour ago Exited (143) About an hour ago sick_ramanujan
# 根據容器 ID 查看錯誤日志,下面三個參數必須指定一個
[root@localhost ~]# docker logs 42f09819908b
error: database is uninitialized and password option is not specified
You need to specify one of MYSQL_ROOT_PASSWORD, MYSQL_ALLOW_EMPTY_PASSWORD and MYSQL_RANDOM_ROOT_PASSWORD。
正確的啟動:
[root@localhost ~]# docker run --name mysql01 -e MYSQL_ROOT_PASSWORD=123456 -d mysql
b874c56bec49fb43024b3805ab51e9097da779f2f572c22c695305dedd684c5f
[root@localhost ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
b874c56bec49 mysql "docker-entrypoint.sh" 4 seconds ago Up 3 seconds 3306/tcp mysql01
端口映射,將主機的端口映射到容器的端口:
[root@localhost ~]# docker run -p 3306:3306 --name mysql02 -e MYSQL_ROOT_PASSWORD=123456 -d mysql
ad10e4bc5c6a0f61cbad43898de71d366117d120e39db651844c0e73863b9434
[root@localhost ~]# docker ps
CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMES
ad10e4bc5c6a mysql "docker-entrypoint.sh" 4 seconds ago Up 2 seconds 0.0.0.0:3306->3306/tcp mysql02
遠程 MySQL 連接不上的解決方法:
[root@localhost ~]# docker run -p 3306:3306 --name mysql01 -e MYSQL_ROOT_PASSWORD=123456 -d mysql
18e7e4fc89685c1fb8fac0c999d41f67bcd1c993b6597b1ecd514a121e132c5b
[root@localhost ~]# docker exec -it mysql01 bash
root@18e7e4fc8968:/# mysql -u root -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.19 MySQL Community Server - GPL
Copyright (c) 2000, 2020, Oracle and/or its affiliates. All rights reserved.
Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.
Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.
mysql> alter user 'root'@'%' identified with mysql_native_password by 'test1234';
Query OK, 0 rows affected (0.02 sec)
mysql> flush privileges;
Query OK, 0 rows affected (0.00 sec)
# Ctrl+D退出
掛載文件夾:
[root@localhost ~]# docker run --name mysql03 -v /conf/mysql:/etc/mysql/conf.d -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag
把主機的 /conf/mysql 文件夾掛載到容器的 /etc/mysql/conf.d 文件夾里面。
修改 MySQL 的配置文件就只需要把 MySQL 配置文件放在主機的 /conf/mysql 文件夾下)。
指定 MySQL 的一些配置參數:
[root@localhost ~]# docker run --name some-mysql -e MYSQL_ROOT_PASSWORD=my-secret-pw -d mysql:tag --character-set-server=utf8mb4 --collation-server=utf8mb4_unicode_ci
4.4 安裝 Redis
七、數據訪問
1、SQL
1.1 數據源的自動配置
SpringBoot 默認是用 HikariDataSource
數據源(數據庫連接池)。
1.1.1 導入 JDBC 場景
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jdbc</artifactId>
</dependency>
為什么需要顯式導入 JDBC 場景,官方不導入數據庫驅動?因為官方不知道我們接下要操作什么數據庫。
數據庫版本和驅動版本對應:
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
<!-- 想要修改版本
1、直接依賴引入具體版本(Maven 的就近依賴原則)
2、重新聲明版本(Maven 的屬性的就近優先原則) -->
<properties>
<java.version>1.8</java.version>
<mysql.version>5.1.49</mysql.version>
</properties>
1.1.2 分析自動配置
自動配置的類:
DataSourceAutoConfiguration
: 數據源的自動配置;- 修改數據源相關的配置:spring.datasource;
- 數據庫連接池的配置,是容器中沒有
DataSource
才自動配置的; - 底層配置好的數據庫連接池是:HikariDataSource。
@Configuration(proxyBeanMethods = false)
@Conditional(PooledDataSourceCondition.class)
@ConditionalOnMissingBean({ DataSource.class, XADataSource.class })
@Import({ DataSourceConfiguration.Hikari.class, DataSourceConfiguration.Tomcat.class,
DataSourceConfiguration.Dbcp2.class, DataSourceConfiguration.OracleUcp.class,
DataSourceConfiguration.Generic.class, DataSourceJmxConfiguration.class })
protected static class PooledDataSourceConfiguration
-
JdbcTemplateAutoConfiguration
:JdbcTemplate
的自動配置,可以來對數據庫進行 CRUD;- 可以修改這個配置項
@ConfigurationProperties(prefix = "spring.jdbc")
來修改JdbcTemplate
; @Bean
、@Primary
修飾JdbcTemplate
,從容器中獲取這個組件。
- 可以修改這個配置項
-
JndiDataSourceAutoConfiguration
: JNDI 的自動配置; -
DataSourceTransactionManagerAutoConfiguration
: 事務管理器的自動配置; -
XADataSourceAutoConfiguration
: 分布式事務相關的自動配置。
1.1.3 修改配置項
spring:
datasource:
url: jdbc:mysql://localhost:3306/db_account?serverTimezone=GMT%2B8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
1.1.4 測試
@Slf4j
@SpringBootTest
class Boot05WebAdminApplicationTests {
@Autowired
JdbcTemplate jdbcTemplate;
@Test
void contextLoads() {
Long aLong = jdbcTemplate.queryForObject("select count(*) from account_tbl", Long.class);
log.info("記錄總數:{}",aLong);
}
}
1.2 使用 Druid 數據源
1.2.1 Druid 官方 Github 地址
https://github.com/alibaba/druid
整合第三方技術的兩種方式
- 自定義;
- 使用官方 starter。
1.2.2 自定義方式
1、創建數據源
這種方式可能無法使用。
<!-- pom.xml -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.17</version>
</dependency>
<!-- bean.xml -->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource"
destroy-method="close">
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
<property name="maxActive" value="20" />
<property name="initialSize" value="1" />
<property name="maxWait" value="60000" />
<property name="minIdle" value="1" />
<property name="timeBetweenEvictionRunsMillis" value="60000" />
<property name="minEvictableIdleTimeMillis" value="300000" />
<property name="testWhileIdle" value="true" />
<property name="testOnBorrow" value="false" />
<property name="testOnReturn" value="false" />
<property name="poolPreparedStatements" value="true" />
<property name="maxOpenPreparedStatements" value="20" />
Spring Boot 2.5.2 版本導入 druid 1.1.17 依賴之后並不能使用監控功能,使用相同版本的 druid-spring-boot-starter 之后可以使用監控功能。
2、StatViewServlet
用途:
- 提供監控信息展示的 HTML 頁面;
- 提供監控信息的 JSON API。
<servlet>
<servlet-name>DruidStatView</servlet-name>
<servlet-class>com.alibaba.druid.support.http.StatViewServlet</servlet-class>
</servlet>
<servlet-mapping>
<servlet-name>DruidStatView</servlet-name>
<url-pattern>/druid/*</url-pattern>
</servlet-mapping>
3、StatFilter
用於統計監控信息:如 SQL 監控、URI 監控。
需要給數據源中配置如下屬性,可以允許多個 Filter,多個用 ,
分割;如:
<property name="filters" value="stat, slf4j" />
系統中的 Filter:
別名 | Filter 類名 |
---|---|
default | com.alibaba.druid.filter.stat.StatFilter |
stat | com.alibaba.druid.filter.stat.StatFilter |
mergeStat | com.alibaba.druid.filter.stat.MergeStatFilter |
encoding | com.alibaba.druid.filter.encoding.EncodingConvertFilter |
log4j | com.alibaba.druid.filter.logging.Log4jFilter |
log4j2 | com.alibaba.druid.filter.logging.Log4j2Filter |
slf4j | com.alibaba.druid.filter.logging.Slf4jLogFilter |
commonlogging | com.alibaba.druid.filter.logging.CommonsLogFilter |
使用 slowSqlMillis
定義慢 SQL 的時長:
<bean id="stat-filter" class="com.alibaba.druid.filter.stat.StatFilter">
<property name="slowSqlMillis" value="10000" />
<property name="logSlowSql" value="true" />
</bean>
1.2.3 使用官方 starter 方式
1、引入 druid-starter
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>
2、分析自動配置
-
擴展配置項 spring.datasource.druid
-
DruidSpringAopConfiguration
.class, 監控 Bean 的配置項:spring.datasource.druid.aop-patterns -
DruidStatViewServletConfiguration
.class,默認開啟監控頁的配置:spring.datasource.druid.stat-view-servlet -
DruidWebStatFilterConfiguration
.class,默認開啟 Web 監控配置;spring.datasource.druid.web-stat-filter -
DruidFilterConfiguration
.class,所有 Druid 自己 Filter 的配置。
private static final String FILTER_STAT_PREFIX = "spring.datasource.druid.filter.stat";
private static final String FILTER_CONFIG_PREFIX = "spring.datasource.druid.filter.config";
private static final String FILTER_ENCODING_PREFIX = "spring.datasource.druid.filter.encoding";
private static final String FILTER_SLF4J_PREFIX = "spring.datasource.druid.filter.slf4j";
private static final String FILTER_LOG4J_PREFIX = "spring.datasource.druid.filter.log4j";
private static final String FILTER_LOG4J2_PREFIX = "spring.datasource.druid.filter.log4j2";
private static final String FILTER_COMMONS_LOG_PREFIX = "spring.datasource.druid.filter.commons-log";
private static final String FILTER_WALL_PREFIX = "spring.datasource.druid.filter.wall";
3、配置示例
spring:
datasource:
url: jdbc:mysql://localhost:3306/db_account?serverTimezone=GMT%2B8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
druid:
aop-patterns: com.atguigu.admin.* # 監控 Bean
filters: stat,wall # 底層開啟功能:stat(sql 監控),wall(防火牆)
stat-view-servlet: # 配置監控頁功能
enabled: true
login-username: admin
login-password: admin
resetEnable: false
web-stat-filter: # Web 監控
enabled: true
urlPattern: /*
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*'
filter:
stat: # 對上面 filters 里面的 stat 的詳細配置
slow-sql-millis: 1000
logSlowSql: true
enabled: true
wall:
enabled: true
config:
drop-table-allow: false
SpringBoot 配置示例:
https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter
1.3 整合 MyBatis 操作
引入 MyBatis 的官方 starter:
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.4</version>
</dependency>
1.3.1 配置模式
-
配置 MyBatis 的配置文件,SqlMapConfig.xml(名稱不固定);
-
通過配置文件,加載 MyBatis 運行環境,創建
SqlSessionFactory
會話工廠,SqlSessionFactory
使用單例方式; -
通過
SqlSessionFactory
創建SqlSession
,SqlSession
是一個面向用戶的接口(提供操作數據庫方法),實現對象是線程不安全的,建議sqlSession
應用在方法體內; -
調用
sqlSession
的方法去操作數據,如果需要提交事務,需要執行sqlSession
的commit()
方法; -
釋放資源,關閉
sqlSession
;
自動配置類:
// 表示這是一個 Spring 配置類
@Configuration
// SqlSessionFactory 和 SqlSessionFactoryBean 存在時才生效
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
// DataSource 的 Canidate 注冊到 Spring 容器中時才生效
@ConditionalOnSingleCandidate(DataSource.class)
// 使 MybatisProperties 注解類生效
@EnableConfigurationProperties({MybatisProperties.class})
// 在 DataSourceAutoConfiguration 和 MybatisLanguageDriverAutoConfiguration 自動配置之后執行
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class})
public class MybatisAutoConfiguration implements InitializingBean {
@ConfigurationProperties(prefix = "mybatis")
public class MybatisProperties
-
@Import(AutoConfiguredMapperScannerRegistrar.class)
; -
Mapper
: 我們只要寫操作MyBatis
的接口,@Mapper
就會被自動掃描進來。
修改配置文件:
mybatis:
config-location: classpath:mybatis/mybatis-config.xml # 全局配置文件位置
mapper-locations: classpath:mybatis/mapper/*.xml # sql 映射文件位置
Mapper
接口綁定 xml:
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.atguigu.admin.mapper.AccountMapper">
<!-- public Account getAcct(Long id); -->
<select id="getAcct" resultType="com.atguigu.admin.bean.Account">
select * from account_tbl where id = #{id}
</select>
</mapper>
配置 MyBatis 規則:
mybatis:
# config-location: classpath:mybatis/mybatis-config.xml
mapper-locations: classpath:mybatis/mapper/*.xml
configuration:
map-underscore-to-camel-case: true
步驟:
- 導入 MyBatis 官方 starter;
- 編寫
Mapper
接口,標准@Mapper
注解; - 編寫 sql 映射文件並綁定
Mapper
接口; - 在 **application.yaml **中指定
Mapper
配置文件的位置,以及指定全局配置文件的信息 (建議配置在 mybatis.configuration)。
1.3.2 注解模式
@Mapper
public interface CityMapper {
@Select("select * from city where id=#{id}")
public City getById(Long id);
public void insert(City city);
}
1.3.3 混合模式
@Mapper
public interface CityMapper {
@Select("select * from city where id=#{id}")
public City getById(Long id);
public void insert(City city);
}
1.3.4 最佳實踐
- 引入 mybatis-starter;
- 配置 application.yaml 中,指定 mapper-location 位置即可;
- 編寫
Mapper
接口並標注@Mapper
注解; - 簡單方法直接注解方式;
- 復雜方法編寫 mapper.xml 進行綁定映射;
@MapperScan("com.atguigu.admin.mapper")
標注在主應用類上,其他的接口就可以不用標注@Mapper
注解。
1.4 整合 MyBatis-Plus 完成 CRUD
1.4.1 什么是 MyBatis-Plus
MyBatis-Plus(簡稱 MP)是一個 MyBatis 的增強工具,在 MyBatis 的基礎上只做增強不做改變,為簡化開發、提高效率而生。
建議安裝 MybatisX 插件。
1.4.2 整合 MyBatis-Plus
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
自動配置:
-
MybatisPlusAutoConfiguration
配置類,MybatisPlusProperties
配置項綁定; -
SqlSessionFactory
是自動配置好的,底層是容器中默認的數據源; -
mapperLocations
是自動配置好的,有默認值classpath*:/mapper/**/*.xml
,任意包的類路徑下的所有 mapper 徑下的所有 xml 都是 sql 映射文件。 -
容器中也自動配置好了
SqlSessionTemplate
; -
@Mapper
標注的接口也會被自動掃描,建議直接@MapperScan("com.atguigu.admin.mapper")
批量掃描就行。
優點:只需要我們的 Mapper
繼承 BaseMapper
就可以擁有 CRUD 能力。
1.4.3 CRUD 功能
@GetMapping("/user/delete/{id}")
public String deleteUser(@PathVariable("id") Long id,
@RequestParam(value = "pn",defaultValue = "1") Integer pn,
RedirectAttributes ra){
userService.removeById(id);
ra.addAttribute("pn", pn);
return "redirect:/dynamic_table";
}
@GetMapping("/dynamic_table")
public String dynamic_table(@RequestParam(value="pn",defaultValue = "1") Integer pn,Model model){
// 表格內容的遍歷
// response.sendError
// List<User> users = Arrays.asList(new User("zhangsan", "123456"),
// new User("lisi", "123444"),
// new User("haha", "aaaaa"),
// new User("hehe ", "aaddd"));
// model.addAttribute("users",users);
//
// if(users.size()>3){
// throw new UserTooManyException();
// }
// 從數據庫中查出user表中的用戶進行展示
// 構造分頁參數
Page<User> page = new Page<>(pn, 2);
// 調用page進行分頁
Page<User> userPage = userService.page(page, null);
// userPage.getRecords()
// userPage.getCurrent()
// userPage.getPages()
model.addAttribute("users",userPage);
return "table/dynamic_table";
}
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements UserService {}
public interface UserService extends IService<User> {}
2、NoSQL
Redis 是一個開源(BSD 許可)的,內存中的數據結構存儲系統,它可以用作數據庫、緩存和消息中間件。 它支持多種類型的數據結構,如 字符串(strings), 散列(hashes), 列表(lists), 集合(sets), 有序集合(sorted sets) 與范圍查詢, bitmaps,基數統計(hyperloglogs) 和 地理空間(geospatial) 索引半徑查詢。 Redis 內置了 復制(replication),LUA腳本(Lua scripting), LRU驅動事件(LRU eviction),事務(transactions) 和不同級別的 磁盤持久化(persistence), 並通過 Redis哨兵(Sentinel)和自動 分區(Cluster)提供高可用性(High Availability)。
2.1 Redis 自動配置
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
自動配置:
-
RedisAutoConfiguration
自動配置類,RedisProperties
屬性類; -
連接工廠是准備好的,
LettuceConnectionConfiguration
、JedisConnectionConfiguration
; -
自動注入了
RedisTemplate<Object, Object>
; -
自動注入了
StringRedisTemplate
,鍵值都是String
; -
我們只要使用
StringRedisTemplate
、RedisTemplate
就可以操作 Redis。
2.2 RedisTemplate 使用
@Test
void testRedis(){
ValueOperations<String, String> operations = redisTemplate.opsForValue();
operations.set("hello","world");
String hello = operations.get("hello");
System.out.println(hello);
}
2.3 切換至 Jedis
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 導入 Jedis-->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
redis:
host: 172.20.134.231
port: 6379
client-type: jedis
jedis:
pool:
max-active: 10
八、單元測試
1、JUnit5 的變化
SpringBoot 2.2.0 版本開始引入 JUnit5 作為單元測試默認庫。
作為最新版本的 JUnit 框架,JUnit5 與之前版本的 Junit 框架有很大的不同。由三個不同子項目的幾個不同模塊組成。
JUnit5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
-
JUnit Platform:在 JVM 上啟動測試框架的基礎,不僅支持 Junit 自制的測試引擎,其他測試引擎也都可以接入。
-
JUnit Jupiter:提供了 JUnit5 的新的編程模型,是 JUnit5 新特性的核心。內部包含了一個測試引擎,用於在 Junit Platform 上運行。
-
JUnit Vintage:由於 JUint 已經發展多年,為了照顧老的項目,JUnit Vintage 提供了兼容 JUnit4.x、Junit3.x 的測試引擎。
JUnit5:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
@SpringBootTest
class Boot05WebAdminApplicationTests {
@Test // 注意這里可以沒有 public
void contextLoads() {
}
}
JUnit4:
SpringBoot 2.4 以上版本移除了默認對 Vintage 的依賴,如果需要兼容 JUnit4 需要自行引入。
<dependency>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.hamcrest</groupId>
<artifactId>hamcrest-core</artifactId>
</exclusion>
</exclusions>
</dependency>
@RunWith(SpringRunner.class)
@SpringBootTest // 如果啟動報錯,則需要指定啟動類的 class
class Boot05WebAdminApplicationTests {
@Test
public void contextLoads() {
}
}
SpringBoot 整合 JUnit 以后。
- 編寫測試方法:
@Test
標注(注意需要使用 JUnit5 版本的注解); - JUnit 類具有 Spring 的功能,
@Autowired
、比如@Transactional
標注測試方法,測試完成后自動回滾。
2、JUnit5 常用注解
JUnit5的注解與JUnit4的注解有所變化:
https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations
-
@Test
:表示方法是測試方法。但是與 JUnit4 的@Test
不同,他的職責非常單一不能聲明任何屬性,拓展的測試將會由 Jupiter 提供額外測試; -
@ParameterizedTest
:表示方法是參數化測試,下方會有詳細介紹; -
@RepeatedTest
:表示方法可重復執行,下方會有詳細介紹; -
@DisplayName
:為測試類或者測試方法設置展示名稱; -
@BeforeEach
:表示在每個單元測試之前執行; -
@AfterEach
:表示在每個單元測試之后執行; -
@BeforeAll
:表示在所有單元測試之前執行; -
@AfterAll
:表示在所有單元測試之后執行; -
@Tag
:表示單元測試類別,類似於 JUnit4 中的@Categories
; -
@Disabled
:表示測試類或測試方法不執行,類似於 JUnit4 中的@Ignore
; -
@Timeout
:表示測試方法運行如果超過了指定時間將會返回錯誤; -
@ExtendWith
:為測試類或測試方法提供擴展類引用。
import org.junit.jupiter.api.Test; // 注意這里使用的是 jupiter 的 Test 注解
@SpringBootTest
public class TestDemo {
@Test
@DisplayName("第一次測試")
public void firstTest() {
System.out.println("hello world");
}
3、斷言(Assertions)
斷言(Assertions)是測試方法中的核心部分,用來對測試需要滿足的條件進行驗證。這些斷言方法都是 org.junit.jupiter.api.Assertions
的靜態方法。JUnit5 內置的斷言可以分成如下幾個類別:
3.1 簡單斷言
用來對單個值進行簡單的驗證,如:
方法 | 說明 |
---|---|
assertEquals |
判斷兩個對象或兩個原始類型是否相等 |
assertNotEquals |
判斷兩個對象或兩個原始類型是否不相等 |
assertSame |
判斷兩個對象引用是否指向同一個對象 |
assertNotSame |
判斷兩個對象引用是否指向不同的對象 |
assertTrue |
判斷給定的布爾值是否為 true |
assertFalse |
判斷給定的布爾值是否為 false |
assertNull |
判斷給定的對象引用是否為 null |
assertNotNull |
判斷給定的對象引用是否不為 null |
@Test
@DisplayName("simple assertion")
public void simple() {
assertEquals(3, 1 + 2, "simple math");
assertNotEquals(3, 1 + 1);
assertNotSame(new Object(), new Object());
Object obj = new Object();
assertSame(obj, obj);
assertFalse(1 > 2);
assertTrue(1 < 2);
assertNull(null);
assertNotNull(new Object());
}
3.2 數組斷言
通過 assertArrayEquals
方法來判斷兩個對象或原始類型的數組內容是否相等:
@Test
@DisplayName("array assertion")
public void array() {
assertArrayEquals(new int[]{1, 2}, new int[] {1, 2});
}
3.3 組合斷言
assertAll
方法接受多個 org.junit.jupiter.api.Executable
函數式接口的實例作為要驗證的斷言,可以通過 lambda
表達式很容易的提供這些斷言:
@Test
@DisplayName("assert all")
public void all() {
assertAll("Math",
() -> assertEquals(2, 1 + 1),
() -> assertTrue(1 > 0)
);
}
3.4 異常斷言
在 JUnit4 時期,想要測試方法的異常情況時,需要用 @Rule
注解的 ExpectedException
變量還是比較麻煩的。而 JUnit5 提供了一種新的斷言方式 Assertions.assertThrows()
,配合函數式編程就可以進行使用:
@Test
@DisplayName("異常測試")
public void exceptionTest() { // 扔出斷言異常
ArithmeticException exception = Assertions.assertThrows(
ArithmeticException.class, () -> System.out.println(1 % 0));
}
3.5 超時斷言
Junit5 還提供了 Assertions.assertTimeout()
為測試方法設置了超時時間:
@Test
@DisplayName("超時測試")
public void timeoutTest() {
// 如果測試方法時間超過 1s 將會異常
Assertions.assertTimeout(Duration.ofMillis(1000), () -> Thread.sleep(500));
}
3.6 快速失敗
通過 fail
方法直接使得測試失敗:
@Test
@DisplayName("fail")
public void shouldFail() {
fail("This should fail");
}
4、前置條件(Assumptions)
JUnit 5 中的前置條件(Assumptions【假設】)類似於斷言,不同之處在於不滿足的斷言會使得測試方法失敗,而不滿足的前置條件只會使得測試方法的執行終止。前置條件可以看成是測試方法執行的前提,當該前提不滿足時,就沒有繼續執行的必要。
@DisplayName("前置條件")
public class AssumptionsTest {
private final String environment = "DEV";
@Test
@DisplayName("simple")
public void simpleAssume() {
assumeTrue(Objects.equals(this.environment, "DEV"));
assumeFalse(() -> Objects.equals(this.environment, "PROD"));
}
@Test
@DisplayName("assume then do")
public void assumeThenDo() {
assumingThat(
Objects.equals(this.environment, "DEV"),
() -> System.out.println("In DEV")
);
}
}
assumeTrue
和 assumFalse
確保給定的條件為 true
或 false
,不滿足條件會使得測試執行終止。assumingThat
的參數是表示條件的布爾值和對應的 Executable
接口的實現對象。只有條件滿足時,Executable
對象才會被執行;當條件不滿足時,測試執行並不會終止。
5、嵌套測試
JUnit5 可以通過 Java 中的內部類和 @Nested
注解實現嵌套測試,從而可以更好的把相關的測試方法組織在一起。在內部類中可以使用 @BeforeEach
和 @AfterEach
注解,而且嵌套的層次沒有限制。
內層的 test 可以驅動外層的 Before(After)Each/All 之類的方法提前/延后運行,外層的不能驅動內層的。
@DisplayName("A stack")
class TestingAStackDemo {
Stack<Object> stack;
@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
}
@Nested
@DisplayName("when new")
class WhenNew {
@BeforeEach
void createNewStack() {
stack = new Stack<>();
}
@Test
@DisplayName("is empty")
void isEmpty() {
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("throws EmptyStackException when popped")
void throwsExceptionWhenPopped() {
assertThrows(EmptyStackException.class, stack::pop);
}
@Test
@DisplayName("throws EmptyStackException when peeked")
void throwsExceptionWhenPeeked() {
assertThrows(EmptyStackException.class, stack::peek);
}
@Nested
@DisplayName("after pushing an element")
class AfterPushing {
String anElement = "an element";
@BeforeEach
void pushAnElement() {
stack.push(anElement);
}
@Test
@DisplayName("it is no longer empty")
void isNotEmpty() {
assertFalse(stack.isEmpty());
}
@Test
@DisplayName("returns the element when popped and is empty")
void returnElementWhenPopped() {
assertEquals(anElement, stack.pop());
assertTrue(stack.isEmpty());
}
@Test
@DisplayName("returns the element when peeked but remains not empty")
void returnElementWhenPeeked() {
assertEquals(anElement, stack.peek());
assertFalse(stack.isEmpty());
}
}
}
}
6、參數化測試
參數化測試是 JUnit5 很重要的一個新特性,它使得用不同的參數多次運行測試成為了可能,也為我們的單元測試帶來許多便利。
利用 @ValueSource
等注解,指定入參,我們將可以使用不同的參數進行多次單元測試,而不需要每新增一個參數就新增一個單元測試,省去了很多冗余代碼。
-
@ValueSource
:為參數化測試指定入參來源,支持八大基礎類以及String
類型,Class
類型; -
@NullSource
:表示為參數化測試提供一個null
的入參; -
@EnumSource
:表示為參數化測試提供一個枚舉入參; -
@CsvFileSource
:表示讀取指定 CSV 文件內容作為參數化測試入參; -
@MethodSource
:表示讀取指定靜態方法的返回值作為參數化測試入參,注意方法返回需要是一個Stream
。
當然如果參數化測試僅僅只能做到指定普通的入參,還達不到讓我覺得驚艷的地步。讓我真正感到他的強大之處的地方在於他可以支持外部的各類入參。如 CSV、YAML、JSON 文件甚至方法的返回值也可以作為入參。只需要去實現 ArgumentsProvider
接口,任何外部文件都可以作為它的入參。
@ParameterizedTest
@ValueSource(strings = {"one", "two", "three"})
@DisplayName("參數化測試1")
public void parameterizedTest1(String string) {
System.out.println(string);
Assertions.assertTrue(StringUtils.isNotBlank(string));
}
@ParameterizedTest
@MethodSource("method") // 指定方法名
@DisplayName("方法來源參數")
public void testWithExplicitLocalMethodSource(String name) {
System.out.println(name);
Assertions.assertNotNull(name);
}
static Stream<String> method() {
return Stream.of("apple", "banana");
}
7、遷移指南
在進行遷移的時候需要注意如下的變化:
-
注解在 org.junit.jupiter.api 包中,斷言在 org.junit.jupiter.api.
Assertions
類中,前置條件在 org.junit.jupiter.api.``Assumptions` 類中; -
把
@Before
和@After
替換成@BeforeEach
和@AfterEach
; -
把
@BeforeClass
和@AfterClass
替換成@BeforeAll
和@AfterAll
; -
把
@Ignore
替換成@Disabled
; -
把
@Category
替換成@Tag
; -
把
@RunWith
、@Rule
和@ClassRule
替換成@ExtendWith
。
九、指標監控
1、SpringBoot Actuator
1.1 簡介
未來每一個微服務在雲上部署以后,我們都需要對其進行監控、追蹤、審計、控制等。SpringBoot 就抽取了 Actuator 場景,使得我們每個微服務快速引用即可獲得生產級別的應用監控、審計等功能。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
1.2 1.x 與 2.x 的不同
1.3 如何使用
-
引入場景 starter;
-
暴露所有監控信息為 HTTP;
management:
endpoints:
enabled-by-default: true # 暴露所有端點信息
web:
exposure:
include: '*' # 以 Web 方式暴露
- 測試。
http://localhost:8080/actuator/beans
http://localhost:8080/actuator/configprops
http://localhost:8080/actuator/metrics
http://localhost:8080/actuator/metrics/jvm.gc.pause
http://localhost:8080/actuator/endpointName/detailPath
1.4 可視化
https://github.com/codecentric/spring-boot-admin
2、Actuator Endpoint
2.1 最常使用的 Endpoint
ID | 描述 |
---|---|
auditevents |
暴露當前應用程序的審核事件信息。需要一個 AuditEventRepository 組件。 |
beans |
顯示應用程序中所有 Bean 的完整列表。 |
caches |
暴露可用的緩存。 |
conditions |
顯示自動配置的所有條件信息,包括匹配或不匹配的原因。 |
configprops |
顯示所有 @ConfigurationProperties 。 |
env |
暴露 Spring 的屬性 ConfigurableEnvironment 。 |
flyway |
顯示已應用的所有 Flyway 數據庫遷移, 需要一個或多個 Flyway 組件。 |
health |
顯示應用程序運行狀況信息。 |
httptrace |
顯示 HTTP 跟蹤信息(默認情況下,最近 100 個 HTTP 請求-響應)。需要一個 HttpTraceRepository 組件。 |
info |
顯示應用程序信息。 |
integrationgraph |
顯示Spring integrationgraph 。需要依賴 spring-integration-core 。 |
loggers |
顯示和修改應用程序中日志的配置。 |
liquibase |
顯示已應用的所有 Liquibase 數據庫遷移。需要一個或多個Liquibase 組件。 |
metrics |
顯示當前應用程序的指標信息。 |
mappings |
顯示所有 @RequestMapping 路徑列表。 |
scheduledtasks |
顯示應用程序中的計划任務。 |
sessions |
允許從 Spring Session 支持的會話存儲中檢索和刪除用戶會話。需要使用 Spring Session 的基於 Servlet 的 Web 應用程序。 |
shutdown |
使應用程序正常關閉,默認禁用。 |
startup |
顯示由 ApplicationStartup 收集的啟動步驟數據。需要使用 SpringApplication 進行配置 BufferingApplicationStartup 。 |
threaddump |
執行線程轉儲。 |
如果您的應用程序是 Web 應用程序(Spring MVC,Spring WebFlux 或 Jersey),則可以使用以下附加端點:
ID | 描述 |
---|---|
heapdump |
返回 hprof 堆轉儲文件。 |
jolokia |
通過 HTTP 暴露 JMX Bean(需要引入 Jolokia,不適用於 WebFlux)。需要引入依賴 jolokia-core 。 |
logfile |
返回日志文件的內容(如果已設置 logging.file.name 或 logging.file.path 屬性)。支持使用 HTTP Range 標頭來檢索部分日志文件的內容。 |
prometheus |
以 Prometheus 服務器可以抓取的格式公開指標。需要依賴 micrometer-registry-prometheus 。 |
最常用的 Endpoint:
-
Health
:監控狀況 -
Metrics
:運行時指標 -
Loggers
:日志記錄
2.2 Health Endpoint
健康檢查端點,我們一般用於在雲平台,平台會定時的檢查應用的健康狀況,我們就需要 Health Endpoint 可以為平台返回當前應用的一系列組件健康狀況的集合。
重要的幾點:
-
Health Endpoint 返回的結果,應該是一系列健康檢查后的一個匯總報告;
-
很多的健康檢查默認已經自動配置好了,比如數據庫、Redis 等;
-
可以很容易的添加自定義的健康檢查機制。
2.3 Metrics Endpoint
提供詳細的、層級的、空間指標信息,這些信息可以被 pull
(主動推送)或者 push
(被動獲取)方式得到;
-
通過 Metrics 對接多種監控系統;
-
簡化核心 Metrics 開發;
-
添加自定義 Metrics 或者擴展已有 Metrics。
2.4 管理 Endpoints
2.4.1 開啟與禁用 Endpoints
- 默認所有的 Endpoint 除了
shutdown
都是開啟的; - 需要開啟或者禁用某個 Endpoint。配置模式為
management.endpoint.<endpointName>.enabled = true
;
management:
endpoint:
beans:
enabled: true
- 或者禁用所有的 Endpoint 然后手動開啟指定的 Endpoint。
management:
endpoints:
enabled-by-default: false
endpoint:
beans:
enabled: true
health:
enabled: true
2.4.2 暴露 Endpoints
支持的暴露方式
-
HTTP:默認只暴露 Health 和Info Endpoint;
-
JMX:默認暴露所有 Endpoint,JConsole 就是通過 JMX 實現的;
-
除了 Health 和 Info,剩下的 Endpoint 都應該進行保護訪問。如果引入
SpringSecurity
,則會默認配置安全訪問規則。
ID | JMX | Web |
---|---|---|
auditevents |
Yes | No |
beans |
Yes | No |
caches |
Yes | No |
conditions |
Yes | No |
configprops |
Yes | No |
env |
Yes | No |
flyway |
Yes | No |
health |
Yes | Yes |
heapdump |
N/A | No |
httptrace |
Yes | No |
info |
Yes | Yes |
integrationgraph |
Yes | No |
jolokia |
N/A | No |
logfile |
N/A | No |
loggers |
Yes | No |
liquibase |
Yes | No |
metrics |
Yes | No |
mappings |
Yes | No |
prometheus |
N/A | No |
scheduledtasks |
Yes | No |
sessions |
Yes | No |
shutdown |
Yes | No |
startup |
Yes | No |
threaddump |
Yes | No |
3、定制 Endpoint
3.1 定制 Health 信息
@Component
public class MyHealthIndicator implements HealthIndicator {
@Override
public Health health() {
int errorCode = check(); // perform some specific health check
if (errorCode != 0) {
return Health.down().withDetail("Error Code", errorCode).build();
}
return Health.up().build();
}
}
// 構建 Health
Health build = Health.down()
.withDetail("msg", "error service")
.withDetail("code", "500")
.withException(new RuntimeException())
.build();
management:
health:
enabled: true
show-details: always # 總是顯示詳細信息,可顯示每個模塊的狀態信息
@Component
public class MyComHealthIndicator extends AbstractHealthIndicator {
// 真實的檢查方法
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
// MongoDB,獲取連接進行測試
Map<String,Object> map = new HashMap<>();
// 檢查完成
if(1 == 2){
// builder.up(); // 健康
builder.status(Status.UP);
map.put("count",1);
map.put("ms",100);
}else {
// builder.down();
builder.status(Status.OUT_OF_SERVICE);
map.put("err","連接超時");
map.put("ms",3000);
}
builder.withDetail("code",100).withDetails(map);
}
}
3.2 定制info信息
常用兩種方式:
3.2.1 編寫配置文件
info:
appName: boot-admin
version: 2.0.1
mavenProjectName: @project.artifactId@ #使用@@可以獲取maven的pom文件值
mavenProjectVersion: @project.version@
3.2.2 編寫 InfoContributor
@Component
public class ExampleInfoContributor implements InfoContributor {
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("example",
Collections.singletonMap("key", "value"));
}
}
http://localhost:8080/actuator/info:會輸出以上方式返回的所有 Info 信息。
3.3 定制 Metrics 信息
3.3.1 SpringBoot 支持自動適配的 Metrics
-
JVM metrics, report utilization of:
-
Various memory and buffer pools
-
Statistics related to garbage collection
-
Threads utilization
-
Number of classes loaded/unloaded
-
CPU metrics
-
File descriptor metrics
-
Kafka consumer and producer metrics
-
Log4j2 metrics: record the number of events logged to Log4j2 at each level
-
Logback metrics: record the number of events logged to Logback at each level
-
Uptime metrics: report a gauge for uptime and a fixed gauge representing the application’s absolute start time
-
Tomcat metrics (
server.tomcat.mbeanregistry.enabled
must be set totrue
for all Tomcat metrics to be registered) -
Spring Integration metrics
3.2.2 增加定制 Metrics
class MyService{
Counter counter;
public MyService(MeterRegistry meterRegistry){
counter = meterRegistry.counter("myservice.method.running.counter");
}
public void hello() {
counter.increment();
}
}
// 也可以使用下面的方式
@Bean
MeterBinder queueSize(Queue queue) {
return (registry) -> Gauge.builder("queueSize", queue::size).register(registry);
}
3.4定制 Endpoint
@Component
@Endpoint(id = "container")
public class DockerEndpoint {
@ReadOperation
public Map getDockerInfo(){
return Collections.singletonMap("info","docker started...");
}
@WriteOperation
private void restartDocker(){
System.out.println("docker restarted....");
}
}
場景:開發 ReadinessEndpoint 來管理程序是否就緒,或者 **Liveness Endpoint **來管理程序是否存活。
十、原理解析
10.1 Profile 功能
為了方便多環境適配,SpringBoot 簡化了 Profile 功能。
10.1.1 application-profile 功能
-
默認配置文件 application.yaml 任何時候都會加載;
-
環境配置文件 application-{env}.yaml;
-
激活指定環境;
- 配置文件激活;
- 命令行激活:``java -jar xxx.jar --spring.profiles.active=prod --person.name=haha`。
- 優先級:命令行 > 環境配置文件 > 默認配置文件
-
默認配置與環境配置同時生效;
-
同名配置項,Profile 配置優先;
10.1.2 @Profile
條件裝配功能
@Configuration(proxyBeanMethods = false)
@Profile("production")
public class ProductionConfiguration {
// ...
}
10.1.3 Profile 分組
spring.profiles.group.production[0]=proddb
spring.profiles.group.production[1]=prodmq
# 使用:--spring.profiles.active=production 激活
10.2 外部化配置
Spring Boot uses a very particular PropertySource
order that is designed to allow sensible overriding of values. Properties are considered in the following order (with values from lower items overriding earlier ones):
- Default properties (specified by setting
SpringApplication.setDefaultProperties
). @PropertySource
annotations on your@Configuration
classes. Please note that such property sources are not added to theEnvironment
until the application context is being refreshed. This is too late to configure certain properties such aslogging.*
andspring.main.*
which are read before refresh begins.- Config data (such as
application.properties
files) - A
RandomValuePropertySource
that has properties only inrandom.*
. - OS environment variables.
- Java System properties (
System.getProperties()
). - JNDI attributes from
java:comp/env
. ServletContext
init parameters.ServletConfig
init parameters.- Properties from
SPRING_APPLICATION_JSON
(inline JSON embedded in an environment variable or system property). - Command line arguments.
properties
attribute on your tests. Available on@SpringBootTest
and the test annotations for testing a particular slice of your application.@TestPropertySource
annotations on your tests.- Devtools global settings properties in the
$HOME/.config/spring-boot
directory when devtools is active.
Config data files are considered in the following order:
- Application properties packaged inside your jar (
application.properties
and YAML variants). - Profile-specific application properties packaged inside your jar (
application-{profile}.properties
and YAML variants). - Application properties outside of your packaged jar (
application.properties
and YAML variants). - Profile-specific application properties outside of your packaged jar (
application-{profile}.properties
and YAML variants).
10.2.1 外部配置源
常用:Java 屬性文件、環境變量、命令行參數、**YAML **文件。
10.2.2 配置文件查找位置
越往下優先級越高,會覆蓋上面的配置:
- classpath 根路徑;
- classpath 根路徑下 config 目錄;
- jar 包當前目錄;
- jar 包當前目錄的 config 目錄;
- /config 子目錄的直接子目錄。
10.2.3 配置文件加載順序:
- 當前jar包內部的application.properties和application.yml
- 當前jar包內部的application-{profile}.properties 和 application-{profile}.yml
- 引用的外部jar包的application.properties和application.yml
- 引用的外部jar包的application-{profile}.properties 和 application-{profile}.yml
10.2.4 指定環境優先,外部優先,后面的可以覆蓋前面的同名配置項
10.3 自定義 starter
10.3.1 starter 啟動原理
- starter-pom 引入 autoconfigurer 包;
- autoconfigure 包中配置使用 META-INF/spring.factories 中
EnableAutoConfiguration
的值,使得項目啟動時加載指定的自動配置類; - 編寫自動配置類
xxxAutoConfiguration
->xxxxProperties
; - 編寫業務:``@Configuration
、
@Conditional、
@EnableConfigurationProperties、
@Bean`......
引入 starter --- xxxAutoConfiguration
--- 容器中放入組件
---- 綁定 xxxProperties
---- 配置項
10.3.2 自定義 starter
atguigu-hello-spring-boot-starter(啟動器)
atguigu-hello-spring-boot-starter-autoconfigure(自動配置包)
10.4 SpringBoot 原理
Spring原理【Spring注解】、SpringMVC 原理、自動配置原理
10.4.1 SpringBoot 啟動過程
- 創建
SpringApplication
;- 保存一些信息;
- 用工具類
ClassUtils
獲取到應用類型為Servlet
、還可能為 Reactive 或 None; - 去 spring.factories 找
Bootstrapper
;List<Bootstrapper> bootstrappers
- 去 spring.factories 找
ApplicationContextInitializer
;List<ApplicationContextInitializer<?>> initializers
- 去 spring.factories 找
ApplicationListener
List<ApplicationListener<?>> listeners
- 運行
SpringApplication
。StopWatch
用於監聽整個應用的啟動停止;- 記錄應用的啟動時間;
- 調用
createBootstrapContext()
創建引導上下文(Context
環境);- 獲取到所有之前的
bootstrappers
挨個執行intitialize()
來設置引導啟動器上下文環境
- 獲取到所有之前的
- 讓當前應用進入 java.awt.
headless
模式(自力更生模式); - 獲取所有
RunListener
(運行監聽器),為了方便所有Listener
進行事件感知;getSpringFactoriesInstances()
去 spring.factories 找SpringApplicationRunListener
- 遍歷
SpringApplicationRunListener
調用starting()
方法;- 相當於通知所有感興趣系統正在啟動過程的人,項目正在 starting
- 保存命令行參數
ApplicationArguments
; - 准備環境
prepareEnvironment()
;- 返回或者創建基礎環境信息對象
StandardServletEnvironment
; - 配置環境信息對象;
- 讀取所有的配置源的配置屬性值
- 綁定環境信息;
- 監聽器調用 `listener.environmentPrepared() 通知所有的監聽器當前環境准備完成。
- 返回或者創建基礎環境信息對象
- 創建 IOC 容器
createApplicationContext
;- 根據項目類型(
Servlet
)創建容器; - 當前會創建
AnnotationConfigServletWebServerApplicationContext
。
- 根據項目類型(
- 准備
ApplicationContext
容器的基本信息prepareContext()
- 保存環境信息;
- IOC容器的后置處理流程;
- 應用初始化器
applyInitializers()
;- 遍歷所有的
ApplicationContextInitializer
,調用initialize()
來對容器進行初始化擴展功能; - 遍歷所有的
listener
調用contextPrepared()
。通知所有的監聽器EventPublishRunListenr
上下文准備完成。
- 遍歷所有的
- 所有的監聽器調用
contextLoaded()
,通知所有的監聽器上下文加載完成;
- 刷新容器
refreshContext()
;- 創建容器中的所有組件(Spring 注解)
- 容器刷新完成后工作
afterRefresh()
; - 所有監聽器調用
listeners.started(context)
通知所有的監聽器啟動完成; - 調用所有
callRunners()
;- 獲取容器中的
ApplicationRunner
; - 獲取容器中的
CommandLineRunner
(執行數據初始化等操作); - 合並所有 runner 並且按照
@Order
進行排序; - 遍歷所有的 runner 調用
run()
方法。
- 獲取容器中的
- 如果以上有異常;
- 調用
Listener
的failed()
方法通知所有的監聽器 failed
- 調用
- 調用所有監聽器的
listeners.running(context)
通知所有的監聽器正在運行; - running 如果有問題,繼續調調用
Listener
的failed()
方法通知所有的監聽器 failed。
Spring 的核心方法實際調用的是抽象類
ApplicationContext.refresh()
方法。該方法是一種模版方法的設計模式,定義了方法的執行順序和骨架,其中onRefresh()
是一個抽象方法,由具體子類實現。因為啟動的是 Web 版的容器因此執行的是
ServletWebServerApplicationContext.refresh()
方法。該方法從容器中獲得一個ServletWebServerFactory
工廠(如果有多個工廠,則拋異常),根據工廠對象獲得WebServer
對象。因為內嵌 Tomcat,所以是
TomcatWevServer
后續通過new Tomcat()
並執行start()
啟動 Tomcat 服務器。
public interface Bootstrapper {
/**
* Initialize the given {@link BootstrapRegistry} with any required registrations.
* @param registry the registry to initialize
*/
void intitialize(BootstrapRegistry registry);
}
@FunctionalInterface
public interface ApplicationRunner {
/**
* Callback used to run the bean.
* @param args incoming application arguments
* @throws Exception on error
*/
void run(ApplicationArguments args) throws Exception;
}
@FunctionalInterface
public interface CommandLineRunner {
/**
* Callback used to run the bean.
* @param args incoming main method arguments
* @throws Exception on error
*/
void run(String... args) throws Exception;
}
10.4.2 Application Events and Listeners
ApplicationContextInitializer
ApplicationListener
SpringApplicationRunListener