墨测,全自动化接口测试平台

1. 自动化测试面临的挑战

在软件开发周期中,测试是保障产品质量至关重要的一环,但也是往往容易被忽视的一环,测试的工期置后、重复回归执行等各种因素都影响着测试的效率,影响着高质量的产品发布。

自动化测试被认为是提高测试质量和效率的法宝,通过自动化测试平台,可以实现用例的编写、数据的准备、测试执行、结果验证,将测试工作通过脚本的方式重复执行,但是仅仅如此是不够的,测试还面临着如下的挑战,

  1. 用例的编写:测试在软件开发周期中,由于处于项目排期偏后,经常导致测试只有时间来了解新功能,却没有足够的时间来写自动化用例脚本,工期紧,自动化只能等下一次排期,进而一拖再拖。
  2. 用例的维护:软件产品的功能繁多、变化频繁,用例不是写一次就结束,用例的维护也需要花费测试人员相当多的时间,即使对一个资深软件测试工程师,也难以记住所有需要测试的功能接口,上个月刚测完的功能接口,本月再看,估计又需要大半天来上手的时间。
  3. 问题的定位:在微服务系统中,一不小心就会拆分出上百个应用,每个功能接口涉及的应用多、调用链路长。自动化测试跑完后,需要对失败的用例进行查看,其到底是不是软件bug?是用例的数据问题、或环境应用部署问题?面对失败的用例一个个调查时,查看运行日志,有时候还得需要开发人员的加入,一起来定位问题。自动化测试执行容易,用例失败问题定位困难重重。
  4. 环境的稳定性:测试环境本身就是不稳定的,每天需要测试的产品部署配置频繁,加上团队对测试环境的共享,会造成很多由于测试环境的不稳定导致的无效测试,很多时候等到测试到一半的时候,才发现环境不对。

这些挑战需要更好的方法和技术手段来解决。

墨测从产品设计之初就着手思考这些问题的解决方案,如何通过跨语言、跨平台、全链路的接口调用跟踪,通过真实流量,依靠AI智能,全自动化地创建用例、执行测试任务、验证结果、定位问题。

2. 什么是墨测

墨测,是一个面向AI时代的自动化测试平台,致力于实现测试的全自动化执行。

墨测平台能够自动地创建和维护接口测试用例,探知环境和应用变化,根据变化事件自动地选择最小测试集合,执行测试任务,在测试完毕后为每个测试用例建立相应的全链路关联追踪,方便问题根因分析,让测试更快、更准、更高效。

主要特点,

  • 从流量中创建和维护接口用例,数据和参数生成即可用。
  • 分析应用代码,从根源获悉应用变化。
  • 接口用例和全链路追踪关联,方便定位问题。
  • 从接口用例无缝快速对接压测任务,分布式压测机设计,支持压测流量水平扩展。

3. 视频简介

更多信息,请访问墨测网站 www.phantomn.com

通过WxJava获取微信AccessToken出现acquire timeouted问题

图片来自pixabay.com的Alexas_Fotos会员

WxJava是一个目前应用广泛的微信SDK工具开发包,本文记录一个WxJava获取微信AccessToken失败出现acquire timeouted的问题。

1. 故障现象

最近产线出现了一个严重的技术故障问题,用户无法登录长达1个多小时,通过查看日志发现和WxJava相关,在故障期间内,WxJava一直无法正常获取微信Access Token,报acquire timeouted的错误,见下图。

期间尝试重启两次,在19:20第一次重启应用后问题有所减弱,但是过了5分钟又重新大量发生,直到第二次重启应用后问题消失。

2. 问题定位

通过查看错误日志的堆栈信息,

java.lang.RuntimeException: acquire timeouted
    at cn.binarywang.wx.miniapp.config.impl.WxMaRedisConfigImpl$DistributedLock.lock(WxMaRedisConfigImpl.java:332)
    at cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl.getAccessToken(WxMaServiceImpl.java:110)
    at cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl.executeInternal(WxMaServiceImpl.java:249)
    at cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl.execute(WxMaServiceImpl.java:215)
    at cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl.get(WxMaServiceImpl.java:199)
    at cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl.jsCode2SessionInfo(WxMaServiceImpl.java:178)
    at cn.binarywang.wx.miniapp.api.impl.WxMaUserServiceImpl.getSessionInfo(WxMaUserServiceImpl.java:28)

可以看到和WxJava通过分布式锁获取token有关,产线用的WxJava代码版本是3.6.0,之前已经稳定运行了一年多。

下载相应版本WxJava代码,找到报错的代码行,

// 类文件 - cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl

public class WxMaServiceImpl implements WxMaService {

  public String getAccessToken(boolean forceRefresh) throws WxErrorException {
    if (!this.getWxMaConfig().isAccessTokenExpired() && !forceRefresh) {
      return this.getWxMaConfig().getAccessToken();
    }

    Lock lock = this.getWxMaConfig().getAccessTokenLock();
    lock.lock(); // <-- 问题发生代码行 WxMaServiceImpl.java:110
    try {
      String url = String.format(WxMaService.GET_ACCESS_TOKEN_URL, this.getWxMaConfig().getAppid(),
        this.getWxMaConfig().getSecret());
      try {
        HttpGet httpGet = new HttpGet(url);
        if (this.getRequestHttpProxy() != null) {
          RequestConfig config = RequestConfig.custom().setProxy(this.getRequestHttpProxy()).build();
          httpGet.setConfig(config);
        }
        try (CloseableHttpResponse response = getRequestHttpClient().execute(httpGet)) {
          String resultContent = new BasicResponseHandler().handleResponse(response);
          WxError error = WxError.fromJson(resultContent, WxType.MiniApp);
          if (error.getErrorCode() != 0) {
            throw new WxErrorException(error);
          }
          WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
          this.getWxMaConfig().updateAccessToken(accessToken.getAccessToken(), accessToken.getExpiresIn());

          return this.getWxMaConfig().getAccessToken();
        } finally {
          httpGet.releaseConnection();
        }
      } catch (IOException e) {
        throw new RuntimeException(e);
      }
    } finally {
      lock.unlock();
    }

  }
}

// 类文件 - cn.binarywang.wx.miniapp.config.impl.WxMaRedisConfigImpl$DistributedLock

public class WxMaRedisConfigImpl implements WxMaConfig {

  private class DistributedLock implements Lock {

    private JedisLock lock;

    private DistributedLock(String key) {
      this.lock = new JedisLock(getRedisKey(key));
    }

    @Override
    public void lock() {
      try (Jedis jedis = jedisPool.getResource()) {
        if (!lock.acquire(jedis)) {
          throw new RuntimeException("acquire timeouted"); // <-- 问题发生代码行 WxMaRedisConfigImpl.java:332
        }
      } catch (InterruptedException e) {
        throw new RuntimeException("lock failed", e);
      }
    }

    // ...
  }
}

上面的代码主要是通过redis分布式锁,使得只有一个进程中的一个线程执行token获取并存储到redis,梳理getAccessToken()方法的执行路径,可以发现其大概经历的步骤如下,

若仔细研读这个流程图,可以发现其中有很多相互抢占竞争资源的问题,其中有竞争的资源有3处,

  1. DistributedLock.lock():分布式锁,多个进程/线程抢占这个锁,只有获取到此锁的线程才能执行锁更新操作。
  2. jedisPool.getResource():单进程中多个线程竞争从连接池获取redis连接。
  3. JedisLock.acquire()和JedisLock.release():两个方法被synchronized修饰,这个是一个比较隐蔽的同步琐,被单进程中的多个线程所竞争抢占。

2.1 问题一:在jedisPool.getResource()时等待超时

如下图所示,

整个获取token过程需要多次获取jedisPool.getResource(),特别是获取到分布式锁的线程,即使通过http请求获取到了微信token之后,并不一定能够顺利地将token写入redis,因为写入时还需要再获取一次redis连接,若获取redis连接失败,则可能刷新token失败。

2.2 问题二:多次频繁刷新token

如下图所示,

在多个并发情况下,受到分布式锁的影响,当拥有锁的一个线程在更新token的时候,多个线程会被阻塞在distributedLock.lock(),但第一个拥有分布式锁的线程更新完token,所有其它阻塞的线程会继续执行后续的token刷新操作,进而导致频繁刷新token,在最差情况下,可能会导致一直无序地循环刷新token。

2.3 问题三:无法通过JedisLock.release()释放分布式锁

如下图所示,

当拥有分布式锁的一个线程结束更新token时,需要通过JedisLock.release()释放分布式锁,但是注意JedisLock.acquire()和JedisLock.release()两个方法被synchronized修饰,它们两个需要竞争抢占同一个JedisLock对象上的同步锁,很有可能的情况下,由于其它线程不停的acquire(),导致当前拥有分布式锁的线程永久无法得到释放。

3. 解决方案

可以看到3.6.0版本上的WxMaServiceImpl.getAccessToken()很多问题,根本原因就是在代码实现中几个竞争资源被来回穿插抢占,如何将锁资源的竞争关系解放,是解决关键。

如下是一个解决方案的示例代码,

// 类文件 - cn.binarywang.wx.miniapp.api.impl.WxMaServiceImpl

  private String getAccessToken(boolean forceRefresh) throws WxErrorException{
    if (!this.getWxMaConfig().isAccessTokenExpired() && !forceRefresh) {
      return this.getWxMaConfig().getAccessToken();
    }

    // 若是redis分布式锁实现,则走新方法更新token
    if (this.getWxMaConfig() instanceof WxMaRedisConfigImpl){
      return getAccessTokenV2((WxMaRedisConfigImpl) this.getWxMaConfig(), forceRefresh);
    }

    // 略...
  }

  /**
   * 更新redis中的token并返回
   * @param redisConfig
   * @param forceRefresh
   * @return
   */
  private String getAccessTokenV2(WxMaRedisConfigImpl redisConfig, boolean forceRefresh) throws WxErrorException{

    // 处理1:获取分布式锁的当前线程,后续都将使用当前jedis连接完成后续操作
    Jedis jedis = redisConfig.getJedis();

    try{
      DistributedLock lock = redisConfig.AccessTokenLock();

      lock.lock(jedis);
      log.info("当前线程获取到redis分布式锁");
      try {

        // 处理2:二次判断token是否有效
        if (!redisConfig.isAccessTokenExpired(jedis) && !forceRefresh) {
          log.info("二次判断token有效,无需再次刷新");
          return redisConfig.getAccessToken(jedis);
        }

        String url = String.format(WxMaService.GET_ACCESS_TOKEN_URL, this.getWxMaConfig().getAppid(),
          this.getWxMaConfig().getSecret());
        try {
          HttpGet httpGet = new HttpGet(url);
          if (this.getRequestHttpProxy() != null) {
            RequestConfig config = RequestConfig.custom().setProxy(this.getRequestHttpProxy()).build();
            httpGet.setConfig(config);
          }
          Date start = new Date();

          try (CloseableHttpResponse response = getRequestHttpClient().execute(httpGet)) {
            Date end = new Date();
            String resultContent = new BasicResponseHandler().handleResponse(response);
            WxError error = WxError.fromJson(resultContent, WxType.MiniApp);
            if (error.getErrorCode() != 0) {
              throw new WxErrorException(error);
            }
            WxAccessToken accessToken = WxAccessToken.fromJson(resultContent);
            redisConfig.updateAccessToken(jedis, accessToken.getAccessToken(), accessToken.getExpiresIn());
            return accessToken.getAccessToken();
          } finally {
            httpGet.releaseConnection();
          }
        } catch (IOException e) {
          throw new RuntimeException(e);
        }
      } finally {
        lock.unlock(jedis);
      }
    } finally {
      jedis.close();
    }
  }

// 类文件 - cn.binarywang.wx.miniapp.config.impl.WxMaRedisConfigImpl$DistributedLock

  /**
   * 基于redis的分布式锁.
   * 处理3:通过已有jedis连接获取和释放分布式锁,见lock()/unlock()
   */
  private class DistributedLock implements GuoquanRedisLock {

    private JedisLock lock;

    private DistributedLock(String key) {
      this.lock = new JedisLock(getRedisKey(key));
    }

    @Override
    public void lock(Jedis jedis) {
      try {
        if (!lock.acquire(jedis)) {
          throw new RuntimeException("acquire timeouted");
        }
      }catch (InterruptedException e){
        throw new RuntimeException("lock failed", e);
      }
    }

    @Override
    public void unlock(Jedis jedis) {
      lock.release(jedis);
    }

  }

在上面的代码中,主要改变如下,

  1. 处理1:获取分布式锁的当前线程,后续都将使用当前jedis连接完成后续操作。
  2. 处理2:二次判断token是否有效,避免循环刷新token。
  3. 处理3:通过已有jedis连接获取和释放分布式锁,见lock()/unlock()。

应用通过如上代码上线后,acquire timeouted的问题没有再出现,问题得到解决。

4. 发生的版本和后续WxJava优化

上面的问题存在WxJava的多个版本,包括v3.6.0到v3.8.0的版本。

直到v3.9.0及后续版本,可以看到相关问题的陆续修复,如下是两个相关的代码提交,

      if (!this.getWxMaConfig().isAccessTokenExpired() && !forceRefresh) {
        return this.getWxMaConfig().getAccessToken();
      }

locks同一目录下有另外一个RedisTemplateSimpleDistributedLock的实现供配置使用,其在版本v3.9.0已经支持,需要进行如下配置,

# 存储配置redis(可选)
wx.mp.config-storage.type = redistemplate             # 配置类型: Memory(默认), Jedis, RedisTemplate

更多配置请参考README文件

5. 思考

有些问题一直不出现,不代表着不存在,在高并发情况下锁、线程池、连接池等资源竞争问题都将无限放大,可靠的软件产品需要经历一个持续不断打磨的历练过程。

Java字节码增强技术Bytebuddy探路篇二(NoClassDefFoundError问题)

图片来自pixabay.com的NickRivers会员

最近为了实现Java应用RPC调用的录制和Mock回放,需要以无侵入方式获取到RPC方法的出入参数和返回响应消息等数据,于是踏上了Java字节码增强技术的道路摸索。技术路径上选择了Java Agent + Bytebuddy框架,但是在应用实践过程中,对tomcat/dubbo/rocketmq进行类切面增强时出现了NoClassDefFoundError的问题,本文针对这个关键问题的出现原因进行分析讨论,提供相应的解决思路。

1. 演示代码工程

为了方便讨论,请先下载演示代码工程,整个工程结构如下,

+ phantom-core 演示基础类。
+ phantom-demo 演示web应用,这是一个简单的sprint boot应用,运行在端口18080。
+ phantom-agent 一个Java agent,里面通过ByteBuddy对指定类以无侵入方式切面增强,在演示工程中主要对Tomcat web的请求进行切面处理。
+ phantom-agent-plugin 一个增强类插件,这个是为了解决NoClassDefFoundError问题而提供的一个插件解决方案,应用于phantom-agent的TransformerV3.tranform()中,项目构建后需要复制构建包到路径/tmp/phantom-agent-plugin.jar上。

下载后,分别对项目进行构建、启动和测试,命令如下,

# 使用mvn工具编译构建
mvn clean package

# 启动演示web应用
java -jar ./phantom-demo/target/phantom-demo.jar

# 测试web请求命令
curl -X POST http://127.0.0.1:18080/api/hello

若上面的web应用启动成功,则测试web请求将会收到“hello,world”的消息响应,这个简单的测试请求已经走过web服务的完整路径。工程中的Transformer将通过java agent方式对tomcat web请求进行切面处理,获取所有http请求的执行前、执行后、执行异常相关情况并打印到日志中,tomcat切点定义为,

# 切点:tomcat核心类StandardHostValve的invoke方法
org.apache.catalina.core.StandardHostValve.invoke(Request request, Response response);

演示工程中Transformer类有三个,

  1. com.phantom.agent.trace.TransformerV1 :用于演示问题的复现和定位
  2. com.phantom.agent.trace.TransformerV2 :用于演示问题的解决思路1
  3. com.phantom.agent.trace.TransformerV3 :用于演示问题的解决思路2

这三个类都各自继承自ByteBuddy的AgentBuilder.Transformer接口类,实现了tranform()方法。

2. 问题复现和定位

我们先复现一下NoClassDefFoundError的问题,通过设置agent启动参数agent.transformer.version=v1,执行TransformerV1版本的类增强变换,启动命令如下,

# 启动命令(加载agent)
java \
-javaagent:./phantom-agent/target/phantom-agent.jar \
-Dagent.transformer.version=v1 \
-jar ./phantom-demo/target/phantom-demo.jar

这个时候对hello接口发起请求,该请求可以成功“hello,world”的响应消息,翻开web应用后台,可以观察如下日志,

[20220808 16:22:37-453][http-nio-18080-exec-2][INFO] [trace]beforeMethod(), method = org.apache.catalina.core.StandardHostValve.invoke
[20220808 16:22:37-453][http-nio-18080-exec-2][ERROR] EnhancerProxy failure - beforeMethod, [class org.apache.catalina.core.StandardHostValve].[invoke], msg = java.lang.NoClassDefFoundError: org/apache/catalina/connector/Request
[20220808 16:22:37-455][http-nio-18080-exec-2][INFO] [trace]afterMethod(), method = org.apache.catalina.core.StandardHostValve.invoke

可以看到,增强切面在后台报java.lang.NoClassDefFoundError的错误,即找不到org.apache.catalina.connector.Request类,若通过调试的话,可以看到这个异常是从TomcatEnhancer报出来,增强切面出现异常,找不到相应的Request类。

这个Request类定义在tomcat-embedd-core类包中,这个tomcat核心类包其实已经被web应用正常加载,tomcat web处于正常工作状态,hello接口的请求可以得到hello响应消息证明了这点。另外,ByteBuddy也正常在指定invoke切点织入了切面,如下的两条来自TomcatEnhancer日志证明了这点:

[20220808 16:22:37-453][http-nio-18080-exec-2][INFO] [trace]beforeMethod(), method = org.apache.catalina.core.StandardHostValve.invoke
[20220808 16:22:37-455][http-nio-18080-exec-2][INFO] [trace]afterMethod(), method = org.apache.catalina.core.StandardHostValve.invoke

现在的问题是,为什么在增强切面类TomcatEnhancer执行时会报找不到tomcat核心类包的Request类?

打开应用的远程调试,在执行到TomcatEnhancer时,检查一下是否能够直接通过loadClass找到Request类,

this.getClass().classLoader.loadClass("org.apache.catalina.connector.Request")

没有出意外,确实没有找到这个类。查看了一下TomcatEnhancer的类加载器,是AppClassLoader,这是系统类加载器,负责加载来自Java classpath指定路径下的JAR类包。难道Request类不在AppClassLoader的搜索路径?

于是通过arthas工具查看当前应用的类加载树,

# 查看类加载器树
[arthas@78212]$ classloader -t
+-BootstrapClassLoader
+-sun.misc.Launcher$ExtClassLoader@2503dbd3
  +-com.taobao.arthas.agent.ArthasClassloader@463899fd
  +-sun.misc.Launcher$AppClassLoader@18b4aac2
    +-org.springframework.boot.loader.LaunchedURLClassLoader@6fdbe764
Affect(row-cnt:5) cost in 10 ms.

# 查看类文件信息,通过AppClassLoader,未找到
[arthas@78212]$ classloader -c 18b4aac2 -r org/apache/catalina/connector/Request.class
Affect(row-cnt:0) cost in 0 ms.

# 查看类文件信息,通过LaunchedURLClassLoader,找到
[arthas@78212]$ classloader -c 6fdbe764 -r org/apache/catalina/connector/Request.class
 jar:file:/Users/huangyinhuang/hyh/gitee/simple-demo/demo-bytebuddy/phantom-demo/target/phantom-demo.jar!/BOOT-INF/lib/tomcat-embed-core-9.0.65.jar!/org/apache/catalina/connector/Request.class
Affect(row-cnt:1) cost in 1 ms.

通过arthas的探针查看证实了Request类由LaunchedURLClassLoader加载,确实无法通过AppClassLoader进行加载。

3. 解决思路1

知道了这个问题的原因后,一个首先想到的解决方案就是让LaunchedURLClassLoader来加载TomcatEnhancer类,即延迟TomcatEnhancer类加载到类增强时刻,而不是在agent启动时刻,这样TomcatEnhancer类和Request类都由Spring的LaunchedURLClassLoader进行加载。

我们可以在transform方法中通过传入的LaunchedURLClassLoader来动态创建TomcatEnhancer实例,于是就有了TransformerV2版本的类增强变换方案,打开其transform()方法,可以看到其如下代码实现,

@Override
public DynamicType.Builder transform(DynamicType.Builder builder, TypeDescription typeDescription, ClassLoader loader, JavaModule javaModule) {

        // enhanceClass = "com.phantom.agent.enhancer.impl.TomcatEnhancer"
        // loader = org.springframework.boot.loader.LaunchedURLClassLoader

        EnhancerProxy proxy = new EnhancerProxy();
        try {
            AbstractEnhancer enhancer = (AbstractEnhancer) Class.forName(this.enhanceClass, true, loader).newInstance();
            proxy.setEnhancer(enhancer);
        } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
            Logger.error("failed to initialized the proxy %s", e.toString());
        }

        // return ...
    }

通过设置agent启动参数agent.transformer.version=v2,执行上面TransformerV2版本的类增强变换,

# 启动命令(加载agent)
java \
-javaagent:./phantom-agent/target/phantom-agent.jar \
-Dagent.transformer.version=v2 \
-jar ./phantom-demo/target/phantom-demo.jar

发一个hello接口的测试请求,测试请求可以成功收到“hello,world”的响应消息,翻开web应用后台,却仍然收到如下NoClassDefFoundError日志,

[20220808 18:29:44-174][http-nio-18080-exec-2][INFO] [trace]beforeMethod(), method = org.apache.catalina.core.StandardHostValve.invoke
[20220808 18:29:48-173][http-nio-18080-exec-2][ERROR] EnhancerProxy failure - beforeMethod, [class org.apache.catalina.core.StandardHostValve].[invoke], msg = java.lang.NoClassDefFoundError: org/apache/catalina/connector/Request
[20220808 18:29:48-176][http-nio-18080-exec-2][INFO] [trace]afterMethod(), method = org.apache.catalina.core.StandardHostValve.invoke

问题并没有得到解决,继续跟踪调试,发现如下Class.forName方法参数重虽然指定了LaunchedURLClassLoader,但实际上返回TomcatEnhancer实例仍然由AppClassLoader加载,并没有被指定的LaunchedURLClassLoader所加载。

// enhanceClass = "com.phantom.agent.enhancer.impl.TomcatEnhancer"
// loader = org.springframework.boot.loader.LaunchedURLClassLoader
Class.forName(this.enhanceClass, true, loader).newInstance()

见调试图片,

这个时候突然想到类的双亲委派机制,恍然大悟,这里即使指定了LaunchedURLClassLoader来创建,但是由于AppClassLoader是LaunchedURLClassLoader父加载器,基于Java的双亲委派机制,最终还是会被AppClassLoader进行创建。

4. 解决思路2

这个时候,就不能再依赖LaunchedURLClassLoader了,得另谋思路。要改变Java的双亲委派机制,就得自定义类加载器,这也是TransformerV3版本采用的技术方案。

