微服務學習計劃——SpringCloud

秋落雨微涼發表於2023-02-13

微服務學習計劃——SpringCloud

在學習並掌握了眾多基礎框架之後,我們的專案繁雜且難以掌握,那麼我們就需要開啟一門新的課程,也就是我們常說的微服務架構

隨著網際網路行業的發展,對服務的要求也越來越高,服務架構也從單體架構逐漸演變為現在流行的微服務架構。

這篇文章我們將會概括到下面幾個知識:

  • 認識微服務
  • 服務拆分和遠端呼叫
  • Eureka註冊中心
  • Ribbon負載均衡
  • Nacos註冊中心
  • Nacos配置管理
  • Feign遠端呼叫
  • GateWay服務閘道器

認識微服務

首先我們需要去了解這麼一個宏觀的概念——微服務

單體架構

在微服務沒有出現之前,也就是我們之前的專案書寫,一般都是採用單體架構:

  • 將業務的所有功能集中在一個專案中開發,打成一個包部署。

我們可以給出單體架構的直觀圖:

但是單體架構的優缺點也十分明顯:

  • 優點:
    • 架構簡單
    • 部署成本低
  • 缺點:
    • 耦合度高(維護困難、升級困難)

分散式架構

當專案逐漸龐大之後,我們就開始使用分散式架構去處理專案:

  • 根據業務功能對系統做拆分,每個業務功能模組作為獨立專案開發,稱為一個服務。

我們給出分散式架構的直觀圖:

同樣我們也可以很直觀的獲得分散式架構的優缺點:

  • 優點:

    • 降低服務耦合
    • 有利於服務升級和擴充
  • 缺點:

    • 服務呼叫關係錯綜複雜

微服務架構

我們從單體架構升級到分散式架構自然會存在一些我們目前無法解決的問題:

  • 服務拆分的粒度如何界定?
  • 服務之間如何呼叫?
  • 服務的呼叫關係如何管理?
  • 服務叢集地址如何維護?
  • 服務健康狀態如何感知

而我們的微服務架構為我們的上述問題提供了一個統一的標準,因而微服務就此而生!

下面我們就來介紹微服務,微服務架構實際上是分散式架構的一種細化,我們給出微服務的架構特徵:

  • 單一職責:微服務拆分粒度更小,每一個服務都對應唯一的業務能力,做到單一職責
  • 自治:團隊獨立、技術獨立、資料獨立,獨立部署和交付
  • 面向服務:服務提供統一標準的介面,與語言和技術無關
  • 隔離性強:服務呼叫做好隔離、容錯、降級,避免出現級聯問題

我們同樣給出微服務的一張直觀圖:

微服務的上述特性其實是在給分散式架構制定一個標準,進一步降低服務之間的耦合度,提供服務的獨立性和靈活性。

因此,可以認為微服務是一種經過良好架構設計的分散式架構方案 。

微服務擴充套件

微服務這種方案需要技術框架來落地,目前國內知名度較高的就是SpringCloud和阿里巴巴的Dubbo

我們針對三種微服務技術做一個簡單的對比:

Dubbo SpringCloud SpringCloudAlibaba
註冊中心 zookeeper、Redis Eureka、Consul Nacos、Eureka
服務遠端呼叫 Dubbo協議 Feign(http協議) Dubbo、Feign
配置中心 SpringCloudConfig SpringCloudConfig、Nacos
服務閘道器 SpringCloudGateway、Zuul SpringCloudGateway、Zuul
服務監控和保護 dubbo-admin,功能弱 Hystix Sentinel

最後我們再給出目前企業所常使用的微服務組合:

  1. SpringCloud + Feign
  • 使用SpringCloud技術棧
  • 服務介面採用Restful風格
  • 服務呼叫採用Feign方式
  1. SpringCloudAlibaba + Feign
  • 使用SpringCloudAlibaba技術棧
  • 服務介面採用Restful風格
  • 服務呼叫採用Feign方式
  1. SpringCloudAlibaba + Dubbo
  • 使用SpringCloudAlibaba技術棧
  • 服務介面採用Dubbo協議標準
  • 服務呼叫採用Dubbo方式
  1. Dubbo原始模式
  • 基於Dubbo老舊技術體系
  • 服務介面採用Dubbo協議標準
  • 服務呼叫採用Dubbo方式

SpringCloud

最後我們介紹一下SpringCloud:

  • SpringCloud是目前國內使用最廣泛的微服務框架。
  • 官網地址:https://spring.io/projects/spring-cloud。
  • SpringCloud整合了各種微服務功能元件,並基於SpringBoot實現了這些元件的自動裝配,從而提供了良好的開箱即用體驗。

其中SpringCloud常用元件包括有:

  • 服務註冊發現:Eureka,Nacos,Consul
  • 服務遠端呼叫:OpenFeign,Dubbo
  • 服務鏈路監控:Zipkin,Sleuth
  • 統一配置管理:SpringCloudConfig,Nacos
  • 統一閘道器路由:SpringCloudGateway,Zuul
  • 流控降級保護:Hystix,Sentinel

服務拆分和遠端呼叫

下面一個小節我們來學習服務拆分和遠端呼叫兩方面

服務拆分原則

我們前面提及到了分散式架構需要將功能拆分出來並分離開發,那麼我們該如何進行拆分:

  • 不同微服務,不要重複開發相同業務
  • 微服務資料獨立,不要訪問其它微服務的資料庫
  • 微服務可以將自己的業務暴露為介面,供其它微服務呼叫

服務拆分案例

我們給出一個簡單的案例來展示服務拆分操作:

  • 我們給出一個簡單邏輯專案
  • 首先我們存在一個父工程,名為cloud-demo
  • 此外我們還有兩個子工程,分別為order-service控制訂單資訊,user-service控制使用者資訊

我們首先給出圖示邏輯:

我們需要滿足一下需求:

  • 訂單微服務和使用者微服務都必須有各自的資料庫,相互獨立
  • 訂單服務和使用者服務都對外暴露Restful的介面
  • 訂單服務如果需要查詢使用者資訊,只能呼叫使用者服務的Restful介面,不能查詢使用者資料庫

那麼我們給出案例書寫:

  1. 匯入資料庫
# order訂單資料庫

-- ----------------------------
-- Table structure for tb_order
-- ----------------------------
DROP TABLE IF EXISTS `tb_order`;
CREATE TABLE `tb_order`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '訂單id',
  `user_id` bigint(20) NOT NULL COMMENT '使用者id',
  `name` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '商品名稱',
  `price` bigint(20) NOT NULL COMMENT '商品價格',
  `num` int(10) NULL DEFAULT 0 COMMENT '商品數量',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `username`(`name`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 109 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

-- ----------------------------
-- Records of tb_order
-- ----------------------------
INSERT INTO `tb_order` VALUES (101, 1, 'Apple 蘋果 iPhone 12 ', 699900, 1);
INSERT INTO `tb_order` VALUES (102, 2, '雅迪 yadea 新國標電動車', 209900, 1);
INSERT INTO `tb_order` VALUES (103, 3, '駱駝(CAMEL)休閒運動鞋女', 43900, 1);
INSERT INTO `tb_order` VALUES (104, 4, '小米10 雙模5G 驍龍865', 359900, 1);
INSERT INTO `tb_order` VALUES (105, 5, 'OPPO Reno3 Pro 雙模5G 影片雙防抖', 299900, 1);
INSERT INTO `tb_order` VALUES (106, 6, '美的(Midea) 新能效 冷靜星II ', 544900, 1);
INSERT INTO `tb_order` VALUES (107, 2, '西昊/SIHOO 人體工學電腦椅子', 79900, 1);
INSERT INTO `tb_order` VALUES (108, 3, '梵班(FAMDBANN)休閒男鞋', 31900, 1);

SET FOREIGN_KEY_CHECKS = 1;

# user使用者資料庫

-- ----------------------------
-- Table structure for tb_user
-- ----------------------------
DROP TABLE IF EXISTS `tb_user`;
CREATE TABLE `tb_user`  (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `username` varchar(100) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '收件人',
  `address` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci NULL DEFAULT NULL COMMENT '地址',
  PRIMARY KEY (`id`) USING BTREE,
  UNIQUE INDEX `username`(`username`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 109 CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Compact;

-- ----------------------------
-- Records of tb_user
-- ----------------------------
INSERT INTO `tb_user` VALUES (1, '柳巖', '湖南省衡陽市');
INSERT INTO `tb_user` VALUES (2, '文二狗', '陝西省西安市');
INSERT INTO `tb_user` VALUES (3, '華沉魚', '湖北省十堰市');
INSERT INTO `tb_user` VALUES (4, '張必沉', '天津市');
INSERT INTO `tb_user` VALUES (5, '鄭爽爽', '遼寧省瀋陽市大東區');
INSERT INTO `tb_user` VALUES (6, '範兵兵', '山東省青島市');

SET FOREIGN_KEY_CHECKS = 1;
  1. IDEA程式碼書寫

我們會建立一個如下框架的IDEA框架:

我們對上述資訊進行講解:

/* cloud-demo:父工程,攜帶pom.xml */

/* order-service訂單工程 user-service使用者工程 */

// 具有完整的dao,mapper,service,Controller層並完整書寫Application啟動類

// 具有yml配置檔案,包含有port埠資訊,mysql資料庫資訊等

遠端呼叫案例

當我們執行程式後,我們可以在瀏覽器中查詢到order相關資料:

但是我們會發現我們是無法查詢到user的詳細資訊的,這是因為我們的order是沒有user的資料庫資訊的

所以我們在完成了服務拆分之後就需要去了解遠端呼叫:

  • 目前我們的order資料庫和user資料庫是分開的,我們如果在查詢order時還希望獲得到order對應的user資訊就需要採用遠端呼叫

那麼我們該如何實現遠端呼叫:

  • 藉助RestTemplate類去完成遠端呼叫

下面我們給出具體步驟及相關程式碼:

  1. 獲得RestTemplate物件
// 我們需要將RestTemplate設定為Bean物件(這裡直接在Application中設定Bean)

package cn.itcast.order;

import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.config.DefaultFeignConfiguration;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }

    /**
     * 建立RestTemplate並注入Spring容器
     */
    @Bean
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

}
  1. 藉助RestTemplate物件去傳送遠端呼叫資訊
package cn.itcast.order.service;

import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.pojo.User;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

// 這裡在Service業務層獲得資訊
@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    // 這裡自動裝填RestTemplate物件
    @Autowired
    private RestTemplate restTemplate;

    public Order queryOrderById(Long orderId) {
        // 1.查詢訂單
        Order order = orderMapper.findById(orderId);
        // 2.利用RestTemplate發起http請求,查詢使用者
        // 2.1.url路徑
        String url = "http://localhost/8081/user/" + order.getUserId();
        // 2.2.傳送http請求,實現遠端呼叫
        // 這裡restTemplate具有getForObject方法和postForObject分別針對get和post請求,後面引數分別為url和對應的class
        User user = restTemplate.getForObject(url, User.class);
        // 3.封裝user到Order
        order.setUser(user);
        // 4.返回
        return order;
    }
}
  1. 查詢頁面獲得完整資訊

提供者和消費者

最後我們針對服務拆分和遠端呼叫給出兩個理論角色概念:

  • 服務提供者:一次業務中,被其它微服務呼叫的服務。(提供介面給其它微服務)
  • 服務消費者:一次業務中,呼叫其它微服務的服務。(呼叫其它微服務提供的介面)

我們需要注意的是:

  • 提供者和消費者的概念是具有相對性的,一個物件可能既是提供者也是消費者
  • 例如A使用B,B使用C,那麼B既是A的提供者也是C的消費者,這個概念並不是固定的

