diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java
index a4ea74168d02d..faff6f520f770 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/PackageConfig.java
@@ -3,6 +3,7 @@
import java.util.List;
import java.util.Optional;
+import io.quarkus.runtime.annotations.ConfigGroup;
import io.quarkus.runtime.annotations.ConfigItem;
import io.quarkus.runtime.annotations.ConfigRoot;
@@ -23,7 +24,7 @@ public class PackageConfig {
/**
* The requested output type.
- *
+ *
* The default built in types are 'jar' (which will use 'fast-jar'), 'legacy-jar' for the pre-1.12 default jar
* packaging, 'uber-jar' and 'native'.
*/
@@ -39,7 +40,7 @@ public class PackageConfig {
/**
* The entry point of the application. This can either be a a fully qualified name of a standard Java
* class with a main method, or {@link io.quarkus.runtime.QuarkusApplication}.
- *
+ *
* If your application has main classes annotated with {@link io.quarkus.runtime.annotations.QuarkusMain}
* then this can also reference the name given in the annotation, to avoid the need to specify fully qualified
* names in the config.
@@ -95,11 +96,11 @@ public class PackageConfig {
/**
* This is an advanced option that only takes effect for the mutable-jar format.
- *
+ *
* If this is specified a directory of this name will be created in the jar distribution. Users can place
* jar files in this directory, and when re-augmentation is performed these will be processed and added to the
* class-path.
- *
+ *
* Note that before reaugmentation has been performed these jars will be ignored, and if they are updated the app
* should be reaugmented again.
*/
@@ -115,6 +116,12 @@ public class PackageConfig {
@ConfigItem(defaultValue = "true")
public boolean includeDependencyList;
+ /**
+ * Fernflower Decompiler configuration
+ */
+ @ConfigItem
+ public FernflowerConfig fernflower;
+
public boolean isAnyJarType() {
return (type.equalsIgnoreCase(PackageConfig.JAR) ||
type.equalsIgnoreCase(PackageConfig.FAST_JAR) ||
@@ -134,4 +141,27 @@ public boolean isLegacyJar() {
return (type.equalsIgnoreCase(PackageConfig.LEGACY_JAR) ||
type.equalsIgnoreCase(PackageConfig.LEGACY));
}
+
+ @ConfigGroup
+ public static class FernflowerConfig {
+
+ /**
+ * An advanced option that will decompile generated and transformed bytecode into the 'decompiled' directory.
+ * This is only taken into account when fast-jar is used.
+ */
+ @ConfigItem(defaultValue = "false")
+ public boolean enabled;
+
+ /**
+ * The git hash to use to download the fernflower tool from https://jitpack.io/com/github/fesh0r/fernflower/
+ */
+ @ConfigItem(defaultValue = "dbf407a655")
+ public String hash;
+
+ /**
+ * The directory into which to save the fernflower tool if it doesn't exist
+ */
+ @ConfigItem(defaultValue = "${user.home}/.quarkus")
+ public String jarDirectory;
+ }
}
diff --git a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java
index 747fac6bfd577..f9475b4f36def 100644
--- a/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java
+++ b/core/deployment/src/main/java/io/quarkus/deployment/pkg/steps/JarResultBuildStep.java
@@ -5,7 +5,9 @@
import static java.nio.file.StandardOpenOption.TRUNCATE_EXISTING;
import static java.nio.file.StandardOpenOption.WRITE;
+import java.io.BufferedInputStream;
import java.io.BufferedWriter;
+import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
@@ -19,6 +21,7 @@
import java.nio.file.FileVisitResult;
import java.nio.file.Files;
import java.nio.file.Path;
+import java.nio.file.Paths;
import java.nio.file.SimpleFileVisitor;
import java.nio.file.StandardCopyOption;
import java.nio.file.StandardOpenOption;
@@ -80,6 +83,7 @@
import io.quarkus.deployment.pkg.builditem.NativeImageSourceJarBuildItem;
import io.quarkus.deployment.pkg.builditem.OutputTargetBuildItem;
import io.quarkus.deployment.pkg.builditem.UberJarRequiredBuildItem;
+import io.quarkus.deployment.util.FileUtil;
/**
* This build step builds both the thin jars and uber jars.
@@ -481,6 +485,26 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem,
}
Map> copiedArtifacts = new HashMap<>();
+ Path fernflowerJar = null;
+ Path decompiledOutputDir = null;
+ boolean wasDecompiledSuccessfully = true;
+ if (packageConfig.fernflower.enabled) {
+ Path jarDirectory = Paths.get(packageConfig.fernflower.jarDirectory);
+ if (!Files.exists(jarDirectory)) {
+ Files.createDirectory(jarDirectory);
+ }
+ fernflowerJar = jarDirectory.resolve(String.format("fernflower-%s.jar", packageConfig.fernflower.hash));
+ if (!Files.exists(fernflowerJar)) {
+ boolean downloadComplete = downloadFernflowerJar(packageConfig, fernflowerJar);
+ if (!downloadComplete) {
+ fernflowerJar = null; // will ensure that no decompilation takes place
+ }
+ }
+ decompiledOutputDir = buildDir.getParent().resolve("decompiled");
+ FileUtil.deleteDirectory(decompiledOutputDir);
+ Files.createDirectory(decompiledOutputDir);
+ }
+
List jars = new ArrayList<>();
List bootJars = new ArrayList<>();
//we process in order of priority
@@ -500,6 +524,9 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem,
}
}
}
+ if (fernflowerJar != null) {
+ wasDecompiledSuccessfully &= decompile(fernflowerJar, decompiledOutputDir, transformedZip);
+ }
}
//now generated classes and resources
Path generatedZip = quarkus.resolve(GENERATED_BYTECODE_JAR);
@@ -522,6 +549,14 @@ private JarBuildItem buildThinJar(CurateOutcomeBuildItem curateOutcomeBuildItem,
Files.write(target, i.getClassData());
}
}
+ if (fernflowerJar != null) {
+ wasDecompiledSuccessfully &= decompile(fernflowerJar, decompiledOutputDir, generatedZip);
+ }
+
+ if (wasDecompiledSuccessfully && (decompiledOutputDir != null)) {
+ log.info("The decompiled output can be found at: " + decompiledOutputDir.toAbsolutePath().toString());
+ }
+
//now the application classes
Path runnerJar = appDir
.resolve(outputTargetBuildItem.getBaseName() + ".jar");
@@ -667,6 +702,58 @@ public void accept(Path path) {
return new JarBuildItem(initJar, null, libDir, packageConfig.type, null);
}
+ private boolean downloadFernflowerJar(PackageConfig packageConfig, Path fernflowerJar) {
+ String downloadURL = String.format("https://jitpack.io/com/github/fesh0r/fernflower/%s/fernflower-%s.jar",
+ packageConfig.fernflower.hash, packageConfig.fernflower.hash);
+ try (BufferedInputStream in = new BufferedInputStream(new URL(downloadURL).openStream());
+ FileOutputStream fileOutputStream = new FileOutputStream(fernflowerJar.toFile())) {
+ byte[] dataBuffer = new byte[1024];
+ int bytesRead;
+ while ((bytesRead = in.read(dataBuffer, 0, 1024)) != -1) {
+ fileOutputStream.write(dataBuffer, 0, bytesRead);
+ }
+ return true;
+ } catch (IOException e) {
+ log.error("Unable to download Fernflower from " + downloadURL, e);
+ return false;
+ }
+ }
+
+ private boolean decompile(Path fernflowerJar, Path decompiledOutputDir, Path jarToDecompile) {
+ int exitCode;
+ try {
+ ProcessBuilder processBuilder = new ProcessBuilder(
+ Arrays.asList("java", "-jar", fernflowerJar.toAbsolutePath().toString(),
+ jarToDecompile.toAbsolutePath().toString(), decompiledOutputDir.toAbsolutePath().toString()));
+ if (log.isDebugEnabled()) {
+ processBuilder.inheritIO();
+ } else {
+ processBuilder.redirectError(NULL_FILE);
+ processBuilder.redirectOutput(NULL_FILE);
+ }
+ exitCode = processBuilder.start().waitFor();
+ } catch (Exception e) {
+ log.error("Failed to launch Fernflower decompiler.", e);
+ return false;
+ }
+
+ if (exitCode != 0) {
+ log.errorf("Fernflower decompiler exited with error code: %d.", exitCode);
+ return false;
+ }
+
+ String jarFileName = jarToDecompile.getFileName().toString();
+ Path decompiledJar = decompiledOutputDir.resolve(jarFileName);
+ try {
+ ZipUtils.unzip(decompiledJar, decompiledOutputDir.resolve(jarFileName.replace(".jar", "")));
+ Files.deleteIfExists(decompiledJar);
+ } catch (IOException ignored) {
+ // it doesn't really matter if we can't unzip the jar as we do it merely for user convenience
+ }
+
+ return true;
+ }
+
private void copyDependency(CurateOutcomeBuildItem curateOutcomeBuildItem, Map> runtimeArtifacts,
Path libDir, Path baseLib, List jars, boolean allowParentFirst, StringBuilder classPath, AppDependency appDep)
throws IOException {
@@ -1226,4 +1313,9 @@ public boolean test(Path path, BasicFileAttributes basicFileAttributes) {
return basicFileAttributes.isRegularFile() && path.toString().endsWith(".json");
}
}
+
+ // copied from Java 9
+ // TODO remove when we move to Java 11
+
+ private static final File NULL_FILE = new File(SystemUtils.IS_OS_WINDOWS ? "NUL" : "/dev/null");
}
diff --git a/docs/src/main/asciidoc/writing-extensions.adoc b/docs/src/main/asciidoc/writing-extensions.adoc
index e4eab8d04ffe5..98cbf49b487a7 100644
--- a/docs/src/main/asciidoc/writing-extensions.adoc
+++ b/docs/src/main/asciidoc/writing-extensions.adoc
@@ -2034,6 +2034,13 @@ The only particular aspect of writing Quarkus extensions in Eclipse is that APT
=== Troubleshooting / Debugging Tips
+==== Automatically decompile Quarkus bytecode
+
+As has been mentioned many times in this document, Quarkus always generates a host of classes as part of the build and in many cases also transforms existing classes.
+When writing extensions it is often extremely useful to see a decompiled version (i.e. Java source code) of those classes, which is why Quarkus provides the `quarkus.package.fernflower.enabled` property (which is disabled by default).
+When the flag is enabled, Quarkus will download and invoke the https://github.com/JetBrains/intellij-community/tree/master/plugins/java-decompiler/engine[Fernflower decompiler] and dump the result in the `decompiled` directory
+of the build tool output (`target/decompiled` for Maven for example).
+
==== Dump the Generated Classes to the File System
During the augmentation phase Quarkus extensions generate new and modify existing classes for various purposes.