【问题标题】:How to track a method result in Java如何在 Java 中跟踪方法结果
【发布时间】:2018-04-22 18:47:06
【问题描述】:

简而言之,我正在开发一个系统,该系统能够为您提供有关执行 java 程序所提供的结果的信息。考虑过下面的问题,不知道用java能不能解决。

我有以下课程:

public class ClassA {
    ClassB classB= new ClassB();
    public Integer method1(){
       return classB.method2();
    }
}

public class ClassB {
    ClassC classC = new ClassC();
    public Integer method2() {
        return this.classC.method3() + this.classC.method4();
    }
}

public class ClassC {
    public Integer method3() {
        return 3;
    }
    public Integer method4() {
        return 4;
    }
}

到目前为止,我可以通过使用动态代理来捕获方法的每次调用。特别是,我使用包 java.lang.reflect 中的 Proxy 和 InvocationHandler 对象。这是我遵循的示例(https://www.concretepage.com/java/dynamic-proxy-with-proxy-and-invocationhandler-in-java)。

我的问题是,如果有人知道我如何提供以下信息: “method1()的返回是由method2()的返回产生的,method2()的返回又是由method3()的返回和method4()的返回产生的”。

【问题讨论】:

  • 有一个调试器api和其他相关的低级api。在程序本身运行的运行时中,您可以在内部执行的操作相当有限。
  • 如果您可以控制 JVM,另一个选项是检测 (java.lang.instrumentation)。当你的类被加载时,你可以将代码注入到每个构建调用树的方法中,如果你愿意的话(带有相关的缩进),你会得到类似:method1 -> method2 -> method3,method4。因此,您可以在填充所述调用树的每个方法的开头添加一些代码,并在方法的末尾从树中弹出子项。

标签: java reflection proxy-pattern


【解决方案1】:

您需要使用 ThreadLocals。您的线程本地将有一个 Map 将由每个方法填充。这是一个示例代码:

public static void main(String... args){
        try{
            new ClassA().method1();

            TrackingThreadLocal.tracker.get().entrySet().stream().forEach( (e) -> System.out.println( 
            "the return of " + e.getKey() + " is generated from the return of " + e.getValue().stream().collect( Collectors.joining(", ") ) ) );

        }finally{
            //make sut to clean up to avoid ThreadLocal memoty leak
            TrackingThreadLocal.tracker.remove();
        }
    }

    public class TrackingThreadLocal{

        public static ThreadLocal< Map<String, List<String> > > tracker = new ThreadLocal< Map< String, List<String> > >(){
            @Override 
            public Map< String, List<String> > initialValue() {
                return new HashMap<>();
            }
        };
    }

    public class ClassA {
        ClassB classB= new ClassB();
        public Integer method1(){
           TrackingThreadLocal.tracker.get().put("method1", Arrays.asList("method2") );
           return classB.method2();
        }
    }

    public class ClassB {
        ClassC classC = new ClassC();
        public Integer method2() {
            TrackingThreadLocal.tracker.get().put( "method2",  Arrays.asList("method3", "method4") );
            return this.classC.method3() + this.classC.method4();
        }
    }

    public class ClassC {
        public Integer method3() {
            return 3;
        }
        public Integer method4() {
            return 4;
        }
    }

【讨论】:

  • 为什么选择 ThreadLocal? OP没有提到线程,所以我宁愿先假设一个单线程解决方案......另外,这会处理方法之间的传递依赖关系吗? (即“方法 1 是方法 2 的结果,它是方法 3 的结果”)
  • @david a. ThreadLocals 适用于我描述的确切情况。它们可以在单线程或多线程代码中使用。
  • 在单线程情况下,线程局部变量将不是必需的。一个简单的地图就可以满足您所描述的解决方案。
【解决方案2】:

我以前用过仪器解决过类似的问题。

免责声明:只有在您可以控制 JVM 并且可以指定它使用 javaagent 运行时,您才能执行以下操作。

代理实现起来相当简单,您只需要一个实现带有premain(String, java.lang.Instrumentation) 签名的方法的类。下面是一个例子:

import javassist.ClassPool;
import javassist.CtClass;
import javassist.CtMethod;

import java.lang.instrumentation.ClassFileTransformer;
import java.lang.instrumentation.IllegalClassFormatException;
import java.lang.instrumentation.Instrumentation;
import java.security.ProtectionDomain;

public class MyAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        inst.addTransformer(new ClassTransformer() {
            public byte[] transform(ClassLoader loader, String className,
                    Class<?> classBeingRedefined, ProtectionDomain protectionDomain,
                    byte[] classfileBuffer) throws IllegalClassFormatException {
            // if you want to target specific classes or packages you can filter
            // on the class name, just remember that it is the JVM class format string
            if(className.startWith("com/my/package/SomeThing")) {
                // javassist provides methods to access classes and generate bytecode
                ClassPool cp = ClassPool.getDefault();
                // you can access the class with the following
                CtClass cc = cp.get("com.my.package.MyClass$InnerClass");
                // access specific methods with
                CtMethod m = cc.getDeclaredMethod("someMethod");
                m.setBody("{MyCallTree tree = com.my.package.TreeSingleton.getTree();\ntree.addCall(" + m.getLongName + ");\n}");
                return cc.toByteCode();
            }
            else {
                // return null so that you don't break methods or classes you
                // don't want to
                return null;
            }
        });
    }
}

在上面的 sn-p 中,我将整个方法体替换为作为字符串传递给CtMethod.setBody 的代码。然而,你几乎可以用 javassist 做任何事情,前置代码,附加代码等。关于使用 javassist 的详细教程可以找到here。这是一个非常强大的库。

如何实现代码来构建调用信息的细节实际上只是 Java 代码,可能首先将代码编写为项目的一部分,然后只需使用在 premain 方法中执行步骤的位。您甚至可以让它编写tsolakp 在他的回答中建议的代码。

下一步是将上述代码打包成一个jar文件,然后在启动时将其注入到你的JVM中,如this答案中所做的那样。

【讨论】:

    【解决方案3】:

    我想到的一件事是检索线程的堆栈跟踪,看看你能做什么。

    您可以使用例如。 Thread.currentThread().getStackTrace() 方法并接收StackTraceElement 的数组,最后使用它们接收带有getMethodName() 方法的方法名称。

    这只是一个想法,我不确定你是否能得到你想要的信息。

    【讨论】:

    • 不幸的是,堆栈跟踪只会产生method1 -&gt; method2 -&gt; method3method1 -&gt; method2 -&gt; method4,并且只有在堆栈跟踪分别在method3method4 中调用时才会产生。我相信 OP 想要评估 Java 代码,这是一种不同的鱼。我之前曾与JavaParser 合作过,但是您需要正确理解Java 的EBNF 和语言结构。语言标记的分析可能会变得非常复杂。
    猜你喜欢
    • 2015-06-05
    • 1970-01-01
    • 2012-03-01
    • 1970-01-01
    • 2019-09-13
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多