Spring Boot Dubbo 入門

weixin_42073629發表於2020-04-19

1. 概述

在 2019.05.21 號,在經歷了 1 年多的孵化,Dubbo 終於迎來了 Apache 畢業。在這期間,Dubbo 做了比較多的功能迭代,提供了 NodeJS、Python、Go 等語言的支援,也舉辦了多次社群活動,在網上的“罵聲”也少了。

艿艿:事實上,大多數成熟的開源專案,都是 KPI 驅動,又或者背後有商業化支撐。

作為一個長期使用,並且堅持使用 Dubbo 的開發者,還是比較愉快的。可能,又經歷了一次技術正確的選擇。當然,更愉快的是,Spring Cloud Alibaba 貌似,也已經完成孵化,雙劍合併,biubiubiu 。

可能胖友有些胖友對 Dubbo 不是很瞭解,這裡艿艿先簡單介紹下:

FROM Dubbo 官網

Apache Dubbo |ˈdʌbəʊ| 是一款高效能、輕量級的開源 Java RPC 框架,它提供了三大核心能力:面向介面的遠端方法呼叫,智慧容錯和負載均衡,以及服務自動註冊和發現。

Dubbo 整體架構

圖中,一共涉及到 5 個角色:

  • Registry 註冊中心,用於服務的註冊與發現。
  • Provider 服務提供者,通過向 Registry 註冊服務。
  • Consumer 服務消費者,通過從 Registry 發現服務。後續直接呼叫 Provider ,無需經過 Registry 。
  • Monitor 監控中心,統計服務的呼叫次數和呼叫時間。
  • Container 服務執行容器。

FROM 《Dubbo 文件 —— 架構》

呼叫關係說明(注意,和上圖的數字,和下面的步驟是一一對應的):

    1. 服務容器負責啟動,載入,執行服務提供者。
    1. 服務提供者在啟動時,向註冊中心註冊自己提供的服務。
    1. 服務消費者在啟動時,向註冊中心訂閱自己所需的服務。
    1. 註冊中心返回服務提供者地址列表給消費者,如果有變更,註冊中心將基於長連線推送變更資料給消費者。
    1. 服務消費者,從提供者地址列表中,基於軟負載均衡演算法,選一臺提供者進行呼叫,如果呼叫失敗,再選另一臺呼叫。
    1. 服務消費者和提供者,在記憶體中累計呼叫次數和呼叫時間,定時每分鐘傳送一次統計資料到監控中心。

本文的重心,在於一起入門 Provider 和 Consumer 的程式碼編寫,這也是實際專案開發中,我們涉及到的角色。

Dubbo 提供了比較多的配置方式,日常開發中主要使用的是 XML 配置 和 註解配置 。我們分別會在 「2. XML 配合」 和 「3. 註解配置」 小節來入門。

考慮到現在 Dubbo 已經提供了 dubbo-spring-boot-project 專案,整合到 Spring Boot 體系中,而大家都基本採用 Spring Boot 框架,所以我們就不像 Dubbo 官方文件 一樣,提供的是 Spring 環境下的示例,而是 Spring Boot 環境下

2. XML 配置

示例程式碼對應倉庫:lab-30-dubbo-xml-demo 。

本小節的示例,需要建立三個 Maven 專案,如下圖所示:

ä¸ä¸ª Maven 项ç®

  • user-rpc-service-api 專案:服務介面,定義 Dubbo Service API 介面,提供給消費者使用。詳細程式碼,我們在 「2.1 API」 講解。
  • user-rpc-service-provider 專案:服務提供者,實現 user-rpc-service-api 專案定義的 Dubbo Service API 介面,提供相應的服務。詳細程式碼,我們在 「2.2 Provider」 中講解。
  • user-rpc-service-consumer 專案:服務消費者,會呼叫 user-rpc-service-provider 專案提供的 Dubbo Service 服務。詳細程式碼,我們在 「2.3 Consumer」 中講解。

2.1 API

對應 user-rpc-service-api 專案,服務介面,定義 Dubbo Service API 介面,提供給消費者使用。

2.1.1 UserDTO

在 cn.iocoder.springboot.lab30.rpc.dto 包下,建立用於 Dubbo Service 傳輸類。這裡,我們建立 UserDTO 類,使用者資訊 DTO 。程式碼如下:

// UserDTO.java

public class UserDTO implements Serializable {

    /**
     * 使用者編號
     */
    private Integer id;
    /**
     * 暱稱
     */
    private String name;
    /**
     * 性別
     */
    private Integer gender;
    
    // ... 省略 set/get 方法
}

注意,要實現 java.io.Serializable 介面。因為,Dubbo RPC 會涉及遠端通訊,需要序列化和反序列化。

2.1.2 UserRpcService

在 cn.iocoder.springboot.lab30.rpc.api 包下,建立 Dubbo Service API 介面。這裡,我們建立 UserRpcService 介面,使用者服務 RPC Service 介面。程式碼如下:

// UserRpcService.java

public interface UserRpcService {

    /**
     * 根據指定使用者編號,獲得使用者資訊
     *
     * @param id 使用者編號
     * @return 使用者資訊
     */
    UserDTO get(Integer id);

}

2.2 Provider

對應 user-rpc-service-provider 專案,服務提供者,實現 user-rpc-service-api 專案定義的 Dubbo Service API 介面,提供相應的服務。

2.2.1 引入依賴

在 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>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>user-rpc-service</artifactId>

    <dependencies>
        <!-- 引入定義的 Dubbo API 介面 -->
        <dependency>
            <groupId>cn.iocoder.springboot.labs</groupId>
            <artifactId>user-rpc-service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <!-- 引入 Spring Boot 依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <!-- 實現對 Dubbo 的自動化配置 -->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
            <version>2.7.4.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
            <version>2.7.4.1</version>
        </dependency>

        <!-- 使用 Zookeeper 作為註冊中心 -->
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>2.13.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>2.13.0</version>
        </dependency>

    </dependencies>

</project>
  • 因為我們希望實現對 Dubbo 的自動化配置,所以引入 dubbo-spring-boot-starter 依賴。

  • 因為我們希望使用 Zookeeper 作為註冊中心,所以引入 curator-framework 和 curator-recipes 依賴。可能胖友不太瞭解 Apache Curator 框架,這裡我們看一段簡介:

    FROM https://www.oschina.net/p/curator

    Zookeeper 的客戶端呼叫過於複雜,Apache Curator 就是為了簡化Zookeeper 客戶端呼叫而生,利用它,可以更好的使用 Zookeeper。 * 雖然說,目前阿里正在大力推廣 Nacos 作為 Dubbo 的註冊中心,但是大多數團隊,採用的還是 Zookeeper 為主。 * 對了,如果胖友不知道怎麼安裝 Zookeeper ,可以看看 《芋道 Zookeeper 極簡入門》 文章。

2.2.2 應用配置檔案

在 resources 目錄下, 建立 application.yml 配置檔案,新增 Dubbo 相關的配置,如下:

# dubbo 配置項,對應 DubboConfigurationProperties 配置類
dubbo:
  # Dubbo 應用配置
  application:
    name: user-service-provider # 應用名
  # Dubbo 註冊中心配
  registry:
    address: zookeeper://127.0.0.1:2181 # 註冊中心地址。個鞥多註冊中心,可見 http://dubbo.apache.org/zh-cn/docs/user/references/registry/introduction.html 文件。
  # Dubbo 服務提供者協議配置
  protocol:
    port: -1 # 協議埠。使用 -1 表示隨機埠。
    name: dubbo # 使用 `dubbo://` 協議。更多協議,可見 http://dubbo.apache.org/zh-cn/docs/user/references/protocol/introduction.html 文件
  # Dubbo 服務提供者配置
  provider:
    timeout: 1000 # 【重要】遠端服務呼叫超時時間,單位:毫秒。預設為 1000 毫秒,胖友可以根據自己業務修改
    UserRpcService:
      version: 1.0.0

2.2.3 UserRpcServiceImpl

在 cn.iocoder.springboot.lab30.rpc.service 包下,建立 Dubbo Service 實現類。這裡,我們建立 UserRpcServiceImpl 類,使用者服務 RPC Service 實現類。程式碼如下:

// UserRpcServiceImpl.java

@Service
public class UserRpcServiceImpl implements UserRpcService {

    @Override
    public UserDTO get(Integer id) {
        return new UserDTO().setId(id)
                .setName("沒有暱稱:" + id)
                .setGender(id % 2 + 1); // 1 - 男;2 - 女
    }

}
  • 實現 UserRpcService 介面,提供 UserRpcService Dubbo 服務。
  • 注意,在類上新增了 Spring @Service 註解,暴露出 UserRpcServiceImpl Bean 物件。? 後續,我們會將該 Bean 暴露成 UserRpcService Dubbo 服務,註冊其到註冊中心中,並提供相應的 Dubbo 服務。

