求获取Jar包中所有类级依赖(含import引用类)的Java程序实现
嗨,这个需求我之前帮不少开发者实现过,其实核心思路是解析Jar包里的字节码文件——毕竟Java编译后import语句会被替换成全类名,所以只要能从class文件里找出所有引用的类,就能满足你的需求。下面给你两个实用的方案,优先推荐第一个,稳定又高效:
方案一:使用ASM字节码解析库(推荐)
ASM是一款轻量、高效的Java字节码操作库,能直接读取class文件的结构和依赖信息,完全不需要加载类到JVM中,非常适合这个场景。
1. 添加ASM依赖
如果用Maven管理项目,先把ASM的依赖加到pom.xml里:
<dependency> <groupId>org.ow2.asm</groupId> <artifactId>asm</artifactId> <version>9.5</version> </dependency> <dependency> <groupId>org.ow2.asm</groupId> <artifactId>asm-commons</artifactId> <version>9.5</version> </dependency>
2. 实现扫描逻辑
下面是完整的扫描工具类,它会遍历Jar里的所有class文件,收集所有被引用的类(包括import语句对应的类、父类、接口、字段类型、方法参数等):
import org.objectweb.asm.ClassReader; import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.Opcodes; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.util.HashSet; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; public class JarUsedClassScanner { private final Set<String> usedClasses = new HashSet<>(); public Set<String> scanJar(String jarPath) throws IOException { try (JarFile jarFile = new JarFile(new File(jarPath))) { jarFile.stream() .filter(entry -> entry.getName().endsWith(".class") && !entry.isDirectory()) .forEach(entry -> { try (InputStream is = jarFile.getInputStream(entry)) { ClassReader classReader = new ClassReader(is); // 自定义ClassVisitor来收集所有引用的类 classReader.accept(new ClassVisitor(Opcodes.ASM9) { // 处理父类和实现的接口 @Override public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { if (superName != null) { usedClasses.add(convertToFullClassName(superName)); } if (interfaces != null) { for (String iface : interfaces) { usedClasses.add(convertToFullClassName(iface)); } } super.visit(version, access, name, signature, superName, interfaces); } // 处理字段类型 @Override public void visitField(int access, String name, String descriptor, String signature, Object value) { String type = getTypeFromDescriptor(descriptor); if (type != null) { usedClasses.add(type); } super.visitField(access, name, descriptor, signature, value); } // 处理方法的参数、返回值和异常 @Override public void visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { // 解析返回值 String returnType = getTypeFromDescriptor(descriptor.split("\\)")[1]); if (returnType != null) { usedClasses.add(returnType); } // 解析参数 String paramPart = descriptor.split("\\)")[0].substring(1); int index = 0; while (index < paramPart.length()) { String paramType = getTypeFromDescriptor(paramPart.substring(index)); if (paramType != null) { usedClasses.add(paramType); index += getDescriptorLength(paramPart.substring(index)); } else { index++; } } // 解析异常 if (exceptions != null) { for (String exception : exceptions) { usedClasses.add(convertToFullClassName(exception)); } } super.visitMethod(access, name, descriptor, signature, exceptions); } // 处理常量池中的类引用(比如new出来的类、静态调用的类) @Override public void visitLdcInsn(Object value) { if (value instanceof org.objectweb.asm.Type) { org.objectweb.asm.Type type = (org.objectweb.asm.Type) value; if (type.getSort() == org.objectweb.asm.Type.OBJECT) { usedClasses.add(type.getClassName()); } } super.visitLdcInsn(value); } }, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES); } catch (IOException e) { e.printStackTrace(); } }); } return usedClasses; } // 把ASM内部类名(比如com/example/MyClass)转换成全类名 private String convertToFullClassName(String internalName) { return internalName.replace('/', '.'); } // 从字节码描述符中提取类名 private String getTypeFromDescriptor(String descriptor) { if (descriptor == null || descriptor.isEmpty()) { return null; } org.objectweb.asm.Type type = org.objectweb.asm.Type.getType(descriptor); if (type.getSort() == org.objectweb.asm.Type.OBJECT || type.getSort() == org.objectweb.asm.Type.ARRAY) { return type.getClassName(); } // 基本类型不需要记录 return null; } // 计算字节码描述符的长度,方便解析参数 private int getDescriptorLength(String descriptor) { if (descriptor.startsWith("L")) { return descriptor.indexOf(';') + 1; } else if (descriptor.startsWith("[")) { return 1 + getDescriptorLength(descriptor.substring(1)); } return 1; // 基本类型长度为1 } public static void main(String[] args) throws IOException { JarUsedClassScanner scanner = new JarUsedClassScanner(); // 替换成你的Jar路径 Set<String> usedClasses = scanner.scanJar("path/to/your/target.jar"); System.out.println("所有被使用的类:"); usedClasses.forEach(System.out::println); } }
关键说明
- 这个工具会收集所有代码中实际引用的类,包括import语句对应的类、父类、接口、字段类型、方法参数/返回值、异常,以及代码中直接实例化或静态调用的类
- 基本类型(比如int、boolean)不会被记录,因为它们不属于类
- 如果Jar经过混淆处理,类名会被打乱,结果可能不符合预期
方案二:基于javap命令的解析(不推荐,仅作参考)
如果不想引入第三方库,也可以通过调用Java自带的javap命令解析class文件,然后提取输出中的类名。不过这个方式依赖环境中存在javap工具,且解析输出文本容易受格式变化影响,稳定性不如ASM:
import java.io.BufferedReader; import java.io.IOException; import java.io.InputStreamReader; import java.util.HashSet; import java.util.Set; import java.util.jar.JarEntry; import java.util.jar.JarFile; public class JavapBasedScanner { public Set<String> scanJar(String jarPath) throws IOException { Set<String> usedClasses = new HashSet<>(); try (JarFile jarFile = new JarFile(jarPath)) { jarFile.stream() .filter(entry -> entry.getName().endsWith(".class") && !entry.isDirectory()) .forEach(entry -> { // 把class路径转换成全类名,比如com/example/MyClass.class -> com.example.MyClass String className = entry.getName().replace(".class", "").replace('/', '.'); try { // 调用javap命令解析类 Process process = Runtime.getRuntime().exec("javap -cp " + jarPath + " " + className); BufferedReader reader = new BufferedReader(new InputStreamReader(process.getInputStream())); String line; while ((line = reader.readLine()) != null) { // 这里需要自己实现文本解析逻辑,提取类名,比如匹配extends、implements、new等关键字后的类 // 示例仅做演示,实际需要更完善的正则匹配 if (line.contains("extends") || line.contains("implements")) { String[] parts = line.split("\\s+"); for (String part : parts) { if (part.contains(".")) { usedClasses.add(part); } } } } reader.close(); process.waitFor(); } catch (Exception e) { e.printStackTrace(); } }); } return usedClasses; } }
内容的提问来源于stack exchange,提问作者Anirudh Kashyap




