文章目录
  1. 1. 需求描述
  2. 2. 原理讲解
    1. 2.1. Filter机制简述
    2. 2.2. SPI机制简述
  3. 3. 编码实现
    1. 3.1. 依赖引入
    2. 3.2. 编写TraceId生成类–TraceIdGenerator
    3. 3.3. 编写线程上下文TraceIdUtil
    4. 3.4. 编写自定义的TraceIdFilter实现com.alibaba.dubbo.rpc.Filter接口
      1. 3.4.1. ConsumerSide消费者侧核心逻辑
      2. 3.4.2. ProviderSide提供者侧核心逻辑
    5. 3.5. 编写配置META-INF/dubbo/com.alibaba.dubbo.rpc.Filter
  4. 4. 打包上传私服
  5. 5. 小结
  6. 6. 参考链接
  7. 7. 补充知识点:Maven中-DskipTests和-Dmaven.test.skip=true的区别

在近期的两篇文章中,我带领大家使用springboot2.x整合了dubbo,完成了一个基础的服务化开发框架的搭建。文章链接如下

[dubbo]springboot2.x整合dubbo之使用dubbo-spring-boot-starter

[dubbo]springboot2.x整合dubbo之基于xml整合dubbo2.6.5

但是在生产环境下还是存在一定的问题,比如我们关心的调用链追踪的问题。

本文中,我将继续带领大家基于dubbo2.6.5开发一个轻量级的TraceId获取及传递工具包,实现在dubbo服务的提供方及消费方中 设置/获取/传递 TraceId的需求。

代码地址

需求描述

通过dubbo的Filter及扩展点机制,开发TraceId获取及传递的工具包,实现在服务业务的上下游中对TraceId的 设置/获取/传递 的需求。

原理讲解

在编写代码之前,先对Filter机制和扩展点机制做一个简要的讲解,以便加深我们的理解,当然如果只是简单的整合使用该工具包,可以直接跳过这段,进入编码环节。

Filter机制简述

Dubbo中的Filter与我们在javaweb中的Filter的理解是类似的,都是在请求处理前后做一些通用的逻辑,Filter可以有多个支持层层嵌套。

Dubbo官方针对Filter做了很多的原生支持,目前大致有20多个,包括我们熟知的RpcContext,accesslog等功能都是通过filter来实现的。

开发一个自定的Filter很简单,只需要编写我们的Filter类,实现com.alibaba.dubbo.rpc.Filter 接口,重写其invoke(Invoker<?> invoker, Invocation invocation) 方法即可。

具体的过程我将在下文展开讲解。

SPI机制简述

这里直接引用官网的描述:

SPI 全称为 Service Provider Interface,是一种服务发现机制。SPI 的本质是将接口实现类的全限定名配置在文件中,并由服务加载器读取配置文件,加载实现类。这样可以在运行时,动态为接口替换实现类。正因此特性,我们可以很容易的通过 SPI 机制为我们的程序提供拓展功能。SPI 机制在第三方框架中也有所应用,比如 Dubbo 就是通过 SPI 机制加载所有的组件。不过,Dubbo 并未使用 Java 原生的 SPI 机制,而是对其进行了增强,使其能够更好的满足需求。

Filter机制便是Dubbo中SPI的一个应用场景,Dubbo对java的原生SPI做了扩展,使Dubbo具备了更丰富的扩展性。

Dubbo官方对Dubbo的定位是微内核架构,不想依赖三方框架如Spring、Guice等,这使得Dubbo的实现更加的优雅。

通常微核心都会采用 Factory、IoC、OSGi 等方式管理插件生命周期。考虑 Dubbo 的适用面,不想强依赖 Spring 等 IoC 容器。自已造一个小的 IoC 容器,也觉得有点过度设计,所以打算采用最简单的 Factory 方式管理插件。–引用自《扩展点重构》

具体的细节可以参考文章最后的参考链接。

接下来就进入到我们的正式的设计编码环节。

编码实现

首先建立一个maven工程,我将其命名为shield-dubbo-tracer。

依赖引入

在项目的pom.xml中引入必要的依赖,如下

<properties>
    <maven.compiler.source>1.8</maven.compiler.source>
    <maven.compiler.target>1.8</maven.compiler.target>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
</properties>

<dependencies>
    <!-- https://mvnrepository.com/artifact/com.alibaba/dubbo -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>dubbo</artifactId>
        <version>2.6.5</version>
        <scope>provided </scope>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.11</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.22</version>
    </dependency>
</dependencies>

由于我们的工具包是基于dubbo运行的,因此要添加dubbo依赖,这里使用provided的作用域的原因是:我们不确定工具包的调用方具体使用哪个版本的dubbo,因此在打包的时候不需要将dubbo依赖都打到最终的jar中,保证我们工程的纯洁性和轻量性,同时也能很好的避免调用方出现依赖冲突的可能。

编写TraceId生成类–TraceIdGenerator

首先定义一个名为TraceIdGenerator的类,该类主要用于生成TraceId,且类的作用域是public,可供外部直接调用,核心代码如下

public class TraceIdGenerator {

    /**
    * 消费端创建TraceId,并设置到线程上下文中
    * 该方法只调用一次
    * @return
    */
    public static String createTraceId() {
        // 创建的同时就设置到上下文中
        String traceId = getTraceid();
        TraceIdUtil.setTraceId(traceId);
        return traceId;
    }

    /**
    * 生成32位traceId
    * @return
    */
    private static String getTraceid() {
        String result = "";
        String ip = "";

        // 获取本地ipv4地址
        try {
            InetAddress address = InetAddress.getLocalHost();
            ip = address.getHostAddress();
        } catch (Exception var5) {
            return result;
        }

        // 根据.截取为String数组
        String[] ipAddressInArray = ip.split("\\.");
        // 拼装为字符串,将每一个元素转换为16进制
        for(int i = 3; i >= 0; --i) {
            Integer id = Integer.parseInt(ipAddressInArray[3 - i]);
            result = result + String.format("%02x", id);
        }
        // 拼装时间戳及随机数
        SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmssSSS");
        result = result + simpleDateFormat.format(new Date()) + UuidUtil.getUuid().substring(0, 7);
        return result;
    }

}

注释写的应当是比较详细的,这里总结一下,createTraceId()方法是提供给外部完成TraceId生成功能的。它内部调用私有静态方法getTraceid()生成32的一串TraceId的同时将该TraceId设置到本地线程的上下文TraceIdUtil中,并将该TraceId返回供后续业务使用。

编写线程上下文TraceIdUtil

上面提到,生成TraceId的同时设置到了TraceIdUtil中,TraceIdUtil可以认为是当前线程/当前线程子线程的TraceId的容器,具体代码如下

......
/**使用InheritableThreadLocal便于在主子线程间传递参数*/
private static final ThreadLocal<String> TRACE_ID = new InheritableThreadLocal<>();

public TraceIdUtil() {
}

/**
 * 从当前线程局部变量获取TraceId
 * 首次调用该方法会生成traceId,后续每次都从线程上下文获取
 * @return
 */
public static String getTraceId() {
    return TRACE_ID.get();
}

public static void setTraceId(String traceId) {
    TRACE_ID.set(traceId);
}

public static void removeTraceId() {
    TRACE_ID.remove();
}
......

有心的你可能发现了,我在这里使用了InheritableThreadLocal类作为TraceId保存的容器,之所以使用InheritableThreadLocal是要达到在当前线程的子线程中也能获取到TraceId的值,如果使用它的父类ThreadLocal则只能在当前线程获取到TraceId。关于InheritableThreadLocal的详细的介绍请自行参看文章末尾的参考链接中的《InheritableThreadLocal详解》一文,这里不再赘述。

通过TraceIdUtil,我们能够实现

  1. 将TraceId设置到当前线程的线程上下文中
  2. 从当前线程上下文中获取TraceId
  3. 从当前线程上下文中移除TraceId等的功能。

编写自定义的TraceIdFilter实现com.alibaba.dubbo.rpc.Filter接口

完成前面的准备工作,我们即将进行工具包开发的重头戏,实现自定义的TraceId传递Filter。

编写类TraceIdFilter,实现接口com.alibaba.dubbo.rpc.Filter,重写其invoke方法。

核心代码如下:

@Activate(group = {Constants.PROVIDER, Constants.CONSUMER})
public class TraceIdFilter implements Filter {

    ......
    @Override
    public Result invoke(Invoker<?> invoker, Invocation invocation) throws RpcException {
        RpcContext rpcContext = RpcContext.getContext();
        String traceId = "";
        if (rpcContext.isConsumerSide()) {
            if (StringUtils.isBlank(TraceIdUtil.getTraceId())) {
                // 根调用,生成TraceId
                traceId = TraceIdGenerator.createTraceId();
            } else {
                // 后续调用,从Rpc上下文取出并设置到线程上下文
                traceId = TraceIdUtil.getTraceId();
            }
            TraceIdUtil.setTraceId(traceId);
            RpcContext.getContext().setAttachment(TraceIdConst.TRACE_ID, TraceIdUtil.getTraceId());
        }
        if (rpcContext.isProviderSide()) {
            // 服务提供方,从Rpc上下文获取traceId
            traceId = RpcContext.getContext().getAttachment(TraceIdConst.TRACE_ID);
            TraceIdUtil.setTraceId(traceId);
        }
        Result result = invoker.invoke(invocation);
        return result;
    }
    ......
}

这里简单先接单介绍下 @Activate 注解,该注解表示一个扩展是否被激活(即是否能够使用), 它可以放在类定义、方法之上。dubbo用它在spi扩展类定义上,表示这个扩展实现激活条件和时机。

这里我将TraceIdFilter标注为

@Activate(group = {Constants.PROVIDER, Constants.CONSUMER})

标识该拓展会在服务提供方和服务消费方同时生效。

接下来,我们详细的梳理一下重写的invoke方法的逻辑。

ConsumerSide消费者侧核心逻辑

首先获取到RpcContext上下文,判断当前调用者属于服务提供方还是服务消费方。

如果是服务消费方,则从线程上下文TraceIdUtil中获取TraceId判断其是否为空。即如下的判断逻辑

if (StringUtils.isBlank(TraceIdUtil.getTraceId())) {

如果判断返回true,表明当前是消费侧的根调用,且消费侧没有显式的生成TraceId,则我们的组件生成一个TraceId设置到当前线程的上下文,并将其通过RpcContext隐式传参的方式传递给后续的服务。代码为

TraceIdUtil.setTraceId(traceId);
RpcContext.getContext().setAttachment(TraceIdConst.TRACE_ID, TraceIdUtil.getTraceId());

如果线程上下文TraceIdUtil存在TraceId,表明当前消费端已经存在TraceId。

因为存在TraceId,则我们直接从线程上下文中取出该TraceId,并通过RpcContext隐式传参方式传递给后续的服务,核心代码为

......
traceId = TraceIdUtil.getTraceId();
......
TraceIdUtil.setTraceId(traceId);
RpcContext.getContext().setAttachment(TraceIdConst.TRACE_ID, TraceIdUtil.getTraceId());

ProviderSide提供者侧核心逻辑

提供者侧的逻辑就比较容易了,提供者的角色主要就是对TraceId的读取。

核心代码如下:

if (rpcContext.isProviderSide()) {
    // 服务提供方,从Rpc上下文获取traceId
    traceId = RpcContext.getContext().getAttachment(TraceIdConst.TRACE_ID);
    TraceIdUtil.setTraceId(traceId);
}

可能这里你有疑问了,如果一个服务既是服务的提供者,又是消费者呢?该如何处理,其实在上文已经讲到了,如果它是提供方,则只需要从RpcContext上下文中读取TraceId进行使用即可。消费方的角色会在上述的消费者侧的逻辑处理,即读取TraceId,设置到当前线程,通过RpcContext隐式传递。

到这里,我们就完成了TraceFilter的主要逻辑,通过

Result result = invoker.invoke(invocation);
return result;

使业务逻辑继续往下走即可。

编写配置META-INF/dubbo/com.alibaba.dubbo.rpc.Filter

最后一步是dubbo的SPI规范要求的,不能更改。

在资源文件夹下创建 META-INF/dubbo 文件夹,在其中创建名为com.alibaba.dubbo.rpc.Filter 的文件,并编辑文件内容,在其中配置我们自定义的TraceIdFilter

traceIdFilter=com.snowalker.shield.dubbo.tracer.TraceIdFilter

具体的文件包路径如下

src
|-main
    |-java
        |-com
            |-xxx
                |-XxxFilter.java (实现Filter接口)
    |-resources
        |-META-INF
            |-dubbo
                |-org.apache.dubbo.rpc.Filter 
                (纯文本文件,内容为:xxx=com.xxx.XxxFilter)

要严格按照该路径进行开发和配置。

打包上传私服

到这里我们便可以打包该工具包并上传至私服,在业务中使用了。后续我会将完整的代码整理并上传至我的github仓库,以便读者自行参考打包使用。

mvn clean deploy -Dmaven.test.skip=true

小结

本文主要从开发及原理的角度,带领大家基于dubbo2.6.5实现了TraceId 设置/获取/传递 的需求,具有较高的实战价值。在下一篇中,我会以实际的案例展示如何在项目中使用该工具包,欢迎阅读。

参考链接

dubbo官网:调用拦截扩展

dubbo官网:DUBBO SPI介绍

扩展点重构

InheritableThreadLocal详解

InheritableThreadLocal——父线程传递本地变量到子线程的解决方式及分析

TransmittableThreadLocal详解

dubbo系列七、dubbo @Activate 注解使用和实现解析

Dubbo链路追踪——生成全局ID(traceId)

maven中scope属性的介绍

%02x 格式化符号代表什么

springboot整合dubbo报错No such extension traceIdFilter for filter/com.alibaba.dubbo.rpc.Filter

补充知识点:Maven中-DskipTests和-Dmaven.test.skip=true的区别

在使用mv编译、打包时为了跳过测试,会使用参数-DskipTests和-Dmaven.test.skip=true,这两个参数的主要区别是:

-DskipTests,不执行测试用例,但编译测试用例类生成相应的class文件至target/test-classes下。

-Dmaven.test.skip=true,不执行测试用例也不编译测试用例类。
文章目录
  1. 1. 需求描述
  2. 2. 原理讲解
    1. 2.1. Filter机制简述
    2. 2.2. SPI机制简述
  3. 3. 编码实现
    1. 3.1. 依赖引入
    2. 3.2. 编写TraceId生成类–TraceIdGenerator
    3. 3.3. 编写线程上下文TraceIdUtil
    4. 3.4. 编写自定义的TraceIdFilter实现com.alibaba.dubbo.rpc.Filter接口
      1. 3.4.1. ConsumerSide消费者侧核心逻辑
      2. 3.4.2. ProviderSide提供者侧核心逻辑
    5. 3.5. 编写配置META-INF/dubbo/com.alibaba.dubbo.rpc.Filter
  4. 4. 打包上传私服
  5. 5. 小结
  6. 6. 参考链接
  7. 7. 补充知识点:Maven中-DskipTests和-Dmaven.test.skip=true的区别
Fork me on GitHub