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

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


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

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

本文在提供完整代码示例,可见 https://github.com/YunaiV/SpringBoot-Labslabx-07-spring-cloud-alibaba-dubbo 目录。

原创不易,给点个 Star 嘿,一起冲鸭!

1. 概述

本文我们来学习 Spring Cloud Alibaba 提供的 Spring Cloud Alibaba Dubbo 组件,接入 Dubbo 实现服务调用。

Dubbo Spring Cloud 基于 Dubbo Spring Boot 2.7.1 和 Spring Cloud 2.x 开发,无论开发人员是 Dubbo 用户还是 Spring Cloud 用户, 都能轻松地驾驭,并以接近“零”成本的代价使应用向上迁移。Dubbo Spring Cloud 致力于简化 Cloud Native 开发成本,提高研发效能以及提升应用性能等目的。

在早期的时候,Dubbo 和 Spring Cloud 是两个隔离的生态,因此经常讨论的一个话题就是《Java 微服务框架选型(Dubbo 和 Spring Cloud?)》。而现在 Spring Cloud Alibaba Dubbo 的出现,将 Dubbo 融合到 Spring Cloud 中,取代 Feign + Ribbon,提供更好的服务治理能力与更优的性能。

由于 Dubbo Spring Cloud 构建在原生的 Spring Cloud 之上,其服务治理方面的能力可认为是 Spring Cloud Plus, 不仅完全覆盖 Spring Cloud 原生特性,而且提供更为稳定和成熟的实现,特性比对如下表所示:

功能组件 Spring Cloud Dubbo Spring Cloud
分布式配置 Git、Zookeeper、Consul、JDBC Spring Cloud 分布式配置 + Dubbo 配置中心
服务注册与发现 Eureka、Zookeeper、Consul Spring Cloud 原生注册中心 + Dubbo 原生注册中心
负载均衡 Ribbon(随机、轮询等算法) Dubbo 内建实现(随机、轮询等算法 + 权重等特性)
服务熔断 Spring Cloud Hystrix Spring Cloud Hystrix + Alibaba Sentinel 等
服务调用 Open Feign、RestTemplate Spring Cloud 服务调用 + Dubbo @Reference
链路跟踪 Spring Cloud Sleuth + Zipkin Zipkin、opentracing 等

Spring Cloud Alibaba Dubbo 比较重要的特性,使用 Spring Cloud 定义的应用级别的注册模型,将 Dubbo 服务注册到 Spring Cloud 编程模型的注册中心。如此,Spring Cloud Alibaba Dubbo 又将 Feign 和 RestTemplate 进一步增强,实现对 Spring Cloud Alibaba Dubbo 服务的调用。最终如下图所示:打通示例

当然还有很多其它细节,我们来一起在入门的过程中,暗搓搓的理解理解。嘻嘻~

友情提示:考虑到 Spring Cloud Alibaba 在大力推广 Nacos 作为注册中心,所以本文就使用 Nacos 啦。如果不了解的胖友,可以阅读《芋道 Spring Cloud Alibaba 注册中心 Nacos 入门》文章。

2. 快速入门

示例代码对应仓库:labx-07-sca-dubbo-demo01

本小节,我们先来一起快速入门下 Spring Cloud Alibaba Dubbo,一共要搭建三个 Maven 项目,如下图所示:三个 Maven 项目

2.1 搭建 API 项目

创建 labx-07-sca-dubbo-demo01-api 项目,服务接口,定义 Dubbo Service API 接口,提供给消费者使用。

2.1.1 DTO

创建 UserAddDTOUserDTO 两个 DTO 类,代码如下:

/**
* 用户添加 DTO
*/
public class UserAddDTO implements Serializable {

/**
* 昵称
*/
private String name;
/**
* 性别
*/
private Integer gender;

// ... 省略 setter/getter 方法
}

/**
* 用户信息 DTO
*/
public class UserDTO implements Serializable {

/**
* 用户编号
*/
private Integer id;
/**
* 昵称
*/
private String name;
/**
* 性别
*/
private Integer gender;

// ... 省略 setter/getter 方法
}

注意,要实现 java.io.Serializable 接口。因为,Dubbo RPC 会涉及远程通信,需要序列化和反序列化。

2.1.2 UserService

创建 UserService 接口,定义用户服务 RPC Service 接口。代码如下:

public interface UserService {

/**
* 根据指定用户编号,获得用户信息
*
* @param id 用户编号
* @return 用户信息
*/
UserDTO get(Integer id);

/**
* 添加新用户,返回新添加的用户编号
*
* @param addDTO 添加的用户信息
* @return 用户编号
*/
Integer add(UserAddDTO addDTO);

}

2.2 搭建服务提供者

创建 labx-07-sca-dubbo-demo01-provider 项目,服务提供者,实现 labx-07-sca-dubbo-demo01-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>
<artifactId>labx-07-sca-dubbo-demo01</artifactId>
<groupId>cn.iocoder.springboot.labs</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>labx-07-sca-dubbo-demo01-provider</artifactId>

<properties>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
<spring.boot.version>2.2.4.RELEASE</spring.boot.version>
<spring.cloud.version>Hoxton.SR1</spring.cloud.version>
<spring.cloud.alibaba.version>2.2.0.RELEASE</spring.cloud.alibaba.version>
</properties>

<!--
引入 Spring Boot、Spring Cloud、Spring Cloud Alibaba 三者 BOM 文件,进行依赖版本的管理,防止不兼容。
在 https://dwz.cn/mcLIfNKt 文章中,Spring Cloud Alibaba 开发团队推荐了三者的依赖关系
-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring.cloud.alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<!-- 引入定义的 Dubbo API 接口 -->
<dependency>
<groupId>cn.iocoder.springboot.labs</groupId>
<artifactId>labx-07-sca-dubbo-demo01-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

<!-- 引入 Spring Cloud Alibaba Nacos Discovery 相关依赖,将 Nacos 作为注册中心,并实现对其的自动配置 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<!-- 引入 Spring Cloud Alibaba Dubbo 相关依赖,实现呢 Dubbo 进行远程调用,并实现对其的自动配置 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-dubbo</artifactId>
</dependency>
</dependencies>

</project>

引入三个依赖,重点是 spring-cloud-starter-dubbo

2.2.2 配置文件

创建 application.yaml 配置类,添加 Nacos 和 Dubbo 配置项。配置如下:

spring:
application:
name: demo-provider
cloud:
# Nacos 作为注册中心的配置项
nacos:
discovery:
server-addr: 127.0.0.1:8848 # Nacos 服务器地址

