关于java:自动化回归测试平台-AREX-Agent-源码再阅读

55次阅读

共计 10948 个字符,预计需要花费 28 分钟才能阅读完成。

AREX 启动过程

通用 Java Agent 的启动过程

Java Agent 是一种 Java 应用程序,它能够在 Java 应用程序启动时动静地注入到 JVM 中,并在利用程序运行时监督和批改应用程序的行为。Java Agent 通常用于性能剖析、代码覆盖率、安全检查等方面。

以下是 Java Agent 的启动过程:

  1. 编写 Java Agent 程序,实现 premain 办法。premain 办法是 Java Agent 的入口办法,它会在 Java 应用程序启动时被调用。在 premain 办法中,能够进行一些初始化操作,如设置代理、加载配置文件等。
  2. 将 Java Agent 打包成 jar 文件,并在 MANIFEST.MF 文件中指定 Premain-Class 属性,该属性指定了 Java Agent 的入口类。
  3. 在启动 Java 应用程序时,通过 -javaagent 参数指定 Java Agent 的 jar 文件门路。例如:

    java -javaagent:/path/to/agent.jar -jar myapp.jar

    在下面的命令中,/path/to/agent.jar 是 Java Agent 的 jar 文件门路,myapp.jar 是 Java 应用程序的 jar 文件门路。

  4. 当 Java 应用程序启动时,JVM 会加载 Java Agent 的 jar 文件,并调用 premain 办法。在 premain 办法中,Java Agent 能够应用 Java Instrumentation API 来批改 Java 应用程序的字节码,实现对应用程序的监督和批改。

AREX 源码视角的启动过程

步骤一

arex-agent module pom.xml文件中,通过配置 manifestEntries,将Premain-Class 属性设置为 io.arex.agent.ArexJavaAgent。这意味着在构建arex-agent.jar 时,将在 manifest 文件中指定 ArexJavaAgent 类作为 Agent 的入口点。

步骤二

ArexJavaAgent 类中,实现了 premain 办法作为 Agent 的入口办法。在 premain 办法中,它调用了 agentmain 办法。在 agentmain 办法中,进一步调用了 init(Instrumentation inst, String agentArgs) 函数。这个函数承受一个 Instrumentation 对象和一个字符串参数agentArgs

步骤三

init 函数中,有两个重要的操作:installBootstrapJar()AgentInitializer.initialize()

installBootstrapJar()

installBootstrapJar()函数依据 AgentInitializer.class 找到其所在的 jar 包,并通过调用 inst.appendToBootstrapClassLoaderSearch(jar) 将其增加到 Bootstrap ClassLoader 的搜寻门路中。Bootstrap ClassLoader 是 Java 虚拟机中负责加载外围类库(如 java.langjava.util等)的非凡类加载器。通过调用 appendToBootstrapClassLoaderSearch 办法,能够将自定义的类库增加到 Bootstrap ClassLoader 的搜寻门路中,从而使得 Java 应用程序可能应用这些自定义的类库。

如要依据 class 对象或者 jar 包的实现,获取一个类所在的 jar 包,能够依照以下步骤进行:

  1. 获取该类的 Class 对象。
  2. 调用 Class 对象的 getProtectionDomain() 办法获取该类的 ProtectionDomain 对象。
  3. 调用 ProtectionDomain 对象的 getCodeSource() 办法获取该类的 CodeSource 对象。
  4. 调用 CodeSource 对象的 getLocation() 办法获取该类所在的 jar 包的 URL。
  5. 通过 URL 对象的 getFile() 办法获取该 jar 包的门路。
AgentInitializer.initialize()

AgentInitializer.initialize()函数是依据 ArexJavaAgent.class 找到其所在的 jar 包(AgentInitializer.java文件),而后设置 arex.agent.jar.file.path 变量,即代理 jar 包所在的目录。

接下来,它会在该目录下查找 /extensions/ 子目录,并读取该目录下的所有 jar 包文件,这些文件是扩大包所在的地位。

而后,调用 createAgentClassLoader(agent jar, 扩大包.jar) 函数,创立一个 AgentClassLoader 对象,它是 AREX 自定义的类加载器。应用自定义的类加载器是为了隔离,避免应用程序可能拜访 AREX Agent 的代码。

接着,调用 createAgentInstaller() 函数,该函数应用后面生成的 AgentClassLoader 加载类 io.arex.agent.instrumentation.InstrumentationInstaller,获取其构造函数并创立实例,而后返回指向AgentInstaller 接口的对象。

AdviceClassesCollector收集代理 jar 文件和扩大 jar 文件。

应用指向 AgentInstaller 接口的 installer(后面返回的对象)调用install() 函数,实际上会调用 BaseAgentInstaller 类的 install() 函数,该函数调用 init(String agentArgs) 进行初始化。

BaseAgentInstaller 类的 install() 函数中,调用 init() 函数执行以下操作:

  1. 初始化 TraceContextManager,生成IDGenerator 用于生成TransactionID
  2. 初始化installSerializer
  3. 初始化RecordLimiter,设置录制频率限度。
  4. ConfigService加载代理配置,包含设置调试模式、动静类配置、排除操作配置、Dubbo 回放阈值、记录速率配置等。
  5. 初始化数据收集器,依据运行模式进行判断,并启动数据收集器。
  6. 再次从服务器获取代理配置,进行三次重试,而后解析配置并进行再次设置和更新(留神:存在一个 BUG,第二次从服务器获取配置后,并没有看到 Dubbo 的回放阈值失去更新)。

BaseAgentInstaller 类的 install() 函数中,调用了名为 transform() 的形象函数。实际上,这个形象函数的具体实现在 InstrumentationInstaller 类的 transform() 函数中。

通过这些配置和操作,ArexJavaAgent类将被作为 Agent 的入口点,在 Java 应用程序启动时被加载,并对 Bootstrap ClassLoader 进行扩大,使得应用程序可能应用自定义的类库。

步骤四

在 InstrumentationInstaller 类的 transform() 函数中实现了对指标应用程序的代码注入操作。

  1. 通过 getAgentBuilder() 获取了 ByteBuddy 的 AgentBuilder。
  2. 通过 SPI 函数获取了所有标识为 ModuleInstrumentation.class 的类的列表,这些类应用了 com.google.auto.service 注解 @AutoService(ModuleInstrumentation.class)。
  3. 依据上文获取的 List, 一一调用 InstallModule(),即应用步骤 6.a 中获取的 AgentBuilder 和 ModuleInstrumentation 注册模块。
  4. 在 ModuleInstrumentation 类中获取了 TypeInstrumentation 的列表,针对每个 TypeInstrumentation,找到其对应的 MethodInstrumentation 列表。
  5. 对于每个 MethodInstrumentation,调用 AgentBuilder.Identified 的 transform() 函数进行代码注入。

简而言之,这一步过程是实现了模块化的插桩性能。通过实现 ModuleInstrumentation 接口,能够定义须要进行代码注入的模块。在每个模块中,通过实现 TypeInstrumentation 接口能够定义须要注入代码的具体类型。而在每个类型中,通过实现 MethodInstrumentation 接口能够定义须要注入代码的具体方法。这样,AREX Agent 就能够依据这些定义,将录制和回放的代码注入到相应的办法中,实现了对应性能的记录和重放。

步骤五

实现所有类的注入,实现 AREX 的启动后 AREX 开始运行。

AREX 录制回放

录制回放概述

AREX 的录制性能不仅仅是独自录制申请报文,而是将申请、应答报文以及外部调用的申请和应答一并保留下来。外围指标是将申请、应答和外部调用的申请应答一一关联起来保留。AREX 采纳相似 OpenTelemetry 的 Tracing 技术,实现全链路跟踪并保留关联的 Tracing ID。

录制

录制分为入口录制和外部调用录制两局部。入口申请中没有 Tracing ID,须要生成惟一的 Tracing ID,并记录下来。入口录制保留申请和 Tracing ID。外部调用录制保留 Tracing ID 和外部调用的申请、应答。

入口申请的响应报文也须要记录,即入口调用的应答和 Tracing ID(这里提到的 Tracing ID 在后文中称为 AREX Record ID)。

回放

回放过程中,入口申请中蕴含 AREX-Replay-ID 和 Record ID 的报文。依据 Record ID 从数据库中获取相应的应答,并返回给调用方。同时,关联 Replay ID 记录回放过程中的数据并保留到数据库。

在外部调用过程中,如果检测到以后处于回放状态,则依据 Record ID 从数据库中获取数据返回(模仿应答),并记录外部调用的申请,关联 Replay ID 并保留到数据库。

依据 Replay ID,找到入口调用的应答报文以及外部调用的申请报文,并进行录制场景和回放场景的差别比对。

最初输入差别后果,完结回放过程。

AREX Servlet 的入口录制和回放

代码所在目录 arex-agent-java\arex-instrumentation\servlet

AREX 注入代码三要素

  1. ModuleInstrumentation: FilterModuleInstrumentationV3
  2. TypeInstrumentation: FilterInstrumentationV3
  3. MethodInstrumentation:
     @Override
    public List<MethodInstrumentation> methodAdvices() {ElementMatcher<MethodDescription> matcher = named("doFilter")
                .and(takesArgument(0, named("javax.servlet.ServletRequest")))
                .and(takesArgument(1, named("javax.servlet.ServletResponse")));

        return Collections.singletonList(new MethodInstrumentation(matcher, FilterAdvice.class.getName()));
    } 

录制回放的步骤

  1. 改写 javax.servlet.Filter 类的 doFilter(request, response) 函数。
  2. 在函数入口处(OnMethodEnter)进行改写,并获取两个参数,0 位是 request,1 位是 response。

    a. 调用 ServletAdviceHelper.onServiceEnter(),传入申请和应答。

    b. 调用 CaseEventDispatcher.onEvent(CaseEvent.ofEnterEvent()),其中包含调用了 TimeCache.remove()、TraceContextManager.remove() 和 ContextManager.overdueCleanUp()。

    c. 调用 CaseEventDispatcher.onEvent(CaseEvent.ofCreateEvent()),其中包含调用了 initContext(source) 和 initClock()。

    initContext() 函数调用设置 ArexContext,入口处会生成 TraceID。ContextManager.currentContext(true, source.getCaseId()) 中的 createIfAbsent 参数传入 True,会调用 TRACE_CONTEXT.set(messageId)。

    initClock() 函数判断以后是否处于回放状态,如果是则解析工夫并调用 TimeCache.put(millis)。如果以后是录制状态(即 ArexContext 不为空且不处于回放状态 ContextManager.needRecord()),则调用 RecordMocker。

  3. 在函数出口处(OnMethodExit)进行改写,调用 ServletAdviceHelper.onServiceExit()。

    调用 new ServletExtractor<>(adapter, httpServletRequest, httpServletResponse).execute() 函数。

    而后调用 doExecute(),构建 Mocker 对象,并为 Mocker 对象设置申请头、Body 和属性。同时为 Mocker 对象设置响应对象、Body 和 Type。

    如果以后处于回放状态,则回放 Mocker 数据。如果以后处于录制状态,则保留 Mocker 数据。

相似的实现形式也实用于入口录制和回放,原理相似,不再赘述。

  • 对于 Dubbo,能够在 DubboProviderExtractor 类的 onServiceEnter() 中实现。
  • 对于 Netty,能够在 io.netty.channel.DefaultChannelPipeline 类中的 add 前缀函数和 replace 函数中实现。

AREX 外部调用的录制回放

代码所在目录 arex-agent-java\arex-instrumentation\netty\arex-netty-v4

AREX 注入代码三要素:

  1. ModuleInstrumentation: NettyModuleInstrumentation
  2. TypeInstrumentation: ChannelPipelineInstrumentation
  3. MethodInstrumentation:
     @Override
    public List<MethodInstrumentation> methodAdvices() {
        return singletonList(new MethodInstrumentation(isMethod().and(nameStartsWith("add").or(named("replace")))
                        .and(takesArgument(1, String.class))
                        .and(takesArgument(2, named("io.netty.channel.ChannelHandler"))),
                AddHandlerAdvice.class.getName()));
    } 

录制回放的步骤

在 Java Netty 中,ChannelPipeline 是一个事件处理机制,用于解决入站和出站事件。它是 Netty 的外围组件之一,用于治理 ChannelHandler 的解决流程。当一个事件被触发时,它会被传递给 ChannelPipeline,而后由 Pipeline 中的每个 ChannelHandler 顺次解决。每个 ChannelHandler 都能够对事件进行解决或者转发给下一个 ChannelHandler。addAfter 办法是用于向 ChannelPipeline 中增加一个新的 ChannelHandler,并将其插入到指定的 ChannelHandler 之后。这个办法能够用于动静地批改 ChannelPipeline 中的解决流程,以便在运行时依据须要增加或删除处理器。

在改写 io.netty.channel.DefaultChannelPipeline 类的 add 前缀函数或者 replace 函数时,咱们能够在函数 OnMethodExit 时获取以后对象的 ChannelPipeline,以及参数 1 handleNamer 和参数 2 handler。

咱们能够进行以下判断和解决:

  • 如果 handler 是 HttpRequestDecoder 实例,则调用 RequestTracingHandler()来解决回放的数据。
  • 如果 handler 是 HttpResponseEncoder 实例,则调用 ResponseTracingHandler()来解决录制的数据。
  • 如果 handler 是 HttpServerCodec 实例,则调用 ServerCodecTracingHandler()来解决。HttpServerCodec 是 Java Netty 中的一个 ChannelHandler,用于将 HTTP 申请和响应音讯编码和解码为 HTTP 音讯。它实现了 HTTP 协定的编解码,能够将 HTTP 申请和响应音讯转换为字节流,以便在网络中传输。

异步拜访的解决

在 Java 生态系统中存在多种异步框架和类库,如 Reactor、RxJava 等,同时还有一些类库提供了异步拜访的实现,例如 lettuce 提供了同步和异步拜访 Redis 的形式。不同的场景通常须要不同的解决方案。

以 ApacheAsyncClient 为例,它是通过在固定运行的线程中监听响应并发动回调(Callback)来实现异步解决。在整个调用、监听和回调的过程中,须要确保多个跨线程的 Trace 传递。

在注入代码中,须要应用 FutureCallbackWrapper 中的 TraceTransmitter 来传递 Trace。具体的注入地位如下:

  • ModuleInstrumentation: SyncClientModuleInstrumentation
  • TypeInstrumentation: InternalHttpAsyncClientInstrumentation(用于异步状况)、InternalHttpClientInstrumentation
  • MethodInstrumentation: 注入到 org.apache.http.impl.nio.client.InternalHttpAsyncClient 类的 execute 函数,应用 named(“execute”)办法进行辨认。

录制回放的步骤

在注入函数中,咱们针对 org.apache.http.impl.nio.client.InternalHttpAsyncClient 类的 execute 函数进行操作,应用函数名 named(“execute”)来标识该函数。

首先,咱们获取 execute 函数的第三个参数 FutureCallback,并将其赋值给 AREX 实现的封装类 FutureCallbackWrapper 的 callback 参数。FutureCallback 接口定义了两个办法:onSuccess 和 onFailure。当异步操作胜利实现时,onSuccess 办法将被调用,并传递异步操作的后果作为参数。当异步操作失败时,onFailure 办法将被调用,并传递异样作为参数。

而后,咱们进行以下判断:

  • 如果须要进行录制,则 FutureCallbackWrapper 的封装类重写了 completed(T) 函数,在 completed 函数中保留响应数据,而后调用原始的 FutureCallback 的 completed 办法。同样地,FutureCallbackWrapper 的封装类也重写了 failed() 函数,在 failed 函数中记录响应数据,并调用原始的 FutureCallback 的 failed 办法。
  • 如果须要进行回放,则获取回放数据并将其保留在本地的 mockResult 变量中。

最初,在注入函数的出口处,如果 mockResult 变量的数据不为空,并且 callback 是 AREX 封装类的实例,那么调用封装类的 replay 函数进行回放操作。

通过以上操作,咱们在 execute 函数的入口和出口处对跨线程的 Trace 传递进行了解决,包含录制和回放性能的实现。

AREX 录制频率设置

在 ServletAdviceHelper 类的 onServiceEnter 函数中(就是 Servlet 进入的函数中),实现了 AREX 的录制频率设置。

CaseEventDispatcher.onEvent(CaseEvent.ofEnterEvent());
if (shouldSkip(adapter, httpServletRequest)) {return null;}

首先,依据报文头和配置进行录制判断:

  • 如果申请报文头中有 caseID 字段,查问配置项 arex.disable.replay,如果该配置项的值为 true,则间接跳过录制。
  • 如果申请报文头中存在 arex-force-record 字段,并且该字段的值为 true,则不能跳过录制。
  • 如果申请报文头中存在 arex-replay-warm-up 字段,并且该字段的值为 true,则跳过录制。

接下来,会解析申请报文:

  • 如果申请 URL 为空,则跳过录制。
  • 如果申请 URL 在配置中的录制疏忽列表中,则跳过录制。

接着,会调用 Config 类的 invalidRecord 办法进行录制有效性的查看:

  • 如果配置处于 debug 状态,则不能跳过录制,间接返回 false。
  • 如果配置的录制速率小于 0,则跳过录制。

最初,依据申请的门路和录制速率判断是否须要跳过录制,这里应用 com.google.common.util.concurrent.RateLimiter 类的 acquire 函数。RateLimiter 是 Google Guava 库中的一个类,用于限度操作的频率。它能够用于管制某个操作在肯定工夫内最多能执行多少次,或者在肯定工夫内最多能执行多少次操作。应用 RateLimiter 类,须要先创立一个 RateLimiter 对象,并指定它的速率限度。而后,咱们能够应用 acquire() 办法获取一个许可证,示意能够执行一个操作。

  • 如果以后速率曾经达到了限度,acquire 函数会阻塞期待,直到可能获取到许可证为止。
  • 如果可能获取到许可证,则不跳过录制。

AREX 代码隔离

在 Java 虚拟机中,判断两个类是否相等时,不仅会比拟它们的全限定名,还会比拟它们的加载器。如果两个类的全限定名雷同,但加载它们的 ClassLoader 不同,Java 虚构机会认为这是两个不同的类。

这种设计有助于保障 Java 虚拟机的安全性和隔离性。不同的 ClassLoader 能够加载同一个类,但它们加载的类是互相独立的,相互不可见的。这样能够防止不同应用程序或模块之间的类抵触和烦扰。

在 AREX 中,波及的 ClassLoader 有以下几种:

  • arex-agent:由 AppClassLoader 加载,用于加载 AREX Agent 的外围组件。
  • arex-agent-bootstrap:由疏导类加载器(Bootstrap ClassLoader)加载,用于加载 AREX Agent 的疏导类。
  • arex-agent-core:由 AgentClassLoader 加载,这是 AREX 自定义的 ClassLoader,负责加载 arex-agent-core 等 jar。
  • arex-instrumentation:由 UserClassLoader 加载,用于加载 AREX 的 Instrumentation、Module 和 Advice 等组件。

    • XXX Instrumentation & Module & Advice:由 AgentClassLoader 加载,用于加载具体的 Instrumentation、Module 和 Advice 等实现。
  • arex-instrumentation-api:由 AgentClassLoader 加载,包含 API 和 Runtime 两局部。

    • api:由 AgentClassLoader 加载,提供给用户应用的 API。
    • runtime:由 AppClassLoader 加载,用于 AREX 运行时的一些性能。
  • arex-instrumentation-foundation`:由 AgentClassLoader 加载,用于加载 AREX 的根底性能,如后端实现等。

这些不同的 ClassLoader 之间具备隔离性,确保了各个组件的独立性和安全性。

其中:

  • AgentClassLoader:AREX 自定义的 ClassLoader。
  • Bootstrap ClassLoader: Java Instrumentation API 是 Java SE 5 中引入的一个功能强大的工具,它容许在运行时批改 Java 类的行为。

    其中,Instrumentation 类是 Java Instrumentation API 的外围类之一,它提供了一些办法来监测和批改 Java 应用程序的运行状态。其中,appendToBootstrapClassLoaderSearch 办法是 Instrumentation 类中的一个办法,它的作用是将指定的 jar 文件增加到 Bootstrap ClassLoader 的搜寻门路中。

    Bootstrap ClassLoader 是 Java 虚拟机中的一个非凡的类加载器,它负责加载 Java 运行时环境中的外围类库,如 java.lang 和 java.util 等。

    通过调用 appendToBootstrapClassLoaderSearch 办法,能够将自定义的类库增加到 Bootstrap ClassLoader 的搜寻门路中,从而使得 Java 应用程序能够应用这些自定义的类库。须要留神的是,因为 appendToBootstrapClassLoaderSearch 办法会批改 Java 虚拟机的运行状态,因而只有具备足够权限的用户能力调用该办法。

  • AppClassLoader 是 Java 应用程序默认的 ClassLoader,它负责加载应用程序的类。AppClassLoader 会从 CLASSPATH 环境变量或者零碎属性 java.class.path 指定的门路中加载类文件。

    如果须要加载的类不在 AppClassLoader 的搜寻门路中,它会委托给父 ClassLoader 进行加载,直到 BootstrapClassLoader 为止。

  • UserClassLoader 用户自定义 ClassLoader, 如 SPIUtil 类中 Load 办法如下获取 ClassLoader 加载

    ClassLoader cl = Thread.currentThread().getContextClassLoader();

正文完
 0