From 06192a575cac356c2e6c826f4995976c24a337fb Mon Sep 17 00:00:00 2001 From: Olga Maciaszek-Sharma Date: Thu, 23 Mar 2023 16:45:02 +0100 Subject: [PATCH] Register method params for reflection (#849) --- ...BeanFactoryInitializationAotProcessor.java | 47 ++++- ...actoryInitializationAotProcessorTests.java | 197 ++++++++++++++++++ 2 files changed, 237 insertions(+), 7 deletions(-) create mode 100644 spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/aot/FeignClientBeanFactoryInitializationAotProcessorTests.java diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/aot/FeignClientBeanFactoryInitializationAotProcessor.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/aot/FeignClientBeanFactoryInitializationAotProcessor.java index 3e599537..ca802741 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/aot/FeignClientBeanFactoryInitializationAotProcessor.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/aot/FeignClientBeanFactoryInitializationAotProcessor.java @@ -1,5 +1,5 @@ /* - * Copyright 2022-2022 the original author or authors. + * Copyright 2022-2023 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. @@ -16,6 +16,8 @@ package org.springframework.cloud.openfeign.aot; +import java.lang.reflect.Method; +import java.lang.reflect.Parameter; import java.util.Map; import java.util.Objects; import java.util.Set; @@ -25,7 +27,9 @@ import javax.lang.model.element.Modifier; import org.springframework.aot.generate.GenerationContext; import org.springframework.aot.generate.MethodReference; -import org.springframework.aot.hint.ProxyHints; +import org.springframework.aot.hint.BindingReflectionHintsRegistrar; +import org.springframework.aot.hint.ReflectionHints; +import org.springframework.aot.hint.RuntimeHints; import org.springframework.beans.MutablePropertyValues; import org.springframework.beans.factory.BeanFactory; import org.springframework.beans.factory.FactoryBean; @@ -47,6 +51,7 @@ import org.springframework.cloud.openfeign.FeignClientFactory; import org.springframework.cloud.openfeign.FeignClientFactoryBean; import org.springframework.cloud.openfeign.FeignClientSpecification; import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.MethodParameter; import org.springframework.javapoet.MethodSpec; import org.springframework.util.Assert; import org.springframework.util.ClassUtils; @@ -54,7 +59,7 @@ import org.springframework.util.ClassUtils; /** * A {@link BeanFactoryInitializationAotProcessor} that creates an * {@link BeanFactoryInitializationAotContribution} that registers bean definitions and - * proxy hints for Feign client beans. + * proxy and reflection hints for Feign client beans. * * @author Olga Maciaszek-Sharma * @since 4.0.0 @@ -66,6 +71,8 @@ public class FeignClientBeanFactoryInitializationAotProcessor private final Map feignClientBeanDefinitions; + private final BindingReflectionHintsRegistrar bindingRegistrar = new BindingReflectionHintsRegistrar(); + public FeignClientBeanFactoryInitializationAotProcessor(GenericApplicationContext context, FeignClientFactory feignClientFactory) { this.context = context; @@ -86,6 +93,7 @@ public class FeignClientBeanFactoryInitializationAotProcessor .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); } + @SuppressWarnings("NullableProblems") @Override public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { BeanFactory applicationBeanFactory = context.getBeanFactory(); @@ -95,7 +103,26 @@ public class FeignClientBeanFactoryInitializationAotProcessor return new AotContribution(feignClientBeanDefinitions); } - private static final class AotContribution implements BeanFactoryInitializationAotContribution { + private void registerMethodHints(ReflectionHints hints, Class clazz) { + for (Method method : clazz.getDeclaredMethods()) { + registerMethodHints(hints, method); + } + } + + private void registerMethodHints(ReflectionHints hints, Method method) { + for (Parameter parameter : method.getParameters()) { + bindingRegistrar.registerReflectionHints(hints, + MethodParameter.forParameter(parameter).getGenericParameterType()); + } + MethodParameter returnTypeParameter = MethodParameter.forExecutable(method, -1); + if (!void.class.equals(returnTypeParameter.getParameterType())) { + bindingRegistrar.registerReflectionHints(hints, returnTypeParameter.getGenericParameterType()); + } + + } + + // Visible for tests + final class AotContribution implements BeanFactoryInitializationAotContribution { private final Map feignClientBeanDefinitions; @@ -106,7 +133,7 @@ public class FeignClientBeanFactoryInitializationAotProcessor @Override public void applyTo(GenerationContext generationContext, BeanFactoryInitializationCode beanFactoryInitializationCode) { - ProxyHints proxyHints = generationContext.getRuntimeHints().proxies(); + RuntimeHints hints = generationContext.getRuntimeHints(); Set feignClientRegistrationMethods = feignClientBeanDefinitions.values().stream() .map(beanDefinition -> { Assert.notNull(beanDefinition, "beanDefinition cannot be null"); @@ -115,8 +142,9 @@ public class FeignClientBeanFactoryInitializationAotProcessor MutablePropertyValues feignClientProperties = registeredBeanDefinition.getPropertyValues(); String className = (String) feignClientProperties.get("type"); Assert.notNull(className, "className cannot be null"); - Class clazz = ClassUtils.resolveClassName(className, null); - proxyHints.registerJdkProxy(clazz); + Class clazz = ClassUtils.resolveClassName(className, null); + hints.proxies().registerJdkProxy(clazz); + registerMethodHints(hints.reflection(), clazz); return beanFactoryInitializationCode.getMethods() .add(buildMethodName(className), method -> generateFeignClientRegistrationMethod(method, feignClientProperties, registeredBeanDefinition)) @@ -177,6 +205,11 @@ public class FeignClientBeanFactoryInitializationAotProcessor .addStatement("$T.registerBeanDefinition(holder, registry) ", BeanDefinitionReaderUtils.class); } + // Visible for tests + Map getFeignClientBeanDefinitions() { + return feignClientBeanDefinitions; + } + } } diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/aot/FeignClientBeanFactoryInitializationAotProcessorTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/aot/FeignClientBeanFactoryInitializationAotProcessorTests.java new file mode 100644 index 00000000..80988c57 --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/aot/FeignClientBeanFactoryInitializationAotProcessorTests.java @@ -0,0 +1,197 @@ +/* + * Copyright 2022-2023 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.cloud.openfeign.aot; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.function.Consumer; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +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.aot.hint.RuntimeHints; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.MutablePropertyValues; +import org.springframework.beans.PropertyValue; +import org.springframework.beans.PropertyValues; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.ConstructorArgumentValues; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.beans.factory.support.RootBeanDefinition; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.cloud.openfeign.FeignClientFactory; +import org.springframework.cloud.openfeign.FeignClientSpecification; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.javapoet.TypeSpec; +import org.springframework.web.bind.annotation.PostMapping; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.proxies; +import static org.springframework.aot.hint.predicate.RuntimeHintsPredicates.reflection; + +/** + * Tests for {@link FeignClientBeanFactoryInitializationAotProcessor}. + * + * @author Olga Maciaszek-Sharma + */ +class FeignClientBeanFactoryInitializationAotProcessorTests { + + private static final String BEAN_DEFINITION_CLASS_NAME = "org.springframework.cloud.openfeign.aot.FeignClientBeanFactoryInitializationAotProcessorTests$TestClient"; + + private final TestGenerationContext generationContext = new TestGenerationContext(); + + private final FeignClientFactory feignClientFactory = mock(FeignClientFactory.class); + + private final BeanFactoryInitializationCode beanFactoryInitializationCode = new MockBeanFactoryInitializationCode( + generationContext); + + @BeforeEach + void setUp() { + Map configurations = Map.of("test", + new FeignClientSpecification("test", TestClient.class.getCanonicalName(), new Class[] {})); + when(feignClientFactory.getConfigurations()).thenReturn(configurations); + + } + + @Test + void shouldGenerateBeanInitializationCodeAndRegisterHints() { + DefaultListableBeanFactory beanFactory = beanFactory(); + GenericApplicationContext context = new GenericApplicationContext(beanFactory); + FeignClientBeanFactoryInitializationAotProcessor processor = new FeignClientBeanFactoryInitializationAotProcessor( + context, feignClientFactory); + + BeanFactoryInitializationAotContribution contribution = processor.processAheadOfTime(beanFactory); + assertThat(contribution).isNotNull(); + + verifyContribution((FeignClientBeanFactoryInitializationAotProcessor.AotContribution) contribution); + contribution.applyTo(generationContext, beanFactoryInitializationCode); + + RuntimeHints hints = generationContext.getRuntimeHints(); + assertThat(reflection().onType(TestReturnType.class)).accepts(hints); + assertThat(reflection().onType(TestArgType.class)).accepts(hints); + assertThat(proxies().forInterfaces(TestClient.class)).accepts(hints); + } + + private static void verifyContribution( + FeignClientBeanFactoryInitializationAotProcessor.AotContribution contribution) { + BeanDefinition contributionBeanDefinition = contribution.getFeignClientBeanDefinitions() + .get(TestClient.class.getCanonicalName()); + assertThat(contributionBeanDefinition.getBeanClassName()).isEqualTo(BEAN_DEFINITION_CLASS_NAME); + PropertyValues propertyValues = contributionBeanDefinition.getPropertyValues(); + assertThat(propertyValues).isNotEmpty(); + assertThat(((String[]) propertyValues.getPropertyValue("qualifiers").getValue())).containsExactly("test"); + assertThat(propertyValues.getPropertyValue("type").getValue()).isEqualTo(TestClient.class.getCanonicalName()); + assertThat(propertyValues.getPropertyValue("fallback").getValue()).isEqualTo(String.class); + } + + private DefaultListableBeanFactory beanFactory() { + GenericBeanDefinition beanDefinition = new GenericBeanDefinition(new RootBeanDefinition(TestClient.class, + new ConstructorArgumentValues(), + new MutablePropertyValues(List.of(new PropertyValue("type", TestClient.class.getCanonicalName()), + new PropertyValue("qualifiers", new String[] { "test" }), + new PropertyValue("fallback", String.class), + new PropertyValue("fallbackFactory", Void.class))))); + DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory(); + beanFactory.registerBeanDefinition(TestClient.class.getCanonicalName(), beanDefinition); + return beanFactory; + } + + @FeignClient + interface TestClient { + + @PostMapping("/test") + TestReturnType test(TestArgType arg); + + } + + @SuppressWarnings("NullableProblems") + static class MockBeanFactoryInitializationCode implements BeanFactoryInitializationCode { + + private static final Consumer emptyTypeCustomizer = type -> { + }; + + private final GeneratedClass generatedClass; + + MockBeanFactoryInitializationCode(GenerationContext generationContext) { + generatedClass = generationContext.getGeneratedClasses().addForFeature("Test", emptyTypeCustomizer); + } + + @Override + public GeneratedMethods getMethods() { + return generatedClass.getMethods(); + } + + @Override + public void addInitializer(MethodReference methodReference) { + new ArrayList<>(); + } + + } + + static class TestReturnType { + + private String test; + + TestReturnType(String test) { + this.test = test; + } + + TestReturnType() { + } + + String getTest() { + return test; + } + + void setTest(String test) { + this.test = test; + } + + } + + static class TestArgType { + + private String test; + + TestArgType(String test) { + this.test = test; + } + + TestArgType() { + } + + String getTest() { + return test; + } + + void setTest(String test) { + this.test = test; + } + + } + +}