From ba7da1c1b8e5fad0a3d524153e26a9773b8f345e Mon Sep 17 00:00:00 2001 From: "isadliliying@163.com" Date: Thu, 20 Jun 2024 19:35:27 +0800 Subject: [PATCH 1/2] line command implement --- .../grpcweb/grpc/service/advisor/SpyImpl.java | 24 + .../arthas/core/advisor/AccessPoint.java | 3 +- .../taobao/arthas/core/advisor/Advice.java | 29 + .../arthas/core/advisor/AdviceListener.java | 16 + .../core/advisor/AdviceListenerAdapter.java | 38 +- .../core/advisor/AdviceListenerManager.java | 89 ++- .../taobao/arthas/core/advisor/Enhancer.java | 213 +++--- .../taobao/arthas/core/advisor/SpyImpl.java | 29 + .../arthas/core/advisor/SpyInterceptors.java | 30 + .../core/command/BuiltinCommandPack.java | 17 +- .../core/command/klass100/JadCommand.java | 32 +- .../arthas/core/command/model/LineModel.java | 74 +++ .../command/monitor200/EnhancerCommand.java | 16 +- .../monitor200/LineAdviceListener.java | 100 +++ .../core/command/monitor200/LineCommand.java | 182 ++++++ .../core/command/monitor200/LineHelper.java | 606 ++++++++++++++++++ .../arthas/core/command/view/LineView.java | 24 + .../core/command/view/ResultViewResolver.java | 1 + .../taobao/arthas/core/util/EncryptUtils.java | 48 ++ .../taobao/arthas/core/util/StringUtils.java | 17 + .../arthas/core/advisor/EnhancerTest.java | 154 ++++- spy/src/main/java/java/arthas/SpyAPI.java | 40 ++ 22 files changed, 1662 insertions(+), 120 deletions(-) create mode 100644 core/src/main/java/com/taobao/arthas/core/command/model/LineModel.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/monitor200/LineAdviceListener.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/monitor200/LineCommand.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/monitor200/LineHelper.java create mode 100644 core/src/main/java/com/taobao/arthas/core/command/view/LineView.java create mode 100644 core/src/main/java/com/taobao/arthas/core/util/EncryptUtils.java diff --git a/arthas-grpc-web-proxy/src/main/java/com/taobao/arthas/grpcweb/grpc/service/advisor/SpyImpl.java b/arthas-grpc-web-proxy/src/main/java/com/taobao/arthas/grpcweb/grpc/service/advisor/SpyImpl.java index 99f22e49f76..54bab7f4c47 100644 --- a/arthas-grpc-web-proxy/src/main/java/com/taobao/arthas/grpcweb/grpc/service/advisor/SpyImpl.java +++ b/arthas-grpc-web-proxy/src/main/java/com/taobao/arthas/grpcweb/grpc/service/advisor/SpyImpl.java @@ -175,6 +175,30 @@ public void atInvokeException(Class clazz, String invokeInfo, Object target, } } + @Override + public void atLine(Class clazz, String methodInfo, Object target, Object[] args, String line, Object[] vars, String[] varNames) { + ClassLoader classLoader = clazz.getClassLoader(); + + String[] info = StringUtils.splitMethodInfo(methodInfo); + String methodName = info[0]; + String methodDesc = info[1]; + + List listeners = com.taobao.arthas.core.advisor.AdviceListenerManager.queryLineAdviceListeners(classLoader, clazz.getName(), line, + methodName, methodDesc); + if (listeners != null) { + for (AdviceListener adviceListener : listeners) { + try { + if (skipAdviceListener(adviceListener)) { + continue; + } + adviceListener.atLine(clazz, methodName, methodDesc, target, args, line, vars, varNames); + } catch (Throwable e) { + logger.error("class: {}, methodInfo: {}, line: {}", clazz.getName(), methodInfo, line, e); + } + } + } + } + private static boolean skipAdviceListener(AdviceListener adviceListener) { if (adviceListener instanceof ProcessAware) { ProcessAware processAware = (ProcessAware) adviceListener; diff --git a/core/src/main/java/com/taobao/arthas/core/advisor/AccessPoint.java b/core/src/main/java/com/taobao/arthas/core/advisor/AccessPoint.java index 1d361c4edcf..ccdeda524c9 100644 --- a/core/src/main/java/com/taobao/arthas/core/advisor/AccessPoint.java +++ b/core/src/main/java/com/taobao/arthas/core/advisor/AccessPoint.java @@ -1,7 +1,8 @@ package com.taobao.arthas.core.advisor; public enum AccessPoint { - ACCESS_BEFORE(1, "AtEnter"), ACCESS_AFTER_RETUNING(1 << 1, "AtExit"), ACCESS_AFTER_THROWING(1 << 2, "AtExceptionExit"); + + ACCESS_BEFORE(1, "AtEnter"), ACCESS_AFTER_RETUNING(1 << 1, "AtExit"), ACCESS_AFTER_THROWING(1 << 2, "AtExceptionExit"), ACCESS_AT_LINE(1 << 3, "atLine"); private int value; diff --git a/core/src/main/java/com/taobao/arthas/core/advisor/Advice.java b/core/src/main/java/com/taobao/arthas/core/advisor/Advice.java index 51f31b9534f..107ee78f0c6 100644 --- a/core/src/main/java/com/taobao/arthas/core/advisor/Advice.java +++ b/core/src/main/java/com/taobao/arthas/core/advisor/Advice.java @@ -1,5 +1,7 @@ package com.taobao.arthas.core.advisor; +import java.util.Map; + /** * 通知点 Created by vlinux on 15/5/20. */ @@ -12,6 +14,7 @@ public class Advice { private final Object[] params; private final Object returnObj; private final Throwable throwExp; + private final Map varMap; private final boolean isBefore; private final boolean isThrow; private final boolean isReturn; @@ -66,6 +69,7 @@ public ArthasMethod getMethod() { * @param params 调用参数 * @param returnObj 返回值 * @param throwExp 抛出异常 + * @param varMap 变量的Map * @param access 进入场景 */ private Advice( @@ -76,6 +80,7 @@ private Advice( Object[] params, Object returnObj, Throwable throwExp, + Map varMap, int access) { this.loader = loader; this.clazz = clazz; @@ -84,6 +89,7 @@ private Advice( this.params = params; this.returnObj = returnObj; this.throwExp = throwExp; + this.varMap = varMap; isBefore = (access & AccessPoint.ACCESS_BEFORE.getValue()) == AccessPoint.ACCESS_BEFORE.getValue(); isThrow = (access & AccessPoint.ACCESS_AFTER_THROWING.getValue()) == AccessPoint.ACCESS_AFTER_THROWING.getValue(); isReturn = (access & AccessPoint.ACCESS_AFTER_RETUNING.getValue()) == AccessPoint.ACCESS_AFTER_RETUNING.getValue(); @@ -102,6 +108,7 @@ public static Advice newForBefore(ClassLoader loader, params, null, //returnObj null, //throwExp + null, //varMap AccessPoint.ACCESS_BEFORE.getValue() ); } @@ -120,6 +127,7 @@ public static Advice newForAfterReturning(ClassLoader loader, params, returnObj, null, //throwExp + null, //varMap AccessPoint.ACCESS_AFTER_RETUNING.getValue() ); } @@ -138,9 +146,30 @@ public static Advice newForAfterThrowing(ClassLoader loader, params, null, //returnObj throwExp, + null, //varMap AccessPoint.ACCESS_AFTER_THROWING.getValue() ); } + public static Advice newForLine(ClassLoader loader, + Class clazz, + ArthasMethod method, + Object target, + Object[] params, + Map varMap) { + return new Advice( + loader, + clazz, + method, + target, + params, + null, //returnObj + null, //throwExp + varMap, + AccessPoint.ACCESS_AT_LINE.getValue() + ); + + } + } diff --git a/core/src/main/java/com/taobao/arthas/core/advisor/AdviceListener.java b/core/src/main/java/com/taobao/arthas/core/advisor/AdviceListener.java index f9e24980014..8cb365cf719 100644 --- a/core/src/main/java/com/taobao/arthas/core/advisor/AdviceListener.java +++ b/core/src/main/java/com/taobao/arthas/core/advisor/AdviceListener.java @@ -70,4 +70,20 @@ void afterThrowing( Object target, Object[] args, Throwable throwable) throws Throwable; + /** + * 行观测的监听回调方法 + * line 命令中使用,查看本地变量等 + * + * @param clazz 类 + * @param methodName 方法名 + * @param methodDesc 方法描述 + * @param target 目标类实例,若目标为静态方法,则为null + * @param args 参数列表 + * @param line 行标识,可能是行号(LineNumber),也可能是行的特殊标号(LineCode) + * @param vars 本地变量数组 + * @param varNames 本地变量名数组 + * @throws Throwable 通知过程出错 + */ + void atLine(Class clazz, String methodName, String methodDesc, Object target, Object[] args, String line, Object[] vars, String[] varNames) throws Throwable; + } diff --git a/core/src/main/java/com/taobao/arthas/core/advisor/AdviceListenerAdapter.java b/core/src/main/java/com/taobao/arthas/core/advisor/AdviceListenerAdapter.java index 724414d4373..8d2fb8c2d0b 100644 --- a/core/src/main/java/com/taobao/arthas/core/advisor/AdviceListenerAdapter.java +++ b/core/src/main/java/com/taobao/arthas/core/advisor/AdviceListenerAdapter.java @@ -1,15 +1,17 @@ package com.taobao.arthas.core.advisor; -import java.util.concurrent.atomic.AtomicLong; - import com.taobao.arthas.core.command.express.ExpressException; import com.taobao.arthas.core.command.express.ExpressFactory; +import com.taobao.arthas.core.command.monitor200.LineHelper; import com.taobao.arthas.core.shell.command.CommandProcess; import com.taobao.arthas.core.shell.system.Process; import com.taobao.arthas.core.shell.system.ProcessAware; import com.taobao.arthas.core.util.Constants; import com.taobao.arthas.core.util.StringUtils; +import java.util.Map; +import java.util.concurrent.atomic.AtomicLong; + /** * * @author hengyunabc 2020-05-20 @@ -106,6 +108,38 @@ public abstract void afterReturning(ClassLoader loader, Class clazz, ArthasMe public abstract void afterThrowing(ClassLoader loader, Class clazz, ArthasMethod method, Object target, Object[] args, Throwable throwable) throws Throwable; + + /** + * 行观测的监听回调方法 + * line 命令中使用,查看本地变量等 + * + * @param clazz 类 + * @param methodName 方法名 + * @param methodDesc 方法描述 + * @param target 目标类实例,若目标为静态方法,则为null + * @param args 参数列表 + * @param line 行标识,可能是行号(LineNumber),也可能是行的特殊标号(LineCode) + * @param vars 本地变量数组 + * @param varNames 本地变量名数组 + * @throws Throwable 通知过程出错 + */ + @Override + public void atLine(Class clazz, String methodName, String methodDesc, Object target, Object[] args, String line, Object[] vars, String[] varNames) throws Throwable { + Map varMap = LineHelper.buildVarMap(vars, varNames); + Advice advice = Advice.newForLine(clazz.getClassLoader(), clazz, new ArthasMethod(clazz, methodName, methodDesc), target, args, varMap); + atLine(advice, line); + } + + + /** + * line命令中使用,在行间观测 + * 主要用于查看本地变量 + */ + public void atLine(Advice advice, String line) throws Throwable { + //doing nothing by default + + } + /** * 判断条件是否满足,满足的情况下需要输出结果 * diff --git a/core/src/main/java/com/taobao/arthas/core/advisor/AdviceListenerManager.java b/core/src/main/java/com/taobao/arthas/core/advisor/AdviceListenerManager.java index 13c1896c4b5..7daf7c19cfb 100644 --- a/core/src/main/java/com/taobao/arthas/core/advisor/AdviceListenerManager.java +++ b/core/src/main/java/com/taobao/arthas/core/advisor/AdviceListenerManager.java @@ -1,11 +1,5 @@ package com.taobao.arthas.core.advisor; -import java.util.ArrayList; -import java.util.List; -import java.util.Map.Entry; -import java.util.concurrent.ConcurrentHashMap; -import java.util.concurrent.TimeUnit; - import com.alibaba.arthas.deps.org.slf4j.Logger; import com.alibaba.arthas.deps.org.slf4j.LoggerFactory; import com.taobao.arthas.common.concurrent.ConcurrentWeakKeyHashMap; @@ -14,6 +8,12 @@ import com.taobao.arthas.core.shell.system.Process; import com.taobao.arthas.core.shell.system.ProcessAware; +import java.util.ArrayList; +import java.util.List; +import java.util.Map.Entry; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.TimeUnit; + /** * * TODO line 的记录 listener方式? 还是有string为key,不过 classname|method|desc|num 这样子? @@ -111,6 +111,14 @@ private String keyForTrace(String className, String owner, String methodName, St return className + owner + methodName + methodDesc; } + /** + * 获取行观测对应的key + * @param line line 行标识,可能是行号(LineNumber),也可能是行的特殊标号(LineCode) + */ + private String keyForLine(String className, String line, String methodName, String methodDesc) { + return className + line + methodName + methodDesc; + } + public void registerAdviceListener(String className, String methodName, String methodDesc, AdviceListener listener) { synchronized (this) { @@ -162,6 +170,40 @@ public List queryTraceAdviceListeners(String className, String o return listeners; } + + /** + * 注册对应行的Listener + * @param line line 行标识,可能是行号(LineNumber),也可能是行的特殊标号(LineCode) + */ + public void registerLineAdviceListener(String className, String line, String methodName, String methodDesc, + AdviceListener listener) { + + className = className.replace('/', '.'); + String key = keyForLine(className, line, methodName, methodDesc); + + List listeners = map.get(key); + if (listeners == null) { + listeners = new ArrayList(); + map.put(key, listeners); + } + if (!listeners.contains(listener)) { + listeners.add(listener); + } + } + + /** + * 查询对应行的Listener + * @param line line 行标识,可能是行号(LineNumber),也可能是行的特殊标号(LineCode) + */ + public List queryLineAdviceListeners(String className, String line, String methodName, + String methodDesc) { + className = className.replace('/', '.'); + String key = keyForLine(className, line, methodName, methodDesc); + + List listeners = map.get(key); + + return listeners; + } } public static void registerAdviceListener(ClassLoader classLoader, String className, String methodName, @@ -222,6 +264,41 @@ public static List queryTraceAdviceListeners(ClassLoader classLo return null; } + /** + * 注册对应行的Listener + * @param line line 行标识,可能是行号(LineNumber),也可能是行的特殊标号(LineCode) + */ + public static void registerLineAdviceListener(ClassLoader classLoader, String className, String line, + String methodName, String methodDesc, AdviceListener listener) { + classLoader = wrap(classLoader); + className = className.replace('/', '.'); + + ClassLoaderAdviceListenerManager manager = adviceListenerMap.get(classLoader); + + if (manager == null) { + manager = new ClassLoaderAdviceListenerManager(); + adviceListenerMap.put(classLoader, manager); + } + manager.registerLineAdviceListener(className, line, methodName, methodDesc, listener); + } + + /** + * 查询对应行的Listener + * @param line line 行标识,可能是行号(LineNumber),也可能是行的特殊标号(LineCode) + */ + public static List queryLineAdviceListeners(ClassLoader classLoader, String className, + String line, String methodName, String methodDesc) { + classLoader = wrap(classLoader); + className = className.replace('/', '.'); + ClassLoaderAdviceListenerManager manager = adviceListenerMap.get(classLoader); + + if (manager != null) { + return manager.queryLineAdviceListeners(className, line, methodName, methodDesc); + } + + return null; + } + private static ClassLoader wrap(ClassLoader classLoader) { if (classLoader != null) { return classLoader; diff --git a/core/src/main/java/com/taobao/arthas/core/advisor/Enhancer.java b/core/src/main/java/com/taobao/arthas/core/advisor/Enhancer.java index 7a0ecc0f5b1..4ad7e9a8d31 100644 --- a/core/src/main/java/com/taobao/arthas/core/advisor/Enhancer.java +++ b/core/src/main/java/com/taobao/arthas/core/advisor/Enhancer.java @@ -53,6 +53,7 @@ import com.taobao.arthas.core.advisor.SpyInterceptors.SpyTraceInterceptor1; import com.taobao.arthas.core.advisor.SpyInterceptors.SpyTraceInterceptor2; import com.taobao.arthas.core.advisor.SpyInterceptors.SpyTraceInterceptor3; +import com.taobao.arthas.core.command.monitor200.LineHelper; import com.taobao.arthas.core.server.ArthasBootstrap; import com.taobao.arthas.core.util.ArthasCheckUtils; import com.taobao.arthas.core.util.ClassUtils; @@ -72,6 +73,13 @@ public class Enhancer implements ClassFileTransformer { private final AdviceListener listener; private final boolean isTracing; private final boolean skipJDKTrace; + + /** + * line命令使用的行标识,可能是行号(LineNumber),也可能是行的特殊标号(LineCode) + * 若为line命令,则该字段不为null + */ + private String line; + private final Matcher classNameMatcher; private final Matcher classNameExcludeMatcher; private final Matcher methodNameMatcher; @@ -90,16 +98,18 @@ public class Enhancer implements ClassFileTransformer { /** * @param adviceId 通知编号 * @param isTracing 可跟踪方法调用 + * @param line 行标识,可能是行号(LineNumber),也可能是行的特殊标号(LineCode),如果为空,则说明不是line命令 * @param skipJDKTrace 是否忽略对JDK内部方法的跟踪 * @param matchingClasses 匹配中的类 * @param methodNameMatcher 方法名匹配 * @param affect 影响统计 */ - public Enhancer(AdviceListener listener, boolean isTracing, boolean skipJDKTrace, Matcher classNameMatcher, + public Enhancer(AdviceListener listener, boolean isTracing, boolean skipJDKTrace, String line, Matcher classNameMatcher, Matcher classNameExcludeMatcher, Matcher methodNameMatcher) { this.listener = listener; this.isTracing = isTracing; + this.line = line; this.skipJDKTrace = skipJDKTrace; this.classNameMatcher = classNameMatcher; this.classNameExcludeMatcher = classNameExcludeMatcher; @@ -135,27 +145,6 @@ public byte[] transform(final ClassLoader inClassLoader, String className, Class // remove JSR https://github.com/alibaba/arthas/issues/1304 classNode = AsmUtils.removeJSRInstructions(classNode); - // 生成增强字节码 - DefaultInterceptorClassParser defaultInterceptorClassParser = new DefaultInterceptorClassParser(); - - final List interceptorProcessors = new ArrayList(); - - interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyInterceptor1.class)); - interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyInterceptor2.class)); - interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyInterceptor3.class)); - - if (this.isTracing) { - if (!this.skipJDKTrace) { - interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyTraceInterceptor1.class)); - interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyTraceInterceptor2.class)); - interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyTraceInterceptor3.class)); - } else { - interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyTraceExcludeJDKInterceptor1.class)); - interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyTraceExcludeJDKInterceptor2.class)); - interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyTraceExcludeJDKInterceptor3.class)); - } - } - List matchedMethods = new ArrayList(); for (MethodNode methodNode : classNode.methods) { if (!isIgnore(methodNode, methodNameMatcher)) { @@ -172,80 +161,132 @@ public byte[] transform(final ClassLoader inClassLoader, String className, Class } } - // 用于检查是否已插入了 spy函数,如果已有则不重复处理 - GroupLocationFilter groupLocationFilter = new GroupLocationFilter(); - - LocationFilter enterFilter = new InvokeContainLocationFilter(Type.getInternalName(SpyAPI.class), "atEnter", - LocationType.ENTER); - LocationFilter existFilter = new InvokeContainLocationFilter(Type.getInternalName(SpyAPI.class), "atExit", - LocationType.EXIT); - LocationFilter exceptionFilter = new InvokeContainLocationFilter(Type.getInternalName(SpyAPI.class), - "atExceptionExit", LocationType.EXCEPTION_EXIT); - - groupLocationFilter.addFilter(enterFilter); - groupLocationFilter.addFilter(existFilter); - groupLocationFilter.addFilter(exceptionFilter); - - LocationFilter invokeBeforeFilter = new InvokeCheckLocationFilter(Type.getInternalName(SpyAPI.class), - "atBeforeInvoke", LocationType.INVOKE); - LocationFilter invokeAfterFilter = new InvokeCheckLocationFilter(Type.getInternalName(SpyAPI.class), - "atInvokeException", LocationType.INVOKE_COMPLETED); - LocationFilter invokeExceptionFilter = new InvokeCheckLocationFilter(Type.getInternalName(SpyAPI.class), - "atInvokeException", LocationType.INVOKE_EXCEPTION_EXIT); - groupLocationFilter.addFilter(invokeBeforeFilter); - groupLocationFilter.addFilter(invokeAfterFilter); - groupLocationFilter.addFilter(invokeExceptionFilter); - - for (MethodNode methodNode : matchedMethods) { - if (AsmUtils.isNative(methodNode)) { - logger.info("ignore native method: {}", - AsmUtils.methodDeclaration(Type.getObjectType(classNode.name), methodNode)); - continue; + // 判断是否为line命令 + boolean isLineCommand = line != null; + //line 的增强是只针对到具体的行,跟以往的 watch、trace 有所差异,所以分离开来,写到一起会比较混乱 + if (isLineCommand) { + //手动创建一个 line命令 使用的 InterceptorProcessor + InterceptorProcessor lookInterceptorProcessor = LineHelper.createLineInterceptorProcessor(this.line); + + // 用于检查是否已插入了 spy函数,如果已有则不重复处理 + GroupLocationFilter groupLocationFilter = new GroupLocationFilter(); + LocationFilter locationLineFilter = new InvokeCheckLocationFilter(Type.getInternalName(SpyAPI.class), + "atLineNumber", LocationType.LINE); + LocationFilter locationCodeFilter = new InvokeCheckLocationFilter(Type.getInternalName(SpyAPI.class), + "atLineCode", LocationType.USER_DEFINE); + groupLocationFilter.addFilter(locationLineFilter); + groupLocationFilter.addFilter(locationCodeFilter); + + for (MethodNode methodNode : matchedMethods) { + MethodProcessor methodProcessorWithoutFilter = new MethodProcessor(classNode, methodNode); + // 这里先看看不过滤的时候有多少命中的Location,因为process方法会过滤掉已经被增强的Location,没有Location返回可能是因为Location已经被增强,或者是真的没有匹配的Location + List matchLocations = lookInterceptorProcessor.getLocationMatcher().match(methodProcessorWithoutFilter); + if (!matchLocations.isEmpty()) { + try { + // 有Location时候则进行增强 + MethodProcessor methodProcessor = new MethodProcessor(classNode, methodNode, groupLocationFilter); + lookInterceptorProcessor.process(methodProcessor); + + // 添加Listener + AdviceListenerManager.registerLineAdviceListener(inClassLoader, className, + this.line, methodNode.name, methodNode.desc, listener); + affect.addMethodAndCount(inClassLoader, className, methodNode.name, methodNode.desc); + } catch (Throwable e) { + logger.error("line command enhancer error, class: {}, method: {}, line: {}, interceptor: {}", classNode.name, methodNode.name, this.line, lookInterceptorProcessor.getClass().getName(), e); + } + } + } + } else { + //常规的watch、trace的增强逻辑 + final List interceptorProcessors = new ArrayList(); + + // 生成增强字节码 + DefaultInterceptorClassParser defaultInterceptorClassParser = new DefaultInterceptorClassParser(); + // 常用的进入方法、退出方法、抛出异常 + interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyInterceptor1.class)); + interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyInterceptor2.class)); + interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyInterceptor3.class)); + if (this.isTracing) { + // trace 才会使用到的 invoke + if (!this.skipJDKTrace) { + interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyTraceInterceptor1.class)); + interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyTraceInterceptor2.class)); + interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyTraceInterceptor3.class)); + } else { + interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyTraceExcludeJDKInterceptor1.class)); + interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyTraceExcludeJDKInterceptor2.class)); + interceptorProcessors.addAll(defaultInterceptorClassParser.parse(SpyTraceExcludeJDKInterceptor3.class)); + } } - // 先查找是否有 atBeforeInvoke 函数,如果有,则说明已经有trace了,则直接不再尝试增强,直接插入 listener - if(AsmUtils.containsMethodInsnNode(methodNode, Type.getInternalName(SpyAPI.class), "atBeforeInvoke")) { - for (AbstractInsnNode insnNode = methodNode.instructions.getFirst(); insnNode != null; insnNode = insnNode - .getNext()) { - if (insnNode instanceof MethodInsnNode) { - final MethodInsnNode methodInsnNode = (MethodInsnNode) insnNode; - if(this.skipJDKTrace) { - if(methodInsnNode.owner.startsWith("java/")) { + + // 用于检查是否已插入了 spy函数,如果已有则不重复处理 + GroupLocationFilter groupLocationFilter = new GroupLocationFilter(); + LocationFilter enterFilter = new InvokeContainLocationFilter(Type.getInternalName(SpyAPI.class), "atEnter", + LocationType.ENTER); + LocationFilter existFilter = new InvokeContainLocationFilter(Type.getInternalName(SpyAPI.class), "atExit", + LocationType.EXIT); + LocationFilter exceptionFilter = new InvokeContainLocationFilter(Type.getInternalName(SpyAPI.class), + "atExceptionExit", LocationType.EXCEPTION_EXIT); + + groupLocationFilter.addFilter(enterFilter); + groupLocationFilter.addFilter(existFilter); + groupLocationFilter.addFilter(exceptionFilter); + + LocationFilter invokeBeforeFilter = new InvokeCheckLocationFilter(Type.getInternalName(SpyAPI.class), + "atBeforeInvoke", LocationType.INVOKE); + LocationFilter invokeAfterFilter = new InvokeCheckLocationFilter(Type.getInternalName(SpyAPI.class), + "atInvokeException", LocationType.INVOKE_COMPLETED); + LocationFilter invokeExceptionFilter = new InvokeCheckLocationFilter(Type.getInternalName(SpyAPI.class), + "atInvokeException", LocationType.INVOKE_EXCEPTION_EXIT); + + groupLocationFilter.addFilter(invokeBeforeFilter); + groupLocationFilter.addFilter(invokeAfterFilter); + groupLocationFilter.addFilter(invokeExceptionFilter); + + for (MethodNode methodNode : matchedMethods) { + // 先查找是否有 atBeforeInvoke 函数,如果有,则说明已经有trace了,则直接不再尝试增强,直接插入 listener + if(AsmUtils.containsMethodInsnNode(methodNode, Type.getInternalName(SpyAPI.class), "atBeforeInvoke")) { + for (AbstractInsnNode insnNode = methodNode.instructions.getFirst(); insnNode != null; insnNode = insnNode + .getNext()) { + if (insnNode instanceof MethodInsnNode) { + final MethodInsnNode methodInsnNode = (MethodInsnNode) insnNode; + if(this.skipJDKTrace) { + if(methodInsnNode.owner.startsWith("java/")) { + continue; + } + } + // 原始类型的box类型相关的都跳过 + if(AsmOpUtils.isBoxType(Type.getObjectType(methodInsnNode.owner))) { continue; } + AdviceListenerManager.registerTraceAdviceListener(inClassLoader, className, + methodInsnNode.owner, methodInsnNode.name, methodInsnNode.desc, listener); } - // 原始类型的box类型相关的都跳过 - if(AsmOpUtils.isBoxType(Type.getObjectType(methodInsnNode.owner))) { - continue; - } - AdviceListenerManager.registerTraceAdviceListener(inClassLoader, className, - methodInsnNode.owner, methodInsnNode.name, methodInsnNode.desc, listener); } - } - }else { - MethodProcessor methodProcessor = new MethodProcessor(classNode, methodNode, groupLocationFilter); - for (InterceptorProcessor interceptor : interceptorProcessors) { - try { - List locations = interceptor.process(methodProcessor); - for (Location location : locations) { - if (location instanceof MethodInsnNodeWare) { - MethodInsnNodeWare methodInsnNodeWare = (MethodInsnNodeWare) location; - MethodInsnNode methodInsnNode = methodInsnNodeWare.methodInsnNode(); - - AdviceListenerManager.registerTraceAdviceListener(inClassLoader, className, - methodInsnNode.owner, methodInsnNode.name, methodInsnNode.desc, listener); + }else { + MethodProcessor methodProcessor = new MethodProcessor(classNode, methodNode, groupLocationFilter); + for (InterceptorProcessor interceptor : interceptorProcessors) { + try { + List locations = interceptor.process(methodProcessor); + for (Location location : locations) { + if (location instanceof MethodInsnNodeWare) { + MethodInsnNodeWare methodInsnNodeWare = (MethodInsnNodeWare) location; + MethodInsnNode methodInsnNode = methodInsnNodeWare.methodInsnNode(); + AdviceListenerManager.registerTraceAdviceListener(inClassLoader, className, + methodInsnNode.owner, methodInsnNode.name, methodInsnNode.desc, listener); + } } + } catch (Throwable e) { + logger.error("enhancer error, class: {}, method: {}, interceptor: {}", classNode.name, methodNode.name, interceptor.getClass().getName(), e); } - - } catch (Throwable e) { - logger.error("enhancer error, class: {}, method: {}, interceptor: {}", classNode.name, methodNode.name, interceptor.getClass().getName(), e); } } - } - // enter/exist 总是要插入 listener - AdviceListenerManager.registerAdviceListener(inClassLoader, className, methodNode.name, methodNode.desc, - listener); - affect.addMethodAndCount(inClassLoader, className, methodNode.name, methodNode.desc); + // 这里才处理像watch的这种,enter/exist 总是要插入 listener + AdviceListenerManager.registerAdviceListener(inClassLoader, className, methodNode.name, methodNode.desc, + listener); + affect.addMethodAndCount(inClassLoader, className, methodNode.name, methodNode.desc); + } } // https://github.com/alibaba/arthas/issues/1223 , V1_5 的major version是49 diff --git a/core/src/main/java/com/taobao/arthas/core/advisor/SpyImpl.java b/core/src/main/java/com/taobao/arthas/core/advisor/SpyImpl.java index 8cf54962326..eadc5b82a1e 100644 --- a/core/src/main/java/com/taobao/arthas/core/advisor/SpyImpl.java +++ b/core/src/main/java/com/taobao/arthas/core/advisor/SpyImpl.java @@ -174,6 +174,35 @@ public void atInvokeException(Class clazz, String invokeInfo, Object target, } } + /** + * 在某行进行观测 + * + * @param line 行标识,可能是行号(LineNumber),也可能是行的特殊标号(LineCode) + */ + @Override + public void atLine(Class clazz, String methodInfo, Object target, Object[] args, String line, Object[] vars, String[] varNames) { + ClassLoader classLoader = clazz.getClassLoader(); + + String[] info = StringUtils.splitMethodInfo(methodInfo); + String methodName = info[0]; + String methodDesc = info[1]; + + List listeners = AdviceListenerManager.queryLineAdviceListeners(classLoader, clazz.getName(), line, + methodName, methodDesc); + if (listeners != null) { + for (AdviceListener adviceListener : listeners) { + try { + if (skipAdviceListener(adviceListener)) { + continue; + } + adviceListener.atLine(clazz, methodName, methodDesc, target, args, line, vars, varNames); + } catch (Throwable e) { + logger.error("class: {}, methodInfo: {}", clazz.getName(), methodInfo, e); + } + } + } + } + private static boolean skipAdviceListener(AdviceListener adviceListener) { if (adviceListener instanceof ProcessAware) { ProcessAware processAware = (ProcessAware) adviceListener; diff --git a/core/src/main/java/com/taobao/arthas/core/advisor/SpyInterceptors.java b/core/src/main/java/com/taobao/arthas/core/advisor/SpyInterceptors.java index 027d1044dba..bc9338a1ded 100644 --- a/core/src/main/java/com/taobao/arthas/core/advisor/SpyInterceptors.java +++ b/core/src/main/java/com/taobao/arthas/core/advisor/SpyInterceptors.java @@ -8,6 +8,7 @@ import com.alibaba.bytekit.asm.interceptor.annotation.AtExit; import com.alibaba.bytekit.asm.interceptor.annotation.AtInvoke; import com.alibaba.bytekit.asm.interceptor.annotation.AtInvokeException; +import com.taobao.arthas.core.command.monitor200.LineHelper; /** * @@ -111,4 +112,33 @@ public static void onInvokeException(@Binding.This Object target, @Binding.Class } } + /** + * 行观测(line命令)使用的 Interceptor + * 为什么要用两个参数一模一样的方法? + * 场景:两个人分别使用 LineNumber 和 LineCode 进行观测,如果只用一个方法,其 AdviceLister 能正常注册和监听回调吗? + * 不可以!因为 LineNumber 和 LineCode 不能一对一映射,也就是不能进行转换(eg.需要在方法退出前插桩时) + * 在现有的 AdviceListener 注册和查询机制下,使用的key是增强时就已经确定的了(如类名、方法签名),所以使用 LineNumber 和 LineCode 所计算出来的 key 是不一致的,也没有办法进行转换 + */ + public static class SpyLineInterceptor { + + public static void atLineCode(@Binding.This Object target, @Binding.Class Class clazz, + @Binding.MethodInfo String methodInfo, @Binding.Args Object[] args, + @Binding.LocalVars(excludePattern = LineHelper.LOCAL_VARIABLES_NAME_EXCLUDE_MATCHER) Object[] vars, + @Binding.LocalVarNames(excludePattern = LineHelper.LOCAL_VARIABLES_NAME_EXCLUDE_MATCHER) String[] varNames, + //这个是由Arthas传递的动态变化值,无法使用binding + String location) { + SpyAPI.atLineCode(clazz, methodInfo, target, args, location, vars, varNames); + } + + public static void atLineNumber(@Binding.This Object target, @Binding.Class Class clazz, + @Binding.MethodInfo String methodInfo, @Binding.Args Object[] args, + @Binding.LocalVars(excludePattern = LineHelper.LOCAL_VARIABLES_NAME_EXCLUDE_MATCHER) Object[] vars, + @Binding.LocalVarNames(excludePattern = LineHelper.LOCAL_VARIABLES_NAME_EXCLUDE_MATCHER) String[] varNames, + //这个是由Arthas传递的动态变化值,无法使用binding + String location) { + SpyAPI.atLineNumber(clazz, methodInfo, target, args, location, vars, varNames); + } + + } + } diff --git a/core/src/main/java/com/taobao/arthas/core/command/BuiltinCommandPack.java b/core/src/main/java/com/taobao/arthas/core/command/BuiltinCommandPack.java index 262793812e1..cb3e8783030 100644 --- a/core/src/main/java/com/taobao/arthas/core/command/BuiltinCommandPack.java +++ b/core/src/main/java/com/taobao/arthas/core/command/BuiltinCommandPack.java @@ -20,20 +20,7 @@ import com.taobao.arthas.core.command.klass100.SearchClassCommand; import com.taobao.arthas.core.command.klass100.SearchMethodCommand; import com.taobao.arthas.core.command.logger.LoggerCommand; -import com.taobao.arthas.core.command.monitor200.DashboardCommand; -import com.taobao.arthas.core.command.monitor200.HeapDumpCommand; -import com.taobao.arthas.core.command.monitor200.JvmCommand; -import com.taobao.arthas.core.command.monitor200.MBeanCommand; -import com.taobao.arthas.core.command.monitor200.MemoryCommand; -import com.taobao.arthas.core.command.monitor200.MonitorCommand; -import com.taobao.arthas.core.command.monitor200.PerfCounterCommand; -import com.taobao.arthas.core.command.monitor200.ProfilerCommand; -import com.taobao.arthas.core.command.monitor200.StackCommand; -import com.taobao.arthas.core.command.monitor200.ThreadCommand; -import com.taobao.arthas.core.command.monitor200.TimeTunnelCommand; -import com.taobao.arthas.core.command.monitor200.TraceCommand; -import com.taobao.arthas.core.command.monitor200.VmToolCommand; -import com.taobao.arthas.core.command.monitor200.WatchCommand; +import com.taobao.arthas.core.command.monitor200.*; import com.taobao.arthas.core.shell.command.AnnotatedCommand; import com.taobao.arthas.core.shell.command.Command; import com.taobao.arthas.core.shell.command.CommandResolver; @@ -105,6 +92,8 @@ private void initCommands(List disabledCommands) { commandClassList.add(ProfilerCommand.class); commandClassList.add(VmToolCommand.class); commandClassList.add(StopCommand.class); + commandClassList.add(LineCommand.class); + try { if (ClassLoader.getSystemClassLoader().getResource("jdk/jfr/Recording.class") != null) { commandClassList.add(JFRCommand.class); diff --git a/core/src/main/java/com/taobao/arthas/core/command/klass100/JadCommand.java b/core/src/main/java/com/taobao/arthas/core/command/klass100/JadCommand.java index 1637421f7fc..7e94e39ee7e 100644 --- a/core/src/main/java/com/taobao/arthas/core/command/klass100/JadCommand.java +++ b/core/src/main/java/com/taobao/arthas/core/command/klass100/JadCommand.java @@ -2,13 +2,13 @@ import com.alibaba.arthas.deps.org.slf4j.Logger; import com.alibaba.arthas.deps.org.slf4j.LoggerFactory; +import com.alibaba.bytekit.utils.AsmUtils; +import com.alibaba.deps.org.objectweb.asm.tree.ClassNode; +import com.alibaba.deps.org.objectweb.asm.tree.MethodNode; import com.taobao.arthas.common.Pair; import com.taobao.arthas.core.command.Constants; -import com.taobao.arthas.core.command.model.ClassVO; -import com.taobao.arthas.core.command.model.ClassLoaderVO; -import com.taobao.arthas.core.command.model.JadModel; -import com.taobao.arthas.core.command.model.MessageModel; -import com.taobao.arthas.core.command.model.RowAffectModel; +import com.taobao.arthas.core.command.model.*; +import com.taobao.arthas.core.command.monitor200.LineHelper; import com.taobao.arthas.core.shell.cli.Completion; import com.taobao.arthas.core.shell.cli.CompletionUtils; import com.taobao.arthas.core.shell.command.AnnotatedCommand; @@ -65,6 +65,11 @@ public class JadCommand extends AnnotatedCommand { */ private boolean sourceOnly = false; + /** + * 是否展示用于line命令的渲染视图(LineCode信息) + */ + private boolean lineCode = false; + @Argument(argName = "class-pattern", index = 0) @Description("Class name pattern, use either '.' or '/' as separator") public void setClassPattern(String classPattern) { @@ -121,6 +126,12 @@ public void setDirectory(String directory) { this.directory = directory; } + @Option(longName = "lineCode", flag = true) + @Description("Output the lineCode list, default value false") + public void setLineCode(boolean lineCode) { + this.lineCode = lineCode; + } + @Override public void process(CommandProcess process) { if (directory != null && !FileUtils.isDirectoryOrNotExist(directory)) { @@ -208,6 +219,17 @@ private ExitStatus processExactMatch(CommandProcess process, RowAffect affect, I jadModel.setLocation(ClassUtils.getCodeSource(c.getProtectionDomain().getCodeSource())); } process.appendResult(jadModel); + + //追加line命令用到的LineCode视图 + if (lineCode){ + ClassNode classNode = AsmUtils.toClassNode(com.taobao.arthas.common.FileUtils.readFileToByteArray(classFile)); + for (MethodNode methodNode : classNode.methods) { + if (methodNode.name.equals(methodName)){ + process.appendResult(new EchoModel(LineHelper.renderMethodLineCodeView(methodNode))); + } + } + } + affect.rCnt(classFiles.keySet().size()); return ExitStatus.success(); } catch (Throwable t) { diff --git a/core/src/main/java/com/taobao/arthas/core/command/model/LineModel.java b/core/src/main/java/com/taobao/arthas/core/command/model/LineModel.java new file mode 100644 index 00000000000..bdddbb470b9 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/model/LineModel.java @@ -0,0 +1,74 @@ +package com.taobao.arthas.core.command.model; + +import java.time.LocalDateTime; + +/** + * Line command result model + * + */ +public class LineModel extends ResultModel { + + private LocalDateTime ts; + private ObjectVO value; + + private Integer sizeLimit; + private String className; + private String methodName; + private String accessPoint; + + public LineModel() { + } + + @Override + public String getType() { + return "line"; + } + + public LocalDateTime getTs() { + return ts; + } + + public void setTs(LocalDateTime ts) { + this.ts = ts; + } + + public ObjectVO getValue() { + return value; + } + + public void setValue(ObjectVO value) { + this.value = value; + } + + public void setSizeLimit(Integer sizeLimit) { + this.sizeLimit = sizeLimit; + } + + public Integer getSizeLimit() { + return sizeLimit; + } + + public String getClassName() { + return className; + } + + public void setClassName(String className) { + this.className = className; + } + + public String getMethodName() { + return methodName; + } + + public void setMethodName(String methodName) { + this.methodName = methodName; + } + + public String getAccessPoint() { + return accessPoint; + } + + public void setAccessPoint(String accessPoint) { + this.accessPoint = accessPoint; + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/EnhancerCommand.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/EnhancerCommand.java index 48615a19f5d..2dfbd2d6d08 100644 --- a/core/src/main/java/com/taobao/arthas/core/command/monitor200/EnhancerCommand.java +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/EnhancerCommand.java @@ -166,8 +166,12 @@ protected void enhance(CommandProcess process) { if(listener instanceof AbstractTraceAdviceListener) { skipJDKTrace = ((AbstractTraceAdviceListener) listener).getCommand().isSkipJDKTrace(); } + String line = null; + if(listener instanceof LineAdviceListener) { + line = ((LineAdviceListener) listener).getCommand().getLine(); + } - Enhancer enhancer = new Enhancer(listener, listener instanceof InvokeTraceable, skipJDKTrace, getClassNameMatcher(), getClassNameExcludeMatcher(), getMethodNameMatcher()); + Enhancer enhancer = new Enhancer(listener, listener instanceof InvokeTraceable, skipJDKTrace, line, getClassNameMatcher(), getClassNameExcludeMatcher(), getMethodNameMatcher()); // 注册通知监听器 process.register(listener, enhancer); effect = enhancer.enhance(inst, this.maxNumOfMatchedClass); @@ -193,15 +197,17 @@ protected void enhance(CommandProcess process) { String optionsCommand = Ansi.ansi().fg(Ansi.Color.GREEN).a("options unsafe true").reset().toString(); String javaPackage = Ansi.ansi().fg(Ansi.Color.GREEN).a("java.*").reset().toString(); String resetCommand = Ansi.ansi().fg(Ansi.Color.GREEN).a("reset CLASS_NAME").reset().toString(); + String jadCommand = Ansi.ansi().fg(Ansi.Color.GREEN).a("jad CLASS_NAME METHOD_NAME --lineCode").reset().toString(); String logStr = Ansi.ansi().fg(Ansi.Color.GREEN).a(LogUtil.loggingFile()).reset().toString(); String issueStr = Ansi.ansi().fg(Ansi.Color.GREEN).a("https://github.com/alibaba/arthas/issues/47").reset().toString(); - String msg = "No class or method is affected, try:\n" + String msg = "No class or method or line is affected, try:\n" + "1. Execute `" + smCommand + "` to make sure the method you are tracing actually exists (it might be in your parent class).\n" + "2. Execute `" + optionsCommand + "`, if you want to enhance the classes under the `" + javaPackage + "` package.\n" + "3. Execute `" + resetCommand + "` and try again, your method body might be too large.\n" - + "4. Match the constructor, use ``, for example: `watch demo.MathGame `\n" - + "5. Check arthas log: " + logStr + "\n" - + "6. Visit " + issueStr + " for more details."; + + "4. Execute `" + jadCommand + "` to make sure the lineNumber or lineCode you are watching actually exists.\n" + + "5. Match the constructor, use ``, for example: `watch demo.MathGame `\n" + + "6. Check arthas log: " + logStr + "\n" + + "7. Visit " + issueStr + " for more details."; process.end(-1, msg); return; } diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/LineAdviceListener.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/LineAdviceListener.java new file mode 100644 index 00000000000..4b652e5f34d --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/LineAdviceListener.java @@ -0,0 +1,100 @@ +package com.taobao.arthas.core.command.monitor200; + +import com.alibaba.arthas.deps.org.slf4j.Logger; +import com.alibaba.arthas.deps.org.slf4j.LoggerFactory; +import com.taobao.arthas.core.advisor.AccessPoint; +import com.taobao.arthas.core.advisor.Advice; +import com.taobao.arthas.core.advisor.AdviceListenerAdapter; +import com.taobao.arthas.core.advisor.ArthasMethod; +import com.taobao.arthas.core.command.express.ExpressException; +import com.taobao.arthas.core.command.express.ExpressFactory; +import com.taobao.arthas.core.command.model.LineModel; +import com.taobao.arthas.core.command.model.ObjectVO; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.util.LogUtil; +import com.taobao.arthas.core.util.StringUtils; + +import java.time.LocalDateTime; + +class LineAdviceListener extends AdviceListenerAdapter { + + private static final Logger logger = LoggerFactory.getLogger(LineAdviceListener.class); + private LineCommand command; + private CommandProcess process; + + public LineAdviceListener(LineCommand command, CommandProcess process, boolean verbose) { + this.command = command; + this.process = process; + super.setVerbose(verbose); + } + + @Override + public void before(ClassLoader loader, Class clazz, ArthasMethod method, Object target, Object[] args) + throws Throwable { + + } + + @Override + public void afterReturning(ClassLoader loader, Class clazz, ArthasMethod method, Object target, Object[] args, + Object returnObject) throws Throwable { + + } + + @Override + public void afterThrowing(ClassLoader loader, Class clazz, ArthasMethod method, Object target, Object[] args, + Throwable throwable) { + + } + + @Override + public void atLine(Advice advice, String line) throws Throwable { + try { + boolean conditionResult = isConditionMet(command.getConditionExpress(), advice); + if (this.isVerbose()) { + process.write("Condition express: " + command.getConditionExpress() + " , result: " + conditionResult + "\n"); + } + if (conditionResult) { + Object value = getExpressionResult(command.getExpress(), advice); + + LineModel model = new LineModel(); + model.setTs(LocalDateTime.now()); + model.setValue(new ObjectVO(value, command.getExpand())); + model.setSizeLimit(command.getSizeLimit()); + model.setClassName(advice.getClazz().getName()); + model.setMethodName(advice.getMethod().getName()); + model.setAccessPoint(line); + + process.appendResult(model); + process.times().incrementAndGet(); + if (isLimitExceeded(command.getNumberOfLimit(), process.times().get())) { + abortProcess(process, command.getNumberOfLimit()); + } + } + } catch (Throwable e) { + logger.warn("line command failed.", e); + process.end(-1, "look failed, condition is: " + command.getConditionExpress() + ", express is: " + + command.getExpress() + ", " + e.getMessage() + ", visit " + LogUtil.loggingFile() + + " for more details."); + } + } + + boolean isConditionMet(String conditionExpress, Advice advice) throws ExpressException { + return StringUtils.isEmpty(conditionExpress) + || ExpressFactory.threadLocalExpress(advice).is(conditionExpress); + + } + + Object getExpressionResult(String express, Advice advice) throws ExpressException { + return ExpressFactory.threadLocalExpress(advice).get(express); + } + + public LineCommand getCommand() { + return command; + } + + public void setCommand(LineCommand command) { + this.command = command; + } + + +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/LineCommand.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/LineCommand.java new file mode 100644 index 00000000000..2ff28ecc334 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/LineCommand.java @@ -0,0 +1,182 @@ +package com.taobao.arthas.core.command.monitor200; + +import com.taobao.arthas.core.GlobalOptions; +import com.taobao.arthas.core.advisor.AdviceListener; +import com.taobao.arthas.core.command.Constants; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.util.SearchUtils; +import com.taobao.arthas.core.util.matcher.Matcher; +import com.taobao.arthas.core.view.Ansi; +import com.taobao.arthas.core.view.ObjectView; +import com.taobao.middleware.cli.annotations.*; + + +@Name("line") +@Summary("Display the local variables, input parameter of method specified with LineNumber or LineCode(found in jad --lineCode)") +@Description( + " The express may be one of the following expression:\n" + + " target : the object\n" + + " clazz : the object's class\n" + + " method : the constructor or method\n" + + " params : the parameters array of method\n" + + " params[0..n] : the element of parameters array\n" + + " varMap : the local variables map\n" + + " varMap[\"varName\"] : the local variable value of varName\n" + + "\nExamples:\n" + + " line org.apache.commons.lang.StringUtils isBlank -1 \n" + + " line org.apache.commons.lang.StringUtils isBlank -1 'varMap'\n" + + " line org.apache.commons.lang.StringUtils isBlank 3581 'varMap' 'varMap[\"strLen\"] == 3'\n" + + " line *StringUtils isBlank 128 '{params,varMap}' \n" + + " line org.apache.commons.lang.StringUtils isBlank abcd-1 'varMap'\n" +) +public class LineCommand extends EnhancerCommand { + + private String classPattern; + private String methodPattern; + private String express; + private String line; + private String conditionExpress; + private Integer expand = 1; + private Integer sizeLimit = 10 * 1024 * 1024; + private boolean isRegEx = false; + private int numberOfLimit = 100; + + @Argument(index = 0, argName = "class-pattern") + @Description("The full qualified class name you want to watch in.") + public void setClassPattern(String classPattern) { + this.classPattern = classPattern; + } + + @Argument(index = 1, argName = "method-pattern") + @Description("The method name you want to watch in.") + public void setMethodPattern(String methodPattern) { + this.methodPattern = methodPattern; + } + + @Argument(index = 2, argName = "location") + @Description("The location will be watch before LineNumber(eg:108) or LineCode(eg:abcd-1, found in jad --lineCode).") + public void setLine(String line) { + this.line = line; + } + + @Argument(index = 3, argName = "express", required = false) + @DefaultValue("varMap") + @Description("The express you want to evaluate, written by ognl. Default value is 'varMap'\n") + public void setExpress(String express) { + this.express = express; + } + + @Argument(index = 4, argName = "condition-express", required = false) + @Description(Constants.CONDITION_EXPRESS) + public void setConditionExpress(String conditionExpress) { + this.conditionExpress = conditionExpress; + } + + @Option(shortName = "M", longName = "sizeLimit") + @Description("Upper size limit in bytes for the result (10 * 1024 * 1024 by default)") + public void setSizeLimit(Integer sizeLimit) { + this.sizeLimit = sizeLimit; + } + + @Option(shortName = "x", longName = "expand") + @Description("Expand level of object (1 by default), the max value is " + ObjectView.MAX_DEEP) + public void setExpand(Integer expand) { + this.expand = expand; + } + + @Option(shortName = "E", longName = "regex", flag = true) + @Description("Enable regular expression to match (wildcard matching by default)") + public void setRegEx(boolean regEx) { + isRegEx = regEx; + } + + @Option(shortName = "n", longName = "limits") + @Description("Threshold of execution times") + public void setNumberOfLimit(int numberOfLimit) { + this.numberOfLimit = numberOfLimit; + } + + public String getClassPattern() { + return classPattern; + } + + public String getMethodPattern() { + return methodPattern; + } + + public String getExpress() { + return express; + } + + public String getConditionExpress() { + return conditionExpress; + } + + public Integer getExpand() { + return expand; + } + + public Integer getSizeLimit() { + return sizeLimit; + } + + public boolean isRegEx() { + return isRegEx; + } + + public int getNumberOfLimit() { + return numberOfLimit; + } + + public String getLine() { + return line; + } + + @Override + protected Matcher getClassNameMatcher() { + if (classNameMatcher == null) { + classNameMatcher = SearchUtils.classNameMatcher(getClassPattern(), isRegEx()); + } + return classNameMatcher; + } + + @Override + protected Matcher getClassNameExcludeMatcher() { + if (classNameExcludeMatcher == null && getExcludeClassPattern() != null) { + classNameExcludeMatcher = SearchUtils.classNameMatcher(getExcludeClassPattern(), isRegEx()); + } + return classNameExcludeMatcher; + } + + @Override + protected Matcher getMethodNameMatcher() { + if (methodNameMatcher == null) { + methodNameMatcher = SearchUtils.classNameMatcher(getMethodPattern(), isRegEx()); + } + return methodNameMatcher; + } + + @Override + protected AdviceListener getAdviceListener(CommandProcess process) { + return new LineAdviceListener(this, process, GlobalOptions.verbose || this.verbose); + } + + @Override + public void process(final CommandProcess process) { + if (!LineHelper.hasSupportLineCommand()) { + throw new IllegalArgumentException("this version not support line command!"); + } + //check arg,只是简单的格式校验 + if (!LineHelper.validLocation(line)) { + String helpCommand = Ansi.ansi().fg(Ansi.Color.GREEN).a("line -h").reset().toString(); + String jadCommand = Ansi.ansi().fg(Ansi.Color.GREEN).a("jad CLASS_NAME METHOD_NAME --lineCode").reset().toString(); + String msg = "Your location arg:" + line + " has a wrong format, it should be look like:\n" + + "1. LineNumber: `" + Ansi.ansi().fg(Ansi.Color.GREEN).a("108").reset().toString() + "` \n" + + "2. LineCode: `" + Ansi.ansi().fg(Ansi.Color.GREEN).a("abcd-1").reset().toString() + "` (use `"+jadCommand+"` to find out.)\n" + + "3. Use `" + helpCommand + "` to get more information."; + process.end(-1, msg); + return; + } + super.process(process); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/LineHelper.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/LineHelper.java new file mode 100644 index 00000000000..551ce85ed20 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/LineHelper.java @@ -0,0 +1,606 @@ +package com.taobao.arthas.core.command.monitor200; + +import com.alibaba.bytekit.asm.MethodProcessor; +import com.alibaba.bytekit.asm.binding.Binding; +import com.alibaba.bytekit.asm.binding.BindingContext; +import com.alibaba.bytekit.asm.binding.IntBinding; +import com.alibaba.bytekit.asm.interceptor.InterceptorMethodConfig; +import com.alibaba.bytekit.asm.interceptor.InterceptorProcessor; +import com.alibaba.bytekit.asm.interceptor.annotation.BindingParserUtils; +import com.alibaba.bytekit.asm.location.Location; +import com.alibaba.bytekit.asm.location.LocationMatcher; +import com.alibaba.bytekit.asm.location.LocationType; +import com.alibaba.bytekit.asm.location.filter.LocationFilter; +import com.alibaba.bytekit.utils.AsmOpUtils; +import com.alibaba.bytekit.utils.MatchUtils; +import com.alibaba.bytekit.utils.ReflectionUtils; +import com.alibaba.deps.org.objectweb.asm.Opcodes; +import com.alibaba.deps.org.objectweb.asm.Type; +import com.alibaba.deps.org.objectweb.asm.tree.*; +import com.taobao.arthas.common.Pair; +import com.taobao.arthas.core.advisor.SpyInterceptors; +import com.taobao.arthas.core.util.EncryptUtils; +import com.taobao.arthas.core.util.StringUtils; +import com.taobao.arthas.core.view.Ansi; + +import java.lang.reflect.Method; +import java.util.*; + +/** + * line命令用到的一些方法集合 + */ +public class LineHelper { + + private static final String LOCATION_CODE_SPLITTER = "-"; + public static final String LOCAL_VARIABLES_NAME_EXCLUDE_MATCHER = "*$*"; + private static final String LOCATION_CONTENT_VARIABLE_FORMATTER = "assign-variable:%s"; + private static final String LOCATION_CONTENT_METHOD_FORMATTER = "invoke-method:%s#%s:%s"; + private static final String LOCATION_VIEW_LINE_SPLIT_LINE = "------------------------- lineCode location -------------------------"; + private static final String LOCATION_VIEW_LINE_HEADER = "format: /*LineNumber*/ (LineCode)-> Instruction"; + private static final String LOCATION_VIEW_LINE_FORMATTER_POINTER = "/*%-3s*/ (%s)-> "; + private static final String LOCATION_VIEW_LINE_FORMATTER_INSTRUCTION = " %s"; + public static final String VARIABLE_RENAME = "-renamed-"; + public static final String EXCLUDE_VARIABLE_THIS = "this"; + + /** + * 方法退出前的LineNumber值 + */ + public static final int LINE_LOCATION_BEFORE_METHOD_EXIT = -1; + + /** + * 判断line命令使用的line参数是否合法 + * 1.LineNumber类型,如12 + * 2.LineCode类型,如abcd-1 + */ + public static boolean validLocation(String line) { + if (line == null || line.isEmpty()) { + return false; + } + if (line.equals(String.valueOf(LINE_LOCATION_BEFORE_METHOD_EXIT))) return true; + if (line.contains(LOCATION_CODE_SPLITTER)) { + String[] arr = line.split(LOCATION_CODE_SPLITTER); + if (arr.length != 2) { + return false; + } + if (StringUtils.isBlank(arr[0])){ + return false; + } + return StringUtils.isNumeric(arr[1]) && arr[1].length() < 8; + } + return StringUtils.isNumeric(line) && line.length() < 8; + } + + /** + * 判定是否支持line命令 + * 有个情况是先启动了旧版本,然后再启动新版,就会导致方法找不到,进而报错,需要避免这个问题 + */ + public static boolean hasSupportLineCommand() { + boolean hasAtLineMethod = false; + try { + Class clazz = Class.forName("java.arthas.SpyAPI"); // 加载不到会抛异常 + for (Method declaredMethod : clazz.getDeclaredMethods()) { + if (declaredMethod.getName().equals("atLineCode")) { + hasAtLineMethod = true; + break; + } + } + } catch (Throwable e) { + // ignore + } + return hasAtLineMethod; + } + + /** + * 构建本地变量Map + * @param vars 本地变量数据 + * @param varNames 本地变量名数据 + */ + public static Map buildVarMap(Object[] vars, String[] varNames){ + Map varMap = new HashMap(vars.length); + for (int i = 0; i < vars.length; i++) { + //不放入this,想要获取this,直接在表达式中获取即可 + if (LineHelper.EXCLUDE_VARIABLE_THIS.equals(varNames[i])) continue; + String varName = LineHelper.determinedVarName(varMap.keySet(), varNames[i]); + varMap.put(varName, vars[i]); + } + return varMap; + } + + /** + * 生成方法的lineCode视图 + */ + public static String renderMethodLineCodeView(MethodNode methodNode) { + Object[] lineArray = renderMethodView(methodNode).toArray(); + return StringUtils.join(lineArray, "\n"); + } + + /** + * 创建增强用的 InterceptorProcessor + * @param line 行标识,可能是行号(LineNumber),也可能是行的特殊标号(LineCode) + */ + public static InterceptorProcessor createLineInterceptorProcessor(String line) { + Method method = ReflectionUtils.findMethod(SpyInterceptors.SpyLineInterceptor.class, "atLineNumber", null); + if (isLineCode(line)) { + method = ReflectionUtils.findMethod(SpyInterceptors.SpyLineInterceptor.class, "atLineCode", null); + } + + InterceptorProcessor interceptorProcessor = new InterceptorProcessor(method.getDeclaringClass().getClassLoader()); + + //locationMatcher + if (isLineCode(line)) { + interceptorProcessor.setLocationMatcher(new LineCodeMatcher(line)); + } else { + interceptorProcessor.setLocationMatcher(new LineNumberMatcher(line)); + } + + //interceptorMethodConfig + InterceptorMethodConfig interceptorMethodConfig = new InterceptorMethodConfig(); + interceptorProcessor.setInterceptorMethodConfig(interceptorMethodConfig); + interceptorMethodConfig.setOwner(Type.getInternalName(method.getDeclaringClass())); + interceptorMethodConfig.setMethodName(method.getName()); + interceptorMethodConfig.setMethodDesc(Type.getMethodDescriptor(method)); + + //inline + interceptorMethodConfig.setInline(true); + + //bindings + List bindings = BindingParserUtils.parseBindings(method); + LineLocationBinding lineLocationBinding = new LineLocationBinding(line); + bindings.add(lineLocationBinding); + interceptorMethodConfig.setBindings(bindings); + + return interceptorProcessor; + } + + /** + * 解析变量名 + * 处理重复的变量名 + */ + private static String determinedVarName(Set nameSet, String varName) { + String tmpVarName = varName; + for (int i = 1; i < Integer.MAX_VALUE; i++) { + if (nameSet.contains(tmpVarName)) { + tmpVarName = varName + VARIABLE_RENAME + i; + } else { + return tmpVarName; + } + } + throw new IllegalArgumentException("illegal varName:" + varName); + } + + /** + * 根据lineCode找到对应的InsnNode + */ + private static AbstractInsnNode findInsnNodeByLocationCode(MethodNode methodNode, String lineCode) { + Pair lineCodePair = convertToLineCode(lineCode); + Map uniqMap = genLineCodeMapNode(methodNode, lineCodePair.getFirst().length()); + return uniqMap.get(lineCode); + } + + /** + * 将传入的LineCode(形如abcd-1)转换成 结构化的Pair(first=abcd,second=1) + */ + private static Pair convertToLineCode(String line) { + String[] arr = line.split(LOCATION_CODE_SPLITTER); + return new Pair(arr[0], Integer.valueOf(arr[1])); + } + + /** + * 判断是否为LineCode类型 + */ + private static boolean isLineCode(String line) { + if (line.equals(String.valueOf(LINE_LOCATION_BEFORE_METHOD_EXIT))) return false; + return line.contains(LOCATION_CODE_SPLITTER); + } + + /** + * 判断 InsnNode 类型是否是line想要的 + * 依据:因为line监测的对象是变量,而变量值一般发生在 赋值、作为参数被方法调用 + * 为什么方法调用只过滤了基础类型的box方法? + * 1.首先已经明确这些方法不会改变变量值 + * 2.其次在插桩的时候,生成的代码里边会有这些方法,如果不排除掉,会影响LineCode的匹配和生成 + * 3.目前要判断一个变量是否作为该方法的参数较为复杂,先不做 + */ + private static boolean matchInsnNode(AbstractInsnNode abstractInsnNode, Set allowVariableSet) { + if (abstractInsnNode instanceof VarInsnNode) { + //赋值 + switch (abstractInsnNode.getOpcode()) { + case Opcodes.ISTORE: + case Opcodes.LSTORE: + case Opcodes.FSTORE: + case Opcodes.DSTORE: + case Opcodes.ASTORE: + return allowVariableSet.contains(((VarInsnNode) abstractInsnNode).var); + } + return false; + } else if (abstractInsnNode instanceof MethodInsnNode) { + //box方法,及arthas内部方法 + MethodInsnNode methodInsnNode = (MethodInsnNode) abstractInsnNode; + if (methodInsnNode.owner.equals("java/arthas/SpyAPI")) return false; + if (methodInsnNode.owner.equals("java/lang/Byte") || + methodInsnNode.owner.equals("java/lang/Short") || + methodInsnNode.owner.equals("java/lang/Integer") || + methodInsnNode.owner.equals("java/lang/Long") || + methodInsnNode.owner.equals("java/lang/Boolean") || + methodInsnNode.owner.equals("java/lang/Float") + ) + return !methodInsnNode.name.equals(""); + return true; + } + return false; + } + + /** + * 向前寻找离insnNode最近的LineNumber节点并返回行号 + * 如果找不到,则返回 0 + */ + private static int findPreLineNumber(AbstractInsnNode insnNode) { + while (insnNode != null) { + if (insnNode instanceof LineNumberNode) { + return ((LineNumberNode) insnNode).line; + } + insnNode = insnNode.getPrevious(); + } + return 0; + } + + /** + * 生成InsnNode对应的LocationContent,也就是方便肉眼理解的形式 + * 如方法赋值:assign-variable: varName + * 如方法调用:invoke-method: java/lang/StringBuilder#append:()V + */ + private static List genLocationContentList(List nodeList, Map varIdxMap) { + List contentList = new LinkedList(); + for (AbstractInsnNode abstractInsnNode : nodeList) { + String content = ""; + if (abstractInsnNode instanceof VarInsnNode) { + VarInsnNode varInsnNode = (VarInsnNode) abstractInsnNode; + content = String.format(LOCATION_CONTENT_VARIABLE_FORMATTER, varIdxMap.get(varInsnNode.var)); + } else if (abstractInsnNode instanceof MethodInsnNode) { + MethodInsnNode methodInsnNode = (MethodInsnNode) abstractInsnNode; + content = String.format(LOCATION_CONTENT_METHOD_FORMATTER, methodInsnNode.owner, methodInsnNode.name, methodInsnNode.desc); + } + contentList.add(content); + } + return contentList; + } + + /** + * 生成InsnNode对应的LineNumber + */ + private static List genLineNumberList(List nodeList) { + List preLineNumberList = new LinkedList(); + for (AbstractInsnNode abstractInsnNode : nodeList) { + int preLineNumber = findPreLineNumber(abstractInsnNode); + preLineNumberList.add(preLineNumber); + } + return preLineNumberList; + } + + /** + * 过滤出只会被line命令关注的InsnNode + * 具体规则见:matchInsnNode方法注释 + */ + private static List filterNodeList(InsnList insnList, Map varIdxMap) { + List noteList = new LinkedList(); + for (AbstractInsnNode abstractInsnNode : insnList) { + if (matchInsnNode(abstractInsnNode, varIdxMap.keySet())) { + noteList.add(abstractInsnNode); + } + } + return noteList; + } + + /** + * 生成方法的jad视图,形如: + * /*82* / (f0bd-1)-> + * invoke-method: java.util.ArrayList#:(I)V + * /*83* / (7cda-1)-> + * invoke-method: java.lang.Iterable#iterator:()Ljava/util/Iterator; + * /*83* / (ad72-1)-> + * invoke-method: java.util.Iterator#hasNext:()Z + * /*83* / (b105-1)-> + * invoke-method: java.util.Iterator#next:()Ljava/lang/Object; + * /*84* / (11a1-1)-> + * assign-variable: it + * /*84* / (f9e5-1)-> + * invoke-method: java.lang.Number#intValue:()I + */ + private static List renderMethodView(MethodNode methodNode) { + List printLines = new LinkedList(); + + printLines.add(LOCATION_VIEW_LINE_SPLIT_LINE); + printLines.add(LOCATION_VIEW_LINE_HEADER); + + //读取变量表 + Map varIdxMap = new HashMap(); + for (LocalVariableNode localVariable : methodNode.localVariables) { + if (!MatchUtils.wildcardMatch(localVariable.name, LOCAL_VARIABLES_NAME_EXCLUDE_MATCHER)) { + varIdxMap.put(localVariable.index, localVariable.name); + } + } + + //过滤出符合的Node + List noteList = filterNodeList(methodNode.instructions, varIdxMap); + //渲染出肉眼友好的内容 + List contentList = genLocationContentList(noteList, varIdxMap); + //获取Node对应的行号 + List preLineNumberList = genLineNumberList(noteList); + //生成LineCode + List> contentAndCode = genLineCode(contentList); + + //渲染需要展示的内容 + for (int i = 0; i < contentAndCode.size(); i++) { + Pair contentCodePair = contentAndCode.get(i); + Integer preLineNumber = preLineNumberList.get(i); + String lineCode = Ansi.ansi().fg(Ansi.Color.GREEN).a(contentCodePair.getSecond()).reset().toString(); + String pointer = String.format(LOCATION_VIEW_LINE_FORMATTER_POINTER, preLineNumber, lineCode); + String instruction = String.format(LOCATION_VIEW_LINE_FORMATTER_INSTRUCTION,contentCodePair.getFirst()); + printLines.add(pointer); + printLines.add(instruction); + } + + return printLines; + } + + /** + * 生成LineCode及其匹配的InsnNode + */ + private static Map genLineCodeMapNode(MethodNode methodNode, int uniqLength) { + //本地变量表 + Map varIdxMap = new HashMap(); + for (LocalVariableNode localVariable : methodNode.localVariables) { + if (!MatchUtils.wildcardMatch(localVariable.name, LOCAL_VARIABLES_NAME_EXCLUDE_MATCHER)) { + varIdxMap.put(localVariable.index, localVariable.name); + } + } + //filter node + List nodeList = new LinkedList(); + for (AbstractInsnNode abstractInsnNode : methodNode.instructions) { + if (matchInsnNode(abstractInsnNode, varIdxMap.keySet())) { + nodeList.add(abstractInsnNode); + } + } + //拼凑出content + List contentList = genLocationContentList(nodeList, varIdxMap); + //构建拼凑map + Map uniqMapNode = new HashMap(); + Map contentMapIdx = new HashMap(); + for (int i = 0; i < contentList.size(); i++) { + String c = contentList.get(i); + Integer lastIdx = contentMapIdx.get(c); + int curIdx = lastIdx == null ? 1 : ++lastIdx; + contentMapIdx.put(c, curIdx); + + String md5 = EncryptUtils.md5DigestAsHex(c.getBytes()).substring(0, uniqLength); + + String locationCode = md5 + LOCATION_CODE_SPLITTER + curIdx; + uniqMapNode.put(locationCode, nodeList.get(i)); + } + return uniqMapNode; + } + + /** + * 生成LocationContent及其映射的LineCode + * 就是 content 做唯一标识,目前采用的方法是md5和排序 + */ + private static List> genLineCode(List locationContentList) { + int preSize = locationContentList.size(); + List> locationContentAndCodeList = new LinkedList>(); + Set contentSet = new HashSet(locationContentList); + //采集md5 + Map contentMapMd5 = new HashMap(preSize); + for (String content : contentSet) { + String project = EncryptUtils.md5DigestAsHex(content.getBytes()); + contentMapMd5.put(content, project); + } + //寻找合适长度 + int length = determineLineCodeLength(contentMapMd5.values()); + //生成map + Map contentMapIdx = new HashMap(); + for (String locationContent : locationContentList) { + //维护idx + Integer lastIdx = contentMapIdx.get(locationContent); + int curIdx = lastIdx == null ? 1 : ++lastIdx; + contentMapIdx.put(locationContent, curIdx); + + String locationCode = locationContent + LOCATION_CODE_SPLITTER + curIdx; + if (length != -1) { + String md5 = contentMapMd5.get(locationContent); + locationCode = md5.substring(0, length) + LOCATION_CODE_SPLITTER + curIdx; + } + locationContentAndCodeList.add(new Pair(locationContent, locationCode)); + } + return locationContentAndCodeList; + } + + /** + * 获取出LineCode的长度 + * 目前摘要算法用md5,初始长度给4为,如果有出现重复,则递增 + */ + private static int determineLineCodeLength(Collection md5List) { + Set md5Set = new HashSet(md5List); + if (md5Set.size() != md5List.size()) { + return -1; + } + for (int i = 4; i < 32; i++) { + Set uniqSet = new HashSet(md5Set.size()); + for (String md5 : md5Set) { + String uniq = md5.substring(0, i); + if (!uniqSet.add(uniq)) { + break; + } + } + if (uniqSet.size() == md5Set.size()) { + return i; + } + } + return -1; + } + + /** + * 定义一个LineLocation + */ + private static class LineLocation extends Location { + + private String location; + + public LineLocation(AbstractInsnNode insnNode, String location, boolean whenComplete) { + super(insnNode, whenComplete); + this.location = location; + } + + @Override + public LocationType getLocationType() { + return LocationType.USER_DEFINE; + } + + public String getLocation() { + return location; + } + + public void setLocation(String location) { + this.location = location; + } + + } + + /** + * LineNumber匹配 + */ + private static class LineNumberMatcher implements LocationMatcher { + + /** + * 目标行号 + * -1则代表方法的退出之前 + */ + private final Integer lineNumber; + + public LineNumberMatcher(String locationCode) { + this.lineNumber = Integer.valueOf(locationCode); + } + + @Override + public List match(MethodProcessor methodProcessor) { + List locations = new ArrayList(); + AbstractInsnNode insnNode = methodProcessor.getEnterInsnNode(); + LocationFilter locationFilter = methodProcessor.getLocationFilter(); + + while (insnNode != null) { + + if (lineNumber == LINE_LOCATION_BEFORE_METHOD_EXIT) { + //匹配方法退出之前,可能会有多个return语句 + if (insnNode instanceof InsnNode) { + InsnNode node = (InsnNode) insnNode; + if (matchExit(node)) { + //行前匹配 + boolean filtered = !locationFilter.allow(node, LocationType.LINE, false); + if (!filtered){ + Location location = new LineLocation(node, lineNumber.toString(), false); + locations.add(location); + } + } + } + } else { + //匹配具体的行 + if (insnNode instanceof LineNumberNode) { + LineNumberNode lineNumberNode = (LineNumberNode) insnNode; + if (matchLine(lineNumberNode.line)) { + //行前匹配 + boolean filtered = !locationFilter.allow(lineNumberNode, LocationType.LINE, false); + if (filtered) break; + //目前因为如果直接返回lineNumberNode,增强完之后会导致行号丢失,暂时没找到原因,因此取上一个节点 + Location location = new LineLocation(lineNumberNode.getPrevious(), lineNumber.toString(), false); + locations.add(location); + //存在一个方法内会有多个相同行号的情况,这里只取第一个 + break; + } + } + } + insnNode = insnNode.getNext(); + } + return locations; + } + + private boolean matchLine(int line) { + return line == lineNumber; + } + + public boolean matchExit(InsnNode node) { + switch (node.getOpcode()) { + case Opcodes.RETURN: // empty stack + case Opcodes.IRETURN: // 1 before n/a after + case Opcodes.FRETURN: // 1 before n/a after + case Opcodes.ARETURN: // 1 before n/a after + case Opcodes.LRETURN: // 2 before n/a after + case Opcodes.DRETURN: // 2 before n/a after + return true; + } + return false; + } + + } + + /** + * LineCode匹配 + */ + private static class LineCodeMatcher implements LocationMatcher { + + private final String lineCode; + + public LineCodeMatcher(String lineCode) { + this.lineCode = lineCode; + } + + @Override + public List match(MethodProcessor methodProcessor) { + List locations = new ArrayList(); + LocationFilter locationFilter = methodProcessor.getLocationFilter(); + + AbstractInsnNode insnNode = findInsnNodeByLocationCode(methodProcessor.getMethodNode(), lineCode); + if (insnNode == null) { + return locations; + } + boolean filtered = !locationFilter.allow(insnNode, LocationType.USER_DEFINE, false); + if (filtered) return Collections.emptyList(); + Location location = new LineLocation(insnNode, lineCode, false); + locations.add(location); + return locations; + } + + } + + /** + * LineLocation的Binding + * 参照了 {@link IntBinding} + */ + private static class LineLocationBinding extends Binding { + + private String value; + + + public LineLocationBinding(String value) { + this.value = value; + } + + public String getValue() { + return value; + } + + public void setValue(String value) { + this.value = value; + } + + @Override + public void pushOntoStack(InsnList instructions, BindingContext bindingContext) { + AsmOpUtils.push(instructions, value); + } + + @Override + public Type getType(BindingContext bindingContext) { + return Type.getType(String.class); + } + + } + +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/view/LineView.java b/core/src/main/java/com/taobao/arthas/core/command/view/LineView.java new file mode 100644 index 00000000000..165dd6545e8 --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/command/view/LineView.java @@ -0,0 +1,24 @@ +package com.taobao.arthas.core.command.view; + +import com.taobao.arthas.core.command.model.LineModel; +import com.taobao.arthas.core.command.model.ObjectVO; +import com.taobao.arthas.core.shell.command.CommandProcess; +import com.taobao.arthas.core.util.DateUtils; +import com.taobao.arthas.core.util.StringUtils; +import com.taobao.arthas.core.view.ObjectView; + +/** + * Term view for LineModel + * + */ +public class LineView extends ResultView { + + @Override + public void draw(CommandProcess process, LineModel model) { + ObjectVO objectVO = model.getValue(); + String result = StringUtils.objectToString( + objectVO.needExpand() ? new ObjectView(model.getSizeLimit(), objectVO).draw() : objectVO.getObject()); + process.write("method=" + model.getClassName() + "." + model.getMethodName() + " line=" + model.getAccessPoint() + "\n"); + process.write("ts=" + DateUtils.formatDateTime(model.getTs()) + "; result=" + result + "\n"); + } +} diff --git a/core/src/main/java/com/taobao/arthas/core/command/view/ResultViewResolver.java b/core/src/main/java/com/taobao/arthas/core/command/view/ResultViewResolver.java index 947b60a1ee6..7032103e32f 100644 --- a/core/src/main/java/com/taobao/arthas/core/command/view/ResultViewResolver.java +++ b/core/src/main/java/com/taobao/arthas/core/command/view/ResultViewResolver.java @@ -80,6 +80,7 @@ private void initResultViews() { registerView(WatchView.class); registerView(VmToolView.class); registerView(JFRView.class); + registerView(LineView.class); } catch (Throwable e) { logger.error("register result view failed", e); diff --git a/core/src/main/java/com/taobao/arthas/core/util/EncryptUtils.java b/core/src/main/java/com/taobao/arthas/core/util/EncryptUtils.java new file mode 100644 index 00000000000..9dba716b11d --- /dev/null +++ b/core/src/main/java/com/taobao/arthas/core/util/EncryptUtils.java @@ -0,0 +1,48 @@ +package com.taobao.arthas.core.util; + +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; + +/** + * 加密算法相关工具 + */ +public abstract class EncryptUtils { + + private static final String MD5_ALGORITHM_NAME = "MD5"; + + private static final char[] HEX_CHARS = + {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'}; + + + /** + * md5 as hex + */ + public static String md5DigestAsHex(byte[] bytes) { + MessageDigest messageDigest; + try { + messageDigest = MessageDigest.getInstance(MD5_ALGORITHM_NAME); + } catch (NoSuchAlgorithmException ex) { + throw new IllegalStateException("Could not find MessageDigest with algorithm \"" + MD5_ALGORITHM_NAME + "\"", ex); + } + byte[] digest = messageDigest.digest(bytes); + char[] hexDigest = encodeHex(digest); + return new String(hexDigest); + } + + /** + * bytes转换成hex + */ + private static char[] encodeHex(byte[] bytes) { + char[] chars = new char[32]; + for (int i = 0; i < chars.length; i = i + 2) { + byte b = bytes[i / 2]; + chars[i] = HEX_CHARS[(b >>> 0x4) & 0xf]; + chars[i + 1] = HEX_CHARS[b & 0xf]; + } + return chars; + } + +} + + + diff --git a/core/src/main/java/com/taobao/arthas/core/util/StringUtils.java b/core/src/main/java/com/taobao/arthas/core/util/StringUtils.java index 58993479b5e..47d257a2269 100644 --- a/core/src/main/java/com/taobao/arthas/core/util/StringUtils.java +++ b/core/src/main/java/com/taobao/arthas/core/util/StringUtils.java @@ -995,4 +995,21 @@ public static List toStringList(URL[] urls) { } return Collections.emptyList(); } + + /** + * 判断字符串是否全为数字 + */ + public static boolean isNumeric(final CharSequence cs) { + if (isEmpty(cs)) { + return false; + } + final int sz = cs.length(); + for (int i = 0; i < sz; i++) { + if (!Character.isDigit(cs.charAt(i))) { + return false; + } + } + return true; + } + } diff --git a/core/src/test/java/com/taobao/arthas/core/advisor/EnhancerTest.java b/core/src/test/java/com/taobao/arthas/core/advisor/EnhancerTest.java index bbf97e46614..4cc92d8d0f5 100644 --- a/core/src/test/java/com/taobao/arthas/core/advisor/EnhancerTest.java +++ b/core/src/test/java/com/taobao/arthas/core/advisor/EnhancerTest.java @@ -39,7 +39,7 @@ public void test() throws Throwable { EqualsMatcher methodNameMatcher = new EqualsMatcher("print"); EqualsMatcher classNameMatcher = new EqualsMatcher(MathGame.class.getName()); - Enhancer enhancer = new Enhancer(listener, true, false, classNameMatcher, null, methodNameMatcher); + Enhancer enhancer = new Enhancer(listener, true, false, null, classNameMatcher, null, methodNameMatcher); ClassLoader inClassLoader = MathGame.class.getClassLoader(); String className = MathGame.class.getName(); @@ -92,4 +92,156 @@ public void test() throws Throwable { System.err.println(string); } + /** + * 测试line命令使用的LineNumber的增强 + */ + @Test + public void testLineWithLineNumber() throws Throwable { + Instrumentation instrumentation = ByteBuddyAgent.install(); + + TestHelper.appendSpyJar(instrumentation); + + ArthasBootstrap.getInstance(instrumentation, "ip=127.0.0.1"); + + AdviceListener listener = Mockito.mock(AdviceListener.class); + + EqualsMatcher methodNameMatcher = new EqualsMatcher("run"); + EqualsMatcher classNameMatcher = new EqualsMatcher(MathGame.class.getName()); + + //第25行 + String line = "25"; + Enhancer enhancer = new Enhancer(listener, true, false, line, classNameMatcher, null, methodNameMatcher); + + ClassLoader inClassLoader = MathGame.class.getClassLoader(); + String className = MathGame.class.getName(); + Class classBeingRedefined = MathGame.class; + + ClassNode classNode = AsmUtils.loadClass(MathGame.class); + + byte[] classfileBuffer = AsmUtils.toBytes(classNode); + + byte[] result = enhancer.transform(inClassLoader, className, classBeingRedefined, null, classfileBuffer); + + ClassNode resultClassNode1 = AsmUtils.toClassNode(result); + + result = enhancer.transform(inClassLoader, className, classBeingRedefined, null, result); + + ClassNode resultClassNode2 = AsmUtils.toClassNode(result); + + MethodNode resultMethodNode1 = AsmUtils.findMethods(resultClassNode1.methods, "run").get(0); + MethodNode resultMethodNode2 = AsmUtils.findMethods(resultClassNode2.methods, "run").get(0); + + Assertions + .assertThat(AsmUtils + .findMethodInsnNode(resultMethodNode1, Type.getInternalName(SpyAPI.class), "atLineNumber").size()) + .isEqualTo(AsmUtils.findMethodInsnNode(resultMethodNode2, Type.getInternalName(SpyAPI.class), "atLineNumber") + .size()); + + String string = Decompiler.decompile(result); + + System.err.println(string); + } + + + /** + * 测试line命令使用的LineNumber的增强 + * (方法退出前) + */ + @Test + public void testLineWithLineNumberBeforeMethodExit() throws Throwable { + Instrumentation instrumentation = ByteBuddyAgent.install(); + + TestHelper.appendSpyJar(instrumentation); + + ArthasBootstrap.getInstance(instrumentation, "ip=127.0.0.1"); + + AdviceListener listener = Mockito.mock(AdviceListener.class); + + EqualsMatcher methodNameMatcher = new EqualsMatcher("run"); + EqualsMatcher classNameMatcher = new EqualsMatcher(MathGame.class.getName()); + + //方法退出前 + String line = "-1"; + Enhancer enhancer = new Enhancer(listener, true, false, line, classNameMatcher, null, methodNameMatcher); + + ClassLoader inClassLoader = MathGame.class.getClassLoader(); + String className = MathGame.class.getName(); + Class classBeingRedefined = MathGame.class; + + ClassNode classNode = AsmUtils.loadClass(MathGame.class); + + byte[] classfileBuffer = AsmUtils.toBytes(classNode); + + byte[] result = enhancer.transform(inClassLoader, className, classBeingRedefined, null, classfileBuffer); + + ClassNode resultClassNode1 = AsmUtils.toClassNode(result); + + result = enhancer.transform(inClassLoader, className, classBeingRedefined, null, result); + + ClassNode resultClassNode2 = AsmUtils.toClassNode(result); + + MethodNode resultMethodNode1 = AsmUtils.findMethods(resultClassNode1.methods, "run").get(0); + MethodNode resultMethodNode2 = AsmUtils.findMethods(resultClassNode2.methods, "run").get(0); + + Assertions + .assertThat(AsmUtils + .findMethodInsnNode(resultMethodNode1, Type.getInternalName(SpyAPI.class), "atLineNumber").size()) + .isEqualTo(AsmUtils.findMethodInsnNode(resultMethodNode2, Type.getInternalName(SpyAPI.class), "atLineNumber") + .size()); + + String string = Decompiler.decompile(result); + + System.err.println(string); + } + + /** + * 测试line命令使用的LineCode的增强 + */ + @Test + public void testLineWithLine() throws Throwable { + Instrumentation instrumentation = ByteBuddyAgent.install(); + + TestHelper.appendSpyJar(instrumentation); + + ArthasBootstrap.getInstance(instrumentation, "ip=127.0.0.1"); + + AdviceListener listener = Mockito.mock(AdviceListener.class); + + EqualsMatcher methodNameMatcher = new EqualsMatcher("run"); + EqualsMatcher classNameMatcher = new EqualsMatcher(MathGame.class.getName()); + + // 通过 jad --lineCode demo.MathGame run 找到 + String lineCode = "fbea-1"; + Enhancer enhancer = new Enhancer(listener, true, false, lineCode, classNameMatcher, null, methodNameMatcher); + + ClassLoader inClassLoader = MathGame.class.getClassLoader(); + String className = MathGame.class.getName(); + Class classBeingRedefined = MathGame.class; + + ClassNode classNode = AsmUtils.loadClass(MathGame.class); + + byte[] classfileBuffer = AsmUtils.toBytes(classNode); + + byte[] result = enhancer.transform(inClassLoader, className, classBeingRedefined, null, classfileBuffer); + + ClassNode resultClassNode1 = AsmUtils.toClassNode(result); + + result = enhancer.transform(inClassLoader, className, classBeingRedefined, null, result); + + ClassNode resultClassNode2 = AsmUtils.toClassNode(result); + + MethodNode resultMethodNode1 = AsmUtils.findMethods(resultClassNode1.methods, "run").get(0); + MethodNode resultMethodNode2 = AsmUtils.findMethods(resultClassNode2.methods, "run").get(0); + + Assertions + .assertThat(AsmUtils + .findMethodInsnNode(resultMethodNode1, Type.getInternalName(SpyAPI.class), "atLineCode").size()) + .isEqualTo(AsmUtils.findMethodInsnNode(resultMethodNode2, Type.getInternalName(SpyAPI.class), "atLineCode") + .size()); + + String string = Decompiler.decompile(result); + + System.err.println(string); + } + } diff --git a/spy/src/main/java/java/arthas/SpyAPI.java b/spy/src/main/java/java/arthas/SpyAPI.java index 32762def92e..d41cb3f60b6 100644 --- a/spy/src/main/java/java/arthas/SpyAPI.java +++ b/spy/src/main/java/java/arthas/SpyAPI.java @@ -81,6 +81,34 @@ public static void atInvokeException(Class clazz, String invokeInfo, Object t spyInstance.atInvokeException(clazz, invokeInfo, target, throwable); } + /** + * 使用 LineCode 进行观测的入口 + * 至于为何需要分成 atLineCode 和 atLineNumber ,参见 {@link com.taobao.arthas.core.advisor.SpyInterceptors.SpyLineInterceptor} 的类注释 + * @param lineCode 一个生成的特殊的行标识,可以理解为一种特殊的自己生成的行号 + */ + public static void atLineCode(Class clazz, String methodInfo, Object target, Object[] args, + String lineCode, Object[] vars, String[] varNames) { + try{ + spyInstance.atLine(clazz, methodInfo, target, args, lineCode, vars, varNames); + }catch (Throwable t){ + //ignore 通常情况下不会抛出到外层,但是会有一些新旧版本混用可能会导致报错(先启动了旧版本,再启动新版本),这里做一下保护 + } + } + + /** + * 使用 LineNumber 进行观测的入口 + * 至于为何需要分成 atLineCode 和 atLineNumber ,参见 {@link com.taobao.arthas.core.advisor.SpyInterceptors.SpyLineInterceptor} 的类注释 + * @param lineNumber 行号 + */ + public static void atLineNumber(Class clazz, String methodInfo, Object target, Object[] args, + String lineNumber, Object[] vars, String[] varNames) { + try{ + spyInstance.atLine(clazz, methodInfo, target, args, lineNumber, vars, varNames); + }catch (Throwable t){ + //ignore 通常情况下不会抛出到外层,但是会有一些新旧版本混用可能会导致报错(先启动了旧版本,再启动新版本),这里做一下保护 + } + } + public static abstract class AbstractSpy { public abstract void atEnter(Class clazz, String methodInfo, Object target, Object[] args); @@ -96,6 +124,13 @@ public abstract void atExceptionExit(Class clazz, String methodInfo, Object t public abstract void atAfterInvoke(Class clazz, String invokeInfo, Object target); public abstract void atInvokeException(Class clazz, String invokeInfo, Object target, Throwable throwable); + + /** + * 在某行进行观测 + * @param line 行标识,可能是行号(LineNumber),也可能是行的特殊标号(LineCode) + */ + public abstract void atLine(Class clazz, String methodInfo, Object target, Object[] args, + String line, Object[] vars, String[] varNames); } static class NopSpy extends AbstractSpy { @@ -129,5 +164,10 @@ public void atInvokeException(Class clazz, String invokeInfo, Object target, } + @Override + public void atLine(Class clazz, String methodInfo, Object target, Object[] args, String line, Object[] vars, String[] varNames) { + + } + } } From 1fa35204d0ffc99bb52aa0b4b5a9c4abb9b24dca Mon Sep 17 00:00:00 2001 From: "isadliliying@163.com" Date: Fri, 21 Jun 2024 11:49:08 +0800 Subject: [PATCH 2/2] add introduction for line command --- README.md | 22 ++ README_CN.md | 22 ++ .../core/command/monitor200/LineCommand.java | 3 +- site/docs/.vuepress/configs/sidebar/en.js | 1 + site/docs/.vuepress/configs/sidebar/zh.js | 1 + site/docs/doc/advice-class.md | 24 +- site/docs/doc/commands.md | 3 +- site/docs/doc/line.md | 286 ++++++++++++++++++ site/docs/en/doc/advice-class.md | 4 +- site/docs/en/doc/commands.md | 3 +- site/docs/en/doc/line.md | 270 +++++++++++++++++ 11 files changed, 624 insertions(+), 15 deletions(-) create mode 100644 site/docs/doc/line.md create mode 100644 site/docs/en/doc/line.md diff --git a/README.md b/README.md index 2cc3c3e5d74..6e1911e8488 100644 --- a/README.md +++ b/README.md @@ -320,6 +320,28 @@ ts=2018-09-18 10:26:28;result=@ArrayList[ ] ``` +#### line + +* https://arthas.aliyun.com/doc/en/line + +Observe the local variables before the method `demo.MathGame#run` executes up to line 25. + +```bash +$ line demo.MathGame run 25 -x 2 +Press Q or Ctrl+C to abort. +Affect(class count: 1 , method count: 1) cost in 17 ms, listenerId: 2 +method=demo.MathGame.run line=25 +ts=2024-06-21 09:57:34.452; result=@HashMap[ + @String[primeFactors]:@ArrayList[ + @Integer[2], + @Integer[7], + @Integer[7], + @Integer[991], + ], + @String[number]:@Integer[97118], +] +``` + #### Monitor * https://arthas.aliyun.com/doc/en/monitor diff --git a/README_CN.md b/README_CN.md index 11cd2af5bf6..2d1f6d950f2 100644 --- a/README_CN.md +++ b/README_CN.md @@ -305,6 +305,28 @@ ts=2018-09-18 10:26:28;result=@ArrayList[ ] ``` +#### line + +* https://arthas.aliyun.com/doc/line + +观察方法 `demo.MathGame#run` 执行到第25行之前时的局部变量。 + +```bash +$ line demo.MathGame run 25 -x 2 +Press Q or Ctrl+C to abort. +Affect(class count: 1 , method count: 1) cost in 17 ms, listenerId: 2 +method=demo.MathGame.run line=25 +ts=2024-06-21 09:57:34.452; result=@HashMap[ + @String[primeFactors]:@ArrayList[ + @Integer[2], + @Integer[7], + @Integer[7], + @Integer[991], + ], + @String[number]:@Integer[97118], +] +``` + #### Monitor * https://arthas.aliyun.com/doc/monitor diff --git a/core/src/main/java/com/taobao/arthas/core/command/monitor200/LineCommand.java b/core/src/main/java/com/taobao/arthas/core/command/monitor200/LineCommand.java index 2ff28ecc334..ee28f46d744 100644 --- a/core/src/main/java/com/taobao/arthas/core/command/monitor200/LineCommand.java +++ b/core/src/main/java/com/taobao/arthas/core/command/monitor200/LineCommand.java @@ -27,7 +27,8 @@ " line org.apache.commons.lang.StringUtils isBlank -1 'varMap'\n" + " line org.apache.commons.lang.StringUtils isBlank 3581 'varMap' 'varMap[\"strLen\"] == 3'\n" + " line *StringUtils isBlank 128 '{params,varMap}' \n" + - " line org.apache.commons.lang.StringUtils isBlank abcd-1 'varMap'\n" + " line org.apache.commons.lang.StringUtils isBlank abcd-1 'varMap'\n" + + Constants.WIKI + Constants.WIKI_HOME + "line" ) public class LineCommand extends EnhancerCommand { diff --git a/site/docs/.vuepress/configs/sidebar/en.js b/site/docs/.vuepress/configs/sidebar/en.js index b5f43874835..f8550117471 100644 --- a/site/docs/.vuepress/configs/sidebar/en.js +++ b/site/docs/.vuepress/configs/sidebar/en.js @@ -30,6 +30,7 @@ export const sidebarEN = { "/en/doc/jfr.md", "/en/doc/jvm.md", "/en/doc/keymap.md", + "/en/doc/line.md", "/en/doc/logger.md", "/en/doc/mbean.md", "/en/doc/mc.md", diff --git a/site/docs/.vuepress/configs/sidebar/zh.js b/site/docs/.vuepress/configs/sidebar/zh.js index c8d7d75fa54..9cee4ce7dd4 100644 --- a/site/docs/.vuepress/configs/sidebar/zh.js +++ b/site/docs/.vuepress/configs/sidebar/zh.js @@ -30,6 +30,7 @@ export const sidebarZH = { "/doc/jfr.md", "/doc/jvm.md", "/doc/keymap.md", + "/doc/line.md", "/doc/logger.md", "/doc/mbean.md", "/doc/mc.md", diff --git a/site/docs/doc/advice-class.md b/site/docs/doc/advice-class.md index 623fb108c58..aadb369a693 100644 --- a/site/docs/doc/advice-class.md +++ b/site/docs/doc/advice-class.md @@ -14,6 +14,7 @@ public class Advice { private final Object[] params; private final Object returnObj; private final Throwable throwExp; + private final Map varMap; private final boolean isBefore; private final boolean isThrow; private final boolean isReturn; @@ -24,18 +25,19 @@ public class Advice { 这里列一个表格来说明不同变量的含义 -| 变量名 | 变量解释 | -| --------: | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -| loader | 本次调用类所在的 ClassLoader | -| clazz | 本次调用类的 Class 引用 | -| method | 本次调用方法反射引用 | -| target | 本次调用类的实例 | -| params | 本次调用参数列表,这是一个数组,如果方法是无参方法则为空数组 | -| returnObj | 本次调用返回的对象。当且仅当 `isReturn==true` 成立时候有效,表明方法调用是以正常返回的方式结束。如果当前方法无返回值 `void`,则值为 null | -| throwExp | 本次调用抛出的异常。当且仅当 `isThrow==true` 成立时有效,表明方法调用是以抛出异常的方式结束。 | +| 变量名 | 变量解释 | +|----------:|:----------------------------------------------------------------------------------------------------------------------| +| loader | 本次调用类所在的 ClassLoader | +| clazz | 本次调用类的 Class 引用 | +| method | 本次调用方法反射引用 | +| target | 本次调用类的实例 | +| params | 本次调用参数列表,这是一个数组,如果方法是无参方法则为空数组 | +| returnObj | 本次调用返回的对象。当且仅当 `isReturn==true` 成立时候有效,表明方法调用是以正常返回的方式结束。如果当前方法无返回值 `void`,则值为 null | +| throwExp | 本次调用抛出的异常。当且仅当 `isThrow==true` 成立时有效,表明方法调用是以抛出异常的方式结束。 | +| varMap | 本地变量(局部变量),仅在 **line** 命令中可用 | | isBefore | 辅助判断标记,当前的通知节点有可能是在方法一开始就通知,此时 `isBefore==true` 成立,同时 `isThrow==false` 和 `isReturn==false`,因为在方法刚开始时,还无法确定方法调用将会如何结束。 | -| isThrow | 辅助判断标记,当前的方法调用以抛异常的形式结束。 | -| isReturn | 辅助判断标记,当前的方法调用以正常返回的形式结束。 | +| isThrow | 辅助判断标记,当前的方法调用以抛异常的形式结束。 | +| isReturn | 辅助判断标记,当前的方法调用以正常返回的形式结束。 | 所有变量都可以在表达式中直接使用,如果在表达式中编写了不符合 OGNL 脚本语法或者引入了不在表格中的变量,则退出命令的执行;用户可以根据当前的异常信息修正`条件表达式`或`观察表达式` diff --git a/site/docs/doc/commands.md b/site/docs/doc/commands.md index 41b87c3685c..a6ce9461164 100644 --- a/site/docs/doc/commands.md +++ b/site/docs/doc/commands.md @@ -28,7 +28,7 @@ - [sc](sc.md) - 查看 JVM 已加载的类信息 - [sm](sm.md) - 查看已加载类的方法信息 -## monitor/watch/trace 相关 +## monitor/watch/trace/line 相关 ::: warning 请注意,这些命令,都通过字节码增强技术来实现的,会在指定类的方法中插入一些切面来实现数据统计和观测,因此在线上、预发使用时,请尽量明确需要观测的类、方法以及条件,诊断结束要执行 `stop` 或将增强过的类执行 `reset` 命令。 @@ -39,6 +39,7 @@ - [trace](trace.md) - 方法内部调用路径,并输出方法路径上的每个节点上耗时 - [tt](tt.md) - 方法执行数据的时空隧道,记录下指定方法每次调用的入参和返回信息,并能对这些不同的时间下调用进行观测 - [watch](watch.md) - 方法执行数据观测 +- [line](line.md) - 观测方法执行到指定位置时的数据(支持本地变量) ## profiler/火焰图 diff --git a/site/docs/doc/line.md b/site/docs/doc/line.md new file mode 100644 index 00000000000..f4dbc65cd9e --- /dev/null +++ b/site/docs/doc/line.md @@ -0,0 +1,286 @@ +# line + +::: tip +观测当函数执行到指定位置时的数据状态 +::: + +让你能方便地观察到程序执行到指定位置时的数据状况。能观察到的范围为:`本地变量(局部变量)`、`入参`,如果是实例方法,还能观测到`当前对象`,通过编写 OGNL 表达式进行对应变量的查看。 + +## 参数说明 + +line 的参数如下 + +| 参数名称 | 参数说明 | +|--------------------:|:--------------------------------------------------------| +| _class-pattern_ | 类名表达式匹配 | +| _method-pattern_ | 函数名表达式匹配 | +| _location_ | 行号(**LineNumber**)或者特殊行标识(**LineCode**) | +| _express_ | 观察表达式,默认值:`varMap` | +| _condition-express_ | 条件表达式 | +| [x:] | 指定输出结果的属性遍历深度,默认为 1,最大值是 4 | + +这里重点要说明的是**location**参数,它的作用是确定要在哪个位置进行观测,它的值有两种类型: + +- **LineNumber**:行号,就是通俗意义中的源文件里的第几行,如下方的 `print(number, primeFactors);` 的**LineNumber**=**25**,其代表的含义是在**第25行**执行之前进行观测 +```java +package demo; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +public class MathGame { + private static Random random = new Random(); + + private int illegalArgumentCount = 0; + + public static void main(String[] args) throws InterruptedException { + MathGame game = new MathGame(); + while (true) { + game.run(); + TimeUnit.SECONDS.sleep(1); + } + } + + public void run() throws InterruptedException { + try { + int number = random.nextInt() / 10000; + List primeFactors = primeFactors(number); + print(number, primeFactors); + + } catch (Exception e) { + System.out.println(String.format("illegalArgumentCount:%3d, ", illegalArgumentCount) + e.getMessage()); + } + } +} +``` +::: tip +**LineNumber**=**-1**时,则表示在函数结束前观测 +::: + +::: tip +无法使用**LineNumber**=**26**,因为该行号无法在编译后的class文件中找到 +::: +- **LineCode**:特殊行标识,形如 **abcd-1**,它是由arthas生成的标识,只能通过`jad --lineCode`查看,详细使用可参考下方的 [使用LineCode进行观测](#使用linecode进行观测) + +另外观察表达式是由 ognl 表达式组成,所以同watch命令类似,你也可以这样写`"{params,varMap}"`,只要是一个合法的 ognl 表达式,都能被正常支持。 + +观察的维度和watch有所差异,增加了`varMap`,但没有 `throwExp`、`returnObj`。 + +**特别说明**: + +- 为什么需要用到**LineCode**呢? + + 因为在kotlin中,或者一些复杂的表达式中,不一定能以行号定位到期望观测的位置,而arthas生成的**LineCode**则可以提供更细一层级的定位。 + +- 为什么要限定在某函数内部来指定位置呢? + + 首先绝大部分排查问题都是针对某具体函数的; + 其次若不指定函数,增强代码或者生成**LineCode**时可能需要遍历整个类,成本会比较高; + 再者在日常实践中,本地代码和arthas的宿主代码不一定一致,指定函数也能帮助用户尽早察觉。 + + +## 使用参考 + +### 启动 Demo + +启动[快速入门](quick-start.md)里的`math-game`。 + +### 观察函数执行到指定位置时的本地变量 + +::: tip +观察表达式,默认值是`varMap` +::: + +```bash +$ line demo.MathGame run 25 -x 2 +Press Q or Ctrl+C to abort. +Affect(class count: 1 , method count: 1) cost in 17 ms, listenerId: 2 +method=demo.MathGame.run line=25 +ts=2024-06-21 09:57:34.452; result=@HashMap[ + @String[primeFactors]:@ArrayList[ + @Integer[2], + @Integer[7], + @Integer[7], + @Integer[991], + ], + @String[number]:@Integer[97118], +] +``` + +- 上面的结果里展示的是,当函数执行到第**25**行之前时,本地变量 `primeFactors` 和 `number` 的值 + +### 观察函数执行到指定位置时的参数和本地变量 + +```bash +$ line demo.MathGame run 25 "{params,varMap}" -x 2 +Press Q or Ctrl+C to abort. +Affect(class count: 1 , method count: 1) cost in 16 ms, listenerId: 3 +method=demo.MathGame.run line=25 +ts=2024-06-21 10:02:07.295; result=@ArrayList[ + @Object[][isEmpty=true;size=0], + @HashMap[ + @String[primeFactors]:@ArrayList[isEmpty=false;size=4], + @String[number]:@Integer[44668], + ], +] +``` + +- 同watch命令一致,我们可以通过`params`获取到参数值,因为该函数的参数为空,所以是`Object[]` + +### 调整`-x`的值,观察具体的本地变量值 + +```bash +$ line demo.MathGame run 25 "{varMap}" -x 3 +Press Q or Ctrl+C to abort. +Affect(class count: 1 , method count: 1) cost in 19 ms, listenerId: 4 +method=demo.MathGame.run line=25 +ts=2024-06-21 10:04:09.641; result=@ArrayList[ + @HashMap[ + @String[primeFactors]:@ArrayList[ + @Integer[2], + @Integer[2], + @Integer[3], + @Integer[3], + @Integer[17], + @Integer[79], + ], + @String[number]:@Integer[48348], + ], +] +``` + +- `-x`表示遍历深度,可以调整来打印具体的参数和结果内容,默认值是 1。 +- `-x`最大值是 4,防止展开结果占用太多内存。用户可以在`ognl`表达式里指定更具体的 field。 + +### 条件表达式的例子 + +```bash +$ line demo.MathGame run 25 "{varMap}" "varMap[\"primeFactors\"][0]==2" -x 3 +Press Q or Ctrl+C to abort. +Affect(class count: 1 , method count: 1) cost in 20 ms, listenerId: 12 +method=demo.MathGame.run line=25 +ts=2024-06-21 10:08:03.392; result=@ArrayList[ + @HashMap[ + @String[primeFactors]:@ArrayList[ + @Integer[2], + @Integer[7], + @Integer[31], + @Integer[251], + ], + @String[number]:@Integer[108934], + ], +] + +``` + +- 只有满足条件的调用,才会有响应。 + +### 观察当前对象中的属性 + +如果想查看函数指定位置运行前,当前对象中的属性,可以使用`target`关键字,代表当前对象 + +```bash +$ line demo.MathGame run 25 'target' +Press Q or Ctrl+C to abort. +Affect(class count: 1 , method count: 1) cost in 18 ms, listenerId: 13 +method=demo.MathGame.run line=25 +ts=2024-06-21 10:10:00.761; result=@MathGame[ + random=@Random[java.util.Random@bebdb06], + illegalArgumentCount=@Integer[1677], +] +``` + +然后使用`target.field_name`访问当前对象的某个属性 + +```bash +$ line demo.MathGame run 25 'target.illegalArgumentCount' +Press Q or Ctrl+C to abort. +Affect(class count: 1 , method count: 1) cost in 20 ms, listenerId: 14 +method=demo.MathGame.run line=25 +ts=2024-06-21 10:10:28.838; result=@Integer[1687] +``` + +### 使用LineCode进行观测 +::: tip +当我们无法通过行号获得想要观测的位置时,才需要利用**LineCode**进行指定 +::: + +先使用 `jad --lineCode ` 来获取具体的映射位置 + +```bash +$ jad --lineCode demo.MathGame run +ClassLoader: ++-sun.misc.Launcher$AppClassLoader@18b4aac2 + +-sun.misc.Launcher$ExtClassLoader@1540e19d + +Location: +/Users/xxxxx/IdeaProjects/github/arthas/math-game/target/classes/ + + public void run() throws InterruptedException { + try { +/*23*/ int number = random.nextInt() / 10000; +/*24*/ List primeFactors = this.primeFactors(number); +/*25*/ MathGame.print(number, primeFactors); + } + catch (Exception e) { +/*28*/ System.out.println(String.format("illegalArgumentCount:%3d, ", this.illegalArgumentCount) + e.getMessage()); + } + } + +------------------------- lineCode location ------------------------- +format: /*LineNumber*/ (LineCode)-> Instruction +/*23 */ (aacd-1)-> + invoke-method:java/util/Random#nextInt:()I +/*23 */ (5918-1)-> + assign-variable:e +/*24 */ (653f-1)-> + invoke-method:demo/MathGame#primeFactors:(I)Ljava/util/List; +/*24 */ (d961-1)-> + assign-variable:primeFactors +/*25 */ (416e-1)-> + invoke-method:demo/MathGame#print:(ILjava/util/List;)V +/*27 */ (5918-2)-> + assign-variable:e +/*28 */ (2455-1)-> + invoke-method:java/lang/StringBuilder#:()V +/*28 */ (4076-1)-> + invoke-method:java/lang/Integer#valueOf:(I)Ljava/lang/Integer; +/*28 */ (b6e4-1)-> + invoke-method:java/lang/String#format:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String; +/*28 */ (850c-1)-> + invoke-method:java/lang/StringBuilder#append:(Ljava/lang/String;)Ljava/lang/StringBuilder; +/*28 */ (a53d-1)-> + invoke-method:java/lang/Exception#getMessage:()Ljava/lang/String; +/*28 */ (850c-2)-> + invoke-method:java/lang/StringBuilder#append:(Ljava/lang/String;)Ljava/lang/StringBuilder; +/*28 */ (f7bb-1)-> + invoke-method:java/lang/StringBuilder#toString:()Ljava/lang/String; +/*28 */ (2f1b-1)-> + invoke-method:java/io/PrintStream#println:(Ljava/lang/String;)V +Affect(row-cnt:1) cost in 103 ms. +``` +上边结果中 `--- lineCode location ---` 分割线以下的就是**LineCode**定位相关的信息: +- `/*25 */` 表示该指令相邻的行号 +- `(416e-1)` 表示特殊行标识,也就是**LineCode**,标识在具体的哪个位置进行观测 +- `invoke-method: xxx` 则代表该指令是调用某个方法 +- `assign-variable: xxx` 则代表该指令是给某个变量赋值 + +以下举例为在 `invoke-method:demo/MathGame#print:(ILjava/util/List;)V`执行前观测: +```bash +$ line demo.MathGame run 416e-1 -x 2 +Press Q or Ctrl+C to abort. +Affect(class count: 1 , method count: 1) cost in 21 ms, listenerId: 16 +method=demo.MathGame.run line=416e-1 +ts=2024-06-21 10:37:50.531; result=@HashMap[ + @String[primeFactors]:@ArrayList[ + @Integer[5], + @Integer[5], + @Integer[6907], + ], + @String[number]:@Integer[172675], +] +``` +- 通过 `jad --lineCode` 我们看到 **LineCode=416e-1** 位于 `invoke-method:demo/MathGame#print:(ILjava/util/List;)V` 上方,这就是我们观测的位置 +- 另外结合行号、源码及指令顺序可知,`invoke-method:demo/MathGame#print:(ILjava/util/List;)V` 指令对应的源码是`print(number, primeFactors);` diff --git a/site/docs/en/doc/advice-class.md b/site/docs/en/doc/advice-class.md index f78f99cfe60..87a8eae6f4b 100644 --- a/site/docs/en/doc/advice-class.md +++ b/site/docs/en/doc/advice-class.md @@ -12,6 +12,7 @@ public class Advice { private final Object[] params; private final Object returnObj; private final Throwable throwExp; + private final Map varMap; private final boolean isBefore; private final boolean isThrow; private final boolean isReturn; @@ -23,7 +24,7 @@ public class Advice { Description for the variables in the class `Advice`: | Name | Specification | -| --------: | :------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| --------: |:--------------------------------------------------------------------------------------------------------------------------------------------------------------------| | loader | the class loader for the current called class | | clazz | the reference to the current called class | | method | the reference to the current called method | @@ -31,6 +32,7 @@ Description for the variables in the class `Advice`: | params | the parameters for the current call, which is an array (when there's no parameter, it will be an empty array) | | returnObj | the return value from the current call - only available when the method call returns normally (`isReturn==true`), and `null` is for `void` return value | | throwExp | the exceptions thrown from the current call - only available when the method call throws exception (`isThrow==true`) | +| varMap | Local variables map, only available in the **line** command. | | isBefore | flag to indicate the method is about to execute. `isBefore==true` but `isThrow==false` and `isReturn==false` since it's no way to know how the method call will end | | isThrow | flag to indicate the method call ends with exception thrown | | isReturn | flag to indicate the method call ends normally without exception thrown | diff --git a/site/docs/en/doc/commands.md b/site/docs/en/doc/commands.md index 799b6418bce..20f0eba2a93 100644 --- a/site/docs/en/doc/commands.md +++ b/site/docs/en/doc/commands.md @@ -28,7 +28,7 @@ - [sc](sc.md) - check the info for the classes loaded by JVM - [sm](sm.md) - check methods info for the loaded classes -## monitor/watch/trace - related +## monitor/watch/trace/line - related ::: warning **Attention**: commands here are taking advantage of byte-code-injection, which means we are injecting some [aspects](https://en.wikipedia.org/wiki/Aspect-oriented_programming) into the current classes for monitoring and statistics purpose. Therefore, when using it for online troubleshooting in your production environment, you'd better **explicitly specify** classes/methods/criteria, and remember to remove the injected code by `stop` or `reset`. @@ -39,6 +39,7 @@ - [trace](trace.md) - trace the execution time of specified method invocation - [tt](tt.md) - time tunnel, record the arguments and returned value for the methods and replay - [watch](watch.md) - display the input/output parameter, return object, and thrown exception of specified method invocation +- [line](line.md) - Observe the data when the method executes to a specified position (supports local variables). ## profiler/flame graph diff --git a/site/docs/en/doc/line.md b/site/docs/en/doc/line.md new file mode 100644 index 00000000000..72106cd006b --- /dev/null +++ b/site/docs/en/doc/line.md @@ -0,0 +1,270 @@ +# line + +::: tip +Observe the data when the function executes to a specified position. +::: + +This allows you to easily observe the data at a specific point during function execution. +The observable range includes: `local variables`, `parameters`, and if it's an instance method, you can also observe `the current object` by writing OGNL expressions. + +## Parameter Description + +The parameters for line are as follows: + +| Parameter Name | Description | +|-----------------------------:|:------------------------------------------------| +| _class-pattern_ | pattern for the class name | +| _method-pattern_ | pattern for the method name | +| _location_ | **LineNumber** or **LineCode** | +| _express_ | expression to watch, default value `varMap` | +| _condition-express_ | condition expression to filter | +| [x:] | the depth to print the specified property with default value: 1, the max value is 4 | + +The **location** parameter is crucial as it determines where to observe. It has two types of values: + +- **LineNumber**:Line number, which refers to the specific line in the source file. For example, in the code below, `print(number, primeFactors);` has **LineNumber=25**, meaning observation occurs before executing **line 25**. +```java +package demo; + +import java.util.ArrayList; +import java.util.List; +import java.util.Random; +import java.util.concurrent.TimeUnit; + +public class MathGame { + private static Random random = new Random(); + + private int illegalArgumentCount = 0; + + public static void main(String[] args) throws InterruptedException { + MathGame game = new MathGame(); + while (true) { + game.run(); + TimeUnit.SECONDS.sleep(1); + } + } + + public void run() throws InterruptedException { + try { + int number = random.nextInt() / 10000; + List primeFactors = primeFactors(number); + print(number, primeFactors); + + } catch (Exception e) { + System.out.println(String.format("illegalArgumentCount:%3d, ", illegalArgumentCount) + e.getMessage()); + } + } +} +``` +::: tip +When **LineNumber**=**-1**, it means observation occurs before the function ends. +::: + +::: tip +You cannot use**LineNumber**=**26**, because this line number cannot be found in the classes file. +::: + +- **LineCode**:Special line identifier like **abcd-1**,generated by Arthas and can only be founded using `jad --lineCode`,For detailed usage refer to [Using LineCode for Observation](#Using-LineCode-for-Observation) + +Additionally, observation expressions are composed of OGNL expressions. Similar to watch commands, you can write `"{params,varMap}"`,any valid OGNL expression will be supported. + +Observation dimensions differ from watch command; it adds `varMap`,but lacks `throwExp`、`returnObj`. + +**Special Notes**: + +- Why we need to use **LineCode** ? + + Because in Kotlin or some complex java expressions, it's not always possible to locate desired observation by line number. Arthas-generated **LineCode** provides finer-grained positioning. + +- Why limit observations within a specific function? + + Most troubleshooting targets specific functions; + Without specifying functions, enhancing code or generating **LineCode** might require traversing entire classes which could be costly; + In practice local source code and the code actually running may not always match; specifying functions helps users detect discrepancies early. + + +## Usage + +### Start Demo + +Start `math-game` in [Quick Start](quick-start.md). + +### Observe `Local Variables` at Specific Position During Function Execution + +::: tip +Observation expression defaults to `varMap` +::: + +```bash +$ line demo.MathGame run 25 -x 2 +Press Q or Ctrl+C to abort. +Affect(class count: 1 , method count: 1) cost in 17 ms, listenerId: 2 +method=demo.MathGame.run line=25 +ts=2024-06-21 09:57:34.452; result=@HashMap[ + @String[primeFactors]:@ArrayList[ + @Integer[2], + @Integer[7], + @Integer[7], + @Integer[991], + ], + @String[number]:@Integer[97118], +] +``` + +- The above result shows that before executing line **25** ,local variables `primeFactors` and `number` have these values. + +### Use `-x` to check more details + +```bash +$ line demo.MathGame run 25 "{varMap}" -x 3 +Press Q or Ctrl+C to abort. +Affect(class count: 1 , method count: 1) cost in 19 ms, listenerId: 4 +method=demo.MathGame.run line=25 +ts=2024-06-21 10:04:09.641; result=@ArrayList[ + @HashMap[ + @String[primeFactors]:@ArrayList[ + @Integer[2], + @Integer[2], + @Integer[3], + @Integer[3], + @Integer[17], + @Integer[79], + ], + @String[number]:@Integer[48348], + ], +] +``` + +- `-x`: Expand level of object (1 by default) +- The max value of `-x` is 4, to prevent the expansion result taking up too much memory. Users can specify the field in the `ognl` expression. + +### Use condition expressions to locate specific call + +```bash +$ line demo.MathGame run 25 "{varMap}" "varMap[\"primeFactors\"][0]==2" -x 3 +Press Q or Ctrl+C to abort. +Affect(class count: 1 , method count: 1) cost in 20 ms, listenerId: 12 +method=demo.MathGame.run line=25 +ts=2024-06-21 10:08:03.392; result=@ArrayList[ + @HashMap[ + @String[primeFactors]:@ArrayList[ + @Integer[2], + @Integer[7], + @Integer[31], + @Integer[251], + ], + @String[number]:@Integer[108934], + ], +] + +``` + +- Only calls that meet the conditions will receive a response. + +### Check the field of the target object + +- `target` is the `this` object in java. + +```bash +$ line demo.MathGame run 25 'target' +Press Q or Ctrl+C to abort. +Affect(class count: 1 , method count: 1) cost in 18 ms, listenerId: 13 +method=demo.MathGame.run line=25 +ts=2024-06-21 10:10:00.761; result=@MathGame[ + random=@Random[java.util.Random@bebdb06], + illegalArgumentCount=@Integer[1677], +] +``` + +- `target.field_name`: the field of the current object. + +```bash +$ line demo.MathGame run 25 'target.illegalArgumentCount' +Press Q or Ctrl+C to abort. +Affect(class count: 1 , method count: 1) cost in 20 ms, listenerId: 14 +method=demo.MathGame.run line=25 +ts=2024-06-21 10:10:28.838; result=@Integer[1687] +``` + +### Using LineCode for Observation +::: tip +When we cannot locate the desired observation position through the line number, we need to use **LineCode** for specification. +::: + +First, use `jad --lineCode ` to locate the specific mapping location. + +```bash +$ jad --lineCode demo.MathGame run +ClassLoader: ++-sun.misc.Launcher$AppClassLoader@18b4aac2 + +-sun.misc.Launcher$ExtClassLoader@1540e19d + +Location: +/Users/xxxxx/IdeaProjects/github/arthas/math-game/target/classes/ + + public void run() throws InterruptedException { + try { +/*23*/ int number = random.nextInt() / 10000; +/*24*/ List primeFactors = this.primeFactors(number); +/*25*/ MathGame.print(number, primeFactors); + } + catch (Exception e) { +/*28*/ System.out.println(String.format("illegalArgumentCount:%3d, ", this.illegalArgumentCount) + e.getMessage()); + } + } + +------------------------- lineCode location ------------------------- +format: /*LineNumber*/ (LineCode)-> Instruction +/*23 */ (aacd-1)-> + invoke-method:java/util/Random#nextInt:()I +/*23 */ (5918-1)-> + assign-variable:e +/*24 */ (653f-1)-> + invoke-method:demo/MathGame#primeFactors:(I)Ljava/util/List; +/*24 */ (d961-1)-> + assign-variable:primeFactors +/*25 */ (416e-1)-> + invoke-method:demo/MathGame#print:(ILjava/util/List;)V +/*27 */ (5918-2)-> + assign-variable:e +/*28 */ (2455-1)-> + invoke-method:java/lang/StringBuilder#:()V +/*28 */ (4076-1)-> + invoke-method:java/lang/Integer#valueOf:(I)Ljava/lang/Integer; +/*28 */ (b6e4-1)-> + invoke-method:java/lang/String#format:(Ljava/lang/String;[Ljava/lang/Object;)Ljava/lang/String; +/*28 */ (850c-1)-> + invoke-method:java/lang/StringBuilder#append:(Ljava/lang/String;)Ljava/lang/StringBuilder; +/*28 */ (a53d-1)-> + invoke-method:java/lang/Exception#getMessage:()Ljava/lang/String; +/*28 */ (850c-2)-> + invoke-method:java/lang/StringBuilder#append:(Ljava/lang/String;)Ljava/lang/StringBuilder; +/*28 */ (f7bb-1)-> + invoke-method:java/lang/StringBuilder#toString:()Ljava/lang/String; +/*28 */ (2f1b-1)-> + invoke-method:java/io/PrintStream#println:(Ljava/lang/String;)V +Affect(row-cnt:1) cost in 103 ms. +``` +Below the `--- lineCode location ---` separator in the above result is information related to **LineCode** positioning: +- `/*25 */` indicates the line number adjacent to this instruction. +- `(416e-1)` indicates a special line identifier, which is **LineCode**, specifying the exact position for observation. +- `invoke-method: xxx` represents that this instruction is calling a method. +- `assign-variable: xxx` represents that this instruction is assigning a value to a variable. + +The following example observes before the execution of `invoke-method:demo/MathGame#print:(ILjava/util/List;)V`: +```bash +$ line demo.MathGame run 416e-1 -x 2 +Press Q or Ctrl+C to abort. +Affect(class count: 1 , method count: 1) cost in 21 ms, listenerId: 16 +method=demo.MathGame.run line=416e-1 +ts=2024-06-21 10:37:50.531; result=@HashMap[ + @String[primeFactors]:@ArrayList[ + @Integer[5], + @Integer[5], + @Integer[6907], + ], + @String[number]:@Integer[172675], +] +``` +- By using `jad --lineCode` we see that **LineCode=416e-1** is located above `invoke-method:demo/MathGame#print:(ILjava/util/List;)V` ,which is our observation point. +- Additionally, combining the line number, source code, and instruction sequence, we know that the instruction `invoke-method:demo/MathGame#print:(ILjava/util/List;)V` corresponds to the source code `print(number, primeFactors);`