2.2.4 Dubbo XML 配置檔案

在 resources 目錄下, 建立 dubbo.xml 配置檔案,新增 Dubbo 的 Service 服務提供者,如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://dubbo.apache.org/schema/dubbo
       http://dubbo.apache.org/schema/dubbo/dubbo.xsd">

    <!-- 服務提供者暴露服務配置 -->
    <dubbo:service ref="userRpcServiceImpl" interface="cn.iocoder.springboot.lab30.rpc.api.UserRpcService"
        version="${dubbo.provider.UserRpcService.version}" />

</beans>
  • 使用 Dubbo 自定義的 Spring <dubbo:service> 標籤,配置我們 「2.2.3 UserRpcServiceImpl」 成 UserRpcService 的 Dubbo 服務提供者。

更多 <dubbo:service> 標籤的屬性的說明,可見 《Dubbo 文件 —— dubbo:service》 。

2.2.5 ProviderApplication

建立 ProviderApplication 類,用於啟動該專案,提供 Dubbo 服務。程式碼如下:

// ProviderApplication.java

@SpringBootApplication
@ImportResource("classpath:dubbo.xml")
public class ProviderApplication {

    public static void main(String[] args) {
        // 啟動 Spring Boot 應用
        SpringApplication.run(ProviderApplication.class, args);
    }

}
  • 在類上,新增 @ImportResource 註解,引入 dubbo.xml 配置檔案。

執行 #main(String[] args) 方法,啟動專案。控制檯列印日誌如下:

// ... 省略其它日誌

2019-12-01 22:40:34.721  INFO 64176 --- [pool-1-thread-1] .b.c.e.AwaitingNonWebApplicationListener :  [Dubbo] Current Spring Boot Application is await...
  • 看到該日誌內容,意味著啟動成功。

我們來使用 Zookeeper 客戶端,檢視 UserRpcService 服務是否註冊成功。操作流程如下:

# 使用 Zookeeper 自帶的客戶端,連線到 Zookeeper 伺服器
$ bin/zkCli.sh

# 檢視 /dubbo 目錄下的所有服務。
# 此時,我們檢視到了 UserRpcService 服務
$ ls /dubbo
[cn.iocoder.springboot.lab30.rpc.api.UserRpcService]

# 檢視 /dubbo/cn.iocoder.springboot.lab30.rpc.api.UserRpcService 目錄下的儲存情況。
# 此時,我們看到了 consumers 消費者資訊,providers 提供者資訊,routers 路由資訊,configurators 配置資訊。
$ ls /dubbo/cn.iocoder.springboot.lab30.rpc.api.UserRpcService
[consumers, configurators, routers, providers]

# 檢視 UserRpcService 服務的節點列表
# 此時,可以看到有一個節點,就是我們剛啟動的服務提供者。
$ ls /dubbo/cn.iocoder.springboot.lab30.rpc.api.UserRpcService/providers
[dubbo%3A%2F%2F10.171.1.115%3A20880%2Fcn.iocoder.springboot.lab30.rpc.api.UserRpcService%3Fanyhost%3Dtrue%26application%3Duser-service-provider%26bean.name%3Dcn.iocoder.springboot.lab30.rpc.api.UserRpcService%26deprecated%3Dfalse%26dubbo%3D2.0.2%26dynamic%3Dtrue%26generic%3Dfalse%26interface%3Dcn.iocoder.springboot.lab30.rpc.api.UserRpcService%26methods%3Dget%26pid%3D64176%26release%3D2.7.4.1%26revision%3D1.0.0%26side%3Dprovider%26timeout%3D1000%26timestamp%3D1575211234365%26version%3D1.0.0]

想要了解更多 Dubbo 是如何使用 Zookeeper 儲存資料的,可以看看 《Dubbo 文件 —— Zookeeper 註冊中心》 文件。

2.3 Consumer

對應 user-rpc-service-consumer 專案,服務消費者,會呼叫 user-rpc-service-provider 專案提供的 Dubbo Service 服務。

2.3.1 引入依賴

在 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>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>user-rpc-service-consumer</artifactId>

    <dependencies>
        <!-- 引入定義的 Dubbo API 介面 -->
        <dependency>
            <groupId>cn.iocoder.springboot.labs</groupId>
            <artifactId>user-rpc-service-api</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <!-- 引入 Spring Boot 依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <!-- 實現對 Dubbo 的自動化配置 -->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
            <version>2.7.4.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
            <version>2.7.4.1</version>
        </dependency>

        <!-- 使用 Zookeeper 作為註冊中心 -->
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>2.13.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>2.13.0</version>
        </dependency>

    </dependencies>

</project>

2.3.2 應用配置檔案

在 resources 目錄下, 建立 application.yml 配置檔案,新增 Dubbo 相關的配置,如下:

# dubbo 配置項,對應 DubboConfigurationProperties 配置類
dubbo:
  # Dubbo 應用配置
  application:
    name: user-service-consumer # 應用名
  # Dubbo 註冊中心配置
  registry:
    address: zookeeper://127.0.0.1:2181 # 註冊中心地址。個鞥多註冊中心,可見 http://dubbo.apache.org/zh-cn/docs/user/references/registry/introduction.html 文件。
  # Dubbo 消費者配置
  consumer:
    timeout: 1000 # 【重要】遠端服務呼叫超時時間,單位:毫秒。預設為 1000 毫秒,胖友可以根據自己業務修改
    UserRpcService:
      version: 1.0.0

2.3.3 Dubbo XML 配置檔案

在 resources 目錄下,建立 dubbo.xml 配置檔案,新增 Dubbo 的 Service 服務引用者,如下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:dubbo="http://dubbo.apache.org/schema/dubbo"
       xmlns="http://www.springframework.org/schema/beans"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://dubbo.apache.org/schema/dubbo
       http://dubbo.apache.org/schema/dubbo/dubbo.xsd">

    <!-- 服務消費者引用服務配置 -->
    <dubbo:reference id="userService" interface="cn.iocoder.springboot.lab30.rpc.api.UserRpcService"
                     version="${dubbo.consumer.UserRpcService.version}"/>

</beans>
  • 使用 Dubbo 自定義的 Spring <dubbo:reference> 標籤,引用 UserRpcService 介面對應的 Dubbo Service 服務,並建立一個 Bean 編號為 "userService" 的 Bean 物件。這樣,我們在 Spring 中,就可以直接注入 UserRpcService Bean ,後續就可以像一個“本地”的 UserRpcService 進行呼叫使用。

更多 <dubbo:reference> 標籤的屬性的說明,可見 《Dubbo 文件 —— dubbo:reference》 。

2.3.4 ConsumerApplication

建立 ConsumerApplication 類,用於啟動該專案,呼叫 Dubbo 服務。程式碼如下:

// ConsumerApplication.java

@SpringBootApplication
@ImportResource("classpath:dubbo.xml")
public class ConsumerApplication {

    public static void main(String[] args) {
        // 啟動 Spring Boot 應用
        ConfigurableApplicationContext context = SpringApplication.run(ConsumerApplication.class, args);
    }

    @Component
    public class UserRpcServiceTest implements CommandLineRunner {

        private final Logger logger = LoggerFactory.getLogger(getClass());

        @Resource
        private UserRpcService userRpcService;

        @Override
        public void run(String... args) throws Exception {
            UserDTO user = userRpcService.get(1);
            logger.info("[run][發起一次 Dubbo RPC 請求,獲得使用者為({})", user);
        }

    }

}
  • 在類上,新增 @ImportResource 註解,引入 dubbo.xml 配置檔案。
  • 在 UserRpcServiceTest 中,我們使用 @Resource 註解,引用通過 <dubbo:reference /> 配置的引用的 UserRpcService 服務對應的 UserRpcService Bean 。

執行 #main(String[] args) 方法,啟動專案。控制檯列印日誌如下:

