怎么把網(wǎng)站上傳到域名seo全網(wǎng)圖文推廣
目錄
1.網(wǎng)關(guān)路由轉(zhuǎn)發(fā)
1.1 網(wǎng)關(guān)
1.2 快速入門
1.2.1 創(chuàng)建項目
1.2.2 引入依賴
1.2.3 啟動類
1.2.4 配置路由
1.2.5 測試
1.3 路由過濾
2.網(wǎng)關(guān)登錄校驗
2.1 鑒權(quán)思路分析
2.2 網(wǎng)關(guān)過濾器
2.3 自定義過濾器
2.3.1 自定義GatewayFilter
2.3.2 自定義GlobalFilter
2.4 登錄校驗
2.4.1 JWT工具
2.4.2 登錄校驗過濾器
2.5 網(wǎng)關(guān)傳遞用戶數(shù)據(jù)
2.5.1 保存用戶到請求頭
2.5.2 攔截器獲取用戶
2.5.3 恢復購物車代碼
2.5.4 測試
2.6 OpenFeign實現(xiàn)微服務傳遞用戶數(shù)據(jù)
2.6.1 實現(xiàn)
2.6.2 測試
3.配置管理
3.1 配置共享
3.1.1.添加共享配置
3.1.2 拉取共享配置
3.2 配置熱更新
3.2.1 添加配置到Nacos
3.2.2 配置熱更新(讀取配置)
3.3 動態(tài)路由
3.3.1 監(jiān)聽Nacos配置變更
3.3.2 更新路由
3.3.3 實現(xiàn)動態(tài)路由
前置:
將黑馬商城拆分為5個微服務:
-
用戶服務
-
商品服務
-
購物車服務
-
交易服務
-
支付服務
問題:
每個微服務都有不同的地址或端口,入口不同,相信大家在與前端聯(lián)調(diào)的時候發(fā)現(xiàn)了一些問題:
-
后續(xù)部署在Linux,微服務的地址以及端口可能隨時會改變
-
前端無法調(diào)用nacos,無法實時更新服務列表
思維導圖:
傳遞用戶信息
重點:
1、利用網(wǎng)關(guān)是網(wǎng)絡(luò)的入口,實現(xiàn)登錄校驗,簡化了微服務重復出現(xiàn)的校驗邏輯。
2、微服務只需編寫攔截器,攔截網(wǎng)關(guān)的請求,獲取請求頭的用戶數(shù)據(jù),并保存到ThreadLocal。因為每一個微服務都要攔截器,因此把攔截器定義在common包下。
3、OpenFeign中的攔截器可以實現(xiàn)微服務傳遞用戶數(shù)據(jù),調(diào)用到OpenFeign服務就會被攔截,并保存用戶數(shù)據(jù)到請求頭,在發(fā)送請求時攜帶。其他微服務的攔截器會攔截這次請求,取出用戶數(shù)據(jù),保存到ThreadLocal。
1.網(wǎng)關(guān)路由轉(zhuǎn)發(fā)
1.1 網(wǎng)關(guān)
什么是網(wǎng)關(guān)?
網(wǎng)關(guān)就是網(wǎng)絡(luò)的關(guān)口。數(shù)據(jù)在網(wǎng)絡(luò)間傳輸,從一個網(wǎng)絡(luò)傳輸?shù)搅硪痪W(wǎng)絡(luò)時就需要經(jīng)過網(wǎng)關(guān)來做數(shù)據(jù)的路由和轉(zhuǎn)發(fā)以及數(shù)據(jù)安全的校驗。
前端請求不能直接訪問微服務,而是要請求網(wǎng)關(guān):
-
網(wǎng)關(guān)可以做安全控制,也就是登錄身份校驗,校驗通過才放行
-
通過認證后,網(wǎng)關(guān)再根據(jù)請求判斷應該訪問哪個微服務,將請求轉(zhuǎn)發(fā)到對應的微服務
注意事項:網(wǎng)關(guān)其實本身也是一個微服務,可以去注冊中心拉取服務,進行路由轉(zhuǎn)發(fā)以及負載均衡處理。
在SpringCloud當中,提供了兩種網(wǎng)關(guān)實現(xiàn)方案:
-
Netflix Zuul:早期實現(xiàn),目前已經(jīng)淘汰
-
SpringCloudGateway:基于Spring的WebFlux技術(shù),完全支持響應式編程,吞吐能力更強
1.2 快速入門
接下來,如何利用網(wǎng)關(guān)實現(xiàn)請求路由。由于網(wǎng)關(guān)本身也是一個獨立的微服務,因此也需要創(chuàng)建一個模塊開發(fā)功能。大概步驟如下:
-
創(chuàng)建網(wǎng)關(guān)微服務(獨立的模塊)
-
引入SpringCloudGateway、NacosDiscovery依賴
-
編寫啟動類
-
配置網(wǎng)關(guān)路由
1.2.1 創(chuàng)建項目
首先,我們要在hmall下創(chuàng)建一個新的module,命名為hm-gateway,作為網(wǎng)關(guān)微服務:
1.2.2 引入依賴
在hm-gateway
模塊的pom.xml
文件中引入依賴:
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 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-gateway</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><!--網(wǎng)關(guān)--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-gateway</artifactId></dependency><!--nacos discovery--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId></dependency><!--負載均衡--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-loadbalancer</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>
1.2.3 啟動類
在hm-gateway
模塊的com.hmall.gateway
包下新建一個啟動類:
代碼如下:
@SpringBootApplication
public class GatewayApplication {public static void main(String[] args) {SpringApplication.run(GatewayApplication.class, args);}
}
1.2.4 配置路由
接下來,在hm-gateway
模塊的resources
目錄新建一個application.yaml
文件,內(nèi)容如下:
server:port: 8080
spring:application:name: gatewaycloud:nacos:server-addr: 192.168.150.101:8848gateway:routes:- id: item # 路由規(guī)則id,自定義,唯一,一般用服務名稱uri: lb://item-service # 路由的目標服務,lb代表負載均衡,會從注冊中心拉取服務列表predicates: # 路由斷言,判斷當前請求是否符合當前規(guī)則,符合則路由到目標服務- Path=/items/**,/search/** # 這里是以請求路徑作為判斷規(guī)則- id: carturi: lb://cart-servicepredicates:- Path=/carts/**- id: useruri: lb://user-servicepredicates:- Path=/users/**,/addresses/**- id: tradeuri: lb://trade-servicepredicates:- Path=/orders/**- id: payuri: lb://pay-servicepredicates:- Path=/pay-orders/**
注意事項:
1、網(wǎng)關(guān)服務的端口一般都是8080,因為它是入口。
2、后續(xù)我們不用訪問微服務地址,只需要訪問8080端口對應的服務,網(wǎng)關(guān)會進行路由轉(zhuǎn)發(fā)到對應的微服務。
訪問nacos注冊中心:
http://192.168.85.144:8848/nacos
1.2.5 測試
啟動ItemApplication,然后啟動GatewayApplication,以 http://localhost:8080 拼接微服務接口路徑來測試。例如:
http://localhost:8080/items/page?pageNo=1&pageSize=1
此時,啟動UserApplication、CartApplication,然后打開前端頁面,發(fā)現(xiàn)相關(guān)功能都可以正常訪問了。
1.3 路由過濾
路由規(guī)則的定義語法如下:
spring:cloud:gateway:routes:- id: itemuri: lb://item-servicepredicates:- Path=/items/**,/search/**
其中routes對應的類型如下:
是一個集合,也就是說可以定義很多路由規(guī)則。
集合中的RouteDefinition
就是具體的路由規(guī)則定義,其中常見的屬性如下:
四個屬性含義如下:
-
id
:路由的唯一標示 -
predicates
:路由斷言,其實就是匹配條件。滿足條件才會到路由目標地址。 -
filters
:路由過濾條件 -
uri
:路由目標地址,lb://
代表負載均衡,從注冊中心獲取目標微服務的實例列表,并且負載均衡選擇一個訪問。
這里重點關(guān)注predicates
,也就是路由斷言。SpringCloudGateway中支持的斷言類型有很多:
名稱 | 說明 | 示例 |
---|---|---|
After | 是某個時間點后的請求 | - After=2037-01-20T17:42:47.789-07:00[America/Denver] |
Before | 是某個時間點之前的請求 | - Before=2031-04-13T15:14:47.433+08:00[Asia/Shanghai] |
Between | 是某兩個時間點之前的請求 | - Between=2037-01-20T17:42:47.789-07:00[America/Denver], 2037-01-21T17:42:47.789-07:00[America/Denver] |
Cookie | 請求必須包含某些cookie | - Cookie=chocolate, ch.p |
Header | 請求必須包含某些header | - Header=X-Request-Id, \d+ |
Host | 請求必須是訪問某個host(域名) | - Host=**.somehost.org,**.anotherhost.org |
Method | 請求方式必須是指定方式 | - Method=GET,POST |
Path | 請求路徑必須符合指定規(guī)則 | - Path=/red/{segment},/blue/** |
Query | 請求參數(shù)必須包含指定參數(shù) | - Query=name, Jack或者- Query=name |
RemoteAddr | 請求者的ip必須是指定范圍 | - RemoteAddr=192.168.1.1/24 |
weight | 權(quán)重處理 |
2.網(wǎng)關(guān)登錄校驗
單體架構(gòu)時我們只需要完成一次用戶登錄、身份校驗,就可以在所有業(yè)務中獲取到用戶信息。而微服務拆分后,每個微服務都獨立部署,不再共享數(shù)據(jù)。也就意味著每個微服務都需要做登錄校驗,這顯然不可取。
2.1 鑒權(quán)思路分析
既然網(wǎng)關(guān)是所有微服務的入口,一切請求都需要先經(jīng)過網(wǎng)關(guān)。我們完全可以把登錄校驗的工作放到網(wǎng)關(guān)去做,這樣之前說的問題就解決了:
-
只需要在網(wǎng)關(guān)保存秘鑰
-
只需要在網(wǎng)關(guān)開發(fā)登錄校驗功能
此時,登錄校驗的流程如圖:
不過,這里存在幾個問題:
-
網(wǎng)關(guān)路由是配置的,請求轉(zhuǎn)發(fā)是Gateway內(nèi)部代碼,我們?nèi)绾卧谵D(zhuǎn)發(fā)之前做登錄校驗?
-
自定義網(wǎng)關(guān)過濾器(校驗用戶信息),并且在NettyRoutingFilter(請求轉(zhuǎn)發(fā)到微服務)過濾器前執(zhí)行
-
-
網(wǎng)關(guān)校驗JWT之后,如何將用戶信息傳遞給微服務?
-
通過手段讓網(wǎng)關(guān)發(fā)請求時攜帶用戶數(shù)據(jù)到請求頭。微服務定義攔截器來攔截請求,取出用戶數(shù)據(jù),保存到ThreadLocal。
-
-
微服務之間也會相互調(diào)用,這種調(diào)用不經(jīng)過網(wǎng)關(guān),又該如何傳遞用戶信息?
-
OpenFeign發(fā)起的請求自動攜帶登錄用戶信息,傳遞數(shù)據(jù)給其他微服務。由于其他微服務有定義攔截器,就會去請求頭取出用戶數(shù)據(jù)。
-
2.2 網(wǎng)關(guān)過濾器
而網(wǎng)關(guān)的請求轉(zhuǎn)發(fā)是Gateway
內(nèi)部代碼實現(xiàn)的,要想在請求轉(zhuǎn)發(fā)之前做登錄校驗,就必須了解Gateway
內(nèi)部工作的基本原理。
如圖所示:
-
客戶端請求進入網(wǎng)關(guān)后由
HandlerMapping
對請求做判斷,找到與當前請求匹配的路由規(guī)則(Route
),然后將請求交給WebHandler
去處理。 -
WebHandler
則會加載當前路由下需要執(zhí)行的過濾器鏈(Filter chain
),然后按照順序逐一執(zhí)行過濾器(后面稱為Filter
)。 -
圖中
Filter
被虛線分為左右兩部分,是因為Filter
內(nèi)部的邏輯分為pre
和post
兩部分,分別會在請求路由到微服務之前和之后被執(zhí)行。 -
只有所有
Filter
的pre
邏輯都依次順序執(zhí)行通過后,請求才會被路由到微服務。 -
微服務返回結(jié)果后,再倒序執(zhí)行
Filter
的post
邏輯。 -
最終把響應結(jié)果返回。
如圖中所示,最終請求轉(zhuǎn)發(fā)是有一個名為NettyRoutingFilter
的過濾器來執(zhí)行的,而且這個過濾器是整個過濾器鏈中順序最靠后的一個。如果我們能夠定義一個過濾器,在其中實現(xiàn)登錄校驗邏輯,并且將過濾器執(zhí)行順序定義到NettyRoutingFilter
之前,這就符合我們的需求了!
該如何實現(xiàn)一個網(wǎng)關(guān)過濾器呢?
網(wǎng)關(guān)過濾器鏈中的過濾器有兩種:
-
GatewayFilter
:路由過濾器,作用范圍比較靈活,可以是任意指定的路由Route。
-
GlobalFilter
:全局過濾器,作用范圍是所有路由,不可配置。
其實GatewayFilter
和GlobalFilter
這兩種過濾器的方法簽名完全一致:
/*** 處理請求并將其傳遞給下一個過濾器* @param exchange 當前請求的上下文,其中包含request、response等各種數(shù)據(jù)* @param chain 過濾器鏈,基于它向下傳遞請求* @return 根據(jù)返回值標記當前請求是否被完成或攔截,chain.filter(exchange)就放行了。*/
Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
FilteringWebHandler
在處理請求時,會將GlobalFilter
裝飾為GatewayFilter
,然后放到同一個過濾器鏈中,排序以后依次執(zhí)行。
Gateway
內(nèi)置的GatewayFilter
過濾器使用起來非常簡單,無需編碼,只要在yaml文件中簡單配置即可。而且其作用范圍也很靈活,配置在哪個Route
下,就作用于哪個Route。
例如,有一個過濾器叫做AddRequestHeaderGatewayFilterFacotry
,顧明思議,就是添加請求頭的過濾器,可以給請求添加一個請求頭并傳遞到下游微服務。
使用的使用只需要在application.yaml中這樣配置:
spring:cloud:gateway:routes:- id: test_routeuri: lb://test-servicepredicates:-Path=/test/**filters:- AddRequestHeader=key, value # 逗號之前是請求頭的key,逗號之后是value
如果想要讓過濾器作用于所有的路由,則可以這樣配置(跟routes配置同級):
spring:cloud:gateway:default-filters: # default-filters下的過濾器可以作用于所有路由- AddRequestHeader=key, valueroutes:- id: test_routeuri: lb://test-servicepredicates:-Path=/test/**
2.3 自定義過濾器
無論是GatewayFilter
還是GlobalFilter
都支持自定義,只不過編碼方式、使用方式略有差別。
2.3.1 自定義GatewayFilter
自定義GatewayFilter
不是直接實現(xiàn)GatewayFilter
,而是實現(xiàn)AbstractGatewayFilterFactory
。最簡單的方式是這樣的:
@Component
public class PrintAnyGatewayFilterFactory extends AbstractGatewayFilterFactory<Object> {@Overridepublic GatewayFilter apply(Object config) {return new GatewayFilter() {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 獲取請求ServerHttpRequest request = exchange.getRequest();// 編寫過濾器邏輯System.out.println("過濾器執(zhí)行了");// 放行return chain.filter(exchange);}};}
}
注意事項:該類的名稱一定要以GatewayFilterFactory
為后綴!
2.3.2 自定義GlobalFilter
自定義GlobalFilter則簡單很多,直接實現(xiàn)GlobalFilter即可,而且也無法設(shè)置動態(tài)參數(shù):
@Component
public class PrintAnyGlobalFilter implements GlobalFilter, Ordered {@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 編寫過濾器邏輯System.out.println("未登錄,無法訪問");// 放行// return chain.filter(exchange);// 攔截ServerHttpResponse response = exchange.getResponse();response.setRawStatusCode(401);return response.setComplete();}@Overridepublic int getOrder() {// 過濾器執(zhí)行順序,值越小,優(yōu)先級越高return 0;}
}
注意事項:getOrder方法返回值只需要比int最大值小,就可以實現(xiàn)在NettyRoutingFilter過濾器前執(zhí)行。
2.4 登錄校驗
接下來,我們就利用自定義GlobalFilter
來完成登錄校驗。
2.4.1 JWT工具
登錄校驗需要用到JWT,而且JWT的加密需要秘鑰和加密工具。這些在hm-service
中已經(jīng)有了,我們直接拷貝過來:
具體作用如下:
-
AuthProperties
:配置登錄校驗需要攔截的路徑,因為不是所有的路徑都需要登錄才能訪問 -
JwtProperties
:定義與JWT工具有關(guān)的屬性,比如秘鑰文件位置 -
SecurityConfig
:工具的自動裝配 -
JwtTool
:JWT工具,其中包含了校驗和解析token
的功能 -
hmall.jks
:秘鑰文件
其中AuthProperties
和JwtProperties
所需的屬性要在application.yaml
中配置:
hm:jwt:location: classpath:hmall.jks # 秘鑰地址alias: hmall # 秘鑰別名password: hmall123 # 秘鑰文件密碼tokenTTL: 30m # 登錄有效期auth:excludePaths: # 無需登錄校驗的路徑- /search/**- /users/login- /items/**
2.4.2 登錄校驗過濾器
接下來,我們定義一個登錄校驗的過濾器:
代碼如下:
package com.hmall.gateway.filter;import java.util.List;@Component
@RequiredArgsConstructor
@EnableConfigurationProperties(AuthProperties.class)
public class AuthGlobalFilter implements GlobalFilter, Ordered {private final JwtTool jwtTool;private final AuthProperties authProperties;private final AntPathMatcher antPathMatcher = new AntPathMatcher();@Overridepublic Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {// 1.獲取RequestServerHttpRequest request = exchange.getRequest();// 2.判斷是否不需要攔截if(isExclude(request.getPath().toString())){// 無需攔截,直接放行return chain.filter(exchange);}// 3.獲取請求頭中的tokenString token = null;List<String> headers = request.getHeaders().get("authorization");if (!CollUtils.isEmpty(headers)) {token = headers.get(0);}// 4.校驗并解析tokenLong userId = null;try {userId = jwtTool.parseToken(token);} catch (UnauthorizedException e) {// 如果無效,攔截ServerHttpResponse response = exchange.getResponse();response.setRawStatusCode(401);return response.setComplete();}// TODO 5.如果有效,傳遞用戶信息System.out.println("userId = " + userId);// 6.放行return chain.filter(exchange);}private boolean isExclude(String antPath) {for (String pathPattern : authProperties.getExcludePaths()) {if(antPathMatcher.match(pathPattern, antPath)){return true;}}return false;}@Overridepublic int getOrder() {return 0;}
}
登錄完成,訪問其他接口,打印用戶信息:
注意事項:
1、該過濾器還需要實現(xiàn)Ordered接口,并且重寫getOrder方法,并且返回一個整數(shù)類型。
這個值越小,過濾器的優(yōu)先級越高。
2、getOrder方法返回值只需要比int最大值小,就可以實現(xiàn)在NettyRoutingFilter過濾器前執(zhí)行。
重啟測試,會發(fā)現(xiàn)訪問/items開頭的路徑,未登錄狀態(tài)下不會被攔截:
訪問其他路徑則,比如:http://localhost:8080/carts,未登錄狀態(tài)下請求會被攔截,并且返回401
狀態(tài)碼:
2.5 網(wǎng)關(guān)傳遞用戶數(shù)據(jù)
現(xiàn)在,網(wǎng)關(guān)已經(jīng)可以完成登錄校驗并獲取登錄用戶身份信息。但是當網(wǎng)關(guān)將請求轉(zhuǎn)發(fā)到微服務時,微服務又該如何獲取用戶身份呢?
由于網(wǎng)關(guān)發(fā)送請求到微服務依然采用的是Http
請求,因此我們可以將用戶信息以請求頭的方式傳遞到下游微服務。然后微服務可以從請求頭中獲取登錄用戶信息。考慮到微服務內(nèi)部可能很多地方都需要用到登錄用戶信息,因此我們可以利用SpringMVC的攔截器來實現(xiàn)登錄用戶信息獲取,并存入ThreadLocal,方便后續(xù)使用。
據(jù)圖流程圖如下:
因此,接下來我們要做的事情有:
-
改造網(wǎng)關(guān)過濾器,在獲取用戶信息后保存到請求頭,轉(zhuǎn)發(fā)到下游微服務
-
編寫微服務攔截器,攔截請求獲取用戶信息,保存到ThreadLocal后放行
2.5.1 保存用戶到請求頭
首先,我們修改登錄校驗攔截器的處理邏輯,保存用戶信息到請求頭中:
注意事項:網(wǎng)關(guān)向微服務發(fā)送數(shù)據(jù)也是通過HTTP請求,只不過底層幫我們自動轉(zhuǎn)發(fā)請求。
2.5.2 攔截器獲取用戶
在hm-common中已經(jīng)有一個用于保存登錄用戶的ThreadLocal工具:
其中已經(jīng)提供了保存和獲取用戶的方法:
接下來,只需要編寫攔截器,獲取用戶信息并保存到UserContext
,然后放行即可。
由于每個微服務都有獲取登錄用戶的需求,因此攔截器我們直接寫在hm-common
中,并寫好自動裝配。這樣微服務只需要引入hm-common
就可以直接具備攔截器功能,無需重復編寫。
我們在hm-common
模塊下定義一個攔截器:
具體代碼如下:
package com.hmall.common.interceptor;import cn.hutool.core.util.StrUtil;
import com.hmall.common.utils.UserContext;
import org.springframework.web.servlet.HandlerInterceptor;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;public class UserInfoInterceptor implements HandlerInterceptor {@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {// 1.獲取請求頭中的用戶信息String userInfo = request.getHeader("user-info");// 2.判斷是否為空if (StrUtil.isNotBlank(userInfo)) {// 不為空,保存到ThreadLocalUserContext.setUser(Long.valueOf(userInfo));}// 3.放行return true;}@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 移除用戶UserContext.removeUser();}
}
注意事項:該攔截器作用是獲取請求頭的用戶信息,然后存入ThreadLocal中,并不攔截請求。
接著在hm-common
模塊下編寫SpringMVC
的配置類,配置登錄攔截器:
具體代碼如下:
package com.hmall.common.config;import com.hmall.common.interceptors.UserInfoInterceptor;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.DispatcherServlet;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
//當類路徑下存在DispatcherServlet.class這個類時,被標注的配置或組件才會被加載。
//當@ConditionalOnClass(DispatcherServlet.class)出現(xiàn)時,通常意味著這個配置或組件是與 Spring MVC 相關(guān)的。
//網(wǎng)關(guān)微服務不會注冊該組件,其他微服務會被注冊
@ConditionalOnClass(DispatcherServlet.class)
public class MvcConfig implements WebMvcConfigurer {@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(new UserInfoInterceptor());}
}
注意事項:
1、這個配置類默認是不會生效的,因為它所在的包是com.hmall.common.config
,與其它微服務的掃描包不一致,無法被掃描到,因此無法生效。
2、基于SpringBoot的自動裝配原理,我們要將其添加到resources
目錄下的META-INF/spring.factories
文件中:
內(nèi)容如下:
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\com.hmall.common.config.MyBatisConfig,\com.hmall.common.config.MvcConfig
實現(xiàn)自動裝配之后,后續(xù)其他服務引入該模塊就可以直接使用。
2.5.3 恢復購物車代碼
之前我們無法獲取登錄用戶,所以把購物車服務的登錄用戶寫死了,現(xiàn)在需要恢復到原來的樣子。
找到cart-service
模塊的com.hmall.cart.service.impl.CartServiceImpl
:
修改其中的queryMyCarts
方法:
2.5.4 測試
訪問:http://localhost:18080/cart.html,可以看到根據(jù)用戶查詢購物車信息功能實現(xiàn)。
2.6 OpenFeign實現(xiàn)微服務傳遞用戶數(shù)據(jù)
2.6.1 實現(xiàn)
前端發(fā)起的請求都會經(jīng)過網(wǎng)關(guān)再到微服務,由于我們之前編寫的過濾器和攔截器功能,微服務可以輕松獲取登錄用戶信息。
應用場景:但有些業(yè)務是比較復雜的,請求到達微服務后還需要調(diào)用其它多個微服務。
比如下單業(yè)務,流程如下:
下單的過程中,需要調(diào)用商品服務扣減庫存,調(diào)用購物車服務清理用戶購物車。而清理購物車時必須知道當前登錄的用戶身份。但是,訂單服務調(diào)用購物車時并沒有傳遞用戶信息,購物車服務無法知道當前用戶是誰!
由于微服務獲取用戶信息是通過攔截器在請求頭中讀取,因此要想實現(xiàn)微服務之間的用戶信息傳遞,就必須在微服務發(fā)起調(diào)用時把用戶信息存入請求頭。
微服務之間調(diào)用是基于OpenFeign來實現(xiàn)的,并不是我們自己發(fā)送的請求。
那如何才能讓每一個由OpenFeign發(fā)起的請求自動攜帶登錄用戶信息呢?
這里要借助Feign中提供的一個攔截器接口:feign.RequestInterceptor??
??
????
public interface RequestInterceptor {/*** Called for every request. * Add data using methods on the supplied {@link RequestTemplate}.*/void apply(RequestTemplate template);
}
只需要實現(xiàn)這個接口,然后實現(xiàn)apply方法,利用RequestTemplate
類來添加請求頭,將用戶信息保存到請求頭中。這樣以來,每次通過OpenFeign發(fā)起請求的時候都會調(diào)用該方法,傳遞用戶信息。
由于FeignClient
全部都是在hm-api
模塊,因此我們在hm-api
模塊的com.hmall.api.config.DefaultFeignConfig
中編寫這個攔截器:
在com.hmall.api.config.DefaultFeignConfig
中添加一個Bean:
@Bean
public RequestInterceptor userInfoRequestInterceptor(){return new RequestInterceptor() {@Overridepublic void apply(RequestTemplate template) {// 獲取登錄用戶Long userId = UserContext.getUser();if(userId == null) {// 如果為空則直接跳過return;}// 如果不為空則放入請求頭中,傳遞給下游微服務template.header("user-info", userId.toString());}};
}
注意事項:這個攔截器的配置類需要在服務發(fā)現(xiàn)的啟動類中聲明。
@EnableFeignClients(basePackages = "com.hmall.api.client",defaultConfiguration = DefaultFeignConfig.class)
好了,現(xiàn)在微服務之間通過OpenFeign調(diào)用時也會傳遞登錄用戶信息了。
2.6.2 測試
點擊提交訂單:
1、創(chuàng)建訂單表以及詳情表
2、清理用戶購物車數(shù)據(jù)
3、扣減商品庫存
TradeApplication:
CartApplication:
清理用戶購物車數(shù)據(jù)
ItemApplication:
注意事項:
這里需要加載OpenFeign的攔截器,不然攔截器不生效,不能進行微服務之間的用戶信息傳遞。
3.配置管理
微服務共享的配置可以統(tǒng)一交給Nacos保存和管理,在Nacos控制臺修改配置后,Nacos會將配置變更推送給相關(guān)的微服務,并且無需重啟即可生效,實現(xiàn)配置熱更新。
到目前為止我們已經(jīng)解決了微服務相關(guān)的幾個問題:
-
微服務遠程調(diào)用(實現(xiàn)微服務之間的通信)
-
微服務注冊、發(fā)現(xiàn)(微服務的集群注冊)
-
微服務請求路由、負載均衡(OpenFeign動態(tài)代理機制)
-
微服務登錄用戶信息傳遞(瀏覽器 -> 網(wǎng)關(guān) -> 微服務 -> 微服務與微服務)
不過,現(xiàn)在依然還有幾個問題需要解決:
-
網(wǎng)關(guān)路由在配置文件中寫死了,如果變更必須重啟微服務
-
某些業(yè)務配置在配置文件中寫死了,每次修改都要重啟服務
-
每個微服務都有很多重復的配置,維護成本高(像配置MYSQL,Mybatis配置)
實現(xiàn)配置共享
這些問題都可以通過統(tǒng)一的配置管理器服務解決。
而Nacos不僅僅具備注冊中心功能,也具備配置管理的功能:
1、微服務共享的配置可以統(tǒng)一交給Nacos保存和管理,在Nacos控制臺修改配置后,Nacos會將配置變更推送給相關(guān)的微服務,并且無需重啟即可生效,實現(xiàn)配置熱更新。
2、網(wǎng)關(guān)的路由同樣是配置,因此同樣可以基于這個功能實現(xiàn)動態(tài)路由功能,無需重啟網(wǎng)關(guān)即可修改路由配置。
3.1 配置共享
我們可以把微服務共享的配置抽取到Nacos中統(tǒng)一管理,這樣就不需要每個微服務都重復配置了。分為兩步:
-
在Nacos中添加共享配置
-
微服務拉取配置
3.1.1.添加共享配置
以cart-service為例,我們看看有哪些配置是重復的,可以抽取的:
首先是jdbc相關(guān)配置:
然后是日志配置:
然后是swagger以及OpenFeign的配置:
我們在nacos控制臺分別添加這些配置。
首先是jdbc相關(guān)配置,在配置管理
->配置列表
中點擊+
新建一個配置:
在彈出的表單中填寫信息:
其中詳細的配置如下:
spring:datasource:url: jdbc:mysql://${hm.db.host:192.168.150.101}:${hm.db.port:3306}/${hm.db.database}?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghaidriver-class-name: com.mysql.cj.jdbc.Driverusername: ${hm.db.un:root}password: ${hm.db.pw:123}
mybatis-plus:configuration:default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandlerglobal-config:db-config:update-strategy: not_nullid-type: auto
注意這里的jdbc的相關(guān)參數(shù)并沒有寫死,例如:
-
數(shù)據(jù)庫ip
:通過${hm.db.host:192.168.150.101}
配置了默認值為192.168.150.101
,同時允許通過${hm.db.host}
來覆蓋默認值 -
數(shù)據(jù)庫端口
:通過${hm.db.port:3306}
配置了默認值為3306
,同時允許通過${hm.db.port}
來覆蓋默認值 -
數(shù)據(jù)庫database
:可以通過${hm.db.database}
來設(shè)定,無默認值
然后是統(tǒng)一的日志配置,命名為shared-log.
yaml
,配置內(nèi)容如下:
logging:level:com.hmall: debugpattern:dateformat: HH:mm:ss:SSSfile:path: "logs/${spring.application.name}"
然后是統(tǒng)一的swagger配置,命名為shared-swagger.yaml
,配置內(nèi)容如下:
knife4j:enable: trueopenapi:title: ${hm.swagger.title:黑馬商城接口文檔}description: ${hm.swagger.description:黑馬商城接口文檔}email: ${hm.swagger.email:zhanghuyi@itcast.cn}concat: ${hm.swagger.concat:虎哥}url: https://www.itcast.cnversion: v1.0.0group:default:group-name: defaultapi-rule: packageapi-rule-resources:- ${hm.swagger.package}
注意,這里的swagger相關(guān)配置我們沒有寫死,例如:
-
title
:接口文檔標題,我們用了${hm.swagger.title}
來代替,將來可以有用戶手動指定 -
email
:聯(lián)系人郵箱,我們用了${hm.swagger.email:
zhanghuyi@itcast.cn
}
,默認值是zhanghuyi@itcast.cn
,同時允許用戶利用${hm.swagger.email}
來覆蓋。
3.1.2 拉取共享配置
將拉取到的共享配置與本地的application.yaml
配置合并,完成項目上下文的初始化。
不過,需要注意的是,讀取Nacos配置是SpringCloud上下文(ApplicationContext
)初始化時處理的,發(fā)生在項目的引導階段。然后才會初始化SpringBoot上下文,去讀取application.yaml
。
問題:也就是說引導階段,application.yaml
文件尚未讀取,根本不知道nacos 地址,該如何去加載nacos中的配置文件呢?
答案:SpringCloud在初始化上下文的時候會先讀取一個名為bootstrap.yaml
(或者bootstrap.properties
)的文件,如果我們將nacos地址配置到bootstrap.yaml
中,那么在項目引導階段就可以讀取nacos中的配置了。
因此,微服務整合Nacos配置管理的步驟如下:
1)引入依賴:
在cart-service模塊引入依賴:
<!--nacos配置管理--><dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId></dependency><!--讀取bootstrap文件--><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-bootstrap</artifactId></dependency>
2)新建bootstrap.yaml
在cart-service中的resources目錄新建一個bootstrap.yaml文件:
內(nèi)容如下:
spring:application:name: cart-service # 服務名稱profiles:active: devcloud:nacos:server-addr: 192.168.150.101 # nacos地址config:file-extension: yaml # 文件后綴名shared-configs: # 共享配置- dataId: shared-jdbc.yaml # 共享mybatis配置- dataId: shared-log.yaml # 共享日志配置- dataId: shared-swagger.yaml # 共享日志配置
內(nèi)容包含:服務名稱,環(huán)境,nacos地址,以及共享的配置文件坐標。
3)修改application.yaml
由于一些配置挪到了bootstrap.yaml,因此application.yaml需要修改為:
server:port: 8082
feign:okhttp:enabled: true # 開啟OKHttp連接池支持
hm:swagger:title: 購物車服務接口文檔package: com.hmall.cart.controllerdb:database: hm-cart
重啟服務,發(fā)現(xiàn)所有配置都生效了。
測試效果:
CartApplication控制臺:
可以訪問swagger接口
拿得到用戶的購物車數(shù)據(jù):(數(shù)據(jù)庫起作用)
3.2 配置熱更新
有很多的業(yè)務相關(guān)參數(shù),將來可能會根據(jù)實際情況臨時調(diào)整。例如購物車業(yè)務,購物車數(shù)量有一個上限,默認是10,對應代碼如下:
現(xiàn)在這里購物車是寫死的固定值,我們應該將其配置在配置文件中,方便后期修改。
但現(xiàn)在的問題是,即便寫在配置文件中,修改了配置還是需要重新打包、重啟服務才能生效。能不能不用重啟,直接生效呢?
這就要用到Nacos的配置熱更新能力了,分為兩步:
-
在Nacos中添加配置
-
在微服務讀取配置
3.2.1 添加配置到Nacos
首先,我們在nacos中添加一個配置文件,將購物車的上限數(shù)量添加到配置中:
注意文件的dataId格式:
[服務名]-[spring.active.profile].[后綴名]
文件名稱由三部分組成:
-
服務名
:我們是購物車服務,所以是cart-service
-
spring.active.profile
:就是spring boot中的spring.active.profile
,可以省略,則所有profile共享該配置 -
后綴名
:例如yaml
這里我們直接使用cart-service.yaml
這個名稱,則不管是dev還是local環(huán)境都可以共享該配置。
配置內(nèi)容如下:
hm:cart:maxAmount: 1 # 購物車商品數(shù)量上限
提交配置,在控制臺能看到新添加的配置:
3.2.2 配置熱更新(讀取配置)
接著,我們在微服務中讀取配置,實現(xiàn)配置熱更新。
在cart-service
中新建一個屬性讀取類:
代碼如下:
@Data
@Component
@ConfigurationProperties(prefix = "hm.cart")
public class CartProperties {private Integer maxAmount;
}
接著,在業(yè)務中使用該屬性加載類:
測試,向購物車中添加多個商品:
我們在nacos控制臺,將購物車上限配置為5:
無需重啟,再次測試購物車功能:
加入成功!
無需重啟服務,配置熱更新就生效了!
3.3 動態(tài)路由
網(wǎng)關(guān)的路由配置全部是在項目啟動時由org.springframework.cloud.gateway.route.CompositeRouteDefinitionLocator
在項目啟動的時候加載,并且一經(jīng)加載就會緩存到內(nèi)存中的路由表內(nèi)(一個Map),不會改變。也不會監(jiān)聽路由變更,所以,我們無法利用配置熱更新來實現(xiàn)路由更新。
因此,我們必須監(jiān)聽Nacos的配置變更,然后手動把最新的路由更新到路由表中。這里有兩個難點:
-
如何監(jiān)聽Nacos配置變更?
-
如何把路由信息更新到路由表?
3.3.1 監(jiān)聽Nacos配置變更
在Nacos官網(wǎng)中給出了手動監(jiān)聽Nacos配置變更的SDK:
如果希望 Nacos 推送配置變更,可以使用 Nacos 動態(tài)監(jiān)聽配置接口來實現(xiàn)。
public void addListener(String dataId, String group, Listener listener)
請求參數(shù)說明:
參數(shù)名 | 參數(shù)類型 | 描述 |
---|---|---|
dataId | string | 配置 ID,保證全局唯一性,只允許英文字符和 4 種特殊字符("."、":"、"-"、"_")。不超過 256 字節(jié)。 |
group | string | 配置分組,一般是默認的DEFAULT_GROUP。 |
listener | Listener | 監(jiān)聽器,配置變更進入監(jiān)聽器的回調(diào)函數(shù)。 |
示例代碼:
String serverAddr = "{serverAddr}";
String dataId = "{dataId}";
String group = "{group}";
// 1.創(chuàng)建ConfigService,連接Nacos
Properties properties = new Properties();
properties.put("serverAddr", serverAddr);
ConfigService configService = NacosFactory.createConfigService(properties);
// 2.讀取配置
String content = configService.getConfig(dataId, group, 5000);
// 3.添加配置監(jiān)聽器
configService.addListener(dataId, group, new Listener() {@Overridepublic void receiveConfigInfo(String configInfo) {// 配置變更的通知處理System.out.println("recieve1:" + configInfo);}@Overridepublic Executor getExecutor() {return null;}
});
這里核心的步驟有2步:
-
創(chuàng)建ConfigService,目的是連接到Nacos
-
添加配置監(jiān)聽器,編寫配置變更的通知處理邏輯
第一步:
由于我們采用了spring-cloud-starter-alibaba-nacos-config
自動裝配,因此ConfigService
已經(jīng)在com.alibaba.cloud.nacos.NacosConfigAutoConfiguration
中自動創(chuàng)建好了:
NacosConfigManager中是負責管理Nacos的ConfigService的,具體代碼如下:
因此,只要我們拿到NacosConfigManager
就等于拿到了ConfigService
,第一步就實現(xiàn)了。
第二步:
編寫監(jiān)聽器。雖然官方提供的SDK是ConfigService中的addListener,不過項目第一次啟動時不僅僅需要添加監(jiān)聽器,也需要讀取配置,因此建議使用的API是這個:
String getConfigAndSignListener(String dataId, // 配置文件idString group, // 配置組,走默認long timeoutMs, // 讀取配置的超時時間Listener listener // 監(jiān)聽器
) throws NacosException;
既可以配置監(jiān)聽器,并且會根據(jù)dataId和group讀取配置并返回。我們就可以在項目啟動時先更新一次路由,后續(xù)隨著配置變更通知到監(jiān)聽器,完成路由更新。
3.3.2 更新路由
更新路由要用到org.springframework.cloud.gateway.route.RouteDefinitionWriter
這個接口:
package org.springframework.cloud.gateway.route;import reactor.core.publisher.Mono;/*** @author Spencer Gibb*/
public interface RouteDefinitionWriter {/*** 更新路由到路由表,如果路由id重復,則會覆蓋舊的路由*/Mono<Void> save(Mono<RouteDefinition> route);/*** 根據(jù)路由id刪除某個路由*/Mono<Void> delete(Mono<String> routeId);}
這里更新的路由,也就是RouteDefinition,之前我們見過,包含下列常見字段:
-
id:路由id
-
predicates:路由匹配規(guī)則
-
filters:路由過濾器
-
uri:路由目的地
將來我們保存到Nacos的配置也要符合這個對象結(jié)構(gòu),將來我們以JSON來保存,格式如下:
{"id": "item","predicates": [{"name": "Path","args": {"_genkey_0":"/items/**", "_genkey_1":"/search/**"}}],"filters": [],"uri": "lb://item-service"
}
注意事項:這里用JSON格式是方便后續(xù)類型的轉(zhuǎn)換。
以上JSON配置就等同于:
spring:cloud:gateway:routes:- id: itemuri: lb://item-servicepredicates:- Path=/items/**,/search/**
3.3.3 實現(xiàn)動態(tài)路由
首先, 我們在網(wǎng)關(guān)gateway引入依賴:
<!--統(tǒng)一配置管理-->
<dependency><groupId>com.alibaba.cloud</groupId><artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
<!--加載bootstrap-->
<dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-bootstrap</artifactId>
</dependency>
然后在網(wǎng)關(guān)gateway
的resources
目錄創(chuàng)建bootstrap.yaml
文件,內(nèi)容如下:
spring:application:name: gatewaycloud:nacos:server-addr: 192.168.150.101config:file-extension: yamlshared-configs:- dataId: shared-log.yaml # 共享日志配置
接著,修改gateway
的resources
目錄下的application.yml
,把之前的路由移除,最終內(nèi)容如下:
server:port: 8080 # 端口
hm:jwt:location: classpath:hmall.jks # 秘鑰地址alias: hmall # 秘鑰別名password: hmall123 # 秘鑰文件密碼tokenTTL: 30m # 登錄有效期auth:excludePaths: # 無需登錄校驗的路徑- /search/**- /users/login- /items/**
然后,在gateway
中定義配置監(jiān)聽器:
其代碼如下:
@Slf4j
@Component
@RequiredArgsConstructor
public class DynamicRouteLoader {private final RouteDefinitionWriter writer;private final NacosConfigManager nacosConfigManager;// 路由配置文件的id和分組private final String dataId = "gateway-routes.json";private final String group = "DEFAULT_GROUP";// 保存更新過的路由idprivate final Set<String> routeIds = new HashSet<>();@PostConstructpublic void initRouteConfigListener() throws NacosException {// 1.注冊監(jiān)聽器并首次拉取配置String configInfo = nacosConfigManager.getConfigService().getConfigAndSignListener(dataId, group, 5000, new Listener() {@Overridepublic Executor getExecutor() {return null;}@Overridepublic void receiveConfigInfo(String configInfo) {updateConfigInfo(configInfo);}});// 2.首次啟動時,更新一次配置updateConfigInfo(configInfo);}private void updateConfigInfo(String configInfo) {log.debug("監(jiān)聽到路由配置變更,{}", configInfo);// 1.反序列化List<RouteDefinition> routeDefinitions = JSONUtil.toList(configInfo, RouteDefinition.class);// 2.更新前先清空舊路由// 2.1.清除舊路由for (String routeId : routeIds) {writer.delete(Mono.just(routeId)).subscribe();}routeIds.clear();// 2.2.判斷是否有新的路由要更新if (CollUtils.isEmpty(routeDefinitions)) {// 無新路由配置,直接結(jié)束return;}// 3.更新路由routeDefinitions.forEach(routeDefinition -> {// 3.1.更新路由writer.save(Mono.just(routeDefinition)).subscribe();// 3.2.記錄路由id,方便將來刪除routeIds.add(routeDefinition.getId());});}
}
重啟網(wǎng)關(guān),任意訪問一個接口,比如 http://localhost:8080/search/list?pageNo=1&pageSize=1:
發(fā)現(xiàn)是404,無法訪問。
接下來,我們直接在Nacos控制臺添加路由,路由文件名為gateway-routes.json
,類型為json
:
配置內(nèi)容如下:
[{"id": "item","predicates": [{"name": "Path","args": {"_genkey_0":"/items/**", "_genkey_1":"/search/**"}}],"filters": [],"uri": "lb://item-service"},{"id": "cart","predicates": [{"name": "Path","args": {"_genkey_0":"/carts/**"}}],"filters": [],"uri": "lb://cart-service"},{"id": "user","predicates": [{"name": "Path","args": {"_genkey_0":"/users/**", "_genkey_1":"/addresses/**"}}],"filters": [],"uri": "lb://user-service"},{"id": "trade","predicates": [{"name": "Path","args": {"_genkey_0":"/orders/**"}}],"filters": [],"uri": "lb://trade-service"},{"id": "pay","predicates": [{"name": "Path","args": {"_genkey_0":"/pay-orders/**"}}],"filters": [],"uri": "lb://pay-service"}
]
無需重啟網(wǎng)關(guān),稍等幾秒鐘后,再次訪問剛才的地址:
網(wǎng)關(guān)路由成功了!