中文亚洲精品无码_熟女乱子伦免费_人人超碰人人爱国产_亚洲熟妇女综合网

當(dāng)前位置: 首頁(yè) > news >正文

福建省住房和城鄉(xiāng)建設(shè)廳的網(wǎng)站2024年最新時(shí)政熱點(diǎn)

福建省住房和城鄉(xiāng)建設(shè)廳的網(wǎng)站,2024年最新時(shí)政熱點(diǎn),宣傳冊(cè)設(shè)計(jì)與制作合同,衡陽(yáng)市住房和城鄉(xiāng)建設(shè)局官方網(wǎng)站SpringBoot開(kāi)發(fā)實(shí)用篇 KF-1.熱部署 ? 什么是熱部署?簡(jiǎn)單說(shuō)就是你程序改了,現(xiàn)在要重新啟動(dòng)服務(wù)器,嫌麻煩?不用重啟,服務(wù)器會(huì)自己悄悄的把更新后的程序給重新加載一遍,這就是熱部署。 ? 熱部署的功能是如…

SpringBoot開(kāi)發(fā)實(shí)用篇

KF-1.熱部署

? 什么是熱部署?簡(jiǎn)單說(shuō)就是你程序改了,現(xiàn)在要重新啟動(dòng)服務(wù)器,嫌麻煩?不用重啟,服務(wù)器會(huì)自己悄悄的把更新后的程序給重新加載一遍,這就是熱部署。

? 熱部署的功能是如何實(shí)現(xiàn)的呢?這就要分兩種情況來(lái)說(shuō)了,非springboot工程和springboot工程的熱部署實(shí)現(xiàn)方式完全不一樣。先說(shuō)一下原始的非springboot項(xiàng)目是如何實(shí)現(xiàn)熱部署的。

非springboot項(xiàng)目熱部署實(shí)現(xiàn)原理

? 開(kāi)發(fā)非springboot項(xiàng)目時(shí),我們要制作一個(gè)web工程并通過(guò)tomcat啟動(dòng),通常需要先安裝tomcat服務(wù)器到磁盤(pán)中,開(kāi)發(fā)的程序配置發(fā)布到安裝的tomcat服務(wù)器上。如果想實(shí)現(xiàn)熱部署的效果,這種情況其實(shí)有兩種做法,一種是在tomcat服務(wù)器的配置文件中進(jìn)行配置,這種做法與你使用什么IDE工具無(wú)關(guān),不管你使用eclipse還是idea都行。還有一種做法是通過(guò)IDE工具進(jìn)行配置,比如在idea工具中進(jìn)行設(shè)置,這種形式需要依賴IDE工具,每款I(lǐng)DE工具不同,對(duì)應(yīng)的配置也不太一樣。但是核心思想是一樣的,就是使用服務(wù)器去監(jiān)控其中加載的應(yīng)用,發(fā)現(xiàn)產(chǎn)生了變化就重新加載一次。

? 上面所說(shuō)的非springboot項(xiàng)目實(shí)現(xiàn)熱部署看上去是一個(gè)非常簡(jiǎn)單的過(guò)程,幾乎每個(gè)小伙伴都能自己寫(xiě)出來(lái)。如果你不會(huì)寫(xiě),我給你個(gè)最簡(jiǎn)單的思路,但是實(shí)際設(shè)計(jì)要比這復(fù)雜一些。例如啟動(dòng)一個(gè)定時(shí)任務(wù),任務(wù)啟動(dòng)時(shí)記錄每個(gè)文件的大小,以后每5秒比對(duì)一下每個(gè)文件的大小是否有改變,或者是否有新文件。如果沒(méi)有改變,放行,如果有改變,刷新當(dāng)前記錄的文件信息,然后重新啟動(dòng)服務(wù)器,這就可以實(shí)現(xiàn)熱部署了。當(dāng)然,這個(gè)過(guò)程肯定不能這么做,比如我把一個(gè)打印輸出的字符串"abc"改成"cba",比對(duì)大小是沒(méi)有變化的,但是內(nèi)容缺實(shí)變了,所以這么做肯定不行,只是給大家打個(gè)比方,而且重啟服務(wù)器這就是冷啟動(dòng)了,不能算熱部署,領(lǐng)會(huì)精神吧。

? 看上去這個(gè)過(guò)程也沒(méi)多復(fù)雜,在springboot項(xiàng)目中難道還有其他的彎彎繞嗎?還真有。

springboot項(xiàng)目熱部署實(shí)現(xiàn)原理

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

? 下面就說(shuō)說(shuō),怎么搞出來(lái)這么一個(gè)程序X,肯定不是我們自己手寫(xiě)了,springboot早就做好了,搞一個(gè)坐標(biāo)導(dǎo)入進(jìn)去就行了。

KF-1-1.手動(dòng)啟動(dòng)熱部署

步驟①:導(dǎo)入開(kāi)發(fā)者工具對(duì)應(yīng)的坐標(biāo)

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

步驟②:構(gòu)建項(xiàng)目,可以使用快捷鍵激活此功能

在這里插入圖片描述

? 對(duì)應(yīng)的快捷鍵一定要記得

<CTRL>+<F9>

? 以上過(guò)程就實(shí)現(xiàn)了springboot工程的熱部署,是不是挺簡(jiǎn)單的。不過(guò)這里需要把底層的工作工程給普及一下。

重啟與重載

? 一個(gè)springboot項(xiàng)目在運(yùn)行時(shí)實(shí)際上是分兩個(gè)過(guò)程進(jìn)行的,根據(jù)加載的東西不同,劃分成base類加載器與restart類加載器。

  • base類加載器:用來(lái)加載jar包中的類,jar包中的類和配置文件由于不會(huì)發(fā)生變化,因此不管加載多少次,加載的內(nèi)容不會(huì)發(fā)生變化
  • restart類加載器:用來(lái)加載開(kāi)發(fā)者自己開(kāi)發(fā)的類、配置文件、頁(yè)面等信息,這一類文件受開(kāi)發(fā)者影響

? 當(dāng)springboot項(xiàng)目啟動(dòng)時(shí),base類加載器執(zhí)行,加載jar包中的信息后,restart類加載器執(zhí)行,加載開(kāi)發(fā)者制作的內(nèi)容。當(dāng)執(zhí)行構(gòu)建項(xiàng)目后,由于jar中的信息不會(huì)變化,因此base類加載器無(wú)需再次執(zhí)行,所以僅僅運(yùn)行restart類加載即可,也就是將開(kāi)發(fā)者自己制作的內(nèi)容重新加載就行了,這就完成了一次熱部署的過(guò)程,也可以說(shuō)熱部署的過(guò)程實(shí)際上是重新加載restart類加載器中的信息。

總結(jié)

  1. 使用開(kāi)發(fā)者工具可以為當(dāng)前項(xiàng)目開(kāi)啟熱部署功能
  2. 使用構(gòu)建項(xiàng)目操作對(duì)工程進(jìn)行熱部署

思考

? 上述過(guò)程每次進(jìn)行熱部署都需要開(kāi)發(fā)者手工操作,不管是點(diǎn)擊按鈕還是快捷鍵都需要開(kāi)發(fā)者手工執(zhí)行。這種操作的應(yīng)用場(chǎng)景主要是在開(kāi)發(fā)調(diào)試期,并且調(diào)試的代碼處于不同的文件中,比如服務(wù)器啟動(dòng)了,我需要改4個(gè)文件中的內(nèi)容,然后重啟,等4個(gè)文件都改完了再執(zhí)行熱部署,使用一個(gè)快捷鍵就OK了。但是如果現(xiàn)在開(kāi)發(fā)者要修改的內(nèi)容就只有一個(gè)文件中的少量代碼,這個(gè)時(shí)候代碼修改完畢如果能夠讓程序自己執(zhí)行熱部署功能,就可以減少開(kāi)發(fā)者的操作,也就是自動(dòng)進(jìn)行熱部署,能這么做嗎?是可以的。咱們下一節(jié)再說(shuō)。

?

KF-1-2.自動(dòng)啟動(dòng)熱部署

? 自動(dòng)熱部署其實(shí)就是設(shè)計(jì)一個(gè)開(kāi)關(guān),打開(kāi)這個(gè)開(kāi)關(guān)后,IDE工具就可以自動(dòng)熱部署。因此這個(gè)操作和IDE工具有關(guān),以下以idea為例設(shè)置idea中啟動(dòng)熱部署

步驟①:設(shè)置自動(dòng)構(gòu)建項(xiàng)目

? 打開(kāi)【File】,選擇【settings…】,在面板左側(cè)的菜單中找到【Compile】選項(xiàng),然后勾選【Build project automatically】,意思是自動(dòng)構(gòu)建項(xiàng)目

在這里插入圖片描述

? 自動(dòng)構(gòu)建項(xiàng)目選項(xiàng)勾選后

步驟②:允許在程序運(yùn)行時(shí)進(jìn)行自動(dòng)構(gòu)建

? 使用快捷鍵【Ctrl】+【Alt】+【Shit】+【/】打開(kāi)維護(hù)面板,選擇第1項(xiàng)【Registry…】

在這里插入圖片描述

? 在選項(xiàng)中搜索comple,然后勾選對(duì)應(yīng)項(xiàng)即可

在這里插入圖片描述

? 這樣程序在運(yùn)行的時(shí)候就可以進(jìn)行自動(dòng)構(gòu)建了,實(shí)現(xiàn)了熱部署的效果。

關(guān)注:如果你每敲一個(gè)字母,服務(wù)器就重新構(gòu)建一次,這未免有點(diǎn)太頻繁了,所以idea設(shè)置當(dāng)idea工具失去焦點(diǎn)5秒后進(jìn)行熱部署。其實(shí)就是你從idea工具中切換到其他工具時(shí)進(jìn)行熱部署,比如改完程序需要到瀏覽器上去調(diào)試,這個(gè)時(shí)候idea就自動(dòng)進(jìn)行熱部署操作。

總結(jié)

  1. 自動(dòng)熱部署要開(kāi)啟自動(dòng)構(gòu)建項(xiàng)目
  2. 自動(dòng)熱部署要開(kāi)啟在程序運(yùn)行時(shí)自動(dòng)構(gòu)建項(xiàng)目

思考

? 現(xiàn)在已經(jīng)實(shí)現(xiàn)了熱部署了,但是到企業(yè)開(kāi)發(fā)的時(shí)候你會(huì)發(fā)現(xiàn),為了便于管理,在你的程序目錄中除了有代碼,還有可能有文檔,如果你修改了一下文檔,這個(gè)時(shí)候會(huì)進(jìn)行熱部署嗎?不管是否進(jìn)行熱部署,這個(gè)過(guò)程我們需要自己控制才比較合理。

KF-1-3.參與熱部署監(jiān)控的文件范圍配置

? 通過(guò)修改項(xiàng)目中的文件,你可以發(fā)現(xiàn)其實(shí)并不是所有的文件修改都會(huì)激活熱部署的,原因在于在開(kāi)發(fā)者工具中有一組配置,當(dāng)滿足了配置中的條件后,才會(huì)啟動(dòng)熱部署,配置中默認(rèn)不參與熱部署的目錄信息如下

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

? 以上目錄中的文件如果發(fā)生變化,是不參與熱部署的。如果想修改配置,可以通過(guò)application.yml文件進(jìn)行設(shè)定哪些文件不參與熱部署操作

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

總結(jié)

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

思考

? 熱部署功能是一個(gè)典型的開(kāi)發(fā)階段使用的功能,到了線上環(huán)境運(yùn)行程序時(shí),這個(gè)功能就沒(méi)有意義了。

KF-1-4.關(guān)閉熱部署

? 線上環(huán)境運(yùn)行時(shí)是不可能使用熱部署功能的,所以需要強(qiáng)制關(guān)閉此功能,通過(guò)配置可以關(guān)閉此功能。

spring:devtools:restart:enabled: false

? 如果當(dāng)心配置文件層級(jí)過(guò)多導(dǎo)致相符覆蓋最終引起配置失效,可以提高配置的層級(jí),在更高層級(jí)中配置關(guān)閉熱部署。例如在啟動(dòng)容器前通過(guò)系統(tǒng)屬性設(shè)置關(guān)閉熱部署功能。

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

? 其實(shí)上述擔(dān)心略微有點(diǎn)多余,因?yàn)榫€上環(huán)境的維護(hù)是不可能出現(xiàn)修改代碼的操作的,這么做唯一的作用是降低資源消耗,畢竟那雙盯著你項(xiàng)目是不是產(chǎn)生變化的眼睛只要閉上了,就不具有熱部署功能了,這個(gè)開(kāi)關(guān)的作用就是禁用對(duì)應(yīng)功能。

總結(jié)

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

KF-2.配置高級(jí)

? 進(jìn)入開(kāi)發(fā)實(shí)用篇第二章內(nèi)容,配置高級(jí),其實(shí)配置在基礎(chǔ)篇講了一部分,在運(yùn)維實(shí)用篇講了一部分,這里還要講,講的東西有什么區(qū)別呢?距離開(kāi)發(fā)過(guò)程越來(lái)越接近,解決的問(wèn)題也越來(lái)越靠近線上環(huán)境,下面就開(kāi)啟本章的學(xué)習(xí)。

KF-2-1.@ConfigurationProperties

? 在基礎(chǔ)篇學(xué)習(xí)了@ConfigurationProperties注解,此注解的作用是用來(lái)為bean綁定屬性的。開(kāi)發(fā)者可以在yml配置文件中以對(duì)象的格式添加若干屬性

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

? 然后再開(kāi)發(fā)一個(gè)用來(lái)封裝數(shù)據(jù)的實(shí)體類,注意要提供屬性對(duì)應(yīng)的setter方法

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

? 使用@ConfigurationProperties注解就可以將配置中的屬性值關(guān)聯(lián)到開(kāi)發(fā)的模型類上

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

? 這樣加載對(duì)應(yīng)bean的時(shí)候就可以直接加載配置屬性值了。但是目前我們學(xué)的都是給自定義的bean使用這種形式加載屬性值,如果是第三方的bean呢?能不能用這種形式加載屬性值呢?為什么會(huì)提出這個(gè)疑問(wèn)?原因就在于當(dāng)前@ConfigurationProperties注解是寫(xiě)在類定義的上方,而第三方開(kāi)發(fā)的bean源代碼不是你自己書(shū)寫(xiě)的,你也不可能到源代碼中去添加@ConfigurationProperties注解,這種問(wèn)題該怎么解決呢?下面就來(lái)說(shuō)說(shuō)這個(gè)問(wèn)題。

? 使用@ConfigurationProperties注解其實(shí)可以為第三方bean加載屬性,格式特殊一點(diǎn)而已。

步驟①:使用@Bean注解定義第三方bean

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

步驟②:在yml中定義要綁定的屬性,注意datasource此時(shí)全小寫(xiě)

datasource:driverClassName: com.mysql.jdbc.Driver

步驟③:使用@ConfigurationProperties注解為第三方bean進(jìn)行屬性綁定,注意前綴是全小寫(xiě)的datasource

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

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

? 做到這其實(shí)就出現(xiàn)了一個(gè)新的問(wèn)題,目前我們定義bean不是通過(guò)類注解定義就是通過(guò)@Bean定義,使用@ConfigurationProperties注解可以為bean進(jìn)行屬性綁定,那在一個(gè)業(yè)務(wù)系統(tǒng)中,哪些bean通過(guò)注解@ConfigurationProperties去綁定屬性了呢?因?yàn)檫@個(gè)注解不僅可以寫(xiě)在類上,還可以寫(xiě)在方法上,所以找起來(lái)就比較麻煩了。為了解決這個(gè)問(wèn)題,spring給我們提供了一個(gè)全新的注解,專門(mén)標(biāo)注使用@ConfigurationProperties注解綁定屬性的bean是哪些。這個(gè)注解叫做@EnableConfigurationProperties。具體如何使用呢?

步驟①:在配置類上開(kāi)啟@EnableConfigurationProperties注解,并標(biāo)注要使用@ConfigurationProperties注解綁定屬性的類

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

步驟②:在對(duì)應(yīng)的類上直接使用@ConfigurationProperties進(jìn)行屬性綁定

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

? 有人感覺(jué)這沒(méi)區(qū)別啊?注意觀察,現(xiàn)在綁定屬性的ServerConfig類并沒(méi)有聲明@Component注解。當(dāng)使用@EnableConfigurationProperties注解時(shí),spring會(huì)默認(rèn)將其標(biāo)注的類定義為bean,因此無(wú)需再次聲明@Component注解了。

? 最后再說(shuō)一個(gè)小技巧,使用@ConfigurationProperties注解時(shí),會(huì)出現(xiàn)一個(gè)提示信息

在這里插入圖片描述

? 出現(xiàn)這個(gè)提示后只需要添加一個(gè)坐標(biāo)此提醒就消失了

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

總結(jié)

  1. 使用@ConfigurationProperties可以為使用@Bean聲明的第三方bean綁定屬性
  2. 當(dāng)使用@EnableConfigurationProperties聲明進(jìn)行屬性綁定的bean后,無(wú)需使用@Component注解再次進(jìn)行bean聲明

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

? 在進(jìn)行屬性綁定時(shí),可能會(huì)遇到如下情況,為了進(jìn)行標(biāo)準(zhǔn)命名,開(kāi)發(fā)者會(huì)將屬性名嚴(yán)格按照駝峰命名法書(shū)寫(xiě),在yml配置文件中將datasource修改為dataSource,如下:

dataSource:driverClassName: com.mysql.jdbc.Driver

? 此時(shí)程序可以正常運(yùn)行,然后又將代碼中的前綴datasource修改為dataSource,如下:

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

? 此時(shí)就發(fā)生了編譯錯(cuò)誤,而且并不是idea工具導(dǎo)致的,運(yùn)行后依然會(huì)出現(xiàn)問(wèn)題,配置屬性名dataSource是無(wú)效的

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

? 為什么會(huì)出現(xiàn)這種問(wèn)題,這就要來(lái)說(shuō)一說(shuō)springboot進(jìn)行屬性綁定時(shí)的一個(gè)重要知識(shí)點(diǎn)了,有關(guān)屬性名稱的寬松綁定,也可以稱為寬松綁定。

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

? 在ServerConfig中的ipAddress屬性名

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

? 可以與下面的配置屬性名規(guī)則全兼容

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

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

? 到這里我們掌握了一個(gè)知識(shí)點(diǎn),就是命名的規(guī)范問(wèn)題。再來(lái)看開(kāi)始出現(xiàn)的編程錯(cuò)誤信息

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

? 其中Reason描述了報(bào)錯(cuò)的原因,規(guī)范的名稱應(yīng)該是烤肉串(kebab)模式(case),即使用-分隔,使用小寫(xiě)字母數(shù)字作為標(biāo)準(zhǔn)字符,且必須以字母開(kāi)頭。然后再看我們寫(xiě)的名稱dataSource,就不滿足上述要求。鬧了半天,在書(shū)寫(xiě)前綴時(shí),這個(gè)詞不是隨意支持的,必須使用上述標(biāo)準(zhǔn)。編程寫(xiě)了這么久,基本上編程習(xí)慣都養(yǎng)成了,到這里又被springboot教育了,沒(méi)轍,誰(shuí)讓人家東西好用呢,按照人家的要求寫(xiě)吧。

? 最后說(shuō)一句,以上規(guī)則僅針對(duì)springboot中@ConfigurationProperties注解進(jìn)行屬性綁定時(shí)有效,對(duì)@Value注解進(jìn)行屬性映射無(wú)效。。

總結(jié)

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

KF-2-3.常用計(jì)量單位綁定

? 在前面的配置中,我們書(shū)寫(xiě)了如下配置值,其中第三項(xiàng)超時(shí)時(shí)間timeout描述了服務(wù)器操作超時(shí)時(shí)間,當(dāng)前值是-1表示永不超時(shí)。

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

? 但是每個(gè)人都這個(gè)值的理解會(huì)產(chǎn)生不同,比如線上服務(wù)器完成一次主從備份,配置超時(shí)時(shí)間240,這個(gè)240如果單位是秒就是超時(shí)時(shí)間4分鐘,如果單位是分鐘就是超時(shí)時(shí)間4小時(shí)。面對(duì)一次線上服務(wù)器的主從備份,設(shè)置4分鐘,簡(jiǎn)直是開(kāi)玩笑,別說(shuō)拷貝過(guò)程,備份之前的壓縮過(guò)程4分鐘也搞不定,這個(gè)時(shí)候問(wèn)題就來(lái)了,怎么解決這個(gè)誤會(huì)?

? 除了加強(qiáng)約定之外,springboot充分利用了JDK8中提供的全新的用來(lái)表示計(jì)量單位的新數(shù)據(jù)類型,從根本上解決這個(gè)問(wèn)題。以下模型類中添加了兩個(gè)JDK8中新增的類,分別是Duration和DataSize

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

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

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

? 使用上述兩個(gè)單位就可以有效避免因溝通不同步或文檔不健全導(dǎo)致的信息不對(duì)稱問(wèn)題,從根本上解決了問(wèn)題,避免產(chǎn)生誤讀。

Druation常用單位如下:

在這里插入圖片描述

DataSize常用單位如下:

在這里插入圖片描述

KF-2-4.校驗(yàn)

? 目前我們?cè)谶M(jìn)行屬性綁定時(shí)可以通過(guò)松散綁定規(guī)則在書(shū)寫(xiě)時(shí)放飛自我了,但是在書(shū)寫(xiě)時(shí)由于無(wú)法感知模型類中的數(shù)據(jù)類型,就會(huì)出現(xiàn)類型不匹配的問(wèn)題,比如代碼中需要int類型,配置中給了非法的數(shù)值,例如寫(xiě)一個(gè)“a",這種數(shù)據(jù)肯定無(wú)法有效的綁定,還會(huì)引發(fā)錯(cuò)誤。 SpringBoot給出了強(qiáng)大的數(shù)據(jù)校驗(yàn)功能,可以有效的避免此類問(wèn)題的發(fā)生。在JAVAEE的JSR303規(guī)范中給出了具體的數(shù)據(jù)校驗(yàn)標(biāo)準(zhǔn),開(kāi)發(fā)者可以根據(jù)自己的需要選擇對(duì)應(yīng)的校驗(yàn)框架,此處使用Hibernate提供的校驗(yàn)框架來(lái)作為實(shí)現(xiàn)進(jìn)行數(shù)據(jù)校驗(yàn)。書(shū)寫(xiě)應(yīng)用格式非常固定,話不多說(shuō),直接上步驟

步驟①:開(kāi)啟校驗(yàn)框架

<!--1.導(dǎo)入JSR303規(guī)范-->
<dependency><groupId>javax.validation</groupId><artifactId>validation-api</artifactId>
</dependency>
<!--使用hibernate框架提供的校驗(yàn)器做實(shí)現(xiàn)-->
<dependency><groupId>org.hibernate.validator</groupId><artifactId>hibernate-validator</artifactId>
</dependency>

步驟②:在需要開(kāi)啟校驗(yàn)功能的類上使用注解@Validated開(kāi)啟校驗(yàn)功能

@Component
@Data
@ConfigurationProperties(prefix = "servers")
//開(kāi)啟對(duì)當(dāng)前bean的屬性注入校驗(yàn)
@Validated
public class ServerConfig {
}

步驟③:對(duì)具體的字段設(shè)置校驗(yàn)規(guī)則

@Component
@Data
@ConfigurationProperties(prefix = "servers")
//開(kāi)啟對(duì)當(dāng)前bean的屬性注入校驗(yàn)
@Validated
public class ServerConfig {//設(shè)置具體的規(guī)則@Max(value = 8888,message = "最大值不能超過(guò)8888")@Min(value = 202,message = "最小值不能低于202")private int port;
}

? 通過(guò)設(shè)置數(shù)據(jù)格式校驗(yàn),就可以有效避免非法數(shù)據(jù)加載,其實(shí)使用起來(lái)還是挺輕松的,基本上就是一個(gè)格式。

總結(jié)

  1. 開(kāi)啟Bean屬性校驗(yàn)功能一共3步:導(dǎo)入JSR303與Hibernate校驗(yàn)框架坐標(biāo)、使用@Validated注解啟用校驗(yàn)功能、使用具體校驗(yàn)規(guī)則規(guī)范數(shù)據(jù)校驗(yàn)格式

