《Dubbo 实现原理与源码解析 —— 精品合集》 《Netty 实现原理与源码解析 —— 精品合集》
《Spring 实现原理与源码解析 —— 精品合集》 《MyBatis 实现原理与源码解析 —— 精品合集》
《Spring MVC 实现原理与源码解析 —— 精品合集》 《数据库实体设计合集》
《Spring Boot 实现原理与源码解析 —— 精品合集》 《Java 面试题 + Java 学习指南》

摘要: 原创出处 http://www.iocoder.cn/Spring-Boot/Dubbo/ 「芋道源码」欢迎转载,保留摘要,谢谢!


🙂🙂🙂关注**微信公众号:【芋道源码】**有福利:

  1. RocketMQ / MyCAT / Sharding-JDBC 所有源码分析文章列表
  2. RocketMQ / MyCAT / Sharding-JDBC 中文注释源码 GitHub 地址
  3. 您对于源码的疑问每条留言将得到认真回复。甚至不知道如何读源码也可以请教噢
  4. 新的源码解析文章实时收到通知。每周更新一篇左右
  5. 认真的源码交流微信群。

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-frameworkcurator-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 方法
}

  • namegender 属性上,我们添加了参数校验的注解。

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 中,会把 ServiceExceptionDubboExceptionFilter 放在 common-framework 框架项目中,而不是各个业务项目中。

666. 彩蛋

现在,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 框架的入门文章,这里在推荐一些不错的内容:

文章目录
  1. 1. 1. 概述
  2. 2. 2. XML 配置
    1. 2.1. 2.1 API
      1. 2.1.1. 2.1.1 UserDTO
      2. 2.1.2. 2.1.2 UserRpcService
    2. 2.2. 2.2 Provider
      1. 2.2.1. 2.2.1 引入依赖
      2. 2.2.2. 2.2.2 应用配置文件
      3. 2.2.3. 2.2.3 UserRpcServiceImpl
      4. 2.2.4. 2.2.4 Dubbo XML 配置文件
      5. 2.2.5. 2.2.5 ProviderApplication
    3. 2.3. 2.3 Consumer
      1. 2.3.1. 2.3.1 引入依赖
      2. 2.3.2. 2.3.2 应用配置文件
      3. 2.3.3. 2.3.3 Dubbo XML 配置文件
      4. 2.3.4. 2.3.4 ConsumerApplication
  3. 3. 3. 注解配置
    1. 3.1. 3.1 API
      1. 3.1.1. 3.1.1 UserDTO
      2. 3.1.2. 3.1.2 UserRpcService
    2. 3.2. 3.2 Provider
      1. 3.2.1. 3.2.1 引入依赖
      2. 3.2.2. 3.2.2 应用配置文件
      3. 3.2.3. 3.2.3 UserRpcServiceImpl
      4. 3.2.4. 3.2.4 ProviderApplication
    3. 3.3. 3.3 Consumer
      1. 3.3.1. 3.3.1 引入依赖
      2. 3.3.2. 3.3.2 应用配置文件
      3. 3.3.3. 3.3.3 ConsumerApplication
    4. 3.4. 3.4 选择注解还是 XML 配置?
  4. 4. 4. 参数验证
    1. 4.1. 4.1 API
      1. 4.1.1. 4.1.1 引入依赖
      2. 4.1.2. 4.1.2 UserAddDTO
      3. 4.1.3. 4.1.3 UserRpcService
    2. 4.2. 4.2 Provider
      1. 4.2.1. 4.2.1 UserRpcServiceImpl
      2. 4.2.2. 4.2.2 Dubbo XML 配置文件
    3. 4.3. 4.3 Consumer
      1. 4.3.1. 4.3.1 Dubbo XML 配置文件
      2. 4.3.2. 4.3.2 ConsumerApplication
    4. 4.4. 4.4 存在的问题
  5. 5. 5. 自定义实现拓展点
    1. 5.1. 5.1 API
      1. 5.1.1. 5.1.1 ServiceExceptionEnum
      2. 5.1.2. 5.1.2 ServiceException
    2. 5.2. 5.2 Provider
      1. 5.2.1. 5.2.1 DubboExceptionFilter
    3. 5.3. 5.2.2 Dubbo SPI 配置文件
      1. 5.3.1. 5.2.3 UserRpcServiceImpl
      2. 5.3.2. 5.2.4 应用配置文件
    4. 5.4. 5.3 Consumer
      1. 5.4.1. 5.3.1 ConsumerApplication
    5. 5.5. 5.4 小结
  6. 6. 666. 彩蛋