Eureka註冊中心

下面我們來介紹一種註冊中心EUreka

Eureka問題

首先我們需要知道Eureka是什麼:

  • Eureka是一種註冊中心
  • 假設我們的一個消費者需要去使用提供者,但是提供者的地址具有多個,那麼我們的消費者該如何選擇並使用對應的提供者

我們給出一個簡單的圖示展示:

例如上圖:

  • 我們的order-service需要使用user-service
  • 但是user-service一共有三個,如果我們固定書寫url地址資訊,那麼只會在一臺伺服器中獲取,導致其他伺服器空閒

那麼我們就需要注意到三個問題:

  • order-service在發起遠端呼叫的時候,該如何得知user-service例項的ip地址和埠?
  • 有多個user-service例項地址,order-service呼叫時該如何選擇?
  • order-service如何得知某個user-service例項是否依然健康,是不是已經當機?

Rureka結構與作用

首先我們給出Eureka的具體結構並對其分析:

我們對上圖進行簡單介紹:

  • 首先我們的Eureka屬於一個子工程,我們需要對其進行配置
  • 其餘的消費者或者提供者均屬於Eureka-client,屬於客戶佇列,我們採用Eureka對其服務
  • 一個微服務,既可以是服務提供者,又可以是服務消費者,因此eureka將服務註冊、服務發現等功能統一封裝到了eureka-client端
  • 服務提供者在啟動時會向Eureka-server提供一個註冊資訊,那麼Eureka-server就會記錄下對應資訊儲存
  • 當我們的消費者需要對某些提供者進行消費時,會向Eureka-server索要對應資訊,並根據Rureka對其提供者進行選擇並呼叫
  • 我們的Eureka-server可以判斷服務提供者是否還存在或是否出現新的服務提供者,預設每30s去更新一次資料

那麼我們就可以回答上述問題:

/* 問題1:order-service如何得知user-service例項地址? */

// - user-service服務例項啟動後,將自己的資訊註冊到eureka-server(Eureka服務端)。這個叫服務註冊
// - eureka-server儲存服務名稱到服務例項地址列表的對映關係
// - order-service根據服務名稱,拉取例項地址列表。這個叫服務發現或服務拉取

/* 問題2;order-service如何從多個user-service例項中選擇具體的例項? */

// - order-service從例項列表中利用負載均衡演算法選中一個例項地址
// - 向該例項地址發起遠端呼叫

/* 問題3:order-service如何得知某個user-service例項是否依然健康,是不是已經當機? */

// - user-service會每隔一段時間(預設30秒)向eureka-server發起請求,報告自己狀態,稱為心跳
// - 當超過一定時間沒有傳送心跳時,eureka-server會認為微服務例項故障,將該例項從服務列表中剔除
// - order-service拉取服務時,就能將故障例項排除了

Eureka-server服務搭建

下面我們逐漸介紹搭建Eureka-server的操作:

  1. 以cloud-demo為父工程,建立專案Eureka-server,引入依賴
<!-- 依賴寫在Eureka-server的pom.xml檔案裡 -->

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>
  1. 編寫啟動類,並新增@EnableEurekaServer註解
package cn.itcast.eureka;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

// 表示啟動Eureka
@EnableEurekaServer
@SpringBootApplication
public class EurekaApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaApplication.class, args);
    }
}
  1. 新增yml檔案,編寫配置資訊
server:
  port: 10086 # 服務埠
spring:
  application:
    name: eurekaserver # eureka的服務名稱
eureka:
  client:
    service-url:  # eureka的地址資訊
      defaultZone: http://127.0.0.1:10086/eureka
  1. 啟動Eureka並開啟對應頁面即可

Eureka服務註冊

接下來我們來進行服務註冊功能:

  1. 在user-service專案中引入Eureka-client依賴
<?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>cloud-demo</artifactId>
        <groupId>cn.itcast.demo</groupId>
        <version>1.0</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>user-service</artifactId>

    <dependencies>
        <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>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
        </dependency>
        <!--eureka客戶端依賴-->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
        </dependency>
    </dependencies>
    <build>
        <finalName>app</finalName>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>
</project>
  1. 在yml配置檔案下配置相關資訊
spring:
  application: # 配置服務名稱
    name: userservice
eureka:
  client:
    service-url: # 配置對應註冊的Rureka地址
      defaultZone: http://127.0.0.1:10086/eureka
  1. 啟動多個user-service例項物件
// 1.複製一份user-service啟動配置(在啟動第一份後在左下角可以找到user-service,右鍵Copy Configuration)

// 2.在複製介面的VMoptions修改埠:-Dserver.port=8082

// 3.啟動即可在Eureka頁面看到兩個user-service

Eureka服務拉取

我們在前面的註冊環境已經將兩個user-service設定在同一服務中

那麼我們的order-service如果想要呼叫user-service的介面,我們就需要稍微修改程式碼使其在兩個伺服器中拉取資料:

  1. 修改OrderService程式碼,修改url路徑,用服務名代替url
package cn.itcast.order.service;

import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.pojo.User;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private RestTemplate restTemplate;

    public Order queryOrderById(Long orderId) {

        Order order = orderMapper.findById(orderId);

        // url路徑(這裡的路徑直接修改為服務名userservice,使其在相同服務名的伺服器中選擇)
        String url = "http://userservice/user/" + order.getUserId();

        User user = restTemplate.getForObject(url, User.class);

        order.setUser(user);

        return order;
    }
}
  1. 修改RestTemplate的註解,使其採用負載均衡選擇伺服器
package cn.itcast.order;

import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.config.DefaultFeignConfiguration;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }

	// @LoadBalanced表示負載均衡
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

}

Ribbon負載均衡

下面我們來對上述的@Loadbalanced負載均衡進行部分介紹

