编程工具您现在的位置是:首页 > 工具箱 > 编程工具

Java 反编译工具的使用与对比分析

<a href='mailto:'>微wx笑</a>的头像微wx笑 2022-02-23编程工具 4 0关键字: Java  反编译  工具  

前言Java 反编译,一听可能觉得高深莫测,其实反编译并不是什么特别高级的操作,Java 对于 Class 字节码文件的生成有着严格的要求,如果你非常熟悉 Java 虚拟机规范,了解 Class 字节

前言

Java 反编译,一听可能觉得高深莫测,其实反编译并不是什么特别高级的操作,Java 对于 Class 字节码文件的生成有着严格的要求,如果你非常熟悉 Java 虚拟机规范,了解 Class 字节码文件中一些字节的作用,那么理解反编译的原理并不是什么问题。 甚至像下面这样的 Class 文件你都能看懂一二。let无知

let无知

一般在逆向研究和代码分析中,反编译用到的比较多。不过在日常开发中,有时候只是简单的看一下所用依赖类的反编译,也是十分重要的。let无知

恰好最近工作中也需要用到 Java 反编译,所以这篇文章介绍目前常见的的几种 Java 反编译工具的使用,在文章的最后也会通过编译速度语法支持以及代码可读性三个维度,对它们进行测试,分析几款工具的优缺点let无知

#Procyon

Github 链接:https://github.com/mstrobel/procyon (opens new window)Procyon 不仅仅是反编译工具,它其实是专注于 Java 代码的生成和分析的一整套的 Java 元编程工具。 主要包括下面几个部分:let无知

  • Core Frameworklet无知

  • Reflection Frameworklet无知

  • Expressions Frameworklet无知

  • Compiler Toolset (Experimental)let无知

  • Java Decompiler (Experimental)let无知

可以看到反编译只是 Procyon 的其中一个模块,Procyon 原来托管于 bitbucket,后来迁移到了 GitHub,根据 GitHub 的提交记录来看,也有将近两年没有更新了。不过也有依赖 Procyon 的其他的开源反编译工具如** decompiler-procyon**,更新频率还是很高的,下面也会选择这个工具进行反编译测试。let无知

#使用 Procyon

<!-- https://mvnrepository.com/artifact/org.jboss.windup.decompiler/decompiler-procyon --><dependency>
    <groupId>org.jboss.windup.decompiler</groupId>
    <artifactId>decompiler-procyon</artifactId>
    <version>5.1.4.Final</version></dependency>
1
2
3
4
5
6

写一个简单的反编译测试。let无知

