Slice bean registrations in separate file if necessary
The compiler has a constants pool limit of 65536 entries per source file which can be hit with a very large amount of beans to register in the bean factory. This commit makes sure to create separate source files if the number of beans to register is very large. The main generated source file delegate to those. Closes gh-35044
This commit is contained in:
parent
1ad05db877
commit
e88b70e6ad
|
@ -17,6 +17,7 @@
|
|||
package org.springframework.beans.factory.aot;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.function.BiConsumer;
|
||||
|
||||
import javax.lang.model.element.Modifier;
|
||||
|
||||
|
@ -52,6 +53,10 @@ class BeanRegistrationsAotContribution
|
|||
|
||||
private static final String BEAN_FACTORY_PARAMETER_NAME = "beanFactory";
|
||||
|
||||
private static final int MAX_REGISTRATIONS_PER_FILE = 5000;
|
||||
|
||||
private static final int MAX_REGISTRATIONS_PER_METHOD = 1000;
|
||||
|
||||
private static final ArgumentCodeGenerator argumentCodeGenerator = ArgumentCodeGenerator
|
||||
.of(DefaultListableBeanFactory.class, BEAN_FACTORY_PARAMETER_NAME);
|
||||
|
||||
|
@ -67,14 +72,10 @@ class BeanRegistrationsAotContribution
|
|||
public void applyTo(GenerationContext generationContext,
|
||||
BeanFactoryInitializationCode beanFactoryInitializationCode) {
|
||||
|
||||
GeneratedClass generatedClass = generationContext.getGeneratedClasses()
|
||||
.addForFeature("BeanFactoryRegistrations", type -> {
|
||||
type.addJavadoc("Register bean definitions for the bean factory.");
|
||||
type.addModifiers(Modifier.PUBLIC);
|
||||
});
|
||||
GeneratedClass generatedClass = createBeanFactoryRegistrationClass(generationContext);
|
||||
BeanRegistrationsCodeGenerator codeGenerator = new BeanRegistrationsCodeGenerator(generatedClass);
|
||||
GeneratedMethod generatedBeanDefinitionsMethod = new BeanDefinitionsRegistrationGenerator(
|
||||
generationContext, codeGenerator, this.registrations).generateRegisterBeanDefinitionsMethod();
|
||||
GeneratedMethod generatedBeanDefinitionsMethod = generateBeanRegistrationCode(generationContext,
|
||||
generatedClass, codeGenerator);
|
||||
beanFactoryInitializationCode.addInitializer(generatedBeanDefinitionsMethod.toMethodReference());
|
||||
GeneratedMethod generatedAliasesMethod = codeGenerator.getMethods().add("registerAliases",
|
||||
this::generateRegisterAliasesMethod);
|
||||
|
@ -82,6 +83,48 @@ class BeanRegistrationsAotContribution
|
|||
generateRegisterHints(generationContext.getRuntimeHints(), this.registrations);
|
||||
}
|
||||
|
||||
private GeneratedMethod generateBeanRegistrationCode(GenerationContext generationContext, GeneratedClass mainGeneratedClass, BeanRegistrationsCodeGenerator mainCodeGenerator) {
|
||||
if (this.registrations.size() < MAX_REGISTRATIONS_PER_FILE) {
|
||||
return generateBeanRegistrationClass(generationContext, mainCodeGenerator, 0, this.registrations.size());
|
||||
}
|
||||
else {
|
||||
return mainGeneratedClass.getMethods().add("registerBeanDefinitions", method -> {
|
||||
method.addJavadoc("Register the bean definitions.");
|
||||
method.addModifiers(Modifier.PUBLIC);
|
||||
method.addParameter(DefaultListableBeanFactory.class, BEAN_FACTORY_PARAMETER_NAME);
|
||||
CodeBlock.Builder body = CodeBlock.builder();
|
||||
Registration.doWithSlice(this.registrations, MAX_REGISTRATIONS_PER_FILE, (start, end) -> {
|
||||
GeneratedClass sliceGeneratedClass = createBeanFactoryRegistrationClass(generationContext);
|
||||
BeanRegistrationsCodeGenerator sliceCodeGenerator = new BeanRegistrationsCodeGenerator(sliceGeneratedClass);
|
||||
GeneratedMethod generatedMethod = generateBeanRegistrationClass(generationContext, sliceCodeGenerator, start, end);
|
||||
body.addStatement(generatedMethod.toMethodReference().toInvokeCodeBlock(argumentCodeGenerator));
|
||||
});
|
||||
method.addCode(body.build());
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
private GeneratedMethod generateBeanRegistrationClass(GenerationContext generationContext,
|
||||
BeanRegistrationsCodeGenerator codeGenerator, int start, int end) {
|
||||
|
||||
return codeGenerator.getMethods().add("registerBeanDefinitions", method -> {
|
||||
method.addJavadoc("Register the bean definitions.");
|
||||
method.addModifiers(Modifier.PUBLIC);
|
||||
method.addParameter(DefaultListableBeanFactory.class, BEAN_FACTORY_PARAMETER_NAME);
|
||||
List<Registration> sliceRegistrations = this.registrations.subList(start, end);
|
||||
new BeanDefinitionsRegistrationGenerator(
|
||||
generationContext, codeGenerator, sliceRegistrations, start).generateBeanRegistrationsCode(method);
|
||||
});
|
||||
}
|
||||
|
||||
private static GeneratedClass createBeanFactoryRegistrationClass(GenerationContext generationContext) {
|
||||
return generationContext.getGeneratedClasses()
|
||||
.addForFeature("BeanFactoryRegistrations", type -> {
|
||||
type.addJavadoc("Register bean definitions for the bean factory.");
|
||||
type.addModifiers(Modifier.PUBLIC);
|
||||
});
|
||||
}
|
||||
|
||||
private void generateRegisterAliasesMethod(MethodSpec.Builder method) {
|
||||
method.addJavadoc("Register the aliases.");
|
||||
method.addModifiers(Modifier.PUBLIC);
|
||||
|
@ -117,6 +160,28 @@ class BeanRegistrationsAotContribution
|
|||
return this.registeredBean.getBeanName();
|
||||
}
|
||||
|
||||
/**
|
||||
* Invoke an action for each slice of the given {@code registrations}. The
|
||||
* {@code action} is invoked for each slice with the start and end index of the
|
||||
* given list of registrations. Elements to process can be retrieved using
|
||||
* {@link List#subList(int, int)}.
|
||||
* @param registrations the registrations to process
|
||||
* @param sliceSize the size of a slice
|
||||
* @param action the action to invoke for each slice
|
||||
*/
|
||||
static void doWithSlice(List<Registration> registrations, int sliceSize,
|
||||
BiConsumer<Integer, Integer> action) {
|
||||
|
||||
int index = 0;
|
||||
int end = 0;
|
||||
while (end < registrations.size()) {
|
||||
int start = index * sliceSize;
|
||||
end = Math.min(start + sliceSize, registrations.size());
|
||||
action.accept(start, end);
|
||||
index++;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
@ -144,6 +209,10 @@ class BeanRegistrationsAotContribution
|
|||
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate code for bean registrations. Limited to {@value #MAX_REGISTRATIONS_PER_METHOD}
|
||||
* beans per method to avoid hitting a limit.
|
||||
*/
|
||||
static final class BeanDefinitionsRegistrationGenerator {
|
||||
|
||||
private final GenerationContext generationContext;
|
||||
|
@ -152,44 +221,38 @@ class BeanRegistrationsAotContribution
|
|||
|
||||
private final List<Registration> registrations;
|
||||
|
||||
private final int globalStart;
|
||||
|
||||
|
||||
BeanDefinitionsRegistrationGenerator(GenerationContext generationContext,
|
||||
BeanRegistrationsCodeGenerator codeGenerator, List<Registration> registrations) {
|
||||
BeanRegistrationsCodeGenerator codeGenerator, List<Registration> registrations, int globalStart) {
|
||||
|
||||
this.generationContext = generationContext;
|
||||
this.codeGenerator = codeGenerator;
|
||||
this.registrations = registrations;
|
||||
this.globalStart = globalStart;
|
||||
}
|
||||
|
||||
|
||||
GeneratedMethod generateRegisterBeanDefinitionsMethod() {
|
||||
return this.codeGenerator.getMethods().add("registerBeanDefinitions", method -> {
|
||||
method.addJavadoc("Register the bean definitions.");
|
||||
method.addModifiers(Modifier.PUBLIC);
|
||||
method.addParameter(DefaultListableBeanFactory.class, BEAN_FACTORY_PARAMETER_NAME);
|
||||
void generateBeanRegistrationsCode(MethodSpec.Builder method) {
|
||||
if (this.registrations.size() <= 1000) {
|
||||
generateRegisterBeanDefinitionMethods(method, this.registrations);
|
||||
}
|
||||
else {
|
||||
Builder code = CodeBlock.builder();
|
||||
code.add("// Registration is sliced to avoid exceeding size limit\n");
|
||||
int index = 0;
|
||||
int end = 0;
|
||||
while (end < this.registrations.size()) {
|
||||
int start = index * 1000;
|
||||
end = Math.min(start + 1000, this.registrations.size());
|
||||
Registration.doWithSlice(this.registrations, MAX_REGISTRATIONS_PER_METHOD,
|
||||
(start, end) -> {
|
||||
GeneratedMethod sliceMethod = generateSliceMethod(start, end);
|
||||
code.addStatement(sliceMethod.toMethodReference().toInvokeCodeBlock(
|
||||
argumentCodeGenerator, this.codeGenerator.getClassName()));
|
||||
index++;
|
||||
}
|
||||
});
|
||||
method.addCode(code.build());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
private GeneratedMethod generateSliceMethod(int start, int end) {
|
||||
String description = "Register the bean definitions from %s to %s.".formatted(start, end - 1);
|
||||
String description = "Register the bean definitions from %s to %s."
|
||||
.formatted(this.globalStart + start, this.globalStart + end - 1);
|
||||
List<Registration> slice = this.registrations.subList(start, end);
|
||||
return this.codeGenerator.getMethods().add("registerBeanDefinitions", method -> {
|
||||
method.addJavadoc(description);
|
||||
|
|
|
@ -21,6 +21,7 @@ import java.util.List;
|
|||
import java.util.function.BiConsumer;
|
||||
import java.util.function.Consumer;
|
||||
import java.util.function.Function;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.lang.model.element.Modifier;
|
||||
|
||||
|
@ -54,6 +55,8 @@ import org.springframework.javapoet.ParameterizedTypeName;
|
|||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.mockito.BDDMockito.then;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.reflection;
|
||||
|
||||
/**
|
||||
|
@ -230,6 +233,40 @@ class BeanRegistrationsAotContributionTests {
|
|||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void doWithSliceWithOnlyLessThanOneSlice() {
|
||||
List<Registration> registration = Stream.generate(() -> mock(Registration.class)).limit(10).toList();
|
||||
BiConsumer<Integer, Integer> sliceAction = mockSliceAction();
|
||||
Registration.doWithSlice(registration, 20, sliceAction);
|
||||
then(sliceAction).should().accept(0, 10);
|
||||
then(sliceAction).shouldHaveNoMoreInteractions();
|
||||
}
|
||||
|
||||
@Test
|
||||
void doWithSliceWithOnlyOneExactSlice() {
|
||||
List<Registration> registration = Stream.generate(() -> mock(Registration.class)).limit(20).toList();
|
||||
BiConsumer<Integer, Integer> sliceAction = mockSliceAction();
|
||||
Registration.doWithSlice(registration, 20, sliceAction);
|
||||
then(sliceAction).should().accept(0, 20);
|
||||
then(sliceAction).shouldHaveNoMoreInteractions();
|
||||
}
|
||||
|
||||
@Test
|
||||
void doWithSeveralSlices() {
|
||||
List<Registration> registration = Stream.generate(() -> mock(Registration.class)).limit(20).toList();
|
||||
BiConsumer<Integer, Integer> sliceAction = mockSliceAction();
|
||||
Registration.doWithSlice(registration, 7, sliceAction);
|
||||
then(sliceAction).should().accept(0, 7);
|
||||
then(sliceAction).should().accept(7, 14);
|
||||
then(sliceAction).should().accept(14, 20);
|
||||
then(sliceAction).shouldHaveNoMoreInteractions();
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
BiConsumer<Integer, Integer> mockSliceAction() {
|
||||
return mock(BiConsumer.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyToWithLargeBeanDefinitionsCreatesSlices() {
|
||||
BeanRegistrationsAotContribution contribution = createContribution(1001, i -> "testBean" + i);
|
||||
|
@ -250,6 +287,38 @@ class BeanRegistrationsAotContributionTests {
|
|||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
void applyToWithVeryLargeBeanDefinitionsCreatesSeparateSourceFiles() {
|
||||
BeanRegistrationsAotContribution contribution = createContribution(10001, i -> "testBean" + i);
|
||||
contribution.applyTo(this.generationContext, this.beanFactoryInitializationCode);
|
||||
compile((consumer, compiled) -> {
|
||||
assertThat(compiled.getSourceFile(".*BeanFactoryRegistrations1"))
|
||||
.contains("Register the bean definitions from 0 to 999.",
|
||||
"Register the bean definitions from 1000 to 1999.",
|
||||
"Register the bean definitions from 2000 to 2999.",
|
||||
"Register the bean definitions from 3000 to 3999.",
|
||||
"Register the bean definitions from 4000 to 4999.",
|
||||
"// Registration is sliced to avoid exceeding size limit");
|
||||
assertThat(compiled.getSourceFile(".*BeanFactoryRegistrations2"))
|
||||
.contains("Register the bean definitions from 5000 to 5999.",
|
||||
"Register the bean definitions from 6000 to 6999.",
|
||||
"Register the bean definitions from 7000 to 7999.",
|
||||
"Register the bean definitions from 8000 to 8999.",
|
||||
"Register the bean definitions from 9000 to 9999.",
|
||||
"// Registration is sliced to avoid exceeding size limit");
|
||||
assertThat(compiled.getSourceFile(".*BeanFactoryRegistrations3"))
|
||||
.doesNotContain("// Registration is sliced to avoid exceeding size limit");
|
||||
DefaultListableBeanFactory freshBeanFactory = new DefaultListableBeanFactory();
|
||||
consumer.accept(freshBeanFactory);
|
||||
for (int i = 0; i < 10001; i++) {
|
||||
String beanName = "testBean" + i;
|
||||
assertThat(freshBeanFactory.containsBeanDefinition(beanName)).isTrue();
|
||||
assertThat(freshBeanFactory.getBean(beanName)).isInstanceOf(TestBean.class);
|
||||
}
|
||||
assertThat(freshBeanFactory.getBeansOfType(TestBean.class)).hasSize(10001);
|
||||
});
|
||||
}
|
||||
|
||||
private BeanRegistrationsAotContribution createContribution(int size, Function<Integer, String> beanNameFactory) {
|
||||
List<Registration> registrations = new ArrayList<>();
|
||||
for (int i = 0; i < size; i++) {
|
||||
|
|
Loading…
Reference in New Issue