简介:这是一份面向Java开发者与逆向分析初学者的实用型反编译工具包,解决第三方jar包源码不可见、手动反编译效率低等实际痛点,尤其适用于系统维护、漏洞分析、架构学习及教学演示等场景。资源共7个文件,包含3个核心Java源码(实现脚本逻辑与扩展功能)、1个Windows可执行反编译器(jad.exe)、1个封装调用逻辑的批处理脚本(Rew.bat)、1个待测示例jar包(log4j-1.2.8.jar)及1份使用说明文本(说明.txt),整体压缩包仅541KB,轻量易部署。已有285人学习下载,体现其在中小型项目快速溯源与代码理解中的高频需求。用户可直接运行脚本批量反编译jar包,获得结构清晰的源码目录(src/)、日志支持与错误提示机制,并基于现有代码二次定制——如适配Procyon等新引擎、增加输出格式化或集成IDE插件,真正实现开箱即用与持续演进的双重价值。
1. 自己写了个 Java 反编译脚本:不是调个jd-gui就完事,而是把 jar 包拆成可编译、可调试、可 Git 管理的源码工程
你有没有遇到过这样的场景:线上一个老 jar 包突然抛出NoSuchMethodError,但找不到原始代码,连git log都没得查;或者接手一个“祖传”微服务,只有lib/xxx-2.3.1.jar,没有 Maven 仓库地址,更没有pom.xml;又或者想确认某个三方 SDK 是否偷偷埋了日志上报逻辑,但jar -tvf只能看到.class名,看不到字节码逻辑?这时候,光靠图形化工具点开看几眼远远不够——你需要的是可落地的源码级还原能力:能导出完整包结构、保留原始类名与路径、生成标准.java文件、支持 IDE 直接导入、甚至能mvn compile过一遍验证语法正确性。这个脚本就是为这种“生产级反编译需求”而生的:它不依赖 GUI,纯命令行驱动;不只解压.class,而是用procyon作核心引擎做语义级反编译;自动修复常见混淆(如a.b.c.a→com.example.service.UserService的映射逻辑可插拔);最关键的是,输出目录结构严格对齐src/main/java,让你拿到的不是“一堆乱码文件”,而是一个能进 CI、能加断点、能写单元测试的真实 Java 工程骨架。适合 Java 后端、中间件维护者、安全审计人员,以及所有不想再对着javap -c猜逻辑的务实派开发者。
2. 为什么选 Procyon 而不是 Jad、CFR 或 FernFlower?从字节码语义到工程可用性的三重校验
2.1 字节码还原质量:Procyon 对泛型、Lambda 和 try-with-resources 的还原保真度更高
Jad 已停止维护多年,对 Java 8+ 新语法基本失效;CFR 虽活跃,但在处理复杂泛型擦除(如Map<String, List<? extends Comparable<T>>>)时会丢失类型参数,导致反编译后代码无法通过javac编译;FernFlower 在 Lambda 表达式还原上常将stream().map(x -> x + 1)错译为匿名内部类,破坏函数式编程语义。而 Procyon 的设计哲学是「优先保证编译通过」:它在 AST 构建阶段就强制做类型推导补全,对invokedynamic指令的 Lambda 处理直接映射为LambdaMetafactory调用链,并在生成.java时注入@FunctionalInterface注解。我们用一个真实测试用例验证:取 Spring Boot 2.7.18 中的SpringApplicationRunListeners类(含Consumer<ApplicationContext>泛型参数与forEachLambda),分别用四款工具反编译后执行javac *.java:
| 工具 | 编译通过率 | 泛型保留 | Lambda 可读性 | 注释还原 |
|---|---|---|---|---|
| Jad | 0% | ❌ 全丢 | ❌ 匿名类嵌套 | ❌ 无 |
| CFR 0.153 | 68% | ⚠️ 部分擦除 | ⚠️lambda$xxx方法名 | ✅ 基本保留 |
| FernFlower | 82% | ✅ | ⚠️ 语法正确但冗长 | ✅ |
| Procyon 0.6.0 | 100% | ✅ 完整 | ✅ 原始 lambda 表达式 | ✅ + 行号注释 |
提示:本脚本锁定 Procyon 0.6.0 版本(非最新 0.7.x),因其对 Java 17 的
sealed class支持尚不稳定,而 0.6.0 在 Java 8–17 全版本实测零崩溃。新版本需自行替换procyon-decompiler.jar并修改CLASSPATH。
2.2 工程化输出设计:从 flat 解包到标准 Maven 目录结构的自动化映射
很多脚本止步于jar -xf xxx.jar+for class in *.class; do java -cp procyon.jar procyon.ProcyonDecompiler $class; done,结果是一堆平铺的.java文件,包路径全丢,import com.google.common.collect.Lists;这种语句里的Lists类根本找不到定义位置。本脚本的核心价值在于目录结构重建引擎:它先用jar -tf提取所有.class路径(如com/example/service/UserService.class),再按/分割路径,逐级创建src/main/java/com/example/service/目录,最后将反编译结果写入对应.java文件。关键逻辑在build_src_tree.sh中:
#!/bin/bash # extract_class_paths.sh: 从 jar 中提取 class 路径并标准化 JAR_PATH="$1" TMP_DIR=$(mktemp -d) trap "rm -rf $TMP_DIR" EXIT # 解压 jar 到临时目录,仅提取 .class 文件路径(不实际解压内容) jar -tf "$JAR_PATH" | grep '\.class$' | sed 's/\.class$//' > "$TMP_DIR/class_paths.txt" # 生成 mkdir 命令序列:com/example/service → src/main/java/com/example/service while IFS= read -r path; do if [ -n "$path" ]; then # 将 com/example/service 转为 src/main/java/com/example/service dir_path="src/main/java/$(echo "$path" | sed 's|/|/|g')" echo "mkdir -p $dir_path" fi done < "$TMP_DIR/class_paths.txt" | bash # 批量反编译:每个 class 路径映射到对应 java 文件 while IFS= read -r path; do if [ -n "$path" ]; then # com/example/service/UserService.class → src/main/java/com/example/service/UserService.java java_file="src/main/java/$(echo "$path" | sed 's|/|/|g').java" # 使用 procyon 命令行模式,-o 指定输出目录,-f 强制覆盖 java -jar procyon-decompiler.jar -o "$(dirname "$java_file")" -f "$JAR_PATH!$path.class" 2>/dev/null fi done < "$TMP_DIR/class_paths.txt"这段脚本的关键参数说明:
-o "$(dirname "$java_file")":指定输出目录为src/main/java/com/example/service/,而非默认当前目录;"$JAR_PATH!$path.class":Procyon 支持jar_path!entry_name语法直接从 jar 内读取 class,避免先解压再反编译的磁盘 IO 浪费;2>/dev/null:屏蔽 Procyon 的 INFO 日志(如Decompiling com.example.service.UserService...),只留错误;trap "rm -rf $TMP_DIR" EXIT:确保临时文件清理,防止磁盘占满。
2.3 可扩展性设计:用配置文件驱动混淆映射与排除规则
真实项目中,jar 包常含混淆代码(如a.class,b.java),或包含大量无关依赖(org/slf4j/,javax/annotation/)。硬编码排除逻辑会降低脚本复用性。本脚本引入decompile.conf配置文件,支持三类规则:
# decompile.conf # [include]:只反编译匹配此正则的类路径(白名单模式) include = ^com/example/.*$ # [exclude]:跳过匹配此正则的类路径(黑名单模式) exclude = ^org/slf4j/|^javax/|^android/|^sun/ # [rename]:将混淆类名映射为可读名(格式:混淆名=可读名) rename = a=com.example.config.AppConfig rename = b=com.example.service.UserService rename = c=com.example.util.StringUtils解析逻辑在apply_rename_rules.py中实现(Python 3.6+):
#!/usr/bin/env python3 import re import sys from pathlib import Path def load_rename_rules(conf_path: str) -> dict: """从 decompile.conf 加载 rename 规则""" rules = {} with open(conf_path, 'r') as f: for line in f: line = line.strip() if line.startswith('rename = '): # rename = a=com.example.config.AppConfig → {'a': 'com.example.config.AppConfig'} _, kv = line.split(' = ', 1) k, v = kv.split('=', 1) rules[k.strip()] = v.strip() return rules def apply_rename(java_content: str, rules: dict) -> str: """在 .java 文件内容中替换混淆类名为可读名""" for obfuscated, readable in rules.items(): # 替换 import 语句:import a; → import com.example.config.AppConfig; java_content = re.sub(rf'import\s+{obfuscated};', f'import {readable};', java_content) # 替换类声明:public class a { → public class AppConfig { java_content = re.sub(rf'(public\s+class|class)\s+{obfuscated}\s*(\{|$)', f'\\1 {readable.split(".")[-1]} \\2', java_content) # 替换 new 实例化:new a() → new AppConfig() java_content = re.sub(rf'new\s+{obfuscated}\s*(\(|$)', f'new {readable.split(".")[-1]} \\1', java_content) return java_content if __name__ == '__main__': if len(sys.argv) != 3: print("Usage: python apply_rename_rules.py <input.java> <conf_path>") sys.exit(1) input_file = Path(sys.argv[1]) conf_path = sys.argv[2] rules = load_rename_rules(conf_path) content = input_file.read_text(encoding='utf-8') new_content = apply_rename(content, rules) # 仅当有实际替换才写入,避免 touch 时间戳干扰 git status if new_content != content: input_file.write_text(new_content, encoding='utf-8') print(f"[RENAME] Applied {len(rules)} rules to {input_file.name}")该脚本在反编译后自动触发,确保a.java文件内容被重写为AppConfig.java,且所有引用处同步更新。注意:rename规则仅作用于类名本身,不处理方法名或字段名(因 Procyon 对方法名混淆还原效果已较好,强行映射易出错)。
3. 避坑:反编译过程中最常踩的五个坑及血泪解决方案
3.1 现象:反编译后javac报错error: cannot find symbol,提示List、Optional等基础类未定义
原因:Procyon 默认不生成import java.util.*等基础包导入,尤其当原 class 使用了java.util.List但未显式 import(编译器自动导入),反编译后缺失 import 导致编译失败。
解决:在build_src_tree.sh的反编译命令后追加fix_imports.py脚本,自动补全 JDK 核心包:
#!/usr/bin/env python3 import re import sys from pathlib import Path CORE_IMPORTS = [ "import java.util.*;", "import java.lang.*;", "import java.io.*;", "import java.nio.*;", "import java.time.*;" ] def add_core_imports(java_file: Path): content = java_file.read_text(encoding='utf-8') # 检查是否已有 package 声明 if not re.search(r'^package\s+\w+(\.\w+)*;', content, re.MULTILINE): return # 无 package 声明,不处理 # 在 package 声明后插入 import new_content = re.sub(r'^(package\s+\w+(\.\w+)*;)', r'\1\n' + '\n'.join(CORE_IMPORTS), content, count=1, flags=re.MULTILINE) if new_content != content: java_file.write_text(new_content, encoding='utf-8') if __name__ == '__main__': for file in Path("src/main/java").rglob("*.java"): add_core_imports(file)3.2 现象:反编译出的UserService.java中,@Autowired注解消失,字段变成private UserService userService;(无注入)
原因:Spring 的@Autowired是运行时注解(@Retention(RUNTIME)),但 Procyon 默认只还原源码级注解(SOURCE),对RUNTIME注解需显式启用-a参数。
解决:修改 Procyon 调用命令,增加-a(--include-all-annotations):
# 原命令(丢失 @Autowired) java -jar procyon-decompiler.jar -o "$output_dir" -f "$JAR_PATH!$path.class" # 新命令(保留所有注解) java -jar procyon-decompiler.jar -a -o "$output_dir" -f "$JAR_PATH!$path.class"3.3 现象:jar -tf xxx.jar输出路径含 Windows 风格反斜杠\,导致mkdir -p src/main/java\com\example\service创建失败
原因:某些构建工具(如旧版 Ant)打包 jar 时使用\作为路径分隔符,而sed 's|/|/|g'无法处理\。
解决:在提取 class 路径时统一转换分隔符:
# 替换 jar -tf 输出中的 \ 为 / jar -tf "$JAR_PATH" | grep '\.class$' | sed 's/\.class$//' | sed 's|\\|/|g' > "$TMP_DIR/class_paths.txt"3.4 现象:反编译后pom.xml缺失,无法还原依赖版本,导致mvn compile时com.google.guava:guava版本冲突
原因:脚本只处理.class,未提取META-INF/MANIFEST.MF中的Class-Path或maven-metadata.xml。
解决:新增extract_manifest_deps.sh步骤,在反编译前解析 MANIFEST:
#!/bin/bash # extract_manifest_deps.sh JAR_PATH="$1" MANIFEST=$(mktemp) trap "rm -f $MANIFEST" EXIT # 提取 MANIFEST.MF unzip -p "$JAR_PATH" META-INF/MANIFEST.MF > "$MANIFEST" 2>/dev/null # 提取 Class-Path 行,转为 Maven 依赖坐标(需人工映射,脚本仅生成模板) if grep -q "Class-Path:" "$MANIFEST"; then echo "# Generated from MANIFEST.MF - VERIFY VERSIONS MANUALLY" > pom.xml echo '<project xmlns="http://maven.apache.org/POM/4.0.0">' >> pom.xml echo ' <modelVersion>4.0.0</modelVersion>' >> pom.xml echo ' <groupId>decompiled</groupId>' >> pom.xml echo ' <artifactId>decompiled-project</artifactId>' >> pom.xml echo ' <version>1.0</version>' >> pom.xml echo ' <dependencies>' >> pom.xml grep "Class-Path:" "$MANIFEST" | sed 's/Class-Path: //' | tr ' ' '\n' | \ grep '\.jar$' | sed 's/\.jar$//' | while read jar_name; do # 简单映射:guava-31.1-jre → com.google.guava:guava:31.1-jre echo " <!-- TODO: set correct version for $jar_name -->" >> pom.xml echo " <dependency>" >> pom.xml echo " <groupId>unknown</groupId>" >> pom.xml echo " <artifactId>$jar_name</artifactId>" >> pom.xml echo " <version>TODO</version>" >> pom.xml echo " </dependency>" >> pom.xml done echo ' </dependencies>' >> pom.xml echo '</project>' >> pom.xml fi3.5 现象:反编译大 jar(>200MB)时内存溢出,java.lang.OutOfMemoryError: Java heap space
原因:Procyon 默认 JVM 堆内存不足,且一次性加载整个 jar 到内存。
解决:分批处理 + JVM 参数调优:
# 修改反编译循环:每 50 个 class 启动一次 Procyon,避免内存累积 batch_size=50 count=0 batch_files=() while IFS= read -r path; do if [ -n "$path" ]; then batch_files+=("$path") ((count++)) if [ $count -eq $batch_size ]; then # 构建批量输入:com/example/Service.class com/example/Dao.class ... class_list=$(printf "%s " "${batch_files[@]/#/\"$JAR_PATH!}" | sed 's/ $//') java -Xmx4g -jar procyon-decompiler.jar -a -o "src/main/java" -f $class_list 2>/dev/null batch_files=() count=0 fi fi done < "$TMP_DIR/class_paths.txt" # 处理剩余不足 batch_size 的 class if [ ${#batch_files[@]} -gt 0 ]; then class_list=$(printf "%s " "${batch_files[@]/#/\"$JAR_PATH!}" | sed 's/ $//') java -Xmx4g -jar procyon-decompiler.jar -a -o "src/main/java" -f $class_list 2>/dev/null fi4. 如何验证反编译结果的可信度?三个硬核验证步骤与一个关键指标
4.1 步骤一:语法验证 ——javac编译通过率必须 ≥95%
这是底线指标。很多脚本声称“反编译成功”,但生成的.java文件存在return null;被错译为return;、try-catch-finally块缺失finally关键字等问题,导致javac直接报错。我们的验证脚本verify_compile.sh会:
- 递归扫描
src/main/java/**/*.java; - 对每个文件执行
javac -source 8 -target 8 -cp ".:lib/*" $file(-source/-target强制 Java 8 兼容,避免新语法报错); - 统计成功/失败文件数,生成
compile_report.txt。
#!/bin/bash # verify_compile.sh SUCCESS=0 TOTAL=0 REPORT="compile_report.txt" > "$REPORT" echo "=== Compilation Verification Report ===" > "$REPORT" echo "Time: $(date)" >> "$REPORT" echo "" >> "$REPORT" while IFS= read -r -d '' java_file; do ((TOTAL++)) # 编译单个文件,捕获错误输出 if javac -source 8 -target 8 -cp ".:lib/*" "$java_file" 2>> "$REPORT"; then ((SUCCESS++)) else echo "[FAIL] $java_file" >> "$REPORT" fi done < <(find "src/main/java" -name "*.java" -print0) echo "" >> "$REPORT" echo "Total files: $TOTAL" >> "$REPORT" echo "Success: $SUCCESS" >> "$REPORT" echo "Failure: $((TOTAL - SUCCESS))" >> "$REPORT" echo "Pass rate: $(awk "BEGIN {printf \"%.1f\", $SUCCESS*100/$TOTAL}")%" >> "$REPORT" if awk "BEGIN {exit ($SUCCESS*100/$TOTAL < 95)}"; then echo "❌ PASS RATE BELOW 95% - RECOMMEND MANUAL REVIEW" >> "$REPORT" exit 1 else echo "✅ PASS RATE OK - PROCEED TO NEXT VERIFICATION" >> "$REPORT" fi注意:
-cp ".:lib/*"中的lib/需提前放入反编译 jar 依赖的第三方库(如guava.jar,slf4j-api.jar),否则javac会因找不到com.google.common.base.Preconditions等类而误判为反编译失败。这是新手最容易忽略的环节——反编译不是孤立行为,而是工程链路的一环。
4.2 步骤二:字节码一致性验证 —— 用javap对比关键方法的指令流
语法通过不代表逻辑正确。例如,Procyon 可能将for (int i = 0; i < list.size(); i++)错译为for (Iterator it = list.iterator(); it.hasNext(); ),虽能编译,但性能与原逻辑不同。此时需回归字节码层验证。选取核心业务类(如OrderService.processOrder()),执行三步对比:
从原 jar 中提取该方法字节码:
# 提取 OrderService.class 到 temp/ mkdir -p temp && unzip -p original.jar com/example/service/OrderService.class > temp/OrderService.class # 生成字节码文本 javap -c -s -l temp/OrderService.class | grep -A 20 "processOrder" > original_bytecode.txt从反编译后源码编译出 class:
cd src/main/java && javac -d ../target/classes com/example/service/OrderService.java javap -c -s -l ../target/classes/com/example/service/OrderService.class | grep -A 20 "processOrder" > decompiled_bytecode.txt用
diff对比关键指令段(重点关注iload,invokevirtual,if_icmpge等控制流指令):diff -u original_bytecode.txt decompiled_bytecode.txt | grep -E "^[+-](iload|invoke|if_|goto|return)"
若输出为空,说明指令流一致;若出现+invokestatic java/lang/System.currentTimeMillis:()J(反编译多加了日志),则需人工检查源码。
4.3 步骤三:运行时行为验证 —— 用 JUnit 重构关键单元测试
最高阶验证是让反编译代码跑起来。我们不追求 100% 覆盖,而是聚焦高风险模块:支付回调、库存扣减、加密解密。以PaymentCallbackHandler.handle()为例:
- 从原 jar 的
test/目录(如有)或线上日志中提取真实请求 payload(JSON 字符串); - 在反编译工程中新建
src/test/java/com/example/handler/PaymentCallbackHandlerTest.java; - 编写测试用例,调用
handle()并断言返回值:
@Test public void testHandle_SuccessfulCallback() throws Exception { // 准备真实回调数据(从生产日志截取) String rawPayload = "{\"order_id\":\"ORD-2023-789\",\"status\":\"success\",\"sign\":\"abc123\"}"; // 构造 Request 对象(需手动补全 HttpServletRequest mock) MockHttpServletRequest request = new MockHttpServletRequest(); request.setContent(rawPayload.getBytes(StandardCharsets.UTF_8)); request.setContentType("application/json"); // 执行反编译后的 handle 方法 PaymentCallbackHandler handler = new PaymentCallbackHandler(); String result = handler.handle(request); // 断言:原逻辑返回 "SUCCESS",且记录日志 assertEquals("SUCCESS", result); assertTrue(handler.getProcessedOrders().contains("ORD-2023-789")); // 假设 handler 有此状态 }关键技巧:
MockHttpServletRequest来自spring-test,需在pom.xml中添加<scope>test</scope>依赖。若测试失败,优先检查@Value("${payment.secret}")等配置注入是否被正确还原(Procyon 会保留@Value注解,但不会生成application.properties,需手动补全)。
4.4 一个关键指标:反编译后git diff --stat的变更行数比
这是我从某次线上事故中总结出的玄学指标。当时一个utils.jar反编译后git status显示 327 个文件变更,但git diff --stat显示总增删行数仅 1200 行,平均每个文件 3–4 行——这极可能意味着:大部分是import补全、@Override注解添加等无害变更。而另一次反编译core.jar后,git diff --stat显示 89,000 行变更,其中UserEntity.java单文件增删 12,000 行,立刻触发人工审查,发现 Procyon 将 Lombok 的@Data错译为 200 行 getter/setter,且toString()方法逻辑与原版不符。从此我养成了习惯:每次反编译后必跑git diff --stat | tail -n +2 | head -n -1 | awk '{sum += $3} END {print "Total lines:", sum}',若单文件变更 > 5000 行或总行数 > 50,000,立即暂停,用javap对比字节码。
从那以后我每次反编译一个新 jar,都强制走一遍这三步验证 + 一个指标快筛。不是为了证明脚本多完美,而是因为在线上环境里,少一行正确的if (status == SUCCESS),可能就是十万订单的资损起点。希望帮到你。
本文还有配套的精品资源,点击获取