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 CloudDubbo Spring Cloud分布式配置Git、Zookeeper、Consul、JDBCSpring Cloud 分布式配置 + Dubbo 配置中心服务注册与发现Eureka、Zookeeper、ConsulSpring Cloud 原生注册中心 + Dubbo 原生注册中心负载均衡Ribbon(随机、轮询等算法)Dubbo 内建实现(随机、轮询等算法 + 权重等特性)服务熔断Spring Cloud HystrixSpring Cloud Hystrix + Alibaba Sentinel 等服务调用Open Feign、RestTemplateSpring Cloud 服务调用 + Dubbo
@Reference
链路跟踪Spring Cloud Sleuth + ZipkinZipkin、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 项目,如下图所示:
- labx-07-sca-dubbo-demo01-api 项目:服务接口,定义 Dubbo Service API 接口,提供给消费者使用。
详细代码,我们在 「2.1 搭建 API 项目」 讲解。
- labx-07-sca-dubbo-demo01-provider 项目:服务提供者,实现
labx-07-sca-dubbo-demo01-api
项目定义的 Dubbo Service API 接口,提供相应的服务。
详细代码,我们在 「2.2 搭建服务提供者」 中讲解。
- labx-07-sca-dubbo-demo01-consumer 项目:服务消费者,会调用
labx-07-sca-dubbo-demo01-provider
项目提供的 Dubbo Service 服务。
详细代码,我们在 「2.3 搭建服务消费者」 中讲解。
2.1 搭建 API 项目
创建 labx-07-sca-dubbo-demo01-api 项目,服务接口,定义 Dubbo Service API 接口,提供给消费者使用。
2.1.1 DTO
创建 UserAddDTO 和 UserDTO 两个 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.scan
、dubbo.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
服务,如下图所示:
如果之前使用过 Dubbo 的胖友应该知道,Dubbo 是基于 Service 服务为粒度注册到注册中心的,这点和 Spring Cloud 是基于 Application 应用为粒度是不同的。Spring Cloud Alibaba Dubbo 定义了 DubboMetadataService 接口,用于获取对应的应用实例的 Service 的元数据。
② 执行 ConsumerApplication 来启动服务消费者。可以在 Nacos 可以看到 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 项目,如下图所示:
- labx-07-sca-dubbo-demo02-api 项目:服务接口,定义 Dubbo Service API 接口,提供给消费者使用。
详细代码,我们在 「3.1 搭建 API 项目」 讲解。
- labx-07-sca-dubbo-demo02-provider-rest 项目:额外使用 rest:// 协议来提供 HTTP 接口的服务提供者,实现
labx-07-sca-dubbo-demo02-api
项目定义的 Dubbo Service API 接口,提供相应的服务。
详细代码,我们在 「3.2 搭建服务提供者(Rest)」 中讲解。
- labx-07-sca-dubbo-demo02-provider-springmvc 项目:额外使用 SpringMVC 来提供 HTTP 接口的服务提供者,实现
labx-07-sca-dubbo-demo02-api
项目定义的 Dubbo Service API 接口,提供相应的服务。
详细代码,我们在 「3.3 搭建服务提供者(SpringMVC)」 中讲解。
- labx-07-sca-dubbo-demo02-consumer 项目:服务消费者,会使用 Feign 来调用
labx-07-sca-dubbo-demo02-provider-rest
或labx-07-sca-dubbo-demo02-provider-springmvc
项目提供的 Dubbo Service 服务。
详细代码,我们在 「3.4 搭建服务消费者」 中讲解。
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
服务,如下图所示:
重点是端口为 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
服务,如下图所示:
重点是端口为 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-rest
或 labx-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
依赖,这里使用到的 UserDTO 和 UserAddDTO 类都是在 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 来实现验证。
- 我们在 《Spring Boot 参数校验 Validation 入门》 中学习的,也是基于 JSR303 规范实现的,所以在使用上,是基本一致的。有统一的规范,真好。
还是老样子,我们从「2. 快速入门」小节,复制出对应的三个 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 接口,不传递任何参数,触发参数验证不通过的情况。如下图所示:
此时,我们在 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)
目前有两种解决方案:
- 方案一,不要关闭掉服务消费者的参数校验功能。
- 方案二,参考 《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. 自定义实现拓展点
示例代码对应仓库:labx-07-sca-dubbo-demo04-filter 。
在「4. 参数校验」 小节中,我们入门了 Dubbo 提供的参数校验的功能,它是由 ValidationFilter 过滤器,通过拦截请求,根据我们添加 JSR303 定义的注解,校验参数是否正确。在 Dubbo 框架中,还提供了 AccessLogFilter、ExceptionFilter 等等过滤器,他们都属于 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 增强的过滤器的功能。最终项目如下图所示:
艿艿:关于本小节的内容,艿艿希望胖友有看过 《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 增强的过滤器。代码如下图:
① 在类上,添加 @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=yudaoyuanma
和 gender=1
,触发抛出 ServiceException 的情况。如下图所示:
符合预期~
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 中,会把 ServiceException 和 DubboExceptionFilter 放在 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 的整合。最终项目如下图所示:
友情提示:本小节需要搭建一个 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 # 设置订阅的应用列表,默认为 * 订阅所有应用。
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)
资源。如下图所示:
点击 cn.iocoder.springcloudalibaba.labx7.api.UserService:get(java.lang.Integer)
资源所在列的「流控」按钮,弹出「新增流控规则」。填写流控规则,如下图所示:
- 这里,我们创建的是比较简单的规则,仅允许该资源被每秒调用一次。
④ 使用浏览器,快速访问 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 处理。
- 我们只需要实现自定义的 DubboFallback 接口,并通过 DubboFallbackRegistry 进行注册即可。
- 默认情况下,使用 DubboFallback 的 DefaultDubboFallback 实现类,它会将 BlockException 包装成 SentinelRpcException 异常后抛出。
另外,我们还可以配合 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,返回结果如下图:
③ [dubbo-spring-boot-actuator
] (https://github.com/apache/dubbo-spring-boot-project/tree/master/dubbo-spring-boot-actuator) 提供的 Dubbo 的自定义监控端点,胖友参考《Dubbo Spring Boot Production-Ready》文档来测试哈。
要注意,dubboservices
和 dubboreferences
监控端点默认是关闭的,所以胖友需要自己在配置文件中,主要进行下开启噢。
至此,我们已经完成 Spring Cloud Alibaba Dubbo 的学习。如下是 Dubbo 相关的官方文档: