JD-Core
GiHub 地址:https://github.com/java-decompiler/jd-core
JD-core 官方网址:https://java-decompiler.github.io/
JD-core 是一个的独立的 Java 库,可以用于 Java 的反编译,支持从 Java 1 至 Java 12 的字节码反编译,包括 Lambda 表达式、方式引用、默认方法等。知名的 JD-GUI 和 Eclipse 无缝集成反编译引擎就是 JD-core。JD-core 提供了一些反编译的核心功能,也提供了单独的 Class 反编译方法,但是如果你想在自己的代码中去直接反编译整个 JAR 包,还是需要一些改造的,如果是代码中有匿名函数,Lambda 等,虽然可以直接反编译,不过也需要额外考虑。
使用 JD-core
<!-- https://mvnrepository.com/artifact/org.jd/jd-core --> <dependency> <groupId>org.jd</groupId> <artifactId>jd-core</artifactId> <version>1.1.3</version> </dependency>
为了可以反编译整个 JAR 包,使用的代码我做了一些简单改造,以便于最后一部分的对比测试,但是这个示例中没有考虑内部类,Lambda 等会编译出多个 Class 文件的情况,所以不能直接使用在生产中。
package com.wdbyte.decompiler; import java.io.File; import java.io.IOException; import java.io.InputStream; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.util.Enumeration; import java.util.HashMap; import java.util.jar.JarFile; import java.util.zip.ZipEntry; import java.util.zip.ZipFile; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.jd.core.v1.ClassFileToJavaSourceDecompiler; import org.jd.core.v1.api.loader.Loader; import org.jd.core.v1.api.printer.Printer; /** * @author https://github.com/niumoo * @date 2021/05/15 */ public class JDCoreTest { public static void main(String[] args) throws Exception { JDCoreDecompiler jdCoreDecompiler = new JDCoreDecompiler(); Long time = jdCoreDecompiler.decompiler("decompiler.jar","jd_output_jar"); System.out.println(String.format("decompiler time: %dms", time)); } } class JDCoreDecompiler{ private ClassFileToJavaSourceDecompiler decompiler = new ClassFileToJavaSourceDecompiler(); // 存放字节码 private HashMap<String,byte[]> classByteMap = new HashMap<>(); /** * 注意:没有考虑一个 Java 类编译出多个 Class 文件的情况。 * * @param source * @param target * @return * @throws Exception */ public Long decompiler(String source,String target) throws Exception { long start = System.currentTimeMillis(); // 解压 archive(source); for (String className : classByteMap.keySet()) { String path = StringUtils.substringBeforeLast(className, "/"); String name = StringUtils.substringAfterLast(className, "/"); if (StringUtils.contains(name, "$")) { name = StringUtils.substringAfterLast(name, "$"); } name = StringUtils.replace(name, ".class", ".java"); decompiler.decompile(loader, printer, className); String context = printer.toString(); Path targetPath = Paths.get(target + "/" + path + "/" + name); if (!Files.exists(Paths.get(target + "/" + path))) { Files.createDirectories(Paths.get(target + "/" + path)); } Files.deleteIfExists(targetPath); Files.createFile(targetPath); Files.write(targetPath, context.getBytes()); } return System.currentTimeMillis() - start; } private void archive(String path) throws IOException { try (ZipFile archive = new JarFile(new File(path))) { Enumeration<? extends ZipEntry> entries = archive.entries(); while (entries.hasMoreElements()) { ZipEntry entry = entries.nextElement(); if (!entry.isDirectory()) { String name = entry.getName(); if (name.endsWith(".class")) { byte[] bytes = null; try (InputStream stream = archive.getInputStream(entry)) { bytes = IOUtils.toByteArray(stream); } classByteMap.put(name, bytes); } } } } } private Loader loader = new Loader() { @Override public byte[] load(String internalName) { return classByteMap.get(internalName); } @Override public boolean canLoad(String internalName) { return classByteMap.containsKey(internalName); } }; private Printer printer = new Printer() { protected static final String TAB = " "; protected static final String NEWLINE = "\n"; protected int indentationCount = 0; protected StringBuilder sb = new StringBuilder(); @Override public String toString() { String toString = sb.toString(); sb = new StringBuilder(); return toString; } @Override public void start(int maxLineNumber, int majorVersion, int minorVersion) {} @Override public void end() {} @Override public void printText(String text) { sb.append(text); } @Override public void printNumericConstant(String constant) { sb.append(constant); } @Override public void printStringConstant(String constant, String ownerInternalName) { sb.append(constant); } @Override public void printKeyword(String keyword) { sb.append(keyword); } @Override public void printDeclaration(int type, String internalTypeName, String name, String descriptor) { sb.append(name); } @Override public void printReference(int type, String internalTypeName, String name, String descriptor, String ownerInternalName) { sb.append(name); } @Override public void indent() { this.indentationCount++; } @Override public void unindent() { this.indentationCount--; } @Override public void startLine(int lineNumber) { for (int i=0; i<indentationCount; i++) sb.append(TAB); } @Override public void endLine() { sb.append(NEWLINE); } @Override public void extraLine(int count) { while (count-- > 0) sb.append(NEWLINE); } @Override public void startMarker(int type) {} @Override public void endMarker(int type) {} }; }
JD-GUI
GitHub 地址:https://github.com/java-decompiler/jd-gui
JD-core 也提供了官方的 GUI 界面,需要的也可以直接下载尝试。
Jadx
GitHub 地址:https://github.com/skylot/jadx
Jadx 是一款可以反编译 JAR、APK、DEX、AAR、AAB、ZIP 文件的反编译工具,并且也配有 Jadx-gui 用于界面操作。Jadx 使用 Grade 进行依赖管理,可以自行克隆仓库打包运行。
git clone https://github.com/skylot/jadx.git cd jadx ./gradlew dist # 查看帮助 ./build/jadx/bin/jadx --help jadx - dex to java decompiler, version: dev usage: jadx [options] <input files> (.apk, .dex, .jar, .class, .smali, .zip, .aar, .arsc, .aab) options: -d, --output-dir - output directory -ds, --output-dir-src - output directory for sources -dr, --output-dir-res - output directory for resources -r, --no-res - do not decode resources -s, --no-src - do not decompile source code --single-class - decompile a single class --output-format - can be 'java' or 'json', default: java -e, --export-gradle - save as android gradle project -j, --threads-count - processing threads count, default: 6 --show-bad-code - show inconsistent code (incorrectly decompiled) --no-imports - disable use of imports, always write entire package name --no-debug-info - disable debug info --add-debug-lines - add comments with debug line numbers if available --no-inline-anonymous - disable anonymous classes inline --no-replace-consts - don't replace constant value with matching constant field --escape-unicode - escape non latin characters in strings (with \u) --respect-bytecode-access-modifiers - don't change original access modifiers --deobf - activate deobfuscation --deobf-min - min length of name, renamed if shorter, default: 3 --deobf-max - max length of name, renamed if longer, default: 64 --deobf-cfg-file - deobfuscation map file, default: same dir and name as input file with '.jobf' extension --deobf-rewrite-cfg - force to save deobfuscation map --deobf-use-sourcename - use source file name as class name alias --deobf-parse-kotlin-metadata - parse kotlin metadata to class and package names --rename-flags - what to rename, comma-separated, 'case' for system case sensitivity, 'valid' for java identifiers, 'printable' characters, 'none' or 'all' (default) --fs-case-sensitive - treat filesystem as case sensitive, false by default --cfg - save methods control flow graph to dot file --raw-cfg - save methods control flow graph (use raw instructions) -f, --fallback - make simple dump (using goto instead of 'if', 'for', etc) -v, --verbose - verbose output (set --log-level to DEBUG) -q, --quiet - turn off output (set --log-level to QUIET) --log-level - set log level, values: QUIET, PROGRESS, ERROR, WARN, INFO, DEBUG, default: PROGRESS --version - print jadx version -h, --help - print this help Example: jadx -d out classes.dex
根据 HELP 信息,如果想要反编译 decompiler.jar 到 out 文件夹。
./build/jadx/bin/jadx -d ./out ~/Desktop/decompiler.jar INFO - loading ... INFO - processing ... INFO - doneress: 1143 of 1217 (93%)
Fernflower
GitHub 地址:https://github.com/fesh0r/fernflower
Fernflower 和 Jadx 一样使用 Grade 进行依赖管理,可以自行克隆仓库打包运行。
➜ fernflower-master ./gradlew build BUILD SUCCESSFUL in 32s 4 actionable tasks: 4 executed ➜ fernflower-master java -jar build/libs/fernflower.jar Usage: java -jar fernflower.jar [-<option>=<value>]* [<source>]+ <destination> Example: java -jar fernflower.jar -dgs=true c:\my\source\ c:\my.jar d:\decompiled\ ➜ fernflower-master mkdir out ➜ fernflower-master java -jar build/libs/fernflower.jar ~/Desktop/decompiler.jar ./out INFO: Decompiling class com/strobel/assembler/metadata/ArrayTypeLoader INFO: ... done INFO: Decompiling class com/strobel/assembler/metadata/ParameterDefinition INFO: ... done INFO: Decompiling class com/strobel/assembler/metadata/MethodHandle ... ➜ fernflower-master ll out total 1288 -rw-r--r-- 1 darcy staff 595K 5 16 17:47 decompiler.jar ➜ fernflower-master
Fernflower 在反编译 JAR 包时,默认反编译的结果也是一个 JAR 包。Jad
反编译速度
到这里已经介绍了五款 Java 反编译工具了,那么在日常开发中我们应该使用哪一个呢?又或者在代码分析时我们又该选择哪一个呢?我想这两种情况的不同,使用时的关注点也是不同的。如果是日常使用,读读代码,我想应该是对可读性要求更高些,如果是大量的代码分析工作,那么可能反编译的速度和语法的支持上要求更高些。为了能有一个简单的参考数据,我使用 JMH 微基准测试工具分别对这五款反编译工具进行了简单的测试,下面是一些测试结果。
测试环境
环境变量 | 描述 |
处理器 | 2.6 GHz 六核Intel Core i7 |
内存 | 16 GB 2667 MHz DDR4 |
Java 版本 | JDK 14.0.2 |
测试方式 | JMH 基准测试。 |
待反编译 JAR 1 | procyon-compilertools-0.5.33.jar (1.5 MB) |
待反编译 JAR 2 | python2java4common-1.0.0-20180706.084921-1.jar (42 MB) |
反编译 JAR 1:procyon-compilertools-0.5.33.jar (1.5 MB)
Benchmark | Mode | Cnt | Score | Units |
cfr | avgt | 10 | 6548.642 ± 363.502 | ms/op |
fernflower | avgt | 10 | 12699.147 ± 1081.539 | ms/op |
jdcore | avgt | 10 | 5728.621 ± 310.645 | ms/op |
procyon | avgt | 10 | 26776.125 ± 2651.081 | ms/op |
jadx | avgt | 10 | 7059.354 ± 323.351 | ms/op |
反编译 JAR 2: python2java4common-1.0.0-20180706.084921-1.jar (42 MB)
JAR 2 这个包是比较大的,是拿很多代码仓库合并到一起的,同时还有很多 Python 转 Java 生成的代码,理论上代码的复杂度会更高。
Benchmark | Cnt | Score |
Cfr | 1 | 413838.826ms |
fernflower | 1 | 246819.168ms |
jdcore | 1 | Error |
procyon | 1 | 487647.181ms |
jadx | 1 | 505600.231ms |