tio-boot整合dubbo 2.6.6
本章把示例工程升级到 Dubbo 2.6.6,代码结构与 dubbo 2.6.0 一致,但 2.6.6 有三处默认行为变化,必须同步调整依赖和配置,否则会出现「进程起来了但服务没导出」或「启动直接失败」。
示例源码位于 https://github.com/litongjava/java-ee-tio-boot-study 的 tio-boot-latest-study/tio-boot-dubbo-2-6-6-demo01 目录。
运行前先准备好 ZooKeeper,安装步骤见 安装 ZooKeeper;依赖注入的写法见 tio-boot 整合 jfinal-aop 依赖注入示例。
与 2.6.0 的差异
1. QOS 改用 qos.enable 开关
2.6.6 不再提供 com.alibaba.dubbo.qos.common.Constants,ApplicationConfig 新增了 qosEnable、qosPort、qosAcceptForeignIp 三个属性:
applicationConfig.setQosEnable(false);这比 2.6.0 只能把端口指到 -1 让绑定失败要清晰得多。关闭后日志会出现一行确认:
[DUBBO] qos won't be started because it is disabled. Please check dubbo.application.qos.enable is configured either in system property, dubbo.properties or XML/spring boot configuration.2. 默认传输层从 netty3 换成 netty4
Transporter 的 SPI 注册里,netty 这个别名现在指向 netty4:
netty3=com.alibaba.dubbo.remoting.transport.netty.NettyTransporter
netty4=com.alibaba.dubbo.remoting.transport.netty4.NettyTransporter
netty=com.alibaba.dubbo.remoting.transport.netty4.NettyTransporter项目原先只引入 netty3,导出时会报 NoClassDefFoundError: io.netty.channel.EventLoopGroup。因为 DubboProvider.export() 跑在异步线程里,这个异常不会打印,表现为「provider 正常启动、端口也绑上了,但只有部分服务导出成功」。所以必须显式引入 netty4。
3. 注册中心客户端默认从 zkclient 换成 curator
ZookeeperTransporter 的 SPI 里有 zkclient 和 curator 两个实现,2.6.6 默认使用 curator。如果没有引入 curator,消费者订阅服务时会报 NoClassDefFoundError: org/apache/curator/framework/api/CuratorWatcher。
示例采用 zkclient,通过 RegistryConfig 显式指定:
registryConfig.setClient("zkclient");如果更倾向 curator,把这一行删掉并引入 org.apache.curator:curator-framework 与 curator-recipes 即可,两种方式效果一致。
创建工程
三个模块与 2.6.0 示例相同,只是坐标换成 2-6-6:
tio-boot-dubbo-2-6-6-demo01-api
tio-boot-dubbo-2-6-6-demo01-provider
tio-boot-dubbo-2-6-6-demo01-consumer父工程统一声明版本属性:
<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-6-demo01-api</module>
<module>tio-boot-dubbo-2-6-6-demo01-provider</module>
<module>tio-boot-dubbo-2-6-6-demo01-consumer</module>
</modules>tio-boot-dubbo-2-6-6-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-6-demo01-provider
添加依赖
与 2.6.0 版本相比,改动只有三处:dubbo 版本改为 2.6.6,新增 netty-all,其余保持一致。
<dependency>
<groupId>nexus.io</groupId>
<artifactId>tio-boot-dubbo-2-6-6-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.6</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>
<!-- 2.6.6 默认使用 netty4 作为传输层 -->
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.42.Final</version>
</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>配置文件
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 服务,不占 Web 端口,多个实例同时运行也不会冲突。
服务实现类
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;
}
}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 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() {
// 使用 slf4j 输出日志
System.setProperty("dubbo.application.logger", "slf4j");
// 创建应用配置
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName(EnvUtils.get(ServerConfigKeys.APP_NAME));
// 2.6.6 起可以通过 qos.enable 关闭 QOS 服务
applicationConfig.setQosEnable(false);
// 创建注册中心配置
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress(EnvUtils.get(ServerConfigKeys.ZOOKEEPER_URL));
// 2.6.6 默认使用 curator 客户端,这里显式使用 zkclient
registryConfig.setClient("zkclient");
// 创建服务提供者配置
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();
});
}
}启动类
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-6-demo01-consumer
添加依赖
与 provider 相同的依赖集合,外加 api 模块:
<dependency>
<groupId>nexus.io</groupId>
<artifactId>tio-boot-dubbo-2-6-6-demo01-api</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>dubbo</artifactId>
<version>2.6.6</version>
</dependency>
<dependency>
<groupId>io.netty</groupId>
<artifactId>netty-all</artifactId>
<version>4.1.42.Final</version>
</dependency>
<dependency>
<groupId>com.101tec</groupId>
<artifactId>zkclient</artifactId>
<version>0.9</version>
</dependency>netty-all 对消费者同样必要:RPC 客户端走的是 NettyClient,缺了它远程调用无法建立连接。
配置文件
src/main/resources/app.properties:
server.port=7000
app.name=service-consumer
ZOOKEEPER_URL=zookeeper://127.0.0.1:2181Dubbo 配置
package nexus.io.tio.web.hello.config;
import com.alibaba.dubbo.config.ApplicationConfig;
import com.alibaba.dubbo.config.RegistryConfig;
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");
// 创建应用配置
ApplicationConfig applicationConfig = new ApplicationConfig();
applicationConfig.setName(EnvUtils.get(ServerConfigKeys.APP_NAME));
// 2.6.6 起可以通过 qos.enable 关闭 QOS 服务
applicationConfig.setQosEnable(false);
// 创建注册中心配置
RegistryConfig registryConfig = new RegistryConfig();
registryConfig.setAddress(EnvUtils.get(ServerConfigKeys.ZOOKEEPER_URL));
// 2.6.6 默认使用 curator 客户端,这里显式使用 zkclient
registryConfig.setClient("zkclient");
// 设置dubbo
Dubbo.initialize(applicationConfig, registryConfig, dubboTimeout);
// 获取服务器类
Dubbo.get(HelloService.class);
Dubbo.get(HiService.class);
HookCan.me().addDestroyMethod(() -> {
Dubbo.clear();
});
}
}控制器类
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");
}
}远程代理在配置阶段由 Dubbo.get(...) 注册进 jfinal-aop 容器,控制器里直接用 Aop.get(...) 取出来即可。
启动类
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");
}
}启动流程
启动服务提供者
在
tio-boot-dubbo-2-6-6-demo01-provider目录执行:bashmvn spring-boot:runTioApplication.run扫描@AService与@AConfigurationDubboProviderConfig创建应用、注册中心、provider 三项配置DubboProvider.export()在异步线程里导出服务并注册到 ZooKeeperserver.listening.enable=false使 provider 不监听 HTTP 端口
启动服务消费者
在
tio-boot-dubbo-2-6-6-demo01-consumer目录执行同样命令,启动后:TioApplication.run启动内嵌 HTTP 服务,默认端口 7000Dubbo.get(HelloService.class)从 ZooKeeper 订阅服务并生成远程代理
访问接口
bashcurl http://127.0.0.1:7000/返回:
Hello Tong Li_Hi Tong Li
服务查找规则
Dubbo.get(HiService.class) 只接收一个接口类型,没有服务名。消费者能定位到正确的提供者,靠的是接口全限定名。
查找键是接口名
缓存未命中时,Dubbo.get 会创建一个 ReferenceConfig,只设置三项内容:
ReferenceConfig<T> reference = new ReferenceConfig<>();
reference.setInterface(targetClass); // 接口全限定名
reference.setApplication(applicationConfig);
reference.setRegistry(registryConfig);
reference.setTimeout(defaultTimeout);setInterface(Class) 取的是 targetClass.getName(),也就是 nexus.io.tio.dubbo.demo001.service.HiService。reference.get() 随后订阅 ZooKeeper 上对应的节点:
/dubbo/nexus.io.tio.dubbo.demo001.service.HiService/providersprovider 导出时会在这条路径下写入自己的地址(实例节点是临时的,进程退出即删除):
dubbo://192.168.31.151:20880/nexus.io.tio.dubbo.demo001.service.HiService
?anyhost=true&application=service-provider&dubbo=2.0.2&generic=false
&interface=nexus.io.tio.dubbo.demo001.service.HiService&logger=slf4j
&methods=sayHi&pid=11252&revision=2-6-6-demo01-api-1.0.0&side=provider注意节点数据里的 application=service-provider 只是元信息,不参与查找。两个微服务的应用名完全不同,只要导出同一个接口,就会落在同一个节点下、被同一批消费者看见。
同一个接口有多个实例
这是水平扩容的场景:同一份服务部署多个副本,接口名相同。Dubbo 把它们视为同一组提供者,消费者订阅到的是一组 invoker,由集群策略和负载均衡算法决定这一次调用发给谁。默认是失败切换(failover)加随机选择,调用失败会自动换下一个实例。
这种场景不需要额外配置。多个 provider 实例同时启动时,Dubbo 的端口分配是顺序寻找第一个空闲端口,所以实例会分别落在 20880、20881,互不冲突。
不同业务共用一个接口
如果两个模块各自实现了一套同名的 HiService(哪怕实现逻辑完全不同),它们的接口全限定名相同,注册中心无法区分,消费者会随机调到任意一个实现上。
这时需要用 group 或 version 做逻辑隔离:
// 提供者
serviceConfig.setGroup("order");
serviceConfig.setVersion("1.0.0");
// 消费者
reference.setGroup("order");
reference.setVersion("1.0.0");ZooKeeper 路径不变,但节点数据里会带上 group=order&version=1.0.0,消费者只订阅匹配的那一条。
当前工具类的限制
Dubbo.get(Class) 只接受接口类型,没有暴露 group 和 version,并且创建 ReferenceConfig 后立刻调用 get() 完成初始化。所以通过 Dubbo.getReference(...) 拿到引用再设置分组已经来不及——引用在那之前就已经建立好了。
需要使用分组隔离时,给工具类增加一个重载即可:
public static <T> T get(Class<T> targetClass, String group, String version) {
ReferenceConfig<T> reference = new ReferenceConfig<>();
reference.setInterface(targetClass);
reference.setApplication(applicationConfig);
reference.setRegistry(registryConfig);
reference.setTimeout(defaultTimeout);
reference.setGroup(group);
reference.setVersion(version);
return reference.get();
}同时 dubboCache 的键要从 Class 换成「接口 + 分组 + 版本」,否则同一个接口的不同分组会互相覆盖缓存。
找不到提供者时
Dubbo.get 捕获了 reference.get() 抛出的异常,只打印堆栈然后返回 null:
try {
result = reference.get();
} catch (Exception e) {
e.printStackTrace();
}
if (result != null) {
AopManager.me().addSingletonObject(targetClass, result);
dubboCache.put(targetClass, result);
}默认开启启动检查时,没有可用提供者会抛 IllegalStateException: No provider available for the service ...。此时单例不会注册进 jfinal-aop,控制器里 Aop.get(HiService.class) 得到 null,问题会在真正调用时才以空指针暴露。
排查顺序:先确认 provider 已经启动并完成导出,再看 ZooKeeper 上 providers 节点是否有对应的临时节点,最后检查两边接口的全限定名是否完全一致。包名不同(例如改名前后的 com.litongjava.* 与 nexus.io.*)会形成两个互不相干的节点,这是最常见的原因。
端口说明
- tio-boot HTTP 端口:provider 关闭;consumer 使用
server.port=7000 - Dubbo 端口:provider 设置
setPort(-1),Dubbo 从默认端口开始寻找第一个空闲端口。这是顺序分配而不是随机数,两个实例并存时依次得到 20880、20881 - QOS 端口:
qosEnable=false后不再监听,也不占用任何端口
JDK 版本注意事项
示例编译目标是 Java 8,使用 JDK 8 运行无需额外参数。
JDK 17 及以上运行时,Dubbo 内部的 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示例工程已把这两个参数写进 spring-boot-maven-plugin 的 jvmArguments,用 mvn spring-boot:run 启动时会自动带上。java.math 对应 Hessian 序列化器初始化时的同类反射访问。
小结
从 2.6.0 升级到 2.6.6,业务代码不需要改动,需要留意的是三项默认值变化:
qos.enable提供了官方的 QOS 关闭开关,替代了原先「把端口指到 -1」的做法- 传输层默认 netty4,必须补上
netty-all,否则服务导出会静默失败 - 注册中心客户端默认 curator,沿用 zkclient 时需要显式声明
setClient("zkclient")
排查时可以先看 mvn clean install 是否通过,再确认启动日志里是否同时出现 Export dubbo service ... to url 和 Start NettyServer bind /0.0.0.0:20880 两行——两者都在,才说明导出真正完成。
