【黑馬程序員SpringCloud微服務技術棧實戰教程,涵蓋springcloud微服務架構+Nacos配置中心+分布式事務等】
暫時無法在飛書文檔外展示此內容
之前我們學習的項目一是單體項目,可以滿足小型項目或傳統項目的開發。而在互聯網時代,越來越多的一線互聯網公司都在使用微服務技術。
從谷歌搜索指數來看,國內從自2016年底開始,微服務熱度突然暴漲:
那么:
- 到底什么是微服務?
- 企業該不該引入微服務?
- 微服務技術該如何在企業落地?
接下來幾天,我們就一起來揭開它的神秘面紗。
計劃是這樣的,課前資料中給大家準備了一個單體的電商小項目:黑馬商城,我們會基于這個單體項目來演示從單體架構到微服務架構的演變過程、分析其中存在的問題,以及微服務技術是如何解決這些問題的。
你會發現每一個微服務技術都是在解決服務化過程中產生的問題,你對于每一個微服務技術具體的應用場景和使用方式都會有更深層次的理解。
今天作為課程的第一天,我們要完成下面的內容:
- 知道單體架構的特點
- 知道微服務架構的特點
- 學會拆分微服務
- 會使用Nacos實現服務治理
- 會使用OpenFeign實現遠程調用
0.導入黑馬商城項目
在課前資料中給大家提供了黑馬商城項目的資料,我們需要先導入這個單體項目。不過需要注意的是,本篇及后續的微服務學習都是基于Centos7系統下的Docker部署,因此你必須做好一些準備:
- Centos7的環境及一個好用的SSH客戶端
- 安裝好Docker
- 會使用Docker
如果你沒有這樣的Linux環境,或者不是Centos7的話,那么這里有一篇參考文檔:
- Linux環境搭建
建議按照上面的文檔來搭建虛擬機環境,使用其它版本會出現一些環境問題,比較痛苦。
如果已經有Linux環境,但是沒有安裝Docker的話,那么這里還有一篇參考文檔:
- 安裝Docker
如果不會使用Docker的話可以參考黑馬的微服務前置Docker課程,B站地址如下:
https://www.bilibili.com/video/BV1HP4118797/?share_source=copy_web&vd_source=3362e6914fb759983690e6e0f1072453
注意:
如果是學習過上面Docker課程的同學,虛擬機中已經有了黑馬商城項目及MySQL數據庫了,不過為了跟其他同學保持一致,可以先將整個項目移除。使用下面的命令:
cd /root
docker compose down
0.1.安裝MySQL
在課前資料提供好了MySQL的一個目錄:
其中有MySQL的配置文件和初始化腳本:
我們將其復制到虛擬機的/root
目錄。如果/root
下已經存在mysql
目錄則刪除舊的,如果不存在則直接復制本地的:
然后創建一個通用網絡:
docker network create hm-net
使用下面的命令來安裝MySQL:
docker run -d \--name mysql \-p 3306:3306 \-e TZ=Asia/Shanghai \-e MYSQL_ROOT_PASSWORD=123 \-v /root/mysql/data:/var/lib/mysql \-v /root/mysql/conf:/etc/mysql/conf.d \-v /root/mysql/init:/docker-entrypoint-initdb.d \--network hm-net\mysql
此時,通過命令查看mysql容器:
docker ps
如圖:
發現mysql容器正常運行。
注:圖片中的dps命令是我設置的別名,等同于docker ps --format,可以簡化命令格式。你可以參考黑馬的day02-Docker 的2.1.3小節來配置。
此時,如果我們使用MySQL的客戶端工具連接MySQL,應該能發現已經創建了黑馬商城所需要的表:
0.2.后端
然后是Java代碼,在課前資料提供了一個hmall目錄:
將其復制到你的工作空間,然后利用Idea打開。
項目結構如下:
按下ALT
+ 8
鍵打開services窗口,新增一個啟動項:
在彈出窗口中鼠標向下滾動,找到Spring Boot
:
點擊后應該會在services中出現hmall的啟動項:
點擊對應按鈕,即可實現運行或DEBUG運行。
不過別著急!!
我們還需要對這個啟動項做簡單配置,在HMallApplication
上點擊鼠標右鍵,會彈出窗口,然后選擇Edit Configuration
:
在彈出窗口中配置SpringBoot的啟動環境為local:
點擊OK配置完成。接下來就可以運行了!
啟動完成后,試試看訪問下 http://localhost:8080/hi 吧!
0.3.前端
在課前資料中還提供了一個hmall-nginx的目錄:
其中就是一個nginx程序以及我們的前端代碼,直接在windows下將其復制到一個非中文、不包含特殊字符的目錄下。然后進入hmall-nginx后,利用cmd啟動即可:
# 啟動nginx
start nginx.exe
# 停止
nginx.exe -s stop
# 重新加載配置
nginx.exe -s reload
# 重啟
nginx.exe -s restart
特別注意:
nginx.exe 不要雙擊啟動,而是打開cmd窗口,通過命令行啟動。停止的時候也一樣要是用命令停止。如果啟動失敗不要重復啟動,而是查看logs目錄中的error.log日志,查看是否是端口沖突。如果是端口沖突則自行修改端口解決。
啟動成功后,訪問http://localhost:18080,應該能看到我們的門戶頁面:
1.認識微服務
這一章我們從單體架構的優缺點來分析,看看開發大型項目采用單體架構存在哪些問題,而微服務架構又是如何解決這些問題的。
1.1.單體架構
單體架構(monolithic structure):顧名思義,整個項目中所有功能模塊都在一個工程中開發;項目部署時需要對所有模塊一起編譯、打包;項目的架構設計、開發模式都非常簡單。
當項目規模較小時,這種模式上手快,部署、運維也都很方便,因此早期很多小型項目都采用這種模式。
但隨著項目的業務規模越來越大,團隊開發人員也不斷增加,單體架構就呈現出越來越多的問題:
- 團隊協作成本高:試想一下,你們團隊數十個人同時協作開發同一個項目,由于所有模塊都在一個項目中,不同模塊的代碼之間物理邊界越來越模糊。最終要把功能合并到一個分支,你絕對會陷入到解決沖突的泥潭之中。
- 系統發布效率低:任何模塊變更都需要發布整個系統,而系統發布過程中需要多個模塊之間制約較多,需要對比各種文件,任何一處出現問題都會導致發布失敗,往往一次發布需要數十分鐘甚至數小時。
- 系統可用性差:單體架構各個功能模塊是作為一個服務部署,相互之間會互相影響,一些熱點功能會耗盡系統資源,導致其它服務低可用。
在上述問題中,前兩點相信大家在實戰過程中應該深有體會。對于第三點系統可用性問題,很多同學可能感觸不深。接下來我們就通過黑馬商城這個項目,給大家做一個簡單演示。
首先,我們修改hm-service模塊下的com.hmall.controller.HelloController
中的hello
方法,模擬方法執行時的耗時:
接下來,啟動項目,目前有兩個接口是無需登錄即可訪問的:
http://localhost:8080/hi
http://localhost:8080/search/list
經過測試,目前/search/list
是比較正常的,訪問耗時在30毫秒左右。
接下來,我們假設/hi
這個接口是一個并發較高的熱點接口,我們通過Jemeter來模擬500個用戶不停訪問。在課前資料中已經提供了Jemeter的測試腳本:
導入Jemeter并測試:
這個腳本會開啟500個線程并發請求http://localhost/hi
這個接口。由于該接口存在執行耗時(500毫秒),這就服務端導致每秒能處理的請求數量有限,最終會有越來越多請求積壓,直至Tomcat資源耗盡。這樣,其它本來正常的接口(例如/search/list
)也都會被拖慢,甚至因超時而無法訪問了。
我們測試一下,啟動測試腳本,然后在瀏覽器訪問http://localhost:8080/search/list
這個接口,會發現響應速度非常慢:
如果進一步提高/hi
這個接口的并發,最終會發現/search/list
接口的請求響應速度會越來越慢。
可見,單體架構的可用性是比較差的,功能之間相互影響比較大。
當然,有同學會說我們可以做水平擴展。
此時如果我們對系統做水平擴展,增加更多機器,資源還是會被這樣的熱點接口占用,從而影響到其它接口,并不能從根本上解決問題。這也就是單體架構的擴展性差的一個原因。
而要想解決這些問題,就需要使用微服務架構了。
1.2.微服務
微服務架構,首先是服務化,就是將單體架構中的功能模塊從單體應用中拆分出來,獨立部署為多個服務。同時要滿足下面的一些特點:
- 單一職責:一個微服務負責一部分業務功能,并且其核心數據不依賴于其它模塊。
- 團隊自治:每個微服務都有自己獨立的開發、測試、發布、運維人員,團隊人員規模不超過10人(2張披薩能喂飽)
- 服務自治:每個微服務都獨立打包部署,訪問自己獨立的數據庫。并且要做好服務隔離,避免對其它服務產生影響
例如,黑馬商城項目,我們就可以把商品、用戶、購物車、交易等模塊拆分,交給不同的團隊去開發,并獨立部署:
那么,單體架構存在的問題有沒有解決呢?
- 團隊協作成本高?
-
- 由于服務拆分,每個服務代碼量大大減少,參與開發的后臺人員在1~3名,協作成本大大降低
- 系統發布效率低?
-
- 每個服務都是獨立部署,當有某個服務有代碼變更時,只需要打包部署該服務即可
- 系統可用性差?
-
- 每個服務獨立部署,并且做好服務隔離,使用自己的服務器資源,不會影響到其它服務。
綜上所述,微服務架構解決了單體架構存在的問題,特別適合大型互聯網項目的開發,因此被各大互聯網公司普遍采用。大家以前可能聽說過分布式架構,分布式就是服務拆分的過程,其實微服務架構正式分布式架構的一種最佳實踐的方案。
當然,微服務架構雖然能解決單體架構的各種問題,但在拆分的過程中,還會面臨很多其它問題。比如:
- 如果出現跨服務的業務該如何處理?
- 頁面請求到底該訪問哪個服務?
- 如何實現各個服務之間的服務隔離?
這些問題,我們在后續的學習中會給大家逐一解答。
1.3.SpringCloud
微服務拆分以后碰到的各種問題都有對應的解決方案和微服務組件,而SpringCloud框架可以說是目前Java領域最全面的微服務組件的集合了。
而且SpringCloud依托于SpringBoot的自動裝配能力,大大降低了其項目搭建、組件使用的成本。對于沒有自研微服務組件能力的中小型企業,使用SpringCloud全家桶來實現微服務開發可以說是最合適的選擇了!
https://spring.io/projects/spring-cloud#overview
目前SpringCloud最新版本為2022.0.x
版本,對應的SpringBoot版本為3.x
版本,但它們全部依賴于JDK17,目前在企業中使用相對較少。
SpringCloud版本 | SpringBoot版本 |
2022.0.x aka Kilburn | 3.0.x |
2021.0.x aka Jubilee | 2.6.x, 2.7.x (Starting with 2021.0.3) |
2020.0.x aka Ilford | 2.4.x, 2.5.x (Starting with 2020.0.3) |
Hoxton | 2.2.x, 2.3.x (Starting with SR5) |
Greenwich | 2.1.x |
Finchley | 2.0.x |
Edgware | 1.5.x |
Dalston | 1.5.x |
因此,我們推薦使用次新版本:Spring Cloud 2021.0.x以及Spring Boot 2.7.x版本。
另外,Alibaba的微服務產品SpringCloudAlibaba目前也成為了SpringCloud組件中的一員,我們課堂中也會使用其中的部分組件。
在我們的父工程hmall中已經配置了SpringCloud以及SpringCloudAlibaba的依賴:
對應的版本:
這樣,我們在后續需要使用SpringCloud或者SpringCloudAlibaba組件時,就無需單獨指定版本了。
2.微服務拆分
接下來,我們就一起將黑馬商城這個單體項目拆分為微服務項目,并解決其中出現的各種問題。
2.1.熟悉黑馬商城
首先,我們需要熟悉黑馬商城項目的基本結構:
大家可以直接啟動該項目,測試效果。不過,需要修改數據庫連接參數,在application-local.yaml中:
hm:db:host: 192.168.150.101 # 修改為你自己的虛擬機IP地址pw: 123 # 修改為docker中的MySQL密碼
同時配置啟動項激活的是local環境:
2.1.1.登錄
首先來看一下登錄業務流程:
暫時無法在飛書文檔外展示此內容
登錄入口在com.hmall.controller.UserController
中的login
方法:
2.2.2.搜索商品
在首頁搜索框輸入關鍵字,點擊搜索即可進入搜索列表頁面:
該頁面會調用接口:/search/list
,對應的服務端入口在com.hmall.controller.SearchController
中的search
方法:
這里目前是利用數據庫實現了簡單的分頁查詢。
2.2.3.購物車
在搜索到的商品列表中,點擊按鈕加入購物車
,即可將商品加入購物車:
加入成功后即可進入購物車列表頁,查看自己購物車商品列表:
同時這里還可以對購物車實現修改、刪除等操作。
相關功能全部在com.hmall.controller.CartController
中:
其中,查詢購物車列表時,由于要判斷商品最新的價格和狀態,所以還需要查詢商品信息,業務流程如下:
暫時無法在飛書文檔外展示此內容
2.2.4.下單
在購物車頁面點擊結算
按鈕,會進入訂單結算頁面:
點擊提交訂單,會提交請求到服務端,服務端做3件事情:
- 創建一個新的訂單
- 扣減商品庫存
- 清理購物車中商品
業務入口在com.hmall.controller.OrderController
中的createOrder
方法:
2.2.5.支付
下單完成后會跳轉到支付頁面,目前只支持余額支付:
在選擇余額支付這種方式后,會發起請求到服務端,服務端會立刻創建一個支付流水單,并返回支付流水單號到前端。
當用戶輸入用戶密碼,然后點擊確認支付時,頁面會發送請求到服務端,而服務端會做幾件事情:
- 校驗用戶密碼
- 扣減余額
- 修改支付流水狀態
- 修改交易訂單狀態
請求入口在com.hmall.controller.PayController
中:
2.2.服務拆分原則
服務拆分一定要考慮幾個問題:
- 什么時候拆?
- 如何拆?
2.2.1.什么時候拆
一般情況下,對于一個初創的項目,首先要做的是驗證項目的可行性。因此這一階段的首要任務是敏捷開發,快速產出生產可用的產品,投入市場做驗證。為了達成這一目的,該階段項目架構往往會比較簡單,很多情況下會直接采用單體架構,這樣開發成本比較低,可以快速產出結果,一旦發現項目不符合市場,損失較小。
如果這一階段采用復雜的微服務架構,投入大量的人力和時間成本用于架構設計,最終發現產品不符合市場需求,等于全部做了無用功。
所以,對于大多數小型項目來說,一般是先采用單體架構,隨著用戶規模擴大、業務復雜后再逐漸拆分為微服務架構。這樣初期成本會比較低,可以快速試錯。但是,這么做的問題就在于后期做服務拆分時,可能會遇到很多代碼耦合帶來的問題,拆分比較困難(前易后難)。
而對于一些大型項目,在立項之初目的就很明確,為了長遠考慮,在架構設計時就直接選擇微服務架構。雖然前期投入較多,但后期就少了拆分服務的煩惱(前難后易)。
2.2.2.怎么拆
之前我們說過,微服務拆分時粒度要小,這其實是拆分的目標。具體可以從兩個角度來分析:
- 高內聚:每個微服務的職責要盡量單一,包含的業務相互關聯度高、完整度高。
- 低耦合:每個微服務的功能要相對獨立,盡量減少對其它微服務的依賴,或者依賴接口的穩定性要強。
高內聚首先是單一職責,但不能說一個微服務就一個接口,而是要保證微服務內部業務的完整性為前提。目標是當我們要修改某個業務時,最好就只修改當前微服務,這樣變更的成本更低。
一旦微服務做到了高內聚,那么服務之間的耦合度自然就降低了。
當然,微服務之間不可避免的會有或多或少的業務交互,比如下單時需要查詢商品數據。這個時候我們不能在訂單服務直接查詢商品數據庫,否則就導致了數據耦合。而應該由商品服務對應暴露接口,并且一定要保證微服務對外接口的穩定性(即:盡量保證接口外觀不變)。雖然出現了服務間調用,但此時無論你如何在商品服務做內部修改,都不會影響到訂單微服務,服務間的耦合度就降低了。
明確了拆分目標,接下來就是拆分方式了。我們在做服務拆分時一般有兩種方式:
- 縱向拆分
- 橫向拆分
所謂縱向拆分,就是按照項目的功能模塊來拆分。例如黑馬商城中,就有用戶管理功能、訂單管理功能、購物車功能、商品管理功能、支付功能等。那么按照功能模塊將他們拆分為一個個服務,就屬于縱向拆分。這種拆分模式可以盡可能提高服務的內聚性。
而橫向拆分,是看各個功能模塊之間有沒有公共的業務部分,如果有將其抽取出來作為通用服務。例如用戶登錄是需要發送消息通知,記錄風控數據,下單時也要發送短信,記錄風控數據。因此消息發送、風控數據記錄就是通用的業務功能,因此可以將他們分別抽取為公共服務:消息中心服務、風控管理服務。這樣可以提高業務的復用性,避免重復開發。同時通用業務一般接口穩定性較強,也不會使服務之間過分耦合。
當然,由于黑馬商城并不是一個完整的項目,其中的短信發送、風控管理并沒有實現,這里就不再考慮了。而其它的業務按照縱向拆分,可以分為以下幾個微服務:
- 用戶服務
- 商品服務
- 訂單服務
- 購物車服務
- 支付服務
2.3.拆分購物車、商品服務
接下來,我們先把商品管理功能、購物車功能抽取為兩個獨立服務。
一般微服務項目有兩種不同的工程結構:
- 完全解耦:每一個微服務都創建為一個獨立的工程,甚至可以使用不同的開發語言來開發,項目完全解耦。
-
- 優點:服務之間耦合度低
- 缺點:每個項目都有自己的獨立倉庫,管理起來比較麻煩
- Maven聚合:整個項目為一個Project,然后每個微服務是其中的一個Module
-
- 優點:項目代碼集中,管理和運維方便(授課也方便)
- 缺點:服務之間耦合,編譯時間較長
注意:
為了授課方便,我們會采用Maven聚合工程,大家以后到了企業,可以根據需求自由選擇工程結構。
在hmall父工程之中,我已經提前定義了SpringBoot、SpringCloud的依賴版本,所以為了方便期間,我們直接在這個項目中創建微服務module.
2.3.1.商品服務
在hmall中創建module:
選擇maven模塊,并設定JDK版本為11:
商品模塊,我們起名為item-service
:
引入依賴:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>hmall</artifactId><groupId>com.heima</groupId><version>1.0.0</version></parent><modelVersion>4.0.0</modelVersion><artifactId>item-service</artifactId><properties><maven.compiler.source>11</maven.compiler.source><maven.compiler.target>11</maven.compiler.target></properties><dependencies><!--common--><dependency><groupId>com.heima</groupId><artifactId>hm-common</artifactId><version>1.0.0</version></dependency><!--web--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--數據庫--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><!--mybatis--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId></dependency><!--單元測試--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency></dependencies><build><finalName>${project.artifactId}</finalName><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
編寫啟動類:
代碼如下:
package com.hmall.item;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@MapperScan("com.hmall.item.mapper")
@SpringBootApplication
public class ItemApplication {public static void main(String[] args) {SpringApplication.run(ItemApplication.class, args);}
}
接下來是配置文件,可以從hm-service
中拷貝:
其中,application.yaml
內容如下:
server:port: 8081
spring:application:name: item-serviceprofiles:active: devdatasource:url: jdbc:mysql://${hm.db.host}:3306/hm-item?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghaidriver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: ${hm.db.pw}
mybatis-plus:configuration:default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandlerglobal-config:db-config:update-strategy: not_nullid-type: auto
logging:level:com.hmall: debugpattern:dateformat: HH:mm:ss:SSSfile:path: "logs/${spring.application.name}"
knife4j:enable: trueopenapi:title: 商品服務接口文檔description: "信息"email: zhanghuyi@itcast.cnconcat: 虎哥url: https://www.itcast.cnversion: v1.0.0group:default:group-name: defaultapi-rule: packageapi-rule-resources:- com.hmall.item.controller
剩下的application-dev.yaml
和application-local.yaml
直接從hm-service拷貝即可。
然后拷貝hm-service
中與商品管理有關的代碼到item-service
,如圖:
這里有一個地方的代碼需要改動,就是ItemServiceImpl
中的deductStock
方法:
改動前
改動后
這也是因為ItemMapper的所在包發生了變化,因此這里代碼必須修改包路徑。
最后,還要導入數據庫表。默認的數據庫連接的是虛擬機,在你docker數據庫執行課前資料提供的SQL文件:
最終,會在數據庫創建一個名為hm-item的database,將來的每一個微服務都會有自己的一個database:
注意:在企業開發的生產環境中,每一個微服務都應該有自己的獨立數據庫服務,而不僅僅是database,課堂我們用database來代替。
接下來,就可以啟動測試了,在啟動前我們要配置一下啟動項,讓默認激活的配置為local
而不是dev
:
在打開的編輯框填寫active profiles
:
接著,啟動item-service
,訪問商品微服務的swagger接口文檔:http://localhost:8081/doc.html
然后測試其中的根據id批量查詢商品這個接口:
測試參數:100002672302,100002624500,100002533430,結果如下:
說明商品微服務抽取成功了。
2.3.2.購物車服務
與商品服務類似,在hmall下創建一個新的module
,起名為cart-service
:
然后是依賴:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>hmall</artifactId><groupId>com.heima</groupId><version>1.0.0</version></parent><modelVersion>4.0.0</modelVersion><artifactId>cart-service</artifactId><properties><maven.compiler.source>11</maven.compiler.source><maven.compiler.target>11</maven.compiler.target></properties><dependencies><!--common--><dependency><groupId>com.heima</groupId><artifactId>hm-common</artifactId><version>1.0.0</version></dependency><!--web--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><!--數據庫--><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><!--mybatis--><dependency><groupId>com.baomidou</groupId><artifactId>mybatis-plus-boot-starter</artifactId></dependency><!--單元測試--><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId></dependency></dependencies><build><finalName>${project.artifactId}</finalName><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin></plugins></build>
</project>
然后是啟動類:
package com.hmall.cart;import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;@MapperScan("com.hmall.cart.mapper")
@SpringBootApplication
public class CartApplication {public static void main(String[] args) {SpringApplication.run(CartApplication.class, args);}
}
然后是配置文件,同樣可以拷貝自item-service
,不過其中的application.yaml
需要修改:
server:port: 8082
spring:application:name: cart-serviceprofiles:active: devdatasource:url: jdbc:mysql://${hm.db.host}:3306/hm-cart?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghaidriver-class-name: com.mysql.cj.jdbc.Driverusername: rootpassword: ${hm.db.pw}
mybatis-plus:configuration:default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandlerglobal-config:db-config:update-strategy: not_nullid-type: auto
logging:level:com.hmall: debugpattern:dateformat: HH:mm:ss:SSSfile:path: "logs/${spring.application.name}"
knife4j:enable: trueopenapi:title: 商品服務接口文檔description: "信息"email: zhanghuyi@itcast.cnconcat: 虎哥url: https://www.itcast.cnversion: v1.0.0group:default:group-name: defaultapi-rule: packageapi-rule-resources:- com.hmall.cart.controller
最后,把hm-service中的與購物車有關功能拷貝過來,最終的項目結構如下:
特別注意的是com.hmall.cart.service.impl.CartServiceImpl
,其中有兩個地方需要處理:
- 需要獲取登錄用戶信息,但登錄校驗功能目前沒有復制過來,先寫死固定用戶id
- 查詢購物車時需要查詢商品信息,而商品信息不在當前服務,需要先將這部分代碼注釋
我們對這部分代碼做如下修改:
package com.hmall.cart.service.impl;import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.hmall.cart.domain.dto.CartFormDTO;
import com.hmall.cart.domain.po.Cart;
import com.hmall.cart.domain.vo.CartVO;
import com.hmall.cart.mapper.CartMapper;
import com.hmall.cart.service.ICartService;
import com.hmall.common.exception.BizIllegalException;
import com.hmall.common.utils.BeanUtils;
import com.hmall.common.utils.CollUtils;
import com.hmall.common.utils.UserContext;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;import java.util.Collection;
import java.util.List;/*** <p>* 訂單詳情表 服務實現類* </p>** @author 虎哥* @since 2023-05-05*/
@Service
@RequiredArgsConstructor
public class CartServiceImpl extends ServiceImpl<CartMapper, Cart> implements ICartService {// private final IItemService itemService;@Overridepublic void addItem2Cart(CartFormDTO cartFormDTO) {// 1.獲取登錄用戶Long userId = UserContext.getUser();// 2.判斷是否已經存在if (checkItemExists(cartFormDTO.getItemId(), userId)) {// 2.1.存在,則更新數量baseMapper.updateNum(cartFormDTO.getItemId(), userId);return;}// 2.2.不存在,判斷是否超過購物車數量checkCartsFull(userId);// 3.新增購物車條目// 3.1.轉換POCart cart = BeanUtils.copyBean(cartFormDTO, Cart.class);// 3.2.保存當前用戶cart.setUserId(userId);// 3.3.保存到數據庫save(cart);}@Overridepublic List<CartVO> queryMyCarts() {// 1.查詢我的購物車列表List<Cart> carts = lambdaQuery().eq(Cart::getUserId, 1L /*TODO UserContext.getUser()*/).list();if (CollUtils.isEmpty(carts)) {return CollUtils.emptyList();}// 2.轉換VOList<CartVO> vos = BeanUtils.copyList(carts, CartVO.class);// 3.處理VO中的商品信息handleCartItems(vos);// 4.返回return vos;}private void handleCartItems(List<CartVO> vos) {// 1.獲取商品id TODO 處理商品信息/*Set<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());// 2.查詢商品List<ItemDTO> items = itemService.queryItemByIds(itemIds);if (CollUtils.isEmpty(items)) {throw new BadRequestException("購物車中商品不存在!");}// 3.轉為 id 到 item的mapMap<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));// 4.寫入vofor (CartVO v : vos) {ItemDTO item = itemMap.get(v.getItemId());if (item == null) {continue;}v.setNewPrice(item.getPrice());v.setStatus(item.getStatus());v.setStock(item.getStock());}*/}@Overridepublic void removeByItemIds(Collection<Long> itemIds) {// 1.構建刪除條件,userId和itemIdQueryWrapper<Cart> queryWrapper = new QueryWrapper<Cart>();queryWrapper.lambda().eq(Cart::getUserId, UserContext.getUser()).in(Cart::getItemId, itemIds);// 2.刪除remove(queryWrapper);}private void checkCartsFull(Long userId) {int count = lambdaQuery().eq(Cart::getUserId, userId).count();if (count >= 10) {throw new BizIllegalException(StrUtil.format("用戶購物車課程不能超過{}", 10));}}private boolean checkItemExists(Long itemId, Long userId) {int count = lambdaQuery().eq(Cart::getUserId, userId).eq(Cart::getItemId, itemId).count();return count > 0;}
}
最后,還是要導入數據庫表,在本地數據庫直接執行課前資料對應的SQL文件:
在數據庫中會出現名為hm-cart
的database
,以及其中的cart
表,代表購物車:
接下來,就可以測試了。不過在啟動前,同樣要配置啟動項的active profile
為local
:
然后啟動CartApplication
,訪問swagger文檔頁面:http://localhost:8082/doc.html
我們測試其中的查詢我的購物車列表
接口:
無需填寫參數,直接訪問:
我們注意到,其中與商品有關的幾個字段值都為空!這就是因為剛才我們注釋掉了查詢購物車時,查詢商品信息的相關代碼。
那么,我們該如何在cart-service
服務中實現對item-service
服務的查詢呢?
2.4.服務調用
在拆分的時候,我們發現一個問題:就是購物車業務中需要查詢商品信息,但商品信息查詢的邏輯全部遷移到了item-service
服務,導致我們無法查詢。
最終結果就是查詢到的購物車數據不完整,因此要想解決這個問題,我們就必須改造其中的代碼,把原本本地方法調用,改造成跨微服務的遠程調用(RPC,即Remote Produce Call)。
因此,現在查詢購物車列表的流程變成了這樣:
暫時無法在飛書文檔外展示此內容
代碼中需要變化的就是這一步:
那么問題來了:我們該如何跨服務調用,準確的說,如何在cart-service
中獲取item-service
服務中的提供的商品數據呢?
大家思考一下,我們以前有沒有實現過類似的遠程查詢的功能呢?
答案是肯定的,我們前端向服務端查詢數據,其實就是從瀏覽器遠程查詢服務端數據。比如我們剛才通過Swagger測試商品查詢接口,就是向http://localhost:8081/items
這個接口發起的請求:
而這種查詢就是通過http請求的方式來完成的,不僅僅可以實現遠程查詢,還可以實現新增、刪除等各種遠程請求。
假如我們在cart-service中能模擬瀏覽器,發送http請求到item-service,是不是就實現了跨微服務的遠程調用了呢?
那么:我們該如何用Java代碼發送Http的請求呢?
2.4.1.RestTemplate
Spring給我們提供了一個RestTemplate的API,可以方便的實現Http請求的發送。
org.springframework.web.client public class RestTemplate
extends InterceptingHttpAccessor
implements RestOperations
----------------------------------------------------------------------------------------------------------------
同步客戶端執行HTTP請求,在底層HTTP客戶端庫(如JDK HttpURLConnection、Apache HttpComponents等)上公開一個簡單的模板方法API。RestTemplate通過HTTP方法為常見場景提供了模板,此外還提供了支持不太常見情況的通用交換和執行方法。 RestTemplate通常用作共享組件。然而,它的配置不支持并發修改,因此它的配置通常是在啟動時準備的。如果需要,您可以在啟動時創建多個不同配置的RestTemplate實例。如果這些實例需要共享HTTP客戶端資源,它們可以使用相同的底層ClientHttpRequestFactory。 注意:從5.0開始,這個類處于維護模式,只有對更改和錯誤的小請求才會被接受。請考慮使用org.springframework.web.react .client. webclient,它有更現代的API,支持同步、異步和流場景。
----------------------------------------------------------------------------------------------------------------
自: 3.0 參見: HttpMessageConverter, RequestCallback, ResponseExtractor, ResponseErrorHandler
其中提供了大量的方法,方便我們發送Http請求,例如:
可以看到常見的Get、Post、Put、Delete請求都支持,如果請求參數比較復雜,還可以使用exchange方法來構造請求。
我們在cart-service
服務中定義一個配置類:
先將RestTemplate注冊為一個Bean:
package com.hmall.cart.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;@Configuration
public class RemoteCallConfig {@Beanpublic RestTemplate restTemplate() {return new RestTemplate();}
}
2.4.2.遠程調用
接下來,我們修改cart-service
中的com.hmall.cart.service.impl.
CartServiceImpl
的handleCartItems
方法,發送http請求到item-service
:
可以看到,利用RestTemplate發送http請求與前端ajax發送請求非常相似,都包含四部分信息:
- ① 請求方式
- ② 請求路徑
- ③ 請求參數
- ④ 返回值類型
handleCartItems
方法的完整代碼如下:
private void handleCartItems(List<CartVO> vos) {// TODO 1.獲取商品idSet<Long> itemIds = vos.stream().map(CartVO::getItemId).collect(Collectors.toSet());// 2.查詢商品// List<ItemDTO> items = itemService.queryItemByIds(itemIds);// 2.1.利用RestTemplate發起http請求,得到http的響應ResponseEntity<List<ItemDTO>> response = restTemplate.exchange("http://localhost:8081/items?ids={ids}",HttpMethod.GET,null,new ParameterizedTypeReference<List<ItemDTO>>() {},Map.of("ids", CollUtil.join(itemIds, ",")));// 2.2.解析響應if(!response.getStatusCode().is2xxSuccessful()){// 查詢失敗,直接結束return;}List<ItemDTO> items = response.getBody();if (CollUtils.isEmpty(items)) {return;}// 3.轉為 id 到 item的mapMap<Long, ItemDTO> itemMap = items.stream().collect(Collectors.toMap(ItemDTO::getId, Function.identity()));// 4.寫入vofor (CartVO v : vos) {ItemDTO item = itemMap.get(v.getItemId());if (item == null) {continue;}v.setNewPrice(item.getPrice());v.setStatus(item.getStatus());v.setStock(item.getStock());}
}
好了,現在重啟cart-service
,再次測試查詢我的購物車列表接口:
可以發現,所有商品相關數據都已經查詢到了。
在這個過程中,item-service
提供了查詢接口,cart-service
利用Http請求調用該接口。因此item-service
可以稱為服務的提供者,而cart-service
則稱為服務的消費者或服務調用者。
2.5.總結
什么時候需要拆分微服務?
- 如果是創業型公司,最好先用單體架構快速迭代開發,驗證市場運作模型,快速試錯。當業務跑通以后,隨著業務規模擴大、人員規模增加,再考慮拆分微服務。
- 如果是大型企業,有充足的資源,可以在項目開始之初就搭建微服務架構。
如何拆分?
- 首先要做到高內聚、低耦合
- 從拆分方式來說,有橫向拆分和縱向拆分兩種。縱向就是按照業務功能模塊,橫向則是拆分通用性業務,提高復用性
服務拆分之后,不可避免的會出現跨微服務的業務,此時微服務之間就需要進行遠程調用。微服務之間的遠程調用被稱為RPC,即遠程過程調用。RPC的實現方式有很多,比如:
- 基于Http協議
- 基于Dubbo協議
我們課堂中使用的是Http方式,這種方式不關心服務提供者的具體技術實現,只要對外暴露Http接口即可,更符合微服務的需要。
Java發送http請求可以使用Spring提供的RestTemplate,使用的基本步驟如下:
- 注冊RestTemplate到Spring容器
- 調用RestTemplate的API發送請求,常見方法有:
-
- getForObject:發送Get請求并返回指定類型對象
- PostForObject:發送Post請求并返回指定類型對象
- put:發送PUT請求
- delete:發送Delete請求
- exchange:發送任意類型請求,返回ResponseEntity
3.服務注冊和發現
在上一章我們實現了微服務拆分,并且通過Http請求實現了跨微服務的遠程調用。不過這種手動發送Http請求的方式存在一些問題。
試想一下,假如商品微服務被調用較多,為了應對更高的并發,我們進行了多實例部署,如圖:
暫時無法在飛書文檔外展示此內容
此時,每個item-service
的實例其IP或端口不同,問題來了:
- item-service這么多實例,cart-service如何知道每一個實例的地址?
- http請求要寫url地址,
cart-service
服務到底該調用哪個實例呢? - 如果在運行過程中,某一個
item-service
實例宕機,cart-service
依然在調用該怎么辦? - 如果并發太高,
item-service
臨時多部署了N臺實例,cart-service
如何知道新實例的地址?
為了解決上述問題,就必須引入注冊中心的概念了,接下來我們就一起來分析下注冊中心的原理。
3.1.注冊中心原理
在微服務遠程調用的過程中,包括兩個角色:
- 服務提供者:提供接口供其它微服務訪問,比如
item-service
- 服務消費者:調用其它微服務提供的接口,比如
cart-service
在大型微服務項目中,服務提供者的數量會非常多,為了管理這些服務就引入了注冊中心的概念。注冊中心、服務提供者、服務消費者三者間關系如下:
流程如下:
- 服務啟動時就會注冊自己的服務信息(服務名、IP、端口)到注冊中心
- 調用者可以從注冊中心訂閱想要的服務,獲取服務對應的實例列表(1個服務可能多實例部署)
- 調用者自己對實例列表負載均衡,挑選一個實例
- 調用者向該實例發起遠程調用
當服務提供者的實例宕機或者啟動新實例時,調用者如何得知呢?
- 服務提供者會定期向注冊中心發送請求,報告自己的健康狀態(心跳請求)
- 當注冊中心長時間收不到提供者的心跳時,會認為該實例宕機,將其從服務的實例列表中剔除
- 當服務有新實例啟動時,會發送注冊服務請求,其信息會被記錄在注冊中心的服務實例列表
- 當注冊中心服務列表變更時,會主動通知微服務,更新本地服務列表
3.2.Nacos注冊中心
目前開源的注冊中心框架有很多,國內比較常見的有:
- Eureka:Netflix公司出品,目前被集成在SpringCloud當中,一般用于Java應用
- Nacos:Alibaba公司出品,目前被集成在SpringCloudAlibaba中,一般用于Java應用
- Consul:HashiCorp公司出品,目前集成在SpringCloud中,不限制微服務語言
以上幾種注冊中心都遵循SpringCloud中的API規范,因此在業務開發使用上沒有太大差異。由于Nacos是國內產品,中文文檔比較豐富,而且同時具備配置管理功能(后面會學習),因此在國內使用較多,課堂中我們會Nacos為例來學習。
官方網站如下:
暫時無法在飛書文檔外展示此內容
我們基于Docker來部署Nacos的注冊中心,首先我們要準備MySQL數據庫表,用來存儲Nacos的數據。由于是Docker部署,所以大家需要將資料中的SQL文件導入到你Docker中的MySQL容器中:
最終表結構如下:
然后,找到課前資料下的nacos文件夾:
其中的nacos/custom.env
文件中,有一個MYSQL_SERVICE_HOST也就是mysql地址,需要修改為你自己的虛擬機IP地址:
然后,將課前資料中的nacos
目錄上傳至虛擬機的/root
目錄。
進入root目錄,然后執行下面的docker命令:
docker run -d \
--name nacos \
--env-file ./nacos/custom.env \
-p 8848:8848 \
-p 9848:9848 \
-p 9849:9849 \
--restart=always \
nacos/nacos-server:v2.1.0-slim
啟動完成后,訪問下面地址:http://192.168.150.101:8848/nacos/,注意將192.168.150.101
替換為你自己的虛擬機IP地址。
首次訪問會跳轉到登錄頁,賬號密碼都是nacos
3.3.服務注冊
接下來,我們把item-service
注冊到Nacos,步驟如下:
- 引入依賴
- 配置Nacos地址
- 重啟
3.3.1.添加依賴
在item-service
的pom.xml
中添加依賴:
<!--nacos 服務注冊發現-->
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
3.3.2.配置Nacos
在item-service
的application.yml
中添加nacos地址配置:
spring:application:name: item-service # 服務名稱cloud:nacos:server-addr: 192.168.150.101:8848 # nacos地址
3.3.3.啟動服務實例
為了測試一個服務多個實例的情況,我們再配置一個item-service
的部署實例:
然后配置啟動項,注意重命名并且配置新的端口,避免沖突:
重啟item-service
的兩個實例:
訪問nacos控制臺,可以發現服務注冊成功:
點擊詳情,可以查看到item-service
服務的兩個實例信息:
3.4.服務發現
服務的消費者要去nacos訂閱服務,這個過程就是服務發現,步驟如下:
- 引入依賴
- 配置Nacos地址
- 發現并調用服務
3.4.1.引入依賴
服務發現除了要引入nacos依賴以外,由于還需要負載均衡,因此要引入SpringCloud提供的LoadBalancer依賴。
我們在cart-service
中的pom.xml
中添加下面的依賴:
<!--nacos 服務注冊發現-->
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
可以發現,這里Nacos的依賴于服務注冊時一致,這個依賴中同時包含了服務注冊和發現的功能。因為任何一個微服務都可以調用別人,也可以被別人調用,即可以是調用者,也可以是提供者。
因此,等一會兒cart-service
啟動,同樣會注冊到Nacos
3.4.2.配置Nacos地址
在cart-service
的application.yml
中添加nacos地址配置:
spring:cloud:nacos:server-addr: 192.168.150.101:8848
3.4.3.發現并調用服務
接下來,服務調用者cart-service
就可以去訂閱item-service
服務了。不過item-service有多個實例,而真正發起調用時只需要知道一個實例的地址。
因此,服務調用者必須利用負載均衡的算法,從多個實例中挑選一個去訪問。常見的負載均衡算法有:
- 隨機
- 輪詢
- IP的hash
- 最近最少訪問
- ...
這里我們可以選擇最簡單的隨機負載均衡。
另外,服務發現需要用到一個工具,DiscoveryClient,SpringCloud已經幫我們自動裝配,我們可以直接注入使用:
接下來,我們就可以對原來的遠程調用做修改了,之前調用時我們需要寫死服務提供者的IP和端口:
但現在不需要了,我們通過DiscoveryClient發現服務實例列表,然后通過負載均衡算法,選擇一個實例去調用:
經過swagger測試,發現沒有任何問題。
4.OpenFeign
在上一章,我們利用Nacos實現了服務的治理,利用RestTemplate實現了服務的遠程調用。但是遠程調用的代碼太復雜了:
而且這種調用方式,與原本的本地方法調用差異太大,編程時的體驗也不統一,一會兒遠程調用,一會兒本地調用。
因此,我們必須想辦法改變遠程調用的開發模式,讓遠程調用像本地方法調用一樣簡單。而這就要用到OpenFeign組件了。
其實遠程調用的關鍵點就在于四個:
- 請求方式
- 請求路徑
- 請求參數
- 返回值類型
所以,OpenFeign就利用SpringMVC的相關注解來聲明上述4個參數,然后基于動態代理幫我們生成遠程調用的代碼,而無需我們手動再編寫,非常方便。
接下來,我們就通過一個快速入門的案例來體驗一下OpenFeign的便捷吧。
4.1.快速入門
我們還是以cart-service中的查詢我的購物車為例。因此下面的操作都是在cart-service中進行。
4.1.1.引入依賴
在cart-service
服務的pom.xml中引入OpenFeign
的依賴和loadBalancer
依賴:
<!--openFeign--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><!--負載均衡器--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency>
4.1.2.啟用OpenFeign
接下來,我們在cart-service
的CartApplication
啟動類上添加注解,啟動OpenFeign功能:
4.1.3.編寫OpenFeign客戶端
在cart-service
中,定義一個新的接口,編寫Feign客戶端:
其中代碼如下:
package com.hmall.cart.client;import com.hmall.cart.domain.dto.ItemDTO;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;import java.util.List;@FeignClient("item-service")
public interface ItemClient {@GetMapping("/items")List<ItemDTO> queryItemByIds(@RequestParam("ids") Collection<Long> ids);
}
這里只需要聲明接口,無需實現方法。接口中的幾個關鍵信息:
@FeignClient("item-service")
:聲明服務名稱@GetMapping
:聲明請求方式@GetMapping("/items")
:聲明請求路徑@RequestParam("ids") Collection<Long> ids
:聲明請求參數List<ItemDTO>
:返回值類型
有了上述信息,OpenFeign就可以利用動態代理幫我們實現這個方法,并且向http://item-service/items
發送一個GET
請求,攜帶ids為請求參數,并自動將返回值處理為List<ItemDTO>
。
我們只需要直接調用這個方法,即可實現遠程調用了。
4.1.4.使用FeignClient
最后,我們在cart-service
的com.hmall.cart.service.impl.CartServiceImpl
中改造代碼,直接調用ItemClient
的方法:
feign替我們完成了服務拉取、負載均衡、發送http請求的所有工作,是不是看起來優雅多了。
而且,這里我們不再需要RestTemplate了,還省去了RestTemplate的注冊。
4.2.連接池
Feign底層發起http請求,依賴于其它的框架。其底層支持的http客戶端實現包括:
- HttpURLConnection:默認實現,不支持連接池
- Apache HttpClient :支持連接池
- OKHttp:支持連接池
因此我們通常會使用帶有連接池的客戶端來代替默認的HttpURLConnection。比如,我們使用OK Http.
4.2.1.引入依賴
在cart-service
的pom.xml
中引入依賴:
<!--OK http 的依賴 -->
<dependency><groupId>io.github.openfeign</groupId><artifactId>feign-okhttp</artifactId>
</dependency>
4.2.2.開啟連接池
在cart-service
的application.yml
配置文件中開啟Feign的連接池功能:
feign:okhttp:enabled: true # 開啟OKHttp功能
重啟服務,連接池就生效了。
4.2.3.驗證
我們可以打斷點驗證連接池是否生效,在org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient
中的execute
方法中打斷點:
Debug方式啟動cart-service,請求一次查詢我的購物車方法,進入斷點:
可以發現這里底層的實現已經改為OkHttpClient
4.3.最佳實踐
將來我們要把與下單有關的業務抽取為一個獨立微服務:trade-service
,不過我們先來看一下hm-service
中原本與下單有關的業務邏輯。
入口在com.hmall.controller.OrderController
的createOrder
方法,然后調用了IOrderService
中的createOrder
方法。
由于下單時前端提交了商品id,為了計算訂單總價,需要查詢商品信息:
也就是說,如果拆分了交易微服務(trade-service
),它也需要遠程調用item-service
中的根據id批量查詢商品功能。這個需求與cart-service
中是一樣的。
因此,我們就需要在trade-service
中再次定義ItemClient
接口,這不是重復編碼嗎? 有什么辦法能加避免重復編碼呢?
4.3.1.思路分析
相信大家都能想到,避免重復編碼的辦法就是抽取。不過這里有兩種抽取思路:
- 思路1:抽取到微服務之外的公共module
- 思路2:每個微服務自己抽取一個module
如圖:
方案1抽取更加簡單,工程結構也比較清晰,但缺點是整個項目耦合度偏高。
方案2抽取相對麻煩,工程結構相對更復雜,但服務之間耦合度降低。
由于item-service已經創建好,無法繼續拆分,因此這里我們采用方案1.
4.3.2.抽取Feign客戶端
在hmall
下定義一個新的module,命名為hm-api
其依賴如下:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"><parent><artifactId>hmall</artifactId><groupId>com.heima</groupId><version>1.0.0</version></parent><modelVersion>4.0.0</modelVersion><artifactId>hm-api</artifactId><properties><maven.compiler.source>11</maven.compiler.source><maven.compiler.target>11</maven.compiler.target></properties><dependencies><!--open feign--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency><!-- load balancer--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</artifactId></dependency><!-- swagger 注解依賴 --><dependency><groupId>io.swagger</groupId><artifactId>swagger-annotations</artifactId><version>1.6.6</version><scope>compile</scope></dependency></dependencies>
</project>
然后把ItemDTO和ItemClient都拷貝過來,最終結構如下:
現在,任何微服務要調用item-service
中的接口,只需要引入hm-api
模塊依賴即可,無需自己編寫Feign客戶端了。
4.3.3.掃描包
接下來,我們在cart-service
的pom.xml
中引入hm-api
模塊:
<!--feign模塊--><dependency><groupId>com.heima</groupId><artifactId>hm-api</artifactId><version>1.0.0</version></dependency>
刪除cart-service
中原來的ItemDTO和ItemClient,重啟項目,發現報錯了:
這里因為ItemClient
現在定義到了com.hmall.api.client
包下,而cart-service的啟動類定義在com.hmall.cart
包下,掃描不到ItemClient
,所以報錯了。
解決辦法很簡單,在cart-service的啟動類上添加聲明即可,兩種方式:
- 方式1:聲明掃描包:
- 方式2:聲明要用的FeignClient
4.4.日志配置
OpenFeign只會在FeignClient所在包的日志級別為DEBUG時,才會輸出日志。而且其日志級別有4級:
- NONE:不記錄任何日志信息,這是默認值。
- BASIC:僅記錄請求的方法,URL以及響應狀態碼和執行時間
- HEADERS:在BASIC的基礎上,額外記錄了請求和響應的頭信息
- FULL:記錄所有請求和響應的明細,包括頭信息、請求體、元數據。
Feign默認的日志級別就是NONE,所以默認我們看不到請求日志。
4.4.1.定義日志級別
在hm-api模塊下新建一個配置類,定義Feign的日志級別:
代碼如下:
package com.hmall.api.config;import feign.Logger;
import org.springframework.context.annotation.Bean;public class DefaultFeignConfig {@Beanpublic Logger.Level feignLogLevel(){return Logger.Level.FULL;}
}
4.4.2.配置
接下來,要讓日志級別生效,還需要配置這個類。有兩種方式:
- 局部生效:在某個
FeignClient
中配置,只對當前FeignClient
生效
@FeignClient(value = "item-service", configuration = DefaultFeignConfig.class)
- 全局生效:在
@EnableFeignClients
中配置,針對所有FeignClient
生效。
@EnableFeignClients(defaultConfiguration = DefaultFeignConfig.class)
日志格式:
17:35:32:148 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] ---> GET http://item-service/items?ids=100000006163 HTTP/1.1
17:35:32:148 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] ---> END HTTP (0-byte body)
17:35:32:278 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] <--- HTTP/1.1 200 (127ms)
17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] connection: keep-alive
17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] content-type: application/json
17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] date: Fri, 26 May 2023 09:35:32 GMT
17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] keep-alive: timeout=60
17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] transfer-encoding: chunked
17:35:32:279 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds]
17:35:32:280 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] [{"id":100000006163,"name":"巴布豆(BOBDOG)柔薄悅動嬰兒拉拉褲XXL碼80片(15kg以上)","price":67100,"stock":10000,"image":"https://m.360buyimg.com/mobilecms/s720x720_jfs/t23998/350/2363990466/222391/a6e9581d/5b7cba5bN0c18fb4f.jpg!q70.jpg.webp","category":"拉拉褲","brand":"巴布豆","spec":"{}","sold":11,"commentCount":33343434,"isAD":false,"status":2}]
17:35:32:281 DEBUG 18620 --- [nio-8082-exec-1] com.hmall.api.client.ItemClient : [ItemClient#queryItemByIds] <--- END HTTP (369-byte body)
5.作業
5.1.拆分微服務
將hm-service中的其它業務也都拆分為微服務,包括:
- user-service:用戶微服務,包含用戶登錄、管理等功能
- trade-service:交易微服務,包含訂單相關功能
- pay-service:支付微服務,包含支付相關功能
其中交易服務、支付服務、用戶服務中的業務都需要知道當前登錄用戶是誰,目前暫未實現,先將用戶id寫死。
思考:如何才能在每個微服務中都拿到用戶信息?如何在微服務之間傳遞用戶信息?
5.2.定義FeignClient
在上述業務中,包含大量的微服務調用,將被調用的接口全部定義為FeignClient,將其與對應的DTO放在hm-api模塊
5.3.將微服務與前端聯調
課前資料提供了一個hmall-nginx
目錄,其中包含了Nginx以及我們的前端代碼:
將其拷貝到一個不包含中文、空格、特殊字符的目錄,啟動后即可訪問到頁面:
- 18080是用戶端頁面
- 18081是管理端頁面
之前nginx
內部會將發向服務端請求全部代理到8080端口,但是現在拆分了N個微服務,8080不可用了。請通過Nginx
配置,完成對不同微服務的反向代理。
認真思考這種方式存在哪些問題,有什么好的解決方案?