java之SpringBoot開發實用篇

MENU

  • SpringBoot開發實用篇
  • KF-1.熱部署
    • KF-1-1.手動啟動熱部署
    • KF-1-2.自動啟動熱部署
    • KF-1-3.參與熱部署監控的文件范圍配置
    • KF-1-4.關閉熱部署
  • KF-2.配置高級
    • KF-2-1.@ConfigurationProperties
    • KF-2-2.寬松綁定/松散綁定
    • KF-2-3.常用計量單位綁定
    • KF-2-4.校驗
    • KF-2-5.數據類型轉換
  • KF-3.測試
    • KF-3-1.加載測試專用屬性
    • KF-3-2.加載測試專用配置
    • KF-3-3.Web環境模擬測試
    • KF-3-4.數據層測試回滾
    • KF-3-5.測試用例數據設定
  • KF-4.數據層解決方案
    • KF-4-1.SQL
      • 數據源技術
      • 持久化技術
      • 數據庫技術
    • KF-4-2.NoSQL
      • SpringBoot整合Redis
        • 安裝
        • 基本操作
        • 整合
      • SpringBoot整合MongoDB
        • 安裝
        • 基本操作
        • 整合
      • SpringBoot整合ES
        • 安裝
        • 基本操作
        • 整合
  • KF-5.整合第三方技術
      • KF-5-1.緩存
        • SpringBoot內置緩存解決方案
        • 手機驗證碼案例
        • SpringBoot整合Ehcache緩存
        • SpringBoot整合Redis緩存
        • SpringBoot整合Memcached緩存
        • SpringBoot整合jetcache緩存
          • 純遠程方案
          • 純本地方案
          • 本地+遠程方案
          • 遠程方案的數據同步
          • 數據報表
        • SpringBoot整合j2cache緩存
      • KF-5-2.任務
        • Quartz
        • Task
      • KF-5-3.郵件
        • 發送簡單郵件
        • 發送多組件郵件(附件、復雜正文)
      • KF-5-4.消息
        • 消息的概念
        • Java處理消息的標準規范
          • JMS
          • AMQP
          • MQTT
          • KafKa
        • 購物訂單發送手機短信案例
        • SpringBoot整合ActiveMQ
          • 安裝
          • 整合


SpringBoot開發實用篇

懷著忐忑的心情,開始了開發實用篇文檔的編寫。為什么忐忑?特喵的債欠的太多,不知道從何寫起。哎,不煽情了,開工。


運維實用篇完結以后,開發實用篇采用日更新的形式發布給各位小伙伴,基本上是每天一集,目前已經發布完畢。看評論區,好多小伙伴在求文檔,所以趕緊來補文檔,加班加點把開發實用篇的文檔刨出來。


開發實用篇中因為牽扯到SpringBoot整合各種各樣的技術,由于不是每個小伙伴對各種技術都有所掌握,所以在整合每一個技術之前,都會做一個快速的普及,這樣的話內容整個開發實用篇所包含的內容就會比較多。各位小伙伴在學習的時候,如果對某一個技術不是很清楚,可以先跳過對應章節,或者先補充一下技術知識,然后再來看對應的課程。開發實用篇具體包含的內容如下:

1、熱部署
2、配置高級
3、測試
4、數據層解決方案
5、整合第三方技術
6、監控


看目錄感覺內容量并不是很大,但是在數據層解決方案和整合第三方技術中包含了大量的知識,一點一點慢慢學吧。下面開啟第一部分熱部署相關知識的學習


KF-1.熱部署

什么是熱部署?簡單說就是你程序改了,現在要重新啟動服務器,嫌麻煩?不用重啟,服務器會自己悄悄的把更新后的程序給重新加載一遍,這就是熱部署。


熱部署的功能是如何實現的呢?這就要分兩種情況來說了,非springboot工程和springboot工程的熱部署實現方式完全不一樣。先說一下原始的非springboot項目是如何實現熱部署的。


非springboot項目熱部署實現原理

開發非springboot項目時,我們要制作一個web工程并通過tomcat啟動,通常需要先安裝tomcat服務器到磁盤中,開發的程序配置發布到安裝的tomcat服務器上。如果想實現熱部署的效果,這種情況其實有兩種做法,一種是在tomcat服務器的配置文件中進行配置,這種做法與你使用什么IDE工具無關,不管你使用eclipse還是idea都行。還有一種做法是通過IDE工具進行配置,比如在idea工具中進行設置,這種形式需要依賴IDE工具,每款IDE工具不同,對應的配置也不太一樣。但是核心思想是一樣的,就是使用服務器去監控其中加載的應用,發現產生了變化就重新加載一次。


上面所說的非springboot項目實現熱部署看上去是一個非常簡單的過程,幾乎每個小伙伴都能自己寫出來。如果你不會寫,我給你個最簡單的思路,但是實際設計要比這復雜一些。例如啟動一個定時任務,任務啟動時記錄每個文件的大小,以后每5秒比對一下每個文件的大小是否有改變,或者是否有新文件。如果沒有改變,放行,如果有改變,刷新當前記錄的文件信息,然后重新啟動服務器,這就可以實現熱部署了。當然,這個過程肯定不能這么做,比如我把一個打印輸出的字符串"abc"改成"cba",比對大小是沒有變化的,但是內容缺實變了,所以這么做肯定不行,只是給大家打個比方,而且重啟服務器這就是冷啟動了,不能算熱部署,領會精神吧。


看上去這個過程也沒多復雜,在springboot項目中難道還有其他的彎彎繞嗎?還真有。


springboot項目熱部署實現原理

基于springboot開發的web工程其實有一個顯著的特征,就是tomcat服務器內置了,還記得內嵌服務器嗎?服務器是以一個對象的形式在spring容器中運行的。本來我們期望于tomcat服務器加載程序后由tomcat服務器盯著程序,你變化后我就重新啟動重新加載,但是現在tomcat和我們的程序是平級的了,都是spring容器中的組件,這下就麻煩了,缺乏了一個直接的管理權,那該怎么做呢?簡單,再搞一個程序X在spring容器中盯著你原始開發的程序A不就行了嗎?確實,搞一個盯著程序A的程序X就行了,如果你自己開發的程序A變化了,那么程序X就命令tomcat容器重新加載程序A就OK了。并且這樣做有一個好處,spring容器中東西不用全部重新加載一遍,只需要重新加載你開發的程序那一部分就可以了,這下效率又高了,挺好。


下面就說說,怎么搞出來這么一個程序X,肯定不是我們自己手寫了,springboot早就做好了,搞一個坐標導入進去就行了。


KF-1-1.手動啟動熱部署

步驟①

導入開發者工具對應的坐標

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-devtools</artifactId><optional>true</optional>
</dependency>

步驟②

構建項目,可以使用快捷鍵激活此功能

image-20220222121257218

對應的快捷鍵一定要記得

<CTR>L+<F9>

以上過程就實現了springboot工程的熱部署,是不是挺簡單的。不過這里需要把底層的工作工程給普及一下。


重啟與重載

一個springboot項目在運行時實際上是分兩個過程進行的,根據加載的東西不同,劃分成base類加載器與restart類加載器。
1、base類加載器:用來加載jar包中的類,jar包中的類和配置文件由于不會發生變化,因此不管加載多少次,加載的內容不會發生變化
2、restart類加載器:用來加載開發者自己開發的類、配置文件、頁面等信息,這一類文件受開發者影響


當springboot項目啟動時,base類加載器執行,加載jar包中的信息后,restart類加載器執行,加載開發者制作的內容。當執行構建項目后,由于jar中的信息不會變化,因此base類加載器無需再次執行,所以僅僅運行restart類加載即可,也就是將開發者自己制作的內容重新加載就行了,這就完成了一次熱部署的過程,也可以說熱部署的過程實際上是重新加載restart類加載器中的信息。


總結

1. 使用開發者工具可以為當前項目開啟熱部署功能
2. 使用構建項目操作對工程進行熱部署


思考

上述過程每次進行熱部署都需要開發者手工操作,不管是點擊按鈕還是快捷鍵都需要開發者手工執行。這種操作的應用場景主要是在開發調試期,并且調試的代碼處于不同的文件中,比如服務器啟動了,我需要改4個文件中的內容,然后重啟,等4個文件都改完了再執行熱部署,使用一個快捷鍵就OK了。但是如果現在開發者要修改的內容就只有一個文件中的少量代碼,這個時候代碼修改完畢如果能夠讓程序自己執行熱部署功能,就可以減少開發者的操作,也就是自動進行熱部署,能這么做嗎?是可以的。咱們下一節再說。


KF-1-2.自動啟動熱部署

自動熱部署其實就是設計一個開關,打開這個開關后,IDE工具就可以自動熱部署。因此這個操作和IDE工具有關,以下以idea為例設置idea中啟動熱部署


步驟①

設置自動構建項目
打開【File】,選擇【settings…】,在面板左側的菜單中找到【Compile】選項,然后勾選【Build project automatically】,意思是自動構建項目

image-20220222123543551

自動構建項目選項勾選后


步驟②

允許在程序運行時進行自動構建
使用快捷鍵【Ctrl】+【Alt】+【Shit】+【/】打開維護面板,選擇第1項【Registry…】

image-20220222124006910

在選項中搜索comple,然后勾選對應項即可?

在這里插入圖片描述

這樣程序在運行的時候就可以進行自動構建了,實現了熱部署的效果。


關注:如果你每敲一個字母,服務器就重新構建一次,這未免有點太頻繁了,所以idea設置當idea工具失去焦點5秒后進行熱部署。其實就是你從idea工具中切換到其他工具時進行熱部署,比如改完程序需要到瀏覽器上去調試,這個時候idea就自動進行熱部署操作。


總結

1. 自動熱部署要開啟自動構建項目
2. 自動熱部署要開啟在程序運行時自動構建項目


思考

現在已經實現了熱部署了,但是到企業開發的時候你會發現,為了便于管理,在你的程序目錄中除了有代碼,還有可能有文檔,如果你修改了一下文檔,這個時候會進行熱部署嗎?不管是否進行熱部署,這個過程我們需要自己控制才比較合理,那這個東西能控制嗎?咱們下一節再說。


KF-1-3.參與熱部署監控的文件范圍配置

通過修改項目中的文件,你可以發現其實并不是所有的文件修改都會激活熱部署的,原因在于在開發者工具中有一組配置,當滿足了配置中的條件后,才會啟動熱部署,配置中默認不參與熱部署的目錄信息如下

  • /META-INF/maven
  • /META-INF/resources
  • /resources
  • /static
  • /public
  • /templates

以上目錄中的文件如果發生變化,是不參與熱部署的。如果想修改配置,可以通過application.yml文件進行設定哪些文件不參與熱部署操作

spring:devtools:restart:# 設置不參與熱部署的文件或文件夾exclude: static/**,public/**,config/application.yml

總結

1. 通過配置可以修改不參與熱部署的文件或目錄


思考

熱部署功能是一個典型的開發階段使用的功能,到了線上環境運行程序時,這個功能就沒有意義了。能否關閉熱部署功能呢?咱們下一節再說。


KF-1-4.關閉熱部署

線上環境運行時是不可能使用熱部署功能的,所以需要強制關閉此功能,通過配置可以關閉此功能。

spring:devtools:restart:enabled: false

如果當心配置文件層級過多導致相符覆蓋最終引起配置失效,可以提高配置的層級,在更高層級中配置關閉熱部署。例如在啟動容器前通過系統屬性設置關閉熱部署功能。

@SpringBootApplication
public class SSMPApplication {public static void main(String[] args) {System.setProperty("spring.devtools.restart.enabled","false");SpringApplication.run(SSMPApplication.class);}
}

其實上述擔心略微有點多余,因為線上環境的維護是不可能出現修改代碼的操作的,這么做唯一的作用是降低資源消耗,畢竟那雙盯著你項目是不是產生變化的眼睛只要閉上了,就不具有熱部署功能了,這個開關的作用就是禁用對應功能。


總結

1. 通過配置可以關閉熱部署功能降低線上程序的資源消耗


KF-2.配置高級

進入開發實用篇第二章內容,配置高級,其實配置在基礎篇講了一部分,在運維實用篇講了一部分,這里還要講,講的東西有什么區別呢?距離開發過程越來越接近,解決的問題也越來越靠近線上環境,下面就開啟本章的學習。


KF-2-1.@ConfigurationProperties

在基礎篇學習了@ConfigurationProperties注解,此注解的作用是用來為bean綁定屬性的。開發者可以在yml配置文件中以對象的格式添加若干屬性

servers:ip-address: 192.168.0.1 port: 2345timeout: -1

然后再開發一個用來封裝數據的實體類,注意要提供屬性對應的setter方法

@Component
@Data
public class ServerConfig {private String ipAddress;private int port;private long timeout;
}

使用@ConfigurationProperties注解就可以將配置中的屬性值關聯到開發的模型類上

@Component
@Data
@ConfigurationProperties(prefix = "servers")
public class ServerConfig {private String ipAddress;private int port;private long timeout;
}

這樣加載對應bean的時候就可以直接加載配置屬性值了。但是目前我們學的都是給自定義的bean使用這種形式加載屬性值,如果是第三方的bean呢?能不能用這種形式加載屬性值呢?為什么會提出這個疑問?原因就在于當前@ConfigurationProperties注解是寫在類定義的上方,而第三方開發的bean源代碼不是你自己書寫的,你也不可能到源代碼中去添加@ConfigurationProperties注解,這種問題該怎么解決呢?下面就來說說這個問題。


使用@ConfigurationProperties注解其實可以為第三方bean加載屬性,格式特殊一點而已。


步驟①

使用@Bean注解定義第三方bean

@Bean
public DruidDataSource datasource(){DruidDataSource ds = new DruidDataSource();return ds;
}

步驟②

在yml中定義要綁定的屬性,注意datasource此時全小寫

datasource:driverClassName: com.mysql.jdbc.Driver

步驟③

使用@ConfigurationProperties注解為第三方bean進行屬性綁定,注意前綴是全小寫的datasource

@Bean
@ConfigurationProperties(prefix = "datasource")
public DruidDataSource datasource(){DruidDataSource ds = new DruidDataSource();return ds;
}

操作方式完全一樣,只不過@ConfigurationProperties注解不僅能添加到類上,還可以添加到方法上,添加到類上是為spring容器管理的當前類的對象綁定屬性,添加到方法上是為spring容器管理的當前方法的返回值對象綁定屬性,其實本質上都一樣。


做到這其實就出現了一個新的問題,目前我們定義bean不是通過類注解定義就是通過@Bean定義,使用@ConfigurationProperties注解可以為bean進行屬性綁定,那在一個業務系統中,哪些bean通過注解@ConfigurationProperties去綁定屬性了呢?因為這個注解不僅可以寫在類上,還可以寫在方法上,所以找起來就比較麻煩了。為了解決這個問題,spring給我們提供了一個全新的注解,專門標注使用@ConfigurationProperties注解綁定屬性的bean是哪些。這個注解叫做@EnableConfigurationProperties。具體如何使用呢?


步驟①

在配置類上開啟@EnableConfigurationProperties注解,并標注要使用@ConfigurationProperties注解綁定屬性的類

@SpringBootApplication
@EnableConfigurationProperties(ServerConfig.class)
public class Springboot13ConfigurationApplication {
}

步驟②

在對應的類上直接使用@ConfigurationProperties進行屬性綁定

@Data
@ConfigurationProperties(prefix = "servers")
public class ServerConfig {private String ipAddress;private int port;private long timeout;
}

有人感覺這沒區別啊?注意觀察,現在綁定屬性的ServerConfig類并沒有聲明@Component注解。當使用@EnableConfigurationProperties注解時,spring會默認將其標注的類定義為bean,因此無需再次聲明@Component注解了。


最后再說一個小技巧,使用@ConfigurationProperties注解時,會出現一個提示信息

image-20220222145535749

出現這個提示后只需要添加一個坐標此提醒就消失了

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-configuration-processor</artifactId>
</dependency>

總結

1. 使用@ConfigurationProperties可以為使用@Bean聲明的第三方bean綁定屬性
2. 當使用@EnableConfigurationProperties聲明進行屬性綁定的bean后,無需使用@Component注解再次進行bean聲明


KF-2-2.寬松綁定/松散綁定

在進行屬性綁定時,可能會遇到如下情況,為了進行標準命名,開發者會將屬性名嚴格按照駝峰命名法書寫,在yml配置文件中將datasource修改為dataSource,如下:

dataSource:driverClassName: com.mysql.jdbc.Driver

此時程序可以正常運行,然后又將代碼中的前綴datasource修改為dataSource,如下:

@Bean
@ConfigurationProperties(prefix = "dataSource")
public DruidDataSource datasource(){DruidDataSource ds = new DruidDataSource();return ds;
}

此時就發生了編譯錯誤,而且并不是idea工具導致的,運行后依然會出現問題,配置屬性名dataSource是無效的

Configuration property name 'dataSource' is not valid:Invalid characters: 'S'Bean: datasourceReason: Canonical names should be kebab-case ('-' separated), lowercase > alpha-numeric characters and must start with a letterAction:
Modify 'dataSource' so that it conforms to the canonical names requirements.

為什么會出現這種問題,這就要來說一說springboot進行屬性綁定時的一個重要知識點了,有關屬性名稱的寬松綁定,也可以稱為寬松綁定。


什么是寬松綁定?實際上是springboot進行編程時人性化設計的一種體現,即配置文件中的命名格式與變量名的命名格式可以進行格式上的最大化兼容。兼容到什么程度呢?幾乎主流的命名格式都支持,例如:


在ServerConfig中的ipAddress屬性名

@Component
@Data
@ConfigurationProperties(prefix = "servers")
public class ServerConfig {private String ipAddress;
}

可以與下面的配置屬性名規則全兼容

servers:ipAddress: 192.168.0.2       # 駝峰模式ip_address: 192.168.0.2      # 下劃線模式ip-address: 192.168.0.2      # 烤肉串模式IP_ADDRESS: 192.168.0.2      # 常量模式

也可以說,以上4種模式最終都可以匹配到ipAddress這個屬性名。為什么這樣呢?原因就是在進行匹配時,配置中的名稱要去掉中劃線和下劃線后,忽略大小寫的情況下去與java代碼中的屬性名進行忽略大小寫的等值匹配,以上4種命名去掉下劃線中劃線忽略大小寫后都是一個詞ipaddress,java代碼中的屬性名忽略大小寫后也是ipaddress,這樣就可以進行等值匹配了,這就是為什么這4種格式都能匹配成功的原因。不過springboot官方推薦使用烤肉串模式,也就是中劃線模式。


到這里我們掌握了一個知識點,就是命名的規范問題。再來看開始出現的編程錯誤信息

Configuration property name 'dataSource' is not valid:Invalid characters: 'S'Bean: datasourceReason: Canonical names should be kebab-case ('-' separated), lowercase > alpha-numeric characters and must start with a letterAction:
Modify 'dataSource' so that it conforms to the canonical names requirements.

其中Reason描述了報錯的原因,規范的名稱應該是烤肉串(kebab)模式(case),即使用-分隔,使用小寫字母數字作為標準字符,且必須以字母開頭。然后再看我們寫的名稱dataSource,就不滿足上述要求。鬧了半天,在書寫前綴時,這個詞不是隨意支持的,必須使用上述標準。編程寫了這么久,基本上編程習慣都養成了,到這里又被springboot教育了,沒轍,誰讓人家東西好用呢,按照人家的要求寫吧。


最后說一句,以上規則僅針對springboot中@ConfigurationProperties注解進行屬性綁定時有效,對@Value注解進行屬性映射無效。有人就說,那我不用你不就行了?不用,你小看springboot的推廣能力了,到原理篇我們看源碼時,你會發現內部全是這玩意兒,算了,拿人手短吃人嘴短,認慫吧。


總結

1. @ConfigurationProperties綁定屬性時支持屬性名寬松綁定,這個寬松體現在屬性名的命名規則上
2. @Value注解不支持松散綁定規則
3. 綁定前綴名推薦采用烤肉串命名規則,即使用中劃線做分隔符


KF-2-3.常用計量單位綁定

在前面的配置中,我們書寫了如下配置值,其中第三項超時時間timeout描述了服務器操作超時時間,當前值是-1表示永不超時。

servers:ip-address: 192.168.0.1 port: 2345timeout: -1

但是每個人都這個值的理解會產生不同,比如線上服務器完成一次主從備份,配置超時時間240,這個240如果單位是秒就是超時時間4分鐘,如果單位是分鐘就是超時時間4小時。面對一次線上服務器的主從備份,設置4分鐘,簡直是開玩笑,別說拷貝過程,備份之前的壓縮過程4分鐘也搞不定,這個時候問題就來了,怎么解決這個誤會?


除了加強約定之外,springboot充分利用了JDK8中提供的全新的用來表示計量單位的新數據類型,從根本上解決這個問題。以下模型類中添加了兩個JDK8中新增的類,分別是Duration和DataSize

@Component
@Data
@ConfigurationProperties(prefix = "servers")
public class ServerConfig {@DurationUnit(ChronoUnit.HOURS)private Duration serverTimeOut;@DataSizeUnit(DataUnit.MEGABYTES)private DataSize dataSize;
}

Duration:表示時間間隔,可以通過@DurationUnit注解描述時間單位,例如上例中描述的單位為小時(ChronoUnit.HOURS)


DataSize:表示存儲空間,可以通過@DataSizeUnit注解描述存儲空間單位,例如上例中描述的單位為MB(DataUnit.MEGABYTES)


使用上述兩個單位就可以有效避免因溝通不同步或文檔不健全導致的信息不對稱問題,從根本上解決了問題,避免產生誤讀。


Druation常用單位如下:

image-20220222173911102


DataSize常用單位如下:
image-20220222174130102


KF-2-4.校驗

目前我們在進行屬性綁定時可以通過松散綁定規則在書寫時放飛自我了,但是在書寫時由于無法感知模型類中的數據類型,就會出現類型不匹配的問題,比如代碼中需要int類型,配置中給了非法的數值,例如寫一個“a",這種數據肯定無法有效的綁定,還會引發錯誤。


SpringBoot給出了強大的數據校驗功能,可以有效的避免此類問題的發生。在JAVAEE的JSR303規范中給出了具體的數據校驗標準,開發者可以根據自己的需要選擇對應的校驗框架,此處使用Hibernate提供的校驗框架來作為實現進行數據校驗。書寫應用格式非常固定,話不多說,直接上步驟


步驟①

開啟校驗框架

<!--1.導入JSR303規范-->
<dependency><groupId>javax.validation</groupId><artifactId>validation-api</artifactId>
</dependency>
<!--使用hibernate框架提供的校驗器做實現-->
<dependency><groupId>org.hibernate.validator</groupId><artifactId>hibernate-validator</artifactId>
</dependency>

步驟②

在需要開啟校驗功能的類上使用注解@Validated開啟校驗功能

@Component
@Data
@ConfigurationProperties(prefix = "servers")
//開啟對當前bean的屬性注入校驗
@Validatedpublic class ServerConfig {
}

步驟③

對具體的字段設置校驗規則

@Component
@Data
@ConfigurationProperties(prefix = "servers")
//開啟對當前bean的屬性注入校驗
@Validated
public class ServerConfig {//設置具體的規則@Max(value = 8888,message = "最大值不能超過8888")@Min(value = 202,message = "最小值不能低于202")private int port;
}

通過設置數據格式校驗,就可以有效避免非法數據加載,其實使用起來還是挺輕松的,基本上就是一個格式。


總結

1. 開啟Bean屬性校驗功能一共3步:導入JSR303與Hibernate校驗框架坐標、使用@Validated注解啟用校驗功能、使用具體校驗規則規范數據校驗格式


KF-2-5.數據類型轉換

有關spring屬性注入的問題到這里基本上就講完了,但是最近一名開發者向我咨詢了一個問題,我覺得需要給各位學習者分享一下。在學習階段其實我們遇到的問題往往復雜度比較低,單一性比較強,但是到了線上開發時,都是綜合性的問題,而這個開發者遇到的問題就是由于bean的屬性注入引發的災難。


先把問題描述一下,這位開發者連接數據庫正常操作,但是運行程序后顯示的信息是密碼錯誤。

java.sql.SQLException: Access denied for user 'root'@'localhost' (using password: YES)

?其實看到這個報錯,幾乎所有的學習者都能分辨出來,這是用戶名和密碼不匹配,就就是密碼輸入錯了,但是問題就在于密碼并沒有輸入錯誤,這就比較討厭了。給的報錯信息無法幫助你有效的分析問題,甚至會給你帶到溝里。如果是初學者,估計這會心態就崩了,我密碼沒錯啊,你怎么能說我有錯誤呢?來看看用戶名密碼的配置是如何寫的:

spring:datasource:driver-class-name: com.mysql.cj.jdbc.Driverurl: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTCusername: rootpassword: 0127

這名開發者的生日是1月27日,所以密碼就使用了0127,其實問題就出在這里了。


之前在基礎篇講屬性注入時,提到過類型相關的知識,在整數相關知識中有這么一句話,支持二進制,八進制,十六進制

image-20220222225748370

這個問題就處在這里了,因為0127在開發者眼中是一個字符串“0127”,但是在springboot看來,這就是一個數字,而且是一個八進制的數字。當后臺使用String類型接收數據時,如果配置文件中配置了一個整數值,他是先安裝整數進行處理,讀取后再轉換成字符串。巧了,0127撞上了八進制的格式,所以最終以十進制數字87的結果存在了。


這里提兩個注意點,第一,字符串標準書寫加上引號包裹,養成習慣,第二,遇到0開頭的數據多注意吧。


總結

1. yaml文件中對于數字的定義支持進制書寫格式,如需使用字符串請使用引號明確標注


KF-3.測試

說完bean配置相關的內容,下面要對前面講過的一個知識做加強了,測試。測試是保障程序正確性的唯一屏障,在企業級開發中更是不可缺少,但是由于測試代碼往往不產生實際效益,所以一些小型公司并不是很關注,導致一些開發者從小型公司進入中大型公司后,往往這一塊比較短板,所以還是要拿出來把這一塊知識好好說說,做一名專業的開發人員。


KF-3-1.加載測試專用屬性

測試過程本身并不是一個復雜的過程,但是很多情況下測試時需要模擬一些線上情況,或者模擬一些特殊情況。如果當前環境按照線上環境已經設定好了,例如是下面的配置

env:maxMemory: 32GBminMemory: 16GB

但是你現在想測試對應的兼容性,需要測試如下配置

env:maxMemory: 16GBminMemory: 8GB

這個時候我們能不能每次測試的時候都去修改源碼application.yml中的配置進行測試呢?顯然是不行的。每次測試前改過來,每次測試后改回去,這太麻煩了。于是我們就想,需要在測試環境中創建一組臨時屬性,去覆蓋我們源碼中設定的屬性,這樣測試用例就相當于是一個獨立的環境,能夠獨立測試,這樣就方便多了。


臨時屬性

springboot已經為我們開發者早就想好了這種問題該如何解決,并且提供了對應的功能入口。在測試用例程序中,可以通過對注解@SpringBootTest添加屬性來模擬臨時屬性,具體如下:

//properties屬性可以為當前測試用例添加臨時的屬性配置
@SpringBootTest(properties = {"test.prop=testValue1"})
public class PropertiesAndArgsTest {@Value("${test.prop}")private String msg;@Testvoid testProperties(){System.out.println(msg);}
}

使用注解@SpringBootTest的properties屬性就可以為當前測試用例添加臨時的屬性,覆蓋源碼配置文件中對應的屬性值進行測試。


臨時參數

除了上述這種情況,在前面講解使用命令行啟動springboot程序時講過,通過命令行參數也可以設置屬性值。而且線上啟動程序時,通常都會添加一些專用的配置信息。作為運維人員他們才不懂java,更不懂這些配置的信息具體格式該怎么寫,那如果我們作為開發者提供了對應的書寫內容后,能否提前測試一下這些配置信息是否有效呢?當時是可以的,還是通過注解@SpringBootTest的另一個屬性來進行設定。

//args屬性可以為當前測試用例添加臨時的命令行參數
@SpringBootTest(args={"--test.prop=testValue2"})
public class PropertiesAndArgsTest {@Value("${test.prop}")private String msg;@Testvoid testProperties(){System.out.println(msg);}
}

使用注解@SpringBootTest的args屬性就可以為當前測試用例模擬命令行參數并進行測試。


說到這里,好奇寶寶們肯定就有新問題了,如果兩者共存呢?其實如果思考一下配置屬性與命令行參數的加載優先級,這個結果就不言而喻了。在屬性加載的優先級設定中,有明確的優先級設定順序,還記得下面這個順序嗎?

image-20211206100859236

在這個屬性加載優先級的順序中,明確規定了命令行參數的優先級排序是11,而配置屬性的優先級是3,結果不言而喻了,args屬性配置優先于properties屬性配置加載。


到這里我們就掌握了如果在測試用例中去模擬臨時屬性的設定。


總結

1. 加載測試臨時屬性可以通過注解@SpringBootTest的properties和args屬性進行設定,此設定應用范圍僅適用于當前測試用例


思考

應用于測試環境的臨時屬性解決了,如果想在測試的時候臨時加載一些bean能不做呢?也就是說我測試時,想搞一些獨立的bean出來,專門應用于測試環境,能否實現呢?咱們下一節再講。


KF-3-2.加載測試專用配置

上一節提出了臨時配置一些專用于測試環境的bean的需求,這一節我們就來解決這個問題。


學習過Spring的知識,我們都知道,其實一個spring環境中可以設置若干個配置文件或配置類,若干個配置信息可以同時生效。現在我們的需求就是在測試環境中再添加一個配置類,然后啟動測試環境時,生效此配置就行了。其實做法和spring環境中加載多個配置信息的方式完全一樣。具體操作步驟如下:


步驟①

在測試包test中創建專用的測試環境配置類

@Configuration
public class MsgConfig {@Beanpublic String msg(){return "bean msg";}
}

上述配置僅用于演示當前實驗效果,實際開發可不能這么注入String類型的數據


步驟②

在啟動測試環境時,導入測試環境專用的配置類,使用@Import注解即可實現

@SpringBootTest
@Import({MsgConfig.class})
public class ConfigurationTest {@Autowiredprivate String msg;@Testvoid testConfiguration(){System.out.println(msg);}
}

到這里就通過@Import屬性實現了基于開發環境的配置基礎上,對配置進行測試環境的追加操作,實現了1+1的配置環境效果。這樣我們就可以實現每一個不同的測試用例加載不同的bean的效果,豐富測試用例的編寫,同時不影響開發環境的配置。


總結

1. 定義測試環境專用的配置類,然后通過@Import注解在具體的測試中導入臨時的配置,例如測試用例,方便測試過程,且上述配置不影響其他的測試類環境


思考

當前我們已經可以實現業務層和數據層的測試,并且通過臨時配置,控制每個測試用例加載不同的測試數據。但是實際企業開發不僅要保障業務層與數據層的功能安全有效,也要保障表現層的功能正常。但是我們目的對表現層的測試都是通過postman手工測試的,并沒有在打包過程中體現表現層功能被測試通過。能否在測試用例中對表現層進行功能測試呢?還真可以,咱們下一節再講。


KF-3-3.Web環境模擬測試

在測試中對表現層功能進行測試需要一個基礎和一個功能。所謂的一個基礎是運行測試程序時,必須啟動web環境,不然沒法測試web功能。一個功能是必須在測試程序中具備發送web請求的能力,不然無法實現web功能的測試。所以在測試用例中測試表現層接口這項工作就轉換成了兩件事,一,如何在測試類中啟動web測試,二,如何在測試類中發送web請求。下面一件事一件事進行,先說第一個


測試類中啟動web環境

每一個springboot的測試類上方都會標準@SpringBootTest注解,而注解帶有一個屬性,叫做webEnvironment。通過該屬性就可以設置在測試用例中啟動web環境,具體如下:

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)public class WebTest {	
}

測試類中啟動web環境時,可以指定啟動的Web環境對應的端口,springboot提供了4種設置值,分別如下:

image-20220223125453317

1、MOCK:根據當前設置確認是否啟動web環境,例如使用了Servlet的API就啟動web環境,屬于適配性的配置
2、DEFINED_PORT:使用自定義的端口作為web服務器端口
3、RANDOM_PORT:使用隨機端口作為web服務器端口
4、NONE:不啟動web環境


通過上述配置,現在啟動測試程序時就可以正常啟用web環境了,建議大家測試時使用RANDOM_PORT,避免代碼中因為寫死設定引發線上功能打包測試時由于端口沖突導致意外現象的出現。就是說你程序中寫了用8080端口,結果線上環境8080端口被占用了,結果你代碼中所有寫的東西都要改,這就是寫死代碼的代價。現在你用隨機端口就可以測試出來你有沒有這種問題的隱患了。


測試環境中的web環境已經搭建好了,下面就可以來解決第二個問題了,如何在程序代碼中發送web請求。


測試類中發送請求

對于測試類中發送請求,其實java的API就提供對應的功能,只不過平時各位小伙伴接觸的比較少,所以較為陌生。springboot為了便于開發者進行對應的功能開發,對其又進行了包裝,簡化了開發步驟,具體操作如下:


步驟①

在測試類中開啟web虛擬調用功能,通過注解@AutoConfigureMockMvc實現此功能的開啟

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
//開啟虛擬MVC調用
@AutoConfigureMockMvcpublic class WebTest {
}

步驟②

定義發起虛擬調用的對象MockMVC,通過自動裝配的形式初始化對象

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
//開啟虛擬MVC調用
@AutoConfigureMockMvc
public class WebTest {@Testvoid testWeb(@Autowired MockMvc mvc) {}
}

步驟③

創建一個虛擬請求對象,封裝請求的路徑,并使用MockMVC對象發送對應請求

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
//開啟虛擬MVC調用
@AutoConfigureMockMvc
public class WebTest {@Testvoid testWeb(@Autowired MockMvc mvc) throws Exception {//http://localhost:8080/books//創建虛擬請求,當前訪問/booksMockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");//執行對應的請求mvc.perform(builder);}
}

執行測試程序,現在就可以正常的發送/books對應的請求了,注意訪問路徑不要寫http://localhost:8080/books,因為前面的服務器IP地址和端口使用的是當前虛擬的web環境,無需指定,僅指定請求的具體路徑即可。


總結

1. 在測試類中測試web層接口要保障測試類啟動時啟動web容器,使用@SpringBootTest注解的webEnvironment屬性可以虛擬web環境用于測試
2. 為測試方法注入MockMvc對象,通過MockMvc對象可以發送虛擬請求,模擬web請求調用過程


思考

目前已經成功的發送了請求,但是還沒有起到測試的效果,測試過程必須出現預計值與真實值的比對結果才能確認測試結果是否通過,虛擬請求中能對哪些請求結果進行比對呢?咱們下一節再講。


web環境請求結果比對

上一節已經在測試用例中成功的模擬出了web環境,并成功的發送了web請求,本節就來解決發送請求后如何比對發送結果的問題。其實發完請求得到的信息只有一種,就是響應對象。至于響應對象中包含什么,就可以比對什么。常見的比對內容如下:


1、響應狀態匹配

@Test
void testStatus(@Autowired MockMvc mvc) throws Exception {MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");ResultActions action = mvc.perform(builder);//設定預期值 與真實值進行比較,成功測試通過,失敗測試失敗//定義本次調用的預期值StatusResultMatchers status = MockMvcResultMatchers.status();//預計本次調用時成功的:狀態200ResultMatcher ok = status.isOk();//添加預計值到本次調用過程中進行匹配action.andExpect(ok);
}

2、響應體匹配(非json數據格式)

@Test
void testBody(@Autowired MockMvc mvc) throws Exception {MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");ResultActions action = mvc.perform(builder);//設定預期值 與真實值進行比較,成功測試通過,失敗測試失敗//定義本次調用的預期值ContentResultMatchers content = MockMvcResultMatchers.content();ResultMatcher result = content.string("springboot2");//添加預計值到本次調用過程中進行匹配action.andExpect(result);
}

3、響應體匹配(json數據格式,開發中的主流使用方式)

@Test
void testJson(@Autowired MockMvc mvc) throws Exception {MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");ResultActions action = mvc.perform(builder);//設定預期值 與真實值進行比較,成功測試通過,失敗測試失敗//定義本次調用的預期值ContentResultMatchers content = MockMvcResultMatchers.content();ResultMatcher result = content.json("{\"id\":1,\"name\":\"springboot2\",\"type\":\"springboot\"}");//添加預計值到本次調用過程中進行匹配action.andExpect(result);
}

4、響應頭信息匹配

@Test
void testContentType(@Autowired MockMvc mvc) throws Exception {MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");ResultActions action = mvc.perform(builder);//設定預期值 與真實值進行比較,成功測試通過,失敗測試失敗//定義本次調用的預期值HeaderResultMatchers header = MockMvcResultMatchers.header();ResultMatcher contentType = header.string("Content-Type", "application/json");//添加預計值到本次調用過程中進行匹配action.andExpect(contentType);
}

基本上齊了,頭信息,正文信息,狀態信息都有了,就可以組合出一個完美的響應結果比對結果了。以下范例就是三種信息同時進行匹配校驗,也是一個完整的信息匹配過程。


@Test
void testGetById(@Autowired MockMvc mvc) throws Exception {MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");ResultActions action = mvc.perform(builder);StatusResultMatchers status = MockMvcResultMatchers.status();ResultMatcher ok = status.isOk();action.andExpect(ok);HeaderResultMatchers header = MockMvcResultMatchers.header();ResultMatcher contentType = header.string("Content-Type", "application/json");action.andExpect(contentType);ContentResultMatchers content = MockMvcResultMatchers.content();ResultMatcher result = content.json("{\"id\":1,\"name\":\"springboot\",\"type\":\"springboot\"}");action.andExpect(result);
}

總結

1. web虛擬調用可以對本地虛擬請求的返回響應信息進行比對,分為響應頭信息比對、響應體信息比對、響應狀態信息比對


KF-3-4.數據層測試回滾

當前我們的測試程序可以完美的進行表現層、業務層、數據層接口對應的功能測試了,但是測試用例開發完成后,在打包的階段由于test生命周期屬于必須被運行的生命周期,如果跳過會給系統帶來極高的安全隱患,所以測試用例必須執行。但是新的問題就呈現了,測試用例如果測試時產生了事務提交就會在測試過程中對數據庫數據產生影響,進而產生垃圾數據。這個過程不是我們希望發生的,作為開發者測試用例該運行運行,但是過程中產生的數據不要在我的系統中留痕,這樣該如何處理呢?


springboot早就為開發者想到了這個問題,并且針對此問題給出了最簡解決方案,在原始測試用例中添加注解@Transactional即可實現當前測試用例的事務不提交。當程序運行后,只要注解@Transactional出現的位置存在注解@SpringBootTest,springboot就會認為這是一個測試程序,無需提交事務,所以也就可以避免事務的提交。

@SpringBootTest
@Transactional
@Rollback(true)
public class DaoTest {@Autowiredprivate BookService bookService;@Testvoid testSave(){Book book = new Book();book.setName("springboot3");book.setType("springboot3");book.setDescription("springboot3");bookService.save(book);}
}

如果開發者想提交事務,也可以,再添加一個@RollBack的注解,設置回滾狀態為false即可正常提交事務,是不是很方便?springboot在輔助開發者日常工作這一塊展現出了驚人的能力,實在太貼心了。


總結

1. 在springboot的測試類中通過添加注解@Transactional來阻止測試用例提交事務
2. 通過注解@Rollback控制springboot測試類執行結果是否提交事務,需要配合注解@Transactional使用


思考

當前測試程序已經近乎完美了,但是由于測試用例中書寫的測試數據屬于固定數據,往往失去了測試的意義,開發者可以針對測試用例進行針對性開發,這樣就有可能出現測試用例不能完美呈現業務邏輯代碼是否真實有效的達成業務目標的現象,解決方案其實很容易想,測試用例的數據只要隨機產生就可以了,能實現嗎?咱們下一節再講。


KF-3-5.測試用例數據設定

對于測試用例的數據固定書寫肯定是不合理的,springboot提供了在配置中使用隨機值的機制,確保每次運行程序加載的數據都是隨機的。具體如下:

testcase:book:id: ${random.int}id2: ${random.int(10)}type: ${random.int!5,10!}name: ${random.value}uuid: ${random.uuid}publishTime: ${random.long}

當前配置就可以在每次運行程序時創建一組隨機數據,避免每次運行時數據都是固定值的尷尬現象發生,有助于測試功能的進行。數據的加載按照之前加載數據的形式,使用@ConfigurationProperties注解即可

@Component
@Data
@ConfigurationProperties(prefix = "testcase.book")
public class BookCase {private int id;private int id2;private int type;private String name;private String uuid;private long publishTime;
}

對于隨機值的產生,還有一些小的限定規則,比如產生的數值性數據可以設置范圍等,具體如下:
image-20220223135454862

1、 r a n d o m . i n t 表示隨機整數 2 、 {random.int}表示隨機整數 2、 random.int表示隨機整數2{random.int(10)}表示10以內的隨機數
3、${random.int(10,20)}表示10到20的隨機數
4、其中()可以是任意字符,例如[],!!均可


總結

1. 使用隨機數據可以替換測試用例中書寫的固定數據,提高測試用例中的測試數據有效性


KF-4.數據層解決方案

開發實用篇前三章基本上是開胃菜,從第四章開始,開發實用篇進入到了噩夢難度了,從這里開始,不再是單純的在springboot內部搞事情了,要涉及到很多相關知識。本章節主要內容都是和數據存儲與讀取相關,前期學習的知識與數據層有關的技術基本上都圍繞在數據庫這個層面上,所以本章要講的第一個大的分支就是SQL解決方案相關的內容,除此之外,數據的來源還可以是非SQL技術相關的數據操作,因此第二部分圍繞著NOSQL解決方案講解。至于什么是NOSQL解決方案,講到了再說吧。下面就從SQL解決方案說起。


KF-4-1.SQL

回憶一下之前做SSMP整合的時候數據層解決方案涉及到了哪些技術?MySQL數據庫與MyBatisPlus框架,后面又學了Druid數據源的配置,所以現在數據層解決方案可以說是Mysql+Druid+MyBatisPlus。而三個技術分別對應了數據層操作的三個層面:
1、數據源技術:Druid
2、持久化技術:MyBatisPlus
3、數據庫技術:MySQL

下面的研究就分為三個層面進行研究,對應上面列出的三個方面,咱們就從第一個數據源技術開始說起。


數據源技術

目前我們使用的數據源技術是Druid,運行時可以在日志中看到對應的數據源初始化信息,具體如下:

INFO 28600 --- [           main] c.a.d.s.b.a.DruidDataSourceAutoConfigure : Init DruidDataSource
INFO 28600 --- [           main] com.alibaba.druid.pool.DruidDataSource   : {dataSource-1} inited

如果不使用Druid數據源,程序運行后是什么樣子呢?是獨立的數據庫連接對象還是有其他的連接池技術支持呢?將Druid技術對應的starter去掉再次運行程序可以在日志中找到如下初始化信息:

INFO 31820 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
INFO 31820 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.

雖然沒有DruidDataSource相關的信息了,但是我們發現日志中有HikariDataSource這個信息,就算不懂這是個什么技術,看名字也能看出來,以DataSource結尾的名稱,這一定是一個數據源技術。我們又沒有手工添加這個技術,這個技術哪里來的呢?這就是這一節要講的知識,springboot內嵌數據源。


數據層技術是每一個企業級應用程序都會用到的,而其中必定會進行數據庫連接的管理。springboot根據開發者的習慣出發,開發者提供了數據源技術,就用你提供的,開發者沒有提供,那總不能手工管理一個一個的數據庫連接對象啊,怎么辦?我給你一個默認的就好了,這樣省心又省事,大家都方便。


springboot提供了3款內嵌數據源技術,分別如下:
1、HikariCP
2、Tomcat提供DataSource
3、Commons DBCP


第一種,HikartCP,這是springboot官方推薦的數據源技術,作為默認內置數據源使用。啥意思?你不配置數據源,那就用這個。


第二種,Tomcat提供的DataSource,如果不想用HikartCP,并且使用tomcat作為web服務器進行web程序的開發,使用這個。為什么是Tomcat,不是其他web服務器呢?因為web技術導入starter后,默認使用內嵌tomcat,既然都是默認使用的技術了,那就一用到底,數據源也用它的。有人就提出怎么才能不使用HikartCP用tomcat提供的默認數據源對象呢?把HikartCP技術的坐標排除掉就OK了。


第三種,DBCP,這個使用的條件就更苛刻了,既不使用HikartCP也不使用tomcat的DataSource時,默認給你用這個。


springboot這心操的,也是稀碎啊,就怕你自己管不好連接對象,給你一頓推薦,真是開發界的最強輔助。既然都給你奶上了,那就受用吧,怎么配置使用這些東西呢?之前我們配置druid時使用druid的starter對應的配置如下:

spring:datasource:druid:	url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTCdriver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: root

換成是默認的數據源HikariCP后,直接吧druid刪掉就行了,如下:

spring:datasource:url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTCdriver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: root

當然,也可以寫上是對hikari做的配置,但是url地址要單獨配置,如下:

spring:datasource:url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTChikari:driver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: root

這就是配置hikari數據源的方式。如果想對hikari做進一步的配置,可以繼續配置其獨立的屬性。例如:

spring:datasource:url: jdbc:mysql://localhost:3306/ssm_db?serverTimezone=UTChikari:driver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: rootmaximum-pool-size: 50

如果不想使用hikari數據源,使用tomcat的數據源或者DBCP配置格式也是一樣的。學習到這里,以后我們做數據層時,數據源對象的選擇就不再是單一的使用druid數據源技術了,可以根據需要自行選擇。


總結

1. springboot技術提供了3種內置的數據源技術,分別是Hikari、tomcat內置數據源、DBCP


持久化技術

說完數據源解決方案,再來說一下持久化解決方案。springboot充分發揮其最強輔助的特征,給開發者提供了一套現成的數據層技術,叫做JdbcTemplate。其實這個技術不能說是springboot提供的,因為不使用springboot技術,一樣能使用它,誰提供的呢?spring技術提供的,所以在springboot技術范疇中,這個技術也是存在的,畢竟springboot技術是加速spring程序開發而創建的。


這個技術其實就是回歸到jdbc最原始的編程形式來進行數據層的開發,下面直接上操作步驟:


步驟①

導入jdbc對應的坐標,記得是starter

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-jdbc</artifactId>
</dependency

步驟②

自動裝配JdbcTemplate對象

@SpringBootTest
class Springboot15SqlApplicationTests {@Testvoid testJdbcTemplate(@Autowired JdbcTemplate jdbcTemplate){}
}

步驟③

使用JdbcTemplate實現查詢操作(非實體類封裝數據的查詢操作)

@Test
void testJdbcTemplate(@Autowired JdbcTemplate jdbcTemplate){String sql = "select * from tbl_book";List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);System.out.println(maps);
}

步驟④

使用JdbcTemplate實現查詢操作(實體類封裝數據的查詢操作)

@Test
void testJdbcTemplate(@Autowired JdbcTemplate jdbcTemplate){String sql = "select * from tbl_book";RowMapper<Book> rm = new RowMapper<Book>() {@Overridepublic Book mapRow(ResultSet rs, int rowNum) throws SQLException {Book temp = new Book();temp.setId(rs.getInt("id"));temp.setName(rs.getString("name"));temp.setType(rs.getString("type"));temp.setDescription(rs.getString("description"));return temp;}};List<Book> list = jdbcTemplate.query(sql, rm);System.out.println(list);
}

步驟⑤

使用JdbcTemplate實現增刪改操作

@Test
void testJdbcTemplateSave(@Autowired JdbcTemplate jdbcTemplate){String sql = "insert into tbl_book > values(3,'springboot1','springboot2','springboot3')";jdbcTemplate.update(sql);
}

如果想對JdbcTemplate對象進行相關配置,可以在yml文件中進行設定,具體如下:

spring:jdbc:template:query-timeout: -1   # 查詢超時時間max-rows: 500       # 最大行數fetch-size: -1      # 緩存行數

總結

1. SpringBoot內置JdbcTemplate持久化解決方案
2. 使用JdbcTemplate需要導入spring-boot-starter-jdbc的坐標


數據庫技術

截止到目前,springboot給開發者提供了內置的數據源解決方案和持久化解決方案,在數據層解決方案三件套中還剩下一個數據庫,莫非springboot也提供有內置的解決方案?還真有,還不是一個,三個,這一節就來說說內置的數據庫解決方案。


springboot提供了3款內置的數據庫,分別是H2、HSQL和Derby
以上三款數據庫除了可以獨立安裝之外,還可以像是tomcat服務器一樣,采用內嵌的形式運行在spirngboot容器中。內嵌在容器中運行,那必須是java對象啊,對,這三款數據庫底層都是使用java語言開發的。


我們一直使用MySQL數據庫就挺好的,為什么有需求用這個呢?原因就在于這三個數據庫都可以采用內嵌容器的形式運行,在應用程序運行后,如果我們進行測試工作,此時測試的數據無需存儲在磁盤上,但是又要測試使用,內嵌數據庫就方便了,運行在內存中,該測試測試,該運行運行,等服務器關閉后,一切煙消云散,多好,省得你維護外部數據庫了。這也是內嵌數據庫的最大優點,方便進行功能測試。


下面以H2數據庫為例講解如何使用這些內嵌數據庫,操作步驟也非常簡單,簡單才好用嘛


步驟①

導入H2數據庫對應的坐標,一共2個

<dependency><groupId>com.h2database</groupId><artifactId>h2</artifactId>
</dependency>
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>

步驟②

將工程設置為web工程,啟動工程時啟動H2數據庫

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId>
</dependency>

步驟③

通過配置開啟H2數據庫控制臺訪問程序,也可以使用其他的數據庫連接軟件操作

spring:h2:console:enabled: truepath: /h2

web端訪問路徑/h2,訪問密碼123456,如果訪問失敗,先配置下列數據源,啟動程序運行后再次訪問/h2路徑就可以正常訪問了

datasource:url: jdbc:h2:~/testhikari:driver-class-name: org.h2.Driverusername: sapassword: 123456

步驟④

使用JdbcTemplate或MyBatisPlus技術操作數據庫
(略)


其實我們只是換了一個數據庫而已,其他的東西都不受影響。一個重要提醒,別忘了,上線時,把內存級數據庫關閉,采用MySQL數據庫作為數據持久化方案,關閉方式就是設置enabled屬性為false即可。


總結

1. H2內嵌式數據庫啟動方式,添加坐標,添加配置
2. H2數據庫線上運行時請務必關閉


到這里SQL相關的數據層解決方案就講完了,現在的可選技術就豐富的多了。
1、數據源技術:Druid、Hikari、tomcat DataSource、DBCP
2、持久化技術:MyBatisPlus、MyBatis、JdbcTemplate
3、數據庫技術:MySQL、H2、HSQL、Derby

現在開發程序時就可以在以上技術中任選一種組織成一套數據庫解決方案了。


KF-4-2.NoSQL

SQL數據層解決方案說完了,下面來說收NoSQL數據層解決方案。這個NoSQL是什么意思呢?從字面來看,No表示否定,NoSQL就是非關系型數據庫解決方案,意思就是數據該存存該取取,只是這些數據不放在關系型數據庫中了,那放在哪里?自然是一些能夠存儲數據的其他相關技術中了,比如Redis等。本節講解的內容就是springboot如何整合這些技術,在springboot官方文檔中提供了10種相關技術的整合方案,我們將講解國內市場上最流行的幾款NoSQL數據庫整合方案,分別是Redis、MongoDB、ES。


因為每個小伙伴學習這門課程的時候起點不同,為了便于各位學習者更好的學習,每種技術在講解整合前都會先講一下安裝和基本使用,然后再講整合。如果對某個技術比較熟悉的小伙伴可以直接跳過安裝的學習過程,直接看整合方案即可。此外上述這些技術最佳使用方案都是在Linux服務器上部署,但是考慮到各位小伙伴的學習起點差異過大,所以下面的課程都是以Windows平臺作為安裝基礎講解,如果想看Linux版軟件安裝,可以再找到對應技術的學習文檔查閱學習。


SpringBoot整合Redis

Redis是一款采用key-value數據存儲格式的內存級NoSQL數據庫,重點關注數據存儲格式,是key-value格式,也就是鍵值對的存儲形式。與MySQL數據庫不同,MySQL數據庫有表、有字段、有記錄,Redis沒有這些東西,就是一個名稱對應一個值,并且數據以存儲在內存中使用為主。什么叫以存儲在內存中為主?其實Redis有它的數據持久化方案,分別是RDB和AOF,但是Redis自身并不是為了數據持久化而生的,主要是在內存中保存數據,加速數據訪問的,所以說是一款內存級數據庫。


Redis支持多種數據存儲格式,比如可以直接存字符串,也可以存一個map集合,list集合,后面會涉及到一些不同格式的數據操作,這個需要先學習一下才能進行整合,所以在基本操作中會介紹一些相關操作。下面就先安裝,再操作,最后說整合


安裝

windows版安裝包下載地址:https://github.com/tporadowski/redis/releases


下載的安裝包有兩種形式,一種是一鍵安裝的msi文件,還有一種是解壓縮就能使用的zip文件,哪種形式都行,這里就不介紹安裝過程了,本課程采用的是msi一鍵安裝的msi文件進行安裝的。


啥是msi,其實就是一個文件安裝包,不僅安裝軟件,還幫你把安裝軟件時需要的功能關聯在一起,打包操作。比如如安裝序列、創建和設置安裝路徑、設置系統依賴項、默認設定安裝選項和控制安裝過程的屬性。說簡單點就是一站式服務,安裝過程一條龍操作一氣呵成,就是為小白用戶提供的軟件安裝程序。


安裝完畢后會得到如下文件,其中有兩個文件對應兩個命令,是啟動Redis的核心命令,需要再CMD命令行模式執行。

image-20220224091709173


啟動服務器

redis-server.exe redis.windows.conf

初學者無需調整服務器對外服務端口,默認6379。


啟動客戶端

redis-cli.exe

如果啟動redis服務器失敗,可以先啟動客戶端,然后執行shutdown操作后退出,此時redis服務器就可以正常執行了。


基本操作

服務器啟動后,使用客戶端就可以連接服務器,類似于啟動完MySQL數據庫,然后啟動SQL命令行操作數據庫。


放置一個字符串數據到redis中,先為數據定義一個名稱,比如name,age等,然后使用命令set設置數據到redis服務器中即可

set name itheima
set age 12

從redis中取出已經放入的數據,根據名稱取,就可以得到對應數據。如果沒有對應數據就會得到(nil)

get name
get age

以上使用的數據存儲是一個名稱對應一個值,如果要維護的數據過多,可以使用別的數據存儲結構。例如hash,它是一種一個名稱下可以存儲多個數據的存儲模型,并且每個數據也可以有自己的二級存儲名稱。向hash結構中存儲數據格式如下:

hset a a1 aa1		#對外key名稱是a,在名稱為a的存儲模型中,a1這個key中保存了數據aa1
hset a a2 aa2

獲取hash結構中的數據命令如下

hget a a1			#得到aa1
hget a a2			#得到aa2

有關redis的基礎操作就普及到這里,需要全面掌握redis技術,請參看相關教程學習。


整合

在進行整合之前先梳理一下整合的思想,springboot整合任何技術其實就是在springboot中使用對應技術的API。如果兩個技術沒有交集,就不存在整合的概念了。所謂整合其實就是使用springboot技術去管理其他技術,幾個問題是躲不掉的。
第一,需要先導入對應技術的坐標,而整合之后,這些坐標都有了一些變化
第二,任何技術通常都會有一些相關的設置信息,整合之后,這些信息如何寫,寫在哪是一個問題
第三,沒有整合之前操作如果是模式A的話,整合之后如果沒有給開發者帶來一些便捷操作,那整合將毫無意義,所以整合后操作肯定要簡化一些,那對應的操作方式自然也有所不同
按照上面的三個問題去思考springboot整合所有技術是一種通用思想,在整合的過程中會逐步摸索出整合的套路,而且適用性非常強,經過若干種技術的整合后基本上可以總結出一套固定思維。
下面就開始springboot整合redis,操作步驟如下:


步驟①

導入springboot整合redis的starter坐標

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

上述坐標可以在創建模塊的時候通過勾選的形式進行選擇,歸屬NoSQL分類中

image-20220224101142220


步驟②

進行基礎配置

spring:redis:host: localhostport: 6379

操作redis,最基本的信息就是操作哪一臺redis服務器,所以服務器地址屬于基礎配置信息,不可缺少。但是即便你不配置,目前也是可以用的。因為以上兩組信息都有默認配置,剛好就是上述配置值。


步驟③

使用springboot整合redis的專用客戶端接口操作,此處使用的是RedisTemplate

@SpringBootTest
class Springboot16RedisApplicationTests {@Autowiredprivate RedisTemplate redisTemplate;@Testvoid set() {ValueOperations ops = redisTemplate.opsForValue();ops.set("age",41);}@Testvoid get() {ValueOperations ops = redisTemplate.opsForValue();Object age = ops.get("name");System.out.println(age);}@Testvoid hset() {HashOperations ops = redisTemplate.opsForHash();ops.put("info","b","bb");}@Testvoid hget() {HashOperations ops = redisTemplate.opsForHash();Object val = ops.get("info", "b");System.out.println(val);}
}

在操作redis時,需要先確認操作何種數據,根據數據種類得到操作接口。例如使用opsForValue()獲取string類型的數據操作接口,使用opsForHash()獲取hash類型的數據操作接口,剩下的就是調用對應api操作了。各種類型的數據操作接口如下:

image-20220224103104908


總結

springboot整合redis步驟
1.導入springboot整合redis的starter坐標
2.進行基礎配置
3.使用springboot整合redis的專用客戶端接口RedisTemplate操作


StringRedisTemplate

由于redis內部不提供java對象的存儲格式,因此當操作的數據以對象的形式存在時,會進行轉碼,轉換成字符串格式后進行操作。為了方便開發者使用基于字符串為數據的操作,springboot整合redis時提供了專用的API接口StringRedisTemplate,你可以理解為這是RedisTemplate的一種指定數據泛型的操作API。

@SpringBootTest
public class StringRedisTemplateTest {@Autowiredprivate StringRedisTemplate stringRedisTemplate;@Testvoid get(){ValueOperations<String, String> ops = stringRedisTemplate.opsForValue();String name = ops.get("name");System.out.println(name);}
}

redis客戶端選擇

springboot整合redis技術提供了多種客戶端兼容模式,默認提供的是lettucs客戶端技術,也可以根據需要切換成指定客戶端技術,例如jedis客戶端技術,切換成jedis客戶端技術操作步驟如下:


步驟①

導入jedis坐標

<dependency><groupId>redis.clients</groupId><artifactId>jedis</artifactId>
</dependency>

jedis坐標受springboot管理,無需提供版本號


步驟②

配置客戶端技術類型,設置為jedis

spring:redis:host: localhostport: 6379client-type: jedis

步驟③

根據需要設置對應的配置

spring:redis:host: localhostport: 6379client-type: jedislettuce:pool:max-active: 16jedis:pool:max-active: 16

lettcus與jedis區別

1、jedis連接Redis服務器是直連模式,當多線程模式下使用jedis會存在線程安全問題,解決方案可以通過配置連接池使每個連接專用,這樣整體性能就大受影響
2、lettcus基于Netty框架進行與Redis服務器連接,底層設計中采用StatefulRedisConnection。 StatefulRedisConnection自身是線程安全的,可以保障并發訪問安全問題,所以一個連接可以被多線程復用。當然lettcus也支持多連接實例一起工作


總結

1. springboot整合redis提供了StringRedisTemplate對象,以字符串的數據格式操作redis
2. 如果需要切換redis客戶端實現技術,可以通過配置的形式進行


SpringBoot整合MongoDB

使用Redis技術可以有效的提高數據訪問速度,但是由于Redis的數據格式單一性,無法操作結構化數據,當操作對象型的數據時,Redis就顯得捉襟見肘。在保障訪問速度的情況下,如果想操作結構化數據,看來Redis無法滿足要求了,此時需要使用全新的數據存儲結束來解決此問題,本節講解springboot如何整合MongoDB技術。


MongoDB是一個開源、高性能、無模式的文檔型數據庫,它是NoSQL數據庫產品中的一種,是最像關系型數據庫的非關系型數據庫。


上述描述中幾個詞,其中對于我們最陌生的詞是無模式的。什么叫無模式呢?簡單說就是作為一款數據庫,沒有固定的數據存儲結構,第一條數據可能有A、B、C一共3個字段,第二條數據可能有D、E、F也是3個字段,第三條數據可能是A、C、E3個字段,也就是說數據的結構不固定,這就是無模式。有人會說這有什么用啊?靈活,隨時變更,不受約束。基于上述特點,MongoDB的應用面也會產生一些變化。以下列出了一些可以使用MongoDB作為數據存儲的場景,但是并不是必須使用MongoDB的場景:


1、淘寶用戶數據
1.1、存儲位置:數據庫
1.2、特征:永久性存儲,修改頻度極低
2、游戲裝備數據、游戲道具數據
2.1、存儲位置:數據庫、Mongodb
2.2、特征:永久性存儲與臨時存儲相結合、修改頻度較高
3、直播數據、打賞數據、粉絲數據
3.1、存儲位置:數據庫、Mongodb
3.2、特征:永久性存儲與臨時存儲相結合,修改頻度極高
4、物聯網數據
4.1、存儲位置:Mongodb
4.2、特征:臨時存儲,修改頻度飛速


快速了解一下MongoDB,下面直接開始我們的學習,老規矩,先安裝,再操作,最后說整合


安裝

windows版安裝包下載地址:https://www.mongodb.com/try/download


下載的安裝包也有兩種形式,一種是一鍵安裝的msi文件,還有一種是解壓縮就能使用的zip文件,哪種形式都行,本課程采用解壓縮zip文件進行安裝。


解壓縮完畢后會得到如下文件,其中bin目錄包含了所有mongodb的可執行命令


image-20220224111306933


mongodb在運行時需要指定一個數據存儲的目錄,所以創建一個數據存儲目錄,通常放置在安裝目錄中,此處創建data的目錄用來存儲數據,具體如下


image-20220224111053408


如果在安裝的過程中出現了如下警告信息,就是告訴你,你當前的操作系統缺少了一些系統文件,這個不用擔心。


image-20220224113956882


根據下列方案即可解決,在瀏覽器中搜索提示缺少的名稱對應的文件,并下載,將下載的文件拷貝到windows安裝目錄的system32目錄下,然后在命令行中執行regsvr32命令注冊此文件。根據下載的文件名不同,執行命令前更改對應名稱。

regsvr32 vcruntime140_1.dll

啟動服務器

mongod --dbpath=..\data\db

啟動服務器時需要指定數據存儲位置,通過參數–dbpath進行設置,可以根據需要自行設置數據存儲路徑。默認服務端口27017。


啟動客戶端

mongo --host=127.0.0.1 --port=27017

基本操作

MongoDB雖然是一款數據庫,但是它的操作并不是使用SQL語句進行的,因此操作方式各位小伙伴可能比較陌生,好在有一些類似于Navicat的數據庫客戶端軟件,能夠便捷的操作MongoDB,先安裝一個客戶端,再來操作MongoDB。


同類型的軟件較多,本次安裝的軟件時Robo3t,Robot3t是一款綠色軟件,無需安裝,解壓縮即可。解壓縮完畢后進入安裝目錄雙擊robot3t.exe即可使用。


image-20220224114911573


打開軟件首先要連接MongoDB服務器,選擇【File】菜單,選擇【Connect…】


image-20220224115202422


進入連接管理界面后,選擇左上角的【Create】鏈接,創建新的連接設置


image-20220224115254200


如果輸入設置值即可連接(默認不修改即可連接本機27017端口)


image-20220224115300266
連接成功后在命令輸入區域輸入命令即可操作MongoDB。
創建數據庫:在左側菜單中使用右鍵創建,輸入數據庫名稱即可
創建集合:在Collections上使用右鍵創建,輸入集合名稱即可,集合等同于數據庫中的表的作用
新增文檔:(文檔是一種類似json格式的數據,初學者可以先把數據理解為就是json數據)

db.集合名稱.insert/save/insertOne(文檔)

刪除文檔:

db.集合名稱.remove(條件)

修改文檔:

db.集合名稱.update(條件,{操作種類:{文檔}})

查詢文檔:

基礎查詢
查詢全部:		   db.集合.find();
查第一條:		   db.集合.findOne()
查詢指定數量文檔:	db.集合.find().limit(10)					//查10條文檔
跳過指定數量文檔:	db.集合.find().skip(20)					//跳過20條文檔
統計:			  	db.集合.count()
排序:				db.集合.sort({age:1})						//按age升序排序
投影:				db.集合名稱.find(條件,{name:1,age:1})		 //僅保留name與age域條件查詢
基本格式:			db.集合.find({條件})
模糊查詢:			db.集合.find({域名:/正則表達式/})		  //等同SQL中的like,比like強大,可以執行正則所有規則
條件比較運算:		   db.集合.find({域名:{$gt:值}})				//等同SQL中的數值比較操作,例如:name>18
包含查詢:			db.集合.find({域名:{$in:[值1,值2]}})		//等同于SQL中的in
條件連接查詢:		   db.集合.find({$and:[{條件1},{條件2}]})	   //等同于SQL中的and、or

有關MongoDB的基礎操作就普及到這里,需要全面掌握MongoDB技術,請參看相關教程學習。


整合

使用springboot整合MongDB該如何進行呢?其實springboot為什么使用的開發者這么多,就是因為他的套路幾乎完全一樣。導入坐標,做配置,使用API接口操作。整合Redis如此,整合MongoDB同樣如此。
第一,先導入對應技術的整合starter坐標
第二,配置必要信息
第三,使用提供的API操作即可
下面就開始springboot整合MongoDB,操作步驟如下:


步驟①

導入springboot整合MongoDB的starter坐標

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>

上述坐標也可以在創建模塊的時候通過勾選的形式進行選擇,同樣歸屬NoSQL分類中

image-20220224120721626


步驟②

進行基礎配置

spring:data:mongodb:uri: mongodb://localhost/itheima

操作MongoDB需要的配置與操作redis一樣,最基本的信息都是操作哪一臺服務器,區別就是連接的服務器IP地址和端口不同,書寫格式不同而已。


步驟③

使用springboot整合MongoDB的專用客戶端接口MongoTemplate來進行操作

@SpringBootTest
class Springboot17MongodbApplicationTests {@Autowiredprivate MongoTemplate mongoTemplate;@Testvoid contextLoads() {Book book = new Book();book.setId(2);book.setName("springboot2");book.setType("springboot2");book.setDescription("springboot2");mongoTemplate.save(book);}@Testvoid find(){List<Book> all = mongoTemplate.findAll(Book.class);System.out.println(all);}
}

整合工作到這里就做完了,感覺既熟悉也陌生。熟悉的是這個套路,三板斧,就這三招,導坐標做配置用API操作,陌生的是這個技術,里面具體的操作API可能會不熟悉,有關springboot整合MongoDB我們就講到這里。有興趣可以繼續學習MongoDB的操作,然后再來這里通過編程的形式操作MongoDB。


總結

springboot整合MongoDB步驟
1.導入springboot整合MongoDB的starter坐標
2.進行基礎配置
3.使用springboot整合MongoDB的專用客戶端接口MongoTemplate操作


SpringBoot整合ES

NoSQL解決方案已經講完了兩種技術的整合了,Redis可以使用內存加載數據并實現數據快速訪問,MongoDB可以在內存中存儲類似對象的數據并實現數據的快速訪問,在企業級開發中對于速度的追求是永無止境的。下面要講的內容也是一款NoSQL解決方案,只不過他的作用不是為了直接加速數據的讀寫,而是加速數據的查詢的,叫做ES技術。


ES(Elasticsearch)是一個分布式全文搜索引擎,重點是全文搜索。


那什么是全文搜索呢?比如用戶要買一本書,以Java為關鍵字進行搜索,不管是書名中還是書的介紹中,甚至是書的作者名字,只要包含java就作為查詢結果返回給用戶查看,上述過程就使用了全文搜索技術。搜索的條件不再是僅用于對某一個字段進行比對,而是在一條數據中使用搜索條件去比對更多的字段,只要能匹配上就列入查詢結果,這就是全文搜索的目的。而ES技術就是一種可以實現上述效果的技術。


要實現全文搜索的效果,不可能使用數據庫中like操作去進行比對,這種效率太低了。ES設計了一種全新的思想,來實現全文搜索。具體操作過程如下:

1. 將被查詢的字段的數據全部文本信息進行查分,分成若干個詞
例如“中華人民共和國”就會被拆分成三個詞,分別是“中華”、“人民”、“共和國”,此過程有專業術語叫做分詞。分詞的策略不同,分出的效果不一樣,不同的分詞策略稱為分詞器。


2. 將分詞得到的結果存儲起來,對應每條數據的id
2.1、例如id為1的數據中名稱這一項的值是“中華人民共和國”,那么分詞結束后,就會出現“中華”對應id為1,“人民”對應id為1,“共和國”對應id為1
2.2、例如id為2的數據中名稱這一項的值是“人民代表大會“,那么分詞結束后,就會出現“人民”對應id為2,“代表”對應id為2,“大會”對應id為2
2.3、此時就會出現如下對應結果,按照上述形式可以對所有文檔進行分詞。需要注意分詞的過程不是僅對一個字段進行,而是對每一個參與查詢的字段都執行,最終結果匯總到一個表格中

分詞結果關鍵字對應id
中華1
人民1,2
共和國1
代表2
大會2

3. 當進行查詢時,如果輸入“人民”作為查詢條件,可以通過上述表格數據進行比對,得到id值1,2,然后根據id值就可以得到查詢的結果數據了。


上述過程中分詞結果關鍵字內容每一個都不相同,作用有點類似于數據庫中的索引,是用來加速數據查詢的。但是數據庫中的索引是對某一個字段進行添加索引,而這里的分詞結果關鍵字不是一個完整的字段值,只是一個字段中的其中的一部分內容。并且索引使用時是根據索引內容查找整條數據,全文搜索中的分詞結果關鍵字查詢后得到的并不是整條的數據,而是數據的id,要想獲得具體數據還要再次查詢,因此這里為這種分詞結果關鍵字起了一個全新的名稱,叫做倒排索引


通過上述內容的學習,發現使用ES其實準備工作還是挺多的,必須先建立文檔的倒排索引,然后才能繼續使用。快速了解一下ES的工作原理,下面直接開始我們的學習,老規矩,先安裝,再操作,最后說整合。


安裝

1、windows版安裝包下載地址
2、www.elastic.co/cn/downloads/elasticsearch

下載的安裝包是解壓縮就能使用的zip文件,解壓縮完畢后會得到如下文件

image-20220225132756400

1、bin目錄:包含所有的可執行命令
2、config目錄:包含ES服務器使用的配置文件
3、jdk目錄:此目錄中包含了一個完整的jdk工具包,版本17,當ES升級時,使用最新版本的jdk確保不會出現版本支持性不足的問題
3、lib目錄:包含ES運行的依賴jar文件
4、logs目錄:包含ES運行后產生的所有日志文件
5、modules目錄:包含ES軟件中所有的功能模塊,也是一個一個的jar包。和jar目錄不同,jar目錄是ES運行期間依賴的jar包,modules是ES軟件自己的功能jar包
6、plugins目錄:包含ES軟件安裝的插件,默認為空


啟動服務器

elasticsearch.bat

雙擊elasticsearch.bat文件即可啟動ES服務器,默認服務端口9200。通過瀏覽器訪問http://localhost:9200看到如下信息視為ES服務器正常啟動

{"name" : "CZBK-**********","cluster_name" : "elasticsearch","cluster_uuid" : "j137DSswTPG8U4Yb-0T1Mg","version" : {"number" : "7.16.2","build_flavor" : "default","build_type" : "zip","build_hash" : "2b937c44140b6559905130a8650c64dbd0879cfb","build_date" : "2021-12-18T19:42:46.604893745Z","build_snapshot" : false,"lucene_version" : "8.10.1","minimum_wire_compatibility_version" : "6.8.0","minimum_index_compatibility_version" : "6.0.0-beta1"},"tagline" : "You Know, for Search"
}

基本操作

ES中保存有我們要查詢的數據,只不過格式和數據庫存儲數據格式不同而已。在ES中我們要先創建倒排索引,這個索引的功能又點類似于數據庫的表,然后將數據添加到倒排索引中,添加的數據稱為文檔。所以要進行ES的操作要先創建索引,再添加文檔,這樣才能進行后續的查詢操作。


要操作ES可以通過Rest風格的請求來進行,也就是說發送一個請求就可以執行一個操作。比如新建索引,刪除索引這些操作都可以使用發送請求的形式來進行。


1、創建索引,books是索引名稱,下同

PUT請求		http://localhost:9200/books

發送請求后,看到如下信息即索引創建成功

{"acknowledged": true,"shards_acknowledged": true,"index": "books"
}

重復創建已經存在的索引會出現錯誤信息,reason屬性中描述錯誤原因

{"error": {"root_cause": [{"type": "resource_already_exists_exception","reason": "index [books/VgC_XMVAQmedaiBNSgO2-w] already exists","index_uuid": "VgC_XMVAQmedaiBNSgO2-w","index": "books"}],"type": "resource_already_exists_exception","reason": "index [books/VgC_XMVAQmedaiBNSgO2-w] already exists",	# books索引已經存在"index_uuid": "VgC_XMVAQmedaiBNSgO2-w","index": "book"},"status": 400
}

2、查詢索引

GET請求		http://localhost:9200/books

查詢索引得到索引相關信息,如下

{"book": {"aliases": {},"mappings": {},"settings": {"index": {"routing": {"allocation": {"include": {"_tier_preference": "data_content"}}},"number_of_shards": "1","provided_name": "books","creation_date": "1645768584849","number_of_replicas": "1","uuid": "VgC_XMVAQmedaiBNSgO2-w","version": {"created": "7160299"}}}}
}

如果查詢了不存在的索引,會返回錯誤信息,例如查詢名稱為book的索引后信息如下

{"error": {"root_cause": [{"type": "index_not_found_exception","reason": "no such index [book]","resource.type": "index_or_alias","resource.id": "book","index_uuid": "_na_","index": "book"}],"type": "index_not_found_exception","reason": "no such index [book]",		# 沒有book索引"resource.type": "index_or_alias","resource.id": "book","index_uuid": "_na_","index": "book"},"status": 404
}

3、刪除索引

DELETE請求	http://localhost:9200/books

刪除所有后,給出刪除結果

{"acknowledged": true
}

如果重復刪除,會給出錯誤信息,同樣在reason屬性中描述具體的錯誤原因

{"error": {"root_cause": [{"type": "index_not_found_exception","reason": "no such index [books]","resource.type": "index_or_alias","resource.id": "book","index_uuid": "_na_","index": "book"}],"type": "index_not_found_exception","reason": "no such index [books]",		# 沒有books索引"resource.type": "index_or_alias","resource.id": "book","index_uuid": "_na_","index": "book"},"status": 404
}

4、創建索引并指定分詞器

前面創建的索引是未指定分詞器的,可以在創建索引時添加請求參數,設置分詞器。目前國內較為流行的分詞器是IK分詞器,使用前先在下對應的分詞器,然后使用。IK分詞器下載地址:https://github.com/medcl/elasticsearch-analysis-ik/releases


分詞器下載后解壓到ES安裝目錄的plugins目錄中即可,安裝分詞器后需要重新啟動ES服務器。使用IK分詞器創建索引格式:

PUT請求		http://localhost:9200/books請求參數如下(注意是json格式的參數)
{"mappings":{							#定義mappings屬性,替換創建索引時對應的mappings屬性		"properties":{						#定義索引中包含的屬性設置"id":{							#設置索引中包含id屬性"type":"keyword"			#當前屬性可以被直接搜索},"name":{						#設置索引中包含name屬性"type":"text",              #當前屬性是文本信息,參與分詞  "analyzer":"ik_max_word",   #使用IK分詞器進行分詞             "copy_to":"all"				#分詞結果拷貝到all屬性中},"type":{"type":"keyword"},"description":{"type":"text",	                "analyzer":"ik_max_word",                "copy_to":"all"},"all":{							#定義屬性,用來描述多個字段的分詞結果集合,當前屬性可以參與查詢"type":"text",	                "analyzer":"ik_max_word"}}}
}

創建完畢后返回結果和不使用分詞器創建索引的結果是一樣的,此時可以通過查看索引信息觀察到添加的請求參數mappings已經進入到了索引屬性中

{"books": {"aliases": {},"mappings": {						#mappings屬性已經被替換"properties": {"all": {"type": "text","analyzer": "ik_max_word"},"description": {"type": "text","copy_to": ["all"],"analyzer": "ik_max_word"},"id": {"type": "keyword"},"name": {"type": "text","copy_to": ["all"],"analyzer": "ik_max_word"},"type": {"type": "keyword"}}},"settings": {"index": {"routing": {"allocation": {"include": {"_tier_preference": "data_content"}}},"number_of_shards": "1","provided_name": "books","creation_date": "1645769809521","number_of_replicas": "1","uuid": "DohYKvr_SZO4KRGmbZYmTQ","version": {"created": "7160299"}}}}
}

目前我們已經有了索引了,但是索引中還沒有數據,所以要先添加數據,ES中稱數據為文檔,下面進行文檔操作。


5、添加文檔,有三種方式

POST請求	http://localhost:9200/books/_doc		#使用系統生成id
POST請求	http://localhost:9200/books/_create/1	#使用指定id
POST請求	http://localhost:9200/books/_doc/1		#使用指定id,不存在創建,存在更新(版本遞增)文檔通過請求參數傳遞,數據格式json
{"name":"springboot","type":"springboot","description":"springboot"
}  

6、查詢文檔

GET請求	http://localhost:9200/books/_doc/1		 #查詢單個文檔 		
GET請求	http://localhost:9200/books/_search		 #查詢全部文檔

7、條件查詢

GET請求	http://localhost:9200/books/_search?q=name:springboot	# q=查詢屬性名:查詢屬性值

8、刪除文檔

DELETE請求	http://localhost:9200/books/_doc/1

9、修改文檔(全量更新)

PUT請求	http://localhost:9200/books/_doc/1文檔通過請求參數傳遞,數據格式json
{"name":"springboot","type":"springboot","description":"springboot"
}

10、修改文檔(部分更新)

POST請求	http://localhost:9200/books/_update/1文檔通過請求參數傳遞,數據格式json
{			"doc":{						#部分更新并不是對原始文檔進行更新,而是對原始文檔對象中的doc屬性中的指定屬性更新"name":"springboot"		#僅更新提供的屬性值,未提供的屬性值不參與更新操作}
}

整合

使用springboot整合ES該如何進行呢?老規矩,導入坐標,做配置,使用API接口操作。整合Redis如此,整合MongoDB如此,整合ES依然如此。太沒有新意了,其實不是沒有新意,這就是springboot的強大之處,所有東西都做成相同規則,對開發者來說非常友好。


下面就開始springboot整合ES,操作步驟如下:


步驟①

導入springboot整合ES的starter坐標

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-elasticsearch</artifactId>
</dependency>

步驟②

進行基礎配置

spring:elasticsearch:rest:uris: http://localhost:9200

配置ES服務器地址,端口9200


步驟③

使用springboot整合ES的專用客戶端接口ElasticsearchRestTemplate來進行操作

@SpringBootTest
class Springboot18EsApplicationTests {@Autowiredprivate ElasticsearchRestTemplate template;
}

上述操作形式是ES早期的操作方式,使用的客戶端被稱為Low Level Client,這種客戶端操作方式性能方面略顯不足,于是ES開發了全新的客戶端操作方式,稱為High Level Client。高級別客戶端與ES版本同步更新,但是springboot最初整合ES的時候使用的是低級別客戶端,所以企業開發需要更換成高級別的客戶端模式。


下面使用高級別客戶端方式進行springboot整合ES,操作步驟如下:


步驟①

導入springboot整合ES高級別客戶端的坐標,此種形式目前沒有對應的starter

<dependency><groupId>org.elasticsearch.client</groupId><artifactId>elasticsearch-rest-high-level-client</artifactId>
</dependency>

步驟②

使用編程的形式設置連接的ES服務器,并獲取客戶端對象

@SpringBootTest
class Springboot18EsApplicationTests {private RestHighLevelClient client;@Testvoid testCreateClient() throws IOException {HttpHost host = HttpHost.create("http://localhost:9200");RestClientBuilder builder = RestClient.builder(host);client = new RestHighLevelClient(builder);client.close();}
}

配置ES服務器地址與端口9200,記得客戶端使用完畢需要手工關閉。由于當前客戶端是手工維護的,因此不能通過自動裝配的形式加載對象。


步驟③

使用客戶端對象操作ES,例如創建索引

@SpringBootTest
class Springboot18EsApplicationTests {private RestHighLevelClient client;@Testvoid testCreateIndex() throws IOException {HttpHost host = HttpHost.create("http://localhost:9200");RestClientBuilder builder = RestClient.builder(host);client = new RestHighLevelClient(builder);CreateIndexRequest request = new CreateIndexRequest("books");client.indices().create(request, RequestOptions.DEFAULT); client.close();}
}

高級別客戶端操作是通過發送請求的方式完成所有操作的,ES針對各種不同的操作,設定了各式各樣的請求對象,上例中創建索引的對象是CreateIndexRequest,其他操作也會有自己專用的Request對象。


當前操作我們發現,無論進行ES何種操作,第一步永遠是獲取RestHighLevelClient對象,最后一步永遠是關閉該對象的連接。在測試中可以使用測試類的特性去幫助開發者一次性的完成上述操作,但是在業務書寫時,還需要自行管理。將上述代碼格式轉換成使用測試類的初始化方法和銷毀方法進行客戶端對象的維護。

@SpringBootTest
class Springboot18EsApplicationTests {@BeforeEach		//在測試類中每個操作運行前運行的方法void setUp() {HttpHost host = HttpHost.create("http://localhost:9200");RestClientBuilder builder = RestClient.builder(host);client = new RestHighLevelClient(builder);}@AfterEach		//在測試類中每個操作運行后運行的方法void tearDown() throws IOException {client.close();}private RestHighLevelClient client;@Testvoid testCreateIndex() throws IOException {CreateIndexRequest request = new CreateIndexRequest("books");client.indices().create(request, RequestOptions.DEFAULT);}
}

現在的書寫簡化了很多,也更合理。下面使用上述模式將所有的ES操作執行一遍,測試結果


創建索引(IK分詞器)

@Test
void testCreateIndexByIK() throws IOException {CreateIndexRequest request = new CreateIndexRequest("books");String json = "{\n" +"    \"mappings\":{\n" +"        \"properties\":{\n" +"            \"id\":{\n" +"                \"type\":\"keyword\"\n" +"            },\n" +"            \"name\":{\n" +"                \"type\":\"text\",\n" +"                \"analyzer\":\"ik_max_word\",\n" +"                \"copy_to\":\"all\"\n" +"            },\n" +"            \"type\":{\n" +"                \"type\":\"keyword\"\n" +"            },\n" +"            \"description\":{\n" +"                \"type\":\"text\",\n" +"                \"analyzer\":\"ik_max_word\",\n" +"                \"copy_to\":\"all\"\n" +"            },\n" +"            \"all\":{\n" +"                \"type\":\"text\",\n" +"                \"analyzer\":\"ik_max_word\"\n" +"            }\n" +"        }\n" +"    }\n" +"}";//設置請求中的參數request.source(json, XContentType.JSON);client.indices().create(request, RequestOptions.DEFAULT);
}

IK分詞器是通過請求參數的形式進行設置的,設置請求參數使用request對象中的source方法進行設置,至于參數是什么,取決于你的操作種類。當請求中需要參數時,均可使用當前形式進行參數設置。


添加文檔

@Test
//添加文檔
void testCreateDoc() throws IOException {Book book = bookDao.selectById(1);IndexRequest request = new IndexRequest("books").id(book.getId().toString());String json = JSON.toJSONString(book);request.source(json,XContentType.JSON);client.index(request,RequestOptions.DEFAULT);
}

**添加文檔使用的請求對象是IndexRequest,與創建索引使用的請求對象不同。 **


批量添加文檔

@Test
//批量添加文檔
void testCreateDocAll() throws IOException {List<Book> bookList = bookDao.selectList(null);BulkRequest bulk = new BulkRequest();for (Book book : bookList) {IndexRequest request = new IndexRequest("books").id(book.getId().toString());String json = JSON.toJSONString(book);request.source(json,XContentType.JSON);bulk.add(request);}client.bulk(bulk,RequestOptions.DEFAULT);
}

批量做時,先創建一個BulkRequest的對象,可以將該對象理解為是一個保存request對象的容器,將所有的請求都初始化好后,添加到BulkRequest對象中,再使用BulkRequest對象的bulk方法,一次性執行完畢。


按id查詢文檔

@Test
//按id查詢
void testGet() throws IOException {GetRequest request = new GetRequest("books","1");GetResponse response = client.get(request, RequestOptions.DEFAULT);String json = response.getSourceAsString();System.out.println(json);
}

根據id查詢文檔使用的請求對象是GetRequest。


按條件查詢文檔

@Test
//按條件查詢
void testSearch() throws IOException {SearchRequest request = new SearchRequest("books");SearchSourceBuilder builder = new SearchSourceBuilder();builder.query(QueryBuilders.termQuery("all","spring"));request.source(builder);SearchResponse response = client.search(request, RequestOptions.DEFAULT);SearchHits hits = response.getHits();for (SearchHit hit : hits) {String source = hit.getSourceAsString();//System.out.println(source);Book book = JSON.parseObject(source, Book.class);System.out.println(book);}
}

按條件查詢文檔使用的請求對象是SearchRequest,查詢時調用SearchRequest對象的termQuery方法,需要給出查詢屬性名,此處支持使用合并字段,也就是前面定義索引屬性時添加的all屬性。


springboot整合ES的操作到這里就說完了,與前期進行springboot整合redis和mongodb的差別還是蠻大的,主要原始就是我們沒有使用springboot整合ES的客戶端對象。至于操作,由于ES操作種類過多,所以顯得操作略微有點復雜。有關springboot整合ES就先學習到這里吧。


總結

springboot整合ES步驟
1.導入springboot整合ES的High Level Client坐標
2.手工管理客戶端對象,包括初始化和關閉操作
3.使用High Level Client根據操作的種類不同,選擇不同的Request對象完成對應操作


KF-5.整合第三方技術

通過第四章的學習,我們領略到了springboot在整合第三方技術時強大的一致性,在第五章中我們要使用springboot繼續整合各種各樣的第三方技術,通過本章的學習,可以將之前學習的springboot整合第三方技術的思想貫徹到底,還是那三板斧。導坐標、做配置、調API。


springboot能夠整合的技術實在是太多了,可以說是萬物皆可整。本章將從企業級開發中常用的一些技術作為出發點,對各種各樣的技術進行整合。


KF-5-1.緩存

企業級應用主要作用是信息處理,當需要讀取數據時,由于受限于數據庫的訪問效率,導致整體系統性能偏低。

image-20220226154148303

應用程序直接與數據庫打交道,訪問效率低
為了改善上述現象,開發者通常會在應用程序與數據庫之間建立一種臨時的數據存儲機制,該區域中的數據在內存中保存,讀寫速度較快,可以有效解決數據庫訪問效率低下的問題。這一塊臨時存儲數據的區域就是緩存。

image-20220226154233010

使用緩存后,應用程序與緩存打交道,緩存與數據庫打交道,數據訪問效率提高


緩存是什么?緩存是一種介于數據永久存儲介質與應用程序之間的數據臨時存儲介質,使用緩存可以有效的減少低速數據讀取過程的次數(例如磁盤IO),提高系統性能。此外緩存不僅可以用于提高永久性存儲介質的數據讀取效率,還可以提供臨時的數據存儲空間。而springboot提供了對市面上幾乎所有的緩存技術進行整合的方案,下面就一起開啟springboot整合緩存之旅。


SpringBoot內置緩存解決方案

springboot技術提供有內置的緩存解決方案,可以幫助開發者快速開啟緩存技術,并使用緩存技術進行數據的快速操作,例如讀取緩存數據和寫入數據到緩存。


步驟①

導入springboot提供的緩存技術對應的starter

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId>
</dependency>

步驟②

啟用緩存,在引導類上方標注注解@EnableCaching配置springboot程序中可以使用緩存

@SpringBootApplication
//開啟緩存功能
@EnableCaching
public class Springboot19CacheApplication {public static void main(String[] args) {SpringApplication.run(Springboot19CacheApplication.class, args);}
}

步驟③

設置操作的數據是否使用緩存

@Service
public class BookServiceImpl implements BookService {@Autowiredprivate BookDao bookDao;@Cacheable(value="cacheSpace",key="#id")public Book getById(Integer id) {return bookDao.selectById(id);}
}

在業務方法上面使用注解@Cacheable聲明當前方法的返回值放入緩存中,其中要指定緩存的存儲位置,以及緩存中保存當前方法返回值對應的名稱。上例中value屬性描述緩存的存儲位置,可以理解為是一個存儲空間名,key屬性描述了緩存中保存數據的名稱,使用#id讀取形參中的id值作為緩存名稱。


使用@Cacheable注解后,執行當前操作,如果發現對應名稱在緩存中沒有數據,就正常讀取數據,然后放入緩存;如果對應名稱在緩存中有數據,就終止當前業務方法執行,直接返回緩存中的數據。


手機驗證碼案例

為了便于下面演示各種各樣的緩存技術,我們創建一個手機驗證碼的案例環境,模擬使用緩存保存手機驗證碼的過程。


手機驗證碼案例需求如下:
1. 輸入手機號獲取驗證碼,組織文檔以短信形式發送給用戶(頁面模擬)
2. 輸入手機號和驗證碼驗證結果


為了描述上述操作,我們制作兩個表現層接口,一個用來模擬發送短信的過程,其實就是根據用戶提供的手機號生成一個驗證碼,然后放入緩存,另一個用來模擬驗證碼校驗的過程,其實就是使用傳入的手機號和驗證碼進行匹配,并返回最終匹配結果。下面直接制作本案例的模擬代碼,先以上例中springboot提供的內置緩存技術來完成當前案例的制作。


步驟①

導入springboot提供的緩存技術對應的starter

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-cache</artifactId>
</dependency>

步驟②

啟用緩存,在引導類上方標注注解@EnableCaching配置springboot程序中可以使用緩存

@SpringBootApplication
//開啟緩存功能
@EnableCaching
public class Springboot19CacheApplication {public static void main(String[] args) {SpringApplication.run(Springboot19CacheApplication.class, args);}
}

步驟③

定義驗證碼對應的實體類,封裝手機號與驗證碼兩個屬性

@Data
public class SMSCode {private String tele;private String code;
}

步驟④

定義驗證碼功能的業務層接口與實現類

public interface SMSCodeService {public String sendCodeToSMS(String tele);public boolean checkCode(SMSCode smsCode);
}@Service
public class SMSCodeServiceImpl implements SMSCodeService {@Autowiredprivate CodeUtils codeUtils;@CachePut(value = "smsCode", key = "#tele")public String sendCodeToSMS(String tele) {String code = codeUtils.generator(tele);return code;}public boolean checkCode(SMSCode smsCode) {//取出內存中的驗證碼與傳遞過來的驗證碼比對,如果相同,返回trueString code = smsCode.getCode();String cacheCode = codeUtils.get(smsCode.getTele());return code.equals(cacheCode);}
}

獲取驗證碼后,當驗證碼失效時必須重新獲取驗證碼,因此在獲取驗證碼的功能上不能使用@Cacheable注解,@Cacheable注解是緩存中沒有值則放入值,緩存中有值則取值。此處的功能僅僅是生成驗證碼并放入緩存,并不具有從緩存中取值的功能,因此不能使用@Cacheable注解,應該使用僅具有向緩存中保存數據的功能,使用@CachePut注解即可。


對于校驗驗證碼的功能建議放入工具類中進行。


步驟⑤

定義驗證碼的生成策略與根據手機號讀取驗證碼的功能

@Component
public class CodeUtils {private String [] patch = {"000000","00000","0000","000","00","0",""};public String generator(String tele){int hash = tele.hashCode();int encryption = 20206666;long result = hash ^ encryption;long nowTime = System.currentTimeMillis();result = result ^ nowTime;long code = result % 1000000;code = code < 0 ? -code : code;String codeStr = code + "";int len = codeStr.length();return patch[len] + codeStr;}@Cacheable(value = "smsCode",key="#tele")public String get(String tele){return null;}
}

步驟⑥

定義驗證碼功能的web層接口,一個方法用于提供手機號獲取驗證碼,一個方法用于提供手機號和驗證碼進行校驗

@RestController
@RequestMapping("/sms")
public class SMSCodeController {@Autowiredprivate SMSCodeService smsCodeService;@GetMappingpublic String getCode(String tele){String code = smsCodeService.sendCodeToSMS(tele);return code;}@PostMappingpublic boolean checkCode(SMSCode smsCode){return smsCodeService.checkCode(smsCode);}
}

SpringBoot整合Ehcache緩存

手機驗證碼的案例已經完成了,下面就開始springboot整合各種各樣的緩存技術,第一個整合Ehcache技術。Ehcache是一種緩存技術,使用springboot整合Ehcache其實就是變更一下緩存技術的實現方式,話不多說,直接開整


步驟①

導入Ehcache的坐標

<dependency><groupId>net.sf.ehcache</groupId><artifactId>ehcache</artifactId>
</dependency>

此處為什么不是導入Ehcache的starter,而是導入技術坐標呢?其實springboot整合緩存技術做的是通用格式,不管你整合哪種緩存技術,只是實現變化了,操作方式一樣。這也體現出springboot技術的優點,統一同類技術的整合方式。


步驟②

配置緩存技術實現使用Ehcache

spring:cache:type: ehcacheehcache:config: ehcache.xml

配置緩存的類型type為ehcache,此處需要說明一下,當前springboot可以整合的緩存技術中包含有ehcach,所以可以這樣書寫。其實這個type不可以隨便寫的,不是隨便寫一個名稱就可以整合的。配置緩存的類型type為ehcache,此處需要說明一下,當前springboot可以整合的緩存技術中包含有ehcach,所以可以這樣書寫。其實這個type不可以隨便寫的,不是隨便寫一個名稱就可以整合的。


由于ehcache的配置有獨立的配置文件格式,因此還需要指定ehcache的配置文件,以便于讀取相應配置

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd"updateCheck="false"><diskStore path="D:\ehcache" /><!--默認緩存策略 --><!-- external:是否永久存在,設置為true則不會被清除,此時與timeout沖突,通常設置為false--><!-- diskPersistent:是否啟用磁盤持久化--><!-- maxElementsInMemory:最大緩存數量--><!-- overflowToDisk:超過最大緩存數量是否持久化到磁盤--><!-- timeToIdleSeconds:最大不活動間隔,設置過長緩存容易溢出,設置過短無效果,可用于記錄時效性數據,例如驗證碼--><!-- timeToLiveSeconds:最大存活時間--><!-- memoryStoreEvictionPolicy:緩存清除策略--><defaultCacheeternal="false"diskPersistent="false"maxElementsInMemory="1000"overflowToDisk="false"timeToIdleSeconds="60"timeToLiveSeconds="60"memoryStoreEvictionPolicy="LRU" /><cachename="smsCode"eternal="false"diskPersistent="false"maxElementsInMemory="1000"overflowToDisk="false"timeToIdleSeconds="10"timeToLiveSeconds="10"memoryStoreEvictionPolicy="LRU" />
</ehcache>

注意前面的案例中,設置了數據保存的位置是smsCode

@CachePut(value = "smsCode", key = "#tele")
public String sendCodeToSMS(String tele) {String code = codeUtils.generator(tele);return code;
}	

這個設定需要保障ehcache中有一個緩存空間名稱叫做smsCode的配置,前后要統一。在企業開發過程中,通過設置不同名稱的cache來設定不同的緩存策略,應用于不同的緩存數據。


到這里springboot整合Ehcache就做完了,可以發現一點,原始代碼沒有任何修改,僅僅是加了一組配置就可以變更緩存供應商了,這也是springboot提供了統一的緩存操作接口的優勢,變更實現并不影響原始代碼的書寫。


總結

1. springboot使用Ehcache作為緩存實現需要導入Ehcache的坐標
2. 修改設置,配置緩存供應商為ehcache,并提供對應的緩存配置文件


SpringBoot整合Redis緩存

上節使用Ehcache替換了springboot內置的緩存技術,其實springboot支持的緩存技術還很多,下面使用redis技術作為緩存解決方案來實現手機驗證碼案例。


比對使用Ehcache的過程,加坐標,改緩存實現類型為ehcache,做Ehcache的配置。如果還成redis做緩存呢?一模一樣,加坐標,改緩存實現類型為redis,做redis的配置。差別之處只有一點,redis的配置可以在yml文件中直接進行配置,無需制作獨立的配置文件。


步驟①

導入redis的坐標

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

步驟②

配置緩存技術實現使用redis

spring:redis:host: localhostport: 6379cache:type: redis

如果需要對redis作為緩存進行配置,注意不是對原始的redis進行配置,而是配置redis作為緩存使用相關的配置,隸屬于spring.cache.redis節點下,注意不要寫錯位置了。

spring:redis:host: localhostport: 6379cache:type: redisredis:use-key-prefix: falsekey-prefix: sms_cache-null-values: falsetime-to-live: 10s

總結

1. springboot使用redis作為緩存實現需要導入redis的坐標
2. 修改設置,配置緩存供應商為redis,并提供對應的緩存配置


SpringBoot整合Memcached緩存

目前我們已經掌握了3種緩存解決方案的配置形式,分別是springboot內置緩存,ehcache和redis,本節研究一下國內比較流行的一款緩存memcached。


按照之前的套路,其實變更緩存并不繁瑣,但是springboot并沒有支持使用memcached作為其緩存解決方案,也就是說在type屬性中沒有memcached的配置選項,這里就需要更變一下處理方式了。在整合之前先安裝memcached。


安裝

windows版安裝包下載地址
下載的安裝包是解壓縮就能使用的zip文件,解壓縮完畢后會得到如下文件

image-20220226174957040

可執行文件只有一個memcached.exe,使用該文件可以將memcached作為系統服務啟動,執行此文件時會出現報錯信息,如下:

image-20220226175141986

此處出現問題的原因是注冊系統服務時需要使用管理員權限,當前賬號權限不足導致安裝服務失敗,切換管理員賬號權限啟動命令行

image-20220226175302903

然后再次執行安裝服務的命令即可,如下:

memcached.exe -d install

服務安裝完畢后可以使用命令啟動和停止服務,如下:

memcached.exe -d start		# 啟動服務
memcached.exe -d stop		# 停止服務

也可以在任務管理器中進行服務狀態的切換

image-20220226175441675


變更緩存為Memcached

由于memcached未被springboot收錄為緩存解決方案,因此使用memcached需要通過手工硬編碼的方式來使用,于是前面的套路都不適用了,需要自己寫了。


memcached目前提供有三種客戶端技術,分別是Memcached Client for Java、SpyMemcached和Xmemcached,其中性能指標各方面最好的客戶端是Xmemcached,本次整合就使用這個作為客戶端實現技術了。下面開始使用Xmemcached


步驟①

導入xmemcached的坐標

<dependency><groupId>com.googlecode.xmemcached</groupId><artifactId>xmemcached</artifactId><version>2.4.7</version>
</dependency>

步驟②

配置memcached,制作memcached的配置類

@Configuration
public class XMemcachedConfig {@Beanpublic MemcachedClient getMemcachedClient() throws IOException {MemcachedClientBuilder memcachedClientBuilder = new XMemcachedClientBuilder("localhost:11211");MemcachedClient memcachedClient = memcachedClientBuilder.build();return memcachedClient;}
}

memcached默認對外服務端口11211。


步驟③

使用xmemcached客戶端操作緩存,注入MemcachedClient對象

@Service
public class SMSCodeServiceImpl implements SMSCodeService {@Autowiredprivate CodeUtils codeUtils;@Autowiredprivate MemcachedClient memcachedClient;public String sendCodeToSMS(String tele) {String code = codeUtils.generator(tele);try {memcachedClient.set(tele,10,code);} catch (Exception e) {e.printStackTrace();}return code;}public boolean checkCode(SMSCode smsCode) {String code = null;try {code = memcachedClient.get(smsCode.getTele()).toString();} catch (Exception e) {e.printStackTrace();}return smsCode.getCode().equals(code);}
}

設置值到緩存中使用set操作,取值使用get操作,其實更符合我們開發者的習慣。


上述代碼中對于服務器的配置使用硬編碼寫死到了代碼中,將此數據提取出來,做成獨立的配置屬性。


定義配置屬性

以下過程采用前期學習的屬性配置方式進行,當前操作有助于理解原理篇中的很多知識。


1、定義配置類,加載必要的配置屬性,讀取配置文件中memcached節點信息

@Component
@ConfigurationProperties(prefix = "memcached")
@Data
public class XMemcachedProperties {private String servers;private int poolSize;private long opTimeout;
}

2、定義memcached節點信息

memcached:servers: localhost:11211poolSize: 10opTimeout: 3000

3、在memcached配置類中加載信息

@Configuration
public class XMemcachedConfig {@Autowiredprivate XMemcachedProperties props;@Beanpublic MemcachedClient getMemcachedClient() throws IOException {MemcachedClientBuilder memcachedClientBuilder = new XMemcachedClientBuilder(props.getServers());memcachedClientBuilder.setConnectionPoolSize(props.getPoolSize());memcachedClientBuilder.setOpTimeout(props.getOpTimeout());MemcachedClient memcachedClient = memcachedClientBuilder.build();return memcachedClient;}
}

總結

1. memcached安裝后需要啟動對應服務才可以對外提供緩存功能,安裝memcached服務需要基于windows系統管理員權限
2. 由于springboot沒有提供對memcached的緩存整合方案,需要采用手工編碼的形式創建xmemcached客戶端操作緩存
3. 導入xmemcached坐標后,創建memcached配置類,注冊MemcachedClient對應的bean,用于操作緩存
4. 初始化MemcachedClient對象所需要使用的屬性可以通過自定義配置屬性類的形式加載


思考

到這里已經完成了三種緩存的整合,其中redis和mongodb需要安裝獨立的服務器,連接時需要輸入對應的服務器地址,這種是遠程緩存,Ehcache是一個典型的內存級緩存,因為它什么也不用安裝,啟動后導入jar包就有緩存功能了。這個時候就要問了,能不能這兩種緩存一起用呢?咱們下節再說。


SpringBoot整合jetcache緩存

目前我們使用的緩存都是要么A要么B,能不能AB一起用呢?這一節就解決這個問題。springboot針對緩存的整合僅僅停留在用緩存上面,如果緩存自身不支持同時支持AB一起用,springboot也沒辦法,所以要想解決AB緩存一起用的問題,就必須找一款緩存能夠支持AB兩種緩存一起用,有這種緩存嗎?還真有,阿里出品,jetcache。


jetcache嚴格意義上來說,并不是一個緩存解決方案,只能說他算是一個緩存框架,然后把別的緩存放到jetcache中管理,這樣就可以支持AB緩存一起用了。并且jetcache參考了springboot整合緩存的思想,整體技術使用方式和springboot的緩存解決方案思想非常類似。下面咱們就先把jetcache用起來,然后再說它里面的一些小的功能。


做之前要先明確一下,jetcache并不是隨便拿兩個緩存都能拼到一起去的。目前jetcache支持的緩存方案本地緩存支持兩種,遠程緩存支持兩種,分別如下:
1、本地緩存(Local)
1.1、LinkedHashMap
1.2、Caffeine
2、遠程緩存(Remote)
2.1、Redis
2.2、Tair


其實也有人問我,為什么jetcache只支持2+2這么4款緩存呢?阿里研發這個技術其實主要是為了滿足自身的使用需要。最初肯定只有1+1種,逐步變化成2+2種。下面就以LinkedHashMap+Redis的方案實現本地與遠程緩存方案同時使用。


純遠程方案

步驟①

導入springboot整合jetcache對應的坐標starter,當前坐標默認使用的遠程方案是redis

<dependency><groupId>com.alicp.jetcache</groupId><artifactId>jetcache-starter-redis</artifactId><version>2.6.2</version>
</dependency>

步驟②

遠程方案基本配置

jetcache:remote:default:type: redishost: localhostport: 6379poolConfig:maxTotal: 50

其中poolConfig是必配項,否則會報錯


步驟③

啟用緩存,在引導類上方標注注解@EnableCreateCacheAnnotation配置springboot程序中可以使用注解的形式創建緩存

@SpringBootApplication
//jetcache啟用緩存的主開關
@EnableCreateCacheAnnotation
public class Springboot20JetCacheApplication {public static void main(String[] args) {SpringApplication.run(Springboot20JetCacheApplication.class, args);}
}

步驟④

創建緩存對象Cache,并使用注解@CreateCache標記當前緩存的信息,然后使用Cache對象的API操作緩存,put寫緩存,get讀緩存。

@Service
public class SMSCodeServiceImpl implements SMSCodeService {@Autowiredprivate CodeUtils codeUtils;@CreateCache(name="jetCache_",expire = 10,timeUnit = TimeUnit.SECONDS)private Cache<String ,String> jetCache;public String sendCodeToSMS(String tele) {String code = codeUtils.generator(tele);jetCache.put(tele,code);return code;}public boolean checkCode(SMSCode smsCode) {String code = jetCache.get(smsCode.getTele());return smsCode.getCode().equals(code);}
}

通過上述jetcache使用遠程方案連接redis可以看出,jetcache操作緩存時的接口操作更符合開發者習慣,使用緩存就先獲取緩存對象Cache,放數據進去就是put,取數據出來就是get,更加簡單易懂。并且jetcache操作緩存時,可以為某個緩存對象設置過期時間,將同類型的數據放入緩存中,方便有效周期的管理。


上述方案中使用的是配置中定義的default緩存,其實這個default是個名字,可以隨便寫,也可以隨便加。例如再添加一種緩存解決方案,參照如下配置進行:

jetcache:remote:default:type: redishost: localhostport: 6379poolConfig:maxTotal: 50sms:type: redishost: localhostport: 6379poolConfig:maxTotal: 50

如果想使用名稱是sms的緩存,需要再創建緩存時指定參數area,聲明使用對應緩存即可

@Service
public class SMSCodeServiceImpl implements SMSCodeService {@Autowiredprivate CodeUtils codeUtils;@CreateCache(area="sms",name="jetCache_",expire = 10,timeUnit = TimeUnit.SECONDS)private Cache<String ,String> jetCache;public String sendCodeToSMS(String tele) {String code = codeUtils.generator(tele);jetCache.put(tele,code);return code;}public boolean checkCode(SMSCode smsCode) {String code = jetCache.get(smsCode.getTele());return smsCode.getCode().equals(code);}
}

純本地方案

遠程方案中,配置中使用remote表示遠程,換成local就是本地,只不過類型不一樣而已。


步驟①

導入springboot整合jetcache對應的坐標starter

<dependency><groupId>com.alicp.jetcache</groupId><artifactId>jetcache-starter-redis</artifactId><version>2.6.2</version>
</dependency>

步驟②

本地緩存基本配置

jetcache:local:default:type: linkedhashmapkeyConvertor: fastjson

為了加速數據獲取時key的匹配速度,jetcache要求指定key的類型轉換器。簡單說就是,如果你給了一個Object作為key的話,我先用key的類型轉換器給轉換成字符串,然后再保存。等到獲取數據時,仍然是先使用給定的Object轉換成字符串,然后根據字符串匹配。由于jetcache是阿里的技術,這里推薦key的類型轉換器使用阿里的fastjson。


步驟③

啟用緩存

@SpringBootApplication
//jetcache啟用緩存的主開關
@EnableCreateCacheAnnotation
public class Springboot20JetCacheApplication {public static void main(String[] args) {SpringApplication.run(Springboot20JetCacheApplication.class, args);}
}

步驟④

創建緩存對象Cache時,標注當前使用本地緩存

@Service
public class SMSCodeServiceImpl implements SMSCodeService {@CreateCache(name="jetCache_",expire = 1000,timeUnit = TimeUnit.SECONDS,cacheType = CacheType.LOCAL)private Cache<String ,String> jetCache;public String sendCodeToSMS(String tele) {String code = codeUtils.generator(tele);jetCache.put(tele,code);return code;}public boolean checkCode(SMSCode smsCode) {String code = jetCache.get(smsCode.getTele());return smsCode.getCode().equals(code);}
}

cacheType控制當前緩存使用本地緩存還是遠程緩存,配置cacheType=CacheType.LOCAL即使用本地緩存。


本地+遠程方案

本地和遠程方法都有了,兩種方案一起使用如何配置呢?其實就是將兩種配置合并到一起就可以了。

jetcache:local:default:type: linkedhashmapkeyConvertor: fastjsonremote:default:type: redishost: localhostport: 6379poolConfig:maxTotal: 50sms:type: redishost: localhostport: 6379poolConfig:maxTotal: 50

在創建緩存的時候,配置cacheType為BOTH即則本地緩存與遠程緩存同時使用。

@Service
public class SMSCodeServiceImpl implements SMSCodeService {@CreateCache(name="jetCache_",expire = 1000,timeUnit = TimeUnit.SECONDS,cacheType = CacheType.BOTH)private Cache<String ,String> jetCache;
}

cacheType如果不進行配置,默認值是REMOTE,即僅使用遠程緩存方案。關于jetcache的配置,參考以下信息


屬性默認值說明
jetcache.statIntervalMinutes0統計間隔,0表示不統計
jetcache.hiddenPackages自動生成name時,隱藏指定的包名前綴
jetcache.[local|remote].${area}.type緩存類型,本地支持linkedhashmap、caffeine,遠程支持redis、tair
jetcache.[local|remote].${area}.keyConvertorkey轉換器,當前僅支持fastjson
jetcache.[local|remote].${area}.valueEncoderjava僅remote類型的緩存需要指定,可選java和kryo
jetcache.[local|remote].${area}.valueDecoderjava僅remote類型的緩存需要指定,可選java和kryo
jetcache.[local|remote].${area}.limit100僅local類型的緩存需要指定,緩存實例最大元素數
jetcache.[local|remote].${area}.expireAfterWriteInMillis無窮大默認過期時間,毫秒單位
jetcache.local.${area}.expireAfterAccessInMillis0僅local類型的緩存有效,毫秒單位,最大不活動間隔

以上方案僅支持手工控制緩存,但是springcache方案中的方法緩存特別好用,給一個方法添加一個注解,方法就會自動使用緩存。jetcache也提供了對應的功能,即方法緩存。


方法緩存

jetcache提供了方法緩存方案,只不過名稱變更了而已。在對應的操作接口上方使用注解@Cached即可


步驟①

導入springboot整合jetcache對應的坐標starter

<dependency><groupId>com.alicp.jetcache</groupId><artifactId>jetcache-starter-redis</artifactId><version>2.6.2</version>
</dependency>

步驟②

配置緩存

jetcache:local:default:type: linkedhashmapkeyConvertor: fastjsonremote:default:type: redishost: localhostport: 6379keyConvertor: fastjsonvalueEncode: javavalueDecode: javapoolConfig:maxTotal: 50sms:type: redishost: localhostport: 6379poolConfig:maxTotal: 50

由于redis緩存中不支持保存對象,因此需要對redis設置當Object類型數據進入到redis中時如何進行類型轉換。需要配置keyConvertor表示key的類型轉換方式,同時標注value的轉換類型方式,值進入redis時是java類型,標注valueEncode為java,值從redis中讀取時轉換成java,標注valueDecode為java。


注意,為了實現Object類型的值進出redis,需要保障進出redis的Object類型的數據必須實現序列化接口。

@Data
public class Book implements Serializable {private Integer id;private String type;private String name;private String description;
}

步驟③

啟用緩存時開啟方法緩存功能,并配置basePackages,說明在哪些包中開啟方法緩存

@SpringBootApplication
//jetcache啟用緩存的主開關
@EnableCreateCacheAnnotation
//開啟方法注解緩存
@EnableMethodCache(basePackages = "com.itheima")
public class Springboot20JetCacheApplication {public static void main(String[] args) {SpringApplication.run(Springboot20JetCacheApplication.class, args);}
}

步驟④

使用注解@Cached標注當前方法使用緩存

@Service
public class BookServiceImpl implements BookService {@Autowiredprivate BookDao bookDao;@Override@Cached(name="book_",key="#id",expire = 3600,cacheType = CacheType.REMOTE)public Book getById(Integer id) {return bookDao.selectById(id);}
}

遠程方案的數據同步

由于遠程方案中redis保存的數據可以被多個客戶端共享,這就存在了數據同步問題。jetcache提供了3個注解解決此問題,分別在更新、刪除操作時同步緩存數據,和讀取緩存時定時刷新數據


更新緩存

@CacheUpdate(name="book_",key="#book.id",value="#book")
public boolean update(Book book) {return bookDao.updateById(book) > 0;
}

刪除緩存

@CacheInvalidate(name="book_",key = "#id")
public boolean delete(Integer id) {return bookDao.deleteById(id) > 0;
}

定時刷新緩存

@Cached(name="book_",key="#id",expire = 3600,cacheType = CacheType.REMOTE)
@CacheRefresh(refresh = 5)
public Book getById(Integer id) {return bookDao.selectById(id);
}

數據報表

jetcache還提供有簡單的數據報表功能,幫助開發者快速查看緩存命中信息,只需要添加一個配置即可

jetcache:statIntervalMinutes: 1

設置后,每1分鐘在控制臺輸出緩存數據命中信息

[DefaultExecutor] c.alicp.jetcache.support.StatInfoLogger  : jetcache stat from 2022-02-28 09:32:15,892 to 2022-02-28 09:33:00,003
cache    |    qps|   rate|   get|    hit|   fail|   expire|   avgLoadTime|   maxLoadTime
---------+-------+-------+------+-------+-------+---------+--------------+--------------
book_    |   0.66| 75.86%|    29|     22|      0|        0|          28.0|           188
---------+-------+-------+------+-------+-------+---------+--------------+--------------

總結

1. jetcache是一個類似于springcache的緩存解決方案,自身不具有緩存功能,它提供有本地緩存與遠程緩存多級共同使用的緩存解決方案
2. jetcache提供的緩存解決方案受限于目前支持的方案,本地緩存支持兩種,遠程緩存支持兩種
3. 注意數據進入遠程緩存時的類型轉換問題
4. jetcache提供方法緩存,并提供了對應的緩存更新與刷新功能
5. jetcache提供有簡單的緩存信息命中報表方便開發者即時監控緩存數據命中情況


思考

jetcache解決了前期使用緩存方案單一的問題,但是仍然不能靈活的選擇緩存進行搭配使用,是否存在一種技術可以靈活的搭配各種各樣的緩存使用呢?有,咱們下一節再講。


SpringBoot整合j2cache緩存

jetcache可以在限定范圍內構建多級緩存,但是靈活性不足,不能隨意搭配緩存,本節介紹一種可以隨意搭配緩存解決方案的緩存整合框架,j2cache。下面就來講解如何使用這種緩存框架,以Ehcache與redis整合為例:


步驟①

導入j2cache、redis、ehcache坐標

<dependency><groupId>net.oschina.j2cache</groupId><artifactId>j2cache-core</artifactId><version>2.8.4-release</version>
</dependency>
<dependency><groupId>net.oschina.j2cache</groupId><artifactId>j2cache-spring-boot2-starter</artifactId><version>2.8.0-release</version>
</dependency>
<dependency><groupId>net.sf.ehcache</groupId><artifactId>ehcache</artifactId>
</dependency>

j2cache的starter中默認包含了redis坐標,官方推薦使用redis作為二級緩存,因此此處無需導入redis坐標


步驟②

配置一級與二級緩存,并配置一二級緩存間數據傳遞方式,配置書寫在名稱為j2cache.properties的文件中。如果使用ehcache還需要單獨添加ehcache的配置文件

# 1級緩存
j2cache.L1.provider_class = ehcache
ehcache.configXml = ehcache.xml# 2級緩存
j2cache.L2.provider_class = > net.oschina.j2cache.cache.support.redis.SpringRedisProvider
j2cache.L2.config_section = redis
redis.hosts = localhost:6379# 1級緩存中的數據如何到達二級緩存
j2cache.broadcast = net.oschina.j2cache.cache.support.redis.SpringRedisPubSubPolicy

此處配置不能亂配置,需要參照官方給出的配置說明進行。例如1級供應商選擇ehcache,供應商名稱僅僅是一個ehcache,但是2級供應商選擇redis時要寫專用的Spring整合Redis的供應商類名SpringRedisProvider,而且這個名稱并不是所有的redis包中能提供的,也不是spring包中提供的。因此配置j2cache必須參照官方文檔配置,而且還要去找專用的整合包,導入對應坐標才可以使用。


一級與二級緩存最重要的一個配置就是兩者之間的數據溝通方式,此類配置也不是隨意配置的,并且不同的緩存解決方案提供的數據溝通方式差異化很大,需要查詢官方文檔進行設置。


步驟③

使用緩存

@Service
public class SMSCodeServiceImpl implements SMSCodeService {@Autowiredprivate CodeUtils codeUtils;@Autowiredprivate CacheChannel cacheChannel;public String sendCodeToSMS(String tele) {String code = codeUtils.generator(tele);cacheChannel.set("sms",tele,code);return code;}public boolean checkCode(SMSCode smsCode) {String code = cacheChannel.get("sms",smsCode.getTele()).asString();return smsCode.getCode().equals(code);}
}

j2cache的使用和jetcache比較類似,但是無需開啟使用的開關,直接定義緩存對象即可使用,緩存對象名CacheChannel。


j2cache的使用不復雜,配置是j2cache的核心,畢竟是一個整合型的緩存框架。緩存相關的配置過多,可以查閱j2cache-core核心包中的j2cache.properties文件中的說明。如下:

#J2Cache configuration
#########################################
# Cache Broadcast Method
# values:
# jgroups -> use jgroups's multicast
# redis -> use redis publish/subscribe mechanism (using jedis)
# lettuce -> use redis publish/subscribe mechanism (using lettuce, Recommend)
# rabbitmq -> use RabbitMQ publisher/consumer mechanism
# rocketmq -> use RocketMQ publisher/consumer mechanism
# none -> don't notify the other nodes in cluster
# xx.xxxx.xxxx.Xxxxx your own cache broadcast policy classname that implement net.oschina.j2cache.cluster.ClusterPolicy
#########################################
j2cache.broadcast = redis# jgroups properties
jgroups.channel.name = j2cache
jgroups.configXml = /network.xml# RabbitMQ properties
rabbitmq.exchange = j2cache
rabbitmq.host = localhost
rabbitmq.port = 5672
rabbitmq.username = guest
rabbitmq.password = guest# RocketMQ properties
rocketmq.name = j2cache
rocketmq.topic = j2cache
# use ; to split multi hosts
rocketmq.hosts = 127.0.0.1:9876#########################################
# Level 1&2 provider
# values:
# none -> disable this level cache
# ehcache -> use ehcache2 as level 1 cache
# ehcache3 -> use ehcache3 as level 1 cache
# caffeine -> use caffeine as level 1 cache(only in memory)
# redis -> use redis as level 2 cache (using jedis)
# lettuce -> use redis as level 2 cache (using lettuce)
# readonly-redis -> use redis as level 2 cache ,but never write data to it. if use this provider, you must uncomment `j2cache.L2.config_section` to make the redis configurations available.
# memcached -> use memcached as level 2 cache (xmemcached),
# [classname] -> use custom provider
#########################################j2cache.L1.provider_class = caffeine
j2cache.L2.provider_class = redis# When L2 provider isn't `redis`, using `L2.config_section = redis` to read redis configurations
# j2cache.L2.config_section = redis# Enable/Disable ttl in redis cache data (if disabled, the object in redis will never expire, default:true)
# NOTICE: redis hash mode (redis.storage = hash) do not support this feature)
j2cache.sync_ttl_to_redis = true# Whether to cache null objects by default (default false)
j2cache.default_cache_null_object = true#########################################
# Cache Serialization Provider
# values:
# fst -> using fast-serialization (recommend)
# kryo -> using kryo serialization
# json -> using fst's json serialization (testing)
# fastjson -> using fastjson serialization (embed non-static class not support)
# java -> java standard
# fse -> using fse serialization
# [classname implements Serializer]
#########################################j2cache.serialization = json
#json.map.person = net.oschina.j2cache.demo.Person#########################################
# Ehcache configuration
########################################## ehcache.configXml = /ehcache.xml# ehcache3.configXml = /ehcache3.xml
# ehcache3.defaultHeapSize = 1000#########################################
# Caffeine configuration
# caffeine.region.[name] = size, xxxx[s|m|h|d]
#
#########################################
caffeine.properties = /caffeine.properties#########################################
# Redis connection configuration
##################################################################################
# Redis Cluster Mode
#
# single -> single redis server
# sentinel -> master-slaves servers
# cluster -> cluster servers (數據庫配置無效,使用 database = 0)
# sharded -> sharded servers  (密碼、數據庫必須在 hosts 中指定,且連接池配置無效 ; redis://user:password@127.0.0.1:6379/0)
#
#########################################redis.mode = single#redis storage mode (generic|hash)
redis.storage = generic## redis pub/sub channel name
redis.channel = j2cache
## redis pub/sub server (using redis.hosts when empty)
redis.channel.host =#cluster name just for sharded
redis.cluster_name = j2cache## redis cache namespace optional, default[empty]
redis.namespace =## redis command scan parameter count, default[1000]
#redis.scanCount = 1000## connection
# Separate multiple redis nodes with commas, such as 192.168.0.10:6379,192.168.0.11:6379,192.168.0.12:6379redis.hosts = 127.0.0.1:6379
redis.timeout = 2000
redis.password =
redis.database = 0
redis.ssl = false## redis pool properties
redis.maxTotal = 100
redis.maxIdle = 10
redis.maxWaitMillis = 5000
redis.minEvictableIdleTimeMillis = 60000
redis.minIdle = 1
redis.numTestsPerEvictionRun = 10
redis.lifo = false
redis.softMinEvictableIdleTimeMillis = 10
redis.testOnBorrow = true
redis.testOnReturn = false
redis.testWhileIdle = true
redis.timeBetweenEvictionRunsMillis = 300000
redis.blockWhenExhausted = false
redis.jmxEnabled = false#########################################
# Lettuce scheme
#
# redis -> single redis server
# rediss -> single redis server with ssl
# redis-sentinel -> redis sentinel
# redis-cluster -> cluster servers
#
##################################################################################
# Lettuce Mode
#
# single -> single redis server
# sentinel -> master-slaves servers
# cluster -> cluster servers (數據庫配置無效,使用 database = 0)
# sharded -> sharded servers  (密碼、數據庫必須在 hosts 中指定,且連接池配置無效 ; redis://user:password@127.0.0.1:6379/0)
#
########################################### redis command scan parameter count, default[1000]
#lettuce.scanCount = 1000
lettuce.mode = single
lettuce.namespace =
lettuce.storage = hash
lettuce.channel = j2cache
lettuce.scheme = redis
lettuce.hosts = 127.0.0.1:6379
lettuce.password =
lettuce.database = 0
lettuce.sentinelMasterId =
lettuce.maxTotal = 100
lettuce.maxIdle = 10
lettuce.minIdle = 10
# timeout in milliseconds
lettuce.timeout = 10000
# redis cluster topology refresh interval in milliseconds
lettuce.clusterTopologyRefresh = 3000#########################################
# memcached server configurations
# refer to https://gitee.com/mirrors/XMemcached
#########################################memcached.servers = 127.0.0.1:11211
memcached.username =
memcached.password =
memcached.connectionPoolSize = 10
memcached.connectTimeout = 1000
memcached.failureMode = false
memcached.healSessionInterval = 1000
memcached.maxQueuedNoReplyOperations = 100
memcached.opTimeout = 100
memcached.sanitizeKeys = false

總結

1. j2cache是一個緩存框架,自身不具有緩存功能,它提供多種緩存整合在一起使用的方案
2. j2cache需要通過復雜的配置設置各級緩存,以及緩存之間數據交換的方式
3. j2cache操作接口通過CacheChannel實現


KF-5-2.任務

springboot整合第三方技術第二部分我們來說說任務系統,其實這里說的任務系統指的是定時任務。定時任務是企業級開發中必不可少的組成部分,諸如長周期業務數據的計算,例如年度報表,諸如系統臟數據的處理,再比如系統性能監控報告,還有搶購類活動的商品上架,這些都離不開定時任務。本節將介紹兩種不同的定時任務技術。


Quartz

Quartz技術是一個比較成熟的定時任務框架,怎么說呢?有點繁瑣,用過的都知道,配置略微復雜。springboot對其進行整合后,簡化了一系列的配置,將很多配置采用默認設置,這樣開發階段就簡化了很多。再學習springboot整合Quartz前先普及幾個Quartz的概念。


1、工作(Job):用于定義具體執行的工作
2、工作明細(JobDetail):用于描述定時工作相關的信息
3、觸發器(Trigger):描述了工作明細與調度器的對應關系
4、調度器(Scheduler):用于描述觸發工作的執行規則,通常使用cron表達式定義規則


簡單說就是你定時干什么事情,這就是工作,工作不可能就是一個簡單的方法,還要設置一些明細信息。工作啥時候執行,設置一個調度器,可以簡單理解成設置一個工作執行的時間。工作和調度都是獨立定義的,它們兩個怎么配合到一起呢?用觸發器。完了,就這么多。下面開始springboot整合Quartz。


步驟①

導入springboot整合Quartz的starter

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-quartz</artifactId>
</dependency>

步驟②

定義任務Bean,按照Quartz的開發規范制作,繼承QuartzJobBean

public class MyQuartz extends QuartzJobBean {@Overrideprotected void executeInternal(JobExecutionContext context) throws JobExecutionException {System.out.println("quartz task run...");}
}

步驟③

創建Quartz配置類,定義工作明細(JobDetail)與觸發器的(Trigger)bean

@Configuration
public class QuartzConfig {@Beanpublic JobDetail printJobDetail(){//綁定具體的工作return JobBuilder.newJob(MyQuartz.class).storeDurably().build();}@Beanpublic Trigger printJobTrigger(){ScheduleBuilder schedBuilder = CronScheduleBuilder.cronSchedule("0/5 * * * * ?");//綁定對應的工作明細return TriggerBuilder.newTrigger().forJob(printJobDetail()).withSchedule(schedBuilder).build();}
}

工作明細中要設置對應的具體工作,使用newJob()操作傳入對應的工作任務類型即可。


觸發器需要綁定任務,使用forJob()操作傳入綁定的工作明細對象。此處可以為工作明細設置名稱然后使用名稱綁定,也可以直接調用對應方法綁定。觸發器中最核心的規則是執行時間,此處使用調度器定義執行時間,執行時間描述方式使用的是cron表達式。有關cron表達式的規則,各位小伙伴可以去參看相關課程學習,略微復雜,而且格式不能亂設置,不是寫個格式就能用的,寫不好就會出現沖突問題。


總結

1. springboot整合Quartz就是將Quartz對應的核心對象交給spring容器管理,包含兩個對象,JobDetail和Trigger對象
2. JobDetail對象描述的是工作的執行信息,需要綁定一個QuartzJobBean類型的對象
3. Trigger對象定義了一個觸發器,需要為其指定綁定的JobDetail是哪個,同時要設置執行周期調度器


思考

上面的操作看上去不多,但是Quartz將其中的對象劃分粒度過細,導致開發的時候有點繁瑣,spring針對上述規則進行了簡化,開發了自己的任務管理組件——Task,如何用呢?咱們下節再說。


Task

spring根據定時任務的特征,將定時任務的開發簡化到了極致。怎么說呢?要做定時任務總要告訴容器有這功能吧,然后定時執行什么任務直接告訴對應的bean什么時間執行就行了,就這么簡單,一起來看怎么做


步驟①

開啟定時任務功能,在引導類上開啟定時任務功能的開關,使用注解@EnableScheduling

@SpringBootApplication
//開啟定時任務功能
@EnableScheduling
public class Springboot22TaskApplication {public static void main(String[] args) {SpringApplication.run(Springboot22TaskApplication.class, args);}
}

步驟②

定義Bean,在對應要定時執行的操作上方,使用注解@Scheduled定義執行的時間,執行時間的描述方式還是cron表達式

@Component
public class MyBean {@Scheduled(cron = "0/1 * * * * ?")public void print(){System.out.println(Thread.currentThread().getName()+" :spring task run...");}
}

完事,這就完成了定時任務的配置。總體感覺其實什么東西都沒少,只不過沒有將所有的信息都抽取成bean,而是直接使用注解綁定定時執行任務的事情而已。


如何想對定時任務進行相關配置,可以通過配置文件進行

spring:task:scheduling:pool:size: 1							# 任務調度線程池大小 默認 1thread-name-prefix: ssm_      	# 調度線程名稱前綴 默認 scheduling-      shutdown:await-termination: false		# 線程池關閉時等待所有任務完成await-termination-period: 10s	# 調度線程關閉前最大等待時間,確保最后一定關閉

總結

1. spring task需要使用注解@EnableScheduling開啟定時任務功能
2. 為定時執行的的任務設置執行周期,描述方式cron表達式


KF-5-3.郵件

springboot整合第三方技術第三部分我們來說說郵件系統,發郵件是java程序的基本操作,springboot整合javamail其實就是簡化開發。不熟悉郵件的小伙伴可以先學習完javamail的基礎操作,再來看這一部分內容才能感觸到springboot整合javamail究竟簡化了哪些操作。簡化的多碼?其實不多,差別不大,只是還個格式而已。


學習郵件發送之前先了解3個概念,這些概念規范了郵件操作過程中的標準。
1、SMTP(Simple Mail Transfer Protocol):簡單郵件傳輸協議,用于發送電子郵件的傳輸協議
2、POP3(Post Office Protocol - Version 3):用于
接收電子郵件的標準協議
3、IMAP(Internet Mail Access Protocol):互聯網消息協議,是POP3的替代協議


簡單說就是SMPT是發郵件的標準,POP3是收郵件的標準,IMAP是對POP3的升級。我們制作程序中操作郵件,通常是發郵件,所以SMTP是使用的重點,收郵件大部分都是通過郵件客戶端完成,所以開發收郵件的代碼極少。除非你要讀取郵件內容,然后解析,做郵件功能的統一處理。例如HR的郵箱收到求職者的簡歷,可以讀取后統一處理。但是為什么不制作獨立的投遞簡歷的系統呢?所以說,好奇怪的需求,因為要想收郵件就要規范發郵件的人的書寫格式,這個未免有點強人所難,并且極易收到外部攻擊,你不可能使用白名單來收郵件。如果能使用白名單來收郵件然后解析郵件,還不如開發個系統給白名單中的人專用呢,更安全,總之就是雞肋了。下面就開始學習springboot如何整合javamail發送郵件。


發送簡單郵件

步驟①

導入springboot整合javamail的starter

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-mail</artifactId>
</dependency>

步驟②

配置郵箱的登錄信息

spring:mail:host: smtp.126.comusername: test@126.compassword: test

java程序僅用于發送郵件,郵件的功能還是郵件供應商提供的,所以這里是用別人的郵件服務,要配置對應信息。


host配置的是提供郵件服務的主機協議,當前程序僅用于發送郵件,因此配置的是smtp的協議。


password并不是郵箱賬號的登錄密碼,是郵件供應商提供的一個加密后的密碼,也是為了保障系統安全性。不然外部人員通過地址訪問下載了配置文件,直接獲取到了郵件密碼就會有極大的安全隱患。有關該密碼的獲取每個郵件供應商提供的方式都不一樣,此處略過。可以到郵件供應商的設置頁面找POP3或IMAP這些關鍵詞找到對應的獲取位置。下例僅供參考:

image-20220228111251036


步驟③

使用JavaMailSender接口發送郵件

@Service
public class SendMailServiceImpl implements SendMailService {@Autowiredprivate JavaMailSender javaMailSender;//發送人private String from = "test@qq.com";//接收人private String to = "test@126.com";//標題private String subject = "測試郵件";//正文private String context = "測試郵件正文內容";@Overridepublic void sendMail() {SimpleMailMessage message = new SimpleMailMessage();message.setFrom(from+"(小甜甜)");message.setTo(to);message.setSubject(subject);message.setText(context);javaMailSender.send(message);}
}

將發送郵件的必要信息(發件人、收件人、標題、正文)封裝到SimpleMailMessage對象中,可以根據規則設置發送人昵稱等。


發送多組件郵件(附件、復雜正文)

發送簡單郵件僅需要提供對應的4個基本信息就可以了,如果想發送復雜的郵件,需要更換郵件對象。使用MimeMessage可以發送特殊的郵件。


發送網頁正文郵件

@Service
public class SendMailServiceImpl2 implements SendMailService {@Autowiredprivate JavaMailSender javaMailSender;//發送人private String from = "test@qq.com";//接收人private String to = "test@126.com";//標題private String subject = "測試郵件";//正文private String context = "<img src='ABC.JPG'/><a href='https://www.itcast.cn'>點開有驚喜</a>";public void sendMail() {try {MimeMessage message = javaMailSender.createMimeMessage();MimeMessageHelper helper = new MimeMessageHelper(message);helper.setFrom(to+"(小甜甜)");helper.setTo(from);helper.setSubject(subject);helper.setText(context,true);		//此處設置正文支持html解析javaMailSender.send(message);} catch (Exception e) {e.printStackTrace();}}
}

發送帶有附件的郵件

@Service
public class SendMailServiceImpl2 implements SendMailService {@Autowiredprivate JavaMailSender javaMailSender;//發送人private String from = "test@qq.com";//接收人private String to = "test@126.com";//標題private String subject = "測試郵件";//正文private String context = "測試郵件正文";public void sendMail() {try {MimeMessage message = javaMailSender.createMimeMessage();MimeMessageHelper helper = new MimeMessageHelper(message,true);		//此處設置支持附件helper.setFrom(to+"(小甜甜)");helper.setTo(from);helper.setSubject(subject);helper.setText(context);//添加附件File f1 = new File("springboot_23_mail-0.0.1-SNAPSHOT.jar");File f2 = new File("resources\\logo.png");helper.addAttachment(f1.getName(),f1);helper.addAttachment("最靠譜的培訓結構.png",f2);javaMailSender.send(message);} catch (Exception e) {e.printStackTrace();}}
}

總結

1. springboot整合javamail其實就是簡化了發送郵件的客戶端對象JavaMailSender的初始化過程,通過配置的形式加載信息簡化開發過程


KF-5-4.消息

springboot整合第三方技術最后一部分我們來說說消息中間件,首先先介紹一下消息的應用。


消息的概念

從廣義角度來說,消息其實就是信息,但是和信息又有所不同。信息通常被定義為一組數據,而消息除了具有數據的特征之外,還有消息的來源與接收的概念。通常發送消息的一方稱為消息的生產者,接收消息的一方稱為消息的消費者。這樣比較后,發現其實消息和信息差別還是很大的。


為什么要設置生產者和消費者呢?這就是要說到消息的意義了。信息通常就是一組數據,但是消息由于有了生產者和消費者,就出現了消息中所包含的信息可以被二次解讀,生產者發送消息,可以理解為生產者發送了一個信息,也可以理解為生產者發送了一個命令;消費者接收消息,可以理解為消費者得到了一個信息,也可以理解為消費者得到了一個命令。對比一下我們會發現信息是一個基本數據,而命令則可以關聯下一個行為動作,這樣就可以理解為基于接收的消息相當于得到了一個行為動作,使用這些行為動作就可以組織成一個業務邏輯,進行進一步的操作。總的來說,消息其實也是一組信息,只是為其賦予了全新的含義,因為有了消息的流動,并且是有方向性的流動,帶來了基于流動的行為產生的全新解讀。開發者就可以基于消息的這種特殊解,將其換成代碼中的指令。


對于消息的理解,初學者總認為消息內部的數據非常復雜,這是一個誤區。比如我發送了一個消息,要求接受者翻譯發送過去的內容。初學者會認為消息中會包含被翻譯的文字,已經本次操作要執行翻譯操作而不是打印操作。其實這種現象有點過度解讀了,發送的消息中僅僅包含被翻譯的文字,但是可以通過控制不同的人接收此消息來確認要做的事情。例如發送被翻譯的文字僅到A程序,而A程序只能進行翻譯操作,這樣就可以發送簡單的信息完成復雜的業務了,是通過接收消息的主體不同,進而執行不同的操作,而不會在消息內部定義數據的操作行為,當然如果開發者希望消息中包含操作種類信息也是可以的,只是提出消息的內容可以更簡單,更單一。


對于消息的生產者與消費者的工作模式,還可以將消息劃分成兩種模式,同步消費與異步消息。


所謂同步消息就是生產者發送完消息,等待消費者處理,消費者處理完將結果告知生產者,然后生產者繼續向下執行業務。這種模式過于卡生產者的業務執行連續性,在現在的企業級開發中,上述這種業務場景通常不會采用消息的形式進行處理。


所謂異步消息就是生產者發送完消息,無需等待消費者處理完畢,生產者繼續向下執行其他動作。比如生產者發送了一個日志信息給日志系統,發送過去以后生產者就向下做其他事情了,無需關注日志系統的執行結果。日志系統根據接收到的日志信息繼續進行業務執行,是單純的記錄日志,還是記錄日志并報警,這些和生產者無關,這樣生產者的業務執行效率就會大幅度提升。并且可以通過添加多個消費者來處理同一個生產者發送的消息來提高系統的高并發性,改善系統工作效率,提高用戶體驗。一旦某一個消費者由于各種問題宕機了,也不會對業務產生影響,提高了系統的高可用性。


以上簡單的介紹了一下消息這種工作模式存在的意義,希望對各位學習者有所幫助。


Java處理消息的標準規范

目前企業級開發中廣泛使用的消息處理技術共三大類,具體如下:
JMS
AMQP
MQTT


為什么是三大類,而不是三個技術呢?因為這些都是規范,就想JDBC技術,是個規范,開發針對規范開發,運行還要靠實現類,例如MySQL提供了JDBC的實現,最終運行靠的還是實現。并且這三類規范都是針對異步消息進行處理的,也符合消息的設計本質,處理異步的業務。對以上三種消息規范做一下普及


JMS

JMS(Java Message Service),這是一個規范,作用等同于JDBC規范,提供了與消息服務相關的API接口。


JMS消息模型

JMS規范中規范了消息有兩種模型。分別是點對點模型發布訂閱模型


點對點模型:peer-2-peer,生產者會將消息發送到一個保存消息的容器中,通常使用隊列模型,使用隊列保存消息。一個隊列的消息只能被一個消費者消費,或未被及時消費導致超時。這種模型下,生產者和消費者是一對一綁定的。


發布訂閱模型:publish-subscribe,生產者將消息發送到一個保存消息的容器中,也是使用隊列模型來保存。但是消息可以被多個消費者消費,生產者和消費者完全獨立,相互不需要感知對方的存在。


以上這種分類是從消息的生產和消費過程來進行區分,針對消息所包含的信息不同,還可以進行不同類別的劃分。


JMS消息種類

根據消息中包含的數據種類劃分,可以將消息劃分成6種消息。
1.TextMessage
2.MapMessage
3.BytesMessage
4.StreamMessage
5.ObjectMessage
6.Message (只有消息頭和屬性)


JMS主張不同種類的消息,消費方式不同,可以根據使用需要選擇不同種類的消息。但是這一點也成為其詬病之處,后面再說。整體上來說,JMS就是典型的保守派,什么都按照J2EE的規范來,做一套規范,定義若干個標準,每個標準下又提供一大批API。目前對JMS規范實現的消息中間件技術還是挺多的,畢竟是皇家御用,肯定有人舔,例如ActiveMQ、Redis、HornetMQ。但是也有一些不太規范的實現,參考JMS的標準設計,但是又不完全滿足其規范,例如:RabbitMQ、RocketMQ。


AMQP

JMS的問世為消息中間件提供了很強大的規范性支撐,但是使用的過程中就開始被人詬病,比如JMS設置的極其復雜的多種類消息處理機制。本來分門別類處理挺好的,為什么會被詬病呢?原因就在于JMS的設計是J2EE規范,站在Java開發的角度思考問題。但是現實往往是復雜度很高的。比如我有一個.NET開發的系統A,有一個Java開發的系統B,現在要從A系統給B系統發業務消息,結果兩邊數據格式不統一,沒法操作。JMS不是可以統一數據格式嗎?提供了6種數據種類,總有一款適合你啊。NO,一個都不能用。因為A系統的底層語言不是Java語言開發的,根本不支持那些對象。這就意味著如果想使用現有的業務系統A繼續開發已經不可能了,必須推翻重新做使用Java語言開發的A系統。


這時候有人就提出說,你搞那么復雜,整那么多種類干什么?找一種大家都支持的消息數據類型不就解決這個跨平臺的問題了嗎?大家一想,對啊,于是AMQP孕育而生。


單從上面的說明中其實可以明確感知到,AMQP的出現解決的是消息傳遞時使用的消息種類的問題,化繁為簡,但是其并沒有完全推翻JMS的操作API,所以說AMQP僅僅是一種協議,規范了數據傳輸的格式而已。


AMQP(advanced message queuing protocol):一種協議(高級消息隊列協議,也是消息代理規范),規范了網絡交換的數據格式,兼容JMS操作。


優點

具有跨平臺性,服務器供應商,生產者,消費者可以使用不同的語言來實現


JMS消息種類

AMQP消息種類:byte[]


AMQP在JMS的消息模型基礎上又進行了進一步的擴展,除了點對點和發布訂閱的模型,開發了幾種全新的消息模型,適應各種各樣的消息發送。


AMQP消息模型

1、direct exchange
2、fanout exchange
3、topic exchange
4、headers exchange
5、system exchange

目前實現了AMQP協議的消息中間件技術也很多,而且都是較為流行的技術,例如:RabbitMQ、StormMQ、RocketMQ


MQTT

MQTT(Message Queueing Telemetry Transport)消息隊列遙測傳輸,專為小設備設計,是物聯網(IOT)生態系統中主要成分之一。由于與JavaEE企業級開發沒有交集,此處不作過多的說明。


除了上述3種J2EE企業級應用中廣泛使用的三種異步消息傳遞技術,還有一種技術也不能忽略,Kafka。


KafKa

Kafka,一種高吞吐量的分布式發布訂閱消息系統,提供實時消息功能。Kafka技術并不是作為消息中間件為主要功能的產品,但是其擁有發布訂閱的工作模式,也可以充當消息中間件來使用,而且目前企業級開發中其身影也不少見。


本節內容講圍繞著上述內容中的幾種實現方案講解springboot整合各種各樣的消息中間件。由于各種消息中間件必須先安裝再使用,下面的內容采用Windows系統安裝,降低各位學習者的學習難度,基本套路和之前學習NoSQL解決方案一樣,先安裝再整合。


購物訂單發送手機短信案例

為了便于下面演示各種各樣的消息中間件技術,我們創建一個購物過程生成訂單時為用戶發送短信的案例環境,模擬使用消息中間件實現發送手機短信的過程。


手機驗證碼案例需求如下:
1、執行下單業務時(模擬此過程),調用消息服務,將要發送短信的訂單id傳遞給消息中間件
2、消息處理服務接收到要發送的訂單id后輸出訂單id(模擬發短信)


由于不涉及數據讀寫,僅開發業務層與表現層,其中短信處理的業務代碼獨立開發,代碼如下:


訂單業務
業務層接口

public interface OrderService {void order(String id);
}

模擬傳入訂單id,執行下訂單業務,參數為虛擬設定,實際應為訂單對應的實體類


業務層實現

@Service
public class OrderServiceImpl implements OrderService {@Autowiredprivate MessageService messageService;@Overridepublic void order(String id) {//一系列操作,包含各種服務調用,處理各種業務System.out.println("訂單處理開始");//短信消息處理messageService.sendMessage(id);System.out.println("訂單處理結束");System.out.println();}
}

業務層轉調短信處理的服務MessageService


表現層服務

@RestController
@RequestMapping("/orders")
public class OrderController {@Autowiredprivate OrderService orderService;@PostMapping("{id}")public void order(@PathVariable String id){orderService.order(id);}
}

表現層對外開發接口,傳入訂單id即可(模擬)


短信處理業務
業務層接口

public interface MessageService {void sendMessage(String id);String doMessage();
}

短信處理業務層接口提供兩個操作,發送要處理的訂單id到消息中間件,另一個操作目前暫且設計成處理消息,實際消息的處理過程不應該是手動執行,應該是自動執行,到具體實現時再進行設計


業務層實現

@Service
public class MessageServiceImpl implements MessageService {private ArrayList<String> msgList = new ArrayList<String>();@Overridepublic void sendMessage(String id) {System.out.println("待發送短信的訂單已納入處理隊列,id:"+id);msgList.add(id);}@Overridepublic String doMessage() {String id = msgList.remove(0);System.out.println("已完成短信發送業務,id:"+id);return id;}
}

短信處理業務層實現中使用集合先模擬消息隊列,觀察效果


表現層服務

@RestController
@RequestMapping("/msgs")
public class MessageController {@Autowiredprivate MessageService messageService;@GetMappingpublic String doMessage(){String id = messageService.doMessage();return id;}
}

短信處理表現層接口暫且開發出一個處理消息的入口,但是此業務是對應業務層中設計的模擬接口,實際業務不需要設計此接口。


下面開啟springboot整合各種各樣的消息中間件,從嚴格滿足JMS規范的ActiveMQ開始


SpringBoot整合ActiveMQ

ActiveMQ是MQ產品中的元老級產品,早期標準MQ產品之一,在AMQP協議沒有出現之前,占據了消息中間件市場的絕大部分份額,后期因為AMQP系列產品的出現,迅速走弱,目前僅在一些線上運行的產品中出現,新產品開發較少采用。


安裝

windows版安裝包下載地址
下載的安裝包是解壓縮就能使用的zip文件,解壓縮完畢后會得到如下文件

image-20220228160001620


啟動服務器

activemq.bat

運行bin目錄下的win32或win64目錄下的activemq.bat命令即可,根據自己的操作系統選擇即可,默認對外服務端口61616。


訪問web管理服務

ActiveMQ啟動后會啟動一個Web控制臺服務,可以通過該服務管理ActiveMQ。

http://127.0.0.1:8161/

web管理服務默認端口8161,訪問后可以打開ActiveMQ的管理界面,如下:

image-20220228160844972

首先輸入訪問用戶名和密碼,初始化用戶名和密碼相同,均為:admin,成功登錄后進入管理后臺界面,如下:

image-20220228161010401

看到上述界面視為啟動ActiveMQ服務成功。


啟動失敗

在ActiveMQ啟動時要占用多個端口,以下為正常啟動信息:

wrapper  | --> Wrapper Started as Console
wrapper  | Launching a JVM...
jvm 1    | Wrapper (Version 3.2.3) http://wrapper.tanukisoftware.org
jvm 1    |   Copyright 1999-2006 Tanuki Software, Inc.  All Rights Reserved.
jvm 1    |
jvm 1    | Java Runtime: Oracle Corporation 1.8.0_172 D:\soft\jdk1.8.0_172\jre
jvm 1    |   Heap sizes: current=249344k  free=235037k  max=932352k
jvm 1    |     JVM args: -Dactivemq.home=../.. -Dactivemq.base=../.. -Djavax.net.ssl.keyStorePassword=password -Djavax.net.ssl.trustStorePassword=password -Djavax.net.ssl.keyStore=../../conf/broker.ks -Djavax.net.ssl.trustStore=../../conf/broker.ts -Dcom.sun.management.jmxremote -Dorg.apache.activemq.UseDedicatedTaskRunner=true -Djava.util.logging.config.file=logging.properties -Dactivemq.conf=../../conf -Dactivemq.data=../../data -Djava.security.auth.login.config=../../conf/login.config -Xmx1024m -Djava.library.path=../../bin/win64 -Dwrapper.key=7ySrCD75XhLCpLjd -Dwrapper.port=32000 -Dwrapper.jvm.port.min=31000 -Dwrapper.jvm.port.max=31999 -Dwrapper.pid=9364 -Dwrapper.version=3.2.3 -Dwrapper.native_library=wrapper -Dwrapper.cpu.timeout=10 -Dwrapper.jvmid=1
jvm 1    | Extensions classpath:
jvm 1    |   [..\..\lib,..\..\lib\camel,..\..\lib\optional,..\..\lib\web,..\..\lib\extra]
jvm 1    | ACTIVEMQ_HOME: ..\..
jvm 1    | ACTIVEMQ_BASE: ..\..
jvm 1    | ACTIVEMQ_CONF: ..\..\conf
jvm 1    | ACTIVEMQ_DATA: ..\..\data
jvm 1    | Loading message broker from: xbean:activemq.xml
jvm 1    |  INFO | Refreshing org.apache.activemq.xbean.XBeanBrokerFactory$1@5f3ebfe0: startup date [Mon Feb 28 16:07:48 CST 2022]; root of context hierarchy
jvm 1    |  INFO | Using Persistence Adapter: KahaDBPersistenceAdapter[D:\soft\activemq\bin\win64\..\..\data\kahadb]
jvm 1    |  INFO | KahaDB is version 7
jvm 1    |  INFO | PListStore:[D:\soft\activemq\bin\win64\..\..\data\localhost\tmp_storage] started
jvm 1    |  INFO | Apache ActiveMQ 5.16.3 (localhost, ID:CZBK-20210302VL-10434-1646035669595-0:1) is starting
jvm 1    |  INFO | Listening for connections at: tcp://CZBK-20210302VL:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600
jvm 1    |  INFO | Connector openwire started
jvm 1    |  INFO | Listening for connections at: amqp://CZBK-20210302VL:5672?maximumConnections=1000&wireFormat.maxFrameSize=104857600
jvm 1    |  INFO | Connector amqp started
jvm 1    |  INFO | Listening for connections at: stomp://CZBK-20210302VL:61613?maximumConnections=1000&wireFormat.maxFrameSize=104857600
jvm 1    |  INFO | Connector stomp started
jvm 1    |  INFO | Listening for connections at: mqtt://CZBK-20210302VL:1883?maximumConnections=1000&wireFormat.maxFrameSize=104857600
jvm 1    |  INFO | Connector mqtt started
jvm 1    |  INFO | Starting Jetty server
jvm 1    |  INFO | Creating Jetty connector
jvm 1    |  WARN | ServletContext@o.e.j.s.ServletContextHandler@7350746f{/,null,STARTING} has uncovered http methods for path: /
jvm 1    |  INFO | Listening for connections at ws://CZBK-20210302VL:61614?maximumConnections=1000&wireFormat.maxFrameSize=104857600
jvm 1    |  INFO | Connector ws started
jvm 1    |  INFO | Apache ActiveMQ 5.16.3 (localhost, ID:CZBK-20210302VL-10434-1646035669595-0:1) started
jvm 1    |  INFO | For help or more information please see: http://activemq.apache.org
jvm 1    |  WARN | Store limit is 102400 mb (current store usage is 0 mb). The data directory: D:\soft\activemq\bin\win64\..\..\data\kahadb only has 68936 mb of usable space. - resetting to maximum available disk space: 68936 mb
jvm 1    |  INFO | ActiveMQ WebConsole available at http://127.0.0.1:8161/
jvm 1    |  INFO | ActiveMQ Jolokia REST API available at http://127.0.0.1:8161/api/jolokia/

其中占用的端口有:61616、5672、61613、1883、61614,如果啟動失敗,請先管理對應端口即可。以下就是某個端口占用的報錯信息,可以從拋出異常的位置看出,啟動5672端口時端口被占用,顯示java.net.BindException: Address already in use: JVM_Bind。Windows系統中終止端口運行的操作參看【命令行啟動常見問題及解決方案】

wrapper  | --> Wrapper Started as Console
wrapper  | Launching a JVM...
jvm 1    | Wrapper (Version 3.2.3) http://wrapper.tanukisoftware.org
jvm 1    |   Copyright 1999-2006 Tanuki Software, Inc.  All Rights Reserved.
jvm 1    |
jvm 1    | Java Runtime: Oracle Corporation 1.8.0_172 D:\soft\jdk1.8.0_172\jre
jvm 1    |   Heap sizes: current=249344k  free=235038k  max=932352k
jvm 1    |     JVM args: -Dactivemq.home=../.. -Dactivemq.base=../.. -Djavax.net.ssl.keyStorePassword=password -Djavax.net.ssl.trustStorePassword=password -Djavax.net.ssl.keyStore=../../conf/broker.ks -Djavax.net.ssl.trustStore=../../conf/broker.ts -Dcom.sun.management.jmxremote -Dorg.apache.activemq.UseDedicatedTaskRunner=true -Djava.util.logging.config.file=logging.properties -Dactivemq.conf=../../conf -Dactivemq.data=../../data -Djava.security.auth.login.config=../../conf/login.config -Xmx1024m -Djava.library.path=../../bin/win64 -Dwrapper.key=QPJoy9ZoXeWmmwTS -Dwrapper.port=32000 -Dwrapper.jvm.port.min=31000 -Dwrapper.jvm.port.max=31999 -Dwrapper.pid=14836 -Dwrapper.version=3.2.3 -Dwrapper.native_library=wrapper -Dwrapper.cpu.timeout=10 -Dwrapper.jvmid=1
jvm 1    | Extensions classpath:
jvm 1    |   [..\..\lib,..\..\lib\camel,..\..\lib\optional,..\..\lib\web,..\..\lib\extra]
jvm 1    | ACTIVEMQ_HOME: ..\..
jvm 1    | ACTIVEMQ_BASE: ..\..
jvm 1    | ACTIVEMQ_CONF: ..\..\conf
jvm 1    | ACTIVEMQ_DATA: ..\..\data
jvm 1    | Loading message broker from: xbean:activemq.xml
jvm 1    |  INFO | Refreshing org.apache.activemq.xbean.XBeanBrokerFactory$1@2c9392f5: startup date [Mon Feb 28 16:06:16 CST 2022]; root of context hierarchy
jvm 1    |  INFO | Using Persistence Adapter: KahaDBPersistenceAdapter[D:\soft\activemq\bin\win64\..\..\data\kahadb]
jvm 1    |  INFO | KahaDB is version 7
jvm 1    |  INFO | PListStore:[D:\soft\activemq\bin\win64\..\..\data\localhost\tmp_storage] started
jvm 1    |  INFO | Apache ActiveMQ 5.16.3 (localhost, ID:CZBK-20210302VL-10257-1646035577620-0:1) is starting
jvm 1    |  INFO | Listening for connections at: tcp://CZBK-20210302VL:61616?maximumConnections=1000&wireFormat.maxFrameSize=104857600
jvm 1    |  INFO | Connector openwire started
jvm 1    | ERROR | Failed to start Apache ActiveMQ (localhost, ID:CZBK-20210302VL-10257-1646035577620-0:1)
jvm 1    | java.io.IOException: Transport Connector could not be registered in JMX: java.io.IOException: Failed to bind to server socket: amqp://0.0.0.0:5672?maximumConnections=1000&wireFormat.maxFrameSize=104857600 due to: java.net.BindException: Address already in use: JVM_Bind
jvm 1    |      at org.apache.activemq.util.IOExceptionSupport.create(IOExceptionSupport.java:28)
jvm 1    |      at org.apache.activemq.broker.BrokerService.registerConnectorMBean(BrokerService.java:2288)
jvm 1    |      at org.apache.activemq.broker.BrokerService.startTransportConnector(BrokerService.java:2769)
jvm 1    |      at org.apache.activemq.broker.BrokerService.startAllConnectors(BrokerService.java:2665)
jvm 1    |      at org.apache.activemq.broker.BrokerService.doStartBroker(BrokerService.java:780)
jvm 1    |      at org.apache.activemq.broker.BrokerService.startBroker(BrokerService.java:742)
jvm 1    |      at org.apache.activemq.broker.BrokerService.start(BrokerService.java:645)
jvm 1    |      at org.apache.activemq.xbean.XBeanBrokerService.afterPropertiesSet(XBeanBrokerService.java:73)
jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
jvm 1    |      at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
jvm 1    |      at java.lang.reflect.Method.invoke(Method.java:498)
jvm 1    |      at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeCustomInitMethod(AbstractAutowireCapableBeanFactory.java:1748)
jvm 1    |      at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.invokeInitMethods(AbstractAutowireCapableBeanFactory.java:1685)
jvm 1    |      at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.initializeBean(AbstractAutowireCapableBeanFactory.java:1615)
jvm 1    |      at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:553)
jvm 1    |      at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:481)
jvm 1    |      at org.springframework.beans.factory.support.AbstractBeanFactory$1.getObject(AbstractBeanFactory.java:312)
jvm 1    |      at org.springframework.beans.factory.support.DefaultSingletonBeanRegistry.getSingleton(DefaultSingletonBeanRegistry.java:230)
jvm 1    |      at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:308)
jvm 1    |      at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:197)
jvm 1    |      at org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:756)
jvm 1    |      at org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:867)
jvm 1    |      at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:542)
jvm 1    |      at org.apache.xbean.spring.context.ResourceXmlApplicationContext.<init>(ResourceXmlApplicationContext.java:64)
jvm 1    |      at org.apache.xbean.spring.context.ResourceXmlApplicationContext.<init>(ResourceXmlApplicationContext.java:52)
jvm 1    |      at org.apache.activemq.xbean.XBeanBrokerFactory$1.<init>(XBeanBrokerFactory.java:104)
jvm 1    |      at org.apache.activemq.xbean.XBeanBrokerFactory.createApplicationContext(XBeanBrokerFactory.java:104)
jvm 1    |      at org.apache.activemq.xbean.XBeanBrokerFactory.createBroker(XBeanBrokerFactory.java:67)
jvm 1    |      at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:71)
jvm 1    |      at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:54)
jvm 1    |      at org.apache.activemq.console.command.StartCommand.runTask(StartCommand.java:87)
jvm 1    |      at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:63)
jvm 1    |      at org.apache.activemq.console.command.ShellCommand.runTask(ShellCommand.java:154)
jvm 1    |      at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:63)
jvm 1    |      at org.apache.activemq.console.command.ShellCommand.main(ShellCommand.java:104)
jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
jvm 1    |      at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
jvm 1    |      at java.lang.reflect.Method.invoke(Method.java:498)
jvm 1    |      at org.apache.activemq.console.Main.runTaskClass(Main.java:262)
jvm 1    |      at org.apache.activemq.console.Main.main(Main.java:115)
jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
jvm 1    |      at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
jvm 1    |      at java.lang.reflect.Method.invoke(Method.java:498)
jvm 1    |      at org.tanukisoftware.wrapper.WrapperSimpleApp.run(WrapperSimpleApp.java:240)
jvm 1    |      at java.lang.Thread.run(Thread.java:748)
jvm 1    | Caused by: java.io.IOException: Failed to bind to server socket: amqp://0.0.0.0:5672?maximumConnections=1000&wireFormat.maxFrameSize=104857600 due to: java.net.BindException: Address already in use: JVM_Bind
jvm 1    |      at org.apache.activemq.util.IOExceptionSupport.create(IOExceptionSupport.java:34)
jvm 1    |      at org.apache.activemq.transport.tcp.TcpTransportServer.bind(TcpTransportServer.java:146)
jvm 1    |      at org.apache.activemq.transport.tcp.TcpTransportFactory.doBind(TcpTransportFactory.java:62)
jvm 1    |      at org.apache.activemq.transport.TransportFactorySupport.bind(TransportFactorySupport.java:40)
jvm 1    |      at org.apache.activemq.broker.TransportConnector.createTransportServer(TransportConnector.java:335)
jvm 1    |      at org.apache.activemq.broker.TransportConnector.getServer(TransportConnector.java:145)
jvm 1    |      at org.apache.activemq.broker.TransportConnector.asManagedConnector(TransportConnector.java:110)
jvm 1    |      at org.apache.activemq.broker.BrokerService.registerConnectorMBean(BrokerService.java:2283)
jvm 1    |      ... 46 more
jvm 1    | Caused by: java.net.BindException: Address already in use: JVM_Bind
jvm 1    |      at java.net.DualStackPlainSocketImpl.bind0(Native Method)
jvm 1    |      at java.net.DualStackPlainSocketImpl.socketBind(DualStackPlainSocketImpl.java:106)
jvm 1    |      at java.net.AbstractPlainSocketImpl.bind(AbstractPlainSocketImpl.java:387)
jvm 1    |      at java.net.PlainSocketImpl.bind(PlainSocketImpl.java:190)
jvm 1    |      at java.net.ServerSocket.bind(ServerSocket.java:375)
jvm 1    |      at java.net.ServerSocket.<init>(ServerSocket.java:237)
jvm 1    |      at javax.net.DefaultServerSocketFactory.createServerSocket(ServerSocketFactory.java:231)
jvm 1    |      at org.apache.activemq.transport.tcp.TcpTransportServer.bind(TcpTransportServer.java:143)
jvm 1    |      ... 52 more
jvm 1    |  INFO | Apache ActiveMQ 5.16.3 (localhost, ID:CZBK-20210302VL-10257-1646035577620-0:1) is shutting down
jvm 1    |  INFO | socketQueue interrupted - stopping
jvm 1    |  INFO | Connector openwire stopped
jvm 1    |  INFO | Could not accept connection during shutdown  : null (null)
jvm 1    |  INFO | Connector amqp stopped
jvm 1    |  INFO | Connector stomp stopped
jvm 1    |  INFO | Connector mqtt stopped
jvm 1    |  INFO | Connector ws stopped
jvm 1    |  INFO | PListStore:[D:\soft\activemq\bin\win64\..\..\data\localhost\tmp_storage] stopped
jvm 1    |  INFO | Stopping async queue tasks
jvm 1    |  INFO | Stopping async topic tasks
jvm 1    |  INFO | Stopped KahaDB
jvm 1    |  INFO | Apache ActiveMQ 5.16.3 (localhost, ID:CZBK-20210302VL-10257-1646035577620-0:1) uptime 0.426 seconds
jvm 1    |  INFO | Apache ActiveMQ 5.16.3 (localhost, ID:CZBK-20210302VL-10257-1646035577620-0:1) is shutdown
jvm 1    |  INFO | Closing org.apache.activemq.xbean.XBeanBrokerFactory$1@2c9392f5: startup date [Mon Feb 28 16:06:16 CST 2022]; root of context hierarchy
jvm 1    |  WARN | Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'org.apache.activemq.xbean.XBeanBrokerService#0' defined in class path resource [activemq.xml]: Invocation of init method failed; nested exception is java.io.IOException: Transport Connector could not be registered in JMX: java.io.IOException: Failed to bind to server socket: amqp://0.0.0.0:5672?maximumConnections=1000&wireFormat.maxFrameSize=104857600 due to: java.net.BindException: Address already in use: JVM_Bind
jvm 1    | ERROR: java.lang.RuntimeException: Failed to execute start task. Reason: java.lang.IllegalStateException: BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContext
jvm 1    | java.lang.RuntimeException: Failed to execute start task. Reason: java.lang.IllegalStateException: BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContext
jvm 1    |      at org.apache.activemq.console.command.StartCommand.runTask(StartCommand.java:91)
jvm 1    |      at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:63)
jvm 1    |      at org.apache.activemq.console.command.ShellCommand.runTask(ShellCommand.java:154)
jvm 1    |      at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:63)
jvm 1    |      at org.apache.activemq.console.command.ShellCommand.main(ShellCommand.java:104)
jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
jvm 1    |      at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
jvm 1    |      at java.lang.reflect.Method.invoke(Method.java:498)
jvm 1    |      at org.apache.activemq.console.Main.runTaskClass(Main.java:262)
jvm 1    |      at org.apache.activemq.console.Main.main(Main.java:115)
jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
jvm 1    |      at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
jvm 1    |      at java.lang.reflect.Method.invoke(Method.java:498)
jvm 1    |      at org.tanukisoftware.wrapper.WrapperSimpleApp.run(WrapperSimpleApp.java:240)
jvm 1    |      at java.lang.Thread.run(Thread.java:748)
jvm 1    | Caused by: java.lang.IllegalStateException: BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContext
jvm 1    |      at org.springframework.context.support.AbstractRefreshableApplicationContext.getBeanFactory(AbstractRefreshableApplicationContext.java:164)
jvm 1    |      at org.springframework.context.support.AbstractApplicationContext.destroyBeans(AbstractApplicationContext.java:1034)
jvm 1    |      at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:555)
jvm 1    |      at org.apache.xbean.spring.context.ResourceXmlApplicationContext.<init>(ResourceXmlApplicationContext.java:64)
jvm 1    |      at org.apache.xbean.spring.context.ResourceXmlApplicationContext.<init>(ResourceXmlApplicationContext.java:52)
jvm 1    |      at org.apache.activemq.xbean.XBeanBrokerFactory$1.<init>(XBeanBrokerFactory.java:104)
jvm 1    |      at org.apache.activemq.xbean.XBeanBrokerFactory.createApplicationContext(XBeanBrokerFactory.java:104)
jvm 1    |      at org.apache.activemq.xbean.XBeanBrokerFactory.createBroker(XBeanBrokerFactory.java:67)
jvm 1    |      at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:71)
jvm 1    |      at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:54)
jvm 1    |      at org.apache.activemq.console.command.StartCommand.runTask(StartCommand.java:87)
jvm 1    |      ... 16 more
jvm 1    | ERROR: java.lang.IllegalStateException: BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContext
jvm 1    | java.lang.IllegalStateException: BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContext
jvm 1    |      at org.springframework.context.support.AbstractRefreshableApplicationContext.getBeanFactory(AbstractRefreshableApplicationContext.java:164)
jvm 1    |      at org.springframework.context.support.AbstractApplicationContext.destroyBeans(AbstractApplicationContext.java:1034)
jvm 1    |      at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:555)
jvm 1    |      at org.apache.xbean.spring.context.ResourceXmlApplicationContext.<init>(ResourceXmlApplicationContext.java:64)
jvm 1    |      at org.apache.xbean.spring.context.ResourceXmlApplicationContext.<init>(ResourceXmlApplicationContext.java:52)
jvm 1    |      at org.apache.activemq.xbean.XBeanBrokerFactory$1.<init>(XBeanBrokerFactory.java:104)
jvm 1    |      at org.apache.activemq.xbean.XBeanBrokerFactory.createApplicationContext(XBeanBrokerFactory.java:104)
jvm 1    |      at org.apache.activemq.xbean.XBeanBrokerFactory.createBroker(XBeanBrokerFactory.java:67)
jvm 1    |      at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:71)
jvm 1    |      at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:54)
jvm 1    |      at org.apache.activemq.console.command.StartCommand.runTask(StartCommand.java:87)
jvm 1    |      at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:63)
jvm 1    |      at org.apache.activemq.console.command.ShellCommand.runTask(ShellCommand.java:154)
jvm 1    |      at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:63)
jvm 1    |      at org.apache.activemq.console.command.ShellCommand.main(ShellCommand.java:104)
jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
jvm 1    |      at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
jvm 1    |      at java.lang.reflect.Method.invoke(Method.java:498)
jvm 1    |      at org.apache.activemq.console.Main.runTaskClass(Main.java:262)
jvm 1    |      at org.apache.activemq.console.Main.main(Main.java:115)
jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
jvm 1    |      at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
jvm 1    |      at java.lang.reflect.Method.invoke(Method.java:498)
jvm 1    |      at org.tanukisoftware.wrapper.WrapperSimpleApp.run(WrapperSimpleApp.java:240)
jvm 1    |      at java.lang.Thread.run(Thread.java:748)
wrapper  | <-- Wrapper Stopped
請按任意鍵繼續. . .

整合

做了這么多springboot整合第三方技術,已經摸到門路了,加坐標,做配置,調接口,直接開工


步驟①

導入springboot整合ActiveMQ的starter

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-activemq</artifactId>
</dependency>

步驟②

配置ActiveMQ的服務器地址

spring:activemq:broker-url: tcp://localhost:61616

步驟③

使用JmsMessagingTemplate操作ActiveMQ

@Service
public class MessageServiceActivemqImpl implements MessageService {@Autowiredprivate JmsMessagingTemplate messagingTemplate;@Overridepublic void sendMessage(String id) {System.out.println("待發送短信的訂單已納入處理隊列,id:"+id);messagingTemplate.convertAndSend("order.queue.id",id);}@Overridepublic String doMessage() {String id = messagingTemplate.receiveAndConvert("order.queue.id",String.class);System.out.println("已完成短信發送業務,id:"+id);return id;}
}

發送消息需要先將消息的類型轉換成字符串,然后再發送,所以是convertAndSend,定義消息發送的位置,和具體的消息內容,此處使用id作為消息內容。


接收消息需要先將消息接收到,然后再轉換成指定的數據類型,所以是receiveAndConvert,接收消息除了提供讀取的位置,還要給出轉換后的數據的具體類型。


步驟④

使用消息監聽器在服務器啟動后,監聽指定位置,當消息出現后,立即消費消息

@Component
public class MessageListener {@JmsListener(destination = "order.queue.id")@SendTo("order.other.queue.id")public String receive(String id){System.out.println("已完成短信發送業務,id:"+id);return "new:"+id;}
}

使用注解@JmsListener定義當前方法監聽ActiveMQ中指定名稱的消息隊列。


如果當前消息隊列處理完還需要繼續向下傳遞當前消息到另一個隊列中使用注解@SendTo即可,這樣即可構造連續執行的順序消息隊列。


步驟⑤

切換消息模型由點對點模型到發布訂閱模型,修改jms配置即可

spring:activemq:broker-url: tcp://localhost:61616jms:pub-sub-domain: true
ception: Transport Connector could not be registered in JMX: java.io.IOException: Failed to bind to server socket: amqp://0.0.0.0:5672?maximumConnections=1000&wireFormat.maxFrameSize=104857600 due to: java.net.BindException: Address already in use: JVM_Bind
jvm 1    | ERROR: java.lang.RuntimeException: Failed to execute start task. Reason: java.lang.IllegalStateException: BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContext
jvm 1    | java.lang.RuntimeException: Failed to execute start task. Reason: java.lang.IllegalStateException: BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContext
jvm 1    |      at org.apache.activemq.console.command.StartCommand.runTask(StartCommand.java:91)
jvm 1    |      at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:63)
jvm 1    |      at org.apache.activemq.console.command.ShellCommand.runTask(ShellCommand.java:154)
jvm 1    |      at org.apache.activemq.console.command.AbstractCommand.execute(AbstractCommand.java:63)
jvm 1    |      at org.apache.activemq.console.command.ShellCommand.main(ShellCommand.java:104)
jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
jvm 1    |      at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
jvm 1    |      at java.lang.reflect.Method.invoke(Method.java:498)
jvm 1    |      at org.apache.activemq.console.Main.runTaskClass(Main.java:262)
jvm 1    |      at org.apache.activemq.console.Main.main(Main.java:115)
jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
jvm 1    |      at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
jvm 1    |      at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
jvm 1    |      at java.lang.reflect.Method.invoke(Method.java:498)
jvm 1    |      at org.tanukisoftware.wrapper.WrapperSimpleApp.run(WrapperSimpleApp.java:240)
jvm 1    |      at java.lang.Thread.run(Thread.java:748)
jvm 1    | Caused by: java.lang.IllegalStateException: BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContext
jvm 1    |      at org.springframework.context.support.AbstractRefreshableApplicationContext.getBeanFactory(AbstractRefreshableApplicationContext.java:164)
jvm 1    |      at org.springframework.context.support.AbstractApplicationContext.destroyBeans(AbstractApplicationContext.java:1034)
jvm 1    |      at org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:555)
jvm 1    |      at org.apache.xbean.spring.context.ResourceXmlApplicationContext.<init>(ResourceXmlApplicationContext.java:64)
jvm 1    |      at org.apache.xbean.spring.context.ResourceXmlApplicationContext.<init>(ResourceXmlApplicationContext.java:52)
jvm 1    |      at org.apache.activemq.xbean.XBeanBrokerFactory$1.<init>(XBeanBrokerFactory.java:104)
jvm 1    |      at org.apache.activemq.xbean.XBeanBrokerFactory.createApplicationContext(XBeanBrokerFactory.java:104)
jvm 1    |      at org.apache.activemq.xbean.XBeanBrokerFactory.createBroker(XBeanBrokerFactory.java:67)
jvm 1    |      at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:71)
jvm 1    |      at org.apache.activemq.broker.BrokerFactory.createBroker(BrokerFactory.java:54)
jvm 1    |      at org.apache.activemq.console.command.StartCommand.runTask(StartCommand.java:87)
jvm 1    |      ... 16 more
jvm 1    | ERROR: java.lang.IllegalStateException: BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContext
jvm 1    | java.lang.IllegalStateException: BeanFactory not initialized or already closed - call 'refresh' before accessing beans via the ApplicationContext
jv

本文來自互聯網用戶投稿,該文觀點僅代表作者本人,不代表本站立場。本站僅提供信息存儲空間服務,不擁有所有權,不承擔相關法律責任。
如若轉載,請注明出處:http://www.pswp.cn/news/212731.shtml
繁體地址,請注明出處:http://hk.pswp.cn/news/212731.shtml
英文地址,請注明出處:http://en.pswp.cn/news/212731.shtml

如若內容造成侵權/違法違規/事實不符,請聯系多彩編程網進行投訴反饋email:809451989@qq.com,一經查實,立即刪除!

相關文章

【頭歌系統數據庫實驗】實驗8 SQL的復雜多表查詢-2

目錄 第1關&#xff1a;基于派生表查詢每個隊員解答中超過他平均memory的user_id及題目編號problem_id 第2關&#xff1a;用ANY/ALL實現查詢2019級選手&#xff08;user_id前4位為2019&#xff09;滿足比2020級其中一個選手注冊時間早即可的選手 第3關&#xff1a;用聚集查詢…

python zblog API實現類似XMLRPC/發布文章

我發現python對Zblog的XML發布并不友好&#xff0c;雖然也有對應的模塊&#xff0c;但是遠遠沒有XPCRPC更直接方便&#xff0c;但是使用xmlRpc是直接給發布文章帶來了不小的便利&#xff0c;但是對系統也并不友好&#xff0c;但是zblog也開放了Api&#xff0c;但是干部子弟不樂…

UE小:物品拼裝功能

藍圖B1的實現步驟&#xff1a; 獲取玩家控制器和視角&#xff1a;首先獲取玩家控制器&#xff0c;然后使用Deproject Screen to World節點將屏幕上的鼠標位置轉換為世界空間中的一條射線。 射線檢測&#xff1a;使用Line Trace by Channel或Line Trace for Objects節點發射射線…

深度學習測試流程

深度學習模型測試的功能旨在驗證模型在各種情況下的性能和魯棒性。以下是深度學習模型測試的主要功能&#xff1a; 性能評估&#xff1a; 測試模型在任務目標上的整體性能&#xff0c;例如分類準確性、回歸誤差等。評估指標的選擇取決于具體的任務類型。 泛化能力&#xff1a;…

《信息技術時代》期刊雜志論文發表投稿

《信息技術時代》期刊收稿方向&#xff1a;通信工程、大數據、計算機、辦公自動化、信息或計算機教育、電子技術、系統設計、移動信息、圖情信息研究、人工智能、智能技術、信息技術與網絡安全等。 刊名&#xff1a;信息技術時代 主管主辦單位&#xff1a;深圳灣科技發展有限…

C++筆記之int、size_t、uint8_t、unsigned char*區別

C筆記之int、size_t、uint8_t、unsigned char*區別 code review! 文章目錄 C筆記之int、size_t、uint8_t、unsigned char*區別1.ChatGPT第一次查詢解釋2.ChatGPT第二次查詢解釋3.分別的使用示例 1.ChatGPT第一次查詢解釋 size_t、uint8_t 和 int 是編程中使用的不同類型&…

《微信小程序開發從入門到實戰》學習四十七

4.4 云函數 4.4.5 云函數的定時觸發 如果云函數需要定時執行&#xff0c;可以使用云函數定時觸發器。配置了定時觸發器&#xff0c;云函數會在相應時間點被自動觸發。函數返回結果不會返回調用方 在需要添加觸發器的云函數下新建文件config.json。格式如下&#xff1a; &quo…

05-詳解調用服務時負載均衡的配置及其原理

負載均衡 負載均衡的原理(通用) LoadBalanced注解用來攔截它所標記的RestTemplate發起的http請求, 底層是利用了一個名為Ribbon的組件來實現負載均衡功能(Cloud高版本已經棄用) LoadBalancerInterceptor的intercept方法會對RestTemplate的請求進行攔截 public class LoadBal…

【Linux】使用Bash和GNU Parallel并行解壓縮文件

介紹 在本教程中&#xff0c;我們將學習如何使用Bash腳本和GNU Parallel實現高效并行解壓縮多個文件。這種方法在處理大量文件時可以顯著加快提取過程。 先決條件 確保系統上已安裝以下內容&#xff1a; BashGNU Parallel 你可以使用以下命令在不同Linux系統上安裝它們&am…

【數據庫】分支與循環函數存儲過程

目錄 函數 【1】 【2】 【3】? 【4】 存儲過程 思考&分析 函數 【1】 --&#xff08;1&#xff09;定義一個根據學生姓名查詢該生學習課程的函數stu_count。 --create function stu_count(sname char(8))--學生姓名 --returns char(12)--學習課程 --begin --…

layui分頁laypage結合Flask+Jinja2實現流程

Layui2.0普通用法<!DOCTYPE html> <html> <head><meta charset"utf-8"><meta name"viewport" content"widthdevice-width, initial-scale1"><title>Demo</title><!-- 請勿在項目正式環境中引用該 …

uniapp iOS離線打包——運行項目到模擬器報錯?

運行項目、打包時報錯問題 記錄個人在開發過程中遇到的相關問題&#xff0c;后續有時間會不定時更新 文章目錄 運行項目、打包時報錯問題運行到模擬器報錯解決方案 打包報錯解決方案 運行到模擬器報錯 解決方案 選中項目工程 —> Build Settings 滑動底部 —> User-Defi…

Java基礎課的中下基礎課04

目錄 二十三、集合相關 23.1 集合 &#xff08;1&#xff09;集合的分支 23.2 List有序可重復集合 &#xff08;1&#xff09;ArrayList類 &#xff08;2&#xff09;泛型 &#xff08;3&#xff09;ArrayList常用方法 &#xff08;4&#xff09;Vector類 &#xff08;…

gin博客項目開發日志1

gin項目博客系統偽第一代 項目概述 1.1 目標 實現一個功能完整、易用的博客系統&#xff0c;允許用戶發布、編輯和管理博客文章。 1.2 背景 看到網上有很多語言寫的博客系統&#xff0c;但go的卻很少&#xff0c;正好&#xff0c;現在我在學go&#xff0c;可以拿博客來練練…

組件之間傳值

目錄 1&#xff1a;組件中的關系 2&#xff1a;父向子傳值 3&#xff1a;子組件向父組件共享數據 4&#xff1a;兄弟組件數據共享 1&#xff1a;組件中的關系 在項目中使用到的組件關系最常用兩種是&#xff0c;父子關系&#xff0c;兄弟關系 例如A組件使用B組件或者C組件…

深入了解UDP協議:特點、應用場景及市面上常見軟件案例

目錄 引言 UDP的特點 UDP的應用場景 市面上使用UDP的軟件案例 結論 引言 在計算機網絡中&#xff0c;UDP&#xff08;User Datagram Protocol&#xff09;是一種面向無連接、無狀態的傳輸層協議。與TCP相比&#xff0c;UDP具有獨特的特點和適用場景。本文將深入探討UDP協…

解碼方法dp

1.狀態表示 2.狀態轉移方程 3.初始化 4.填表順序 從左往右 5.返回值 dp[n-1] 6.處理邊界問題以及初始化問題的技巧

Docker筆記:數據卷掛載的三種方式及物理機與容器內的環境變量的傳遞

容器數據掛載到物理機上的三種方式 1 &#xff09; 實名(指定路徑)掛載數據卷 docker run -v 物理機目錄:容器內目錄 鏡像id示例 docker run -it -d --name mynginx_p_v -p 82:80 -v /root/www:/usr/share/nginx/html nginx此時訪問 會報 403 forbidden因為 物理機上的 root/…

Electron[5] 渲染進程和主進程

1 進程 Electron里頭的進程分為渲染進程和主進程。簡單理解&#xff1a; main.js就是主進程每個頁面就是渲染進程一個Electron應用僅有一個主進程&#xff0c;可以有多個渲染進程 上面的這些概念很重要&#xff0c;不展開細講。 2 進程職責 主進程是用來實現應用的基礎功能…

【小沐學Python】Python實現TTS文本轉語音(speech、pyttsx3、百度AI)

文章目錄 1、簡介2、Windows語音2.1 簡介2.2 安裝2.3 代碼 3、pyttsx33.1 簡介3.2 安裝3.3 代碼 4、ggts4.1 簡介4.2 安裝4.3 代碼 5、SAPI6、SpeechLib7、百度AI8、百度飛槳結語 1、簡介 TTS(Text To Speech) 譯為從文本到語音&#xff0c;TTS是人工智能AI的一個模組&#xf…