Skip to content

Instantly share code, notes, and snippets.

@soberich
Last active May 22, 2025 12:16
Show Gist options
  • Save soberich/396726583579892e9b1fafc2a8aa8b10 to your computer and use it in GitHub Desktop.
Save soberich/396726583579892e9b1fafc2a8aa8b10 to your computer and use it in GitHub Desktop.
"Modern" Java JDK Main Method Finder (Java 22+/24+, Classfile API, Gatherers, Single-File Source-Code Programs + Shebang, ADT / Records / ADT)

"Main Method Finder" Modern

A modern single-file Java JDK main method finder using the Java 22+/24+ Classfile API.
Recursively scans a JDK installation for all JAR and JMOD files, listing every class with a main method (legacy and preview signatures supported).

  • Parallel and fast: Uses the Gatherers API and streams for concurrency.
  • No dependencies: Uses only standard Java APIs, works as .jsh, .java, or compiled class.
  • Rich or simple output: Set -DshowCommand=true for copy-pasteable Java launch commands, or get just <module|jar> <main-class>.
  • Cross-platform: Output always uses the standard / module/class separator—works on Windows, Linux, Mac.
  • Preview support: Handles preview main signatures as well as standard ones.
  • GraalVM ready: Can be compiled to a native image.
  • Output formatting is elegant: Uses the Formattable interface; the output style is determined by the format string and the flag.

Example Usage

# Show all main classes with full launch commands (auto-detects current JDK)
java --source=24 --enable-preview -DshowCommand=true MainMethodFinderModern.jsh

# Or, scan a specific Java Home:
java --source=24 --enable-preview -DshowCommand=true MainMethodFinderModern.jsh /Library/Java/JavaVirtualMachines/jdk-24.jdk/Contents/Home

# Compile with GraalVM Native Image:
native-image -cp target/classes wb.java24.MainMethodFinderModern MainMethodFinderModern

# Run GraalVM Native Image:
./MainMethodFinderModern -DshowCommand=true /Library/Java/JavaVirtualMachines/jdk-24.jdk/Contents/Home

What it Does

  • Recursively finds every JAR and JMOD in a JDK (or other directory).
  • For each class, inspects the bytecode for a valid main method:
  • public static void main(String[] args)
  • (And preview single-argless main methods, if enabled)
  • Outputs a list of every runnable main class.
  • Optionally prints out ready-to-use Java commands for launching each found main.
