Spring Boot Dubbo 入门

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 项目,如下图所示:Spring Boot Dubbo 入门_第1张图片

  • 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 文件中,引入相关依赖。



    
        org.springframework.boot
        spring-boot-starter-parent
        2.2.1.RELEASE
         
    
    4.0.0

    user-rpc-service

    
        
        
            cn.iocoder.springboot.labs
            user-rpc-service-api
            1.0-SNAPSHOT
        

        
        
            org.springframework.boot
            spring-boot-starter
        

        
        
            org.apache.dubbo
            dubbo
            2.7.4.1
        
        
            org.apache.dubbo
            dubbo-spring-boot-starter
            2.7.4.1
        

        
        
            org.apache.curator
            curator-framework
            2.13.0
        
        
            org.apache.curator
            curator-recipes
            2.13.0
        

    

  • 因为我们希望实现对 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
  • dubbo-spring-boot-starter 依赖,会根据 dubbo 配置项,实现对 Dubbo 的自动化配置。下面呢,我们来逐个配置项看看。

    艿艿:本小节,我们的 「XML 配置」 ,指的是使用 XML 来配置 Dubbo Service 服务。如果胖友想看纯粹的全量 XML 配置,可以看看 《Dubbo 官方文档 —— XML 配置》 。

  • dubbo.application 配置项,Dubbo 应用信息配置。更多属性,可见 ApplicationConfig 类。每个属性的说明,可见 《Dubbo 文档 —— dubbo:application》 。

  • dubbo.registry 配置项,Dubbo 注册中心配置。更多属性,可见 RegistryConfig 类。每个属性的说明,可见 《Dubbo 文档 —— dubbo:registry》 。
  • dubbo.protocol 配置项,Dubbo 服务提供者协议配置。更多属性,可见 ProtocolConfig 类。每个属性的说明,可见 《Dubbo 文档 —— dubbo:protocol》 。
  • dubbo.provider 配置项,Dubbo 服务提供者配置。更多属性,可见 ProviderConfig 类。每个属性的说明,可见 《Dubbo 文档 —— dubbo:provider》 。
  • dubbo.provider.UserRpcService 配置项,是我们自定义的,设置每个 Service 服务的配置。更多属性,可见 ServiceConfig 类。每个属性的说明,可见 《Dubbo 文档 —— dubbo:service》 。

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 服务提供者,如下:




    
    

  • 使用 Dubbo 自定义的 Spring  标签,配置我们 「2.2.3 UserRpcServiceImpl」 成 UserRpcService 的 Dubbo 服务提供者。

更多  标签的属性的说明,可见 《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 文件中,引入相关依赖。



    
        org.springframework.boot
        spring-boot-starter-parent
        2.2.1.RELEASE
         
    
    4.0.0

    user-rpc-service-consumer

    
        
        
            cn.iocoder.springboot.labs
            user-rpc-service-api
            1.0-SNAPSHOT
        

        
        
            org.springframework.boot
            spring-boot-starter
        

        
        
            org.apache.dubbo
            dubbo
            2.7.4.1
        
        
            org.apache.dubbo
            dubbo-spring-boot-starter
            2.7.4.1
        

        
        
            org.apache.curator
            curator-framework
            2.13.0
        
        
            org.apache.curator
            curator-recipes
            2.13.0
        

    

  • 和 「2.2.1 引入依赖」 一模一样,除了  改成了 "user-rpc-service-consumer" 值。

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.2.2 应用配置文件」 看起来有点类似,我们仅仅说说差异性。
  • 去掉 dubbo.protocol 配置项,因为我们是作为 Dubbo 服务的消费者,所以无需添加 Dubbo 服务提供者协议配置。
  • dubbo.consumer 配置项,Dubbo 服务消费者配置。更多属性,可见 ConsumerConfig 类。每个属性的说明,可见 《Dubbo 文档 —— dubbo:consumer》 。
  • dubbo.consumer.UserRpcService 配置项,是我们自定义的,设置每个 Service 服务的配置。更多属性,可见 ReferenceConfig 类。每个属性的说明,可见 《Dubbo 文档 —— dubbo:reference》 。

2.3.3 Dubbo XML 配置文件

在 resources 目录下,创建 dubbo.xml 配置文件,添加 Dubbo 的 Service 服务引用者,如下:




    
    

  • 使用 Dubbo 自定义的 Spring  标签,引用 UserRpcService 接口对应的 Dubbo Service 服务,并创建一个 Bean 编号为 "userService" 的 Bean 对象。这样,我们在 Spring 中,就可以直接注入 UserRpcService Bean ,后续就可以像一个“本地”的 UserRpcService 进行调用使用。

更多  标签的属性的说明,可见 《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 注解,引用通过  配置的引用的 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 项目,如下图所示:Spring Boot Dubbo 入门_第2张图片

  • 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 文件中,引入相关依赖。



    
        org.springframework.boot
        spring-boot-starter-parent
        2.2.1.RELEASE
         
    
    4.0.0

    user-rpc-service-provider-02

    
        
        
            cn.iocoder.springboot.labs
            user-rpc-service-api-02
            1.0-SNAPSHOT
        

        
        
            org.springframework.boot
            spring-boot-starter
        

        
        
            org.apache.dubbo
            dubbo
            2.7.4.1
        
        
            org.apache.dubbo
            dubbo-spring-boot-starter
            2.7.4.1
        

        
        
            org.apache.curator
            curator-framework
            2.13.0
        
        
            org.apache.curator
            curator-recipes
            2.13.0
        

    

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 文档 —— 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 文件中,引入相关依赖。



    
        org.springframework.boot
        spring-boot-starter-parent
        2.2.1.RELEASE
         
    
    4.0.0

    user-rpc-service-consumer-02

    
        
        
            cn.iocoder.springboot.labs
            user-rpc-service-api-02
            1.0-SNAPSHOT
        

        
        
            org.springframework.boot
            spring-boot-starter
        

        
        
            org.apache.dubbo
            dubbo
            2.7.4.1
        
        
            org.apache.dubbo
            dubbo-spring-boot-starter
            2.7.4.1
        

        
        
            org.apache.curator
            curator-framework
            2.13.0
        
        
            org.apache.curator
            curator-recipes
            2.13.0
        

    

  • 和 「3.2.1 引入依赖」 一模一样,除了  改成了 "user-rpc-service-consumer-02" 值。

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 文档 —— 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 来实现验证。

  • 我们在 《Spring Boot 参数校验 Validation 入门》 中学习的,也是基于 JSR303 规范实现的,所以在使用上,是基本一致的。有统一的规范,真好。

下面,我们开始在 「2. XML 配置」 小节的 lab-30-dubbo-xml-demo 示例项目,进行修改,添加参数校验的功能。

4.1 API

本小节,我们来看看对 user-rpc-service-api 项目的改造。

4.1.1 引入依赖

修改 pom.xml 文件中,引入相关依赖。



    
        lab-30-dubbo-xml-demo
        cn.iocoder.springboot.labs
        1.0-SNAPSHOT
    
    4.0.0

    user-rpc-service-api

    
        
        
            javax.validation
            validation-api 
            2.0.1.Final
        
        
            org.hibernate.validator
            hibernate-validator 
            6.0.18.Final
        
        
            org.glassfish
            javax.el 
            3.0.1-b11
        
    

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 的参数校验功能。配置如下:

  • 这里,我们将 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 的参数校验功能。配置如下:


  • 这里,我们将 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)

 

目前有两种解决方案:

  • 方案一,不要关闭掉服务消费者的参数校验功能。
  • 方案二,参考 《Dubbo 使用 JSR303 框架 hibernate-validator 遇到 ConstraintDescriptorImpl could not be instantiated》 文章的方法三。
  • 方案三,Service 接口上,不要抛出 ConstraintViolationException 异常。这样,该异常就可以被 Dubbo 内置的 ExceptionFilter 封装成 RuntimeException 异常,就不会存在反序列化的问题。

不过目前方案二,提交在 https://github.com/apache/incubator-dubbo/pull/1708 的 PR 代码,已经被 Dubbo 开发团队否决了。所以,目前建议还是采用方案一来解决。

5. 自定义实现拓展点

在「4. 参数校验」 小节中,我们入门了 Dubbo 提供的参数校验的功能,它是由 ValidationFilter 过滤器,通过拦截请求,根据我们添加 JSR303 定义的注解,校验参数是否正确。在 Dubbo 框架中,还提供了 AccessLogFilter、ExceptionFilter 等等过滤器,他们都属于 Dubbo Filter 接口的实现类。

而实际上,Filter 是 Dubbo 定义的 调用拦截 拓展点。除了 Filter 拓展点,Dubbo 还定义了 协议、路由、注册中心 等等拓展点。如下图所示:Spring Boot Dubbo 入门_第3张图片

而这些 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 配置文件,配置如下:

  • 这里,我们将 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 的整合。最终项目如下图所示:Spring Boot Dubbo 入门_第4张图片

友情提示:本小节需要搭建一个 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 作为注册中心。改动点如下图:Spring Boot Dubbo 入门_第5张图片

6.2.1 引入依赖

修改 pom.xml 文件,额外引入 Sentinel 相关的依赖如下:



    com.alibaba.nacos
    nacos-client
    1.2.1


    org.apache.dubbo
    dubbo-registry-nacos
    2.7.4.1

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 作为注册中心。改动点如下图:Spring Boot Dubbo 入门_第6张图片

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

6.3.1 引入依赖

修改 pom.xml 文件,额外引入 Sentinel 相关的依赖如下:



    com.alibaba.nacos
    nacos-client
    1.2.1


    org.apache.dubbo
    dubbo-registry-nacos
    2.7.4.1

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 为开头的服务提供者,如下图所示:

  • Spring Boot Dubbo 入门_第7张图片
  • Spring Boot Dubbo 入门_第8张图片

② 使用 ConsumerApplication 启动服务消费者。在 Nacos 控台中,我们可以看到以 consumers 为开头的服务消费者,如下图所示:

  • Spring Boot Dubbo 入门_第9张图片
  • Spring Boot Dubbo 入门_第10张图片

更多关于 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 的整合。最终项目如下图所示:Spring Boot Dubbo 入门_第11张图片

友情提示:本小节需要搭建一个 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 实现服务消费者的流量控制。改动点如下图:Spring Boot Dubbo 入门_第12张图片

7.2.1 引入依赖

修改 pom.xml 文件,额外引入 Sentinel 相关的依赖如下:



    com.alibaba.csp
    sentinel-core
    1.7.1



    com.alibaba.csp
    sentinel-transport-simple-http
    1.7.1



    com.alibaba.csp
    sentinel-apache-dubbo-adapter
    1.7.1

7.2.2 Sentinel 配置文件

在 resources 目录下,创建 Sentinel 自定义的sentinel.properties 配置文件。内容如下:

csp.sentinel.dashboard.server=127.0.0.1:7070
  • csp.sentinel.dashboard.server 配置项,设置 Sentinel 控制台地址。
  • 更多其它配置项,可见《Sentinel 官方文档 —— 启动配置项》。

7.3 Consumer

将「2. 快速入门」小节的 user-rpc-service-consumer-02,复制出 user-rpc-service-consumer-04接入 Sentinel 实现服务消费者的流量控制。改动点如下图:Spring Boot Dubbo 入门_第13张图片

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

7.3.1 引入依赖

修改 pom.xml 文件,额外引入 Sentinel 相关的依赖如下:



    com.alibaba.csp
    sentinel-core
    1.7.1



    com.alibaba.csp
    sentinel-transport-simple-http
    1.7.1



    com.alibaba.csp
    sentinel-apache-dubbo-adapter
    1.7.1

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) 资源。如下图所示:Spring Boot Dubbo 入门_第14张图片

点击 n.iocoder.springboot.lab30.rpc.api.UserRpcService.UserRpcService:get(java.lang.Integer) 资源所在列的「流控」按钮,弹出「新增流控规则」。填写流控规则,如下图所示:Spring Boot Dubbo 入门_第15张图片

  • 这里,我们创建的是比较简单的规则,仅允许该资源被每秒调用一次。

④ 使用浏览器,快速访问 http://127.0.0.1:8080/user/get?id=1 接口两次,会调用 UserService#get(Integer id) 方法两次,会有一次被 Sentinel 流量控制而拒绝,返回结果如下图所示:Spring Boot Dubbo 入门_第16张图片

因为默认的错误提示不是很友好,所以胖友可以自定义 SpringMVC 全局错误处理器,对 Sentinel 的异常进行处理。感兴趣的胖友,可以阅读《芋道 Spring Boot SpringMVC 入门》文章的「5. 全局异常处理」小节。

重要的友情提示:更多 Sentinel 的使用方式,胖友可以阅读《芋道 Spring Boot 服务容错 Sentinel 入门》文章。

7.5 DubboFallback

sentinel-apache-dubbo-adapter 支持配置全局的 fallback 函数,可以在 Dubbo 服务被 Sentinel 限流/降级/负载保护的时候,进行相应的 fallback 处理。

  • 我们只需要实现自定义的 DubboFallback 接口,并通过 DubboFallbackRegistry 进行注册即可。
  • 默认情况下,使用 DubboFallback 的 DefaultDubboFallback 实现类,它会将 BlockException 包装成 SentinelRpcException 异常后抛出。

另外,我们还可以配合 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 框架的入门文章,这里在推荐一些不错的内容:

  • 《Dubbo 官方文档》 :还有比官方文档更香的东西么?在国内的开源项目中,Dubbo 的文档质量,起码 TOP10 吧。
  • dubbo-samples 仓库:提供了大量的示例,美滋滋。
  • 《设计 RPC 接口时,你有考虑过这些吗?》 :让你更优雅的设计 RPC 接口。
  • 《你的项目应该如何正确分层?》 :可以借鉴的项目分层。

你可能感兴趣的:(Dubbo,服务调用,Spring,Boot)