Last active
May 23, 2023 21:22
-
-
Save thomasdarimont/2d464558573c6116e9473f4a9002694b to your computer and use it in GitHub Desktop.
Main Method finder to discovery main methods in JDK jars or jmods
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
$ 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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
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