From a3b979c5ecb7eda96ebf264672ce522177c6fc77 Mon Sep 17 00:00:00 2001 From: Sam Brannen <104798+sbrannen@users.noreply.github.com> Date: Fri, 1 Nov 2024 16:03:56 +0100 Subject: [PATCH] =?UTF-8?q?Register=20runtime=20hints=20for=20@=E2=81=A0Te?= =?UTF-8?q?stBean=20fully-qualified=20method=20names?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit This commit introduces a TestBeanReflectiveProcessor that registers GraalVM native image reflection hints for a fully-qualified method name configured via @⁠TestBean. Closes gh-33836 --- .../bean/override/convention/TestBean.java | 2 + .../TestBeanReflectiveProcessor.java | 57 +++++++++++++++++++ ...stContextAotGeneratorIntegrationTests.java | 55 +++++++++++++----- .../bean/override/GreetingServiceFactory.java | 31 ++++++++++ .../bean/override/TestBeanJupiterTests.java | 43 ++++++++++++++ 5 files changed, 174 insertions(+), 14 deletions(-) create mode 100644 spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanReflectiveProcessor.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/GreetingServiceFactory.java create mode 100644 spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/TestBeanJupiterTests.java diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java index a333ca73ac..c2bd8925be 100644 --- a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBean.java @@ -22,6 +22,7 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; +import org.springframework.aot.hint.annotation.Reflective; import org.springframework.core.annotation.AliasFor; import org.springframework.test.context.bean.override.BeanOverride; @@ -115,6 +116,7 @@ import org.springframework.test.context.bean.override.BeanOverride; @Retention(RetentionPolicy.RUNTIME) @Documented @BeanOverride(TestBeanOverrideProcessor.class) +@Reflective(TestBeanReflectiveProcessor.class) public @interface TestBean { /** diff --git a/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanReflectiveProcessor.java b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanReflectiveProcessor.java new file mode 100644 index 0000000000..6cf4552342 --- /dev/null +++ b/spring-test/src/main/java/org/springframework/test/context/bean/override/convention/TestBeanReflectiveProcessor.java @@ -0,0 +1,57 @@ +/* + * 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.convention; + +import java.lang.reflect.AnnotatedElement; +import java.util.List; + +import org.springframework.aot.hint.ReflectionHints; +import org.springframework.aot.hint.TypeReference; +import org.springframework.aot.hint.annotation.ReflectiveProcessor; +import org.springframework.core.annotation.MergedAnnotation; +import org.springframework.core.annotation.MergedAnnotations; +import org.springframework.util.Assert; + +import static org.springframework.aot.hint.ExecutableMode.INVOKE; + +/** + * {@link ReflectiveProcessor} that processes {@link TestBean @TestBean} annotations. + * + * @author Sam Brannen + * @since 6.2 + */ +class TestBeanReflectiveProcessor implements ReflectiveProcessor { + + @Override + public void registerReflectionHints(ReflectionHints hints, AnnotatedElement element) { + MergedAnnotations.from(element) + .get(TestBean.class) + .synthesize(MergedAnnotation::isPresent) + .map(TestBean::methodName) + .filter(methodName -> methodName.contains("#")) + .ifPresent(methodName -> { + int indexOfHash = methodName.lastIndexOf('#'); + String className = methodName.substring(0, indexOfHash).trim(); + Assert.hasText(className, () -> "No class name present in fully-qualified method name: " + methodName); + String methodNameToUse = methodName.substring(indexOfHash + 1).trim(); + Assert.hasText(methodNameToUse, () -> "No method name present in fully-qualified method name: " + methodName); + hints.registerType(TypeReference.of(className), builder -> + builder.withMethod(methodNameToUse, List.of(), INVOKE)); + }); + } + +} 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 97e7c5068d..7b11ed66b7 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 @@ -53,7 +53,9 @@ import org.springframework.test.context.aot.samples.basic.BasicSpringJupiterTest 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.GreetingServiceFactory; import org.springframework.test.context.aot.samples.bean.override.MockitoBeanJupiterTests; +import org.springframework.test.context.aot.samples.bean.override.TestBeanJupiterTests; 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; @@ -108,6 +110,7 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests { BasicSpringVintageTests.class, EasyMockBeanJupiterTests.class, MockitoBeanJupiterTests.class, + TestBeanJupiterTests.class, SqlScriptsSpringJupiterTests.class, XmlSpringJupiterTests.class, WebSpringJupiterTests.class); @@ -162,6 +165,9 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests { else if (testClass.getPackageName().contains("jdbc")) { assertContextForJdbcTests(context); } + else if (testClass.equals(TestBeanJupiterTests.class)) { + assertContextForTestBeanOverrideTests(context); + } else if (testClass.equals(EasyMockBeanJupiterTests.class)) { assertContextForEasyMockBeanOverrideTests(context); } @@ -275,12 +281,18 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests { // @BeanOverride(value = ...) Stream.of( + // @TestBean + "org.springframework.test.context.bean.override.convention.TestBeanOverrideProcessor", // @MockitoBean "org.springframework.test.context.bean.override.mockito.MockitoBeanOverrideProcessor", // @EasyMockBean "org.springframework.test.context.bean.override.easymock.EasyMockBeanOverrideProcessor" ).forEach(type -> assertReflectionRegistered(runtimeHints, type, INVOKE_DECLARED_CONSTRUCTORS)); + // @TestBean(methodName = ) + assertThat(reflection().onMethod(GreetingServiceFactory.class, "createEnigmaGreetingService")) + .accepts(runtimeHints); + // GenericApplicationContext.preDetermineBeanTypes() should have registered proxy // hints for the EasyMock interface-based mocks. assertProxyRegistered(runtimeHints, GreetingService.class); @@ -346,6 +358,11 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests { assertThat(context.getBean(DataSource.class)).as("DataSource").isNotNull(); } + private void assertContextForTestBeanOverrideTests(ApplicationContext context) { + GreetingService greetingService = context.getBean(GreetingService.class); + assertThat(greetingService.greeting()).isEqualTo("enigma"); + } + private void assertContextForEasyMockBeanOverrideTests(ApplicationContext context) { GreetingService greetingService = context.getBean(GreetingService.class); MessageService messageService = context.getBean(MessageService.class); @@ -512,29 +529,39 @@ class TestContextAotGeneratorIntegrationTests extends AbstractAotTests { "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", - // SqlScriptsSpringJupiterTests + + // TestBeanJupiterTests "org/springframework/context/event/DefaultEventListenerFactory__TestContext007_BeanDefinitions.java", "org/springframework/context/event/EventListenerMethodProcessor__TestContext007_BeanDefinitions.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/aot/samples/bean/override/TestBeanJupiterTests__TestContext007_ApplicationContextInitializer.java", + "org/springframework/test/context/aot/samples/bean/override/TestBeanJupiterTests__TestContext007_BeanFactoryRegistrations.java", "org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext007_BeanDefinitions.java", - // WebSpringJupiterTests + + // SqlScriptsSpringJupiterTests "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/aot/samples/jdbc/SqlScriptsSpringJupiterTests__TestContext008_ApplicationContextInitializer.java", + "org/springframework/test/context/aot/samples/jdbc/SqlScriptsSpringJupiterTests__TestContext008_BeanFactoryRegistrations.java", + "org/springframework/test/context/jdbc/EmptyDatabaseConfig__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 + + // WebSpringJupiterTests "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/aot/samples/web/WebSpringJupiterTests__TestContext009_ApplicationContextInitializer.java", + "org/springframework/test/context/aot/samples/web/WebSpringJupiterTests__TestContext009_BeanFactoryRegistrations.java", + "org/springframework/test/context/aot/samples/web/WebTestConfiguration__TestContext009_BeanDefinitions.java", "org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext009_BeanDefinitions.java", + "org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration__TestContext009_Autowiring.java", + "org/springframework/web/servlet/config/annotation/DelegatingWebMvcConfiguration__TestContext009_BeanDefinitions.java", + + // XmlSpringJupiterTests + "org/springframework/context/event/DefaultEventListenerFactory__TestContext010_BeanDefinitions.java", + "org/springframework/context/event/EventListenerMethodProcessor__TestContext010_BeanDefinitions.java", + "org/springframework/test/context/aot/samples/common/DefaultMessageService__TestContext010_BeanDefinitions.java", + "org/springframework/test/context/aot/samples/xml/XmlSpringJupiterTests__TestContext010_ApplicationContextInitializer.java", + "org/springframework/test/context/aot/samples/xml/XmlSpringJupiterTests__TestContext010_BeanFactoryRegistrations.java", + "org/springframework/test/context/support/DynamicPropertyRegistrarBeanInitializer__TestContext010_BeanDefinitions.java" }; } diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/GreetingServiceFactory.java b/spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/GreetingServiceFactory.java new file mode 100644 index 0000000000..dae863f87e --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/GreetingServiceFactory.java @@ -0,0 +1,31 @@ +/* + * 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.springframework.test.context.aot.samples.common.GreetingService; + +/** + * @author Sam Brannen + * @since 6.2 + */ +public class GreetingServiceFactory { + + public static GreetingService createEnigmaGreetingService() { + return () -> "enigma"; + } + +} diff --git a/spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/TestBeanJupiterTests.java b/spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/TestBeanJupiterTests.java new file mode 100644 index 0000000000..cca32117bb --- /dev/null +++ b/spring-test/src/test/java/org/springframework/test/context/aot/samples/bean/override/TestBeanJupiterTests.java @@ -0,0 +1,43 @@ +/* + * 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.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.test.context.aot.samples.common.GreetingService; +import org.springframework.test.context.bean.override.convention.TestBean; +import org.springframework.test.context.junit.jupiter.SpringExtension; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author Sam Brannen + * @since 6.2 + */ +@ExtendWith(SpringExtension.class) +public class TestBeanJupiterTests { + + @TestBean(methodName = "org.springframework.test.context.aot.samples.bean.override.GreetingServiceFactory#createEnigmaGreetingService") + GreetingService greetingService; + + @Test + void test() { + assertThat(greetingService.greeting()).isEqualTo("enigma"); + } + +}