KF-2-5.數(shù)據(jù)類型轉(zhuǎn)換

? 有關(guān)spring屬性注入的問(wèn)題到這里基本上就講完了,但是最近一名開(kāi)發(fā)者向我咨詢了一個(gè)問(wèn)題,我覺(jué)得需要給各位學(xué)習(xí)者分享一下。在學(xué)習(xí)階段其實(shí)我們遇到的問(wèn)題往往復(fù)雜度比較低,單一性比較強(qiáng),但是到了線上開(kāi)發(fā)時(shí),都是綜合性的問(wèn)題,而這個(gè)開(kāi)發(fā)者遇到的問(wèn)題就是由于bean的屬性注入引發(fā)的災(zāi)難。

? 先把問(wèn)題描述一下,這位開(kāi)發(fā)者連接數(shù)據(jù)庫(kù)正常操作,但是運(yùn)行程序后顯示的信息是密碼錯(cuò)誤。

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

? 其實(shí)看到這個(gè)報(bào)錯(cuò),幾乎所有的學(xué)習(xí)者都能分辨出來(lái),這是用戶名和密碼不匹配,就就是密碼輸入錯(cuò)了,但是問(wèn)題就在于密碼并沒(méi)有輸入錯(cuò)誤,這就比較討厭了。給的報(bào)錯(cuò)信息無(wú)法幫助你有效的分析問(wèn)題,甚至?xí)o你帶到溝里。如果是初學(xué)者,估計(jì)這會(huì)心態(tài)就崩了,我密碼沒(méi)錯(cuò)啊,你怎么能說(shuō)我有錯(cuò)誤呢?來(lái)看看用戶名密碼的配置是如何寫(xiě)的:

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

? 這名開(kāi)發(fā)者的生日是1月27日,所以密碼就使用了0127,其實(shí)問(wèn)題就出在這里了。

? 之前在基礎(chǔ)篇講屬性注入時(shí),提到過(guò)類型相關(guān)的知識(shí),在整數(shù)相關(guān)知識(shí)中有這么一句話,支持二進(jìn)制,八進(jìn)制,十六進(jìn)制

在這里插入圖片描述

? 這個(gè)問(wèn)題就處在這里了,因?yàn)?127在開(kāi)發(fā)者眼中是一個(gè)字符串“0127”,但是在springboot看來(lái),這就是一個(gè)數(shù)字,而且是一個(gè)八進(jìn)制的數(shù)字。當(dāng)后臺(tái)使用String類型接收數(shù)據(jù)時(shí),如果配置文件中配置了一個(gè)整數(shù)值,他是先安裝整數(shù)進(jìn)行處理,讀取后再轉(zhuǎn)換成字符串。巧了,0127撞上了八進(jìn)制的格式,所以最終以十進(jìn)制數(shù)字87的結(jié)果存在了。

? 這里提兩個(gè)注意點(diǎn),第一,字符串標(biāo)準(zhǔn)書(shū)寫(xiě)加上引號(hào)包裹,養(yǎng)成習(xí)慣,第二,遇到0開(kāi)頭的數(shù)據(jù)多注意吧。

總結(jié)

  1. yaml文件中對(duì)于數(shù)字的定義支持進(jìn)制書(shū)寫(xiě)格式,如需使用字符串請(qǐng)使用引號(hào)明確標(biāo)注

KF-3.測(cè)試

? 說(shuō)完bean配置相關(guān)的內(nèi)容,下面要對(duì)前面講過(guò)的一個(gè)知識(shí)做加強(qiáng)了,測(cè)試。測(cè)試是保障程序正確性的唯一屏障,在企業(yè)級(jí)開(kāi)發(fā)中更是不可缺少,但是由于測(cè)試代碼往往不產(chǎn)生實(shí)際效益,所以一些小型公司并不是很關(guān)注,導(dǎo)致一些開(kāi)發(fā)者從小型公司進(jìn)入中大型公司后,往往這一塊比較短板,所以還是要拿出來(lái)把這一塊知識(shí)好好說(shuō)說(shuō),做一名專業(yè)的開(kāi)發(fā)人員。

KF-3-1.加載測(cè)試專用屬性

? 測(cè)試過(guò)程本身并不是一個(gè)復(fù)雜的過(guò)程,但是很多情況下測(cè)試時(shí)需要模擬一些線上情況,或者模擬一些特殊情況。如果當(dāng)前環(huán)境按照線上環(huán)境已經(jīng)設(shè)定好了,例如是下面的配置

env:maxMemory: 32GBminMemory: 16GB

? 但是你現(xiàn)在想測(cè)試對(duì)應(yīng)的兼容性,需要測(cè)試如下配置

env:maxMemory: 16GBminMemory: 8GB

? 這個(gè)時(shí)候我們能不能每次測(cè)試的時(shí)候都去修改源碼application.yml中的配置進(jìn)行測(cè)試呢?顯然是不行的。每次測(cè)試前改過(guò)來(lái),每次測(cè)試后改回去,這太麻煩了。于是我們就想,需要在測(cè)試環(huán)境中創(chuàng)建一組臨時(shí)屬性,去覆蓋我們?cè)创a中設(shè)定的屬性,這樣測(cè)試用例就相當(dāng)于是一個(gè)獨(dú)立的環(huán)境,能夠獨(dú)立測(cè)試,這樣就方便多了。

臨時(shí)屬性

? springboot已經(jīng)為我們開(kāi)發(fā)者早就想好了這種問(wèn)題該如何解決,并且提供了對(duì)應(yīng)的功能入口。在測(cè)試用例程序中,可以通過(guò)對(duì)注解@SpringBootTest添加屬性來(lái)模擬臨時(shí)屬性,具體如下:

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

? 使用注解@SpringBootTest的properties屬性就可以為當(dāng)前測(cè)試用例添加臨時(shí)的屬性,覆蓋源碼配置文件中對(duì)應(yīng)的屬性值進(jìn)行測(cè)試。

臨時(shí)參數(shù)

? 除了上述這種情況,在前面講解使用命令行啟動(dòng)springboot程序時(shí)講過(guò),通過(guò)命令行參數(shù)也可以設(shè)置屬性值。而且線上啟動(dòng)程序時(shí),通常都會(huì)添加一些專用的配置信息。作為運(yùn)維人員他們才不懂java,更不懂這些配置的信息具體格式該怎么寫(xiě),那如果我們作為開(kāi)發(fā)者提供了對(duì)應(yīng)的書(shū)寫(xiě)內(nèi)容后,能否提前測(cè)試一下這些配置信息是否有效呢?當(dāng)時(shí)是可以的,還是通過(guò)注解@SpringBootTest的另一個(gè)屬性來(lái)進(jìn)行設(shè)定。

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

? 使用注解@SpringBootTest的args屬性就可以為當(dāng)前測(cè)試用例模擬命令行參數(shù)并進(jìn)行測(cè)試。

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

在這里插入圖片描述

? 在這個(gè)屬性加載優(yōu)先級(jí)的順序中,明確規(guī)定了命令行參數(shù)的優(yōu)先級(jí)排序是11,而配置屬性的優(yōu)先級(jí)是3,結(jié)果不言而喻了,args屬性配置優(yōu)先于properties屬性配置加載。

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

總結(jié)

  1. 加載測(cè)試臨時(shí)屬性可以通過(guò)注解@SpringBootTest的properties和args屬性進(jìn)行設(shè)定,此設(shè)定應(yīng)用范圍僅適用于當(dāng)前測(cè)試用例

思考

? 應(yīng)用于測(cè)試環(huán)境的臨時(shí)屬性解決了,如果想在測(cè)試的時(shí)候臨時(shí)加載一些bean能不做呢?也就是說(shuō)我測(cè)試時(shí),想搞一些獨(dú)立的bean出來(lái),專門(mén)應(yīng)用于測(cè)試環(huán)境,能否實(shí)現(xiàn)呢?咱們下一節(jié)再講。

KF-3-2.加載測(cè)試專用配置

? 上一節(jié)提出了臨時(shí)配置一些專用于測(cè)試環(huán)境的bean的需求,這一節(jié)我們就來(lái)解決這個(gè)問(wèn)題。

? 學(xué)習(xí)過(guò)Spring的知識(shí),我們都知道,其實(shí)一個(gè)spring環(huán)境中可以設(shè)置若干個(gè)配置文件或配置類,若干個(gè)配置信息可以同時(shí)生效。現(xiàn)在我們的需求就是在測(cè)試環(huán)境中再添加一個(gè)配置類,然后啟動(dòng)測(cè)試環(huán)境時(shí),生效此配置就行了。其實(shí)做法和spring環(huán)境中加載多個(gè)配置信息的方式完全一樣。具體操作步驟如下:

步驟①:在測(cè)試包test中創(chuàng)建專用的測(cè)試環(huán)境配置類

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

? 上述配置僅用于演示當(dāng)前實(shí)驗(yàn)效果,實(shí)際開(kāi)發(fā)可不能這么注入String類型的數(shù)據(jù)

步驟②:在啟動(dòng)測(cè)試環(huán)境時(shí),導(dǎo)入測(cè)試環(huán)境專用的配置類,使用@Import注解即可實(shí)現(xiàn)

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

? 到這里就通過(guò)@Import屬性實(shí)現(xiàn)了基于開(kāi)發(fā)環(huán)境的配置基礎(chǔ)上,對(duì)配置進(jìn)行測(cè)試環(huán)境的追加操作,實(shí)現(xiàn)了1+1的配置環(huán)境效果。這樣我們就可以實(shí)現(xiàn)每一個(gè)不同的測(cè)試用例加載不同的bean的效果,豐富測(cè)試用例的編寫(xiě),同時(shí)不影響開(kāi)發(fā)環(huán)境的配置。

總結(jié)

  1. 定義測(cè)試環(huán)境專用的配置類,然后通過(guò)@Import注解在具體的測(cè)試中導(dǎo)入臨時(shí)的配置,例如測(cè)試用例,方便測(cè)試過(guò)程,且上述配置不影響其他的測(cè)試類環(huán)境

思考

? 當(dāng)前我們已經(jīng)可以實(shí)現(xiàn)業(yè)務(wù)層和數(shù)據(jù)層的測(cè)試,并且通過(guò)臨時(shí)配置,控制每個(gè)測(cè)試用例加載不同的測(cè)試數(shù)據(jù)。但是實(shí)際企業(yè)開(kāi)發(fā)不僅要保障業(yè)務(wù)層與數(shù)據(jù)層的功能安全有效,也要保障表現(xiàn)層的功能正常。但是我們目的對(duì)表現(xiàn)層的測(cè)試都是通過(guò)postman手工測(cè)試的,并沒(méi)有在打包過(guò)程中體現(xiàn)表現(xiàn)層功能被測(cè)試通過(guò)。

KF-3-3.Web環(huán)境模擬測(cè)試

? 在測(cè)試中對(duì)表現(xiàn)層功能進(jìn)行測(cè)試需要一個(gè)基礎(chǔ)和一個(gè)功能。所謂的一個(gè)基礎(chǔ)是運(yùn)行測(cè)試程序時(shí),必須啟動(dòng)web環(huán)境,不然沒(méi)法測(cè)試web功能。一個(gè)功能是必須在測(cè)試程序中具備發(fā)送web請(qǐng)求的能力,不然無(wú)法實(shí)現(xiàn)web功能的測(cè)試。所以在測(cè)試用例中測(cè)試表現(xiàn)層接口這項(xiàng)工作就轉(zhuǎn)換成了兩件事,一,如何在測(cè)試類中啟動(dòng)web測(cè)試,二,如何在測(cè)試類中發(fā)送web請(qǐng)求。下面一件事一件事進(jìn)行,先說(shuō)第一個(gè)

測(cè)試類中啟動(dòng)web環(huán)境

? 每一個(gè)springboot的測(cè)試類上方都會(huì)標(biāo)準(zhǔn)@SpringBootTest注解,而注解帶有一個(gè)屬性,叫做webEnvironment。通過(guò)該屬性就可以設(shè)置在測(cè)試用例中啟動(dòng)web環(huán)境,具體如下:

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

? 測(cè)試類中啟動(dòng)web環(huán)境時(shí),可以指定啟動(dòng)的Web環(huán)境對(duì)應(yīng)的端口,springboot提供了4種設(shè)置值,分別如下:

在這里插入圖片描述

  • MOCK:根據(jù)當(dāng)前設(shè)置確認(rèn)是否啟動(dòng)web環(huán)境,例如使用了Servlet的API就啟動(dòng)web環(huán)境,屬于適配性的配置
  • DEFINED_PORT:使用自定義的端口作為web服務(wù)器端口
  • RANDOM_PORT:使用隨機(jī)端口作為web服務(wù)器端口
  • NONE:不啟動(dòng)web環(huán)境

? 通過(guò)上述配置,現(xiàn)在啟動(dòng)測(cè)試程序時(shí)就可以正常啟用web環(huán)境了,建議大家測(cè)試時(shí)使用RANDOM_PORT,避免代碼中因?yàn)閷?xiě)死設(shè)定引發(fā)線上功能打包測(cè)試時(shí)由于端口沖突導(dǎo)致意外現(xiàn)象的出現(xiàn)。就是說(shuō)你程序中寫(xiě)了用8080端口,結(jié)果線上環(huán)境8080端口被占用了,結(jié)果你代碼中所有寫(xiě)的東西都要改,這就是寫(xiě)死代碼的代價(jià)?,F(xiàn)在你用隨機(jī)端口就可以測(cè)試出來(lái)你有沒(méi)有這種問(wèn)題的隱患了。

? 測(cè)試環(huán)境中的web環(huán)境已經(jīng)搭建好了,下面就可以來(lái)解決第二個(gè)問(wèn)題了,如何在程序代碼中發(fā)送web請(qǐng)求。

測(cè)試類中發(fā)送請(qǐng)求

? 對(duì)于測(cè)試類中發(fā)送請(qǐng)求,其實(shí)java的API就提供對(duì)應(yīng)的功能,只不過(guò)平時(shí)各位小伙伴接觸的比較少,所以較為陌生。springboot為了便于開(kāi)發(fā)者進(jìn)行對(duì)應(yīng)的功能開(kāi)發(fā),對(duì)其又進(jìn)行了包裝,簡(jiǎn)化了開(kāi)發(fā)步驟,具體操作如下:

步驟①:在測(cè)試類中開(kāi)啟web虛擬調(diào)用功能,通過(guò)注解@AutoConfigureMockMvc實(shí)現(xiàn)此功能的開(kāi)啟

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
//開(kāi)啟虛擬MVC調(diào)用
@AutoConfigureMockMvc
public class WebTest {
}

步驟②:定義發(fā)起虛擬調(diào)用的對(duì)象MockMVC,通過(guò)自動(dòng)裝配的形式初始化對(duì)象

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

步驟③:創(chuàng)建一個(gè)虛擬請(qǐng)求對(duì)象,封裝請(qǐng)求的路徑,并使用MockMVC對(duì)象發(fā)送對(duì)應(yīng)請(qǐng)求

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
//開(kāi)啟虛擬MVC調(diào)用
@AutoConfigureMockMvc
public class WebTest {@Testvoid testWeb(@Autowired MockMvc mvc) throws Exception {//http://localhost:8080/books//創(chuàng)建虛擬請(qǐng)求,當(dāng)前訪問(wèn)/booksMockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");//執(zhí)行對(duì)應(yīng)的請(qǐng)求mvc.perform(builder);}
}

? 執(zhí)行測(cè)試程序,現(xiàn)在就可以正常的發(fā)送/books對(duì)應(yīng)的請(qǐng)求了,注意訪問(wèn)路徑不要寫(xiě)http://localhost:8080/books,因?yàn)榍懊娴姆?wù)器IP地址和端口使用的是當(dāng)前虛擬的web環(huán)境,無(wú)需指定,僅指定請(qǐng)求的具體路徑即可。

總結(jié)

  1. 在測(cè)試類中測(cè)試web層接口要保障測(cè)試類啟動(dòng)時(shí)啟動(dòng)web容器,使用@SpringBootTest注解的webEnvironment屬性可以虛擬web環(huán)境用于測(cè)試
  2. 為測(cè)試方法注入MockMvc對(duì)象,通過(guò)MockMvc對(duì)象可以發(fā)送虛擬請(qǐng)求,模擬web請(qǐng)求調(diào)用過(guò)程

思考

? 目前已經(jīng)成功的發(fā)送了請(qǐng)求,但是還沒(méi)有起到測(cè)試的效果,測(cè)試過(guò)程必須出現(xiàn)預(yù)計(jì)值與真實(shí)值的比對(duì)結(jié)果才能確認(rèn)測(cè)試結(jié)果是否通過(guò),虛擬請(qǐng)求中能對(duì)哪些請(qǐng)求結(jié)果進(jìn)行比對(duì)呢?咱們下一節(jié)再講。

web環(huán)境請(qǐng)求結(jié)果比對(duì)

? 上一節(jié)已經(jīng)在測(cè)試用例中成功的模擬出了web環(huán)境,并成功的發(fā)送了web請(qǐng)求,本節(jié)就來(lái)解決發(fā)送請(qǐng)求后如何比對(duì)發(fā)送結(jié)果的問(wèn)題。其實(shí)發(fā)完請(qǐng)求得到的信息只有一種,就是響應(yīng)對(duì)象。至于響應(yīng)對(duì)象中包含什么,就可以比對(duì)什么。常見(jiàn)的比對(duì)內(nèi)容如下:

  • 響應(yīng)狀態(tài)匹配

    @Test
    void testStatus(@Autowired MockMvc mvc) throws Exception {MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");ResultActions action = mvc.perform(builder);//設(shè)定預(yù)期值 與真實(shí)值進(jìn)行比較,成功測(cè)試通過(guò),失敗測(cè)試失敗//定義本次調(diào)用的預(yù)期值StatusResultMatchers status = MockMvcResultMatchers.status();//預(yù)計(jì)本次調(diào)用時(shí)成功的:狀態(tài)200ResultMatcher ok = status.isOk();//添加預(yù)計(jì)值到本次調(diào)用過(guò)程中進(jìn)行匹配action.andExpect(ok);
    }
    
  • 響應(yīng)體匹配(非json數(shù)據(jù)格式)

    @Test
    void testBody(@Autowired MockMvc mvc) throws Exception {MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");ResultActions action = mvc.perform(builder);//設(shè)定預(yù)期值 與真實(shí)值進(jìn)行比較,成功測(cè)試通過(guò),失敗測(cè)試失敗//定義本次調(diào)用的預(yù)期值ContentResultMatchers content = MockMvcResultMatchers.content();ResultMatcher result = content.string("springboot2");//添加預(yù)計(jì)值到本次調(diào)用過(guò)程中進(jìn)行匹配action.andExpect(result);
    }
    
  • 響應(yīng)體匹配(json數(shù)據(jù)格式,開(kāi)發(fā)中的主流使用方式)

    @Test
    void testJson(@Autowired MockMvc mvc) throws Exception {MockHttpServletRequestBuilder builder = MockMvcRequestBuilders.get("/books");ResultActions action = mvc.perform(builder);//設(shè)定預(yù)期值 與真實(shí)值進(jìn)行比較,成功測(cè)試通過(guò),失敗測(cè)試失敗//定義本次調(diào)用的預(yù)期值ContentResultMatchers content = MockMvcResultMatchers.content();ResultMatcher result = content.json("{\"id\":1,\"name\":\"springboot2\",\"type\":\"springboot\"}");//添加預(yù)計(jì)值到本次調(diào)用過(guò)程中進(jìn)行匹配action.andExpect(result);
    }
    
  • 響應(yīng)頭信息匹配

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

? 基本上齊了,頭信息,正文信息,狀態(tài)信息都有了,就可以組合出一個(gè)完美的響應(yīng)結(jié)果比對(duì)結(jié)果了。以下范例就是三種信息同時(shí)進(jìn)行匹配校驗(yàn),也是一個(gè)完整的信息匹配過(guò)程。

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

總結(jié)

  1. web虛擬調(diào)用可以對(duì)本地虛擬請(qǐng)求的返回響應(yīng)信息進(jìn)行比對(duì),分為響應(yīng)頭信息比對(duì)、響應(yīng)體信息比對(duì)、響應(yīng)狀態(tài)信息比對(duì)

KF-3-4.數(shù)據(jù)層測(cè)試回滾

? 當(dāng)前我們的測(cè)試程序可以完美的進(jìn)行表現(xiàn)層、業(yè)務(wù)層、數(shù)據(jù)層接口對(duì)應(yīng)的功能測(cè)試了,但是測(cè)試用例開(kāi)發(fā)完成后,在打包的階段由于test生命周期屬于必須被運(yùn)行的生命周期,如果跳過(guò)會(huì)給系統(tǒng)帶來(lái)極高的安全隱患,所以測(cè)試用例必須執(zhí)行。但是新的問(wèn)題就呈現(xiàn)了,測(cè)試用例如果測(cè)試時(shí)產(chǎn)生了事務(wù)提交就會(huì)在測(cè)試過(guò)程中對(duì)數(shù)據(jù)庫(kù)數(shù)據(jù)產(chǎn)生影響,進(jìn)而產(chǎn)生垃圾數(shù)據(jù)。這個(gè)過(guò)程不是我們希望發(fā)生的,作為開(kāi)發(fā)者測(cè)試用例該運(yùn)行運(yùn)行,但是過(guò)程中產(chǎn)生的數(shù)據(jù)不要在我的系統(tǒng)中留痕,這樣該如何處理呢?

? springboot早就為開(kāi)發(fā)者想到了這個(gè)問(wèn)題,并且針對(duì)此問(wèn)題給出了最簡(jiǎn)解決方案,在原始測(cè)試用例中添加注解@Transactional即可實(shí)現(xiàn)當(dāng)前測(cè)試用例的事務(wù)不提交。當(dāng)程序運(yùn)行后,只要注解@Transactional出現(xiàn)的位置存在注解@SpringBootTest,springboot就會(huì)認(rèn)為這是一個(gè)測(cè)試程序,無(wú)需提交事務(wù),所以也就可以避免事務(wù)的提交。

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

? 如果開(kāi)發(fā)者想提交事務(wù),也可以,再添加一個(gè)@RollBack的注解,設(shè)置回滾狀態(tài)為false即可正常提交事務(wù),是不是很方便?springboot在輔助開(kāi)發(fā)者日常工作這一塊展現(xiàn)出了驚人的能力,實(shí)在太貼心了。

總結(jié)

  1. 在springboot的測(cè)試類中通過(guò)添加注解@Transactional來(lái)阻止測(cè)試用例提交事務(wù)
  2. 通過(guò)注解@Rollback控制springboot測(cè)試類執(zhí)行結(jié)果是否提交事務(wù),需要配合注解@Transactional使用

思考

? 當(dāng)前測(cè)試程序已經(jīng)近乎完美了,但是由于測(cè)試用例中書(shū)寫(xiě)的測(cè)試數(shù)據(jù)屬于固定數(shù)據(jù),往往失去了測(cè)試的意義,開(kāi)發(fā)者可以針對(duì)測(cè)試用例進(jìn)行針對(duì)性開(kāi)發(fā),這樣就有可能出現(xiàn)測(cè)試用例不能完美呈現(xiàn)業(yè)務(wù)邏輯代碼是否真實(shí)有效的達(dá)成業(yè)務(wù)目標(biāo)的現(xiàn)象,解決方案其實(shí)很容易想,測(cè)試用例的數(shù)據(jù)只要隨機(jī)產(chǎn)生就可以了,能實(shí)現(xiàn)嗎?咱們下一節(jié)再講。

KF-3-5.測(cè)試用例數(shù)據(jù)設(shè)定

? 對(duì)于測(cè)試用例的數(shù)據(jù)固定書(shū)寫(xiě)肯定是不合理的,springboot提供了在配置中使用隨機(jī)值的機(jī)制,確保每次運(yùn)行程序加載的數(shù)據(jù)都是隨機(jī)的。具體如下:

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

? 當(dāng)前配置就可以在每次運(yùn)行程序時(shí)創(chuàng)建一組隨機(jī)數(shù)據(jù),避免每次運(yùn)行時(shí)數(shù)據(jù)都是固定值的尷尬現(xiàn)象發(fā)生,有助于測(cè)試功能的進(jìn)行。數(shù)據(jù)的加載按照之前加載數(shù)據(jù)的形式,使用@ConfigurationProperties注解即可

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

? 對(duì)于隨機(jī)值的產(chǎn)生,還有一些小的限定規(guī)則,比如產(chǎn)生的數(shù)值性數(shù)據(jù)可以設(shè)置范圍等,具體如下:

在這里插入圖片描述

  • ${random.int}表示隨機(jī)整數(shù)
  • ${random.int(10)}表示10以內(nèi)的隨機(jī)數(shù)
  • ${random.int(10,20)}表示10到20的隨機(jī)數(shù)
  • 其中()可以是任意字符,例如[],!!均可

總結(jié)

  1. 使用隨機(jī)數(shù)據(jù)可以替換測(cè)試用例中書(shū)寫(xiě)的固定數(shù)據(jù),提高測(cè)試用例中的測(cè)試數(shù)據(jù)有效性

KF-4.數(shù)據(jù)層解決方案

KF-4-1.SQL

? 回憶一下之前做SSMP整合的時(shí)候數(shù)據(jù)層解決方案涉及到了哪些技術(shù)?MySQL數(shù)據(jù)庫(kù)與MyBatisPlus框架,后面又學(xué)了Druid數(shù)據(jù)源的配置,所以現(xiàn)在數(shù)據(jù)層解決方案可以說(shuō)是Mysql+Druid+MyBatisPlus。而三個(gè)技術(shù)分別對(duì)應(yīng)了數(shù)據(jù)層操作的三個(gè)層面:

  • 數(shù)據(jù)源技術(shù):Druid
  • 持久化技術(shù):MyBatisPlus
  • 數(shù)據(jù)庫(kù)技術(shù):MySQL

? 下面的研究就分為三個(gè)層面進(jìn)行研究,對(duì)應(yīng)上面列出的三個(gè)方面,咱們就從第一個(gè)數(shù)據(jù)源技術(shù)開(kāi)始說(shuō)起。

數(shù)據(jù)源技術(shù)

? 目前我們使用的數(shù)據(jù)源技術(shù)是Druid,運(yùn)行時(shí)可以在日志中看到對(duì)應(yīng)的數(shù)據(jù)源初始化信息,具體如下:

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

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

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

? 雖然沒(méi)有DruidDataSource相關(guān)的信息了,但是我們發(fā)現(xiàn)日志中有HikariDataSource這個(gè)信息,就算不懂這是個(gè)什么技術(shù),看名字也能看出來(lái),以DataSource結(jié)尾的名稱,這一定是一個(gè)數(shù)據(jù)源技術(shù)。我們又沒(méi)有手工添加這個(gè)技術(shù),這個(gè)技術(shù)哪里來(lái)的呢?springboot內(nèi)嵌數(shù)據(jù)源。

? 數(shù)據(jù)層技術(shù)是每一個(gè)企業(yè)級(jí)應(yīng)用程序都會(huì)用到的,而其中必定會(huì)進(jìn)行數(shù)據(jù)庫(kù)連接的管理。springboot根據(jù)開(kāi)發(fā)者的習(xí)慣出發(fā),開(kāi)發(fā)者提供了數(shù)據(jù)源技術(shù),就用你提供的,開(kāi)發(fā)者沒(méi)有提供,那總不能手工管理一個(gè)一個(gè)的數(shù)據(jù)庫(kù)連接對(duì)象啊,怎么辦?我給你一個(gè)默認(rèn)的就好了,這樣省心又省事,大家都方便。

? springboot提供了3款內(nèi)嵌數(shù)據(jù)源技術(shù),分別如下:

  • HikariCP
  • Tomcat提供DataSource
  • Commons DBCP

? 第一種,HikartCP,這是springboot官方推薦的數(shù)據(jù)源技術(shù),作為默認(rèn)內(nèi)置數(shù)據(jù)源使用。啥意思?你不配置數(shù)據(jù)源,那就用這個(gè)。

? 第二種,Tomcat提供的DataSource,如果不想用HikartCP,并且使用tomcat作為web服務(wù)器進(jìn)行web程序的開(kāi)發(fā),使用這個(gè)。為什么是Tomcat,不是其他web服務(wù)器呢?因?yàn)閣eb技術(shù)導(dǎo)入starter后,默認(rèn)使用內(nèi)嵌tomcat,既然都是默認(rèn)使用的技術(shù)了,那就一用到底,數(shù)據(jù)源也用它的。有人就提出怎么才能不使用HikartCP用tomcat提供的默認(rèn)數(shù)據(jù)源對(duì)象呢?把HikartCP技術(shù)的坐標(biāo)排除掉就OK了。

? 第三種,DBCP,這個(gè)使用的條件就更苛刻了,既不使用HikartCP也不使用tomcat的DataSource時(shí),默認(rèn)給你用這個(gè)。

? 怎么配置使用這些東西呢?之前我們配置druid時(shí)使用druid的starter對(duì)應(yīng)的配置如下:

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

? 換成是默認(rèn)的數(shù)據(jù)源HikariCP后,直接吧druid刪掉就行了,如下:

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

? 當(dāng)然,也可以寫(xiě)上是對(duì)hikari做的配置,但是url地址要單獨(dú)配置,如下:

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

? 這就是配置hikari數(shù)據(jù)源的方式。如果想對(duì)hikari做進(jìn)一步的配置,可以繼續(xù)配置其獨(dú)立的屬性。例如:

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

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

總結(jié)

  1. springboot技術(shù)提供了3種內(nèi)置的數(shù)據(jù)源技術(shù),分別是Hikari、tomcat內(nèi)置數(shù)據(jù)源、DBCP

持久化技術(shù)

? 說(shuō)完數(shù)據(jù)源解決方案,再來(lái)說(shuō)一下持久化解決方案。springboot充分發(fā)揮其最強(qiáng)輔助的特征,給開(kāi)發(fā)者提供了一套現(xiàn)成的數(shù)據(jù)層技術(shù),叫做JdbcTemplate。其實(shí)這個(gè)技術(shù)不能說(shuō)是springboot提供的,因?yàn)椴皇褂胹pringboot技術(shù),一樣能使用它,誰(shuí)提供的呢?spring技術(shù)提供的,所以在springboot技術(shù)范疇中,這個(gè)技術(shù)也是存在的,畢竟springboot技術(shù)是加速spring程序開(kāi)發(fā)而創(chuàng)建的。

? 這個(gè)技術(shù)其實(shí)就是回歸到j(luò)dbc最原始的編程形式來(lái)進(jìn)行數(shù)據(jù)層的開(kāi)發(fā),下面直接上操作步驟:

步驟①:導(dǎo)入jdbc對(duì)應(yīng)的坐標(biāo),記得是starter

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

步驟②:自動(dòng)裝配JdbcTemplate對(duì)象

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

步驟③:使用JdbcTemplate實(shí)現(xiàn)查詢操作(非實(shí)體類封裝數(shù)據(jù)的查詢操作)

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

步驟④:使用JdbcTemplate實(shí)現(xiàn)查詢操作(實(shí)體類封裝數(shù)據(jù)的查詢操作)

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

步驟⑤:使用JdbcTemplate實(shí)現(xiàn)增刪改操作

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

? 如果想對(duì)JdbcTemplate對(duì)象進(jìn)行相關(guān)配置,可以在yml文件中進(jìn)行設(shè)定,具體如下:

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

總結(jié)

  1. SpringBoot內(nèi)置JdbcTemplate持久化解決方案
  2. 使用JdbcTemplate需要導(dǎo)入spring-boot-starter-jdbc的坐標(biāo)

數(shù)據(jù)庫(kù)技術(shù)

? 截止到目前,springboot給開(kāi)發(fā)者提供了內(nèi)置的數(shù)據(jù)源解決方案和持久化解決方案,在數(shù)據(jù)層解決方案三件套中還剩下一個(gè)數(shù)據(jù)庫(kù),莫非springboot也提供有內(nèi)置的解決方案?還真有,還不是一個(gè),三個(gè),這一節(jié)就來(lái)說(shuō)說(shuō)內(nèi)置的數(shù)據(jù)庫(kù)解決方案。

? springboot提供了3款內(nèi)置的數(shù)據(jù)庫(kù),分別是

  • H2
  • HSQL
  • Derby

? 以上三款數(shù)據(jù)庫(kù)除了可以獨(dú)立安裝之外,還可以像是tomcat服務(wù)器一樣,采用內(nèi)嵌的形式運(yùn)行在spirngboot容器中。內(nèi)嵌在容器中運(yùn)行,那必須是java對(duì)象啊,對(duì),這三款數(shù)據(jù)庫(kù)底層都是使用java語(yǔ)言開(kāi)發(fā)的。

? 我們一直使用MySQL數(shù)據(jù)庫(kù)就挺好的,為什么有需求用這個(gè)呢?原因就在于這三個(gè)數(shù)據(jù)庫(kù)都可以采用內(nèi)嵌容器的形式運(yùn)行,在應(yīng)用程序運(yùn)行后,如果我們進(jìn)行測(cè)試工作,此時(shí)測(cè)試的數(shù)據(jù)無(wú)需存儲(chǔ)在磁盤(pán)上,但是又要測(cè)試使用,內(nèi)嵌數(shù)據(jù)庫(kù)就方便了,運(yùn)行在內(nèi)存中,該測(cè)試測(cè)試,該運(yùn)行運(yùn)行,等服務(wù)器關(guān)閉后,一切煙消云散,多好,省得你維護(hù)外部數(shù)據(jù)庫(kù)了。這也是內(nèi)嵌數(shù)據(jù)庫(kù)的最大優(yōu)點(diǎn),方便進(jìn)行功能測(cè)試。

? 下面以H2數(shù)據(jù)庫(kù)為例講解如何使用這些內(nèi)嵌數(shù)據(jù)庫(kù),操作步驟也非常簡(jiǎn)單,簡(jiǎn)單才好用嘛

步驟①:導(dǎo)入H2數(shù)據(jù)庫(kù)對(duì)應(yīng)的坐標(biāo),一共2個(gè)

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

步驟②:將工程設(shè)置為web工程,啟動(dòng)工程時(shí)啟動(dòng)H2數(shù)據(jù)庫(kù)

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

步驟③:通過(guò)配置開(kāi)啟H2數(shù)據(jù)庫(kù)控制臺(tái)訪問(wèn)程序,也可以使用其他的數(shù)據(jù)庫(kù)連接軟件操作

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

? web端訪問(wèn)路徑/h2,訪問(wèn)密碼123456,如果訪問(wèn)失敗,先配置下列數(shù)據(jù)源,啟動(dòng)程序運(yùn)行后再次訪問(wèn)/h2路徑就可以正常訪問(wèn)了

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

步驟④:使用JdbcTemplate或MyBatisPlus技術(shù)操作數(shù)據(jù)庫(kù)

(略)

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

總結(jié)

  1. H2內(nèi)嵌式數(shù)據(jù)庫(kù)啟動(dòng)方式,添加坐標(biāo),添加配置
  2. H2數(shù)據(jù)庫(kù)線上運(yùn)行時(shí)請(qǐng)務(wù)必關(guān)閉

? 到這里SQL相關(guān)的數(shù)據(jù)層解決方案就講完了,現(xiàn)在的可選技術(shù)就豐富的多了。

  • 數(shù)據(jù)源技術(shù):Druid、Hikari、tomcat DataSource、DBCP
  • 持久化技術(shù):MyBatisPlus、MyBatis、JdbcTemplate
  • 數(shù)據(jù)庫(kù)技術(shù):MySQL、H2、HSQL、Derby

? 現(xiàn)在開(kāi)發(fā)程序時(shí)就可以在以上技術(shù)中任選一種組織成一套數(shù)據(jù)庫(kù)解決方案了。

KF-4-2.NoSQL

? SQL數(shù)據(jù)層解決方案說(shuō)完了,下面來(lái)說(shuō)收NoSQL數(shù)據(jù)層解決方案。這個(gè)NoSQL是什么意思呢?從字面來(lái)看,No表示否定,NoSQL就是非關(guān)系型數(shù)據(jù)庫(kù)解決方案,意思就是數(shù)據(jù)該存存該取取,只是這些數(shù)據(jù)不放在關(guān)系型數(shù)據(jù)庫(kù)中了,那放在哪里?自然是一些能夠存儲(chǔ)數(shù)據(jù)的其他相關(guān)技術(shù)中了,比如Redis等。

SpringBoot整合Redis

? Redis是一款采用key-value數(shù)據(jù)存儲(chǔ)格式的內(nèi)存級(jí)NoSQL數(shù)據(jù)庫(kù),重點(diǎn)關(guān)注數(shù)據(jù)存儲(chǔ)格式,是key-value格式,也就是鍵值對(duì)的存儲(chǔ)形式。與MySQL數(shù)據(jù)庫(kù)不同,MySQL數(shù)據(jù)庫(kù)有表、有字段、有記錄,Redis沒(méi)有這些東西,就是一個(gè)名稱對(duì)應(yīng)一個(gè)值,并且數(shù)據(jù)以存儲(chǔ)在內(nèi)存中使用為主。什么叫以存儲(chǔ)在內(nèi)存中為主?其實(shí)Redis有它的數(shù)據(jù)持久化方案,分別是RDB和AOF,但是Redis自身并不是為了數(shù)據(jù)持久化而生的,主要是在內(nèi)存中保存數(shù)據(jù),加速數(shù)據(jù)訪問(wèn)的,所以說(shuō)是一款內(nèi)存級(jí)數(shù)據(jù)庫(kù)。

? Redis支持多種數(shù)據(jù)存儲(chǔ)格式,比如可以直接存字符串,也可以存一個(gè)map集合,list集合,后面會(huì)涉及到一些不同格式的數(shù)據(jù)操作,這個(gè)需要先學(xué)習(xí)一下才能進(jìn)行整合,所以在基本操作中會(huì)介紹一些相關(guān)操作。下面就先安裝,再操作,最后說(shuō)整合

安裝

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

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

? 安裝完畢后會(huì)得到如下文件,其中有兩個(gè)文件對(duì)應(yīng)兩個(gè)命令,是啟動(dòng)Redis的核心命令,需要再CMD命令行模式執(zhí)行。

在這里插入圖片描述

啟動(dòng)服務(wù)器

redis-server.exe redis.windows.conf

? 初學(xué)者無(wú)需調(diào)整服務(wù)器對(duì)外服務(wù)端口,默認(rèn)6379。

啟動(dòng)客戶端

redis-cli.exe

? 如果啟動(dòng)redis服務(wù)器失敗,可以先啟動(dòng)客戶端,然后執(zhí)行shutdown操作后退出,此時(shí)redis服務(wù)器就可以正常執(zhí)行了。

基本操作

? 服務(wù)器啟動(dòng)后,使用客戶端就可以連接服務(wù)器,類似于啟動(dòng)完MySQL數(shù)據(jù)庫(kù),然后啟動(dòng)SQL命令行操作數(shù)據(jù)庫(kù)。

? 放置一個(gè)字符串?dāng)?shù)據(jù)到redis中,先為數(shù)據(jù)定義一個(gè)名稱,比如name,age等,然后使用命令set設(shè)置數(shù)據(jù)到redis服務(wù)器中即可

set name itheima
set age 12

? 從redis中取出已經(jīng)放入的數(shù)據(jù),根據(jù)名稱取,就可以得到對(duì)應(yīng)數(shù)據(jù)。如果沒(méi)有對(duì)應(yīng)數(shù)據(jù)就會(huì)得到(nil)

get name
get age

? 以上使用的數(shù)據(jù)存儲(chǔ)是一個(gè)名稱對(duì)應(yīng)一個(gè)值,如果要維護(hù)的數(shù)據(jù)過(guò)多,可以使用別的數(shù)據(jù)存儲(chǔ)結(jié)構(gòu)。例如hash,它是一種一個(gè)名稱下可以存儲(chǔ)多個(gè)數(shù)據(jù)的存儲(chǔ)模型,并且每個(gè)數(shù)據(jù)也可以有自己的二級(jí)存儲(chǔ)名稱。向hash結(jié)構(gòu)中存儲(chǔ)數(shù)據(jù)格式如下:

hset a a1 aa1		#對(duì)外key名稱是a,在名稱為a的存儲(chǔ)模型中,a1這個(gè)key中保存了數(shù)據(jù)aa1
hset a a2 aa2

? 獲取hash結(jié)構(gòu)中的數(shù)據(jù)命令如下

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

?

整合

? 在進(jìn)行整合之前先梳理一下整合的思想,springboot整合任何技術(shù)其實(shí)就是在springboot中使用對(duì)應(yīng)技術(shù)的API。如果兩個(gè)技術(shù)沒(méi)有交集,就不存在整合的概念了。所謂整合其實(shí)就是使用springboot技術(shù)去管理其他技術(shù),幾個(gè)問(wèn)題是躲不掉的。

? 第一,需要先導(dǎo)入對(duì)應(yīng)技術(shù)的坐標(biāo),而整合之后,這些坐標(biāo)都有了一些變化

? 第二,任何技術(shù)通常都會(huì)有一些相關(guān)的設(shè)置信息,整合之后,這些信息如何寫(xiě),寫(xiě)在哪是一個(gè)問(wèn)題

? 第三,沒(méi)有整合之前操作如果是模式A的話,整合之后如果沒(méi)有給開(kāi)發(fā)者帶來(lái)一些便捷操作,那整合將毫無(wú)意義,所以整合后操作肯定要簡(jiǎn)化一些,那對(duì)應(yīng)的操作方式自然也有所不同

? 按照上面的三個(gè)問(wèn)題去思考springboot整合所有技術(shù)是一種通用思想,在整合的過(guò)程中會(huì)逐步摸索出整合的套路,而且適用性非常強(qiáng),經(jīng)過(guò)若干種技術(shù)的整合后基本上可以總結(jié)出一套固定思維。

? 下面就開(kāi)始springboot整合redis,操作步驟如下:

步驟①:導(dǎo)入springboot整合redis的starter坐標(biāo)

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

? 上述坐標(biāo)可以在創(chuàng)建模塊的時(shí)候通過(guò)勾選的形式進(jìn)行選擇,歸屬NoSQL分類中

在這里插入圖片描述

步驟②:進(jìn)行基礎(chǔ)配置

spring:redis:host: localhostport: 6379

? 操作redis,最基本的信息就是操作哪一臺(tái)redis服務(wù)器,所以服務(wù)器地址屬于基礎(chǔ)配置信息,不可缺少。但是即便你不配置,目前也是可以用的。因?yàn)橐陨蟽山M信息都有默認(rèn)配置,剛好就是上述配置值。

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

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

? 在操作redis時(shí),需要先確認(rèn)操作何種數(shù)據(jù),根據(jù)數(shù)據(jù)種類得到操作接口。例如使用opsForValue()獲取string類型的數(shù)據(jù)操作接口,使用opsForHash()獲取hash類型的數(shù)據(jù)操作接口,剩下的就是調(diào)用對(duì)應(yīng)api操作了。各種類型的數(shù)據(jù)操作接口如下:

在這里插入圖片描述

總結(jié)

  1. springboot整合redis步驟
    1. 導(dǎo)入springboot整合redis的starter坐標(biāo)
    2. 進(jìn)行基礎(chǔ)配置
    3. 使用springboot整合redis的專用客戶端接口RedisTemplate操作

StringRedisTemplate

? 由于redis內(nèi)部不提供java對(duì)象的存儲(chǔ)格式,因此當(dāng)操作的數(shù)據(jù)以對(duì)象的形式存在時(shí),會(huì)進(jìn)行轉(zhuǎn)碼,轉(zhuǎn)換成字符串格式后進(jìn)行操作。為了方便開(kāi)發(fā)者使用基于字符串為數(shù)據(jù)的操作,springboot整合redis時(shí)提供了專用的API接口StringRedisTemplate,你可以理解為這是RedisTemplate的一種指定數(shù)據(jù)泛型的操作API。

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

redis客戶端選擇

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

步驟①:導(dǎo)入jedis坐標(biāo)

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

? jedis坐標(biāo)受springboot管理,無(wú)需提供版本號(hào)

步驟②:配置客戶端技術(shù)類型,設(shè)置為jedis

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

步驟③:根據(jù)需要設(shè)置對(duì)應(yīng)的配置

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

lettcus與jedis區(qū)別

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

總結(jié)

  1. springboot整合redis提供了StringRedisTemplate對(duì)象,以字符串的數(shù)據(jù)格式操作redis
  2. 如果需要切換redis客戶端實(shí)現(xiàn)技術(shù),可以通過(guò)配置的形式進(jìn)行

SpringBoot整合MongoDB

? 使用Redis技術(shù)可以有效的提高數(shù)據(jù)訪問(wèn)速度,但是由于Redis的數(shù)據(jù)格式單一性,無(wú)法操作結(jié)構(gòu)化數(shù)據(jù),當(dāng)操作對(duì)象型的數(shù)據(jù)時(shí),Redis就顯得捉襟見(jiàn)肘。在保障訪問(wèn)速度的情況下,如果想操作結(jié)構(gòu)化數(shù)據(jù),看來(lái)Redis無(wú)法滿足要求了,此時(shí)需要使用全新的數(shù)據(jù)存儲(chǔ)結(jié)束來(lái)解決此問(wèn)題,本節(jié)講解springboot如何整合MongoDB技術(shù)。

? MongoDB是一個(gè)開(kāi)源、高性能、無(wú)模式的文檔型數(shù)據(jù)庫(kù),它是NoSQL數(shù)據(jù)庫(kù)產(chǎn)品中的一種,是最像關(guān)系型數(shù)據(jù)庫(kù)的非關(guān)系型數(shù)據(jù)庫(kù)。

? 上述描述中幾個(gè)詞,其中對(duì)于我們最陌生的詞是無(wú)模式的。什么叫無(wú)模式呢?簡(jiǎn)單說(shuō)就是作為一款數(shù)據(jù)庫(kù),沒(méi)有固定的數(shù)據(jù)存儲(chǔ)結(jié)構(gòu),第一條數(shù)據(jù)可能有A、B、C一共3個(gè)字段,第二條數(shù)據(jù)可能有D、E、F也是3個(gè)字段,第三條數(shù)據(jù)可能是A、C、E3個(gè)字段,也就是說(shuō)數(shù)據(jù)的結(jié)構(gòu)不固定,這就是無(wú)模式。有人會(huì)說(shuō)這有什么用啊?靈活,隨時(shí)變更,不受約束。基于上述特點(diǎn),MongoDB的應(yīng)用面也會(huì)產(chǎn)生一些變化。以下列出了一些可以使用MongoDB作為數(shù)據(jù)存儲(chǔ)的場(chǎng)景,但是并不是必須使用MongoDB的場(chǎng)景:

  • 淘寶用戶數(shù)據(jù)
    • 存儲(chǔ)位置:數(shù)據(jù)庫(kù)
    • 特征:永久性存儲(chǔ),修改頻度極低
  • 游戲裝備數(shù)據(jù)、游戲道具數(shù)據(jù)
    • 存儲(chǔ)位置:數(shù)據(jù)庫(kù)、Mongodb
    • 特征:永久性存儲(chǔ)與臨時(shí)存儲(chǔ)相結(jié)合、修改頻度較高
  • 直播數(shù)據(jù)、打賞數(shù)據(jù)、粉絲數(shù)據(jù)
    • 存儲(chǔ)位置:數(shù)據(jù)庫(kù)、Mongodb
    • 特征:永久性存儲(chǔ)與臨時(shí)存儲(chǔ)相結(jié)合,修改頻度極高
  • 物聯(lián)網(wǎng)數(shù)據(jù)
    • 存儲(chǔ)位置:Mongodb
    • 特征:臨時(shí)存儲(chǔ),修改頻度飛速

? 快速了解一下MongoDB,下面直接開(kāi)始我們的學(xué)習(xí),老規(guī)矩,先安裝,再操作,最后說(shuō)整合

安裝

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

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

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

在這里插入圖片描述

? mongodb在運(yùn)行時(shí)需要指定一個(gè)數(shù)據(jù)存儲(chǔ)的目錄,所以創(chuàng)建一個(gè)數(shù)據(jù)存儲(chǔ)目錄,通常放置在安裝目錄中,此處創(chuàng)建data的目錄用來(lái)存儲(chǔ)數(shù)據(jù),具體如下

在這里插入圖片描述

? 如果在安裝的過(guò)程中出現(xiàn)了如下警告信息,就是告訴你,你當(dāng)前的操作系統(tǒng)缺少了一些系統(tǒng)文件,這個(gè)不用擔(dān)心。

在這里插入圖片描述

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

regsvr32 vcruntime140_1.dll

啟動(dòng)服務(wù)器

mongod --dbpath=..\data\db

? 啟動(dòng)服務(wù)器時(shí)需要指定數(shù)據(jù)存儲(chǔ)位置,通過(guò)參數(shù)–dbpath進(jìn)行設(shè)置,可以根據(jù)需要自行設(shè)置數(shù)據(jù)存儲(chǔ)路徑。默認(rèn)服務(wù)端口27017。

啟動(dòng)客戶端

mongo --host=127.0.0.1 --port=27017
基本操作

? MongoDB雖然是一款數(shù)據(jù)庫(kù),但是它的操作并不是使用SQL語(yǔ)句進(jìn)行的,因此操作方式各位小伙伴可能比較陌生,好在有一些類似于Navicat的數(shù)據(jù)庫(kù)客戶端軟件,能夠便捷的操作MongoDB,先安裝一個(gè)客戶端,再來(lái)操作MongoDB。

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

在這里插入圖片描述

? 打開(kāi)軟件首先要連接MongoDB服務(wù)器,選擇【File】菜單,選擇【Connect…】
在這里插入圖片描述

? 進(jìn)入連接管理界面后,選擇左上角的【Create】鏈接,創(chuàng)建新的連接設(shè)置

在這里插入圖片描述

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

在這里插入圖片描述

? 連接成功后在命令輸入?yún)^(qū)域輸入命令即可操作MongoDB。

? 創(chuàng)建數(shù)據(jù)庫(kù):在左側(cè)菜單中使用右鍵創(chuàng)建,輸入數(shù)據(jù)庫(kù)名稱即可

? 創(chuàng)建集合:在Collections上使用右鍵創(chuàng)建,輸入集合名稱即可,集合等同于數(shù)據(jù)庫(kù)中的表的作用

? 新增文檔:(文檔是一種類似json格式的數(shù)據(jù),初學(xué)者可以先把數(shù)據(jù)理解為就是json數(shù)據(jù))

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

? 刪除文檔:

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

? 修改文檔:

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

? 查詢文檔:

基礎(chǔ)查詢
查詢?nèi)?#xff1a;		   db.集合.find();
查第一條:		   db.集合.findOne()
查詢指定數(shù)量文檔:	db.集合.find().limit(10)					//查10條文檔
跳過(guò)指定數(shù)量文檔:	db.集合.find().skip(20)					//跳過(guò)20條文檔
統(tǒng)計(jì):			  	db.集合.count()
排序:				db.集合.sort({age:1})						//按age升序排序
投影:				db.集合名稱.find(條件,{name:1,age:1})		 //僅保留name與age域條件查詢
基本格式:			db.集合.find({條件})
模糊查詢:			db.集合.find({域名:/正則表達(dá)式/})		  //等同SQL中的like,比like強(qiáng)大,可以執(zhí)行正則所有規(guī)則
條件比較運(yùn)算:		   db.集合.find({域名:{$gt:值}})				//等同SQL中的數(shù)值比較操作,例如:name>18
包含查詢:			db.集合.find({域名:{$in:[值1,值2]}})		//等同于SQL中的in
條件連接查詢:		   db.集合.find({$and:[{條件1},{條件2}]})	   //等同于SQL中的and、or

? 有關(guān)MongoDB的基礎(chǔ)操作就普及到這里,需要全面掌握MongoDB技術(shù),請(qǐng)參看相關(guān)教程學(xué)習(xí)。

整合

? 使用springboot整合MongDB該如何進(jìn)行呢?其實(shí)springboot為什么使用的開(kāi)發(fā)者這么多,就是因?yàn)樗奶茁穾缀跬耆粯印?dǎo)入坐標(biāo),做配置,使用API接口操作。整合Redis如此,整合MongoDB同樣如此。

? 第一,先導(dǎo)入對(duì)應(yīng)技術(shù)的整合starter坐標(biāo)

? 第二,配置必要信息

? 第三,使用提供的API操作即可

? 下面就開(kāi)始springboot整合MongoDB,操作步驟如下:

步驟①:導(dǎo)入springboot整合MongoDB的starter坐標(biāo)

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

? 上述坐標(biāo)也可以在創(chuàng)建模塊的時(shí)候通過(guò)勾選的形式進(jìn)行選擇,同樣歸屬NoSQL分類中

在這里插入圖片描述

步驟②:進(jìn)行基礎(chǔ)配置

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

? 操作MongoDB需要的配置與操作redis一樣,最基本的信息都是操作哪一臺(tái)服務(wù)器,區(qū)別就是連接的服務(wù)器IP地址和端口不同,書(shū)寫(xiě)格式不同而已。

步驟③:使用springboot整合MongoDB的專用客戶端接口MongoTemplate來(lái)進(jìn)行操作

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

? 整合工作到這里就做完了,感覺(jué)既熟悉也陌生。熟悉的是這個(gè)套路,三板斧,就這三招,導(dǎo)坐標(biāo)做配置用API操作,陌生的是這個(gè)技術(shù),里面具體的操作API可能會(huì)不熟悉,有關(guān)springboot整合MongoDB我們就講到這里。有興趣可以繼續(xù)學(xué)習(xí)MongoDB的操作,然后再來(lái)這里通過(guò)編程的形式操作MongoDB。

總結(jié)

  1. springboot整合MongoDB步驟
    1. 導(dǎo)入springboot整合MongoDB的starter坐標(biāo)
    2. 進(jìn)行基礎(chǔ)配置
    3. 使用springboot整合MongoDB的專用客戶端接口MongoTemplate操作

SpringBoot整合ES

? NoSQL解決方案已經(jīng)講完了兩種技術(shù)的整合了,Redis可以使用內(nèi)存加載數(shù)據(jù)并實(shí)現(xiàn)數(shù)據(jù)快速訪問(wèn),MongoDB可以在內(nèi)存中存儲(chǔ)類似對(duì)象的數(shù)據(jù)并實(shí)現(xiàn)數(shù)據(jù)的快速訪問(wèn),在企業(yè)級(jí)開(kāi)發(fā)中對(duì)于速度的追求是永無(wú)止境的。下面要講的內(nèi)容也是一款NoSQL解決方案,只不過(guò)他的作用不是為了直接加速數(shù)據(jù)的讀寫(xiě),而是加速數(shù)據(jù)的查詢的,叫做ES技術(shù)。

? ES(Elasticsearch)是一個(gè)分布式全文搜索引擎,重點(diǎn)是全文搜索。

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

? 要實(shí)現(xiàn)全文搜索的效果,不可能使用數(shù)據(jù)庫(kù)中l(wèi)ike操作去進(jìn)行比對(duì),這種效率太低了。ES設(shè)計(jì)了一種全新的思想,來(lái)實(shí)現(xiàn)全文搜索。具體操作過(guò)程如下:

  1. 將被查詢的字段的數(shù)據(jù)全部文本信息進(jìn)行查分,分成若干個(gè)詞

    • 例如“中華人民共和國(guó)”就會(huì)被拆分成三個(gè)詞,分別是“中華”、“人民”、“共和國(guó)”,此過(guò)程有專業(yè)術(shù)語(yǔ)叫做分詞。分詞的策略不同,分出的效果不一樣,不同的分詞策略稱為分詞器。
  2. 將分詞得到的結(jié)果存儲(chǔ)起來(lái),對(duì)應(yīng)每條數(shù)據(jù)的id

    • 例如id為1的數(shù)據(jù)中名稱這一項(xiàng)的值是“中華人民共和國(guó)”,那么分詞結(jié)束后,就會(huì)出現(xiàn)“中華”對(duì)應(yīng)id為1,“人民”對(duì)應(yīng)id為1,“共和國(guó)”對(duì)應(yīng)id為1

    • 例如id為2的數(shù)據(jù)中名稱這一項(xiàng)的值是“人民代表大會(huì)“,那么分詞結(jié)束后,就會(huì)出現(xiàn)“人民”對(duì)應(yīng)id為2,“代表”對(duì)應(yīng)id為2,“大會(huì)”對(duì)應(yīng)id為2

    • 此時(shí)就會(huì)出現(xiàn)如下對(duì)應(yīng)結(jié)果,按照上述形式可以對(duì)所有文檔進(jìn)行分詞。需要注意分詞的過(guò)程不是僅對(duì)一個(gè)字段進(jìn)行,而是對(duì)每一個(gè)參與查詢的字段都執(zhí)行,最終結(jié)果匯總到一個(gè)表格中

      分詞結(jié)果關(guān)鍵字對(duì)應(yīng)id
      中華1
      人民1,2
      共和國(guó)1
      代表2
      大會(huì)2
  3. 當(dāng)進(jìn)行查詢時(shí),如果輸入“人民”作為查詢條件,可以通過(guò)上述表格數(shù)據(jù)進(jìn)行比對(duì),得到id值1,2,然后根據(jù)id值就可以得到查詢的結(jié)果數(shù)據(jù)了。

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

? 通過(guò)上述內(nèi)容的學(xué)習(xí),發(fā)現(xiàn)使用ES其實(shí)準(zhǔn)備工作還是挺多的,必須先建立文檔的倒排索引,然后才能繼續(xù)使用。快速了解一下ES的工作原理,下面直接開(kāi)始我們的學(xué)習(xí),老規(guī)矩,先安裝,再操作,最后說(shuō)整合。

安裝

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

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

在這里插入圖片描述

  • bin目錄:包含所有的可執(zhí)行命令
  • config目錄:包含ES服務(wù)器使用的配置文件
  • jdk目錄:此目錄中包含了一個(gè)完整的jdk工具包,版本17,當(dāng)ES升級(jí)時(shí),使用最新版本的jdk確保不會(huì)出現(xiàn)版本支持性不足的問(wèn)題
  • lib目錄:包含ES運(yùn)行的依賴jar文件
  • logs目錄:包含ES運(yùn)行后產(chǎn)生的所有日志文件
  • modules目錄:包含ES軟件中所有的功能模塊,也是一個(gè)一個(gè)的jar包。和jar目錄不同,jar目錄是ES運(yùn)行期間依賴的jar包,modules是ES軟件自己的功能jar包
  • plugins目錄:包含ES軟件安裝的插件,默認(rèn)為空

啟動(dòng)服務(wù)器

elasticsearch.bat

? 雙擊elasticsearch.bat文件即可啟動(dòng)ES服務(wù)器,默認(rèn)服務(wù)端口9200。通過(guò)瀏覽器訪問(wèn)http://localhost:9200看到如下信息視為ES服務(wù)器正常啟動(dòng)

{"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中保存有我們要查詢的數(shù)據(jù),只不過(guò)格式和數(shù)據(jù)庫(kù)存儲(chǔ)數(shù)據(jù)格式不同而已。在ES中我們要先創(chuàng)建倒排索引,這個(gè)索引的功能又點(diǎn)類似于數(shù)據(jù)庫(kù)的表,然后將數(shù)據(jù)添加到倒排索引中,添加的數(shù)據(jù)稱為文檔。所以要進(jìn)行ES的操作要先創(chuàng)建索引,再添加文檔,這樣才能進(jìn)行后續(xù)的查詢操作。

? 要操作ES可以通過(guò)Rest風(fēng)格的請(qǐng)求來(lái)進(jìn)行,也就是說(shuō)發(fā)送一個(gè)請(qǐng)求就可以執(zhí)行一個(gè)操作。比如新建索引,刪除索引這些操作都可以使用發(fā)送請(qǐng)求的形式來(lái)進(jìn)行。

  • 創(chuàng)建索引,books是索引名稱,下同

    PUT請(qǐng)求		http://localhost:9200/books
    

    發(fā)送請(qǐng)求后,看到如下信息即索引創(chuàng)建成功

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

    重復(fù)創(chuàng)建已經(jīng)存在的索引會(huì)出現(xiàn)錯(cuò)誤信息,reason屬性中描述錯(cuò)誤原因

    {"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索引已經(jīng)存在"index_uuid": "VgC_XMVAQmedaiBNSgO2-w","index": "book"},"status": 400
    }
    
  • 查詢索引

    GET請(qǐng)求		http://localhost:9200/books
    

    查詢索引得到索引相關(guān)信息,如下

    {"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"}}}}
    }
    

    如果查詢了不存在的索引,會(huì)返回錯(cuò)誤信息,例如查詢名稱為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]",		# 沒(méi)有book索引"resource.type": "index_or_alias","resource.id": "book","index_uuid": "_na_","index": "book"},"status": 404
    }
    
  • 刪除索引

    DELETE請(qǐng)求	http://localhost:9200/books
    

    刪除所有后,給出刪除結(jié)果

    {"acknowledged": true
    }
    

    如果重復(fù)刪除,會(huì)給出錯(cuò)誤信息,同樣在reason屬性中描述具體的錯(cuò)誤原因

    {"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]",		# 沒(méi)有books索引"resource.type": "index_or_alias","resource.id": "book","index_uuid": "_na_","index": "book"},"status": 404
    }
    
  • 創(chuàng)建索引并指定分詞器

    ? 前面創(chuàng)建的索引是未指定分詞器的,可以在創(chuàng)建索引時(shí)添加請(qǐng)求參數(shù),設(shè)置分詞器。目前國(guó)內(nèi)較為流行的分詞器是IK分詞器,使用前先在下對(duì)應(yīng)的分詞器,然后使用。IK分詞器下載地址:https://github.com/medcl/elasticsearch-analysis-ik/releases

    ? 分詞器下載后解壓到ES安裝目錄的plugins目錄中即可,安裝分詞器后需要重新啟動(dòng)ES服務(wù)器。使用IK分詞器創(chuàng)建索引格式:

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

    ? 創(chuàng)建完畢后返回結(jié)果和不使用分詞器創(chuàng)建索引的結(jié)果是一樣的,此時(shí)可以通過(guò)查看索引信息觀察到添加的請(qǐng)求參數(shù)mappings已經(jīng)進(jìn)入到了索引屬性中

    {"books": {"aliases": {},"mappings": {						#mappings屬性已經(jīng)被替換"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"}}}}
    }
    

目前我們已經(jīng)有了索引了,但是索引中還沒(méi)有數(shù)據(jù),所以要先添加數(shù)據(jù),ES中稱數(shù)據(jù)為文檔,下面進(jìn)行文檔操作。

  • 添加文檔,有三種方式

    POST請(qǐng)求	http://localhost:9200/books/_doc		#使用系統(tǒng)生成id
    POST請(qǐng)求	http://localhost:9200/books/_create/1	#使用指定id
    POST請(qǐng)求	http://localhost:9200/books/_doc/1		#使用指定id,不存在創(chuàng)建,存在更新(版本遞增)文檔通過(guò)請(qǐng)求參數(shù)傳遞,數(shù)據(jù)格式j(luò)son
    {"name":"springboot","type":"springboot","description":"springboot"
    }  
    
  • 查詢文檔

    GET請(qǐng)求	http://localhost:9200/books/_doc/1		 #查詢單個(gè)文檔 		
    GET請(qǐng)求	http://localhost:9200/books/_search		 #查詢?nèi)课臋n
    
  • 條件查詢

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

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

    PUT請(qǐng)求	http://localhost:9200/books/_doc/1文檔通過(guò)請(qǐng)求參數(shù)傳遞,數(shù)據(jù)格式j(luò)son
    {"name":"springboot","type":"springboot","description":"springboot"
    }
    
  • 修改文檔(部分更新)

    POST請(qǐng)求	http://localhost:9200/books/_update/1文檔通過(guò)請(qǐng)求參數(shù)傳遞,數(shù)據(jù)格式j(luò)son
    {			"doc":{						#部分更新并不是對(duì)原始文檔進(jìn)行更新,而是對(duì)原始文檔對(duì)象中的doc屬性中的指定屬性更新"name":"springboot"		#僅更新提供的屬性值,未提供的屬性值不參與更新操作}
    }
    
整合

? 使用springboot整合ES該如何進(jìn)行呢?老規(guī)矩,導(dǎo)入坐標(biāo),做配置,使用API接口操作。整合Redis如此,整合MongoDB如此,整合ES依然如此。太沒(méi)有新意了,其實(shí)不是沒(méi)有新意,這就是springboot的強(qiáng)大之處,所有東西都做成相同規(guī)則,對(duì)開(kāi)發(fā)者來(lái)說(shuō)非常友好。

? 下面就開(kāi)始springboot整合ES,操作步驟如下:

步驟①:導(dǎo)入springboot整合ES的starter坐標(biāo)

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

步驟②:進(jìn)行基礎(chǔ)配置

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

? 配置ES服務(wù)器地址,端口9200

步驟③:使用springboot整合ES的專用客戶端接口ElasticsearchRestTemplate來(lái)進(jìn)行操作

@SpringBootTest
class Springboot18EsApplicationTests {@Autowiredprivate ElasticsearchRestTemplate template;
}

? 上述操作形式是ES早期的操作方式,使用的客戶端被稱為L(zhǎng)ow Level Client,這種客戶端操作方式性能方面略顯不足,于是ES開(kāi)發(fā)了全新的客戶端操作方式,稱為High Level Client。高級(jí)別客戶端與ES版本同步更新,但是springboot最初整合ES的時(shí)候使用的是低級(jí)別客戶端,所以企業(yè)開(kāi)發(fā)需要更換成高級(jí)別的客戶端模式。

? 下面使用高級(jí)別客戶端方式進(jìn)行springboot整合ES,操作步驟如下:

步驟①:導(dǎo)入springboot整合ES高級(jí)別客戶端的坐標(biāo),此種形式目前沒(méi)有對(duì)應(yīng)的starter

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

步驟②:使用編程的形式設(shè)置連接的ES服務(wù)器,并獲取客戶端對(duì)象

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

? 配置ES服務(wù)器地址與端口9200,記得客戶端使用完畢需要手工關(guān)閉。由于當(dāng)前客戶端是手工維護(hù)的,因此不能通過(guò)自動(dòng)裝配的形式加載對(duì)象。

步驟③:使用客戶端對(duì)象操作ES,例如創(chuàng)建索引

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

? 高級(jí)別客戶端操作是通過(guò)發(fā)送請(qǐng)求的方式完成所有操作的,ES針對(duì)各種不同的操作,設(shè)定了各式各樣的請(qǐng)求對(duì)象,上例中創(chuàng)建索引的對(duì)象是CreateIndexRequest,其他操作也會(huì)有自己專用的Request對(duì)象。

? 當(dāng)前操作我們發(fā)現(xiàn),無(wú)論進(jìn)行ES何種操作,第一步永遠(yuǎn)是獲取RestHighLevelClient對(duì)象,最后一步永遠(yuǎn)是關(guān)閉該對(duì)象的連接。在測(cè)試中可以使用測(cè)試類的特性去幫助開(kāi)發(fā)者一次性的完成上述操作,但是在業(yè)務(wù)書(shū)寫(xiě)時(shí),還需要自行管理。將上述代碼格式轉(zhuǎn)換成使用測(cè)試類的初始化方法和銷(xiāo)毀方法進(jìn)行客戶端對(duì)象的維護(hù)。

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

? 現(xiàn)在的書(shū)寫(xiě)簡(jiǎn)化了很多,也更合理。下面使用上述模式將所有的ES操作執(zhí)行一遍,測(cè)試結(jié)果

創(chuàng)建索引(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" +"}";//設(shè)置請(qǐng)求中的參數(shù)request.source(json, XContentType.JSON);client.indices().create(request, RequestOptions.DEFAULT);
}

? IK分詞器是通過(guò)請(qǐng)求參數(shù)的形式進(jìn)行設(shè)置的,設(shè)置請(qǐng)求參數(shù)使用request對(duì)象中的source方法進(jìn)行設(shè)置,至于參數(shù)是什么,取決于你的操作種類。當(dāng)請(qǐng)求中需要參數(shù)時(shí),均可使用當(dāng)前形式進(jìn)行參數(shù)設(shè)置。

添加文檔

@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);
}

? 添加文檔使用的請(qǐng)求對(duì)象是IndexRequest,與創(chuàng)建索引使用的請(qǐng)求對(duì)象不同。

批量添加文檔

@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);
}

? 批量做時(shí),先創(chuàng)建一個(gè)BulkRequest的對(duì)象,可以將該對(duì)象理解為是一個(gè)保存request對(duì)象的容器,將所有的請(qǐng)求都初始化好后,添加到BulkRequest對(duì)象中,再使用BulkRequest對(duì)象的bulk方法,一次性執(zhí)行完畢。

按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);
}

? 根據(jù)id查詢文檔使用的請(qǐng)求對(duì)象是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);}
}

? 按條件查詢文檔使用的請(qǐng)求對(duì)象是SearchRequest,查詢時(shí)調(diào)用SearchRequest對(duì)象的termQuery方法,需要給出查詢屬性名,此處支持使用合并字段,也就是前面定義索引屬性時(shí)添加的all屬性。

? springboot整合ES的操作到這里就說(shuō)完了,與前期進(jìn)行springboot整合redis和mongodb的差別還是蠻大的,主要原始就是我們沒(méi)有使用springboot整合ES的客戶端對(duì)象。至于操作,由于ES操作種類過(guò)多,所以顯得操作略微有點(diǎn)復(fù)雜。有關(guān)springboot整合ES就先學(xué)習(xí)到這里吧。

總結(jié)

  1. springboot整合ES步驟
    1. 導(dǎo)入springboot整合ES的High Level Client坐標(biāo)
    2. 手工管理客戶端對(duì)象,包括初始化和關(guān)閉操作
    3. 使用High Level Client根據(jù)操作的種類不同,選擇不同的Request對(duì)象完成對(duì)應(yīng)操作

KF-5.整合第三方技術(shù)

? 通過(guò)第四章的學(xué)習(xí),我們領(lǐng)略到了springboot在整合第三方技術(shù)時(shí)強(qiáng)大的一致性,在第五章中我們要使用springboot繼續(xù)整合各種各樣的第三方技術(shù),通過(guò)本章的學(xué)習(xí),可以將之前學(xué)習(xí)的springboot整合第三方技術(shù)的思想貫徹到底,還是那三板斧。導(dǎo)坐標(biāo)、做配置、調(diào)API。

? springboot能夠整合的技術(shù)實(shí)在是太多了,可以說(shuō)是萬(wàn)物皆可整。本章將從企業(yè)級(jí)開(kāi)發(fā)中常用的一些技術(shù)作為出發(fā)點(diǎn),對(duì)各種各樣的技術(shù)進(jìn)行整合。

KF-5-1.緩存

? 企業(yè)級(jí)應(yīng)用主要作用是信息處理,當(dāng)需要讀取數(shù)據(jù)時(shí),由于受限于數(shù)據(jù)庫(kù)的訪問(wèn)效率,導(dǎo)致整體系統(tǒng)性能偏低。

在這里插入圖片描述

? 應(yīng)用程序直接與數(shù)據(jù)庫(kù)打交道,訪問(wèn)效率低

? 為了改善上述現(xiàn)象,開(kāi)發(fā)者通常會(huì)在應(yīng)用程序與數(shù)據(jù)庫(kù)之間建立一種臨時(shí)的數(shù)據(jù)存儲(chǔ)機(jī)制,該區(qū)域中的數(shù)據(jù)在內(nèi)存中保存,讀寫(xiě)速度較快,可以有效解決數(shù)據(jù)庫(kù)訪問(wèn)效率低下的問(wèn)題。這一塊臨時(shí)存儲(chǔ)數(shù)據(jù)的區(qū)域就是緩存。

在這里插入圖片描述

使用緩存后,應(yīng)用程序與緩存打交道,緩存與數(shù)據(jù)庫(kù)打交道,數(shù)據(jù)訪問(wèn)效率提高

? 緩存是什么?緩存是一種介于數(shù)據(jù)永久存儲(chǔ)介質(zhì)與應(yīng)用程序之間的數(shù)據(jù)臨時(shí)存儲(chǔ)介質(zhì),使用緩存可以有效的減少低速數(shù)據(jù)讀取過(guò)程的次數(shù)(例如磁盤(pán)IO),提高系統(tǒng)性能。此外緩存不僅可以用于提高永久性存儲(chǔ)介質(zhì)的數(shù)據(jù)讀取效率,還可以提供臨時(shí)的數(shù)據(jù)存儲(chǔ)空間。而springboot提供了對(duì)市面上幾乎所有的緩存技術(shù)進(jìn)行整合的方案,下面就一起開(kāi)啟springboot整合緩存之旅。

SpringBoot內(nèi)置緩存解決方案

? springboot技術(shù)提供有內(nèi)置的緩存解決方案,可以幫助開(kāi)發(fā)者快速開(kāi)啟緩存技術(shù),并使用緩存技術(shù)進(jìn)行數(shù)據(jù)的快速操作,例如讀取緩存數(shù)據(jù)和寫(xiě)入數(shù)據(jù)到緩存。

步驟①:導(dǎo)入springboot提供的緩存技術(shù)對(duì)應(yīng)的starter

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

步驟②:啟用緩存,在引導(dǎo)類上方標(biāo)注注解@EnableCaching配置springboot程序中可以使用緩存

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

步驟③:設(shè)置操作的數(shù)據(jù)是否使用緩存

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

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

? 使用@Cacheable注解后,執(zhí)行當(dāng)前操作,如果發(fā)現(xiàn)對(duì)應(yīng)名稱在緩存中沒(méi)有數(shù)據(jù),就正常讀取數(shù)據(jù),然后放入緩存;如果對(duì)應(yīng)名稱在緩存中有數(shù)據(jù),就終止當(dāng)前業(yè)務(wù)方法執(zhí)行,直接返回緩存中的數(shù)據(jù)。

手機(jī)驗(yàn)證碼案例

? 為了便于下面演示各種各樣的緩存技術(shù),我們創(chuàng)建一個(gè)手機(jī)驗(yàn)證碼的案例環(huán)境,模擬使用緩存保存手機(jī)驗(yàn)證碼的過(guò)程。

? 手機(jī)驗(yàn)證碼案例需求如下:

  • 輸入手機(jī)號(hào)獲取驗(yàn)證碼,組織文檔以短信形式發(fā)送給用戶(頁(yè)面模擬)
  • 輸入手機(jī)號(hào)和驗(yàn)證碼驗(yàn)證結(jié)果

? 為了描述上述操作,我們制作兩個(gè)表現(xiàn)層接口,一個(gè)用來(lái)模擬發(fā)送短信的過(guò)程,其實(shí)就是根據(jù)用戶提供的手機(jī)號(hào)生成一個(gè)驗(yàn)證碼,然后放入緩存,另一個(gè)用來(lái)模擬驗(yàn)證碼校驗(yàn)的過(guò)程,其實(shí)就是使用傳入的手機(jī)號(hào)和驗(yàn)證碼進(jìn)行匹配,并返回最終匹配結(jié)果。下面直接制作本案例的模擬代碼,先以上例中springboot提供的內(nèi)置緩存技術(shù)來(lái)完成當(dāng)前案例的制作。

步驟①:導(dǎo)入springboot提供的緩存技術(shù)對(duì)應(yīng)的starter

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

步驟②:啟用緩存,在引導(dǎo)類上方標(biāo)注注解@EnableCaching配置springboot程序中可以使用緩存

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

步驟③:定義驗(yàn)證碼對(duì)應(yīng)的實(shí)體類,封裝手機(jī)號(hào)與驗(yàn)證碼兩個(gè)屬性

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

步驟④:定義驗(yàn)證碼功能的業(yè)務(wù)層接口與實(shí)現(xiàn)類

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

? 獲取驗(yàn)證碼后,當(dāng)驗(yàn)證碼失效時(shí)必須重新獲取驗(yàn)證碼,因此在獲取驗(yàn)證碼的功能上不能使用@Cacheable注解,@Cacheable注解是緩存中沒(méi)有值則放入值,緩存中有值則取值。此處的功能僅僅是生成驗(yàn)證碼并放入緩存,并不具有從緩存中取值的功能,因此不能使用@Cacheable注解,應(yīng)該使用僅具有向緩存中保存數(shù)據(jù)的功能,使用@CachePut注解即可。

? 對(duì)于校驗(yàn)驗(yàn)證碼的功能建議放入工具類中進(jìn)行。

步驟⑤:定義驗(yàn)證碼的生成策略與根據(jù)手機(jī)號(hào)讀取驗(yàn)證碼的功能

@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;}
}

步驟⑥:定義驗(yàn)證碼功能的web層接口,一個(gè)方法用于提供手機(jī)號(hào)獲取驗(yàn)證碼,一個(gè)方法用于提供手機(jī)號(hào)和驗(yàn)證碼進(jìn)行校驗(yàn)

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

SpringBoot整合Ehcache緩存

? 手機(jī)驗(yàn)證碼的案例已經(jīng)完成了,下面就開(kāi)始springboot整合各種各樣的緩存技術(shù),第一個(gè)整合Ehcache技術(shù)。Ehcache是一種緩存技術(shù),使用springboot整合Ehcache其實(shí)就是變更一下緩存技術(shù)的實(shí)現(xiàn)方式,話不多說(shuō),直接開(kāi)整

步驟①:導(dǎo)入Ehcache的坐標(biāo)

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

? 此處為什么不是導(dǎo)入Ehcache的starter,而是導(dǎo)入技術(shù)坐標(biāo)呢?其實(shí)springboot整合緩存技術(shù)做的是通用格式,不管你整合哪種緩存技術(shù),只是實(shí)現(xiàn)變化了,操作方式一樣。這也體現(xiàn)出springboot技術(shù)的優(yōu)點(diǎn),統(tǒng)一同類技術(shù)的整合方式。

步驟②:配置緩存技術(shù)實(shí)現(xiàn)使用Ehcache

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

? 配置緩存的類型type為ehcache,此處需要說(shuō)明一下,當(dāng)前springboot可以整合的緩存技術(shù)中包含有ehcach,所以可以這樣書(shū)寫(xiě)。其實(shí)這個(gè)type不可以隨便寫(xiě)的,不是隨便寫(xiě)一個(gè)名稱就可以整合的。

? 由于ehcache的配置有獨(dú)立的配置文件格式,因此還需要指定ehcache的配置文件,以便于讀取相應(yīng)配置

<?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" /><!--默認(rèn)緩存策略 --><!-- external:是否永久存在,設(shè)置為true則不會(huì)被清除,此時(shí)與timeout沖突,通常設(shè)置為false--><!-- diskPersistent:是否啟用磁盤(pán)持久化--><!-- maxElementsInMemory:最大緩存數(shù)量--><!-- overflowToDisk:超過(guò)最大緩存數(shù)量是否持久化到磁盤(pán)--><!-- timeToIdleSeconds:最大不活動(dòng)間隔,設(shè)置過(guò)長(zhǎng)緩存容易溢出,設(shè)置過(guò)短無(wú)效果,可用于記錄時(shí)效性數(shù)據(jù),例如驗(yàn)證碼--><!-- timeToLiveSeconds:最大存活時(shí)間--><!-- memoryStoreEvictionPolicy:緩存清除策略--><defaultCacheeternal="false"diskPersistent="false"maxElementsInMemory="1000"overflowToDisk="false"timeToIdleSeconds="60"timeToLiveSeconds="60"memoryStoreEvictionPolicy="LRU" /><cachename="smsCode"eternal="false"diskPersistent="false"maxElementsInMemory="1000"overflowToDisk="false"timeToIdleSeconds="10"timeToLiveSeconds="10"memoryStoreEvictionPolicy="LRU" />
</ehcache>

? 注意前面的案例中,設(shè)置了數(shù)據(jù)保存的位置是smsCode

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

? 這個(gè)設(shè)定需要保障ehcache中有一個(gè)緩存空間名稱叫做smsCode的配置,前后要統(tǒng)一。在企業(yè)開(kāi)發(fā)過(guò)程中,通過(guò)設(shè)置不同名稱的cache來(lái)設(shè)定不同的緩存策略,應(yīng)用于不同的緩存數(shù)據(jù)。

? 到這里springboot整合Ehcache就做完了,可以發(fā)現(xiàn)一點(diǎn),原始代碼沒(méi)有任何修改,僅僅是加了一組配置就可以變更緩存供應(yīng)商了,這也是springboot提供了統(tǒng)一的緩存操作接口的優(yōu)勢(shì),變更實(shí)現(xiàn)并不影響原始代碼的書(shū)寫(xiě)。

總結(jié)

  1. springboot使用Ehcache作為緩存實(shí)現(xiàn)需要導(dǎo)入Ehcache的坐標(biāo)
  2. 修改設(shè)置,配置緩存供應(yīng)商為ehcache,并提供對(duì)應(yīng)的緩存配置文件

?

SpringBoot整合Redis緩存

? 上節(jié)使用Ehcache替換了springboot內(nèi)置的緩存技術(shù),其實(shí)springboot支持的緩存技術(shù)還很多,下面使用redis技術(shù)作為緩存解決方案來(lái)實(shí)現(xiàn)手機(jī)驗(yàn)證碼案例。

? 比對(duì)使用Ehcache的過(guò)程,加坐標(biāo),改緩存實(shí)現(xiàn)類型為ehcache,做Ehcache的配置。如果還成redis做緩存呢?一模一樣,加坐標(biāo),改緩存實(shí)現(xiàn)類型為redis,做redis的配置。差別之處只有一點(diǎn),redis的配置可以在yml文件中直接進(jìn)行配置,無(wú)需制作獨(dú)立的配置文件。

步驟①:導(dǎo)入redis的坐標(biāo)

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

步驟②:配置緩存技術(shù)實(shí)現(xiàn)使用redis

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

? 如果需要對(duì)redis作為緩存進(jìn)行配置,注意不是對(duì)原始的redis進(jìn)行配置,而是配置redis作為緩存使用相關(guān)的配置,隸屬于spring.cache.redis節(jié)點(diǎn)下,注意不要寫(xiě)錯(cuò)位置了。

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

總結(jié)

  1. springboot使用redis作為緩存實(shí)現(xiàn)需要導(dǎo)入redis的坐標(biāo)
  2. 修改設(shè)置,配置緩存供應(yīng)商為redis,并提供對(duì)應(yīng)的緩存配置

SpringBoot整合Memcached緩存

? 目前我們已經(jīng)掌握了3種緩存解決方案的配置形式,分別是springboot內(nèi)置緩存,ehcache和redis,本節(jié)研究一下國(guó)內(nèi)比較流行的一款緩存memcached。

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

安裝

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

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

在這里插入圖片描述

? 可執(zhí)行文件只有一個(gè)memcached.exe,使用該文件可以將memcached作為系統(tǒng)服務(wù)啟動(dòng),執(zhí)行此文件時(shí)會(huì)出現(xiàn)報(bào)錯(cuò)信息,如下:

在這里插入圖片描述

? 此處出現(xiàn)問(wèn)題的原因是注冊(cè)系統(tǒng)服務(wù)時(shí)需要使用管理員權(quán)限,當(dāng)前賬號(hào)權(quán)限不足導(dǎo)致安裝服務(wù)失敗,切換管理員賬號(hào)權(quán)限啟動(dòng)命令行

在這里插入圖片描述

? 然后再次執(zhí)行安裝服務(wù)的命令即可,如下:

memcached.exe -d install

? 服務(wù)安裝完畢后可以使用命令啟動(dòng)和停止服務(wù),如下:

memcached.exe -d start		# 啟動(dòng)服務(wù)
memcached.exe -d stop		# 停止服務(wù)

? 也可以在任務(wù)管理器中進(jìn)行服務(wù)狀態(tài)的切換

在這里插入圖片描述

變更緩存為Memcached

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

? memcached目前提供有三種客戶端技術(shù),分別是Memcached Client for Java、SpyMemcached和Xmemcached,其中性能指標(biāo)各方面最好的客戶端是Xmemcached,本次整合就使用這個(gè)作為客戶端實(shí)現(xiàn)技術(shù)了。下面開(kāi)始使用Xmemcached

步驟①:導(dǎo)入xmemcached的坐標(biāo)

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

步驟②:配置memcached,制作memcached的配置類

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

? memcached默認(rèn)對(duì)外服務(wù)端口11211。

步驟③:使用xmemcached客戶端操作緩存,注入MemcachedClient對(duì)象

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

? 設(shè)置值到緩存中使用set操作,取值使用get操作,其實(shí)更符合我們開(kāi)發(fā)者的習(xí)慣。

? 上述代碼中對(duì)于服務(wù)器的配置使用硬編碼寫(xiě)死到了代碼中,將此數(shù)據(jù)提取出來(lái),做成獨(dú)立的配置屬性。

定義配置屬性

? 以下過(guò)程采用前期學(xué)習(xí)的屬性配置方式進(jìn)行,當(dāng)前操作有助于理解原理篇中的很多知識(shí)。

  • 定義配置類,加載必要的配置屬性,讀取配置文件中memcached節(jié)點(diǎn)信息

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

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

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

總結(jié)

  1. memcached安裝后需要啟動(dòng)對(duì)應(yīng)服務(wù)才可以對(duì)外提供緩存功能,安裝memcached服務(wù)需要基于windows系統(tǒng)管理員權(quán)限
  2. 由于springboot沒(méi)有提供對(duì)memcached的緩存整合方案,需要采用手工編碼的形式創(chuàng)建xmemcached客戶端操作緩存
  3. 導(dǎo)入xmemcached坐標(biāo)后,創(chuàng)建memcached配置類,注冊(cè)MemcachedClient對(duì)應(yīng)的bean,用于操作緩存
  4. 初始化MemcachedClient對(duì)象所需要使用的屬性可以通過(guò)自定義配置屬性類的形式加載

思考

? 到這里已經(jīng)完成了三種緩存的整合,其中redis和mongodb需要安裝獨(dú)立的服務(wù)器,連接時(shí)需要輸入對(duì)應(yīng)的服務(wù)器地址,這種是遠(yuǎn)程緩存,Ehcache是一個(gè)典型的內(nèi)存級(jí)緩存,因?yàn)樗裁匆膊挥冒惭b,啟動(dòng)后導(dǎo)入jar包就有緩存功能了。這個(gè)時(shí)候就要問(wèn)了,能不能這兩種緩存一起用呢?咱們下節(jié)再說(shuō)。

SpringBoot整合jetcache緩存

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

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

? 做之前要先明確一下,jetcache并不是隨便拿兩個(gè)緩存都能拼到一起去的。目前jetcache支持的緩存方案本地緩存支持兩種,遠(yuǎn)程緩存支持兩種,分別如下:

  • 本地緩存(Local)
    • LinkedHashMap
    • Caffeine
  • 遠(yuǎn)程緩存(Remote)
    • Redis
    • Tair

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

純遠(yuǎn)程方案

步驟①:導(dǎo)入springboot整合jetcache對(duì)應(yīng)的坐標(biāo)starter,當(dāng)前坐標(biāo)默認(rèn)使用的遠(yuǎn)程方案是redis

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

步驟②:遠(yuǎn)程方案基本配置

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

? 其中poolConfig是必配項(xiàng),否則會(huì)報(bào)錯(cuò)

步驟③:啟用緩存,在引導(dǎo)類上方標(biāo)注注解@EnableCreateCacheAnnotation配置springboot程序中可以使用注解的形式創(chuàng)建緩存

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

步驟④:創(chuàng)建緩存對(duì)象Cache,并使用注解@CreateCache標(biāo)記當(dāng)前緩存的信息,然后使用Cache對(duì)象的API操作緩存,put寫(xiě)緩存,get讀緩存。

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

? 通過(guò)上述jetcache使用遠(yuǎn)程方案連接redis可以看出,jetcache操作緩存時(shí)的接口操作更符合開(kāi)發(fā)者習(xí)慣,使用緩存就先獲取緩存對(duì)象Cache,放數(shù)據(jù)進(jìn)去就是put,取數(shù)據(jù)出來(lái)就是get,更加簡(jiǎn)單易懂。并且jetcache操作緩存時(shí),可以為某個(gè)緩存對(duì)象設(shè)置過(guò)期時(shí)間,將同類型的數(shù)據(jù)放入緩存中,方便有效周期的管理。

? 上述方案中使用的是配置中定義的default緩存,其實(shí)這個(gè)default是個(gè)名字,可以隨便寫(xiě),也可以隨便加。例如再添加一種緩存解決方案,參照如下配置進(jìn)行:

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

? 如果想使用名稱是sms的緩存,需要再創(chuàng)建緩存時(shí)指定參數(shù)area,聲明使用對(duì)應(yīng)緩存即可

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

? 遠(yuǎn)程方案中,配置中使用remote表示遠(yuǎn)程,換成local就是本地,只不過(guò)類型不一樣而已。

步驟①:導(dǎo)入springboot整合jetcache對(duì)應(yīng)的坐標(biāo)starter

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

步驟②:本地緩存基本配置

jetcache:local:default:type: linkedhashmapkeyConvertor: fastjson

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

步驟③:啟用緩存

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

步驟④:創(chuàng)建緩存對(duì)象Cache時(shí),標(biāo)注當(dāng)前使用本地緩存

@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控制當(dāng)前緩存使用本地緩存還是遠(yuǎn)程緩存,配置cacheType=CacheType.LOCAL即使用本地緩存。

本地+遠(yuǎn)程方案

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

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

? 在創(chuàng)建緩存的時(shí)候,配置cacheType為BOTH即則本地緩存與遠(yuǎn)程緩存同時(shí)使用。

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

? cacheType如果不進(jìn)行配置,默認(rèn)值是REMOTE,即僅使用遠(yuǎn)程緩存方案。關(guān)于jetcache的配置,參考以下信息

屬性默認(rèn)值說(shuō)明
jetcache.statIntervalMinutes0統(tǒng)計(jì)間隔,0表示不統(tǒng)計(jì)
jetcache.hiddenPackages無(wú)自動(dòng)生成name時(shí),隱藏指定的包名前綴
jetcache.[local|remote].${area}.type無(wú)緩存類型,本地支持linkedhashmap、caffeine,遠(yuǎn)程支持redis、tair
jetcache.[local|remote].${area}.keyConvertor無(wú)key轉(zhuǎn)換器,當(dāng)前僅支持fastjson
jetcache.[local|remote].${area}.valueEncoderjava僅remote類型的緩存需要指定,可選java和kryo
jetcache.[local|remote].${area}.valueDecoderjava僅remote類型的緩存需要指定,可選java和kryo
jetcache.[local|remote].${area}.limit100僅local類型的緩存需要指定,緩存實(shí)例最大元素?cái)?shù)
jetcache.[local|remote].${area}.expireAfterWriteInMillis無(wú)窮大默認(rèn)過(guò)期時(shí)間,毫秒單位
jetcache.local.${area}.expireAfterAccessInMillis0僅local類型的緩存有效,毫秒單位,最大不活動(dòng)間隔

? 以上方案僅支持手工控制緩存,但是springcache方案中的方法緩存特別好用,給一個(gè)方法添加一個(gè)注解,方法就會(huì)自動(dòng)使用緩存。jetcache也提供了對(duì)應(yīng)的功能,即方法緩存。

方法緩存

? jetcache提供了方法緩存方案,只不過(guò)名稱變更了而已。在對(duì)應(yīng)的操作接口上方使用注解@Cached即可

步驟①:導(dǎo)入springboot整合jetcache對(duì)應(yīng)的坐標(biāo)starter

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

步驟②:配置緩存

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

? 由于redis緩存中不支持保存對(duì)象,因此需要對(duì)redis設(shè)置當(dāng)Object類型數(shù)據(jù)進(jìn)入到redis中時(shí)如何進(jìn)行類型轉(zhuǎn)換。需要配置keyConvertor表示key的類型轉(zhuǎn)換方式,同時(shí)標(biāo)注value的轉(zhuǎn)換類型方式,值進(jìn)入redis時(shí)是java類型,標(biāo)注valueEncode為java,值從redis中讀取時(shí)轉(zhuǎn)換成java,標(biāo)注valueDecode為java。

? 注意,為了實(shí)現(xiàn)Object類型的值進(jìn)出redis,需要保障進(jìn)出redis的Object類型的數(shù)據(jù)必須實(shí)現(xiàn)序列化接口。

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

步驟③:啟用緩存時(shí)開(kāi)啟方法緩存功能,并配置basePackages,說(shuō)明在哪些包中開(kāi)啟方法緩存

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

步驟④:使用注解@Cached標(biāo)注當(dāng)前方法使用緩存

@Service
public class BookServiceImpl implements BookService {@Autowiredprivate BookDao bookDao;@Override@Cached(name="book_",key="#id",expire = 3600,cacheType = CacheType.REMOTE)public Book getById(Integer id) {return bookDao.selectById(id);}
}
遠(yuǎn)程方案的數(shù)據(jù)同步

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

更新緩存

@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;
}

定時(shí)刷新緩存

@Cached(name="book_",key="#id",expire = 3600,cacheType = CacheType.REMOTE)
@CacheRefresh(refresh = 5)
public Book getById(Integer id) {return bookDao.selectById(id);
}
數(shù)據(jù)報(bào)表

? jetcache還提供有簡(jiǎn)單的數(shù)據(jù)報(bào)表功能,幫助開(kāi)發(fā)者快速查看緩存命中信息,只需要添加一個(gè)配置即可

jetcache:statIntervalMinutes: 1

? 設(shè)置后,每1分鐘在控制臺(tái)輸出緩存數(shù)據(jù)命中信息

[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
---------+-------+-------+------+-------+-------+---------+--------------+--------------

總結(jié)

  1. jetcache是一個(gè)類似于springcache的緩存解決方案,自身不具有緩存功能,它提供有本地緩存與遠(yuǎn)程緩存多級(jí)共同使用的緩存解決方案
  2. jetcache提供的緩存解決方案受限于目前支持的方案,本地緩存支持兩種,遠(yuǎn)程緩存支持兩種
  3. 注意數(shù)據(jù)進(jìn)入遠(yuǎn)程緩存時(shí)的類型轉(zhuǎn)換問(wèn)題
  4. jetcache提供方法緩存,并提供了對(duì)應(yīng)的緩存更新與刷新功能
  5. jetcache提供有簡(jiǎn)單的緩存信息命中報(bào)表方便開(kāi)發(fā)者即時(shí)監(jiān)控緩存數(shù)據(jù)命中情況

思考

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

SpringBoot整合j2cache緩存

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

步驟①:導(dǎo)入j2cache、redis、ehcache坐標(biāo)

<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中默認(rèn)包含了redis坐標(biāo),官方推薦使用redis作為二級(jí)緩存,因此此處無(wú)需導(dǎo)入redis坐標(biāo)

步驟②:配置一級(jí)與二級(jí)緩存,并配置一二級(jí)緩存間數(shù)據(jù)傳遞方式,配置書(shū)寫(xiě)在名稱為j2cache.properties的文件中。如果使用ehcache還需要單獨(dú)添加ehcache的配置文件

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

? 此處配置不能亂配置,需要參照官方給出的配置說(shuō)明進(jìn)行。例如1級(jí)供應(yīng)商選擇ehcache,供應(yīng)商名稱僅僅是一個(gè)ehcache,但是2級(jí)供應(yīng)商選擇redis時(shí)要寫(xiě)專用的Spring整合Redis的供應(yīng)商類名SpringRedisProvider,而且這個(gè)名稱并不是所有的redis包中能提供的,也不是spring包中提供的。因此配置j2cache必須參照官方文檔配置,而且還要去找專用的整合包,導(dǎo)入對(duì)應(yīng)坐標(biāo)才可以使用。

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

步驟③:使用緩存

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

? j2cache的使用和jetcache比較類似,但是無(wú)需開(kāi)啟使用的開(kāi)關(guān),直接定義緩存對(duì)象即可使用,緩存對(duì)象名CacheChannel。

? j2cache的使用不復(fù)雜,配置是j2cache的核心,畢竟是一個(gè)整合型的緩存框架。緩存相關(guān)的配置過(guò)多,可以查閱j2cache-core核心包中的j2cache.properties文件中的說(shuō)明。如下:

#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 (數(shù)據(jù)庫(kù)配置無(wú)效,使用 database = 0)
# sharded -> sharded servers  (密碼、數(shù)據(jù)庫(kù)必須在 hosts 中指定,且連接池配置無(wú)效 ; redis://user:password@127.0.0.1:6379/0)
#
#########################################redis.mode = single#redis storage mode (generic|hash)
redis.storage = generic## redis pub/sub channel name
redis.channel = j2cache
## redis pub/sub server (using redis.hosts when empty)
redis.channel.host =#cluster name just for sharded
redis.cluster_name = j2cache## redis cache namespace optional, default[empty]
redis.namespace =## redis command scan parameter count, default[1000]
#redis.scanCount = 1000## connection
# Separate multiple redis nodes with commas, such as 192.168.0.10:6379,192.168.0.11:6379,192.168.0.12:6379redis.hosts = 127.0.0.1:6379
redis.timeout = 2000
redis.password =
redis.database = 0
redis.ssl = false## redis pool properties
redis.maxTotal = 100
redis.maxIdle = 10
redis.maxWaitMillis = 5000
redis.minEvictableIdleTimeMillis = 60000
redis.minIdle = 1
redis.numTestsPerEvictionRun = 10
redis.lifo = false
redis.softMinEvictableIdleTimeMillis = 10
redis.testOnBorrow = true
redis.testOnReturn = false
redis.testWhileIdle = true
redis.timeBetweenEvictionRunsMillis = 300000
redis.blockWhenExhausted = false
redis.jmxEnabled = false#########################################
# Lettuce scheme
#
# redis -> single redis server
# rediss -> single redis server with ssl
# redis-sentinel -> redis sentinel
# redis-cluster -> cluster servers
#
##################################################################################
# Lettuce Mode
#
# single -> single redis server
# sentinel -> master-slaves servers
# cluster -> cluster servers (數(shù)據(jù)庫(kù)配置無(wú)效,使用 database = 0)
# sharded -> sharded servers  (密碼、數(shù)據(jù)庫(kù)必須在 hosts 中指定,且連接池配置無(wú)效 ; 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

總結(jié)

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

KF-5-2.任務(wù)

? springboot整合第三方技術(shù)第二部分我們來(lái)說(shuō)說(shuō)任務(wù)系統(tǒng),其實(shí)這里說(shuō)的任務(wù)系統(tǒng)指的是定時(shí)任務(wù)。定時(shí)任務(wù)是企業(yè)級(jí)開(kāi)發(fā)中必不可少的組成部分,諸如長(zhǎng)周期業(yè)務(wù)數(shù)據(jù)的計(jì)算,例如年度報(bào)表,諸如系統(tǒng)臟數(shù)據(jù)的處理,再比如系統(tǒng)性能監(jiān)控報(bào)告,還有搶購(gòu)類活動(dòng)的商品上架,這些都離不開(kāi)定時(shí)任務(wù)。本節(jié)將介紹兩種不同的定時(shí)任務(wù)技術(shù)。

Quartz

? Quartz技術(shù)是一個(gè)比較成熟的定時(shí)任務(wù)框架,怎么說(shuō)呢?有點(diǎn)繁瑣,用過(guò)的都知道,配置略微復(fù)雜。springboot對(duì)其進(jìn)行整合后,簡(jiǎn)化了一系列的配置,將很多配置采用默認(rèn)設(shè)置,這樣開(kāi)發(fā)階段就簡(jiǎn)化了很多。再學(xué)習(xí)springboot整合Quartz前先普及幾個(gè)Quartz的概念。

  • 工作(Job):用于定義具體執(zhí)行的工作
  • 工作明細(xì)(JobDetail):用于描述定時(shí)工作相關(guān)的信息
  • 觸發(fā)器(Trigger):描述了工作明細(xì)與調(diào)度器的對(duì)應(yīng)關(guān)系
  • 調(diào)度器(Scheduler):用于描述觸發(fā)工作的執(zhí)行規(guī)則,通常使用cron表達(dá)式定義規(guī)則

? 簡(jiǎn)單說(shuō)就是你定時(shí)干什么事情,這就是工作,工作不可能就是一個(gè)簡(jiǎn)單的方法,還要設(shè)置一些明細(xì)信息。工作啥時(shí)候執(zhí)行,設(shè)置一個(gè)調(diào)度器,可以簡(jiǎn)單理解成設(shè)置一個(gè)工作執(zhí)行的時(shí)間。工作和調(diào)度都是獨(dú)立定義的,它們兩個(gè)怎么配合到一起呢?用觸發(fā)器。完了,就這么多。下面開(kāi)始springboot整合Quartz。

步驟①:導(dǎo)入springboot整合Quartz的starter

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

步驟②:定義任務(wù)Bean,按照Quartz的開(kāi)發(fā)規(guī)范制作,繼承QuartzJobBean

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

步驟③:創(chuàng)建Quartz配置類,定義工作明細(xì)(JobDetail)與觸發(fā)器的(Trigger)bean

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

? 工作明細(xì)中要設(shè)置對(duì)應(yīng)的具體工作,使用newJob()操作傳入對(duì)應(yīng)的工作任務(wù)類型即可。

? 觸發(fā)器需要綁定任務(wù),使用forJob()操作傳入綁定的工作明細(xì)對(duì)象。此處可以為工作明細(xì)設(shè)置名稱然后使用名稱綁定,也可以直接調(diào)用對(duì)應(yīng)方法綁定。觸發(fā)器中最核心的規(guī)則是執(zhí)行時(shí)間,此處使用調(diào)度器定義執(zhí)行時(shí)間,執(zhí)行時(shí)間描述方式使用的是cron表達(dá)式。有關(guān)cron表達(dá)式的規(guī)則,各位小伙伴可以去參看相關(guān)課程學(xué)習(xí),略微復(fù)雜,而且格式不能亂設(shè)置,不是寫(xiě)個(gè)格式就能用的,寫(xiě)不好就會(huì)出現(xiàn)沖突問(wèn)題。

總結(jié)

  1. springboot整合Quartz就是將Quartz對(duì)應(yīng)的核心對(duì)象交給spring容器管理,包含兩個(gè)對(duì)象,JobDetail和Trigger對(duì)象
  2. JobDetail對(duì)象描述的是工作的執(zhí)行信息,需要綁定一個(gè)QuartzJobBean類型的對(duì)象
  3. Trigger對(duì)象定義了一個(gè)觸發(fā)器,需要為其指定綁定的JobDetail是哪個(gè),同時(shí)要設(shè)置執(zhí)行周期調(diào)度器

思考

? 上面的操作看上去不多,但是Quartz將其中的對(duì)象劃分粒度過(guò)細(xì),導(dǎo)致開(kāi)發(fā)的時(shí)候有點(diǎn)繁瑣,spring針對(duì)上述規(guī)則進(jìn)行了簡(jiǎn)化,開(kāi)發(fā)了自己的任務(wù)管理組件——Task,如何用呢?咱們下節(jié)再說(shuō)。

Task

? spring根據(jù)定時(shí)任務(wù)的特征,將定時(shí)任務(wù)的開(kāi)發(fā)簡(jiǎn)化到了極致。怎么說(shuō)呢?要做定時(shí)任務(wù)總要告訴容器有這功能吧,然后定時(shí)執(zhí)行什么任務(wù)直接告訴對(duì)應(yīng)的bean什么時(shí)間執(zhí)行就行了,就這么簡(jiǎn)單,一起來(lái)看怎么做

步驟①:開(kāi)啟定時(shí)任務(wù)功能,在引導(dǎo)類上開(kāi)啟定時(shí)任務(wù)功能的開(kāi)關(guān),使用注解@EnableScheduling

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

步驟②:定義Bean,在對(duì)應(yīng)要定時(shí)執(zhí)行的操作上方,使用注解@Scheduled定義執(zhí)行的時(shí)間,執(zhí)行時(shí)間的描述方式還是cron表達(dá)式

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

? 完事,這就完成了定時(shí)任務(wù)的配置??傮w感覺(jué)其實(shí)什么東西都沒(méi)少,只不過(guò)沒(méi)有將所有的信息都抽取成bean,而是直接使用注解綁定定時(shí)執(zhí)行任務(wù)的事情而已。

? 如何想對(duì)定時(shí)任務(wù)進(jìn)行相關(guān)配置,可以通過(guò)配置文件進(jìn)行

spring:task:scheduling:pool:size: 1							# 任務(wù)調(diào)度線程池大小 默認(rèn) 1thread-name-prefix: ssm_      	# 調(diào)度線程名稱前綴 默認(rèn) scheduling-      shutdown:await-termination: false		# 線程池關(guān)閉時(shí)等待所有任務(wù)完成await-termination-period: 10s	# 調(diào)度線程關(guān)閉前最大等待時(shí)間,確保最后一定關(guān)閉

總結(jié)

  1. spring task需要使用注解@EnableScheduling開(kāi)啟定時(shí)任務(wù)功能

  2. 為定時(shí)執(zhí)行的的任務(wù)設(shè)置執(zhí)行周期,描述方式cron表達(dá)式

KF-5-3.郵件

? springboot整合第三方技術(shù)第三部分我們來(lái)說(shuō)說(shuō)郵件系統(tǒng),發(fā)郵件是java程序的基本操作,springboot整合javamail其實(shí)就是簡(jiǎn)化開(kāi)發(fā)。不熟悉郵件的小伙伴可以先學(xué)習(xí)完javamail的基礎(chǔ)操作,再來(lái)看這一部分內(nèi)容才能感觸到springboot整合javamail究竟簡(jiǎn)化了哪些操作。簡(jiǎn)化的多碼?其實(shí)不多,差別不大,只是還個(gè)格式而已。

? 學(xué)習(xí)郵件發(fā)送之前先了解3個(gè)概念,這些概念規(guī)范了郵件操作過(guò)程中的標(biāo)準(zhǔn)。

  • SMTP(Simple Mail Transfer Protocol):簡(jiǎn)單郵件傳輸協(xié)議,用于發(fā)送電子郵件的傳輸協(xié)議
  • POP3(Post Office Protocol - Version 3):用于接收電子郵件的標(biāo)準(zhǔn)協(xié)議
  • IMAP(Internet Mail Access Protocol):互聯(lián)網(wǎng)消息協(xié)議,是POP3的替代協(xié)議

? 簡(jiǎn)單說(shuō)就是SMPT是發(fā)郵件的標(biāo)準(zhǔn),POP3是收郵件的標(biāo)準(zhǔn),IMAP是對(duì)POP3的升級(jí)。我們制作程序中操作郵件,通常是發(fā)郵件,所以SMTP是使用的重點(diǎn),收郵件大部分都是通過(guò)郵件客戶端完成,所以開(kāi)發(fā)收郵件的代碼極少。除非你要讀取郵件內(nèi)容,然后解析,做郵件功能的統(tǒng)一處理。例如HR的郵箱收到求職者的簡(jiǎn)歷,可以讀取后統(tǒng)一處理。但是為什么不制作獨(dú)立的投遞簡(jiǎn)歷的系統(tǒng)呢?所以說(shuō),好奇怪的需求,因?yàn)橐胧锗]件就要規(guī)范發(fā)郵件的人的書(shū)寫(xiě)格式,這個(gè)未免有點(diǎn)強(qiáng)人所難,并且極易收到外部攻擊,你不可能使用白名單來(lái)收郵件。如果能使用白名單來(lái)收郵件然后解析郵件,還不如開(kāi)發(fā)個(gè)系統(tǒng)給白名單中的人專用呢,更安全,總之就是雞肋了。下面就開(kāi)始學(xué)習(xí)springboot如何整合javamail發(fā)送郵件。

發(fā)送簡(jiǎn)單郵件

步驟①:導(dǎo)入springboot整合javamail的starter

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

步驟②:配置郵箱的登錄信息

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

? java程序僅用于發(fā)送郵件,郵件的功能還是郵件供應(yīng)商提供的,所以這里是用別人的郵件服務(wù),要配置對(duì)應(yīng)信息。

? host配置的是提供郵件服務(wù)的主機(jī)協(xié)議,當(dāng)前程序僅用于發(fā)送郵件,因此配置的是smtp的協(xié)議。

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

在這里插入圖片描述

步驟③:使用JavaMailSender接口發(fā)送郵件

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

? 將發(fā)送郵件的必要信息(發(fā)件人、收件人、標(biāo)題、正文)封裝到SimpleMailMessage對(duì)象中,可以根據(jù)規(guī)則設(shè)置發(fā)送人昵稱等。

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

? 發(fā)送簡(jiǎn)單郵件僅需要提供對(duì)應(yīng)的4個(gè)基本信息就可以了,如果想發(fā)送復(fù)雜的郵件,需要更換郵件對(duì)象。使用MimeMessage可以發(fā)送特殊的郵件。

發(fā)送網(wǎng)頁(yè)正文郵件

@Service
public class SendMailServiceImpl2 implements SendMailService {@Autowiredprivate JavaMailSender javaMailSender;//發(fā)送人private String from = "test@qq.com";//接收人private String to = "test@126.com";//標(biāo)題private String subject = "測(cè)試郵件";//正文private String context = "<img src='ABC.JPG'/><a href='https://www.itcast.cn'>點(diǎn)開(kāi)有驚喜</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);		//此處設(shè)置正文支持html解析javaMailSender.send(message);} catch (Exception e) {e.printStackTrace();}}
}

發(fā)送帶有附件的郵件

@Service
public class SendMailServiceImpl2 implements SendMailService {@Autowiredprivate JavaMailSender javaMailSender;//發(fā)送人private String from = "test@qq.com";//接收人private String to = "test@126.com";//標(biāo)題private String subject = "測(cè)試郵件";//正文private String context = "測(cè)試郵件正文";public void sendMail() {try {MimeMessage message = javaMailSender.createMimeMessage();MimeMessageHelper helper = new MimeMessageHelper(message,true);		//此處設(shè)置支持附件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("最靠譜的培訓(xùn)結(jié)構(gòu).png",f2);javaMailSender.send(message);} catch (Exception e) {e.printStackTrace();}}
}

總結(jié)

  1. springboot整合javamail其實(shí)就是簡(jiǎn)化了發(fā)送郵件的客戶端對(duì)象JavaMailSender的初始化過(guò)程,通過(guò)配置的形式加載信息簡(jiǎn)化開(kāi)發(fā)過(guò)程

KF-5-4.消息

? springboot整合第三方技術(shù)最后一部分我們來(lái)說(shuō)說(shuō)消息中間件,首先先介紹一下消息的應(yīng)用。

消息的概念

? 從廣義角度來(lái)說(shuō),消息其實(shí)就是信息,但是和信息又有所不同。信息通常被定義為一組數(shù)據(jù),而消息除了具有數(shù)據(jù)的特征之外,還有消息的來(lái)源與接收的概念。通常發(fā)送消息的一方稱為消息的生產(chǎn)者,接收消息的一方稱為消息的消費(fèi)者。這樣比較后,發(fā)現(xiàn)其實(shí)消息和信息差別還是很大的。

? 為什么要設(shè)置生產(chǎn)者和消費(fèi)者呢?這就是要說(shuō)到消息的意義了。信息通常就是一組數(shù)據(jù),但是消息由于有了生產(chǎn)者和消費(fèi)者,就出現(xiàn)了消息中所包含的信息可以被二次解讀,生產(chǎn)者發(fā)送消息,可以理解為生產(chǎn)者發(fā)送了一個(gè)信息,也可以理解為生產(chǎn)者發(fā)送了一個(gè)命令;消費(fèi)者接收消息,可以理解為消費(fèi)者得到了一個(gè)信息,也可以理解為消費(fèi)者得到了一個(gè)命令。對(duì)比一下我們會(huì)發(fā)現(xiàn)信息是一個(gè)基本數(shù)據(jù),而命令則可以關(guān)聯(lián)下一個(gè)行為動(dòng)作,這樣就可以理解為基于接收的消息相當(dāng)于得到了一個(gè)行為動(dòng)作,使用這些行為動(dòng)作就可以組織成一個(gè)業(yè)務(wù)邏輯,進(jìn)行進(jìn)一步的操作。總的來(lái)說(shuō),消息其實(shí)也是一組信息,只是為其賦予了全新的含義,因?yàn)橛辛讼⒌牧鲃?dòng),并且是有方向性的流動(dòng),帶來(lái)了基于流動(dòng)的行為產(chǎn)生的全新解讀。開(kāi)發(fā)者就可以基于消息的這種特殊解,將其換成代碼中的指令。

? 對(duì)于消息的理解,初學(xué)者總認(rèn)為消息內(nèi)部的數(shù)據(jù)非常復(fù)雜,這是一個(gè)誤區(qū)。比如我發(fā)送了一個(gè)消息,要求接受者翻譯發(fā)送過(guò)去的內(nèi)容。初學(xué)者會(huì)認(rèn)為消息中會(huì)包含被翻譯的文字,已經(jīng)本次操作要執(zhí)行翻譯操作而不是打印操作。其實(shí)這種現(xiàn)象有點(diǎn)過(guò)度解讀了,發(fā)送的消息中僅僅包含被翻譯的文字,但是可以通過(guò)控制不同的人接收此消息來(lái)確認(rèn)要做的事情。例如發(fā)送被翻譯的文字僅到A程序,而A程序只能進(jìn)行翻譯操作,這樣就可以發(fā)送簡(jiǎn)單的信息完成復(fù)雜的業(yè)務(wù)了,是通過(guò)接收消息的主體不同,進(jìn)而執(zhí)行不同的操作,而不會(huì)在消息內(nèi)部定義數(shù)據(jù)的操作行為,當(dāng)然如果開(kāi)發(fā)者希望消息中包含操作種類信息也是可以的,只是提出消息的內(nèi)容可以更簡(jiǎn)單,更單一。

? 對(duì)于消息的生產(chǎn)者與消費(fèi)者的工作模式,還可以將消息劃分成兩種模式,同步消費(fèi)與異步消息。

? 所謂同步消息就是生產(chǎn)者發(fā)送完消息,等待消費(fèi)者處理,消費(fèi)者處理完將結(jié)果告知生產(chǎn)者,然后生產(chǎn)者繼續(xù)向下執(zhí)行業(yè)務(wù)。這種模式過(guò)于卡生產(chǎn)者的業(yè)務(wù)執(zhí)行連續(xù)性,在現(xiàn)在的企業(yè)級(jí)開(kāi)發(fā)中,上述這種業(yè)務(wù)場(chǎng)景通常不會(huì)采用消息的形式進(jìn)行處理。

? 所謂異步消息就是生產(chǎn)者發(fā)送完消息,無(wú)需等待消費(fèi)者處理完畢,生產(chǎn)者繼續(xù)向下執(zhí)行其他動(dòng)作。比如生產(chǎn)者發(fā)送了一個(gè)日志信息給日志系統(tǒng),發(fā)送過(guò)去以后生產(chǎn)者就向下做其他事情了,無(wú)需關(guān)注日志系統(tǒng)的執(zhí)行結(jié)果。日志系統(tǒng)根據(jù)接收到的日志信息繼續(xù)進(jìn)行業(yè)務(wù)執(zhí)行,是單純的記錄日志,還是記錄日志并報(bào)警,這些和生產(chǎn)者無(wú)關(guān),這樣生產(chǎn)者的業(yè)務(wù)執(zhí)行效率就會(huì)大幅度提升。并且可以通過(guò)添加多個(gè)消費(fèi)者來(lái)處理同一個(gè)生產(chǎn)者發(fā)送的消息來(lái)提高系統(tǒng)的高并發(fā)性,改善系統(tǒng)工作效率,提高用戶體驗(yàn)。一旦某一個(gè)消費(fèi)者由于各種問(wèn)題宕機(jī)了,也不會(huì)對(duì)業(yè)務(wù)產(chǎn)生影響,提高了系統(tǒng)的高可用性。

? 以上簡(jiǎn)單的介紹了一下消息這種工作模式存在的意義,希望對(duì)各位學(xué)習(xí)者有所幫助。

Java處理消息的標(biāo)準(zhǔn)規(guī)范

? 目前企業(yè)級(jí)開(kāi)發(fā)中廣泛使用的消息處理技術(shù)共三大類,具體如下:

  • JMS
  • AMQP
  • MQTT

? 為什么是三大類,而不是三個(gè)技術(shù)呢?因?yàn)檫@些都是規(guī)范,就想JDBC技術(shù),是個(gè)規(guī)范,開(kāi)發(fā)針對(duì)規(guī)范開(kāi)發(fā),運(yùn)行還要靠實(shí)現(xiàn)類,例如MySQL提供了JDBC的實(shí)現(xiàn),最終運(yùn)行靠的還是實(shí)現(xiàn)。并且這三類規(guī)范都是針對(duì)異步消息進(jìn)行處理的,也符合消息的設(shè)計(jì)本質(zhì),處理異步的業(yè)務(wù)。對(duì)以上三種消息規(guī)范做一下普及

JMS

? JMS(Java Message Service),這是一個(gè)規(guī)范,作用等同于JDBC規(guī)范,提供了與消息服務(wù)相關(guān)的API接口。

JMS消息模型

? JMS規(guī)范中規(guī)范了消息有兩種模型。分別是點(diǎn)對(duì)點(diǎn)模型發(fā)布訂閱模型

? 點(diǎn)對(duì)點(diǎn)模型:peer-2-peer,生產(chǎn)者會(huì)將消息發(fā)送到一個(gè)保存消息的容器中,通常使用隊(duì)列模型,使用隊(duì)列保存消息。一個(gè)隊(duì)列的消息只能被一個(gè)消費(fèi)者消費(fèi),或未被及時(shí)消費(fèi)導(dǎo)致超時(shí)。這種模型下,生產(chǎn)者和消費(fèi)者是一對(duì)一綁定的。

? 發(fā)布訂閱模型:publish-subscribe,生產(chǎn)者將消息發(fā)送到一個(gè)保存消息的容器中,也是使用隊(duì)列模型來(lái)保存。但是消息可以被多個(gè)消費(fèi)者消費(fèi),生產(chǎn)者和消費(fèi)者完全獨(dú)立,相互不需要感知對(duì)方的存在。

? 以上這種分類是從消息的生產(chǎn)和消費(fèi)過(guò)程來(lái)進(jìn)行區(qū)分,針對(duì)消息所包含的信息不同,還可以進(jìn)行不同類別的劃分。

JMS消息種類

? 根據(jù)消息中包含的數(shù)據(jù)種類劃分,可以將消息劃分成6種消息。

  • TextMessage
  • MapMessage
  • BytesMessage
  • StreamMessage
  • ObjectMessage
  • Message (只有消息頭和屬性)

? JMS主張不同種類的消息,消費(fèi)方式不同,可以根據(jù)使用需要選擇不同種類的消息。但是這一點(diǎn)也成為其詬病之處,后面再說(shuō)。整體上來(lái)說(shuō),JMS就是典型的保守派,什么都按照J(rèn)2EE的規(guī)范來(lái),做一套規(guī)范,定義若干個(gè)標(biāo)準(zhǔn),每個(gè)標(biāo)準(zhǔn)下又提供一大批API。目前對(duì)JMS規(guī)范實(shí)現(xiàn)的消息中間件技術(shù)還是挺多的,畢竟是皇家御用,肯定有人舔,例如ActiveMQ、Redis、HornetMQ。但是也有一些不太規(guī)范的實(shí)現(xiàn),參考JMS的標(biāo)準(zhǔn)設(shè)計(jì),但是又不完全滿足其規(guī)范,例如:RabbitMQ、RocketMQ。

AMQP

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

? 這時(shí)候有人就提出說(shuō),你搞那么復(fù)雜,整那么多種類干什么?找一種大家都支持的消息數(shù)據(jù)類型不就解決這個(gè)跨平臺(tái)的問(wèn)題了嗎?大家一想,對(duì)啊,于是AMQP孕育而生。

