Skip to content

Commit 9a1d9f6

Browse files
committed
Add support for finding package-private and parameterless main
Fixes gh-47309
1 parent b6a1c29 commit 9a1d9f6

File tree

2 files changed

+118
-3
lines changed

2 files changed

+118
-3
lines changed

spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/MainClassFinder.java

Lines changed: 26 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,8 @@ public abstract class MainClassFinder {
6060

6161
private static final Type MAIN_METHOD_TYPE = Type.getMethodType(Type.VOID_TYPE, STRING_ARRAY_TYPE);
6262

63+
private static final Type PARAMETERLESS_MAIN_METHOD_TYPE = Type.getMethodType(Type.VOID_TYPE);
64+
6365
private static final String MAIN_METHOD_NAME = "main";
6466

6567
private static final FileFilter CLASS_FILE_FILTER = MainClassFinder::isClassFile;
@@ -286,10 +288,20 @@ private static class ClassDescriptor extends ClassVisitor {
286288

287289
private boolean mainMethodFound;
288290

291+
private boolean java25OrLater = false;
292+
289293
ClassDescriptor() {
290294
super(SpringAsmInfo.ASM_VERSION);
291295
}
292296

297+
@Override
298+
public void visit(int version, int access, String name, String signature, String superName,
299+
String[] interfaces) {
300+
if (version >= 69) {
301+
this.java25OrLater = true;
302+
}
303+
}
304+
293305
@Override
294306
public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
295307
this.annotationNames.add(Type.getType(desc).getClassName());
@@ -298,13 +310,24 @@ public AnnotationVisitor visitAnnotation(String desc, boolean visible) {
298310

299311
@Override
300312
public MethodVisitor visitMethod(int access, String name, String desc, String signature, String[] exceptions) {
301-
if (isAccess(access, Opcodes.ACC_PUBLIC, Opcodes.ACC_STATIC) && MAIN_METHOD_NAME.equals(name)
302-
&& MAIN_METHOD_TYPE.getDescriptor().equals(desc)) {
303-
this.mainMethodFound = true;
313+
if (hasRequiredAccess(access) && MAIN_METHOD_NAME.equals(name)) {
314+
if (MAIN_METHOD_TYPE.getDescriptor().equals(desc)
315+
|| (this.java25OrLater && PARAMETERLESS_MAIN_METHOD_TYPE.getDescriptor().equals(desc))) {
316+
this.mainMethodFound = true;
317+
}
304318
}
305319
return null;
306320
}
307321

322+
private boolean hasRequiredAccess(int access) {
323+
if (this.java25OrLater) {
324+
return !isAccess(access, Opcodes.ACC_PRIVATE) && isAccess(access, Opcodes.ACC_STATIC);
325+
}
326+
else {
327+
return isAccess(access, Opcodes.ACC_PUBLIC, Opcodes.ACC_STATIC);
328+
}
329+
}
330+
308331
private boolean isAccess(int access, int... requiredOpsCodes) {
309332
for (int requiredOpsCode : requiredOpsCodes) {
310333
if ((access & requiredOpsCode) == 0) {

spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/MainClassFinderTests.java

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,21 @@
1616

1717
package org.springframework.boot.loader.tools;
1818

19+
import java.io.ByteArrayInputStream;
1920
import java.io.File;
21+
import java.lang.reflect.Modifier;
2022
import java.util.ArrayList;
2123
import java.util.List;
2224
import java.util.jar.JarFile;
2325

26+
import net.bytebuddy.ByteBuddy;
27+
import net.bytebuddy.ClassFileVersion;
28+
import net.bytebuddy.description.method.MethodDescription;
29+
import net.bytebuddy.dynamic.scaffold.InstrumentedType;
30+
import net.bytebuddy.implementation.Implementation;
31+
import net.bytebuddy.implementation.bytecode.ByteCodeAppender;
32+
import net.bytebuddy.jar.asm.MethodVisitor;
33+
import net.bytebuddy.jar.asm.Opcodes;
2434
import org.junit.jupiter.api.BeforeEach;
2535
import org.junit.jupiter.api.Test;
2636
import org.junit.jupiter.api.io.TempDir;
@@ -179,6 +189,88 @@ void doWithJarMainMethods() throws Exception {
179189
}
180190
}
181191

192+
@Test
193+
void packagePrivateMainMethod() throws Exception {
194+
this.testJarFile.addFile("a/b/c/D.class", packagePrivateMainMethod(ClassFileVersion.JAVA_V25));
195+
ClassNameCollector callback = new ClassNameCollector();
196+
try (JarFile jarFile = this.testJarFile.getJarFile()) {
197+
MainClassFinder.doWithMainClasses(jarFile, null, callback);
198+
assertThat(callback.getClassNames()).hasToString("[a.b.c.D]");
199+
}
200+
}
201+
202+
@Test
203+
void packagePrivateMainMethodBeforeJava25() throws Exception {
204+
this.testJarFile.addFile("a/b/c/D.class", packagePrivateMainMethod(ClassFileVersion.JAVA_V24));
205+
ClassNameCollector callback = new ClassNameCollector();
206+
try (JarFile jarFile = this.testJarFile.getJarFile()) {
207+
MainClassFinder.doWithMainClasses(jarFile, null, callback);
208+
assertThat(callback.getClassNames()).isEmpty();
209+
}
210+
}
211+
212+
@Test
213+
void parameterlessMainMethod() throws Exception {
214+
this.testJarFile.addFile("a/b/c/D.class", parameterlessMainMethod(ClassFileVersion.JAVA_V25));
215+
ClassNameCollector callback = new ClassNameCollector();
216+
try (JarFile jarFile = this.testJarFile.getJarFile()) {
217+
MainClassFinder.doWithMainClasses(jarFile, null, callback);
218+
assertThat(callback.getClassNames()).hasToString("[a.b.c.D]");
219+
}
220+
}
221+
222+
@Test
223+
void parameterlessMainMethodBeforeJava25() throws Exception {
224+
this.testJarFile.addFile("a/b/c/D.class", parameterlessMainMethod(ClassFileVersion.JAVA_V24));
225+
ClassNameCollector callback = new ClassNameCollector();
226+
try (JarFile jarFile = this.testJarFile.getJarFile()) {
227+
MainClassFinder.doWithMainClasses(jarFile, null, callback);
228+
assertThat(callback.getClassNames()).isEmpty();
229+
}
230+
}
231+
232+
private ByteArrayInputStream packagePrivateMainMethod(ClassFileVersion classFileVersion) {
233+
byte[] bytecode = new ByteBuddy(classFileVersion).subclass(Object.class)
234+
.defineMethod("main", void.class, Modifier.STATIC)
235+
.withParameter(String[].class)
236+
.intercept(new EmptyBodyImplementation())
237+
.make()
238+
.getBytes();
239+
return new ByteArrayInputStream(bytecode);
240+
}
241+
242+
private ByteArrayInputStream parameterlessMainMethod(ClassFileVersion classFileVersion) {
243+
byte[] bytecode = new ByteBuddy(classFileVersion).subclass(Object.class)
244+
.defineMethod("main", void.class, Modifier.STATIC | Modifier.PUBLIC)
245+
.intercept(new EmptyBodyImplementation())
246+
.make()
247+
.getBytes();
248+
return new ByteArrayInputStream(bytecode);
249+
}
250+
251+
static class EmptyBodyImplementation implements Implementation {
252+
253+
@Override
254+
public InstrumentedType prepare(InstrumentedType instrumentedType) {
255+
return instrumentedType;
256+
}
257+
258+
@Override
259+
public ByteCodeAppender appender(Target implementationTarget) {
260+
return new ByteCodeAppender() {
261+
262+
@Override
263+
public Size apply(MethodVisitor methodVisitor, Context implementationContext,
264+
MethodDescription instrumentedMethod) {
265+
methodVisitor.visitInsn(Opcodes.RETURN);
266+
return Size.ZERO;
267+
}
268+
269+
};
270+
}
271+
272+
}
273+
182274
static class ClassNameCollector implements MainClassCallback<Object> {
183275

184276
private final List<String> classNames = new ArrayList<>();

0 commit comments

Comments
 (0)