2019-12-01 23:15:47.380  INFO 65726 --- [           main] r.ConsumerApplication$UserRpcServiceTest : [run][發起一次 Dubbo RPC 請求,獲得使用者為(cn.iocoder.springboot.lab30.rpc.dto.UserDTO@a0a9fa5)
  • 我們在應用啟動完成後,成功的發起了一次 UserRpcService 的 Dubbo RPC 的呼叫。

我們來使用 Zookeeper 客戶端,檢視 UserRpcService 服務是否多了一個消費者。操作流程如下:

# 使用 Zookeeper 自帶的客戶端,連線到 Zookeeper 伺服器
$ bin/zkCli.sh

# 檢視 UserRpcService 服務的消費者列表
# 此時,可以看到有一個節點,就是我們剛啟動的服務消費者。
$ ls /dubbo/cn.iocoder.springboot.lab30.rpc.api.UserRpcService/consumers
[consumer%3A%2F%2F10.171.1.115%2Fcn.iocoder.springboot.lab30.rpc.api.UserRpcService%3Fapplication%3Duser-service-consumer%26category%3Dconsumers%26check%3Dfalse%26dubbo%3D2.0.2%26interface%3Dcn.iocoder.springboot.lab30.rpc.api.UserRpcService%26lazy%3Dfalse%26methods%3Dget%26pid%3D65726%26qos.enable%3Dfalse%26release%3D2.7.4.1%26revision%3D1.0.0%26side%3Dconsumer%26sticky%3Dfalse%26timeout%3D1000%26timestamp%3D1575213346748%26version%3D1.0.0]

至此,我們已經完成了使用 XML 配置的方式,在 Spring Boot 中使用 Dubbo 的入門。? 雖然篇幅長了一點點,但是還是比較簡單的。個人建議的話,此時此刻僅僅是看到這裡,但是並沒有手敲程式碼的胖友,可以趕緊開啟 IDEA 自己敲(“抄”)一波,嘿嘿。

3. 註解配置

示例程式碼對應倉庫:lab-30-dubbo-annotations-demo 。

本小節的示例,需要建立三個 Maven 專案,如下圖所示:三個 Maven 專案

  • user-rpc-service-api-02 專案:服務介面,定義 Dubbo Service API 介面,提供給消費者使用。詳細程式碼,我們在 「3.1 API」 講解。
  • user-rpc-service-provider-02 專案:服務提供者,實現 user-rpc-service-api-02 專案定義的 Dubbo Service API 介面,提供相應的服務。詳細程式碼,我們在 「3.2 Provider」 中講解。
  • user-rpc-service-consumer-02 專案:服務消費者,會呼叫 user-rpc-service-provider-02 專案提供的 Dubbo Service 服務。詳細程式碼,我們在 「3.3 Consumer」 中講解。

? 本小節的內容上,和 「2.1 XML 配置」 會比較接近,所以會講的相對簡略,重點說差異。

艿艿:為了保證閱讀體驗,即使一致的內容,艿艿還是貼一遍比較好。

3.1 API

對應 user-rpc-service-api-02 專案,服務介面,定義 Dubbo Service API 介面,提供給消費者使用。

3.1.1 UserDTO

和 「2.1.1 UserDTO」 一致。

在 cn.iocoder.springboot.lab30.rpc.dto 包下,建立用於 Dubbo Service 傳輸類。這裡,我們建立 UserDTO 類,使用者資訊。程式碼如下:

// UserDTO.java

public class UserDTO implements Serializable {

    /**
     * 使用者編號
     */
    private Integer id;
    /**
     * 暱稱
     */
    private String name;
    /**
     * 性別
     */
    private Integer gender;
    
    // ... 省略 set/get 方法
}

注意,要實現 java.io.Serializable 介面。因為,Dubbo RPC 會涉及遠端通訊,需要序列化和反序列化。

3.1.2 UserRpcService

和 3.1.2 UserRpcService」 一致。

在 cn.iocoder.springboot.lab30.rpc.api 包下,建立 Dubbo Service API 介面。這裡,我們建立 UserRpcService 介面,使用者服務 RPC Service 介面。程式碼如下:

// UserRpcService.java

public interface UserRpcService {

    /**
     * 根據指定使用者編號,獲得使用者資訊
     *
     * @param id 使用者編號
     * @return 使用者資訊
     */
    UserDTO get(Integer id);

}

3.2 Provider

對應 user-rpc-service-provider-02 專案,服務提供者,實現 user-rpc-service-api 專案定義的 Dubbo Service API 介面,提供相應的服務。

3.2.1 引入依賴

和 「2.2.1 引入依賴」 一致。

在 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>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>user-rpc-service-provider-02</artifactId>

    <dependencies>
        <!-- 引入定義的 Dubbo API 介面 -->
        <dependency>
            <groupId>cn.iocoder.springboot.labs</groupId>
            <artifactId>user-rpc-service-api-02</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <!-- 引入 Spring Boot 依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <!-- 實現對 Dubbo 的自動化配置 -->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
            <version>2.7.4.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
            <version>2.7.4.1</version>
        </dependency>

        <!-- 使用 Zookeeper 作為註冊中心 -->
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>2.13.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>2.13.0</version>
        </dependency>

    </dependencies>

</project>

3.2.2 應用配置檔案

在 resources 目錄下, 建立 application.yml 配置檔案,新增 Dubbo 相關的配置,如下:

# dubbo 配置項,對應 DubboConfigurationProperties 配置類
dubbo:
  # Dubbo 應用配置
  application:
    name: user-service-provider # 應用名
  # Dubbo 註冊中心配
  registry:
    address: zookeeper://127.0.0.1:2181 # 註冊中心地址。個鞥多註冊中心,可見 http://dubbo.apache.org/zh-cn/docs/user/references/registry/introduction.html 文件。
  # Dubbo 服務提供者協議配置
  protocol:
    port: -1 # 協議埠。使用 -1 表示隨機埠。
    name: dubbo # 使用 `dubbo://` 協議。更多協議,可見 http://dubbo.apache.org/zh-cn/docs/user/references/protocol/introduction.html 文件
  # Dubbo 服務提供者配置
  provider:
    timeout: 1000 # 【重要】遠端服務呼叫超時時間,單位:毫秒。預設為 1000 毫秒,胖友可以根據自己業務修改
    UserRpcService:
      version: 1.0.
  # 配置掃描 Dubbo 自定義的 @Service 註解,暴露成 Dubbo 服務提供者
  scan:
    base-packages: cn.iocoder.springboot.lab30.rpc.service

和 「2.2.2 應用配置」 基本一致,差異在於多出了 dubbo.scan.base-packages 配置項,配置掃描的基礎路徑,後續會根據該路徑,掃描使用了 Dubbo 自定義的 @Service 註解的 Service 類們,將它們暴露成 Dubbo 服務提供者。

如此,我們就不需要使用 「2.2.4 Dubbo XML 配置檔案」 ,配置暴露的 Service 服務,而是通過 Dubbo 定義的 @Service 註解。

3.2.3 UserRpcServiceImpl

在 cn.iocoder.springboot.lab30.rpc.service 包下,建立 Dubbo Service 實現類。這裡,我們建立 UserRpcServiceImpl 類,使用者服務 RPC Service 實現類。程式碼如下:

// UserRpcServiceImpl.java

@Service(version = "${dubbo.provider.UserRpcService.version}")
public class UserRpcServiceImpl implements UserRpcService {

    @Override
    public UserDTO get(Integer id) {
        return new UserDTO().setId(id)
                .setName("沒有暱稱:" + id)
                .setGender(id % 2 + 1); // 1 - 男;2 - 女
    }

}
  • 在類上,我們新增的是 Dubbo 定義的 @Service 註解。並且,在該註解裡,我們可以新增該 Service 服務的配置。當然,每個屬性和 <dubbo:service /> 標籤是基本一致的。也因此,每個屬性的說明,還可見 《Dubbo 文件 —— dubbo:service》 。

3.2.4 ProviderApplication

建立 ProviderApplication 類,用於啟動該專案,提供 Dubbo 服務。程式碼如下:

// ProviderApplication.java

@SpringBootApplication
public class ProviderApplication {

    public static void main(String[] args) {
        // 啟動 Spring Boot 應用
        SpringApplication.run(ProviderApplication.class, args);
    }

}
  • 在類上,無需新增 @ImportResource 註解,引入 dubbo.xml 配置檔案。

艿艿:後續的操作,和 「2.2.5 ProviderApplication」 是一致的,這裡就不重複贅述了。

3.3 Consumer

對應 user-rpc-service-consumer-02 專案,服務消費者,會呼叫 user-rpc-service-provider-02 專案提供的 Dubbo Service 服務。

3.3.1 引入依賴

和 「2.3.1 引入依賴」 一致。

在 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>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.2.1.RELEASE</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>user-rpc-service-consumer-02</artifactId>

    <dependencies>
        <!-- 引入定義的 Dubbo API 介面 -->
        <dependency>
            <groupId>cn.iocoder.springboot.labs</groupId>
            <artifactId>user-rpc-service-api-02</artifactId>
            <version>1.0-SNAPSHOT</version>
        </dependency>

        <!-- 引入 Spring Boot 依賴 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <!-- 實現對 Dubbo 的自動化配置 -->
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo</artifactId>
            <version>2.7.4.1</version>
        </dependency>
        <dependency>
            <groupId>org.apache.dubbo</groupId>
            <artifactId>dubbo-spring-boot-starter</artifactId>
            <version>2.7.4.1</version>
        </dependency>

        <!-- 使用 Zookeeper 作為註冊中心 -->
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-framework</artifactId>
            <version>2.13.0</version>
        </dependency>
        <dependency>
            <groupId>org.apache.curator</groupId>
            <artifactId>curator-recipes</artifactId>
            <version>2.13.0</version>
        </dependency>

    </dependencies>

</project>

3.3.2 應用配置檔案

和 「2.3.2 應用配置檔案」 一致。

# dubbo 配置項,對應 DubboConfigurationProperties 配置類
dubbo:
  # Dubbo 應用配置
  application:
    name: user-service-consumer # 應用名
  # Dubbo 註冊中心配置
  registry:
    address: zookeeper://127.0.0.1:2181 # 註冊中心地址。個鞥多註冊中心,可見 http://dubbo.apache.org/zh-cn/docs/user/references/registry/introduction.html 文件。
  # Dubbo 消費者配置
  consumer:
    timeout: 1000 # 【重要】遠端服務呼叫超時時間,單位:毫秒。預設為 1000 毫秒,胖友可以根據自己業務修改
    UserRpcService:
      version: 1.0.0

3.3.3 ConsumerApplication

建立 ConsumerApplication 類,用於啟動該專案,呼叫 Dubbo 服務。程式碼如下:

// ConsumerApplication.java

@SpringBootApplication
public class ConsumerApplication {

    public static void main(String[] args) {
        // 啟動 Spring Boot 應用
        ConfigurableApplicationContext context = SpringApplication.run(ConsumerApplication.class, args);
    }

    @Component
    public class UserRpcServiceTest implements CommandLineRunner {

        private final Logger logger = LoggerFactory.getLogger(getClass());

        @Reference(version = "${dubbo.consumer.UserRpcService.version}")
        private UserRpcService userRpcService;

        @Override
        public void run(String... args) throws Exception {
            UserDTO user = userRpcService.get(1);
            logger.info("[run][發起一次 Dubbo RPC 請求,獲得使用者為({})", user);
        }

    }

}
  • 在類上,無需新增 @ImportResource 註解,引入 dubbo.xml 配置檔案。
  • 在 UserRpcServiceTest 中,我們使用 Dubbo 定義的 @Reference 註解,**“直接”**引用的 UserRpcService 服務對應的 UserRpcService Bean 。並且,在該註解裡,我們可以新增該 Service 服務的配置。當然,每個屬性和 <dubbo:reference /> 標籤是基本一致的。也因此,每個屬性的說明,還可見 《Dubbo 文件 —— dubbo:reference》 。

艿艿:後續的操作,和 「2.3.4 ConsumerApplication」 是一致的,這裡就不重複贅述了。

3.4 選擇註解還是 XML 配置?

艿艿個人傾向的話,偏向使用 XML 配置

主要原因是,@Reference 註解,每次引用服務的時候,都需要在註解上新增好多配置的屬性。這樣,服務的引用的配置後就散落到各個類裡了。

雖然說,我們可以把 @Reference 註解的配置的屬性值,放到 application.yaml 等等配置檔案裡,但是如果我們要給相同 Service 的多個 @Reference 增加新的配置屬性時,就要每個註解都修改一遍。

對於這種情況,XML 配置的方式,只要修改一下該 Service 的 XML 配置,就可以全部生效了。

4. 引數驗證

引數校驗,對於提供 API 呼叫的服務來說,必然是必不可少的。在 《芋道 Spring Boot 引數校驗 Validation 入門》 中,我們已經看了如何在 SpringMVC 和本地的 Service 使用引數校驗的示例。

本小節,我們來學習下,如何在 Dubbo RPC Service 中,使用引數校驗。在 《Dubbo 文件 —— 引數驗證》 中,對該功能的描述如下:

引數驗證功能是基於 JSR303 實現的,使用者只需標識 JSR303 標準的驗證 annotation,並通過宣告 filter 來實現驗證。

下面,我們開始在 「2. XML 配置」 小節的 lab-30-dubbo-xml-demo 示例專案,進行修改,新增引數校驗的功能。

4.1 API

本小節,我們來看看對 user-rpc-service-api 專案的改造。

4.1.1 引入依賴

修改 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>lab-30-dubbo-xml-demo</artifactId>
        <groupId>cn.iocoder.springboot.labs</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>user-rpc-service-api</artifactId>

    <dependencies>
        <!-- 引數校驗相關依賴 -->
        <dependency>
            <groupId>javax.validation</groupId>
            <artifactId>validation-api</artifactId> <!-- JSR 引數校驗規範 API -->
            <version>2.0.1.Final</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate.validator</groupId>
            <artifactId>hibernate-validator</artifactId> <!-- JSR 引數校驗規範實現,我們使用 hibernate-validator -->
            <version>6.0.18.Final</version>
        </dependency>
        <dependency>
            <groupId>org.glassfish</groupId>
            <artifactId>javax.el</artifactId> <!-- 可能涉及到 EL 表達,所以引入,否則 hibernate-validator 在初始化會報錯 -->
            <version>3.0.1-b11</version>
        </dependency>
    </dependencies>

</project>

4.1.2 UserAddDTO

在 cn.iocoder.springboot.lab30.rpc.dto 包下,建立 UserAddDTO 類,使用者新增 DTO。程式碼如下:

// UserAddDTO.java

public class UserAddDTO implements Serializable {

    /**
     * 暱稱
     */
    @NotEmpty(message = "暱稱不能為空")
    @Length(min = 5, max = 16, message = "賬號長度為 5-16 位")
    private String name;
    /**
     * 性別
     */
    @NotNull(message = "性別不能為空")
    private Integer gender;
    
    // ... 省略 set/get 方法
}
  • 在 name 和 gender 屬性上,我們新增了引數校驗的註解。

4.1.3 UserRpcService

修改 UserRpcService 介面,程式碼如下:

// UserRpcService.java

public interface UserRpcService {

    /**
     * 根據指定使用者編號,獲得使用者資訊
     *
     * @param id 使用者編號
     * @return 使用者資訊
     */
    UserDTO get(@NotNull(message = "使用者編號不能為空") Integer id)
            throws ConstraintViolationException;

    /**
     * 新增新使用者,返回新新增的使用者編號
     *
     * @param addDTO 新增的使用者資訊
     * @return 使用者編號
     */
    Integer add(UserAddDTO addDTO)
            throws ConstraintViolationException;

}
  • 在已有的 #get(Integer id) 方法上,新增 @NotNull 註解,校驗使用者編號不允許傳空。
  • 新增 #add(UserAddDTO addDTO) 方法,新增新使用者,返回新新增的使用者編號。我們已經在 UserAddDTO 類,新增了相應的引數校驗的註解。
  • 注意,因為引數校驗不通過時,會丟擲 ConstraintViolationException 異常,所以需要在介面的方法,顯示使用 throws 註明。具體的原因,可以看看 《淺談 Dubbo 的 ExceptionFilter 異常處理》 文章,瞭解下 Dubbo 的異常處理機制。

4.2 Provider

本小節,我們來看看對 user-rpc-service-provider 專案的改造。

4.2.1 UserRpcServiceImpl

修改 UserRpcServiceImpl 類,簡單實現下 #add(UserAddDTO addDTO) 方法。程式碼如下:

// UserRpcServiceImpl.java

@Override
public Integer add(UserAddDTO addDTO) {
    return (int) (System.currentTimeMillis() / 1000); // 嘿嘿,隨便返回一個 id
}

4.2.2 Dubbo XML 配置檔案

修改 dubbo.xml 配置檔案,開啟 UserRpcService 的引數校驗功能。配置如下:

<dubbo:service ref="userRpcServiceImpl" interface="cn.iocoder.springboot.lab30.rpc.api.UserRpcService"
    version="${dubbo.provider.UserRpcService.version}" validation="true" />
  • 這裡,我們將 validation 設定為 "true" ,開啟 Dubbo 服務提供者的 UserRpcService 服務的引數校驗的功能。

? 如果胖友想把 Dubbo 服務提供者的所有 Service 服務的引數校驗都開啟,可以修改 application.yaml 配置檔案,增加 dubbo.provider.validation = true 配置。

4.3 Consumer

本小節,我們來看看對 user-rpc-service-consumer 專案的改造。

4.3.1 Dubbo XML 配置檔案

修改 dubbo.xml 配置檔案,開啟 UserRpcService 的引數校驗功能。配置如下:

<dubbo:reference id="userService" interface="cn.iocoder.springboot.lab30.rpc.api.UserRpcService"
                 version="${dubbo.consumer.UserRpcService.version}" validation="true" />
  • 這裡,我們將 validation 設定為 "true" ,開啟 Dubbo 服務消費者的 UserRpcService 服務的引數校驗的功能。

? 如果胖友想把 Dubbo 服務消費者的所有 Service 服務的引數校驗都開啟,可以修改 application.yaml 配置檔案,增加 dubbo.consumer.validation = true 配置。

可能胖友會有疑惑,服務提供者和服務消費者的 validation = true ,都是開啟引數校驗規則,會有什麼區別呢?Dubbo 內建 ValidationFilter 過濾器,實現引數校驗的功能,可作用於服務提供者和服務消費者。效果如下:

  • 如果服務消費者開啟引數校驗,請求引數校驗不通過時,結束請求,丟擲 ConstraintViolationException 異常。即,不會向服務提供者發起請求
  • 如果服務提供者開啟引數校驗,請求引數校驗不通過時,結束請求,丟擲 ConstraintViolationException 異常。即,不會執行後續的業務邏輯

實際專案在使用時,至少要開啟服務提供者的引數校驗功能

4.3.2 ConsumerApplication

修改 ConsumerApplication 類,增加呼叫 UserRpcService 服務時,引數校驗不通過的示例。程式碼如下:

// ConsumerApplication.java

@Component
public class UserRpcServiceTest02 implements CommandLineRunner {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Resource
    private UserRpcService userRpcService;

    @Override
    public void run(String... args) throws Exception {
        // 獲得使用者
        try {
            // 發起呼叫
            UserDTO user = userRpcService.get(null); // 故意傳入空的編號,為了校驗編號不通過
            logger.info("[run][發起一次 Dubbo RPC 請求,獲得使用者為({})]", user);
        } catch (Exception e) {
            logger.error("[run][獲得使用者發生異常,資訊為:[{}]", e.getMessage());
        }

        // 新增使用者
        try {
            // 建立 UserAddDTO
            UserAddDTO addDTO = new UserAddDTO();
            addDTO.setName("yudaoyuanmayudaoyuanma"); // 故意把名字打的特別長,為了校驗名字不通過
            addDTO.setGender(null); // 不傳遞性別,為了校驗性別不通過
            // 發起呼叫
            userRpcService.add(addDTO);
            logger.info("[run][發起一次 Dubbo RPC 請求,新增使用者為({})]", addDTO);
        } catch (Exception e) {
            logger.error("[run][新增使用者發生異常,資訊為:[{}]", e.getMessage());
        }
    }

}
  • 新增了兩段程式碼,分別呼叫 UserRpcService 服務的 #get(Integer id) 和 #add(UserAddDTO addDTO) 方法,並且是引數不符合校驗條件的示例。

執行 #main(String[] args) 方法,啟動專案。控制檯列印日誌如下:

// 呼叫 UserRpcService 服務的 `#get(Integer id)` 方法,引數不通過
2019-12-01 13:19:08.836 ERROR 7055 --- [           main] ConsumerApplication$UserRpcServiceTest02 : [run][獲得使用者發生異常,資訊為:[Failed to validate service: cn.iocoder.springboot.lab30.rpc.api.UserRpcService, method: get, cause: [ConstraintViolationImpl{interpolatedMessage='使用者編號不能為空', propertyPath=getArgument0, rootBeanClass=class cn.iocoder.springboot.lab30.rpc.api.UserRpcService_GetParameter_java.lang.Integer, messageTemplate='使用者編號不能為空'}]]

// 呼叫 UserRpcService 服務的 `#add(UserAddDTO addDTO)` 方法,引數不通過
2019-12-01 13:19:08.840 ERROR 7055 --- [           main] ConsumerApplication$UserRpcServiceTest02 : [run][新增使用者發生異常,資訊為:[Failed to validate service: cn.iocoder.springboot.lab30.rpc.api.UserRpcService, method: add, cause: [ConstraintViolationImpl{interpolatedMessage='性別不能為空', propertyPath=gender, rootBeanClass=class cn.iocoder.springboot.lab30.rpc.dto.UserAddDTO, messageTemplate='性別不能為空'}, ConstraintViolationImpl{interpolatedMessage='賬號長度為 5-16 位', propertyPath=name, rootBeanClass=class cn.iocoder.springboot.lab30.rpc.dto.UserAddDTO, messageTemplate='賬號長度為 5-16 位'}]]
  • 上述賊長的兩段日誌,我們可以看到兩次 UserRpcService 服務的呼叫,都丟擲了 ConstraintViolationException 異常。

4.4 存在的問題

如果我們關閉掉服務消費者的引數校驗功能,而只使用服務提供者的引數校驗功能的情況下,當引數校驗不通過時,因為 Hibernate ConstraintDescriptorImpl 沒有預設空構造方法,所以 Hessian 反序列化時,會丟擲 HessianProtocolException 異常。詳細如下:

Caused by: com.alibaba.com.caucho.hessian.io.HessianProtocolException: 'org.hibernate.validator.internal.metadata.descriptor.ConstraintDescriptorImpl' could not be instantiated
	at com.alibaba.com.caucho.hessian.io.JavaDeserializer.instantiate(JavaDeserializer.java:316)
	at com.alibaba.com.caucho.hessian.io.JavaDeserializer.readObject(JavaDeserializer.java:201)
	at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObjectInstance(Hessian2Input.java:2818)
	at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2145)
	at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2074)
	at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2118)
	at com.alibaba.com.caucho.hessian.io.Hessian2Input.readObject(Hessian2Input.java:2074)
	at com.alibaba.com.caucho.hessian.io.JavaDeserializer$ObjectFieldDeserializer.deserialize(JavaDeserializer.java:406)

目前有兩種解決方案:

不過目前方案二,提交在 https://github.com/apache/incubator-dubbo/pull/1708 的 PR 程式碼,已經被 Dubbo 開發團隊否決了。所以,目前建議還是採用方案一來解決。

5. 自定義實現擴充點

「4. 引數校驗」 小節中,我們入門了 Dubbo 提供的引數校驗的功能,它是由 ValidationFilter 過濾器,通過攔截請求,根據我們新增 JSR303 定義的註解,校驗引數是否正確。在 Dubbo 框架中,還提供了 AccessLogFilterExceptionFilter 等等過濾器,他們都屬於 Dubbo Filter 介面的實現類。

而實際上,Filter 是 Dubbo 定義的 呼叫攔截 擴充點。除了 Filter 擴充點,Dubbo 還定義了 協議路由註冊中心 等等擴充點。如下圖所示:擴充點

而這些 Dubbo 擴充點,通過 Dubbo SPI 機制,進行載入。可能胖友對 Dubbo SPI 機制有點懵逼。嘿嘿,一定沒有好好讀過 Dubbo 的官方文件:

FROM 《Dubbo 擴充套件點載入》

Dubbo 的擴充套件點載入從 JDK 標準的 SPI (Service Provider Interface) 擴充套件點發現機制加強而來。

Dubbo 的擴充套件點載入從 JDK 標準的 SPI (Service Provider Interface) 擴充套件點發現機制加強而來。

Dubbo 改進了 JDK 標準的 SPI 的以下問題:

  • JDK 標準的 SPI 會一次性例項化擴充套件點所有實現,如果有擴充套件實現初始化很耗時,但如果沒用上也載入,會很浪費資源。
  • 如果擴充套件點載入失敗,連擴充套件點的名稱都拿不到了。比如:JDK 標準的 ScriptEngine,通過 getName() 獲取指令碼型別的名稱,但如果 RubyScriptEngine 因為所依賴的 jruby.jar 不存在,導致 RubyScriptEngine 類載入失敗,這個失敗原因被吃掉了,和 ruby 對應不起來,當使用者執行 ruby 指令碼時,會報不支援 ruby,而不是真正失敗的原因。
  • 增加了對擴充套件點 IoC 和 AOP 的支援,一個擴充套件點可以直接 setter 注入其它擴充套件點。

下面,我們實現一個對 ExceptionFilter 增強的過濾器,實現即使 Service API 介面上,未定義 ServiceException、ConstraintViolationException 等異常,也不會自動封裝成 RuntimeException 。? 畢竟,要求每個開發同學記得在 Service API 介面上,新增 ServiceException、ConstraintViolationException 等異常,是挺困難的事情,總是可能不經意遺忘。

下面,我們繼續在 「2. XML 配置」 小節的 lab-30-dubbo-xml-demo 示例專案,進行修改,新增自定義 ExceptionFilter 增強的過濾器的功能。

艿艿:關於本小節的內容,艿艿希望胖友有看過 《芋道 Spring Boot SpringMVC 入門》 的 「4. 全域性統一返回」 和 「5. 全域性異常處理」 小節的內容,因為涉及到的思路是一致的。

5.1 API

本小節,我們來看看對 user-rpc-service-api 專案的改造。

5.1.1 ServiceExceptionEnum

