I need to write a tool that lists the classes that call methods of specified interfaces. It will be used as part of the build process of a large java application consisting of many modules. The goal is to automatically document the dependencies between certain java modules.
I found several tools for dependency analysis, but they don't work on the method level, just for packages or jars. Finally I found ASM, that seems to do what I need.
The following code prints the method dependencies of all class files in a given directory:
import java.io.*;
import java.util.*;
import org.objectweb.asm.ClassReader;
public class Test {
public static void main(String[] args) throws Exception {
File dir = new File(args[0]);
List<File> classFiles = new LinkedList<File>();
findClassFiles(classFiles, dir);
for (File classFile : classFiles) {
InputStream input = new FileInputStream(classFile);
new ClassReader(input).accept(new MyClassVisitor(), 0);
input.close();
}
}
private static void findClassFiles(List<File> list, File dir) {
for (File file : dir.listFiles()) {
if (file.isDirectory()) {
findClassFiles(list, file);
} else if (file.getName().endsWith(".class")) {
list.add(file);
}
}
}
}
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.commons.EmptyVisitor;
public class MyClassVisitor extends EmptyVisitor {
private String className;
@Override
public void visit(int version, int access, String name, String signature,
String superName, String[] interfaces) {
this.className = name;
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
System.out.println(className + "." + name);
return new MyMethodVisitor();
}
}
import org.objectweb.asm.commons.EmptyVisitor;
public class MyMethodVisi开发者_JS百科tor extends EmptyVisitor {
@Override
public void visitMethodInsn(int opcode, String owner, String name,
String desc) {
String key = owner + "." + name;
System.out.println(" " + key);
}
}
The Problem:
The code works for regular classes only! If the class file contains an interface, visitMethod is called, but not visitMethodInsn. I don't get any info about the callers of interface methods.
Any ideas?
I think this is because Interface methods do not have a method body. Try writing an empty method as part of a 'normal' class and see if visitMethodInsn is invoked.
By the way, have you considered using java.lang.instrument to discover the classes that are loaded at runtime and do your instrumentation that way, rather than reading the class files from disk?
I have to admit, I was confused...
I thought that asm visitors do some magic to give me the list of all callers of a given method, like a stacktrace. Instead they justs parse classes and method bodies. Fortunatly, this is totally sufficent for my needs as I can build the call tree by myself.
The following code lists all methods that are called by other methods, checking class files in given directory (and subdirectories) only:
import java.io.*;
import java.util.*;
import org.objectweb.asm.ClassReader;
public class Test {
public static void main(String[] args) throws Exception {
File dir = new File(args[0]);
Map<String, Set<String>> callMap = new HashMap<String, Set<String>>();
List<File> classFiles = new LinkedList<File>();
findClassFiles(classFiles, dir);
for (File classFile : classFiles) {
InputStream input = new FileInputStream(classFile);
new ClassReader(input).accept(new MyClassVisitor(callMap), 0);
input.close();
}
for (Map.Entry<String, Set<String>> entry : callMap.entrySet()) {
String method = entry.getKey();
Set<String> callers = entry.getValue();
if (callers != null && !callers.isEmpty()) {
System.out.println(method);
for (String caller : callers) {
System.out.println(" " + caller);
}
}
}
}
private static void findClassFiles(List<File> list, File dir) {
for (File file : dir.listFiles()) {
if (file.isDirectory()) {
findClassFiles(list, file);
} else if (file.getName().endsWith(".class")) {
list.add(file);
}
}
}
}
import java.util.*;
import org.objectweb.asm.MethodVisitor;
import org.objectweb.asm.commons.EmptyVisitor;
public class MyClassVisitor extends EmptyVisitor {
private String className;
private Map<String, Set<String>> callMap;
public MyClassVisitor(Map<String, Set<String>> callMap) {
this.callMap = callMap;
}
@Override
public void visit(int version, int access, String name, String signature,
String superName, String[] interfaces) {
this.className = name;
}
@Override
public MethodVisitor visitMethod(int access, String name, String desc,
String signature, String[] exceptions) {
return new MyMethodVisitor(className + "." + name, callMap);
}
}
import java.util.*;
import org.objectweb.asm.commons.EmptyVisitor;
public class MyMethodVisitor extends EmptyVisitor {
private String currentMethod;
private Map<String, Set<String>> callMap;
public MyMethodVisitor(String currentMethod,
Map<String, Set<String>> callMap) {
this.currentMethod = currentMethod;
this.callMap = callMap;
}
@Override
public void visitMethodInsn(int opcode, String owner, String name,
String desc) {
String calledMethod = owner + "." + name;
Set<String> callers = callMap.get(calledMethod);
if (callers == null) {
callers = new TreeSet<String>();
callMap.put(calledMethod, callers);
}
callers.add(currentMethod);
}
}
Interesting, someone downvoted my answer without making any comments to improve my answer, I don't think my answer is wrong, it describes why ASM doesn't go into interface method.
public interface FF {
default void hh(String name) {
}
String hello(String hello);
}
javap executtion result ,like below , interface method body no message , like LineNumberTable,LocalVariableTable, so can't be parse by asm
➜ ~ javap -v /Users/chenshun/tool/winter/target/test-classes/io/github/chenshun00/web/asm/FF.class
Classfile /Users/chenshun/tool/winter/target/test-classes/io/github/chenshun00/web/asm/FF.class
Last modified 2022-7-15; size 384 bytes
MD5 checksum de24290be11cb7706f83d3a355261367
Compiled from "FF.java"
public interface io.github.chenshun00.web.asm.FF
minor version: 0
major version: 52
flags: ACC_PUBLIC, ACC_INTERFACE, ACC_ABSTRACT
Constant pool:
#1 = Class #16 // io/github/chenshun00/web/asm/FF
#2 = Class #17 // java/lang/Object
#3 = Utf8 hh
#4 = Utf8 (Ljava/lang/String;)V
#5 = Utf8 Code
#6 = Utf8 LineNumberTable
#7 = Utf8 LocalVariableTable
#8 = Utf8 this
#9 = Utf8 Lio/github/chenshun00/web/asm/FF;
#10 = Utf8 name
#11 = Utf8 Ljava/lang/String;
#12 = Utf8 hello
#13 = Utf8 (Ljava/lang/String;)Ljava/lang/String;
#14 = Utf8 SourceFile
#15 = Utf8 FF.java
#16 = Utf8 io/github/chenshun00/web/asm/FF
#17 = Utf8 java/lang/Object
{
public void hh(java.lang.String);
descriptor: (Ljava/lang/String;)V
flags: ACC_PUBLIC
Code:
stack=0, locals=2, args_size=2
0: return
LineNumberTable:
line 17: 0
LocalVariableTable:
Start Length Slot Name Signature
0 1 0 this Lio/github/chenshun00/web/asm/FF;
0 1 1 name Ljava/lang/String;
public abstract java.lang.String hello(java.lang.String);
descriptor: (Ljava/lang/String;)Ljava/lang/String;
flags: ACC_PUBLIC, ACC_ABSTRACT
}
SourceFile: "FF.java"
精彩评论