Skip to content

tio-boot整合dubbo 2.6.0 ​

Introduction ​

dubbo

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 聚合三个子模块:

xml
<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 共同依赖。

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-0-demo01-provider ​

添加依赖 ​

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

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 服务,不需要占用 HTTP 端口,这样多个 provider 实例同时运行时也不会争抢 Web 端口。

ZOOKEEPER_URL 指向你本机的 ZooKeeper;如果部署在其他机器上,把地址换成实际 IP。

服务实现类 ​

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;
  }
}

HelloServiceImpl 用 @AService 交给 jfinal-aop 管理。HiServiceImpl 保留 Dubbo 的 @Service 注解作为语义标注,真正的导出动作由下面的 DubboProvider.add 显式完成。

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 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(...) 注册销毁回调,应用关闭时取消导出

启动类 ​

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-0-demo01-consumer ​

添加依赖 ​

consumer 依赖 api 模块、tio-boot、jfinal-aop、dubbo 和 zkclient,除 api 模块外与 provider 完全一致:

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

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

consumer 需要对外提供 HTTP 接口,所以保留 server.port,示例中监听 7000。

Dubbo 配置 ​

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

控制器类 ​

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

启动类 ​

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. 启动服务提供者

    • ProviderApp 启动时,TioApplication.run 会扫描带 @AComponentScan 的包,加载 @AService 实现类和 @AConfiguration 配置类
    • DubboProviderConfig.config() 创建应用配置、注册中心配置和 provider 配置,再由 DubboProvider.export() 把服务导出并注册到 ZooKeeper
    • 因为配置了 server.listening.enable=false,provider 不会启动 HTTP 服务,进程只监听 Dubbo 端口
  2. 启动服务消费者

    • ConsumerApp 启动时,TioApplication.run 启动内嵌 HTTP 服务并加载 DubboConsumerConfig
    • Dubbo.get(HelloService.class) 与 Dubbo.get(HiService.class) 从 ZooKeeper 拿到 provider 地址并生成远程代理
  3. 访问接口

    bash
    curl http://127.0.0.1:7000/

    返回:

    Hello Tong Li_Hi Tong Li

服务调用流程 ​

  1. 服务消费者通过 Aop.get(HelloService.class) 取出 HelloService 的远程代理
  2. 调用 HelloService.sayHello("Tong Li") 时,Dubbo 通过注册中心获取服务提供者的地址,把请求发送到对应的 provider
  3. 服务提供者收到请求后,调用 HelloServiceImpl.sayHello("Tong Li"),返回 Hello Tong Li
  4. 消费者收到响应,把结果返回给调用方

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 启动时会自动带上:

xml
<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 序列化器初始化时的同类反射访问,加上之后日志里不会再出现相关堆栈。

详细说明 ​

  1. 接口参数的传递:客户端调用 Dubbo 服务时,会把方法名和方法参数序列化(例如 Hessian),通过网络发送到服务端。服务端反序列化后调用对应实现,再把结果返回。

  2. 序列化与反序列化

    • 客户端把方法名、参数和元数据序列化成二进制格式发送
    • 服务端反序列化后,根据接口名和方法名找到实现类,完成调用
  3. 服务对象的传递

    服务对象本身不会随调用传递。服务提供者启动时把自身信息注册到 ZooKeeper,消费者通过注册中心查找可用地址,再发起调用。

  4. 结论

    在 Dubbo 中调用远程服务时,传输的是接口方法的参数,而不是服务对象本身。参数在传输过程中被序列化,服务端反序列化后调用实现方法。

MIT Licensed