Skip to content

Instantly share code, notes, and snippets.

@thomasdarimont
Last active May 23, 2023 21:22
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save thomasdarimont/2d464558573c6116e9473f4a9002694b to your computer and use it in GitHub Desktop.
Save thomasdarimont/2d464558573c6116e9473f4a9002694b to your computer and use it in GitHub Desktop.
Main Method finder to discovery main methods in JDK jars or jmods
$ java --show-version -DshowCommand=true --add-exports java.base/jdk.internal.org.objectweb.asm=ALL-UNNAMED -cp target/classes demo.MainMethodFinder
openjdk 21-ea 2023-09-19
OpenJDK Runtime Environment (build 21-ea+23-1988)
OpenJDK 64-Bit Server VM (build 21-ea+23-1988, mixed mode, sharing)
java -m java.base/java.util.regex.PrintPattern
java -m java.base/jdk.internal.org.objectweb.asm.util.ASMifier
java -m java.base/jdk.internal.org.objectweb.asm.util.CheckClassAdapter
java -m java.base/jdk.internal.org.objectweb.asm.util.Textifier
java -m java.base/sun.launcher.LauncherHelper$FXHelper
java -m java.base/sun.security.provider.PolicyParser
java -m java.base/sun.security.tools.keytool.Main
java -m java.desktop/sun.java2d.loops.GraphicsPrimitiveMgr
java -m java.prefs/java.util.prefs.Base64
java -m java.rmi/sun.rmi.registry.RegistryImpl
java -m java.scripting/com.sun.tools.script.shell.Main
java -m java.xml/com.sun.org.apache.xalan.internal.xsltc.ProcessorVersion
java -m java.xml/com.sun.org.apache.xerces.internal.impl.Constants
java -m java.xml/com.sun.org.apache.xerces.internal.impl.xpath.XPath
java -m java.xml/com.sun.org.apache.xerces.internal.impl.xpath.regex.REUtil
java -m jdk.compiler/com.sun.tools.javac.Main
java -m jdk.compiler/com.sun.tools.javac.launcher.Main
java -m jdk.compiler/sun.tools.serialver.SerialVer
java -m jdk.hotspot.agent/com.sun.java.swing.ui.TabsDlg
java -m jdk.hotspot.agent/com.sun.java.swing.ui.WizardDlg
java -m jdk.hotspot.agent/sun.jvm.hotspot.CLHSDB
java -m jdk.hotspot.agent/sun.jvm.hotspot.DebugServer
java -m jdk.hotspot.agent/sun.jvm.hotspot.HSDB
java -m jdk.hotspot.agent/sun.jvm.hotspot.HelloWorld
java -m jdk.hotspot.agent/sun.jvm.hotspot.ObjectHistogram
java -m jdk.hotspot.agent/sun.jvm.hotspot.SALauncher
java -m jdk.hotspot.agent/sun.jvm.hotspot.StackTrace
java -m jdk.hotspot.agent/sun.jvm.hotspot.debugger.bsd.BsdAddress
java -m jdk.hotspot.agent/sun.jvm.hotspot.debugger.dummy.DummyAddress
java -m jdk.hotspot.agent/sun.jvm.hotspot.debugger.linux.LinuxAddress
java -m jdk.hotspot.agent/sun.jvm.hotspot.debugger.posix.elf.ELFFileParser
java -m jdk.hotspot.agent/sun.jvm.hotspot.debugger.remote.RemoteAddress
java -m jdk.hotspot.agent/sun.jvm.hotspot.debugger.win32.coff.DumpExports
java -m jdk.hotspot.agent/sun.jvm.hotspot.debugger.win32.coff.TestDebugInfo
java -m jdk.hotspot.agent/sun.jvm.hotspot.debugger.win32.coff.TestParser
java -m jdk.hotspot.agent/sun.jvm.hotspot.debugger.windbg.WindbgAddress
java -m jdk.hotspot.agent/sun.jvm.hotspot.tools.ClassLoaderStats
java -m jdk.hotspot.agent/sun.jvm.hotspot.tools.FinalizerInfo
java -m jdk.hotspot.agent/sun.jvm.hotspot.tools.FlagDumper
java -m jdk.hotspot.agent/sun.jvm.hotspot.tools.HeapDumper
java -m jdk.hotspot.agent/sun.jvm.hotspot.tools.HeapSummary
java -m jdk.hotspot.agent/sun.jvm.hotspot.tools.JInfo
java -m jdk.hotspot.agent/sun.jvm.hotspot.tools.JMap
java -m jdk.hotspot.agent/sun.jvm.hotspot.tools.JSnap
java -m jdk.hotspot.agent/sun.jvm.hotspot.tools.JStack
java -m jdk.hotspot.agent/sun.jvm.hotspot.tools.ObjectHistogram
java -m jdk.hotspot.agent/sun.jvm.hotspot.tools.PMap
java -m jdk.hotspot.agent/sun.jvm.hotspot.tools.PStack
java -m jdk.hotspot.agent/sun.jvm.hotspot.tools.StackTrace
java -m jdk.hotspot.agent/sun.jvm.hotspot.tools.SysPropsDumper
java -m jdk.hotspot.agent/sun.jvm.hotspot.tools.jcore.ClassDump
java -m jdk.hotspot.agent/sun.jvm.hotspot.ui.AnnotatedMemoryPanel
java -m jdk.hotspot.agent/sun.jvm.hotspot.ui.CommandProcessorPanel
java -m jdk.hotspot.agent/sun.jvm.hotspot.ui.DebuggerConsolePanel
java -m jdk.hotspot.agent/sun.jvm.hotspot.ui.HighPrecisionJScrollBar
java -m jdk.hotspot.agent/sun.jvm.hotspot.ui.ObjectHistogramPanel
java -m jdk.hotspot.agent/sun.jvm.hotspot.utilities.PlatformInfo
java -m jdk.hotspot.agent/sun.jvm.hotspot.utilities.RBTree
java -m jdk.httpserver/sun.net.httpserver.simpleserver.JWebServer
java -m jdk.httpserver/sun.net.httpserver.simpleserver.Main
java -m jdk.internal.le/jdk.internal.org.jline.terminal.impl.Diag
java -m jdk.jartool/sun.security.tools.jarsigner.Main
java -m jdk.jartool/sun.tools.jar.Main
java -m jdk.javadoc/jdk.javadoc.internal.doclint.DocLint
java -m jdk.javadoc/jdk.javadoc.internal.tool.Main
java -m jdk.jcmd/sun.tools.jcmd.JCmd
java -m jdk.jcmd/sun.tools.jinfo.JInfo
java -m jdk.jcmd/sun.tools.jmap.JMap
java -m jdk.jcmd/sun.tools.jps.Jps
java -m jdk.jcmd/sun.tools.jstack.JStack
java -m jdk.jcmd/sun.tools.jstat.Jstat
java -m jdk.jconsole/sun.tools.jconsole.JConsole
java -m jdk.jdeps/com.sun.tools.javap.Main
java -m jdk.jdeps/com.sun.tools.jdeprscan.Main
java -m jdk.jdeps/com.sun.tools.jdeps.Main
java -m jdk.jdeps/com.sun.tools.jdeps.Profile
java -m jdk.jdi/com.sun.tools.example.debug.expr.ExpressionParser
java -m jdk.jdi/com.sun.tools.example.debug.tty.TTY
java -m jdk.jfr/jdk.jfr.internal.tool.Main
java -m jdk.jfr/jdk.jfr.snippets.Snippets
java -m jdk.jfr/jdk.jfr.snippets.Snippets$ConfigurationOverview
java -m jdk.jfr/jdk.jfr.snippets.Snippets$Example
java -m jdk.jfr/jdk.jfr.snippets.consumer.Snippets$EventStreamMetadata
java -m jdk.jfr/jdk.jfr.snippets.consumer.Snippets$PackageOverview
java -m jdk.jlink/jdk.tools.jimage.Main
java -m jdk.jlink/jdk.tools.jlink.internal.Main
java -m jdk.jlink/jdk.tools.jmod.Main
java -m jdk.jpackage/jdk.jpackage.main.Main
java -m jdk.jshell/jdk.internal.jshell.tool.JShellToolProvider
java -m jdk.jshell/jdk.jshell.execution.RemoteExecutionControl
java -m jdk.jstatd/sun.tools.jstatd.Jstatd
java -m jdk.security.auth/com.sun.security.auth.module.Crypt
java -m jdk.zipfs/jdk.nio.zipfs.ZipInfo
package wb.java21;
import jdk.internal.org.objectweb.asm.ClassReader;
import jdk.internal.org.objectweb.asm.ClassVisitor;
import jdk.internal.org.objectweb.asm.MethodVisitor;
import jdk.internal.org.objectweb.asm.Opcodes;
import java.io.BufferedInputStream;
import java.io.File;
import java.io.IOException;
import java.nio.file.FileSystem;
import java.nio.file.FileSystems;
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.attribute.BasicFileAttributes;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
import java.util.Optional;
import java.util.Queue;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.CopyOnWriteArrayList;
import java.util.concurrent.RecursiveAction;
import java.util.concurrent.TimeUnit;
import java.util.function.Consumer;
/**
* Compile:
* <pre>javac --add-exports java.base/jdk.internal.org.objectweb.asm=ALL-UNNAMED -d target/classes src/main/java/wb/java17/MainMethodFinder.java</pre>
* <p>
* Run:
* <pre>java -DshowCommand=true --add-exports java.base/jdk.internal.org.objectweb.asm=ALL-UNNAMED -cp target/classes wb.java17.MainMethodFinder</pre>
* <p>
* Run with different Java Home:
* <pre>java -DshowCommand=true --add-exports java.base/jdk.internal.org.objectweb.asm=ALL-UNNAMED -cp target/classes wb.java17.MainMethodFinder ~/.sdkman/candidates/java/8.0.282.hs-adpt</pre>
* <p>
* Compile with GraalVM Native Image:
* <pre>native-image -cp target/classes wb.java17.MainMethodFinder MainMethodFinder</pre>
* <p>
* Run GraalVM Native Image
* <pre>./MainMethodFinder -DshowCommand=true ~/.sdkman/candidates/java/8.0.282.hs-adpt</pre>
* <pre>./MainMethodFinder -DshowCommand=true ~/.sdkman/candidates/java/11.0.10.hs-adpt</pre>
*/
public class MainMethodFinder {
private static final boolean VERBOSE = Boolean.getBoolean("verbose");
private static final boolean IS_NATIVE_IMAGE = System.getProperty("org.graalvm.nativeimage.imagecode") != null;
private static final boolean SHOW_COMMAND = Boolean.getBoolean("showCommand");
public static void main(String[] args) throws IOException {
long time = System.nanoTime();
try {
boolean customJdkPathProvided = args.length > 0;
if (!customJdkPathProvided && IS_NATIVE_IMAGE) {
System.err.println("MainMethodFinder: Missing path operand.");
System.err.println("Usage: MainMethodFinder /path/to/jdk");
System.exit(-1);
return;
}
var jdkHomePath = customJdkPathProvided ? Path.of(args[0]) : detectCurrentJdkPath();
if (VERBOSE) {
System.out.printf("Scanning Java Installation: %s%n", jdkHomePath);
}
MainMethodReportingVisitor visitor = new MainMethodReportingVisitor();
Files.walkFileTree(jdkHomePath, visitor);
Consumer<MainMethod> printMainMethodInfo = mainMethod -> {
String libraryFileName = mainMethod.library.getName();
if (libraryFileName.endsWith(".jar")) {
libraryFileName = jdkHomePath.relativize(mainMethod.library.toPath()).toString();
}
String mainClassName = mainMethod.className;
String libraryName = libraryFileName.replaceAll("\\.jmod", "");
if (SHOW_COMMAND) {
String launchCommand = generateLaunchCommand(!customJdkPathProvided, jdkHomePath, libraryFileName, mainClassName, libraryName);
System.out.printf("%s%n", launchCommand);
} else {
System.out.printf("%s %s%n", libraryName, mainClassName);
}
};
var mainMethods = visitor.waitForCompletionAndReturnMainMethods();
mainMethods.forEach(printMainMethodInfo);
} finally {
time = System.nanoTime() - time;
if (VERBOSE) {
System.out.printf("time = %dms%n", TimeUnit.NANOSECONDS.toMillis(time));
// System.out.printf("ForkJoin Pool Stats: %s%n", ForkJoinPool.commonPool());
}
}
}
private static String generateLaunchCommand(boolean useCurrentJdk, Path jdkHomePath, String libraryFileName, String mainClassName, String libraryName) {
var launchCommand = (useCurrentJdk ? "" : jdkHomePath.toFile().getAbsolutePath() + "/bin/") + "java";
if (libraryFileName.endsWith(".jmod")) {
launchCommand += (useCurrentJdk ? "" : " --module-path " + jdkHomePath.toFile().getAbsolutePath() + "/jmods");
launchCommand += " -m " + libraryName + "/" + mainClassName;
} else if (libraryFileName.endsWith(".jar")) {
launchCommand += " -cp " + jdkHomePath.toFile().getAbsolutePath() + "/" + libraryFileName;
launchCommand += " " + mainClassName;
} else {
launchCommand = String.format("Could not generate launch command for: %s %s", libraryFileName, mainClassName);
}
return launchCommand;
}
private static Path detectCurrentJdkPath() {
return Paths.get(ProcessHandle.current().info().command().orElseThrow()).resolve("../..").normalize();
}
static class MainMethodReportingVisitor extends SimpleFileVisitor<Path> {
private final CopyOnWriteArrayList<MainMethod> mainMethods = new CopyOnWriteArrayList<>();
private final Queue<RecursiveAction> outstanding = new ConcurrentLinkedQueue<>();
@Override
public FileVisitResult visitFile(Path filePath, BasicFileAttributes attrs) {
var maybeJdkLibrary = filePath.toFile();
if (isJdkLibrary(maybeJdkLibrary.getName())) {
var action = new RecursiveAction() {
protected void compute() {
scanLibraryForMainClasses(maybeJdkLibrary);
}
};
action.fork();
outstanding.add(action);
}
return FileVisitResult.CONTINUE;
}
public List<MainMethod> waitForCompletionAndReturnMainMethods() {
for (RecursiveAction action; (action = outstanding.poll()) != null; ) {
action.join();
}
List<MainMethod> result = new ArrayList<>(this.mainMethods);
this.mainMethods.clear();
var cmp = Comparator.comparing((MainMethod left) -> left.library.getName()).thenComparing((MainMethod left) -> left.className);
result.sort(cmp);
return result;
}
private boolean isJdkLibrary(String maybeJdkLibraryName) {
return maybeJdkLibraryName.endsWith(".jar") || maybeJdkLibraryName.endsWith(".jmod");
}
private void scanLibraryForMainClasses(File library) {
// Using FileSystems.newFileSystem(library.toPath(), (ClassLoader)null) for Java 11 compatibility
try (var fileSystem = FileSystems.newFileSystem(library.toPath(), (ClassLoader) null)) {
var root = fileSystem.getRootDirectories().iterator().next();
var visitor = new MainMethodVisitor(library, mainMethods::add, fileSystem);
// How to close the stream after walking all the files?
Files.walk(root).parallel().filter(this::isClassFile).forEach(visitor::scanClassForMainMethod);
} catch (Exception e) {
System.err.println("Could not scan library: " + library.getAbsolutePath());
e.printStackTrace();
}
}
private boolean isClassFile(Path nestedFilePath) {
return nestedFilePath.toString().endsWith(".class");
}
}
// we don't use record here to support JDKs < 16
static class MainMethod {
File library;
String className;
public MainMethod(File library, String className) {
this.library = library;
this.className = className;
}
}
static class MainMethodVisitor extends ClassVisitor {
// Adapted from Opcodes.ASM* to be usable across jdk versions.
private static final int ASM6 = 6 << 16;
private static final int ASM7 = 7 << 16;
private static final int ASM8 = 8 << 16;
private static final int ASM9 = 9 << 16;
private static final String JAVA_VERSION_STRING = System.getProperty("java.version");
private static final int ASM_API_VERSION =
// not using Opcodes.* constants to support running on Java11-21
JAVA_VERSION_STRING.startsWith("11.") //
? ASM6 //
: JAVA_VERSION_STRING.startsWith("15.") //
? ASM7 //
: JAVA_VERSION_STRING.startsWith("20.") //
? ASM8 //
: ASM9;
private final File library;
private final FileSystem fileSystem;
private final Consumer<MainMethod> mainMethodConsumer;
private final ThreadLocal<String> currentInternalClassName;
public MainMethodVisitor(File library, Consumer<MainMethod> mainMethodConsumer, FileSystem fileSystem) {
super(ASM_API_VERSION);
this.library = library;
this.fileSystem = fileSystem;
this.mainMethodConsumer = mainMethodConsumer;
this.currentInternalClassName = new ThreadLocal<>();
}
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
currentInternalClassName.set(name);
super.visit(version, access, name, signature, superName, interfaces);
}
@Override
public void visitEnd() {
super.visitEnd();
currentInternalClassName.remove();
}
@Override
public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) {
if (isRunnableMainMethod(access, name, descriptor)) {
mainMethodConsumer.accept(new MainMethod(library, currentInternalClassName.get().replace('/', '.')));
}
return super.visitMethod(access, name, descriptor, signature, exceptions);
}
private boolean isRunnableMainMethod(int access, String name, String descriptor) {
return "main".equals(name) && (access & Opcodes.ACC_STATIC) != 0 && "([Ljava/lang/String;)V".equals(descriptor);
}
private void scanClassForMainMethod(Path pathToLibraryClass) {
tryGetClassBytes(pathToLibraryClass, fileSystem).ifPresent(this::visitClassBytes);
}
private void visitClassBytes(byte[] classBytes) {
new ClassReader(classBytes).accept(this, 0);
}
private Optional<byte[]> tryGetClassBytes(Path nestedFilePath, FileSystem fileSystem) {
// use filesystem to access .jar and .jmod file contents
try (var is = new BufferedInputStream(fileSystem.provider().newInputStream(nestedFilePath))) {
return Optional.of(is.readAllBytes());
} catch (IOException e) {
e.printStackTrace();
}
return Optional.empty();
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment