如何并行调试Kotlin纯函数的两组参数执行过程?
并行对比纯函数不同参数执行过程的实现方案
针对你不想修改原函数源码、需要并行对比同一纯函数不同参数执行过程的需求,以下是几种可行的非侵入式实现方案:
方案一:AspectJ编译时织入(推荐)
通过AspectJ的切面能力,在编译阶段自动给目标函数的每一行插入日志逻辑,无需改动原函数代码,最终收集两个线程的执行数据并按行对比。
1. 配置Gradle依赖
plugins { id 'org.jetbrains.kotlin.jvm' version '1.9.20' id 'io.freefair.aspectj.post-compile-weaving' version '8.6' } dependencies { implementation 'org.aspectj:aspectjrt:1.9.20' }
2. 编写切面类
import org.aspectj.lang.JoinPoint import org.aspectj.lang.annotation.After import org.aspectj.lang.annotation.Aspect import org.aspectj.lang.annotation.Pointcut import java.util.concurrent.ConcurrentHashMap @Aspect class FunctionTracer { // 定义切点:匹配目标test1函数的所有执行行 @Pointcut("execution(* test1(..)) && within(你的包名.*)") fun test1Execution() {} // 存储每个线程的行号与变量值映射 private val threadData = ConcurrentHashMap<Thread, MutableMap<Int, String>>() @After("test1Execution() && cflow(execution(* test1(..)))") fun logLineExecution(joinPoint: JoinPoint) { val thread = Thread.currentThread() val lineNum = joinPoint.sourceLocation.line val stackFrame = joinPoint.staticPart.asInstanceOf<org.aspectj.lang.reflect.CodeSignature>().let { sig -> // 通过AspectJ的代码签名获取局部变量表,读取res的值 val localVarIndex = sig.parameterNames.indexOf("res") + sig.parameterCount val localVarValue = joinPoint.args.getOrNull(localVarIndex) "res=$localVarValue" } val lineMap = threadData.computeIfAbsent(thread) { mutableMapOf() } lineMap[lineNum] = stackFrame } // 对外提供获取线程执行数据的方法 fun getThreadData(thread: Thread): Map<Int, String> = threadData[thread] ?: emptyMap() }
3. 并行执行并对比输出
fun main() { val tracer = FunctionTracer() val thread1 = Thread { test1(1) } val thread2 = Thread { test1(0) } thread1.start() thread2.start() thread1.join() thread2.join() // 收集两个线程的执行数据 val data1 = tracer.getThreadData(thread1) val data2 = tracer.getThreadData(thread2) // 按行号排序后对比输出 val allLines = (data1.keys + data2.keys).sorted() allLines.forEach { line -> val val1 = data1[line] ?: "无数据" val val2 = data2[line] ?: "无数据" println("line$line: thread1: $val1 vs thread2: $val2") } }
注意:如果局部变量
res的位置无法通过参数索引获取,可结合ASM工具解析字节码的局部变量表,精准定位变量位置。
方案二:JDI自定义调试器
利用Java Debug Interface(JDI)编写调试客户端,在目标函数的每一行设置断点,实时捕获两个线程的变量值并对比输出。
1. 编写JDI调试代码
import com.sun.jdi.* import com.sun.jdi.connect.AttachingConnector import com.sun.jdi.event.BreakpointEvent import com.sun.jdi.request.BreakpointRequest import java.util.concurrent.CountDownLatch fun main() { // 连接到以调试模式启动的目标JVM val vmManager = Bootstrap.virtualMachineManager() val connector = vmManager.attachingConnectors().find { it.name() == "com.sun.jdi.SocketAttach" } as AttachingConnector val args = connector.defaultArguments().apply { get("hostname").setValue("localhost") get("port").setValue("5005") } val vm = connector.attach(args) // 获取目标类和函数 val targetClass = vm.classesByName("你的包名.你的类名").first() val test1Method = targetClass.methodsByName("test1").first() // 在函数的每一行设置断点 val breakpoints = mutableListOf<BreakpointRequest>() test1Method.allLineLocations().forEach { location -> val bpReq = vm.eventRequestManager().createBreakpointRequest(location) bpReq.setSuspendPolicy(BreakpointRequest.SUSPEND_EVENT_THREAD) bpReq.enable() breakpoints.add(bpReq) } // 存储线程执行数据,用CountDownLatch同步同一行的两个线程 val threadData = mutableMapOf<ThreadReference, MutableMap<Int, String>>() val lineLatchMap = mutableMapOf<Int, CountDownLatch>() // 处理调试事件 while (true) { val eventSet = vm.eventQueue().remove() eventSet.forEach { event -> if (event is BreakpointEvent) { val thread = event.thread() val lineNum = event.location().lineNumber() // 获取当前栈帧的res变量值 val stackFrame = thread.frame(0) val resVar = stackFrame.visibleVariables().find { it.name() == "res" } as LocalVariable val resValue = stackFrame.getValue(resVar).value() // 记录当前线程的行数据 val lineMap = threadData.computeIfAbsent(thread) { mutableMapOf() } lineMap[lineNum] = "res=$resValue" // 同步两个线程的同一行输出 val latch = lineLatchMap.computeIfAbsent(lineNum) { CountDownLatch(2) } latch.countDown() if (latch.count == 0L) { val val1 = threadData.values.first()[lineNum]!! val val2 = threadData.values.last()[lineNum]!! println("line$lineNum: thread1: $val1 vs thread2: $val2") lineLatchMap.remove(lineNum) } eventSet.resume() } } } }
2. 启动目标程序
目标程序需以调试模式启动:
kotlin -J-agentlib:jdwp=transport=dt_socket,server=y,suspend=y,address=5005 你的类名Kt
方案三:ByteBuddy字节码修改
通过ByteBuddy在运行时动态修改目标函数的字节码,插入日志逻辑,同样无需改动原源码:
import net.bytebuddy.ByteBuddy import net.bytebuddy.agent.ByteBuddyAgent import net.bytebuddy.asm.Advice import net.bytebuddy.matcher.ElementMatchers.named import java.util.concurrent.ConcurrentHashMap val threadData = ConcurrentHashMap<Thread, MutableMap<Int, String>>() fun main() { ByteBuddyAgent.install() ByteBuddy() .redefine(你的类名::class.java) .visit(Advice.to(Test1Advice::class.java).on(named("test1"))) .make() .load(你的类名::class.java.classLoader) // 并行执行并对比输出(同方案一的main逻辑) } class Test1Advice { @Advice.OnMethodEnter fun enter(@Advice.Origin line: String) { val lineNum = line.substringAfterLast(':').toInt() // 初始化行记录 threadData.computeIfAbsent(Thread.currentThread()) { mutableMapOf() }[lineNum] = "" } @Advice.OnMethodExit(onThrowable = Throwable::class) fun exit(@Advice.Local("res") res: Int, @Advice.Origin line: String) { val lineNum = line.substringAfterLast(':').toInt() threadData[Thread.currentThread()]!![lineNum] = "res=$res" } }
内容的提问来源于stack exchange,提问作者A-_-S
相关产品推荐
相关产品推荐