? 單從上面的說(shuō)明中其實(shí)可以明確感知到,AMQP的出現(xiàn)解決的是消息傳遞時(shí)使用的消息種類的問(wèn)題,化繁為簡(jiǎn),但是其并沒(méi)有完全推翻JMS的操作API,所以說(shuō)AMQP僅僅是一種協(xié)議,規(guī)范了數(shù)據(jù)傳輸?shù)母袷蕉选?/p>

? AMQP(advanced message queuing protocol):一種協(xié)議(高級(jí)消息隊(duì)列協(xié)議,也是消息代理規(guī)范),規(guī)范了網(wǎng)絡(luò)交換的數(shù)據(jù)格式,兼容JMS操作。
優(yōu)點(diǎn)

? 具有跨平臺(tái)性,服務(wù)器供應(yīng)商,生產(chǎn)者,消費(fèi)者可以使用不同的語(yǔ)言來(lái)實(shí)現(xiàn)

JMS消息種類

? AMQP消息種類:byte[]

? AMQP在JMS的消息模型基礎(chǔ)上又進(jìn)行了進(jìn)一步的擴(kuò)展,除了點(diǎn)對(duì)點(diǎn)和發(fā)布訂閱的模型,開(kāi)發(fā)了幾種全新的消息模型,適應(yīng)各種各樣的消息發(fā)送。

AMQP消息模型

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

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

MQTT

? MQTT(Message Queueing Telemetry Transport)消息隊(duì)列遙測(cè)傳輸,專為小設(shè)備設(shè)計(jì),是物聯(lián)網(wǎng)(IOT)生態(tài)系統(tǒng)中主要成分之一。由于與JavaEE企業(yè)級(jí)開(kāi)發(fā)沒(méi)有交集,此處不作過(guò)多的說(shuō)明。

? 除了上述3種J2EE企業(yè)級(jí)應(yīng)用中廣泛使用的三種異步消息傳遞技術(shù),還有一種技術(shù)也不能忽略,Kafka。

KafKa

? Kafka,一種高吞吐量的分布式發(fā)布訂閱消息系統(tǒng),提供實(shí)時(shí)消息功能。Kafka技術(shù)并不是作為消息中間件為主要功能的產(chǎn)品,但是其擁有發(fā)布訂閱的工作模式,也可以充當(dāng)消息中間件來(lái)使用,而且目前企業(yè)級(jí)開(kāi)發(fā)中其身影也不少見(jiàn)。

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

購(gòu)物訂單發(fā)送手機(jī)短信案例

? 為了便于下面演示各種各樣的消息中間件技術(shù),我們創(chuàng)建一個(gè)購(gòu)物過(guò)程生成訂單時(shí)為用戶發(fā)送短信的案例環(huán)境,模擬使用消息中間件實(shí)現(xiàn)發(fā)送手機(jī)短信的過(guò)程。

? 手機(jī)驗(yàn)證碼案例需求如下:

  • 執(zhí)行下單業(yè)務(wù)時(shí)(模擬此過(guò)程),調(diào)用消息服務(wù),將要發(fā)送短信的訂單id傳遞給消息中間件

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

    由于不涉及數(shù)據(jù)讀寫(xiě),僅開(kāi)發(fā)業(yè)務(wù)層與表現(xiàn)層,其中短信處理的業(yè)務(wù)代碼獨(dú)立開(kāi)發(fā),代碼如下:

訂單業(yè)務(wù)

? 業(yè)務(wù)層接口

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

? 模擬傳入訂單id,執(zhí)行下訂單業(yè)務(wù),參數(shù)為虛擬設(shè)定,實(shí)際應(yīng)為訂單對(duì)應(yīng)的實(shí)體類

? 業(yè)務(wù)層實(shí)現(xiàn)

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

? 業(yè)務(wù)層轉(zhuǎn)調(diào)短信處理的服務(wù)MessageService

? 表現(xiàn)層服務(wù)

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

? 表現(xiàn)層對(duì)外開(kāi)發(fā)接口,傳入訂單id即可(模擬)

短信處理業(yè)務(wù)

? 業(yè)務(wù)層接口

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

? 短信處理業(yè)務(wù)層接口提供兩個(gè)操作,發(fā)送要處理的訂單id到消息中間件,另一個(gè)操作目前暫且設(shè)計(jì)成處理消息,實(shí)際消息的處理過(guò)程不應(yīng)該是手動(dòng)執(zhí)行,應(yīng)該是自動(dòng)執(zhí)行,到具體實(shí)現(xiàn)時(shí)再進(jìn)行設(shè)計(jì)

? 業(yè)務(wù)層實(shí)現(xiàn)

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

? 短信處理業(yè)務(wù)層實(shí)現(xiàn)中使用集合先模擬消息隊(duì)列,觀察效果

? 表現(xiàn)層服務(wù)

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

? 短信處理表現(xiàn)層接口暫且開(kāi)發(fā)出一個(gè)處理消息的入口,但是此業(yè)務(wù)是對(duì)應(yīng)業(yè)務(wù)層中設(shè)計(jì)的模擬接口,實(shí)際業(yè)務(wù)不需要設(shè)計(jì)此接口。

? 下面開(kāi)啟springboot整合各種各樣的消息中間件,從嚴(yán)格滿足JMS規(guī)范的ActiveMQ開(kāi)始

SpringBoot整合ActiveMQ

? ActiveMQ是MQ產(chǎn)品中的元老級(jí)產(chǎn)品,早期標(biāo)準(zhǔn)MQ產(chǎn)品之一,在AMQP協(xié)議沒(méi)有出現(xiàn)之前,占據(jù)了消息中間件市場(chǎng)的絕大部分份額,后期因?yàn)锳MQP系列產(chǎn)品的出現(xiàn),迅速走弱,目前僅在一些線上運(yùn)行的產(chǎn)品中出現(xiàn),新產(chǎn)品開(kāi)發(fā)較少采用。

安裝

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

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

在這里插入圖片描述

啟動(dòng)服務(wù)器

activemq.bat

? 運(yùn)行bin目錄下的win32或win64目錄下的activemq.bat命令即可,根據(jù)自己的操作系統(tǒng)選擇即可,默認(rèn)對(duì)外服務(wù)端口61616。

訪問(wèn)web管理服務(wù)

? ActiveMQ啟動(dòng)后會(huì)啟動(dòng)一個(gè)Web控制臺(tái)服務(wù),可以通過(guò)該服務(wù)管理ActiveMQ。

http://127.0.0.1:8161/

? web管理服務(wù)默認(rèn)端口8161,訪問(wèn)后可以打開(kāi)ActiveMQ的管理界面,如下:

在這里插入圖片描述

? 首先輸入訪問(wèn)用戶名和密碼,初始化用戶名和密碼相同,均為:admin,成功登錄后進(jìn)入管理后臺(tái)界面,如下:

在這里插入圖片描述

? 看到上述界面視為啟動(dòng)ActiveMQ服務(wù)成功。

啟動(dòng)失敗

? 在ActiveMQ啟動(dòng)時(shí)要占用多個(gè)端口,以下為正常啟動(dòng)信息:

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,如果啟動(dòng)失敗,請(qǐng)先管理對(duì)應(yīng)端口即可。以下就是某個(gè)端口占用的報(bào)錯(cuò)信息,可以從拋出異常的位置看出,啟動(dòng)5672端口時(shí)端口被占用,顯示java.net.BindException: Address already in use: JVM_Bind。Windows系統(tǒng)中終止端口運(yùn)行的操作參看【命令行啟動(dòng)常見(jiàn)問(wèn)題及解決方案】

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
請(qǐng)按任意鍵繼續(xù). . .
整合

? 做了這么多springboot整合第三方技術(shù),已經(jīng)摸到門(mén)路了,加坐標(biāo),做配置,調(diào)接口,直接開(kāi)工

步驟①:導(dǎo)入springboot整合ActiveMQ的starter

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

步驟②:配置ActiveMQ的服務(wù)器地址

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

步驟③:使用JmsMessagingTemplate操作ActiveMQ

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

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

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

步驟④:使用消息監(jiān)聽(tīng)器在服務(wù)器啟動(dòng)后,監(jiān)聽(tīng)指定位置,當(dāng)消息出現(xiàn)后,立即消費(fèi)消息

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

? 使用注解@JmsListener定義當(dāng)前方法監(jiān)聽(tīng)ActiveMQ中指定名稱的消息隊(duì)列。

? 如果當(dāng)前消息隊(duì)列處理完還需要繼續(xù)向下傳遞當(dāng)前消息到另一個(gè)隊(duì)列中使用注解@SendTo即可,這樣即可構(gòu)造連續(xù)執(zhí)行的順序消息隊(duì)列。

步驟⑤:切換消息模型由點(diǎn)對(duì)點(diǎn)模型到發(fā)布訂閱模型,修改jms配置即可

spring:activemq:broker-url: tcp://localhost:61616jms:pub-sub-domain: true

? pub-sub-domain默認(rèn)值為false,即點(diǎn)對(duì)點(diǎn)模型,修改為true后就是發(fā)布訂閱模型。

總結(jié)

  1. springboot整合ActiveMQ提供了JmsMessagingTemplate對(duì)象作為客戶端操作消息隊(duì)列
  2. 操作ActiveMQ需要配置ActiveMQ服務(wù)器地址,默認(rèn)端口61616
  3. 企業(yè)開(kāi)發(fā)時(shí)通常使用監(jiān)聽(tīng)器來(lái)處理消息隊(duì)列中的消息,設(shè)置監(jiān)聽(tīng)器使用注解@JmsListener
  4. 配置jms的pub-sub-domain屬性可以在點(diǎn)對(duì)點(diǎn)模型和發(fā)布訂閱模型間切換消息模型

SpringBoot整合RabbitMQ

? RabbitMQ是MQ產(chǎn)品中的目前較為流行的產(chǎn)品之一,它遵從AMQP協(xié)議。RabbitMQ的底層實(shí)現(xiàn)語(yǔ)言使用的是Erlang,所以安裝RabbitMQ需要先安裝Erlang。

Erlang安裝

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

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

? 安裝的過(guò)程中可能會(huì)出現(xiàn)依賴Windows組件的提示,根據(jù)提示下載安裝即可,都是自動(dòng)執(zhí)行的,如下:

在這里插入圖片描述

? Erlang安裝后需要配置環(huán)境變量,否則RabbitMQ將無(wú)法找到安裝的Erlang。需要配置項(xiàng)如下,作用等同JDK配置環(huán)境變量的作用。

  • ERLANG_HOME
  • PATH
安裝

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

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

在這里插入圖片描述

啟動(dòng)服務(wù)器

rabbitmq-service.bat start		# 啟動(dòng)服務(wù)
rabbitmq-service.bat stop		# 停止服務(wù)
rabbitmqctl status				# 查看服務(wù)狀態(tài)

? 運(yùn)行sbin目錄下的rabbitmq-service.bat命令即可,start參數(shù)表示啟動(dòng),stop參數(shù)表示退出,默認(rèn)對(duì)外服務(wù)端口5672。

? 注意:啟動(dòng)rabbitmq的過(guò)程實(shí)際上是開(kāi)啟rabbitmq對(duì)應(yīng)的系統(tǒng)服務(wù),需要管理員權(quán)限方可執(zhí)行。

? 說(shuō)明:有沒(méi)有感覺(jué)5672的服務(wù)端口很熟悉?activemq與rabbitmq有一個(gè)端口沖突問(wèn)題,學(xué)習(xí)階段無(wú)論操作哪一個(gè)?請(qǐng)確保另一個(gè)處于關(guān)閉狀態(tài)。

? 說(shuō)明:不喜歡命令行的小伙伴可以使用任務(wù)管理器中的服務(wù)頁(yè),找到RabbitMQ服務(wù),使用鼠標(biāo)右鍵菜單控制服務(wù)的啟停。

在這里插入圖片描述

訪問(wèn)web管理服務(wù)

? RabbitMQ也提供有web控制臺(tái)服務(wù),但是此功能是一個(gè)插件,需要先啟用才可以使用。

rabbitmq-plugins.bat list							# 查看當(dāng)前所有插件的運(yùn)行狀態(tài)
rabbitmq-plugins.bat enable rabbitmq_management		# 啟動(dòng)rabbitmq_management插件

? 啟動(dòng)插件后可以在插件運(yùn)行狀態(tài)中查看是否運(yùn)行,運(yùn)行后通過(guò)瀏覽器即可打開(kāi)服務(wù)后臺(tái)管理界面

http://localhost:15672

? web管理服務(wù)默認(rèn)端口15672,訪問(wèn)后可以打開(kāi)RabbitMQ的管理界面,如下:

在這里插入圖片描述

? 首先輸入訪問(wèn)用戶名和密碼,初始化用戶名和密碼相同,均為:guest,成功登錄后進(jìn)入管理后臺(tái)界面,如下:

在這里插入圖片描述

整合(direct模型)

? RabbitMQ滿足AMQP協(xié)議,因此不同的消息模型對(duì)應(yīng)的制作不同,先使用最簡(jiǎn)單的direct模型開(kāi)發(fā)。

步驟①:導(dǎo)入springboot整合amqp的starter,amqp協(xié)議默認(rèn)實(shí)現(xiàn)為rabbitmq方案

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

步驟②:配置RabbitMQ的服務(wù)器地址

spring:rabbitmq:host: localhostport: 5672

步驟③:初始化直連模式系統(tǒng)設(shè)置

? 由于RabbitMQ不同模型要使用不同的交換機(jī),因此需要先初始化RabbitMQ相關(guān)的對(duì)象,例如隊(duì)列,交換機(jī)等

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

? 隊(duì)列Queue與直連交換機(jī)DirectExchange創(chuàng)建后,還需要綁定他們之間的關(guān)系Binding,這樣就可以通過(guò)交換機(jī)操作對(duì)應(yīng)隊(duì)列。

步驟④:使用AmqpTemplate操作RabbitMQ

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

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

步驟⑤:使用消息監(jiān)聽(tīng)器在服務(wù)器啟動(dòng)后,監(jiān)聽(tīng)指定位置,當(dāng)消息出現(xiàn)后,立即消費(fèi)消息

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

? 使用注解@RabbitListener定義當(dāng)前方法監(jiān)聽(tīng)RabbitMQ中指定名稱的消息隊(duì)列。

整合(topic模型)

步驟①:同上

步驟②:同上

步驟③:初始化主題模式系統(tǒng)設(shè)置

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

? 主題模式支持routingKey匹配模式,*表示匹配一個(gè)單詞,#表示匹配任意內(nèi)容,這樣就可以通過(guò)主題交換機(jī)將消息分發(fā)到不同的隊(duì)列中,詳細(xì)內(nèi)容請(qǐng)參看RabbitMQ系列課程。

匹配鍵topic.*.*topic.#
topic.order.idtruetrue
order.topic.idfalsefalse
topic.sm.order.idfalsetrue
topic.sm.idfalsetrue
topic.id.ordertruetrue
topic.idfalsetrue
topic.orderfalsetrue

步驟④:使用AmqpTemplate操作RabbitMQ

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

? 發(fā)送消息后,根據(jù)當(dāng)前提供的routingKey與綁定交換機(jī)時(shí)設(shè)定的routingKey進(jìn)行匹配,規(guī)則匹配成功消息才會(huì)進(jìn)入到對(duì)應(yīng)的隊(duì)列中。

步驟⑤:使用消息監(jiān)聽(tīng)器在服務(wù)器啟動(dòng)后,監(jiān)聽(tīng)指定隊(duì)列

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

? 使用注解@RabbitListener定義當(dāng)前方法監(jiān)聽(tīng)RabbitMQ中指定名稱的消息隊(duì)列。

總結(jié)

  1. springboot整合RabbitMQ提供了AmqpTemplate對(duì)象作為客戶端操作消息隊(duì)列
  2. 操作ActiveMQ需要配置ActiveMQ服務(wù)器地址,默認(rèn)端口5672
  3. 企業(yè)開(kāi)發(fā)時(shí)通常使用監(jiān)聽(tīng)器來(lái)處理消息隊(duì)列中的消息,設(shè)置監(jiān)聽(tīng)器使用注解@RabbitListener
  4. RabbitMQ有5種消息模型,使用的隊(duì)列相同,但是交換機(jī)不同。交換機(jī)不同,對(duì)應(yīng)的消息進(jìn)入的策略也不同

SpringBoot整合RocketMQ

? RocketMQ由阿里研發(fā),后捐贈(zèng)給apache基金會(huì),目前是apache基金會(huì)頂級(jí)項(xiàng)目之一,也是目前市面上的MQ產(chǎn)品中較為流行的產(chǎn)品之一,它遵從AMQP協(xié)議。

安裝

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

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

在這里插入圖片描述

? RocketMQ安裝后需要配置環(huán)境變量,具體如下:

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

? 關(guān)于NAMESRV_ADDR對(duì)于初學(xué)者來(lái)說(shuō)建議配置此項(xiàng),也可以通過(guò)命令設(shè)置對(duì)應(yīng)值,操作略顯繁瑣,建議配置。系統(tǒng)學(xué)習(xí)RocketMQ知識(shí)后即可靈活控制該項(xiàng)。

RocketMQ工作模式

? 在RocketMQ中,處理業(yè)務(wù)的服務(wù)器稱為broker,生產(chǎn)者與消費(fèi)者不是直接與broker聯(lián)系的,而是通過(guò)命名服務(wù)器進(jìn)行通信。broker啟動(dòng)后會(huì)通知命名服務(wù)器自己已經(jīng)上線,這樣命名服務(wù)器中就保存有所有的broker信息。當(dāng)生產(chǎn)者與消費(fèi)者需要連接broker時(shí),通過(guò)命名服務(wù)器找到對(duì)應(yīng)的處理業(yè)務(wù)的broker,因此命名服務(wù)器在整套結(jié)構(gòu)中起到一個(gè)信息中心的作用。并且broker啟動(dòng)前必須保障命名服務(wù)器先啟動(dòng)。

在這里插入圖片描述

啟動(dòng)服務(wù)器

mqnamesrv		# 啟動(dòng)命名服務(wù)器
mqbroker		# 啟動(dòng)broker

? 運(yùn)行bin目錄下的mqnamesrv命令即可啟動(dòng)命名服務(wù)器,默認(rèn)對(duì)外服務(wù)端口9876。

? 運(yùn)行bin目錄下的mqbroker命令即可啟動(dòng)broker服務(wù)器,如果環(huán)境變量中沒(méi)有設(shè)置NAMESRV_ADDR則需要在運(yùn)行mqbroker指令前通過(guò)set指令設(shè)置NAMESRV_ADDR的值,并且每次開(kāi)啟均需要設(shè)置此項(xiàng)。

測(cè)試服務(wù)器啟動(dòng)狀態(tài)

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

tools org.apache.rocketmq.example.quickstart.Producer		# 生產(chǎn)消息
tools org.apache.rocketmq.example.quickstart.Consumer		# 消費(fèi)消息
整合(異步消息)

步驟①:導(dǎo)入springboot整合RocketMQ的starter,此坐標(biāo)不由springboot維護(hù)版本

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

步驟②:配置RocketMQ的服務(wù)器地址

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

? 設(shè)置默認(rèn)的生產(chǎn)者消費(fèi)者所屬組group。

步驟③:使用RocketMQTemplate操作RocketMQ

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

? 使用asyncSend方法發(fā)送異步消息。

步驟④:使用消息監(jiān)聽(tīng)器在服務(wù)器啟動(dòng)后,監(jiān)聽(tīng)指定位置,當(dāng)消息出現(xiàn)后,立即消費(fèi)消息

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

? RocketMQ的監(jiān)聽(tīng)器必須按照標(biāo)準(zhǔn)格式開(kāi)發(fā),實(shí)現(xiàn)RocketMQListener接口,泛型為消息類型。

? 使用注解@RocketMQMessageListener定義當(dāng)前類監(jiān)聽(tīng)RabbitMQ中指定組、指定名稱的消息隊(duì)列。

總結(jié)

  1. springboot整合RocketMQ使用RocketMQTemplate對(duì)象作為客戶端操作消息隊(duì)列
  2. 操作RocketMQ需要配置RocketMQ服務(wù)器地址,默認(rèn)端口9876
  3. 企業(yè)開(kāi)發(fā)時(shí)通常使用監(jiān)聽(tīng)器來(lái)處理消息隊(duì)列中的消息,設(shè)置監(jiān)聽(tīng)器使用注解@RocketMQMessageListener

SpringBoot整合Kafka

安裝

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

? 下載完畢后得到tgz壓縮文件,使用解壓縮軟件解壓縮即可使用,解壓后得到如下文件
在這里插入圖片描述

? 建議使用windows版2.8.1版本。

啟動(dòng)服務(wù)器

? kafka服務(wù)器的功能相當(dāng)于RocketMQ中的broker,kafka運(yùn)行還需要一個(gè)類似于命名服務(wù)器的服務(wù)。在kafka安裝目錄中自帶一個(gè)類似于命名服務(wù)器的工具,叫做zookeeper,它的作用是注冊(cè)中心,相關(guān)知識(shí)請(qǐng)到對(duì)應(yīng)課程中學(xué)習(xí)。

zookeeper-server-start.bat ..\..\config\zookeeper.properties		# 啟動(dòng)zookeeper
kafka-server-start.bat ..\..\config\server.properties				# 啟動(dòng)kafka

? 運(yùn)行bin目錄下的windows目錄下的zookeeper-server-start命令即可啟動(dòng)注冊(cè)中心,默認(rèn)對(duì)外服務(wù)端口2181。

? 運(yùn)行bin目錄下的windows目錄下的kafka-server-start命令即可啟動(dòng)kafka服務(wù)器,默認(rèn)對(duì)外服務(wù)端口9092。

創(chuàng)建主題

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

# 創(chuàng)建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

測(cè)試服務(wù)器啟動(dòng)狀態(tài)

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

kafka-console-producer.bat --broker-list localhost:9092 --topic itheima							# 測(cè)試生產(chǎn)消息
kafka-console-consumer.bat --bootstrap-server localhost:9092 --topic itheima --from-beginning	# 測(cè)試消息消費(fèi)
整合

步驟①:導(dǎo)入springboot整合Kafka的starter,此坐標(biāo)由springboot維護(hù)版本

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

步驟②:配置Kafka的服務(wù)器地址

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

? 設(shè)置默認(rèn)的生產(chǎn)者消費(fèi)者所屬組id。

步驟③:使用KafkaTemplate操作Kafka

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

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

步驟④:使用消息監(jiān)聽(tīng)器在服務(wù)器啟動(dòng)后,監(jiān)聽(tīng)指定位置,當(dāng)消息出現(xiàn)后,立即消費(fèi)消息

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

? 使用注解@KafkaListener定義當(dāng)前方法監(jiān)聽(tīng)Kafka中指定topic的消息,接收到的消息封裝在對(duì)象ConsumerRecord中,獲取數(shù)據(jù)從ConsumerRecord對(duì)象中獲取即可。

總結(jié)

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

  2. 操作Kafka需要配置Kafka服務(wù)器地址,默認(rèn)端口9092

  3. 企業(yè)開(kāi)發(fā)時(shí)通常使用監(jiān)聽(tīng)器來(lái)處理消息隊(duì)列中的消息,設(shè)置監(jiān)聽(tīng)器使用注解@KafkaListener。接收消息保存在形參ConsumerRecord對(duì)象中

KF-6.監(jiān)控

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

? 什么是監(jiān)控?就是通過(guò)軟件的方式展示另一個(gè)軟件的運(yùn)行情況,運(yùn)行的情況則通過(guò)各種各樣的指標(biāo)數(shù)據(jù)反饋給監(jiān)控人員。例如網(wǎng)絡(luò)是否順暢、服務(wù)器是否在運(yùn)行、程序的功能是否能夠整百分百運(yùn)行成功,內(nèi)存是否夠用,等等等等。

? 本章要講解的監(jiān)控就是對(duì)軟件的運(yùn)行情況進(jìn)行監(jiān)督,但是springboot程序與非springboot程序的差異還是很大的,為了方便監(jiān)控軟件的開(kāi)發(fā),springboot提供了一套功能接口,為開(kāi)發(fā)者加速開(kāi)發(fā)過(guò)程。

