From e7cc9a6104db786e44a28c15c66ee2c639ad9571 Mon Sep 17 00:00:00 2001 From: Christoph Strobl Date: Thu, 25 Aug 2022 07:36:33 +0200 Subject: [PATCH] Generate custom bean initialization code for types exposed via ManagedTypes during AOT. We now replace ManagedTypes bean definitions with generated code that contain the discovered types to avoid class path scaning. Closes: #2680 Original pull request: #2682. --- pom.xml | 5 + ...agedTypesBeanRegistrationAotProcessor.java | 6 +- ...nagedTypesRegistrationAotContribution.java | 166 +++++++++++++++++- .../aot/RegisteredBeanAotContribution.java | 27 +++ .../aot/AotTestCodeContributionBuilder.java | 92 ++++++++++ .../BeanRegistrationContributionAssert.java | 1 - .../data/aot/DeferredTypeBuilder.java | 42 +++++ ...BeanRegistrationAotProcessorUnitTests.java | 149 ++++++++++++++++ .../data/aot/MockBeanRegistrationCode.java | 61 +++++++ 9 files changed, 543 insertions(+), 6 deletions(-) create mode 100644 src/main/java/org/springframework/data/aot/RegisteredBeanAotContribution.java create mode 100644 src/test/java/org/springframework/data/aot/AotTestCodeContributionBuilder.java create mode 100644 src/test/java/org/springframework/data/aot/DeferredTypeBuilder.java create mode 100644 src/test/java/org/springframework/data/aot/MockBeanRegistrationCode.java diff --git a/pom.xml b/pom.xml index 29ed32f2a..f637fcad1 100644 --- a/pom.xml +++ b/pom.xml @@ -121,6 +121,11 @@ reactor-test test + + org.springframework + spring-core-test + test + diff --git a/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java b/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java index 77c30babe..4bd4fe92e 100644 --- a/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java +++ b/src/main/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessor.java @@ -56,7 +56,7 @@ public class ManagedTypesBeanRegistrationAotProcessor implements BeanRegistratio } BeanFactory beanFactory = registeredBean.getBeanFactory(); - return contribute(AotContext.from(beanFactory), resolveManagedTypes(registeredBean)); + return contribute(AotContext.from(beanFactory), resolveManagedTypes(registeredBean), registeredBean); } ManagedTypes resolveManagedTypes(RegisteredBean registeredBean) { @@ -114,8 +114,8 @@ public class ManagedTypesBeanRegistrationAotProcessor implements BeanRegistratio * @return new instance of {@link ManagedTypesBeanRegistrationAotProcessor} or {@literal null} if nothing to do. */ @Nullable - protected BeanRegistrationAotContribution contribute(AotContext aotContext, ManagedTypes managedTypes) { - return new ManagedTypesRegistrationAotContribution(aotContext, managedTypes, this::contributeType); + protected BeanRegistrationAotContribution contribute(AotContext aotContext, ManagedTypes managedTypes, RegisteredBean registeredBean) { + return new ManagedTypesRegistrationAotContribution(aotContext, managedTypes, registeredBean, this::contributeType); } /** diff --git a/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotContribution.java b/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotContribution.java index 3b81186a2..da455df23 100644 --- a/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotContribution.java +++ b/src/main/java/org/springframework/data/aot/ManagedTypesRegistrationAotContribution.java @@ -15,35 +15,72 @@ */ package org.springframework.data.aot; +import java.lang.reflect.Executable; +import java.lang.reflect.Method; import java.util.List; import java.util.function.BiConsumer; +import javax.lang.model.element.Modifier; + +import org.springframework.aot.generate.AccessVisibility; +import org.springframework.aot.generate.GeneratedMethod; import org.springframework.aot.generate.GenerationContext; import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; import org.springframework.beans.factory.aot.BeanRegistrationCode; +import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments; +import org.springframework.beans.factory.support.InstanceSupplier; +import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.core.ResolvableType; import org.springframework.data.domain.ManagedTypes; +import org.springframework.data.util.Lazy; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.MethodSpec.Builder; +import org.springframework.javapoet.ParameterizedTypeName; import org.springframework.lang.Nullable; +import org.springframework.util.ClassUtils; +import org.springframework.util.ObjectUtils; +import org.springframework.util.ReflectionUtils; /** * {@link BeanRegistrationAotContribution} used to contribute a {@link ManagedTypes} registration. + *

+ * Will try to resolve bean definition arguments if possible and fall back to resolving the bean from the context if + * that is not possible. To avoid duplicate invocations of potential scan operations hidden by the {@link ManagedTypes} + * instance the {@link BeanRegistrationAotContribution} will write custom instantiation code via + * {@link BeanRegistrationAotContribution#customizeBeanRegistrationCodeFragments(GenerationContext, BeanRegistrationCodeFragments)}. + * The generated code resolves potential factory methods accepting either a {@link ManagedTypes} instance, or a + * {@link List} of either {@link Class} or {@link String} (classname) values. + * + *

+ * 
+ * public static InstanceSupplier<ManagedTypes> instance() {
+ *   return (registeredBean) -> {
+ *     var types = List.of("com.example.A", "com.example.B");
+ *     return ManagedTypes.ofStream(types.stream().map(it -> ClassUtils.forName(it, registeredBean.getBeanFactory().getBeanClassLoader())));
+ *   }
+ * }
+ * 
+ * 
* * @author John Blum + * @author Christoph Strobl * @see org.springframework.beans.factory.aot.BeanRegistrationAotContribution * @since 3.0.0 */ -public class ManagedTypesRegistrationAotContribution implements BeanRegistrationAotContribution { +public class ManagedTypesRegistrationAotContribution implements RegisteredBeanAotContribution { private final AotContext aotContext; private final ManagedTypes managedTypes; private final BiConsumer contributionAction; + private final RegisteredBean source; public ManagedTypesRegistrationAotContribution(AotContext aotContext, @Nullable ManagedTypes managedTypes, - BiConsumer contributionAction) { + RegisteredBean registeredBean, BiConsumer contributionAction) { this.aotContext = aotContext; this.managedTypes = managedTypes; this.contributionAction = contributionAction; + this.source = registeredBean; } protected AotContext getAotContext() { @@ -63,4 +100,129 @@ public class ManagedTypesRegistrationAotContribution implements BeanRegistration TypeCollector.inspect(types).forEach(type -> contributionAction.accept(type, generationContext)); } } + + @Override + public BeanRegistrationCodeFragments customizeBeanRegistrationCodeFragments(GenerationContext generationContext, + BeanRegistrationCodeFragments codeFragments) { + + if (managedTypes == null) { + return codeFragments; + } + + ManagedTypesInstanceCodeFragment fragment = new ManagedTypesInstanceCodeFragment(getManagedTypes(), source, + codeFragments); + return fragment.canGenerateCode() ? fragment : codeFragments; + } + + @Override + public RegisteredBean getSource() { + return source; + } + + static class ManagedTypesInstanceCodeFragment extends BeanRegistrationCodeFragments { + + private ManagedTypes sourceTypes; + private RegisteredBean source; + private Lazy instanceMethod = Lazy.of(this::findInstanceFactory); + + protected ManagedTypesInstanceCodeFragment(ManagedTypes managedTypes, RegisteredBean source, + BeanRegistrationCodeFragments codeFragments) { + + super(codeFragments); + + this.sourceTypes = managedTypes; + this.source = source; + } + + /** + * @return {@literal true} if the instance method code can be generated. {@literal false} otherwise. + */ + boolean canGenerateCode() { + + if (ObjectUtils.nullSafeEquals(source.getBeanClass(), ManagedTypes.class)) { + return true; + } + return instanceMethod.getNullable() != null; + } + + @Override + public CodeBlock generateInstanceSupplierCode(GenerationContext generationContext, + BeanRegistrationCode beanRegistrationCode, Executable constructorOrFactoryMethod, + boolean allowDirectSupplierShortcut) { + + GeneratedMethod generatedMethod = beanRegistrationCode.getMethods().add("Instance", + this::generateInstanceFactory); + + return CodeBlock.of("$T.$L()", beanRegistrationCode.getClassName(), generatedMethod.getName()); + } + + private CodeBlock toCodeBlock(List> values, boolean allPublic) { + + if (allPublic) { + return CodeBlock.join(values.stream().map(value -> CodeBlock.of("$T.class", value)).toList(), ", "); + } + return CodeBlock.join(values.stream().map(value -> CodeBlock.of("$S", value.getName())).toList(), ", "); + } + + private Method findInstanceFactory() { + + for (Method beanMethod : ReflectionUtils.getDeclaredMethods(source.getBeanClass())) { + + if (beanMethod.getParameterCount() == 1 && java.lang.reflect.Modifier.isPublic(beanMethod.getModifiers()) + && java.lang.reflect.Modifier.isStatic(beanMethod.getModifiers())) { + ResolvableType parameterType = ResolvableType.forMethodParameter(beanMethod, 0, source.getBeanClass()); + if (parameterType.isAssignableFrom(ResolvableType.forType(List.class)) + || parameterType.isAssignableFrom(ResolvableType.forType(ManagedTypes.class))) { + return beanMethod; + } + } + } + return null; + } + + void generateInstanceFactory(Builder method) { + + List> sourceTypes = this.sourceTypes.toList(); + boolean allSourceTypesVisible = sourceTypes.stream() + .allMatch(it -> AccessVisibility.PUBLIC.equals(AccessVisibility.forClass(it))); + + ParameterizedTypeName targetTypeName = ParameterizedTypeName.get(InstanceSupplier.class, source.getBeanClass()); + + method.addModifiers(Modifier.PRIVATE, Modifier.STATIC); + method.returns(targetTypeName); + + CodeBlock.Builder builder = CodeBlock.builder().add("return ").beginControlFlow("(registeredBean -> "); + + builder.addStatement("var types = $T.of($L)", List.class, toCodeBlock(sourceTypes, allSourceTypesVisible)); + + if (allSourceTypesVisible) { + builder.addStatement("var managedTypes = $T.fromIterable($L)", ManagedTypes.class, "types"); + } else { + builder.add(CodeBlock.builder() + .beginControlFlow("var managedTypes = $T.fromStream(types.stream().map(it ->", ManagedTypes.class) + .beginControlFlow("try") + .addStatement("return $T.forName(it, registeredBean.getBeanFactory().getBeanClassLoader())", + ClassUtils.class) + .nextControlFlow("catch ($T e)", ClassNotFoundException.class) + .addStatement("throw new $T($S, e)", IllegalArgumentException.class, "Cannot to load type").endControlFlow() + .endControlFlow("))").build()); + } + if (ObjectUtils.nullSafeEquals(source.getBeanClass(), ManagedTypes.class)) { + builder.add("return managedTypes"); + } else { + Method instanceFactoryMethod = instanceMethod.get(); + if (ResolvableType.forMethodParameter(instanceFactoryMethod, 0) + .isAssignableFrom(ResolvableType.forType(ManagedTypes.class))) { + builder.addStatement("return $T.$L($L)", instanceFactoryMethod.getDeclaringClass(), + instanceFactoryMethod.getName(), "managedTypes"); + + } else { + builder.addStatement("return $T.$L($L.toList())", instanceFactoryMethod.getDeclaringClass(), + instanceFactoryMethod.getName(), "managedTypes"); + } + } + builder.endControlFlow(")"); + method.addCode(builder.build()); + } + } } diff --git a/src/main/java/org/springframework/data/aot/RegisteredBeanAotContribution.java b/src/main/java/org/springframework/data/aot/RegisteredBeanAotContribution.java new file mode 100644 index 000000000..b35567546 --- /dev/null +++ b/src/main/java/org/springframework/data/aot/RegisteredBeanAotContribution.java @@ -0,0 +1,27 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.aot; + +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.support.RegisteredBean; + +/** + * @author Christoph Strobl + */ +public interface RegisteredBeanAotContribution extends BeanRegistrationAotContribution { + + RegisteredBean getSource(); +} diff --git a/src/test/java/org/springframework/data/aot/AotTestCodeContributionBuilder.java b/src/test/java/org/springframework/data/aot/AotTestCodeContributionBuilder.java new file mode 100644 index 000000000..b924209cf --- /dev/null +++ b/src/test/java/org/springframework/data/aot/AotTestCodeContributionBuilder.java @@ -0,0 +1,92 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.aot; + +import java.util.function.Consumer; + +import javax.lang.model.element.Modifier; + +import org.mockito.Mockito; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.aot.test.generate.compile.Compiled; +import org.springframework.aot.test.generate.compile.TestCompiler; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments; +import org.springframework.beans.factory.support.InstanceSupplier; +import org.springframework.javapoet.CodeBlock; +import org.springframework.javapoet.MethodSpec; +import org.springframework.javapoet.ParameterizedTypeName; + +/** + * @author Christoph Strobl + */ +public class AotTestCodeContributionBuilder { + + TestGenerationContext generationContext; + MockBeanRegistrationCode beanRegistrationCode; + BeanRegistrationAotContribution contribution; + + static AotTestCodeContributionBuilder withContextFor(Class type) { + return withContext(new TestGenerationContext(type)); + } + + static AotTestCodeContributionBuilder withContext(TestGenerationContext ctx) { + + AotTestCodeContributionBuilder codeGenerationBuilder = new AotTestCodeContributionBuilder(); + codeGenerationBuilder.generationContext = ctx; + codeGenerationBuilder.beanRegistrationCode = new MockBeanRegistrationCode(ctx); + return codeGenerationBuilder; + } + + BeanRegistrationCodeFragments getFragments(BeanRegistrationAotContribution contribution) { + + this.contribution = contribution; + + return contribution.customizeBeanRegistrationCodeFragments(generationContext, + Mockito.mock(BeanRegistrationCodeFragments.class)); + } + + AotTestCodeContributionBuilder writeContentFor(BeanRegistrationAotContribution contribution) { + + CodeBlock codeBlock = getFragments(contribution).generateInstanceSupplierCode(generationContext, + beanRegistrationCode, null, false); + + Class beanType = Object.class; + try { + beanType = contribution instanceof RegisteredBeanAotContribution + ? ((RegisteredBeanAotContribution) contribution).getSource().getBeanClass() + : Object.class; + } catch (Exception e) {} + + ParameterizedTypeName parameterizedReturnTypeName = ParameterizedTypeName.get(InstanceSupplier.class, beanType); + beanRegistrationCode.getTypeBuilder().set(type -> { + type.addModifiers(Modifier.PUBLIC); + type.addMethod(MethodSpec.methodBuilder("get").addModifiers(Modifier.PUBLIC).returns(parameterizedReturnTypeName) + .addStatement("return $L", codeBlock).build()); + }); + + return this; + } + + public void compile() { + compile(it -> {}); + } + + public void compile(Consumer compiled) { + generationContext.writeGeneratedContent(); + TestCompiler.forSystem().withFiles(generationContext.getGeneratedFiles()).compile(compiled); + } +} diff --git a/src/test/java/org/springframework/data/aot/BeanRegistrationContributionAssert.java b/src/test/java/org/springframework/data/aot/BeanRegistrationContributionAssert.java index d500c072d..8564c3588 100644 --- a/src/test/java/org/springframework/data/aot/BeanRegistrationContributionAssert.java +++ b/src/test/java/org/springframework/data/aot/BeanRegistrationContributionAssert.java @@ -20,7 +20,6 @@ import static org.mockito.Mockito.*; import java.util.function.Consumer; import org.assertj.core.api.AbstractAssert; - import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; diff --git a/src/test/java/org/springframework/data/aot/DeferredTypeBuilder.java b/src/test/java/org/springframework/data/aot/DeferredTypeBuilder.java new file mode 100644 index 000000000..997d86a3f --- /dev/null +++ b/src/test/java/org/springframework/data/aot/DeferredTypeBuilder.java @@ -0,0 +1,42 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.aot; + +import java.util.function.Consumer; + +import org.springframework.javapoet.TypeSpec; +import org.springframework.javapoet.TypeSpec.Builder; +import org.springframework.lang.Nullable; +import org.springframework.util.Assert; + +/** + * @author Christoph Strobl + */ +public class DeferredTypeBuilder implements Consumer { + + @Nullable + private Consumer type; + + @Override + public void accept(Builder type) { + Assert.notNull(this.type, "No type builder set"); + this.type.accept(type); + } + + public void set(Consumer type) { + this.type = type; + } +} diff --git a/src/test/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessorUnitTests.java b/src/test/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessorUnitTests.java index 56d083110..4fa54d870 100644 --- a/src/test/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessorUnitTests.java +++ b/src/test/java/org/springframework/data/aot/ManagedTypesBeanRegistrationAotProcessorUnitTests.java @@ -19,21 +19,32 @@ import static org.assertj.core.api.Assertions.*; import static org.mockito.ArgumentMatchers.*; import static org.mockito.Mockito.*; +import java.util.Arrays; import java.util.Collections; +import java.util.concurrent.atomic.AtomicInteger; import java.util.function.Consumer; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.hint.predicate.RuntimeHintsPredicates; import org.springframework.aot.test.generate.TestGenerationContext; import org.springframework.beans.factory.BeanCreationException; import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationCodeFragments; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.InstanceSupplier; import org.springframework.beans.factory.support.RegisteredBean; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.aot.ManagedTypesRegistrationAotContribution.ManagedTypesInstanceCodeFragment; import org.springframework.data.domain.ManagedTypes; +import org.springframework.javapoet.MethodSpec; +import org.springframework.javapoet.MethodSpec.Builder; +import org.springframework.test.util.ReflectionTestUtils; /** * @author Christoph Strobl @@ -47,6 +58,9 @@ class ManagedTypesBeanRegistrationAotProcessorUnitTests { final RootBeanDefinition myManagedTypesDefinition = (RootBeanDefinition) BeanDefinitionBuilder .rootBeanDefinition(MyManagedTypes.class).getBeanDefinition(); + final RootBeanDefinition invocationCountingManagedTypesDefinition = (RootBeanDefinition) BeanDefinitionBuilder + .rootBeanDefinition(InvocationRecordingManagedTypes.class).getBeanDefinition(); + DefaultListableBeanFactory beanFactory; @BeforeEach @@ -154,6 +168,79 @@ class ManagedTypesBeanRegistrationAotProcessorUnitTests { verify(beanFactory).getBean(eq("commons.managed-types"), eq(ManagedTypes.class)); } + @Test // GH-2680 + void generatesInstanceSupplierCodeFragmentToAvoidDuplicateInvocations() { + + beanFactory.registerBeanDefinition("commons.managed-types", invocationCountingManagedTypesDefinition); + RegisteredBean registeredBean = RegisteredBean.of(beanFactory, "commons.managed-types"); + + BeanRegistrationAotContribution contribution = createPostProcessor("commons") + .processAheadOfTime(RegisteredBean.of(beanFactory, "commons.managed-types")); + + AotTestCodeContributionBuilder.withContextFor(this.getClass()).writeContentFor(contribution).compile(it -> { + + InvocationRecordingManagedTypes sourceTypes = beanFactory.getBean(InvocationRecordingManagedTypes.class); + assertThat(sourceTypes.getCounter()).isOne(); + + InstanceSupplier types = ReflectionTestUtils + .invokeMethod(it.getAllCompiledClasses().iterator().next(), "instance"); + try { + assertThat(types.get(registeredBean).source).isNotSameAs(sourceTypes); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + @Test // GH-2680 + void generatesInstanceSupplierCodeFragmentForTypeWithCustomFactoryMethod() { + + beanFactory.registerBeanDefinition("commons.managed-types", + BeanDefinitionBuilder.rootBeanDefinition(StoreManagedTypesWithCustomFactoryMethod.class).getBeanDefinition()); + + RegisteredBean registeredBean = RegisteredBean.of(beanFactory, "commons.managed-types"); + + BeanRegistrationAotContribution contribution = createPostProcessor("commons").processAheadOfTime(registeredBean); + + AotTestCodeContributionBuilder.withContextFor(this.getClass()).writeContentFor(contribution).compile(it -> { + + InstanceSupplier types = ReflectionTestUtils + .invokeMethod(it.getAllCompiledClasses().iterator().next(), "instance"); + + try { + assertThat(types.get(registeredBean).toList()).containsExactlyInAnyOrder(A.class, B.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + }); + } + + @Test // GH-2680 + void canGenerateCodeReturnsTrueIfFactoryMethodPresent() { + + beanFactory.registerBeanDefinition("managed-types", managedTypesDefinition); + RegisteredBean registeredBean = RegisteredBean.of(beanFactory, "managed-types"); + + ManagedTypesInstanceCodeFragment fragment = new ManagedTypesInstanceCodeFragment( + ManagedTypes.from(A.class, B.class), registeredBean, Mockito.mock(BeanRegistrationCodeFragments.class)); + Builder methodBuilder = MethodSpec.methodBuilder("instance"); + fragment.generateInstanceFactory(methodBuilder); + + assertThat(fragment.canGenerateCode()).isTrue(); + } + + @Test // GH-2680 + void canGenerateCodeReturnsFalseIfNoFactoryMethodPresent() { + + beanFactory.registerBeanDefinition("managed-types", myManagedTypesDefinition); + RegisteredBean registeredBean = RegisteredBean.of(beanFactory, "managed-types"); + + ManagedTypesInstanceCodeFragment fragment = new ManagedTypesInstanceCodeFragment( + ManagedTypes.from(A.class, B.class), registeredBean, Mockito.mock(BeanRegistrationCodeFragments.class)); + + assertThat(fragment.canGenerateCode()).isFalse(); + } + private ManagedTypesBeanRegistrationAotProcessor createPostProcessor(String moduleIdentifier) { ManagedTypesBeanRegistrationAotProcessor postProcessor = new ManagedTypesBeanRegistrationAotProcessor(); postProcessor.setModuleIdentifier(moduleIdentifier); @@ -172,5 +259,67 @@ class ManagedTypesBeanRegistrationAotProcessorUnitTests { } } + static class StoreManagedTypesWithFactoryMethodOfClassNames implements ManagedTypes { + @Override + public void forEach(Consumer> action) { + // just do nothing ¯\_(ツ)_/¯ + } + } + + public static class StoreManagedTypesWithCustomFactoryMethod implements ManagedTypes { + + private ManagedTypes source; + + public StoreManagedTypesWithCustomFactoryMethod() { + source = it -> Arrays.asList(A.class, B.class).forEach(it); + } + + public StoreManagedTypesWithCustomFactoryMethod(ManagedTypes source) { + this.source = source; + } + + public static StoreManagedTypesWithCustomFactoryMethod of(ManagedTypes source) { + return new StoreManagedTypesWithCustomFactoryMethod(source); + } + + @Override + public void forEach(Consumer> action) { + source.forEach(action); + } + } + + public static class InvocationRecordingManagedTypes implements ManagedTypes { + + private AtomicInteger counter = new AtomicInteger(0); + private ManagedTypes source = ManagedTypes.from(A.class, B.class); + + public static InvocationRecordingManagedTypes from(ManagedTypes source) { + + InvocationRecordingManagedTypes newInstance = new InvocationRecordingManagedTypes(); + newInstance.source = source; + return newInstance; + } + + @Override + public void forEach(Consumer> action) { + + counter.getAndIncrement(); + source.forEach(action); + } + + public int getCounter() { + return counter.get(); + } + } + static class NotManagedTypes {} + + @Configuration(proxyBeanMethods = false) + public static class EntityManagerWithPackagesToScanConfiguration { + + @Bean(name = "commons.managed-types") + ManagedTypes managedTypes() { + return ManagedTypes.from(A.class, B.class); + } + } } diff --git a/src/test/java/org/springframework/data/aot/MockBeanRegistrationCode.java b/src/test/java/org/springframework/data/aot/MockBeanRegistrationCode.java new file mode 100644 index 000000000..5b909e92c --- /dev/null +++ b/src/test/java/org/springframework/data/aot/MockBeanRegistrationCode.java @@ -0,0 +1,61 @@ +/* + * Copyright 2022 the original author or authors. + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.springframework.data.aot; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.aot.generate.GeneratedClass; +import org.springframework.aot.generate.GeneratedMethods; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.aot.generate.MethodReference; +import org.springframework.beans.factory.aot.BeanRegistrationCode; +import org.springframework.javapoet.ClassName; + +/** + * @author Christoph Strobl + */ +public class MockBeanRegistrationCode implements BeanRegistrationCode { + + private final GeneratedClass generatedClass; + + private final List instancePostProcessors = new ArrayList<>(); + + private final DeferredTypeBuilder typeBuilder = new DeferredTypeBuilder(); + + public MockBeanRegistrationCode(GenerationContext generationContext) { + this.generatedClass = generationContext.getGeneratedClasses().addForFeature("TestCode", this.typeBuilder); + } + + public DeferredTypeBuilder getTypeBuilder() { + return this.typeBuilder; + } + + @Override + public ClassName getClassName() { + return this.generatedClass.getName(); + } + + @Override + public GeneratedMethods getMethods() { + return this.generatedClass.getMethods(); + } + + @Override + public void addInstancePostProcessor(MethodReference methodReference) { + this.instancePostProcessors.add(methodReference); + } +}