package com.wdbyte.decompiler;import java.io.IOException;import java.nio.file.Path;import java.nio.file.Paths;import java.util.Iterator;import java.util.List;import org.jboss.windup.decompiler.api.DecompilationFailure;import org.jboss.windup.decompiler.api.DecompilationListener;import org.jboss.windup.decompiler.api.DecompilationResult;import org.jboss.windup.decompiler.api.Decompiler;import org.jboss.windup.decompiler.procyon.ProcyonDecompiler;/**
 * Procyon 反编译测试
 *
 *  @author https://github.com/niumoo
 * @date 2021/05/15
 */public class ProcyonTest {
    public static void main(String[] args) throws IOException {
        Long time = procyon("decompiler.jar", "procyon_output_jar");
        System.out.println(String.format("decompiler time: %dms", time));
    }
    public static Long procyon(String source,String targetPath) throws IOException {
        long start = System.currentTimeMillis();
        Path outDir = Paths.get(targetPath);
        Path archive = Paths.get(source);
        Decompiler dec = new ProcyonDecompiler();
        DecompilationResult res = dec.decompileArchive(archive, outDir, new DecompilationListener() {
            public void decompilationProcessComplete() {
                System.out.println("decompilationProcessComplete");
            }
            public void decompilationFailed(List<String> inputPath, String message) {
                System.out.println("decompilationFailed");
            }
            public void fileDecompiled(List<String> inputPath, String outputPath) {
            }
            public boolean isCancelled() {
                return false;
            }
        });

        if (!res.getFailures().isEmpty()) {
            StringBuilder sb = new StringBuilder();
            sb.append("Failed decompilation of " + res.getFailures().size() + " classes: ");
            Iterator failureIterator = res.getFailures().iterator();
            while (failureIterator.hasNext()) {
                DecompilationFailure dex = (DecompilationFailure)failureIterator.next();
                sb.append(System.lineSeparator() + "    ").append(dex.getMessage());
            }
            System.out.println(sb.toString());
        }
        System.out.println("Compilation results: " + res.getDecompiledFiles().size() + " succeeded, " + res.getFailures().size() + " failed.");
        dec.close();
        Long end = System.currentTimeMillis();
        return end - start;
    }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60

Procyon 在反编译时会实时输出反编译文件数量的进度情况,最后还会统计反编译成功和失败的 Class 文件数量。let无知

....五月 15, 2021 10:58:28 下午 org.jboss.windup.decompiler.procyon.ProcyonDecompiler$3 call
信息: Decompiling 650 / 783五月 15, 2021 10:58:30 下午 org.jboss.windup.decompiler.procyon.ProcyonDecompiler$3 call
信息: Decompiling 700 / 783五月 15, 2021 10:58:37 下午 org.jboss.windup.decompiler.procyon.ProcyonDecompiler$3 call
信息: Decompiling 750 / 783decompilationProcessComplete
Compilation results: 783 succeeded, 0 failed.
decompiler time: 40599ms
1
2
3
4
5
6
7
8
9
10

#Procyon GUI

对于 Procyon 反编译来说,在 GitHub 上也有基于此实现的开源 GUI 界面,感兴趣的可以下载尝试。 Github 地址:https://github.com/deathmarine/Luyten(opens new window)let无知

#CFR

GitHub 地址:https://github.com/leibnitz27/cfr (opens new window)CFR 官方网站:http://www.benf.org/other/cfr/ (opens new window)(访问可能需要FQ) Maven 仓库: https://mvnrepository.com/artifact/org.benf/cfr(opens new window)let无知

CFR(Class File Reader) 可以支持 Java 9、Java 12、Java 14 以及其他的最新版 Java 代码的反编译工作。而且 CFR 本身的代码是由 Java 6 编写,所以基本可以使用 CFR 在任何版本的 Java 程序中。值得一提的是,使用 CFR 甚至可以将使用其他语言编写的的 JVM 类文件反编译回 Java 文件。let无知

#CFR 命令行使用

使用 CFR 反编译时,你可以下载已经发布的 JAR 包,进行命令行反编译,也可以使用 Maven 引入的方式,在代码中使用。下面先说命令行运行的方式。let无知

直接在 GitHub Tags 下载 (opens new window)已发布的最新版 JAR. 可以直接运行查看帮助。let无知

# 查看帮助java -jar cfr-0.151.jar --help
1
2

如果只是反编译某个 class.let无知

# 反编译 class 文件,结果输出到控制台java -jar cfr-0.151.jar WindupClasspathTypeLoader.class# 反编译 class 文件,结果输出到 out 文件夹java -jar cfr-0.151.jar WindupClasspathTypeLoader.class --outputpath ./out
1
2
3
4

反编译某个 JAR.let无知

# 反编译 jar 文件,结果输出到 output_jar 文件夹➜  Desktop java -jar cfr-0.151.jar decompiler.jar --outputdir ./output_jar
Processing decompiler.jar (use silent to silence)Processing com.strobel.assembler.metadata.ArrayTypeLoader
Processing com.strobel.assembler.metadata.ParameterDefinition
Processing com.strobel.assembler.metadata.MethodHandle
Processing com.strobel.assembler.metadata.signatures.FloatSignature.....
1
2
3
4
5
6
7
8

反编译结果会按照 class 的包路径写入到指定文件夹中。 let无知

#CFR 代码中使用

添加依赖这里不提。let无知

<!-- https://mvnrepository.com/artifact/org.benf/cfr --><dependency>
    <groupId>org.benf</groupId>
    <artifactId>cfr</artifactId>
    <version>0.151</version>
</dependency>
1
2
3
4
5
6

实际上我在官方网站和 GitHub 上都没有看到具体的单元测试示例。不过没有关系,既然能在命令行运行,那么直接在 IDEA 中查看反编译后的 Main 方法入口,看下命令行是怎么执行的,就可以写出自己的单元测试了。let无知

package com.wdbyte.decompiler;import java.io.IOException;import java.util.ArrayList;import java.util.HashMap;import java.util.List;import org.benf.cfr.reader.api.CfrDriver;import org.benf.cfr.reader.util.getopt.OptionsImpl;/**
 * CFR Test
 *
 * @author https://github.com/niumoo
 * @date 2021/05/15
 */public class CFRTest {
    public static void main(String[] args) throws IOException {
        Long time = cfr("decompiler.jar", "./cfr_output_jar");
        System.out.println(String.format("decompiler time: %dms", time));
        // decompiler time: 11655ms
    }
    public static Long cfr(String source, String targetPath) throws IOException {
        Long start = System.currentTimeMillis();
        // source jar
        List<String> files = new ArrayList<>();
        files.add(source);
        // target dir
        HashMap<String, String> outputMap = new HashMap<>();
        outputMap.put("outputdir", targetPath);

        OptionsImpl options = new OptionsImpl(outputMap);
        CfrDriver cfrDriver = new CfrDriver.Builder().withBuiltOptions(options).build();
        cfrDriver.analyse(files);
        Long end = System.currentTimeMillis();
        return (end - start);
    }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38

#JD-Core

GiHub 地址:https://github.com/java-decompiler/jd-core (opens new window)JD-core 官方网址:https://java-decompiler.github.io/ (opens new window)JD-core 是一个的独立的 Java 库,可以用于 Java 的反编译,支持从 Java 1 至 Java 12 的字节码反编译,包括 Lambda 表达式、方式引用、默认方法等。知名的 JD-GUI 和 Eclipse 无缝集成反编译引擎就是 JD-core。 JD-core 提供了一些反编译的核心功能,也提供了单独的 Class 反编译方法,但是如果你想在自己的代码中去直接反编译整个 JAR 包,还是需要一些改造的,如果是代码中有匿名函数,Lambda 等,虽然可以直接反编译,不过也需要额外考虑。let无知

#使用 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>
1
2
3
4
5
6

为了可以反编译整个 JAR 包,使用的代码我做了一些简单改造,以便于最后一部分的对比测试,但是这个示例中没有考虑内部类,Lambda 等会编译出多个 Class 文件的情况,所以不能直接使用在生产中。let无知

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) {}
    };}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128

#JD-GUI

GitHub 地址:https://github.com/java-decompiler/jd-gui (opens new window)JD-core 也提供了官方的 GUI 界面,需要的也可以直接下载尝试。 let无知

#Jadx

GitHub 地址:https://github.com/skylot/jadx (opens new window)Jadx 是一款可以反编译 JAR、APK、DEX、AAR、AAB、ZIP 文件的反编译工具,并且也配有 Jadx-gui 用于界面操作。 Jadx 使用 Grade 进行依赖管理,可以自行克隆仓库打包运行。let无知

git clone https://github.com/skylot/jadx.gitcd 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 helpExample:
  jadx -d out classes.dex
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46

根据 HELP 信息,如果想要反编译 decompiler.jar 到 out 文件夹。let无知

./build/jadx/bin/jadx -d ./out ~/Desktop/decompiler.jar 
INFO  - loading ...
INFO  - processing ...
INFO  - doneress: 1143 of 1217 (93%)
1
2
3
4

#Fernflower

GitHub 地址:https://github.com/fesh0r/fernflower (opens new window)Fernflower 和 Jadx 一样使用 Grade 进行依赖管理,可以自行克隆仓库打包运行。let无知

➜  fernflower-master ./gradlew build

BUILD SUCCESSFUL in 32s4 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:  ... doneINFO:  Decompiling class com/strobel/assembler/metadata/ParameterDefinition
INFO:  ... doneINFO:  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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22

