【问题标题】:Find out if one method could call another找出一种方法是否可以调用另一种方法
【发布时间】:2026-02-04 21:10:01
【问题描述】:

我正在尝试弄清楚如何获取 Java pojo,并分析它的方法以了解它可以调用的所有其他方法和函数。例如,这里是输出的硬编码示例。我怎样才能使这个通用?我需要以编程方式分析 Java 对象,以确定如果执行它们可以调用哪些方法。示例:

package com.example.analyze;

public class Main
{

    private static class Foo {

        public void foo(int value, Bar bar) {
            if(value > 5)
                bar.gaz();
        }
    }

    private static class Bar {

        public void gaz() {
            System.out.println("gaz");
        }
    }

    private static class Analyzer {

        public void analyze(Object object){
            System.out.println("Object method foo could call Bar method gaz");
        }

    }

    public static void main(String[] args)
    {
        Foo foo = new Foo();
        Analyzer analyzer = new Analyzer();
        analyzer.analyze(foo);
    }
}

【问题讨论】:

标签: java static-analysis


【解决方案1】:

你需要构建一个调用图,然后询问调用图中是否有两个节点(调用者和被调用者)相连。这不是一件容易的事。

你需要做什么:

  • 解析构成您的应用程序的源代码。 Java 解析器相对容易找到。 Java 1.8 解析器,不是那么容易,但是有一个隐藏在您可以使用的 Java 编译器中,另一个隐藏在 Eclipse JDT 中;我的公司还提供了我们的 DMS 工具包。
  • 为相同构建抽象语法树;你需要代码结构。 Java 编译器、JDT 和 DMS 都可以做到这一点。
  • 执行名称和类型解析。你需要知道每个符号的定义是什么意思。 Java 编译器肯定会一次为一个编译单元执行此操作。 JDT 可以对许多文件执行此操作;我对此没有太多经验。 DMS 可以一次对非常大的 Java 源文件集执行此操作。
  • 现在您需要进行(对象)指向分析:您想知道,对于任何(对象值)字段,它可能指向哪些特定实例对象;这最终会告诉你它可以用来触发什么方法。您将通过检查 AST 和说明每个符号含义的符号表定义来获取此任务的信息。如果你看到 X.f=new foo;您知道 X 中的 f 可以指向 foo,这是一个基本事实。泛型和类型擦除使这变得混乱。如果你看到 Y.g=Z.h,你就知道 Y 中的 g 可以指向 Z 中的 h 可以指向的任何东西;当然 Z 可能是继承自 Z 的类。如果您看到 Y.g=a[...],那么您知道 Y 中的 g 可以指向可能已分配给数组 a 的任何对象。如果你看到 Y.g=bar(...) 那么你知道 Y 中的 g 可以指向 bar 可能返回的任何东西;不幸的是,您现在需要一个调用图来狭隘地回答这个问题。您可以通过各种方式对此进行近似,以获得保守的答案。既然您知道值是如何相互关联的,那么您必须对这个集合进行传递闭包,以了解每个 Y 中的每个 g 可以指向的内容。如果您考虑到各个方法的控制和数据流,您可以获得更准确的答案,但这需要更多的构建机制。 (这里有更多关于points-to analysis 的详细信息。)Java 编译器在编译时计算其中的一些信息,但不是针对整个源文件系统;请记住,它一次处理一个源文件。我认为 JDT 根本不会尝试这样做。我们的 DMS(还)没有这样做,但我们已经为 2600 万行的 C 代码系统做到了;这可以说是一个更难的问题,因为人们用指针做各种滥用的事情,包括撒谎的演员表。
  • 终于可以构建调用图了。对于每个方法,构造一个调用图节点。对于方法中的每个调用站点,确定其被调用者集合并将调用节点链接到被调用节点。上一步收集了提供这些链接所需的信息。

[您也许可以使用Wala 避免上述解析/名称类型解析部分,这基本上是通过执行上述大部分操作来构建的]。

通过调用图,如果你想知道A是否可以调用B,在调用图中找到A的节点,看看是否有到B的路径。

这里的另一个注释表明这是一个编译器类的 6 个月任务。我认为对于一个有经验的编译器人员来说需要 6 个月或更长时间(而且我们还没有解决诸如类加载器和反射调用之类的令人讨厌的问题)。

我认为您最好为此找到其他人已经构建的解决方案。可能有人有;不太可能很容易找到,或者她想放弃它。您可能会在大学中找到实现;有各种由学者撰写的论文(并由原型支持)来计算对象图。不利的一面是所有这些系统都是原型,并且是由小型、无偿的毕业生团队构建的,他们通常不能处理所有的边缘情况,更不用说最新版本的 Java(lambdas,任何人? )

【讨论】:

  • 所以我写的解决方案很像这样。基本上解析字节码,寻找invoke* 调用,并将节点和有向边添加到图结构中。然后方法依赖关系是在其出站链接上的节点上的深度优先搜索。下面史蒂夫的答案使用javassist,我认为一个完整的答案是两者兼而有之。现在我正在修改原型以使用 ASM 而不是 Javap,如果您对这个问题有任何想法...*.com/questions/26575111/…
  • @DavidWilliams:您的图表似乎是 instance-method-M 调用 abstract-method-x。想象一下,我有一个类 X,它有一个(可能是抽象的)方法 x,类 X1 和类 X2 都从 X 继承,其中方法 x' 和 x'' 覆盖 x。您构建图表的方式似乎只知道方法 m 调用 some x,但不具体是 x、x' 或 x''。那是您真正想要的调用图吗?如果您想了解更多信息,您必须知道调用站点使用了 X、X1 或​​ X2 中的哪个;这就是为什么我说你需要“指向”分析。
【解决方案2】:

您可以使用ASM api 来查找有关类文件的信息,示例代码提供了有关如何获取方法详细信息的合理思路。

分析器类

package sample.code.analyze;

import java.io.IOException;

import org.objectweb.asm.ClassReader;
import org.objectweb.asm.ClassVisitor;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.Opcodes;

public class Analyzer {
    public void analyze(Object object) {
        ClassVisitor cv = new ClassVisitor(Opcodes.ASM4) {
            @Override
            public MethodVisitor visitMethod(int access, String name,
                    String desc, String signature, String[] exceptions) {

                System.out.println("Method: " + name + " -- " + desc);
                return new MethodVisitor(Opcodes.ASM4) {
                    @Override
                    public void visitMethodInsn(int opcode, String owner,
                            String name, String desc, boolean arg4) {
                        System.out.println("--  opcode  --  " + opcode
                                + " --  owner  --  " + owner + "name  --  "
                                + name + "desc  --  " + desc);
                        super.visitMethodInsn(opcode, owner, name, desc, arg4);
                    }
                };
            }
        };
        try {
            ClassReader classReader = new ClassReader(object.getClass().getCanonicalName());
            classReader.accept(cv, 0);
        } catch (IOException e) {
            System.err.println("Something went wrong !! " + e.getMessage());
        }
    }

    public static void main(String[] args) {
        Foo foo = new Foo();
        Analyzer analyzer = new Analyzer();
        analyzer.analyze(foo);
    }
}

酒吧类

package sample.code.analyze;

    public class Bar {
        public void gaz() {
            System.out.println("gaz");
        }
    }

Foo 类

package sample.code.analyze;

import sample.code.analyze.Bar;

public class Foo {
    public void foo(int value, Bar bar) {
        if (value > 5) {
            bar.gaz();
        }
    }
}

【讨论】:

    【解决方案3】:

    您尝试做的事情称为static code analysis - 特别是数据流分析,但有点扭曲...您没有显示您正在查看源代码,而是查看编译代码...如果您想这样做它在运行时,您必须处理已编译的(字节码)代码而不是源代码。因此,您正在寻找一个能够进行字节码数据流分析的库。有很多图书馆可以提供帮助(现在您知道要搜索什么,如果您愿意,可以找到我推荐的替代品)。

    好的,不举个例子...我喜欢javassist - 我发现它与字节码库一样清晰,可以在线提供很好的示例和文档。 javassit 有一些更高级别的bytecode analysis API,因此您甚至可能不必深入挖掘,具体取决于您需要做什么。

    要打印上面 Foo/Bar 示例的输出,请使用以下代码:

    public static void main (String... args) throws Exception {
        Analyzer a = new Analyzer();
    
        ClassPool pool = ClassPool.getDefault();
        CtClass cc = pool.get("test.Foo");
        for (CtMethod cm : cc.getDeclaredMethods()) {
            Frame[] frames = a.analyze(cm);
            for (Frame f : frames) {
                System.out.println(f);
            }
        }
    }
    

    将打印:

    locals = [test.Foo, int, test.Bar] stack = []
    locals = [test.Foo, int, test.Bar] stack = [int]
    locals = [test.Foo, int, test.Bar] stack = [int, int]
    null
    null
    locals = [test.Foo, int, test.Bar] stack = []
    locals = [test.Foo, int, test.Bar] stack = [test.Bar]
    null
    null
    locals = [test.Foo, int, test.Bar] stack = []
    

    如果您需要更多详细信息,则需要实际读取字节码,并准备好 JVM specification

    public static void main (String... args) throws Exception {
            ClassPool pool = ClassPool.getDefault();
            CtClass cc = pool.get("test.Foo");
            for (CtMethod cm : cc.getDeclaredMethods()) {
                MethodInfo mi = cm.getMethodInfo();
                CodeAttribute ca = mi.getCodeAttribute();
                CodeIterator ci = ca.iterator();
                while (ci.hasNext()) {
                    int index = ci.next();
                    int op = ci.byteAt(index);
                    switch (op) {
                        case Opcode.INVOKEVIRTUAL:
                            System.out.println("virutal");
                            //lookup in the JVM spec how to extract the actual method
                            //call info here
                            break;
                    }
                }
            }
        }
    

    我希望这可以帮助您入门 =)

    【讨论】:

    • 这会解析类文件吗?我将如何在罐子上运行它?
    • 感谢您的回答,我准备尝试使用javassist。目前我正在尝试 ASM 来解析字节码。
    • 嗯,抱歉耽搁了……我没有收到电子邮件通知。我写的例子假设有问题的类已经加载到类路径中以匹配你的例子——但看起来你已经解决了这个问题 =)
    【解决方案4】:

    这非常困难 - 您将需要使用 Java Reflect API 并进行一些繁重的解析以及编译器会做的大量工作。相反,您可以只使用已经可用的众多 Java 依赖项工具/插件之一(例如来自 https://*.com/a/2366872/986160 的 JDepend)

    【讨论】:

    • 我熟悉 Reflection api。你认为解析会带来什么?有没有办法在pojo的内存中做到这一点?
    • 它将涉及解析所有方法体并查找方法调用(使用正则表达式和语法树)。您将需要跟踪变量及其类型,以便您可以记录对这些类类型的依赖关系。您可能需要对所有文件进行多次传递。您还需要构建符号树和语法树,然后构建依赖关系图。但正如我所说,这可能是编译器课程中的六个月课程项目。
    • 我认为字节码甚至更低级别 - 如果您指的是将在 JVM 上运行的指令。你不需要那个。
    【解决方案5】:

    参考答案:

    我们的目标是让它发挥作用:

        MethodInvocationGraph methodInvocationGraph =
            new MethodInvocationGraph(
                Disassembler.disassembleThisJar());
    
        methodInvocationGraph.printObjectMethodDependencyTree(methodInvocationGraph);
    

    这将打印对象自己的依赖关系。为此,您需要:

    深入了解 ASM Tree API:

    http://asm.ow2.org/

    打开和访问Jar内容的方法,包括

    MethodInvocationGraph.class.getProtectionDomain().getCodeSource()
    

    一个 JNI 签名解析器

    http://journals.ecs.soton.ac.uk/java/tutorial/native1.1/implementing/method.html

    还有一个图框架比如

    http://jgrapht.org/

    【讨论】:

      【解决方案6】:

      由于采用静态类型,方法存在问题。 静态方法将在类的开始时间调用第一次执行因此所有将在第一阶段执行并且由于方法的静态特性而无法第二次调用。所以 main 方法将无法调用上述方法。

      【讨论】:

      • OP 试图查找方法调用依赖项,但不明白什么是允许的,什么是不使用静态的。
      【解决方案7】:

      如果您调用任何方法,我认为您可以从堆栈跟踪中获取所有信息。当我们得到任何异常时,我们可以使用 printStackTrace() 看到堆栈跟踪;方法。这不是一个答案,但它可以帮助您找到解决问题的方法。

      【讨论】:

      • OP 想知道一个方法是否可能调用另一个方法。如果堆栈跟踪发生在正确的时刻,堆栈跟踪最多只能提供它确实的意外证据。海报是对的:这不是答案。
      最近更新 更多