From 9a1d9f677b12325ee9d3d94ab62301fff78ba061 Mon Sep 17 00:00:00 2001 From: Andy Wilkinson Date: Wed, 24 Sep 2025 14:54:06 +0100 Subject: [PATCH] Add support for finding package-private and parameterless main Fixes gh-47309 --- .../boot/loader/tools/MainClassFinder.java | 29 +++++- .../loader/tools/MainClassFinderTests.java | 92 +++++++++++++++++++ 2 files changed, 118 insertions(+), 3 deletions(-) diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/MainClassFinder.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/MainClassFinder.java index 797daf3413a..ba641fdb27c 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/MainClassFinder.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/MainClassFinder.java @@ -60,6 +60,8 @@ public abstract class MainClassFinder { private static final Type MAIN_METHOD_TYPE = Type.getMethodType(Type.VOID_TYPE, STRING_ARRAY_TYPE); + private static final Type PARAMETERLESS_MAIN_METHOD_TYPE = Type.getMethodType(Type.VOID_TYPE); + private static final String MAIN_METHOD_NAME = "main"; private static final FileFilter CLASS_FILE_FILTER = MainClassFinder::isClassFile; @@ -286,10 +288,20 @@ public abstract class MainClassFinder { private boolean mainMethodFound; + private boolean java25OrLater = false; + ClassDescriptor() { super(SpringAsmInfo.ASM_VERSION); } + @Override + public void visit(int version, int access, String name, String signature, String superName, + String[] interfaces) { + if (version >= 69) { + this.java25OrLater = true; + } + } + @Override public AnnotationVisitor visitAnnotation(String desc, boolean visible) { this.annotationNames.add(Type.getType(desc).getClassName()); @@ -298,13 +310,24 @@ public abstract class MainClassFinder { @Override public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) { - if (isAccess(access, Opcodes.ACC_PUBLIC, Opcodes.ACC_STATIC) && MAIN_METHOD_NAME.equals(name) - && MAIN_METHOD_TYPE.getDescriptor().equals(desc)) { - this.mainMethodFound = true; + if (hasRequiredAccess(access) && MAIN_METHOD_NAME.equals(name)) { + if (MAIN_METHOD_TYPE.getDescriptor().equals(desc) + || (this.java25OrLater && PARAMETERLESS_MAIN_METHOD_TYPE.getDescriptor().equals(desc))) { + this.mainMethodFound = true; + } } return null; } + private boolean hasRequiredAccess(int access) { + if (this.java25OrLater) { + return !isAccess(access, Opcodes.ACC_PRIVATE) && isAccess(access, Opcodes.ACC_STATIC); + } + else { + return isAccess(access, Opcodes.ACC_PUBLIC, Opcodes.ACC_STATIC); + } + } + private boolean isAccess(int access, int... requiredOpsCodes) { for (int requiredOpsCode : requiredOpsCodes) { if ((access & requiredOpsCode) == 0) { diff --git a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/MainClassFinderTests.java b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/MainClassFinderTests.java index 977c3067846..9c8b8ed750b 100644 --- a/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/MainClassFinderTests.java +++ b/spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/MainClassFinderTests.java @@ -16,11 +16,21 @@ package org.springframework.boot.loader.tools; +import java.io.ByteArrayInputStream; import java.io.File; +import java.lang.reflect.Modifier; import java.util.ArrayList; import java.util.List; import java.util.jar.JarFile; +import net.bytebuddy.ByteBuddy; +import net.bytebuddy.ClassFileVersion; +import net.bytebuddy.description.method.MethodDescription; +import net.bytebuddy.dynamic.scaffold.InstrumentedType; +import net.bytebuddy.implementation.Implementation; +import net.bytebuddy.implementation.bytecode.ByteCodeAppender; +import net.bytebuddy.jar.asm.MethodVisitor; +import net.bytebuddy.jar.asm.Opcodes; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.io.TempDir; @@ -179,6 +189,88 @@ class MainClassFinderTests { } } + @Test + void packagePrivateMainMethod() throws Exception { + this.testJarFile.addFile("a/b/c/D.class", packagePrivateMainMethod(ClassFileVersion.JAVA_V25)); + ClassNameCollector callback = new ClassNameCollector(); + try (JarFile jarFile = this.testJarFile.getJarFile()) { + MainClassFinder.doWithMainClasses(jarFile, null, callback); + assertThat(callback.getClassNames()).hasToString("[a.b.c.D]"); + } + } + + @Test + void packagePrivateMainMethodBeforeJava25() throws Exception { + this.testJarFile.addFile("a/b/c/D.class", packagePrivateMainMethod(ClassFileVersion.JAVA_V24)); + ClassNameCollector callback = new ClassNameCollector(); + try (JarFile jarFile = this.testJarFile.getJarFile()) { + MainClassFinder.doWithMainClasses(jarFile, null, callback); + assertThat(callback.getClassNames()).isEmpty(); + } + } + + @Test + void parameterlessMainMethod() throws Exception { + this.testJarFile.addFile("a/b/c/D.class", parameterlessMainMethod(ClassFileVersion.JAVA_V25)); + ClassNameCollector callback = new ClassNameCollector(); + try (JarFile jarFile = this.testJarFile.getJarFile()) { + MainClassFinder.doWithMainClasses(jarFile, null, callback); + assertThat(callback.getClassNames()).hasToString("[a.b.c.D]"); + } + } + + @Test + void parameterlessMainMethodBeforeJava25() throws Exception { + this.testJarFile.addFile("a/b/c/D.class", parameterlessMainMethod(ClassFileVersion.JAVA_V24)); + ClassNameCollector callback = new ClassNameCollector(); + try (JarFile jarFile = this.testJarFile.getJarFile()) { + MainClassFinder.doWithMainClasses(jarFile, null, callback); + assertThat(callback.getClassNames()).isEmpty(); + } + } + + private ByteArrayInputStream packagePrivateMainMethod(ClassFileVersion classFileVersion) { + byte[] bytecode = new ByteBuddy(classFileVersion).subclass(Object.class) + .defineMethod("main", void.class, Modifier.STATIC) + .withParameter(String[].class) + .intercept(new EmptyBodyImplementation()) + .make() + .getBytes(); + return new ByteArrayInputStream(bytecode); + } + + private ByteArrayInputStream parameterlessMainMethod(ClassFileVersion classFileVersion) { + byte[] bytecode = new ByteBuddy(classFileVersion).subclass(Object.class) + .defineMethod("main", void.class, Modifier.STATIC | Modifier.PUBLIC) + .intercept(new EmptyBodyImplementation()) + .make() + .getBytes(); + return new ByteArrayInputStream(bytecode); + } + + static class EmptyBodyImplementation implements Implementation { + + @Override + public InstrumentedType prepare(InstrumentedType instrumentedType) { + return instrumentedType; + } + + @Override + public ByteCodeAppender appender(Target implementationTarget) { + return new ByteCodeAppender() { + + @Override + public Size apply(MethodVisitor methodVisitor, Context implementationContext, + MethodDescription instrumentedMethod) { + methodVisitor.visitInsn(Opcodes.RETURN); + return Size.ZERO; + } + + }; + } + + } + static class ClassNameCollector implements MainClassCallback { private final List classNames = new ArrayList<>();