Fernflower 在反编译 JAR 包时,默认反编译的结果也是一个 JAR 包。Jadlet无知

#反编译速度

到这里已经介绍了五款 Java 反编译工具了,那么在日常开发中我们应该使用哪一个呢?又或者在代码分析时我们又该选择哪一个呢?我想这两种情况的不同,使用时的关注点也是不同的。如果是日常使用,读读代码,我想应该是对可读性要求更高些,如果是大量的代码分析工作,那么可能反编译的速度和语法的支持上要求更高些。 为了能有一个简单的参考数据,我使用 JMH 微基准测试工具分别对这五款反编译工具进行了简单的测试,下面是一些测试结果。let无知

测试环境let无知

环境变量描述
处理器2.6 GHz 六核Intel Core i7
内存16 GB 2667 MHz DDR4
Java 版本JDK 14.0.2
测试方式JMH 基准测试。
待反编译 JAR 1procyon-compilertools-0.5.33.jar (1.5 MB)
待反编译 JAR 2python2java4common-1.0.0-20180706.084921-1.jar (42 MB)

let无知

反编译 JAR 1:procyon-compilertools-0.5.33.jar (1.5 MB)let无知

BenchmarkModeCntScoreUnits
cfravgt106548.642 ±  363.502ms/op
fernfloweravgt1012699.147 ± 1081.539ms/op
jdcoreavgt105728.621 ±  310.645ms/op
procyonavgt1026776.125 ± 2651.081ms/op
jadxavgt107059.354 ±  323.351ms/op

let无知

反编译 JAR 2:  python2java4common-1.0.0-20180706.084921-1.jar (42 MB)let无知

JAR 2 这个包是比较大的,是拿很多代码仓库合并到一起的,同时还有很多 Python 转 Java 生成的代码,理论上代码的复杂度会更高。let无知

BenchmarkCntScore
Cfr1413838.826ms
fernflower1246819.168ms
jdcore1Error
procyon1487647.181ms
jadx1505600.231ms

let无知

#语法支持和可读性

如果反编译后的代码需要自己看的话,那么可读性更好的代码更占优势,下面我写了一些代码,主要是 Java 8 及以下的代码语法和一些嵌套的流程控制,看看反编译后的效果如何。let无知

  • 查看源码let无知

  • Procyonlet无知

  • CFRlet无知

  • JD-CORElet无知

  • JadXlet无知

  • Fernflowerlet无知

package com.wdbyte.decompiler;import java.util.ArrayList;import java.util.List;import java.util.stream.IntStream;import org.benf.cfr.reader.util.functors.UnaryFunction;/**
 * @author https://www.wdbyte.com
 * @date 2021/05/16
 */public class HardCode <A, B> {
    public HardCode(A a, B b) { }

    public static void test(int... args) { }

    public static void main(String... args) {
        test(1, 2, 3, 4, 5, 6);
    }

    int byteAnd0() {
        int b = 1;
        int x = 0;
        do {
            b = (byte)((b ^ x));
        } while (b++ < 10);
        return b;
    }

    private void a(Integer i) {
        a(i);
        b(i);
        c(i);
    }

    private void b(int i) {
        a(i);
        b(i);
        c(i);
    }

    private void c(double d) {
        c(d);
        d(d);
    }

    private void d(Double d) {
        c(d);
        d(d);
    }

    private void e(Short s) {
        b(s);
        c(s);
        e(s);
        f(s);
    }

    private void f(short s) {
        b(s);
        c(s);
        e(s);
        f(s);
    }

    void test1(String path) {
        try {
            int x = 3;
        } catch (NullPointerException t) {
            System.out.println("File Not found");
            if (path == null) { return; }
            throw t;
        } finally {
            System.out.println("Fred");
            if (path == null) { throw new IllegalStateException(); }
        }
    }

    private final List<Integer> stuff = new ArrayList<>();{
        stuff.add(1);
        stuff.add(2);
    }

    public static int plus(boolean t, int a, int b) {
        int c = t ? a : b;
        return c;
    }

    // Lambda
    Integer lambdaInvoker(int arg, UnaryFunction<Integer, Integer> fn) {
        return fn.invoke(arg);
    }

    // Lambda
    public int testLambda() {
        return lambdaInvoker(3, x -> x + 1);
        //        return 1;
    }

    // Lambda
    public Integer testLambda(List<Integer> stuff, int y, boolean b) {
        return stuff.stream().filter(b ? x -> x > y : x -> x < 3).findFirst().orElse(null);
    }

    // stream
    public static <Y extends Integer> void testStream(List<Y> list) {
        IntStream s = list.stream()
            .filter(x -> {
                System.out.println(x);
                return x.intValue() / 2 == 0;
                })
            .map(x -> (Integer)x+2)
            .mapToInt(x -> x);
        s.toArray();
    }

    // switch
    public void testSwitch1(){
        int i = 0;
        switch(((Long)(i + 1L)) + "") {
            case "1":
                System.out.println("one");
        }
    }
    // switch
    public void testSwitch2(String string){
        switch (string) {
            case "apples":
                System.out.println("apples");
                break;
            case "pears":
                System.out.println("pears");
                break;
        }
    }

    // switch
    public static void testSwitch3(int x) {
        while (true) {
            if (x < 5) {
                switch ("test") {
                    case "okay":
                        continue;
                    default:
                        continue;
                }
            }
            System.out.println("wow x2!");
        }
    }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152

#Procyon

看到 Procyon 的反编译结果,还是比较吃惊的,在正常反编译的情况下,反编译后的代码基本上都是原汁原味。唯一一处反编译后和源码语法上有变化的地方,是一个集合的初始化操作略有不同。let无知

  • 待反编译源码let无知

  • Procyon 反编译let无知

// 源码
 public HardCode(A a, B b) { }
 private final List<Integer> stuff = new ArrayList<>();{
    stuff.add(1);
    stuff.add(2);
 }
1
2
3
4
5
6

而其他部分代码, 比如装箱拆箱,Switch 语法,Lambda 表达式,流式操作以及流程控制等,几乎完全一致,阅读没有障碍。let无知

装箱拆箱操作反编译后完全一致,没有多余的类型转换代码。let无知

  • 待反编译源码let无知

  • Procyon 反编译let无知

// 源码private void a(Integer i) {
    a(i);
    b(i);
    c(i);}private void b(int i) {
    a(i);
    b(i);
    c(i);}private void c(double d) {
    c(d);
    d(d);}private void d(Double d) {
    c(d);
    d(d);}private void e(Short s) {
    b(s);
    c(s);
    e(s);
    f(s);}private void f(short s) {
    b(s);
    c(s);
    e(s);
    f(s);}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

Switch 部分也是一致,流程控制部分也没有变化。let无知

  • 待反编译源码let无知

  • Procyon 反编译let无知

// 源码 switchpublic void testSwitch1(){
    int i = 0;
    switch(((Long)(i + 1L)) + "") {
        case "1":
            System.out.println("one");
    }}public void testSwitch2(String string){
    switch (string) {
        case "apples":
            System.out.println("apples");
            break;
        case "pears":
            System.out.println("pears");
            break;
    }}public static void testSwitch3(int x) {
    while (true) {
        if (x < 5) {
            switch ("test") {
                case "okay":
                    continue;
                default:
                    continue;
            }
        }
        System.out.println("wow x2!");
    }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33

Lambda 表达式和流式操作完全一致。let无知

  • 源码let无知

  • Procyon 反编译let无知

// 源码// Lambdapublic Integer testLambda(List<Integer> stuff, int y, boolean b) {
    return stuff.stream().filter(b ? x -> x > y : x -> x < 3).findFirst().orElse(null);}// streampublic static <Y extends Integer> void testStream(List<Y> list) {
    IntStream s = list.stream()
        .filter(x -> {
            System.out.println(x);
            return x.intValue() / 2 == 0;
            })
        .map(x -> (Integer)x+2)
        .mapToInt(x -> x);
    s.toArray();}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

流程控制,反编译后发现丢失了无意义的代码部分,阅读来说并无障碍。let无知

  • 源码let无知

  • Procyon 反编译let无知

// 源码void test1(String path) {
    try {
        int x = 3;
    } catch (NullPointerException t) {
        System.out.println("File Not found");
        if (path == null) { return; }
        throw t;
    } finally {
        System.out.println("Fred");
        if (path == null) { throw new IllegalStateException(); }
    }}
1
2
3
4
5
6
7
8
9
10
11
12
13

鉴于代码篇幅,下面几种的反编译结果的对比只会列出不同之处,相同之处会直接跳过。let无知

#CFR

CFR 的反编译结果多出了类型转换部分,个人来看没有 Procyon 那么原汁原味,不过也算是十分优秀,测试案例中唯一不满意的地方是对 while continue 的处理。let无知

// CFR 反编译结果// 装箱拆箱private void e(Short s) {
   this.b(s.shortValue()); // 装箱拆箱多出了类型转换部分。
   this.c(s.shortValue()); // 装箱拆箱多出了类型转换部分。
   this.e(s);
   this.f(s);}// 流程控制void test1(String path) {
    try {
        int n = 3;// 流程控制反编译结果十分满意,原汁原味,甚至此处的无意思代码都保留了。
    }
    catch (NullPointerException t) {
        System.out.println("File Not found");
        if (path == null) {
            return;
        }
        throw t;
    }
    finally {
        System.out.println("Fred");
        if (path == null) {
            throw new IllegalStateException();
        }
    }}// Lambda 和 Stream 操作完全一致,不提。// switch 处,反编译后功能一致,但是流程控制有所更改。public static void testSwitch3(int x) {
    block6: while (true) { // 源码中只有 while(true),反编译后多了 block6
        if (x < 5) {
            switch ("test") {
                case "okay": {
                    continue block6; // 多了 block6
                }
            }
            continue;
        }
        System.out.println("wow x2!");
    }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42

#JD-Core

JD-Core 和 CFR 一样,对于装箱拆箱操作,反编译后不再一致,多了类型转换部分,而且自动优化了数据类型。个人感觉,如果是反编译后自己阅读,通篇的数据类型的转换优化影响还是挺大的。let无知

// JD-Core 反编译private void d(Double d) {
  c(d.doubleValue()); // 新增了数据类型转换
  d(d);}private void e(Short s) {
  b(s.shortValue()); // 新增了数据类型转换
  c(s.shortValue()); // 新增了数据类型转换
  e(s);
  f(s.shortValue()); // 新增了数据类型转换}private void f(short s) {
  b(s);
  c(s);
  e(Short.valueOf(s)); // 新增了数据类型转换
  f(s);}// Stream 操作中,也自动优化了数据类型转换,阅读起来比较累。public static <Y extends Integer> void testStream(List<Y> list) {
  IntStream s = list.stream().filter(x -> {
        System.out.println(x);
        return (x.intValue() / 2 == 0);
      }).map(x -> Integer.valueOf(x.intValue() + 2)).mapToInt(x -> x.intValue());
  s.toArray();}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27

#Jadx

首先 Jadx 在反编译测试代码时,报出了错误,反编译的结果里也有提示不能反编 Lambda 和 Stream 操作,反编译结果中变量名称杂乱无章流程控制几乎阵亡,如果你想反编译后生物肉眼阅读,Jadx 肯定不是一个好选择。let无知

// Jadx 反编译private void e(Short s) {
    b(s.shortValue());// 新增了数据类型转换
    c((double) s.shortValue());// 新增了数据类型转换
    e(s);
    f(s.shortValue());// 新增了数据类型转换}private void f(short s) {
    b(s);
    c((double) s);// 新增了数据类型转换
    e(Short.valueOf(s));// 新增了数据类型转换
    f(s);}public int testLambda() { // testLambda 反编译失败
    /*
        r2 = this;
        r0 = 3
        r1 = move-result
        java.lang.Integer r0 = r2.lambdaInvoker(r0, r1)
        int r0 = r0.intValue()
        return r0
    */
    throw new UnsupportedOperationException("Method not decompiled: com.wdbyte.decompiler.HardCode.testLambda():int");}// Stream 反编译失败public static <Y extends java.lang.Integer> void testStream(java.util.List<Y> r3) {
    /*
        java.util.stream.Stream r1 = r3.stream()
        r2 = move-result
        java.util.stream.Stream r1 = r1.filter(r2)
        r2 = move-result
        java.util.stream.Stream r1 = r1.map(r2)
        r2 = move-result
        java.util.stream.IntStream r0 = r1.mapToInt(r2)
        r0.toArray()
        return
    */
    throw new UnsupportedOperationException("Method not decompiled: com.wdbyte.decompiler.HardCode.testStream(java.util.List):void");}public void testSwitch2(String string) { // switch 操作无法正常阅读,和源码出入较大。
    char c = 65535;
    switch (string.hashCode()) {
        case -1411061671:
            if (string.equals("apples")) {
                c = 0;
                break;
            }
            break;
        case 106540109:
            if (string.equals("pears")) {
                c = 1;
                break;
            }
            break;
    }
    switch (c) {
        case 0:
            System.out.println("apples");
            return;
        case 1:
            System.out.println("pears");
            return;
        default:
            return;
    }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67

#Fernflower

Fernflower 的反编译结果总体上还是不错的,不过也有不足,它对变量名称的指定,以及 Switch 字符串时的反编译结果不够理想。let无知

//反编译后变量命名不利于阅读,有很多 var 变量int byteAnd0() {
   int b = 1;
   byte x = 0;

   byte var10000;
   do {
      int b = (byte)(b ^ x);
      var10000 = b;
      b = b + 1;
   } while(var10000 < 10);

   return b;}// switch 反编译结果使用了hashCodepublic static void testSwitch3(int x) {
   while(true) {
      if (x < 5) {
         String var1 = "test";
         byte var2 = -1;
         switch(var1.hashCode()) {
         case 3412756: 
            if (var1.equals("okay")) {
               var2 = 0;
           }
         default:
            switch(var2) {
            case 0:
            }
         }
      } else {
         System.out.println("wow x2!");
      }
   }}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35

#总结

五种反编译工具比较下来,结合反编译速度和代码可读性测试,看起来 CFR 工具胜出,Procyon 紧随其后。CFR 在速度上不落下风,在反编译的代码可读性上,是最好的,主要体现在反编译后的变量命名装箱拆箱类型转换流程控制上,以及对 Lambda 表达式、Stream 流式操作和 Switch 的语法支持上,都非常优秀。根据 CFR 官方介绍,已经支持到 Java 14 语法,而且截止写这篇测试文章时,CFR 最新提交代码时间实在 11 小时之前,更新速度很快。let无知

文章中部分代码已经上传 GitHub :github.com/niumoo/lab-notes/tree/master/java-decompilerlet无知


let无知

文章作者: 程序猿阿朗
文章链接:https://www.wdbyte.com/2021/05/java-decompiler/
版权声明:本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自 未读代码


let无知

本文为转载文章,版权归原作者所有,不代表本站立场和观点。

很赞哦! () 有话说 ()