#!/usr/bin/env -S java --source=24 --enable-preview -DshowCommand=true
import java.util.Map.Entry;
import static java.lang.Boolean.getBoolean;
import static java.lang.constant.ConstantDescs.CD_String;
import static java.lang.constant.ConstantDescs.CD_void;
import static java.lang.Integer.max;
import static java.lang.Integer.MAX_VALUE;
import static java.lang.reflect.AccessFlag.ABSTRACT;
import static java.lang.reflect.AccessFlag.STATIC;
import static java.lang.System.err;
import static java.lang.System.exit;
import static java.lang.System.getProperty;
import static java.lang.System.out;
import static java.util.Comparator.comparing;
import static java.util.FormattableFlags.ALTERNATE;
import static java.util.Locale.ROOT;
import static java.util.stream.Collectors.groupingByConcurrent;
import static java.util.stream.Collectors.toCollection;
import static java.util.stream.Gatherers.mapConcurrent;
import module java.base;
/// Run:
/// <pre>java --show-version -DshowCommand=true MainMethodFinderModern.jsh</pre>
///
/// Run with different Java Home:
/// <pre>java -DshowCommand=true MainMethodFinderModern.jsh /Library/Java/JavaVirtualMachines/jdk-24.jdk/Contents/Home</pre>
///
/// Compile with GraalVM Native Image:
/// <pre>native-image -cp target/classes wb.java24.MainMethodFinderModern MainMethodFinderModern</pre>
///
/// Run GraalVM Native Image
/// <pre>./MainMethodFinderModern -DshowCommand=true /Library/Java/JavaVirtualMachines/jdk-24.jdk/Contents/Home</pre>
sealed interface JArchive extends Entry<Path, String>, Formattable permits JArchive.Jmod, JArchive.Jar {
boolean SHOW_COMMAND = getBoolean("showCommand");
@Override default void formatTo(Formatter f, int flags, int w, int p) { f.format("%s %s", getKey(), getValue()); }
record Jmod(Path getKey/* moduleName */, String getValue, Path cmd, Path modulePath, Path jmod) implements JArchive {
Jmod(boolean isCustomJdk, Path home, Path jmod, String fqn) {
this(Path.of(jmod.getFileName().toString().replace(".jmod", "")),fqn,
isCustomJdk ? home.resolve("bin", "java") : Path.of("java"),
(getBoolean("showCommand") && isCustomJdk) ? home.resolve("jmods") : Path.of(""), jmod);
}
@Override public void formatTo(Formatter f, int flags, int w, int p) {
if ((flags & ALTERNATE) == 0) JArchive.super.formatTo(f, flags, w, p);
else f.format("%s%s -m %s/%s", cmd(), modulePath().toString().isEmpty() ? "" : " --module-path " + modulePath(), getKey(), getValue());
}
}
record Jar(Path getKey/* classPath */, String getValue, Path cmd, Path classPath) implements JArchive {
Jar(boolean isCustomJdk, Path home, Path jar, String fqn) {
this(home.relativize(jar), fqn,
isCustomJdk ? home.resolve("bin", "java") : Path.of("java"), jar);
}
@Override public void formatTo(Formatter f, int flags, int w, int p) {
if ((flags & ALTERNATE) == 0) JArchive.super.formatTo(f, flags, w, p);
else f.format("%s -cp %s %s", cmd(), classPath(), getValue());
}
}
@Override default String setValue(String ignored) { throw new UnsupportedOperationException("Immutable!"); }
static void main(String... args) throws IOException {
final var isCustomJdkSupplied = args.length > 0;
if (!isCustomJdkSupplied && getProperty("org.graalvm.nativeimage.imagecode") != null) {
err.println("""
MainMethodFinderModern: Missing path operand.
Usage: MainMethodFinderModern path/to/jdk""");
exit(1);
return;
}
final var javaHome = isCustomJdkSupplied
? Path.of(args[0])
: ProcessHandle.current().info().command().map(Path::of).map(Path::getParent).map(Path::getParent).orElseThrow();
try (var javaArchives = Files.find(javaHome, MAX_VALUE, (p, attrs) -> attrs.isRegularFile() && (p.toString().endsWith(".jar") || p.toString().endsWith(".jmod")))) {
javaArchives
.gather(mapConcurrent(max(1, Runtime.getRuntime().availableProcessors() - 1), javaArchive -> {
try (var zfs = FileSystems.newFileSystem(javaArchive);
var root = Files.find(zfs.getRootDirectories().iterator().next(), MAX_VALUE, (p, _) -> p.toString().endsWith(".class"))) {
return root
.map(classFilePath -> {
try (var is = zfs.provider().newInputStream(classFilePath)) { return is.readAllBytes(); }
catch (IOException e) { err.println("Error scanning " + javaArchive + ": " + e.getMessage()); return null; }
}).filter(Objects::nonNull)
.map(ClassFile.of()::parse)
.filter(c ->
c.methods().stream()
.anyMatch(it ->
!it.flags().has(ABSTRACT) &&
it.flags().has(STATIC) &&
CD_void.equals(it.methodTypeSymbol().returnType()) &&
(MethodTypeDesc.of(CD_void).equals(it.methodTypeSymbol()) || MethodTypeDesc.of(CD_void, CD_String.arrayType()).equals(it.methodTypeSymbol())) &&
("main".equalsIgnoreCase(it.methodName().stringValue()) || "_main".equalsIgnoreCase(it.methodName().stringValue()))
)
).map(ClassModel::thisClass)
.collect(groupingByConcurrent(_ -> javaArchive, toCollection(ConcurrentHashMap::newKeySet)));
} catch (IOException e) { throw new RuntimeException(e); }
})).map(Map::entrySet)
.flatMap(Collection::stream)
.sorted(Entry.comparingByKey(comparing(Iterable::iterator, comparing(Iterator::next)))/* .thenComparing(Entry.comparingByValue()) */)
.<Formattable>mapMulti((mains, acc) -> {
mains.getValue().stream()
.map(ClassEntry::asInternalName)
.map(it -> it.replace('/', '.'))
.map(main -> switch (mains.getKey().getFileName().toString().toLowerCase(ROOT)) {
case String s when s.endsWith(".jmod") -> new JArchive.Jmod(isCustomJdkSupplied, javaHome, mains.getKey(), main);
case String s when s.endsWith(".jar") -> new JArchive.Jar(isCustomJdkSupplied, javaHome, mains.getKey(), main);
default -> throw new IllegalStateException("Could not generate launch command for: %s %s".formatted(mains.getKey().getFileName(), main)); // You can create an "UnknownRes" record for default if you wish
}).filter(Objects::nonNull)
.forEach(acc);
}).forEach(it -> out.format("%" + (SHOW_COMMAND ? "#" : "") + "s%n", it));
}
}
}
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment