Java 反思:在自定义 AbstractProcessor 中查找方法用法

Java Reflection: Find method usage in custom AbstractProcessor

我是反思中的新手。有什么方法可以检测特定方法在哪里被调用?例如:

public class MyClass {

   public static void method(){ 
       //DO SOMETHING
   }

}

public class Test {

    public test(){
       MyClass.method();
    }

}

public class MyProcessor extends AbstractProcessor {

   public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {

      Method method = MyClass.class.getDeclaredMethod("method");

      Class classWhereMethodIsInvoked = obtainClassWhereMethodIsInvoked(method); 

   }

   public Class obtainClassWhereMethodIsInvoked(Method method) {
      //here I want to search one class that invoke that method, in this case Test.class
   }

}

这样的事情有可能吗,还是我要疯了?

您可以定义自己的机制。使用一个Map来存储每个方法的调用者:

public static Map<Method, List<String>> callStack = new HashMap<Method, List<String>>();

public static void registerCaller(Method m)
{
    List<String> callers = callStack.get(m);
    if (callers == null)
    {
        callers = new ArrayList<String>();
        callStack.put(m, callers);
    }

    StackTraceElement[] stackTraceElements = Thread.currentThread().getStackTrace();
    callers.add(stackTraceElements[3].getClassName());
}

目标class:

class MyClass
{   
    public static void method()
    {
        registerCaller(new Object(){}.getClass().getEnclosingMethod());
        // DO SOMETHING
    }
}

一些来电者class是:

package the.package.of;

class Test
{
    public void test()
    {
       MyClass.method();
    }
}

class Foo
{
    public void bar()
    {
       MyClass.method();
    }
}

最后,测试:

new Test().test();
new Foo().bar();

Method method = MyClass.class.getDeclaredMethod("method");
for (String clazz : callStack.get(method))
{
    System.out.println(clazz);
}

打印:

the.package.of.Test
the.package.of.Foo

好吧,如果您将 Eclipse 用作 IDE,您可以通过 "Open Call Hierarchy" 函数找到完整的调用层次结构。这将在任何打开的 Eclipse 项目中找到您的方法的所有用法。 但是,如果您想在运行时以编程方式查找,则需要集成一些库,该库可以静态分析类路径的字节码以使用您的方法。

如评论中所述,Apache BCEL 适合您的问题。这些库通常特别用于从生成的字节码中确定方法使用和控制流分析等编译时信息,并且即使不是不可能,也很难使用反射检索这些信息。如果您使用 BCEL 解决方案,您可能不再需要自定义注释处理器。

但是由于您似乎已经在使用自定义注释处理器,所以它的全部意义在于能够处理源文件中的注释。因此,一种方法是定义一个自定义注释来标记正在调用的方法,并让自定义处理器读取这些注释以了解哪些 classes 调用哪些方法:

@CallerClass("MyClass.method")
public class Test {

    public test() {
       MyClass.method();
    }

} 

在上面的(简单的)示例中,自定义 CallerClass 注释标记 class 调用括号内注释元素中指定的方法。注解处理器可以读取这个注解并构造调用者信息。

您可以在测试方法中获取堆栈跟踪:

public class Test {

    public void test() {
        System.out.println(getCallerClass());
    }

    public static String getCallerClass()  {
        for (StackTraceElement e: Thread.currentThread().getStackTrace()) {
            if (!"java.lang.Thread".equals(e.getClassName()) && !e.getClassName().equals(Test.class.getName()))
               return e.getClassName();
        }
        return null;
    }
}

是的,如果你真的想要它是可行的。您可以使用 classLoader 搜索 class 路径并在所有 class 文件中扫描方法名称。下面是一个非常简单的例子来证明它是可行的。在下面的示例中,我发现在 class 中使用了 "println" 方法。从本质上讲,您可以将范围从我示例中的一个文件扩大到所有 class 个文件。

public class SearchClasses {

    /**
     * @param args the command line arguments
     */
    public static void main(String[] args) throws FileNotFoundException {


//      InputStream is = SearchClasses.class.getClassLoader().getResourceAsStream("resources.SearchClasses.class");
        InputStream is = new FileInputStream(new File("build/classes/resources/SearchClasses.class"));

        boolean found = false;
        Scanner scanner = new Scanner(is);
        while (scanner.hasNext()) {
            if (scanner.nextLine().contains("println")) {
                System.out.print("println found");
                found = true;
                break;
            }
        }

        if (!found) {
                System.out.print("println NOT found");          
        }

    }

    public static void testMethod() {
        System.out.println("testing");
    }

}

在我的 IDE 中,我必须使用 FileInputStream 来访问我正在搜索的 class 文件....但是如果您正在搜索 jar 文件,那么您可以使用 class改为装载机。您将需要一种机制来搜索所有 class 路径...这并非不可能,但为了简洁起见,我将其保留了下来。

编辑:这是让它完全工作的尝试。搜索 class 路径中的所有文件以查找您的方法。

public class SearchClasses {

    /**
     * @param args the command line arguments
     * @throws java.io.FileNotFoundException
     */
    public static void main(String[] args) throws FileNotFoundException, IOException {

        printAllFileWithMethod("println");
    }

    public static void printAllFileWithMethod(String methodName) throws FileNotFoundException, IOException {
        Enumeration<URL> roots = SearchClasses.class.getClassLoader().getResources("");

        List<File> allClassFiles = new ArrayList<>();
        while (roots.hasMoreElements()) {
            File root = new File(roots.nextElement().getPath());
            allClassFiles.addAll(getFilesInDirectoryWithSuffix(root, "class"));
        }

        for (File classFile : allClassFiles) {
            InputStream is = new FileInputStream(classFile);

            boolean found = false;
            Scanner scanner = new Scanner(is);
            while (scanner.hasNext()) {
                if (scanner.nextLine().contains(methodName)) {
                    System.out.print(methodName + " found in " + classFile.getName() + "\n");
                    found = true;
                    break;
                }
            }

        }
    }

    public static void testMethod() {
        System.out.println("testing");
    }

    static List<File> getFilesInDirectoryWithSuffix(File dir, String suffix) {
        List<File> foundFiles = new ArrayList<>();
        if (!dir.isDirectory()) {
            return foundFiles;
        }
        for (File file : dir.listFiles()) {
            if (file.isDirectory()) {
                foundFiles.addAll(getFilesInDirectoryWithSuffix(file, suffix));
            } else {
                String name = file.getName();
                if (name.endsWith(suffix)) {
                    foundFiles.add(file);
                }
            }

        }
        return foundFiles;
    }

}