# Dubbo 配置项,对应 DubboConfigurationProperties 类
dubbo:
scan:
base-packages: cn.iocoder.springcloudalibaba.labx7.providerdemo.service # 指定 Dubbo 服务实现类的扫描基准包
# Dubbo 服务暴露的协议配置,对应 ProtocolConfig Map
protocols:
dubbo:
name: dubbo # 协议名称
port: -1 # 协议端口,-1 表示自增端口,从 20880 开始
# Dubbo 服务注册中心配置,对应 RegistryConfig 类
registry:
address: spring-cloud://127.0.0.1:8848 # 指定 Dubbo 服务注册中心的地址
# Spring Cloud Alibaba Dubbo 专属配置项,对应 DubboCloudProperties 类
cloud:
subscribed-services: '' # 设置订阅的应用列表,默认为 * 订阅所有应用

spring.cloud.nacos.discovery 配置项,使用 Nacos 作为 Spring Cloud 注册中心的配置项。默认情况下,使用 spring.application.name 作为注册的服务名

dubbo 配置项,Dubbo 配置项,对应 DubboConfigurationProperties 类,由 Dubbo Spring Boot 项目定义。

dubbo.scan.base-packages 配置项,指定 Dubbo 服务实现类的扫描基准包。稍后我们会实现 Service 服务在 cn.iocoder.springcloudalibaba.labx7.providerdemo.service 包下。

dubbo.protocols 配置项,Dubbo 服务暴露的协议配置,对应 ProtocolConfig Map。其中 key 为稍后定义的 Protocol Bean 的名字,一般设置为和使用的协议名称相同即可。

  • name:协议名称。这里使用 dubbo:// 协议。
  • port:协议端口。这里设置为 -1,表示自增端口,从 20880 开始。

dubbo.registry 配置项,Dubbo 服务注册中心配置,对应 RegistryConfig 类。

  • address:指定 Dubbo 服务注册中心的地址。这里设置为 spring-cloud://127.0.0.1:8848,使用 spring.cloud.nacos.discovery 配置项的 Nacos 注册中心。

dubbo.cloud 配置项,Spring Cloud Alibaba Dubbo 专属配置项,对应 DubboCloudProperties 类。

  • subscribed-services 配置项,设置订阅的应用列表,默认为 * 订阅所有应用。这里,设置为 '' 表示空,不订阅任何的应用。

2.2.3 UserServiceImpl

创建 UserServiceImpl 类,实现 UserService 接口,用户服务具体实现类。代码如下:

@org.apache.dubbo.config.annotation.Service(protocol = "dubbo", version = "1.0.0")
public class UserServiceImpl implements UserService {

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

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

}

@Service 注解,声明为一个 Dubbo 服务

  • protocol 注解,设置使用的 Protocol Bean 的名字。这里设置为 dubbo 来声明暴露 dubbo:// 协议的服务。
  • version 属性,服务的版本号。

2.2.4 ProviderApplication

创建 ProviderApplication 类,服务提供者的启动类。代码如下:

@SpringBootApplication
public class ProviderApplication {

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

}

2.3 搭建服务消费者

创建 labx-07-sca-dubbo-demo01-consumer 项目,服务消费者,会调用 labx-07-sca-dubbo-demo01-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>
<artifactId>labx-07-sca-dubbo-demo01</artifactId>
<groupId>cn.iocoder.springboot.labs</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>

<artifactId>labx-07-sca-dubbo-demo01-provider</artifactId>

<properties>
<maven.compiler.target>1.8</maven.compiler.target>
<maven.compiler.source>1.8</maven.compiler.source>
<spring.boot.version>2.2.4.RELEASE</spring.boot.version>
<spring.cloud.version>Hoxton.SR1</spring.cloud.version>
<spring.cloud.alibaba.version>2.2.0.RELEASE</spring.cloud.alibaba.version>
</properties>

<!--
引入 Spring Boot、Spring Cloud、Spring Cloud Alibaba 三者 BOM 文件,进行依赖版本的管理,防止不兼容。
在 https://dwz.cn/mcLIfNKt 文章中,Spring Cloud Alibaba 开发团队推荐了三者的依赖关系
-->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>${spring.boot.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-dependencies</artifactId>
<version>${spring.cloud.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-alibaba-dependencies</artifactId>
<version>${spring.cloud.alibaba.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<!-- 引入定义的 Dubbo API 接口 -->
<dependency>
<groupId>cn.iocoder.springboot.labs</groupId>
<artifactId>labx-07-sca-dubbo-demo01-api</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>

<!-- 引入 Spring Cloud Alibaba Nacos Discovery 相关依赖,将 Nacos 作为注册中心,并实现对其的自动配置 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
</dependency>

<!-- 引入 Spring Cloud Alibaba Dubbo 相关依赖,实现呢 Dubbo 进行远程调用,并实现对其的自动配置 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-dubbo</artifactId>
</dependency>
</dependencies>

</project>

2.3.2 配置文件

创建 application.yaml 配置类,添加 Nacos 和 Dubbo 配置项。配置如下:

spring:
application:
name: demo-connsumer
cloud:
# Nacos 作为注册中心的配置项
nacos:
discovery:
server-addr: 127.0.0.1:8848

# Dubbo 配置项,对应 DubboConfigurationProperties 类
dubbo:
# Dubbo 服务注册中心配置,对应 RegistryConfig 类
registry:
address: spring-cloud://127.0.0.1:8848 # 指定 Dubbo 服务注册中心的地址
# Spring Cloud Alibaba Dubbo 专属配置项,对应 DubboCloudProperties 类
cloud:
subscribed-services: demo-provider # 设置订阅的应用列表,默认为 * 订阅所有应用。

总体和「2.2.2 配置文件」差不多,我们仅来说说差异的地方。

① 去掉 dubbo.scandubbo.protocols 配置项,因为没有需要扫描的服务实现类,并进行服务暴露。

② 设置 dubbo.cloud.subscribed-services 配置项为 demo-provider,订阅「2.2 搭建服务提供者」的实例列表。

2.3.3 UserController

创建 UserController 类,提供调用 UserService 服务的 HTTP 接口。代码如下:

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

@Reference(version = "1.0.0")
private UserService userService;

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

@PostMapping("/add")
public Integer add(UserAddDTO addDTO) {
return userService.add(addDTO);
}

}

@Reference 注解,声明引用 Dubbo 服务

  • protocol 注解,设置使用的 Protocol Bean 的名字。
  • version 属性,服务的版本号。

2.3.4 ConsumerApplication

创建 ConsumerApplication 类,服务消费者的启动类。代码如下:

@SpringBootApplication
public class ConsumerApplication {

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

}

2.4 简单测试

① 执行 ProviderApplication 来启动服务提供者。可以在 Nacos 可以看到 demo-provider 服务,如下图所示:`demo-provider` 服务

如果之前使用过 Dubbo 的胖友应该知道,Dubbo 是基于 Service 服务为粒度注册到注册中心的,这点和 Spring Cloud 是基于 Application 应用为粒度是不同的。Spring Cloud Alibaba Dubbo 定义了 DubboMetadataService 接口,用于获取对应的应用实例Service 的元数据

② 执行 ConsumerApplication 来启动服务消费者。可以在 Nacos 可以看到 demo-consumer 服务,如下图所示:`demo-consumer` 服务

③ 请求 <http://127.0.0.1:8080/user/get?id=1> 接口,返回结果如下:

{
"id": 1,
"name": "没有昵称:1",
"gender": 2
}

搭建成功,美滋滋~

3. Feign 调用 Dubbo 服务

示例代码对应仓库:labx-07-sca-dubbo-demo02

本小节,我们来实现 Feign 调用 Dubbo 服务。这样的好处是,对于正在使用 Feign 进行服务调用的“” Spring Cloud 项目,进行及其少量的改造,就可以调用“”搭建的 Spring Cloud Alibaba Dubbo 服务。

友情提示:如果把“”的 Dubbo 服务略微改造成 Spring Cloud Alibaba Dubbo 服务,也可以被 Feign 进行调用。

为了演示 Feign 调用 Dubbo 服务,一共要搭建四个 Maven 项目,如下图所示:四个 Maven 项目

3.1 搭建 API 项目

「2. 快速入门」小节的 labx-07-sca-dubbo-demo01-api,复制出 labx-07-sca-dubbo-demo02-api,无需做任何改动。

3.2 搭建服务提供者(Rest)

「2. 快速入门」小节的 labx-07-sca-dubbo-demo01-provider,复制出 labx-07-sca-dubbo-demo02-provider-rest接入 Dubbo rest:// 协议来提供 HTTP 接口,从而实现 Feign 可以调用 Dubbo 服务。

3.2.1 引入依赖

修改 pom.xml 文件,额外引入 Dubbo rest:// 协议需要的依赖如下:

<!-- 引入 Dubbo Rest 协议相关的依赖 -->
<dependency>
<groupId>org.jboss.resteasy</groupId>
<artifactId>resteasy-netty4</artifactId>
<version>3.0.19.Final</version>
</dependency>
<dependency>
<groupId>org.hibernate.validator</groupId>
<artifactId>hibernate-validator</artifactId>
</dependency>
<dependency>
<groupId>javax.servlet</groupId>
<artifactId>javax.servlet-api</artifactId>
<version>3.1.0</version> <!-- Resolve the Dubbo REST RPC issue -->
</dependency>

另外,排除 javax.ws.rs 依赖,解决该异常,最终如下:

<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
<exclusions>
<!-- 参考文章 https://stackoverflow.com/questions/48432225/nested-exception-is-java-lang-nosuchmethoderror-javax-ws-rs-clienterrorexceptio -->
<exclusion>
<artifactId>jsr311-api</artifactId>
<groupId>javax.ws.rs</groupId>
</exclusion>
</exclusions>
</dependency>

3.2.2 配置文件

修改 application.yaml 配置文件,增加 Dubbo rest:// 协议需要的配置。完整配置如下:

spring:
application:
name: demo-provider
cloud:
# Nacos 作为注册中心的配置项
nacos:
discovery:
server-addr: 127.0.0.1:8848 # Nacos 服务器地址
main:
web-application-type: NONE # Web 应用类型,这里设置为 NONE

# Dubbo 配置项,对应 DubboConfigurationProperties 类
dubbo:
scan:
base-packages: cn.iocoder.springcloudalibaba.labx7.providerdemo.service # 指定 Dubbo 服务实现类的扫描基准包
# Dubbo 服务暴露的协议配置,对应 ProtocolConfig Map
protocols:
dubbo:
name: dubbo # 协议名称
port: -1 # 协议端口,-1 表示自增端口,从 20880 开始
rest:
name: rest
port: 9090 # 协议端口
server: netty # 使用的服务器
# Dubbo 服务注册中心配置,对应 RegistryConfig 类
registry:
address: spring-cloud://127.0.0.1:8848 # 指定 Dubbo 服务注册中心的地址
# Spring Cloud Alibaba Dubbo 专属配置项,对应 DubboCloudProperties 类
cloud:
subscribed-services: '' # 设置订阅的应用列表,默认为 * 订阅所有应用。

① 增加 spring.main.web-application-type 配置项为 NONE,设置无需 Web 环境,否则项目启动会报错。

原因是,因为我们引入 javax.servlet-api 依赖,所以被识别成了 SERVLET 环境,因此项目启动时会去获取 WebServer,结果获取不到导致报错。

② 在 dubbo.protocols 配置项下,增加了用于 Dubbo rest:// 协议rest 配置项。

3.2.3 UserServiceImpl

修改 UserServiceImpl 类,增加 Dubbo rest:// 协议的暴露。代码如下:

@org.apache.dubbo.config.annotation.Service(version = "1.0.0", protocol = {"dubbo", "rest"})
@Path("/user")
public class UserServiceImpl implements UserService {

@Override
@GET
@Path("/get")
@Produces(APPLICATION_JSON_VALUE)
public UserDTO get(@QueryParam("id") Integer id) {
return new UserDTO().setId(id)
.setName("没有昵称:" + id)
.setGender(id % 2 + 1); // 1 - 男;2 - 女
}

@Override
@POST
@Path("/add")
@Consumes(MediaType.APPLICATION_JSON)
public Integer add(UserAddDTO addDTO) {
return (int) (System.currentTimeMillis() / 1000); // 嘿嘿,随便返回一个 id
}

}

① 在 @Service 注解的 protocol 属性,增加 rest 来声明暴露 rest:// 协议的服务。

② 在类和方法上,增加了 @Path@GET@POST@Produces@Consumes@QueryParam 等等 JAX-RS 定义的注解,因为 Dubbo rest:// 协议是基于标准的 Java REST API —— JAX-RS 2.0(Java API for RESTful Web Services 的简写)来实现的。

3.2.4 启动服务提供者

执行 RestProviderApplication 来启动服务提供者。可以在 Nacos 可以看到 demo-provider 服务,如下图所示:`demo-provider` 服务

重点是端口为 9090,就是我们暴露的 Dubbo 服务的 rest:// 协议的端口,非常关键!这样原本 Feign + Ribbon 的组合,从注册中心加载到该服务的实例时,使用该端口进行 HTTP 调用,即可请求到服务,是不是蛮巧妙的。

3.3 搭建服务提供者(SpringMVC)

「2. 快速入门」小节的 labx-07-sca-dubbo-demo01-provider,复制出 labx-07-sca-dubbo-demo02-provider-springmvc接入 SpringMVC 来提供 HTTP 接口,从而实现 Feign 可以调用 SpringMVC 的 HTTP 接口,间接调用到 Dubbo 服务。

3.3.1 引入依赖

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

<!-- 引入 SpringMVC 相关依赖,并实现对其的自动配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>

3.3.2 配置文件

修改 application.yaml 配置文件,增加 server.port 配置项,设置 Web 服务器的端口。完整配置如下:

spring:
application:
name: demo-provider
cloud:
# Nacos 作为注册中心的配置项
nacos:
discovery:
server-addr: 127.0.0.1:8848 # Nacos 服务器地址

server:
port: 18080 # 服务器端口,默认为 8080

# Dubbo 配置项,对应 DubboConfigurationProperties 类
dubbo:
scan:
base-packages: cn.iocoder.springcloudalibaba.labx7.providerdemo.service # 指定 Dubbo 服务实现类的扫描基准包
# Dubbo 服务暴露的协议配置,对应 ProtocolConfig Map
protocols:
dubbo:
name: dubbo # 协议名称
port: -1 # 协议端口,-1 表示自增端口,从 20880 开始
# Dubbo 服务注册中心配置,对应 RegistryConfig 类
registry:
address: spring-cloud://127.0.0.1:8848 # 指定 Dubbo 服务注册中心的地址
# Spring Cloud Alibaba Dubbo 专属配置项,对应 DubboCloudProperties 类
cloud:
subscribed-services: '' # 设置订阅的应用列表,默认为 * 订阅所有应用。

3.3.3 UserServiceImpl

修改 UserServiceImpl 类,增加 SpringMVC 的注解。代码如下:

@org.apache.dubbo.config.annotation.Service(version = "1.0.0", protocol = {"dubbo"})
@RestController
@RequestMapping("/user")
public class UserServiceImpl implements UserService {

@Override
@GetMapping("/get")
public UserDTO get(@RequestParam("id") Integer id) {
return new UserDTO().setId(id)
.setName("没有昵称:" + id)
.setGender(id % 2 + 1); // 1 - 男;2 - 女
}

@Override
@PostMapping("/add")
public Integer add(@RequestBody UserAddDTO addDTO) {
return (int) (System.currentTimeMillis() / 1000); // 嘿嘿,随便返回一个 id
}

}

3.3.4 启动服务提供者

执行 SpringMVCProviderApplication 来启动服务提供者。可以在 Nacos 可以看到 demo-provider 服务,如下图所示:`demo-provider` 服务

重点是端口为 18080,就是 Spring 提供 HTTP 接口的服务器的端口,非常关键!这样原本 Feign + Ribbon 的组合,从注册中心加载到该服务的实例时,使用该端口进行 HTTP 调用,即可请求到服务,是不是蛮巧妙的。

3.4 搭建服务消费者

「2. 快速入门」小节的 labx-07-sca-dubbo-demo01-consumer,复制出 labx-07-sca-dubbo-demo02-consumer服务消费者,会使用 Feign 来调用 labx-07-sca-dubbo-demo02-provider-restlabx-07-sca-dubbo-demo02-provider-springmvc 项目提供的 Dubbo Service 服务。

3.4.1 引入依赖

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

<!-- 引入 Spring Cloud OpenFeign 相关依赖,使用 OpenFeign 提供声明式调用,并实现对其的自动配置 -->
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

3.4.2 FeignConsumerApplication

将 ConsumerApplication 修改成 FeignConsumerApplication 类,并增加 @EnableFeignClients 注解,开启 Feign 的功能。代码如下:

@SpringBootApplication
@EnableFeignClients
public class FeignConsumerApplication {

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

}

3.4.3 服务调用方式一:Feign + Dubbo

① 创建 UserFeignClient 接口,使用 Feign + Dubbo 调用服务。代码如下:

@FeignClient(name = "demo-provider")
@DubboTransported(protocol = "dubbo")
// @DubboTransported(protocol = "rest")
public interface UserFeignClient {

/**
* 根据指定用户编号,获得用户信息
*
* @param id 用户编号
* @return 用户信息
*/
@GetMapping("/user/get")
UserDTO get(@RequestParam("id") Integer id);

/**
* 添加新用户,返回新添加的用户编号
*
* @param addDTO 添加的用户信息
* @return 用户编号
*/
@PostMapping("/user/add")
Integer add(@RequestBody UserAddDTO addDTO);

}

比较有趣的是,我们在接口上添加了 Spring Cloud Alibaba Dubbo 定义的 @DubboTransported 注解。通过该注解,可以将 Feign 客户端的底层实际使用 Dubbo 调用服务,也就是说实际变成了 Dubbo 客户端,仅仅是使用 Feign 来声明服务调用的接口。

友情提示:为什么 Spring Cloud Alibaba Dubbo 定义了 DubboMetadataService 接口提供元数据呢?就是为了获取远程服务的方法的元数据,从而能够使用 Dubbo 调用远程服务。

@DubboTransported 注解的目的,是将原本使用 Feign 进行服务调用的 Spring Cloud 项目,逐步迁移到使用 Dubbo 进行服务调用。也就是说,如果是新的 Spring Cloud 项目,就不要使用 @DubboTransported 注解,而是使用 @Reference 注解。感兴趣的胖友,可以看看 ISSUE#602

② 创建 User01Controller 类,提供使用 UserFeignClient 调用 UserService 服务的 HTTP 接口。代码如下:

@RestController
@RequestMapping("/user01")
public class User01Controller {

@Autowired
private UserFeignClient userFeignClient;

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

@PostMapping("/add")
public Integer add(UserAddDTO addDTO) {
return userFeignClient.add(addDTO);
}

}

③ 因为底层实际使用 Dubbo 来调用服务,所以在使用 dubbo:// 协议时,仍需要引入 labx-07-sca-dubbo-demo02-api 依赖,避免反序列化报错。

3.4.4 服务调用方式二:Feign + Ribbon

① 创建 UserFeignClient02 接口,使用 Feign + Ribbon 调用服务。代码如下:

@FeignClient(name = "demo-provider")
public interface UserFeignClient02 {

/**
* 根据指定用户编号,获得用户信息
*
* @param id 用户编号
* @return 用户信息
*/
@GetMapping("/user/get")
UserDTO get(@RequestParam("id") Integer id);

/**
* 添加新用户,返回新添加的用户编号
*
* @param addDTO 添加的用户信息
* @return 用户编号
*/
@PostMapping("/user/add")
Integer add(@RequestBody UserAddDTO addDTO);

}

标准的 Feign + Ribbon 的组合,最终调用服务的 rest:// 协议或 SpringMVC 提供的 HTTP 接口。

③ 因为是 HTTP 接口的调用,所以实际无需引入 labx-07-sca-dubbo-demo02-api 依赖,这里使用到的 UserDTOUserAddDTO 类都是在 dto 包下另外创建的。

3.4.5 服务调用方式三:RestTemplate + Dubbo

① 创建 RestTemplateConfig 配置类,创建 RestTemplate Bean。代码如下:

@Configuration
public class RestTemplateConfig {

@Bean
@LoadBalanced
@DubboTransported(protocol = "dubbo")
public RestTemplate restTemplate() {
return new RestTemplate();
}

}

在创建 RestTemplate Bean 的方法上,也添加了 @DubboTransported 注解,这样 RestTemplate 调用服务时,底层也变成了是使用 Dubbo 进行调用服务,也就是说 RestTemplate 变成了 DubboTemplate,嘿嘿。

② 创建 User03Controller 类,提供使用 RestTemplate 调用 UserService 服务的 HTTP 接口。代码如下:

@RestController
@RequestMapping("/user03")
public class User03Controller {

@Autowired
private RestTemplate restTemplate;

@GetMapping("/get")
public UserDTO get(@RequestParam("id") Integer id) {
String url = String.format("http://%s/user/get?id=%d", "demo-provider", id);
return restTemplate.getForObject(url, UserDTO.class);
}

@PostMapping("/add")
public Integer add(UserAddDTO addDTO) {
// 请求头
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
// 请求体
String body = JSON.toJSONString(addDTO);
// 创建 HttpEntity 对象
HttpEntity<String> entity = new HttpEntity<>(body, headers);
// 执行请求
String url = String.format("http://%s/user/add", "demo-provider");
return restTemplate.postForObject(url, entity, Integer.class);
}

}

③ 因为底层实际使用 Dubbo 来调用服务,所以在使用 dubbo:// 协议时,仍需要引入 labx-07-sca-dubbo-demo02-api 依赖,避免反序列化报错。

3.4.6 服务调用方式四:Dubbo

创建 User04Controller 类,提供使用 Dubbo 调用 UserService 服务的 HTTP 接口。代码如下:

@RestController
@RequestMapping("/user04")
public class User04Controller {

@Reference(version = "1.0.0", protocol = "dubbo")
private UserService userService;

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

@PostMapping("/add")
public Integer add(UserAddDTO addDTO) {
return userService.add(addDTO);
}

}

该方式是 Spring Cloud Alibaba Dubbo 开发者最推荐的方式,没有之一。所以说,上述的方式一、二、三都是过渡方案,嘿嘿。

3.5 简单测试

① 执行 RestProviderApplication 或 SpringMVCProviderApplication 来启动服务提供者。

② 执行 FeignConsumerApplication 来启动服务消费者。

③ 请求 Controller 里的接口,进行不同方式的测试。这里就不一一演示,胖友自己享受,啊哈哈~

默默吐槽:本小节的示例,搭建花费了大半天的时间,一来是官方示例竟然是启动报错而有问题,二来是官方文档对 @DubboTransported 注解毫无介绍。

这样难免会让国内的开发者对 Spring Cloud Alibaba 项目产生质疑。希望 Spring Cloud Alibaba 越来越完善,能够成为新的 Spring Cloud 的实现标准!

4. 参数验证

示例代码对应仓库:labx-07-sca-dubbo-demo03-validation

参数校验,对于提供 API 调用的服务来说,必然是必不可少的。在 《芋道 Spring Boot 参数校验 Validation 入门》 中,我们已经看了如何在 SpringMVC 和本地的 Service 使用参数校验的示例。

本小节,我们来学习下,如何在 Dubbo RPC Service 中,使用参数校验。在 《Dubbo 文档 —— 参数验证》 中,对该功能的描述如下:

参数验证功能是基于 JSR303 实现的,用户只需标识 JSR303 标准的验证 annotation,并通过声明 filter 来实现验证。

还是老样子,我们从「2. 快速入门」小节,复制出对应的三个 Maven 项目来进行改造,增加参数验证的功能。最终项目如下图所示:三个 Maven 项目

4.1 搭建 API 项目

「2. 快速入门」小节的 labx-07-sca-dubbo-demo01-api,复制出 labx-07-sca-dubbo-demo03-api增加 JSR303 标准的验证注解

4.1.1 引入依赖

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

<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>

4.1.2 UserService

修改 UserService 接口,增加验证注解。代码如下:

public interface UserService {

/**
* 根据指定用户编号,获得用户信息
*
* @param id 用户编号
* @return 用户信息
*/
UserDTO get(@NotNull(message = "用户编号不能为空") Integer id)
throws ConstraintViolationException;;

/**
* 添加新用户,返回新添加的用户编号
*
* @param addDTO 添加的用户信息
* @return 用户编号
*/
Integer add(UserAddDTO addDTO)
throws ConstraintViolationException;;

}

4.1.3 UserAddDTO

修改 UserAddDTO 接口,增加验证注解。代码如下:

public class UserAddDTO implements Serializable {

/**
* 昵称
*/
@NotEmpty(message = "昵称不能为空")
@Length(min = 5, max = 16, message = "账号长度为 5-16 位")
private String name;
/**
* 性别
*/
@NotNull(message = "性别不能为空")
private Integer gender;

// 省略 setter/getter 方法

}

4.2 搭建服务提供者

「2. 快速入门」小节的 labx-07-sca-dubbo-demo01-provider,复制出 labx-07-sca-dubbo-demo03-provider开启参数校验功能

修改 UserServiceImpl 类,将 @Service 注解的 validation 属性设置为 true,开启参数校验功能。完整代码如下:

@org.apache.dubbo.config.annotation.Service(protocol = "dubbo", version = "1.0.0", validation = "true")
public class UserServiceImpl implements UserService {

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

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

}

推荐:如果胖友想把 Dubbo 服务提供者的所有 Service 服务的参数校验都开启,可以修改 application.yaml 配置文件,增加 dubbo.provider.validation = true 配置。

4.3 搭建服务消费者

「2. 快速入门」小节的 labx-07-sca-dubbo-demo01-consumer,复制出 labx-07-sca-dubbo-demo03-consumer开启参数校验功能

修改 UserController 类,将 @Reference 注解的 validation 属性设置为 true,开启参数校验功能。完整代码如下:

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

@Reference(protocol = "dubbo", version = "1.0.0", validation = "true")
private UserService userService;

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

@PostMapping("/add")
public Integer add(UserAddDTO addDTO) {
return userService.add(addDTO);
}

}

推荐:如果胖友想把 Dubbo 服务消费者的所有 Service 服务的参数校验都开启,可以修改 application.yaml 配置文件,增加 dubbo.consumer.validation = true 配置。

可能胖友会有疑惑,服务提供者和服务消费者的 validation = true ,都是开启参数校验规则,会有什么区别呢?Dubbo 内置 ValidationFilter 过滤器,实现参数校验的功能,可作用于服务提供者和服务消费者。效果如下:

  • 如果服务消费者开启参数校验,请求参数校验不通过时,结束请求,抛出 ConstraintViolationException 异常。即,不会向服务提供者发起请求
  • 如果服务提供者开启参数校验,请求参数校验不通过时,结束请求,抛出 ConstraintViolationException 异常。即,不会执行后续的业务逻辑

实际项目在使用时,至少要开启服务提供者的参数校验功能

4.4 简单测试

① 执行 ProviderApplication 来启动服务提供者

② 执行 ConsumerApplication 来启动服务消费者

POST 请求 http://127.0.0.1:8080/user/add 接口,不传递任何参数,触发参数验证不通过的情况。如下图所示:Postman

此时,我们在 IDEA 控制台也可以看到校验不通过的错误日志,如下图所示:控制台错误

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

4.5 存在的问题

如果我们关闭掉服务消费者的参数校验功能,而只使用服务提供者的参数校验功能的情况下,当参数校验不通过时,因为 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. 自定义实现拓展点

示例代码对应仓库:labx-07-sca-dubbo-demo04-filter

「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 注入其它扩展点。

下面,我们实现一个对 Dubbo 内置的 ExceptionFilter 增强的过滤器,实现即使 Service API 接口上,未定义 ServiceException、ConstraintViolationException 等异常,也不会自动封装成 RuntimeException 。😈 毕竟,要求每个开发同学记得在 Service API 接口上,添加 ServiceException、ConstraintViolationException 等异常,是挺困难的事情,总是可能不经意遗忘。

友情提示:可能有胖友对 ExceptionFilter 异常处理不是很了解,建议看看 《浅谈 Dubbo 的 ExceptionFilter 异常处理》 文章。

因为该文章涉及到一些 Dubbo 的源码,可以先滑到结尾,直接看下结论。

还是老样子,我们从「4. 参数校验」小节,复制出对应的三个 Maven 项目来进行改造,添加自定义 ExceptionFilter 增强的过滤器的功能。最终项目如下图所示:三个 Maven 项目

艿艿:关于本小节的内容,艿艿希望胖友有看过 《芋道 Spring Boot SpringMVC 入门》「4. 全局统一返回」「5. 全局异常处理」 小节的内容,因为涉及到的思路是一致的。

5.1 搭建 API 项目

「4. 参数验证」小节的 labx-07-sca-dubbo-demo03-api,复制出 labx-07-sca-dubbo-demo04-api增加业务异常类

5.1.1 ServiceExceptionEnum

ServiceExceptionEnum 枚举类,枚举项目中的错误码。代码如下:

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

创建 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 搭建服务提供者

「4. 参数验证」小节的 labx-07-sca-dubbo-demo03-provider,复制出 labx-07-sca-dubbo-demo04-provider添加自定义 ExceptionFilter 增强的过滤器的功能

5.2.1 DubboExceptionFilter

创建 DubboExceptionFilter ,继承 ListenableFilter 抽象类,实现对 ExceptionFilter 增强的过滤器。代码如下图:DubboExceptionFilter 代码

① 在类上,添加 @Activate 注解,并设置 "group = CommonConstants.PROVIDER" 属性,将 DubboExceptionFilter 过滤器仅在服务提供者生效。

② 因为目前 Dubbo 源码改版,建议在对于 Filter 拓展点的实现,继承 ListenableFilter 抽象类,更简易的实现对调用结果的处理。

③ 在构造方法中,我们创建了 ExceptionListenerX 类,作为 listener 监听器。而 ExceptionListenerX 继承自的 ExceptionListener 类,是我们直接从 Dubbo ExceptionFilter.ExceptionListener 复制过来的逻辑,为了保持 ExceptionFilter 原有逻辑的不变。下面,让我们来看看 ExceptionListenerX 的实现代码:

复制过来的原因是,ExceptionFilter.ExceptionListener 不是 public 修饰的,导致无法直接 extends 继承。

  • <1> 处,如果是 ServiceException 异常,直接返回。
  • <2> 处,如果是参数校验的 ConstraintViolationException 异常,则调用 #handleConstraintViolationException(ConstraintViolationException ex) 方法,将 ConstraintViolationException 封装成 ServiceException 异常,之后返回。
  • <3> 处,其它情况,继续使用父类 ExceptionListener 来处理。

友情提示:可能有胖友对 ExceptionFilter 异常处理不是很了解,建议看看 《浅谈 Dubbo 的 ExceptionFilter 异常处理》 文章。

另外,DubboExceptionFilter 是 「4.5 存在问题」 的方案二的一种变种解决方案。

5.2.2 Dubbo SPI 配置文件

resources 目录下,创建 META-INF/dubbo/ 目录,然后创建 org.apache.dubbo.rpc.Filter 配置文件,配置如下:

dubboExceptionFilter=cn.iocoder.springcloudalibaba.labx7.providerdemo.filter
  • org.apache.dubbo.rpc.Filter 配置文件名,不要乱创建,就是 DubboExceptionFilter 对应的 Dubbo SPI 拓展点 Filter 。
  • 该配置文件里的每一行,格式为 ${拓展名}=${拓展类全名}。这里,我们配置了一个拓展名为 dubboExceptionFilter

5.2.3 UserRpcServiceImpl

修改 UserRpcServiceImpl 类,一共有两处修改。完整代码如下:

@org.apache.dubbo.config.annotation.Service(protocol = "dubbo", version = "1.0.0", validation = "true", filter = "-exception")
public class UserServiceImpl implements UserService {

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

@Override
public Integer add(UserAddDTO addDTO) {
// <X>【额外添加】这里,模拟用户已经存在的情况
if ("yudaoyuanma".equals(addDTO.getName())) {
throw new ServiceException(ServiceExceptionEnum.USER_EXISTS);
}
return (int) (System.currentTimeMillis() / 1000); // 嘿嘿,随便返回一个 id
}

}

① 将 @Service 注解的 filter 属性设置为 -exception,去掉服务提供者的 UserRpcService 的 ExceptionFilter 过滤器。

推荐,一般情况下啊,我们采用全局配置,即通过 dubbo.provider.filter=-exception 配置项。

② 修改下 #add(UserAddDTO addDTO) 方法,在 <X> 处抛出 ServiceException 异常。

5.3 搭建服务消费者

「4. 参数验证」小节的 labx-07-sca-dubbo-demo03-consumer,复制出 labx-07-sca-dubbo-demo04-consumer,无需做任何改动。

5.4 简单测试

① 执行 ProviderApplication 来启动服务提供者

② 执行 ConsumerApplication 来启动服务消费者

POST 请求 http://127.0.0.1:8080/user/add 接口,传递参数为 name=yudaoyuanmagender=1 ,触发抛出 ServiceException 的情况。如下图所示:Postman

符合预期~

5.5 小结

实际上,因为我们把 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 框架项目中,而不是各个业务项目中。

6. 整合 Sentinel

示例代码对应仓库:labx-07-sca-dubbo-demo05-sentinel

本小节我们来进行 Dubbo 和 Sentinel 的整合,使用 Sentinel 进行 Dubbo 的流量保护。Sentinel 提供了 sentinel-apache-dubbo-adapter 子项目,已经对 Dubbo 进行适配,所以我们只要引入它,基本就完成了 Dubbo 和 Sentinel 的整合,贼方便。

Sentinel 是阿里中间件团队开源的,面向分布式服务架构的轻量级流量控制产品,主要以流量为切入点,从流量控制熔断降级系统负载保护等多个维度来帮助用户保护服务的稳定性。

还是老样子,我们从「2. 快速入门」小节,复制出对应的三个 Maven 项目来进行改造,进行 Sentinel 的整合。最终项目如下图所示:三个 Maven 项目

友情提示:本小节需要搭建一个 Sentinel 服务,可以参考《Sentinel 极简入门》文章。

6.1 搭建 API 项目

「2. 快速入门」小节的 labx-07-sca-dubbo-demo01-api,复制出 labx-07-sca-dubbo-demo05-api,无需做任何改动。

6.2 搭建服务提供者

「2. 快速入门」小节的 labx-07-sca-dubbo-demo01-provider,复制出 labx-07-sca-dubbo-demo05-provider接入 Sentinel 实现服务提供者的流量控制

6.2.1 引入依赖

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

<!-- 引入 Spring Cloud Alibaba Sentinel 相关依赖,使用 Sentinel 提供服务保障,并实现对其的自动配置 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-apache-dubbo-adapter</artifactId>
</dependency>

6.2.2 配置文件

修改 application.yaml 配置文件,增加 spring.cloud.sentinel 配置项,进行 Sentinel 的设置。完整配置如下:

spring:
application:
name: demo-provider
cloud:
# Nacos 作为注册中心的配置项
nacos:
discovery:
server-addr: 127.0.0.1:8848 # Nacos 服务器地址
# Sentinel 配置项
sentinel:
eager: true # 是否饥饿加载。默认为 false 关闭
transport:
dashboard: 127.0.0.1:7070 # Sentinel 控制台地址

# Dubbo 配置项,对应 DubboConfigurationProperties 类
dubbo:
scan:
base-packages: cn.iocoder.springcloudalibaba.labx7.providerdemo.service # 指定 Dubbo 服务实现类的扫描基准包
# Dubbo 服务暴露的协议配置,对应 ProtocolConfig Map
protocols:
dubbo:
name: dubbo # 协议名称
port: -1 # 协议端口,-1 表示自增端口,从 20880 开始
# Dubbo 服务注册中心配置,对应 RegistryConfig 类
registry:
address: spring-cloud://127.0.0.1:8848 # 指定 Dubbo 服务注册中心的地址
# Spring Cloud Alibaba Dubbo 专属配置项,对应 DubboCloudProperties 类
cloud:
subscribed-services: '' # 设置订阅的应用列表,默认为 * 订阅所有应用。

友情提示:艿艿本机搭建的 Sentinel 控制台启动在 7070 端口。

6.3 搭建服务消费者

「2. 快速入门」小节的 labx-07-sca-dubbo-demo01-consumer,复制出 labx-07-sca-dubbo-demo05-consumer接入 Sentinel 实现服务消费者的流量控制

友情提示:整合的过程,和「6.2 搭建服务提供者」一模一样。

6.3.1 引入依赖

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

<!-- 引入 Spring Cloud Alibaba Sentinel 相关依赖,使用 Sentinel 提供服务保障,并实现对其的自动配置 -->
<dependency>
<groupId>com.alibaba.cloud</groupId>
<artifactId>spring-cloud-starter-alibaba-sentinel</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba.csp</groupId>
<artifactId>sentinel-apache-dubbo-adapter</artifactId>
</dependency>

6.2.2 配置文件

修改 application.yaml 配置文件,增加 spring.cloud.sentinel 配置项,进行 Sentinel 的设置。完整配置如下:

spring:
application:
name: demo-consumer
cloud:
# Nacos 作为注册中心的配置项
nacos:
discovery:
server-addr: 127.0.0.1:8848
# Sentinel 配置项
sentinel:
eager: true # 是否饥饿加载。默认为 false 关闭
transport:
dashboard: 127.0.0.1:7070 # Sentinel 控制台地址

# Dubbo 配置项,对应 DubboConfigurationProperties 类
dubbo:
# Dubbo 服务注册中心配置,对应 RegistryConfig 类
registry:
address: spring-cloud://127.0.0.1:8848 # 指定 Dubbo 服务注册中心的地址
# Spring Cloud Alibaba Dubbo 专属配置项,对应 DubboCloudProperties 类
cloud:
subscribed-services: demo-provider # 设置订阅的应用列表,默认为 * 订阅所有应用。

友情提示:艿艿本机搭建的 Sentinel 控制台启动在 7070 端口。

6.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.springcloudalibaba.labx7.api.UserService:get(java.lang.Integer) 资源。如下图所示:Sentinel 控制台 - 簇点链路

点击 cn.iocoder.springcloudalibaba.labx7.api.UserService: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 Cloud Alibaba 服务容错 Sentinel 入门》文章。

6.5 DubboFallback

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

另外,我们还可以配合 Dubbo 的 fallback 机制,来为降级的服务提供替代的实现。

7. 监控端点

示例代码对应仓库:labx-07-sca-dubbo-demo02-provider-springmvc

Spring Cloud Alibaba Dubbo 的 actuate 模块,基于 Spring Boot Actuator,提供了自定义监控端点 dubborestmetadata,用于获得 Dubbo Rest 元数据。

Dubbo Spring Boot 提供了 [dubbo-spring-boot-actuator] (https://github.com/apache/dubbo-spring-boot-project/tree/master/dubbo-spring-boot-actuator) 子项目,提供了更多的 Dubbo 的自定义监控端点,感兴趣的胖友可以看看《Dubbo Spring Boot Production-Ready》文档。

考虑到方便,我们直接修改「3.3 搭建服务提供者(SpringMVC)」小节的labx-07-sca-dubbo-demo02-provider-springmvc项目,演示 Spring Cloud Alibaba Dubbo 的监控端点。

7.1 引入依赖

pom.xml 文件中,额外引入 Spring Boot Actuator 相关依赖。代码如下:

<!-- 实现对 Actuator 的自动化配置 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>

<!-- 引入 Dubbo 拓展的 Actuator 自动化配置 -->
<dependency>
<groupId>org.apache.dubbo</groupId>
<artifactId>dubbo-spring-boot-actuator</artifactId>
<version>2.7.4.1</version>
</dependency>

7.2 配置文件

修改 application.yaml 配置文件,额外增加 Spring Boot Actuator 配置项。配置如下:

management:
endpoints:
web:
exposure:
include: '*' # 需要开放的端点。默认值只打开 health 和 info 两个端点。通过设置 * ,可以开放所有端点。
endpoint:
# Health 端点配置项,对应 HealthProperties 配置类
health:
enabled: true # 是否开启。默认为 true 开启。
show-details: ALWAYS # 何时显示完整的健康信息。默认为 NEVER 都不展示。可选 WHEN_AUTHORIZED 当经过授权的用户;可选 ALWAYS 总是展示。

每个配置项的作用,胖友看下艿艿添加的注释。如果还不理解的话,后续看下《芋道 Spring Boot 监控端点 Actuator 入门》文章。

7.3 简单测试

① 执行 SpringMVCProviderApplication 来启动服务提供者。

② 访问服务提供者的 dubborestmetadata 监控端点 http://127.0.0.1:18080/actuator/bindings,返回结果如下图:`dubborestmetadata` 监控端点

③ [dubbo-spring-boot-actuator] (https://github.com/apache/dubbo-spring-boot-project/tree/master/dubbo-spring-boot-actuator) 提供的 Dubbo 的自定义监控端点,胖友参考《Dubbo Spring Boot Production-Ready》文档来测试哈。

要注意,dubboservicesdubboreferences 监控端点默认是关闭的,所以胖友需要自己在配置文件中,主要进行下开启噢。

666. 彩蛋

至此,我们已经完成 Spring Cloud Alibaba Dubbo 的学习。如下是 Dubbo 相关的官方文档:

另外,想要在 Spring Boot 项目中使用 Dubbo 作为 RPC 框架的胖友,可以阅读《芋道 Spring Boot Dubbo 入门》文章。

哈哈,如果胖友对 Dubbo 的源码感兴趣,可以看看《Dubbo 实现原理与源码解析系列 —— 精品合》

文章目录
  1. 1. 1. 概述
  2. 2. 2. 快速入门
    1. 2.1. 2.1 搭建 API 项目
      1. 2.1.1. 2.1.1 DTO
      2. 2.1.2. 2.1.2 UserService
    2. 2.2. 2.2 搭建服务提供者
      1. 2.2.1. 2.2.1 引入依赖
      2. 2.2.2. 2.2.2 配置文件
      3. 2.2.3. 2.2.3 UserServiceImpl
      4. 2.2.4. 2.2.4 ProviderApplication
    3. 2.3. 2.3 搭建服务消费者
      1. 2.3.1. 2.3.1 引入依赖
      2. 2.3.2. 2.3.2 配置文件
      3. 2.3.3. 2.3.3 UserController
      4. 2.3.4. 2.3.4 ConsumerApplication
    4. 2.4. 2.4 简单测试
  3. 3. 3. Feign 调用 Dubbo 服务
    1. 3.1. 3.1 搭建 API 项目
    2. 3.2. 3.2 搭建服务提供者(Rest)
      1. 3.2.1. 3.2.1 引入依赖
      2. 3.2.2. 3.2.2 配置文件
      3. 3.2.3. 3.2.3 UserServiceImpl
      4. 3.2.4. 3.2.4 启动服务提供者
    3. 3.3. 3.3 搭建服务提供者(SpringMVC)
      1. 3.3.1. 3.3.1 引入依赖
      2. 3.3.2. 3.3.2 配置文件
      3. 3.3.3. 3.3.3 UserServiceImpl
      4. 3.3.4. 3.3.4 启动服务提供者
    4. 3.4. 3.4 搭建服务消费者
      1. 3.4.1. 3.4.1 引入依赖
      2. 3.4.2. 3.4.2 FeignConsumerApplication
      3. 3.4.3. 3.4.3 服务调用方式一:Feign + Dubbo
      4. 3.4.4. 3.4.4 服务调用方式二:Feign + Ribbon
      5. 3.4.5. 3.4.5 服务调用方式三:RestTemplate + Dubbo
      6. 3.4.6. 3.4.6 服务调用方式四:Dubbo
    5. 3.5. 3.5 简单测试
  4. 4. 4. 参数验证
    1. 4.1. 4.1 搭建 API 项目
      1. 4.1.1. 4.1.1 引入依赖
      2. 4.1.2. 4.1.2 UserService
      3. 4.1.3. 4.1.3 UserAddDTO
    2. 4.2. 4.2 搭建服务提供者
    3. 4.3. 4.3 搭建服务消费者
    4. 4.4. 4.4 简单测试
    5. 4.5. 4.5 存在的问题
  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 搭建服务提供者
      1. 5.2.1. 5.2.1 DubboExceptionFilter
      2. 5.2.2. 5.2.2 Dubbo SPI 配置文件
      3. 5.2.3. 5.2.3 UserRpcServiceImpl
    3. 5.3. 5.3 搭建服务消费者
    4. 5.4. 5.4 简单测试
    5. 5.5. 5.5 小结
  6. 6. 6. 整合 Sentinel
    1. 6.1. 6.1 搭建 API 项目
    2. 6.2. 6.2 搭建服务提供者
      1. 6.2.1. 6.2.1 引入依赖
      2. 6.2.2. 6.2.2 配置文件
    3. 6.3. 6.3 搭建服务消费者
      1. 6.3.1. 6.3.1 引入依赖
      2. 6.3.2. 6.2.2 配置文件
    4. 6.4. 6.4 简单测试
    5. 6.5. 6.5 DubboFallback
  7. 7. 7. 监控端点
    1. 7.1. 7.1 引入依赖
    2. 7.2. 7.2 配置文件
    3. 7.3. 7.3 简单测试
  8. 8. 666. 彩蛋