<output id="qn6qe"></output>

    1. <output id="qn6qe"><tt id="qn6qe"></tt></output>
    2. <strike id="qn6qe"></strike>

      亚洲 日本 欧洲 欧美 视频,日韩中文字幕有码av,一本一道av中文字幕无码,国产线播放免费人成视频播放,人妻少妇偷人无码视频,日夜啪啪一区二区三区,国产尤物精品自在拍视频首页,久热这里只有精品12

      springboot2(nosql,整合第三方技術,緩存,任務,郵件,消息隊列,監控)

      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

      啟動服務器

      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

      步驟②:進行基礎配置

      spring:
        redis:
          host: localhost
          port: 6379
      

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

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

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

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

      image

      總結

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

      StringRedisTemplate

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

      @SpringBootTest
      public class StringRedisTemplateTest {
          @Autowired
          private StringRedisTemplate stringRedisTemplate;
          @Test
          void 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: localhost
          port: 6379
          client-type: jedis
      

      步驟③:根據需要設置對應的配置

      spring:
        redis:
          host: localhost
          port: 6379
          client-type: jedis
          lettuce:
            pool:
              max-active: 16
          jedis:
            pool:
              max-active: 16
      

      lettcus與jedis區別

      • jedis連接Redis服務器是直連模式,當多線程模式下使用jedis會存在線程安全問題,解決方案可以通過配置連接池使每個連接專用,這樣整體性能就大受影響
      • 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的場景:

      • 淘寶用戶數據
        • 存儲位置:數據庫
        • 特征:永久性存儲,修改頻度極低
      • 游戲裝備數據、游戲道具數據
        • 存儲位置:數據庫、Mongodb
        • 特征:永久性存儲與臨時存儲相結合、修改頻度較高
      • 直播數據、打賞數據、粉絲數據
        • 存儲位置:數據庫、Mongodb
        • 特征:永久性存儲與臨時存儲相結合,修改頻度極高
      • 物聯網數據
        • 存儲位置:Mongodb
        • 特征:臨時存儲,修改頻度飛速

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

      安裝

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

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

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

      image

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

      image

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

      image

      ? 根據下列方案即可解決,在瀏覽器中搜索提示缺少的名稱對應的文件,并下載,將下載的文件拷貝到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

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

      image

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

      image

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

      image

      ? 連接成功后在命令輸入區域輸入命令即可操作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

      步驟②:進行基礎配置

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

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

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

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

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

      總結

      1. 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

        • 例如id為1的數據中名稱這一項的值是“中華人民共和國”,那么分詞結束后,就會出現“中華”對應id為1,“人民”對應id為1,“共和國”對應id為1

        • 例如id為2的數據中名稱這一項的值是“人民代表大會“,那么分詞結束后,就會出現“人民”對應id為2,“代表”對應id為2,“大會”對應id為2

        • 此時就會出現如下對應結果,按照上述形式可以對所有文檔進行分詞。需要注意分詞的過程不是僅對一個字段進行,而是對每一個參與查詢的字段都執行,最終結果匯總到一個表格中

          分詞結果關鍵字 對應id
          中華 1
          人民 1,2
          共和國 1
          代表 2
          大會 2
      3. 當進行查詢時,如果輸入“人民”作為查詢條件,可以通過上述表格數據進行比對,得到id值1,2,然后根據id值就可以得到查詢的結果數據了。

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

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

      安裝

      ? windows版安裝包下載地址:https://www.elastic.co/cn/downloads/elasticsearch

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

      image

      • bin目錄:包含所有的可執行命令
      • config目錄:包含ES服務器使用的配置文件
      • jdk目錄:此目錄中包含了一個完整的jdk工具包,版本17,當ES升級時,使用最新版本的jdk確保不會出現版本支持性不足的問題
      • lib目錄:包含ES運行的依賴jar文件
      • logs目錄:包含ES運行后產生的所有日志文件
      • modules目錄:包含ES軟件中所有的功能模塊,也是一個一個的jar包。和jar目錄不同,jar目錄是ES運行期間依賴的jar包,modules是ES軟件自己的功能jar包
      • 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風格的請求來進行,也就是說發送一個請求就可以執行一個操作。比如新建索引,刪除索引這些操作都可以使用發送請求的形式來進行。

      • 創建索引,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
        }
        
      • 查詢索引

        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
        }
        
      • 刪除索引

        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
        }
        
      • 創建索引并指定分詞器

        ? 前面創建的索引是未指定分詞器的,可以在創建索引時添加請求參數,設置分詞器。目前國內較為流行的分詞器是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中稱數據為文檔,下面進行文檔操作。

      • 添加文檔,有三種方式

        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"
        }  
        
      • 查詢文檔

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

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

        DELETE請求	http://localhost:9200/books/_doc/1
        
      • 修改文檔(全量更新)

        PUT請求	http://localhost:9200/books/_doc/1
        
        文檔通過請求參數傳遞,數據格式json
        {
            "name":"springboot",
            "type":"springboot",
            "description":"springboot"
        }
        
      • 修改文檔(部分更新)

        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 {
          @Autowired
          private 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;
            @Test
            void 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;
            @Test
            void 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;
      
          @Test
          void 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就先學習到這里吧。

      總結

      1. 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

      ? 應用程序直接與數據庫打交道,訪問效率低

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

      image

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

      ? 緩存是什么?緩存是一種介于數據永久存儲介質與應用程序之間的數據臨時存儲介質,使用緩存可以有效的減少低速數據讀取過程的次數(例如磁盤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 {
          @Autowired
          private BookDao bookDao;
      
          @Cacheable(value="cacheSpace",key="#id")
          public Book getById(Integer id) {
              return bookDao.selectById(id);
          }
      }
      

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

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

      手機驗證碼案例

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

      ? 手機驗證碼案例需求如下:

      • 輸入手機號獲取驗證碼,組織文檔以短信形式發送給用戶(頁面模擬)
      • 輸入手機號和驗證碼驗證結果

      ? 為了描述上述操作,我們制作兩個表現層接口,一個用來模擬發送短信的過程,其實就是根據用戶提供的手機號生成一個驗證碼,然后放入緩存,另一個用來模擬驗證碼校驗的過程,其實就是使用傳入的手機號和驗證碼進行匹配,并返回最終匹配結果。下面直接制作本案例的模擬代碼,先以上例中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 {
          @Autowired
          private CodeUtils codeUtils;
      
          @CachePut(value = "smsCode", key = "#tele")
          public String sendCodeToSMS(String tele) {
              String code = codeUtils.generator(tele);
              return code;
          }
      
          public boolean checkCode(SMSCode smsCode) {
              //取出內存中的驗證碼與傳遞過來的驗證碼比對,如果相同,返回true
              String 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 {
          @Autowired
          private SMSCodeService smsCodeService;
          
          @GetMapping
          public String getCode(String tele){
              String code = smsCodeService.sendCodeToSMS(tele);
              return code;
          }
          
          @PostMapping
          public 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: ehcache
          ehcache:
            config: ehcache.xml
      

      ? 配置緩存的類型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:緩存清除策略-->
          <defaultCache
              eternal="false"
              diskPersistent="false"
              maxElementsInMemory="1000"
              overflowToDisk="false"
              timeToIdleSeconds="60"
              timeToLiveSeconds="60"
              memoryStoreEvictionPolicy="LRU" />
      
          <cache
              name="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: localhost
          port: 6379
        cache:
          type: redis
      

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

      spring:
        redis:
          host: localhost
          port: 6379
        cache:
          type: redis
          redis:
            use-key-prefix: false
            key-prefix: sms_
            cache-null-values: false
            time-to-live: 10s
      

      總結

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

      SpringBoot整合Memcached緩存

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

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

      安裝

      ? windows版安裝包下載地址:https://www.runoob.com/memcached/window-install-memcached.html

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

      image

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

      image

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

      image

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

      memcached.exe -d install
      

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

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

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

      image

      變更緩存為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 {
          @Bean
          public 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 {
          @Autowired
          private CodeUtils codeUtils;
          @Autowired
          private 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操作,其實更符合我們開發者的習慣。

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

      定義配置屬性

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

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

        @Component
        @ConfigurationProperties(prefix = "memcached")
        @Data
        public class XMemcachedProperties {
            private String servers;
            private int poolSize;
            private long opTimeout;
        }
        
      • 定義memcached節點信息

        memcached:
          servers: localhost:11211
          poolSize: 10
          opTimeout: 3000
        
      • 在memcached配置類中加載信息

      @Configuration
      public class XMemcachedConfig {
          @Autowired
          private XMemcachedProperties props;
          @Bean
          public 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支持的緩存方案本地緩存支持兩種,遠程緩存支持兩種,分別如下:

      • 本地緩存(Local)
        • LinkedHashMap
        • Caffeine
      • 遠程緩存(Remote)
        • Redis
        • 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: redis
            host: localhost
            port: 6379
            poolConfig:
              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 {
          @Autowired
          private 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: redis
            host: localhost
            port: 6379
            poolConfig:
              maxTotal: 50
          sms:
            type: redis
            host: localhost
            port: 6379
            poolConfig:
              maxTotal: 50
      

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

      @Service
      public class SMSCodeServiceImpl implements SMSCodeService {
          @Autowired
          private 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: linkedhashmap
            keyConvertor: 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: linkedhashmap
            keyConvertor: fastjson
        remote:
          default:
            type: redis
            host: localhost
            port: 6379
            poolConfig:
              maxTotal: 50
          sms:
            type: redis
            host: localhost
            port: 6379
            poolConfig:
              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.statIntervalMinutes 0 統計間隔,0表示不統計
      jetcache.hiddenPackages 自動生成name時,隱藏指定的包名前綴
      jetcache.[local|remote].${area}.type 緩存類型,本地支持linkedhashmap、caffeine,遠程支持redis、tair
      jetcache.[local|remote].${area}.keyConvertor key轉換器,當前僅支持fastjson
      jetcache.[local|remote].${area}.valueEncoder java 僅remote類型的緩存需要指定,可選java和kryo
      jetcache.[local|remote].${area}.valueDecoder java 僅remote類型的緩存需要指定,可選java和kryo
      jetcache.[local|remote].${area}.limit 100 僅local類型的緩存需要指定,緩存實例最大元素數
      jetcache.[local|remote].${area}.expireAfterWriteInMillis 無窮大 默認過期時間,毫秒單位
      jetcache.local.${area}.expireAfterAccessInMillis 0 僅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: linkedhashmap
            keyConvertor: fastjson
        remote:
          default:
            type: redis
            host: localhost
            port: 6379
            keyConvertor: fastjson
            valueEncode: java
            valueDecode: java
            poolConfig:
              maxTotal: 50
          sms:
            type: redis
            host: localhost
            port: 6379
            poolConfig:
              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 {
          @Autowired
          private 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 {
          @Autowired
          private CodeUtils codeUtils;
      
          @Autowired
          private 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:6379
      
      redis.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的概念。

      • 工作(Job):用于定義具體執行的工作
      • 工作明細(JobDetail):用于描述定時工作相關的信息
      • 觸發器(Trigger):描述了工作明細與調度器的對應關系
      • 調度器(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 {
          @Override
          protected void executeInternal(JobExecutionContext context) throws JobExecutionException {
              System.out.println("quartz task run...");
          }
      }
      

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

      @Configuration
      public class QuartzConfig {
          @Bean
          public JobDetail printJobDetail(){
              //綁定具體的工作
              return JobBuilder.newJob(MyQuartz.class).storeDurably().build();
          }
          @Bean
          public 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							# 任務調度線程池大小 默認 1
            thread-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個概念,這些概念規范了郵件操作過程中的標準。

      • SMTP(Simple Mail Transfer Protocol):簡單郵件傳輸協議,用于發送電子郵件的傳輸協議
      • POP3(Post Office Protocol - Version 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.com
          username: test@126.com
          password: test
      

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

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

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

      image

      步驟③:使用JavaMailSender接口發送郵件

      @Service
      public class SendMailServiceImpl implements SendMailService {
          @Autowired
          private JavaMailSender javaMailSender;
      
          //發送人
          private String from = "test@qq.com";
          //接收人
          private String to = "test@126.com";
          //標題
          private String subject = "測試郵件";
          //正文
          private String context = "測試郵件正文內容";
      
          @Override
          public 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 {
          @Autowired
          private 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 ;
      
          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 {
          @Autowired
          private 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種消息。

      • TextMessage
      • MapMessage
      • BytesMessage
      • StreamMessage
      • ObjectMessage
      • 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消息模型

      • direct exchange
      • fanout exchange
      • topic exchange
      • headers exchange
      • system exchange

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

      MQTT

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

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

      KafKa

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

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

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

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

      ? 手機驗證碼案例需求如下:

      • 執行下單業務時(模擬此過程),調用消息服務,將要發送短信的訂單id傳遞給消息中間件

      • 消息處理服務接收到要發送的訂單id后輸出訂單id(模擬發短信)

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

      訂單業務

      ? 業務層接口

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

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

      ? 業務層實現

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

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

      ? 表現層服務

      @RestController
      @RequestMapping("/orders")
      public class OrderController {
      
          @Autowired
          private 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>();
      
          @Override
          public void sendMessage(String id) {
              System.out.println("待發送短信的訂單已納入處理隊列,id:"+id);
              msgList.add(id);
          }
      
          @Override
          public String doMessage() {
              String id = msgList.remove(0);
              System.out.println("已完成短信發送業務,id:"+id);
              return id;
          }
      }
      

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

      ? 表現層服務

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

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

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

      SpringBoot整合ActiveMQ

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

      安裝

      ? windows版安裝包下載地址:https://activemq.apache.org/components/classic/download/

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

      image

      啟動服務器

      activemq.bat
      

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

      訪問web管理服務

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

      http://127.0.0.1:8161/
      

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

      image

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

      image

      ? 看到上述界面視為啟動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 {
          @Autowired
          private JmsMessagingTemplate messagingTemplate;
      
          @Override
          public void sendMessage(String id) {
              System.out.println("待發送短信的訂單已納入處理隊列,id:"+id);
              messagingTemplate.convertAndSend("order.queue.id",id);
          }
      
          @Override
          public 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:61616
        jms:
          pub-sub-domain: true
      

      ? pub-sub-domain默認值為false,即點對點模型,修改為true后就是發布訂閱模型。

      總結

      1. springboot整合ActiveMQ提供了JmsMessagingTemplate對象作為客戶端操作消息隊列
      2. 操作ActiveMQ需要配置ActiveMQ服務器地址,默認端口61616
      3. 企業開發時通常使用監聽器來處理消息隊列中的消息,設置監聽器使用注解@JmsListener
      4. 配置jms的pub-sub-domain屬性可以在點對點模型和發布訂閱模型間切換消息模型

      SpringBoot整合RabbitMQ

      ? RabbitMQ是MQ產品中的目前較為流行的產品之一,它遵從AMQP協議。RabbitMQ的底層實現語言使用的是Erlang,所以安裝RabbitMQ需要先安裝Erlang。

      Erlang安裝

      ? windows版安裝包下載地址:https??/www.erlang.org/downloads

      ? 下載完畢后得到exe安裝文件,一鍵傻瓜式安裝,安裝完畢需要重啟,需要重啟,需要重啟。

      ? 安裝的過程中可能會出現依賴Windows組件的提示,根據提示下載安裝即可,都是自動執行的,如下:

      image

      ? Erlang安裝后需要配置環境變量,否則RabbitMQ將無法找到安裝的Erlang。需要配置項如下,作用等同JDK配置環境變量的作用。

      • ERLANG_HOME
      • PATH
      安裝

      ? windows版安裝包下載地址:https://rabbitmq.com/install-windows.html

      ? 下載完畢后得到exe安裝文件,一鍵傻瓜式安裝,安裝完畢后會得到如下文件

      image

      啟動服務器

      rabbitmq-service.bat start		# 啟動服務
      rabbitmq-service.bat stop		# 停止服務
      rabbitmqctl status				# 查看服務狀態
      

      ? 運行sbin目錄下的rabbitmq-service.bat命令即可,start參數表示啟動,stop參數表示退出,默認對外服務端口5672。

      ? 注意:啟動rabbitmq的過程實際上是開啟rabbitmq對應的系統服務,需要管理員權限方可執行。

      ? 說明:有沒有感覺5672的服務端口很熟悉?activemq與rabbitmq有一個端口沖突問題,學習階段無論操作哪一個?請確保另一個處于關閉狀態。

      ? 說明:不喜歡命令行的小伙伴可以使用任務管理器中的服務頁,找到RabbitMQ服務,使用鼠標右鍵菜單控制服務的啟停。

      image

      訪問web管理服務

      ? RabbitMQ也提供有web控制臺服務,但是此功能是一個插件,需要先啟用才可以使用。

      rabbitmq-plugins.bat list							# 查看當前所有插件的運行狀態
      rabbitmq-plugins.bat enable rabbitmq_management		# 啟動rabbitmq_management插件
      

      ? 啟動插件后可以在插件運行狀態中查看是否運行,運行后通過瀏覽器即可打開服務后臺管理界面

      http://localhost:15672
      

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

      image

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

      image

      整合(direct模型)

      ? RabbitMQ滿足AMQP協議,因此不同的消息模型對應的制作不同,先使用最簡單的direct模型開發。

      步驟①:導入springboot整合amqp的starter,amqp協議默認實現為rabbitmq方案

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

      步驟②:配置RabbitMQ的服務器地址

      spring:
        rabbitmq:
          host: localhost
          port: 5672
      

      步驟③:初始化直連模式系統設置

      ? 由于RabbitMQ不同模型要使用不同的交換機,因此需要先初始化RabbitMQ相關的對象,例如隊列,交換機等

      @Configuration
      public class RabbitConfigDirect {
          @Bean
          public Queue directQueue(){
              return new Queue("direct_queue");
          }
          @Bean
          public Queue directQueue2(){
              return new Queue("direct_queue2");
          }
          @Bean
          public DirectExchange directExchange(){
              return new DirectExchange("directExchange");
          }
          @Bean
          public Binding bindingDirect(){
              return BindingBuilder.bind(directQueue()).to(directExchange()).with("direct");
          }
          @Bean
          public Binding bindingDirect2(){
              return BindingBuilder.bind(directQueue2()).to(directExchange()).with("direct2");
          }
      }
      

      ? 隊列Queue與直連交換機DirectExchange創建后,還需要綁定他們之間的關系Binding,這樣就可以通過交換機操作對應隊列。

      步驟④:使用AmqpTemplate操作RabbitMQ

      @Service
      public class MessageServiceRabbitmqDirectImpl implements MessageService {
          @Autowired
          private AmqpTemplate amqpTemplate;
      
          @Override
          public void sendMessage(String id) {
              System.out.println("待發送短信的訂單已納入處理隊列(rabbitmq direct),id:"+id);
              amqpTemplate.convertAndSend("directExchange","direct",id);
          }
      }
      

      ? amqp協議中的操作API接口名稱看上去和jms規范的操作API接口很相似,但是傳遞參數差異很大。

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

      @Component
      public class MessageListener {
          @RabbitListener(queues = "direct_queue")
          public void receive(String id){
              System.out.println("已完成短信發送業務(rabbitmq direct),id:"+id);
          }
      }
      

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

      整合(topic模型)

      步驟①:同上

      步驟②:同上

      步驟③:初始化主題模式系統設置

      @Configuration
      public class RabbitConfigTopic {
          @Bean
          public Queue topicQueue(){
              return new Queue("topic_queue");
          }
          @Bean
          public Queue topicQueue2(){
              return new Queue("topic_queue2");
          }
          @Bean
          public TopicExchange topicExchange(){
              return new TopicExchange("topicExchange");
          }
          @Bean
          public Binding bindingTopic(){
              return BindingBuilder.bind(topicQueue()).to(topicExchange()).with("topic.*.id");
          }
          @Bean
          public Binding bindingTopic2(){
              return BindingBuilder.bind(topicQueue2()).to(topicExchange()).with("topic.orders.*");
          }
      }
      

      ? 主題模式支持routingKey匹配模式,*表示匹配一個單詞,#表示匹配任意內容,這樣就可以通過主題交換機將消息分發到不同的隊列中,詳細內容請參看RabbitMQ系列課程。

      匹配鍵 topic.*.* topic.#
      topic.order.id true true
      order.topic.id false false
      topic.sm.order.id false true
      topic.sm.id false true
      topic.id.order true true
      topic.id false true
      topic.order false true

      步驟④:使用AmqpTemplate操作RabbitMQ

      @Service
      public class MessageServiceRabbitmqTopicImpl implements MessageService {
          @Autowired
          private AmqpTemplate amqpTemplate;
      
          @Override
          public void sendMessage(String id) {
              System.out.println("待發送短信的訂單已納入處理隊列(rabbitmq topic),id:"+id);
              amqpTemplate.convertAndSend("topicExchange","topic.orders.id",id);
          }
      }
      

      ? 發送消息后,根據當前提供的routingKey與綁定交換機時設定的routingKey進行匹配,規則匹配成功消息才會進入到對應的隊列中。

      步驟⑤:使用消息監聽器在服務器啟動后,監聽指定隊列

      @Component
      public class MessageListener {
          @RabbitListener(queues = "topic_queue")
          public void receive(String id){
              System.out.println("已完成短信發送業務(rabbitmq topic 1),id:"+id);
          }
          @RabbitListener(queues = "topic_queue2")
          public void receive2(String id){
              System.out.println("已完成短信發送業務(rabbitmq topic 22222222),id:"+id);
          }
      }
      

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

      總結

      1. springboot整合RabbitMQ提供了AmqpTemplate對象作為客戶端操作消息隊列
      2. 操作ActiveMQ需要配置ActiveMQ服務器地址,默認端口5672
      3. 企業開發時通常使用監聽器來處理消息隊列中的消息,設置監聽器使用注解@RabbitListener
      4. RabbitMQ有5種消息模型,使用的隊列相同,但是交換機不同。交換機不同,對應的消息進入的策略也不同

      SpringBoot整合RocketMQ

      ? RocketMQ由阿里研發,后捐贈給apache基金會,目前是apache基金會頂級項目之一,也是目前市面上的MQ產品中較為流行的產品之一,它遵從AMQP協議。

      安裝

      ? windows版安裝包下載地址:https://rocketmq.apache.org/

      ? 下載完畢后得到zip壓縮文件,解壓縮即可使用,解壓后得到如下文件

      image

      ? RocketMQ安裝后需要配置環境變量,具體如下:

      • ROCKETMQ_HOME
      • PATH
      • NAMESRV_ADDR (建議): 127.0.0.1:9876

      ? 關于NAMESRV_ADDR對于初學者來說建議配置此項,也可以通過命令設置對應值,操作略顯繁瑣,建議配置。系統學習RocketMQ知識后即可靈活控制該項。

      RocketMQ工作模式

      ? 在RocketMQ中,處理業務的服務器稱為broker,生產者與消費者不是直接與broker聯系的,而是通過命名服務器進行通信。broker啟動后會通知命名服務器自己已經上線,這樣命名服務器中就保存有所有的broker信息。當生產者與消費者需要連接broker時,通過命名服務器找到對應的處理業務的broker,因此命名服務器在整套結構中起到一個信息中心的作用。并且broker啟動前必須保障命名服務器先啟動。

      image

      啟動服務器

      mqnamesrv		# 啟動命名服務器
      mqbroker		# 啟動broker
      

      ? 運行bin目錄下的mqnamesrv命令即可啟動命名服務器,默認對外服務端口9876。

      ? 運行bin目錄下的mqbroker命令即可啟動broker服務器,如果環境變量中沒有設置NAMESRV_ADDR則需要在運行mqbroker指令前通過set指令設置NAMESRV_ADDR的值,并且每次開啟均需要設置此項。

      測試服務器啟動狀態

      ? RocketMQ提供有一套測試服務器功能的測試程序,運行bin目錄下的tools命令即可使用。

      tools org.apache.rocketmq.example.quickstart.Producer		# 生產消息
      tools org.apache.rocketmq.example.quickstart.Consumer		# 消費消息
      
      整合(異步消息)

      步驟①:導入springboot整合RocketMQ的starter,此坐標不由springboot維護版本

      <dependency>
          <groupId>org.apache.rocketmq</groupId>
          <artifactId>rocketmq-spring-boot-starter</artifactId>
          <version>2.2.1</version>
      </dependency>
      

      步驟②:配置RocketMQ的服務器地址

      rocketmq:
        name-server: localhost:9876
        producer:
          group: group_rocketmq
      

      ? 設置默認的生產者消費者所屬組group。

      步驟③:使用RocketMQTemplate操作RocketMQ

      @Service
      public class MessageServiceRocketmqImpl implements MessageService {
          @Autowired
          private RocketMQTemplate rocketMQTemplate;
      
          @Override
          public void sendMessage(String id) {
              System.out.println("待發送短信的訂單已納入處理隊列(rocketmq),id:"+id);
              SendCallback callback = new SendCallback() {
                  @Override
                  public void onSuccess(SendResult sendResult) {
                      System.out.println("消息發送成功");
                  }
                  @Override
                  public void onException(Throwable e) {
                      System.out.println("消息發送失敗!!!!!");
                  }
              };
              rocketMQTemplate.asyncSend("order_id",id,callback);
          }
      }
      
      

      ? 使用asyncSend方法發送異步消息。

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

      @Component
      @RocketMQMessageListener(topic = "order_id",consumerGroup = "group_rocketmq")
      public class MessageListener implements RocketMQListener<String> {
          @Override
          public void onMessage(String id) {
              System.out.println("已完成短信發送業務(rocketmq),id:"+id);
          }
      }
      

      ? RocketMQ的監聽器必須按照標準格式開發,實現RocketMQListener接口,泛型為消息類型。

      ? 使用注解@RocketMQMessageListener定義當前類監聽RabbitMQ中指定組、指定名稱的消息隊列。

      總結

      1. springboot整合RocketMQ使用RocketMQTemplate對象作為客戶端操作消息隊列
      2. 操作RocketMQ需要配置RocketMQ服務器地址,默認端口9876
      3. 企業開發時通常使用監聽器來處理消息隊列中的消息,設置監聽器使用注解@RocketMQMessageListener

      SpringBoot整合Kafka

      安裝

      ? windows版安裝包下載地址:https://kafka.apache.org/downloads

      ? 下載完畢后得到tgz壓縮文件,使用解壓縮軟件解壓縮即可使用,解壓后得到如下文件

      image

      ? 建議使用windows版2.8.1版本。

      啟動服務器

      ? kafka服務器的功能相當于RocketMQ中的broker,kafka運行還需要一個類似于命名服務器的服務。在kafka安裝目錄中自帶一個類似于命名服務器的工具,叫做zookeeper,它的作用是注冊中心,相關知識請到對應課程中學習。

      zookeeper-server-start.bat ..\..\config\zookeeper.properties		# 啟動zookeeper
      kafka-server-start.bat ..\..\config\server.properties				# 啟動kafka
      

      ? 運行bin目錄下的windows目錄下的zookeeper-server-start命令即可啟動注冊中心,默認對外服務端口2181。

      ? 運行bin目錄下的windows目錄下的kafka-server-start命令即可啟動kafka服務器,默認對外服務端口9092。

      創建主題

      ? 和之前操作其他MQ產品相似,kakfa也是基于主題操作,操作之前需要先初始化topic。

      # 創建topic
      kafka-topics.bat --create --zookeeper localhost:2181 --replication-factor 1 --partitions 1 --topic itheima
      # 查詢topic
      kafka-topics.bat --zookeeper 127.0.0.1:2181 --list					
      # 刪除topic
      kafka-topics.bat --delete --zookeeper localhost:2181 --topic itheima
      

      測試服務器啟動狀態

      ? Kafka提供有一套測試服務器功能的測試程序,運行bin目錄下的windows目錄下的命令即可使用。

      kafka-console-producer.bat --broker-list localhost:9092 --topic itheima							# 測試生產消息
      kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic itheima --from-beginning	# 測試消息消費
      
      整合

      步驟①:導入springboot整合Kafka的starter,此坐標由springboot維護版本

      <dependency>
          <groupId>org.springframework.kafka</groupId>
          <artifactId>spring-kafka</artifactId>
      </dependency>
      

      步驟②:配置Kafka的服務器地址

      spring:
        kafka:
          bootstrap-servers: localhost:9092
          consumer:
            group-id: order
      

      ? 設置默認的生產者消費者所屬組id。

      步驟③:使用KafkaTemplate操作Kafka

      @Service
      public class MessageServiceKafkaImpl implements MessageService {
          @Autowired
          private KafkaTemplate<String,String> kafkaTemplate;
      
          @Override
          public void sendMessage(String id) {
              System.out.println("待發送短信的訂單已納入處理隊列(kafka),id:"+id);
              kafkaTemplate.send("itheima2022",id);
          }
      }
      

      ? 使用send方法發送消息,需要傳入topic名稱。

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

      @Component
      public class MessageListener {
          @KafkaListener(topics = "itheima2022")
          public void onMessage(ConsumerRecord<String,String> record){
              System.out.println("已完成短信發送業務(kafka),id:"+record.value());
          }
      }
      

      ? 使用注解@KafkaListener定義當前方法監聽Kafka中指定topic的消息,接收到的消息封裝在對象ConsumerRecord中,獲取數據從ConsumerRecord對象中獲取即可。

      總結

      1. springboot整合Kafka使用KafkaTemplate對象作為客戶端操作消息隊列

      2. 操作Kafka需要配置Kafka服務器地址,默認端口9092

      3. 企業開發時通常使用監聽器來處理消息隊列中的消息,設置監聽器使用注解@KafkaListener。接收消息保存在形參ConsumerRecord對象中

      KF-6.監控

      ? 在說監控之前,需要回顧一下軟件業的發展史。最早的軟件完成一些非常簡單的功能,代碼不多,錯誤也少。隨著軟件功能的逐步完善,軟件的功能變得越來越復雜,功能不能得到有效的保障,這個階段出現了針對軟件功能的檢測,也就是軟件測試。伴隨著計算機操作系統的逐步升級,軟件的運行狀態也變得開始讓人捉摸不透,出現了不穩定的狀況。伴隨著計算機網絡的發展,程序也從單機狀態切換成基于計算機網絡的程序,應用于網絡的程序開始出現,由于網絡的不穩定性,程序的運行狀態讓使用者更加堪憂。互聯網的出現徹底打破了軟件的思維模式,隨之而來的互聯網軟件就更加凸顯出應對各種各樣復雜的網絡情況之下的弱小。計算機軟件的運行狀況已經成為了軟件運行的一個大話題,針對軟件的運行狀況就出現了全新的思維,建立起了初代的軟件運行狀態監控。

      ? 什么是監控?就是通過軟件的方式展示另一個軟件的運行情況,運行的情況則通過各種各樣的指標數據反饋給監控人員。例如網絡是否順暢、服務器是否在運行、程序的功能是否能夠整百分百運行成功,內存是否夠用,等等等等。

      ? 本章要講解的監控就是對軟件的運行情況進行監督,但是springboot程序與非springboot程序的差異還是很大的,為了方便監控軟件的開發,springboot提供了一套功能接口,為開發者加速開發過程。

      KF-6-1.監控的意義

      ? 對于現代的互聯網程序來說,規模越來越大,功能越來越復雜,還要追求更好的客戶體驗,因此要監控的信息量也就比較大了。由于現在的互聯網程序大部分都是基于微服務的程序,一個程序的運行需要若干個服務來保障,因此第一個要監控的指標就是服務是否正常運行,也就是監控服務狀態是否處理宕機狀態。一旦發現某個服務宕機了,必須馬上給出對應的解決方案,避免整體應用功能受影響。其次,由于互聯網程序服務的客戶量是巨大的,當客戶的請求在短時間內集中達到服務器后,就會出現各種程序運行指標的波動。比如內存占用嚴重,請求無法及時響應處理等,這就是第二個要監控的重要指標,監控服務運行指標。雖然軟件是對外提供用戶的訪問需求,完成對應功能的,但是后臺的運行是否平穩,是否出現了不影響客戶使用的功能隱患,這些也是要密切監控的,此時就需要在不停機的情況下,監控系統運行情況,日志是一個不錯的手段。如果在眾多日志中找到開發者或運維人員所關注的日志信息,簡單快速有效的過濾出要看的日志也是監控系統需要考慮的問題,這就是第三個要監控的指標,監控程序運行日志。雖然我們期望程序一直平穩運行,但是由于突發情況的出現,例如服務器被攻擊、服務器內存溢出等情況造成了服務器宕機,此時當前服務不能滿足使用需要,就要將其重啟甚至關閉,如果快速控制服務器的啟停也是程序運行過程中不可回避的問題,這就是第四個監控項,管理服務狀態。以上這些僅僅是從大的方面來思考監控這個問題,還有很多的細節點,例如上線了一個新功能,定時提醒用戶續費,這種功能不是上線后馬上就運行的,但是當前功能是否真的啟動,如果快速的查詢到這個功能已經開啟,這也是監控中要解決的問題,等等。看來監控真的是一項非常重要的工作。

      ? 通過上述描述,可以看出監控很重要。那具體的監控要如何開展呢?還要從實際的程序運行角度出發。比如現在有3個服務支撐著一個程序的運行,每個服務都有自己的運行狀態。

      image

      ? 此時被監控的信息就要在三個不同的程序中去查詢并展示,但是三個服務是服務于一個程序的運行的,如果不能合并到一個平臺上展示,監控工作量巨大,而且信息對稱性差,要不停的在三個監控端查看數據。如果將業務放大成30個,300個,3000個呢?看來必須有一個單獨的平臺,將多個被監控的服務對應的監控指標信息匯總在一起,這樣更利于監控工作的開展。

      image

      ? 新的程序專門用來監控,新的問題就出現了,是被監控程序主動上報信息還是監控程序主動獲取信息?如果監控程序不能主動獲取信息,這就意味著監控程序有可能看到的是很久之前被監控程序上報的信息,萬一被監控程序宕機了,監控程序就無法區分究竟是好久沒法信息了,還是已經下線了。所以監控程序必須具有主動發起請求獲取被監控服務信息的能力。

      image

      ? 如果監控程序要監控服務時,主動獲取對方的信息。那監控程序如何知道哪些程序被自己監控呢?不可能在監控程序中設置我監控誰,這樣互聯網上的所有程序豈不是都可以被監控到,這樣的話信息安全將無法得到保障。合理的做法只能是在被監控程序啟動時上報監控程序,告訴監控程序你可以監控我了。看來需要在被監控程序端做主動上報的操作,這就要求被監控程序中配置對應的監控程序是誰。

      image

      ? 被監控程序可以提供各種各樣的指標數據給監控程序看,但是每一個指標都代表著公司的機密信息,并不是所有的指標都可以給任何人看的,乃至運維人員,所以對被監控指標的是否開放出來給監控系統看,也需要做詳細的設定。

      ? 以上描述的整個過程就是一個監控系統的基本流程。

      總結

      1. 監控是一個非常重要的工作,是保障程序正常運行的基礎手段
      2. 監控的過程通過一個監控程序進行,它匯總所有被監控的程序的信息集中統一展示
      3. 被監控程序需要主動上報自己被監控,同時要設置哪些指標被監控

      思考

      ? 下面就要開始做監控了,新的問題就來了,監控程序怎么做呢?難道要自己寫嗎?肯定是不現實的,如何進行監控,咱們下節再講。

      KF-6-2.可視化監控平臺

      ? springboot抽取了大部分監控系統的常用指標,提出了監控的總思想。然后就有好心的同志根據監控的總思想,制作了一個通用性很強的監控系統,因為是基于springboot監控的核心思想制作的,所以這個程序被命名為Spring Boot Admin

      ? Spring Boot Admin,這是一個開源社區項目,用于管理和監控SpringBoot應用程序。這個項目中包含有客戶端和服務端兩部分,而監控平臺指的就是服務端。我們做的程序如果需要被監控,將我們做的程序制作成客戶端,然后配置服務端地址后,服務端就可以通過HTTP請求的方式從客戶端獲取對應的信息,并通過UI界面展示對應信息。

      ? 下面就來開發這套監控程序,先制作服務端,其實服務端可以理解為是一個web程序,收到一些信息后展示這些信息。

      服務端開發

      步驟①:導入springboot admin對應的starter,版本與當前使用的springboot版本保持一致,并將其配置成web工程

      <dependency>
          <groupId>de.codecentric</groupId>
          <artifactId>spring-boot-admin-starter-server</artifactId>
          <version>2.5.4</version>
      </dependency>
      
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      

      ? 上述過程可以通過創建項目時使用勾選的形式完成。

      image

      步驟②:在引導類上添加注解@EnableAdminServer,聲明當前應用啟動后作為SpringBootAdmin的服務器使用

      @SpringBootApplication
      @EnableAdminServer
      public class Springboot25AdminServerApplication {
          public static void main(String[] args) {
              SpringApplication.run(Springboot25AdminServerApplication.class, args);
          }
      }
      

      ? 做到這里,這個服務器就開發好了,啟動后就可以訪問當前程序了,界面如下。

      image

      ? 由于目前沒有啟動任何被監控的程序,所以里面什么信息都沒有。下面制作一個被監控的客戶端程序。

      客戶端開發

      ? 客戶端程序開發其實和服務端開發思路基本相似,多了一些配置而已。

      步驟①:導入springboot admin對應的starter,版本與當前使用的springboot版本保持一致,并將其配置成web工程

      <dependency>
          <groupId>de.codecentric</groupId>
          <artifactId>spring-boot-admin-starter-client</artifactId>
          <version>2.5.4</version>
      </dependency>
      
      <dependency>
          <groupId>org.springframework.boot</groupId>
          <artifactId>spring-boot-starter-web</artifactId>
      </dependency>
      

      ? 上述過程也可以通過創建項目時使用勾選的形式完成,不過一定要小心,端口配置成不一樣的,否則會沖突。

      步驟②:設置當前客戶端將信息上傳到哪個服務器上,通過yml文件配置

      spring:
        boot:
          admin:
            client:
              url: http://localhost:8080
      

      ? 做到這里,這個客戶端就可以啟動了。啟動后再次訪問服務端程序,界面如下。

      image

      ? 可以看到,當前監控了1個程序,點擊進去查看詳細信息。

      image

      ? 由于當前沒有設置開放哪些信息給監控服務器,所以目前看不到什么有效的信息。下面需要做兩組配置就可以看到信息了。

      1. 開放指定信息給服務器看

      2. 允許服務器以HTTP請求的方式獲取對應的信息

        配置如下:

      server:
        port: 80
      spring:
        boot:
          admin:
            client:
              url: http://localhost:8080
      management:
        endpoint:
          health:
            show-details: always
        endpoints:
          web:
            exposure:
              include: "*"
      

      ? 上述配置對于初學者來說比較容易混淆。簡單解釋一下,到下一節再做具體的講解。springbootadmin的客戶端默認開放了13組信息給服務器,但是這些信息除了一個之外,其他的信息都不讓通過HTTP請求查看。所以你看到的信息基本上就沒什么內容了,只能看到一個內容,就是下面的健康信息。

      image

      ? 但是即便如此我們看到健康信息中也沒什么內容,原因在于健康信息中有一些信息描述了你當前應用使用了什么技術等信息,如果無腦的對外暴露功能會有安全隱患。通過配置就可以開放所有的健康信息明細查看了。

      management:
        endpoint:
          health:
            show-details: always
      

      ? 健康明細信息如下:

      image

      ? 目前除了健康信息,其他信息都查閱不了。原因在于其他12種信息是默認不提供給服務器通過HTTP請求查閱的,所以需要開啟查閱的內容項,使用*表示查閱全部。記得帶引號。

      endpoints:
        web:
          exposure:
            include: "*"
      

      ? 配置后再刷新服務器頁面,就可以看到所有的信息了。

      image

      ? 以上界面中展示的信息量就非常大了,包含了13組信息,有性能指標監控,加載的bean列表,加載的系統屬性,日志的顯示控制等等。

      配置多個客戶端

      ? 可以通過配置客戶端的方式在其他的springboot程序中添加客戶端坐標,這樣當前服務器就可以監控多個客戶端程序了。每個客戶端展示不同的監控信息。

      image

      ? 進入監控面板,如果你加載的應用具有功能,在監控面板中可以看到3組信息展示的與之前加載的空工程不一樣。

      • 類加載面板中可以查閱到開發者自定義的類,如左圖

      image
      image

      • 映射中可以查閱到當前應用配置的所有請求

      image
      image

      • 性能指標中可以查閱當前應用獨有的請求路徑統計數據

      image
      image

      總結

      1. 開發監控服務端需要導入坐標,然后在引導類上添加注解@EnableAdminServer,并將其配置成web程序即可
      2. 開發被監控的客戶端需要導入坐標,然后配置服務端服務器地址,并做開放指標的設定即可
      3. 在監控平臺中可以查閱到各種各樣被監控的指標,前提是客戶端開放了被監控的指標

      思考

      ? 之前說過,服務端要想監控客戶端,需要主動的獲取到對應信息并展示出來。但是目前我們并沒有在客戶端開發任何新的功能,但是服務端確可以獲取監控信息,誰幫我們做的這些功能呢?咱們下一節再講。

      KF-6-3.監控原理

      ? 通過查閱監控中的映射指標,可以看到當前系統中可以運行的所有請求路徑,其中大部分路徑以/actuator開頭

      image

      ? 首先這些請求路徑不是開發者自己編寫的,其次這個路徑代表什么含義呢?既然這個路徑可以訪問,就可以通過瀏覽器發送該請求看看究竟可以得到什么信息。

      image

      ? 通過發送請求,可以得到一組json信息,如下

      {
          "_links": {
              "self": {
                  "href": "http://localhost:81/actuator",
                  "templated": false
              },
              "beans": {
                  "href": "http://localhost:81/actuator/beans",
                  "templated": false
              },
              "caches-cache": {
                  "href": "http://localhost:81/actuator/caches/{cache}",
                  "templated": true
              },
              "caches": {
                  "href": "http://localhost:81/actuator/caches",
                  "templated": false
              },
              "health": {
                  "href": "http://localhost:81/actuator/health",
                  "templated": false
              },
              "health-path": {
                  "href": "http://localhost:81/actuator/health/{*path}",
                  "templated": true
              },
              "info": {
                  "href": "http://localhost:81/actuator/info",
                  "templated": false
              },
              "conditions": {
                  "href": "http://localhost:81/actuator/conditions",
                  "templated": false
              },
              "shutdown": {
                  "href": "http://localhost:81/actuator/shutdown",
                  "templated": false
              },
              "configprops": {
                  "href": "http://localhost:81/actuator/configprops",
                  "templated": false
              },
              "configprops-prefix": {
                  "href": "http://localhost:81/actuator/configprops/{prefix}",
                  "templated": true
              },
              "env": {
                  "href": "http://localhost:81/actuator/env",
                  "templated": false
              },
              "env-toMatch": {
                  "href": "http://localhost:81/actuator/env/{toMatch}",
                  "templated": true
              },
              "loggers": {
                  "href": "http://localhost:81/actuator/loggers",
                  "templated": false
              },
              "loggers-name": {
                  "href": "http://localhost:81/actuator/loggers/{name}",
                  "templated": true
              },
              "heapdump": {
                  "href": "http://localhost:81/actuator/heapdump",
                  "templated": false
              },
              "threaddump": {
                  "href": "http://localhost:81/actuator/threaddump",
                  "templated": false
              },
              "metrics-requiredMetricName": {
                  "href": "http://localhost:81/actuator/metrics/{requiredMetricName}",
                  "templated": true
              },
              "metrics": {
                  "href": "http://localhost:81/actuator/metrics",
                  "templated": false
              },
              "scheduledtasks": {
                  "href": "http://localhost:81/actuator/scheduledtasks",
                  "templated": false
              },
              "mappings": {
                  "href": "http://localhost:81/actuator/mappings",
                  "templated": false
              }
          }
      }
      

      ? 其中每一組數據都有一個請求路徑,而在這里請求路徑中有之前看到過的health,發送此請求又得到了一組信息

      {
          "status": "UP",
          "components": {
              "diskSpace": {
                  "status": "UP",
                  "details": {
                      "total": 297042808832,
                      "free": 72284409856,
                      "threshold": 10485760,
                      "exists": true
                  }
              },
              "ping": {
                  "status": "UP"
              }
          }
      }
      

      ? 當前信息與監控面板中的數據存在著對應關系

      image

      ? 原來監控中顯示的信息實際上是通過發送請求后得到json數據,然后展示出來。按照上述操作,可以發送更多的以/actuator開頭的鏈接地址,獲取更多的數據,這些數據匯總到一起組成了監控平臺顯示的所有數據。

      ? 到這里我們得到了一個核心信息,監控平臺中顯示的信息實際上是通過對被監控的應用發送請求得到的。那這些請求誰開發的呢?打開被監控應用的pom文件,其中導入了springboot admin的對應的client,在這個資源中導入了一個名稱叫做actuator的包。被監控的應用之所以可以對外提供上述請求路徑,就是因為添加了這個包。

      image

      ? 這個actuator是什么呢?這就是本節要講的核心內容,監控的端點。

      ? Actuator,可以稱為端點,描述了一組監控信息,SpringBootAdmin提供了多個內置端點,通過訪問端點就可以獲取對應的監控信息,也可以根據需要自定義端點信息。通過發送請求路勁/actuator可以訪問應用所有端點信息,如果端點中還有明細信息可以發送請求/actuator/端點名稱來獲取詳細信息。以下列出了所有端點信息說明:

      ID 描述 默認啟用
      auditevents 暴露當前應用程序的審計事件信息。
      beans 顯示應用程序中所有 Spring bean 的完整列表。
      caches 暴露可用的緩存。
      conditions 顯示在配置和自動配置類上評估的條件以及它們匹配或不匹配的原因。
      configprops 顯示所有 @ConfigurationProperties 的校對清單。
      env 暴露 Spring ConfigurableEnvironment 中的屬性。
      flyway 顯示已應用的 Flyway 數據庫遷移。
      health 顯示應用程序健康信息
      httptrace 顯示 HTTP 追蹤信息(默認情況下,最后 100 個 HTTP 請求/響應交換)。
      info 顯示應用程序信息。
      integrationgraph 顯示 Spring Integration 圖。
      loggers 顯示和修改應用程序中日志記錄器的配置。
      liquibase 顯示已應用的 Liquibase 數據庫遷移。
      metrics 顯示當前應用程序的指標度量信息。
      mappings 顯示所有 @RequestMapping 路徑的整理清單。
      scheduledtasks 顯示應用程序中的調度任務。
      sessions 允許從 Spring Session 支持的會話存儲中檢索和刪除用戶會話。當使用 Spring Session 的響應式 Web 應用程序支持時不可用。
      shutdown 正常關閉應用程序。
      threaddump 執行線程 dump。
      heapdump 返回一個 hprof 堆 dump 文件。
      jolokia 通過 HTTP 暴露 JMX bean(當 Jolokia 在 classpath 上時,不適用于 WebFlux)。
      logfile 返回日志文件的內容(如果已設置 logging.file 或 logging.path 屬性)。支持使用 HTTP Range 頭來檢索部分日志文件的內容。
      prometheus 以可以由 Prometheus 服務器抓取的格式暴露指標。

      ? 上述端點每一項代表被監控的指標,如果對外開放則監控平臺可以查詢到對應的端點信息,如果未開放則無法查詢對應的端點信息。通過配置可以設置端點是否對外開放功能。使用enable屬性控制端點是否對外開放。其中health端點為默認端點,不能關閉。

      management:
        endpoint:
          health:						# 端點名稱
            show-details: always
          info:						# 端點名稱
            enabled: true				# 是否開放
      

      ? 為了方便開發者快速配置端點,springboot admin設置了13個較為常用的端點作為默認開放的端點,如果需要控制默認開放的端點的開放狀態,可以通過配置設置,如下:

      management:
        endpoints:
          enabled-by-default: true	# 是否開啟默認端點,默認值true
      

      ? 上述端點開啟后,就可以通過端點對應的路徑查看對應的信息了。但是此時還不能通過HTTP請求查詢此信息,還需要開啟通過HTTP請求查詢的端點名稱,使用“*”可以簡化配置成開放所有端點的WEB端HTTP請求權限。

      management:
        endpoints:
          web:
            exposure:
              include: "*"
      

      ? 整體上來說,對于端點的配置有兩組信息,一組是endpoints開頭的,對所有端點進行配置,一組是endpoint開頭的,對具體端點進行配置。

      management:
        endpoint:		# 具體端點的配置
          health:
            show-details: always
          info:
            enabled: true
        endpoints:	# 全部端點的配置
          web:
            exposure:
              include: "*"
          enabled-by-default: true
      

      總結

      1. 被監控客戶端通過添加actuator的坐標可以對外提供被訪問的端點功能

      2. 端點功能的開放與關閉可以通過配置進行控制

      3. web端默認無法獲取所有端點信息,通過配置開放端點功能

      KF-6-4.自定義監控指標

      ? 端點描述了被監控的信息,除了系統默認的指標,還可以自行添加顯示的指標,下面就通過3種不同的端點的指標自定義方式來學習端點信息的二次開發。

      INFO端點

      ? info端點描述了當前應用的基本信息,可以通過兩種形式快速配置info端點的信息

      • 配置形式

        在yml文件中通過設置info節點的信息就可以快速配置端點信息

        info:
          appName: @project.artifactId@
          version: @project.version@
          company: 傳智教育
          author: itheima
        

        配置完畢后,對應信息顯示在監控平臺上

      image

      也可以通過請求端點信息路徑獲取對應json信息

      image

      • 編程形式

        通過配置的形式只能添加固定的數據,如果需要動態數據還可以通過配置bean的方式為info端點添加信息,此信息與配置信息共存

        @Component
        public class InfoConfig implements InfoContributor {
            @Override
            public void contribute(Info.Builder builder) {
                builder.withDetail("runTime",System.currentTimeMillis());		//添加單個信息
                Map infoMap = new HashMap();		
                infoMap.put("buildTime","2006");
                builder.withDetails(infoMap);									//添加一組信息
            }
        }
        

      Health端點

      ? health端點描述當前應用的運行健康指標,即應用的運行是否成功。通過編程的形式可以擴展指標信息。

      @Component
      public class HealthConfig extends AbstractHealthIndicator {
          @Override
          protected void doHealthCheck(Health.Builder builder) throws Exception {
              boolean condition = true;
              if(condition) {
                  builder.status(Status.UP);					//設置運行狀態為啟動狀態
                  builder.withDetail("runTime", System.currentTimeMillis());
                  Map infoMap = new HashMap();
                  infoMap.put("buildTime", "2006");
                  builder.withDetails(infoMap);
              }else{
                  builder.status(Status.OUT_OF_SERVICE);		//設置運行狀態為不在服務狀態
                  builder.withDetail("上線了嗎?","你做夢");
              }
          }
      }
      

      ? 當任意一個組件狀態不為UP時,整體應用對外服務狀態為非UP狀態。

      image

      Metrics端點

      ? metrics端點描述了性能指標,除了系統自帶的監控性能指標,還可以自定義性能指標。

      @Service
      public class BookServiceImpl extends ServiceImpl<BookDao, Book> implements IBookService {
          @Autowired
          private BookDao bookDao;
      
          private Counter counter;
      
          public BookServiceImpl(MeterRegistry meterRegistry){
              counter = meterRegistry.counter("用戶付費操作次數:");
          }
      
          @Override
          public boolean delete(Integer id) {
              //每次執行刪除業務等同于執行了付費業務
              counter.increment();
              return bookDao.deleteById(id) > 0;
          }
      }
      

      ? 在性能指標中就出現了自定義的性能指標監控項

      image

      自定義端點

      ? 可以根據業務需要自定義端點,方便業務監控

      @Component
      @Endpoint(id="pay",enableByDefault = true)
      public class PayEndpoint {
          @ReadOperation
          public Object getPay(){
              Map payMap = new HashMap();
              payMap.put("level 1","300");
              payMap.put("level 2","291");
              payMap.put("level 3","666");
              return payMap;
          }
      }
      

      ? 由于此端點數據spirng boot admin無法預知該如何展示,所以通過界面無法看到此數據,通過HTTP請求路徑可以獲取到當前端點的信息,但是需要先開啟當前端點對外功能,或者設置當前端點為默認開發的端點。

      image

      總結

      1. 端點的指標可以自定義,但是每種不同的指標根據其功能不同,自定義方式不同
      2. info端點通過配置和編程的方式都可以添加端點指標
      3. health端點通過編程的方式添加端點指標,需要注意要為對應指標添加啟動狀態的邏輯設定
      4. metrics指標通過在業務中添加監控操作設置指標
      5. 可以自定義端點添加更多的指標

      開發實用篇完結

      ? 開發實用篇到這里就暫時完結了,在開發實用篇中我們講解了大量的第三方技術的整合方案,選擇的方案都是市面上比較流行的常用方案,還有一些國內流行度較低的方案目前還沒講,留到番外篇中慢慢講吧。

      ? 整體開發實用篇中講解的內容可以分為兩大類知識:實用性知識與經驗性知識。

      ? 實用性知識就是新知識了,springboot整合各種技術,每種技術整合中都有一些特殊操作,整體來說其實就是三句話。加坐標做配置調接口。經驗性知識是對前面兩篇中出現的一些知識的補充,在學習基礎篇時如果將精力放在這些東西上就有點學偏了,容易鉆牛角尖,放到實用開發篇中結合實際開發說一些不常見的但是對系統功能又危害的操作解決方案,提升理解。

      ? 開發實用篇做到這里就告一段落,下面就要著手準備原理篇了。市面上很多課程原理篇講的過于高深莫測,在新手還沒明白123的時候就開始講微積分了,著實讓人看了著急。至于原理篇我講成什么樣子?一起期待吧。

      posted @ 2025-10-24 23:15  a-tao必須奧利給  閱讀(25)  評論(0)    收藏  舉報
      主站蜘蛛池模板: 国产精品午夜福利在线观看| 亚洲综合色区另类av| 影音先锋亚洲成aⅴ人在| 国产黑色丝袜在线播放| 精品中文字幕人妻一二| 男女性高爱潮免费网站| 午夜高清福利在线观看| 国产午夜精品无码一区二区| 精品乱码一区二区三四五区| 亚洲码欧洲码一二三四五| 国产精品一区二区久久岳| 久久国产乱子伦免费精品无码 | 少妇激情av一区二区三区| 91久久偷偷做嫩草影院免费看| 日韩大片高清播放器| 亚洲精品综合网在线8050影院| 霍城县| 人妻激情视频一区二区三区| 黑森林福利视频导航| 我国产码在线观看av哈哈哈网站| 2020精品自拍视频曝光| 少妇人妻偷人精品系列| 国产精品亚洲一区二区z| 99久久久无码国产精品免费 | 国产激情一区二区三区不卡| 国产91午夜福利精品| jizz视频在线观看| 亚洲国产区男人本色vr| 永久免费AV无码网站YY| 欧美日韩中文字幕久久伊人| 精品国产免费第一区二区三区| 久久狠狠高潮亚洲精品夜色| 国产精品中文第一字幕| 日韩国产精品无码一区二区三区| 午夜福利日本一区二区无码| 亚洲av色香蕉一区二区三区精品| 久久精产国品一二三产品| 中国女人和老外的毛片 | 天堂mv在线mv免费mv香蕉| 日本狂喷奶水在线播放212| 国产婷婷精品av在线|