SpringCloud 文章推薦:Eureka:Spring Cloud服務註冊與發現組件(非常詳細) (biancheng.net) 概述 Spring Cloud 是一個服務治理平臺,是若幹個框架的集合,提供了全套的分散式系統解決方案。包含了:服務註冊與發現、配置中心、服務網關、智能路由、負載均 ...
SpringCloud
文章推薦:Eureka:Spring Cloud服務註冊與發現組件(非常詳細) (biancheng.net)
概述
Spring Cloud 是一個服務治理平臺,是若幹個框架的集合,提供了全套的分散式系統解決方案。包含了:服務註冊與發現、配置中心、服務網關、智能路由、負載均衡、斷路器、監控跟蹤、分散式消息隊列等等。
Spring Cloud 通過 Spring Boot 風格的封裝,屏蔽掉了複雜的配置和實現原理,最終給開發者留出了一套簡單易懂、容易部署的分散式系統開發工具包。開發者可以快速的啟動服務或構建應用、同時能夠快速和雲平臺資源進行對接。微服務是可以獨立部署、水平擴展、獨立訪問(或者有獨立的資料庫)的服務單元
,Spring Cloud 就是這些微服務的大管家,採用了微服務這種架構之後,項目的數量會非常多,Spring Cloud 做為大管家需要管理好這些微服務,自然需要很多小弟來幫忙。
SpringCloud常用組件表
服務的註冊和發現(eureka,nacos,consul)
服務的負載均衡(ribbon,dubbo)
服務的相互調用(openFeign,dubbo)
服務的容錯(hystrix,sentinel)
服務網關(gateway,zuul)
服務配置的統一管理(config-server,nacos,apollo)
服務消息匯流排(bus)
服務安全組件(security,Oauth2.0)
服務監控(admin)(jvm)
鏈路追綜(sleuth+zipkin)
SpringCloud Alibaba與SpringCloud Netflix對照
註冊和發現中心
Eureka快速入門
什麼是CAP原則?
Eureka和zookeeper的區別
CAP原則是指一個分散式系統中,一致性,可用性,分區容錯性
一致性:多個節點的數據保持一致。 (consistent)
可用性:當一個節點發生異常不可用之後,其他的節點任然可以提供服務。 available
分區容錯性:由於每個節點存在的機房或者是分區不一樣,存在數據傳輸時間的消耗,所以每個節點上的數據可能會短暫的不一致。 partition
CAP原則指的是,這三個要素最多只能同時存在實現兩個,不可能三者兼顧。但是每一個分散式系統中都會存在P原則,所以通常只會出現CP和AP組合。
Zookeeper:CP 註重的數據的一致性,當節點發生異常不可用時,可能會造成幾分鐘無法訪問
Eureka:AP 註重的時可可用,但是可能用戶訪問的數據存在一定的差異。
Eureka快速入門
搭建一個組測中心
創建Eureka-server
導入相關的依賴,註意SpringCloud有相應對應的版本。不要隨意的搭配boot和Cloud的版本
。
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>1.8</java.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
書寫application.yml配置文件
server:
port: 8761 #預設埠
spring:
application:
name: eureka-server #之前我們很少的指定過模塊的名稱,這裡通常指定。
在啟動類上開啟@EnableEurekaServer //開啟Euraka註冊中心的功能
訪問localhost:8761 表示Eureka-server註冊成功
創建Eureka客戶端
導入依賴
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.12.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<java.version>17</java.version>
<spring-cloud.version>Hoxton.SR12</spring-cloud.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring-cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
改寫配置文件:
server:
port: 8081
spring:
application:
name: eureka-client-a
#客戶端需要將自己的信息註冊(告訴)伺服器(server)
#發送到哪?
eureka:
client:
service-url: #指定註冊地址
defaultZone: http://localhost:8761/eureka
啟動類上開啟服務@EnableEurekaClient
,
如果我們想將同一臺實列註冊多次,只需要修改配置文件中埠就行了。
Eureka配置文件介紹
瞭解配置文件,我們需要知道Eureka-server需要做一些什麼?
作為註冊中心(房東)
- 需要有一個服務列表(容器)保存註冊的應用的信息。
- 需要知道自己的下麵註冊的應用是否還線上(心跳機制)
- 應用A和應用B需要聯繫,不能總是的通過Eureka-serve實現聯繫,需要在應用的本地保存一份註冊列表。這時需要考慮是否可以容忍臟讀。
- 如果某段時間內有大量的應用沒有聯繫自己,這時Eureka-server會認為自己出現了問題。體現Ap原則
Eureka-server配置文件實列
# eureka-server的配置文件主要分為三類: server類 client類 instance類
eureka:
server:
eviction-interval-timer-in-ms: 10000 #服務間隔多少時間之後進行一個刪除操作,當一個應用在該時間之內沒有與伺服器進行聯繫的話,伺服器會認為應用已經下線
renewal-percent-threshold: 0.85 #續約百分比,當超過規定範圍的應用沒有聯繫伺服器時,伺服器會認為自己出現了問題。
instance:
hostname: localhost
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port} #實列id
prefer-ip-address: true #以ip形式顯示具體的服務信息
lease-renewal-interval-in-seconds: 5 #指示 eureka 客戶端需要多久(以秒為單位)向 eureka 伺服器發送檢測信號,這個時間應該小於eviction-interval-timer-in-ms
Eureka-client配置文件
#客戶端需要將自己的信息註冊(告訴)伺服器(server)
#發送到哪?
eureka:
client:
service-url: #指定註冊地址
defaultZone: http://localhost:8761/eureka
register-with-eureka: true #是否註冊到註冊中心上去
fetch-registry: true #應用是否拉取服務列表到本地
registry-fetch-interval-seconds: 10 #間隔多少時間拉取列表到本地
instance:
lease-renewal-interval-in-seconds: 5
hostname: localhost
prefer-ip-address: true
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
分散式系統一致性演算法
在我們分散式系統中,存在多個系統之間實現數據的集群,採用CP一致性演算法保證每個節點數據的一致性的問題。比如Eureka、Zookeeper、Nacos實現集群都必須保證每個節點數據同步性的問題。
Zookeeper基於ZAP協議實現保證每個節點數據同步的問題,中心化思想集群模式。分為領導和跟隨者角色。(主從模式)
Eureka基於AP模式實現註冊中心,去中心化的思想、每個節點都是對等的,採用你中有我,我中有你的形式實現註冊中心。
常見分散式一致性演算法:
- ZAP協議(底層就是基於Paxos實現),核心底層基於2PC兩階段提交協議實現。
- Nacos中集群保證一致性演算法採ratf協議模式,採用心跳機制實現選舉的。Raft (thesecretlivesofdata.com)
- Eureka沒有分散式數據一致性的機制 節點都是相同的
Eureka運行的理解
Eureka服務註冊、下線、續約、剝離都是註冊列表的CRUD
Eureka註冊
Eureka客戶端的register源碼解釋:
boolean register() throws Throwable {
logger.info(PREFIX + "{}: registering service...", appPathIdentifier);
EurekaHttpResponse<Void> httpResponse;
try {
//註冊的主要實現調用,instanceInfo讀取配置文件配置項(ip,port,hostname)
httpResponse = eurekaTransport.registrationClient.register(instanceInfo);
} catch (Exception e) {
logger.warn(PREFIX + "{} - registration failed {}", appPathIdentifier, e.getMessage(), e);
throw e;
}
if (logger.isInfoEnabled()) {
logger.info(PREFIX + "{} - registration status: {}", appPathIdentifier, httpResponse.getStatusCode());
}
return httpResponse.getStatusCode() == Status.NO_CONTENT.getStatusCode();
}
eurekaTransport.registrationClient.register(instanceInfo);調用register解釋
@Override
public EurekaHttpResponse<Void> register(InstanceInfo info) {
String urlPath = "apps/" + info.getAppName();
ClientResponse response = null;
try {
Builder resourceBuilder = jerseyClient.resource(serviceUrl).path(urlPath).getRequestBuilder();
addExtraHeaders(resourceBuilder);
//發送post請求給urlPath地址,restFul風格post表示增加
response = resourceBuilder
.header("Accept-Encoding", "gzip")
.type(MediaType.APPLICATION_JSON_TYPE)
.accept(MediaType.APPLICATION_JSON)
.post(ClientResponse.class, info);
return anEurekaHttpResponse(response.getStatus()).headers(headersOf(response)).build();
} finally {
if (logger.isDebugEnabled()) {
logger.debug("Jersey HTTP POST {}/{} with instance {}; statusCode={}", serviceUrl, urlPath, info.getId(),
response == null ? "N/A" : response.getStatus());
}
if (response != null) {
response.close();
}
}
}
客服端發送post請求之後,那麼服務端是如何處理保存Client信息的
註冊表結構的第一個Key是應用名稱(全大寫) spring.application.name
Value中的key是應用的實例id Eureka.instance.instance-id
Value中的value是具體的服務節點信息
private final ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>> registry
= new ConcurrentHashMap<String, Map<String, Lease<InstanceInfo>>>();
public void register(InstanceInfo registrant, int leaseDuration, boolean isReplication) {
read.lock();
try {
Map<String, Lease<InstanceInfo>> gMap = registry.get(registrant.getAppName());
REGISTER.increment(isReplication);
if (gMap == null) {
final ConcurrentHashMap<String, Lease<InstanceInfo>> gNewMap = new ConcurrentHashMap<String, Lease<InstanceInfo>>();
gMap = registry.putIfAbsent(registrant.getAppName(), gNewMap);
if (gMap == null) {
gMap = gNewMap;
}
}
Lease<InstanceInfo> existingLease = gMap.get(registrant.getId());
……………………………………………………………………………………………………………………………………………………………………
} finally {
read.unlock();
}
}
Eureka續約
@Override
public EurekaHttpResponse<InstanceInfo> sendHeartBeat(String appName, String id, InstanceInfo info, InstanceStatus overriddenStatus) {
String urlPath = "apps/" + appName + '/' + id;
ClientResponse response = null;
try {
WebResource webResource = jerseyClient.resource(serviceUrl)
.path(urlPath)
.queryParam("status", info.getStatus().toString())
.queryParam("lastDirtyTimestamp", info.getLastDirtyTimestamp().toString());
if (overriddenStatus != null) {
webResource = webResource.queryParam("overriddenstatus", overriddenStatus.name());
}
Builder requestBuilder = webResource.getRequestBuilder();
addExtraHeaders(requestBuilder);
//發送put請求給伺服器端
response = requestBuilder.put(ClientResponse.class);
EurekaHttpResponseBuilder<InstanceInfo> eurekaResponseBuilder = anEurekaHttpResponse(response.getStatus(), InstanceInfo.class).headers(headersOf(response));
if (response.hasEntity() &&
!HTML.equals(response.getType().getSubtype())) { //don't try and deserialize random html errors from the server
eurekaResponseBuilder.entity(response.getEntity(InstanceInfo.class));
}
return eurekaResponseBuilder.build();
} finally {
if (logger.isDebugEnabled()) {
logger.debug("Jersey HTTP PUT {}/{}; statusCode={}", serviceUrl, urlPath, response == null ? "N/A" : response.getStatus());
}
if (response != null) {
response.close();
}
}
}
Eureka的剝除也是一樣發送相應的delete
請求給伺服器端。
服務發現
什麼是服務發現?
通過服務名稱獲取到服務具體實例的過程。
@GetMapping("/test")
public String doDiscovery(String serverName){
//通過服務名稱獲取相應的服務實例
//註意返回值是一個list集合類型,因為一個實例名稱可以開啟多個實列,實現一個集群的實現
List<ServiceInstance> instances = discoveryClient.getInstances(serverName);
instances.forEach(System.out::println);
ServiceInstance serviceInstance = instances.get(0);
String host = serviceInstance.getHost();
int port = serviceInstance.getPort();
String url="http://"+host+port;
//使用restTemplate發送Http請求給url就可以了
return serviceInstance.toString();
}
返回結果:從中我們可以通過服務名稱獲取到服務的埠和hostName,通過字元串的拼接就可以實現發送Http請求給相應的地址,只需知道相應API的功能就可以了。
[EurekaDiscoveryClient.EurekaServiceInstance@3aa089fe instance = InstanceInfo [instanceId = localhost:eureka-client-b:8082, appName = EUREKA-CLIENT-B, hostName = 192.168.117.1, status = UP, ipAddr = 192.168.117.1, port = 8082, securePort = 443, dataCenterInfo = com.netflix.appinfo.MyDataCenterInfo@276b4a2]
RestTemplate
RestTemplateApi介紹
void RestTemplateApi(){
String url="";
String data="";
//Entity 將會返回消息的完整消息(包括狀態碼,相應的結果)
//Object 只會返回相應的結果
restTemplate.getForEntity(url,String.class);
restTemplate.getForObject(url,String.class);
restTemplate.postForEntity(url,data,String.class);
restTemplate.postForObject(url,data,String.class);
restTemplate.put(url,String.class);
restTemplate.delete(url);
}
ribbon
負載均衡
在任何一個系統中,負載均衡都是一個十分重要且不得不去實施的內容,它是系統處理高併發、緩解網路壓力和服務端擴容的重要手段之一。
負載均衡(Load Balance) ,簡單點說就是將用戶的請求平攤分配到多個伺服器上運行,以達到擴展伺服器帶寬、增強數據處理能力、增加吞吐量、提高網路的可用性和靈活性的目的。
常見的負載均衡演算法:
- 輪詢分發
- 固定ip
- 隨機分發
- 權重分發
- Hash
常見的負載均衡方式有兩種:
- 服務端負載均衡
- 客戶端負載均衡
服務端負載均衡
服務端負載均衡是最常見的負載均衡方式,其工作原理如下圖。
客戶端負載均衡
相較於服務端負載均衡,客戶端服務在均衡則是一個比較小眾的概念。
客戶端負載均衡的工作原理如下圖。
客戶端負載均衡是將負載均衡邏輯以代碼的形式
封裝到客戶端上,即負載均衡器位於客戶端。客戶端通過服務註冊中心(例如 Eureka Server)獲取到一份服務端提供的可用服務清單。有了服務清單後,負載均衡器會在客戶端發送請求前通過負載均衡演算法選擇一個服務端實例再進行訪問,以達到負載均衡的目的;
客戶端負載均衡具有以下特點:
- 負載均衡器位於客戶端,不需要單獨搭建一個負載均衡伺服器。
- 負載均衡是在客戶端發送請求前進行的,因此客戶端清楚地知道是哪個服務端提供的服務。
- 客戶端都維護了一份可用服務清單,而這份清單都是從服務註冊中心獲取的。
Ribbon入門
<!--Spring Cloud Ribbon 依賴-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-ribbon</artifactId>
</dependency>
使用Ribbon+RestTemplate
@Bean //將 RestTemplate 註入到容器中
@LoadBalanced //在客戶端使用 RestTemplate 請求服務端時,開啟負載均衡(Ribbon)
public RestTemplate restTemplate() {
return new RestTemplate();
}
使用LoadBalancerClient
1.註入LoadBalancerClient
@Autowired
private LoadBalancerClient loadBalancerClient;
2.調用loadBalancerClient或者加了 @LoadBalanced
註解的RestTemplate
@GetMapping("/ribbon")
public URI ribbon(){
// 通過地址自動負載均衡
ServiceInstance choose = loadBalancerClient.choose("provider");
return choose.getUri();
}
在consumer發送發送請求給provider時只需要將原來的URL中的IP使用應用名稱來代替。
Ribbon為我們所做的事情
- 將我們的請求攔截。獲取當中的服務名稱。
- 通過服務名稱結合Eureka的服務發現,獲取到服務列表
- 通過服務列表使用負載均衡演算法,獲取相應的ip和port
- 重構URL
- 發送Http請求
Ribbon 實現負載均衡
Ribbon 是一個客戶端的負載均衡器,它可以與 Eureka 配合使用輕鬆地實現客戶端的負載均衡。Ribbon 會先從 Eureka Server(服務註冊中心)去獲取服務端列表,然後通過負載均衡策略將請求分攤給多個服務端,從而達到負載均衡的目的。
Spring Cloud Ribbon 提供了一個 IRule 介面,該介面主要用來定義負載均衡策略,它有 7 個預設實現類,每一個實現類都是一種負載均衡策略。
序號 | 實現類 | 負載均衡策略 |
---|---|---|
1 | RoundRobinRule | 按照線性輪詢策略,即按照一定的順序依次選取服務實例 |
2 | RandomRule | 隨機選取一個服務實例 |
3 | RetryRule | 按照 RoundRobinRule(輪詢)的策略來獲取服務,如果獲取的服務實例為 null 或已經失效,則在指定的時間之內不斷地進行重試(重試時獲取服務的策略還是 RoundRobinRule 中定義的策略),如果超過指定時間依然沒獲取到服務實例則返回 null 。 |
4 | WeightedResponseTimeRule | WeightedResponseTimeRule 是 RoundRobinRule 的一個子類,它對 RoundRobinRule 的功能進行了擴展。 根據平均響應時間,來計算所有服務實例的權重,響應時間越短的服務實例權重越高,被選中的概率越大。剛啟動時,如果統計信息不足,則使用線性輪詢策略,等信息足夠時,再切換到 WeightedResponseTimeRule。 |
5 | BestAvailableRule | 繼承自 ClientConfigEnabledRoundRobinRule。先過濾點故障或失效的服務實例,然後再選擇併發量最小的服務實例。 |
6 | AvailabilityFilteringRule | 先過濾掉故障或失效的服務實例,然後再選擇併發量較小的服務實例。 |
7 | ZoneAvoidanceRule | 預設的負載均衡策略 ,綜合判斷服務所在區域(zone)的性能和服務(server)的可用性,來選擇服務實例。在沒有區域的環境下,該策略與輪詢(RandomRule)策略類似。 |
探究一些ZoneAvoidanceRule,通過調用父類PredicateBasedRule的Choose函數,服務數量進行一個取模運算。
為了保持線程安全,Ribbon使用CAS,和原子類保證了線程的安全。
private int incrementAndGetModulo(int modulo) {
for (;;) {
int current = nextIndex.get();
int next = (current + 1) % modulo;
if (nextIndex.compareAndSet(current, next) && current < modulo)
return current;
}
}
更改負載均衡演算法
指定不同的服務使用不同的負載均衡演算法
#訪問不同的服務可以使用不同的演算法規則
provider: #先寫服務提供者的應用名稱
rabbion:
NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule
更改全局的負載均衡演算法
@Bean
public IRule myRule(){
return new RandomRule();
}
由於IRule是一個介面,所以我可以通過實現這個介面,自定義自己的負載均衡演算法。
OpenFeign:Spring Cloud聲明式服務調用組件
OpenFeign是一個Web聲明式的Http客戶端調用工具,提供介面和註解形式調用
OpenFeign是一個聲明式RESTful網路請求客戶端。OpenFeign會根據帶有註解的函數信息構建出網路請求的模板,在發送網路請求之前,OpenFeign會將函數的參數值設置到這些請求模板中。雖然OpenFeign只能支持基於文本的網路請求,但是它可以極大簡化網路請求的實現,方便編程人員快速構建自己的網路請求應用
OpenFeign 常用註解
使用 OpenFegin 進行遠程服務調用時,常用註解如下表。
註解 | 說明 |
---|---|
@FeignClient | 該註解用於通知 OpenFeign 組件對 @RequestMapping 註解下的介面進行解析,並通過動態代理的方式產生實現類,實現負載均衡和服務調用。 |
@EnableFeignClients | 該註解用於開啟 OpenFeign 功能,當 Spring Cloud 應用啟動時,OpenFeign 會掃描標有 @FeignClient 註解的介面,生成代理並註冊到 Spring 容器中。 |
@RequestMapping | Spring MVC 註解,在 Spring MVC 中使用該註解映射請求,通過它來指定控制器(Controller)可以處理哪些 URL 請求,相當於 Servlet 中 web.xml 的配置。 |
@GetMapping | Spring MVC 註解,用來映射 GET 請求,它是一個組合註解,相當於 @RequestMapping(method = RequestMethod.GET) 。 |
@PostMapping | Spring MVC 註解,用來映射 POST 請求,它是一個組合註解,相當於 @RequestMapping(method = RequestMethod.POST) 。 |
OpenFeign的入門
<!--添加 OpenFeign 依賴-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
開啟OpenFeign服務@EnableFeignClients
簡單應用
使用OpenFeign,UserServer遠程調用OrderServer中DoOrder介面
OrderServer
@RestController
public class OrderController {
@GetMapping("/DoOrder")
public String DoOrder(){
return "用戶下了訂單";
}
}
在UserServer中需要定義一個介面
@FeignClient(value = "eureka-client-a") //服務名稱
@Component
public interface UserOrderFeign {
//需要遠程調用服務方法的方法簽名
@GetMapping("/DoOrder")
public String DoOrder();
}
@RestController
public class UserDoOrder {
@Autowired
private UserOrderFeign userOrderFeign;
@GetMapping("/UserDoOrder")
public String UserDoOrder(){
return userOrderFeign.DoOrder();
}
}
註意點:如果在我們請求訂單模塊時,訂單模塊需要對資料庫進行操作,可能會比較的消耗時間。那麼我們的UserOrder會不會出現超時異常,通過實驗,結果是UserOrder會拋出一個超時異常。
修改OpenFeign遠程調用的超時時間
由於OpenFeign的原理是將Ribbon進行一個封裝,所以如果我們希望修改OpenFeign的超時時間的話,其實質是修改Ribbon的配置。預設超時時間預設是1S
ribbon:
ReadTimeout: 3000 #訪問超時時間
ConnectTimeout: 3000 #連接超時時間
OpenFeign核心探索
OPenFeign使用一個註解就可以實現遠程調用是如何做到
可以猜測這個介面一個會議代理對象,我們知道只有兩種代理的方式(JDK動態代理,cglib動態代理)
JDK動態代理為介面創建代理實例
,
CGLIB通過繼承方式實現代理
所以OPenFeign一定是採用的是JDK動態代理生成代理對象的
@Test
void contextLoads() {
UserOrderFeign o = (UserOrderFeign)Proxy.newProxyInstance(EurekaClientBApplication.class.getClassLoader(), new Class[]{UserOrderFeign.class}, new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
/*
* 通過代理獲取到方法註解上的API名稱
* 通過獲取介面上方的FeignClient()中的Value,應用的名稱,結合上Eureka的方法發現
* 通過拼接URL,使用Ribbon向URL介面發送請求就可以實現遠程調用
* */
//獲取API
GetMapping annotation = method.getAnnotation(GetMapping.class);
String[] Api = annotation.value();
String API = Api[0];
//獲取應用名稱
Class<?> aClass = method.getDeclaringClass();
FeignClient feignClient = aClass.getAnnotation(FeignClient.class);
String appName = feignClient.value();
//拼接URL
String url="http://"+appName+"/"+API;
//使用Ribbon發送請求實現負載均衡
String forObject = restTemplate.getForObject(url, String.class);
return forObject;
}
});
//使用JDK動態代理所以一定會調用invoke方法,類似與AOP機制
String s = o.DoOrder();
System.out.println(s);
}
OpenFeign 日誌增強
Logger.Level 的具體級別如下:
- NONE:不記錄任何信息。
- BASIC:僅記錄請求方法、URL 以及響應狀態碼和執行時間。
- HEADERS:除了記錄 BASIC 級別的信息外,還會記錄請求和響應的頭信息。
- FULL:記錄所有請求與響應的明細,包括頭信息、請求體、元數據等等。
/**
* Controls the level of logging.
*/
public enum Level {
/**
* No logging.
*/
NONE,
/**
* Log only the request method and URL and the response status code and execution time.
*/
BASIC,
/**
* Log the basic information along with request and response headers.
*/
HEADERS,
/**
* Log the headers, body, and metadata for both requests and responses.
*/
FULL
}
在配置文件中開啟介面的日誌級別
logging:
level:
#feign 日誌以什麼樣的級別監控該介面
net.biancheng.c.service.DeptFeignService: debug
@Bean
public Logger.Level level(){
return Logger.Level.FULL;
}
Hystrix:Spring Cloud服務熔斷與降級組件
服務雪崩
在一個微服務系統中,我們的一個服務可能是一個鏈式調用的,A->B->C如果C發生了宕機的話,那麼A,B中的線程只會在等待超時之後才會將線程回收,在併發量很大的時候,就會出現服務線程無法及時的回收,導致整個伺服器出現崩潰。
解決方案:
- 調整我們的超時時間,將超時時間縮短就可以做到及時的回收我們的線程。缺點如果伺服器中本身就存在服務耗時比較長,這種方式就會影響到我們正常的服務了。
- 如果我們上層的服務知道下層的服務已經發生了宕機的話,那麼我之後的請求就可以直接的返回,不需要等待超時時間。及時回收線程。
這種方式並不是保證服務的正常服務的,知道保證當存在一個服務宕機時,緩解伺服器壓力。
熔斷器
熔斷器(Circuit Breaker)一詞來源物理學中的電路知識,它的作用是當線路出現故障時,迅速切斷電源以保護電路的安全。
與物理學中的熔斷器作用相似,微服務架構中的熔斷器能夠在某個服務發生故障後,向服務調用方返回一個符合預期的、可處理的降級響應(FallBack),而不是長時間的等待或者拋出調用方無法處理的異常。這樣就保證了服務調用方的線程不會被長時間、不必要地占用,避免故障在微服務系統中的蔓延,防止系統雪崩效應的發生。
Hystrix的基本使用
導入依賴
<!--hystrix 依賴-->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>
書寫一個Hystrix實現類作為超時失敗後的備選方案
@Component
public class UserOrderHystrix implements UserOrderFeign {
@Override
public String DoOrder() {
return "我是服務超時時的備選方案";
}
}
當然只有經過了遠程調用的方法才會需要一個熔斷所以需要在Feign介面上面指定失敗的回調類是什麼
@FeignClient(value = "eureka-client-a",fallback = UserOrderHystrix.class) //服務名稱
public interface UserOrderFeign {
@GetMapping("/DoOrder")
public String DoOrder();
}
還需要開啟Hystrix服務
@EnableHystrix
feign:
hystrix:
enabled: true #在SpringCloud F版本之前是預設開啟的。
hystrix的配置文件
隔離級別預設是使用thread
thread消費者會為每個提供者分配好線程(預設是10個),
優點: 每個線程都有自己的線程組,高度隔離,互不影響
缺點:存線上程的切換,效率比較低
場景:併發量比較大的場景
Semaphore:
優點:不會存線上程的切換,效率比較高。
缺點:但是提供者之間存在影響。
併發量比較小,內部調用。
hystrix: #hystrix的全局控制
command:
default: #default是全局控制,也可以換成的單個方法控制,把default換成方法名
circuitBreaker:
enabled: true #開啟短路器
requestVolumeThreshold: 3 #失敗次數(閾值) 10次
sleepWindowInMilliseconds: 20000 #視窗時間
errorThresholdPercentage: 60 #失敗率
execution:
isolation:
Strategy: thread #隔離方式thread線程隔離集合和semaphore信號量隔離
thread:
timeoutInMilliseconds: 3000 #調用超時時長
fallback:
isolation:
semaphore:
maxConcurrentRequests: 1000 #信號量隔離級別最大併發數
上面是全部配置。
摸一個方法配置,在方法使用
@HystrixCommand(fallbackMethod = "deptCircuitBreaker_fallback", commandProperties = {
//以下參數在 HystrixCommandProperties 類中有預設配置
@HystrixProperty(name = "circuitBreaker.enabled", value = "true"), //是否開啟熔斷器
@HystrixProperty(name = "metrics.rollingStats.timeInMilliseconds",value = "1000"), //統計時間窗
@HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"), //統計時間窗內請求次數
@HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000"), //休眠時間視窗期
@HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "60"), //在統計時間視窗期以內,請求失敗率達到 60% 時進入熔斷狀態
})
Hystrix 服務熔斷
熔斷機制是為了應對雪崩效應而出現的一種微服務鏈路保護機制。
當微服務系統中的某個微服務不可用或響應時間太長時,為了保護系統的整體可用性,熔斷器會暫時切斷請求對該服務的調用,並快速返回一個友好的錯誤響應。這種熔斷狀態不是永久的,在經歷了一定的時間後,熔斷器會再次檢測該微服務是否恢復正常,若服務恢復正常則恢復其調用鏈路。
熔斷狀態
在熔斷機制中涉及了三種熔斷狀態:
- 熔斷關閉狀態(Closed):當服務訪問正常時,熔斷器處於關閉狀態,服務調用方可以正常地對服務進行調用。
- 熔斷開啟狀態(Open):預設情況下,在固定時間內介面調用出錯比率達到一個閾值(例如 50%),熔斷器會進入熔斷開啟狀態。進入熔斷狀態後,後續對該服務的調用都會被切斷,熔斷器會執行本地的降級(FallBack)方法。
- 半熔斷狀態(Half-Open): 在熔斷開啟一段時間之後,熔斷器會進入半熔斷狀態。在半熔斷狀態下,熔斷器會嘗試恢復服務調用方對服務的調用,允許部分請求調用該服務,並監控其調用成功率。如果成功率達到預期,則說明服務已恢復正常,熔斷器進入關閉狀態;如果成功率仍舊很低,則重新進入熔斷開啟狀態。
三種熔斷狀態之間的轉化關係如下圖:
Hystrix 實現熔斷機制
在 Spring Cloud 中,熔斷機制是通過 Hystrix 實現的。Hystrix 會監控微服務間調用的狀況,當失敗調用到一定比例時(例如 5 秒內失敗 20 次),就會啟動熔斷機制。
Hystrix 實現服務熔斷的步驟如下:
- 當服務的調用出錯率達到或超過 Hystix 規定的比率(預設為 50%)後,熔斷器進入熔斷開啟狀態。
- 熔斷器進入熔斷開啟狀態後,Hystrix 會啟動一個休眠時間窗,在這個時間窗內,該服務的降級邏輯會臨時充當業務主邏輯,而原來的業務主邏輯不可用。
- 當有請求再次調用該服務時,會直接調用降級邏輯快速地返回失敗響應,以避免系統雪崩。
- 當休眠時間窗到期後,Hystrix 會進入半熔斷轉態,允許部分請求對服務原來的主業務邏輯進行調用,並監控其調用成功率。
- 如果調用成功率達到預期,則說明服務已恢復正常,Hystrix 進入熔斷關閉狀態,服務原來的主業務邏輯恢復;否則 Hystrix 重新進入熔斷開啟狀態,休眠時間視窗重新計時,繼續重覆第 2 到第 5 步。
第一個微服務架構
sleuth:鏈路追蹤
什麼是鏈路追蹤
單純的理解鏈路追蹤,就是指一次任務的開始到結束,期間調用的所有系統及耗時(時間跨度)都可以完整記錄下來。
zipkin
Zipkin是Twitter開源的調用鏈分析工具,目前基於springcloud sleuth得到了廣泛的使用,特點是輕量,使用部署簡單。用於展示鏈路情況。
基本使用
導入sleuth依賴:因為sleth需要記錄沒有一次的調用情況,所以所有的consumer-server和provider-server基本需要導入依賴。
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-zipkin</artifactId>
</dependency>
zipkin下載
Spring Cloud Edgware 版本之後,改為強制採用官方提供的 Jar 包的形式啟動。
下載地址:https://repo1.maven.org/maven2/io/zipkin/zipkin-server/
zipkin啟動
添加配置
spring:
application:
name: user-service
zipkin:
base-url: http://localhost:9411
sleuth:
sampler:
probability: 1 #配置採樣率 預設的採樣比例為:0.1,即10%,所設置的值介於0 到 1,1表示會全部採集
rate: 10 #為了使用速率限制採樣器,選擇每秒間隔接受trace量,最小數字為0
將所有的項目啟動之後訪問http://127.0.0.1:9411/
Admin監控
功能
Spring Boot Admin提供了很多服務治理方面的功能,利用它能節省我們很多在治理服務方面的時間和精力Spring Boot Admin提供瞭如下功能(包括但不限於):
- 顯示健康狀態及詳細信息,如JVM和記憶體指標、數據源指標、緩存指標
- 跟蹤並下載日誌文件
- 查看jvm系統-和環境屬性
- 查看Spring啟動配置屬性方便loglevel管理
- 查看線程轉儲視圖http-traces
- 查看http端點查看計劃任務
- 查看和刪除活動會話(使用spring-session)
- 狀態更改通知(通過電子郵件、Slack、Hipchat…)
- 狀態變化的事件日誌(非持久性)
- 下載 heapdump
- 查看 Spring Boot 配置屬性
- 支持 Spring Cloud 的環境端點和刷新端點
- 支持 K8s
- 易用的日誌級別管理
- 與JMX-beans交互
- 查看線程轉儲
- 查看http跟蹤
- 查看auditevents
- 查看http-endpoints
- 查看計劃任務
- 查看和刪除活動會話(使用 Spring Session )
- 查看Flyway/Liquibase資料庫遷移
- 狀態變更通知(通過電子郵件,Slack,Hipchat等,支持釘釘)
- 狀態更改的事件日誌(非持久化)
用於管理和監視您的Spring Boot®應用程式。這些應用程式在我們的Spring Boot Admin Client中註冊(通過HTTP),或者是通過Spring Cloud®(例如Eureka,Consul)發現的。 UI只是Spring Boot Actuator端點之上的Vue.js應用程式。
使用
admin存在連個端:一個service端一個是client端,在springBoot項目中使用 Spring Boot Admin,賊好使! - 掘金 (juejin.cn)
在SpringCloud項目中使用可以結合Eureka獲取到所有服務的信息
配置開放所有監控項
# 開啟監控所有項
management:
endpoints:
web:
exposure:
includem: "*"
註意這個配置並不是springBoot自帶的,而是actuator依賴帶的,我們需要在相應的server服務導入依賴
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
最終效果
Gateway:Spring Cloud API網關組件
Gateway:Spring Cloud API網關組件(非常詳細) (biancheng.net)
在微服務架構中,一個系統往往由多個微服務組成,而這些服務可能部署在不同機房、不同地區、不同功能變數名稱下。這種情況下,客戶端(例如瀏覽器、手機、軟體工具等)想要直接請求這些服務,就需要知道它們具體的地址信息,例如 IP 地址、埠號等。
API 網關是一個搭建在客戶端和微服務之間的服務,我們可以在 API 網關中處理一些非業務功能的邏輯,例如許可權驗證、監控、緩存、請求路由等。
API 網關就像整個微服務系統的門面一樣,是系統對外的唯一入口。有了它,客戶端會先將請求發送到 API 網關,然後由 API 網關根據請求的標識信息將請求轉發到微服務實例。
所以可以猜測一下網關的功能應該需要有哪些
- 路由轉發,所有的請求全部需要在網關這通過相應的方式找到具體的API,轉發到具體的服務上。
- 安全方面,不需要直接將微服務裡面的服務埠暴露出去。
- 負載均衡。一個服務如果有多台機器運行,那麼我們需要負載均衡一下,緩解伺服器壓力。
作用:就是可以實現用戶的驗證登陸、解決跨域、日誌攔截、許可權控制、限流熔斷、負載均衡、黑名單和白名單機制等。
Zuul與GateWay有那些區別
Zuul網關屬於NetFix公司開源框架,屬於第一代微服務網關
GateWay屬於SpringCloud自己研發的網關框架,屬於第二代微服務網關。相比來說GateWay比Zuul網關的性能要好很多。
Zuul 1.0網關底層基於Servlet實現,阻塞式(BIO)api,不支持長連接
Zuul2.0 NIO
SpringBoot-WebSpringCloudGateWay基於Spring5構建,能夠實現響應式非阻塞式(NIO)api,支持長連接,能夠更好的支持Spring體系產品,依賴SpringBoot-WebFux
springCloud沒有集成和支持Zuul2.0
SpringCloudGateway是基於webFlux框架實現的,而webFlux框架底層則使用了高性能的Reactor模式
通信框架的Netty
網關服務的埠號一般多少:80或者443
Spring Cloud Gateway 核心概念
Spring Cloud GateWay 最主要的功能就是路由轉發,而在定義轉發規則時主要涉及了以下三個核心概念,如下表。
核心概念 | 描述 |
---|---|
Route(路由) | 網關最基本的模塊。它由一個 ID、一個目標 URI、一組斷言(Predicate)和一組過濾器(Filter)組成。 |
Predicate(斷言) | 路由轉發的判斷條件,我們可以通過 Predicate 對 HTTP 請求進行匹配,例如請求方式、請求路徑、請求頭、參數等,如果請求與斷言匹配成功,則將請求轉發到相應的服務。 |
Filter(過濾器) | 過濾器,我們可以使用它對請求進行攔截和修改,還可以使用它對上文的響應進行再處理。 |
Gateway 的工作流程
Spring Cloud Gateway 工作流程如下圖。
Spring Cloud Gateway 工作流程說明如下:
- 客戶端將請求發送到 Spring Cloud Gateway 上。
- Spring Cloud Gateway 通過 Gateway Handler Mapping 找到與請求相匹配的路由,將其發送給 Gateway Web Handler。
- Gateway Web Handler 通過指定的過濾器鏈(Filter Chain),將請求轉發到實際的服務節點中,執行業務邏輯返迴響應結果。
- 過濾器之間用虛線分開是因為過濾器可能會在轉發請求之前(pre)或之後(post)執行業務邏輯。
- 過濾器(Filter)可以在請求被轉發到服務端前,對請求進行攔截和修改,例如參數校驗、許可權校驗、流量監控、日誌輸出以及協議轉換等。
- 過濾器可以在響應返回客戶端之前,對響應進行攔截和再處理,例如修改響應內容或響應頭、日誌輸出、流量監控等。
- 響應原路返回給客戶端。
Nginx和Gateway區別
相同點:
都是可以實現api的攔截,負載均衡、反向代理、請求過濾,可以完全和網關實現一樣的效果。
不同點:
Nginx性能好,併發量在30000到50000,使用C+lua編寫。
GateWay,性能較差,併發量在1000,使用java編寫
GateWay入門
導入依賴
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
註意的當導入了GateWay的依賴之後,就不能導入spring-boot-starter-web依賴了,因為web預設的伺服器是tomcat,而GataWay的伺服器是Netty。
GateWay配置文件
server:
port: 80
spring:
application:
name: gateway-server
cloud:
gateway:
enabled: true #只要添加了依賴預設開啟
routes:
- id: user-server-route
uri: http://localhost:88
predicates:
- Path=/UserDoOrder
通過代碼的方式實現路由
package com.zl.config;
import org.springframework.cloud.gateway.route.RouteLocator;
import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class GateWayConfig {
/*
* 代碼實現和yml實現可以一起使用
* */
@Bean
public RouteLocator routeLocator(RouteLocatorBuilder builder){
return builder.routes()
.route("dance-id",r->r.path("/v/dance").uri("https://www.bilibili.com"))
.build();
}
}
Spring Cloud Gateway 動態路由
預設情況下,Spring Cloud Gateway 會根據服務註冊中心(例如 Eureka Server)中維護的服務列表,以服務名(spring.application.name)作為路徑創建動態路由進行轉發,從而實現動態路由功能。
我們可以在配置文件中,將 Route 的 uri 地址修改為以下形式。
lb://service-name
以上配置說明如下:
- lb:uri 的協議,表示開啟 Spring Cloud Gateway 的負載均衡功能。
- service-name:服務名,Spring Cloud Gateway 會根據它獲取到具體的微服務地址。
server:
port: 81
spring:
application:
name: gateway-server
cloud:
gateway:
enabled: true #只要添加了依賴預設開啟
routes:
- id: user-server-route
uri: lb://user-service #使用lb:將會實現一個負載均衡的效果。
predicates:
- Path=/UserDoOrder
discovery:
locator:
enabled: true #開啟動態路由,但是在訪問API實現,需要在前面添加/應用名稱/APi
lower-case-service-id: true #開啟服務名稱小寫
# 需要將GateWay服務註冊到Eureka服務上面去,因為GateWay需要通過拉取服務列表,結合服務發現實現動態路由的效果。
eureka:
client:
service-url: #指定註冊地址
defaultZone: http://localhost:8761/eureka
register-with-eureka: true #是否註冊到註冊中心上去
fetch-registry: true #應用是否拉取服務列表到本地
registry-fetch-interval-seconds: 3 #間隔多少時間拉取列表到本地
instance:
lease-renewal-interval-in-seconds: 5
hostname: localhost
prefer-ip-address: true
instance-id: ${eureka.instance.hostname}:${spring.application.name}:${server.port}
斷言工廠predicate
Spring Cloud Gateway 中文文檔 (springdoc.cn)
常見的 Predicate 斷言如下表(假設轉發的 URI 為 http://localhost:8001)。簡單來說predicate就是給請求路由添加了一定限制條件。
斷言 | 示例 | 說明 |
---|---|---|
Path | - Path=/dept/list/** | 當請求路徑與 /dept/list/** 匹配時,該請求才能被轉發到 http://localhost:8001 上。 |
Before | - Before=2021-10-20T11:47:34.255+08:00[Asia/Shanghai] | 在 2021 年 10 月 20 日 11 時 47 分 34.255 秒之前的請求,才會被轉發到 http://localhost:8001 上。 |
After | - After=2021-10-20T11:47:34.255+08:00[Asia/Shanghai] | 在 2021 年 10 月 20 日 11 時 47 分 34.255 秒之後的請求,才會被轉發到 http://localhost:8001 上。 |
Between | - Between=2021-10-20T15:18:33.226+08:00[Asia/Shanghai],2021-10-20T15:23:33.226+08:00[Asia/Shanghai] | 在 2021 年 10 月 20 日 15 時 18 分 33.226 秒 到 2021 年 10 月 20 日 15 時 23 分 33.226 秒之間的請求,才會被轉發到 http://localhost:8001 伺服器上。 |
Cookie | - Cookie=name,c.biancheng.net | 攜帶 Cookie 且 Cookie 的內容為 name=c.biancheng.net 的請求,才會被轉發到 http://localhost:8001 上。 |
Header | - Header=X-Request-Id,\d+ | 請求頭上攜帶屬性 X-Request-Id 且屬性值為整數的請求,才會被轉發到 http://localhost:8001 上。 |
Method | - Method=GET | 只有 GET 請求才會被轉發到 http://localhost:8001 上。 |
Gateway過濾器
Filter 的分類
Spring Cloud Gateway 提供了以下兩種類型的過濾器,可以對請求和響應進行精細化控制。
過濾器類型 | 說明 |
---|---|
Pre 類型 | 這種過濾器在請求被轉發到微服務之前可以對請求進行攔截和修改,例如參數校驗、許可權校驗、流量監控、日誌輸出以及協議轉換等操作。 |
Post 類型 | 這種過濾器在微服務對請求做出響應後可以對響應進行攔截和再處理,例如修改響應內容或響應頭、日誌輸出、流量監控等。 |
按照作用範圍劃分,Spring Cloud gateway 的 Filter 可以分為 2 類:
- GatewayFilter:應用在
單個路由或者一組路由上
的過濾器。 - GlobalFilter:應用在所有的路由上的過濾器。
自定義GlobalFilter過濾器
package com.zl.filter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.RequestPath;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.net.URI;
import java.util.HashMap;
@Component
public class myGlobalFilter implements GlobalFilter, Ordered {
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
//可以通過exchange獲取到相應的Request和Response
//通過Request獲取到請求的一下參數
ServerHttpRequest request = exchange.getRequest();
URI uri = request.getURI();
RequestPath path = request.getPath();
//當請求失敗之後可以使用response發送相應的JSON數據給前端 forExample code:403 message: "你沒有許可權"
ServerHttpResponse response = exchange.getResponse();
response.getHeaders().set("content-type","application/json:charset=utf-8");
HashMap<String, Object> map = new HashMap<>();
map.put("code",200);
map.put("message","你沒有許可權");
ObjectMapper objectMapper = new ObjectMapper();
try {
byte[] bytes = objectMapper.writeValueAsBytes(map);
DataBuffer wrap = response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(wrap));
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
// return chain.filter(exchange);
}
//配置當前過濾器的位置 數字越小越先執行
@Override
public int getOrder() {
return 0;
}
}
IP攔截
package com.zl.filter;
import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.Ordered;
import org.springframework.core.io.buffer.DataBuffer;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.http.server.reactive.ServerHttpResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
@Component
public class IpCheckFilter implements GlobalFilter, Ordered {
public static final String blockList= "0:0:0:0:0:0:0:1";
/*
* 一般使用一個資料庫來存儲黑名單的IP,獲取有的地方使用白名單。
* 兩種的區別
* 黑名單表示在該名單中的IP是不可以進行訪問的
* 百名單表示只有這個名單中的IP是可以進行訪問的。
* 在網關中我不要做一下比較耗時的操作,比如查詢資料庫,因為網關併發量比較大。通常會將名單存儲到redis中。
* 如果是名單IP比較少時,直接將IP保存到記憶體中
* */
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
ServerHttpRequest request = exchange.getRequest();
//獲取到訪問這個的IP 通常IPV6,
String hostString = request.getRemoteAddress().getAddress().getHostName();
System.out.println(hostString);
//查詢黑名單中是否存在該IP地址
if(!blockList.equals(hostString)){
return chain.filter(exchange);
}
ServerHttpResponse response = exchange.getResponse();
HashMap<Object, Object> map = new HashMap<>();
response.getHeaders().set("content-type","application/json;charset=utf-8");
map.put("code",438);
map.put("message","你是黑名單");
ObjectMapper objectMapper = new ObjectMapper();
byte[] bytes = new byte[0];
try {
bytes = objectMapper.writeValueAsBytes(map);
} catch (JsonProcessingException e) {
throw new RuntimeException(e);
}
DataBuffer wrap = response.bufferFactory().wrap(bytes);
return response.writeWith(Mono.just(wrap));
}
@Override
public int getOrder() {
return -5;
}
}
自定義token攔截
@Component
/*
* 1、獲取到url,判斷該URL是否不需要驗證token
* 2、獲取請求頭Authorization
* 3、判斷是否為null
* 4、判斷token是否存在與redis中
* 5、進行放行與攔截
* */
public class tokenFilter implements GlobalFilter, Ordered {
private static List whiterList= Arrays.asList("/doLogin");
@Autowired
private StringRedisTemplate redisTemplate;
@Override
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
RequestPath url = exchange.getRequest().getPath();
if(whiterList.contains(url)){
return chain.filter(exchange);
}
HttpHeaders headers = exchange.getRequest().getHeaders();
List<String> authorization = headers.get("Authorization");
if(!CollectionUtils.isEmpty(authorization)){
String token = authorization.get(0);
if(StringUtils.hasText(token)){
//判斷token中是否存在token
if(redisTemplate.hasKey(token)){
return chain.filter(exchange);
}
}
}
//對請求進行攔截
ServerHttpResponse response = exchange.getResponse();
Map<String,Object> map=new HashMap<>();
map.put("code",401);
map.put("msg","未授權");
ObjectMapper objectMapper = new ObjectMapper();
byte[] bytes = new byte[0];
try {