1. SpringBoot 簡介
1.1 Spring能做什麼

1.2 Spring 的生态
官網:https://spring.io/projects/spring-boot
覆寫了:web 開發、資料通路、安全控制、分布式、消息服務、移動開發、批處理…
1.3 Spring 5 重大更新
1.3.1 響應式程式設計
1.3.2 内部源碼設計
基于 java 的版本最低為 jdk1.8
基于 Java8 的一些新特性,如:接口預設實作,重新設計源碼架構
1.4 為什麼用 SpringBoot
SpringBoot makes it easy to create stand-alone,production-grade
Spring based Applications that you can “just run”
能快速建立出生産級别的 Spring 應用
1.4.1 SpringBoot 有點
- Create stand-alone Spring applications
- 建立獨立 Spring 應用
- Embed Tomcat,Jetty or Undertow directly (no need to deploy WAR files)
- 内嵌web 伺服器
- Provide opinionated ‘starter’ dependencies to simplify your build configuration
- 自動 starer 依賴,簡化建構配置
- Automatically configure Spring and 3rd part 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 技術棧的快速開發腳手架
1.4.2 SpringBoot 缺點
版本疊代快,需要時刻關注變化,封裝太深,内部原理複雜,不容易精通
1.5 時代背景
1.5.1 微服務
James Lewis and Martin Fowler (2014) 提出微服務完整概念。
https://martinfowler.com/microservices/
- 微服務是一種架構風格
- 一個應用拆分成一組小型服務
- 每個服務運作在自己的程序内,也就是可獨立部署和更新
- 服務之間使用輕量級 HTTP 互動
- 服務圍繞業務功能拆分
- 可以由全自動部署機制獨立部署
- 去中心化,服務自治。服務可以使用不同的語言、不同的存儲技術
1.5.2 分布式
分布式的困難:
- 遠端調用
- 服務發現
- 負載均衡
- 服務容錯
- 配置管理
- 服務監控
- 鍊路追蹤
- 日志管理
- 任務排程
- …
分布式的解決:
- SpringBoot + SpringCloud
1.6 雲原生
原生應用如何上雲,Cloud Native
上雲的困難
-
服務自愈
加入 a服務在 5 台伺服器上都有,3 台b 伺服器,3 台 c 伺服器,然後全部部署上去,突然後一天 c的一台伺服器當機了,然後c 服務能不能自愈(在别的伺服器又拉起一個 c 服務)?
-
彈性伸縮
突然流量高峰期,a 要調用 b,b 要調用 c,c 部署的少了不夠用,我們希望在 c 不夠用的時候在自動的擴充 3 伺服器,流量高峰期過去後将他們再下架
-
服務隔離
假設 c 再 1 号伺服器部署,然後再 1 号伺服器同時部署的可能有 d,e,f,應該希望當同一台伺服器上的服務某一個出現故障後不會影響别的服務的正常運作
-
自動化部署
整個微服務全部部署不可能手工去部署
-
灰階釋出
某一個服務版本有更新,如果直接将之前的版本替換成新的版本,有可能會出現故障,如果新版本不穩定,那麼整個系統就會壞掉,可以先将 多個伺服器中的舊版本替換為新的,驗證是否能正常運作,經過長時間的驗證沒有出現問題則全部替換為新版本
-
流量治理
b 伺服器性能不高,是以可以通過流量治理手段伺服器隻能接受少量的流量
- …
1.7 SpringBoot 官網
https://docs.spring.io/spring-boot/docs/current/reference/html/
- 檢視版本新特性
2. 第一個 SpringBoot應用
開發環境環境版本要求:
- jdk 1.8
- maven 3.3+
搭建步驟:
- 使用ides 建立一個空的 maven 項目
- 導入依賴
<!-- SpringBoot 的父依賴 -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.2</version>
</parent>
<!-- web 依賴 -->
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>
<!-- maven 打包工具,可以直接打包成jar 包 -->
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
- 建立主程式(啟動類)
/**
* 主程式類
* @SpringBootApplication 表示這是一個 SpringBoot 應用
*/
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class);
}
}
- controller 業務代碼
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello() {
return "Hello SpringBoot2";
}
}
-
運作測試
可以看到端口号預設 8080,可以建立配置檔案,再配置檔案中進行修改:
server.port=8088
3. 自動裝配原理
3.1 SpringBoot 特點
- 父項目做依賴
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.5.2</version>
</parent>
幾乎聲明了所有開發中常用依賴的版本号,自動版本仲裁機制
- 開發導入 starter 場景啟動器
在 pom.xml 中見到很多的 spring-boot-starter-*,隻要引入 starter,這個場景的所有正常需要的依賴都會自動引入
類似于 :*-spring-boot-starter,這種是第三方為我們提供的簡化的依賴
所有的場景啟動器最底層的依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter</artifactId>
<version>2.5.2</version>
<scope>compile</scope>
</dependency>
- 無需關注需要引入依賴的版本号,springboot 會自動進行版本的仲裁
- 引入依賴預設都可以不寫版本号
- 引入非版本仲裁的jar 要加入版本号
- 可以修改預設的版本号
-
可以修改預設的版本号
首先檢視預設配置的版本号使用的方式,然後使用key 進行修改
<properties>
<mysql.version>5.1.4</mysql.version>
</properties>
3.2 自動配置
-
自動配置好了 Tomcat
我們隻需要在配置檔案中設定tomcat 的屬性就可以了
- 自動配置了 SpringMVC 的各個元件
-
自動配置好 Web 常見功能,如:字元編碼問題
在之前 SpringMVC 開發中,需要在 web.xml 中配置
,在 SpirngBoot 中隻需要引入 web 的啟動器就可以了。characterEncodingFilter
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class);
String[] names = run.getBeanDefinitionNames();
for (String name : names) {
System.out.println(name);
}
-
自動配置好包結構
在 SpringMvc中需要在 xml 檔案中設定 componentScan,SpringBoot中無需配置
- 主程式所在包及其下面的所有子包裡面的元件都會被預設掃描(約定的規則)
- 無需以前的包掃描配置
- 要想改變掃描的路徑
或者使用注解@SpringBootApplication(scanBasePackages = "xx.xx.xx")
指定掃描路徑@ComponentScan
- 各種配置擁有預設值
- 例如:tomcat 有預設端口
SpringBoot2 核心知識點 - 例如檔案上傳:MultipartProperties
SpringBoot2 核心知識點 - 預設配置最終都是映射到某個類上,如:MultipartProperties 映射到 multipartProperties 類上
SpringBoot2 核心知識點 - 配置檔案最終會綁定每個類上,這個類會在容器中建立對象
- 按需加載所有自動配置項 Conditional
- 非常多的 starter,pom.xml 中引入那個啟動那個
- 引入了那些場景這個場景的自動配置才會開啟
- SpringBoot 所有的自動配置功能都在
包裡面。spring-boot-autoconfigure
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-autoconfigure</artifactId>
<version>2.5.2</version>
<scope>compile</scope>
</dependency>
3.3 容器功能
3.3.1 元件添加
-
@Configuration
Full 模式與 Lite 模式
配置類元件之間無依賴關系用 Lite 模式加速容器啟動過程,配置類元件之間有依賴關系,方法會調用得到之前單執行個體元件,用 Full 模式
假設有一個 User 類,想要注冊元件
- 在 SpringMVC 情況下需要先建立一個 beans.xml 檔案,然後使用 bean标簽進行配置
<bean id="user1" class="com.example.pojo.User">
<property name="name" value="zhangsan"></property>
</bean>
- 在 SpringBoot 情況下隻需要建立一個配置類加上 @Configuration
/**
* 1. 配置類裡面使用 @Bean 标注在方法上給容器注冊元件,預設也是單執行個體的
* 2. 配置類加上 @Configuration 也是一個元件
* 3. proxyBeanMethods:代理 bean 的方法,如果為 true 外部無論對配置類中的這個元件注冊方法調用多少次
* 擷取的都是之前注冊容器中的單執行個體對象。如果為 true 都會去容器中找元件
* Full(proxyBeanMethods = true)
* Lite(proxyBeanMethods = false) 為 false 元件在容器中不會儲存代理對象,每一次調用都會産生一個新的對象
* 解決元件依賴的場景
* 元件依賴必須使用 Full 模式(預設),其他預設是否 Lite 模式
* 4. 如果是 false ,SpringBoot 不會檢查容器中方法傳回的東西是否存在,提高運作的效率
* 如果是 true,則每次執行都會檢查
* 5. 如果隻是向容器中配置元件,别人也不依賴這個元件則設定成 false
* 如果元件在下面别人還要用就設定為true,保證容器中的元件就是要依賴的元件
*/
@Configuration(proxyBeanMethods = true) // 告訴 SpringBoot 這是一個配置類 == SpringMvc 中的 beans.xml
public class MyConfig {
@Bean // 給容器中添加元件,id 為方法名,傳回類型為元件類型,傳回值就是元件在容器中的執行個體
public User user1() {
return new User("zhangsan");
}
}
-
@Bean、@Component、@Controller、@Service、@Repository
與 SpringMvc 中使用方法一樣
- @ComponentScan、@Import
- @ComponentScan 就是配置包掃描的
-
@Import 給容器中導入一個元件
在容器中元件上面使用
@Import({User.class, DispatcherServlet.class})
@Configuration
public class MyConfig {
}
然後在啟動類中得到 bean 然後輸出檢視
@SpringBootApplication
public class MainApplication {
public static void main(String[] args) {
ConfigurableApplicationContext run = SpringApplication.run(MainApplication.class);
// 因為可能有多個,是以傳回值結果為一個數組類型
String[] beanNamesForType = run.getBeanNamesForType(User.class);
for (String s : beanNamesForType) {
System.out.println(s);
}
String[] beanNamesForType1 = run.getBeanNamesForType(DispatcherServlet.class);
for (String s : beanNamesForType1) {
System.out.println(s);
}
}
}
- 使用 ImportSelector 接口導入元件
- 建立一個 MyImportSelector 類實作 ImportSelector 接口,并且實作selectImports 方法
- 然後傳回一個數組,包括要注冊的類的全類名
// 自定義邏輯傳回需要導入的元件
// 由于實作了 ImportSelector,是以把注冊的方法的全類名傳回
public class MyImportSelector implements ImportSelector {
// 傳回值,就是要導入到容器中的元件全類名
// AnnotationMetadata:目前标注 @Import 注解的類的所有注解資訊
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
// 方法不要傳回 null 值,可以傳回一個 空空數組
return new String[]{"com.example.pojo.Test1","com.example.pojo.Test2"};
}
}
- 使用 Import 注解注冊 MyImportSelector
//2. ImportSlector:傳回需要導入的元件的全類名數組
@Import({User.class, DispatcherServlet.class,MyImportSelector.class})
- 在主啟動類中輸出檢視
- 使用 ImportBeanDefinitionRegistrar 接口注冊bean
- 建立一個 MyImportBeanDefinitionRegistrar 類實作 ImportBeanDefinitionRegistrar 接口,并且實作registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry)方法
public class MyImportBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar {
/**
* @param importingClassMetadata:目前類的注釋資訊
* @param registry:BeanDefinition 的注冊類
* 把所有需要添加到容器中的 Bean,
* 調用BeanDefinitionRegistry.registerBeanDefinition手工注冊進來
*/
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
// 指定 bean 名
boolean test1 = registry.containsBeanDefinition("com.example.pojo.Test1");
boolean test2 = registry.containsBeanDefinition("com.example.pojo.Test2");
if (test1 && test2) {
// 指定 Bean 定義資訊(Bean 的類型,bean 的作用域都可以在這裡指定)
RootBeanDefinition rootBeanDefinition = new RootBeanDefinition(Test1.class);
// 注冊一個 bean,指定 bean 名為 test
registry.registerBeanDefinition("test",rootBeanDefinition );
}
}
}
- 使用 Import 注解注冊 MyImportBeanDefinitionRegistrar
- 在主啟動類中輸出檢視注冊成功
- Conditional
條件裝配:滿足 Conditional 指定條件,則進行元件的注入
- ConditionalOnBean:當容器中有某些元件的時候做一些事情
- ConditionalOnMissingBean:當容器中沒有某些元件的時候做一些事情
- ConditionalOnClass:當容器中有某些類的時候做一些事情
- ConditionalOnMissingBean:當容器中沒有某些類的時候做一些事情
- ConditionalOnJava:當 java 版本是某一個版本的時候做一些事情
- ConditionalOnResource:當根目錄下有某些資源的時候做一些事情
- ConditionalOnWebApplication:是web應用的時候做一些事情
- ConditionalOnSingleCandidate:當容器元件隻有一個執行個體或者有多個執行個體但是隻有一個主執行個體的時候才生效
- ConditionalOnProperty:當配置檔案中配置了某個屬性的時候生效
@Configuration
public class MyConfigConditional {
@ConditionalOnBean(name = "pet")
// @ConditionalOnBean(name = "pet") 如果 pet 元件注冊到容器中,則 user 元件也會被注冊到容器中
// 如果 pet 沒有 @Bean 注冊,則 user 元件也不會注冊到容器中
// @ConditionalOnBean 是容器如果有某一個元件,就會将加本注解的元件注冊到容器,條件不成立不會将元件注冊到容器
@Bean
public User user() {
return new User("張三");
}
//@Bean
public Pet pet() {
return new Pet();
}
}
3.3.2 原生配置檔案引入
- @ImportResource 導入 Spring 的配置檔案,讓配置檔案進行生效
在 SpringMvc 模式下有一個配置檔案,然後再配置檔案中有很多的 bean 标簽注冊了很多的元件
然後再 SpringBoot 中想使用這些元件不用一個一個進行修改,隻需要再要使用的類上使用注解
@ImportResource("classpath:beans.xml")
3.3.3 配置綁定
我們習慣于把經常愛變化的東西配置到配置檔案中,比如資料庫的已連接配接位址賬号密碼等,之前的操作中我們需要加載配置檔案,然後得到每一個 key value 的值,然後把這些 k v 值一一對應封裝到 JavaBean 中。在 SpringBoot 中這個過程會變得非常簡單,這個過程就叫做配置綁定。
方式一:@ConfigurationProperties
- 建立一個 Car 類,包括 brand 和 price 屬性
- 在配置檔案中設定屬性
car.brand=BMcar.price=1999
-
在 Car 類上面進行綁定、
注意:隻有在容器中的元件,才能使用這個配置綁定的功能
@Component
/**
* 隻有在容器中的元件,才能使用這個配置綁定的功能
*/
@Component
@ConfigurationProperties(prefix = "car")
public class Car {
private String brand;
private Integer price;
// 省略 getter / setter 方法
}
- 在 Controller 中使用 @Autowired 注入,然後傳回
@Autowired
private Car car;
@RequestMapping("/car")
public Car car() {
return car;
}
- 通過浏覽器通路端口測試,輸出結果
方式二:@EnabledConfigurationProperties + @ConfigurationProperties
因為我們有時候可能需要使用第三方的元件,而這些元件我們是不能使用 @Component 注冊到容器中的,是以可以使用這種方式
- 在我們自動的配置檔案上使用 @EnabledConfigurationProperties 注解
@Configuration
@EnableConfigurationProperties(Car.class)
// 1. 開啟 Car 配置綁定功能
// 2. 把這個 Car 這個元件自動注冊到容器中
public class MyConfig {
}
- 在 Car 類上使用 @ConfigurationProperties(prefix = “car”)
- 測試運作
3.4 自動配置原理
3.4.1 啟動類引導加載自動配置類
@SpringBootApplication 點進去是一個合成注解的類
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(excludeFilters = { @Filter(type = FilterType.CUSTOM, classes = TypeExcludeFilter.class),
@Filter(type = FilterType.CUSTOM, classes = AutoConfigurationExcludeFilter.class) })
public @interface SpringBootApplication {
-
@SpringBootConfiguration
點進去看到一個有 @Configuration 注解的類,代表目前就是一個配置類,也就是 main 程式也就是 Spring 中的一個核心配置類
@Configuration
@Indexed
public @interface SpringBootConfiguration {
@AliasFor(
annotation = Configuration.class
)
boolean proxyBeanMethods() default true;
}
-
@ComponentScan
表示指定要掃描那些包
-
@EnableAutoConfiguration
點選後可以看到也是一個合成注解
@AutoConfigurationPackage
@Import(AutoConfigurationImportSelector.class)
public @interface EnableAutoConfiguration {}
-
@AutoConfigurationPackage
自動配置包,指定了預設的包結構的規則
這就解釋了為啥 MainApplication 所在的包的注解才能生效
// 給容器中導入一個元件,這裡的 Register 是給容器中批量注冊元件
// 将指定的一個包下的所有元件導入進來
@Import(AutoConfigurationPackages.Registrar.class)
public @interface AutoConfigurationPackage {
- @Import(AutoConfigurationImportSelector.class)
@Override
public String[] selectImports(AnnotationMetadata annotationMetadata) {
if (!isEnabled(annotationMetadata)) {
return NO_IMPORTS;
}
AutoConfigurationEntry autoConfigurationEntry = getAutoConfigurationEntry(annotationMetadata);
return StringUtils.toStringArray(autoConfigurationEntry.getConfigurations());
}
利用
getAutoConfigurationEntry(annotationMetadata);
方法給容器中批量導入一些元件,擷取所有配置的集合
這個方法會先将得到的所有元件去掉重複的,移除一些沒有用到的等等操作,然後傳回。
getCandidateConfigurations(),利用 Spring 的工廠加載一些東西
protected List<String> getCandidateConfigurations(AnnotationMetadata metadata, AnnotationAttributes attributes) {
List<String> configurations = SpringFactoriesLoader.loadFactoryNames(getSpringFactoriesLoaderFactoryClass(),
getBeanClassLoader());
Assert.notEmpty(configurations, "No auto configuration classes found in META-INF/spring.factories. If you "
+ "are using a custom packaging, make sure that file is correct.");
return configurations;
}
public static List<String> loadFactoryNames(Class<?> factoryType, @Nullable ClassLoader classLoader) {
ClassLoader classLoaderToUse = classLoader;
if (classLoaderToUse == null) {
classLoaderToUse = SpringFactoriesLoader.class.getClassLoader();
}
String factoryTypeName = factoryType.getName();
return loadSpringFactories(classLoaderToUse).getOrDefault(factoryTypeName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(ClassLoader classLoader) {
Map<String, List<String>> result = cache.get(classLoader);
if (result != null) {
return result;
}
result = new HashMap<>();
try {
Enumeration<URL> urls = classLoader.getResources(FACTORIES_RESOURCE_LOCATION);
while (urls.hasMoreElements()) {
URL url = urls.nextElement();
classLoader.getResources(FACTORIES_RESOURCE_LOCATION); 這個會加載資源檔案,檔案位置點進去就可以看到
public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
// 這個方法最終會加載得到所有的元件
從
META-INF/spring.factories
位置來加載一個檔案,預設掃描我們目前系統裡面所有 META-INF/spring.factories 位置的檔案
在這個檔案裡面就是所有自動配置的東西,通過這個機制進行自動配置,其實就是在配置檔案中寫死的
意思就是這個檔案中寫死了 spring-boot 一啟動就要給容器中加載的所有配置類
3.4.2 按需開啟自動配置項
雖然自動配置項在啟動的時候會預設全部加載,但是最終會按照條件裝配規則按需裝配的。
3.4.3 修改預設配置
以 DispatcherServletAutoConfiguration 為例
首先檢視類上面的 Conditioinal 注解生效後,然後檢視方法上面的 Conditional 注解生效,然後就會注冊這個 bean,為什麼使用 dispatcherServlet 不會進行一些别的操作,因為在這裡Spring 已經為我們建立好了對象,并且做了一系列的配置然後傳回 dispatcherServlet
@EnableConfigurationProperties(WebMvcProperties.class)
這裡就是對 application.properties 檔案進行一個綁定
這個就是我們要導入的配置檔案,他會從我們的 application.properties 檔案中找到相應的自己的配置,然後進行配置的設定
@ConfigurationProperties(prefix = “spring.mvc”)
通過 spirng.mvc 這個字首就可以修改預設的配置資訊
- multipartResolver
@Bean
@ConditionalOnBean(MultipartResolver.class) // 判斷容器中有這個類的元件,條件判斷是否生效
@ConditionalOnMissingBean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME)// 如果容器中沒有 multipartResolver 這個元件生效,如果有的話就是使用者自己定義了自己的元件,然後不生效
public MultipartResolver multipartResolver(MultipartResolver resolver) {
// @Bean 标注的方法傳入到了對象參數,這個參數的值就會從容器中找
// multipartResolver ,防止有一些使用者配置的檔案上傳解析器不符合規範
// Detect if the user has created a MultipartResolver but named it incorrectly
return resolver;
}
大概就是這個意思,可能不太準确:這個方法并沒有進行什麼設定,隻是怕有的人使用的時候把檔案上傳的名字寫錯了,然後這裡 MultipartResolver resolver 接受到傳入的值,然後進行一個相當于重命名的操作進行 return,multipartResolver 這個名字就是注冊好的 bean
public static final String MULTIPART_RESOLVER_BEAN_NAME = “multipartResolver”;
SpringBoot 預設會在底層配置好所有的元件,但是說如果使用者使用 @Bean 等一些注解配置了自己的元件,則以使用者配置的元件優先 @ConditionalOnMissingBean
- SpringBoot 先加載所有的自動配置類 xxxxAutoConfiguration
-
每個自動配置類按照條件進行生效,預設都會綁定配置檔案指定的值
從 xxxxProperties 裡面拿到,xxxxProperties 和配置檔案進行了綁定
- 生效的配置類就會給容器中裝配很多元件
- 隻要容器中有這些元件,相當于這些功能就已經是實作了
- 自定義配置
- 直接 @Bean 替換底層的元件
- 使用者去看這個元件是擷取的配置檔案什麼值就去修改
3.4.4 使用
- 引入場景的依賴
-
檢視自動配置了那些(選擇檢視)
https://docs.spring.io/spring-boot/docs/current/reference/html/using.html#using.auto-configuration
- 引入場景對應的自動配置一般都生效了
- 配置檔案中
開啟自動配置報告。debug = true
- Negative 不生效的
- Positive 生效的
- 是否需要修改
- 修改配置項目
- https://docs.spring.io/spring-boot/docs/current/reference/html/application-properties.html#application-properties
- 自己分析 xxxxProperties 綁定了配置檔案的那些字首
- 自定義加入或者替換的元件
- @Bean 等注解實作
- 自定義器:xxxxCustomizer
- 修改配置項目
3.5 開發技巧
3.5.1 Lombok
在我們床架實體類後我們需要手動生成它的有/無 參構造方法、get/set 方法,toString 方法等,使用 Lombok 後隻需要使用簡單的注解可以實作以上的内容
- 在 Idea 中 setting – plugins 中安裝 Lombok 插件
- 導入 Lombok 的啟動器
在 Spring Boot 父依賴中找到,然後在 pom.xml 檔案中引入
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
- 在實體類加上注解
@Data // 生成 get/set 方法
@ToString // 生成 toString 方法
@AllArgsConstructor // 生成全參構造器
@NoArgsConstructor // 生成無參構造器
public class User {
private String name;
}
- 在類上面使用 @Slf4j 注解,使用日志功能
@Slf4j
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello() {
log.info("請求進到這裡來了。。。。。");
return "Hello SpringBoot2";
}
}
啟動浏覽器通路這個接口後,控制台會輸出我們給定的日志資訊
3.5.2 Spring Initailizr
快速的建立好 Spring Boot 應用
選擇好場景之後,我們點選 Next ,然後 idea 就會聯網把我們的項目下載下傳好
3.5.3 dev-tools
熱更新,我們在做項目的時候可能會進行修改,然後不想每次啟動後去點選啟動按鈕,可以使用 dev-tools 熱更新,修改完代碼後使用 Ctrl + F9(對項目重新編譯一下,然後重新加載) 就可以實時更新了
- 導入依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
</dependencies>
- 項目修改後使用快捷鍵 Ctrl + F9 就可以重新附加元件目
4. 配置檔案
4.1 檔案類型
4.1.1 properties
在之前使用過的 properties 對屬性繼續配置,配置名=值
4.1.2 yaml
- 簡介
YMAL 是 “YAML Aint’s Markup Language” (YAML 不是一種标志語言)的遞歸縮 寫。在開發的這種語言時,YMAL 的意思其實是:“Yet Another Markup Language”(仍是一種标記語言)。
- 非常适合用來做資料為中心的配置檔案
- 基本文法
- key: value — 注意:key 和 value 中間必須有一個空格,類似于 json 字元串的格式
- 大小寫敏感
- 使用縮進表示層級關系
- 縮進不允許使用 tab,隻允許空格
- 縮進的空格數不重要,隻要相同層級的元素左對齊即可
- ‘#’ 表示注解
- ‘’ 與 “” 表示字元串内容,會被 轉義/不轉義
- 資料類型
- 字面量:單個的、不可再分的值。date、boolean、string、number、null
key: value
- 對象:鍵值對的集合。map、hash、set、object
行内寫法: k: {k1:v1,k2:v2,K3:v3}
或者
k:
k1: v1
k2: v2
k3: v3
- 數組:一組按次序排列的值。array、list、queue
行内寫法: k: [v1,v2,v3]
或者
k:
- v1
- v2
- 案例
- Person.java
@ConfigurationProperties(prefix = "person")
@Component
@Data
@ToString
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;
}
- Pet.java
@Component
@ToString
@Data
public class Pet {
private String name;
private Integer age;
}
- properties.yml
person:
userName: 張三
boss: true
birth: 2020/09/23
age: 20
pet:
name: 小黃
age: 10
interests:
- 籃球
- 足球
animal:
- 小貓
- 小狗
score:
國文:
first: 33
second: 44
third: 55
數學: [30,50,89]
salarys:
- 20000
- 1000
allPets:
sick:
- name: 小貓
age: 10
health:
- name: 小狗
age: 30
- 測試運作結果
4.2 配置處理器
在編寫 yaml 檔案的時候,輸入的時候是沒有提示資訊的,在項目中加入配置處理器
官方位址:https://docs.spring.io/spring-boot/docs/current/reference/html/configuration-metadata.html#configuration-metadata.format
- 在pom.xml 中導入依賴
<!-- 注釋處理器 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
- 重新開機項目
- 然後在 yaml 中輸入就會有提示資訊
- 在打包插件中加入一個移除打包配置處理器的插件,因為這個隻是開發的時候使用,打包是用不着的
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
5. Web 開發
一個源碼分析的連結:https://www.cnblogs.com/seazean/p/15109440.html
官網内容:https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-web-applications
5.1 SpringMVC 自動配置概覽
Spring Boot provides auto-configuration for Spring MVC that works well with most applications.(大多數場景我們都無需自定義配置)
The auto-configuration adds the following features on top of Spring’s defaults:
- Inclusion of
andContentNegotiatingViewResolver
beans.BeanNameViewResolver
- 内容協商試圖解析器和 BeanName 試圖解析器
- Support for serving static resources, including support for WebJars (covered later in this document).
- 靜态資源(包括 webjars)
- Automatic registration of
,Converter
, andGenericConverter
beans.Formatter
- 自動注冊:
,Converter
, andGenericConverter
Formatter
- 自動注冊:
- Support for
(covered later in this document).HttpMessageConverters
- 支援
HttpMessageConverters
- 支援
- Automatic registration of
(covered later in this document).MessageCodesResolver
- 自動注冊
,國際化用MessageCodesResolver
- 自動注冊
- Static
support.index.html
- 靜态 index.html 頁支援
- Automatic use of a
bean (covered later in this document).ConfigurableWebBindingInitializer
- 自動使用
,DataBinder 負責将請求資料綁定到 JavaBean 上ConfigurableWebBindingInitializer
- 自動使用
If you want to keep those Spring Boot MVC customizations and make more MVC customizations (interceptors, formatters, view controllers, and other features), you can add your own
@Configuration
class of type
WebMvcConfigurer
but without
@EnableWebMvc
.
不用
@EnableWebMvc
注解,使用
@Configuration
+
WebMvcConfigurer
自定義規則
If you want to provide custom instances of
RequestMappingHandlerMapping
,
RequestMappingHandlerAdapter
, or
ExceptionHandlerExceptionResolver
, and still keep the Spring Boot MVC customizations, you can declare a bean of type
WebMvcRegistrations
and use it to provide custom instances of those components.
聲明
WebMvcRegistrations
改變預設底層元件
If you want to take complete control of Spring MVC, you can add your own
@Configuration
annotated with
@EnableWebMvc
, or alternatively add your own
@Configuration
-annotated
DelegatingWebMvcConfiguration
as described in the Javadoc of
@EnableWebMvc
.
使用
@EnableWebMvc
+
@Configuration
+
DelegatingWebMvcConfiguration
全面接管 SpringMVC
5.2 簡單功能分析
5.2.1 靜态資源通路
官網位址:https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-web-applications.spring-mvc.static-content
- By default, Spring Boot serves static content from a directory called
(or/static
or/public
or/resources
) in the classpath or from the root of the/META-INF/resources
.ServletContext
- Spring Boot 為我們提供靜态資源的目錄,也就是說當我們将所有的靜态資源(圖檔,js,css檔案等)放到
(or/static
or/public
or/resources
) 這些檔案夾下,然後通路我們目前項目的根路徑就可以通路到靜态資源了,因為它使用了/META-INF/resources
處理了ResourceHttpRequestHandler
- Spring Boot 為我們提供靜态資源的目錄,也就是說當我們将所有的靜态資源(圖檔,js,css檔案等)放到
- 隻要我們将靜态資源放在類路徑下的:/static /public /resources /META-INF/resources 檔案下,我們就可以通過目前項目的根路徑/ + 靜态資源名來進行通路。
測試通路根目錄下的資源
- 首先在根目錄下建立相對應的 4 個檔案夾,并且在每一個檔案夾下放入一個圖檔
- 然後使用浏覽器通過
就可以通路到localhost:8080/資源名字.擴充名
- 在每一個檔案下的檔案修改為同一個名稱,然後通過浏覽器通路測試優先級
- /META-INF/resources
- /resources
- static
- public
假設我們 controller 中的動态請求路徑與根目錄下靜态資源的名字相同
在這種情況下,當請求進來的時候,會先去 Controller 中看能不能進行處理,不能處理的請求又都交給資源靜态處理器,因為靜态資源映射的是 /**,是以會在根目錄下找相應的靜态資源,如果靜态資源也找不到就會響應 404 頁面。
靜态資源通路字首
- By default, resources are mapped on
, but you can tune that with the/**
property. For instance, relocating all resources tospring.mvc.static-path-pattern
/resources/**
can be achieved as follows:
預設情況下,映射的路徑是 /**,也就是說通路我們的靜态資源隻需要寫靜态資源名就會自動的找到靜态資源。如果想要改變這個靜态資源通路的路徑,可以通過修改
實作,也就是說給請求加一個字首,以防止項目中使用攔截器靜态資源會被攔截spring.mvc.static-path-pattern
spring:
mvc:
static-path-pattern: /res/**
# 表示 /res 下面的都是靜态資源請求,在通路靜态資源的時候就通路這個位址加靜态資源名
改變預設的靜态資源路徑
spring:
web:
resources:
static-locations: classpath:/myresources
指定了 static-locations 後,所有請求的靜态資源檔案都會去指定的檔案加下找,别的位置找不到的。
支援 webjars
官網:https://www.webjars.org/
webjars 就是将 jquery、js、css 等靜态檔案打包成 jar 包
以 jquery 為例測試
- 在官網找到對應 jar 包
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.5.1</version>
</dependency>
- 找打 jquery 的 webjars 目錄
- 浏覽器通路路徑:localhost:8080/webjars/jquery/3.5.1/jquery.js 就可以看到這個js 檔案的内容了
5.2.2 歡迎頁
Spring Boot supports both static and templated welcome pages. It first looks for an
index.html
file in the configured static content locations. If one is not found, it then looks for an
index
template. If either is found, it is automatically used as the welcome page of the application.
Spring Boot 既支援靜态歡迎頁面,也支援模闆歡迎頁面。如果我們是前者,我們将 index.html 靜态資源檔案放到靜态資源路徑下,就會被當成歡迎頁面,也就是通路項目根路徑預設展示的頁面。或者靜态資源路徑下沒有存在這個頁面,也會給我們找 index 這個模闆(有一個 Controller 處理 index 請求,最終跳回頁面,這個 index 模闆最終也會作為我們的歡迎頁)
靜态資源路徑下 index.html 頁面
這裡我們可以在 yml 檔案中配置自己的靜态資源路徑,然後将我們的 index.html 頁面放到自己定義的靜态資源檔案下,但是不可以配置靜态資源通路路徑,否則導緻 index.html 不能被預設通路;也可以放到預設生成的 static 靜态資源目錄下,然後通過 localhost:8080 就可以通路到我們的歡迎頁
Controller 根據請求處理 /index 跳轉到 歡迎頁
5.2.3 自定義 Favicon
每一個網站都有一個自己的圖示,例如 Spring 官網的:
這個配置好像在 2.3.x 版本後就沒有了
如果要使用,隻需要把圖示改名為
favicon.ico
放到靜态資源目錄下就可以了,就會被自動配置為應用的圖示
5.2.4 靜态資源配置原理
- SpringBoot 啟動後會預設加載很多的 xxxxAutoConfiguration 類即自動配置類
- 如果想要檢視 SpringMVC 功能的自動配置類,大多數都集中在:
這個類中WebMvcAutoConfiguration
檢視 WebMvcAutoConfiguration 是否生效
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication(type = Type.SERVLET)
@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, WebMvcConfigurer.class })
@ConditionalOnMissingBean(WebMvcConfigurationSupport.class)
@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10)
@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, TaskExecutionAutoConfiguration.class,
ValidationAutoConfiguration.class })
public class WebMvcAutoConfiguration {}
- 檢視 WebMvcAutoConfiguration 給容器中配置了什麼東西
-
SpringMvc 為了相容 RestFul 風格的OrderedHiddenHttpMethodFilter
-
表單内容的過濾器OrderedFormContentFilter
-
WebMvcAutoConfigurationAdapter
-
@Configuration(proxyBeanMethods = false)
@Import(EnableWebMvcConfiguration.class)
// 配置檔案的相關屬性和 xxx 進行了綁定
@EnableConfigurationProperties({ WebMvcProperties.class,
org.springframework.boot.autoconfigure.web.ResourceProperties.class, WebProperties.class })
@Order(0)
public static class WebMvcAutoConfigurationAdapter implements WebMvcConfigurer, ServletContextAware {
-
WebMvcProperties
與字首
的配置檔案進行綁定spring.mvc
@ConfigurationProperties(prefix = "spring.mvc")
public class WebMvcProperties {
-
ResourceProperties
與字首
的配置檔案進行綁定spring.resources
@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false)
public class ResourceProperties extends Resources {
注意: 一個配置類隻有一個有參構造器,特性:有參構造器中所有參數的值都會從容器中确定
// ResourceProperties resourceProperties 擷取所有和 `spring.resources` 綁定的所有值的對象
// WebMvcProperties mvcProperties 擷取所有和 `spring.mvc` 綁定的所有值的對象
// ListableBeanFactory beanFactory 相當于是找的 IOC,容器工廠 bean 工廠,找 Spring 的容器
// HttpMessageConverters 找到所有的 HttpMessageConverters
// ResourceHandlerRegistrationCustomizer 找到資源處理器的自定義器
// DispatcherServletPath 處理的路徑
// ServletRegistrationBean 給應用注冊原生的 servlet,filter 等
public WebMvcAutoConfigurationAdapter(
org.springframework.boot.autoconfigure.web.ResourceProperties resourceProperties,
WebProperties webProperties, WebMvcProperties mvcProperties, ListableBeanFactory beanFactory,
ObjectProvider<HttpMessageConverters> messageConvertersProvider,
ObjectProvider<ResourceHandlerRegistrationCustomizer> resourceHandlerRegistrationCustomizerProvider,
ObjectProvider<DispatcherServletPath> dispatcherServletPath,
ObjectProvider<ServletRegistrationBean<?>> servletRegistrations) {
this.resourceProperties = resourceProperties.hasBeenCustomized() ? resourceProperties
: webProperties.getResources();
this.mvcProperties = mvcProperties;
this.beanFactory = beanFactory;
this.messageConvertersProvider = messageConvertersProvider;
this.resourceHandlerRegistrationCustomizer = resourceHandlerRegistrationCustomizerProvider.getIfAvailable();
this.dispatcherServletPath = dispatcherServletPath;
this.servletRegistrations = servletRegistrations;
this.mvcProperties.checkConfiguration();
}
資源處理的預設規則
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
logger.debug("Default resource handling disabled");
return;
}
// webjars 規則
addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
// 靜态資源路徑的配置規則
addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration) -> {
registration.addResourceLocations(this.resourceProperties.getStaticLocations());
if (this.servletContext != null) {
ServletContextResource resource = new ServletContextResource(this.servletContext, SERVLET_LOCATION);
registration.addResourceLocations(resource);
}
});
}
-
isAddMapping 點進去,可以得到資訊:if (!this.resourceProperties.isAddMappings()) {
private boolean addMappings = true;
,這裡預設是true ,我們可以在 yaml 檔案中設定為false ,如果為 false 則表示過濾掉所有靜态資源的請求
因為這個方法的下面都是靜态資源的配置的資訊,是以如果設定為 false 則直接return,不會向下執行,是以可以了解為過濾掉所有靜态資源
spring:
resources:
add-mappings: false #禁用掉所有靜态資源
- 注冊第一種通路規則
,這就是為什麼通路 webjars 檔案夾下的靜态資源可以直接通路addResourceHandler(registry, "/webjars/**", "classpath:/META-INF/resources/webjars/");
-
靜态資源路徑的配置規則addResourceHandler(registry, this.mvcProperties.getStaticPathPattern(), (registration)
this.mvcProperties.getStaticPathPattern()
在 WebMvcProperties 中,這個檔案與 prefix = “spring.mvc” 綁定的
然後在 this.resourceProperties.getStaticLocations() 找靜态資源的路徑
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = { "classpath:/META-INF/resources/","classpath:/resources/", "classpath:/static/","classpath:/public/" };
歡迎頁的處理規則
HandlerMapping 處理器映射,裡面儲存了每一個 Handler 能處理那些請求,請求一過來 HandlerMapping 就會看,那個請求交給誰處理,找到以後用反射調用可以處理的方法
@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(TemplateAvailabilityProviders templateAvailabilityProviders,
ApplicationContext applicationContext, Resource welcomePage, String staticPathPattern) {
// 這裡可以得到資訊,要使用 歡迎頁 就必須配置路徑為 /**
if (welcomePage != null && "/**".equals(staticPathPattern)) {
logger.info("Adding welcome page: " + welcomePage);
setRootViewName("forward:index.html");
}
// 否則調用 Controller 看能不能處理這個請求
else if (welcomeTemplateExists(templateAvailabilityProviders, applicationContext)) {
logger.info("Adding welcome page template: index");
setRootViewName("index");
}
}
5.3 請求參數處理
5.3.1 Rest使用原理
- @xxxxMapping
- Rest 風格支援(使用 Http 請求方式動詞來表示對資源的操作)
- 之前:/getUser 擷取使用者 /delUser 删除使用者 /updUser 修改使用者 /addUser 添加使用者
- 現在:/user GET-擷取使用者 DELETE-删除使用者 PUT-修改使用者 POST-添加使用者
HiddenHttpMethodFilter
假設沒有配置 HiddenHttpMethodFilter
因為在 html 表單送出之後 get 和 post 兩種方式
- html,表示 4 中送出方式
<form action="/user" method="get">
<input type="submit" value="GET-送出">
</form>
<form action="/user" method="post">
<input type="submit" value="POST-送出">
</form>
<form action="/user" method="delete">
<input type="submit" value="DELETE-送出">
</form>
<form action="/user" method="put">
<input type="submit" value="PUT-送出">
</form>
- controller,4 中接受請求的方式
@RequestMapping(value = "/user",method = RequestMethod.GET)
public String getUser() {
return "GET-使用者";
}
@RequestMapping(value = "/user",method = RequestMethod.POST)
public String postUser() {
return "POST-使用者";
}
@RequestMapping(value = "/user",method = RequestMethod.DELETE)
public String deleteUser() {
return "DELETE-使用者";
}
@RequestMapping(value = "/user",method = RequestMethod.PUT)
public String putUser() {
return "PUT-使用者";
}
- 沒有配置
,這種情況下 get 通路會請求 get 的controller,post 通路會請求 post 的controlller,但是如果如果使用 delete、put 則都會走 get 請求,因為 html 中表達的送出方式隻有兩種,如果請求方式不是這兩種就預設以 get 方式請求處理HiddenHttpMethodFilter
解決方式:
在 SpringBoot 中已經配置好了
HiddenHttpMethodFilter
@Bean
@ConditionalOnMissingBean(HiddenHttpMethodFilter.class)
@ConditionalOnProperty(prefix = "spring.mvc.hiddenmethod.filter", name = "enabled")
public OrderedHiddenHttpMethodFilter hiddenHttpMethodFilter() {
return new OrderedHiddenHttpMethodFilter();
}
點進去我們會發現
public class HiddenHttpMethodFilter extends OncePerRequestFilter {
private static final List<String> ALLOWED_METHODS =
Collections.unmodifiableList(Arrays.asList(HttpMethod.PUT.name(),
HttpMethod.DELETE.name(), HttpMethod.PATCH.name()));
/** Default method parameter: {@code _method}. */
// 這裡表示我們隻需要帶一個隐藏的 _method 項就可以使用 Rest 風格
public static final String DEFAULT_METHOD_PARAM = "_method";
注意:在html 總 form 請求的時候 method 必須是 post 方式,因為在 doFilterInternal 中隻有 POST 請求才能生效 "POST".equals(request.getMethod()
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
HttpServletRequest requestToUse = request;
if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
// 如果是 post 請求,就會拿到 methodParam = DEFAULT_METHOD_PARAM = "_method"
String paramValue = request.getParameter(this.methodParam);
if (StringUtils.hasLength(paramValue)) {
String method = paramValue.toUpperCase(Locale.ENGLISH);
if (ALLOWED_METHODS.contains(method)) {
requestToUse = new HttpMethodRequestWrapper(request, method);
}
}
}
根據上面得到的資訊,在 html 中加上隐藏 并且設定 _method,發現還是不行
<form action="/user" method="get">
<input type="submit" value="GET-送出">
</form>
<form action="/user" method="post">
<input type="submit" value="POST-送出">
</form>
<form action="/user" method="post">
<input name="_method" type="hidden" value="DELETE"/>
<input type="submit" value="DELETE-送出">
</form>
<form action="/user" method="post">
<input name="_method" type="hidden" value="PUT"/>
<input type="submit" value="PUT-送出">
</form>
然後我們看到 @ConditionalOnProperty(prefix = “spring.mvc.hiddenmethod.filter”, name = “enabled”),可能預設值是 false
在 yaml 檔案中将這個屬性的值設定為 true 就可以正常使用 Rest 風格進行通路了
spring:
mvc:
hiddenmethod:
filter:
enabled: true
Rest 原理 — 基于表單送出使用 Rest
首先,送出表單的時候會帶上
_method
參數,以及真正送出方式的參數
在 Spring Boot 有過濾器,是以當請求過來的時候會被
HiddenMethodFilter
攔截
然後處理請求
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
HttpServletRequest requestToUse = request; // 原生的請求
// "POST".equals(request.getMethod()) 然後判斷原生的請求方式是不是 POST,這就是為什麼使用 delete 和 put 的時候要求請求方式是 POST 才能使用 Rest 風格
// request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) 判斷我們目前的請求中有沒有錯誤
if ("POST".equals(request.getMethod()) && request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) == null) {
// request.getParameter 在原生的請求中能擷取到請求的參數,即擷取到 _method 參數的值
// methodParam = "_method"
String paramValue = request.getParameter(this.methodParam);
if (StringUtils.hasLength(paramValue)) {
// 然後将 _method 參數的轉換成大寫,也就是說表單送出的參數 delete 大小寫無所謂
String method = paramValue.toUpperCase(Locale.ENGLISH);
// 判斷它們允許(除了 get和 post 外 相容 PUT、DELETE、PATCH)的請求方式中包不包含送出的請求
if (ALLOWED_METHODS.contains(method)) {
// 原生 request(post) 包裝模式 requestWrapper 重寫了 getMethod 方法,傳回的是傳入的值
requestToUse = new HttpMethodRequestWrapper(request, method);
}
}
}
// 過濾器鍊放行的時候用 wrapper,以後的方法調用 getMethod 是調用 requestWrapper 的
filterChain.doFilter(requestToUse, response);
}
上面 Controller 請求方式的切換
- @GetMapping("/user")
- @PostMapping("/user")
- @DeleteMapping("/user")
- @PutMapping("/user")
擴充:把 _method 自定義
自定義一個 WebConfig 類,注冊自己的 HiddenMethodFilter
@Bean
public HiddenHttpMethodFilter hiddenHttpMethodFilter() {
HiddenHttpMethodFilter hiddenHttpMethodFilter = new HiddenHttpMethodFilter();
hiddenHttpMethodFilter.setMethodParam("_m");
return hiddenHttpMethodFilter;
}
5.3.2 請求映射
在 Spring Boot 中,所有的請求都會到
DispatherServlet
中,其實 SpringBoot 底層使用的還是 SpringMVC
DispatcherServelt 繼承 FrameworkServelt 繼承 HttpServelt
當請求開始的時候 HttpServlet 的doGet 最終都會調用到 FrameServlet 中的 processRequest,在 processRequest 中又去調用 doService,在最終的 DispatcherServlet 中對 doService 進行了實作
- FrameworkServlet.java,在 HttpServletBean 中沒有找到 doGet 請求,然後再它的子類FrameworkServlet 中找
@Override
protected final void doGet(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
// HttpServlet 的doGet 最終都會調用到 FrameServlet 中的 processRequest
processRequest(request, response);
// 點進去這個方法發現又調用了本類的 doService 方法
}
protected final void processRequest(HttpServletRequest request, HttpServletResponse response)
throws ServletException, IOException {
long startTime = System.currentTimeMillis();
Throwable failureCause = null;
LocaleContext previousLocaleContext = LocaleContextHolder.getLocaleContext();
LocaleContext localeContext = buildLocaleContext(request);
RequestAttributes previousAttributes = RequestContextHolder.getRequestAttributes();
ServletRequestAttributes requestAttributes = buildRequestAttributes(request, response, previousAttributes);
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
asyncManager.registerCallableInterceptor(FrameworkServlet.class.getName(), new RequestBindingInterceptor());
initContextHolders(request, localeContext, requestAttributes);
try {
// 這是核心部分
doService(request, response);
}
catch (ServletException | IOException ex) {
failureCause = ex;
throw ex;
}
catch (Throwable ex) {
failureCause = ex;
throw new NestedServletException("Request processing failed", ex);
}
finally {
resetContextHolders(request, previousLocaleContext, previousAttributes);
if (requestAttributes != null) {
requestAttributes.requestCompleted();
}
logResult(request, response, failureCause, asyncManager);
publishRequestHandledEvent(request, response, startTime, failureCause);
}
}
進去 doService 發現是一個抽象方法,然後去 DispatcherServlet 中找到對應的方法
protected abstract void doService(HttpServletRequest request, HttpServletResponse response)
throws Exception;
- DispatcherServlet 最終對 doService 做了實作,然後發現 doService 中又調用了 doDispatch 方法,而這個 doDispatch 方法就是請求映射的核心内容,每個請求都會調用 doDispatch 方法
- doDispatch 所有 SpringMVC 功能分析都從這個方法開始
protected void doDispatch(HttpServletRequest request, HttpServletResponse response) throws Exception {
HttpServletRequest processedRequest = request;
HandlerExecutionChain mappedHandler = null;
boolean multipartRequestParsed = false;
WebAsyncManager asyncManager = WebAsyncUtils.getAsyncManager(request);
try {
ModelAndView mv = null;
Exception dispatchException = null;
try {
processedRequest = checkMultipart(request);
multipartRequestParsed = (processedRequest != request);
// 找到那個 Handler 來處理我們的請求
mappedHandler = getHandler(processedRequest);
- processedRequest 中會儲存有我們的請求位址資訊等
進入 getHandler 中我們會看到 HadnlerMapping(處理器映射) 有 5 個,也就是說我們的 這裡面儲存到了我們的映射規則, /*** 由誰處理等
- WelcomePageHandlerMapping 裡面儲存了所有 index 請求
- RequestMappingHandlerMapping ,這個就是所有的 @RequestMapping 注解的處理器映射,這個裡面儲存了所有的 @RequestMappnig 和 Handler 的映射規則,就是當我們的應用啟動的時候 SpirngMVC 自動掃描我們的所有的 Controller ,并解析注解,把注解資訊儲存到 HandlerMapping 中
這裡有 5 個 HandlerMapping ,啟動後會周遊這 5 個查找看誰能處理這個請求
- 檢視 RequestMappingHandlerMapping 中的資訊,發現我們自己寫的所有的路徑在這裡都進行了映射
所有的請求映射都儲存在 HandlerMapping 中
SpringBoot 中自動除了配置歡迎頁的 HandlerMapping。通路 / 能通路到 index.html
請求進來的時候,會逐個檢視所有的 HandlerMapping 是否有請求的資訊
如果有就找到這個請求對應的 handler
如果沒有就找下一個 HandlerMapping
上面這些 HandlerMapping 都可以在 WebMvcConfiguration 類中找到
HandlerMapping 其實就是儲存那個請求由誰進行處理
5.3.3 普通參數與基本注解
第一部分:注解
- @PathVariable 路徑變量
@GetMapping("/user/{name}")
public Map testParam(@PathVariable String name) {
HashMap<Object, Object> map = new HashMap<>();
map.put("name",name);
return map;
}
- @RequestHeader 擷取請求頭
@GetMapping("/user/{name}")
public Map testParam(@RequestHeader Map<String,String> head) {
HashMap<Object, Object> map = new HashMap<>();
map.put("head",head);
return map;
}
- @RequestParam
@GetMapping("/user1")
public Map testParam(@RequestParam("name") String name) {
HashMap<Object, Object> map = new HashMap<>();
map.put("name",name);
return map;
}
- @CookieValue
@GetMapping("/user1")
public Map testParam(@CookieValue("_ga") String _ga) {
HashMap<Object, Object> map = new HashMap<>();
map.put("_ga",_ga);
return map;
}
- @RequestBody
<form action="/save" method="post">
<input name="name" />
<input name="age" />
<input type="submit" value="送出" />
</form>
@PostMapping("/save")
public Map save(@RequestBody String content) {
HashMap<String, Object> map = new HashMap<>();
map.put("content",content);
return map;
}
- RequestAttribute 擷取到 request 域屬性
模拟頁面的跳轉
@Controller
public class RequestAttributeController {
@GetMapping("/goto")
public String goTo(HttpServletRequest request) {
request.setAttribute("name","張三");
request.setAttribute("age",20);
return "forward:/success";
}
@ResponseBody
@GetMapping("/success")
public Map successTest(@RequestAttribute("name") String name,
@RequestAttribute("age") Integer age,
HttpServletRequest request) {
String nameRequest = (String) request.getAttribute("name");
HashMap<String, Object> map = new HashMap<>();
map.put("nameRequest",nameRequest);
map.put("nameAnnotation", name);
return map;
}
}
測試運作
- MatrixVariable 矩陣變量
矩陣變量需要在 SpringBoot 中手動開啟,還應當綁定在路徑變量中,若是有多個矩陣變量,應當使用英文符号;進行分割,若是一個矩陣變量有多個值,應當使用英文符号進行分割,或者命名多個重複的 key即可。
啟動矩陣變量
在 WebMvcConfiguration 中找到方法
public void configurePathMatch(PathMatchConfigurer configurer) {
,進入url 路徑幫助器
UrlPathHelper urlPathHelper = new UrlPathHelper();
,然後可以看到
private boolean removeSemicolonContent = true;
這裡有一個屬性是移除分号的,預設的true
/**
* 分号要是移除就會把 url 中分号後面的内容全部都去掉,即忽略了參數
* Set if ";" (semicolon) content should be stripped from the request URI.
* <p>Default is "true".
*/
public void setRemoveSemicolonContent(boolean removeSemicolonContent) {
checkReadOnly();
this.removeSemicolonContent = removeSemicolonContent;
}
對于路徑的處理都是 UrlPathHelper 進行解析的,
removeSemicolonContent --移除分号内容
解決方法:
自定義我們自己的 UrlPathHelper
@Controller
// 實作接口,重寫方法,設定為 false
public class WebConfig implements WebMvcConfigurer {
@Override
public void configurePathMatch(PathMatchConfigurer configurer) {
UrlPathHelper urlPathHelper = new UrlPathHelper();
urlPathHelper.setRemoveSemicolonContent(false);
configurer.setUrlPathHelper(urlPathHelper);
}
}
在之前的通路路徑中我們使用
/user/{id}?xxx=xxx&xxx=xxx
,隻用 RequestParam 獲得參數
矩陣變量:
- /cars/sell;low=34;brand=byd,audi,yd
- /cars/sell;low=34;brand=byd;brand=audi;brand=yd
// 1. /cars/sell;low=34;brand=byd,audi,yd 通路路徑方式
/cars/sell;low=34;brand=byd;brand=audi;brand=yd
// 2. SpringBoot 中預設是禁用了矩陣變量功能,需要手動開啟矩陣變量的url 的路徑變量才能被解析
// 3. 矩陣變量必須有 url 路徑變量才能被解析,如果直接寫路徑會找到 404
@GetMapping("/cars/{path}")
public Map carsSell(@MatrixVariable("low") Integer low,
@MatrixVariable("brand") List<String> brand) {
HashMap<String, Object> map = new HashMap<>();
map.put("low",low);
map.put("brand",brand);
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) {
HashMap<Object, Object> map = new HashMap<>();
map.put("boossAge",bossAge);
map.put("empAge",empAge);
return map;
}
5.3.4 各種類型參數解析原理分析
進入 DispatcherServlet 類中,找到 doDispatch 方法,然後
首先在 HandlerMapping 中找到處理請求的 Handler,為目前 Handler 找一個擴充卡 HandlerAdapter
// Determine handler adapter for the current request.
// 決定一個 Handler 的擴充卡為目前請求
// 在此之前我們已經找到那個方法能夠處理這個請求了
// SpringMVC 需要在底層通過反射調用controller 中的方法,以及一大堆的參數,SpringBoot 就把這些封裝到了 HandlerAdapter 中,相當于這就是一個大的反射工具。
HandlerAdapter ha = getHandlerAdapter(mappedHandler.getHandler());
然後找到了 4 個 HandlerAdapter(處理器擴充卡,可以完成不同的功能)
- RequestMappingHandlerAdapter
支援方法上标注 @RequestMapping 這些注解的擴充卡
- HandlerFunctionAdapter
支援函數式程式設計的擴充卡
進入 supports 方法,這裡會把 handler 封裝一個 HandlerMethod
@Override
public final boolean supports(Object handler) {
return (handler instanceof HandlerMethod && supportsInternal((HandlerMethod) handler));
}
傳回的是一個 RequestMappingHandlerAdapter
至此,找到的請求的擴充卡
DiapatcherServlet 的 doDispatche 方法中
- 執行目标方法
// Actually invoke the handler.
// 真正的執行 handler
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
進去 ha.handle,然後進入 handlerInternal
對于目标方法的真正執行都在 RequestMappingHandlerAdapter 類的 handleInternal 方法中
向下走到 invokeHandlerMethod 方法
// No synchronization on session demanded at all...
// 執行目标方法
mav = invokeHandlerMethod(request, response, handlerMethod);
-
參數解析argumentResolvers
進到 invokeHandlerMethod 方法可以看到 27 個參數解析器
argumentResolvers
執行目标方法的核心關鍵會設定參數解析器,将來目标方法的每一個參數值是什麼是由這個參數解析器确定的,确定将要執行的目标方法的每一個參數值是什麼
SpringMVC 目标方法能寫多少種參數類型,取決于參數解析器
這個參數解析器其實就是一個接口:HandlerMethodArgumentResolver,
這個接口中,接口第一個方法 supportsParameter 判斷接口是否支援這個方法,即目前解析器支援解析那種參數。
如果支援就調用 resolveArgument 解析方法進行解析
- returnValueHandlers 傳回值處理器
從這裡我們可以看到目标方法可以寫多少種類型的傳回值
SpringMVC 會提前把參數解析器和傳回值處理器都放到一個目标方法包裝的 ServletInvocableHandlerMethod 這個可執行的方法中
向下找到方法
invocableMethod.invokeAndHandle(webRequest, mavContainer);
執行并處理方法來執行目标方法,invocableMethod 裡面封裝了各種處理器
public void invokeAndHandle(ServletWebRequest webRequest, ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 這個方法執行的時候進入目标方法,然後再向下執行,真正執行目标方法
Object returnValue = invokeForRequest(webRequest, mavContainer, providedArgs);
setResponseStatus(webRequest);
if (returnValue == null) {
if (isRequestNotModified(webRequest) || getResponseStatus() != null || mavContainer.isRequestHandled()) {
disableContentCachingIfNecessary(webRequest);
mavContainer.setRequestHandled(true);
return;
}
}
else if (StringUtils.hasText(getResponseStatusReason())) {
mavContainer.setRequestHandled(true);
return;
}
進入
invokeForRequest(webRequest, mavContainer, providedArgs);
方法中
@Nullable
public Object invokeForRequest(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 擷取方法的所有參數的值 确定方法參數值
Object[] args = getMethodArgumentValues(request, mavContainer, providedArgs);
if (logger.isTraceEnabled()) {
logger.trace("Arguments: " + Arrays.toString(args));
}
return doInvoke(args);
}
// 真正的如何确定目标方法的每一個值
protected Object[] getMethodArgumentValues(NativeWebRequest request, @Nullable ModelAndViewContainer mavContainer,
Object... providedArgs) throws Exception {
// 擷取所有參數的參數聲明
MethodParameter[] parameters = getMethodParameters();
if (ObjectUtils.isEmpty(parameters)) {
return EMPTY_ARGS;
}
// 所有目标方法确定好的值
Object[] args = new Object[parameters.length];
// 周遊所有參數
for (int i = 0; i < parameters.length; i++) {
MethodParameter parameter = parameters[i];
parameter.initParameterNameDiscovery(this.parameterNameDiscoverer);
// 給 args 指派
args[i] = findProvidedArgument(parameter, providedArgs);
if (args[i] != null) {
continue;
}
// 首先判斷目前解析器是否支援這中參數類型
if (!this.resolvers.supportsParameter(parameter)) {
throw new IllegalStateException(formatArgumentError(parameter, "No suitable resolver"));
}
try {
//
args[i] = this.resolvers.resolveArgument(parameter, mavContainer, request, this.dataBinderFactory);
}
catch (Exception ex) {
// Leave stack trace for later, exception may actually be resolved and handled...
if (logger.isDebugEnabled()) {
String exMsg = ex.getMessage();
if (exMsg != null && !exMsg.contains(parameter.getExecutable().toGenericString())) {
logger.debug(formatArgumentError(parameter, exMsg));
}
}
throw ex;
}
}
// 所有目标方法确定好的值
return args;
}
确定目标方法的參數值
首先周遊判斷所有參數解析器那個支援解析這個參數
@Nullable
private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
if (result == null) {
for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
if (resolver.supportsParameter(parameter)) {
result = resolver;
this.argumentResolverCache.put(parameter, result);
break;
}
}
}
return result;
}
解析參數的值
@Override
@Nullable
public final Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,
NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
NamedValueInfo namedValueInfo = getNamedValueInfo(parameter);
MethodParameter nestedParameter = parameter.nestedIfOptional();
// 得到參數變量
Object resolvedName = resolveEmbeddedValuesAndExpressions(namedValueInfo.name);
if (resolvedName == null) {
throw new IllegalArgumentException(
"Specified name must not resolve to null: [" + namedValueInfo.name + "]");
}
// 确定值
Object arg = resolveName(resolvedName.toString(), nestedParameter, webRequest);
if (arg == null) {
if (namedValueInfo.defaultValue != null) {
arg = resolveEmbeddedValuesAndExpressions(namedValueInfo.defaultValue);
}
else if (namedValueInfo.required && !nestedParameter.isOptional()) {
handleMissingValue(namedValueInfo.name, nestedParameter, webRequest);
}
arg = handleNullValue(namedValueInfo.name, arg, nestedParameter.getNestedParameterType());
}
else if ("".equals(arg) && namedValueInfo.defaultValue != null) {
所有支援的注解的類型
HandlerMethodArgumentResolver
5.3.5 Servlet API
WebRequest、ServletRequest、MultipartRequest、HttpSession、java.servlet.http.PushBuilder、Principal、InputStream、Reader、HttpMethod、Locale、TimeZone、Zoneld
ServletRequestMethodArgumentResolver
以上的部分參數
@Override
public boolean supportsParameter(MethodParameter parameter) {
Class<?> paramType = parameter.getParameterType();
return (WebRequest.class.isAssignableFrom(paramType) ||
ServletRequest.class.isAssignableFrom(paramType) ||
MultipartRequest.class.isAssignableFrom(paramType) ||
HttpSession.class.isAssignableFrom(paramType) ||
(pushBuilder != null && pushBuilder.isAssignableFrom(paramType)) ||
Principal.class.isAssignableFrom(paramType) ||
InputStream.class.isAssignableFrom(paramType) ||
Reader.class.isAssignableFrom(paramType) ||
HttpMethod.class == paramType ||
Locale.class == paramType ||
TimeZone.class == paramType ||
ZoneId.class == paramType);
}
5.3.6 複雜參數
Map、Model(map、model裡面的資料會被放在request的請求域 request.setAttribute)、Erros/BindingResult、RedirectAttributes(重定向攜帶資料)、ServletResponse(response 響應)、SessionStatus、UriComponentsBuilder、ServletUriComponentsBuilder
Map<String,Object> map, Model model, HttpServletRequest request
// 都是可以給 request 域中放資料,以後友善 request.Attribute 擷取
原理
Map、Model 類型的參數,會傳回 mavContainer.getModel(); --> BindinigAwareModelMap 是 Model 也是 Map
無論是 Map 還是 Model 類型最終都會調用這個方法 mavContainer.getModel(); 擷取到值的
public class ModelAndViewContainer {
private boolean ignoreDefaultModelOnRedirect = false;
@Nullable
private Object view;
private final ModelMap defaultModel = new BindingAwareModelMap();
解析完參數後會進行轉發,
InvocableHandlerMethod.java
類中執行 this.returnValueHandlers.hanleReturnValue 進行傳回值的處理
解析參數的值後,将所有的資料都放在 ModelAndViewContainer中,包含要去的頁面位址 View,還包括 Model 資料。
5.4 視圖解析與模闆引擎
視圖解析就是 SpringBoot 在處理完請求之後來跳轉到某個頁面的這個過程。
視圖解析:因為 SpringBoot 預設打包方式是一個jar包即壓縮包,jsp 不支援打包成壓縮包,是以 SpringBoot 預設不支援 jsp,需要引入第三方模闆引擎技術實作頁面的渲染
5.4.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)
-
視圖解析:
- 傳回值以 forward: 開始:new InternalResourceView(forwardUrl); --> 轉發 request.getRequestDispatcher(path).forward(request,response);
- 傳回值以 redirect:開始:new RedirectView() --> render 就是重定向
- 傳回值是普通字元串:new ThymeleafView() --> 自定義視圖解析器、自定義視圖
5.4.2 thymeleaf 模闆引擎
官網:thymeleaf.org
Thymeleaf is a modern server-side Java template engine for both web and standalone environments, capable of
processing HTML, XML, JavaScript, CSS and even plain text.
thymeleaf 是一個現代化的服務端的 Java 模闆引擎。
5.4.3 thymeleaf 基礎使用文法
- 表達式
表達式名字 | 文法 | 用途 |
---|---|---|
變量取值 | ${} | 擷取請求域、session域、對象等值 |
選擇變量 | *{} | 擷取上下文對象值 |
消息 | #{} | 擷取國際化等值 |
連結 | @{} | 生成連結 |
片段表達式 | ~{} | jsp:include 作用,引入公共頁面片段 |
- 字面量
- 文本值:‘text’
- 數字:0,44,3.3
- 布爾值:true,false
- 空值:null
- 變量:value,key
-
文本操作
字元串拼接:+
變量替換:|My name is $(name)|
-
數字運算
+,-,*,/,%
-
布爾運算
and,or,!,not
-
比較運算
< > >= <= (gt,lt,ge,le)
等式:== != (eq,ne)
-
條件運算
if-then:(if)?(then)
if-then-else:(if)?(then):(else)
Default:(value)?:(defaultvalue)
- 設定屬性值 th:attr
- 設定單個值
<form action="subscribe.html" th:attr="[email protected]{/subscribe}">
<fieldset>
<input type="text" name="email" />
<input type="submit" value="Subscribe!" th:attr="value=#{subscribe.submit}"/>
</fieldset>
</form>
- 設定多個值
- 疊代
<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>
- 條件運算
<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.4.4 SpringBoot 中使用 thymelef 模闆引擎
- 建立項目
- 引入 xml 啟動器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
- 引入 啟動器後 SpringBoot 會為我們自動配置好 thymeleaf
@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ThymeleafProperties.class)
@ConditionalOnClass({ TemplateMode.class, SpringTemplateEngine.class })
@AutoConfigureAfter({ WebMvcAutoConfiguration.class, WebFluxAutoConfiguration.class })
public class ThymeleafAutoConfiguration {
自動配置好的東西,在 ThymeleafAutoConfiguration 類中可以看到
- SpringBoot 配置好了
private final ThymeleafProperties properties;
- 配置好了模闆引擎:
SpringTemplateEngine engine = new SpringTemplateEngine();
- 配置好了視圖解析器:
ThymeleafViewResolver
使用的時候隻需要開發頁面就可以了。
在
ThymeleafProperties
類中可以看到已經配置好的字首和字尾
@ConfigurationProperties(prefix = "spring.thymeleaf")
public class ThymeleafProperties {
private static final Charset DEFAULT_ENCODING = StandardCharsets.UTF_8;
// 字首,templates 這個檔案夾在建立項目的時候檔案夾已經建立好了
public static final String DEFAULT_PREFIX = "classpath:/templates/";
// 字尾,預設都是 xxxx.html 頁面
public static final String DEFAULT_SUFFIX = ".html";
- 編寫 Controller 實作頁面的跳轉
@Controller
public class ViewController {
@GetMapping("/thymeleaf")
public String toPage(Model model) {
// model 中的資料會被放到請求域中,request.setAttribute("xxx","xxx");
model.addAttribute("message","Hello,World");
model.addAttribute("url","http://www.baidu.com");
return "success";
}
}
- html 頁面
<!--使用前需要引入thymeleaf 的命名空間 xmlns:th="http://www.thymeleaf.org"-->
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1 th:text="${message}">Thymeleaf 你好</h1>
<!--這塊沒有使用 @ 符号取值是引入我們從controller 傳來的就是一個值-->
<!--如果使用 @ 就是對通路路徑的拼接-->
<a href="www.baidu.com" th:href="${url}">百度</a>
</body>
</html>
5.5 攔截器
public interface HandlerInterceptor {
default boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
throws Exception {
return true;
}
default void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable ModelAndView modelAndView) throws Exception {
}
default void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler,
@Nullable Exception ex) throws Exception {
}
}
使用步驟
- 編寫一個攔截器實作
接口,攔截器中寫上攔截規則HandlerInterceptor
/**
* 登入檢查
* 1. 配置好攔截器要攔截那些請求
* 2. 把這些配置放在容器中
*/
public class LoginInterceptor implements HandlerInterceptor {
/**
* 目标方法執行之前
* @param request
* @param response
* @param handler
* @return
* @throws Exception
*/
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 登入檢查
HttpSession session = request.getSession();
Object loginUser = session.getAttribute("loginUser");
if (loginUser != null) {
// 放行
return true;
}
// 攔截,攔截住的都是為登入的,跳轉都登陸頁
request.setAttribute("msg","請登入後請求");
request.getRequestDispatcher("/").forward(request,response);
return false;
}
/**
* 目标方法執行完成之後
* @param request
* @param response
* @param handler
* @param modelAndView
* @throws Exception
*/
@Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
}
/**
* 頁面渲染以後
* @param request
* @param response
* @param handler
* @param ex
* @throws Exception
*/
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
}
}
- 攔截器注冊到容器中(實作 WebMvcConfigurer 的 addInterceptors),指定攔截規則【如果是攔截所有,靜态資源也會被攔截】
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginInterceptor())
.addPathPatterns("/**") // 所有請求都被攔截,包括靜态資源
.excludePathPatterns("/","/login","/css/**","/data/**","/font-awesome/**","images/**","/js/**","/lib/**","/plugins/**");
}
}
5.6 檔案上傳
- html
檔案上傳表單
<form th:action="@{/upload}" method="post" enctype="multipart/form-data">
<label>單個檔案</label>
<input type="file" name="headImage">
<label>多個檔案</label>
<input type="file" name="photos" multiple>
<input type="submit" value="送出">
</form>
- Controller
/**
* 測試檔案上傳
*/
@Slf4j
@Controller
public class FormController {
@GetMapping("/form")
public String toFormPage() {
return "form/forms-upload";
}
/**
* MultipartFile 自動封裝上傳過來的檔案
*
* @param name
* @param age
* @param headImage
* @param photos
* @return
*/
@PostMapping("/upload")
public String upload(@RequestParam("name") String name,
@RequestParam("age") Integer age,
@RequestPart("headImage") MultipartFile headImage,
@RequestPart("photos") MultipartFile[] photos) throws IOException {
log.info("name={},age={},headImage={},photoSize={}", name, age, headImage.getSize(), photos.length);
if (!headImage.isEmpty()) {
// 儲存到檔案伺服器:OSS 伺服器
String filename = headImage.getOriginalFilename();
headImage.transferTo(new File("D:\\" + filename));
}
if (photos.length > 0) {
for (MultipartFile photo : photos) {
if (!photo.isEmpty()) {
String filename1 = photo.getOriginalFilename();
photo.transferTo(new File("D:\\" + filename1));
}
}
}
return "main";
}
}
- 修改檔案上傳大小
spring.servlet.multipart.max-file-size=10MB
spring.servlet.multipart.max-request-size=100MB
檔案上傳原理
檔案上傳的所有配置都被封裝到了
MultipartAutoConfiguration
類裡面了
檔案上傳所有的配置被封裝到了
MultipartProperties.class
中
自動配置好了檔案上傳解析器
StandardServletMultipartResolver
@Bean(name = DispatcherServlet.MULTIPART_RESOLVER_BEAN_NAME) // MULTIPART_RESOLVER_BEAN_NAME = multipartResolver
@ConditionalOnMissingBean(MultipartResolver.class) // 如果類中沒有自定義配置的時候生效
// 檔案上傳解析器,隻能上傳标準的以 Servlet 方式上傳的檔案
public StandardServletMultipartResolver multipartResolver() {
StandardServletMultipartResolver multipartResolver = new StandardServletMultipartResolver();
multipartResolver.setResolveLazily(this.multipartProperties.isResolveLazily());
return multipartResolver;
}
首先找到
DispatcherServlet
的
doDispatch
方法中
boolean multipartRequestParsed = false;
記錄一下檔案上傳是否已經被解析了
processedRequest = checkMultipart(request);
判斷目前請求是不是一個檔案上傳請求,如果是把這個 request 包裝,包裝成一個 processedRequest。進去之後可以看到詳情
protected HttpServletRequest checkMultipart(HttpServletRequest request) throws MultipartException {
// this.multipartResolver.isMultipart(request) 判斷目前是不是檔案上傳請求,全系統隻有一個
if (this.multipartResolver != null && this.multipartResolver.isMultipart(request)) {
if (WebUtils.getNativeRequest(request, MultipartHttpServletRequest.class) != null) {
if (DispatcherType.REQUEST.equals(request.getDispatcherType())) {
logger.trace("Request already resolved to MultipartHttpServletRequest, e.g. by MultipartFilter");
}
}
else if (hasMultipartException(request)) {
logger.debug("Multipart resolution previously failed for current request - " +
"skipping re-resolution for undisturbed error rendering");
}
else {
try {
return this.multipartResolver.resolveMultipart(request);
}
catch (MultipartException ex) {
if (request.getAttribute(WebUtils.ERROR_EXCEPTION_ATTRIBUTE) != null) {
logger.debug("Multipart resolution failed for error dispatch", ex);
// Keep processing error dispatch with regular request handle below
}
else {
throw ex;
}
}
}
}
// If not returned before: return original request.
return request;
}
進去
isMultipart
方法
@Override
public boolean isMultipart(HttpServletRequest request) {
return StringUtils.startsWithIgnoreCase(request.getContentType(),
// 判斷上傳是否是 multipart/
(this.strictServletCompliance ? MediaType.MULTIPART_FORM_DATA_VALUE : "multipart/"));
}
因為上面的 multipart/ 判斷,是以在上傳檔案的表單中必須寫
<form th:action="@{/upload}" method="post" enctype="multipart/form-data"></form>
5.7 異常處理
5.7.1 錯誤處理
- 預設規則
預設情況下,Spring Boot 會提供 /error 處理所有錯誤的映射
對于機器用戶端,它将生成 JSON 響應,其中包含錯誤,HTTP 狀态和異常消息的詳細資訊,對于浏覽器用戶端,響應一個 whitelabel 錯誤視圖,以 HTML 格式呈現相同的資料。
- 浏覽器端
要對其進行自定義,添加 View 解析為 error
要完全替換預設行為,可以實作
ErrorController
并注冊該類型的 Bean 定義,或添加
ErrorAttributes
類型的元件以使用現有機制來替換其内容。
如果我們想要自定義錯誤頁面,在 public 檔案夾下或者 templates 檔案夾下建立 error檔案夾,在檔案夾建立錯誤頁面(4xx.html,5xx.html),這裡的錯誤檔案會被自動解析
- 定制錯誤處理邏輯
- 自定義錯誤頁面
- error/404.html error/5xx.html 有精确的錯誤狀态頁面就精确比對,沒有就找到 4xx.html,如果都沒有就觸發白頁
- ControllerAdvice + @ExceptionHandler 處理全局異常,底層是
提供的處理支援ExceptionHandlerExceptionResolver
/**
* 處理整個 web controller 的異常
*/
@Slf4j
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler({ArithmeticException.class,NullPointerException.class})// 處理異常
public String mathException(Exception e) {
log.info("異常{}",e);
return "login";// 處理異常後跳轉的視圖位址
}
}
- ResponseStatus + 自定義異常
底層是
ResponseStatusExceptionResolver
,把 responseStatus 注解的資訊底層調用 response.sendError(statusCode,resolverReason); tomcat 發送的 /error
/**
* 自定義異常類,當 throw 抛出此異常的時候給出狀态資訊,異常資訊
*/
@ResponseStatus(value = HttpStatus.FORBIDDEN, reason = "使用者數量太多")
public class UserTooManyException extends RuntimeException{
public UserTooManyException() {
}
public UserTooManyException(String message) {
super(message);
}
}
// Controller 中 模拟異常
@GetMapping("/form")
public String toFormPage() {
if (3 > 1) {
// 抛出異常
throw new UserTooManyException();
}
return "form/forms-upload";
}
抛出異常的時候會跳轉到 404 頁面給出提示資訊 message
- Spring 底層的異常,如 參數類型轉換異常
DefaultHandlerExceptionResolver
處理架構底層的異常
response.sendError(HttpServletResponse.SC_BAD_REQUEST,ex.getMessage());
- 自定義實作 HandlerExceptionResolver 處理異常,可以作為預設的全局異常處理規則
@Order(value = Ordered.HIGHEST_PRECEDENCE) // 優先級,數字越小優先級越高
@Component
public class CustomerHandlerExceptionResolver implements HandlerExceptionResolver {
@Override
public ModelAndView resolveException(HttpServletRequest request,
HttpServletResponse response,
Object handler,
Exception ex) {
try {
response.sendError(500,"我的錯誤資訊");
} catch (IOException e) {
e.printStackTrace();
}
return new ModelAndView();
}
}
-
ErrorViewResolver 實作自定義處理異常
response.sendError error 請求就會轉發給 Controller
你的異常沒有任何人能處理。tomcat 底層 response.sendError error 請求就會轉給 Controller
basicErrorController 要去的頁面位址是 ErrorViewResolver
- 異常處理自動配置
ErrorMvcAutoConfiguration
自動配置了異常處理規則
@AutoConfigureBefore(WebMvcAutoConfiguration.class)
// 綁定了一些配置檔案
@EnableConfigurationProperties({ ServerProperties.class, WebMvcProperties.class })
public class ErrorMvcAutoConfiguration {
@Bean
// 當容器中沒有這個元件的時候生效,容器中放入的元件 ,類型 DefaultErrorAttributes id 為 errorAttributes
@ConditionalOnMissingBean(value = ErrorAttributes.class, search = SearchStrategy.CURRENT)
public DefaultErrorAttributes errorAttributes() {
return new DefaultErrorAttributes();
}
@Bean
// 容器中放入的元件,類型:BasicErrorController id:basicErrorController
@ConditionalOnMissingBean(value = ErrorController.class, search = SearchStrategy.CURRENT)
public BasicErrorController basicErrorController(ErrorAttributes errorAttributes,
ObjectProvider<ErrorViewResolver> errorViewResolvers) {
return new BasicErrorController(errorAttributes, this.serverProperties.getError(),
errorViewResolvers.orderedStream().collect(Collectors.toList()));
}
- BasicErrorController進去之後發現,響應 JSON 和 白頁适配響應
@Controller
@RequestMapping("${server.error.path:${error.path:/error}}") // 處理預設 /error 路徑的請求
public class BasicErrorController extends AbstractErrorController {
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
HttpStatus status = getStatus(request);
Map<String, Object> model = Collections
.unmodifiableMap(getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.TEXT_HTML)));
response.setStatus(status.value());
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
return (modelAndView != null) ? modelAndView : new ModelAndView("error", model);// 頁面響應
}
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
HttpStatus status = getStatus(request);
if (status == HttpStatus.NO_CONTENT) {
return new ResponseEntity<>(status);
}
Map<String, Object> body = getErrorAttributes(request, getErrorAttributeOptions(request, MediaType.ALL));
return new ResponseEntity<>(body, status);
}
要麼響應頁面,要麼把 ResponseEntity 中的資料響應出去,相當于一個 json
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty(prefix = "server.error.whitelabel", name = "enabled", matchIfMissing = true)
@Conditional(ErrorTemplateMissingCondition.class)
protected static class WhitelabelErrorViewConfiguration {
private final StaticView defaultErrorView = new StaticView();
// 容器中還會有一個 view 元件,這個元件的id 叫做 error
@Bean(name = "error")
@ConditionalOnMissingBean(name = "error")
public View defaultErrorView() {
return this.defaultErrorView;
}
// If the user adds @EnableWebMvc then the bean name view resolver from
// WebMvcAutoConfiguration disappears, so add it back in to avoid disappointment.
@Bean
@ConditionalOnMissingBean
// 為了解析 view 視圖,配置了一個 BeanNameViewResolver 的視圖解析器
// 按照傳回的視圖名(error)作為元件的 id 去容器中找 View 對象
// 隻要請求發到 /error 路徑,就會找 error 視圖,error 視圖又是 View 中的一個元件,利用視圖解析器找到 error 視圖,最終 View 渲染的是什麼樣,頁面就是什麼樣
public BeanNameViewResolver beanNameViewResolver() {
BeanNameViewResolver resolver = new BeanNameViewResolver();
resolver.setOrder(Ordered.LOWEST_PRECEDENCE - 10);
return resolver;
}
}
如果想要傳回頁面,就會找 error 視圖,預設是一個百頁
// 寫出去的是 JSON
@RequestMapping
public ResponseEntity<Map<String, Object>> error(HttpServletRequest request) {
// 錯誤視圖、錯誤頁
@RequestMapping(produces = MediaType.TEXT_HTML_VALUE)
public ModelAndView errorHtml(HttpServletRequest request, HttpServletResponse response) {
-
DefaultErrorViewResolverConfiguration
錯誤視圖解析器元件
進去
DefaultErrorViewResolver
public class DefaultErrorViewResolver implements ErrorViewResolver, Ordered {
private static final Map<Series, String> SERIES_VIEWS;
static {
Map<Series, String> views = new EnumMap<>(Series.class);
// 如果是用戶端錯誤就是 4xx
views.put(Series.CLIENT_ERROR, "4xx");
// 如果是服務端錯誤就是 5xx
views.put(Series.SERVER_ERROR, "5xx");
SERIES_VIEWS = Collections.unmodifiableMap(views);
}
@Override
// 解析得到視圖對象
public ModelAndView resolveErrorView(HttpServletRequest request, HttpStatus status, Map<String, Object> model) {
ModelAndView modelAndView = resolve(String.valueOf(status.value()), model);
if (modelAndView == null && SERIES_VIEWS.containsKey(status.series())) {
modelAndView = resolve(SERIES_VIEWS.get(status.series()), model);
}
return modelAndView;
}
// 如果發生錯誤,會以 HTTP 的狀态碼作為試圖頁面位址
// viewName 得到的其實是一個狀态碼,如果是 404錯誤就會找 error/404.html 的頁面
private ModelAndView resolve(String viewName, Map<String, Object> model) {
String errorViewName = "error/" + viewName;
TemplateAvailabilityProvider provider = this.templateAvailabilityProviders.getProvider(errorViewName,
this.applicationContext);
if (provider != null) {
return new ModelAndView(errorViewName, model);
}
return resolveResource(errorViewName, model);
}
-
定義了最終錯誤裡面可以包含的那些内容DefaultErrorAttributes
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 儲存錯誤的預設屬性 status trace exception ......
this.storeErrorAttributes(request, ex);
return null;
}
- 異常處理流程
- 執行目标方法,目标方法運作期間有任何異常都會被 catch,并且用 dispatchException 進行封裝,标志目前請求結束
DispatcherServlet 的 doDispatcher 方法中可以看到
mv = ha.handle(processedRequest, response, mappedHandler.getHandler());
if (asyncManager.isConcurrentHandlingStarted()) {
return;
}
applyDefaultViewName(processedRequest, mv);
mappedHandler.applyPostHandle(processedRequest, response, mv);
}
catch (Exception ex) {
dispatchException = ex;
}
catch (Throwable err) {
// As of 4.3, we're processing Errors thrown from handler methods as well,
// making them available for @ExceptionHandler methods and other scenarios.
dispatchException = new NestedServletException("Handler dispatch failed", err);
}
-
當異常被捕獲之後進入視圖解析流程(頁面渲染流程)
mappedHandler:那個 Controller 處理器
mv:隻有目标方法正确執行了才有值
dispatchException:目标方法中存在的異常
processDispatchResult(processedRequest, response, mappedHandler, mv, dispatchException);
}
catch (Exception ex) {
triggerAfterCompletion(processedRequest, response, mappedHandler, ex);
}
catch (Throwable err) {
triggerAfterCompletion(processedRequest, response, mappedHandler,
new NestedServletException("Handler processing failed", err));
}
finally {
if (asyncManager.isConcurrentHandlingStarted()) {
// Instead of postHandle and afterCompletion
if (mappedHandler != null) {
mappedHandler.applyAfterConcurrentHandlingStarted(processedRequest, response);
}
}
else {
// Clean up any resources used by a multipart request.
if (multipartRequestParsed) {
cleanupMultipart(processedRequest);
}
}
}
private void processDispatchResult(HttpServletRequest request, HttpServletResponse response,
@Nullable HandlerExecutionChain mappedHandler, @Nullable ModelAndView mv,
@Nullable Exception exception) throws Exception {
boolean errorView = false;
// execption 儲存的異常,如果異常不為空則執行下面代碼
if (exception != null) {
// 判斷異常是不是定義資訊異常
if (exception instanceof ModelAndViewDefiningException) {
logger.debug("ModelAndViewDefiningException encountered", exception);
mv = ((ModelAndViewDefiningException) exception).getModelAndView();
}
else {
Object handler = (mappedHandler != null ? mappedHandler.getHandler() : null);
// 處理 handler 的異常,處理結果儲存為一個 mv(ModelAndView)
mv = processHandlerException(request, response, handler, exception);
errorView = (mv != null);
}
}
-
mv = processHandlerException
處理 handler 發生的異常,處理完成傳回 mv(ModelAndView)
進去方法
@Nullable
protected ModelAndView processHandlerException(HttpServletRequest request, HttpServletResponse response,
@Nullable Object handler, Exception ex) throws Exception {
// Success and error responses may use different content types
request.removeAttribute(HandlerMapping.PRODUCIBLE_MEDIA_TYPES_ATTRIBUTE);
// Check registered HandlerExceptionResolvers...
ModelAndView exMv = null; // 首先定義一個 ModelAndView
if (this.handlerExceptionResolvers != null) {
// 周遊所有的 HandlerExceptionResolver,檢視誰能處理目前異常,處理器異常解析器
for (HandlerExceptionResolver resolver : this.handlerExceptionResolvers) {
exMv = resolver.resolveException(request, response, handler, ex);
if (exMv != null) {
break;
}
}
}
if (exMv != null) {
if (exMv.isEmpty()) {
request.setAttribute(EXCEPTION_ATTRIBUTE, ex);
return null;
}
// We might still need view name translation for a plain error model...
if (!exMv.hasView()) {
String defaultViewName = getDefaultViewName(request);
if (defaultViewName != null) {
exMv.setViewName(defaultViewName);
}
}
if (logger.isTraceEnabled()) {
logger.trace("Using resolved error view: " + exMv, ex);
}
else if (logger.isDebugEnabled()) {
logger.debug("Using resolved error view: " + exMv);
}
WebUtils.exposeErrorRequestAttributes(request, ex, getServletName());
return exMv;
}
throw ex;
}
- 系統中預設的異常解析器
在上面的異常的自動配置的時候就放了一個
DefaultErrorAttributes
元件,其實就是一個 Handler 的異常處理器,專門處理異常
-
DefaultErrorAttributes
調用接口方法處理異常
public ModelAndView resolveException(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 儲存 errorAttributes 錯誤的屬性資訊
this.storeErrorAttributes(request, ex);
// 傳回 null
return null;
}
private void storeErrorAttributes(HttpServletRequest request, Exception ex) {
// 給 request 域中 ERROR_INTERNAL_ATTRIBUTE 屬性
request.setAttribute(ERROR_INTERNAL_ATTRIBUTE, ex);
}
預設沒有任何人能夠處理異常,則異常會被抛出,如果沒有任何能處理,則底層會發送 /error 請求
發送 /error 請求後會被底層的
BasicErrorController
進行處理
// 解析錯誤視圖,包括錯誤的狀态請求資料等資訊
ModelAndView modelAndView = resolveErrorView(request, response, status, model);
protected ModelAndView resolveErrorView(HttpServletRequest request, HttpServletResponse response, HttpStatus status,
Map<String, Object> model) {
// 周遊所有的 ErrorViewResolver 檢視誰能解析,如果能解析則封裝 ModelAndView
for (ErrorViewResolver resolver : this.errorViewResolvers) {
ModelAndView modelAndView = resolver.resolveErrorView(request, status, model);
if (modelAndView != null) {
return modelAndView;
}
}
return null;
}
預設隻有一個
DefaultErrorViewResolver
,就是之前在
ErrorMvcAutoConfiguration
中放入到元件
DefaultErrorViewResolver 作用就是把響應狀态碼作為錯誤頁的位址拼接成 error/5xx.html,最終把模闆引擎響應這個頁面
‘//error/404.’
‘//error/404.html’
‘//error/4xx.’
‘//error/4xx.html’
5.8 Web 原生元件注入(Servlet、Filter、Listener)
如何在使用 Spring Boot 的過程中注入 web 的原生元件(Servlet、Filter、Listener)
官網:https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.developing-web-applications.embedded-container.servlets-filters-listeners
在之前 SpringMVC 要使用這些元件,需要把這些元件寫好之後配置在 web.xml 檔案中
5.8.1 使用 Servlet API
When using an embedded container, automatic registration of classes annotated with
@WebServlet
,
@WebFilter
, and
@WebListener
can be enabled by using
@ServletComponentScan
.
編寫一些 servelt ,然後在主啟動類上使用注解
@ServletComponentScan
// 指定原生 Servlet 元件都放在哪裡
@ServletComponentScan(basePackages = "com.thymeleaf")
@SpringBootApplication
public class ThymeleafApplication {
public static void main(String[] args) {
SpringApplication.run(ThymeleafApplication.class, args);
}
}
Servlet
// 直接響應,沒有經過 Spring 的攔截器
@WebServlet(urlPatterns = "/myservlet")
public class MyServlet extends HttpServlet {
@Override
protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
resp.getWriter().write("resources Servlet");
}
}
-
為什麼自己寫的 MyServlet 映射的路徑直接相應,而不會經過 Spring 的攔截器?
從整個系統來看,一共有兩個 Serlvet,
一個是自定義的 MyServlet,它要處理的路徑是 /myservlet 路徑
另一個是 DispatcherServlet,它處理的路徑是 / 路徑
擴充:DispatcherServlet 如何注冊的
- 容器中自動配置了 DispatcherServlet,屬性綁定到 WebMvcProperties 中,對應的配置項部分是
spring.mvc
- 然後通過
把 DispatcherServlet 配置進來ServletRegistrationBean<DispatcherServlet>
- 多個請求路徑的話會采用精确優先原則
DispatcherServletRegistrationBean registration = new DispatcherServletRegistrationBean(dispatcherServlet, webMvcProperties.getServlet().getPath()); 這裡的 getPath() 進去之後找到的是就是 / 路徑 private String path = "/";
- 容器中自動配置了 DispatcherServlet,屬性綁定到 WebMvcProperties 中,對應的配置項部分是
Filter
@Slf4j
@WebFilter(urlPatterns = {"/myservlet"})// 要攔截的 url 位址
public class MyFilter extends HttpFilter {
@Override
public void init(FilterConfig filterConfig) throws ServletException {
log.info("MyFilter初始化完成");
}
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
log.info("MyFilter工作");
chain.doFilter(request,response);
}
@Override
public void destroy() {
log.info("MyFilter銷毀");
}
}
Listener
@Slf4j
@WebListener
public class MyServletContextListener implements ServletContextListener {
@Override
public void contextInitialized(ServletContextEvent sce) {
log.info("MyServletContextListener監聽到初始化項目完成");
}
@Override
public void contextDestroyed(ServletContextEvent sce) {
log.info("MyServletContextListener監聽到項目摧毀");
}
}
5.8.2 使用 RegistrationBean
If convention-based mapping is not flexible enough, you can use the
ServletRegistrationBean
,
FilterRegistrationBean
, and
ServletListenerRegistrationBean
classes for complete control.
@Configuration(proxyBeanMethods = true) // 保證依賴的元件始終是單執行個體的
public class MyRegistConfig {
@Bean
public ServletRegistrationBean myServlet() {
MyServlet myServlet = new MyServlet();
// 傳入參數,1.自定剛才建立好的 MyServlet 類,2.通路的路徑
return new ServletRegistrationBean(myServlet, "/myservlet", "/myservlet1");
}
@Bean
public FilterRegistrationBean myFilter() {
MyFilter myFilter = new MyFilter();
// 第一個參數是自定義的 MyFilter類,第二個參數是元件中的 myServlet,表示攔截的是 myServlet元件的通路路徑
// return new FilterRegistrationBean(myFilter,myServlet());
// 攔截指定的路徑
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(myFilter);
filterRegistrationBean.setUrlPatterns(Arrays.asList("/myservlet", "/css/*"));
return filterRegistrationBean;
}
@Bean
public ServletListenerRegistrationBean myServletListenerRegistration() {
MyServletContextListener myServletContextListener = new MyServletContextListener();
return new ServletListenerRegistrationBean(myServletContextListener);
}
}
5.9 嵌入式 Servlet 容器
5.9.1 切換嵌入式 Servlet 容器
Under the hood, Spring Boot uses a different type of
ApplicationContext
for embedded servlet container support. The
ServletWebServerApplicationContext
is a special type of
WebApplicationContext
that bootstraps itself by searching for a single
ServletWebServerFactory
bean. Usually a
TomcatServletWebServerFactory
,
JettyServletWebServerFactory
, or
UndertowServletWebServerFactory
has been auto-configured.
Spring Boot 啟動期間用了一個特殊的 IOC 容器(ServletWebServerApplicationContext),如果 Spring Boot 發現目前是一個 web 容器的話,IOC 容器就會變成ServletWebServerApplicationContext,這個容器在項目啟動的時候會搜尋
ServletWebServerFactory
(Servlet 的web 伺服器工廠),
- 當Spring Boot 應用啟動發現目前是 Web 應用,web 場景包-導入 tomcat
- web 應用會建立一個 web 版的 IOC 容器(ServletWebServerApplicationContext)
- Spring Boot 底層有很多的 Web 伺服器工廠
,TomcatServletWebServerFactory
, orJettyServletWebServerFactory
UndertowServletWebServerFactory
- 底層會有一個自動配置類
ServletWebServerFactoryAutoConfiguration
-
導入了ServletWebServerFactoryAutoConfiguration
(工廠的配置類)ServletWebServerFactoryConfiguration
-
根據動态判斷系統中到底導入了那個 Web 伺服器的包,(預設 web-starter 導入 tomcat 的包),進去會看到給容器中放了ServletWebServerFactoryConfiguration
,TomcatServletWebServerFactory
, orJettyServletWebServerFactory
UndertowServletWebServerFactory
-
TomcatServletWebServerFactory
最終建立出來 Tomcat 伺服器,并啟動
TomcatWebServer 的構造器有 初始化方法
public TomcatWebServer(Tomcat tomcat, boolean autoStart, Shutdown shutdown) {
Assert.notNull(tomcat, "Tomcat Server must not be null");
this.tomcat = tomcat;
this.autoStart = autoStart;
this.gracefulShutdown = (shutdown == Shutdown.GRACEFUL) ? new GracefulShutdown(tomcat) : null;
initialize();
}
initialize() 方法中調用啟動伺服器
this.tomcat.start();
- 内嵌伺服器,就是手動把啟動伺服器的代碼調用(tomcat 的 jar 包存在)
預設支援的 WebServer
-
,Tomcat
,Jetty
UnderTow
-
容器啟動尋找ServletWebServerApplicationContext
并引導建立伺服器ServletWebServerFactory
- 切換伺服器
<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>
<dependency>
<!--切換為 undertow 伺服器 -->
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-undertow</artifactId>
</dependency>
5.9.2 定制 Servlet 容器
@EnableConfigurationProperties(ServerProperties.class)
public class ServletWebServerFactoryAutoConfiguration {
@ConfigurationProperties(prefix = "server", ignoreUnknownFields = true)
public class ServerProperties {
- 實作 WebServletFactoryCustomizer< ConfigurableServletWebServerFactory >
- 把配置檔案的值和
進行綁定ServletWebServerFactory
- xxxxCustomizer:定制器,可以改變 xxxx 的預設規則
- 把配置檔案的值和
import org.springframework.boot.web.server.WebServerFactoryCustomizer;
import org.springframework.boot.web.servlet.server.ConfigurableServletWebServerFactory;
import org.springframework.stereotype.Component;
@Component
public class MyWebServerFactoryCustomizer implements WebServerFactoryCustomizer<ConfigurableServletWebServerFactory> {
@Override
public void customize(ConfigurableServletWebServerFactory server) {
server.setPort(9000);
}
}
- 修改配置檔案
server.xxx
- 直接自定義
ConfigurableServletWebServerFactory
@Bean
public ConfigurableServletWebServerFactory getConfigurableServletWebServerFactory () throws UnknownHostException {
TomcatServletWebServerFactory tomcatServletWebServerFactory = new TomcatServletWebServerFactory();
tomcatServletWebServerFactory.setPort(8088);
InetAddress address = InetAddress.getByName("127.0.0.1");
tomcatServletWebServerFactory.setAddress(address);
return tomcatServletWebServerFactory;
}
5.10 定制化原理
5.10.1 定制化的常見方式
- 修改配置檔案
- xxxxCustomizer;
- 編寫自定義的配置類 xxxConfiguation; + @Bean 替換、增加容器中預設元件;視圖解析器
- Web 應用編寫一個配置類實作 WebMvcConfigurer 即可定制化 Web 功能; + @Bean 給容器中再擴充一些元件
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
-
@EnableWebMvc + WebMvcConfigurer ---- @Bean 可以全面接管 SpringMVC,所有規則全部自己重新配置;實作定制和擴充功能
原理:
- WebMvcAutoConfiguration 預設的 SpringMVC的自動配置功能類。靜态資源、歡迎頁…
- 一旦使用 @EnableWebMvc,會 @Import(DelegatingWebMvcConfiguration.class)
- DelegatingWebMvcConfigurer 的作用,隻保證 SpringMVC 最基本的使用
- 把所有系統中的 WebMvcCofigurer 拿過來,所有功能的定制都是這些 WebMvcConfigurer 合起來一起生效
- 自動配置了一些非常底層的元件。RequestMappingHandlerMapping、這些元件依賴的元件都是從容器中擷取
- public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport
-
WebMvcAutoConfiguration 裡面的配置要能生效必須
@ConditionalOnMissingBean (WebMvcConfigurationSupport.class)
- @EnableWebMvc 導緻了 WebMvcAutoConfiguration 沒有生效
5.10.2 原理分析
引入場景 starter – xxxxAutoConfiguration – 導入xxx元件 – 綁定xxxProperties — 綁定配置檔案項
在我們使用過程中,第一步引入場景starter,然後綁定配置檔案就可以使用了,中間的部分 Spring Boot 幫助我們處理了。
6. 資料通路
6.1 SQL
6.1.1 資料源的自動配置
- 導入 JDBC 的場景
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
從上面導入的内容我們可以看到,少了一個重要的内容,就是資料的驅動
因為它也不知道我們要使用什麼資料庫(MySQL,SQLServer,還是 Orcalc)
引入 mysql 驅動依賴,不需要寫 version,因為 Spring Boot 已經對驅動的版本進行了仲裁
<!-- mysql 驅動-->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>
官方(預設)版本:
<mysql.version>8.0.26</mysql.version>
,需要注意我們自己的資料庫版本要和預設的版本保持對應
方法1:依賴的時候引入具體的版本(maven 的就近依賴原則)
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
方法2:官方裡面定義版本在
properties
中
<properties>
<mysql.version>5.1.49</mysql.version>
</properties>
6.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 {
事務管理器的自動配置DataSourceTransactionManagerAutoConfiguration
JdbcTemplate 的自動配置,可以操作資料庫JdbcTemplateConfiguration
-
可以修改這個配置項來修改 JdbcTemplates@ConfigurationProperties(prefix = "spring.jdbc")
- 要想資料源的消息,隻需要在配置檔案中修改
修改配置項
spring:
datasource:
url: jdbc:mysql://localhost:3306/studentgrade?userUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
- 測試連接配接的資料庫
// 使用 Spirng Boot 給我們注冊好的 JdbcTemplate m
@Autowired
JdbcTemplate jdbcTemplate;
@Test
void contextLoads() {
Long count = jdbcTemplate.queryForObject("select count(*) from student", Long.class);
System.out.println(count);
}
6.1.3 Druid 資料源
平常的開發中 Druid 資料源也是非常受歡迎的,由于它有對資料源的整套的解決方案,資料源的全通路監控(防止 SQL 的注入等…)
druid 官方 github 位址 https://github.com/alibaba/druid
整合第三方技術的兩種方式:
- 自定義
- 找 starter
6.1.4 自定義方式
- 引入依賴
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.17</version>
</dependency>
- 配置資料源
配置檔案中可以配置的屬性資訊
spring:
datasource:
username: root
password: root
url: jdbc:mysql://localhost:3306/studentgrade?useUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
driver-class-name: com.mysql.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
#SpringBoot預設是不注入這些的,需要自己綁定
#druid資料源專有配置
initialSize: 5
minIdle: 5
maxActive: 20
maxWait: 60000
timeBetweenEvictionRunsMillis: 60000
minEvictableIdleTimeMillis: 300000
validationQuery: SELECT 1 FROM DUAL
testWhileIdle: true
testOnBorrow: false
testOnReturn: false
poolPreparedStatements: true
#配置監控統計攔截的filters,stat:監控統計、log4j:日志記錄、wall:防禦sql注入
#如果允許報錯,java.lang.ClassNotFoundException: org.apache.Log4j.Properity
#則導入log4j 依賴就行
filters: stat,wall,log4j
maxPoolPreparedStatementPerConnectionSize: 20
useGlobalDataSourceStat: true
connectionoProperties: druid.stat.mergeSql=true;druid.stat.slowSqlMillis=500
@Configuration
public class MyDataSourceConfig {
// 預設的自動配置是判斷容器中沒有才會配置@ConditionalOnMissingBean(DataSource.class)
@Bean
@ConfigurationProperties("spring.datasource")// 綁定 application.yml中配置資料源的資訊
public DataSource dataSource() throws SQLException {
DruidDataSource druidDataSource = new DruidDataSource();
// 開啟内置監控功能
// 這裡的 set 都可以寫在 yml 配置檔案中
druidDataSource.setFilters("stat");
return druidDataSource;
}
}
- 測試運作就可以正常的使用 Druid 的連接配接池了
Druid 的内置監控功能
在自定義的資料源配置類
MyDataSourceConfig
中注冊監控的元件
/**
* 配置 druid 的監控頁功能,這裡配置好之後需要在 DataSource 元件中開啟内置監控功能,上面代碼中有(druidDataSource.setFilters("stat");)
* @return
*/
@Bean
public ServletRegistrationBean statViewServlet() {
StatViewServlet statViewServlet = new StatViewServlet();
ServletRegistrationBean<StatViewServlet> registrationBean = new ServletRegistrationBean<StatViewServlet>(statViewServlet,"/druid/*");
// 設定檢視的時候的使用者名和密碼
registrationBean.addInitParameter("loginUsername","admin");
registrationBean.addInitParameter("loginPassword","111111");
return registrationBean;
}
然後啟動程式後通過浏覽器通路
localhost:8080/druid
就可以跳轉到監控頁面了
登入之後在 Session 監控中可以看到資訊
開啟 Web 應用
/**
* WebStatFilter 用于采集 web-jdbc 關聯監控的資料
* @return
*/
@Bean
public FilterRegistrationBean WebStatFilter() {
WebStatFilter webStatFilter = new WebStatFilter();
FilterRegistrationBean<WebStatFilter> registrationBean = new FilterRegistrationBean<WebStatFilter>(webStatFilter);
registrationBean.setUrlPatterns(Arrays.asList("/*"));
// 排除掉一些靜态
registrationBean.addInitParameter("exclusions","*.js,*.jpg,*.gif,*.css,*.png,*.ico,/druid/*");
return registrationBean;
}
SQL防火牆
6.1.5 官方 starter 方式
使用官方的場景啟動器,上面的那些配置就不需要了
- 引入啟動器
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-starter</artifactId>
<version>1.1.17</version>
</dependency>
- 自動配置
@Configuration
@ConditionalOnClass(DruidDataSource.class)
// 如果 Spring 官方的資料源在前,則下面的 DataSource 就會不生效了
@AutoConfigureBefore(DataSourceAutoConfiguration.class)
// 綁定的配置檔案 在 `spring.datasource.druid` 下配置
@EnableConfigurationProperties({DruidStatProperties.class, DataSourceProperties.class})
@Import({
DruidSpringAopConfiguration.class,// 監控 Spring Bean,在`spring.datasource.druid.aop-patterns`下配置
DruidStatViewServletConfiguration.class,// 開啟監控頁的功能,在`spring.datasource.druid.stat-view-servlet.enabled`下配置,預設是開啟的
DruidWebStatFilterConfiguration.class,// web 監控配置,預設開啟的,在`spring.datasource.druid.web-stat-filter`下開啟
DruidFilterConfiguration.class// 所有 Druid 自己 filter 的配置,這個會給容器中放入很多的元件,想要開啟什麼功能,這個裡面都有配置的
})
public class DruidDataSourceAutoConfigure {
private static final Logger LOGGER = LoggerFactory.getLogger(DruidDataSourceAutoConfigure.class);
@Bean(initMethod = "init")
@ConditionalOnMissingBean
public DataSource dataSource() {
LOGGER.info("Init DruidDataSource");
return new DruidDataSourceWrapper();
}
}
- DruidFilterConfiguration.class 類
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";
private static final String FILTER_WALL_CONFIG_PREFIX = FILTER_WALL_PREFIX + ".config";
yml 配置檔案,隻是部分,詳細檢視druid 的官方文檔,github位址上面有
spring:
datasource:
url: jdbc:mysql://localhost:3306/studentgrade?userUnicode=true&characterEncoding=utf-8&serverTimezone=UTC
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver
druid:
# 監控 Spring 這個包下的所有元件
aop-patterns: com.thymeleaf.*
filters: stat,wall,slf4j #底層開啟功能,stat(sql監控),wall(防火牆),slf4j(日志記錄)
stat-view-servlet: # 配置監控頁功能
enabled: true
login-username: admin
login-password: 111111
# 禁用掉重置
reset-enable: false
web-stat-filter:
# 開啟監控web 應用
enabled: true
url-pattern: /*
exclusions: '*.js,*.jpg,*.gif,*.css,*.png,*.ico,/druid/*'
filter:
stat: # 對上面 filters 裡面的 stat 的詳細配置
# 慢查詢時間
slow-sql-millis: 1000
log-slow-sql: true
enabled: true
wall:
enabled: true
6.1.6 Mybatis
Mybatis 是第三方,是以 starter 是
mybatis-spring-boot-starter
github位址:https://github.com/mybatis
starter
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.2.0</version>
</dependency>
6.1.6.1 配置模式
之前使用 mybatis 的時候,需要有一個全局配置檔案,建立一個
SqlSessionFactory
,然後通過
SqlSession
找到 Mapper 接口來操作資料庫,所有的東西都需要手動進行編寫。
- MybatisAutoConfiguration 類
// 當引入了 mybatis 的 jar 包就不會生效了
@ConditionalOnClass({ SqlSessionFactory.class, SqlSessionFactoryBean.class })
// 當整個容器中隻有一個候選的資料源生效
@ConditionalOnSingleCandidate(DataSource.class)
// 綁定配置檔案
@EnableConfigurationProperties(MybatisProperties.class)
@AutoConfigureAfter({ DataSourceAutoConfiguration.class, MybatisLanguageDriverAutoConfiguration.class })
public class MybatisAutoConfiguration implements InitializingBean {}
@ConfigurationProperties(prefix = "mybatis")
public class MybatisProperties {
public static final String MYBATIS_PREFIX = "mybatis";
可以在配置檔案中修改
mybatis
開始的所有項來對mybatis 進行配置。
// 自動配置好了 SqlSessionFactory
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
SqlSessionFactoryBean factory = new SqlSessionFactoryBean();
factory.setDataSource(dataSource);
factory.setVfs(SpringBootVFS.class);
if (StringUtils.hasText(this.properties.getConfigLocation())) {
factory.setConfigLocation(this.resourceLoader.getResource(this.properties.getConfigLocation()));
// SqlSessionTemplate 裡面組合了 SqlSession
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
ExecutorType executorType = this.properties.getExecutorType();
if (executorType != null) {
return new SqlSessionTemplate(sqlSessionFactory, executorType);
} else {
return new SqlSessionTemplate(sqlSessionFactory);
}
// AutoConfiguredMapperScannerRegistrar 掃描配置檔案都在那個位置,接口位置
@Import(AutoConfiguredMapperScannerRegistrar.class)
@ConditionalOnMissingBean({ MapperFactoryBean.class, MapperScannerConfigurer.class })
public static class MapperScannerRegistrarNotFoundConfiguration implements InitializingBean {
public static class AutoConfiguredMapperScannerRegistrar implements BeanFactoryAware, ImportBeanDefinitionRegistrar
private BeanFactory beanFactory;
@Override
// AnnotationMetadata 拿到注解
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
if (!AutoConfigurationPackages.has(this.beanFactory)) {
logger.debug("Could not determine auto-configuration package, automatic mapper scanning disabled.");
return;
}
// 找到所有标注了 @Mapper 注解的接口,隻要我們寫的操作Mybatis 的接口标注了 @Mapper 注解就會被自動掃描進來
logger.debug("Searching for mappers annotated with @Mapper");
List<String> packages = AutoConfigurationPackages.get(this.beanFactory);
if (logger.isDebugEnabled()) {
packages.forEach(pkg -> logger.debug("Using auto-configuration base package '{}'", pkg));
}
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(MapperScannerConfigurer.class);
使用
- 在根路徑下建立 mybatis 檔案夾用于儲存 mybatis 的配置檔案,位置可以随意
<!--mybatis-config.xml Mybatis 的全局配置檔案-->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 開啟駝峰命名-->
<setting name="mapUnderscoreToCamelCase " value="true"/>
</settings>
</configuration>
<!--xxxxMapper.xml sql映射的檔案 名字要與建立的接口名字一樣-->
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
<settings>
<!-- 開啟駝峰命名-->
<setting name="mapUnderscoreToCamelCase " value="true"/>
</settings>
</configuration>
- 在 yml 檔案中指定建立的 mybatis 配置檔案的位置
mybatis:
config-location: classpath:mybatis/mybatis-config.xml
mapper-locations: classpath:mybatis/mapper/*.xml
-
運作啟動測試就可以了
這裡省略了 實體類、服務類、控制層、方法接口(名字必須與 xxxxMapper.xml名字一樣)的代碼,這些寫法與之前使用 Mybatis 方法一樣。
mybatis:
# config-location: classpath:mybatis/mybatis-config.xml
mapper-locations: classpath:mybatis/mapper/*.xml
configuration: # 指定 mybatis 全局配置檔案中的相關配置項,注意兩個不能同時使用,要麼使用 yml,要麼建立xml檔案yml指定位置
map-underscore-to-camel-case: true # 也可以在 yml 配置檔案中設定屬性
總結:
- 導入 mybatis 官方starter
- 編寫 mapper 接口
- 編寫 sql 映射檔案并綁定 mapper 接口
- 在 application.yml 中指定 Mapper 配置檔案的位置,以及指定全局配置檔案的位置(建議不适用全局檔案,直接使用yml 中的 mybatis 标簽下寫配置資訊)
6.1.6.2 注解模式
- 可以在建立 Spring Boot 項目的時候指定 Mybatis 架構
使用注解方式與之前使用 MyBatis 一樣,不需要寫 mapper 的映射檔案,隻需要在接口上使用注解即可
@Mapper
public interface CityMapper {
@Select("select * from city where id = #{id}")
City getById(Long id);
}
6.1.6.3 混合模式
混合方式就是可以使用注解也可以使用接口映射檔案來進行資料庫的存儲通路,簡單的 SQL 語句可以使用注解方式操作;如果 SQL 語句比較麻煩,就可以使用接口映射檔案xml 的方式進行操作。
有的的複雜的語句也可以使用 @Options 注解來完成
@Insert("insert into city (name,state,country) values(#{name},#{state},#{country})")
@Options(useGeneratedKeys = true,keyProperty = "id") // 設定自增的主鍵
Integer insertCity(City city);
6.1.7 MyBatis-Plus
6.1.7.1 簡介
MyBatis-Plus(簡稱 MP)是一個 MyBatis 的增強工具,在 MyBatis 的基礎上隻做增強不做改變,為了簡化開發、提高效率而生
官方:https://mp.baomidou.com/
6.1.7.2 簡單的查詢操作
-
引入依賴
引入 Mybatis-Plus 的依賴後,之前的 jdbc 和 mybatis 的依賴都可以去掉,這個全部進行了封裝
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.3.1.tmp</version>
</dependency>
自動配置了什麼
@Configuration
@ConditionalOnClass({SqlSessionFactory.class, SqlSessionFactoryBean.class})
@ConditionalOnSingleCandidate(DataSource.class) // 底層用的是我們的預設的資料源
@EnableConfigurationProperties({MybatisPlusProperties.class})
@AutoConfigureAfter({DataSourceAutoConfiguration.class, MybatisPlusLanguageDriverAutoConfiguration.class})
public class MybatisPlusAutoConfiguration implements InitializingBean {
@ConfigurationProperties(
prefix = "mybatis-plus" // 配置項綁定,這塊就是對 MyBatis-Plus 的綁定
)
public class MybatisPlusProperties { // 配置類
private String[] mapperLocations = new String[]{"classpath*:/mapper/**/*.xml"}; // mapperLocations 自動配置好了,有預設值 classpath*:/mapper/**/*.xml 任意包的類路徑下的所有 mapper 檔案夾下任意路徑下的所有 xml都是 SQL 映射檔案
// SqlSessionFactory 核心元件配置好了
@Bean
@ConditionalOnMissingBean
public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
@Mapper 标注的接口也會被自動掃描
// 容器中也自動配置好了 SqlSessionTemplate
@Bean
@ConditionalOnMissingBean
public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
- 建議在主啟動類加上掃描包注解
,這樣接口上不用再一個一個加 @Mapper 注解@MapperScan("com/thymeleaf/mapper")
- 編寫資料庫表對應的實體類,編寫 UserMapper 接口
/**
* 隻需要我們的 mapper 繼承 BaseMapper 就可以進行 CRUD
*/
public interface UserMapper extends BaseMapper<User> {
}
繼承的 BaseMapper 類中封裝大量的操作方法
- 在測試類中直接調用方法就可以了
@Test
void testUserMapper() {
User user = userMapper.selectById(1);
System.out.println(user);
}
6.1.7.3 分頁查找
以 User 類為例
- 首先定義一個 UserMapper 接口
/**
* 隻需要我們的 mapper 繼承 BaseMapper 就可以進行 CRUD
*/
public interface UserMapper extends BaseMapper<User> {
}
- 寫 UserService 接口
/**
* extends IService<User> 繼承 MyBatis-Plus 中的接口,IService 是所有 Service 的接口
*/
public interface UserService extends IService<User> {
}
ef
3. 寫 UserServiceImpl (UserService 的實作類),這兩個有一定的聯系,為了簡化,接口繼承了 IService 接口,實作類繼承了 ServiceImpl 類
/**
* ServiceImpl<UserMapper,User>
* UserMapper 表示的是要操作那個 Mapper
* User 傳回資料的類型
*/
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper,User> implements com.thymeleaf.service.UserService {
}
- Controller 調用接口進行資料的查詢
@Autowired
UserService userService;
@GetMapping("/table-datatable")
public String dataTable(@RequestParam(value = "page", defaultValue = "1") Integer page, Model model) {
List<User> users = userService.list();
model.addAttribute("usersAll",users);
// 分頁查詢資料
Page<User> userPage = new Page<>(page, 2);
// 分頁查詢結果
Page<User> page1 = userService.page(userPage);
// 獲得目前頁的資料
model.addAttribute("page", page1);
return "table/tables-datatable";
}
- 頁面拿到分頁資訊内容進行展示
<div class="card-body">
<div class="table-responsive">
<table id="example1" class="table table-bordered table-hover display">
<thead>
<tr>
<th>#</th>
<th>id</th>
<th>name</th>
<th>age</th>
<th>email</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr th:each="user,stat : ${page.records}">
<td th:text="${stat.count}"></td>
<td th:text="${user.id}">Tiger Nixon</td>
<td th:text="${user.name}">System Architect</td>
<td th:text="${user.age}">Edinburgh</td>
<td th:text="${user.email}">61</td>
<td><a th:href="@{/deleteUser/{id}(id=${user.id},page=${page.current})}">删除</a></td>
</tr>
</tbody>
</table>
</div>
<div class="row-fluid">
<div class="span6">
<div class="dataTables_info" id="dynamic-table_info">
目前第 [[${page.current}]] 頁 總計 [[${page.pages}]] 頁 共 [[${page.total}]] 條記錄
</div>
</div>
<div class="span6">
<div class="dataTables_paginate paging_bootstrap pagination">
<ul>
<li class="prev disabled"><a href="#">⬅ Previous</a></li>
<li th:class="${num == page.current?'active':''}" th:each="num : ${#numbers.sequence(1,page.pages)}">
<a th:href="@{/table-datatable(page=${num})}">[[${num}]]</a></li>
<li class="next"><a href="#">Next ➡</a></li>
</ul>
</div>
</div>
</div>
</div>
- 運作展示
6.1.7.4 删除操作
其他定義的接口與上面一樣
- Controller
/**
* RedirectAttributes 帶資料進行重定向
**/
@GetMapping("/deleteUser/{id}")
public String deleteUser(@PathVariable("id") Integer id,
@RequestParam(value = "page",defaultValue = "1") Integer page,
RedirectAttributes redirectAttributes) {
userService.removeById(id);
redirectAttributes.addAttribute("page",page);
return "redirect:/table-datatable";
}
- HTML 頁面的内容
<!--删除按鈕-->
<td><a th:href="@{/deleteUser/{id}(id=${user.id},page=${page.current})}">删除</a></td>
6.2 NoSQL(Redis)
Redis 是一個開源(BSD許可)的,記憶體中的資料結構存儲系統,它可以用作資料庫、緩存和消息中間件。它支援多種類型的資料結構,如字元串(strings),散列(hashes),清單(lists),集合(sets),有序集合(sorted sets) 與範圍查詢,bitmaps,hyperloglogs 和 地理空間(geospatial) 索引半徑查詢。Redis 内置了複制(replication),LUA 腳本(Lua scripting),事務(transactions) 和不同級别的磁盤持久化(persistence),并通過 Redis哨兵(Sentinel) 和分區(Cluster) 提供高可用性(high availablity)。
6.2.1 Redis 自動配置
使用的第一步引入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisOperations.class)
@EnableConfigurationProperties(RedisProperties.class)// 綁定配置檔案
// 導入了 Lettuce 的用戶端的連接配接配置,同時支援兩個用戶端操作 Redis
@Import({ LettuceConnectionConfiguration.class, JedisConnectionConfiguration.class })
public class RedisAutoConfiguration {// 自動配置類
// 屬性類 spring.redis 下面的所有配置是對 Redis 的配置
@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {
// 自動注入了 RedisTemplate,是以 K v 鍵值對的方式進行存儲的,k 是Object ,v 是Object
@Bean
@ConditionalOnMissingBean(name = "redisTemplate")
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
// 還自動注入了一個 StringRedisTemplate ,這個認為 key 和 value 都是 String 的
@Bean
@ConditionalOnMissingBean
@ConditionalOnSingleCandidate(RedisConnectionFactory.class)
public StringRedisTemplate stringRedisTemplate(RedisConnectionFactory redisConnectionFactory) {
在底層使用 RedisTemplate 和 StringRedisTemplate 就可以操作 Redis l
@Configuration(proxyBeanMethods = false)
@ConditionalOnClass(RedisClient.class)
// 當spring.redis.client-type 用戶端類型是 lettuce 的時候
@ConditionalOnProperty(name = "spring.redis.client-type", havingValue = "lettuce", matchIfMissing = true)
class LettuceConnectionConfiguration extends RedisConnectionConfiguration {
// 給容器中放一個用戶端的資源
@Bean(destroyMethod = "shutdown")
@ConditionalOnMissingBean(ClientResources.class)
DefaultClientResources lettuceClientResources() {
// 用戶端連接配接工廠,之後用戶端擷取的連接配接都是從這裡擷取的
@Bean
@ConditionalOnMissingBean(RedisConnectionFactory.class)
LettuceConnectionFactory redisConnectionFactory(
6.2.2 Lettuce用戶端操作
預設使用的就是 Lettuce 用戶端
-
啟動 Redis 的服務
http://lss-coding.top/2021/09/19/%E6%95%B0%E6%8D%AE%E5%BA%93/Redis/Redis%E5%9F%BA%E7%A1%80%E7%9F%A5%E8%AF%86%E7%82%B9%E6%80%BB%E7%BB%93/
- 在配置檔案中配置 Redis 的通路位址和端口号
spring:
redis:
host: 192.168.43.219
port: 6379
- 在測試類中輸入 StringRedisTemplate 進行操作
@Autowired
StringRedisTemplate stringRedisTemplate;
@Test
void testRedis() {
ValueOperations<String, String> stringStringValueOperations = stringRedisTemplate.opsForValue();
stringStringValueOperations.set("k2","v2");
String k1 = stringStringValueOperations.get("k2");
System.out.println(k1);
}
6.2.3 jedis 用戶端操作
- 導入 jedis 的依賴,Spring Boot 底層會對版本進行仲裁
<!--導入 jedis -->
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
- 在 yml 配置檔案中進行配置即可
spring:
redis:
host: 192.168.43.219
port: 6379
client-type: jedis
jedis:
pool: # 對連接配接池的配置
max-active: 10
6.2.4 通路統計案例
對每一個通路的 url 進行通路次數的統計,統計結果放到 Redis 中,存儲的 key 的路徑的名稱,value 每一次通路 + 1
- 定義一個攔截器,用于對通路的路徑 + 1
@Controller
public class RedisUrlCountInterceptor implements HandlerInterceptor {
@Autowired
StringRedisTemplate redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
String requestURI = request.getRequestURI();
// 每次通路目前計數 rui+1
redisTemplate.opsForValue().increment(requestURI);
return true;
}
}
- 在自定義的 WebMvcConfiguration 中注冊
@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
/**
* Filter 、Interceptor 差別
* Filter 是 Servlet 定義的原生元件,好處就是脫離的 Spring 也可以使用
* Interceptor 是 Spring 定義的接口,可以使用 Spring 的自動裝配的功能
*/
@Autowired
RedisUrlCountInterceptor redisUrlCountInterceptor;
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(redisUrlCountInterceptor).addPathPatterns("/**")
.excludePathPatterns("/","/login","/css/**","/data/**","/font-awesome/**","images/**","/js/**","/lib/**","/plugins/**");
}
}
Filter 、Interceptor 差別
Filter 是 Servlet 定義的原生元件,好處就是脫離的 Spring 也可以使用
Interceptor 是 Spring 定義的接口,可以使用 Spring 的自動裝配的功能
7. 單元測試
7.1 JUnit5 的變化
Spring Boot 2.2.0 版本開始引入 JUnit 5 作為單元測試預設庫
作為最新版本的 JUnit 架構,JUnit5 與之前版本的 JUnit 架構有很大的不同。由三個不同子項目的幾個不同子產品組成。
JUnit 5 = JUnit Platform + JUnit Jupiter + JUnit Vintage
JUnit Platform:JUnit Platform 是在 JVM 上啟動測試架構的基礎,不僅支援 JUnit 自制的測試引擎,其他測試引擎也都可以接入
JUnit Jupiter:提供了 JUnit 5 的新的程式設計模型,是 JUnit 5 新特性的核心。内部包含了一個測試引擎,用于在 JUnit Platform 上運作
JUnit Vintage:由于 JUnit 已經發展很多年,為了照顧老的項目,JUnit Vintage 提供了相容 JUnit4.x,JUnit3.x 的測試引擎
注意:Spring Boot 2.4 以上的版本移除了預設對 Vintage 的依賴。如果需要相容 JUnit 4 需要自行引入
- JUnit 5’s Vintage Engine Removed from
spring-boot-starter-test
https://github.com/spring-projects/spring-boot/wiki/Spring-Boot-2.4-Release-Notes
2.4 版本移除了 JUnit 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>
- 現在的使用方式
使用非常友善,隻需要寫一個測試類,在測試類上使用 @Test 注解就可以了
@SpringBootTest
class ThymeleafApplicationTests {
// 當我們建立一個 Spring Boot 項目後會自動給我們生成一個帶有 @Test 的測試方法
@Test
void contextLoads() {
}
}
如果想要做單元測試隻需要引入測試的啟動器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
- 以前使用
需要使用 @Spring BootTest + @RunWith(SpringTest.class) 注解實作
Spring Boot 整合 JUnit 後使用非常的友善
- 編寫測試方法,标注
注解(注意是 JUnit 5 版本的注解)@Test
- JUnit 類具有 Spring 的功能,@Autowired、@Transactional(标注事務的注解,事務完成後會自動復原) 注解都可以使用
7.2 JUnit 5 常用注解
官網列舉了很多的注解:https://junit.org/junit5/docs/current/user-guide/#writing-tests-annotations
- @Test:表示方法是測試方法。但是與 JUnit 4 的@Test 不同,它的職責非常單一不能聲明任何屬性,擴充的測試将會由 Jupiter 提供額外測試
- @ParameterizedTest:表示方法是參數化測試
- @RepeatedTest:表示方法可重複執行
@RepeatedTest(5)
@Test
void testRepeated() {
System.out.println("測試Repeated");
}
- @ DisplayName:為測試類或者測試方法設定展示名稱
@DisplayName("測試JUnit5功能測試類")
public class JUnit5Test {
@DisplayName("測試DisplayName注解")
@Test
void testDisplayName() {
System.out.println(1);
}
}
- @BeforeEach:表示在單個測試之前執行
@BeforeEach
void testBeforeEach() {
System.out.println("測試就要開始了");
}
- @AfterEach:表示在單個測試之後執行
@AfterEach
void testAfterEach() {
System.out.println("測試結束了");
}
- @BeforeAll:表示所有單元測試之前執行
寫兩個 @Test 方法,點類上的運作按鈕執行兩個測試方法
@DisplayName("測試DisplayName注解")
@Test
void testDisplayName() {
System.out.println(1);
}
@Test
void test() {
System.out.println(2);
}
@BeforeAll
static void testBeforeAll() { // 需要定義為 static 方法,因為啟動的時候就會調用這個方法
System.out.println("所有測試就要開始了");
}
- @AfterAll:表示在所有單元測試之後執行
與 @BeforeAll 一樣:定義兩個測試類,定義為static 方法
@AfterAll
static void testAfterAll() {
System.out.println("所有測試都結束了");
}
- @Tag:表示單元測試類型,類似于 JUnit 4 中的 @Categories
- @Disabled:表示測試類或者測試方法不執行,類似于 JUnit4 中的 @Ignore
@Disabled
@DisplayName("測試方法 2")
@Test
void test() {
System.out.println(2);
}
- @Timeout:表示測試方法運作結果超過了指定的時間将會傳回錯誤
/**
* 方法的逾時時間,如果逾時抛出異常 TimeoutException
* @throws InterruptedException
*/
@Test
@Timeout(value = 5,unit = TimeUnit.MILLISECONDS)
void testTimeout() throws InterruptedException {
Thread.sleep(600);
}
- @ExtendWith:為測試類或測試方法提供擴充類引用,類似于 JUnit4 的 @RunWith 注解
在我們自定義的的測試類中沒辦法使用容器中元件
@Autowired
RedisTemplate redisTemplate;
@DisplayName("測試DisplayName注解")
@Test
void testDisplayName() {
System.out.println(redisTemplate);
System.out.println(1);
}
如果想要使用 容器中的元件,需要跟 Spring Boot 建立的時候自動建立的測試類一樣,在類上加
@SpringTest
注解
@SpringBootTest
@DisplayName("測試JUnit5功能測試類")
public class JUnit5Test {
@BootstrapWith(SpringBootTestContextBootstrapper.class)
@ExtendWith({SpringExtension.class}) // Spring Boot 底層注解就有@ExtendWith注解,意思就是下面的測試都是使用 Spring的整個測試驅動進行測試
public @interface SpringBootTest {
7.3 斷言(assertions)
斷定某一件事情一定會發生,如果沒發生就會認為它出了别的情況的錯誤。
斷言是測試方法中的核心部分,用來對測試需要滿足的條件進行驗證。這些斷言方法都是
org.junit.jupiter.api.Assertions
的靜态方法。JUnit 5 内置的斷言可以分為下面的幾種類别。
檢查業務邏輯傳回的資料是否合理,所有的測試運作結束以後,會有一個詳細的測試報告(報告裡面就會有那些方法成功,那些方法失敗,失敗的原因等等)
注意:如果有兩個斷言,第一個執行失敗了第二個則不會執行
用來對單個值進行簡單的驗證
方法 | 說明 |
---|---|
assertEquals | 判斷兩個對象或兩個原始類型是否相等 |
assertNotEquals | 判斷兩個對象或兩個原始類型是否不相等 |
assertSame | 判斷兩個對象引用是否指向同一個對象 |
assertNotSame | 判斷兩個對象引用是否指向不同的對象 |
assertTrue | 判斷給定的布爾值是否為 true |
assertFalse | 判斷給定的布爾值是否為 false |
assertNull | 判斷給定的對象引用是否為 null |
assertNotNull | 判斷給定的對象引用是否不為 null |
- 簡單斷言
- assertEquals
@DisplayName("測試簡單斷言")
@Test
void testSimpleAssertions() {
int add = add(2, 3);
// 判斷相等,如果不相等給出錯誤資訊 AssertionFailedError
assertEquals(6,add,"算數邏輯錯誤");
}
public int add(int num1, int num2) {
return num1 + num2;
}
- assertSame
@DisplayName("測試簡單斷言")
@Test
void testSimpleAssertions() {
int add = add(2, 3);
// 判斷相等,如果不相等給出錯誤資訊 AssertionFailedError
assertEquals(6,add,"算數邏輯錯誤");
Object ob1 = new Object();
Object ob2 = new Object();
assertSame(ob1,ob2);
}
- 數組斷言
- assertArrayEquals
@DisplayName("測試簡單斷言")
@Test
void testSimpleAssertions() {
assertSame(ob1,ob2,"兩個對象不一樣");
assertArrayEquals(new int[]{2,2},new int[]{1,2},"數組内容不相等");
}
- 組合斷言
assertAll 方法接受多個 org.junit.jupter.api.Executable 函數式接口的執行個體作為要驗證的斷言,可以通過 lambda 表達式很容易的提供這些斷言。
判斷的時候當所有的斷言執行成功才成功,否則失敗
@DisplayName("組合斷言")
@Test
void all() {
/**
* 斷言所有成功才會往下走
*/
assertAll("test", () -> assertTrue(true && true,"結果不為true"),
() -> assertEquals(1, 2,"值不相等"));
}
- 異常斷言
在 JUnit 4 的時候,想要測試方法的異常情況時,需要用 @Rule 注解的 ExpectedException 變量還是比較麻煩的。而 JUnit 5 提供了一種新的斷言方式 Assertions.assertThrows(),配合函數式程式設計就可以進行使用
@DisplayName("異常斷言")
@Test
void testException() {
// 斷定業務邏輯一定會出現異常
assertThrows(ArithmeticException.class, () -> {
int i = 10 / 2;
}, "為什麼正常執行了,不應該有異常嗎?");
}
- 逾時斷言
Assertions.assertTimeout();為測試方法設定了逾時時間
@DisplayName("逾時斷言")
@Test
void testAssertTimeout() {
// 如果測試方法超過 1 s 就會出現異常
Assertions.assertTimeout(Duration.ofMillis(1000),()->Thread.sleep(500));
}
- 快速失效
通過 fail 方法直接使得測試失敗
@DisplayName("快速失敗")
@Test
void testFail() {
fail("測試失敗");
}
7.4 前置條件(assumptions假設)
JUnit5 中的前置條件類似于斷言,不同之處在于不滿足的斷言會使得測試方法失敗,而不滿足的前置條件隻會使得測試方法終止執行。前置條件可以看成是測試方法執行的前提,當該前提不滿足時,就沒有繼續執行的必要。
assumeTrue 和 assumeFalse 確定給定的條件為 true 或 false,不滿足條件會使得測試執行終止。assumingThat 的參數是表示條件的布爾值和對應的 Executable 接口的實作對象。隻有條件滿足時,Executable 對象才會被執行;當條件不滿足,測試執行并不會終止。
/**
* 測試前置條件
*/
@DisplayName("測試前置條件")
@Test
void testAssumptions() {
Assumptions.assumeTrue(true, "結果不是true");
System.out.println("執行成功");
}
7.5 嵌套測試
JUnit 5 可以通過 Java 中的内部類和 @Nested 注解實作嵌套測試,進而可以更好的把相關的測試方法組織在一起。在内部類中可以使用 @BeforeEach 和 @AfterEach 注解,而且嵌套的層次沒有限制。
- 内層的 Test 可以驅動外層的 Test
- 外層的 Test 不能驅動内層的 Test
@DisplayName("嵌套測試")
public class TestingAStackDemo {
Stack<Object> stack;
@Test
@DisplayName("is instantiated with new Stack()")
void isInstantiatedWithNew() {
new Stack<>();
// 嵌套測試下,外層的 test 不能驅動内層的 BeforeEach BeforeAll (After) 之類的方法提前或者之後運作
assertNull(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);
}
/**
* 記憶體的 test 可以驅動外層,外層的不能驅動内層的
*/
@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());
}
}
}
}
7.6 參數化測試
參數化測試是 JUnit 5 很重要的一個新特性,它使得用不同的參數多次運作測試成為了可能,也為我們的單元測試帶來許多便利。
利用 @ValueSource 等注解,指定入參,我們将可以使用不同的參數進行多次單元測試,而不需要沒新增一個參數就新增一個單元測試,省去了很多備援代碼。
- @ValueSource:為參數測試指定入參來源,支援八大基礎類以及 String 類型,Class 類型
@DisplayName("參數化測試")
@ParameterizedTest // 标注為參數化測試
@ValueSource(ints = {1,2,3,4,5})
void testParameterized(int i) {
System.out.println(i);
}
- @NullSource:表示為參數化測試提供一個 null 的入參
- @EnumSource:表示為參數化測試提供一個枚舉入參
- @CsvFileSource:表示讀取指定 CSV檔案内容作為參數化測試入參
- @MethodSource:表示讀取方法的傳回值作為參數化測試入參(注意方法傳回值需要是一個流)
@DisplayName("參數化測試")
@ParameterizedTest // 标注為參數化測試
@MethodSource("stringProvider")
void testParameterized2(String i) {
System.out.println(i);
}
// 方法傳回 Stream
static Stream<String> stringProvider() {
return Stream.of("apple","banana");
}
如果參數化測試僅僅隻能做到指定普通的入參還不是最厲害的,最強大之處的地方在于它可以支援外部的各類入參。如:CSV,YML,JSON 檔案甚至方法的傳回值也可以作為入參。隻需要去實作
ArgumentsProvider
接口,任何外部檔案都可以作為它的入參。
8. 名額監控
8.1 Spring Boot Actuator
8.1.1 簡介
在以後每一個微服務在雲上部署以後,我們都需要對其進行監控、追蹤、審計、控制等。Spring Boot 就抽取了 Actuator 場景,使得我們每個微服務快速引用即可獲得生産級别的應用監控、審計等功能。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
8.1.2 1.x 與 2.x 不同
- 1.x
- 支援 SpringMVC
- 基于繼承方式進行擴充
- 層級 Metrics 配置
- 自定義 Metrics 收集
- 預設較少的安全政策
- 2.x
- 支援 SpringMVC、JAX-RS 以及 WebFlus
- 注解驅動進行擴充
- 層級 & 名稱空間 Metrics
- 底層使用 MicroMeter,強大、便捷
- 預設豐富的安全政策
8.1.3 使用
官網:https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator
- 引入場景啟動器,上面有
- 啟動項目通路路徑:http://localhost:8080/actuator/**
因為代碼中 Redis 連接配接是失敗的,是以通路 health 端口顯示狀态是 DOWN 掉的
通路路徑 locahost:8080/actuator/health 中 actuator 後面的稱為 Endpoints
官網上的 Endpoints 都是可以監控的名額,可以看到有很多的監控端點,但是這些端點預設不是全部開啟的,除了 shutdown 這個端點外
- 配置檔案中配置所有 Endpoints 生效
# management 是所有 actuator 的配置
management:
endpoints:
enabled-by-default: true # 預設開啟所有的監控端點
web:
exposure:
include: '*' # 以 web 方式暴露所有端點
- 例如通路:localhost:8080/actuator/conditions,得到所有配置生效的元件,/beans 可以得到所有 元件
8.2 Actuator Endpoint
8.2.1 常用端點
最常用的 Endpoint
- Health:健康狀況,目前應用是否健康
- Metrics:運作時名額
- Loggers:日志記錄,友善追蹤錯誤
官網全部有羅列
https://docs.spring.io/spring-boot/docs/current/reference/html/actuator.html#actuator.endpoints
8.2.2 Health Endpoint
健康檢查端點,一般用于雲平台,平台會定時檢查應用的健康狀況,我們就需要 Health Endpoint 可以為平台傳回目前應用的一系列元件健康狀況的集合。
中的幾點:
- health endpoint 傳回的結果,應該是一系列健康檢查後的一個彙總報告
- 很多的健康檢查預設已經自動配置好了,比如:資料庫、redis 等
- 可以很容易的添加自定義的健康檢查機制
判斷健康與否,需要取決于所有元件都是健康的才算健康,否則就是不健康。不健康就會提示 DOWN,健康提示 UP
management:
endpoints:
enabled-by-default: true # 預設開啟所有的監控端點
web:
exposure:
include: '*' # 以 web 方式暴露所有端點
endpoint:
health:
show-details: always
8.2.3 Metrics Endpoint
提供詳細的、層級的、空間名額資訊,這些資訊可以被 pull(主動推送)或者 push(被動擷取)方式得到:
- 通過 Metrics 對接多種監控系統
- 簡化核心 Metrics 開發
- 添加自定義 Metrics 或者擴充已有 Metrics
8.2.4 暴露 Endpoints
支援的暴露方式
- JMX:預設暴露所有的 Endpoint
- HTTP:預設隻暴漏 health 和 info 的 Endpoints
如果在使用過程中開啟所有名額的通路是非常危險的,是以有時候可以自定義開啟某一個需要的名額
management:
endpoints:
enabled-by-default: false # 關閉所有監控端點 為 true 表示開啟所有監控端點
web:
exposure:
include: '*' # 以 web 方式暴露所有端點
# 将某一個名額的 enabled 設定為 true
endpoint:
health:
show-details: always
enabled: true
info:
enabled: true
beans:
enabled: true
metrics:
enabled: true
8.3 定制 EndPoint
8.3.1 定制 Health 資訊
@Component
public class MyHealthIndicator extends AbstractHealthIndicator {
// 真實的檢查方法
@Override
protected void doHealthCheck(Health.Builder builder) throws Exception {
// 加入判斷 MySQL 連接配接,在這裡擷取連接配接資訊然後進行判斷
// 儲存在一些判斷過程中資訊
Map<String,Object> map = new HashMap<>();
if (1 == 1) {
// 健康
// builder.up();
builder.status(Status.UP);
map.put("msg","判斷了10次");
}else{
// 不健康
// builder.down();
builder.status(Status.DOWN);
map.put("msg","判斷了 0次");
}
builder.withDetail("code",100) // 傳回的狀态資訊
.withDetails(map); // 傳回攜帶的資訊
}
}
8.3.2 定制 Info 資訊
- 編寫配置檔案
info:
appName: thymeleaf
version: 1.0
mavenProjectVersion: @[email protected] # 得到 pom 檔案中maven 的版本資訊
- InfoContributor
@Component
public class AppInfo implements InfoContributor{
@Override
public void contribute(Info.Builder builder) {
builder.withDetail("msg","你好")
.withDetail("hello","world");
}
}
8.3.3 定制 Metrics
- Spring Boot 支援自動配置的 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 to true for all Tomcat metrics to be registered)
- Spring Integration metrics
- 增強Metrics
想要判斷某一個方法被通路了多少次
- 在接口實作類中定義名額 Metrics
Counter counter; // 數量
public UserServiceImpl(MeterRegistry registry) {
counter = registry.counter("UserService.list.count");//UserService.list.count 自定義的名額的名稱
}
@Override
public void TestMeterRegistry() {
counter.increment(); // 調用這個方法一次加 1
}
- 測試通路
- localhost:8080/actuator/metrics
8.4 定制 Endpoint
- 建立一個自定義 Endpoint 類,注冊到容器中
@Component
@Endpoint(id = "myservice")// 端點名
public class MyServiceEndpoint {
// 端點的讀操作
@ReadOperation
public Map getDockerInfo() {
return Collections.singletonMap("dockerInfo","docker is running......");
}
@WriteOperation
public void stopDocker() {
System.out.println("Docker stopped......");
}
}
- 啟動運作通路
可以看到我們自定義的端點
拿到端點的值
8.5 可視化監控系統
https://github.com/codecentric/spring-boot-admin
使用手冊:https://codecentric.github.io/spring-boot-admin/2.5.1/#getting-started
9. 原了解析
9.1 Profile 功能
為了友善多環境适配,Spring Boot 簡化了 profile 功能
在整個應用系統開發的時候可能會有一套資料庫的配置資訊,上線的時候又有另一套配置資訊,如果每次開發上線都去修改配置檔案會非常的麻煩,是以可以配置兩套配置檔案,一個上線的時候使用,一個平常開發測試的時候使用。
9.1.1 application-profile 功能
- 預設配置檔案 application.yml 任何時候都會被加載
- 在預設配置檔案中指定環境配置檔案 application-{env}.yaml
- 激活指定環境
- 配置檔案激活
- 指令行激活
- 預設配置與環境配置同時生效
- 同名配置項,profile.active 激活的 配置優先
使用
建立兩個配置檔案:application-test.yml(測試環境),application-prod.yml(生産環境)
在配置檔案中寫入相同的配置
person:
name: test-張三
@RestController
public class HelloController {
@Value("${person.name:李四}") // 如果person.name 為空給預設值李四
private String name;
@GetMapping("/")
public String hello() {
return "Hello " + name;
}
}
如果沒有指定配置環境,則預設的配置檔案生效
如果想要指定是測試環境還是生成環境,則在預設的配置檔案中進行指定
# 指定使用那個環境的配置檔案,預設配置檔案和指定環境的配置檔案都會生效(如果預設配置檔案和指定的環境配置中有相同的配置屬性,則指定的會覆寫預設的)
spring.profiles.active=test
項目都是需要打包然後進行部署的,如果我們打包好了又需要切換環境重新打包非常的麻煩,是以我們可以在執行 jar 包的時候切換環境配置檔案,指令行方式可以修改配置檔案同時也可以修改配置檔案中的内容資訊
java -jar 打包好的項目 --spring.profile.active=prod --person.name=王五
9.1.2 @Profile 條件裝配功能
官網:https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.profiles
模拟使用
有兩個類,一個 Boss,一個Worker類,分别都包含屬性 name、age,實作一個 Person 接口,配置檔案 test 中配置了老闆的資訊,prod 配置了勞工的資訊,如果我們在 Controller 使用某一個對象的時候,自動注入 Person 接口,傳回 person 的資訊,不能确定生效的老闆的類還是員工的類。是以在Boss 類和 Worker 類中分别進行指定配置檔案的裝配,
@Profile
注解
// 激活配置檔案,當 prod 檔案的時候生效
@Profile("prod")
@Data
@Component
@ConfigurationProperties("person")
public class Worker implements Person{
private String name;
private Integer age;
}
// 激活配置檔案,當 test 檔案的時候生效// 激活配置檔案,當 test 檔案的時候生效
@Profile("test")
@Data
@Component
@ConfigurationProperties("person")
public class Boss implements Person{
private String name;
private Integer age;
}
#配置檔案
# 預設配置檔案 application.properties 進行激活使用的環境配置資訊
spring.profiles.active=prod
# 測試環境配置檔案 application-test.yml
person:
name: boss-張三
age: 30
# 生産環境配置檔案 application-prod.yml
person:
name: worker-張三
age: 10
然後啟動測試運作結果
啟動後可以看到目前使用的生産 prod 環境,當然上面預設配置檔案中是指定好的
傳回值資訊是 prod中配置好的内容
9.1.3 Profile 分組
官網:https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.profiles.groups
# 激活一個組
spring.profiles.active=myprod
spring.profiles.group.myprod[0]=prod
spring.profiles.group.myprod[1]=dev
9.2外部化配置
簡單的說外部化配置就是抽取一些檔案,放在外邊集中管理
官網:https://docs.spring.io/spring-boot/docs/current/reference/html/features.html#features.external-config
Spring Boot 可以讓我們使用外部化配置,外部化設定可以在不修改代碼的情況下可以适配多種環境,外部化來源包括:Java properties files, YAML files, environment variables, 和 command-line arguments.
9.2.1 配置檔案查找位置
- classpath 根路徑
- classpath 根路徑下 config 目錄
- jar 包目前目錄
- jar 包目前目錄的config 目錄
- /config 子目錄的直接子目錄(這裡的 / 指的是 Linux 系統的 / 目錄)
優先級:5 > 4 > 3 > 2 > 1
9.2.2 配置檔案加載順序
- 目前 jar 包内部的 application.properties 和 application.yml
- 目前 jar 包内部的 application-{profile}.properties 和 application-{profile}.yml
- 引用的外部 jar 包的 application.properties 和 application.yml
- 應用的外部 jar 包的 application-{profile}.properties 和 application-{profile}.yml
後面的可以覆寫前面的同名配置項,指定環境優先,外部優先
9.3 自定義 starter
9.3.1 starter 啟動原理
- starter-pom 引入 autoconfigure 包
- autoconfigure 包中配置使用 META-INF/spring.factories 中 EnableAutoConfigurtion 的值,使得項目啟動加載指定自動配置類
- 編寫自動配置類 xxxxAutoConfiguration -> xxxxProperties
- @Configuration
- Conditional
- @EnableConfigurationProperties
- @Bean
引入 starter — xxxxAutoConfiguration — 容器中放入元件 — 綁定 xxxxProperties — 配置項
9.3.2 自定義一個 starter
- 打開 Idea ,建立一個空的項目
customer-starter
- 建立一個空的 Maven 項目,
作為我們的啟動器hello-spring-boot-starter
- 建立一個 Spring Boot 初始化項目
自動配置包hello-spring-boot-starter-autoconfigure
建立好之後
- 在
的 pom.xml 檔案中配置好自動配置類hello-spring-boot-starter
// 這個内容是自動配置類中pom.xml中的資訊
<dependencies>
<dependency>
<groupId>com.example</groupId>
<artifactId>hello-spring-boot-starter-autoconfigure</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
至此
hello-spring-boot-starter
配置好了,這個啟動器隻需要配置一個自動配置的資訊
- 配置
自動配置類hello-spring-boot-starter-autoconfigure
- 建立一個
用于儲存配置類的配置屬性資訊HelloProperties
// 指定字首資訊,在配置檔案中使用這個 example.hello 就是配置我們自定義的starter 的屬性
@ConfigurationProperties("example.hello")
public class HelloProperties {
private String prefix;
private String suffix;
// 省略 get/set 方法
}
- 建立一個服務類
,真正執行的代碼HelloService
/**
* 預設不放到容器中
*/
public class HelloService {
@Autowired
HelloProperties helloProperties;
public String sayHello(String name) {
return helloProperties.getPrefix() + "你好" + name + helloProperties.getSuffix();
}
}
- 建立一個自動配置類
HelloServiceAutoConfiguration
@Configuration
@ConditionalOnMissingBean(HelloService.class)
@EnableConfigurationProperties(HelloProperties.class)// 預設把 HelloProperties 放到容器中
public class HelloServiceAutoConfiguration {
@Bean
public HelloService helloService() {
HelloService helloService = new HelloService();
return helloService;
}
}
- 在 resources 檔案夾下建立檔案
,指定自動配置的類/META-INF/spring.factories
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
com.example.auto.HelloServiceAutoConfiguration
至此啟動器和自動配置類建立完成,然後将我們自己建立好了項目安裝到 maven 中
- 安裝到 maven 中
- 在别的項目就可以用這個自定義的 starter 了
使用
- 引入啟動器
<dependency>
<groupId>org.example</groupId>
<artifactId>hello-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
- 配置屬性資訊
example.hello.suffix="111"
example.hello.prefix="222"
- Controller 中注入
類,執行方法HelloService
@RestController
public class HelloController {
@Autowired
HelloService helloService;
@RequestMapping("/hello")
public String sayHello() {
String name = helloService.sayHello("張三三");
return name;
}
}
- 測試運作
9.4 Spring Boot 原理
9.4.1 Spring Boot 啟動過程
主啟動類 debug 運作
// 這塊是建立的流程 new SpringApplication(primarySources)
return run(new Class[]{primarySource}, args); // new 一個 Class 類
進去,1. 建立一個 Spring 應用,2. 然後調用 run 方法啟動
return (new SpringApplication(primarySources)).run(args);
進去進去
@SuppressWarnings({ "unchecked", "rawtypes" })
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
// 資源加載器
this.resourceLoader = resourceLoader;
// 斷言程式中有主配置類,如果沒有失敗
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
// 判斷目前應用的類型
this.webApplicationType = WebApplicationType.deduceFromClasspath();
// 初始啟動引導器,去 spring.factories 檔案中找 Bootstrap類型的
this.bootstrapRegistryInitializers = getBootstrapRegistryInitializersFromSpringFactories();
// 找 ApplicationContextInitializer 也是去 spring.factories 檔案中找
setInitializers((Collection) getSpringFactoriesInstances(ApplicationContextInitializer.class));
// 找應用 監聽器 ApplicationListener
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
// 決定哪一個是主程式類
this.mainApplicationClass = deduceMainApplicationClass();
}
private Class<?> deduceMainApplicationClass() {
try {
// 進到 堆棧中,找到有 main 方法的類就是主啟動類
StackTraceElement[] stackTrace = new RuntimeException().getStackTrace();
for (StackTraceElement stackTraceElement : stackTrace) {
if ("main".equals(stackTraceElement.getMethodName())) {
return Class.forName(stackTraceElement.getClassName());
}
}
}
catch (ClassNotFoundException ex) {
// Swallow and continue
}
return null;
}
// 這塊是 run 的流程 run(args)
// String... args 就是 main 方法中的參數 main(String[] args)
public ConfigurableApplicationContext run(String... args) {
// 停止的監聽器,監控應用的啟動停止
StopWatch stopWatch = new StopWatch();
// 記錄的啟動時間
stopWatch.start();
// 建立 引導上下文,并且擷取到之前的 BootstrapRegistryInitializer 并且挨個執行 initializer() 方法 來完成對引導啟動器上下文環境設定
DefaultBootstrapContext bootstrapContext = createBootstrapContext();
ConfigurableApplicationContext context = null;
// 讓目前應用進入 headless 模式(自力更生模式)
configureHeadlessProperty();
// 擷取所有的 運作時監聽器 并進行儲存
SpringApplicationRunListeners listeners = getRunListeners(args);
// 周遊所有的 RunListener,調用 starting 方法,相當于通知所有感興趣系統正在啟動過程的正在starting,為了友善所有 Listener 進行事件感覺
listeners.starting(bootstrapContext, this.mainApplicationClass);
try {
// 儲存指令行參數
ApplicationArguments applicationArguments = new DefaultApplicationArguments(args);
// 準備基礎環境(儲存環境變量等等),調用方法,如果有就傳回,沒有就建立基礎一個,無論如何得有一個環境資訊,配置環境環境變量資訊,加載全系統的配置源的屬性資訊,綁定環境資訊,監聽器調用 environmentPrepard,通知所有的監聽器目前環境準備完成
// prepareEnvironment 結束後所有環境資訊準備完畢
ConfigurableEnvironment environment = prepareEnvironment(listeners, bootstrapContext, applicationArguments);
// 配置一些忽略的 bean 資訊
configureIgnoreBeanInfo(environment);
// 列印 Banner
Banner printedBanner = printBanner(environment);
// 建立 IOC 容器 就是建立 ApplicationContext,根據目前項目類型,servlet,AnnotationConfigServletWebServerApplicationContex,容器建立對象new 出來了
context = createApplicationContext();
// 記錄目前應用的 startup事件
context.setApplicationStartup(this.applicationStartup);
// 準備 IOC 容器的資訊 ,儲存環境資訊,後置處理流程,應用初始化器(周遊所有的 ApplicationContextInitlalizer,調用initialize 來對 IOC 容器進行初始化擴充功能),周遊所有的 listener 調用 contextPrepared,EvenPublishRunListener 通知所有的監聽器 contextPrepared 完成
prepareContext(bootstrapContext, context, en vironment, listeners, applicationArguments, printedBanner);
// 重新整理 IOC 容器,(執行個體化容器中的所有元件)
refreshContext(context);
// 重新整理完成後工作,
afterRefresh(context, applicationArguments);
stopWatch.stop();
if (this.logStartupInfo) {
new StartupInfoLogger(this.mainApplicationClass).logStarted(getApplicationLog(), stopWatch);
}
// 所有監聽器調用started 方法,通知所有監聽器 started
listeners.started(context);
// 調用所有的 Runners,擷取容器中 ApplicationRunner,CommandLineRunner,合并所有 Runner 按照 @Order 進行排序,周遊所有的 Runner,調用 run 方法,如果以上有異常調用 Listener 的faild 方法
callRunners(context, applicationArguments);
}
catch (Throwable ex) {
handleRunFailure(context, ex, listeners);
throw new IllegalStateException(ex);
}
try {
// 如果以上都準備好并且沒有異常,調用所有監聽器的 running 方法,通知所有監聽器進入 running 狀态了,running 如果有錯誤繼續通知 failed,通知監聽器目前失敗
listeners.running(context);
}
catch (Throwable ex) {
handleRunFailure(context, ex, null);
throw new IllegalStateException(ex);
}
return context;
}
// 如果一開始找到了 BootstrapRegistryInitializer 引導啟動器就會調用這個方法,把每一個啟動器周遊調用 initialize 方法
private DefaultBootstrapContext createBootstrapContext() {
DefaultBootstrapContext bootstrapContext = new DefaultBootstrapContext();
this.bootstrapRegistryInitializers.forEach((initializer) -> initializer.initialize(bootstrapContext));
return bootstrapContext;
}
@FunctionalInterface
public interface BootstrapRegistryInitializer {
/**
* Initialize the given {@link BootstrapRegistry} with any required registrations.
* @param registry the registry to initialize
*/
void initialize(BootstrapRegistry registry);
}
private SpringApplicationRunListeners getRunListeners(String[] args) {
// 拿到上下文 SpringApplication
Class<?>[] types = new Class<?>[] { SpringApplication.class, String[].class };
return new SpringApplicationRunListeners(logger,
// 去 spring.factories 中找 SpringApplicationRunListener.class
getSpringFactoriesInstances(SpringApplicationRunListener.class, types, this, args),
this.applicationStartup);
}
9.4.2 自定義事件監聽元件
幾個重要元件的自定義
- ApplicationContextInitializer
public class MyApplicationContextInitializer implements ApplicationContextInitializer {
@Override
public void initialize(ConfigurableApplicationContext applicationContext) {
System.out.println("MyApplicationContextInitializer...initialize");
}
}
- ApplicationLineRunner
public class MyCommandLineRunner implements CommandLineRunner {
@Override
public void run(String... args) throws Exception {
System.out.println("MyCommandLineRunner...run...");
}
}
- SpringApplicationRunListener
public class MySpringApplicationRunListener implements SpringApplicationRunListener {
public SpringApplication application;
public MySpringApplicationRunListener(SpringApplication application,String[] args) {
this.application = application;
}
// 調用時機:應用剛一開始運作,剛建立好容器的基本資訊的時候就調用 starting,相當于應用開始啟動了
@Override
public void starting(ConfigurableBootstrapContext bootstrapContext) {
System.out.println("MySpringApplicationRunListener...starting...");
}
// 環境準備完成的時候調用
@Override
public void environmentPrepared(ConfigurableBootstrapContext bootstrapContext, ConfigurableEnvironment environment) {
System.out.println("MySpringApplicationRunListener...environmentPrepared...");
}
// IOC 容器準備完成
@Override
public void contextPrepared(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener...contextPrepared...");
}
// IOC 容器加載完成
@Override
public void contextLoaded(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener...contextLoaded...");
}
// IOC 容器啟動,調用 reflash 方法後
@Override
public void started(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener...started...");
}
// 整個容器建立完成全部都執行個體之後,整個容器沒有異常都啟動起來的時候調用
@Override
public void running(ConfigurableApplicationContext context) {
System.out.println("MySpringApplicationRunListener...running...");
}
@Override
public void failed(ConfigurableApplicationContext context, Throwable exception) {
System.out.println("MySpringApplicationRunListener...failed...");
}
}
- ApplicationListener
@Component
public class MyApplicationListener implements ApplicationListener {
@Override
public void onApplicationEvent(ApplicationEvent event) {
System.out.println("MyApplicationListener...onApplicationEvent");
}
}
- ApplicationRunner
@Component
public class MyApplicationRunner implements ApplicationRunner {
@Override
public void run(ApplicationArguments args) throws Exception {
System.out.println("MyApplicationRunner...run...");
}
}
然後再resources 檔案夾下建立 /META-INF/spring.factories
org.springframework.context.ApplicationContextInitializer=\
com.lss.listener.MyApplicationContextInitializer
org.springframework.context.ApplicationListener=\
com.lss.listener.MyApplicationListener
org.springframework.boot.SpringApplicationRunListener=\
com.lss.listener.MySpringApplicationRunListener
執行檢視輸出結果就可以直到那個事件先執行了
**學習參考視訊:**https://www.bilibili.com/video/BV19K4y1L7MT?p=1