在 cn.iocoder.springboot.lab30.rpc.core 包路徑,建立 ServiceExceptionEnum 列舉類,列舉專案中的錯誤碼。程式碼如下:

// ServiceExceptionEnum.java

public enum ServiceExceptionEnum {

    // ========== 系統級別 ==========
    SUCCESS(0, "成功"),
    SYS_ERROR(2001001000, "服務端發生異常"),
    MISSING_REQUEST_PARAM_ERROR(2001001001, "引數缺失"),

    // ========== 使用者模組 ==========
    USER_NOT_FOUND(1001002000, "使用者不存在"),

    // ========== 訂單模組 ==========

    // ========== 商品模組 ==========
    ;

    /**
     * 錯誤碼
     */
    private int code;
    /**
     * 錯誤提示
     */
    private String message;

    ServiceExceptionEnum(int code, String message) {
        this.code = code;
        this.message = message;
    }

    // ... 省略 getting 方法

}

因為錯誤碼是全域性的,最好按照模組來拆分。如下是艿艿在 onemall 專案的實踐: 

/**
 * 服務異常
 *
 * 參考 https://www.kancloud.cn/onebase/ob/484204 文章
 *
 * 一共 10 位,分成四段
 *
 * 第一段,1 位,型別
 *      1 - 業務級別異常
 *      2 - 系統級別異常
 * 第二段,3 位,系統型別
 *      001 - 使用者系統
 *      002 - 商品系統
 *      003 - 訂單系統
 *      004 - 支付系統
 *      005 - 優惠劵系統
 *      ... - ...
 * 第三段,3 位,模組
 *      不限制規則。
 *      一般建議,每個系統裡面,可能有多個模組,可以再去做分段。以使用者系統為例子:
 *          001 - OAuth2 模組
 *          002 - User 模組
 *          003 - MobileCode 模組
 * 第四段,3 位,錯誤碼
 *       不限制規則。
 *       一般建議,每個模組自增。
 */

5.1.2 ServiceException

在 cn.iocoder.springboot.lab30.rpc.core 包路徑,建立 ServiceException 異常類,繼承 RuntimeException 異常類,用於定義業務異常。程式碼如下:

public final class ServiceException extends RuntimeException {

    /**
     * 錯誤碼
     */
    private Integer code;

    public ServiceException() { // 建立預設構造方法,用於反序列化的場景。
    }

    public ServiceException(ServiceExceptionEnum serviceExceptionEnum) {
        // 使用父類的 message 欄位
        super(serviceExceptionEnum.getMessage());
        // 設定錯誤碼
        this.code = serviceExceptionEnum.getCode();
    }

    public ServiceException(ServiceExceptionEnum serviceExceptionEnum, String message) {
        // 使用父類的 message 欄位
        super(message);
        // 設定錯誤碼
        this.code = serviceExceptionEnum.getCode();
    }

    public Integer getCode() {
        return code;
    }

}

5.2 Provider

本小節,我們來看看對 user-rpc-service-provider 專案的改造。

5.2.1 DubboExceptionFilter

在 cn.iocoder.springboot.lab30.rpc.filter 包路徑,建立 DubboExceptionFilter ,繼承 ListenableFilter 抽象類,實現對 ExceptionFilter 增強的過濾器。程式碼如下:

// DubboExceptionFilter.java

@Activate(group = CommonConstants.PROVIDER)
public class DubboExceptionFilter extends ListenableFilter {

    public DubboExceptionFilter() {
        super.listener = new ExceptionListenerX();
    }

    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        return invoker.invoke(invocation);
    }

    static class ExceptionListenerX extends ExceptionListener {

        @Override
        public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
            // 發生異常,並且非泛化呼叫
            if (appResponse.hasException() && GenericService.class != invoker.getInterface()) {
                Throwable exception = appResponse.getException();
                // <1> 如果是 ServiceException 異常,直接返回
                if (exception instanceof ServiceException) {
                    return;
                }
                // <2> 如果是引數校驗的 ConstraintViolationException 異常,則封裝返回
                if (exception instanceof ConstraintViolationException) {
                    appResponse.setException(this.handleConstraintViolationException((ConstraintViolationException) exception));
                    return;
                }
            }
            // <3> 其它情況,繼續使用父類處理
            super.onResponse(appResponse, invoker, invocation);
        }

        private ServiceException handleConstraintViolationException(ConstraintViolationException ex) {
            // 拼接錯誤
            StringBuilder detailMessage = new StringBuilder();
            for (ConstraintViolation<?> constraintViolation : ex.getConstraintViolations()) {
                // 使用 ; 分隔多個錯誤
                if (detailMessage.length() > 0) {
                    detailMessage.append(";");
                }
                // 拼接內容到其中
                detailMessage.append(constraintViolation.getMessage());
            }
            // 返回異常
            return new ServiceException(ServiceExceptionEnum.INVALID_REQUEST_PARAM_ERROR,
                    detailMessage.toString());
        }

    }

    static class ExceptionListener implements Listener {

        private Logger logger = LoggerFactory.getLogger(ExceptionListener.class);

        @Override
        public void onResponse(Result appResponse, Invoker<?> invoker, Invocation invocation) {
            if (appResponse.hasException() && GenericService.class != invoker.getInterface()) {
                try {
                    Throwable exception = appResponse.getException();

                    // directly throw if it's checked exception
                    if (!(exception instanceof RuntimeException) && (exception instanceof Exception)) {
                        return;
                    }
                    // directly throw if the exception appears in the signature
                    try {
                        Method method = invoker.getInterface().getMethod(invocation.getMethodName(), invocation.getParameterTypes());
                        Class<?>[] exceptionClassses = method.getExceptionTypes();
                        for (Class<?> exceptionClass : exceptionClassses) {
                            if (exception.getClass().equals(exceptionClass)) {
                                return;
                            }
                        }
                    } catch (NoSuchMethodException e) {
                        return;
                    }

                    // for the exception not found in method's signature, print ERROR message in server's log.
                    logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + exception.getClass().getName() + ": " + exception.getMessage(), exception);

                    // directly throw if exception class and interface class are in the same jar file.
                    String serviceFile = ReflectUtils.getCodeBase(invoker.getInterface());
                    String exceptionFile = ReflectUtils.getCodeBase(exception.getClass());
                    if (serviceFile == null || exceptionFile == null || serviceFile.equals(exceptionFile)) {
                        return;
                    }
                    // directly throw if it's JDK exception
                    String className = exception.getClass().getName();
                    if (className.startsWith("java.") || className.startsWith("javax.")) {
                        return;
                    }
                    // directly throw if it's dubbo exception
                    if (exception instanceof RpcException) {
                        return;
                    }

                    // otherwise, wrap with RuntimeException and throw back to the client
                    appResponse.setException(new RuntimeException(StringUtils.toString(exception)));
                    return;
                } catch (Throwable e) {
                    logger.warn("Fail to ExceptionFilter when called by " + RpcContext.getContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);
                    return;
                }
            }
        }

        @Override
        public void onError(Throwable e, Invoker<?> invoker, Invocation invocation) {
            logger.error("Got unchecked and undeclared exception which called by " + RpcContext.getContext().getRemoteHost() + ". service: " + invoker.getInterface().getName() + ", method: " + invocation.getMethodName() + ", exception: " + e.getClass().getName() + ": " + e.getMessage(), e);
        }

        // For test purpose
        public void setLogger(Logger logger) {
            this.logger = logger;
        }
    }

}
  • 在類上,新增 @Activate 註解,並設定 "group = CommonConstants.PROVIDER" 屬性,將 DubboExceptionFilter 過濾器僅在服務提供者生效。
  • 因為目前 Dubbo 原始碼改版,建議在對於 Filter 擴充點的實現,繼承 ListenableFilter 抽象類,更簡易的實現對呼叫結果的處理。
  • 在構造方法中,我們建立了 ExceptionListenerX 類,作為 listener 監聽器。而 ExceptionListenerX 繼承自的 ExceptionListener 類,是我們直接從 Dubbo ExceptionFilter.ExceptionListener 複製過來的邏輯,為了保持 ExceptionFilter 原有邏輯的不變。下面,讓我們來看看 ExceptionListenerX 的實現程式碼:
    • <1> 處,如果是 ServiceException 異常,直接返回。
    • <2> 處,如果是引數校驗的 ConstraintViolationException 異常,則呼叫 #handleConstraintViolationException(ConstraintViolationException ex) 方法,封裝成 ServiceException 異常,之後返回。
    • <3> 處,其它情況,繼續使用父類 ExceptionListener 來處理。

這裡,可能有胖友對 ExceptionFilter 異常處理不是很瞭解,建議看看 《淺談 Dubbo 的 ExceptionFilter 異常處理》 文章。

