Skip to content

Commit ae7c0b6

Browse files
author
wuerror
committed
支持直读 class 并回写原目录
1 parent 7f13d72 commit ae7c0b6

3 files changed

Lines changed: 213 additions & 94 deletions

File tree

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
-**多线程并行处理**: 自动使用 CPU 核心数提升反编译效率
1010
-**-tp 参数过滤**: 仅反编译包含目标包的 JAR,第三方库不移入输出目录
1111
-**智能目录跳过**: 自动跳过 `.java-decompile``.woodpecker``target/classes` 目录
12+
-**直读 .class**: 支持单个 `.class` 或仅包含 `.class` 的目录,结果直接写回原目录
1213

1314
## 快速开始
1415

@@ -40,6 +41,16 @@ java -jar target/decompile-1.0-SNAPSHOT.jar D:\path\to\libs -tp com.example.myap
4041
java -jar target/decompile-1.0-SNAPSHOT.jar D:\path\to\libs --target-package com.example.myapp
4142
```
4243

44+
**3. 直接反编译 `.class` 文件或 class 输出目录(结果写回原目录,不创建 `.java-decompile`):**
45+
46+
```bash
47+
# 单个 .class
48+
java -jar target/decompile-1.0-SNAPSHOT.jar D:\path\to\Foo.class
49+
50+
# 仅包含 .class 的目录(如 target/classes)
51+
java -jar target/decompile-1.0-SNAPSHOT.jar D:\path\to\classes
52+
```
53+
4354
### -tp 参数说明
4455

4556
使用 `-tp` 参数时:

src/main/java/DecompileUtil.java

Lines changed: 94 additions & 72 deletions
Original file line numberDiff line numberDiff line change
@@ -5,11 +5,13 @@
55

66
import java.io.*;
77
import java.nio.file.Files;
8+
import java.nio.file.Path;
89
import java.nio.file.Paths;
910
import java.util.*;
1011
import java.util.jar.Manifest;
1112
import java.util.zip.ZipEntry;
1213
import java.util.zip.ZipFile;
14+
import java.util.stream.Stream;
1315

1416
public class DecompileUtil {
1517

@@ -32,42 +34,7 @@ private static void decompileJar(String jarPath, String outputDir, boolean isTar
3234
outputJarDir.mkdirs();
3335

3436
IResultSaver saver = new ConsoleResultSaver(outputJarDir.getAbsolutePath());
35-
36-
Map<String, Object> options = new HashMap<>();
37-
options.put(IFernflowerPreferences.REMOVE_BRIDGE, "0");
38-
options.put(IFernflowerPreferences.DUMP_CODE_LINES, "1");
39-
options.put(IFernflowerPreferences.IGNORE_INVALID_BYTECODE, "1");
40-
options.put(IFernflowerPreferences.VERIFY_ANONYMOUS_CLASSES, "0");
41-
options.put(IFernflowerPreferences.INCLUDE_ENTIRE_CLASSPATH, "0");
42-
options.put(IFernflowerPreferences.TERNARY_CONDITIONS, "1");
43-
options.put(IFernflowerPreferences.FORCE_JSR_INLINE, "1");
44-
options.put(IFernflowerPreferences.DUMP_BYTECODE_ON_ERROR, "1");
45-
options.put(IFernflowerPreferences.DUMP_EXCEPTION_ON_ERROR, "1");
46-
options.put(IFernflowerPreferences.DECOMPILER_COMMENTS, "0");
47-
options.put(IFernflowerPreferences.DECOMPILE_INNER, "1");
48-
options.put(IFernflowerPreferences.DECOMPILE_ASSERTIONS, "1");
49-
options.put(IFernflowerPreferences.HIDE_EMPTY_SUPER, "1");
50-
options.put(IFernflowerPreferences.DECOMPILE_ENUM, "1");
51-
options.put(IFernflowerPreferences.DECOMPILE_PREVIEW, "1");
52-
options.put(IFernflowerPreferences.REMOVE_GET_CLASS_NEW, "1");
53-
options.put(IFernflowerPreferences.DECOMPILE_GENERIC_SIGNATURES, "1");
54-
options.put(IFernflowerPreferences.SKIP_EXTRA_FILES, "1");
55-
56-
Fernflower engine = new Fernflower(saver, options, new IFernflowerLogger() {
57-
@Override
58-
public void writeMessage(String message, Severity severity) {
59-
if (severity == Severity.ERROR) {
60-
System.err.println("[Vineflower] " + message);
61-
}
62-
}
63-
64-
@Override
65-
public void writeMessage(String message, Severity severity, Throwable t) {
66-
if (severity == Severity.ERROR) {
67-
System.err.println("[Vineflower] " + message + " - " + t.getMessage());
68-
}
69-
}
70-
});
37+
Fernflower engine = createFernflower(saver);
7138

7239
engine.addSource(jarFile);
7340
engine.decompileContext();
@@ -160,42 +127,7 @@ private static void decompileJarToSpecificDir(String jarPath, String outputDir,
160127
outputJarDir.mkdirs();
161128

162129
IResultSaver saver = new ConsoleResultSaver(outputJarDir.getAbsolutePath());
163-
164-
Map<String, Object> options = new HashMap<>();
165-
options.put(IFernflowerPreferences.REMOVE_BRIDGE, "0");
166-
options.put(IFernflowerPreferences.DUMP_CODE_LINES, "1");
167-
options.put(IFernflowerPreferences.IGNORE_INVALID_BYTECODE, "1");
168-
options.put(IFernflowerPreferences.VERIFY_ANONYMOUS_CLASSES, "0");
169-
options.put(IFernflowerPreferences.INCLUDE_ENTIRE_CLASSPATH, "0");
170-
options.put(IFernflowerPreferences.TERNARY_CONDITIONS, "1");
171-
options.put(IFernflowerPreferences.FORCE_JSR_INLINE, "1");
172-
options.put(IFernflowerPreferences.DUMP_BYTECODE_ON_ERROR, "1");
173-
options.put(IFernflowerPreferences.DUMP_EXCEPTION_ON_ERROR, "1");
174-
options.put(IFernflowerPreferences.DECOMPILER_COMMENTS, "0");
175-
options.put(IFernflowerPreferences.DECOMPILE_INNER, "1");
176-
options.put(IFernflowerPreferences.DECOMPILE_ASSERTIONS, "1");
177-
options.put(IFernflowerPreferences.HIDE_EMPTY_SUPER, "1");
178-
options.put(IFernflowerPreferences.DECOMPILE_ENUM, "1");
179-
options.put(IFernflowerPreferences.DECOMPILE_PREVIEW, "1");
180-
options.put(IFernflowerPreferences.REMOVE_GET_CLASS_NEW, "1");
181-
options.put(IFernflowerPreferences.DECOMPILE_GENERIC_SIGNATURES, "1");
182-
options.put(IFernflowerPreferences.SKIP_EXTRA_FILES, "1");
183-
184-
Fernflower engine = new Fernflower(saver, options, new IFernflowerLogger() {
185-
@Override
186-
public void writeMessage(String message, Severity severity) {
187-
if (severity == Severity.ERROR) {
188-
System.err.println("[Vineflower] " + message);
189-
}
190-
}
191-
192-
@Override
193-
public void writeMessage(String message, Severity severity, Throwable t) {
194-
if (severity == Severity.ERROR) {
195-
System.err.println("[Vineflower] " + message + " - " + t.getMessage());
196-
}
197-
}
198-
});
130+
Fernflower engine = createFernflower(saver);
199131

200132
engine.addSource(jarFile);
201133
engine.decompileContext();
@@ -221,6 +153,58 @@ private static boolean containsTargetPackage(String jarPath, String targetPackag
221153
return false;
222154
}
223155
}
156+
157+
public static void decompileClassFileInPlace(String classPath) throws IOException {
158+
File classFile = new File(classPath);
159+
if (!classFile.exists() || !classFile.isFile()) {
160+
throw new FileNotFoundException("Class file not found: " + classPath);
161+
}
162+
163+
File outputDir = classFile.getParentFile();
164+
if (outputDir == null) {
165+
outputDir = new File(".");
166+
}
167+
168+
decompileSourcesInPlace(Collections.singletonList(classFile), outputDir);
169+
}
170+
171+
public static void decompileClassDirectoryInPlace(String directoryPath) throws IOException {
172+
File dir = new File(directoryPath);
173+
if (!dir.exists() || !dir.isDirectory()) {
174+
throw new IOException("Directory not found: " + directoryPath);
175+
}
176+
177+
List<File> classFiles = new ArrayList<>();
178+
try (Stream<Path> stream = Files.walk(dir.toPath())) {
179+
stream.filter(Files::isRegularFile)
180+
.filter(path -> path.toString().endsWith(".class"))
181+
.forEach(path -> classFiles.add(path.toFile()));
182+
}
183+
184+
if (classFiles.isEmpty()) {
185+
throw new IOException("No .class files found under directory: " + directoryPath);
186+
}
187+
188+
decompileSourcesInPlace(classFiles, dir);
189+
}
190+
191+
private static void decompileSourcesInPlace(List<File> sources, File outputDir) throws IOException {
192+
if (sources.isEmpty()) {
193+
return;
194+
}
195+
196+
try {
197+
IResultSaver saver = new ConsoleResultSaver(outputDir.getAbsolutePath());
198+
Fernflower engine = createFernflower(saver);
199+
for (File source : sources) {
200+
engine.addSource(source);
201+
}
202+
engine.decompileContext();
203+
engine.clearContext();
204+
} catch (Exception | OutOfMemoryError e) {
205+
throw new IOException("Decompilation failed: " + e.getMessage(), e);
206+
}
207+
}
224208

225209
private static void copyNonClassFiles(String jarPath, String outputDir) throws IOException {
226210
try (ZipFile zipFile = new ZipFile(jarPath)) {
@@ -255,6 +239,44 @@ private static void copyNonClassFiles(String jarPath, String outputDir) throws I
255239
}
256240
}
257241
}
242+
243+
private static Fernflower createFernflower(IResultSaver saver) {
244+
Map<String, Object> options = new HashMap<>();
245+
options.put(IFernflowerPreferences.REMOVE_BRIDGE, "0");
246+
options.put(IFernflowerPreferences.DUMP_CODE_LINES, "1");
247+
options.put(IFernflowerPreferences.IGNORE_INVALID_BYTECODE, "1");
248+
options.put(IFernflowerPreferences.VERIFY_ANONYMOUS_CLASSES, "0");
249+
options.put(IFernflowerPreferences.INCLUDE_ENTIRE_CLASSPATH, "0");
250+
options.put(IFernflowerPreferences.TERNARY_CONDITIONS, "1");
251+
options.put(IFernflowerPreferences.FORCE_JSR_INLINE, "1");
252+
options.put(IFernflowerPreferences.DUMP_BYTECODE_ON_ERROR, "1");
253+
options.put(IFernflowerPreferences.DUMP_EXCEPTION_ON_ERROR, "1");
254+
options.put(IFernflowerPreferences.DECOMPILER_COMMENTS, "0");
255+
options.put(IFernflowerPreferences.DECOMPILE_INNER, "1");
256+
options.put(IFernflowerPreferences.DECOMPILE_ASSERTIONS, "1");
257+
options.put(IFernflowerPreferences.HIDE_EMPTY_SUPER, "1");
258+
options.put(IFernflowerPreferences.DECOMPILE_ENUM, "1");
259+
options.put(IFernflowerPreferences.DECOMPILE_PREVIEW, "1");
260+
options.put(IFernflowerPreferences.REMOVE_GET_CLASS_NEW, "1");
261+
options.put(IFernflowerPreferences.DECOMPILE_GENERIC_SIGNATURES, "1");
262+
options.put(IFernflowerPreferences.SKIP_EXTRA_FILES, "1");
263+
264+
return new Fernflower(saver, options, new IFernflowerLogger() {
265+
@Override
266+
public void writeMessage(String message, Severity severity) {
267+
if (severity == Severity.ERROR) {
268+
System.err.println("[Vineflower] " + message);
269+
}
270+
}
271+
272+
@Override
273+
public void writeMessage(String message, Severity severity, Throwable t) {
274+
if (severity == Severity.ERROR) {
275+
System.err.println("[Vineflower] " + message + " - " + t.getMessage());
276+
}
277+
}
278+
});
279+
}
258280

259281
private static class ConsoleResultSaver implements IResultSaver {
260282
private final String outputPath;

src/main/java/Main.java

Lines changed: 108 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
import java.util.List;
1313
import java.util.concurrent.*;
1414
import java.util.concurrent.atomic.AtomicInteger;
15+
import java.util.stream.Stream;
1516

1617
public class Main {
1718
private static String targetPackage = null;
@@ -45,32 +46,36 @@ public static void main(String[] args) throws IOException {
4546
targetPackage = (String) ns.get("target_package");
4647

4748
File inputFile = new File(path);
48-
File baseDir;
49+
boolean handledClassInput = processClassInputIfNeeded(inputFile);
4950

50-
if (inputFile.isFile()) {
51-
baseDir = inputFile.getParentFile();
52-
} else {
53-
baseDir = inputFile;
54-
}
51+
if (!handledClassInput) {
52+
File baseDir;
5553

56-
File decompileDir = new File(baseDir, ".java-decompile");
57-
if (!decompileDir.exists()) {
58-
decompileDir.mkdirs();
59-
}
54+
if (inputFile.isFile()) {
55+
baseDir = inputFile.getParentFile();
56+
} else {
57+
baseDir = inputFile;
58+
}
6059

61-
System.out.println("Starting decompilation...");
62-
System.out.println("Source: " + inputFile.getAbsolutePath());
63-
System.out.println("Output directory: " + decompileDir.getAbsolutePath());
64-
if (targetPackage != null) {
65-
System.out.println("Target package filter: " + targetPackage);
66-
}
67-
System.out.println("Thread pool size: " + executor.getCorePoolSize());
68-
System.out.println();
60+
File decompileDir = new File(baseDir, ".java-decompile");
61+
if (!decompileDir.exists()) {
62+
decompileDir.mkdirs();
63+
}
6964

70-
if (inputFile.isFile()) {
71-
processSingleFile(inputFile.getAbsolutePath(), decompileDir.getAbsolutePath());
72-
} else {
73-
processDirectory(path, decompileDir.getAbsolutePath());
65+
System.out.println("Starting decompilation...");
66+
System.out.println("Source: " + inputFile.getAbsolutePath());
67+
System.out.println("Output directory: " + decompileDir.getAbsolutePath());
68+
if (targetPackage != null) {
69+
System.out.println("Target package filter: " + targetPackage);
70+
}
71+
System.out.println("Thread pool size: " + executor.getCorePoolSize());
72+
System.out.println();
73+
74+
if (inputFile.isFile()) {
75+
processSingleFile(inputFile.getAbsolutePath(), decompileDir.getAbsolutePath());
76+
} else {
77+
processDirectory(path, decompileDir.getAbsolutePath());
78+
}
7479
}
7580

7681
executor.shutdown();
@@ -152,6 +157,87 @@ private static void processDirectory(String path, String decompilePath) throws I
152157
}
153158
}
154159

160+
private static boolean processClassInputIfNeeded(File inputFile) throws IOException {
161+
if (inputFile.isFile() && inputFile.getName().endsWith(".class")) {
162+
processClassFile(inputFile);
163+
return true;
164+
}
165+
166+
if (inputFile.isDirectory()) {
167+
DirectoryScanResult scanResult = analyzeDirectory(inputFile.toPath());
168+
if (!scanResult.hasArchives && scanResult.classCount > 0) {
169+
processClassDirectory(inputFile, scanResult.classCount);
170+
return true;
171+
}
172+
}
173+
174+
return false;
175+
}
176+
177+
private static void processClassFile(File classFile) {
178+
File parent = classFile.getParentFile();
179+
String output = parent == null ? new File(".").getAbsolutePath() : parent.getAbsolutePath();
180+
181+
System.out.println("Starting class decompilation...");
182+
System.out.println("Source: " + classFile.getAbsolutePath());
183+
System.out.println("Output directory: " + output);
184+
if (targetPackage != null) {
185+
System.out.println("Target package filter: " + targetPackage);
186+
}
187+
System.out.println();
188+
189+
try {
190+
DecompileUtil.decompileClassFileInPlace(classFile.getAbsolutePath());
191+
processedCount.incrementAndGet();
192+
System.out.println("[DECOMPILED CLASS] " + classFile.getAbsolutePath());
193+
} catch (Exception e) {
194+
failedCount.incrementAndGet();
195+
System.err.println("[ERROR] Failed to decompile class: " + classFile.getAbsolutePath() + " - " + e.getMessage());
196+
}
197+
}
198+
199+
private static void processClassDirectory(File directory, int classCount) {
200+
System.out.println("Starting class directory decompilation...");
201+
System.out.println("Source: " + directory.getAbsolutePath());
202+
System.out.println("Output directory: " + directory.getAbsolutePath());
203+
if (targetPackage != null) {
204+
System.out.println("Target package filter: " + targetPackage);
205+
}
206+
System.out.println();
207+
208+
try {
209+
DecompileUtil.decompileClassDirectoryInPlace(directory.getAbsolutePath());
210+
processedCount.addAndGet(classCount);
211+
System.out.println("[DECOMPILED CLASS DIR] " + directory.getAbsolutePath() + " (" + classCount + " classes)");
212+
} catch (Exception e) {
213+
failedCount.incrementAndGet();
214+
System.err.println("[ERROR] Failed to decompile directory: " + directory.getAbsolutePath() + " - " + e.getMessage());
215+
}
216+
}
217+
218+
private static DirectoryScanResult analyzeDirectory(Path directoryPath) throws IOException {
219+
DirectoryScanResult result = new DirectoryScanResult();
220+
221+
try (Stream<Path> stream = Files.walk(directoryPath)) {
222+
stream.filter(Files::isRegularFile)
223+
.forEach(path -> {
224+
String strPath = path.toString();
225+
if (strPath.endsWith(".jar") || strPath.endsWith(".war")) {
226+
result.hasArchives = true;
227+
} else if (strPath.endsWith(".class")) {
228+
result.classCount++;
229+
}
230+
});
231+
}
232+
233+
return result;
234+
}
235+
236+
private static class DirectoryScanResult {
237+
private boolean hasArchives;
238+
private int classCount;
239+
}
240+
155241
private static class DecompileTask implements Callable<Void> {
156242
private final String jarPath;
157243
private final String decompilePath;

0 commit comments

Comments
 (0)