Skip to content

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 三个属性:

java
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 显式指定:

java
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

父工程统一声明版本属性:

xml
<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 共用:

java
package nexus.io.tio.dubbo.demo001.service;

public interface HelloService {
  String sayHello(String name);
}
java
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,其余保持一致。

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

properties
server.listening.enable=false
app.name=service-provider
ZOOKEEPER_URL=zookeeper://127.0.0.1:2181

server.listening.enable=false 关闭 tio-boot 的 HTTP 监听。provider 只提供 RPC 服务,不占 Web 端口,多个实例同时运行也不会冲突。

服务实现类 ​

java
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;
  }
}
java
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 配置 ​

java
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();
    });
  }
}

启动类 ​

java
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 模块:

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

properties
server.port=7000
app.name=service-consumer
ZOOKEEPER_URL=zookeeper://127.0.0.1:2181

Dubbo 配置 ​

java
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();
    });
  }
}

控制器类 ​

java
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(...) 取出来即可。

启动类 ​

java
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");
  }
}

启动流程 ​

  1. 启动服务提供者

    在 tio-boot-dubbo-2-6-6-demo01-provider 目录执行:

    bash
    mvn spring-boot:run
    • TioApplication.run 扫描 @AService 与 @AConfiguration
    • DubboProviderConfig 创建应用、注册中心、provider 三项配置
    • DubboProvider.export() 在异步线程里导出服务并注册到 ZooKeeper
    • server.listening.enable=false 使 provider 不监听 HTTP 端口
  2. 启动服务消费者

    在 tio-boot-dubbo-2-6-6-demo01-consumer 目录执行同样命令,启动后:

    • TioApplication.run 启动内嵌 HTTP 服务,默认端口 7000
    • Dubbo.get(HelloService.class) 从 ZooKeeper 订阅服务并生成远程代理
  3. 访问接口

    bash
    curl http://127.0.0.1:7000/

    返回:

    Hello Tong Li_Hi Tong Li

服务查找规则 ​

Dubbo.get(HiService.class) 只接收一个接口类型,没有服务名。消费者能定位到正确的提供者,靠的是接口全限定名。

查找键是接口名 ​

缓存未命中时,Dubbo.get 会创建一个 ReferenceConfig,只设置三项内容:

java
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/providers

provider 导出时会在这条路径下写入自己的地址(实例节点是临时的,进程退出即删除):

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 做逻辑隔离:

java
// 提供者
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(...) 拿到引用再设置分组已经来不及——引用在那之前就已经建立好了。

需要使用分组隔离时,给工具类增加一个重载即可:

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

java
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 两行——两者都在,才说明导出真正完成。

MIT Licensed