From 01f101a910d4704877366cee4a8dc795e2ef7633 Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Wed, 13 Jan 2021 10:15:05 +0100 Subject: [PATCH] Lazy openfeign bean registration (#455) without this change we're eagerly resolving placeholder properties in passed URLs / names. Since this happens at bean definition level it's extremely early e.g. Spring Cloud Contract has not yet registered any placeholders that Feign could try to consume. with this change we're changing the bean to become lazy initialized and we resolve the placeholder values at runtime - as late as possible. --- .../openfeign/FeignClientFactoryBean.java | 24 +++++-- .../openfeign/FeignClientsRegistrar.java | 72 +++++++++++++------ .../FeignClientUsingConfigurerTest.java | 21 +++--- 3 files changed, 83 insertions(+), 34 deletions(-) diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientFactoryBean.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientFactoryBean.java index 50d46d84..d02aebed 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientFactoryBean.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientFactoryBean.java @@ -38,6 +38,8 @@ import feign.codec.ErrorDecoder; import org.springframework.beans.BeanUtils; import org.springframework.beans.BeansException; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.BeanFactoryAware; import org.springframework.beans.factory.FactoryBean; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.NoSuchBeanDefinitionException; @@ -58,7 +60,8 @@ import org.springframework.util.StringUtils; * @author Olga Maciaszek-Sharma * @author Ilia Ilinykh */ -public class FeignClientFactoryBean implements FactoryBean, InitializingBean, ApplicationContextAware { +public class FeignClientFactoryBean + implements FactoryBean, InitializingBean, ApplicationContextAware, BeanFactoryAware { /*********************************** * WARNING! Nothing in this class should be @Autowired. It causes NPEs because of some @@ -81,6 +84,8 @@ public class FeignClientFactoryBean implements FactoryBean, Initializing private ApplicationContext applicationContext; + private BeanFactory beanFactory; + private Class fallback = void.class; private Class fallbackFactory = void.class; @@ -125,7 +130,8 @@ public class FeignClientFactoryBean implements FactoryBean, Initializing } protected void configureFeign(FeignContext context, Feign.Builder builder) { - FeignClientProperties properties = applicationContext.getBean(FeignClientProperties.class); + FeignClientProperties properties = beanFactory != null ? beanFactory.getBean(FeignClientProperties.class) + : applicationContext.getBean(FeignClientProperties.class); FeignClientConfigurer feignClientConfigurer = getOptional(context, FeignClientConfigurer.class); setInheritParentContext(feignClientConfigurer.inheritParentConfiguration()); @@ -261,7 +267,7 @@ public class FeignClientFactoryBean implements FactoryBean, Initializing private T getOrInstantiate(Class tClass) { try { - return applicationContext.getBean(tClass); + return beanFactory != null ? beanFactory.getBean(tClass) : applicationContext.getBean(tClass); } catch (NoSuchBeanDefinitionException e) { return BeanUtils.instantiateClass(tClass); @@ -321,7 +327,8 @@ public class FeignClientFactoryBean implements FactoryBean, Initializing * information */ T getTarget() { - FeignContext context = applicationContext.getBean(FeignContext.class); + FeignContext context = this.beanFactory != null ? this.beanFactory.getBean(FeignContext.class) + : applicationContext.getBean(FeignContext.class); Feign.Builder builder = feign(context); if (!StringUtils.hasText(url)) { @@ -437,6 +444,7 @@ public class FeignClientFactoryBean implements FactoryBean, Initializing @Override public void setApplicationContext(ApplicationContext context) throws BeansException { this.applicationContext = context; + this.beanFactory = context; } public Class getFallback() { @@ -464,7 +472,8 @@ public class FeignClientFactoryBean implements FactoryBean, Initializing return false; } FeignClientFactoryBean that = (FeignClientFactoryBean) o; - return Objects.equals(applicationContext, that.applicationContext) && decode404 == that.decode404 + return Objects.equals(applicationContext, that.applicationContext) + && Objects.equals(beanFactory, that.beanFactory) && decode404 == that.decode404 && inheritParentContext == that.inheritParentContext && Objects.equals(fallback, that.fallback) && Objects.equals(fallbackFactory, that.fallbackFactory) && Objects.equals(name, that.name) && Objects.equals(path, that.path) && Objects.equals(type, that.type) && Objects.equals(url, that.url); @@ -486,4 +495,9 @@ public class FeignClientFactoryBean implements FactoryBean, Initializing .append(fallbackFactory).append("}").toString(); } + @Override + public void setBeanFactory(BeanFactory beanFactory) throws BeansException { + this.beanFactory = beanFactory; + } + } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsRegistrar.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsRegistrar.java index 11a545f7..cd904265 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsRegistrar.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsRegistrar.java @@ -30,6 +30,9 @@ import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition; import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.config.BeanExpressionContext; +import org.springframework.beans.factory.config.BeanExpressionResolver; +import org.springframework.beans.factory.config.ConfigurableBeanFactory; import org.springframework.beans.factory.support.AbstractBeanDefinition; import org.springframework.beans.factory.support.BeanDefinitionBuilder; import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; @@ -194,23 +197,40 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map attributes) { String className = annotationMetadata.getClassName(); - BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class); - validate(attributes); - definition.addPropertyValue("url", getUrl(attributes)); - definition.addPropertyValue("path", getPath(attributes)); + Class clazz = ClassUtils.resolveClassName(className, null); + ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory + ? (ConfigurableBeanFactory) registry : null; + String contextId = getContextId(beanFactory, attributes); String name = getName(attributes); - definition.addPropertyValue("name", name); - String contextId = getContextId(attributes); - definition.addPropertyValue("contextId", contextId); - definition.addPropertyValue("type", className); - definition.addPropertyValue("decode404", attributes.get("decode404")); - definition.addPropertyValue("fallback", attributes.get("fallback")); - definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory")); + FeignClientFactoryBean factoryBean = new FeignClientFactoryBean(); + factoryBean.setBeanFactory(beanFactory); + factoryBean.setName(name); + factoryBean.setContextId(contextId); + factoryBean.setType(clazz); + BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> { + factoryBean.setUrl(getUrl(beanFactory, attributes)); + factoryBean.setPath(getPath(beanFactory, attributes)); + factoryBean.setDecode404(Boolean.parseBoolean(String.valueOf(attributes.get("decode404")))); + Object fallback = attributes.get("fallback"); + if (fallback != null) { + factoryBean.setFallback(fallback instanceof Class ? (Class) fallback + : ClassUtils.resolveClassName(fallback.toString(), null)); + } + Object fallbackFactory = attributes.get("fallbackFactory"); + if (fallbackFactory != null) { + factoryBean.setFallbackFactory(fallbackFactory instanceof Class ? (Class) fallbackFactory + : ClassUtils.resolveClassName(fallbackFactory.toString(), null)); + } + return factoryBean.getObject(); + }); definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); + definition.setLazyInit(true); + validate(attributes); String alias = contextId + "FeignClient"; AbstractBeanDefinition beanDefinition = definition.getBeanDefinition(); beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className); + beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean); // has a default, won't be null boolean primary = (Boolean) attributes.get("primary"); @@ -235,6 +255,10 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo } /* for testing */ String getName(Map attributes) { + return getName(null, attributes); + } + + String getName(ConfigurableBeanFactory beanFactory, Map attributes) { String name = (String) attributes.get("serviceId"); if (!StringUtils.hasText(name)) { name = (String) attributes.get("name"); @@ -242,34 +266,42 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo if (!StringUtils.hasText(name)) { name = (String) attributes.get("value"); } - name = resolve(name); + name = resolve(beanFactory, name); return getName(name); } - private String getContextId(Map attributes) { + private String getContextId(ConfigurableBeanFactory beanFactory, Map attributes) { String contextId = (String) attributes.get("contextId"); if (!StringUtils.hasText(contextId)) { return getName(attributes); } - contextId = resolve(contextId); + contextId = resolve(beanFactory, contextId); return getName(contextId); } - private String resolve(String value) { + private String resolve(ConfigurableBeanFactory beanFactory, String value) { if (StringUtils.hasText(value)) { - return this.environment.resolvePlaceholders(value); + if (beanFactory == null) { + return this.environment.resolvePlaceholders(value); + } + BeanExpressionResolver resolver = beanFactory.getBeanExpressionResolver(); + String resolved = beanFactory.resolveEmbeddedValue(value); + if (resolver == null) { + return resolved; + } + return String.valueOf(resolver.evaluate(resolved, new BeanExpressionContext(beanFactory, null))); } return value; } - private String getUrl(Map attributes) { - String url = resolve((String) attributes.get("url")); + private String getUrl(ConfigurableBeanFactory beanFactory, Map attributes) { + String url = resolve(beanFactory, (String) attributes.get("url")); return getUrl(url); } - private String getPath(Map attributes) { - String path = resolve((String) attributes.get("path")); + private String getPath(ConfigurableBeanFactory beanFactory, Map attributes) { + String path = resolve(beanFactory, (String) attributes.get("path")); return getPath(path); } diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientUsingConfigurerTest.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientUsingConfigurerTest.java index 060ce120..e342a1b5 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientUsingConfigurerTest.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientUsingConfigurerTest.java @@ -26,10 +26,10 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.cloud.openfeign.clientconfig.FeignClientConfigurer; -import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.test.annotation.DirtiesContext; @@ -49,18 +49,19 @@ import static org.assertj.core.api.Assertions.assertThat; "feign.client.config.default.requestInterceptors[1]=org.springframework.cloud.openfeign.FeignClientUsingPropertiesTests.BarRequestInterceptor" }) public class FeignClientUsingConfigurerTest { - private static final String BEAN_NAME_PREFIX = "&org.springframework.cloud.openfeign.FeignClientUsingConfigurerTest$"; + private static final String BEAN_NAME_PREFIX = "org.springframework.cloud.openfeign.FeignClientUsingConfigurerTest$"; @Autowired - private ApplicationContext applicationContext; + private ConfigurableListableBeanFactory beanFactory; @Autowired private FeignContext context; @Test public void testFeignClient() { - FeignClientFactoryBean factoryBean = (FeignClientFactoryBean) applicationContext - .getBean(BEAN_NAME_PREFIX + "TestFeignClient"); + FeignClientFactoryBean factoryBean = (FeignClientFactoryBean) beanFactory + .getBeanDefinition(BEAN_NAME_PREFIX + "TestFeignClient") + .getAttribute("feignClientsRegistrarFactoryBean"); Feign.Builder builder = factoryBean.feign(context); List interceptors = (List) getBuilderValue(builder, "requestInterceptors"); @@ -77,8 +78,9 @@ public class FeignClientUsingConfigurerTest { @Test public void testNoInheritFeignClient() { - FeignClientFactoryBean factoryBean = (FeignClientFactoryBean) applicationContext - .getBean(BEAN_NAME_PREFIX + "NoInheritFeignClient"); + FeignClientFactoryBean factoryBean = (FeignClientFactoryBean) beanFactory + .getBeanDefinition(BEAN_NAME_PREFIX + "NoInheritFeignClient") + .getAttribute("feignClientsRegistrarFactoryBean"); Feign.Builder builder = factoryBean.feign(context); List interceptors = (List) getBuilderValue(builder, "requestInterceptors"); @@ -89,8 +91,9 @@ public class FeignClientUsingConfigurerTest { @Test public void testNoInheritFeignClient_ignoreProperties() { - FeignClientFactoryBean factoryBean = (FeignClientFactoryBean) applicationContext - .getBean(BEAN_NAME_PREFIX + "NoInheritFeignClient"); + FeignClientFactoryBean factoryBean = (FeignClientFactoryBean) beanFactory + .getBeanDefinition(BEAN_NAME_PREFIX + "NoInheritFeignClient") + .getAttribute("feignClientsRegistrarFactoryBean"); Feign.Builder builder = factoryBean.feign(context); assertThat(getBuilderValue(builder, "logLevel")).as("log level not set").isEqualTo(Logger.Level.HEADERS);