KF-6-1.監(jiān)控的意義

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

? 通過(guò)上述描述,可以看出監(jiān)控很重要。那具體的監(jiān)控要如何開(kāi)展呢?還要從實(shí)際的程序運(yùn)行角度出發(fā)。比如現(xiàn)在有3個(gè)服務(wù)支撐著一個(gè)程序的運(yùn)行,每個(gè)服務(wù)都有自己的運(yùn)行狀態(tài)。

在這里插入圖片描述

? 此時(shí)被監(jiān)控的信息就要在三個(gè)不同的程序中去查詢并展示,但是三個(gè)服務(wù)是服務(wù)于一個(gè)程序的運(yùn)行的,如果不能合并到一個(gè)平臺(tái)上展示,監(jiān)控工作量巨大,而且信息對(duì)稱性差,要不停的在三個(gè)監(jiān)控端查看數(shù)據(jù)。如果將業(yè)務(wù)放大成30個(gè),300個(gè),3000個(gè)呢?看來(lái)必須有一個(gè)單獨(dú)的平臺(tái),將多個(gè)被監(jiān)控的服務(wù)對(duì)應(yīng)的監(jiān)控指標(biāo)信息匯總在一起,這樣更利于監(jiān)控工作的開(kāi)展。

在這里插入圖片描述

? 新的程序?qū)iT(mén)用來(lái)監(jiān)控,新的問(wèn)題就出現(xiàn)了,是被監(jiān)控程序主動(dòng)上報(bào)信息還是監(jiān)控程序主動(dòng)獲取信息?如果監(jiān)控程序不能主動(dòng)獲取信息,這就意味著監(jiān)控程序有可能看到的是很久之前被監(jiān)控程序上報(bào)的信息,萬(wàn)一被監(jiān)控程序宕機(jī)了,監(jiān)控程序就無(wú)法區(qū)分究竟是好久沒(méi)法信息了,還是已經(jīng)下線了。所以監(jiān)控程序必須具有主動(dòng)發(fā)起請(qǐng)求獲取被監(jiān)控服務(wù)信息的能力。

在這里插入圖片描述

? 如果監(jiān)控程序要監(jiān)控服務(wù)時(shí),主動(dòng)獲取對(duì)方的信息。那監(jiān)控程序如何知道哪些程序被自己監(jiān)控呢?不可能在監(jiān)控程序中設(shè)置我監(jiān)控誰(shuí),這樣互聯(lián)網(wǎng)上的所有程序豈不是都可以被監(jiān)控到,這樣的話信息安全將無(wú)法得到保障。合理的做法只能是在被監(jiān)控程序啟動(dòng)時(shí)上報(bào)監(jiān)控程序,告訴監(jiān)控程序你可以監(jiān)控我了。看來(lái)需要在被監(jiān)控程序端做主動(dòng)上報(bào)的操作,這就要求被監(jiān)控程序中配置對(duì)應(yīng)的監(jiān)控程序是誰(shuí)。

在這里插入圖片描述

? 被監(jiān)控程序可以提供各種各樣的指標(biāo)數(shù)據(jù)給監(jiān)控程序看,但是每一個(gè)指標(biāo)都代表著公司的機(jī)密信息,并不是所有的指標(biāo)都可以給任何人看的,乃至運(yùn)維人員,所以對(duì)被監(jiān)控指標(biāo)的是否開(kāi)放出來(lái)給監(jiān)控系統(tǒng)看,也需要做詳細(xì)的設(shè)定。

? 以上描述的整個(gè)過(guò)程就是一個(gè)監(jiān)控系統(tǒng)的基本流程。

總結(jié)

  1. 監(jiān)控是一個(gè)非常重要的工作,是保障程序正常運(yùn)行的基礎(chǔ)手段
  2. 監(jiān)控的過(guò)程通過(guò)一個(gè)監(jiān)控程序進(jìn)行,它匯總所有被監(jiān)控的程序的信息集中統(tǒng)一展示
  3. 被監(jiān)控程序需要主動(dòng)上報(bào)自己被監(jiān)控,同時(shí)要設(shè)置哪些指標(biāo)被監(jiān)控

思考

? 下面就要開(kāi)始做監(jiān)控了,新的問(wèn)題就來(lái)了,監(jiān)控程序怎么做呢?難道要自己寫(xiě)嗎?肯定是不現(xiàn)實(shí)的,如何進(jìn)行監(jiān)控,咱們下節(jié)再講。

KF-6-2.可視化監(jiān)控平臺(tái)

? springboot抽取了大部分監(jiān)控系統(tǒng)的常用指標(biāo),提出了監(jiān)控的總思想。然后就有好心的同志根據(jù)監(jiān)控的總思想,制作了一個(gè)通用性很強(qiáng)的監(jiān)控系統(tǒng),因?yàn)槭腔趕pringboot監(jiān)控的核心思想制作的,所以這個(gè)程序被命名為Spring Boot Admin。

? Spring Boot Admin,這是一個(gè)開(kāi)源社區(qū)項(xiàng)目,用于管理和監(jiān)控SpringBoot應(yīng)用程序。這個(gè)項(xiàng)目中包含有客戶端和服務(wù)端兩部分,而監(jiān)控平臺(tái)指的就是服務(wù)端。我們做的程序如果需要被監(jiān)控,將我們做的程序制作成客戶端,然后配置服務(wù)端地址后,服務(wù)端就可以通過(guò)HTTP請(qǐng)求的方式從客戶端獲取對(duì)應(yīng)的信息,并通過(guò)UI界面展示對(duì)應(yīng)信息。

? 下面就來(lái)開(kāi)發(fā)這套監(jiān)控程序,先制作服務(wù)端,其實(shí)服務(wù)端可以理解為是一個(gè)web程序,收到一些信息后展示這些信息。

服務(wù)端開(kāi)發(fā)

步驟①:導(dǎo)入springboot admin對(duì)應(yīng)的starter,版本與當(dāng)前使用的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>

? 上述過(guò)程可以通過(guò)創(chuàng)建項(xiàng)目時(shí)使用勾選的形式完成。

在這里插入圖片描述

步驟②:在引導(dǎo)類上添加注解@EnableAdminServer,聲明當(dāng)前應(yīng)用啟動(dòng)后作為SpringBootAdmin的服務(wù)器使用

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

? 做到這里,這個(gè)服務(wù)器就開(kāi)發(fā)好了,啟動(dòng)后就可以訪問(wèn)當(dāng)前程序了,界面如下。

在這里插入圖片描述

? 由于目前沒(méi)有啟動(dòng)任何被監(jiān)控的程序,所以里面什么信息都沒(méi)有。下面制作一個(gè)被監(jiān)控的客戶端程序。

客戶端開(kāi)發(fā)

? 客戶端程序開(kāi)發(fā)其實(shí)和服務(wù)端開(kāi)發(fā)思路基本相似,多了一些配置而已。

步驟①:導(dǎo)入springboot admin對(duì)應(yīng)的starter,版本與當(dāng)前使用的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>

? 上述過(guò)程也可以通過(guò)創(chuàng)建項(xiàng)目時(shí)使用勾選的形式完成,不過(guò)一定要小心,端口配置成不一樣的,否則會(huì)沖突。

步驟②:設(shè)置當(dāng)前客戶端將信息上傳到哪個(gè)服務(wù)器上,通過(guò)yml文件配置

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

? 做到這里,這個(gè)客戶端就可以啟動(dòng)了。啟動(dòng)后再次訪問(wèn)服務(wù)端程序,界面如下。

在這里插入圖片描述

? 可以看到,當(dāng)前監(jiān)控了1個(gè)程序,點(diǎn)擊進(jìn)去查看詳細(xì)信息。

在這里插入圖片描述

? 由于當(dāng)前沒(méi)有設(shè)置開(kāi)放哪些信息給監(jiān)控服務(wù)器,所以目前看不到什么有效的信息。下面需要做兩組配置就可以看到信息了。

  1. 開(kāi)放指定信息給服務(wù)器看

  2. 允許服務(wù)器以HTTP請(qǐng)求的方式獲取對(duì)應(yīng)的信息

    配置如下:

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

? 上述配置對(duì)于初學(xué)者來(lái)說(shuō)比較容易混淆。簡(jiǎn)單解釋一下,到下一節(jié)再做具體的講解。springbootadmin的客戶端默認(rèn)開(kāi)放了13組信息給服務(wù)器,但是這些信息除了一個(gè)之外,其他的信息都不讓通過(guò)HTTP請(qǐng)求查看。所以你看到的信息基本上就沒(méi)什么內(nèi)容了,只能看到一個(gè)內(nèi)容,就是下面的健康信息。

在這里插入圖片描述

? 但是即便如此我們看到健康信息中也沒(méi)什么內(nèi)容,原因在于健康信息中有一些信息描述了你當(dāng)前應(yīng)用使用了什么技術(shù)等信息,如果無(wú)腦的對(duì)外暴露功能會(huì)有安全隱患。通過(guò)配置就可以開(kāi)放所有的健康信息明細(xì)查看了。

management:endpoint:health:show-details: always

? 健康明細(xì)信息如下:

在這里插入圖片描述

? 目前除了健康信息,其他信息都查閱不了。原因在于其他12種信息是默認(rèn)不提供給服務(wù)器通過(guò)HTTP請(qǐng)求查閱的,所以需要開(kāi)啟查閱的內(nèi)容項(xiàng),使用*表示查閱全部。記得帶引號(hào)。

endpoints:web:exposure:include: "*"

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

在這里插入圖片描述

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

配置多個(gè)客戶端

? 可以通過(guò)配置客戶端的方式在其他的springboot程序中添加客戶端坐標(biāo),這樣當(dāng)前服務(wù)器就可以監(jiān)控多個(gè)客戶端程序了。每個(gè)客戶端展示不同的監(jiān)控信息。

在這里插入圖片描述

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

  • 類加載面板中可以查閱到開(kāi)發(fā)者自定義的類,如左圖

? 在這里插入圖片描述

  • 映射中可以查閱到當(dāng)前應(yīng)用配置的所有請(qǐng)求

? 在這里插入圖片描述

  • 性能指標(biāo)中可以查閱當(dāng)前應(yīng)用獨(dú)有的請(qǐng)求路徑統(tǒng)計(jì)數(shù)據(jù)

? 在這里插入圖片描述

總結(jié)

  1. 開(kāi)發(fā)監(jiān)控服務(wù)端需要導(dǎo)入坐標(biāo),然后在引導(dǎo)類上添加注解@EnableAdminServer,并將其配置成web程序即可
  2. 開(kāi)發(fā)被監(jiān)控的客戶端需要導(dǎo)入坐標(biāo),然后配置服務(wù)端服務(wù)器地址,并做開(kāi)放指標(biāo)的設(shè)定即可
  3. 在監(jiān)控平臺(tái)中可以查閱到各種各樣被監(jiān)控的指標(biāo),前提是客戶端開(kāi)放了被監(jiān)控的指標(biāo)

思考

? 之前說(shuō)過(guò),服務(wù)端要想監(jiān)控客戶端,需要主動(dòng)的獲取到對(duì)應(yīng)信息并展示出來(lái)。但是目前我們并沒(méi)有在客戶端開(kāi)發(fā)任何新的功能,但是服務(wù)端確可以獲取監(jiān)控信息,誰(shuí)幫我們做的這些功能呢?咱們下一節(jié)再講。

KF-6-3.監(jiān)控原理

? 通過(guò)查閱監(jiān)控中的映射指標(biāo),可以看到當(dāng)前系統(tǒng)中可以運(yùn)行的所有請(qǐng)求路徑,其中大部分路徑以/actuator開(kāi)頭

在這里插入圖片描述

? 首先這些請(qǐng)求路徑不是開(kāi)發(fā)者自己編寫(xiě)的,其次這個(gè)路徑代表什么含義呢?既然這個(gè)路徑可以訪問(wèn),就可以通過(guò)瀏覽器發(fā)送該請(qǐng)求看看究竟可以得到什么信息。

在這里插入圖片描述

? 通過(guò)發(fā)送請(qǐng)求,可以得到一組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}}
}

? 其中每一組數(shù)據(jù)都有一個(gè)請(qǐng)求路徑,而在這里請(qǐng)求路徑中有之前看到過(guò)的health,發(fā)送此請(qǐng)求又得到了一組信息

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

? 當(dāng)前信息與監(jiān)控面板中的數(shù)據(jù)存在著對(duì)應(yīng)關(guān)系

在這里插入圖片描述

? 原來(lái)監(jiān)控中顯示的信息實(shí)際上是通過(guò)發(fā)送請(qǐng)求后得到j(luò)son數(shù)據(jù),然后展示出來(lái)。按照上述操作,可以發(fā)送更多的以/actuator開(kāi)頭的鏈接地址,獲取更多的數(shù)據(jù),這些數(shù)據(jù)匯總到一起組成了監(jiān)控平臺(tái)顯示的所有數(shù)據(jù)。

? 到這里我們得到了一個(gè)核心信息,監(jiān)控平臺(tái)中顯示的信息實(shí)際上是通過(guò)對(duì)被監(jiān)控的應(yīng)用發(fā)送請(qǐng)求得到的。那這些請(qǐng)求誰(shuí)開(kāi)發(fā)的呢?打開(kāi)被監(jiān)控應(yīng)用的pom文件,其中導(dǎo)入了springboot admin的對(duì)應(yīng)的client,在這個(gè)資源中導(dǎo)入了一個(gè)名稱叫做actuator的包。被監(jiān)控的應(yīng)用之所以可以對(duì)外提供上述請(qǐng)求路徑,就是因?yàn)樘砑恿诉@個(gè)包。

在這里插入圖片描述

? 這個(gè)actuator是什么呢?這就是本節(jié)要講的核心內(nèi)容,監(jiān)控的端點(diǎn)。

? Actuator,可以稱為端點(diǎn),描述了一組監(jiān)控信息,SpringBootAdmin提供了多個(gè)內(nèi)置端點(diǎn),通過(guò)訪問(wèn)端點(diǎn)就可以獲取對(duì)應(yīng)的監(jiān)控信息,也可以根據(jù)需要自定義端點(diǎn)信息。通過(guò)發(fā)送請(qǐng)求路勁**/actuator可以訪問(wèn)應(yīng)用所有端點(diǎn)信息,如果端點(diǎn)中還有明細(xì)信息可以發(fā)送請(qǐng)求/actuator/端點(diǎn)名稱**來(lái)獲取詳細(xì)信息。以下列出了所有端點(diǎn)信息說(shuō)明:

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

? 上述端點(diǎn)每一項(xiàng)代表被監(jiān)控的指標(biāo),如果對(duì)外開(kāi)放則監(jiān)控平臺(tái)可以查詢到對(duì)應(yīng)的端點(diǎn)信息,如果未開(kāi)放則無(wú)法查詢對(duì)應(yīng)的端點(diǎn)信息。通過(guò)配置可以設(shè)置端點(diǎn)是否對(duì)外開(kāi)放功能。使用enable屬性控制端點(diǎn)是否對(duì)外開(kāi)放。其中health端點(diǎn)為默認(rèn)端點(diǎn),不能關(guān)閉。

management:endpoint:health:						# 端點(diǎn)名稱show-details: alwaysinfo:						# 端點(diǎn)名稱enabled: true				# 是否開(kāi)放

? 為了方便開(kāi)發(fā)者快速配置端點(diǎn),springboot admin設(shè)置了13個(gè)較為常用的端點(diǎn)作為默認(rèn)開(kāi)放的端點(diǎn),如果需要控制默認(rèn)開(kāi)放的端點(diǎn)的開(kāi)放狀態(tài),可以通過(guò)配置設(shè)置,如下:

management:endpoints:enabled-by-default: true	# 是否開(kāi)啟默認(rèn)端點(diǎn),默認(rèn)值true

? 上述端點(diǎn)開(kāi)啟后,就可以通過(guò)端點(diǎn)對(duì)應(yīng)的路徑查看對(duì)應(yīng)的信息了。但是此時(shí)還不能通過(guò)HTTP請(qǐng)求查詢此信息,還需要開(kāi)啟通過(guò)HTTP請(qǐng)求查詢的端點(diǎn)名稱,使用“*”可以簡(jiǎn)化配置成開(kāi)放所有端點(diǎn)的WEB端HTTP請(qǐng)求權(quán)限。

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

? 整體上來(lái)說(shuō),對(duì)于端點(diǎn)的配置有兩組信息,一組是endpoints開(kāi)頭的,對(duì)所有端點(diǎn)進(jìn)行配置,一組是endpoint開(kāi)頭的,對(duì)具體端點(diǎn)進(jìn)行配置。

management:endpoint:		# 具體端點(diǎn)的配置health:show-details: alwaysinfo:enabled: trueendpoints:	# 全部端點(diǎn)的配置web:exposure:include: "*"enabled-by-default: true

總結(jié)

  1. 被監(jiān)控客戶端通過(guò)添加actuator的坐標(biāo)可以對(duì)外提供被訪問(wèn)的端點(diǎn)功能

  2. 端點(diǎn)功能的開(kāi)放與關(guān)閉可以通過(guò)配置進(jìn)行控制

  3. web端默認(rèn)無(wú)法獲取所有端點(diǎn)信息,通過(guò)配置開(kāi)放端點(diǎn)功能

KF-6-4.自定義監(jiān)控指標(biāo)

? 端點(diǎn)描述了被監(jiān)控的信息,除了系統(tǒng)默認(rèn)的指標(biāo),還可以自行添加顯示的指標(biāo),下面就通過(guò)3種不同的端點(diǎn)的指標(biāo)自定義方式來(lái)學(xué)習(xí)端點(diǎn)信息的二次開(kāi)發(fā)。

INFO端點(diǎn)

? info端點(diǎn)描述了當(dāng)前應(yīng)用的基本信息,可以通過(guò)兩種形式快速配置info端點(diǎn)的信息

  • 配置形式

    在yml文件中通過(guò)設(shè)置info節(jié)點(diǎn)的信息就可以快速配置端點(diǎn)信息

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

    配置完畢后,對(duì)應(yīng)信息顯示在監(jiān)控平臺(tái)上

在這里插入圖片描述

也可以通過(guò)請(qǐng)求端點(diǎn)信息路徑獲取對(duì)應(yīng)json信息

在這里插入圖片描述

  • 編程形式

    通過(guò)配置的形式只能添加固定的數(shù)據(jù),如果需要?jiǎng)討B(tài)數(shù)據(jù)還可以通過(guò)配置bean的方式為info端點(diǎn)添加信息,此信息與配置信息共存

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

Health端點(diǎn)

? health端點(diǎn)描述當(dāng)前應(yīng)用的運(yùn)行健康指標(biāo),即應(yīng)用的運(yùn)行是否成功。通過(guò)編程的形式可以擴(kuò)展指標(biāo)信息。

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

? 當(dāng)任意一個(gè)組件狀態(tài)不為UP時(shí),整體應(yīng)用對(duì)外服務(wù)狀態(tài)為非UP狀態(tài)。

在這里插入圖片描述

Metrics端點(diǎn)

? metrics端點(diǎn)描述了性能指標(biāo),除了系統(tǒng)自帶的監(jiān)控性能指標(biāo),還可以自定義性能指標(biāo)。

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

? 在性能指標(biāo)中就出現(xiàn)了自定義的性能指標(biāo)監(jiān)控項(xiàng)

在這里插入圖片描述

自定義端點(diǎn)

? 可以根據(jù)業(yè)務(wù)需要自定義端點(diǎn),方便業(yè)務(wù)監(jiān)控

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

? 由于此端點(diǎn)數(shù)據(jù)spirng boot admin無(wú)法預(yù)知該如何展示,所以通過(guò)界面無(wú)法看到此數(shù)據(jù),通過(guò)HTTP請(qǐng)求路徑可以獲取到當(dāng)前端點(diǎn)的信息,但是需要先開(kāi)啟當(dāng)前端點(diǎn)對(duì)外功能,或者設(shè)置當(dāng)前端點(diǎn)為默認(rèn)開(kāi)發(fā)的端點(diǎn)。

在這里插入圖片描述

總結(jié)

  1. 端點(diǎn)的指標(biāo)可以自定義,但是每種不同的指標(biāo)根據(jù)其功能不同,自定義方式不同
  2. info端點(diǎn)通過(guò)配置和編程的方式都可以添加端點(diǎn)指標(biāo)
  3. health端點(diǎn)通過(guò)編程的方式添加端點(diǎn)指標(biāo),需要注意要為對(duì)應(yīng)指標(biāo)添加啟動(dòng)狀態(tài)的邏輯設(shè)定
  4. metrics指標(biāo)通過(guò)在業(yè)務(wù)中添加監(jiān)控操作設(shè)置指標(biāo)
  5. 可以自定義端點(diǎn)添加更多的指標(biāo)
http://www.risenshineclean.com/news/4962.html

相關(guān)文章:

  • 西安做推廣網(wǎng)站設(shè)計(jì)軟件拉新推廣平臺(tái)
  • 本地建設(shè)網(wǎng)站西安網(wǎng)約車(chē)平臺(tái)
  • 網(wǎng)站footer設(shè)計(jì)網(wǎng)頁(yè)制作html代碼
  • 網(wǎng)站關(guān)鍵詞幾個(gè)好策劃方案模板
  • wordpress虛擬主機(jī)安裝教程seo如何進(jìn)行優(yōu)化
  • 如何用ip地址做網(wǎng)站軟文推廣哪個(gè)平臺(tái)好
  • 網(wǎng)站虛擬域名seo指的是搜索引擎
  • 網(wǎng)頁(yè)游戲賺錢(qián)平臺(tái)有哪些沈陽(yáng)高端關(guān)鍵詞優(yōu)化
  • b2b電子商務(wù)網(wǎng)站介紹chrome官方下載
  • 網(wǎng)站做成app網(wǎng)頁(yè)設(shè)計(jì)收費(fèi)標(biāo)準(zhǔn)
  • 一個(gè)網(wǎng)站怎么做seo搜索引擎優(yōu)化方式
  • 在百度上建網(wǎng)站怎么建設(shè)今日國(guó)內(nèi)重大新聞
  • java做軟件的網(wǎng)站企業(yè)網(wǎng)站建設(shè)案例
  • 石家莊做網(wǎng)站最好的公司有哪些搜狗收錄入口
  • 連州網(wǎng)站建設(shè)關(guān)鍵詞搜索量查詢工具
  • 只做百度移動(dòng)端網(wǎng)站可以嗎怎樣制作一個(gè)網(wǎng)站
  • 微網(wǎng)站怎么做的好處國(guó)內(nèi)營(yíng)銷(xiāo)推廣渠道
  • 茄子直播搜索引擎網(wǎng)站排名優(yōu)化方案
  • 哈爾濱創(chuàng)意網(wǎng)站建設(shè)黃岡網(wǎng)站推廣軟件視頻下載
  • 鹽亭做網(wǎng)站廣州最新新聞事件
  • 做公司網(wǎng)站 國(guó)外系統(tǒng)個(gè)人網(wǎng)頁(yè)生成器
  • 哪個(gè)網(wǎng)站可以做高像素動(dòng)圖優(yōu)化搜索點(diǎn)擊次數(shù)的方法
  • 合肥最好的網(wǎng)站建設(shè)營(yíng)銷(xiāo)管理
  • 青島網(wǎng)景互聯(lián)網(wǎng)站建設(shè)公司年度關(guān)鍵詞
  • 做承諾的網(wǎng)站上海百度推廣排名
  • 百度網(wǎng)站排名全掉軟件開(kāi)發(fā)定制
  • 寫(xiě)論文的好網(wǎng)站寧波網(wǎng)站建設(shè)網(wǎng)站排名優(yōu)化
  • 免費(fèi)的網(wǎng)站推廣在線推廣百度seo新站優(yōu)化
  • 網(wǎng)站怎樣做多語(yǔ)言切換做app的網(wǎng)站
  • 廣西柳州網(wǎng)站建設(shè)安陽(yáng)企業(yè)網(wǎng)站優(yōu)化外包