Byteman 在故障测试中有广泛应用,我第一次接触它是在 Chaos Mesh 平台上,之前也写过一些相关文章。不过,正如我之前提到的,Chaos Mesh 对 Byteman 的开发支持不到 30%。今天我分享的内容是 Byteman 的另一个用法:调用第三方类的方法。
这听起来可能和故障测试关系不大,但其实 Byteman 的功能设计中,DO 执行模块是可以用来执行方法的,这为我们提供了一个很好的切入点。尽管这个需求的解决方案不一定是最优的,但在你身处 Chaos Mesh 平台,并且需要操作多个节点时,这种方法能省去不少事。
我们的需求是服务启动后,需要调用某个类的静态方法,来完成数据初始化,甚至是周期性任务的调度。看起来这个需求和故障测试没有直接关系,但 Byteman 提供的能力恰恰能帮我们解决这个问题。通过这种方法,我们可以在不修改核心代码的情况下,实现特定的方法调用。
Byteman 本身需要一个触发点来执行注入代码。Chaos Agent 提供了一个异步线程,循环执行 org.chaos_mesh.chaos_agent.TriggerThread#triggerFunc 方法,我们可以把它当做全局注入点来用。这里的核心思想是,能够通过定时触发某些代码的执行,而不是每次都手动干预。
以下是 Chaos Mesh 项目中的源代码,展示了如何实现异步线程:
// Copyright 2022 Chaos Mesh Authors.
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// See the License for the specific language governing permissions and
// limitations under the License.
package org.chaos_mesh.chaos_agent;
publicclass TriggerThread extends Thread {
public void run(){
loop();
}
public static void loop() {
while (true)
{
try {
Thread.sleep(5000);
} catch (Exception e) {
System.out.println(e.getMessage());
}
triggerFunc();
}
}
public static void triggerFunc()
{
//System.out.println("chaos agent triger function");
}
}
接下来,我们进入真正的重点:如何调用第三方类的方法。为了演示,我使用了静态方法作为案例。需要注意,这里的“第三方”指的是除了 Byteman 和 Chaos Agent 注入点以外的类,比如一些 Java 类库的静态方法,可以直接调用,但不在本次讨论范围内。
以下是我为此编写的一个简单示例:
package com.funtest.temp;
publicclass BytemanDemo {
public static void main(String[] args) {
while (true) {
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
thrownew RuntimeException(e);
}
print(234);
}
}
static int print(int i) {
int a = i;
System.out.println("Hello Word from Byteman ,By FunTester !!!");
return a * a;
}
public static void pp() {
System.out.println("33333333");
}
}
`` 如果我们要调用某个类的方法,使用反射是最直接的方式:
Class.forName("com.funtest.temp.BytemanDemo").getDeclaredMethod("pp").invoke(null);
事实上,以上代码可以直接执行,但在 Byteman 的 btm 文件中会报错。我猜测是由于 Byteman 使用了 java_cup 解析器,导致与反射的兼容性问题。Java CUP(构造有用的解析器)用于生成 LALR(1) 解析器,它类似于 GNU 的 Bison 或 Yacc。虽然反射代码本身没有问题,但与 Byteman 一起使用时出现了兼容性障碍。
经过一番尝试,我灵机一动,使用 ClassLoader 来加载类,从而解决了问题。代码如下:
ClassLoader contextClassLoader = Thread.currentThread().getContextClassLoader();
contextClassLoader.loadClass("com.funtest.temp.BytemanDemo").getDeclaredMethod("pp").invoke(null);
这一行代码确实有效,但在 Byteman 的 btm 文件中依旧报错。仔细查看报错信息后,我发现了一些线索,最终的 btm 文件如下:
RULE testent
CLASS com.funtest.temp.BytemanDemo
METHOD print
BIND buffer = ClassLoader.getSystemClassLoader().loadClass("com.funtest.temp.BytemanDemo");
m = buffer.getDeclaredMethod("pp", new Class[0]);
AT ENTRY
IF TRUE
DO System.out.println("Hello Word,FunTester");
m.invoke(null,null);
ENDRULE
最终的控制台打印信息如下:
TransformListener() : handling connection on port 9091
retransforming com.funtest.temp.BytemanDemo
org.jboss.byteman.agent.Transformer : possible trigger for rule testent in class com.funtest.temp.BytemanDemo
RuleTriggerMethodAdapter.injectTriggerPoint : insertingtriggerintocom.funtest.temp.BytemanDemo.print(int) intforruletestent
org.jboss.byteman.agent.Transformer : insertedtriggerfortestentinclasscom.funtest.temp.BytemanDemo
Rule.executecalledfortestent_1:0
testentexecute
HelloWord
33333333
HelloWordfromByteman ,ByFunTester !!!
Rule.executecalledfortestent_1:0
testentexecute
HelloWord
33333333
HelloWordfromByteman ,ByFunTester !!!
Rule.executecalledfortestent_1:0
虽然这只是一个粗略的示例,目的是为了演示如何实现功能。实际上,可以对其进行一些优化,避免每次都重复加载类,特别是在 Spring Boot 项目中,可以通过优化加载流程避免不必要的性能开销。
实际上,这个需求的最佳解决方法是定制一个 helper 类,来扩展 Byteman 的原生功能,提供一个专门的方法来调用第三方类的方法(包括类方法、成员方法,甚至构造方法)。虽然 Byteman 的使用文档没有详细讲解这一块,但未来我会有机会分享更多的优化方案。
通过 Byteman,我们不仅能进行故障注入,还能灵活地执行各种操作,帮助我们在复杂的系统环境中执行自动化任务。
FunTesterFunTester 原创精华