点击上方“芋道源码”,选择“设为星标”
管她前浪,还是后浪?
能浪的浪,才是好浪!
每天 8:55 更新文章,每天掉亿点点头发...
源码精品专栏
原创 | Java 2020 超神之路,很肝~
中文详细注释的开源项目
RPC 框架 Dubbo 源码解析
网络应用框架 Netty 源码解析
消息中间件 RocketMQ 源码解析
数据库中间件 Sharding-JDBC 和 MyCAT 源码解析
作业调度中间件 Elastic-Job 源码解析
分布式事务中间件 TCC-Transaction 源码解析
Eureka 和 Hystrix 源码解析
Java 并发源码
1. 概述
2. XML 配置
3. 注解配置
4. 参数验证
5. 自定义实现拓展点
6. 整合 Nacos
6. 整合 Sentinel
666. 彩蛋
本文在提供完整代码示例,可见 https://github.com/YunaiV/SpringBoot-Labs 的 lab-30 目录。
原创不易,给点个 Star 嘿,一起冲鸭!
在 2019.05.21 号,在经历了 1 年多的孵化,Dubbo 终于迎来了 Apache 毕业。在这期间,Dubbo 做了比较多的功能迭代,提供了 NodeJS、Python、Go 等语言的支持,也举办了多次社区活动,在网上的“骂声”也少了。
艿艿:事实上,大多数成熟的开源项目,都是 KPI 驱动,又或者背后有商业化支撑。
作为一个长期使用,并且坚持使用 Dubbo 的开发者,还是比较愉快的。可能,又经历了一次技术正确的选择。当然,更愉快的是,Spring Cloud Alibaba 貌似,也已经完成孵化,双剑合并,biubiubiu 。
可能胖友有些胖友对 Dubbo 不是很了解,这里艿艿先简单介绍下:
Dubbo 整体架构FROM Dubbo 官网
Apache Dubbo |ˈdʌbəʊ| 是一款高性能、轻量级的开源 Java RPC 框架,它提供了三大核心能力:面向接口的远程方法调用,智能容错和负载均衡,以及服务自动注册和发现。
图中,一共涉及到 5 个角色:
Registry 注册中心,用于服务的注册与发现。
Provider 服务提供者,通过向 Registry 注册服务。
Consumer 服务消费者,通过从 Registry 发现服务。后续直接调用 Provider ,无需经过 Registry 。
Monitor 监控中心,统计服务的调用次数和调用时间。
Container 服务运行容器。
FROM 《Dubbo 文档 —— 架构》
调用关系说明(注意,和上图的数字,和下面的步骤是一一对应的):
服务容器负责启动,加载,运行服务提供者。
服务提供者在启动时,向注册中心注册自己提供的服务。
服务消费者在启动时,向注册中心订阅自己所需的服务。
注册中心返回服务提供者地址列表给消费者,如果有变更,注册中心将基于长连接推送变更数据给消费者。
服务消费者,从提供者地址列表中,基于软负载均衡算法,选一台提供者进行调用,如果调用失败,再选另一台调用。
服务消费者和提供者,在内存中累计调用次数和调用时间,定时每分钟发送一次统计数据到监控中心。
本文的重心,在于一起入门 Provider 和 Consumer 的代码编写,这也是实际项目开发中,我们涉及到的角色。
Dubbo 提供了比较多的配置方式,日常开发中主要使用的是 XML 配置 和 注解配置 。我们分别会在 「2. XML 配合」 和 「3. 注解配置」 小节来入门。
考虑到现在 Dubbo 已经提供了 dubbo-spring-boot-project
项目,集成到 Spring Boot 体系中,而大家都基本采用 Spring Boot 框架,所以我们就不像 Dubbo 官方文档 一样,提供的是 Spring 环境下的示例,而是 Spring Boot 环境下。
示例代码对应仓库:lab-30-dubbo-xml-demo 。
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」 中讲解。
对应 user-rpc-service-api
项目,服务接口,定义 Dubbo Service API 接口,提供给消费者使用。
在 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 会涉及远程通信,需要序列化和反序列化。
在 cn.iocoder.springboot.lab30.rpc.api
包下,创建 Dubbo Service API 接口。这里,我们创建 UserRpcService 接口,用户服务 RPC Service 接口。代码如下:
// UserRpcService.java
public interface UserRpcService {
/**
* 根据指定用户编号,获得用户信息
*
* @param id 用户编号
* @return 用户信息
*/
UserDTO get(Integer id);
}
对应 user-rpc-service-provider
项目,服务提供者,实现 user-rpc-service-api
项目定义的 Dubbo Service API 接口,提供相应的服务。
在 pom.xml
文件中,引入相关依赖。
org.springframework.boot
spring-boot-starter-parent
2.2.1.RELEASE
4.0.0
user-rpc-service
cn.iocoder.springboot.labs
user-rpc-service-api
1.0-SNAPSHOT
org.springframework.boot
spring-boot-starter
org.apache.dubbo
dubbo
2.7.4.1
org.apache.dubbo
dubbo-spring-boot-starter
2.7.4.1
org.apache.curator
curator-framework
2.13.0
org.apache.curator
curator-recipes
2.13.0
因为我们希望实现对 Dubbo 的自动化配置,所以引入 dubbo-spring-boot-starter
依赖。
因为我们希望使用 Zookeeper 作为注册中心,所以引入 curator-framework
和 curator-recipes
依赖。可能胖友不太了解 Apache Curator 框架,这里我们看一段简介:
FROM https://www.oschina.net/p/curator
Zookeeper 的客户端调用过于复杂,Apache Curator 就是为了简化Zookeeper 客户端调用而生,利用它,可以更好的使用 Zookeeper。
虽然说,目前阿里正在大力推广 Nacos 作为 Dubbo 的注册中心,但是大多数团队,采用的还是 Zookeeper 为主。
对了,如果胖友不知道怎么安装 Zookeeper ,可以看看 《芋道 Zookeeper 极简入门》 文章。
在 resources
目录下, 创建 application.yml
配置文件,添加 Dubbo 相关的配置,如下:
# dubbo 配置项,对应 DubboConfigurationProperties 配置类
dubbo:
# Dubbo 应用配置
application:
name: user-service-provider # 应用名
# Dubbo 注册中心配
registry:
address: zookeeper://127.0.0.1:2181 # 注册中心地址。个鞥多注册中心,可见 http://dubbo.apache.org/zh-cn/docs/user/references/registry/introduction.html 文档。
# Dubbo 服务提供者协议配置
protocol:
port: -1 # 协议端口。使用 -1 表示随机端口。
name: dubbo # 使用 `dubbo://` 协议。更多协议,可见 http://dubbo.apache.org/zh-cn/docs/user/references/protocol/introduction.html 文档
# Dubbo 服务提供者配置
provider:
timeout: 1000 # 【重要】远程服务调用超时时间,单位:毫秒。默认为 1000 毫秒,胖友可以根据自己业务修改
UserRpcService:
version: 1.0.0
dubbo-spring-boot-starter
依赖,会根据 dubbo
配置项,实现对 Dubbo 的自动化配置。下面呢,我们来逐个配置项看看。
艿艿:本小节,我们的 「XML 配置」 ,指的是使用 XML 来配置 Dubbo Service 服务。如果胖友想看纯粹的全量 XML 配置,可以看看 《Dubbo 官方文档 —— XML 配置》 。
dubbo.application
配置项,Dubbo 应用信息配置。更多属性,可见 ApplicationConfig 类。每个属性的说明,可见 《Dubbo 文档 —— dubbo:application》 。
dubbo.registry
配置项,Dubbo 注册中心配置。更多属性,可见 RegistryConfig 类。每个属性的说明,可见 《Dubbo 文档 —— dubbo:registry》 。
dubbo.protocol
配置项,Dubbo 服务提供者协议配置。更多属性,可见 ProtocolConfig 类。每个属性的说明,可见 《Dubbo 文档 —— dubbo:protocol》 。
dubbo.provider
配置项,Dubbo 服务提供者配置。更多属性,可见 ProviderConfig 类。每个属性的说明,可见 《Dubbo 文档 —— dubbo:provider》 。
dubbo.provider.UserRpcService
配置项,是我们自定义的,设置每个 Service 服务的配置。更多属性,可见 ServiceConfig 类。每个属性的说明,可见 《Dubbo 文档 —— dubbo:service》 。
在 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 服务。
在 resources
目录下, 创建 dubbo.xml
配置文件,添加 Dubbo 的 Service 服务提供者,如下:
使用 Dubbo 自定义的 Spring
标签,配置我们 「2.2.3 UserRpcServiceImpl」 成 UserRpcService 的 Dubbo 服务提供者。
更多
标签的属性的说明,可见 《Dubbo 文档 —— dubbo:service》 。
创建 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 注册中心》 文档。
对应 user-rpc-service-consumer
项目,服务消费者,会调用 user-rpc-service-provider
项目提供的 Dubbo Service 服务。
在 pom.xml
文件中,引入相关依赖。
org.springframework.boot
spring-boot-starter-parent
2.2.1.RELEASE
4.0.0
user-rpc-service-consumer
cn.iocoder.springboot.labs
user-rpc-service-api
1.0-SNAPSHOT
org.springframework.boot
spring-boot-starter
org.apache.dubbo
dubbo
2.7.4.1
org.apache.dubbo
dubbo-spring-boot-starter
2.7.4.1
org.apache.curator
curator-framework
2.13.0
org.apache.curator
curator-recipes
2.13.0
和 「2.2.1 引入依赖」 一模一样,除了
改成了 "user-rpc-service-consumer"
值。
在 resources
目录下, 创建 application.yml
配置文件,添加 Dubbo 相关的配置,如下:
# dubbo 配置项,对应 DubboConfigurationProperties 配置类
dubbo:
# Dubbo 应用配置
application:
name: user-service-consumer # 应用名
# Dubbo 注册中心配置
registry:
address: zookeeper://127.0.0.1:2181 # 注册中心地址。个鞥多注册中心,可见 http://dubbo.apache.org/zh-cn/docs/user/references/registry/introduction.html 文档。
# Dubbo 消费者配置
consumer:
timeout: 1000 # 【重要】远程服务调用超时时间,单位:毫秒。默认为 1000 毫秒,胖友可以根据自己业务修改
UserRpcService:
version: 1.0.0
和 「2.2.2 应用配置文件」 看起来有点类似,我们仅仅说说差异性。
去掉 dubbo.protocol
配置项,因为我们是作为 Dubbo 服务的消费者,所以无需添加 Dubbo 服务提供者协议配置。
dubbo.consumer
配置项,Dubbo 服务消费者配置。更多属性,可见 ConsumerConfig 类。每个属性的说明,可见 《Dubbo 文档 —— dubbo:consumer》 。
dubbo.consumer.UserRpcService
配置项,是我们自定义的,设置每个 Service 服务的配置。更多属性,可见 ReferenceConfig 类。每个属性的说明,可见 《Dubbo 文档 —— dubbo:reference》 。
在 resources
目录下,创建 dubbo.xml
配置文件,添加 Dubbo 的 Service 服务引用者,如下:
使用 Dubbo 自定义的 Spring
标签,引用 UserRpcService 接口对应的 Dubbo Service 服务,并创建一个 Bean 编号为 "userService"
的 Bean 对象。这样,我们在 Spring 中,就可以直接注入 UserRpcService Bean ,后续就可以像一个“本地”的 UserRpcService 进行调用使用。
更多
标签的属性的说明,可见 《Dubbo 文档 —— dubbo:reference》 。
创建 ConsumerApplication 类,用于启动该项目,调用 Dubbo 服务。代码如下:
// ConsumerApplication.java
@SpringBootApplication
@ImportResource("classpath:dubbo.xml")
public class ConsumerApplication {
public static void main(String[] args) {
// 启动 Spring Boot 应用
ConfigurableApplicationContext context = SpringApplication.run(ConsumerApplication.class, args);
}
@Component
public class UserRpcServiceTest implements CommandLineRunner {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Resource
private UserRpcService userRpcService;
@Override
public void run(String... args) throws Exception {
UserDTO user = userRpcService.get(1);
logger.info("[run][发起一次 Dubbo RPC 请求,获得用户为({})", user);
}
}
}
在类上,添加 @ImportResource
注解,引入 dubbo.xml
配置文件。
在 UserRpcServiceTest 中,我们使用 @Resource
注解,引用通过
配置的引用的 UserRpcService 服务对应的 UserRpcService Bean 。
运行 #main(String[] args)
方法,启动项目。控制台打印日志如下:
2019-12-01 23:15:47.380 INFO 65726 --- [ main] r.ConsumerApplication$UserRpcServiceTest : [run][发起一次 Dubbo RPC 请求,获得用户为(cn.iocoder.springboot.lab30.rpc.dto.UserDTO@a0a9fa5)
我们在应用启动完成后,成功的发起了一次 UserRpcService 的 Dubbo RPC 的调用。
我们来使用 Zookeeper 客户端,查看 UserRpcService 服务是否多了一个消费者。操作流程如下:
# 使用 Zookeeper 自带的客户端,连接到 Zookeeper 服务器
$ bin/zkCli.sh
# 查看 UserRpcService 服务的消费者列表
# 此时,可以看到有一个节点,就是我们刚启动的服务消费者。
$ ls /dubbo/cn.iocoder.springboot.lab30.rpc.api.UserRpcService/consumers
[consumer%3A%2F%2F10.171.1.115%2Fcn.iocoder.springboot.lab30.rpc.api.UserRpcService%3Fapplication%3Duser-service-consumer%26category%3Dconsumers%26check%3Dfalse%26dubbo%3D2.0.2%26interface%3Dcn.iocoder.springboot.lab30.rpc.api.UserRpcService%26lazy%3Dfalse%26methods%3Dget%26pid%3D65726%26qos.enable%3Dfalse%26release%3D2.7.4.1%26revision%3D1.0.0%26side%3Dconsumer%26sticky%3Dfalse%26timeout%3D1000%26timestamp%3D1575213346748%26version%3D1.0.0]
至此,我们已经完成了使用 XML 配置的方式,在 Spring Boot 中使用 Dubbo 的入门。???? 虽然篇幅长了一点点,但是还是比较简单的。个人建议的话,此时此刻仅仅是看到这里,但是并没有手敲代码的胖友,可以赶紧打开 IDEA 自己敲(“抄”)一波,嘿嘿。
示例代码对应仓库:lab-30-dubbo-annotations-demo 。
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 配置」 会比较接近,所以会讲的相对简略,重点说差异。
艿艿:为了保证阅读体验,即使一致的内容,艿艿还是贴一遍比较好。
对应 user-rpc-service-api-02
项目,服务接口,定义 Dubbo Service API 接口,提供给消费者使用。
和 「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」 一致。
在 cn.iocoder.springboot.lab30.rpc.api
包下,创建 Dubbo Service API 接口。这里,我们创建 UserRpcService 接口,用户服务 RPC Service 接口。代码如下:
// UserRpcService.java
public interface UserRpcService {
/**
* 根据指定用户编号,获得用户信息
*
* @param id 用户编号
* @return 用户信息
*/
UserDTO get(Integer id);
}
对应 user-rpc-service-provider-02
项目,服务提供者,实现 user-rpc-service-api
项目定义的 Dubbo Service API 接口,提供相应的服务。
和 「2.2.1 引入依赖」 一致。
在 pom.xml
文件中,引入相关依赖。
org.springframework.boot
spring-boot-starter-parent
2.2.1.RELEASE
4.0.0
user-rpc-service-provider-02
cn.iocoder.springboot.labs
user-rpc-service-api-02
1.0-SNAPSHOT
org.springframework.boot
spring-boot-starter
org.apache.dubbo
dubbo
2.7.4.1
org.apache.dubbo
dubbo-spring-boot-starter
2.7.4.1
org.apache.curator
curator-framework
2.13.0
org.apache.curator
curator-recipes
2.13.0
在 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
注解。
在 cn.iocoder.springboot.lab30.rpc.service
包下,创建 Dubbo Service 实现类。这里,我们创建 UserRpcServiceImpl 类,用户服务 RPC Service 实现类。代码如下:
// UserRpcServiceImpl.java
@Service(version = "${dubbo.provider.UserRpcService.version}")
public class UserRpcServiceImpl implements UserRpcService {
@Override
public UserDTO get(Integer id) {
return new UserDTO().setId(id)
.setName("没有昵称:" + id)
.setGender(id % 2 + 1); // 1 - 男;2 - 女
}
}
在类上,我们添加的是 Dubbo 定义的 @Service
注解。并且,在该注解里,我们可以添加该 Service 服务的配置。当然,每个属性和
标签是基本一致的。也因此,每个属性的说明,还可见 《Dubbo 文档 —— dubbo:service》 。
创建 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」 是一致的,这里就不重复赘述了。
对应 user-rpc-service-consumer-02
项目,服务消费者,会调用 user-rpc-service-provider-02
项目提供的 Dubbo Service 服务。
和 「2.3.1 引入依赖」 一致。
在 pom.xml
文件中,引入相关依赖。
org.springframework.boot
spring-boot-starter-parent
2.2.1.RELEASE
4.0.0
user-rpc-service-consumer-02
cn.iocoder.springboot.labs
user-rpc-service-api-02
1.0-SNAPSHOT
org.springframework.boot
spring-boot-starter
org.apache.dubbo
dubbo
2.7.4.1
org.apache.dubbo
dubbo-spring-boot-starter
2.7.4.1
org.apache.curator
curator-framework
2.13.0
org.apache.curator
curator-recipes
2.13.0
和 「3.2.1 引入依赖」 一模一样,除了
改成了 "user-rpc-service-consumer-02"
值。
和 「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
创建 ConsumerApplication 类,用于启动该项目,调用 Dubbo 服务。代码如下:
// ConsumerApplication.java
@SpringBootApplication
public class ConsumerApplication {
public static void main(String[] args) {
// 启动 Spring Boot 应用
ConfigurableApplicationContext context = SpringApplication.run(ConsumerApplication.class, args);
}
@Component
public class UserRpcServiceTest implements CommandLineRunner {
private final Logger logger = LoggerFactory.getLogger(getClass());
@Reference(version = "${dubbo.consumer.UserRpcService.version}")
private UserRpcService userRpcService;
@Override
public void run(String... args) throws Exception {
UserDTO user = userRpcService.get(1);
logger.info("[run][发起一次 Dubbo RPC 请求,获得用户为({})", user);
}
}
}
在类上,无需添加 @ImportResource
注解,引入 dubbo.xml
配置文件。
在 UserRpcServiceTest 中,我们使用 Dubbo 定义的 @Reference
注解,**“直接”**引用的 UserRpcService 服务对应的 UserRpcService Bean 。并且,在该注解里,我们可以添加该 Service 服务的配置。当然,每个属性和
标签是基本一致的。也因此,每个属性的说明,还可见 《Dubbo 文档 —— dubbo:reference》 。
艿艿:后续的操作,和 「2.3.4 ConsumerApplication」 是一致的,这里就不重复赘述了。
艿艿个人倾向的话,偏向使用 XML 配置。
主要原因是,@Reference
注解,每次引用服务的时候,都需要在注解上添加好多配置的属性。这样,服务的引用的配置后就散落到各个类里了。
虽然说,我们可以把 @Reference
注解的配置的属性值,放到 application.yaml
等等配置文件里,但是如果我们要给相同 Service 的多个 @Reference
增加新的配置属性时,就要每个注解都修改一遍。
对于这种情况,XML 配置的方式,只要修改一下该 Service 的 XML 配置,就可以全部生效了。
参数校验,对于提供 API 调用的服务来说,必然是必不可少的。在 《芋道 Spring Boot 参数校验 Validation 入门》 中,我们已经看了如何在 SpringMVC 和本地的 Service 使用参数校验的示例。
本小节,我们来学习下,如何在 Dubbo RPC Service 中,使用参数校验。在 《Dubbo 文档 —— 参数验证》 中,对该功能的描述如下:
参数验证功能是基于 JSR303 实现的,用户只需标识 JSR303 标准的验证 annotation,并通过声明 filter 来实现验证。
我们在 《芋道 Spring Boot 参数校验 Validation 入门》 中学习的,也是基于 JSR303 规范实现的,所以在使用上,是基本一致的。???? 有统一的规范,真好。
下面,我们开始在 「2. XML 配置」 小节的 lab-30-dubbo-xml-demo 示例项目,进行修改,添加参数校验的功能。
本小节,我们来看看对 user-rpc-service-api
项目的改造。
修改 pom.xml
文件中,引入相关依赖。
lab-30-dubbo-xml-demo
cn.iocoder.springboot.labs
1.0-SNAPSHOT
4.0.0
user-rpc-service-api
javax.validation
validation-api
2.0.1.Final
org.hibernate.validator
hibernate-validator
6.0.18.Final
org.glassfish
javax.el
3.0.1-b11
在 cn.iocoder.springboot.lab30.rpc.dto
包下,创建 UserAddDTO 类,用户添加 DTO。代码如下:
// UserAddDTO.java
public class UserAddDTO implements Serializable {
/**
* 昵称
*/
@NotEmpty(message = "昵称不能为空")
@Length(min = 5, max = 16, message = "账号长度为 5-16 位")
private String name;
/**
* 性别
*/
@NotNull(message = "性别不能为空")
private Integer gender;
// ... 省略 set/get 方法
}
在 name
和 gender
属性上,我们添加了参数校验的注解。
修改 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 的异常处理机制。
本小节,我们来看看对 user-rpc-service-provider
项目的改造。
修改 UserRpcServiceImpl 类,简单实现下 #add(UserAddDTO addDTO)
方法。代码如下:
// UserRpcServiceImpl.java
@Override
public Integer add(UserAddDTO addDTO) {
return (int) (System.currentTimeMillis() / 1000); // 嘿嘿,随便返回一个 id
}
修改 dubbo.xml
配置文件,开启 UserRpcService 的参数校验功能。配置如下:
这里,我们将 validation
设置为 "true"
,开启 Dubbo 服务提供者的 UserRpcService 服务的参数校验的功能。
???? 如果胖友想把 Dubbo 服务提供者的所有 Service 服务的参数校验都开启,可以修改 application.yaml
配置文件,增加 dubbo.provider.validation = true
配置。
本小节,我们来看看对 user-rpc-service-consumer
项目的改造。
修改 dubbo.xml
配置文件,开启 UserRpcService 的参数校验功能。配置如下:
这里,我们将 validation
设置为 "true"
,开启 Dubbo 服务消费者的 UserRpcService 服务的参数校验的功能。
???? 如果胖友想把 Dubbo 服务消费者的所有 Service 服务的参数校验都开启,可以修改 application.yaml
配置文件,增加 dubbo.consumer.validation = true
配置。
可能胖友会有疑惑,服务提供者和服务消费者的 validation = true
,都是开启参数校验规则,会有什么区别呢?Dubbo 内置 ValidationFilter 过滤器,实现参数校验的功能,可作用于服务提供者和服务消费者。效果如下:
如果服务消费者开启参数校验,请求参数校验不通过时,结束请求,抛出 ConstraintViolationException 异常。即,不会向服务提供者发起请求。
如果服务提供者开启参数校验,请求参数校验不通过时,结束请求,抛出 ConstraintViolationException 异常。即,不会执行后续的业务逻辑。
实际项目在使用时,至少要开启服务提供者的参数校验功能。
修改 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 异常。
如果我们关闭掉服务消费者的参数校验功能,而只使用服务提供者的参数校验功能的情况下,当参数校验不通过时,因为 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 开发团队否决了。所以,目前建议还是采用方案一来解决。
在「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 注入其它扩展点。
下面,我们实现一个对 ExceptionFilter 增强的过滤器,实现即使 Service API 接口上,未定义 ServiceException、ConstraintViolationException 等异常,也不会自动封装成 RuntimeException 。???? 毕竟,要求每个开发同学记得在 Service API 接口上,添加 ServiceException、ConstraintViolationException 等异常,是挺困难的事情,总是可能不经意遗忘。
下面,我们继续在 「2. XML 配置」 小节的 lab-30-dubbo-xml-demo 示例项目,进行修改,添加自定义 ExceptionFilter 增强的过滤器的功能。
艿艿:关于本小节的内容,艿艿希望胖友有看过 《芋道 Spring Boot SpringMVC 入门》 的 「4. 全局统一返回」 和 「5. 全局异常处理」 小节的内容,因为涉及到的思路是一致的。
本小节,我们来看看对 user-rpc-service-api
项目的改造。
在 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 位,错误码
* 不限制规则。
* 一般建议,每个模块自增。
*/
在 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;
}
}
本小节,我们来看看对 user-rpc-service-provider
项目的改造。
在 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 存在问题」 的方案二的一种变种解决方案。
在 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
。
修改 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
}
修改 application.yml
配置文件,添加 dubbo.provider.filter=-exception
配置项,去掉服务提供者的 ExceptionFilter 过滤器。
如果胖友仅仅想去掉 UserRpcService 服务的 ExceptionFilter 过滤器,可以修改 dubbo.xml
配置文件,配置如下:
这里,我们将 filter
设置为 "-exception"
,去掉服务提供者的 UserRpcService 的 ExceptionFilter 过滤器。
当然,一般情况下啊,我们采用全局配置,即通过 dubbo.provider.filter=-exception
配置项。
本小节,我们来看看对 user-rpc-service-consumer
项目的改造。
修改 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 异常。
实际上,因为我们把 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 框架项目中,而不是各个业务项目中。
示例代码对应仓库:
lab-30-dubbo-annotations-nacos
。
本小节我们来进行 Dubbo 和 Nacos 的整合,使用 Nacos 作为 Dubbo 的注册中心。Dubbo 提供了 dubbo-registry-nacos
子项目,已经对 Nacos 进行适配,所以我们只要引入它,基本就完成了 Dubbo 和 Nacos 的整合,贼方便。
Nacos 致力于帮助您发现、配置和管理微服务。Nacos 提供了一组简单易用的特性集,帮助您快速实现动态服务发现、服务配置、服务元数据及流量管理。
Nacos 帮助您更敏捷和容易地构建、交付和管理微服务平台。Nacos 是构建以“服务”为中心的现代应用架构 (例如微服务范式、云原生范式) 的服务基础设施。
还是老样子,我们从「3. 注解配置」小节,复制出对应的三个 Maven 项目来进行改造,进行 Nacos 的整合。最终项目如下图所示:
友情提示:本小节需要搭建一个 Nacos 服务,可以参考《Nacos 极简入门》文章。
将「3. 注解配置」小节的 user-rpc-service-api-02
,复制出 user-rpc-service-api-03
,无需做任何改动。
将「3. 注解配置」小节的 user-rpc-service-provider-02
,复制出 user-rpc-service-provider-03
,接入 Nacos 作为注册中心。改动点如下图:
修改 pom.xml
文件,额外引入 Sentinel 相关的依赖如下:
com.alibaba.nacos
nacos-client
1.2.1
org.apache.dubbo
dubbo-registry-nacos
2.7.4.1
修改 application.yaml
配置文件,修改 dubbo.registry.address
配置项,设置 Nacos 作为注册中心。完整配置如下:
# dubbo 配置项,对应 DubboConfigurationProperties 配置类
dubbo:
# Dubbo 应用配置
application:
name: user-service-provider # 应用名
# Dubbo 注册中心配
registry:
address: nacos://127.0.0.1:8848 # 注册中心地址。个鞥多注册中心,可见 http://dubbo.apache.org/zh-cn/docs/user/references/registry/introduction.html 文档。
# Dubbo 服务提供者协议配置
protocol:
port: -1 # 协议端口。使用 -1 表示随机端口。
name: dubbo # 使用 `dubbo://` 协议。更多协议,可见 http://dubbo.apache.org/zh-cn/docs/user/references/protocol/introduction.html 文档
# Dubbo 服务提供者配置
provider:
timeout: 1000 # 【重要】远程服务调用超时时间,单位:毫秒。默认为 1000 毫秒,胖友可以根据自己业务修改
UserRpcService:
version: 1.0.0
# 配置扫描 Dubbo 自定义的 @Service 注解,暴露成 Dubbo 服务提供者
scan:
base-packages: cn.iocoder.springboot.lab30.rpc.service
友情提示:艿艿本机搭建的 Nacos 服务启动在默认的 8848 端口。
将「3. 注解配置」小节的 user-rpc-service-consumer-02
,复制出 user-rpc-service-consumer-03
,接入 Nacos 作为注册中心。改动点如下图:
友情提示:整合的过程,和「6.2 Provider」一模一样。
修改 pom.xml
文件,额外引入 Sentinel 相关的依赖如下:
com.alibaba.nacos
nacos-client
1.2.1
org.apache.dubbo
dubbo-registry-nacos
2.7.4.1
修改 application.yaml
配置文件,修改 dubbo.registry.address
配置项,设置 Nacos 作为注册中心。完整配置如下:
# dubbo 配置项,对应 DubboConfigurationProperties 配置类
dubbo:
# Dubbo 应用配置
application:
name: user-service-consumer # 应用名
# Dubbo 注册中心配置
registry:
address: nacos://127.0.0.1:8848 # 注册中心地址。个鞥多注册中心,可见 http://dubbo.apache.org/zh-cn/docs/user/references/registry/introduction.html 文档。
# Dubbo 消费者配置
consumer:
timeout: 1000 # 【重要】远程服务调用超时时间,单位:毫秒。默认为 1000 毫秒,胖友可以根据自己业务修改
UserRpcService:
version: 1.0.0
友情提示:艿艿本机搭建的 Nacos 服务启动在默认的 8848 端口。
① 使用 ProviderApplication 启动服务提供者。在 Nacos 控台中,我们可以看到以 providers
为开头的服务提供者,如下图所示:
② 使用 ConsumerApplication 启动服务消费者。在 Nacos 控台中,我们可以看到以 consumers
为开头的服务消费者,如下图所示:
更多关于 Dubbo 集成 Nacos 作为注册中心的内容,可以看看《Dubbo 文档 —— Nacos 注册中心》。
示例代码对应仓库:
lab-30-dubbo-annotations-sentinel
。
本小节我们来进行 Dubbo 和 Sentinel 的整合,使用 Sentinel 进行 Dubbo 的流量保护。Sentinel 提供了 sentinel-apache-dubbo-adapter
子项目,已经对 Dubbo 进行适配,所以我们只要引入它,基本就完成了 Dubbo 和 Sentinel 的整合,贼方便。
Sentinel 是阿里中间件团队开源的,面向分布式服务架构的轻量级流量控制产品,主要以流量为切入点,从流量控制、熔断降级、系统负载保护等多个维度来帮助用户保护服务的稳定性。
还是老样子,我们从「3. 注解配置」小节,复制出对应的三个 Maven 项目来进行改造,进行 Sentinel 的整合。最终项目如下图所示:
友情提示:本小节需要搭建一个 Sentinel 服务,可以参考《Sentinel 极简入门》文章。
将「3. 注解配置」小节的 user-rpc-service-api-02
,复制出 user-rpc-service-api-04
,无需做任何改动。
将「3. 注解配置」小节的 user-rpc-service-provider-02
,复制出 user-rpc-service-provider-03
,接入 Sentinel 实现服务消费者的流量控制。改动点如下图:
修改 pom.xml
文件,额外引入 Sentinel 相关的依赖如下:
com.alibaba.csp
sentinel-core
1.7.1
com.alibaba.csp
sentinel-transport-simple-http
1.7.1
com.alibaba.csp
sentinel-apache-dubbo-adapter
1.7.1
在 resources
目录下,创建 Sentinel 自定义的sentinel.properties
配置文件。内容如下:
csp.sentinel.dashboard.server=127.0.0.1:7070
csp.sentinel.dashboard.server
配置项,设置 Sentinel 控制台地址。
更多其它配置项,可见《Sentinel 官方文档 —— 启动配置项》。
将「2. 快速入门」小节的 user-rpc-service-consumer-02
,复制出 user-rpc-service-consumer-04
,接入 Sentinel 实现服务消费者的流量控制。改动点如下图:
友情提示:整合的过程,和「7.2 Provider」一模一样。
修改 pom.xml
文件,额外引入 Sentinel 相关的依赖如下:
com.alibaba.csp
sentinel-core
1.7.1
com.alibaba.csp
sentinel-transport-simple-http
1.7.1
com.alibaba.csp
sentinel-apache-dubbo-adapter
1.7.1
在 resources
目录下,创建 Sentinel 自定义的sentinel.properties
配置文件。内容如下:
csp.sentinel.dashboard.server=127.0.0.1:7070
创建 UserController 类,增加调用 UserRpcService 服务的 HTTP API 接口。代码如下:
@RestController
@RequestMapping("/user")
public class UserController {
@Reference(version = "${dubbo.consumer.UserRpcService.version}")
private UserRpcService userRpcService;
@GetMapping("/get")
public UserDTO get(@RequestParam("id") Integer id) {
return userRpcService.get(id);
}
}
友情提示:注意,需要额外引入
spring-boot-starter-web
依赖。因为它不是主角,所以并没有主动写出来哈~
① 使用 ProviderApplication 启动服务提供者。使用 ConsumerApplication 启动服务消费者。
② 访问服务消费者的 http://127.0.0.1:8080/user/get?id=1 接口,保证相关资源的初始化。
下面,我们来演示使用 Sentinel 对服务消费者的流量控制。
而 Sentinel 对服务提供者的流量控制是一样的,胖友可以自己去尝试。
③ 使用浏览器,访问下 http://127.0.0.1:7070/ 地址,进入 Sentinel 控制台。
然后,点击 Sentinel 控制台的「簇点链路」菜单,可以看到看到 Dubbo 服务消费者产生的 cn.iocoder.springboot.lab30.rpc.api.UserRpcService.UserRpcService:get(java.lang.Integer)
资源。如下图所示:
点击 n.iocoder.springboot.lab30.rpc.api.UserRpcService.UserRpcService: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 Boot 服务容错 Sentinel 入门》文章。
sentinel-apache-dubbo-adapter
支持配置全局的 fallback 函数,可以在 Dubbo 服务被 Sentinel 限流/降级/负载保护的时候,进行相应的 fallback 处理。
我们只需要实现自定义的 DubboFallback 接口,并通过 DubboFallbackRegistry 进行注册即可。
默认情况下,使用 DubboFallback 的 DefaultDubboFallback 实现类,它会将 BlockException 包装成 SentinelRpcException 异常后抛出。
另外,我们还可以配合 Dubbo 的 fallback 机制,来为降级的服务提供替代的实现。
现在,Dubbo 可以说从原本的 Java RPC 框架,演化成 Dubbo 生态体系,其周边也越来越丰富。所以,让我们一起来期望 《Dubbo 3.0 预览版详细解读,关注异步化和响应式编程》 。
???? 无意中,发现 Dubbo 官方已经整理了 Dubbo 的整个生态体系,具体可以看看 Build production-ready microservices 页面。咳咳咳,真特喵的齐全,完全学不动了。
另外,有一点需要提醒下,很多初学 Dubbo 的胖友,可能会犯跟艿艿一样的错误,直接把原本的 Service 层,直接接入 Dubbo 框架,提供 Dubbo Service RPC 调用。其实这是不对的!具体的代码结构和项目的示例,可以看看 onemall/demo 项目。
因为本文仅仅是在 Spring Boot 下使用 Dubbo RPC 框架的入门文章,这里在推荐一些不错的内容:
《Dubbo 官方文档》 :还有比官方文档更香的东西么?在国内的开源项目中,Dubbo 的文档质量,起码 TOP10 吧。
dubbo-samples 仓库:提供了大量的示例,美滋滋。
《设计 RPC 接口时,你有考虑过这些吗?》 :让你更优雅的设计 RPC 接口。
《你的项目应该如何正确分层?》 :可以借鉴的项目分层。
欢迎加入我的知识星球,一起探讨架构,交流源码。加入方式,长按下方二维码噢:
已在知识星球更新源码解析如下:
最近更新《芋道 SpringBoot 2.X 入门》系列,已经 20 余篇,覆盖了 MyBatis、Redis、MongoDB、ES、分库分表、读写分离、SpringMVC、Webflux、权限、WebSocket、Dubbo、RabbitMQ、RocketMQ、Kafka、性能测试等等内容。
提供近 3W 行代码的 SpringBoot 示例,以及超 4W 行代码的电商微服务项目。
获取方式:点“在看”,关注公众号并回复 666 领取,更多内容陆续奉上。
兄弟,艿一口,点个赞!????