tio-boot整合dubbo 2.6.0
Introduction

Dubbo 作为一款微服务框架,最重要的是向用户提供跨进程的 RPC 远程调用能力。如上图所示,Dubbo 的服务消费者(Consumer)通过一系列的工作将请求发送给服务提供者(Provider)。
为了实现这样一个目标,Dubbo 引入了注册中心(Registry)组件,通过注册中心,服务消费者可以感知到服务提供者的连接方式,从而将请求发送给正确的服务提供者。
在使用 Dubbo 进行服务调用时,传递的参数是接口参数,而不是序列化后的服务对象。
版本与模块说明
本章示例使用 com.alibaba 命名空间的 Dubbo 2.6.0。从 2.7 开始 Maven 坐标改为 org.apache.dubbo,包名和配置方式都有变化,两套 API 不能混用,本章内容只适用于 2.6.x。
示例源码位于 https://github.com/litongjava/java-ee-tio-boot-study 的 tio-boot-latest-study/tio-boot-dubbo-2-6-0-demo01 目录。依赖注入的写法见 tio-boot 整合 jfinal-aop 依赖注入示例。
工程分为三个模块:
tio-boot-dubbo-2-6-0-demo01-api:服务接口定义,provider 与 consumer 共用tio-boot-dubbo-2-6-0-demo01-provider:服务提供者tio-boot-dubbo-2-6-0-demo01-consumer:服务消费者
运行前需要先准备好 ZooKeeper,安装步骤见 安装 ZooKeeper。
创建工程
父工程的 pom.xml 使用 pom 打包,并通过 modules 聚合三个子模块:
<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 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>nexus.io</groupId>
<artifactId>tio-boot-dubbo-2-6-0-demo01</artifactId>
<version>1.0.0</version>
<packaging>pom</packaging>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<java.version>1.8</java.version>
<maven.compiler.source>${java.version}</maven.compiler.source>
<maven.compiler.target>${java.version}</maven.compiler.target>
<!-- 在这里声明项目使用的 tio-boot 与 jfinal-aop 版本 -->
<tio-boot.version></tio-boot.version>
<jfinal-aop.version></jfinal-aop.version>
</properties>
<modules>
<module>tio-boot-dubbo-2-6-0-demo01-api</module>
<module>tio-boot-dubbo-2-6-0-demo01-provider</module>
<module>tio-boot-dubbo-2-6-0-demo01-consumer</module>
</modules>
</project>上面的 tio-boot.version 与 jfinal-aop.version 留空只是示意,实际工程填入项目使用的版本号,子模块直接引用属性即可。
tio-boot-dubbo-2-6-0-demo01-api
这个模块只包含服务接口定义,由 provider 与 consumer 共同依赖。
package nexus.io.tio.dubbo.demo001.service;
public interface HelloService {
String sayHello(String name);
}package nexus.io.tio.dubbo.demo001.service;
public interface HiService {
String sayHi(String name);
}tio-boot-dubbo-2-6-0-demo01-provider
添加依赖
<dependency>
<groupId>nexus.io</groupId>
<artifactId>tio-boot-dubbo-2-6-0-demo01-api</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
</dependency>
<dependency>
<groupId>nexus.io</groupId>
<artifactId>tio-boot</artifactId>
<version>${tio-boot.version}</version>
</dependency>
<dependency>
<groupId>nexus.io</groupId>
<artifactId>jfinal-aop</artifactId>
<version>${jfinal-aop.version}</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.6.0</version>
<exclusions>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-beans</artifactId>
</exclusion>
<exclusion>
<groupId>org.springframework</groupId>
<artifactId>spring-web</artifactId>
</exclusion>
</exclusions>
</dependency>
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.9</version>
<exclusions>
<exclusion>
<artifactId>slf4j-log4j12</artifactId>
<groupId>org.slf4j</groupId>
</exclusion>
</exclusions>
</dependency>Dubbo 2.6.0 自带一组 Spring 依赖,本示例用 tio-boot 加 jfinal-aop 管理容器,因此把 spring-context、spring-beans、spring-web 排除掉,避免两套容器同时生效。zkclient 里排除 slf4j-log4j12,统一使用 logback 输出日志。
配置文件
src/main/resources/app.properties:
server.listening.enable=false
app.name=service-provider
ZOOKEEPER_URL=zookeeper://127.0.0.1:2181server.listening.enable=false 关闭 tio-boot 的 HTTP 监听。provider 只对外提供 RPC 服务,不需要占用 HTTP 端口,这样多个 provider 实例同时运行时也不会争抢 Web 端口。
ZOOKEEPER_URL 指向你本机的 ZooKeeper;如果部署在其他机器上,把地址换成实际 IP。
服务实现类
package nexus.io.tio.dubbo.demo001.service.impl;
import nexus.io.annotation.AService;
import nexus.io.tio.dubbo.demo001.service.HelloService;
@AService
public class HelloServiceImpl implements HelloService {
public String sayHello(String name) {
return "Hello " + name;
}
}package nexus.io.tio.dubbo.demo001.service.impl;
import com.alibaba.dubbo.config.annotation.Service;
import nexus.io.tio.dubbo.demo001.service.HiService;
@Service
public class HiServiceImpl implements HiService {
@Override
public String sayHi(String name) {
return "Hi " + name;
}
}HelloServiceImpl 用 @AService 交给 jfinal-aop 管理。HiServiceImpl 保留 Dubbo 的 @Service 注解作为语义标注,真正的导出动作由下面的 DubboProvider.add 显式完成。
Dubbo 配置
package nexus.io.tio.dubbo.demo001.config;
import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.ProviderConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import com.alibaba.dubbo.qos.common.Constants;
import nexus.io.annotation.AConfiguration;
import nexus.io.annotation.Initialization;
import nexus.io.constants.ServerConfigKeys;
import nexus.io.hook.HookCan;
import nexus.io.jfinal.aop.Aop;
import nexus.io.jfinal.dubbo.DubboProvider;
import nexus.io.tio.dubbo.demo001.service.HelloService;
import nexus.io.tio.dubbo.demo001.service.HiService;
import nexus.io.tio.dubbo.demo001.service.impl.HelloServiceImpl;
import nexus.io.tio.dubbo.demo001.service.impl.HiServiceImpl;
import nexus.io.tio.utils.environment.EnvUtils;
import nexus.io.tio.utils.thread.TioThreadUtils;
@AConfiguration
public class DubboProviderConfig {
@Initialization
public void config() {
// 防止qos端口冲突
System.setProperty("dubbo.application.logger", "slf4j");
// qos端口设置为随机端口
System.setProperty(Constants.QOS_PORT, "0");
// 配置dubbo.qos.accept.foreign.ip是否关闭远程连接
System.setProperty(Constants.ACCEPT_FOREIGN_IP, "false");
// 创建应用配置
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName(EnvUtils.get(ServerConfigKeys.APP_NAME));
// 创建注册中心配置
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress(EnvUtils.get(ServerConfigKeys.ZOOKEEPER_URL));
// 创建服务提供者配置
ProviderConfig providerConfig = new ProviderConfig();
// dubbo端口设置为随机可用端口
providerConfig.setPort(-1);
// init
DubboProvider.init(applicationConfig, registryConfig, providerConfig);
// add
DubboProvider.add(HelloService.class, Aop.get(HelloServiceImpl.class));
DubboProvider.add(HiService.class, Aop.get(HiServiceImpl.class));
// export
TioThreadUtils.submit(() -> {
DubboProvider.export();
});
// unexport
HookCan.me().addDestroyMethod(() -> {
DubboProvider.unexport();
});
}
}几个要点:
providerConfig.setPort(-1)让 Dubbo 从默认端口开始寻找可用端口,多个 provider 实例可以并行启动QOS_PORT设为0,由操作系统分配一个随机端口,避免多实例抢占同一个 QOS 端口DubboProvider.export()放在TioThreadUtils.submit中异步执行,导出过程不会阻塞启动HookCan.me().addDestroyMethod(...)注册销毁回调,应用关闭时取消导出
启动类
package nexus.io.tio.dubbo.demo001;
import nexus.io.annotation.AComponentScan;
import nexus.io.tio.boot.TioApplication;
@AComponentScan
public class ProviderApp {
public static void main(String[] args) {
long start = System.currentTimeMillis();
TioApplication.run(ProviderApp.class, args);
long end = System.currentTimeMillis();
System.out.println((end - start) + "ms");
}
}tio-boot-dubbo-2-6-0-demo01-consumer
添加依赖
consumer 依赖 api 模块、tio-boot、jfinal-aop、dubbo 和 zkclient,除 api 模块外与 provider 完全一致:
<dependency>
<groupId>nexus.io</groupId>
<artifactId>tio-boot-dubbo-2-6-0-demo01-api</artifactId>
<version>1.0.0</version>
</dependency>配置文件
src/main/resources/app.properties:
server.port=7000
app.name=service-consumer
ZOOKEEPER_URL=zookeeper://127.0.0.1:2181consumer 需要对外提供 HTTP 接口,所以保留 server.port,示例中监听 7000。
Dubbo 配置
package nexus.io.tio.web.hello.config;
import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.RegistryConfig;
import com.alibaba.dubbo.qos.common.Constants;
import nexus.io.annotation.AConfiguration;
import nexus.io.annotation.Initialization;
import nexus.io.constants.ServerConfigKeys;
import nexus.io.hook.HookCan;
import nexus.io.jfinal.dubbo.Dubbo;
import nexus.io.tio.dubbo.demo001.service.HelloService;
import nexus.io.tio.dubbo.demo001.service.HiService;
import nexus.io.tio.utils.environment.EnvUtils;
@AConfiguration
public class DubboConsumerConfig {
@Initialization
public void config() {
int dubboTimeout = EnvUtils.getInt("dubbo.timeout", 3000); // 设置默认超时时间为3000毫秒
// 配置日志系统
System.setProperty("dubbo.application.logger", "slf4j");
// 关闭qos服务,端口设置为-1
System.setProperty(Constants.QOS_PORT, "-1");
// 配置dubbo.qos.accept.foreign.ip是否关闭远程连接
System.setProperty(Constants.ACCEPT_FOREIGN_IP, "false");
// 创建应用配置
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName(EnvUtils.get(ServerConfigKeys.APP_NAME));
// 创建注册中心配置
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress(EnvUtils.get(ServerConfigKeys.ZOOKEEPER_URL));
// 设置dubbo
Dubbo.initialize(applicationConfig, registryConfig, dubboTimeout);
// 获取服务器类
Dubbo.get(HelloService.class);
Dubbo.get(HiService.class);
HookCan.me().addDestroyMethod(() -> {
Dubbo.clear();
});
}
}Dubbo.get(...) 会从注册中心订阅服务,并把生成的远程代理放入 jfinal-aop 容器,后面在控制器里用 Aop.get(...) 取出来即可。
控制器类
package nexus.io.tio.web.hello.controller;
import nexus.io.annotation.RequestPath;
import nexus.io.jfinal.aop.Aop;
import nexus.io.tio.dubbo.demo001.service.HelloService;
import nexus.io.tio.dubbo.demo001.service.HiService;
@RequestPath("/")
public class IndexController {
@RequestPath()
public String index() {
HelloService helloService = Aop.get(HelloService.class);
HiService hiService = Aop.get(HiService.class);
return helloService.sayHello("Tong Li") + "_" + hiService.sayHi("Tong Li");
}
}启动类
package nexus.io.tio.web.hello;
import nexus.io.annotation.AComponentScan;
import nexus.io.tio.boot.TioApplication;
@AComponentScan
public class ConsumerApp {
public static void main(String[] args) {
long start = System.currentTimeMillis();
TioApplication.run(ConsumerApp.class, args);
long end = System.currentTimeMillis();
System.out.println((end - start) + "ms");
}
}启动流程
启动服务提供者
ProviderApp启动时,TioApplication.run会扫描带@AComponentScan的包,加载@AService实现类和@AConfiguration配置类DubboProviderConfig.config()创建应用配置、注册中心配置和 provider 配置,再由DubboProvider.export()把服务导出并注册到 ZooKeeper- 因为配置了
server.listening.enable=false,provider 不会启动 HTTP 服务,进程只监听 Dubbo 端口
启动服务消费者
ConsumerApp启动时,TioApplication.run启动内嵌 HTTP 服务并加载DubboConsumerConfigDubbo.get(HelloService.class)与Dubbo.get(HiService.class)从 ZooKeeper 拿到 provider 地址并生成远程代理
访问接口
bashcurl http://127.0.0.1:7000/返回:
Hello Tong Li_Hi Tong Li
服务调用流程
- 服务消费者通过
Aop.get(HelloService.class)取出HelloService的远程代理 - 调用
HelloService.sayHello("Tong Li")时,Dubbo 通过注册中心获取服务提供者的地址,把请求发送到对应的 provider - 服务提供者收到请求后,调用
HelloServiceImpl.sayHello("Tong Li"),返回Hello Tong Li - 消费者收到响应,把结果返回给调用方
Dubbo.get(...) 只接收接口类型,消费者靠接口全限定名在注册中心订阅对应的提供者;多实例、分组隔离与找不到提供者时的表现,见 服务查找规则。
端口与 QOS 说明
provider 和 consumer 的端口行为差异较大,部署多实例时需要区分:
- tio-boot HTTP 端口:provider 通过
server.listening.enable=false关闭;consumer 使用server.port=7000 - Dubbo 端口:provider 设置
setPort(-1),Dubbo 会从 20880 开始寻找第一个空闲端口。2.6.0 是顺序分配而不是随机数,所以两个实例并存时依次得到 20880、20881 - QOS 端口:provider 设置
QOS_PORT=0,由操作系统分配随机端口;consumer 设置QOS_PORT=-1,绑定失败,相当于关闭
关于 QOS 需要说明一点:Dubbo 2.6.0 没有 dubbo.qos.enable 开关,可配置的只有 dubbo.qos.port 和 dubbo.qos.accept.foreign.ip。QOS 服务由 ProtocolListenerWrapper 在协议初始化时通过反射启动,失败时静默忽略,不影响 RPC 调用。示例里关闭或改成随机端口,都是为了避免多实例部署时端口冲突。
JDK 版本注意事项
示例的编译目标是 Java 8,直接使用 JDK 8 运行没有任何额外要求。
如果使用 JDK 17 及以上,Dubbo 2.6.0 内部的 javassist 在动态生成自适应扩展类时,需要反射访问 ClassLoader.defineClass,会触发模块系统的强封装限制:
java.lang.reflect.InaccessibleObjectException:
module java.base does not "opens java.lang" to unnamed module这时启动参数需要加上:
--add-opens=java.base/java.lang=ALL-UNNAMED
--add-opens=java.base/java.math=ALL-UNNAMED示例工程的 pom.xml 已经把这两个参数写进 spring-boot-maven-plugin 的 jvmArguments,用 mvn spring-boot:run 启动时会自动带上:
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<version>2.7.4</version>
<configuration>
<fork>true</fork>
<mainClass>${main.class}</mainClass>
<jvmArguments>--add-opens=java.base/java.lang=ALL-UNNAMED --add-opens=java.base/java.math=ALL-UNNAMED</jvmArguments>
<excludeGroupIds>org.projectlombok</excludeGroupIds>
</configuration>
</plugin>java.math 这一项来自 Hessian 序列化器初始化时的同类反射访问,加上之后日志里不会再出现相关堆栈。
详细说明
接口参数的传递:客户端调用 Dubbo 服务时,会把方法名和方法参数序列化(例如 Hessian),通过网络发送到服务端。服务端反序列化后调用对应实现,再把结果返回。
序列化与反序列化
- 客户端把方法名、参数和元数据序列化成二进制格式发送
- 服务端反序列化后,根据接口名和方法名找到实现类,完成调用
服务对象的传递
服务对象本身不会随调用传递。服务提供者启动时把自身信息注册到 ZooKeeper,消费者通过注册中心查找可用地址,再发起调用。
结论
在 Dubbo 中调用远程服务时,传输的是接口方法的参数,而不是服务对象本身。参数在传输过程中被序列化,服务端反序列化后调用实现方法。
