You need to enable JavaScript to run this app.
最新活动
大模型
产品
解决方案
定价
生态与合作
支持与服务
开发者
了解我们

求获取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

火山引擎 最新活动