Browse Source

Add support for finding package-private and parameterless main

Fixes gh-47309
pull/47317/head
Andy Wilkinson 3 months ago
parent
commit
9a1d9f677b
  1. 29
      spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/main/java/org/springframework/boot/loader/tools/MainClassFinder.java
  2. 92
      spring-boot-project/spring-boot-tools/spring-boot-loader-tools/src/test/java/org/springframework/boot/loader/tools/MainClassFinderTests.java

29
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 { @@ -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 { @@ -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 { @@ -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) {

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

@ -16,11 +16,21 @@ @@ -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 { @@ -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<Object> {
private final List<String> classNames = new ArrayList<>();

Loading…
Cancel
Save