SpringBoot 教程
SpringBoot
本項目參考 【狂神說Java】SpringBoot 最新教程 IDEA 版通俗易懂
Github 地址:https://github.com/Lockegogo/Java-Study
Hello, World!
什么是 Spring
- Spring 是一個開源框架,2003 年興起的一個輕量級的Java 開發框架,作者:Rod Johnson
- Spring是為了解決企業級應用開發的復雜性而創建的,簡化開發
Spring 是如何簡化 Java 開發的
為了降低 Java 開發的復雜性,Spring采用了以下 4 種關鍵策略:
- 基于 POJO 的輕量級和最小侵入性編程,所有東西都是 bean;
- 通過 IOC,依賴注入(DI)和面向接口實現松耦合;
- 基于切面(AOP)和慣例進行聲明式編程;
- 通過切面和模板減少樣式代碼:RedisTemplate,xxxTemplate;
什么是 SpringBoot
SpringBoot 是一個 javaweb 的開發框架,和 SpringMVC 類似,對比其他 javaweb 框架的好處,官方說是簡化開發,約定大于配置,能迅速的開發 web 應用,幾行代碼開發一個 http 接口。
所有的技術框架的發展似乎都遵循了一條主線規律:從一個復雜應用場景,衍生一種規范框架,人們只需要進行各種配置而不需要自己去是實現它,這時候強大的配置功能成了優點;發展到一定程度后,人們根據實際生產應用情況,選取其中實用功能和設計精華,重構出一些輕量級的框架,之后為了提高開發效率,嫌棄原先的各類配置過于麻煩,于是開始提倡約定大于配置,進而衍生出一些一站式的解決方案,這就是 Java 企業級應用 \(\to\) J2EE \(\to\) spring \(\to\) springboot 的過程。
SpringBoot 基于 Spring 開發,SpringBoot 本身并不提供 Spring 框架的核心特性以及擴展功能,只是用于快速、敏捷地開發新一代基于 Spring 框架的應用程序。也就是說,它并不是用來替代 Spring 的解決方案,而是和 Spring 框架緊密結合用于提升 Spring 開發者體驗的工具。SpringBoot 以約定大于配置的核心思想,默認幫我們進行了很多設置,多數 SpringBoot 應用只需要很少的 Spring 配置。同時它集成了大量常用的第三方庫配置(例如 Redis、MongoDB、Jpa、RabbitMQ、Quartz 等等),SpringBoot 應用中這些第三方庫幾乎可以零配置的開箱即用。
SpringBoot 的主要優點:
- 為所有 Spring 開發者更快的入門
- 開箱即用,提供各種默認配置來簡化項目配置
- 內嵌式容器簡化 Web 項目
- 沒有冗余代碼生成和 XML 配置的要求
第一個 SpringBoot 程序
HelloController.java
@RestController
public class HelloController {
// 接口:http://localhost:8080/hello
@RequestMapping("/hello")
public String hello() {
// 調用業務,接收前端的參數
return "Hello World";
}
}
pom.xml:自動生成
<dependencies>
<!--web 依賴:tomcat,dispatcherServlet,xml...-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
- 更改項目的端口號
# 更改項目的端口號
server.port=8081
運行原理初探
父依賴
- spring-boot-dependencies:核心依賴在父工程中,管理項目的資源過濾及插件!
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>3.0.2</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
- 點進去后發現還有一個父依賴,這才是真正管理 SpringBoot 應用里面所有版本依賴的地方
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-dependencies</artifactId>
<version>3.0.2</version>
</parent>
- 我們在寫或者引入一些 Springboot 依賴的時候,不需要指定版本,就因為有這些版本倉庫
啟動器 spring-boot-starter
springboot-boot-starter-xxx:Springboot 的啟動場景,Springboot 會將所有的功能場景,都變成一個個的啟動器,例如 spring-boot-starter-web 會幫我們自動導入 web 的所有依賴,我們需要使用什么功能,就只需要找到對應的啟動器就好了。
主程序
默認的主啟動類
// @SpringBootApplication 來標注一個主程序類
// 說明這是一個 SpringBoot 應用
@SpringBootApplication
public class SpringbootApplication {
public static void main(String[] args) {
// 以為是啟動了一個方法,沒想到啟動了一個服務
// 該方法返回一個 ConfigurableApplicationContext 對象
// 參數一:應用入口的類; 參數二:命令行參數
SpringApplication.run(SpringbootApplication.class, args);
}
}
SpringApplication 的實例化
這個類主要做了以下四件事情:
- 推斷應用的類型是普通的項目還是 Web 項目
- 查找并加載所有可用初始化器 , 設置到 initializers 屬性中
- 找出所有的應用程序監聽器,設置到 listeners 屬性中
- 推斷并設置 main 方法的定義類,找到運行的主類
run 方法的執行

注解(@SpringBootApplication)
作用:標注在某個類上說明這個類是 SpringBoot 的主配置
SpringBoot 就應該運行這個類的 main 方法來啟動 SpringBoot 應用;
進入這個注解:可以看到上面還有很多其他注解!
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
excludeFilters = {@Filter(
type = FilterType.CUSTOM,
classes = {TypeExcludeFilter.class}
), @Filter(
type = FilterType.CUSTOM,
classes = {AutoConfigurationExcludeFilter.class}
)}
)
public @interface SpringBootApplication {
// ......
}
- @ComponentScan
- 對應 XML 配置中的元素
- 自動掃描并加載符合條件的組件或者 bean,將這個 bean 定義加載到 IOC 容器中
- @SpringBootConfiguration
- SpringBoot 的配置類,標注在某個類上,表示這是一個 SpringBoot 的配置類
- @EnableAutoConfiguration
- 開啟自動配置功能
- @Import({AutoConfigurationImportSelector.class}):給容器導入組件
- AutoConfigurationImportSelector:自動配置導入選擇器
自動配置真正實現是從 classpath 中搜尋所有的
META-INF/spring.factories配置文件 ,并將其中對應的org.springframework.boot.autoconfigure. 包下的配置項,通過反射實例化為對應標注了@Configuration 的 JavaConfig形式的 IOC 容器配置類 , 然后將這些都匯總成為一個實例并加載到 IOC 容器中。
yaml 配置注入
yaml 語法學習
配置文件
SpringBoot 使用一個全局的配置文件 , 配置文件名稱是固定的:
application.properties- 語法結構 :key=value
application.yaml- key: value
配置文件的作用:修改 SpringBoot 自動配置的默認值,因為 SpringBoot 在底層都給我們自動配置好了。
YAML
YAML是 "YAML Ain't a Markup Language" (YAML不是一種標記語言)的遞歸縮寫。在開發的這種語言時,YAML 的意思其實是:"Yet Another Markup Language"(仍是一種標記語言)
這種語言以數據作為中心,而不是以標記語言為重點!
以前的配置文件,大多數都是使用 xml 來配置;比如一個簡單的端口配置,我們來對比下 yaml 和 xml:
- xml
<server>
<port>8081<port>
</server>
- yaml
server:
port: 8080
基礎語法
說明:語法要求嚴格!
-
空格不能省略
-
以縮進來控制層級關系,只要是左邊對齊的一列數據都是同一個層級的。
-
屬性和值的大小寫都是十分敏感的。
字面量:普通的值 [ 數字,布爾值,字符串 ]
- 字面量直接寫在后面就可以,字符串默認不用加上雙引號或者單引號;
k: v- 雙引號不會轉義字符串里面的特殊字符,特殊字符會作為本身想表示的意思;
- 比如 :name: "kuang \n shen" 輸出 :kuang 換行 shen
- 單引號,會轉義特殊字符,特殊字符最終會變成和普通字符一樣輸出
- 比如 :name: ‘kuang \n shen’ 輸出 :kuang \n shen
- 雙引號不會轉義字符串里面的特殊字符,特殊字符會作為本身想表示的意思;
對象、Map(鍵值對)
# 對象、Map格式
k:
v1:
v2:
在下一行來寫對象的屬性和值的關系,注意縮進;比如:
student:
name: qinjiang
age: 3
行內寫法
student: {name: qinjiang,age: 3}
數組( List、set )
用 - 值表示數組中的一個元素,比如:
pets:
- cat
- dog
- pig
行內寫法
pets: [cat,dog,pig]
修改 SpringBoot 的默認端口號
配置文件中添加,端口號的參數,就可以切換端口;
server:
port: 8082
注入配置文件
yaml 注入配置文件
yaml 文件更強大的地方在于,他可以給我們的實體類直接注入匹配值!
-
在 springboot 項目中的 resources 目錄下新建一個文件 application.yaml
-
編寫一個實體類 Dog;
@Component // 注冊 bean 到容器中
public class Dog {
private String name;
private Integer age;
// 有參無參構造、get、set方法、toString()方法
}
- 給 bean 注入屬性值:
@Value
@Component // 注冊 bean
public class Dog {
@Value("阿黃")
private String name;
@Value("18")
private Integer age;
}
- 在 SpringBoot 的測試類下注入狗狗輸出一下
@SpringBootTest
class Springboot02ConfigApplicationTests {
@Autowired
private Dog dog;
@Test
void contextLoads() {
System.out.println(dog);
}
}
@Autowired 是自動裝配(按類型,這樣就不用在代碼中 new 了),@Component 是注冊 bean 到容器中,注意這其中的概念差異。
- 再編寫一個復雜一點的實體類:Person
@Component
public class Person {
private String name;
private Integer age;
private Boolean happy;
private Date birth;
private Map<String,Object> maps;
private List<Object> lists;
private Dog dog;
// 有參無參構造、get、set方法、toString()方法
}
- 使用 yaml 配置的方式進行注入:
person:
name: locke
age: 3
happy: false
birth: 2000/01/01
maps: {k1: v1,k2: v2}
lists:
- code
- girl
- music
dog:
name: 旺財
age: 1
- 我們剛才已經把 person 這個對象的所有值都寫好了,我們現在來注入到我們的類中!
/*
@ConfigurationProperties 作用:
將配置文件中配置的每一個屬性的值,映射到這個組件中;
告訴 SpringBoot 將本類中的所有屬性和配置文件中相關的配置進行綁定
參數 prefix = “person” : 將配置文件中的 person 下面的所有屬性一一對應
*/
@Component
@ConfigurationProperties(prefix = "person")
public class Person {
private String name;
private Integer age;
private Boolean happy;
private Date birth;
private Map<String,Object> maps;
private List<Object> lists;
private Dog dog;
//有參無參構造、get、set方法、toString()方法
}
- IDEA 提示,配置注解器沒有找到,查看文檔,找到一個依賴
- 注解
@ConfigurationProperties(prefix = "person") - 點擊 open Decumentation 進入官網
- 在 pom 中導入依賴
- 注解
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional>
</dependency>
- 測試類測試
@SpringBootTest
class Springboot02ConfigApplicationTests {
@Autowired
private Person person;
@Test
void contextLoads() {
System.out.println(person);
}
}
加載指定的配置文件
兩種注解:
@PropertySource:加載指定的配置文件;@ConfigurationProperties:默認從全局配置文件中獲取值;
具體操作:
- 在 resources 目錄下新建一個
person.properties文件
name=locke
- 在代碼中加載 person.properties 文件
@PropertySource(value = "classpath:person.properties")
@Component //注冊bean
public class Person {
@Value("${name}")
private String name;
......
}
配置文件占位符
配置文件還可以編寫占位符生成隨機數:
person:
name: qinjiang${random.uuid}
age: ${random.int}
happy: false
birth: 2020/07/13
maps: {k1: v1,k2: v2}
lists:
- code
- music
- girl
dog:
name: ${person.hello:hello}_旺財
age: 3
對比小結
@Value 這個使用起來并不友好!我們需要為每個屬性單獨注解賦值,比較麻煩;我們來看個功能對比圖:
| @ConfigurationProperties | @Value | |
|---|---|---|
| 功能 | 批量注入配置文件中的屬性 | 一個個指定 |
| 松散綁定 | 支持 | 不支持 |
| SpEL | 不支持 | 支持 |
| JSR303 數據校驗 | 支持 | 不支持 |
| 復雜類型封裝 | 支持 | 不支持 |
-
@ConfigurationProperties只需要寫一次即可 , @Value 則需要每個字段都添加 -
松散綁定:這個什么意思呢? 比如我的 yaml 中寫的 last-name,這個和 lastName 是一樣的, - 后面跟著的字母默認是大寫的。這就是松散綁定。
-
JSR303 數據校驗 , 這個就是我們可以在字段是增加一層過濾器驗證,可以保證數據的合法性。
-
復雜類型封裝,yaml 中可以封裝對象 , 使用 value 就不支持。
JSR303 數據校驗原理
JSR 303
Springboot 中可以用 @validated 來校驗數據,如果數據異常則會統一拋出異常,方便異常中心統一處理。
我們這里來寫個注解讓我們的 name 只能支持 Email格式:
- 添加 validation 啟動器
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
@Email添加
@Component // 注冊 bean
@ConfigurationProperties(prefix = "person")
@Validated // 數據校驗
public class Person {
@Email(message="郵箱格式錯誤") // name 必須是郵箱格式
private String name;
}
- 運行結果 :default message [不是一個合法的電子郵件地址]
使用數據校驗,可以保證數據的正確性。
常見參數
@NotNull(message="名字不能為空")
private String userName;
@Max(value=120,message="年齡最大不能查過120")
private int age;
@Email(message="郵箱格式錯誤")
private String email;
空檢查
@Null 驗證對象是否為null
@NotNull 驗證對象是否不為null, 無法查檢長度為0的字符串
@NotBlank 檢查約束字符串是不是Null還有被Trim的長度是否大于0,只對字符串,且會去掉前后空格.
@NotEmpty 檢查約束元素是否為NULL或者是EMPTY.
Booelan檢查
@AssertTrue 驗證 Boolean 對象是否為 true
@AssertFalse 驗證 Boolean 對象是否為 false
長度檢查
@Size(min=, max=) 驗證對象(Array,Collection,Map,String)長度是否在給定的范圍之內
@Length(min=, max=) string is between min and max included.
日期檢查
@Past 驗證 Date 和 Calendar 對象是否在當前時間之前
@Future 驗證 Date 和 Calendar 對象是否在當前時間之后
@Pattern 驗證 String 對象是否符合正則表達式的規則
.......等等
除此以外,我們還可以自定義一些數據校驗規則
多環境切換
profile 是 Spring 對不同環境提供不同配置功能的支持,可以通過激活不同的環境版本,實現快速切換環境;(不同位置的優先級如下圖)
多配置文件
我們在主配置文件編寫的時候,文件名可以是 application-{profile}.properties/yml , 用來指定多個環境版本;
例如:
application-test.properties代表測試環境配置
application-dev.properties代表開發環境配置
但是 Springboot 并不會直接啟動這些配置文件,它默認使用 application.properties 主配置文件。
我們需要通過一個配置來選擇需要激活的環境:
# 比如在配置文件中指定使用 dev 環境,我們可以通過設置不同的端口號進行測試;
# 我們啟動 SpringBoot,就可以看到已經切換到 dev 下的配置了;
spring.profiles.active=dev
yaml 的多文檔塊
和 properties 配置文件中一樣,但是使用 yaml 去實現不需要創建多個配置文件,更加方便了 !
server:
port: 8081
# 選擇要激活那個環境塊
spring:
profiles:
active: test
---
server:
port: 8083
spring:
profiles: dev # 配置環境的名稱
---
server:
port: 8084
spring:
profiles: test # 配置環境的名稱
注意:如果 yaml 和 properties 同時都配置了端口,并且沒有激活其他環境 , 默認會使用 properties 配置文件的!
配置文件加載方式
springboot 啟動會掃描以下位置的 application.properties 或者 application.yaml 文件作為 SpringBoot 的默認配置文件:

優先級1:項目路徑下的 config 文件夾配置文件
優先級2:項目路徑下配置文件
優先級3:資源路徑下的 config 文件夾配置文件
優先級4:資源路徑下配置文件
運維小技巧
指定位置加載配置文件:通過 spring.config.location 來改變默認的配置文件位置。
項目打包好以后,我們可以使用命令行參數的形式,啟動項目的時候來指定配置文件的新位置(這種情況,一般是后期運維做的多,相同配置,外部指定的配置文件優先級最高);
java -jar spring-boot-config.jar --spring.config.location=F:/application.properties
自動配置原理
分析自動配置原理
-
SpringBoot 啟動的時候加載主配置類,開啟了自動配置功能
@EnableAutoConfiguration -
@EnableAutoConfiguration作用-
利用 EnableAutoConfigurationImportSelector 給容器中導入一些組件
-
可以查看
selectImports()方法的內容,他返回了一個 autoConfigurationEnty,來自this.getAutoConfigurationEntry(autoConfigurationMetadata,annotationMetadata);這個方法我們繼續來跟蹤: -
這個方法有一個值:
List<String> configurations = getCandidateConfigurations(annotationMetadata, attributes);叫做獲取候選的配置 ,我們點擊繼續跟蹤SpringFactoriesLoader.loadFactoryNames()- 掃描所有 jar 包類路徑下
META-INF/spring.factories -
- 把掃描到的這些文件的內容包裝成 properties 對象
- 從 properties 中獲取到 EnableAutoConfiguration.class 類(類名)對應的值,然后把他們添加在容器中
-
在類路徑下,
META-INF/spring.factories里面配置的所有 EnableAutoConfiguration 的值加入到容器中; -
每一個這樣的 xxxAutoConfiguration 類都是容器中的一個組件,都加入到容器中;用他們來做自動配置;
-
-
每一個自動配置類進行自動配置功能;
-
我們以 HttpEncodingAutoConfiguration(Http 編碼自動配置)為例解釋自動配置原理;
// 表示這是一個配置類,和以前編寫的配置文件一樣,也可以給容器中添加組件;
@Configuration
// 啟動指定類的 ConfigurationProperties 功能;
// 進入這個 HttpProperties 查看,將配置文件中對應的值和 HttpProperties 綁定起來;
// 并把 HttpProperties 加入到 ioc 容器中
@EnableConfigurationProperties({HttpProperties.class})
//Spring 底層 @Conditional 注解
// 根據不同的條件判斷,如果滿足指定的條件,整個配置類里面的配置就會生效;
// 這里的意思就是判斷當前應用是否是 web 應用,如果是,當前配置類生效
@ConditionalOnWebApplication(
type = Type.SERVLET
)
// 判斷當前項目有沒有這個類 CharacterEncodingFilter;SpringMVC 中進行亂碼解決的過濾器;
@ConditionalOnClass({CharacterEncodingFilter.class})
// 判斷配置文件中是否存在某個配置:spring.http.encoding.enabled;
// 如果不存在,判斷也是成立的
// 即使我們配置文件中不配置 pring.http.encoding.enabled=true,也是默認生效的;
@ConditionalOnProperty(
prefix = "spring.http.encoding",
value = {"enabled"},
matchIfMissing = true
)
public class HttpEncodingAutoConfiguration {
// 他已經和SpringBoot的配置文件映射了
private final Encoding properties;
// 只有一個有參構造器的情況下,參數的值就會從容器中拿
public HttpEncodingAutoConfiguration(HttpProperties properties) {
this.properties = properties.getEncoding();
}
// 給容器中添加一個組件,這個組件的某些值需要從properties中獲取
@Bean
@ConditionalOnMissingBean // 判斷容器沒有這個組件?
public CharacterEncodingFilter characterEncodingFilter() {
CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter();
filter.setEncoding(this.properties.getCharset().name());
filter.setForceRequestEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.REQUEST));
filter.setForceResponseEncoding(this.properties.shouldForce(org.springframework.boot.autoconfigure.http.HttpProperties.Encoding.Type.RESPONSE));
return filter;
}
}
一句話總結:根據當前不同的條件判斷,決定這個配置類是否生效!
- 一旦這個配置類生效,這個配置類就會給容器中添加各種組件;
- 這些組件的屬性是從對應的 properties 類中獲取的,這些類里面的每一個屬性又是和配置文件綁定的;
- 所有在配置文件中能配置的屬性都是在 xxxxProperties 類中封裝著;
- 配置文件能配置什么就可以參照某個功能對應的這個屬性類。
// 從配置文件中獲取指定的值和 bean 的屬性進行綁定
@ConfigurationProperties(prefix = "spring.http")
public class HttpProperties {
// .....
}
這就是自動裝配的原理。
重點
-
SpringBoot 啟動會加載大量的自動配置類
-
我們看我們需要的功能有沒有在 SpringBoot 默認寫好的自動配置類當中;
-
我們再來看這個自動配置類中到底配置了哪些組件;(只要我們要用的組件存在在其中,我們就不需要再手動配置了)
-
給容器中自動配置類添加組件的時候,會從 properties 類中獲取某些屬性。我們只需要在配置文件中指定這些屬性的值即可;
- xxxxAutoConfigurartion:自動配置類;給容器中添加組件
- xxxxProperties:封裝配置文件中相關屬性;
@Conditional
了解完自動裝配的原理后,我們來關注一個細節問題,自動配置類必須在一定的條件下才能生效;
@Conditional派生注解(Spring注解版原生的 @Conditional作用)
作用:必須是 @Conditional 指定的條件成立,才給容器中添加組件,配置配里面的所有內容才生效;
| @Conditional擴展注解 | 作用(判斷是否滿足當前指定條件) |
|---|---|
| @ConditionalOnJava | 系統的 java 版本是否符合要求 |
| @ConditionalOnJava | 容器中存在指定 Bean ; |
| @ConditionalOnMissingBean | 容器中不存在指定 Bean ; |
| @ConditionalOnExpression | 滿足 SpEL 表達式指定 |
| @ConditionalOnClass | 系統中有指定的類 |
| @ConditionalOnMissingClass | 系統中沒有指定的類 |
| @ConditionalOnSingleCandidate | 容器中只有一個指定的 Bean,或者這個 Bean 是首選 Bean |
| @ConditionalOnProperty | 系統中指定的屬性是否有指定的值 |
| @ConditionalOnResource | 類路徑下是否存在指定資源文件 |
| @ConditionalOnWebApplication | 當前是 web 環境 |
| @ConditionalOnNotWebApplication | 當前不是 web 環境 |
| @ConditionalOnJndi | JNDI 存在指定項 |
那么多的自動配置類,必須在一定的條件下才能生效;也就是說,我們加載了這么多的配置類,但不是所有的都生效了。
自動配置類是否生效
我們可以在 application.properties 通過啟用 debug=true屬性;
在控制臺打印自動配置報告,這樣我們就可以很方便的知道哪些自動配置類生效;
# 開啟springboot的調試類
debug=true
-
Positive matches:(自動配置類啟用的:正匹配)
-
Negative matches:(沒有啟動,沒有匹配成功的自動配置類:負匹配)
-
Unconditional classes: (沒有條件的類)
-
演示:查看輸出的日志
自定義 starter
分析完源碼以及自動裝配的過程,我們可以嘗試自定義一個啟動器來玩兒~
說明
啟動器模塊是一個 空 jar 文件,僅提供輔助性依賴管理,這些依賴可能用于自動裝配或者其他類庫;
命名歸約:
-
官方命名:
-
前綴:spring-boot-starter-xxx
-
比如:spring-boot-starter-web....
-
-
自定義命名:
-
xxx-spring-boot-starter
-
比如:mybatis-spring-boot-starter
-
編寫啟動器
- 在 IDEA 中新建一個空項目:
spring-boot-starter-diy - 新建一個普通的 Maven 模塊:
locke-spring-boot-starter - 新建一個 SpringBoot 模塊:
locke-spring-boot-starter-autoconfigure - 點擊 apply 即可,基本結構
- 在 starter 中導入 autoconfigure 的依賴
<!-- 啟動器 -->
<dependencies>
<!-- 引入自動配置模塊 -->
<dependency>
<groupId>com.locke</groupId>
<artifactId>locke-spring-boot-starter-autoconfigure</artifactId>
<version>0.0.1-SNAPSHOT</version>
</dependency>
</dependencies>
- 將
autoconfigure項目下多余的文件都刪掉,Pom 中只留下一個 starter,這是所有的啟動器基本配置! - 編寫一個自己的服務:
public class HelloService {
HelloProperties helloProperties;
public HelloProperties getHelloProperties() {
return helloProperties;
}
public void setHelloProperties(HelloProperties helloProperties) {
this.helloProperties = helloProperties;
}
public String sayHello(String name){
return helloProperties.getPrefix() + name + helloProperties.getSuffix();
}
}
- 編寫
HelloProperties配置類
// 前綴 locke.hello
@ConfigurationProperties(prefix = "locke.hello")
public class HelloProperties {
private String prefix;
private String suffix;
public String getPrefix() {
return prefix;
}
public void setPrefix(String prefix) {
this.prefix = prefix;
}
public String getSuffix() {
return suffix;
}
public void setSuffix(String suffix) {
this.suffix = suffix;
}
}
- 編寫我們的自動配置類并注入 bean,測試
@Configuration
@ConditionalOnWebApplication // web 應用生效
@EnableConfigurationProperties(HelloProperties.class)
public class HelloServiceAutoConfiguration {
@Autowired
HelloProperties helloProperties;
@Bean
public HelloService helloService(){
HelloService service = new HelloService();
service.setHelloProperties(helloProperties);
return service;
}
}
- 在 resources 編寫一個自己的
META-INF\spring.factories
# Auto Configure
org.springframework.boot.autoconfigure.EnableAutoConfiguration=nuc.ss.HelloServiceAutoConfiguration
- 編寫完成后,可以安裝到 maven 倉庫中。
測試啟動器
- 新建一個 SpringBoot 項目
- 導入我們自己寫的啟動器
<dependency>
<groupId>nuc.ss</groupId>
<artifactId>ss-spring-boot-starter</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
- 編寫一個
HelloController進行測試我們自己寫的接口!
@RestController
public class HelloController {
@Autowired
HelloService helloService;
@RequestMapping("/hello")
public String hello(){
return helloService.sayHello("zxc");
}
}
- 編寫配置文件:
application.properties
locke.hello.prefix="ppp"
locke.hello.suffix="sss"
- 啟動項目進行測試!
Web 開發
使用 SpringBoot 的步驟:
-
創建一個 SpringBoot 應用,選擇我們需要的模塊,SpringBoot 就會默認將我們的需要的模塊自動配置好
-
手動在配置文件中配置部分配置項目就可以運行起來了
-
專注編寫業務代碼,不需要考慮以前那樣一大堆的配置了。
- 向容器中自動配置組件 :*** Autoconfiguration
- 自動配置類,封裝配置文件的內容:***Properties
靜態資源處理
靜態資源映射規則
SpringBoot 如何處理靜態資源,如 css, js 等文件?
如果我們是一個 web 應用,main 下面會有一個 webapp,之前是將所有頁面導入在這里面的,但現在的 pom 打包方式是 jar,那怎么辦?
我們先來聊聊這個靜態資源映射規則:
- SpringBoot 中,SpringMVC 的 web 配置都在 WebMvcAutoConfiguration 這個配置類里面;
- WebMvcAutoConfigurationAdapter 有很多配置方法;有一個方法:addResourceHandlers 添加資源處理
@Override
public void addResourceHandlers(ResourceHandlerRegistry registry) {
if (!this.resourceProperties.isAddMappings()) {
// 已禁用默認資源處理
logger.debug("Default resource handling disabled");
return;
}
// 緩存控制
Duration cachePeriod = this.resourceProperties.getCache().getPeriod();
CacheControl cacheControl = this.resourceProperties.getCache().getCachecontrol().toHttpCacheControl();
// webjars 配置
if (!registry.hasMappingForPattern("/webjars/**")) {
customizeResourceHandlerRegistration(registry.addResourceHandler("/webjars/**")
.addResourceLocations("classpath:/META-INF/resources/webjars/")
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
// 靜態資源配置
String staticPathPattern = this.mvcProperties.getStaticPathPattern();
if (!registry.hasMappingForPattern(staticPathPattern)) {
customizeResourceHandlerRegistration(registry.addResourceHandler(staticPathPattern)
.addResourceLocations(getResourceLocations(this.resourceProperties.getStaticLocations()))
.setCachePeriod(getSeconds(cachePeriod)).setCacheControl(cacheControl));
}
}
讀一下源代碼:比如所有的 /webjars/** , 都需要去 classpath:/META-INF/resources/webjars/ 找對應的資源;
webjars
Webjars 本質就是以 jar 包的方式引入我們的靜態資源 , 我們以前要導入一個靜態資源文件,直接導入即可。
第一種靜態資源映射規則
SpringBoot 導入靜態資源需要使用 Webjars。
- 引入 jQuery 對應版本的 pom 依賴:
<dependency>
<groupId>org.webjars</groupId>
<artifactId>jquery</artifactId>
<version>3.4.1</version>
</dependency>
- 導入完畢,查看 webjars 目錄結構,并訪問 Jquery.js 文件!

- 訪問:只要是靜態資源,SpringBoot 就會去對應的路徑尋找資源,我們這里訪問:http://localhost:8080/webjars/jquery/3.4.1/jquery.js
第二種靜態資源映射規則
如果是自己的靜態資源該怎么導入呢?
- 去找
staticPathPattern發現第二種映射規則 :/** , 訪問當前的項目任意資源,它會去找resourceProperties這個類,我們可以點進去看一下分析:
// 進入方法
public String[] getStaticLocations() {
return this.staticLocations;
}
// 找到對應的值
private String[] staticLocations = CLASSPATH_RESOURCE_LOCATIONS;
// 找到路徑
private static final String[] CLASSPATH_RESOURCE_LOCATIONS = {
"classpath:/META-INF/resources/",
"classpath:/resources/",
"classpath:/static/",
"classpath:/public/"
};
- ResourceProperties 可以設置和我們靜態資源有關的參數;這里面指向了它會去尋找資源的文件夾,即上面數組的內容。
- 以下四個目錄存放的靜態資源可以被我們識別:
"classpath:/META-INF/resources/" // localhost:8080/webjars
"classpath:/resources/" // localhost:8080
"classpath:/static/" // localhost:8080
"classpath:/public/" // localhost:8080
- 我們可以在 resources 根目錄下新建對應的文件夾,都可以存放我們的靜態文件;

- 比如我們訪問 http://localhost:8080/1.js , 他就會去這些文件夾中尋找對應的靜態資源文件;
自定義靜態資源路徑
我們也可以自己通過配置文件來指定一下,哪些文件夾是需要我們放靜態資源文件的,在 application.properties 中配置;
spring.resources.static-locations=classpath:/coding/,classpath:/ss/
但是最好不要這么做。
總結
- 在springboot,我們可以使用一下方式處理靜態資源
- webjars
localhost:8080/webjars/ - public,static,/**,resources
localhost:8080/
- webjars
- 優先級:resources > static(默認) > public
首頁處理
首頁定制
靜態資源文件夾說完后,我們繼續向下看源碼!可以看到一個歡迎頁的映射,就是我們的首頁!

-
歡迎頁,靜態資源文件夾下的所有
index.html頁面;被 /** 映射。 -
比如我訪問 http://localhost:8080/ ,就會找靜態資源文件夾下的 index.html
-
新建一個
index.html,在我們上面的 3 個目錄中任意一個;然后訪問測試 http://localhost:8080/ 看結果!
首頁圖標
- 關閉 SpringBoot 默認圖標:
# 關閉默認圖標
spring.mvc.favicon.enabled=false
-
自己放一個圖標在靜態資源目錄下,我放在 public 目錄下
-
清除瀏覽器緩存
Ctrl + F5!刷新網頁,發現圖標已經變成自己的了!
2.2.x之后的版本(如2.3.0)直接執行 2 和 3 就可以了
Thymeleaf 模板引擎
-
前端交給我們的頁面,是 html 頁面。如果是我們以前開發,我們需要把他們轉成 jsp 頁面,JSP 好處就是當我們查出一些數據轉發到 JSP 頁面以后,我們可以用 jsp 輕松實現數據的顯示,及交互等。
-
jsp 支持非常強大的功能,包括能寫 Java 代碼,但是第一 SpringBoot 這個項目首先是以 jar 的方式,不是 war;第二,我們用的還是嵌入式的 Tomcat,所以現在默認是不支持 jsp。
-
不支持 jsp,如果直接用純靜態頁面的方式,會給開發帶來非常大的麻煩,怎么辦?
現在就該輪到模板引擎出場了:Thymeleaf

模板引擎的作用:接收后臺傳遞的模板和數據,將數據進行解析,填充到指定位置并進行展示。
引入 Thymeleaf
<!--thymeleaf-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
分析
引入之后如何使用?
首先按照 SpringBoot 的自動配置原理看一下 Thymeleaf 的自動配置規則,然后根據規則使用。
- 找到 Thymeleaf 的自動配置類:
ThymeleafProperties
@ConfigurationProperties(
prefix = "spring.thymeleaf"
)
public class ThymeleafProperties {
private static final Charset DEFAULT_ENCODING;
public static final String DEFAULT_PREFIX = "classpath:/templates/";
public static final String DEFAULT_SUFFIX = ".html";
private boolean checkTemplate = true;
private boolean checkTemplateLocation = true;
private String prefix = "classpath:/templates/";
private String suffix = ".html";
private String mode = "HTML";
private Charset encoding;
}
- 我們可以在其中看到默認的前綴和后綴!只需要把 html 頁面放在類路徑下的 templates 下,就可以自動渲染了
測試
- 編寫一個 TestController
@Controller
public class TestController {
@RequestMapping("/test")
public String test1(){
//classpath:/templates/test.html
return "test";
}
}
- 編寫一個測試頁面 test.html 放在 templates 目錄下
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<h1>Test 頁面</h1>
</body>
</html>
- 啟動項目請求測試
語法學習
要學習語法,還是參考官網文檔最為準確,我們找到對應的版本看一下;
Thymeleaf 官網:https://www.thymeleaf.org/ , 簡單看一下官網!我們去下載 Thymeleaf 的官方文檔!
入門
- 修改測試請求,增加數據傳輸
@RequestMapping("/t1")
public String test1(Model model){
// 存入數據
model.addAttribute("msg","Hello,Thymeleaf");
// classpath:/templates/test.html
return "test";
}
- 在 html 文件中導入命名空間的約束,方便提示
<html lang="en" xmlns:th="http://www.thymeleaf.org">
- 編寫前端頁面
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>狂神說</title>
</head>
<body>
<h1>測試頁面</h1>
<!--th:text 就是將 div 中的內容設置為它指定的值,和之前學習的 Vue 一樣-->
<div th:text="${msg}"></div>
</body>
</html>
進階
- 我們可以使用任意的
th:attr來替換 html 中原生屬性的值 - 可以寫的表達式如下:
Simple expressions:(表達式語法)
Variable Expressions: ${...}:獲取變量值;OGNL;
1)、獲取對象的屬性、調用方法
2)、使用內置的基本對象:#18
#ctx : the context object.
#vars: the context variables.
#locale : the context locale.
#request : (only in Web Contexts) the HttpServletRequest object.
#response : (only in Web Contexts) the HttpServletResponse object.
#session : (only in Web Contexts) the HttpSession object.
#servletContext : (only in Web Contexts) the ServletContext object.
3)、內置的一些工具對象:
#execInfo : information about the template being processed.
#uris : methods for escaping parts of URLs/URIs
#conversions : methods for executing the configured conversion service (if any).
#dates : methods for java.util.Date objects: formatting, component extraction, etc.
#calendars : analogous to #dates , but for java.util.Calendar objects.
#numbers : methods for formatting numeric objects.
#strings : methods for String objects: contains, startsWith, prepending/appending, etc.
#objects : methods for objects in general.
#bools : methods for boolean evaluation.
#arrays : methods for arrays.
#lists : methods for lists.
#sets : methods for sets.
#maps : methods for maps.
#aggregates : methods for creating aggregates on arrays or collections.
==================================================================================
Selection Variable Expressions: *{...}:選擇表達式:和${}在功能上是一樣;
Message Expressions: #{...}:獲取國際化內容
Link URL Expressions: @{...}:定義URL;
Fragment Expressions: ~{...}:片段引用表達式
Literals(字面量)
Text literals: 'one text' , 'Another one!' ,…
Number literals: 0 , 34 , 3.0 , 12.3 ,…
Boolean literals: true , false
Null literal: null
Literal tokens: one , sometext , main ,…
Text operations:(文本操作)
String concatenation: +
Literal substitutions: |The name is ${name}|
Arithmetic operations:(數學運算)
Binary operators: + , - , * , / , %
Minus sign (unary operator): -
Boolean operations:(布爾運算)
Binary operators: and , or
Boolean negation (unary operator): ! , not
Comparisons and equality:(比較運算)
Comparators: > , < , >= , <= ( gt , lt , ge , le )
Equality operators: == , != ( eq , ne )
Conditional operators: 條件運算(三元運算符)
If-then: (if) ? (then)
If-then-else: (if) ? (then) : (else)
Default: (value) ?: (defaultvalue)
Special tokens:
No-Operation: _
測試
- 編寫一個Controller,放一些數據
@RequestMapping("/test2")
public String test2(Map<String,Object> map){
// 存入數據
map.put("msg","<h1>Hello</h1>");
map.put("users", Arrays.asList("qinjiang","kuangshen"));
// classpath:/templates/test.html
return "test";
}
- 測試頁面讀取數據
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>
<h1>Test頁面</h1>
<!--不轉義-->
<div th:text="${msg}"></div>
<!--轉義-->
<div th:utext="${msg}"></div>
<hr>
<!--遍歷數據-->
<!--th:each每次遍歷都會生成當前這個標簽:官網#9-->
<h3 th:each="user:${users}" th:text="${user}"></h3>
<hr>
<!--行內寫法:官網#12-->
<h3 th:each="user:${users}">[[ ${user} ]]</h3>
</div>
</body>
</html>
MVC 自動配置原理
在進行項目編寫前,我們還需要知道一個東西,就是 SpringBoot 對我們的 SpringMVC 還做了哪些配置,包括如何擴展,如何定制。
只有把這些都搞清楚了,我們在之后使用才會更加得心應手。途徑一:源碼分析,途徑二:官方文檔!
內容協商視圖解析器
ContentNegotiatingViewResolver
- 自動配置了 ViewResolver,就是我們之前學習的 SpringMVC 的視圖解析器
- 即根據方法的返回值取得視圖對象(View),然后由視圖對象決定如何渲染(轉發,重定向)。
轉發和重定向的區別:轉發是服務器行為;重定向是客戶端行為。
- 地址欄顯示
- 轉發 forward:服務器請求資源,服務器直接訪問目標地址的 URL, 把那個 URL 的響應內容讀取過來,然后把這些內容再發給瀏覽器。瀏覽器根本不知道服務器發送的內容從哪里來的,所以它的地址欄還是原來的地址。
- 重定向 redirect:服務端根據邏輯,發送一個狀態碼,告訴瀏覽器重新去請求那個地址。所以地址欄顯示的是新的 URL。
- 數據共享
- 轉發 forward:轉發頁面和轉發到的頁面可以共享 request 里面的數據。
- 重定向 redirect:不能共享數據。
- 運用地方
- 轉發 forward:一般用于用戶登錄的時候,根據角色轉發到相應的模塊。
- 重定向 redirect:一般用于用戶注銷登錄返回主頁面和跳轉到其他的網站等。
- 效率
- 轉發 forward:效率高。
- 重定向 redirect:效率低。
- 我們去看看這里的源碼:我們找到
WebMvcAutoConfiguration, 然后搜索ContentNegotiatingViewResolver。找到viewResolver,繼續點進去查看找到對應的解析視圖的代碼:resolveViewName,繼續點看看其是如何獲得候選的視圖的:Iterator var5 = this.viewResolvers.iterator(); - 得出結論:ContentNegotiatingViewResolver 這個視圖解析器就是用來組合所有的視圖解析器的
- 繼續研究組合邏輯,看到有個屬性
viewResolvers,它是在哪里進行賦值的?
protected void initServletContext(ServletContext servletContext) {
// 這里它是從 beanFactory 工具中獲取容器中的所有視圖解析器
// ViewRescolver.class 把所有的視圖解析器來組合的
Collection<ViewResolver> matchingBeans = BeanFactoryUtils.beansOfTypeIncludingAncestors(this.obtainApplicationContext(), ViewResolver.class).values();
ViewResolver viewResolver;
if (this.viewResolvers == null) {
this.viewResolvers = new ArrayList(matchingBeans.size());
}
// ...............
}
- 既然它是在容器中去找視圖解析器,那我們也可以去實現一個視圖解析器了
自定義視圖解析器
- 在主程序中寫一個視圖解析器:
// 擴展 springmvc DispatchServlet
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
// public interface ViewResolver 實現了視圖解析器接口的類,我們就可以把它看做視圖解析器
@Bean
public ViewResolver myViewResolver() {
return new MyViewResolver();
}
// 自定義了一個自己的視圖解析器
public static class MyViewResolver implements ViewResolver {
@Override
public View resolveViewName(String s, Locale locale) throws Exception {
return null;
}
}
}
注意
@Bean和@ Component(@Controller/@Service/@Repository) 的區別:
- @Component: 作用于類上,告知 Spring,為這個類創建 Bean,通常是通過類路徑掃描來自動偵測以及自動裝配到 Spring 容器中
- @Bean:主要作用于方法上 ,告知 Spring,這個方法會返回一個對象,且要注冊在 Spring 的上下文中。通常方法體中包含產生 Bean 的邏輯,相當于 xml 文件中的
標簽
- 給 DispatcherServlet 中的 doDispatch方法 加個斷點進行調試一下,因為所有的請求都會走到這個方法中

- 啟動我們的項目(以 debug 的方式),然后隨便訪問一個頁面,回到 IDEA 看一下 Debug 信息,找到
this(DispatcherServlet)

- 找到視圖解析器 (viewResolvers),就可以看到自己定義的

如果我們想要使用自己定制化的東西,只需要給容器在添加這個組件就好了,剩下的事情 SpringBoot 會幫我們做。
轉換器和格式化器
- 在
WebMvcAutoConfiguration中找到格式化轉換器:
@Bean
@Override
public FormattingConversionService mvcConversionService() {
// 拿到配置文件中的格式化規則
WebConversionService conversionService =
new WebConversionService(this.mvcProperties.getDateFormat());
addFormatters(conversionService);
return conversionService;
}
- 點擊去:可以看到在我們的 Properties 文件中,我們可以進行自動配置它!
public String getDateFormat() {
return this.format.getDate();
}
public String getDate() {
return this.date;
}
/**
* Date format to use, for example `dd/MM/yyyy`.默認的
*/
private String date;
- 如果配置了自己的格式化方式,就會注冊到 Bean 中生效,我們可以在配置文件中配置日期格式化的規則:
spring.mvc.date=
@Deprecated
public void setDateFormat(String dateFormat) {
this.format.setDate(dateFormat);
}
public void setDate(String date) {
this.date = date;
}
修改默認配置
學習心得:
- 通過 WebMVC 的自動配置原理分析,我們要學會通過源碼探究,得出結論;
- SpringBoot 的底層,大量地用到了這些設計思想,是很好的學習資料;
- SPringleBoot 在自動配置很多組件的時候,先看容器中有沒有用戶自己配置的,如果沒有就用自動配置的;
- 如果有些組件可以存在多個,比如我們的視圖解析器,就將用戶配置的和自己默認的組合起來;
- 擴展使用 SpringMVC,我們需要編寫一個
@Configuration注解類,并且類型要為WebMvcConfigurer,還不能標注@EnableWebMvc注解
@Configuration 用于定義配置類,可替換 xml 配置文件,被注解的類內部包含有一個或多個被 @Bean 注解的方法,這些方法將會被 AnnotationConfigApplicationContext 或 AnnotationConfigWebApplicationContext 類進行掃描,并用于構建 bean 定義,初始化 Spring 容器。
具體實現:
- 新建一個
config包,寫一個類MyMvcConfig:
// 如果我們要擴展 springmvc,官方建議我們這樣去做 @Configuration
// 應為類型要求為 WebMvcConfigurer,所以我們實現其接口
// 擴展 springmvc DispatchServlet
// @EnableWebMvc // 這玩意就是導入了一個類,DelegatingWebMvcConfiguration,從容器中獲取所有的 webMvcConfig
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
// 瀏覽器發送 /test2,就會跳轉到 test 頁面;
registry.addViewController("/test2").setViewName("test");
}
}
全面接管 SpringMVC
- 全面接管即:SpringBoot 對 SpringMVC 的自動配置不需要了,所有都是我們自己去配置!
- 只需在我們的配置類中要加一個
@EnableWebMvc。 - 如果我們全面接管 SpringMVC,之前 SpringBoot 給我們配置的靜態資源映射一定會無效,可以測試一下;
當然在開發中,我們不推薦全面接管 SpringMVC。
員工管理:重要
準備工作
前端頁面
-
將 html 頁面放入 templates 目錄
-
將 css,js,img 放入到 static 目錄
實體類的編寫
- Department
// 部門表
@Data
@AllArgsConstructor
@NoArgsConstructor
public class Department {
private Integer id;
private String departmentName;
}
- Employee
// 員工表
@Data
@NoArgsConstructor
public class Employee {
private Integer id;
private String lastName;
private String email;
private Integer gender; // 0:女,1:男
private Department department;
private Date birth;
public Employee(Integer id, String lastName, String email, Integer gender, Department department) {
this.id = id;
this.lastName = lastName;
this.email = email;
this.gender = gender;
this.department = department;
// 默認的創建日期
this.birth = new Date();
}
}
dao 層模擬數據庫
- DepartmentDao
// 部門 Dao
@Repository
public class DepartmentDao {
// 模擬數據庫數據
private static Map<Integer, Department> departments = null;
static {
departments = new HashMap<Integer, Department>(); // 創建一個部門表
departments.put(101,new Department(101,"教學部"));
departments.put(102,new Department(102,"市場部"));
departments.put(103,new Department(103,"教研部"));
departments.put(104,new Department(104,"運營部"));
departments.put(105,new Department(105,"后勤部"));
}
// 獲得所有部門信息
public Collection<Department> getDepartment() {
return departments.values();
}
// 通過 id 得到部門
public Department getDepartmentById(Integer id) {
return departments.get(id);
}
}
- EmployeeDao
// 員工 Dao
@Repository
public class EmployeeDao {
// 模擬數據庫數據
private static Map<Integer, Employee> employees = null;
// 員工所屬部門
@Autowired
private DepartmentDao departmentDao;
static {
employees = new HashMap<Integer, Employee>();//創建一個員工表
employees.put(1001, new Employee(1001, "AA", "A123456@qq.com", 1, new Department(101, "教學部")));
employees.put(1002, new Employee(1002, "BB", "B123456@qq.com", 0, new Department(102, "市場部")));
employees.put(1003, new Employee(1003, "CC", "C123456@qq.com", 1, new Department(103, "教研部")));
employees.put(1004, new Employee(1004, "DD", "D123456@qq.com", 0, new Department(104, "運營部")));
employees.put(1005, new Employee(1005, "EE", "E123456@qq.com", 1, new Department(105, "后勤部")));
}
// 主鍵自增
private static Integer ininId = 1006;
// 增加一個員工
public void save(Employee employee) {
if (employee.getId() == null) {
employee.setId(ininId++);
}
employee.setDepartment(departmentDao.getDepartmentById(employee.getDepartment().getId()));
employees.put(employee.getId(),employee);
}
// 查詢全部員工信息
public Collection<Employee> getAll() {
return employees.values();
}
// 通過 id 查詢員工
public Employee getEmployeeById(Integer id) {
return employees.get(id);
}
// 刪除員工通過 id
public void delete(Integer id) {
employees.remove(id);
}
}

首頁實現
第一種方式
創建一個 IndexController,寫一個返回首頁的方法(不建議使用)
@Controller
public class IndexController{
@RequestMapping({"/","/index.html"})
public String index() {
return "index";
}
}
第二種方式
創建一個 config 目錄,在里面寫一個 MyMvcConfig,里面重寫 addViewControllers 方法:
@Configuration
public class MyMvcConfig implements WebMvcConfigurer {
@Override
public void addViewControllers(ViewControllerRegistry registry) {
registry.addViewController("/").setViewName("index");
registry.addViewController("/index.html").setViewName("index");
}
}

加載靜態資源
- 導入 thymeleaf 包
<html lang="en" xmlns:th="http://www.thymeleaf.org">
- 將所有頁面的靜態資源使用 thymeleaf 接管:注意在
application.properties中關掉模板引擎的緩存spring.thymeleaf.cache=false
<!-- css 的導入 -->
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
<link th:href="@{/css/signin.css}" rel="stylesheet">
<!-- 圖片的導入 -->
<img class="mb-4" th:src="@{/img/bootstrap-solid.svg}" alt="" width="72" height="72">
<!-- js 導入 -->
<script type="text/javascript" th:src="@{/js/jquery-3.2.1.slim.min.js}"></script>
<script type="text/javascript" th:src="@{/js/popper.min.js}"></script>
<script type="text/javascript" th:src="@{/js/bootstrap.min.js}"></script>
<script type="text/javascript" th:src="@{/js/feather.min.js}"></script>
<script type="text/javascript" th:src="@{/js/Chart.min.js}"></script>
下面是完整的 index.html:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Signin Template for Bootstrap</title>
<!-- Bootstrap core CSS -->
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
<!-- Custom styles for this template -->
<link th:href="@{/css/signin.css}" rel="stylesheet">
</head>
<body class="text-center">
<form class="form-signin" action="dashboard.html">
<img class="mb-4" th:src="@{/img/bootstrap-solid.svg}" alt="" width="72" height="72">
<h1 class="h3 mb-3 font-weight-normal">Please sign in</h1>
<label class="sr-only">Username</label>
<input type="text" class="form-control" placeholder="Username" required="" autofocus="">
<label class="sr-only">Password</label>
<input type="password" class="form-control" placeholder="Password" required="">
<div class="checkbox mb-3">
<label><input type="checkbox" value="remember-me">Remember me</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit">Sign in</button>
<p class="mt-5 mb-3 text-muted">? 2022-2023</p>
<a class="btn btn-sm">中文</a>
<a class="btn btn-sm">English</a>
</form>
</body>
</html>
再次看一下首頁頁面:

頁面國際化
- 在
resources文件夾下新建i18n(internationalization 的簡稱)

注意名字千萬不要寫錯!!!
- 在
application.properties中配置路徑:
spring.thymeleaf.cache=false
# server.servlet.context-path=""
# 我們的配置文件的真實位置
spring.messages.basename=i18n.login
# 時間日期格式化
spring.mvc.format.date=yyyy-MM-dd
- 在
index.html中進行修改:使用#{}號進行取值
@{}:thymeleaf 中的超鏈接表達式#{}:thymeleaf 中的消息表達式,或者資源表達式,一般和th:text一起使用${}:變量表達式,用于訪問的是容器上下文的變量,比如域變量:request 域,session 域
<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>
下面是完整的 index.html:
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:h="http://www.w3.org/1999/xhtml">
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
<meta name="description" content="">
<meta name="author" content="">
<title>Signin Template for Bootstrap</title>
<!-- Bootstrap core CSS -->
<link th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
<!-- Custom styles for this template -->
<link th:href="@{/css/signin.css}" rel="stylesheet">
</head>
<body class="text-center">
<form class="form-signin" action="dashboard.html">
<img class="mb-4" th:src="@{/img/bootstrap-solid.svg}" alt="" width="72" height="72">
<h1 class="h3 mb-3 font-weight-normal" th:text="#{login.tip}">Please sign in</h1>
<input type="text" class="form-control" th:placeholder="#{login.username}" required="" autofocus="">
<input type="password" class="form-control" th:placeholder="#{login.password}" required="">
<div class="checkbox mb-3">
<label>
<input type="checkbox" value="remember-me" th:text="#{login.remember}">
</label>
</div>
<button class="btn btn-lg btn-primary btn-block" type="submit" th:text="#{login.btn}">Sign in</button>
<p class="mt-5 mb-3 text-muted">? 2022-2023</p>
<a class="btn btn-sm" th:href="@{/index.html(l='zh_CN')}">中文</a>
<a class="btn btn-sm" th:href="@{/index.html(l='en_US')}">English</a>
</form>
</body>
</html>
- 在 config 文件夾中新建
MyLocalResolver類
public class MyLocalResolver implements LocaleResolver {
// 解析請求
@Override
public Locale resolveLocale(HttpServletRequest request) {
// 獲取請求中的語言參數
String language = request.getParameter("l");
Locale locale = Locale.getDefault(); // 如果沒有就使用默認的
// 如果請求的鏈接攜帶了國家化的參數
if (!StringUtils.isEmpty(language)) {
// zh_CN
String[] split = language.split("_");
// 國家,地區
locale = new Locale(split[0], split[1]);
}
return locale;
}
@Override
public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) {
}
}
- 在 MyMvcConfig 中將自己寫的組件配置到 spring 容器中:
// 自定義的國際化組件就生效了
@Bean
public LocaleResolver localeResolver() {
return new MyLocalResolver();
}
登錄頁面
- 首頁登錄頁面表單的修改
<form class="form-signin" th:action="@{/user/login}">
......
<!--如果 msg 的消息不為空,則顯示這個消息-->
<p style="color: red" th:text="${msg}" th:if="${not #strings.isEmpty({msg})}"></p>
<input type="text" name="username" class="form-control" th:placeholder="#{login.username}" required="" autofocus="">
<input type="password" name="password" class="form-control" th:placeholder="#{login.password}" required="">
......
</form>
- 寫一個 LoginController 登錄驗證
@Controller
public class LoginController {
@RequestMapping("/user/login")
public String login(@RequestParam("username") String username,
@RequestParam("password") String password,
Model model,
HttpSession session) {
// 具體的業務,登錄成功跳轉到 dashboard 頁面
if (!StringUtils.isEmpty(username) && "123456".equals(password)) {
session.setAttribute("loginUser", username);
return "redirect:/main.html";
} else {
model.addAttribute("msg", "用戶名或者密碼錯誤");
return "index";
}
}
}
注意這里 session 和 model 兩個屬性的區別:
- session:即會話,是客戶為實現特定應用目的與系統的多次交互請求。Http 協議是一種 無狀態 的協議,客戶端每打開一個 web 頁面,它就會與服務器建立一個新連接,發送新請求到服務器,服務器處理請求并將該請求返回到客戶端。服務器不記錄任何客戶端信息,session 是一種能將信息保存于服務器端的技術,能記錄特定的客戶端到服務器的一系列請求。session 里放的數據保存在服務器,可以供其他頁面使用,只要用戶不退出或者 SESSION 過期,這個值就一直可以保留。在當前的 request 周期之內,調用 getAttribute 方法同樣也可以得到。
- model:一個 request 級別的接口,可以將數據放入視圖中。model 的數據,只能在 Controller 返回的頁面使用,其他頁面不能使用。
- 登錄頁面不友好(密碼泄露)

-
解決密碼泄露的問題
- 加一個 main 映射在
MyMvcConfig中
public class MyMvcConfig implements WebMvcConfigurer{ @Override public void addViewControllers(ViewControllerRegistry registry) { registry.addViewController("/").setViewName("index"); registry.addViewController("/index.html").setViewName("index"); registry.addViewController("/main.html").setViewName("dashboard"); } }- 修改
LoginController跳轉頁面代碼 ( redirect 跳轉,這樣地址欄就會顯示新地址)
@Controller public class LoginController { @RequestMapping("/user/login") public String login(@RequestParam("username") String username, @RequestParam("password") String password, Model model, HttpSession session) { // 具體的業務, 登錄成功跳轉到 dashboard 頁面 if (!StringUtils.isEmpty(username) && "123456".equals(password)) { return "redirect:/main.html"; } else { model.addAttribute("msg", "用戶名或者密碼錯誤"); return "index"; } } } - 加一個 main 映射在
-
是否存在問題?
- 登錄成功才可以進入 main 頁面,否則直接輸入 http://localhost:8080/main.html 就可以訪問首頁了,需要攔截器實現
登錄攔截器
- 在
LoginController中添加一個 session 判斷登錄
@Controller
public class LoginController {
@RequestMapping("/user/login")
public String login(@RequestParam("username") String username,
@RequestParam("password")String password,
Model model,
HttpSession session) {
// 具體的業務, 登錄成功跳轉到 dashboard 頁面
if (!StringUtils.isEmpty(username) && "123456".equals(password)) {
session.setAttribute("loginUser",username);
return "redirect:/main.html";
} else {
model.addAttribute("msg","用戶名或者密碼錯誤");
return "index";
}
}
}
- 在
config頁面寫一個LoginHandlerInterceptor攔截器
public class LoginHandlerInterceptor implements HandlerInterceptor {
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
// 登錄成功之后,應該有用戶的 session
Object loginUser = request.getSession().getAttribute("loginUser");
if (loginUser == null) {
request.setAttribute("msg","沒有權限,請先登錄");
// 轉發,服務器跳轉
request.getRequestDispatcher("/index.html").forward(request,response);
return false;
} else {
return true;
}
}
}
MyMvcConfig頁面重寫攔截器方法
@Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new LoginHandlerInterceptor())
.addPathPatterns("/**")
.excludePathPatterns("/index.html","/","/user/login","/css/**","/js/**","/img/**");
}
注意:靜態資源的過濾,否則頁面渲染效果會消失。
- 在
dashboard.html頁面修改登錄信息為 session[[ ${session.loginUser} ]],登錄成功之后會顯示用戶名
員工列表顯示
后臺編寫
后臺編寫 EmployeeController
@Controller
public class EmployeeController {
@Autowired
EmployeeDao employeeDao;
@RequestMapping("/emps")
public String list(Model model) {
Collection<Employee> employees = employeeDao.getAll();
model.addAttribute("emps",employees);
return "emp/list";
}
}
提取公共頁面
- 員工管理前端頁面地址的修改(list.html 和 dashboard.html)
@{/emps}
<li class="nav-item">
<a class="nav-link" th:href="@{/emps}">
......
員工管理
</a>
</li>
-
抽取公共的代碼(list.html 和 dashboard.html)
- dashboard.html 頁面
<!--頂部導航欄--> <nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="topbar"> <!--...--> </nav> <!--側邊欄--> <nav class="col-md-2 d-none d-md-block bg-light sidebar" th:fragment="sidebar"> <!--...--> </nav>- list.html 頁面
<!--頂部導航欄--> <div th:insert="~{dashboard::topbar}"></div> <!--側邊欄--> <div th:insert="~{dashboard::sidebar}"></div> -
進一步抽取公共的代碼
- 在
templates目錄下面創建commons目錄,在commons目錄下面創建commons.html放公共代碼
<!--只寫改變的代碼--> <!DOCTYPE html> <html lang="en" xmlns:th="http://www.thymeleaf.org"> <!--頂部導航欄--> <nav class="navbar navbar-dark sticky-top bg-dark flex-md-nowrap p-0" th:fragment="topbar"> ............. </nav> <!--側邊欄--> <nav class="col-md-2 d-none d-md-block bg-light sidebar" th:fragment="sidebar"> <div class="sidebar-sticky"> <ul class="nav flex-column"> <li class="nav-item"> <a class="nav-link active" th:href="@{/index.html}"> ............. 首頁 <span class="sr-only">(current)</span> </a> </li> ............. <li class="nav-item"> <a class="nav-link" th:href="@{/emps}"> ............. 員工管理 </a> </li> ............. </ul> ............. </div> </nav> </html>dashboard.html和list.html頁面一樣
<!--頂部導航欄--> <div th:replace="~{commons/commons::topbar}"></div> <!--側邊欄--> <div th:replace="~{commons/commons::sidebar}"></div> - 在
-
添加側邊欄點亮
- 在 dashboard.html 和 list.html 頁面中側邊欄傳參(在括號里面直接傳參)
<!--側邊欄--> <div th:replace="~{commons/commons::sidebar(active='list.html')}"></div>- 在 commons.html 中接收參數并判斷
<!--側邊欄--> <nav class="col-md-2 d-none d-md-block bg-light sidebar" th:fragment="sidebar"> <div class="sidebar-sticky"> <ul class="nav flex-column"> <li class="nav-item"> <a th:class="${active=='main.html'?'nav-link active':'nav-link'}" th:href="@{/main.html}"> ............. 首頁 <span class="sr-only">(current)</span> </a> </li> ............. <li class="nav-item"> <a th:class="${active=='list.html'?'nav-link active':'nav-link'}" th:href="@{/emps}"> ............. 員工管理 </a> </li> ............. </ul> ............. </div> </nav> </html>
列表循環展示
員工列表循環(list.html)
<!--側邊欄-->
<div th:replace="~{commons/commons::sidebar(active='list.html')}"></div>
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
<h2>Section title</h2>
<div class="table-responsive">
<table class="table table-striped table-sm">
<thead>
<tr>
<th>id</th>
<th>lastName</th>
<th>email</th>
<th>gender</th>
<th>department</th>
<th>birth</th>
<th>操作</th>
</tr>
</thead>
<tbody>
<tr th:each="emp:${emps}">
<td th:text="${emp.getLastName()}"></td>
<td th:text="${emp.getEmail()}"></td>
<td th:text="${emp.getGender()==0?'女':'男'}"></td>
<td th:text="${emp.department.getDepartmentName()}"></td>
<td th:text="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm:ss')}"></td>
<td>
<button class="btn btn-sm btn-primary">編輯</button>
<button class="btn btn-sm btn-danger">刪除</button>
</td>
</tr>
</tbody>
</table>
</div>
</main>

添加員工信息
按鈕提交
list.html 頁面編寫:
<h2><a class="btn btn-sm btn-success" th:href="@{/emp}">添加員工</a></h2>

跳轉到添加頁面
- 后臺頁面的編寫(跳轉到 add.html 頁面)
// 一定別忘記注入!!!!
@Autowired
DepartmentDao departmentDao;
@GetMapping("/emp")
public String toAddPage(Model model) {
// 查出所有部門的信息
Collection<Department> department = departmentDao.getDepartment();
model.addAttribute("departments",department);
return "emp/add";
}
add.html頁面的編寫(其他部分和list.html頁面一樣,只改 main 中的代碼即可)
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
<form th:action="@{/emp}" method="post">
<div class="form-group">
<label>LastName</label>
<input type="text" name="lastName" class="form-control" placeholder="海綿寶寶">
</div>
<div class="form-group">
<label>Email</label>
<input type="email" name="email" class="form-control" placeholder="1176244270@qq.com">
</div>
<div class="form-group">
<label>Gender</label><br>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="gender" value="1">
<label class="form-check-label">男</label>
</div>
</div>
<div class="form-check form-check-inline">
<input class="form-check-input" type="radio" name="gender" value="0">
<label class="form-check-label">女</label>
</div>
<div class="form-group">
<label>department</label>
<select class="form-control" name="department.id">
<!--我們在 controller 接收的是一個 Employee,所以我們需要提交的是其中的一個屬性-->
<!--前端無法直接提交一個對象!-->
<option th:each="dept:${departments}" th:text="${dept.getDepartmentName()}" th:value="${dept.getId()}">1</option>
</select>
</div>
<div class="form-group">
<label>Birth</label>
<input type="text" name="birth" class="form-control" placeholder="2020/07/25 18:00:00">
</div>
<button type="submit" class="btn btn-primary">添加</button>
</form>
</main>
注意:下拉框提交的時候應提交一個屬性,因為其在 controller 接收的是一個 Employee,否則會報錯。
添加員工成功
后臺頁面的編寫:
@Autowired
DepartmentDao departmentDao;
@PostMapping("/emp")
public String addEmp(Employee employee) {
employeeDao.save(employee); // 調用底層業務方法保存員工信息
return "redirect:/emps";
注意
return "redirect:/emps";和return "redirect:/index.html";區別!
日期格式的修改
- 如果輸入的日期格式為 2020-01-01,則報錯
application.properties文件中添加配置
spring.mvc.format.date=yyyy-MM-dd
修改員工信息
按鈕提交
list.html 頁面編輯按鈕的編寫(’+‘ 報紅別管)
<a class="btn btn-sm btn-primary" th:href="@{/emp/}+${emp.getId()}">編輯</a>
跳轉到修改頁面
- 后臺頁面的接收參數(Restful 風格)
// 去到員工的修改頁面
@GetMapping("/emp/{id}")
public String toUpdateEmp(@PathVariable("id") Integer id, Model model) {
// 查出原來的數據
Employee employee = employeeDao.getEmployeeById(id);
model.addAttribute("emp",employee);
// 查出所有部門的信息
Collection<Department> department = departmentDao.getDepartment();
model.addAttribute("departments",department);
return "emp/update";
}
update.html頁面(main里面修改,其他和list.html頁面一樣)
<main role="main" class="col-md-9 ml-sm-auto col-lg-10 pt-3 px-4">
<form th:action="@{/updateEmp}" method="post">
<input type="hidden" name="id" th:value="${emp.getId()}">
<div class="form-group">
<label>LastName</label>
<input th:value="${emp.getLastName()}" type="text" name="lastName" class="form-control" placeholder="海綿寶寶">
</div>
<div class="form-group">
<label>Email</label>
<input th:value="${emp.getEmail()}" type="email" name="email" class="form-control" placeholder="1176244270@qq.com">
</div>
<div class="form-group">
<label>Gender</label><br>
<div class="form-check form-check-inline">
<input th:checked="${emp.getGender()==1}" class="form-check-input" type="radio" name="gender" value="1">
<label class="form-check-label">男</label>
</div>
</div>
<div class="form-check form-check-inline">
<input th:checked="${emp.getGender()==0}" class="form-check-input" type="radio" name="gender" value="0">
<label class="form-check-label">女</label>
</div>
<div class="form-group">
<label>department</label>
<select class="form-control" name="department.id">
<!--我們在controller接收的是一個Employee,所以我們需要提交的是其中的一個屬性-->
<option th:selected="${dept.getId()==emp.getDepartment().getId()}" th:each="dept:${departments}" th:text="${dept.getDepartmentName()}" th:value="${dept.getId()}"></option>
</select>
</div>
<div class="form-group">
<label>Birth</label>
<input th:value="${#dates.format(emp.getBirth(),'yyyy-MM-dd HH:mm:ss')}" type="text" name="birth" class="form-control" placeholder="2020-07-25 00:00:00">
</div>
<button type="submit" class="btn btn-primary">修改</button>
</form>
</main>
注意
update.html和add.html的區別!
update.html需要把待修改的員工信息展示出來!<input th:value="${emp.getLastName()}" type="text" name="lastName" class="form-control">
add.html只需要顯示默認值!<input type="text" name="lastName" class="form-control" placeholder="海綿寶寶">
修改員工成功
修改員工信息成功:
@PostMapping("/updateEmp")
public String updateEmp(Employee employee) {
employeeDao.save(employee);
return "redirect:/emps";
}
刪除員工信息
按鈕提交
list.html 頁面刪除按鈕的修改:
<a class="btn btn-sm btn-danger" th:href="@{/delemp/}+${emp.getId()}">刪除</a>
接收參數刪除用戶信息
// 刪除員工
@GetMapping("/delemp/{id}")
// @PathVariable 映射 URL 綁定的占位符
public String deleteEmp(@PathVariable("id") Integer id) {
employeeDao.delete(id);
return "redirect:/emps";
}
404 頁面
- 將
404.html頁面放入到 templates 目錄下面的 error 目錄中 - 錯誤運行頁面:

注銷功能的實現
- 在
commons.html中修改注銷按鈕
<a class="nav-link" th:href="@{/user/logout}">注銷</a>
- 在
LoginController.java中編寫注銷頁面代碼
@RequestMapping("/user/logout")
public String logout(HttpSession session) {
session.invalidate();
return "redirect:/index.html";
}
如何寫一個網站
步驟:
- 搞定前端:頁面長什么樣子
- 設計數據庫(難點)
- 前端讓他能自動運行,獨立化工程
- 數據接口如何對接:json,對象,all in one
- 前后端聯調測試
模板:
- 有一套自己熟悉的后臺模板:工作必要 x-admin
- 前端頁面:至少自己能夠通過前端框架(Bootstrap | Layui | semantic-ui),組合出來一個網站頁面
- 柵格系統
- 導航欄
- 側邊欄
- 表單
- 讓這個網站能夠獨立運行
整合 JDBC
創建測試項目測試數據源
- 新建一個項目測試:引入相應的模塊

- 項目建好之后,發現自動幫我們導入了如下的啟動器:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-jdbc</artifactId>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
- 編寫 yaml 配置文件連接數據庫:
spring:
datasource:
username: root
password: 130914
# ?serverTimezone=UTC 解決時區的報錯
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
- 測試類測試
@SpringBootTest
class SpringbootDataJdbcApplicationTests {
// DI 注入數據源
@Autowired
DataSource dataSource;
@Test
public void contextLoads() throws SQLException {
// 看一下默認數據源
System.out.println(dataSource.getClass());
// 獲得連接
Connection connection = dataSource.getConnection();
System.out.println(connection);
// 關閉連接
connection.close();
}
}
JDBCTemplate
-
有了數據源 (com.zaxxer.hikari.HikariDataSource),然后可以拿到數據庫連接 (java.sql.Connection),有了連接,就可以使用原生的 JDBC 語句來操作數據庫;
-
即使不使用第三方第數據庫操作框架,如 MyBatis 等,Spring 本身也對原生的 JDBC 做了輕量級的封裝,即 JdbcTemplate。
-
數據庫操作的所有 CRUD 方法都在 JdbcTemplate 中。
-
Spring Boot 不僅提供了默認的數據源,同時默認已經配置好了 JdbcTemplate 放在了容器中,程序員只需自己注入即可使用
-
JdbcTemplate 的自動配置是依賴 org.springframework.boot.autoconfigure.jdbc 包下的 JdbcTemplateConfiguration 類
JdbcTemplate主要提供以下幾類方法:
execute方法:可以用于執行任何 SQL 語句,一般用于執行 DDL 語句;update方法及 batchUpdate 方法:update方法用于執行新增、修改、刪除等語句;batchUpdate方法用于執行批處理相關語句;query方法及 queryForXXX 方法:用于執行查詢相關語句;call方法:用于執行存儲過程、函數相關語句。
測試
- 編寫一個 Controller,注入 jdbcTemplate,編寫測試方法進行訪問測試;
@RestController
public class JDBCController {
final
JdbcTemplate jdbcTemplate;
public JDBCController(JdbcTemplate jdbcTemplate) {
this.jdbcTemplate = jdbcTemplate;
}
// 查詢數據庫的所有信息
// 沒有實體類,獲取數據庫的東西,怎么獲取? Map
@GetMapping("/userList")
public List<Map<String, Object>> userList() {
String sql = "select * from user";
return jdbcTemplate.queryForList(sql);
}
@GetMapping("/addUser")
public String addUser() {
String sql = "insert into mybatis.user(id, name, pwd) values(7,'小明','123456')";
jdbcTemplate.update(sql);
return "update-ok";
}
@GetMapping("/updateUser/{id}")
public String updateUser(@PathVariable("id") int id) {
String sql = "update mybatis.user set name = ?,pwd = ? where id = " + id;
//封裝
Object[] objects = new Object[2];
objects[0] = "小明2";
objects[1] = "aaaaaaa";
jdbcTemplate.update(sql, objects);
return "update-ok";
}
@GetMapping("/deleteUser/{id}")
public String deleteUser(@PathVariable("id") int id) {
String sql = "delete from mybatis.user where id = ?";
jdbcTemplate.update(sql, id);
return "delete-ok";
}
}
整合 Druid
Java 程序很大一部分要操作數據庫,為了提高性能操作數據庫的時候,又不得不使用數據庫連接池。
Druid 是阿里巴巴開源平臺上一個數據庫連接池實現,結合了 C3P0、DBCP 等 DB 池的優點,同時加入了日志監控。
Druid 可以很好的監控 DB 池連接和 SQL 的執行情況,天生就是針對監控而生的 DB 連接池。
Spring Boot 2.0 以上默認使用 Hikari 數據源,可以說 Hikari 與 Driud 都是當前 Java Web 上最優秀的數據源,我們來重點介紹 Spring Boot 如何集成 Druid 數據源,如何實現數據庫監控。
Github地址:https://github.com/alibaba/druid/
配置數據源
- 添加上 Druid 數據源依賴:這個依賴可以從 Maven 倉庫官網Maven Respository中獲取,注意,如果使用 springboot 3.x,
artifactId一定要改為druid-spring-boot-3-starter,若使用druid-spring-boot-starter會報錯
<!-- springboot 版本為 3.0.2 -->
<!-- https://mvnrepository.com/artifact/com.alibaba/druid-spring-boot-starter -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid-spring-boot-3-starter</artifactId>
<version>1.2.20</version>
</dependency>
- 切換數據源:之前已經說過 Spring Boot 2.0 以上默認使用
com.zaxxer.hikari.HikariDataSource數據源,但可以通過spring.datasource.type指定數據源。
spring:
datasource:
username: root
password: 130914
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource # 自定義數據源
-
在測試類中注入 DataSource,然后獲取到它,輸出一看便知是否成功切換
-
切換成功后,就可以設置數據源連接初始化大小、最大連接數、等待時間、最小連接數,參考文檔:https://github.com/alibaba/druid/tree/master/druid-spring-boot-starter
spring:
datasource:
username: root
password: 130914
# ?serverTimezone=UTC 解決時區的報錯
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
type: com.alibaba.druid.pool.DruidDataSource
# druid 數據源專有配置
druid:
# 初始化時建立物理連接的個數
initial-size: 5
# 連接池的最小空閑數
min-idle: 5
# 連接池最大連接數
max-active: 20
# 獲取連接時最大等待時間,單位毫秒
max-wait: 60000
# 既作為檢測的間隔時間又作為 test-while-idle 執行的依據
time-between-eviction-runs-millis: 60000
# 連接保持空閑而不被驅逐的最小時間
min-evictable-idle-time-millis: 300000
# 用來檢測連接是否有效的 sql,要求是一個查詢語句
validation-query: SELECT 1 FROM DUAL
# 申請連接時執行 validationQuery 檢測連接是否有效,開啟會降低性能
test-while-idle: true
test-on-borrow: false
test-on-return: false
pool-prepared-statements: true
# 配置監控統計攔截的filters,stat:監控統計、slf4j:日志記錄、wall:防御 sql 注入
filters: stat,wall,slf4j
Max-pool-prepared-statement-per-connection-size: 20
use-global-data-source-stat: true
connect-properties:
druid.stat.mergeSql: true
druid.stat.slowSqlMillis: 500
- 現在需要程序員自己為 DruidDataSource 綁定全局配置文件中的參數,再添加到容器中,而不再使用 SpringBoot 的自動生成;我們需要自己添加 DruidDataSource 組件到容器中,并綁定屬性:
@Configuration
public class DruidConfig {
/*
將自定義的 Druid 數據源添加到容器中,不再讓 Spring Boot 自動創建
綁定全局配置文件中的 druid 數據源屬性到 com.alibaba.druid.pool.DruidDataSource 從而讓它們生效
@ConfigurationProperties(prefix = "spring.datasource.druid"):作用就是將全局配置文件中
前綴為 spring.datasource 的屬性值注入到 com.alibaba.druid.pool.DruidDataSource 的同名參數中
*/
@ConfigurationProperties(prefix = "spring.datasource.druid")
@Bean
public DruidDataSource druidDataSource() {
return DruidDataSourceBuilder.create().build();
}
}
- 去測試類中測試一下,看是否成功
@SpringBootTest
class SpringbootDataJdbcApplicationTests {
// DI 注入數據源
@Autowired
DataSource dataSource;
@Test
public void contextLoads() throws SQLException {
// 看一下默認數據源
System.out.println(dataSource.getClass());
// 獲得連接
Connection connection = dataSource.getConnection();
System.out.println(connection);
DruidDataSource druidDataSource = (DruidDataSource) dataSource;
System.out.println("druidDataSource 數據源最大連接數:" + druidDataSource.getMaxActive());
System.out.println("druidDataSource 數據源初始化連接數:" + druidDataSource.getInitialSize());
// 關閉連接
connection.close();
}
}
- 輸出結果:可見配置類已經生效

配置 Druid 數據源監控
Druid 數據源具有監控的功能,并提供了一個 web 界面方便用戶查看,類似安裝路由器時,人家也提供了一個默認的 web 頁面。所以第一步需要設置 Druid 的后臺管理頁面,比如登錄賬號、密碼等;配置后臺管理等
繼續配置 appliacation.yaml 文件:
spring:
datasource:
...
# druid 數據源專有配置
druid:
...
# StatViewServlet 配置:啟用內置的監控頁面
stat-view-servlet:
enabled: true
url-pattern: /druid/*
login-username: admin
login-password: 123
allow: 127.0.0.1 # 為空或者為 null 時,表示允許所有訪問
deny: 192.168.1.20 # 拒絕此 ip 訪問
reset-enable: false
配置完畢后,訪問 :http://localhost:8080/druid/login.html

配置 Druid web 監控 filter 過濾器:
spring:
datasource:
...
# druid 數據源專有配置
druid:
...
# WebStatFilter 配置
web-stat-filter:
enabled: true
url-pattern: /*
exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*,/jdbc/*'
session-stat-enable: true
執行一條 SQL 語句后:http://localhost:8080/queryUserList,查看 Druid 界面的 SQL 監控:

整合 MyBatis
- 導入 MyBatis 所需要的依賴:直接瀏覽器搜索 mybatis-spring-boot-starter 中文文檔,查看版本依賴
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>3.0.1</version>
</dependency>
- 配置數據庫連接信息(不變)
spring:
datasource:
username: root
password: 130914
#?serverTimezone=UTC 解決時區的報錯
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8
driver-class-name: com.mysql.cj.jdbc.Driver
# 整合 mybatis
mybatis:
type-aliases-package: com.locke.pojo
mapper-locations: classpath:mybatis/mapper/*.xml
- 測試數據庫是否連接成功
- 創建實體類,導入 lombok
@Data
@AllArgsConstructor
@NoArgsConstructor
public class User {
private int id;
private String name;
private String pwd;
}
- 創建 mapper 目錄以及對應的 Mapper 接口:
UserMapper.java
// 這個注解表示了這是一個 mybatis 的 mapper 類
@Mapper
@Repository
public interface UserMapper {
List<User> queryUserList();
User queryUserById(int id);
int addUser(User user);
int updateUser(User user);
int deleteUser(int id);
}
- 在 resources 文件夾下建立 mybatis.mapper 包,建立對應的 Mapper 映射文件:
UserMapper.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<!--namespace=綁定一個對應的 Dao/Mapper接口-->
<mapper namespace="com.locke.mapper.UserMapper">
<select id="queryUserList" resultType="User">
select * from mybatis.user;
</select>
<select id="queryUserById" resultType="User">
select * from mybatis.user where id = #{id};
</select>
<insert id="addUser" parameterType="User">
insert into mybatis.user (id, name, pwd) values (#{id},#{name},#{pwd});
</insert>
<update id="updateUser" parameterType="User">
update mybatis.user set name=#{name},pwd = #{pwd} where id = #{id};
</update>
<delete id="deleteUser" parameterType="int">
delete from mybatis.user where id = #{id}
</delete>
</mapper>
- maven 配置資源過濾問題
<resources>
<resource>
<directory>src/main/java</directory>
<includes>
<include>**/*.xml</include>
</includes>
<filtering>true</filtering>
</resource>
</resources>
- 創建 controller 文件夾,編寫 UserController 進行測試
@RestController
public class UserController {
@Autowired
private UserMapper userMapper;
@GetMapping("/queryUserList")
public List<User> queryUserList() {
List<User> userList = userMapper.queryUserList();
for (User user : userList) {
System.out.println(user);
}
return userList;
}
// 添加一個用戶
@GetMapping("/addUser")
public String addUser() {
userMapper.addUser(new User(7, "阿毛", "123456"));
return "ok";
}
// 修改一個用戶
@GetMapping("/updateUser")
public String updateUser() {
userMapper.updateUser(new User(7, "阿毛", "123456"));
return "ok";
}
@GetMapping("/deleteUser")
public String deleteUser() {
userMapper.deleteUser(7);
return "ok";
}
}
- 啟動項目訪問進行測試!
SpringSecurity
安全簡介
在 Web 開發中,安全一直是非常重要的一個方面。安全雖然屬于應用的非功能性需求,但是應該在應用開發的初期就考慮進來。如果在應用開發的后期才考慮安全的問題,就可能陷入一個兩難的境地:一方面,應用存在嚴重的安全漏洞,無法滿足用戶的要求,并可能造成用戶的隱私數據被攻擊者竊取;另一方面,應用的基本架構已經確定,要修復安全漏洞,可能需要對系統的架構做出比較重大的調整,因而需要更多的開發時間,影響應用的發布進程。因此,從應用開發的第一天就應該把安全相關的因素考慮進來,并在整個應用的開發過程中。
市面上存在比較有名的:Shiro,Spring Security
每一個框架的出現都是為了解決某一個問題,Spring Security 是為了解決什么問題?
Spring Security 是一個功能強大且高度可定制的身份驗證和訪問控制框架,側重于為 java 應用程序提供身份驗證和授權,與所有 Spring 項目一樣,Spring 安全性的真正強大之處在于它可以輕松地擴展以滿足定制需求。
一般來說,Web 應用的安全性包括用戶認證(Authentication)和用戶授權(Authorization)兩個部分。
- 用戶認證:指的是驗證某個用戶是否為系統中的合法主體,也就是說用戶能否訪問該系統。用戶認證一般要求用戶提供用戶名和密碼。系統通過校驗用戶名和密碼來完成認證過程。
- 用戶授權:指的是驗證某個用戶是否有權限執行某個操作。在一個系統中,不同用戶所具有的權限是不同的。比如對一個文件來說,有的用戶只能進行讀取,而有的用戶可以進行修改。一般來說,系統會為不同的用戶分配不同的角色,而每個角色則對應一系列的權限。
對于上面提到的兩者應用場景,Spring Security 框架都有很好的支持:
- 用戶認證:Spring Security 框架支持主流的認證方式,包括 HTTP 基本認證、HTTP 表單驗證、HTTP 摘要認證、OpenID 和 LDAP 等。
- 用戶授權:Spring Security 提供了基于角色的訪問控制和訪問控制列表(Access Control List,ACL),可以對應用中的領域對象進行細粒度的控制。
實戰測試
實驗環境搭建
- 新建一個初始的 springboot 項目 web 模塊,thymeleaf 模塊,注意 springboot 降級到 2.6.0
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.6.0</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
-
為了有一個更好的學習體驗,先不要引入
spring-boot-starter-security -
導入靜態資源:

- controller 跳轉:
RouterController.java
@Controller
public class RouterController {
@RequestMapping({"/", "/index"})
public String index() {
return "index";
}
@RequestMapping("/toLogin")
public String toLogin() {
return "views/login";
}
@RequestMapping("/level1/{id}")
public String level1(@PathVariable("id") int id) {
return "views/level1/" + id;
}
@RequestMapping("/level2/{id}")
public String level2(@PathVariable("id") int id) {
return "views/level2/" + id;
}
@RequestMapping("/level3/{id}")
public String level3(@PathVariable("id") int id) {
return "views/level3/" + id;
}
}
- 測試實驗環境是否 OK
- 首頁 & 登錄


認識 Spring Security
對于安全控制,我們僅需要引入 spring-boot-starter-security 模塊,進行少量的配置,即可實現強大的安全管理!
記住幾個類:
WebSecurityConfigurerAdapter:自定義Security策略AuthenticationManagerBuilder:自定義認證策略@EnableWebSecurity:開啟WebSecurity模式
Spring Security 的兩個主要目標是 “認證” 和 “授權”(訪問控制)。
認證和授權
目前,我們的測試環境,是誰都可以訪問的,我們使用 Spring Security 增加上認證和授權的功能。
- 引入 Spring Security 模塊
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId>
</dependency>
-
編寫 Spring Security 配置類
-
查看我們自己項目中的版本,找到對應的幫助文檔:https://docs.spring.io/spring-security/site/docs/5.3.0.RELEASE/reference/html5
-
servlet-applications 8.16.4
-
編寫基礎配置類:

// 開啟 WebSecurity 模式
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
super.configure(http);
}
}
- 定制請求的授權規則:看源碼 + 仿寫
// 鏈式編程
@Override
protected void configure(HttpSecurity http) throws Exception {
// 首頁所有人都可以訪問,功能也只有對應有權限的人才能訪問到
// 請求授權的規則
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
}
- 測試一下:發現除了首頁都進不去了,因為我們目前沒有登錄的角色,請求需要登錄的角色擁有對應的權限才可以!

- 在
configure()方法中加入以下配置,開啟自動配置的登錄功能!
// 開啟自動配置的登錄功能
// /login 請求來到登錄頁
// /login?error 重定向到這里表示登錄失敗
http.formLogin();
- 測試一下:發現沒有權限的時候,會跳轉到登錄的頁面
- 查看剛才登錄頁的注釋信息:我們可以定義認證規則,重寫 configure 的另一個方法

// 認證,springboot 2.1.x 可以直接使用
// 密碼編碼:PasswordEncoder
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 這些數據正常應該中數據庫中讀
auth.inMemoryAuthentication()
.withUser("kuangshen").password("123456").roles("vip2","vip3")
.and()
.withUser("root").password("123456").roles("vip1","vip2","vip3")
.and()
.withUser("guest").password("123456").roles("vip1");
}
- 測試:我們可以使用這些賬號登錄進行測試,發現會報錯

- 原因:我們要將前端傳過來的密碼進行某種方式加密,否則就無法登錄,修改代碼
// 認證,springboot 2.1.x 可以直接使用
// 密碼編碼:PasswordEncoder
// 在spring Secutiry 5.0+ 新增了很多加密方法
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 這些數據正常應該中數據庫中讀
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("kuangshen").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
.and()
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
.and()
.withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
}
- 測試:發現登錄成功,并且每個角色只能訪問自己認證下的規則
權限控制和注銷
- 開啟自動配置的注銷的功能
// 定制請求的授權規則
@Override
protected void configure(HttpSecurity http) throws Exception {
// ....
// 開啟自動配置的注銷的功能
// /logout 注銷請求
http.logout();
}
- 在前端,增加一個注銷的按鈕,
index.html導航欄中
<!--注銷-->
<a class="item" th:href="@{/logout}">
<i class="sign-out icon"></i> 注銷
</a>
-
我們可以去測試一下,登錄成功后點擊注銷,發現注銷完畢會跳轉到登錄頁面!
-
但是,我們想讓他注銷成功后,依舊可以跳轉到首頁,該怎么處理呢?
// .logoutSuccessUrl("/"); 注銷成功來到首頁
http.logout().logoutSuccessUrl("/");
-
我們現在又來一個需求:用戶沒有登錄的時候,導航欄上只顯示登錄按鈕,用戶登錄之后,導航欄可以顯示登錄的用戶信息及注銷按鈕!還有就是,比如 locke 這個用戶,它只有 vip2,vip3 功能,那么登錄則只顯示這兩個功能,而 vip1 的功能菜單不顯示!這個就是真實的網站情況了!該如何做呢?
-
我們需要結合 thymeleaf 中的一些功能:
sec:authorize="isAuthenticated()":是否認證登錄!來顯示不同的頁面- Maven 依賴
<!-- https://mvnrepository.com/artifact/org.thymeleaf.extras/thymeleaf-extras-springsecurity4 --> <dependency> <groupId>org.thymeleaf.extras</groupId> <artifactId>thymeleaf-extras-springsecurity5</artifactId> </dependency>
-
-
修改前端頁面
- 導入命名空間
<html lang="en" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/thymeleaf-extras-springsecurity5">- 修改導航欄,增加認證判斷
<!--登錄注銷--> <div class="right menu"> <!--如果未登錄--> <div sec:authorize="!isAuthenticated()"> <a class="item" th:href="@{/login}"> <i class="address card icon"></i> 登錄 </a> </div> <!--如果已登錄--> <div sec:authorize="isAuthenticated()"> <a class="item"> <i class="address card icon"></i> 用戶名:<span sec:authentication="principal.username"></span> 角色:<span sec:authentication="principal.authorities"></span> </a> </div> <div sec:authorize="isAuthenticated()"> <a class="item" th:href="@{/logout}"> <i class="sign-out icon"></i> 注銷 </a> </div> </div> -
重啟測試
-
點擊注銷產生的問題
- 整合包 4(springsecurity4)
- 整合包 5(springsecurity5)
- 解決問題:
- 默認防止 csrf 跨站請求偽造,因為會產生安全問題
- 將請求改為 post 表單提交
- 在 spring security 中關閉 csrf 功能
http.csrf().disable();
-
繼續將下面的角色功能塊認證完成:
<!--菜單根據用戶的角色動態的實現-->
<div class="column" sec:authorize="hasRole('vip1')">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 1</h5>
<hr>
<div><a th:href="@{/level1/1}"><i class="bullhorn icon"></i> Level-1-1</a></div>
<div><a th:href="@{/level1/2}"><i class="bullhorn icon"></i> Level-1-2</a></div>
<div><a th:href="@{/level1/3}"><i class="bullhorn icon"></i> Level-1-3</a></div>
</div>
</div>
</div>
</div>
<div class="column" sec:authorize="hasRole('vip2')">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 2</h5>
<hr>
<div><a th:href="@{/level2/1}"><i class="bullhorn icon"></i> Level-2-1</a></div>
<div><a th:href="@{/level2/2}"><i class="bullhorn icon"></i> Level-2-2</a></div>
<div><a th:href="@{/level2/3}"><i class="bullhorn icon"></i> Level-2-3</a></div>
</div>
</div>
</div>
</div>
<div class="column" sec:authorize="hasRole('vip3')">
<div class="ui raised segment">
<div class="ui">
<div class="content">
<h5 class="content">Level 3</h5>
<hr>
<div><a th:href="@{/level3/1}"><i class="bullhorn icon"></i> Level-3-1</a></div>
<div><a th:href="@{/level3/2}"><i class="bullhorn icon"></i> Level-3-2</a></div>
<div><a th:href="@{/level3/3}"><i class="bullhorn icon"></i> Level-3-3</a></div>
</div>
</div>
</div>
</div>
記住我
- 開啟記住我功能
//定制請求的授權規則
@Override
protected void configure(HttpSecurity http) throws Exception {
//開啟記住我功能: cookie,默認保存兩周
http.rememberMe();
}
-
再次測試
-
如何實現的?
- 查看瀏覽器 cookie

- 點擊注銷的時候,可以發現 spring security 幫我們自動刪除了這個 cookie

- cookie 發送給瀏覽器保存,以后登錄帶上這個 cookie,只要通過檢查就可以免登陸了,如果點注銷,則會刪除這個 cookie
定制登錄頁
如何實現自己的 Login 界面?
- 在剛才的登錄頁配置后面指定 loginpage
protected void configure(HttpSecurity http) throws Exception {
//......
// 沒有權限默認會到登錄頁面,需要開啟登錄的頁面
// /login頁面
http.formLogin().loginPage("/toLogin");
//......
}
- 然后前端也需要指向我們自己定義的 login 請求
<div sec:authorize="!isAuthenticated()">
<a class="item" th:href="@{/toLogin}">
<i class="address card icon"></i> 登錄
</a>
</div>
- 登錄后需要將這些信息發送到哪里?我們也需要配置,login.html 配置提交請求及方式,方式必須為 post:
protected void configure(HttpSecurity http) throws Exception {
//......
// 沒有權限默認會到登錄頁面,需要開啟登錄的頁面
// /login 頁面
http.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginPage("/toLogin")
.loginProcessingUrl("/login"); // 登陸表單提交請求
//......
}
- 在登錄頁增加我的多選框
<input type="checkbox" name="remember"> 記住我
- 后端驗證處理
protected void configure(HttpSecurity http) throws Exception {
//......
//開啟記住我功能: cookie,默認保存兩周,自定義接收前端的參數
http.rememberMe().rememberMeParameter("remember");
}
完整配置
// AOP:攔截器
// 開啟 WebSecurity 模式
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {
// 鏈式編程
// 授權
@Override
protected void configure(HttpSecurity http) throws Exception {
// 首頁所有人都可以訪問,功能也只有對應有權限的人才能訪問到
// 請求授權的規則
http.authorizeRequests()
.antMatchers("/").permitAll()
.antMatchers("/level1/**").hasRole("vip1")
.antMatchers("/level2/**").hasRole("vip2")
.antMatchers("/level3/**").hasRole("vip3");
// 沒有權限默認會到登錄頁面,需要開啟登錄的頁面
// /login頁面
http.formLogin()
.usernameParameter("username")
.passwordParameter("password")
.loginPage("/toLogin")
.loginProcessingUrl("/login");
// 注銷,開啟了注銷功能,跳到首頁
http.logout().logoutSuccessUrl("/");
// 防止網站工具:get,post
http.csrf().disable();//關閉csrf功能,登錄失敗肯定存在的原因
// 開啟記住我功能: cookie,默認保存兩周,自定義接收前端的參數
http.rememberMe().rememberMeParameter("remember");
}
// 認證,springboot 2.1.x 可以直接使用
// 密碼編碼: PasswordEncoder
// 在spring Secutiry 5.0+ 新增了很多加密方法
@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
// 這些數據正常應該中數據庫中讀
auth.inMemoryAuthentication().passwordEncoder(new BCryptPasswordEncoder())
.withUser("kuangshen").password(new BCryptPasswordEncoder().encode("123456")).roles("vip2","vip3")
.and()
.withUser("root").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1","vip2","vip3")
.and()
.withUser("guest").password(new BCryptPasswordEncoder().encode("123456")).roles("vip1");
}
}
Shiro
概述
功能
Apache Shiro 是一個強大且易用的 Java 安全框架,可以完成身份驗證、授權、密碼和會話管理。
Authentication:身份認證 / 登錄,驗證用戶是不是擁有相應的身份;Authorization:授權,即權限驗證,驗證某個已認證的用戶是否擁有某個權限;即判斷用戶是否能做事情,常見的如:驗證某個用戶是否擁有某個角色。或者細粒度的驗證某個用戶對某個資源是否具有某個權限;Session Manager:會話管理,即用戶登錄后就是一次會話,在沒有退出之前,它的所有信息都在會話中;會話可以是普通 JavaSE 環境的,也可以是如 Web 環境的;Cryptography:加密,保護數據的安全性,如密碼加密存儲到數據庫,而不是明文存儲;Web Support:Web 支持,可以非常容易的集成到 Web 環境;Caching:緩存,比如用戶登錄后,其用戶信息、擁有的角色 / 權限不必每次去查,這樣可以提高效率;Concurrency:shiro 支持多線程應用的并發驗證,即如在一個線程中開啟另一個線程,能把權限自動傳播過去;Testing:提供測試支持;Run As:允許一個用戶假裝為另一個用戶(如果他們允許)的身份進行訪問;Remember Me:記住我,這個是非常常見的功能,即一次登錄后,下次再來的話不用登錄了。
從外部看
對于我們而言,最簡單的一個 Shiro 應用:
- 應用代碼通過 Subject 來進行認證和授權,而 Subject 又委托給 SecurityManager;
- 我們需要給 Shiro 的 SecurityManager 注入 Realm,從而讓 SecurityManager 能得到合法的用戶及其權限進行判斷。
認證流程
用戶 提交 身份信息、憑證信息 封裝成 令牌 交由 安全管理器 認證:

快速入門
拷貝案例
- 按照官網提示找到快速入門案例:shiro/samples/quickstart/
- 復制快速入門案例 POM.xml 文件中的依賴分析案例
<dependencies>
<!-- https://mvnrepository.com/artifact/org.apache.shiro/shiro-core -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.11.0</version>
</dependency>
<!-- configure logging -->
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jcl-over-slf4j</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-slf4j-impl</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-core</artifactId>
<scope>runtime</scope>
</dependency>
</dependencies>
- 把快速入門案例中的 resource 下的
log4j.properties復制下來 - 復制一下
shiro.ini文件 - 復制一下
Quickstart.java文件 - 運行
Quickstart.java,得到結果
分析案例
-
通過 SecurityUtils 獲取當前執行的用戶 Subject
Subject currentUser = SecurityUtils.getSubject(); -
通過當前用戶拿到 Session
Session session = currentUser.getSession(); -
用 Session 存值取值
session.setAttribute("someKey", "aValue"); String value = (String) session.getAttribute("someKey"); -
判斷用戶是否被認證
currentUser.isAuthenticated() -
執行登錄操作
currentUser.login(token); -
打印其標識主體
currentUser.getPrincipal() -
判斷用戶是否有角色
currentUser.hasRole() -
注銷
currentUser.logout();
SpringBoot 集成 Shiro
注意:SringBoot 的版本為 2.7.3,java 版本為 11
編寫配置文件
- 在剛剛的父項目中新建一個 springboot 模塊
- 導入 SpringBoot 和 Shiro 整合包的依賴
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring-boot-web-starter</artifactId>
<version>1.6.0</version>
</dependency>
-
編寫配置的三大要素:
- subject \(\to\) ShiroFilterFactoryBean
- securityManager \(\to\) DefaultWebSecurityManager
- realm
-
實際操作中對象創建的順序:realm \(\to\) securityManager \(\to\) subject
-
編寫自定義的
UserRealm.java,需要繼承AuthorizingRealm
// 自定義的 Realm
public class UserRealm extends AuthorizingRealm {
// 授權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 打印一個提示
System.out.println("執行了授權方法");
return null;
}
// 認證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 打印一個提示
System.out.println("執行了認證方法");
return null;
}
}
- 新建一個
ShiroConfig配置文件:ShiroConfig.java
@Configuration
public class ShiroConfig {
// 第三步:ShiroFilterFactoryBean
@Bean(name = "shiroFilterFactoryBean") // 務必設置 Bean name 為 shiroFilterFactoryBean,否則會報錯
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("SecurityManager") DefaultWebSecurityManager defaultWebSecurityManager) {
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
// 設置安全管理器
shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
return bean;
}
// 第二步:securityManager -> DefaultWebSecurityManager
// @Qualifier("userRealm") 指定 Bean 的名字為 userRealm
// spring 默認的 BeanName 就是方法名
// name 屬性指定 BeanName
@Bean(name = "SecurityManager")
public DefaultWebSecurityManager getDefaultWebSecurity(@Qualifier("userRealm") UserRealm userRealm){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 需要關聯自定義的 Realm,通過參數把 Realm 對象傳遞過來
securityManager.setRealm(userRealm);
return securityManager;
}
// 第一步:創建 realm 對象,需要自定義類
// 讓 spring 托管自定義的 realm 類
@Bean
public UserRealm userRealm(){
return new UserRealm();
}
}
搭建簡單測試環境
- 新建一個登錄頁面
- 新建一個首頁:三個鏈接,一個登錄、一個增加用戶、一個刪除用戶
- 新建一個增加用戶頁面
- 新建一個刪除用戶頁面
- 編寫對應的 Controller
@Controller
public class MyController {
@RequestMapping({"/", "/index"})
public String toIndex(Model model) {
model.addAttribute("msg", "hello, Shiro");
return "index";
}
@RequestMapping({"user/add"})
public String add() {
return "user/add";
}
@RequestMapping({"user/update"})
public String update() {
return "user/update";
}
@RequestMapping("/tologin")
public String toLogin() {
return "login";
}
}
- 登錄頁面如下:注意在 pom 中加上
thymeleaf依賴
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>登錄頁面</title>
</head>
<body>
<h1>登錄</h1>
<hr>
<form th:action="@{/login}">
<p>用戶名:<input type="text" name="username"></p>
<p>密碼:<input type="text" name="password"></p>
<p>提交:<input type="submit"></p>
<p style="color:red;" th:text="${msg}"></p>
</form>
</body>
</html>
使用
登錄攔截
在上面的 getShiroFilterFactoryBean 方法中加上需要攔截的登錄請求:
@Bean(name = "shiroFilterFactoryBean")
public ShiroFilterFactoryBean getShiroFilterFactoryBean(@Qualifier("SecurityManager") DefaultWebSecurityManager securityManager){
ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
shiroFilterFactoryBean.setSecurityManager(securityManager);
//添加 Shiro 的內置過濾器=======================
/*
anon : 無需認證,就可以訪問
authc : 必須認證,才能訪問
user : 必須擁有 “記住我”功能才能用
perms : 擁有對某個資源的權限才能訪問
role : 擁有某個角色權限才能訪問
*/
Map<String, String> map = new LinkedHashMap<>();
// 設置 /user/addUser 這個請求,只有認證過才能訪問
// map.put("/user/addUser","authc");
// map.put("/user/deleteUser","authc");
// 設置 /user/ 下面的所有請求,只有認證過才能訪問
map.put("/user/*","authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
// 設置登錄的請求
shiroFilterFactoryBean.setLoginUrl("/tologin");
//============================================
return shiroFilterFactoryBean;
}
用戶認證
- 在 Controller 中寫一個登錄的控制器:
// 登錄的方法
@RequestMapping("/login")
public String login(String username, String password, Model model) {
// 獲取當前用戶
Subject subject = SecurityUtils.getSubject();
// 沒有認證過
// 封裝用戶的登錄數據,獲得令牌
UsernamePasswordToken token = new UsernamePasswordToken(username, password);
// 登錄及異常處理
try {
// 用戶登錄
subject.login(token);
return "index";
} catch (UnknownAccountException uae) {
// 如果用戶名不存在
System.out.println("用戶名不存在");
model.addAttribute("exception", "用戶名不存在");
return "login";
} catch (IncorrectCredentialsException ice) {
// 如果密碼錯誤
System.out.println("密碼錯誤");
model.addAttribute("exception", "密碼錯誤");
return "login";
}
}
- 重啟并測試
- 可以看出,是先執行了自定義的
UserRealm中的AuthenticationInfo方法,再執行了登錄的相關操作 - 下面去自定義的
UserRealm中的AuthenticationInfo方法中去獲取用戶信息
- 可以看出,是先執行了自定義的
- 修改
UserRealm中的AuthenticationInfo
// 自定義的 Realm
public class UserRealm extends AuthorizingRealm {
// 授權
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
// 打印一個提示
System.out.println("執行了授權方法");
return null;
}
// 認證
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
// 打印一個提示
System.out.println("執行了認證方法");
// 用戶名密碼 (暫時先自定義一個做測試)
String name = "root";
String password = "1234";
// 1. 用戶名認證
// 通過參數獲取登錄的控制器中生成的令牌
UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken;
// 用戶名認證
if (!token.getUsername().equals(name)){
// 用戶名不存在,shiro 底層就會拋出 UnknownAccountException 異常
// return null 就表示控制器中拋出的相關異常
return null;
}
// 2. 密碼認證, Shiro 自己做,為了避免和密碼的接觸
// 最后返回一個 AuthenticationInfo 接口的實現類,這里選擇 SimpleAuthenticationInfo
// 三個參數:獲取當前用戶的認證, 密碼, 認證名
return new SimpleAuthenticationInfo("", password, "");
}
}
退出登錄
- 在控制器中添加一個退出登錄的方法
// 退出登錄
@RequestMapping("/logout")
public String logout(){
Subject subject = SecurityUtils.getSubject();
subject.logout();
return "login";
}
Swagger
-
前后端分離
- Vue + SpringBoot
- 后端時代:前端只用管理靜態頁面;html \(\to\) 后端。模板引擎 JSP \(\to\) 后端才是主力
-
前后端分離時代
- 前端 \(\to\) 前端控制層、視圖層
- 偽造后端數據,json。已經存在了,不需要后端,前端工程隊依舊能夠跑起來
- 后端 \(\to\) 后端控制層、服務層、數據訪問層
- 前后端通過 API 進行交互
- 前后端相對獨立且松耦合
-
產生的問題
- 前后端集成聯調,前端或者后端無法做到“及時協商,盡早解決”,最終導致問題集中爆發
-
解決方案
- 首先定義 schema [ 計劃的提綱 ],并實時跟蹤最新的 API,降低集成風險;
- 早些年:指定 word 計劃文檔;
- 前后端分離:
- 前端測試后端接口:postman
- 后端提供接口,需要實時更新最新的消息及改動
Swagger
-
號稱世界上最流行的 API 框架
-
Restful Api 文檔在線自動生成器 \(\to\) API 文檔 與API 定義同步更新
-
直接運行,在線測試 API
-
支持多種語言 (如:Java,PHP 等)
Spring 集成
版本依賴問題很大!
SpringBoot 集成 Swagger \(\to\) springfox,兩個 jar 包
使用 Swagger 步驟:
-
新建一個 SpringBoot-web 項目,注意 springboot 版本要降到 2.5.6
-
添加 Maven 依賴(注意:2.9.2 版本之前,之后的不行)
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger2</artifactId>
<version>2.9.2</version>
</dependency>
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>
- 編寫 HelloController,測試確保運行成功!
@RestController
public class HelloController {
@RequestMapping("/hello")
public String hello() {
return "hello!";
}
}
- 要使用 Swagger,我們需要編寫一個配置類 SwaggerConfig 來配置 Swagger
@Configuration // 配置類
@EnableSwagger2 // 開啟Swagger2的自動配置
public class SwaggerConfig {
}
- 訪問測試 :Swagger UI ,可以看到 swagger 的界面;
- Swagger 信息
- 接口信息
- 實體類信息
- 組

配置 Swagger
-
Swagger 實例 Bean 是 Docket,所以通過配置 Docket 實例來配置 Swaggger。
@Configuration // 開啟 Swagger @EnableSwagger2 public class SwaggerConfig { @Bean // 配置 docket 以配置 Swagger 具體參數 public Docket docket() { return new Docket(DocumentationType.SWAGGER_2); } } -
可以通過
apiInfo()屬性配置文檔信息// 配置文檔信息 private ApiInfo apiInfo() { Contact contact = new Contact("Lockegogo", "http://www.rzrgm.cn/lockegogo/", "聯系人郵箱"); return new ApiInfo( "LK's Swagger", // 標題 "Life is like a Markov chain.", // 描述 "v1.0", // 版本 "http://terms.service.url/組織鏈接", // 組織鏈接 contact, // 聯系人信息 "Apach 2.0 許可", // 許可 "許可鏈接", // 許可連接 new ArrayList<>()// 擴展 ); -
Docket 實例關聯上 apiInfo()
@Bean public Docket docket() { return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()); } -
重啟項目,訪問測試 http://localhost:8080/swagger-ui.html 看下效果;

配置掃描接口
-
構建 Docket 時通過
select()方法配置怎么掃描接口。@Bean public Docket docket() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() // 通過.select()方法,去配置掃描接口, RequestHandlerSelectors 配置如何掃描接口 .apis(RequestHandlerSelectors.basePackage("com.locke.swagger.controller")) .build(); } -
重啟項目測試,由于我們配置根據包的路徑掃描接口,所以我們只能看到一個類
-
除了通過包路徑配置掃描接口外,還可以通過配置其他方式掃描接口,這里注釋一下所有的配置方式:
// 根據包路徑掃描接口 basePackage(final String basePackage) // 掃描所有,項目中的所有接口都會被掃描到 any() // 不掃描接口 none() // 通過方法上的注解掃描,如 withMethodAnnotation(GetMapping.class) 只掃描 get 請求 withMethodAnnotation(final Class<? extends Annotation> annotation) // 通過類上的注解掃描,如.withClassAnnotation(Controller.class)只掃描有 controller 注解的類中的接口 withClassAnnotation(final Class<? extends Annotation> annotation) -
除此之外,我們還可以配置接口掃描過濾:
@Bean public Docket docket() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .select() .apis(RequestHandlerSelectors.basePackage("com.locke.swagger.controller")) // 配置如何通過 path 過濾, 即這里只掃描請求以 /locke 開頭的接口(網站上的路徑) .paths(PathSelectors.ant("/locke/**")) .build(); } -
這里的可選值還有
any() // 任何請求都掃描 none() // 任何請求都不掃描 regex(final String pathRegex) // 通過正則表達式控制 ant(final String antPattern) // 通過 ant() 控制
配置 Swagger 開關
-
通過
enable()方法配置是否啟用 swagger,如果是 false,swagger 將不能在瀏覽器中訪問了@Bean public Docket docket() { return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) // 配置是否啟用 Swagger,如果是 false,在瀏覽器將無法訪問 .enable(false) .select() .apis(RequestHandlerSelectors.basePackage("com.locke.swagger.controller")) .paths(PathSelectors.ant("/locke/**")) .build(); } -
如何動態配置當項目處于 test、dev 環境時顯示 swagger,處于 prod 時不顯示?
@Bean // 獲得當前的生產環境 public Docket docket(Environment environment) { // 設置要顯示 swagger 的環境 Profiles of = Profiles.of("dev", "test"); // 判斷當前是否處于該環境 // 通過 enable() 接收此參數判斷是否要顯示 boolean b = environment.acceptsProfiles(of); return new Docket(DocumentationType.SWAGGER_2) .apiInfo(apiInfo()) .enable(b) .select() // 通過.select()方法,去配置掃描接口, RequestHandlerSelectors 配置如何掃描接口 .apis(RequestHandlerSelectors.basePackage("com.locke.swagger.controller")) .build(); } -
可以在項目中增加配置文件
- dev 測試環境
server.port=8081
項目運行結果:

- pro 測試環境
server.port=8082

項目運行結果

配置 API 分組
-
如果沒有配置分組,默認是 default。通過 groupName() 方法即可配置分組:
@Bean public Docket docket(Environment environment) { return new Docket(DocumentationType.SWAGGER_2).apiInfo(apiInfo()) .groupName("狂神") // 配置分組 // 省略配置.... } -
重啟項目查看分組

-
如何配置多個分組?配置多個分組只需要配置多個 docket 即可:
@Bean public Docket docket1(){ return new Docket(DocumentationType.SWAGGER_2).groupName("group1"); } @Bean public Docket docket2(){ return new Docket(DocumentationType.SWAGGER_2).groupName("group2"); } @Bean public Docket docket3(){ return new Docket(DocumentationType.SWAGGER_2).groupName("group3"); } -
重啟項目查看即可

實體配置
-
新建一個實體類
@Data @AllArgsConstructor @NoArgsConstructor // @Api("注釋") @ApiModel("用戶實體") public class User { @ApiModelProperty("用戶名") private String username; @ApiModelProperty("密碼") private String password; } -
只要這個實體在請求接口的返回值上(即使是泛型),都能映射到實體項中:
@RestController public class HelloController { // /error 默認錯誤請求 @GetMapping("/hello") public String hello() { return "hello"; } // 只要我們的接口中,返回值中存在實體類,他就會被掃描到 Swagger 中 @PostMapping("/user") public User user() { return new User(); } } -
重啟查看測試:
注:并不是因為 @ApiModel 這個注解讓實體顯示在這里了,而是只要出現在接口方法的返回值上的實體都會顯示在這里,而 @ApiModel 和 @ApiModelProperty 這兩個注解只是為實體添加注釋的。
- @ApiModel 為類添加注釋
- @ApiModelProperty 為類屬性添加注釋
- @ApiOperation 為方法添加注釋
- @ApiParam 給參數加上注釋
總結:
- 我們可以通過 Swagger 給一些比較難理解的接口或者屬性,增加注釋信息
- 接口文檔實時更新
- 可以在線測試
Swagger 是一個優秀的工具,幾乎所有大公司都有使用它
【注意點】:在正式發布的時候,關閉 Swagger!!!
- 出于安全考慮
- 而且節省內存
常用注解
Swagger 的所有注解定義在 io.swagger.annotations 包下
下面列一些經常用到的,未列舉出來的可以另行查閱說明:
| Swagger注解 | 簡單說明 |
|---|---|
| @Api(tags = "xxx模塊說明") | 作用在模塊類上 |
| @ApiOperation("xxx接口說明") | 作用在接口方法上 |
| @ApiModel("xxxPOJO說明") | 作用在模型類上:如VO、BO |
| @ApiModelProperty(value = "xxx屬性說明",hidden = true) | 作用在類方法和屬性上,hidden設置為true可以隱藏該屬性 |
| @ApiParam("xxx參數說明") | 作用在參數、方法和字段上,類似@ApiModelProperty |
我們也可以給請求的接口配置一些注釋
-
在 HelloController 控制類中的接口添加 api 接口注釋
@RestController public class HelloController { ...... @ApiOperation("Hello 控制接口") @GetMapping("/hello") public String hello2(@ApiParam("用戶名") String username) { return "hello" + username; } @ApiOperation("get 測試") @GetMapping("/get") public User hello2(@ApiParam("用戶") User user) { return user; } }
-
進行 try it out 測試

測試結果

總結:
-
這樣的話,可以給一些比較難理解的屬性或者接口,增加一些配置信息,讓人更容易閱讀!
-
相較于傳統的 Postman 或 Curl 方式測試接口,使用 swagger 簡直就是傻瓜式操作,不需要額外說明文檔(寫得好本身就是文檔)而且更不容易出錯,只需要錄入數據然后點擊 Execute,如果再配合自動化框架,可以說基本就不需要人為操作了。
-
Swagger 是個優秀的工具,現在國內已經有很多的中小型互聯網公司都在使用它,相較于傳統的要先出 Word 接口文檔再測試的方式,顯然這樣也更符合現在的快速迭代開發行情。當然了,提醒下大家在正式環境要記得關閉 Swagger,一來出于安全考慮二來也可以節省運行時內存。
拓展
我們可以導入不同的包實現不同的皮膚定義:
1、默認的 訪問 http://localhost:8080/swagger-ui.html
<dependency>
<groupId>io.springfox</groupId>
<artifactId>springfox-swagger-ui</artifactId>
<version>2.9.2</version>
</dependency>

2、bootstrap-ui 訪問 http://localhost:8080/doc.html
<!-- 引入 swagger-bootstrap-ui包 /doc.html-->
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>swagger-bootstrap-ui</artifactId>
<version>1.9.1</version>
</dependency>

3、Layui-ui 訪問 http://localhost:8080/docs.html
<!-- 引入swagger-ui-layer包 /docs.html-->
<dependency>
<groupId>com.github.caspar-chen</groupId>
<artifactId>swagger-ui-layer</artifactId>
<version>1.1.3</version>
</dependency>
4、mg-ui 訪問 http://localhost:8080/document.html
<!-- 引入swagger-ui-layer包 /document.html-->
<dependency>
<groupId>com.zyplayer</groupId>
<artifactId>swagger-mg-ui</artifactId>
<version>1.0.6</version>
</dependency>

異步、定時、郵件任務
異步任務
-
創建一個
service包 -
創建一個類
AsyncService
異步處理還是非常常用的,比如我們在網站上發送郵件,后臺會去發送郵件,此時前臺會造成響應不動,直到郵件發送完畢,響應才會成功,所以我們一般會采用多線程的方式去處理這些任務。
編寫方法,假裝正在處理數據,使用線程設置一些延時,模擬同步等待的情況;
@Service
public class AsyncService {
public void hello() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("業務進行中....");
}
}
- 編寫
controller包 - 編寫
AsyncController類
@RestController
public class AsyncController {
@Autowired
AsyncService asyncService;
@GetMapping("/hello")
public String hello() {
asyncService.hello(); // 停止 3 秒
return "OK";
}
}
- 訪問 http://localhost:8080/hello 進行測試,3 秒后出現 OK,這是同步等待的情況。
問題:我們如果想讓用戶直接得到消息,就在后臺使用多線程的方式進行處理即可,但是每次都需要自己手動去編寫多線程的實現的話,太麻煩了,我們只需要用一個簡單的辦法,在我們的方法上加一個簡單的注解即可,如下:
- 給 hello 方法添加 @Async 注解;
// 告訴 Spring 這是一個異步方法
@Async
public void hello() {
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("業務進行中....");
}
SpringBoot 就會自己開一個線程池,進行調用!但是要讓這個注解生效,我們還需要在主程序上添加一個注解 @EnableAsync ,開啟異步注解功能;
//開啟異步注解功能
@EnableAsync
@SpringBootApplication
public class SpringbootTaskApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootTaskApplication.class, args);
}
}
- 重啟測試,網頁瞬間響應,后臺代碼依舊執行!
郵件任務
郵件發送,在我們的日常開發中,也非常的多,Springboot 也幫我們做了支持
- 郵件發送需要引入 spring-boot-start-mail
- SpringBoot 自動配置 MailSenderAutoConfiguration
- 定義 MailProperties 內容,配置在
application.yaml中 - 自動裝配 JavaMailSender
- 測試郵件發送
測試:
-
引入 pom 依賴
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-mail</artifactId> </dependency>看它引入的依賴,可以看到 jakarta.mail
<dependency> <groupId>com.sun.mail</groupId> <artifactId>jakarta.mail</artifactId> <scope>compile</scope> </dependency> -
查看自動配置類:MailSenderAutoConfiguration

這個類中存在bean,JavaMailSenderImpl

然后我們去看下配置文件
@ConfigurationProperties(prefix = "spring.mail") public class MailProperties { private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8; private String host; private Integer port; private String username; private String password; private String protocol = "smtp"; private Charset defaultEncoding = DEFAULT_CHARSET; private Map<String, String> properties = new HashMap<>(); private String jndiName; //set、get方法省略。。。 } -
配置文件:
spring.mail.username=1710841251@qq.com spring.mail.password=你的qq授權碼 spring.mail.host=smtp.qq.com # qq 需要配置 ssl spring.mail.properties.mail.smtp.ssl.enable=true獲取授權碼:在QQ郵箱中的設置 \(\to\) 賬戶 \(\to\) 開啟 pop3 和 smtp 服務

-
Spring 單元測試
@Autowired JavaMailSenderImpl javaMailSender; @Test//郵件設置1:一個簡單的郵件 void contextLoads() { SimpleMailMessage mailMessage = new SimpleMailMessage(); mailMessage.setSubject("狂神,你好"); mailMessage.setText("謝謝你的狂神說Java系列課程"); mailMessage.setTo("24736743@qq.com"); mailMessage.setFrom("1710841251@qq.com"); javaMailSender.send(mailMessage); } @Test// 一個復雜的郵件 void contextLoads2() throws MessagingException { MimeMessage mimeMessage = javaMailSender.createMimeMessage(); //組裝 MimeMessageHelper helper = new MimeMessageHelper(mimeMessage, true); //正文 helper.setSubject("狂神,你好~plus"); helper.setText("<p style='color:red'>謝謝你的狂神說Java系列課程</p>", true); //附件 helper.addAttachment("1.jpg", new File("")); helper.addAttachment("2.jpg", new File("")); helper.setTo("24736743@qq.com"); helper.setFrom("1710841251@qq.com"); javaMailSender.send(mimeMessage); } -
查看郵箱,郵件接收成功!
我們只需要使用 Thymeleaf 進行前后端結合即可開發自己網站郵件收發功能了!
定時任務
項目開發中經常需要執行一些定時任務,比如需要在每天凌晨的時候,分析一次前一天的日志信息,Spring 為我們提供了異步執行任務調度的方式,提供了兩個接口。
- TaskExecutor 接口(任務執行者)
- TaskScheduler 接口(任務調度者)
兩個注解:
- @EnableScheduling:開啟定時功能的注解
- @Scheduled:什么時候執行
cron 表達式:
| 字段 | 允許值 | 允許特殊字符 |
|---|---|---|
| 秒 | 0-59 | , - * / |
| 分 | 0-59 | , - * / |
| 小時 | 0-23 | , - * / |
| 日期 | 1-31 | , - * / ? L W C |
| 月份 | 1-12 | , - * / |
| 星期 | 0-1 或 SUN-SAT 0,7 是 SUN | , - * / ? L W C |
| 特殊字符 | 代表含義 |
|---|---|
| , | 枚舉 |
| - | 區間 |
| * | 任意 |
| / | 步長 |
| ? | 日/星期沖突匹配 |
| L | 最后 |
| W | 工作日 |
| C | 和 calendar 練習后計算過的值 |
| # | 星期,4#2 第二個星期三 |
測試步驟:
- 創建一個 ScheduledService
我們里面存在一個 hello 方法,他需要定時執行,怎么處理呢?
@Service
public class ScheduledService {
// 在一個特定的時間執行這個方法——Timer
//cron表達式
// 秒 分 時 日 月 周幾
/*
0 49 11 * * ? 每天的11點49分00秒執行
0 0/5 11,12 * * ? 每天的11點和12點每個五分鐘執行一次
0 15 10 ? * 1-6 每個月的周一到周六的10點15分執行一次
0/2 * * * * ? 每2秒執行一次
*/
@Scheduled(cron = "0/2 * * * * ?")
public void hello() {
System.out.println("hello, 你被執行了");
}
}
- 這里寫完定時任務之后,我們需要在主程序上增加 @EnableScheduling 開啟定時任務功能
// 開啟異步注解功能
@EnableAsync
// 開啟基于注解的定時任務
@EnableScheduling
@SpringBootApplication
public class SpringbootTaskApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootTaskApplication.class, args);
}
}
-
我們來詳細了解下cron表達式:http://www.bejson.com/othertools/cron/
-
常用的表達式
(1)0/2 * * * * ? 表示每2秒 執行任務
(1)0 0/2 * * * ? 表示每2分鐘 執行任務
(1)0 0 2 1 * ? 表示在每月的1日的凌晨2點調整任務
(2)0 15 10 ? * MON-FRI 表示周一到周五每天上午10:15執行作業
(3)0 15 10 ? 6L 2002-2006 表示2002-2006年的每個月的最后一個星期五上午10:15執行作
(4)0 0 10,14,16 * * ? 每天上午10點,下午2點,4點
(5)0 0/30 9-17 * * ? 朝九晚五工作時間內每半小時
(6)0 0 12 ? * WED 表示每個星期三中午12點
(7)0 0 12 * * ? 每天中午12點觸發
(8)0 15 10 ? * * 每天上午10:15觸發
(9)0 15 10 * * ? 每天上午10:15觸發
(10)0 15 10 * * ? 每天上午10:15觸發
(11)0 15 10 * * ? 2005 2005年的每天上午10:15觸發
(12)0 * 14 * * ? 在每天下午2點到下午2:59期間的每1分鐘觸發
(13)0 0/5 14 * * ? 在每天下午2點到下午2:55期間的每5分鐘觸發
(14)0 0/5 14,18 * * ? 在每天下午2點到2:55期間和下午6點到6:55期間的每5分鐘觸發
(15)0 0-5 14 * * ? 在每天下午2點到下午2:05期間的每1分鐘觸發
(16)0 10,44 14 ? 3 WED 每年三月的星期三的下午2:10和2:44觸發
(17)0 15 10 ? * MON-FRI 周一至周五的上午10:15觸發
(18)0 15 10 15 * ? 每月15日上午10:15觸發
(19)0 15 10 L * ? 每月最后一日的上午10:15觸發
(20)0 15 10 ? * 6L 每月的最后一個星期五上午10:15觸發
(21)0 15 10 ? * 6L 2002-2005 2002年至2005年的每月的最后一個星期五上午10:15觸發
(22)0 15 10 ? * 6#3 每月的第三個星期五上午10:15觸
Dubbo 和 Zookeeper 集成
分布式理論
什么是分布式系統
在《分布式系統原理與范型》一書中有如下定義:“分布式系統是若干獨立計算機的集合,這些計算機對于用戶來說就像單個相關系統”;
分布式系統是由一組通過網絡進行通信、為了完成共同的任務而協調工作的計算機節點組成的系統。分布式系統的出現是為了用廉價的、普通的機器完成單個計算機無法完成的計算、存儲任務。其目的是利用更多的機器,處理更多的數據。
分布式系統(distributed system)是建立在網絡之上的軟件系統。
首先需要明確的是,只有當單個節點的處理能力無法滿足日益增長的計算、存儲任務的時候,且硬件的提升(加內存、加磁盤、使用更好的CPU)高昂到得不償失的時候,應用程序也不能進一步優化的時候,我們才需要考慮分布式系統。因為,分布式系統要解決的問題本身就是和單機系統一樣的,而由于分布式系統多節點、通過網絡通信的拓撲結構,會引入很多單機系統沒有的問題,為了解決這些問題又會引入更多的機制、協議,帶來更多的問題。
Dubbo 文檔
隨著互聯網的發展,網站應用的規模不斷擴大,常規的垂直應用架構已無法應對,分布式服務架構以及流動計算架構勢在必行,急需一個治理系統確保架構有條不紊的演進。
在 Dubbo 的官網文檔有這樣一張圖:

單一應用架構
當網站流量很小時,只需一個應用,將所有功能都部署在一起,以減少部署節點和成本。此時,用于簡化增刪改查工作量的數據訪問框架 (ORM) 是關鍵。
適用于小型網站,小型管理系統,將所有功能都部署到一個功能里,簡單易用。
缺點:
- 性能擴展比較難
- 協同開發問題
- 不利于升級維護
垂直應用架構
當訪問量逐漸增大,單一應用增加機器帶來的加速度越來越小,將應用拆成互不相干的幾個應用,以提升效率。此時,用于加速前端頁面開發的 Web 框架 (MVC) 是關鍵。
通過切分業務來實現各個模塊獨立部署,降低了維護和部署的難度,團隊各司其職,更易于管理,性能擴展也更方便,更有針對性。
缺點:
- 公用模塊無法重復利用,開發性的浪費。
分布式服務架構
當垂直應用越來越多,應用之間交互不可避免,將核心業務抽取出來,作為獨立的服務,逐漸形成穩定的服務中心,使前端應用能更快速的響應多變的市場需求。此時,用于提高業務復用及整合的分布式服務框架 (RPC)是關鍵。
優點:
- 每個子系統變成小型系統,功能簡單,前期開發成本低,周期短
- 每個子系統可按需伸縮
- 每個子系統可采用不同的技術
缺點:
- 子系統之間存在數據冗余、功能冗余,耦合性高
- 按需伸縮粒度不夠,對同一個子系統中的不同的業務無法實現,比如訂單管理和用戶管理
流動計算架構
當服務越來越多,容量的評估,小服務資源的浪費等問題逐漸顯現,此時需增加一個調度中心基于訪問壓力實時管理集群容量,提高集群利用率。此時,用于提高機器利用率的資源調度和治理中心 (SOA) [ Service Oriented Architecture]是關鍵。
特點:
- 基于 SOA 的架構思想,將重復公用的功能抽取為組件,以服務的方式向各各系統提供服務
- 各各系統與服務之間采用 webservice、rpc 等方式進行通信
- ESB 企業服務總線作為系統與服務之間通信的橋梁
優點:
- 將重復的功能抽取為服務,提高開發效率,提高系統的可重用性、可維護性
- 可以針對不同服務的特點按需伸縮
- 采用 ESB 減少系統中的接口耦合
缺點:
- 系統與服務的界限模糊,會導致抽取的服務的粒度過大,系統與服務之間耦合性高
- 雖然使用了 ESB,但是服務的接口協議不固定,種類繁多,不利于系統維護。
微服務架構
基于 SOA 架構的思想,為了滿足移動互聯網對大型項目及多客戶端的需求,對服務層進行細粒度的拆分,所拆分的每個服務只完成某個特定的業務功能,比如訂單服務只實現訂單相關的業務,用戶服務實現用戶管理相關的業務等等,服務的粒度很小,所以稱為微服務架構。

特點:
- 服務層按業務拆分為一個一個的微服務
- 微服務的職責單一
- 微服務之間采用 RESTful、RPC 等輕量級協議傳輸
- 有利于采用前后端分離架構。
優點:
- 服務拆分粒度更細,有利于資源重復利用,提高開發效率
- 可以更加精準的制定每個服務的優化方案,按需伸縮
- 適用于互聯網時代,產品迭代周期更短
缺點:
- 開發的復雜性增加,因為一個業務流程需要多個微服務通過網絡交互來完成
- 微服務過多,服務治理成本高,不利于系統維護
什么是 RPC
RPC【Remote Procedure Call】是指遠程過程調用,是一種進程間通信方式,他是一種技術的思想,而不是規范。它允許程序調用另一個地址空間(通常是共享網絡的另一臺機器上)的過程或函數,而不用程序員顯式編碼這個遠程調用的細節。即程序員無論是調用本地的還是遠程的函數,本質上編寫的調用代碼基本相同。
也就是說兩臺服務器A,B,一個應用部署在A服務器上,想要調用B服務器上應用提供的函數/方法,由于不在一個內存空間,不能直接調用,需要通過網絡來表達調用的語義和傳達調用的數據。為什么要用RPC呢?就是無法在一個進程內,甚至一個計算機內通過本地調用的方式完成的需求,比如不同的系統間的通訊,甚至不同的組織間的通訊,由于計算能力需要橫向擴展,需要在多臺機器組成的集群上部署應用。RPC就是要像調用本地的函數一樣去調遠程函數;
推薦閱讀文章:https://www.jianshu.com/p/2accc2840a1b
RPC基本原理


RPC 兩個核心模塊:通訊,序列化。
- 序列化:數據傳輸需要轉換
測試環境搭建
Dubbo
Apache Dubbo 是一款高性能、輕量級的開源 Java RPC 框架,它提供了三大核心能力:面向接口的遠程方法調用,智能容錯和負載均衡,以及服務自動注冊和發現。

服務提供者(Provider):暴露服務的服務提供方,服務提供者在啟動時,向注冊中心注冊自己提供的服務。
服務消費者(Consumer):調用遠程服務的服務消費方,服務消費者在啟動時,向注冊中心訂閱自己所需的服務,服務消費者,從提供者地址列表中,基于軟負載均衡算法,選一臺提供者進行調用,如果調用失敗,再選另一臺調用。
注冊中心(Registry):注冊中心返回服務提供者地址列表給消費者,如果有變更,注冊中心將基于長連接推送變更數據給消費者
監控中心(Monitor):服務消費者和提供者,在內存中累計調用次數和調用時間,定時每分鐘發送一次統計數據到監控中心
調用關系說明
- 服務容器負責啟動,加載,運行服務提供者。
- 服務提供者在啟動時,向注冊中心注冊自己提供的服務。
- 服務消費者在啟動時,向注冊中心訂閱自己所需的服務。
- 注冊中心返回服務提供者地址列表給消費者,如果有變更,注冊中心將基于長連接推送變更數據給消費者。
- 服務消費者,從提供者地址列表中,基于軟負載均衡算法,選一臺提供者進行調用,如果調用失敗,再選另一臺調用。
- 服務消費者和提供者,在內存中累計調用次數和調用時間,定時每分鐘發送一次統計數據到監控中心。
安裝 zookeeper
https://juejin.cn/post/6974556676816896030
在 zoo.cfg 中添加:admin.serverPort=2182
- 雙擊
zkServer.cmd開啟服務:端口號是 2181 - 使用
zkCli.cmd測試ls /:列出 zookeeper 根下保存的所有節點create –e /locke 123:創建一個 locke 節點,值為 123get /locke:獲取 /locke 節點的值
安裝 dubbo-admin
dubbo 本身并不是一個服務軟件。它其實就是一個 jar 包,能夠幫你的 java 程序連接到 zookeeper,并利用 zookeeper 消費、提供服務;
但是為了讓用戶更好的管理監控眾多的 dubbo 服務,官方提供了一個可視化的監控程序 dubbo-admin(監控管理后臺),不過這個監控即使不裝也不影響使用。
- 下載 dubbo-admin
- 在項目目錄下 cmd 打包:
mvn clean package -Dmaven.test.skip=true,也可以參考該項目的 README 文件打包

- 啟動后端:
# 切換到目錄:直接在路徑前輸入 cmd
dubbo-Admin-develop/dubbo-admin-distribution/target>
# 執行下面的命令啟動 dubbo-admin,dubbo-admin 后臺由 SpringBoot 構建
java -jar ./dubbo-admin-0.5.0-SNAPSHOT.jar
-
執行完畢后,訪問 http://localhost:38080/, 用戶名和密碼都是 root
-
登錄成功后查看界面:

總結:
- zookeeper:注冊中心
- dubbo-admin:其實就是一個監控管理后臺,可以查看我們注冊了那些服務,哪些服務被消費了
- Dubbo:jar 包
SpringBoot + Dubbo + zookeeper
注意:SringBoot 的版本為 2.7.3
框架搭建
-
啟動 zookeeper
-
IDEA 創建一個空項目
-
創建一個模塊,實現服務提供者 provider-server,選擇 web 依賴即可,
pom.xml如下:<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.7.3</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.locke</groupId> <artifactId>provider-server</artifactId> <version>0.0.1-SNAPSHOT</version> <name>provider-server</name> <description>provider-server</description> <properties> <java.version>11</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> <exclusions> <exclusion> <groupId>org.junit.vintage</groupId> <artifactId>junit-vintage-engine</artifactId> </exclusion> </exclusions> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> -
項目創建完畢,我們寫一個服務,比如買票的服務
- 編寫接口:
public interface TicketService { public String getTicket(); }- 編寫實現類:
public class TicketServiceImpl implements TicketService { @Override public String getTicket() { return "《狂神說Java》"; } } -
創建一個模塊,實現服務消費者 consumer-server,選擇 web 依賴即可,
pom.xml文件同上 -
項目創建完畢,我們寫一個服務,比如用戶的服務
- 編寫 service
public interface UserService { // 我們需要去拿去注冊中心的服務 }
需求:現在我們的用戶想使用買票的服務,如何實現?
服務提供者
- 將服務提供者注冊到注冊中心,我們需要整合 Dubbo 和 zookeeper,所以需要導包
<!-- https://mvnrepository.com/artifact/org.apache.dubbo/dubbo-spring-boot-starter -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-starter</artifactId>
<version>3.1.0</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.dubbo/dubbo-registry-zookeeper -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-registry-zookeeper</artifactId>
<version>3.1.0</version>
</dependency>
- 在 springboot 配置文件中配置 Dubbo 的相關屬性
server.port=8001
# 當前應用名字
dubbo.application.name=provider-server
# 注冊中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
# 掃描指定包下服務
dubbo.scan.base-packages=com.locke.service
注意,如果在服務提供者的啟動類 ProviderServerApplication 前加上 @EnableDubbo 注解,則配置文件中的
dubbo.scan.base-packages可以不用加,@EnableDubbo 會自動將服務注冊到 Dubbo 中。
- 在 service 的實現類中配置服務注解,發布服務!注意導包問題,不要導入spring 的包
import org.apache.dubbo.config.annotation.DubboService;
import org.springframework.stereotype.Component;
// 服務注冊與發現
@DubboService // 可以被掃描到,在項目一啟動就自動注冊到注冊中心 zookeeper
@Component // 使用 Dubbo 后盡量不要用 Service 注解
@Slf4j
public class TicketServiceImpl implements TicketService {
@Override
public String getTicket() {
log.info("買票服務被調用");
return "《狂神說Java》";
}
}
邏輯理解 :應用啟動起來,dubbo 就會掃描指定的包下帶有 @component 注解的服務,將它發布在指定的注冊中心中!
- 運行測試

- 查看服務是否在 zookeeper 中注冊成功:
- 在可視化界面上查看:http://localhost:38080/, 用戶名和密碼都是 root

- 在
zkCli.cmd中查看:在/services/provider-server下面看到了一個 IP+ 端口的地址,說明該服務存在注冊的實例,可以使用
[zk: localhost:2181(CONNECTED) 0] ls ls [-s] [-w] [-R] path [zk: localhost:2181(CONNECTED) 1] ls / [dubbo, services, zookeeper] [zk: localhost:2181(CONNECTED) 2] ls /services [dubbo-admin, provider-server] [zk: localhost:2181(CONNECTED) 3] ls /services/provider-server [192.168.31.103:20880] [zk: localhost:2181(CONNECTED) 4] - 在可視化界面上查看:http://localhost:38080/, 用戶名和密碼都是 root
服務消費者
- 導入依賴:和之前一樣
- 配置參數
server.port=8002
#當前應用名字
dubbo.application.name=consumer-server
#注冊中心地址
dubbo.registry.address=zookeeper://127.0.0.1:2181
- 本來正常步驟是需要將服務提供者的接口打包,然后用 pom 文件導入,我們這里使用簡單的方式,直接將服務的接口拿過來,路徑必須保證正確,即和服務提供者相同;

- 完善消費者的服務類:注意導入的包
import org.apache.dubbo.config.annotation.DubboReference;
import org.springframework.stereotype.Service;
@Service // 注入到容器中
public class UserService {
// 想拿到 provider-server 提供的票,要去注冊中心拿到服務
// 引用,Pom 坐標,可以定義路徑相同的接口名
@DubboReference
TicketService ticketService;
public void bugTicket() {
String ticket = ticketService.getTicket();
System.out.println("在注冊中心買到" + ticket);
}
}
- 測試類編寫
@SpringBootTest
public class ConsumerServerApplicationTests {
@Autowired
UserService userService;
@Test
public void contextLoads() {
userService.bugTicket();
}
}
啟動測試
- 開啟 zookeeper
- 打開 dubbo-admin 實現監控
- 開啟服務者
- 消費者消費測試

以上就是 SpingBoot + dubbo + zookeeper 實現分布式開發的應用,其實就是一個服務拆分的思想。
浙公網安備 33010602011771號