JD2M源码分析

JD2M源码分析 #

Unicode2CN.java #

这个文件的核心功能是把 Java 源码里的 \uXXXX Unicode 转义序列还原成可读的中文字符

CFR 反编译字节码时,遇到中文字符串会输出成 \u4e2d\u6587 这种形态。JD2M 用这个类做后处理,把反编译结果还原成人类可读的 中文。

方法功能 #

1. ascii2Char(String str) — 最底层:单个 \uXXXX 转 char #

private static char ascii2Char(String str) {
	if (str.length() != 6) {
		throw new IllegalArgumentException("Length must be 6, got: " + str.length());    //\uXXXX必须是6个字符
	}
	if (!"\\u".equals(str.substring(0, 2))) {    //前缀检查
		throw new IllegalArgumentException("Must start with \\u");
	}
	String tmp = str.substring(2, 4);
	int code = Integer.parseInt(tmp, 16) << 8;
	tmp = str.substring(4, 6);
	code += Integer.parseInt(tmp, 16);
	return (char) code;
}

由于 Java 字符串里 \u 要写成 \\u,因为 \ 本身需要转义。所以传进来的参数长这样:\\u4e16(6 个字符:\ u 4 e 1 6)。

String tmp = str.substring(2, 4)截取到 unicode 字符的2到4位,int code = Integer.parseInt(tmp, 16) << 8把截取到的tmp当作16进制解析并转化为十进制的int整数然后再左移8位,以便拼接后续8位。

2. isHex(String str, int start, int end) — 辅助:校验是不是十六进制 #

private static boolean isHex(String str, int start, int end) {
    for (int i = start; i < end; i++) {
        char c = str.charAt(i);
        if (!((c >= '0' && c <= '9') || (c >= 'a' && c <= 'f') || (c >= 'A' && c <= 'F'))) {
            return false;
        }
    }
    return true;
}

对整个字符串遍历来判断是不是16进制。

3. ascii2Native(String str) #

public static String ascii2Native(String str) {
	StringBuilder sb = new StringBuilder();
	int begin = 0;    //整个字符串的起始位置
	int index = str.indexOf("\\u");    //获取\u出现的位置,如果不存在则返回-1
	while (index != -1) {
		sb.append(str, begin, index);  //把起始位置和\u之间的字符串放到结果中
		if (index + 6 <= str.length() && isHex(str, index + 2, index + 6)) {
			sb.append(ascii2Char(str.substring(index, index + 6)));//转化为中文
			begin = index + 6;//起始位置后移
		} else {     //后续不是unicode字符
			sb.append("\\u");
			begin = index + 2;
		}
		index = str.indexOf("\\u", begin);    //从起始位置开始找下一个出现\\u的位置
	}
	sb.append(str.substring(begin));   //把从起始位置到末尾的所有字符加到结果中
	return sb.toString();  //把结果转化为字符串返回
}

这个方法的作用是:把字符串中所有 \uXXXX 格式的 Unicode 转义序列,替换成对应的真实字符。

StringBuilder 是 Java 中用于高效拼接字符串的工具类。Java 的 String不可变的,每次拼接都会创建新对象,如果循环里频繁拼接,会产生大量临时对象,效率低、浪费内存

4. toCN(File file, File aim) #

private static void toCN(File file, File aim) throws IOException { //源文件与目标文件
    List<String> allLines = new ArrayList<>(1024); //创建列表存储所有行,假设有1024行
    //使用hutool这个java工具库的方法以UFT8编码读取源文件的所有行并存入列表
    FileUtil.readLines(file, StandardCharsets.UTF_8, allLines);
    for (int i = 0; i < allLines.size(); i++) {     //遍历所有行
        //allLines.get(i)获取第i行的原始内容
        //ascii2Native(allLines.get(i))把第i行的unicode转化为中文
        //allLines.set(i, ...)用转化后的内容替换原来的行
        allLines.set(i, ascii2Native(allLines.get(i)));
    }
    //把转化后的列表中的所有行写入目标文件,以UTF8编码的形式
    FileUtil.writeLines(allLines, aim, StandardCharsets.UTF_8);
}

这个方法的作用是:读取一个文件,把里面所有行的 \uXXXX 转义序列都转换成真实的中文字符,然后写入到另一个文件。

5. toCN(File sourceFile) #

public static void toCN(File sourceFile) throws IOException {
    //创建临时文件,在源文件路径后加.tmp
	File aimFile = new File(sourceFile.getPath() + ".tmp");
    //调用重载的方法
	toCN(sourceFile, aimFile);
    //FileUtil.move是hutool的文件移动方法
    //第三个参数true表示如果目标文件存在就覆盖它
    //把.tmp文件移动(重命名)为源文件的名字
	FileUtil.move(aimFile, sourceFile, true);
}

这个方法的作用是:把源文件中的 \uXXXX 转成中文后,直接替换掉源文件。这个方法就是封装了"转换后覆盖原文件"的逻辑

StringExtractor.java #

这个文件是一个 Java 字节码字符串提取工具。核心作用是把从编译好的.class文件、.jar/.war包或目录中提取出所有长度超过指定值的字符串,然后打印出来。

这个工具底层使用 ASM 字节码框架,Java 编译后的 .class 文件是一堆二进制字节,ASM 库可以读取并解析这些字节,然后通过访问者模式把字节码的各个部分"汇报"给一个访问者对象。

汇报的内容包括:

  • 类名、父类名、接口名
  • 字段名、字段类型
  • 方法名、方法签名
  • 方法体里的字符串常量
  • 注解信息
  • 等等

核心机制ClassReader 负责读取字节码,ClassVisitor 负责接收汇报。我们只需要写一个 ClassVisitor 的子类,在对应的方法里收集信息即可。

StringCollector 内部类 - 自定义访问者 #

private static class StringCollector extends ClassVisitor {
    final Set<String> strings = new LinkedHashSet<>();
    final int minLength;

    StringCollector(int minLength) {
        super(Opcodes.ASM9);
        this.minLength = minLength;
    }

    private void add(String s) {
        if (s != null && s.length() >= minLength) {
            strings.add(s);
        }
    }

    @Override
    public void visit(int version, int access, String name, String signature,
                      String superName, String[] interfaces) {
        add(name);
        add(superName);
        if (interfaces != null) {
            for (String iface : interfaces) add(iface);
        }
        super.visit(version, access, name, signature, superName, interfaces);
    }

    @Override
    public FieldVisitor visitField(int access, String name, String descriptor,
                                    String signature, Object value) {
        add(name);
        add(descriptor);
        if (value instanceof String) add((String) value);
        return super.visitField(access, name, descriptor, signature, value);
    }

    @Override
    public MethodVisitor visitMethod(int access, String name, String descriptor,
                                      String signature, String[] exceptions) {
        add(name);
        add(descriptor);
        if (exceptions != null) {
            for (String ex : exceptions) add(ex);
        }
        return new MethodVisitor(Opcodes.ASM9) {
            @Override
            public void visitLdcInsn(Object cst) {
                if (cst instanceof String) add((String) cst);
                super.visitLdcInsn(cst);
            }

            @Override
            public void visitInvokeDynamicInsn(String name, String descriptor,
                                                Handle bootstrapMethodHandle,
                                                Object... bootstrapMethodArguments) {
                add(name);
                for (Object arg : bootstrapMethodArguments) {
                    if (arg instanceof String) add((String) arg);
                    if (arg instanceof Handle) {
                        Handle h = (Handle) arg;
                        add(h.getName());
                        add(h.getDesc());
                        add(h.getOwner());
                    }
                }
                super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments);
            }
        };
    }

    @Override
    public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
        add(descriptor);
        return new AnnotationCollector();
    }

    @Override
    public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath,
                                                  String descriptor, boolean visible) {
        add(descriptor);
        return new AnnotationCollector();
    }

    private class AnnotationCollector extends AnnotationVisitor {
        AnnotationCollector() {
            super(Opcodes.ASM9);
        }

        @Override
        public void visit(String name, Object value) {
            add(name);
            if (value instanceof String) add((String) value);
            super.visit(name, value);
        }

        @Override
        public void visitEnum(String name, String descriptor, String value) {
            add(name);
            add(descriptor);
            add(value);
            super.visitEnum(name, descriptor, value);
        }

        @Override
        public AnnotationVisitor visitAnnotation(String name, String descriptor) {
            add(name);
            add(descriptor);
            return new AnnotationCollector();
        }

        @Override
        public AnnotationVisitor visitArray(String name) {
            add(name);
            return new AnnotationCollector();
        }
    }
}

这是整个工具的核心,一个自定义的ClassVisitor,重写了各种回调方法。

数据结构 #

final Set<String> strings = new LinkedHashSet<>();

LinkedHashSet这个数据结构有三个特性:去重、保持插入顺序、O(1)的查找和插入。

这就保证了同一个字符串只需收集一次、元素的顺序不变收集字符串的速度很快。

add() #

private void add(String s) {
    if (s != null && s.length() >= minLength) {
        strings.add(s);
    }
}

这个方法是整个StringCollector唯一入口,所有收集到的字符串都要经过它。

由于字节码中有大量的短字符串,比如get之类的方法名,方法描述符、字段名等。我们要找的主要是像“操作成功”这样的较长的字符串。然后把收集到的字符串给添加到strings这个LinkedHashSet

visit() - 访问类的基本信息 #

@Override
public void visit(int version, int access, String name, String signature,
                  String superName, String[] interfaces) {
    add(name);//name是ASM内部格式的类名,用/分隔,如com/example/UserService。
    add(superName);//收集父类名
    if (interfaces != null) {   //收集接口名
        for (String iface : interfaces) add(iface);
    }
    //这一行非常重要,把调用传递给父类ClassVisitor,让ASM继续解析这个类的内部
    super.visit(version, access, name, signature, superName, interfaces);
}

这是 ClassVisitor.visit() 方法的重写,在 ASM 解析到类的定义头时被回调。当ClassReader.accept()扫描到类定义的开头部分,也就是在代码中像public class UserServiceImpl extends BaseService implements Serializable {这一行时就会自动调用ClassVisitor.visit()

参数含义示例
version字节码版本号52(Java 8),61(Java 17)
access访问修饰符ACC_PUBLIC + ACC_SUPER
name类的内部名称org/example/Hello
signature泛型签名Lorg/example/Hello<TT;>;
superName父类内部名称java/lang/Object
interfaces实现的接口列表["java/io/Serializable"]

假设有这样的类:

package com.example;

public class UserServiceImpl extends BaseService implements Serializable, Comparable<User> {
    // ...
}

回调时参数值为:

name:       "com/example/UserServiceImpl"
superName:  "com/example/BaseService"
interfaces: ["java/io/Serializable", "java/lang/Comparable"]

会尝试收集:

字符串长度是否收集
com/example/UserServiceImpl29
com/example/BaseService22
java/io/Serializable20
java/lang/Comparable20

这个方法的逻辑非常简单:

visit() 被回调
  ├── 收集类名 (name)
  ├── 收集父类名 (superName)
  ├── 遍历收集所有接口名 (interfaces[])
  └── super.visit(...) → 让 ASM 继续解析类内部

它确保类的基本身份信息(类名、父类、接口)都能被提取,为后续深入类内部打开通道。

visitField() - 访问字段 #

@Override
public FieldVisitor visitField(int access, String name, String descriptor,
                                String signature, Object value) {
    add(name);  //收集字段名
    add(descriptor); //收集类型描述符
    if (value instanceof String) add((String) value);  //收集静态常量字段的值
    //只有当value是静态的、常量的、基本类型或String类型时才不是null
    //返回一个FieldVisitor对象,ASM拿到这个对象后会用它来解析这个字段上的注解
    return super.visitField(access, name, descriptor, signature, value);
}

这是 ClassVisitor.visitField() 方法的重写,在 ASM 解析到类中的一个字段时被回调

参数含义示例
access访问修饰符ACC_PRIVATE + ACC_STATIC + ACC_FINAL
name字段名"userName", "MSG"
descriptor类型描述符"Ljava/lang/String;", "I"
signature泛型签名"Ljava/util/List<Ljava/lang/String;>;"
value静态常量字段的初始值"操作成功",非静态字段为 null

visitMethod() - 访问方法 #

@Override
public MethodVisitor visitMethod(int access, String name, String descriptor,
                                  String signature, String[] exceptions) {
    add(name);   //收集方法名
    add(descriptor);   //收集方法描述符
    if (exceptions != null) {     
        for (String ex : exceptions) add(ex);   //收集异常类型
    }
    return new MethodVisitor(Opcodes.ASM9) {   
        //返回MethodVisitor来深入方法体并按照 ASM 9 的规范来处理方法体
        @Override      //收集字符串常量 ldc:将字符串加载到操作数栈
        public void visitLdcInsn(Object cst) {   
            //java代码中的每一个字符串字面量都会在字节码中以LDC指令出现
            //每次ASM在方法体内遇到LDC指令就会回调这个方法,把常量传进来
            if (cst instanceof String) add((String) cst);
            super.visitLdcInsn(cst);   
        }

        @Override   //收集动态调用中的字符串,ASM在方法体内遇到invokedynamic指令时回调
        public void visitInvokeDynamicInsn(String name, String descriptor,
                                            Handle bootstrapMethodHandle,
                                            Object... bootstrapMethodArguments) {
            //name:调用点名称 descriptor:方法描述符 bootstrapMethodHandle:引导方法的句柄
            //bootstrapMethodArguments:传给引导方法的额外参数
            add(name);
            for (Object arg : bootstrapMethodArguments) {
                if (arg instanceof String) add((String) arg);
                if (arg instanceof Handle) { //拆解句柄的三个部分
                    Handle h = (Handle) arg;
                    add(h.getName());//方法名
                    add(h.getDesc());//方法描述符
                    add(h.getOwner());//方法所在类
                }
            }
            super.visitInvokeDynamicInsn(name, descriptor, bootstrapMethodHandle, bootstrapMethodArguments);
        }
    };
}

这个函数做了两件事:收集方法签名信息 + 返回一个匿名 MethodVisitor 深入方法体内部

参数含义示例
access访问修饰符ACC_PUBLIC + ACC_STATIC
name方法名"sayHello", "main"
descriptor方法描述符(参数+返回值类型)"(Ljava/lang/String;)V"
signature泛型签名"<T:Ljava/lang/Object;>(TT;)V"
exceptions声明的异常类型列表["java/io/IOException"]

AnnotationCollector - 递归收集注解内所有字符串 #

private class AnnotationCollector extends AnnotationVisitor {
    AnnotationCollector() {
        super(Opcodes.ASM9); //告诉父类按照ASM9规范解析
    }

    @Override
    public void visit(String name, Object value) {  //处理键值对
        add(name); //收集键名
        if (value instanceof String) add((String) value); //收集字符串值
        super.visit(name, value);
    }

    @Override   //处理枚举值
    public void visitEnum(String name, String descriptor, String value) {
        add(name);  //收集键名
        add(descriptor); //枚举类型描述符
        add(value); //枚举值的名字
        super.visitEnum(name, descriptor, value);
    }

    @Override   //处理嵌套注解
    public AnnotationVisitor visitAnnotation(String name, String descriptor) {
        add(name);  //键名
        add(descriptor);//嵌套注解描述符
        return new AnnotationCollector(); //递归处理注解
    }

    @Override   //处理数组
    public AnnotationVisitor visitArray(String name) {
        add(name);  //数组键名
        return new AnnotationCollector();//返回新收集器,处理数组每个元素
    }
}

AnnotationCollector 继承自 AnnotationVisitor,专门处理注解内部的各种元素。当 visitAnnotation()visitTypeAnnotation() 被调用后,ASM 需要继续深入注解内部,就是这个类负责接待。

一个注解内部可以包含:

@Example(
    name = "user",                    // ① 键值对 (String)
    count = 10,                       // ① 键值对 (int)
    type = MyEnum.A,                  // ② 枚举值
    nested = @Nested(value = "x"),    // ③ 嵌套注解
    tags = {"a", "b"}                 // ④ 数组
)
元素类型对应回调方法可能包含字符串的位置
键值对visit(name, value)键名、String 类型的值
枚举值visitEnum(name, desc, value)键名、枚举描述符、枚举值名
嵌套注解visitAnnotation(name, desc)键名、嵌套注解的描述符,以及递归进去
数组visitArray(name)键名,以及数组内每个元素递归进去

visitAnnotation() - 访问注解 #

public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
    add(descriptor); //收集注解类型描述符,只收集注解的类型名,但不收集注解里的键值对
    return new AnnotationCollector();
    //返回AnnotationVisitor,ASM拿到这个对象之后会用它继续解析注解内部的键值对
}

这是 ClassVisitor.visitAnnotation() 方法的重写,在 ASM 解析到类上的一个注解时被回调

参数含义示例
descriptor注解的类型描述符"Lorg/springframework/stereotype/Service;"
visible运行时是否可见true(运行时注解)/ false(编译时注解)

visitTypeAnnotation() - 访问类型注解 #

@Override
public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath,
                                              String descriptor, boolean visible) {
    add(descriptor);
    return new AnnotationCollector();
}

类型注解是 Java 8 引入的新特性。和普通注解不同,它可以贴在任何使用类型的地方

// 普通注解:只能贴在声明上
@Override
public String toString() { ... }

// 类型注解:可以贴在任何类型上
@NotNull String name;                           // 字段类型
List<@NonNull String> list;                      // 泛型参数
public @Nullable String getName() { ... }        // 返回值类型
void foo(@NotNull Object obj) { ... }            // 方法参数类型
new @Interned MyObject();                        // 对象创建
参数含义示例
typeRef目标类型引用表示贴在字段上、方法返回值上、参数上等
typePath复杂类型中的路径List<@NotNull String> 中,路径指向 String
descriptor注解描述符"Ljavax/validation/constraints/NotNull;"
visible运行时可见性true(运行时保留)/ false(仅编译时)

三个入口方法 #

extractFromClass(File, minLength) - 单个class文件提取字符串 #

public static List<String> extractFromClass(File classFile, int minLength) throws IOException {
    byte[] bytes = FileUtil.readBytes(classFile);//hutool的工具方法,读取字节码
    ClassReader cr = new ClassReader(bytes); //交给ASM处理,ASM已经知道字节码的一切,等待汇报
    StringCollector collector = new StringCollector(minLength);//创建字符串收集器
    cr.accept(collector, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
    //启动ASM的遍历,遇到任何东西都告诉收集器,依次回调相关方法,会跳过不必要的信息
    return new ArrayList<>(collector.strings);//把LinkedHashSet<String>包装一下方便调用
}

extractFromJar(File, minLength) - jar/war包提取字符串 #

public static Map<String, List<String>> extractFromJar(File jarFile, int minLength) throws IOException {
    Map<String, List<String>> result = new LinkedHashMap<>();
    JarFile jar = new JarFile(jarFile);//读取jar/zip格式的文件
    try {
        Enumeration<JarEntry> entries = jar.entries();//返回jar包内所有文件和目录的枚举
        while (entries.hasMoreElements()) {
            //逐个取出每个JarEntry代表一个文件或目录
            JarEntry entry = entries.nextElement();
            //跳过目录和非class文件
            if (entry.isDirectory() || !entry.getName().endsWith(".class")) continue;
            //读取并解析单个class文件
            try (InputStream is = jar.getInputStream(entry)) {
                ClassReader cr = new ClassReader(is);
                StringCollector collector = new StringCollector(minLength);
                cr.accept(collector, ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
                //如果提取到字符串,存入结果
                if (!collector.strings.isEmpty()) {
                    //换成java类名格式
                    String className = entry.getName()
                            .replace('/', '.')
                            .replace(".class", "");
                    result.put(className, new ArrayList<>(collector.strings));
                }
            } catch (Exception ignored) {
            }
        }
    } finally {
        jar.close(); //关闭jar包
    }
    return result;
}
参数含义
jarFile.jar.war 文件
minLength最小字符串长度过滤
返回值Map<类名, 该类提取到的字符串列表>

extractFromDir(File, minLength) #

public static Map<String, List<String>> extractFromDir(File dir, int minLength) throws IOException {
    Map<String, List<String>> result = new LinkedHashMap<>();//创建结果容器
    collectAndExtract(dir, dir, result, minLength);//调用递归方法
    return result;
}
//递归核心
private static void collectAndExtract(File root, File current,
                                       Map<String, List<String>> result, int minLength) {
    File[] files = current.listFiles();//列出当前目录下的所有文件
    if (files == null) return;//没有访问权限、路径不是目录等
    //遍历每一个文件/目录
    for (File f : files) {
        if (f.isDirectory()) {
            collectAndExtract(root, f, result, minLength);//是目录就递归调用
        } else if (f.getName().endsWith(".class")) { //遇到class文件就提取
            try {
                List<String> strings = extractFromClass(f, minLength);//单文件提取
                if (!strings.isEmpty()) {
                    //计算相对路径
                    String relPath = f.getAbsolutePath()
                            .substring(root.getAbsolutePath().length() + 1)
                            .replace(File.separator, ".")
                            .replace(".class", "");
                    result.put(relPath, strings);
                }
            } catch (IOException ignored) {
            }
        }
    }
}

参数说明

参数含义作用
root根目录固定不变,用于计算相对路径
current当前正在遍历的目录每次递归变化,逐层深入
result结果容器所有递归层级共享同一个 Map
minLength最小长度透传给 extractFromClass

方法编排 #

extractFromInput() - 入口 #

public static void extractFromInput(File input, int minLength) {
    try {
        long start = System.currentTimeMillis();//记录开始时间,最后计算耗时
        System.out.println("[JD2M] Extracting strings from: " + input.getAbsolutePath());   //输出正在处理的文件/目录 
        System.out.println("[JD2M] Minimum length: " + minLength);//输出过滤阈值
        System.out.println();
		//单个class文件
        if (input.isFile() && input.getName().toLowerCase().endsWith(".class")) {
            List<String> strings = extractFromClass(input, minLength);
            System.out.println("--- " + input.getName() + " ---");
            for (String s : strings) {
                System.out.println("  " + s);
            }
            System.out.println();
            System.out.println("Total: " + strings.size() + " string(s)");
			//jar/war包
        } else if (input.isFile() &&
                (input.getName().toLowerCase().endsWith(".jar") ||
                 input.getName().toLowerCase().endsWith(".war"))) {
            Map<String, List<String>> result = extractFromJar(input, minLength);
            int total = printResults(result);
            System.out.println("Total: " + total + " string(s) in " + result.size() + " class(es)");

        } else if (input.isDirectory()) {
            Map<String, List<String>> result = extractFromDir(input, minLength);
            int total = printResults(result);
            System.out.println("Total: " + total + " string(s) in " + result.size() + " class(es)");
			//目录
        } else {
            System.err.println("[JD2M] Unsupported input: " + input.getName());
            return;
        }

        long elapsed = System.currentTimeMillis() - start;
        System.out.println("[JD2M] Done in " + (elapsed / 1000) + "s");

    } catch (IOException e) {
        System.err.println("[JD2M] Error: " + e.getMessage());
        e.printStackTrace();
    }
}

这是整个工具的入口,负责判断输入类型、分发到对应的处理方法、打印结果。

printResults() - 打印结果 #

private static int printResults(Map<String, List<String>> result) {
    int total = 0;   //统计字符串总数
    for (Map.Entry<String, List<String>> entry : result.entrySet()) {
        System.out.println("--- " + entry.getKey() + " ---");
        for (String s : entry.getValue()) {
            System.out.println("  " + s);
        }
        total += entry.getValue().size();//计数
    }
    System.out.println();
    return total;
}

Decompiler.java #

package org.jd2m;

import cn.hutool.core.io.FileUtil;
import org.benf.cfr.reader.api.CfrDriver;
import org.benf.cfr.reader.util.getopt.OptionsImpl;
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.nio.file.Files;
import java.nio.file.Path;
import java.util.*;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicInteger;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;

public class Decompiler {

    private static final String[] JAR_EXTENSIONS = {".jar", ".war", ".ear"};
    private static final String[] SKIP_PREFIXES = {"META-INF", "WEB-INF"};
    private static final String BOOT_INF = "BOOT-INF/";
    private static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";

    private static final ExecutorService executor = new ThreadPoolExecutor(
            8, 8, 60, TimeUnit.SECONDS,
            new LinkedBlockingQueue<>(),
            new ThreadPoolExecutor.CallerRunsPolicy());

    public static Result decompile(File input, File output, boolean mavenExport) throws IOException {
        if (!input.exists()) {
            throw new IllegalArgumentException("Input not found: " + input.getAbsolutePath());
        }

        long start = System.currentTimeMillis();
        System.out.println("[JD2M] Input : " + input.getAbsolutePath());
        System.out.println("[JD2M] Output: " + output.getAbsolutePath());

        Result result;
        if (input.isFile()) {
            result = decompileFile(input, output, mavenExport);
        } else {
            result = decompileDirectory(input, output, mavenExport);
        }

        convertUnicode(output);
        executor.shutdown();

        long elapsed = System.currentTimeMillis() - start;
        System.out.println("[JD2M] Done in " + (elapsed / 1000) + "s — " +
                result.success + " OK, " + result.failed + " failed");

        if (!result.failedClasses.isEmpty()) {
            System.out.println("[JD2M] Failed classes (" + result.failedClasses.size() + "):");
            for (String cls : result.failedClasses) {
                System.out.println("  FAIL: " + cls);
            }
        }
        return result;
    }

    private static Result decompileFile(File file, File output, boolean mavenExport) throws IOException {
        String name = file.getName().toLowerCase();

        if (name.endsWith(".class")) {
            String javaOutput = output.getAbsolutePath();
            if (mavenExport) {
                javaOutput = output.getAbsolutePath() + File.separator + "src" +
                        File.separator + "main" + File.separator + "java";
                generatePomForClassFile(file, output);
            }
            return decompileSingleClass(file.getAbsolutePath(), javaOutput);
        }

        for (String ext : JAR_EXTENSIONS) {
            if (name.endsWith(ext)) {
                String javaOutput = output.getAbsolutePath();
                if (mavenExport) {
                    javaOutput = output.getAbsolutePath() + File.separator + "src" +
                            File.separator + "main" + File.separator + "java";
                }

                Result result = decompileJarClassByClass(file, new File(javaOutput));

                if (mavenExport) {
                    extractMavenResources(file, output);
                }
                return result;
            }
        }

        throw new IllegalArgumentException("Unsupported file type: " + file.getName());
    }

    private static Result decompileDirectory(File dir, File output, boolean mavenExport) {
        List<String> classFiles = new ArrayList<>();
        collectClassFiles(dir, classFiles);

        if (classFiles.isEmpty()) {
            System.out.println("[JD2M] No .class files found.");
            return new Result(0, 0, Collections.emptyList());
        }

        System.out.println("[JD2M] Found " + classFiles.size() + " .class file(s), decompiling one by one...");
        String javaOutput = output.getAbsolutePath();
        if (mavenExport) {
            javaOutput = output.getAbsolutePath() + File.separator + "src" +
                    File.separator + "main" + File.separator + "java";
            generatePomForDir(dir, output);
        }
        return decompileClassList(classFiles, javaOutput);
    }

    private static void collectClassFiles(File dir, List<String> result) {
        File[] files = dir.listFiles();
        if (files == null) return;
        for (File f : files) {
            if (f.isDirectory()) {
                collectClassFiles(f, result);
            } else if (f.getName().endsWith(".class")) {
                result.add(f.getAbsolutePath());
            }
        }
    }

    private static Result decompileJarClassByClass(File jarFile, File outputDir) {
        Path tempDir = null;
        try {
            tempDir = Files.createTempDirectory("jd2m-");
            List<String> classPaths = new ArrayList<>();

            JarFile jar = new JarFile(jarFile);
            try {
                Enumeration<JarEntry> entries = jar.entries();
                while (entries.hasMoreElements()) {
                    JarEntry entry = entries.nextElement();
                    if (entry.isDirectory() || !entry.getName().endsWith(".class")) continue;

                    File destFile = new File(tempDir.toFile(), entry.getName());
                    destFile.getParentFile().mkdirs();
                    try (InputStream is = jar.getInputStream(entry)) {
                        FileUtil.writeFromStream(is, destFile);
                    }
                    classPaths.add(destFile.getAbsolutePath());
                }
            } finally {
                jar.close();
            }

            System.out.println("[JD2M] Extracted " + classPaths.size() + " class(es), decompiling...");

            Result result = decompileClassList(classPaths, outputDir.getAbsolutePath());
            return result;

        } catch (IOException e) {
            return new Result(0, 0, Collections.singletonList("jar-extract: " + e.getMessage()));
        } finally {
            if (tempDir != null) {
                FileUtil.del(tempDir.toFile());
            }
        }
    }

    private static Result decompileClassList(List<String> classPaths, String outputPath) {
        int success = 0;
        int failed = 0;
        List<String> failedClasses = new ArrayList<>();
        AtomicInteger progress = new AtomicInteger(0);
        int total = classPaths.size();

        for (String classPath : classPaths) {
            try {
                doCfrDecompile(Collections.singletonList(classPath), outputPath);
                success++;
            } catch (Exception e) {
                failed++;
                String shortName = shortenPath(classPath);
                failedClasses.add(shortName);
                System.err.println("[JD2M] Decompile failed: " + shortName + " — " + e.getMessage());
            }

            int done = progress.incrementAndGet();
            if (total > 20 && done % (total / 10) == 0) {
                System.out.println("[JD2M] Progress: " + done + "/" + total);
            }
        }
        return new Result(success, failed, failedClasses);
    }

    private static Result decompileSingleClass(String classPath, String outputPath) {
        try {
            doCfrDecompile(Collections.singletonList(classPath), outputPath);
            return new Result(1, 0, Collections.emptyList());
        } catch (Exception e) {
            System.err.println("[JD2M] Decompile failed: " + shortenPath(classPath) + " — " + e.getMessage());
            return new Result(0, 1, Collections.singletonList(shortenPath(classPath)));
        }
    }

    private static void doCfrDecompile(List<String> sourcePaths, String outputPath) {
        Map<String, String> opts = new HashMap<>();
        opts.put("caseinsensitivefs", "true");
        opts.put("outputdir", outputPath);
        OptionsImpl options = new OptionsImpl(opts);
        CfrDriver driver = new CfrDriver.Builder().withBuiltOptions(options).build();
        driver.analyse(sourcePaths);
    }

    private static boolean isSpringBootJar(JarFile jar) {
        return jar.getEntry(BOOT_INF) != null;
    }

    private static void extractMavenResources(File jarFile, File outputDir) throws IOException {
        String base = outputDir.getAbsolutePath();
        String resourceDir = base + File.separator + "src" + File.separator + "main" + File.separator + "resources";
        boolean pomExtracted = false;

        JarFile jar = new JarFile(jarFile);
        try {
            boolean isBoot = isSpringBootJar(jar);
            if (isBoot) {
                System.out.println("[JD2M] Detected Spring Boot jar");
                String startClass = readManifestMainClass(jar, "Start-Class");
                if (startClass != null) {
                    System.out.println("[JD2M] Start-Class: " + startClass);
                }
            }

            Enumeration<JarEntry> entries = jar.entries();
            while (entries.hasMoreElements()) {
                JarEntry entry = entries.nextElement();
                String name = entry.getName();
                if (entry.isDirectory()) continue;

                if (!pomExtracted && name.endsWith("pom.xml")) {
                    if (name.matches("META-INF/maven/.*/pom.xml") ||
                            (isBoot && name.matches(BOOT_INF_CLASSES + "META-INF/maven/.*/pom.xml"))) {
                        FileUtil.writeFromStream(jar.getInputStream(entry), base + File.separator + "pom.xml");
                        pomExtracted = true;
                        continue;
                    }
                }

                if (name.endsWith(".class")) continue;

                String resourceName = name;
                if (isBoot) {
                    if (name.startsWith(BOOT_INF_CLASSES)) {
                        resourceName = name.substring(BOOT_INF_CLASSES.length());
                    } else if (name.startsWith(BOOT_INF)) {
                        continue;
                    }
                }
                if (resourceName.isEmpty()) continue;

                boolean skip = false;
                for (String prefix : SKIP_PREFIXES) {
                    if (resourceName.startsWith(prefix)) { skip = true; break; }
                }
                if (skip) continue;

                File dest = new File(resourceDir + File.separator + resourceName);
                FileUtil.writeFromStream(jar.getInputStream(entry), dest);
            }
        } finally {
            jar.close();
        }

        if (!pomExtracted) {
            System.out.println("[JD2M] No pom.xml found, auto-generating...");
            generatePom(jarFile, outputDir);
        }
    }

    private static String readManifestMainClass(JarFile jar, String attrName) {
        try {
            java.util.jar.Manifest mf = jar.getManifest();
            if (mf != null) {
                return mf.getMainAttributes().getValue(attrName);
            }
        } catch (Exception ignored) {
        }
        return null;
    }

    private static void generatePom(File jarFile, File outputDir) throws IOException {
        String groupId = "com.example";
        String artifactId = jarFile.getName();
        int dot = artifactId.lastIndexOf('.');
        if (dot > 0) artifactId = artifactId.substring(0, dot);

        JarFile jar = new JarFile(jarFile);
        try {
            boolean isBoot = isSpringBootJar(jar);
            Map<String, Integer> packageCount = new LinkedHashMap<>();
            Enumeration<JarEntry> entries = jar.entries();
            while (entries.hasMoreElements()) {
                JarEntry entry = entries.nextElement();
                String name = entry.getName();
                if (!name.endsWith(".class")) continue;
                if (isBoot && !name.startsWith(BOOT_INF_CLASSES)) continue;
                int lastSlash = name.lastIndexOf('/');
                if (lastSlash <= 0) continue;
                String pkg = name.substring(0, lastSlash);
                if (isBoot && pkg.startsWith(BOOT_INF_CLASSES)) {
                    pkg = pkg.substring(BOOT_INF_CLASSES.length());
                }
                pkg = pkg.replace('/', '.');
                if (pkg.isEmpty()) continue;
                packageCount.put(pkg, packageCount.getOrDefault(pkg, 0) + 1);
            }
            if (!packageCount.isEmpty()) {
                String bestPkg = packageCount.entrySet().stream()
                        .max(Map.Entry.comparingByValue()).get().getKey();
                int firstDot = bestPkg.indexOf('.');
                groupId = firstDot > 0 ? bestPkg.substring(0, firstDot) : bestPkg;
            }
        } finally {
            jar.close();
        }

        writePomXml(outputDir, groupId, artifactId, "1.0-SNAPSHOT");
    }

    private static void generatePomForClassFile(File classFile, File outputDir) {
        String groupId = "com.example";
        String pkg = getClassPackage(classFile);
        if (!pkg.isEmpty()) {
            int firstDot = pkg.indexOf('.');
            groupId = firstDot > 0 ? pkg.substring(0, firstDot) : pkg;
        }
        String artifactId = classFile.getName().replace(".class", "");
        writePomXml(outputDir, groupId, artifactId, "1.0-SNAPSHOT");
    }

    private static void generatePomForDir(File dir, File outputDir) {
        String groupId = "com.example";
        String artifactId = dir.getName();

        List<String> classFiles = new ArrayList<>();
        collectClassFiles(dir, classFiles);
        if (!classFiles.isEmpty()) {
            String firstClass = classFiles.get(0);
            String pkg = getClassPackage(new File(firstClass));
            if (!pkg.isEmpty()) {
                int firstDot = pkg.indexOf('.');
                groupId = firstDot > 0 ? pkg.substring(0, firstDot) : pkg;
            }
        }
        writePomXml(outputDir, groupId, artifactId, "1.0-SNAPSHOT");
    }

    private static String getClassPackage(File classFile) {
        try {
            byte[] bytes = FileUtil.readBytes(classFile);
            ClassReader cr = new ClassReader(bytes);
            final String[] pkg = {""};
            cr.accept(new ClassVisitor(Opcodes.ASM9) {
                @Override
                public void visit(int version, int access, String name,
                                  String signature, String superName, String[] interfaces) {
                    int lastSlash = name.lastIndexOf('/');
                    if (lastSlash > 0) {
                        pkg[0] = name.substring(0, lastSlash).replace('/', '.');
                    }
                }
            }, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);
            return pkg[0];
        } catch (Exception e) {
            return "";
        }
    }

    private static void writePomXml(File outputDir, String groupId, String artifactId, String version) {
        String pom = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
                "<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n" +
                "         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
                "         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 " +
                "http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n" +
                "    <modelVersion>4.0.0</modelVersion>\n" +
                "\n" +
                "    <groupId>" + groupId + "</groupId>\n" +
                "    <artifactId>" + artifactId + "</artifactId>\n" +
                "    <version>" + version + "</version>\n" +
                "\n" +
                "    <properties>\n" +
                "        <maven.compiler.source>8</maven.compiler.source>\n" +
                "        <maven.compiler.target>8</maven.compiler.target>\n" +
                "        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n" +
                "    </properties>\n" +
                "</project>\n";
        FileUtil.writeString(pom, new File(outputDir, "pom.xml"), "UTF-8");
        System.out.println("[JD2M] Generated pom.xml (groupId=" + groupId +
                ", artifactId=" + artifactId + ")");
    }

    private static void convertUnicode(File dir) {
        List<Future<?>> futures = new ArrayList<>();
        collectUnicodeTasks(dir, futures);
        for (Future<?> f : futures) {
            try {
                f.get();
            } catch (Exception ignored) {
            }
        }
        if (!futures.isEmpty()) {
            System.out.println("[JD2M] Unicode -> CN: " + futures.size() + " file(s)");
        }
    }

    private static void collectUnicodeTasks(File dir, List<Future<?>> futures) {
        File[] files = dir.listFiles();
        if (files == null) return;
        for (File f : files) {
            if (f.isDirectory()) {
                collectUnicodeTasks(f, futures);
            } else if (f.getName().endsWith(".java")) {
                futures.add(executor.submit(() -> {
                    try {
                        Unicode2CN.toCN(f);
                    } catch (Exception ignored) {
                    }
                }));
            }
        }
    }

    private static String shortenPath(String path) {
        int idx = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
        if (idx >= 0 && idx < path.length() - 1) {
            path = path.substring(idx + 1);
        }
        if (path.length() > 80) {
            path = "..." + path.substring(path.length() - 77);
        }
        return path;
    }

    public static class Result {
        public final int success;
        public final int failed;
        public final List<String> failedClasses;

        Result(int success, int failed, List<String> failedClasses) {
            this.success = success;
            this.failed = failed;
            this.failedClasses = failedClasses;
        }
    }
}

这个类是整个工具链的最后一步——反编译。它把 .class 文件反编译成 .java 源码,然后把源码中的 \uXXXX 转成中文,最终输出一个可导入 IDE 的 Maven 项目。

整体主流程:

1. 入口方法 decompile()
2. 分发方法 decompileFile() / decompileDirectory()
3. 核心反编译 doCfrDecompile()
4. 批量处理 decompileClassList()
5. jar 特殊处理 decompileJarClassByClass()
6. Maven 导出 extractMavenResources() / generatePom()
7. Unicode 转换 convertUnicode()
8. 辅助方法 shortenPath()、Result 类等

字段 #

private static final String[] JAR_EXTENSIONS = {".jar", ".war", ".ear"};
private static final String[] SKIP_PREFIXES = {"META-INF", "WEB-INF"};
private static final String BOOT_INF = "BOOT-INF/";
private static final String BOOT_INF_CLASSES = "BOOT-INF/classes/";

private static final ExecutorService executor = new ThreadPoolExecutor(
        8, 8, 60, TimeUnit.SECONDS,
        new LinkedBlockingQueue<>(),
        new ThreadPoolExecutor.CallerRunsPolicy());

这几个字段分别用于识别文件类型、过滤不需要的资源、处理 springboot 结构以及提供多线程能力。

JAR_EXTENSIONS #

后缀全称说明
.jarJava Archive标准 Java 库或应用包
.warWeb ArchiveWeb 应用包(Servlet/JSP 等)
.earEnterprise Archive企业级应用包(包含多个 war/jar)

SKIP_PREFIXES #

在提取 jar 包内的非 class 资源文件时,跳过这两个目录。

目录内容为什么跳过
META-INF/MANIFEST.MF、签名文件等构建信息,不是应用资源
WEB-INF/web.xml、lib/ 等Web 容器配置,不是应用资源

跳过这些避免把构建配置和签名文件混入 src/main/resources

BOOT_INF 和 BOOT_INF_CLASSES #

springboot 可执行 jar 包:

myapp.jar
├── BOOT-INF/
│   ├── classes/
│   │   ├── com/example/UserService.class   ← class 在这里
│   │   └── application.properties          ← 资源在这里
│   └── lib/
│       └── spring-boot-2.7.0.jar           ← 依赖包
├── META-INF/
│   └── MANIFEST.MF
└── org/springframework/boot/loader/        ← Spring Boot 启动器

判断是否为 springboot jar

private static boolean isSpringBootJar(JarFile jar) {
    return jar.getEntry(BOOT_INF) != null;
}

executor - 线程池 #

private static final ExecutorService executor = new ThreadPoolExecutor(
        8,                              // 核心线程数
        8,                              // 最大线程数
        60, TimeUnit.SECONDS,           // 空闲线程存活时间
        new LinkedBlockingQueue<>(),    // 无界任务队列
        new ThreadPoolExecutor.CallerRunsPolicy()  // 拒绝策略
);
参数含义
核心线程数8保持 8 个线程常驻
最大线程数8最多 8 个线程(和核心数一样,固定大小)
存活时间60 秒超过核心数的线程空闲 60 秒后回收(这里核心=最大,所以无额外线程)
队列LinkedBlockingQueue(无界)任务排队,不会因为队列满而触发拒绝
拒绝策略CallerRunsPolicy如果线程池已关闭,由提交任务的线程自己执行

decompile() - 入口 #

public static Result decompile(File input, File output, boolean mavenExport) throws IOException {
    if (!input.exists()) {  //检查输入是否存在
        throw new IllegalArgumentException("Input not found: " + input.getAbsolutePath());
    }

    long start = System.currentTimeMillis();   //计时开始
    System.out.println("[JD2M] Input : " + input.getAbsolutePath());
    System.out.println("[JD2M] Output: " + output.getAbsolutePath());

    Result result;
    //判断是否是文件或目录,调用对应的方法
    if (input.isFile()) {    
        result = decompileFile(input, output, mavenExport);
    } else {
        result = decompileDirectory(input, output, mavenExport);
    }

    convertUnicode(output);    //先进行反编译,再unicode转中文
    executor.shutdown();   //关闭线程池

    long elapsed = System.currentTimeMillis() - start;
    //打印统计信息
    System.out.println("[JD2M] Done in " + (elapsed / 1000) + "s — " +
            result.success + " OK, " + result.failed + " failed");
	//列出失败清单
    if (!result.failedClasses.isEmpty()) {
        System.out.println("[JD2M] Failed classes (" + result.failedClasses.size() + "):");
        for (String cls : result.failedClasses) {
            System.out.println("  FAIL: " + cls);
        }
    }
    return result;
}
参数含义
input输入:.class / .jar / .war / 目录
output输出目录
mavenExport是否生成 Maven 项目结构(pom.xml + src/main/java)
private static void convertUnicode(File dir) {
    List<Future<?>> futures = new ArrayList<>();
    collectUnicodeTasks(dir, futures);  //收集所有转换任务
    for (Future<?> f : futures) {    //等待所有任务完成
        try {
            f.get();    //阻塞当前线程,待对应任务执行完
        } catch (Exception ignored) {
        }
    }
    if (!futures.isEmpty()) {     //打印统计
        System.out.println("[JD2M] Unicode -> CN: " + futures.size() + " file(s)");
    }
}
//递归收集任务
private static void collectUnicodeTasks(File dir, List<Future<?>> futures) {
    File[] files = dir.listFiles();
    if (files == null) return;
    for (File f : files) {
        if (f.isDirectory()) {
            collectUnicodeTasks(f, futures);    //递归到子目录
        } else if (f.getName().endsWith(".java")) {
            futures.add(executor.submit(() -> {    //提交到线程池
                try {
                    Unicode2CN.toCN(f);    //unicode转换
                } catch (Exception ignored) {
                }
            }));
        }
    }
}

decompileFile() - 处理单个文件 #

private static Result decompileFile(File file, File output, boolean mavenExport) throws IOException {
    String name = file.getName().toLowerCase();
	//单个class文件,直接反编译
    if (name.endsWith(".class")) {
        String javaOutput = output.getAbsolutePath();
        if (mavenExport) {
            javaOutput = output.getAbsolutePath() + File.separator + "src" +
                    File.separator + "main" + File.separator + "java";
            generatePomForClassFile(file, output);
        }
        return decompileSingleClass(file.getAbsolutePath(), javaOutput);
    }
	//是jar/war/ear,解压到临时目录逐个反编译
    for (String ext : JAR_EXTENSIONS) {
        if (name.endsWith(ext)) {
            String javaOutput = output.getAbsolutePath();
            if (mavenExport) {
                javaOutput = output.getAbsolutePath() + File.separator + "src" +
                        File.separator + "main" + File.separator + "java";
            }

            Result result = decompileJarClassByClass(file, new File(javaOutput));
			//导出为maven项目,生成pom.xml
            if (mavenExport) {
                extractMavenResources(file, output);
            }
            return result;
        }
    }

    throw new IllegalArgumentException("Unsupported file type: " + file.getName());
}
//单个文件反编译时自动生成pom.xml的方法
private static void generatePomForClassFile(File classFile, File outputDir) {
    String groupId = "com.example";  //默认groupId
    String pkg = getClassPackage(classFile);   //从class文件中读取包名
    if (!pkg.isEmpty()) {
        //提取包名第一段作为groupId
        int firstDot = pkg.indexOf('.');
        groupId = firstDot > 0 ? pkg.substring(0, firstDot) : pkg;  
    }
    String artifactId = classFile.getName().replace(".class", "");
    writePomXml(outputDir, groupId, artifactId, "1.0-SNAPSHOT");//写入pom.xml
}
//用ASM快速读取class文件的包名
private static String getClassPackage(File classFile) {
        try {
            byte[] bytes = FileUtil.readBytes(classFile);
            ClassReader cr = new ClassReader(bytes);
            final String[] pkg = {""};
            cr.accept(new ClassVisitor(Opcodes.ASM9) {
                @Override
                public void visit(int version, int access, String name,
                                  String signature, String superName, String[] interfaces) {
                    int lastSlash = name.lastIndexOf('/');  //获取包名
                    if (lastSlash > 0) {
                        pkg[0] = name.substring(0, lastSlash).replace('/', '.');
                    }
                }
            }, ClassReader.SKIP_CODE | ClassReader.SKIP_DEBUG | ClassReader.SKIP_FRAMES);   //只获取包名
            return pkg[0];  //返回包名
        } catch (Exception e) {
            return "";
        }
    }

private static void writePomXml(File outputDir, String groupId, String artifactId, String version) {
	String pom = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n" +
		"<project xmlns=\"http://maven.apache.org/POM/4.0.0\"\n" +
		"         xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\"\n" +
		"         xsi:schemaLocation=\"http://maven.apache.org/POM/4.0.0 " +
		"http://maven.apache.org/xsd/maven-4.0.0.xsd\">\n" +
		"    <modelVersion>4.0.0</modelVersion>\n" +
		"\n" +
		"    <groupId>" + groupId + "</groupId>\n" +
		"    <artifactId>" + artifactId + "</artifactId>\n" +
		"    <version>" + version + "</version>\n" +
		"\n" +
		"    <properties>\n" +
		"        <maven.compiler.source>8</maven.compiler.source>\n" +
		"        <maven.compiler.target>8</maven.compiler.target>\n" +
		"        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>\n" +
		"    </properties>\n" +
		"</project>\n";
	FileUtil.writeString(pom, new File(outputDir, "pom.xml"), "UTF-8");
    //在输出目录下创建pom.xml
	System.out.println("[JD2M] Generated pom.xml (groupId=" + groupId +
	", artifactId=" + artifactId + ")");
}
private static Result decompileSingleClass(String classPath, String outputPath) {
    try {
        //方法参数为列表,进行包装
        doCfrDecompile(Collections.singletonList(classPath), outputPath);
        return new Result(1, 0, Collections.emptyList());
    } catch (Exception e) {
        System.err.println("[JD2M] Decompile failed: " + shortenPath(classPath) + " — " + e.getMessage());
        return new Result(0, 1, Collections.singletonList(shortenPath(classPath)));
    }
}
//路径缩短工具,便于日志输出
private static String shortenPath(String path) {
	int idx = Math.max(path.lastIndexOf('/'), path.lastIndexOf('\\'));
	if (idx >= 0 && idx < path.length() - 1) {
		path = path.substring(idx + 1);
	}
	if (path.length() > 80) {
		path = "..." + path.substring(path.length() - 77);
	}
	return path;
}
//调用CFR反编译器
private static void doCfrDecompile(List<String> sourcePaths, String outputPath) {
    Map<String, String> opts = new HashMap<>();
    opts.put("caseinsensitivefs", "true");//文件系统大小写不敏感
    opts.put("outputdir", outputPath);//反编译后的java文件输出的目录
    //构建CFR驱动
    OptionsImpl options = new OptionsImpl(opts);
    //CfrDriver.Builder()是构建器模式入口
    CfrDriver driver = new CfrDriver.Builder().withBuiltOptions(options).build();
    //执行反编译
    driver.analyse(sourcePaths);
}
//处理jar/war/ear的核心方法
private static Result decompileJarClassByClass(File jarFile, File outputDir) {
    //CFR无法直接读取jar包,先创建临时目录
    Path tempDir = null;
    try {
        tempDir = Files.createTempDirectory("jd2m-");//在系统临时目录下创建
        List<String> classPaths = new ArrayList<>();

        JarFile jar = new JarFile(jarFile);
        try {
            Enumeration<JarEntry> entries = jar.entries();//用来遍历jar包的文件和目录
            while (entries.hasMoreElements()) {
                JarEntry entry = entries.nextElement();
                if (entry.isDirectory() || !entry.getName().endsWith(".class")) continue;
				//把jar中的目录放到临时目录
                File destFile = new File(tempDir.toFile(), entry.getName());
                destFile.getParentFile().mkdirs();//确保父目录存在
                try (InputStream is = jar.getInputStream(entry)) {
                    FileUtil.writeFromStream(is, destFile);
                }
                classPaths.add(destFile.getAbsolutePath());//解压后的绝对路径添加进列表
            }
        } finally {
            jar.close();
        }

        System.out.println("[JD2M] Extracted " + classPaths.size() + " class(es), decompiling...");
		//批量反编译
        Result result = decompileClassList(classPaths, outputDir.getAbsolutePath());
        return result;

    } catch (IOException e) {
        return new Result(0, 0, Collections.singletonList("jar-extract: " + e.getMessage()));
    } finally {
        if (tempDir != null) {
            FileUtil.del(tempDir.toFile());//清理临时目录
        }
    }
}
private static Result decompileClassList(List<String> classPaths, String outputPath) {
    int success = 0;
    int failed = 0;
    List<String> failedClasses = new ArrayList<>();
    AtomicInteger progress = new AtomicInteger(0);  //线程安全的进度计数器
    int total = classPaths.size();  //总数

    for (String classPath : classPaths) {
        try {
            doCfrDecompile(Collections.singletonList(classPath), outputPath);
            success++;
        } catch (Exception e) {
            failed++;
            String shortName = shortenPath(classPath);
            failedClasses.add(shortName);
            System.err.println("[JD2M] Decompile failed: " + shortName + " — " + e.getMessage());
        }

        int done = progress.incrementAndGet();
        if (total > 20 && done % (total / 10) == 0) {
            System.out.println("[JD2M] Progress: " + done + "/" + total);
        }
    }
    return new Result(success, failed, failedClasses);
}
private static void extractMavenResources(File jarFile, File outputDir) throws IOException {
    String base = outputDir.getAbsolutePath();
    String resourceDir = base + File.separator + "src" + File.separator + "main" + File.separator + "resources";
    boolean pomExtracted = false;  //是否找到pom.xml

    JarFile jar = new JarFile(jarFile);
    try {
        boolean isBoot = isSpringBootJar(jar);//检测springboot结构
        if (isBoot) {
            System.out.println("[JD2M] Detected Spring Boot jar");
            String startClass = readManifestMainClass(jar, "Start-Class");
            if (startClass != null) {
                System.out.println("[JD2M] Start-Class: " + startClass);
            }
        }
		//遍历jar包所有条目
        Enumeration<JarEntry> entries = jar.entries();
        while (entries.hasMoreElements()) {
            JarEntry entry = entries.nextElement();
            String name = entry.getName();
            if (entry.isDirectory()) continue;

            if (!pomExtracted && name.endsWith("pom.xml")) {
                //普通maven构建的jar vs springboot构建的jar
                if (name.matches("META-INF/maven/.*/pom.xml") ||
                        (isBoot && name.matches(BOOT_INF_CLASSES + "META-INF/maven/.*/pom.xml"))) {
                    //找到后直接写入pom.xml
                    FileUtil.writeFromStream(jar.getInputStream(entry), base + File.separator + "pom.xml");
                    pomExtracted = true;
                    continue;
                }
            }

            if (name.endsWith(".class")) continue;
			//处理springboot路径偏移
            String resourceName = name;
            if (isBoot) {
                if (name.startsWith(BOOT_INF_CLASSES)) {
                    resourceName = name.substring(BOOT_INF_CLASSES.length());
                } else if (name.startsWith(BOOT_INF)) {
                    continue;
                }
            }
            if (resourceName.isEmpty()) continue;
			//跳过构建配置目录
            boolean skip = false;
            for (String prefix : SKIP_PREFIXES) {
                if (resourceName.startsWith(prefix)) { skip = true; break; }
            }
            if (skip) continue;
			//写入资源文件
            File dest = new File(resourceDir + File.separator + resourceName);
            FileUtil.writeFromStream(jar.getInputStream(entry), dest);
        }
    } finally {
        jar.close();
    }
	//没有找到pom.xml就自动生成
    if (!pomExtracted) {
        System.out.println("[JD2M] No pom.xml found, auto-generating...");
        generatePom(jarFile, outputDir);
    }
}
//从MANIFEST.MF文件中读取指定的属性值,用于获取Spring Boot的启动类名
private static String readManifestMainClass(JarFile jar, String attrName) {
    try {
        java.util.jar.Manifest mf = jar.getManifest();//直接解析META-INF/MANIFEST.MF
        if (mf != null) {
            return mf.getMainAttributes().getValue(attrName);//获取启动类
        }
    } catch (Exception ignored) {
    }
    return null;
}
//通过找出所有包名中出现的最多的取第一段作为groupId
private static void generatePom(File jarFile, File outputDir) throws IOException {
    String groupId = "com.example";
    String artifactId = jarFile.getName();
    int dot = artifactId.lastIndexOf('.');
    if (dot > 0) artifactId = artifactId.substring(0, dot);//去掉后缀

    JarFile jar = new JarFile(jarFile);
    try {
        boolean isBoot = isSpringBootJar(jar);
        Map<String, Integer> packageCount = new LinkedHashMap<>();//统计报名出现次数
        Enumeration<JarEntry> entries = jar.entries();
        while (entries.hasMoreElements()) {
            JarEntry entry = entries.nextElement();
            String name = entry.getName();
            if (!name.endsWith(".class")) continue;//只看class文件
            if (isBoot && !name.startsWith(BOOT_INF_CLASSES)) continue;
            int lastSlash = name.lastIndexOf('/');
            if (lastSlash <= 0) continue;
            String pkg = name.substring(0, lastSlash);
            if (isBoot && pkg.startsWith(BOOT_INF_CLASSES)) {
                pkg = pkg.substring(BOOT_INF_CLASSES.length());//提取包结构
            }
            pkg = pkg.replace('/', '.');
            if (pkg.isEmpty()) continue;
            packageCount.put(pkg, packageCount.getOrDefault(pkg, 0) + 1);
        }
        if (!packageCount.isEmpty()) {
            String bestPkg = packageCount.entrySet().stream()
                    .max(Map.Entry.comparingByValue()).get().getKey();
            int firstDot = bestPkg.indexOf('.');
            groupId = firstDot > 0 ? bestPkg.substring(0, firstDot) : bestPkg;
        }
    } finally {
        jar.close();
    }

    writePomXml(outputDir, groupId, artifactId, "1.0-SNAPSHOT");
}

decompileDirectory() - 处理目录 #

private static Result decompileDirectory(File dir, File output, boolean mavenExport) {
    List<String> classFiles = new ArrayList<>();
    collectClassFiles(dir, classFiles);//递归遍历整个目录树,把所有class文件收集到一个列表中

    if (classFiles.isEmpty()) {
        System.out.println("[JD2M] No .class files found.");
        return new Result(0, 0, Collections.emptyList());
    }

    System.out.println("[JD2M] Found " + classFiles.size() + " .class file(s), decompiling one by one...");
    //输出路径
    String javaOutput = output.getAbsolutePath();  
    if (mavenExport) {
        javaOutput = output.getAbsolutePath() + File.separator + "src" +
                File.separator + "main" + File.separator + "java";
        generatePomForDir(dir, output);//根据目录中的class文件自动推断并生成一个pom.xml
    }
    return decompileClassList(classFiles, javaOutput);//批量反编译
}
private static void collectClassFiles(File dir, List<String> result) {
    File[] files = dir.listFiles();
    if (files == null) return;
    for (File f : files) {
        if (f.isDirectory()) {
            collectClassFiles(f, result);//递归调用
        } else if (f.getName().endsWith(".class")) {
            result.add(f.getAbsolutePath());
        }
    }
}
private static void generatePomForDir(File dir, File outputDir) {
    String groupId = "com.example";
    String artifactId = dir.getName();

    List<String> classFiles = new ArrayList<>();
    collectClassFiles(dir, classFiles);
    if (!classFiles.isEmpty()) {
        String firstClass = classFiles.get(0);
        String pkg = getClassPackage(new File(firstClass));
        if (!pkg.isEmpty()) {
            int firstDot = pkg.indexOf('.');
            groupId = firstDot > 0 ? pkg.substring(0, firstDot) : pkg;
        }
    }
    writePomXml(outputDir, groupId, artifactId, "1.0-SNAPSHOT");
}
private static Result decompileClassList(List<String> classPaths, String outputPath) {
    int success = 0;
    int failed = 0;
    List<String> failedClasses = new ArrayList<>();
    AtomicInteger progress = new AtomicInteger(0);
    int total = classPaths.size();

    for (String classPath : classPaths) {
        try {
            doCfrDecompile(Collections.singletonList(classPath), outputPath);
            success++;
        } catch (Exception e) {
            failed++;
            String shortName = shortenPath(classPath);
            failedClasses.add(shortName);
            System.err.println("[JD2M] Decompile failed: " + shortName + " — " + e.getMessage());
        }

        int done = progress.incrementAndGet();
        if (total > 20 && done % (total / 10) == 0) {
            System.out.println("[JD2M] Progress: " + done + "/" + total);
        }
    }
    return new Result(success, failed, failedClasses);
}

这个方法处理目录输入的反编译。负责收集目录下所有 .class 文件,然后批量反编译。

参数含义
dir输入的目录,如 target/classes/
output输出的根目录
mavenExport是否生成 Maven 项目结构
返回值反编译结果(成功/失败数量)

Main.java #

package org.jd2m;

import java.io.File;
import java.io.IOException;

public class Main {

    private static final String VERSION = "1.0-SNAPSHOT";

    public static void main(String[] args) {
        if (args.length == 0) {
            printUsage();
            return;
        }

        String command = args[0].toLowerCase();

        switch (command) {
            case "decompile":
            case "d":
                handleDecompile(shiftArgs(args));
                break;
            case "strings":
            case "s":
                handleStrings(shiftArgs(args));
                break;
            case "project":
            case "p":
                handleProject(shiftArgs(args));
                break;
            case "help":
            case "-h":
            case "--help":
                printUsage();
                break;
            case "version":
            case "-v":
            case "--version":
                printVersion();
                break;
            default:
                if (new File(command).exists()) {
                    handleDecompile(args);
                } else {
                    System.err.println("Unknown command: " + command);
                    printUsage();
                }
                break;
        }
    }

    private static String[] shiftArgs(String[] args) {
        String[] result = new String[args.length - 1];
        System.arraycopy(args, 1, result, 0, result.length);
        return result;
    }

    private static void handleDecompile(String[] args) {
        String inputPath = null;
        String outputPath = null;
        boolean mavenExport = false;

        for (int i = 0; i < args.length; i++) {
            switch (args[i]) {
                case "--input":
                case "-i":
                    inputPath = args[++i];
                    break;
                case "--output":
                case "-o":
                    outputPath = args[++i];
                    break;
                case "--maven":
                case "-m":
                    mavenExport = true;
                    break;
                default:
                    if (inputPath == null) {
                        inputPath = args[i];
                    } else if (outputPath == null) {
                        outputPath = args[i];
                    }
                    break;
            }
        }

        if (inputPath == null) {
            System.err.println("Error: No input specified.");
            return;
        }

        File input = new File(inputPath);
        if (outputPath == null) {
            outputPath = deriveOutputName(input);
        }
        File output = new File(outputPath);

        try {
            Decompiler.decompile(input, output, mavenExport);
        } catch (IOException e) {
            System.err.println("[JD2M] Error: " + e.getMessage());
            e.printStackTrace();
        }
    }

    private static void handleStrings(String[] args) {
        String inputPath = null;
        int minLength = 4;

        for (int i = 0; i < args.length; i++) {
            switch (args[i]) {
                case "--min-length":
                case "-n":
                    minLength = Integer.parseInt(args[++i]);
                    if (minLength < 1) {
                        System.err.println("Warning: --min-length must be >= 1, using 4");
                        minLength = 4;
                    }
                    break;
                case "--input":
                case "-i":
                    inputPath = args[++i];
                    break;
                default:
                    if (inputPath == null) {
                        inputPath = args[i];
                    }
                    break;
            }
        }

        if (inputPath == null) {
            System.err.println("Error: No input specified.");
            return;
        }

        StringExtractor.extractFromInput(new File(inputPath), minLength);
    }

    private static void handleProject(String[] args) {
        String jarPath = null;
        String outputPath = null;

        for (int i = 0; i < args.length; i++) {
            switch (args[i]) {
                case "--input":
                case "-i":
                    jarPath = args[++i];
                    break;
                case "--output":
                case "-o":
                    outputPath = args[++i];
                    break;
                default:
                    if (jarPath == null) {
                        jarPath = args[i];
                    } else if (outputPath == null) {
                        outputPath = args[i];
                    }
                    break;
            }
        }

        if (jarPath == null) {
            System.err.println("Error: No input jar specified.");
            return;
        }

        File jarFile = new File(jarPath);
        if (!jarFile.exists() || !jarFile.getName().toLowerCase().endsWith(".jar")) {
            System.err.println("Error: Input must be a .jar file.");
            return;
        }

        if (outputPath == null) {
            outputPath = deriveOutputName(jarFile);
        }
        File output = new File(outputPath);

        try {
            Decompiler.decompile(jarFile, output, true);
            System.out.println("[JD2M] Maven project exported to: " + output.getAbsolutePath());
        } catch (IOException e) {
            System.err.println("[JD2M] Error: " + e.getMessage());
            e.printStackTrace();
        }
    }

    private static String deriveOutputName(File input) {
        String name = input.getName();
        int dot = name.lastIndexOf('.');
        String base = dot > 0 ? name.substring(0, dot) : name;
        return input.getParent() + File.separator + base + "-source";
    }

    private static void printVersion() {
        System.out.println("JD2M version " + VERSION);
    }

    private static void printUsage() {
        System.out.println("JD2M - Java Decompile to Maven & CTF Helper");
        System.out.println();
        System.out.println("Commands:");
        System.out.println("  decompile (d)  Decompile .class/.jar/.war/dir to Java source");
        System.out.println("  strings   (s)  Extract string constants from class files");
        System.out.println("  project   (p)  Export .jar as Maven project (with pom + resources)");
        System.out.println("  help      (h)  Print this help");
        System.out.println("  version   (v)  Print version");
        System.out.println();
        System.out.println("Decompile usage:");
        System.out.println("  jd2m decompile <input> [output] [--maven]");
        System.out.println("    <input>   Jar, .class, .war, or directory");
        System.out.println("    [output]  Output directory (default: <input>-source)");
        System.out.println("    --maven   Export as Maven project (for jars)");
        System.out.println();
        System.out.println("Strings usage:");
        System.out.println("  jd2m strings <input> [--min-length N]");
        System.out.println("    <input>      Jar, .class, or directory");
        System.out.println("    --min-length  Minimum string length (default: 4)");
        System.out.println();
        System.out.println("Examples:");
        System.out.println("  jd2m d target.jar                    # Decompile jar to flat files");
        System.out.println("  jd2m decompile app.jar --maven       # Export as Maven project");
        System.out.println("  jd2m d Foo.class                     # Decompile single class");
        System.out.println("  jd2m d ./classes/                    # Decompile all .class in dir");
        System.out.println("  jd2m s app.jar --min-length 6        # Extract strings (6+ chars)");
        System.out.println("  jd2m s Foo.class                     # Extract strings from a class");
    }
}

main() - 入口 #

public static void main(String[] args) {
    if (args.length == 0) {
        printUsage();
        return;
    }
    String command = args[0].toLowerCase();
    switch (command) {
        case "decompile": case "d":  handleDecompile()
        case "strings":   case "s":  handleStrings()
        case "project":   case "p":  handleProject()
        case "help":                  printUsage()
        case "version":               printVersion()
        default:                      如果第一个参数是存在的文件路径自动识别为反编译
    }
}

三种核心命令

命令简写功能调用的类
decompiled反编译为 Java 源码Decompiler
stringss提取字节码中的字符串StringExtractor
projectp反编译并导出 Maven 项目Decompiler(强制 mavenExport=true)

handleDecompile() #

private static void handleDecompile(String[] args) {
    String inputPath = null;
    String outputPath = null;
    boolean mavenExport = false;

    for (int i = 0; i < args.length; i++) {
        switch (args[i]) {
            case "--input":
            case "-i":
                inputPath = args[++i];
                break;
            case "--output":
            case "-o":
                outputPath = args[++i];
                break;
            case "--maven":
            case "-m":
                mavenExport = true;
                break;
            default:
                if (inputPath == null) {
                    inputPath = args[i];
                } else if (outputPath == null) {
                    outputPath = args[i];
                }
                break;
        }
    }

    if (inputPath == null) {
        System.err.println("Error: No input specified.");
        return;
    }

    File input = new File(inputPath);
    if (outputPath == null) {
        outputPath = deriveOutputName(input);
    }
    File output = new File(outputPath);

    try {
        Decompiler.decompile(input, output, mavenExport);
    } catch (IOException e) {
        System.err.println("[JD2M] Error: " + e.getMessage());
        e.printStackTrace();
    }
}

支持的参数

参数简写作用
--input-i输入文件/目录
--output-o输出目录(可选,默认自动生成)
--maven-m是否导出为 Maven 项目

handleStrings() #

private static void handleStrings(String[] args) {
    String inputPath = null;
    int minLength = 4;

    for (int i = 0; i < args.length; i++) {
        switch (args[i]) {
            case "--min-length":
            case "-n":
                minLength = Integer.parseInt(args[++i]);
                if (minLength < 1) {
                    System.err.println("Warning: --min-length must be >= 1, using 4");
                    minLength = 4;
                }
                break;
            case "--input":
            case "-i":
                inputPath = args[++i];
                break;
            default:
                if (inputPath == null) {
                    inputPath = args[i];
                }
                break;
        }
    }

    if (inputPath == null) {
        System.err.println("Error: No input specified.");
        return;
    }

    StringExtractor.extractFromInput(new File(inputPath), minLength);
}

支持的参数

参数简写作用
--input-i输入文件/目录
--min-length-n最小字符串长度(默认 4)

handleProject() #

private static void handleProject(String[] args) {
    String jarPath = null;
    String outputPath = null;

    for (int i = 0; i < args.length; i++) {
        switch (args[i]) {
            case "--input":
            case "-i":
                jarPath = args[++i];
                break;
            case "--output":
            case "-o":
                outputPath = args[++i];
                break;
            default:
                if (jarPath == null) {
                    jarPath = args[i];
                } else if (outputPath == null) {
                    outputPath = args[i];
                }
                break;
        }
    }

    if (jarPath == null) {
        System.err.println("Error: No input jar specified.");
        return;
    }

    File jarFile = new File(jarPath);
    if (!jarFile.exists() || !jarFile.getName().toLowerCase().endsWith(".jar")) {
        System.err.println("Error: Input must be a .jar file.");
        return;
    }

    if (outputPath == null) {
        outputPath = deriveOutputName(jarFile);
    }
    File output = new File(outputPath);

    try {
        Decompiler.decompile(jarFile, output, true);
        System.out.println("[JD2M] Maven project exported to: " + output.getAbsolutePath());
    } catch (IOException e) {
        System.err.println("[JD2M] Error: " + e.getMessage());
        e.printStackTrace();
    }
}
  • 强制要求输入是 .jar 文件
  • 强制开启 Maven 导出模式(mavenExport=true

shiftArgs() #

// 去掉第一个元素(命令名),返回剩余参数
private static String[] shiftArgs(String[] args) {
    String[] result = new String[args.length - 1];
    System.arraycopy(args, 1, result, 0, result.length);
    return result;
}

deriveOutputName() #

private static String deriveOutputName(File input) {
    String name = input.getName();
    int dot = name.lastIndexOf('.');
    String base = dot > 0 ? name.substring(0, dot) : name;
    return input.getParent() + File.separator + base + "-source";
}

自动生成输出目录名:去掉文件后缀,加 -source

核心功能的调用链:

main()
  ├── "decompile" → handleDecompile()
  │     └── Decompiler.decompile(input, output, mavenExport)
  │           ├── .class → decompileSingleClass → doCfrDecompile → CFR
  │           ├── .jar   → decompileJarClassByClass → decompileClassList → CFR
  │           └── 目录   → collectClassFiles → decompileClassList → CFR
  │           └── convertUnicode → Unicode2CN.toCN
  ├── "strings" → handleStrings()
  │     └── StringExtractor.extractFromInput(input, minLength)
  │           ├── .class → extractFromClass → ASM 收集
  │           ├── .jar   → extractFromJar → 遍历 + ASM 收集
  │           └── 目录   → extractFromDir → 递归 + ASM 收集
  └── "project" → handleProject()
        └── Decompiler.decompile(jarFile, output, true)  ← 同上,强制 mavenExport