另外,DubboExceptionFilter 是 「4.4 存在問題」 的方案二的一種變種解決方案。

5.2.2 Dubbo SPI 配置檔案

在 resources 目錄下,建立 META-INF/dubbo/ 目錄,然後建立 org.apache.dubbo.rpc.Filter 配置檔案,配置如下:

dubboExceptionFilter=cn.iocoder.springboot.lab30.rpc.filter.DubboExceptionFilter
  •  org.apache.dubbo.rpc.Filter 配置檔名,不要亂建立,就是 DubboExceptionFilter 對應的 Dubbo SPI 擴充點 Filter 。
  • 該配置檔案裡的每一行,格式為 ${擴充名}=${擴充類全名}。這裡,我們配置了一個擴充名為 dubboExceptionFilter

5.2.3 UserRpcServiceImpl

修改 UserRpcServiceImpl 類,修改下 #add(UserAddDTO addDTO) 方法,丟擲 ServiceException 異常。程式碼如下:

// UserRpcServiceImpl.java

@Override
public Integer add(UserAddDTO addDTO) {
    // 【額外新增】這裡,模擬使用者已經存在的情況
    if ("yudaoyuanma".equals(addDTO.getName())) {
        throw new ServiceException(ServiceExceptionEnum.USER_EXISTS);
    }
    return (int) (System.currentTimeMillis() / 1000); // 嘿嘿,隨便返回一個 id
}

5.2.4 應用配置檔案

修改 application.yml 配置檔案,新增 dubbo.provider.filter=-exception 配置項,去掉服務提供者的 ExceptionFilter 過濾器。

如果胖友僅僅想去掉 UserRpcService 服務的 ExceptionFilter 過濾器,可以修改 dubbo.xml 配置檔案,配置如下:

<dubbo:service ref="userRpcServiceImpl" interface="cn.iocoder.springboot.lab30.rpc.api.UserRpcService"
    version="${dubbo.provider.UserRpcService.version}" validation="true" filter="-exception" />
  • 這裡,我們將 filter 設定為 "-exception" ,去掉服務提供者的 UserRpcService 的 ExceptionFilter 過濾器。

當然,一般情況下啊,我們採用全域性配置,即通過 dubbo.provider.filter=-exception 配置項。

5.3 Consumer

本小節,我們來看看對 user-rpc-service-consumer 專案的改造。

5.3.1 ConsumerApplication

修改 ConsumerApplication 類,增加呼叫 UserRpcService 服務時,丟擲 ServiceException 異常的示例。程式碼如下:

// ConsumerApplication.java

@Component
public class UserRpcServiceTest03 implements CommandLineRunner {

    private final Logger logger = LoggerFactory.getLogger(getClass());

    @Resource
    private UserRpcService userRpcService;

    @Override
    public void run(String... args) {
        // 新增使用者
        try {
            // 建立 UserAddDTO
            UserAddDTO addDTO = new UserAddDTO();
            addDTO.setName("yudaoyuanma"); // 設定為 yudaoyuanma ,觸發 ServiceException 異常
            addDTO.setGender(1);
            // 發起呼叫
            userRpcService.add(addDTO);
            logger.info("[run][發起一次 Dubbo RPC 請求,新增使用者為({})]", addDTO);
        } catch (Exception e) {
            logger.error("[run][新增使用者發生異常({}),資訊為:[{}]", e.getClass().getSimpleName(), e.getMessage());
        }
    }

}
  • 新增了一段程式碼,呼叫 UserRpcService 服務的#add(UserAddDTO addDTO) 方法,並且是丟擲 ServiceException 異常的示例。

執行 #main(String[] args) 方法,啟動專案。控制檯列印日誌如下:

2019-12-01 16:17:39.919 ERROR 14738 --- [           main] ConsumerApplication$UserRpcServiceTest03 : [run][新增使用者發生異常(ServiceException),資訊為:[使用者已存在]
  • 我們可以看到,成功丟擲 ServiceException 異常,即使我們在 UserRpcService API 介面的 #add(UserAddDTO addDTO) 方法上,並未顯示 throws 丟擲 UserRpcService 異常。

5.4 小結

實際上,因為我們把 ServiceException 放在了 Service API 所在的 Maven 專案裡,所以即使使用 Dubbo 內建的 ExceptionFilter 過濾器,並且 UserRpcService API 介面的 #add(UserAddDTO addDTO) 方法並未顯示 throws 丟擲 UserRpcService 異常,ExceptionFilter 也不會把 UserRpcService 封裝成 RuntimeException 異常。咳咳咳 ? 如果不瞭解的胖友,胖友在回看下 《淺談 Dubbo 的 ExceptionFilter 異常處理》 文章,結尾的“4. 把異常放到 provider-api 的 jar 包中”。

實際專案的 ExceptionFilter 增強封裝,可以看看艿艿在開源專案 onemall 中,會把 ServiceException 和 DubboExceptionFilter 放在 common-framework 框架專案中,而不是各個業務專案中。

6. 整合 Nacos

示例程式碼對應倉庫:lab-30-dubbo-annotations-nacos 。

本小節我們來進行 Dubbo 和 Nacos 的整合,使用 Nacos 作為 Dubbo 的註冊中心。Dubbo 提供了 dubbo-registry-nacos 子專案,已經對 Nacos 進行適配,所以我們只要引入它,基本就完成了 Dubbo 和 Nacos 的整合,賊方便。

Nacos 致力於幫助您發現、配置和管理微服務。Nacos 提供了一組簡單易用的特性集,幫助您快速實現動態服務發現、服務配置、服務後設資料及流量管理。

Nacos 幫助您更敏捷和容易地構建、交付和管理微服務平臺。 Nacos 是構建以“服務”為中心的現代應用架構 (例如微服務正規化、雲原生正規化) 的服務基礎設施。

還是老樣子,我們從「3. 註解配置」小節,複製出對應的三個 Maven 專案來進行改造,進行 Nacos 的整合。最終專案如下圖所示:三個 Maven 專案

友情提示:本小節需要搭建一個 Nacos 服務,可以參考《Nacos 極簡入門》文章。

6.1 API

「3. 註解配置」小節的 user-rpc-service-api-02,複製出 user-rpc-service-api-03,無需做任何改動。

6.2 Provider

「3. 註解配置」小節的 user-rpc-service-provider-02,複製出 user-rpc-service-provider-03接入 Nacos 作為註冊中心。改動點如下圖:Provider

6.2.1 引入依賴

修改 pom.xml 檔案,額外引入 Nacos 相關的依賴如下:

<!-- 使用 Nacos 作為註冊中心 -->
<dependency>
    <groupId>com.alibaba.nacos</groupId>
    <artifactId>nacos-client</artifactId>
    <version>1.2.1</version>
</dependency>
<dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-registry-nacos</artifactId>
    <version>2.7.4.1</version>
</dependency>

 6.2.2 配置檔案

修改 application.yaml 配置檔案,修改 dubbo.registry.address 配置項,設定 Nacos 作為註冊中心。完整配置如下:

# dubbo 配置項,對應 DubboConfigurationProperties 配置類
dubbo:
  # Dubbo 應用配置
  application:
    name: user-service-provider # 應用名
  # Dubbo 註冊中心配
  registry:
    address: nacos://127.0.0.1:8848 # 註冊中心地址。個鞥多註冊中心,可見 http://dubbo.apache.org/zh-cn/docs/user/references/registry/introduction.html 文件。
  # Dubbo 服務提供者協議配置
  protocol:
    port: -1 # 協議埠。使用 -1 表示隨機埠。
    name: dubbo # 使用 `dubbo://` 協議。更多協議,可見 http://dubbo.apache.org/zh-cn/docs/user/references/protocol/introduction.html 文件
  # Dubbo 服務提供者配置
  provider:
    timeout: 1000 # 【重要】遠端服務呼叫超時時間,單位:毫秒。預設為 1000 毫秒,胖友可以根據自己業務修改
    UserRpcService:
      version: 1.0.0
  # 配置掃描 Dubbo 自定義的 @Service 註解,暴露成 Dubbo 服務提供者
  scan:
    base-packages: cn.iocoder.springboot.lab30.rpc.service

友情提示:艿艿本機搭建的 Nacos 服務啟動在預設的 8848 埠。

6.3 Consumer

「3. 註解配置」小節的 user-rpc-service-consumer-02,複製出 user-rpc-service-consumer-03接入 Nacos 作為註冊中心。改動點如下圖:Consumer

友情提示:整合的過程,和「6.2 Provider」一模一樣。

6.3.1 引入依賴

修改 pom.xml 檔案,額外引入 Nacos 相關的依賴如下:

<!-- 使用 Nacos 作為註冊中心 -->
<dependency>
    <groupId>com.alibaba.nacos</groupId>
    <artifactId>nacos-client</artifactId>
    <version>1.2.1</version>
</dependency>
<dependency>
    <groupId>org.apache.dubbo</groupId>
    <artifactId>dubbo-registry-nacos</artifactId>
    <version>2.7.4.1</version>
</dependency>

6.3.2 配置檔案

修改 application.yaml 配置檔案,修改 dubbo.registry.address 配置項,設定 Nacos 作為註冊中心。完整配置如下:

# dubbo 配置項,對應 DubboConfigurationProperties 配置類
dubbo:
  # Dubbo 應用配置
  application:
    name: user-service-consumer # 應用名
  # Dubbo 註冊中心配置
  registry:
    address: nacos://127.0.0.1:8848 # 註冊中心地址。個鞥多註冊中心,可見 http://dubbo.apache.org/zh-cn/docs/user/references/registry/introduction.html 文件。
  # Dubbo 消費者配置
  consumer:
    timeout: 1000 # 【重要】遠端服務呼叫超時時間,單位:毫秒。預設為 1000 毫秒,胖友可以根據自己業務修改
    UserRpcService:
      version: 1.0.0

友情提示:艿艿本機搭建的 Nacos 服務啟動在預設的 8848 埠。

6.4 簡單測試

① 使用 ProviderApplication 啟動服務提供者。在 Nacos 控臺中,我們可以看到以 providers 為開頭的服務提供者,如下圖所示:

  • 服務列表
  • 服務提供者

② 使用 ConsumerApplication 啟動服務消費者。在 Nacos 控臺中,我們可以看到以 consumers 為開頭的服務消費者,如下圖所示:

  • 服務列表
  • 服務

更多關於 Dubbo 整合 Nacos 作為註冊中心的內容,可以看看《Dubbo 文件 —— Nacos 註冊中心》

7. 整合 Sentinel

示例程式碼對應倉庫:lab-30-dubbo-annotations-sentinel 。

本小節我們來進行 Dubbo 和 Sentinel 的整合,使用 Sentinel 進行 Dubbo 的流量保護。Sentinel 提供了 sentinel-apache-dubbo-adapter 子專案,已經對 Dubbo 進行適配,所以我們只要引入它,基本就完成了 Dubbo 和 Sentinel 的整合,賊方便。

Sentinel 是阿里中介軟體團隊開源的,面向分散式服務架構的輕量級流量控制產品,主要以流量為切入點,從流量控制熔斷降級系統負載保護等多個維度來幫助使用者保護服務的穩定性。

還是老樣子,我們從「3. 註解配置」小節,複製出對應的三個 Maven 專案來進行改造,進行 Sentinel 的整合。最終專案如下圖所示:三個 Maven 專案

友情提示:本小節需要搭建一個 Sentinel 服務,可以參考《Sentinel 極簡入門》文章。

7.1 API

「3. 註解配置」小節的 user-rpc-service-api-02,複製出 user-rpc-service-api-04,無需做任何改動。

7.2 Provider

「3. 註解配置」小節的 user-rpc-service-provider-02,複製出 user-rpc-service-provider-03接入 Sentinel 實現服務消費者的流量控制。改動點如下圖:Provider

7.2.1 引入依賴

修改 pom.xml 檔案,額外引入 Sentinel 相關的依賴如下:

<!-- Sentinel 核心庫 -->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-core</artifactId>
    <version>1.7.1</version>
</dependency>
<!-- Sentinel 接入控制檯 -->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-transport-simple-http</artifactId>
    <version>1.7.1</version>
</dependency>
<!-- Sentinel 對 Dubbo 的支援 -->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-apache-dubbo-adapter</artifactId>
    <version>1.7.1</version>
</dependency>

7.2.2 Sentinel 配置檔案

在 resources 目錄下,建立 Sentinel 自定義的sentinel.properties 配置檔案。內容如下:

csp.sentinel.dashboard.server=127.0.0.1:7070

7.3 Consumer

「2. 快速入門」小節的 user-rpc-service-consumer-02,複製出 user-rpc-service-consumer-04接入 Sentinel 實現服務消費者的流量控制。改動點如下圖:Consumer

友情提示:整合的過程,和「7.2 Provider」一模一樣。

7.3.1 引入依賴

修改 pom.xml 檔案,額外引入 Sentinel 相關的依賴如下:

<!-- Sentinel 核心庫 -->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-core</artifactId>
    <version>1.7.1</version>
</dependency>
<!-- Sentinel 接入控制檯 -->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-transport-simple-http</artifactId>
    <version>1.7.1</version>
</dependency>
<!-- Sentinel 對 Dubbo 的支援 -->
<dependency>
    <groupId>com.alibaba.csp</groupId>
    <artifactId>sentinel-apache-dubbo-adapter</artifactId>
    <version>1.7.1</version>
</dependency>

7.3.2 Sentinel 配置檔案

在 resources 目錄下,建立 Sentinel 自定義的sentinel.properties 配置檔案。內容如下:

csp.sentinel.dashboard.server=127.0.0.1:7070

 7.3.3 UserController

建立 UserController 類,增加呼叫 UserRpcService 服務的 HTTP API 介面。程式碼如下:

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

    @Reference(version = "${dubbo.consumer.UserRpcService.version}")
    private UserRpcService userRpcService;

    @GetMapping("/get")
    public UserDTO get(@RequestParam("id") Integer id) {
        return userRpcService.get(id);
    }

}

友情提示:注意,需要額外引入 spring-boot-starter-web 依賴。因為它不是主角,所以並沒有主動寫出來哈~

7.4 簡單測試

① 使用 ProviderApplication 啟動服務提供者。使用 ConsumerApplication 啟動服務消費者

② 訪問服務消費者的 http://127.0.0.1:8080/user/get?id=1 介面,保證相關資源的初始化。

下面,我們來演示使用 Sentinel 對服務消費者的流量控制。

而 Sentinel 對服務提供者的流量控制是一樣的,胖友可以自己去嘗試。

③ 使用瀏覽器,訪問下 http://127.0.0.1:7070/ 地址,進入 Sentinel 控制檯。

然後,點選 Sentinel 控制檯的「簇點鏈路」選單,可以看到看到 Dubbo 服務消費者產生的 cn.iocoder.springboot.lab30.rpc.api.UserRpcService.UserRpcService:get(java.lang.Integer) 資源。如下圖所示:Sentinel 控制檯 - 簇點鏈路

點選 n.iocoder.springboot.lab30.rpc.api.UserRpcService.UserRpcService:get(java.lang.Integer) 資源所在列的「流控」按鈕,彈出「新增流控規則」。填寫流控規則,如下圖所示:Sentinel 控制檯 - 新增流控規則

  • 這裡,我們建立的是比較簡單的規則,僅允許該資源被每秒呼叫一次。

④ 使用瀏覽器,快速訪問 http://127.0.0.1:8080/user/get?id=1 介面兩次,會呼叫 UserService#get(Integer id) 方法兩次,會有一次被 Sentinel 流量控制而拒絕,返回結果如下圖所示:返回結果

因為預設的錯誤提示不是很友好,所以胖友可以自定義 SpringMVC 全域性錯誤處理器,對 Sentinel 的異常進行處理。感興趣的胖友,可以閱讀《芋道 Spring Boot SpringMVC 入門》文章的「5. 全域性異常處理」小節。

重要的友情提示:更多 Sentinel 的使用方式,胖友可以閱讀《芋道 Spring Boot 服務容錯 Sentinel 入門》文章。

7.5 DubboFallback

sentinel-apache-dubbo-adapter 支援配置全域性的 fallback 函式,可以在 Dubbo 服務被 Sentinel 限流/降級/負載保護的時候,進行相應的 fallback 處理。

另外,我們還可以配合 Dubbo 的 fallback 機制,來為降級的服務提供替代的實現。

 

現在,Dubbo 可以說從原本的 Java RPC 框架,演化成 Dubbo 生態體系,其周邊也越來越豐富。所以,讓我們一起來期望 《Dubbo 3.0 預覽版詳細解讀,關注非同步化和響應式程式設計》 。

? 無意中,發現 Dubbo 官方已經整理了 Dubbo 的整個生態體系,具體可以看看 Build production-ready microservices 頁面。咳咳咳,真特喵的齊全,完全學不動了。

另外,有一點需要提醒下,很多初學 Dubbo 的胖友,可能會犯跟艿艿一樣的錯誤,直接把原本的 Service 層,直接接入 Dubbo 框架,提供 Dubbo Service RPC 呼叫。其實這是不對的!具體的程式碼結構和專案的示例,可以看看 onemall/demo 專案。

因為本文僅僅是在 Spring Boot 下使用 Dubbo RPC 框架的入門文章,這裡在推薦一些不錯的內容:

相關文章