@Override
public DynamicType.Builder transform(DynamicType.Builder builder, TypeDescription typeDescription, ClassLoader loader, JavaModule javaModule) {

    // enhancerClz = "com.phantom.agent.enhancer.impl.TomcatEnhancer"
    // loader = org.springframework.boot.loader.LaunchedURLClassLoader

    EnhancerProxy proxy = new EnhancerProxy();
    try {
        IAspectEnhancer enhancer = EnhancerInstanceLoader.load(enhancerClz, loader);
        proxy.setEnhancer(enhancer);
    } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {
        Logger.error("failed to initialized the proxy %s", e.toString());
    }

    // return ...
}

上面实现代码中的EnhancerInstanceLoader就是一个自定义类加载器,其将加载指定在路径"/tmp/phantom-agent-plugin.jar"的Jar包。

通过设置agent启动参数agent.transformer.version=v3,执行上面TransformerV3版本的类增强变换,

# 复制文件到/tmp/phantom-agent-plugin.jar
cp ./phantom-agent-plugin/target/phantom-agent-plugin.jar /tmp/

# 启动命令(加载agent)
java \
-javaagent:./phantom-agent/target/phantom-agent.jar \
-Dagent.transformer.version=v3 \
-jar ./phantom-demo/target/phantom-demo.jar

发一个hello接口的测试请求,测试请求可以成功收到“hello,world”的响应消息,这时打开web应用后台日志,NoClassDefFoundError的错误信息不再出现, hello接口的Span详细请求信息也通过Span顺利地打印出来了。

[20220808 18:54:18-878][http-nio-18080-exec-2][INFO] [trace]beforeMethod(), method = org.apache.catalina.core.StandardHostValve.invoke
[20220808 18:54:18-881][http-nio-18080-exec-2][INFO] [trace]afterMethod(), method = org.apache.catalina.core.StandardHostValve.invoke
[20220808 18:54:23-880][BatchSpanProcessor_WorkerThread-1][INFO] demo 1.0.0 - SpanData{spanContext=ImmutableSpanContext{traceId=d23da1005580022dd42e92a3fc4c0754, spanId=cfdd64665bdb7270, traceFlags=01, traceState=ArrayBasedTraceState{entries=[]}, remote=false, valid=true}, parentSpanContext=ImmutableSpanContext{traceId=00000000000000000000000000000000, spanId=0000000000000000, traceFlags=00, traceState=ArrayBasedTraceState{entries=[]}, remote=false, valid=false}, resource=Resource{schemaUrl=null, attributes={service.name="unknown_service:java", telemetry.sdk.language="java", telemetry.sdk.name="opentelemetry", telemetry.sdk.version="1.12.0"}}, instrumentationLibraryInfo=InstrumentationLibraryInfo{name=demo, version=1.0.0, schemaUrl=null}, name=POST /api/hello, kind=PRODUCER, startEpochNanos=1659956058879000000, endEpochNanos=1659956058881771573, attributes=AttributesMap{data={METHOD=POST, URL=http://127.0.0.1:18080/api/hello, Component=Tomcat, URI=/api/hello, RemoteAddr=127.0.0.1, RemoteHost=127.0.0.1, RemotePort=64352}, capacity=128, totalAddedValues=7}, totalAttributeCount=7, events=[], totalRecordedEvents=0, links=[], totalRecordedLinks=0, status=ImmutableStatusData{statusCode=UNSET, description=}, hasEnded=true}
ta={METHOD=POST, URL=http://127.0.0.1:18080/api/hello, Component=Tomcat, URI=/api/hello, RemoteAddr=127.0.0.1, RemoteHost=127.0.0.1, RemotePort=62654}, capacity=128, totalAddedValues=7}, totalAttributeCount=7, events=[], totalRecordedEvents=0, links=[], totalRecordedLinks=0, status=ImmutableStatusData{statusCode=UNSET, description=}, hasEnded=true}

5. 执行时序图

为了更好地理解相关代码的执行路径,下面提供相关的执行时序图,