Ribbon負載均衡流程

我們首先給出負載均衡流程圖:

下面我們採用另一張圖來詳細解釋上述圖:

我們對其進行簡單解釋:

  • 攔截我們的RestTemplate請求http://userservice/user/1
  • RibbonLoadBalancerClient會從請求url中獲取服務名稱,也就是user-service
  • DynamicServerListLoadBalancer根據user-service到eureka拉取服務列表
  • eureka返回列表,localhost:8081、localhost:8082
  • IRule利用內建負載均衡規則,從列表中選擇一個,例如localhost:8081
  • RibbonLoadBalancerClient修改請求地址,用localhost:8081替代userservice,得到http://localhost:8081/user/1,發起真實請求

Ribbon負載均衡策略

我們在上面的解釋中提及到了一個詞彙:

  • IRule:負載均衡的規則都定義在IRule介面中,而IRule有很多不同的實現類

我們給出一張IRule的繼承圖:

我們對其部分規則進行解釋:

內建負載均衡規則類 規則描述
RoundRobinRule 簡單輪詢服務列表來選擇伺服器。它是Ribbon預設的負載均衡規則。
AvailabilityFilteringRule 對以下兩種伺服器進行忽略: (1)在預設情況下,這臺伺服器如果3次連線失敗,這臺伺服器就會被設定為“短路”狀態。短路狀態將持續30秒,如果再次連線失敗,短路的持續時間就會幾何級地增加。 (2)併發數過高的伺服器。如果一個伺服器的併發連線數過高,配置了AvailabilityFilteringRule規則的客戶端也會將其忽略。併發連線數的上限,可以由客戶端的..ActiveConnectionsLimit屬性進行配置。
WeightedResponseTimeRule 為每一個伺服器賦予一個權重值。伺服器響應時間越長,這個伺服器的權重就越小。這個規則會隨機選擇伺服器,這個權重值會影響伺服器的選擇。
ZoneAvoidanceRule 以區域可用的伺服器為基礎進行伺服器的選擇。使用Zone對伺服器進行分類,這個Zone可以理解為一個機房、一個機架等。而後再對Zone內的多個服務做輪詢。
BestAvailableRule 忽略那些短路的伺服器,並選擇併發數較低的伺服器。
RandomRule 隨機選擇一個可用的伺服器。
RetryRule 重試機制的選擇邏輯

其中預設的實現就是ZoneAvoidanceRule,是一種輪詢方案

除此之外我們還可以去自定義實現負載均衡策略,下面我們來介紹兩種實現方法:

  1. 程式碼方式:在order-service中的OrderApplication類中,定義一個新的IRule
@Bean
public IRule randomRule(){
    return new RandomRule();
}
  1. 配置檔案方式:在order-service的application.yml檔案中,新增新的配置也可以修改規則
userservice: # 給某個微服務配置負載均衡規則,這裡是userservice服務
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RandomRule # 負載均衡規則 

Ribbon飢餓載入策略

我們的Ribbon預設是採用懶載入,即第一次訪問時才會去建立LoadBalanceClient,請求時間會很長。

我們可以採用程式碼去修改使其變為餓漢式載入:

ribbon:
  eager-load:
    enabled: true # 是否自動載入
    clients: userservice # 針對的client

Nacos註冊中心

國內公司一般都推崇阿里巴巴的技術,比如註冊中心,SpringCloudAlibaba也推出了一個名為Nacos的註冊中心。

Nacos下載

Nacos是阿里巴巴的技術產品了,我們需要先對其進行下載才可使用:

  1. 開啟官網,下載jar包(下載連結:https://github.com/alibaba/nacos)

  1. 下載後在全英路徑下壓縮,在bin路徑下使用startup.cmd即可
# 跳轉路徑
cd 目錄名

# 啟動startup.cmd
startup.cmd -m standalone

# 這裡注意:下載後預設路徑8848,可以在conf的properties檔案修改port
  1. 開啟頁面http://127.0.0.1:8848/nacos,賬號密碼都是nacos

Nacos服務註冊

我們來介紹nacos的服務註冊過程:

  1. 匯入座標
<!-- cloud-demo 父工程 (SpringCloudAlibaba依賴)-->

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-alibaba-dependencies</artifactId>
    <version>2.2.6.RELEASE</version>
    <type>pom</type>
    <scope>import</scope>
</dependency>

<!-- user-service order-service子工程 (nacos-discovery)-->

<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<!--注意:nacos和Eureka座標衝突,需要註釋掉Eureka依賴-->
  1. 配置Nacos座標
# 在user-service和order-service的application.yml中新增nacos地址:
# 同樣和Eureka衝突,記得註釋掉

spring:
  cloud:
    nacos:
      server-addr: localhost:8848
  1. 啟動並測試即可

Nacos服務分級儲存模型

我們首先給出一張服務分級儲存模型圖並對其解釋:

我們對其進行解釋:

  • 服務就是我們的消費者,例項就是我們的提供者
  • 我們的提供者會被根據所在地點不同被劃分到不同的叢集中去
  • 如果我們本地的服務採用本地叢集的提供者來進行操作,由於路程近其速度也會更快
  • 微服務互相訪問時,應該儘可能訪問同叢集例項,因為本地訪問速度更快。當本叢集內不可用時,才訪問其它叢集。

下面我們來介紹如何設定叢集:

  1. 修改user-service的application.yml檔案,新增叢集配置:
spring:
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: HZ # 叢集名稱
  1. 當我們重啟其user-service例項後,我們可以在Nacos控制中心檢視到相關叢集

但是我們預設的ZoneAvoidanceRule無法實現同叢集優先負載均衡操作,所以我們需要對其進行設定:

  1. 給order-service配置叢集資訊
# 修改order-service的application.yml檔案,新增叢集配置:

spring:
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: HZ # 叢集名稱
  1. 修改負載均衡規則
# 修改order-service的application.yml檔案,修改負載均衡規則: 

userservice:
  ribbon:
    NFLoadBalancerRuleClassName: com.alibaba.cloud.nacos.ribbon.NacosRule # 負載均衡規則 

Nacos權重配置

在我們的Nacos控制系統中的物件都會有一個權重設定:

我們對權重進行簡單解釋:

  • 我們可以透過編輯按鈕修改其權重大小
  • 權重越大被使用的機率越高;權重越小被使用的機率越小
  • 當我們將權重設定為0後,該伺服器將不會在被使用,我們可以藉此更新該伺服器

Nacos環境隔離

Nacos提供了namespace來實現環境隔離功能:

  • nacos中可以有多個namespace
  • namespace下可以有group、service等
  • 不同namespace之間相互隔離,例如不同namespace的服務互相不可見

首先我們先來了解如何在Nacos中新創namespace:

  1. 開啟Nacos,頁面跳轉至namespace,點選右上角的新創空間

  1. 填入名稱空間名和描述即可,注意名稱空間ID,我們後續會使用

在新創名稱空間之後,我們如果希望資料上傳到指定名稱空間,需要手動修改部分程式碼:

# 例如我們在order-service的application.yml檔案中進行修改,那麼後面的order服務就會到達新的名稱空間中

spring:
  cloud:
    nacos:
      server-addr: localhost:8848
      discovery:
        cluster-name: HZ
        namespace: 478f4b7c-c1a5-4dee-a7c7-84774766654d # 名稱空間,填ID

Nacos和Eureka

Nacos和Eureka整體結構類似,服務註冊、服務拉取、心跳等待,但是也存在一些差異

我們首先給出Eureka的展示圖:

我們再給出Nacos的展示圖:

我們可以發現Nacos相比於Eureka有些許不同之處,首先是臨時例項和非臨時例項:

  • 臨時例項:如果例項當機超過一定時間,會從服務列表剔除,預設的型別
  • 非臨時例項:如果例項當機,不會從服務列表剔除,也可以叫永久例項

此外還有Nacos關於服務消費者的區別:

  • 當Nacos中發生改變,會主動向服務消費者推送訊息以加快消費者資料讀取

最後我們給出Nacos和Eureka的相同點與不同點

Nacos與Eureka的共同點:

  • 都支援服務註冊和服務拉取
  • 都支援服務提供者心跳方式做健康檢測

Nacos與Eureka的不同點:

  • Nacos支援服務端主動檢測提供者狀態:臨時例項採用心跳模式,非臨時例項採用主動檢測模式
  • 臨時例項心跳不正常會被剔除,非臨時例項則不會被剔除
  • Nacos支援服務列表變更的訊息推送模式,服務列表更新更及時
  • Nacos叢集預設採用AP方式,當叢集中存在非臨時例項時,採用CP模式;Eureka採用AP方式

Nacos配置管理

在前面我們學習了Nacos去完成微服務註冊功能,下面我們來學習Nacos的配置管理功能

Nacos統一配置管理

首先我們先來學習Nacos中統一配置管理的基本內容:

  1. 開啟Nacos網頁,新創配置管理頁面

  1. 進入新創頁面後,書寫相關資訊

下面我們需要知道一些關於Nacos和application的相關資訊:

  • 上面我們書寫的nacos熱配置會和我們的application配置合併在一起,然後形成總配置
  • 但是如果我們沒有提前得知application.yaml配置檔案,我們無法得知Nacos地址及服務名稱,環境等資訊
  • 所以我們需要一個新的配置檔案,就是我們目前的這個檔案bootstrap.yaml檔案,我們會在裡面書寫最基本的資訊讓其檔案合併

下面我們繼續開始統一配置管理的內容:

  1. 首先匯入Nacos-config的依賴
<!--在使用統一配置的服務下-->

<!--nacos配置管理依賴-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-config</artifactId>
</dependency>
  1. 新創bootstrap.yaml檔案,並書寫基本資訊
# 注:在userservice服務下
spring:
  application:
    name: userservice # 服務名稱
  profiles:
    active: dev #開發環境,這裡是dev 
  cloud:
    nacos:
      server-addr: localhost:8848 # Nacos地址
      config:
        file-extension: yaml # 檔案字尾名
  1. 測試是否接收到Nacos配置
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    // 這裡採用@Value注入配置資訊,輸出成功證明收到Nacos配置資訊
    @Value("${pattern.dateformat}")
    private String dateformat;
    
    @GetMapping("now")
    public String now(){
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
    }
}

Nacos配置熱更新

我們首先來介紹一下熱更新:

  • 我們只需要在Nacos配置中修改配置,服務中的程式就會即時修改對應的配置資訊

下面我們來介紹兩種方法來實現熱更新:

  1. 在使用@Value的類中採用註解註釋
@Slf4j
@RestController
@RequestMapping("/user")
@RefreshScope	// 熱更新註解,使用後Nacos的配置資訊即時更新
public class UserController {

    @Autowired
    private UserService userService;

    @Value("${pattern.dateformat}")
    private String dateformat;
    
    @GetMapping("now")
    public String now(){
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(dateformat));
    }
}
  1. 單獨建立一個類,用於儲存Nacos熱更新配置,並採用註解標識
// 配置屬性實體類

@Component
@Data
@ConfigurationProperties(prefix = "pattern")	// ConfigurationProperties表示熱更新註解,prefix表示共用字首
public class PatternProperties {
    private String dateformat;
}

// 實時使用

@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @Autowired
    private PatternProperties patternProperties;

    @GetMapping("now")
    public String now(){
        return LocalDateTime.now().format(DateTimeFormatter.ofPattern(patternProperties.getDateformat()));
    }

    // 略
}

Nacos配置共享

我們之前採用了特定環境配置,其具體表示為:

  • 【服務名稱】-【環境名稱】.yaml
  • [spring.application.name]-[spring.profiles.active].yaml,例如:userservice-dev.yaml

當我們不使用環境名稱時,其配置就會變為共享配置:

  • 【服務名稱】.yaml
  • [spring.application.name].yaml,例如:userservice.yaml

我們給出一個簡單的示例:

  1. 首先在Nacos中建立新配置

  1. 在IDEA程式碼中書寫對應的屬性使用
// 配置屬性實體類

@Component
@Data
@ConfigurationProperties(prefix = "pattern")	// ConfigurationProperties表示熱更新註解,prefix表示共用字首
public class PatternProperties {
    private String dateformat;
    
    private String envSharedValue;
}

// Controller層
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;

    @Autowired
    private PatternProperties properties;

    @GetMapping("prop")
    public PatternProperties properties(){
        return properties;
    }
}
  1. 在IDEA中以不同的環境執行兩個userService服務
// 在啟動userService時
// 以Edit Configuration開啟,並在Active profiles中修改名稱以修改環境

Nacos配置管理優先順序

最後我們給出配置管理的優先順序展示:

  • 當存在相同屬性時,我們以下述順序左側為優,右側為良
  • Nacos(服務名-環境名.yaml)<- Nacos(服務名.yaml) <- 本地配置

Feign遠端呼叫

下面我們來介紹一下Feign

Feign簡單介紹

首先我們簡單介紹一下Feign:

  • Feign是一個宣告式的http客戶端
  • 我們可以藉助Feign來替代掉RestTemplate的複雜遠端呼叫方法

我們這裡回憶一下RestTemplate的遠端呼叫方法:

package cn.itcast.order.service;

import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.pojo.User;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private RestTemplate restTemplate;

    public Order queryOrderById(Long orderId) {

        Order order = orderMapper.findById(orderId);

        // 需要手動書寫url,並且加入id引數
        String url = "http://userservice/user/" + order.getUserId();

        // 需要呼叫restTemplate的固定方法並指定類class
        User user = restTemplate.getForObject(url, User.class);

        order.setUser(user);

        return order;
    }
}

Feign快速入門

我們下面給出Feign的基本使用:

  1. 引入依賴
<!--在order-service的pom檔案中引入Feign依賴-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  1. 啟動類註解,表示開啟Feign遠端呼叫
package cn.itcast.order;

import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.config.DefaultFeignConfiguration;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.loadbalancer.LoadBalanced;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.context.annotation.Bean;
import org.springframework.web.client.RestTemplate;

@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
@EnableFeignClients
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }

}
  1. 編寫Feign客戶端
package cn.itcast.order.client;

import cn.itcast.order.pojo.User;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

// 整體就是SpringMVC的REST風格
// @FeignClient類似RequestMapping,後面跟上具體的服務名
@FeignClient("userservice")
public interface UserClient {
    
    // 這裡就是具體的方法,採用REST
    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
}

/*
這個客戶端主要是基於SpringMVC的註解來宣告遠端呼叫的資訊,比如:
- 服務名稱:userservice
- 請求方式:GET
- 請求路徑:/user/{id}
- 請求引數:Long id
- 返回值型別:User
*/
  1. 測試
package cn.itcast.order.service;

import cn.itcast.feign.clients.UserClient;
import cn.itcast.feign.pojo.User;
import cn.itcast.order.mapper.OrderMapper;
import cn.itcast.order.pojo.Order;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class OrderService {

    @Autowired
    private OrderMapper orderMapper;

    @Autowired
    private UserClient userClient;

    public Order queryOrderById(Long orderId) {
        // 1.查詢訂單
        Order order = orderMapper.findById(orderId);
        // 2.用Feign遠端呼叫
        User user = userClient.findById(order.getUserId());
        // 3.封裝user到Order
        order.setUser(user);
        // 4.返回
        return order;
    }

}

Feign自定義配置

下面我們來介紹一下Feign的自定義配置:

型別 作用 說明
feign.Logger.Level 修改日誌級別 包含四種不同的級別:NONE、BASIC、HEADERS、FULL
feign.codec.Decoder 響應結果的解析器 http遠端呼叫的結果做解析,例如解析json字串為java物件
feign.codec.Encoder 請求引數編碼 將請求引數編碼,便於透過http請求傳送
feign. Contract 支援的註解格式 預設是SpringMVC的註解
feign. Retryer 失敗重試機制 請求失敗的重試機制,預設是沒有,不過會使用Ribbon的重試

大部分內容我們只需要使用預設就足以滿足我們日常需求了

我們簡單介紹一下Logger日誌級別:

  • 日誌大體分為四種

  • NONE:不記錄任何日誌資訊,這是預設值

  • BASIC:僅記錄請求的方法,URL以及響應狀態碼和執行時間

  • HEADERS:在BASIC的基礎上,額外記錄了請求和響應的頭資訊

  • FULL:記錄所有請求和響應的明細,包括頭資訊、請求體、後設資料

我們給出兩種修改預設配置的方法:

  1. 修改配置檔案
# 修改yaml配置檔案

# 可以針對某個微服務修改
feign:  
  client:
    config: 
      userservice: # 針對某個微服務的配置
        loggerLevel: FULL #  日誌級別 
        
# 也可以針對全部微服務修改
feign:  
  client:
    config: 
      default: # 這裡用default就是全域性配置,如果是寫服務名稱,則是針對某個微服務的配置
        loggerLevel: FULL #  日誌級別 
  1. Java程式碼方式
// 宣告一個類,然後宣告一個Logger.Level的物件
public class DefaultFeignConfiguration  {
    @Bean
    public Logger.Level feignLogLevel(){
        return Logger.Level.BASIC; // 日誌級別為BASIC
    }
}

// 此外我們還需要將該類配置給該服務:

// 如果要全域性生效,將其放到啟動類的@EnableFeignClients這個註解中
@EnableFeignClients(defaultConfiguration = DefaultFeignConfiguration .class) 

// 如果是區域性生效,則把它放到對應的@FeignClient這個註解中
@FeignClient(value = "userservice", configuration = DefaultFeignConfiguration .class) 

Feign使用最佳化

Feign底層發起http請求,依賴於其它的框架,我們這裡給出一些底層框架:

  • URLConnection:預設實現,不支援連線池

  • Apache HttpClient :支援連線池

  • OKHttp:支援連線池

因此提高Feign的效能主要手段就是使用連線池代替預設的URLConnection。

我們給出使用連線池的示例:

  1. 引入依賴
<!--我們這裡以Apache HttpClient為例-->

<!--httpClient的依賴 -->
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>
  1. 配置連線池
# 在對應的yml檔案中配置連線池資訊(這裡就是order-service服務)
feign:
  httpclient:
    enabled: true # 開啟feign對HttpClient的支援
    max-connections: 200 # 最大的連線數
    max-connections-per-route: 50 # 每個路徑的最大連線數

Feign使用技巧

我們可以發現Feign實際上和Controller的程式碼十分相似:

// Feign
@FeignClient("userservice")
public interface UserClient {
    
    // 這裡就是具體的方法,採用REST
    @GetMapping("/user/{id}")
    User findById(@PathVariable("id") Long id);
}

// Controller
@Slf4j
@RestController
@RequestMapping("/user")
public class UserController {

    @Autowired
    private UserService userService;
    
    @GetMapping("/{id}")
    public User queryById(@PathVariable("id") Long id,
                          @RequestHeader(value = "Truth", required = false) String truth) {
        System.out.println("truth: " + truth);
        return userService.queryById(id);
    }
}

我們給出一種抽取方法來減少相同程式碼的書寫:

  • 將Feign的Client抽取為獨立模組,並且把介面有關的POJO、預設的Feign配置都放到這個模組中,提供給所有消費者使用。

  • 例如,將UserClient、User、Feign的預設配置都抽取到一個feign-api包中,所有微服務引用該依賴包,即可直接使用。

我們給出具體例項:

  1. 抽取Feign內容,形成獨立模組

  1. 引入對應的Feign依賴
<!--Feign依賴-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  1. 在order-service中使用feign-api
<!--首先刪除掉order-service的全部Feign相關類和DefaultFeignConfiguration等配置-->

<!--匯入我們編寫的feign-api類-->
<dependency>
    <groupId>cn.itcast.demo</groupId>
    <artifactId>feign-api</artifactId>
    <version>1.0</version>
</dependency>

<!--修改order-service中的所有與上述三個元件有關的導包部分,改成匯入feign-api中的包-->
  1. 解決掃描包問題
/*

由於UserClient現在在cn.itcast.feign.clients包下,
而order-service的@EnableFeignClients註解是在cn.itcast.order包下,不在同一個包,無法掃描到UserClient

所以我們需要手動掃描包,其中可以採用兩種方法:
- 指定Feign應該掃描的包:@EnableFeignClients(basePackages = "cn.itcast.feign.clients")
- 指定需要載入的Client介面:@EnableFeignClients(clients = {UserClient.class})

*/

@MapperScan("cn.itcast.order.mapper")
@SpringBootApplication
@EnableFeignClients(clients = UserClient.class)
public class OrderApplication {

    public static void main(String[] args) {
        SpringApplication.run(OrderApplication.class, args);
    }

}

GateWay服務閘道器

最後我們來介紹一下GateWay服務閘道器

GateWay簡述

我們首先介紹一下GateWay:

  • Gateway閘道器是我們所有微服務的統一入口

我們給出一張GateWay的示意圖:

其中GateWay大致存在三種主要用途:

  • 許可權控制:閘道器作為微服務入口,需要校驗使用者是是否有請求資格,如果沒有則進行攔截。
  • 限流:當請求流量過高時,在閘道器中按照下流的微服務能夠接受的速度來放行請求,避免服務壓力過大。
  • 路由和負載均衡:一切請求都必須先經過gateway,但閘道器不處理業務,而是根據某種規則,把請求轉發到某個微服務,這個過程叫做路由。當然路由的目標服務有多個時,還需要做負載均衡。

關於閘道器大致包括兩種:

  • Zuul:基於Servlet的實現,屬於阻塞式程式設計。
  • SpringCloudGateway:基於Spring5中提供的WebFlux,屬於響應式程式設計的實現,具備更好的效能。

GateWay快速入門

下面我們透過一個簡單的案例來介紹GateWay的基本使用:

  1. 建立SpringBoot工程gateway,引入閘道器依賴
<!--在parent-demo中單獨建立gateway-demo模組,並加入以下閘道器依賴-->

<!--閘道器-->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>
<!--nacos服務發現依賴-->
<dependency>
    <groupId>com.alibaba.cloud</groupId>
    <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>
  1. 編寫啟動類
package cn.itcast.gateway;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class GatewayApplication {

	public static void main(String[] args) {
		SpringApplication.run(GatewayApplication.class, args);
	}
}
  1. 編寫基礎配置和路由規則
# 路由配置包括
# 1. 路由id:路由的唯一標示
# 2. 路由目標(uri):路由的目標地址,http代表固定地址,lb代表根據服務名負載均衡
# 3. 路由斷言(predicates):判斷路由的規則,
# 4. 路由過濾器(filters):對請求或響應做處理

# 我們將符合`Path` 規則的一切請求,都代理到 `uri`引數指定的地址。
# 本例中,我們將 `/user/**`開頭的請求,代理到`lb://userservice`,lb是負載均衡,根據服務名拉取服務列表,實現負載均衡。

server:
  port: 10010 # 閘道器埠
  
spring:
  application:
    name: gateway # 服務名稱
  cloud:
    nacos:
      server-addr: localhost:8848 # nacos地址
    gateway:
      routes: # 閘道器路由配置
        - id: user-service # 路由id,自定義,只要唯一即可
        # uri: http://127.0.0.1:8081 # 路由的目標地址 http就是固定地址
          uri: lb://userservice # 路由的目標地址 lb就是負載均衡,後面跟服務名稱
          predicates: # 路由斷言,也就是判斷請求是否符合路由規則的條件
            - Path=/user/** # 這個是按照路徑匹配,只要以/user/開頭就符合要求
  1. 重啟測試
/*

GateWay閘道器的port是10010

所以我們訪問http://localhost:10010/user/1時,符合`/user/**`規則,請求轉發到uri:http://userservice/user/1

*/

最後我們給出一張閘道器過程展示圖:

GateWay斷言工廠

下面我們來介紹一下斷言:

  • 我們在配置檔案中寫的斷言規則只是字串,這些字串會被Predicate Factory讀取並處理,轉變為路由判斷的條件

我們下面給出幾個簡單的斷言工廠(我們目前只需要PATH斷言工廠即可):

名稱 說明 示例
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 請求路徑必須符合指定規則 - Path=/red/{segment},/blue/**
Query 請求引數必須包含指定引數 - Query=name, Jack或者- Query=name
RemoteAddr 請求者的ip必須是指定範圍 - RemoteAddr=192.168.1.1/24
Weight 權重處理

GateWay過濾器工廠

我們先簡單介紹一下GateWay過濾器:

  • GatewayFilter是閘道器中提供的一種過濾器,可以對進入閘道器的請求和微服務返回的響應做處理

我們給出一張GateWay過濾器展示圖:

其中Spring提供了31種過濾器,這裡僅僅介紹幾種:

名稱 說明
AddRequestHeader 給當前請求新增一個請求頭
RemoveRequestHeader 移除請求中的一個請求頭
AddResponseHeader 給響應結果中新增一個響應頭
RemoveResponseHeader 從響應結果中移除有一個響應頭
RequestRateLimiter 限制請求的流量

然後我們給出過濾器的使用方法:

# 在yaml中進行過濾器配置,我們可以透過各種過濾器達到不同目的,例如新增請求頭AddRequestHeader

# 我們可以採用服務uri路由名稱單獨給某個微服務設定過濾器
spring:
  cloud:
    gateway:
      routes:
      - id: user-service 
        uri: lb://userservice 
        predicates: 
        - Path=/user/** 
        filters: # 過濾器
        - AddRequestHeader=Truth, Itcast is freaking awesome! # 新增請求頭
        
# 我們也可以採用全域性過濾器對所有微服務進行過濾(default-filters)
spring:
  cloud:
    gateway:
      routes:
      - id: user-service 
        uri: lb://userservice 
        predicates: 
        - Path=/user/**
      default-filters: # 預設過濾項
      - AddRequestHeader=Truth, Itcast is freaking awesome! 

GateWay全域性過濾器

我們在前面學習了過濾器工廠,但是過濾器工廠只能實現已經設計好的方法

如果我們希望攔截業務來完成自己的功能增強或攔截,我們就需要設計過濾器:

  • 全域性過濾器的作用也是處理一切進入閘道器的請求和微服務響應,與GatewayFilter的作用一樣。
  • 區別在於GatewayFilter透過配置定義,處理邏輯是固定的;而GlobalFilter的邏輯需要自己寫程式碼實現。

全域性過濾器的底層原理是實現了GlobalFilter介面:

public interface GlobalFilter {
    /**
     *  處理當前請求,有必要的話透過{@link GatewayFilterChain}將請求交給下一個過濾器處理
     *
     * @param exchange 請求上下文,裡面可以獲取Request、Response等資訊
     * @param chain 用來把請求委託給下一個過濾器 
     * @return {@code Mono<Void>} 返回標示當前過濾器業務結束
     */
    Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain);
}

我們給出一個簡單的業務邏輯:

/*

定義全域性過濾器,攔截請求,判斷請求的引數是否滿足下麵條件:
- 引數中是否有authorization,
- authorization引數值是否為admin

*/

package cn.itcast.gateway.filters;

import org.springframework.cloud.gateway.filter.GatewayFilterChain;
import org.springframework.cloud.gateway.filter.GlobalFilter;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;

@Order(-1)	// Order表示執行優先順序,越小優先順序越高
@Component	
public class AuthorizeFilter implements GlobalFilter {
    @Override
    public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
        // 1.獲取請求引數
        MultiValueMap<String, String> params = exchange.getRequest().getQueryParams();
        // 2.獲取authorization引數
        String auth = params.getFirst("authorization");
        // 3.校驗
        if ("admin".equals(auth)) {
            // 放行
            return chain.filter(exchange);
        }
        // 4.攔截
        // 4.1.禁止訪問,設定狀態碼
        exchange.getResponse().setStatusCode(HttpStatus.FORBIDDEN);
        // 4.2.結束處理
        return exchange.getResponse().setComplete();
    }
}

目前我們已經接觸了三種過濾器:

  • 當前路由的過濾器
  • DefaultFilter
  • GlobalFilter

最後我們需要思考GateWay過濾器的整體優先順序:

  • 每一個過濾器都必須指定一個int型別的order值,order值越小,優先順序越高,執行順序越靠前
  • GlobalFilter透過實現Ordered介面,或者新增@Order註解來指定order值,由我們自己指定
  • 路由過濾器和defaultFilter的order由Spring指定,預設是按照宣告順序從1遞增
  • 當過濾器的order值一樣時,會按照 defaultFilter > 路由過濾器 > GlobalFilter的順序執行

結束語

這篇文章中介紹了SpringCloud的整體框架及其知識點,屬於微服務的入門內容,下面我們會繼續學習微服務內容~

附錄

該文章屬於學習內容,具體參考B站黑馬程式設計師的SpringCloud課程

這裡附上影片連結:微服務技術棧導學1_嗶哩嗶哩_bilibili

相關文章