From ce8e06cf10014db3b023e5ebba70054bab73fe00 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 6 Oct 2024 16:02:41 +0200 Subject: [PATCH 1/5] Register Bean Override infrastructure beans as manual singletons Prior to this commit, AOT processing failed for tests that made use of the Bean Override feature, since the Set constructor argument configured in the bean definition for the BeanOverrideBeanFactoryPostProcessor cannot be properly processed by our AOT support. The reason is that each OverrideMetadata instance is effectively an arbitrary object graph that cannot be automatically converted to a functional bean definition for use at AOT runtime. To address that, this commit registers Bean Override infrastructure beans as manual singletons instead of via bean definitions with the infrastructure role. See gh-32933 --- .../BeanOverrideContextCustomizer.java | 52 ++++++------------- .../bean/override/BeanOverrideRegistrar.java | 25 +++------ 2 files changed, 23 insertions(+), 54 deletions(-) diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizer.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizer.java index 07df3d4f08..3753f348ea 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizer.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideContextCustomizer.java @@ -17,13 +17,8 @@ package org.springframework.test.context.bean.override; import java.util.Set; -import java.util.function.Consumer; -import org.springframework.beans.factory.config.BeanDefinition; -import org.springframework.beans.factory.config.ConstructorArgumentValues; -import org.springframework.beans.factory.config.RuntimeBeanReference; -import org.springframework.beans.factory.support.BeanDefinitionRegistry; -import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.test.context.ContextCustomizer; import org.springframework.test.context.MergedContextConfiguration; @@ -34,6 +29,7 @@ import org.springframework.test.context.MergedContextConfiguration; * * @author Simon Baslé * @author Stephane Nicoll + * @author Sam Brannen * @since 6.2 */ class BeanOverrideContextCustomizer implements ContextCustomizer { @@ -56,43 +52,25 @@ class BeanOverrideContextCustomizer implements ContextCustomizer { @Override public void customizeContext(ConfigurableApplicationContext context, MergedContextConfiguration mergedConfig) { - if (!(context instanceof BeanDefinitionRegistry registry)) { - throw new IllegalStateException("Cannot process bean overrides with an ApplicationContext " + - "that doesn't implement BeanDefinitionRegistry: " + context.getClass().getName()); - } - registerInfrastructure(registry); + ConfigurableBeanFactory beanFactory = context.getBeanFactory(); + // Since all three Bean Override infrastructure beans are never injected as + // dependencies into other beans within the ApplicationContext, it is sufficient + // to register them as manual singleton instances. In addition, registration of + // the BeanOverrideBeanFactoryPostProcessor as a singleton is a requirement for + // AOT processing, since a bean definition cannot be generated for the + // Set argument that it accepts in its constructor. + BeanOverrideRegistrar beanOverrideRegistrar = new BeanOverrideRegistrar(beanFactory); + beanFactory.registerSingleton(REGISTRAR_BEAN_NAME, beanOverrideRegistrar); + beanFactory.registerSingleton(INFRASTRUCTURE_BEAN_NAME, + new BeanOverrideBeanFactoryPostProcessor(this.metadata, beanOverrideRegistrar)); + beanFactory.registerSingleton(EARLY_INFRASTRUCTURE_BEAN_NAME, + new WrapEarlyBeanPostProcessor(beanOverrideRegistrar)); } Set getMetadata() { return this.metadata; } - private void registerInfrastructure(BeanDefinitionRegistry registry) { - addInfrastructureBeanDefinition(registry, BeanOverrideRegistrar.class, REGISTRAR_BEAN_NAME, - constructorArgs -> {}); - - RuntimeBeanReference registrarReference = new RuntimeBeanReference(REGISTRAR_BEAN_NAME); - addInfrastructureBeanDefinition(registry, WrapEarlyBeanPostProcessor.class, EARLY_INFRASTRUCTURE_BEAN_NAME, - constructorArgs -> constructorArgs.addIndexedArgumentValue(0, registrarReference)); - addInfrastructureBeanDefinition(registry, BeanOverrideBeanFactoryPostProcessor.class, INFRASTRUCTURE_BEAN_NAME, - constructorArgs -> { - constructorArgs.addIndexedArgumentValue(0, this.metadata); - constructorArgs.addIndexedArgumentValue(1, registrarReference); - }); - } - - private void addInfrastructureBeanDefinition(BeanDefinitionRegistry registry, - Class clazz, String beanName, Consumer constructorArgumentsConsumer) { - - if (!registry.containsBeanDefinition(beanName)) { - RootBeanDefinition definition = new RootBeanDefinition(clazz); - definition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); - ConstructorArgumentValues constructorArguments = definition.getConstructorArgumentValues(); - constructorArgumentsConsumer.accept(constructorArguments); - registry.registerBeanDefinition(beanName, definition); - } - } - @Override public boolean equals(Object other) { if (other == this) { diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistrar.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistrar.java index 3d49207ef0..d9f749a4b4 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistrar.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideRegistrar.java @@ -22,10 +22,7 @@ import java.util.Map; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanCreationException; -import org.springframework.beans.factory.BeanFactory; -import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.config.ConfigurableBeanFactory; -import org.springframework.lang.Nullable; import org.springframework.util.Assert; import org.springframework.util.ReflectionUtils; import org.springframework.util.StringUtils; @@ -36,36 +33,31 @@ import org.springframework.util.StringUtils; * for test execution listeners. * * @author Simon Baslé + * @author Sam Brannen * @since 6.2 */ -class BeanOverrideRegistrar implements BeanFactoryAware { +class BeanOverrideRegistrar { private final Map beanNameRegistry = new HashMap<>(); private final Map earlyOverrideMetadata = new HashMap<>(); - @Nullable - private ConfigurableBeanFactory beanFactory; + private final ConfigurableBeanFactory beanFactory; - @Override - public void setBeanFactory(BeanFactory beanFactory) throws BeansException { - if (!(beanFactory instanceof ConfigurableBeanFactory cbf)) { - throw new IllegalStateException("Cannot process bean override with a BeanFactory " + - "that doesn't implement ConfigurableBeanFactory: " + beanFactory.getClass().getName()); - } - this.beanFactory = cbf; + BeanOverrideRegistrar(ConfigurableBeanFactory beanFactory) { + Assert.notNull(beanFactory, "ConfigurableBeanFactory must not be null"); + this.beanFactory = beanFactory; } /** - * Check {@link #markWrapEarly(OverrideMetadata, String) early override} + * Check {@linkplain #markWrapEarly(OverrideMetadata, String) early override} * records and use the {@link OverrideMetadata} to create an override - * instance from the provided bean, if relevant. + * instance based on the provided bean, if relevant. */ Object wrapIfNecessary(Object bean, String beanName) throws BeansException { OverrideMetadata metadata = this.earlyOverrideMetadata.get(beanName); if (metadata != null && metadata.getStrategy() == BeanOverrideStrategy.WRAP_BEAN) { - Assert.state(this.beanFactory != null, "ConfigurableBeanFactory must not be null"); bean = metadata.createOverride(beanName, null, bean); metadata.track(bean, this.beanFactory); } @@ -99,7 +91,6 @@ class BeanOverrideRegistrar implements BeanFactoryAware { try { ReflectionUtils.makeAccessible(field); Object existingValue = ReflectionUtils.getField(field, target); - Assert.state(this.beanFactory != null, "ConfigurableBeanFactory must not be null"); Object bean = this.beanFactory.getBean(beanName, field.getType()); if (existingValue == bean) { return; From 5ddeb06640c379448f943b1ae24f5ef2facccc37 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Sun, 6 Oct 2024 16:05:36 +0200 Subject: [PATCH 2/5] Skip pseudo bean definition registration during AOT processing Prior to this commit, AOT processing failed for tests that made use of the Bean Override feature to "override" a nonexistent bean. The reason is that we register a "pseudo" bean definition as a placeholder for a nonexistent bean, and our AOT support cannot automatically convert that "pseudo" bean definition to a functional bean definition for use at AOT runtime. To address that, this commit skips registration of "pseudo" bean definitions during AOT processing, and by doing so we enable the JVM runtime and AOT runtime to operate with the same semantics. See gh-32933 --- .../BeanOverrideBeanFactoryPostProcessor.java | 24 +++++++++++++++++++ .../test/context/aot/AotIntegrationTests.java | 1 - 2 files changed, 24 insertions(+), 1 deletion(-) diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java index 466a827c09..c8116f6278 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideBeanFactoryPostProcessor.java @@ -35,6 +35,7 @@ import org.springframework.beans.factory.support.BeanNameGenerator; import org.springframework.beans.factory.support.DefaultBeanNameGenerator; import org.springframework.beans.factory.support.DefaultListableBeanFactory; import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.context.aot.AbstractAotProcessor; import org.springframework.core.Ordered; import org.springframework.core.ResolvableType; import org.springframework.lang.Nullable; @@ -105,6 +106,12 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, private void replaceDefinition(ConfigurableListableBeanFactory beanFactory, OverrideMetadata overrideMetadata, boolean enforceExistingDefinition) { + // NOTE: This method supports 3 distinct scenarios which must be accounted for. + // + // 1) JVM runtime + // 2) AOT processing + // 3) AOT runtime + if (!(beanFactory instanceof BeanDefinitionRegistry registry)) { throw new IllegalStateException("Cannot process bean override with a BeanFactory " + "that doesn't implement BeanDefinitionRegistry: " + beanFactory.getClass().getName()); @@ -147,12 +154,24 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, if (existingBeanDefinition != null) { // Validate the existing bean definition. + // + // Applies during "JVM runtime", "AOT processing", and "AOT runtime". validateBeanDefinition(beanFactory, beanName); } + else if (Boolean.getBoolean(AbstractAotProcessor.AOT_PROCESSING)) { + // There was no existing bean definition, but during "AOT processing" we + // do not register the "pseudo" bean definition since our AOT support + // cannot automatically convert that to a functional bean definition for + // use at "AOT runtime". Furthermore, by not registering a bean definition + // for a nonexistent bean, we allow the "JVM runtime" and "AOT runtime" + // to operate the same in the following else-block. + } else { // There was no existing bean definition, so we register the "pseudo" bean // definition to ensure that a suitable bean definition exists for the given // bean name for proper autowiring candidate resolution. + // + // Applies during "JVM runtime" and "AOT runtime". registry.registerBeanDefinition(beanName, pseudoBeanDefinition); } @@ -163,6 +182,11 @@ class BeanOverrideBeanFactoryPostProcessor implements BeanFactoryPostProcessor, // Now we have an instance (the override) that we can register. At this stage, we don't // expect a singleton instance to be present. If for some reason a singleton instance // already exists, the following will throw an exception. + // + // As a bonus, by manually registering a singleton during "AOT processing", we allow + // GenericApplicationContext's preDetermineBeanType() method to transparently register + // runtime hints for a proxy generated by the above createOverride() invocation -- + // for example, when @MockitoBean creates a mock based on a JDK dynamic proxy. beanFactory.registerSingleton(beanName, override); } diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java index 7695515c8d..e5fa0317f8 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/AotIntegrationTests.java @@ -155,7 +155,6 @@ class AotIntegrationTests extends AbstractAotTests { runEndToEndTests(testClasses, false); } - @Disabled("Comment out to run Bean Override integration tests in AOT mode") @Test void endToEndTestsForBeanOverrides() { List> testClasses = createTestClassScanner() From 6a2e234a8775c4be702b18c48e6e93641b0b07a0 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 24 Sep 2024 14:20:05 +0200 Subject: [PATCH 3/5] Register runtime hints for BeanOverrideProcessors MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces a BeanOverrideReflectiveProcessor which registers runtime hints for any BeanOverrideProcessor configured via @⁠BeanOverride. See gh-32933 --- .../context/bean/override/BeanOverride.java | 4 ++ .../BeanOverrideReflectiveProcessor.java | 46 +++++++++++++++++++ 2 files changed, 50 insertions(+) create mode 100644 spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideReflectiveProcessor.java diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java index a25dff0a1d..8aaceb8529 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverride.java @@ -22,6 +22,8 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.aot.hint.annotation.Reflective; + /** * Mark a composed annotation as eligible for Bean Override processing. * @@ -37,11 +39,13 @@ import java.lang.annotation.Target; * {@link org.springframework.test.context.bean.override.mockito.MockitoSpyBean @MockitoSpyBean}. * * @author Simon Baslé + * @author Sam Brannen * @since 6.2 */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.ANNOTATION_TYPE) @Documented +@Reflective(BeanOverrideReflectiveProcessor.class) public @interface BeanOverride { /** diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideReflectiveProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideReflectiveProcessor.java new file mode 100644 index 0000000000..787fcd0ff3 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/BeanOverrideReflectiveProcessor.java @@ -0,0 +1,46 @@ +/* + * Copyright 2002-2024 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.test.context.bean.override; + +import java.lang.reflect.AnnotatedElement; + +import org.springframework.aot.hint.ReflectionHints; +import org.springframework.aot.hint.annotation.ReflectiveProcessor; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; + +import static org.springframework.aot.hint.MemberCategory.INVOKE_DECLARED_CONSTRUCTORS; + +/** + * {@link ReflectiveProcessor} that processes {@link BeanOverride @BeanOverride} + * annotations. + * + * @author Sam Brannen + * @since 6.2 + */ +class BeanOverrideReflectiveProcessor implements ReflectiveProcessor { + + @Override + public void registerReflectionHints(ReflectionHints hints, AnnotatedElement element) { + MergedAnnotations.from(element) + .get(BeanOverride.class) + .synthesize(MergedAnnotation::isPresent) + .map(BeanOverride::value) + .ifPresent(clazz -> hints.registerType(clazz, INVOKE_DECLARED_CONSTRUCTORS)); + } + +} From 9a4be95dee2a43955eb7f3b19c1a089ad04b7c06 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 8 Oct 2024 14:26:52 +0200 Subject: [PATCH 4/5] Verify registration of runtime hints for Bean Overrides See gh-32933 --- ...stContextAotGeneratorIntegrationTests.java | 121 +++++++++++++++--- .../override/EasyMockBeanJupiterTests.java | 75 +++++++++++ ...ests.java => MockitoBeanJupiterTests.java} | 39 ++++-- .../aot/samples/common/GreetingService.java | 28 ++++ 4 files changed, 233 insertions(+), 30 deletions(-) create mode 100644 spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/EasyMockBeanJupiterTests.java rename spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/{convention/TestBeanJupiterTests.java => MockitoBeanJupiterTests.java} (51%) create mode 100644 spring-test/src/test/java/org/springframework/test/context/aot/samples/common/GreetingService.java diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorIntegrationTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorIntegrationTests.java index 4ab72df7e6..4d791caeee 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorIntegrationTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/TestContextAotGeneratorIntegrationTests.java @@ -17,6 +17,7 @@ package org.springframework.test.context.aot; import java.lang.annotation.Annotation; +import java.lang.reflect.Proxy; import java.util.ArrayList; import java.util.List; import java.util.Set; @@ -24,7 +25,10 @@ import java.util.stream.Stream; import javax.sql.DataSource; +import org.assertj.core.util.Arrays; +import org.easymock.EasyMockSupport; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import org.springframework.aot.AotDetector; import org.springframework.aot.generate.DefaultGenerationContext; @@ -37,6 +41,7 @@ import org.springframework.aot.test.generate.CompilerFiles; import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContextInitializer; import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.aot.AbstractAotProcessor; import org.springframework.core.test.tools.CompileWithForkedClassLoader; import org.springframework.core.test.tools.TestCompiler; import org.springframework.javapoet.ClassName; @@ -47,6 +52,9 @@ import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterShar import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterTests; import org.springframework.test.context.aot.samples.basic.BasicSpringTestNGTests; import org.springframework.test.context.aot.samples.basic.BasicSpringVintageTests; +import org.springframework.test.context.aot.samples.bean.override.EasyMockBeanJupiterTests; +import org.springframework.test.context.aot.samples.bean.override.MockitoBeanJupiterTests; +import org.springframework.test.context.aot.samples.common.GreetingService; import org.springframework.test.context.aot.samples.common.MessageService; import org.springframework.test.context.aot.samples.jdbc.SqlScriptsSpringJupiterTests; import org.springframework.test.context.aot.samples.web.WebSpringJupiterTests; @@ -67,6 +75,7 @@ import static org.springframework.aot.hint.MemberCategory.INVOKE_DECLARED_CONSTR import static org.springframework.aot.hint.MemberCategory.INVOKE_DECLARED_METHODS; import static org.springframework.aot.hint.MemberCategory.INVOKE_PUBLIC_CONSTRUCTORS; import static org.springframework.aot.hint.MemberCategory.INVOKE_PUBLIC_METHODS; +import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.proxies; import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.reflection; import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.resource; import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; @@ -97,6 +106,8 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests { BasicSpringJupiterTests.NestedTests.class, BasicSpringTestNGTests.class, BasicSpringVintageTests.class, + EasyMockBeanJupiterTests.class, + MockitoBeanJupiterTests.class, SqlScriptsSpringJupiterTests.class, XmlSpringJupiterTests.class, WebSpringJupiterTests.class); @@ -104,7 +115,16 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests { InMemoryGeneratedFiles generatedFiles = new InMemoryGeneratedFiles(); TestContextAotGenerator generator = new TestContextAotGenerator(generatedFiles); - generator.processAheadOfTime(testClasses.stream().sorted(comparing(Class::getName))); + try { + // Emulate AbstractAotProcessor.process(). + System.setProperty(AbstractAotProcessor.AOT_PROCESSING, "true"); + + generator.processAheadOfTime(testClasses.stream().sorted(comparing(Class::getName))); + } + finally { + // Emulate AbstractAotProcessor.process(). + System.clearProperty(AbstractAotProcessor.AOT_PROCESSING); + } assertRuntimeHints(generator.getRuntimeHints()); @@ -142,6 +162,12 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests { else if (testClass.getPackageName().contains("jdbc")) { assertContextForJdbcTests(context); } + else if (testClass.equals(EasyMockBeanJupiterTests.class)) { + assertContextForEasyMockBeanOverrideTests(context); + } + else if (testClass.equals(MockitoBeanJupiterTests.class)) { + assertContextForMockitoBeanOverrideTests(context); + } else { assertContextForBasicTests(context); } @@ -243,6 +269,15 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests { .accepts(runtimeHints); assertThat(resource().forResource("org/springframework/test/context/aot/samples/jdbc/SqlScriptsSpringJupiterTests.test.sql")) .accepts(runtimeHints); + + // @BeanOverride(value = ...) + assertReflectionRegistered(runtimeHints, "org.springframework.test.context.bean.override.mockito.MockitoBeanOverrideProcessor", + INVOKE_DECLARED_CONSTRUCTORS); + + // GenericApplicationContext.preDetermineBeanTypes() should have registered proxy + // hints for the EasyMock interface-based mocks. + assertProxyRegistered(runtimeHints, GreetingService.class); + assertProxyRegistered(runtimeHints, MessageService.class); } private static void assertReflectionRegistered(RuntimeHints runtimeHints, String type) { @@ -267,6 +302,13 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests { assertReflectionRegistered(runtimeHints, annotationType, INVOKE_DECLARED_METHODS); } + private static void assertProxyRegistered(RuntimeHints runtimeHints, Class... interfaces) { + assertThat(proxies().forInterfaces(interfaces)) + .as("Proxy hint for %s", Arrays.asList(interfaces)) + .accepts(runtimeHints); + } + + @Test void processAheadOfTimeWithBasicTests() { @@ -297,6 +339,24 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests { assertThat(context.getBean(DataSource.class)).as("DataSource").isNotNull(); } + private void assertContextForEasyMockBeanOverrideTests(ApplicationContext context) { + GreetingService greetingService = context.getBean(GreetingService.class); + MessageService messageService = context.getBean(MessageService.class); + + assertThat(EasyMockSupport.isAMock(greetingService)).as("EasyMock mock").isTrue(); + assertThat(EasyMockSupport.isAMock(messageService)).as("EasyMock mock").isTrue(); + assertThat(Proxy.isProxyClass(greetingService.getClass())).as("JDK proxy").isTrue(); + assertThat(Proxy.isProxyClass(messageService.getClass())).as("JDK proxy").isTrue(); + } + + private void assertContextForMockitoBeanOverrideTests(ApplicationContext context) { + GreetingService greetingService = context.getBean(GreetingService.class); + MessageService messageService = context.getBean(MessageService.class); + + assertThat(Mockito.mockingDetails(greetingService).isMock()).as("Mockito mock").isTrue(); + assertThat(Mockito.mockingDetails(messageService).isMock()).as("Mockito mock").isTrue(); + } + private void assertContextForWebTests(WebApplicationContext wac) throws Exception { assertThat(wac.getEnvironment().getProperty("test.engine")).as("Environment").isNotNull(); @@ -364,10 +424,19 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests { testClasses.forEach(testClass -> { DefaultGenerationContext generationContext = generator.createGenerationContext(testClass); MergedContextConfiguration mergedConfig = buildMergedContextConfiguration(testClass); - ClassName className = generator.processAheadOfTime(mergedConfig, generationContext); - assertThat(className).isNotNull(); - mappings.add(new Mapping(mergedConfig, className)); - generationContext.writeGeneratedContent(); + try { + // Emulate AbstractAotProcessor.process(). + System.setProperty(AbstractAotProcessor.AOT_PROCESSING, "true"); + + ClassName className = generator.processAheadOfTime(mergedConfig, generationContext); + assertThat(className).isNotNull(); + mappings.add(new Mapping(mergedConfig, className)); + generationContext.writeGeneratedContent(); + } + finally { + // Emulate AbstractAotProcessor.process(). + System.clearProperty(AbstractAotProcessor.AOT_PROCESSING); + } }); return mappings; } @@ -422,29 +491,43 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests { "org/springframework/test/context/aot/samples/basic/BasicSpringVintageTests__TestContext004_BeanFactoryRegistrations.java", "org/springframework/test/context/aot/samples/basic/BasicTestConfiguration__TestContext004_BeanDefinitions.java", "org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext004_BeanDefinitions.java", - // SqlScriptsSpringJupiterTests + // EasyMockBeanJupiterTests "org/springframework/context/event/DefaultEventListenerFactory__TestContext005_BeanDefinitions.java", "org/springframework/context/event/EventListenerMethodProcessor__TestContext005_BeanDefinitions.java", - "org/springframework/test/context/aot/samples/jdbc/SqlScriptsSpringJupiterTests__TestContext005_ApplicationContextInitializer.java", - "org/springframework/test/context/aot/samples/jdbc/SqlScriptsSpringJupiterTests__TestContext005_BeanFactoryRegistrations.java", - "org/springframework/test/context/jdbc/EmptyDatabaseConfig__TestContext005_BeanDefinitions.java", + "org/springframework/test/context/aot/samples/bean/override/EasyMockBeanJupiterTests__TestContext005_ApplicationContextInitializer.java", + "org/springframework/test/context/aot/samples/bean/override/EasyMockBeanJupiterTests__TestContext005_BeanDefinitions.java", + "org/springframework/test/context/aot/samples/bean/override/EasyMockBeanJupiterTests__TestContext005_BeanFactoryRegistrations.java", "org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext005_BeanDefinitions.java", - // WebSpringJupiterTests + // MockitoBeanJupiterTests "org/springframework/context/event/DefaultEventListenerFactory__TestContext006_BeanDefinitions.java", "org/springframework/context/event/EventListenerMethodProcessor__TestContext006_BeanDefinitions.java", - "org/springframework/test/context/aot/samples/web/WebSpringJupiterTests__TestContext006_ApplicationContextInitializer.java", - "org/springframework/test/context/aot/samples/web/WebSpringJupiterTests__TestContext006_BeanFactoryRegistrations.java", - "org/springframework/test/context/aot/samples/web/WebTestConfiguration__TestContext006_BeanDefinitions.java", - "org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration__TestContext006_Autowiring.java", - "org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration__TestContext006_BeanDefinitions.java", + "org/springframework/test/context/aot/samples/bean/override/MockitoBeanJupiterTests__TestContext006_ApplicationContextInitializer.java", + "org/springframework/test/context/aot/samples/bean/override/MockitoBeanJupiterTests__TestContext006_BeanDefinitions.java", + "org/springframework/test/context/aot/samples/bean/override/MockitoBeanJupiterTests__TestContext006_BeanFactoryRegistrations.java", "org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext006_BeanDefinitions.java", - // XmlSpringJupiterTests + // SqlScriptsSpringJupiterTests "org/springframework/context/event/DefaultEventListenerFactory__TestContext007_BeanDefinitions.java", "org/springframework/context/event/EventListenerMethodProcessor__TestContext007_BeanDefinitions.java", - "org/springframework/test/context/aot/samples/common/DefaultMessageService__TestContext007_BeanDefinitions.java", - "org/springframework/test/context/aot/samples/xml/XmlSpringJupiterTests__TestContext007_ApplicationContextInitializer.java", - "org/springframework/test/context/aot/samples/xml/XmlSpringJupiterTests__TestContext007_BeanFactoryRegistrations.java", + "org/springframework/test/context/aot/samples/jdbc/SqlScriptsSpringJupiterTests__TestContext007_ApplicationContextInitializer.java", + "org/springframework/test/context/aot/samples/jdbc/SqlScriptsSpringJupiterTests__TestContext007_BeanFactoryRegistrations.java", + "org/springframework/test/context/jdbc/EmptyDatabaseConfig__TestContext007_BeanDefinitions.java", "org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext007_BeanDefinitions.java", + // WebSpringJupiterTests + "org/springframework/context/event/DefaultEventListenerFactory__TestContext008_BeanDefinitions.java", + "org/springframework/context/event/EventListenerMethodProcessor__TestContext008_BeanDefinitions.java", + "org/springframework/test/context/aot/samples/web/WebSpringJupiterTests__TestContext008_ApplicationContextInitializer.java", + "org/springframework/test/context/aot/samples/web/WebSpringJupiterTests__TestContext008_BeanFactoryRegistrations.java", + "org/springframework/test/context/aot/samples/web/WebTestConfiguration__TestContext008_BeanDefinitions.java", + "org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext008_BeanDefinitions.java", + "org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration__TestContext008_Autowiring.java", + "org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration__TestContext008_BeanDefinitions.java", + // XmlSpringJupiterTests + "org/springframework/context/event/DefaultEventListenerFactory__TestContext009_BeanDefinitions.java", + "org/springframework/context/event/EventListenerMethodProcessor__TestContext009_BeanDefinitions.java", + "org/springframework/test/context/aot/samples/common/DefaultMessageService__TestContext009_BeanDefinitions.java", + "org/springframework/test/context/aot/samples/xml/XmlSpringJupiterTests__TestContext009_ApplicationContextInitializer.java", + "org/springframework/test/context/aot/samples/xml/XmlSpringJupiterTests__TestContext009_BeanFactoryRegistrations.java", + "org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext009_BeanDefinitions.java", }; } diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/EasyMockBeanJupiterTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/EasyMockBeanJupiterTests.java new file mode 100644 index 0000000000..39d7dddd71 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/EasyMockBeanJupiterTests.java @@ -0,0 +1,75 @@ +/* + * Copyright 2002-2024 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.test.context.aot.samples.bean.override; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.context.aot.samples.common.GreetingService; +import org.springframework.test.context.aot.samples.common.MessageService; +import org.springframework.test.context.bean.override.easymock.EasyMockBean; +import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.easymock.EasyMock.expect; +import static org.easymock.EasyMock.replay; + +/** + * @author Sam Brannen + * @since 6.2 + */ +@SpringJUnitConfig +public class EasyMockBeanJupiterTests { + + /** + * Mock for nonexistent bean. + */ + @EasyMockBean + GreetingService greetingService; + + /** + * Mock for existing bean. + */ + @EasyMockBean + MessageService messageService; + + @BeforeEach + void configureMocks(@Autowired GreetingService greetingService, @Autowired MessageService messageService) { + expect(greetingService.greeting()).andReturn("enigma"); + expect(messageService.generateMessage()).andReturn("override"); + replay(greetingService, messageService); + } + + @Test + void test() { + assertThat(greetingService.greeting()).isEqualTo("enigma"); + assertThat(messageService.generateMessage()).isEqualTo("override"); + } + + @Configuration(proxyBeanMethods = false) + static class Config { + + @Bean + MessageService messageService() { + return () -> "prod"; + } + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/convention/TestBeanJupiterTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/MockitoBeanJupiterTests.java similarity index 51% rename from spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/convention/TestBeanJupiterTests.java rename to spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/MockitoBeanJupiterTests.java index 464a5cdce7..9cd784a775 100644 --- a/spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/convention/TestBeanJupiterTests.java +++ b/spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/MockitoBeanJupiterTests.java @@ -14,42 +14,59 @@ * limitations under the License. */ -package org.springframework.test.context.aot.samples.bean.override.convention; +package org.springframework.test.context.aot.samples.bean.override; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; -import org.springframework.test.context.bean.override.convention.TestBean; +import org.springframework.test.context.aot.samples.common.GreetingService; +import org.springframework.test.context.aot.samples.common.MessageService; +import org.springframework.test.context.bean.override.mockito.MockitoBean; import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.when; /** * @author Sam Brannen * @since 6.2 */ @SpringJUnitConfig -public class TestBeanJupiterTests { +public class MockitoBeanJupiterTests { - @TestBean - String magicBean; + /** + * Mock for nonexistent bean. + */ + @MockitoBean(enforceOverride = false) + GreetingService greetingService; - static String magicBean() { - return "enigma-override"; + /** + * Mock for existing bean. + */ + @MockitoBean + MessageService messageService; + + @BeforeEach + void configureMocks(@Autowired GreetingService greetingService, @Autowired MessageService messageService) { + when(greetingService.greeting()).thenReturn("enigma"); + when(messageService.generateMessage()).thenReturn("override"); } @Test - void tests() { - assertThat(magicBean).isEqualTo("enigma-override"); + void test() { + assertThat(greetingService.greeting()).isEqualTo("enigma"); + assertThat(messageService.generateMessage()).isEqualTo("override"); } @Configuration(proxyBeanMethods = false) static class Config { @Bean - String magicBean() { - return "enigma"; + MessageService messageService() { + return () -> "prod"; } } diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/samples/common/GreetingService.java b/spring-test/src/test/java/org/springframework/test/context/aot/samples/common/GreetingService.java new file mode 100644 index 0000000000..53e31b7ed9 --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/aot/samples/common/GreetingService.java @@ -0,0 +1,28 @@ +/* + * Copyright 2002-2024 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.test.context.aot.samples.common; + +/** + * @author Sam Brannen + * @since 6.2 + */ +@FunctionalInterface +public interface GreetingService { + + String greeting(); + +} From 65d219131ccead4ce7e568f731e6734769690a93 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Tue, 1 Oct 2024 16:02:11 +0200 Subject: [PATCH 5/5] Stop disabling MockitoResetTestExecutionListener within a native image Instead, MockitoResetTestExecutionListener is now only enabled if the current test class uses Mockito annotations or Mockito-related annotations in spring-test. See gh-32933 --- .../mockito/MockitoAnnotationDetector.java | 66 +++++++++++++++++++ .../MockitoResetTestExecutionListener.java | 7 +- .../mockito/MockitoTestExecutionListener.java | 45 ------------- 3 files changed, 70 insertions(+), 48 deletions(-) create mode 100644 spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoAnnotationDetector.java diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoAnnotationDetector.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoAnnotationDetector.java new file mode 100644 index 0000000000..ed102c702d --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoAnnotationDetector.java @@ -0,0 +1,66 @@ +/* + * Copyright 2002-2024 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.test.context.bean.override.mockito; + +import java.lang.annotation.Annotation; +import java.lang.reflect.AnnotatedElement; +import java.util.Arrays; +import java.util.concurrent.atomic.AtomicBoolean; +import java.util.function.Predicate; + +import org.springframework.util.ReflectionUtils; + +/** + * Utility class that detects {@code org.mockito} annotations as well as the + * annotations in this package (like {@link MockitoBeanSettings @MockitoBeanSettings}). + * + * @author Simon Baslé + * @author Sam Brannen + */ +abstract class MockitoAnnotationDetector { + + private static final String MOCKITO_BEAN_PACKAGE = MockitoBeanSettings.class.getPackageName(); + + private static final String ORG_MOCKITO_PACKAGE = "org.mockito"; + + private static final Predicate isMockitoAnnotation = annotation -> { + String packageName = annotation.annotationType().getPackageName(); + return (packageName.startsWith(MOCKITO_BEAN_PACKAGE) || + packageName.startsWith(ORG_MOCKITO_PACKAGE)); + }; + + static boolean hasMockitoAnnotations(Class testClass) { + if (isAnnotated(testClass)) { + return true; + } + // TODO Ideally we should short-circuit the search once we've found a Mockito annotation, + // since there's no need to continue searching additional fields or further up the class + // hierarchy; however, that is not possible with ReflectionUtils#doWithFields. Plus, the + // previous invocation of isAnnotated(testClass) only finds annotations declared directly + // on the test class. So, we'll likely need a completely different approach that combines + // the "test class/interface is annotated?" and "field is annotated?" checks in a single + // search algorithm. + AtomicBoolean found = new AtomicBoolean(); + ReflectionUtils.doWithFields(testClass, field -> found.set(true), MockitoAnnotationDetector::isAnnotated); + return found.get(); + } + + private static boolean isAnnotated(AnnotatedElement annotatedElement) { + return Arrays.stream(annotatedElement.getAnnotations()).anyMatch(isMockitoAnnotation); + } + +} diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java index 46cf654c18..2785fd7ab1 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoResetTestExecutionListener.java @@ -29,7 +29,6 @@ import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; -import org.springframework.core.NativeDetector; import org.springframework.core.Ordered; import org.springframework.lang.Nullable; import org.springframework.test.context.TestContext; @@ -58,14 +57,16 @@ public class MockitoResetTestExecutionListener extends AbstractTestExecutionList @Override public void beforeTestMethod(TestContext testContext) throws Exception { - if (MockitoTestExecutionListener.mockitoPresent && !NativeDetector.inNativeImage()) { + Class testClass = testContext.getTestClass(); + if (MockitoTestExecutionListener.mockitoPresent && MockitoAnnotationDetector.hasMockitoAnnotations(testClass)) { resetMocks(testContext.getApplicationContext(), MockReset.BEFORE); } } @Override public void afterTestMethod(TestContext testContext) throws Exception { - if (MockitoTestExecutionListener.mockitoPresent && !NativeDetector.inNativeImage()) { + Class testClass = testContext.getTestClass(); + if (MockitoTestExecutionListener.mockitoPresent && MockitoAnnotationDetector.hasMockitoAnnotations(testClass)) { resetMocks(testContext.getApplicationContext(), MockReset.AFTER); } } diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoTestExecutionListener.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoTestExecutionListener.java index 46d7799889..0630e355cd 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoTestExecutionListener.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/mockito/MockitoTestExecutionListener.java @@ -16,12 +16,6 @@ package org.springframework.test.context.bean.override.mockito; -import java.lang.annotation.Annotation; -import java.lang.reflect.AnnotatedElement; -import java.util.Arrays; -import java.util.concurrent.atomic.AtomicBoolean; -import java.util.function.Predicate; - import org.mockito.Mockito; import org.mockito.MockitoSession; import org.mockito.quality.Strictness; @@ -31,7 +25,6 @@ import org.springframework.test.context.TestContext; import org.springframework.test.context.support.AbstractTestExecutionListener; import org.springframework.test.context.support.DependencyInjectionTestExecutionListener; import org.springframework.util.ClassUtils; -import org.springframework.util.ReflectionUtils; /** * {@code TestExecutionListener} that enables {@link MockitoBean @MockitoBean} @@ -127,42 +120,4 @@ public class MockitoTestExecutionListener extends AbstractTestExecutionListener } } - - /** - * Utility class that detects {@code org.mockito} annotations as well as the - * annotations in this package (like {@link MockitoBeanSettings @MockitoBeanSettings}). - */ - private static class MockitoAnnotationDetector { - - private static final String MOCKITO_BEAN_PACKAGE = MockitoBeanSettings.class.getPackageName(); - - private static final String ORG_MOCKITO_PACKAGE = "org.mockito"; - - private static final Predicate isMockitoAnnotation = annotation -> { - String packageName = annotation.annotationType().getPackageName(); - return (packageName.startsWith(MOCKITO_BEAN_PACKAGE) || - packageName.startsWith(ORG_MOCKITO_PACKAGE)); - }; - - static boolean hasMockitoAnnotations(Class testClass) { - if (isAnnotated(testClass)) { - return true; - } - // TODO Ideally we should short-circuit the search once we've found a Mockito annotation, - // since there's no need to continue searching additional fields or further up the class - // hierarchy; however, that is not possible with ReflectionUtils#doWithFields. Plus, the - // previous invocation of isAnnotated(testClass) only finds annotations declared directly - // on the test class. So, we'll likely need a completely different approach that combines - // the "test class/interface is annotated?" and "field is annotated?" checks in a single - // search algorithm. - AtomicBoolean found = new AtomicBoolean(); - ReflectionUtils.doWithFields(testClass, field -> found.set(true), MockitoAnnotationDetector::isAnnotated); - return found.get(); - } - - private static boolean isAnnotated(AnnotatedElement annotatedElement) { - return Arrays.stream(annotatedElement.getAnnotations()).anyMatch(isMockitoAnnotation); - } - } - }