From be8646adc2a179f70c0fb4f7c3ba62965bf6514b Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Tue, 12 Jan 2021 14:17:30 +0100 Subject: [PATCH 1/2] Lazy openfeign bean registration 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. problems: - one of the tests uses the factory bean - there's no more factory bean since the instance supplier for the bean registration acts as one. --- .../openfeign/FeignClientFactoryBean.java | 24 +++++-- .../openfeign/FeignClientsRegistrar.java | 71 +++++++++++++------ .../FeignClientUsingConfigurerTest.java | 4 +- 3 files changed, 73 insertions(+), 26 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 50d46d844..d02aebed1 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 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 @@ * @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 @@ private void applyBuildCustomizers(FeignContext context, Feign.Builder builder) } 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 @@ protected void configureUsingProperties(FeignClientProperties.FeignClientConfigu 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 Object getObject() { * 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 ApplicationContext getApplicationContext() { @Override public void setApplicationContext(ApplicationContext context) throws BeansException { this.applicationContext = context; + this.beanFactory = context; } public Class getFallback() { @@ -464,7 +472,8 @@ public boolean equals(Object o) { 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 String toString() { .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 11a545f76..22139d0a4 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.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,19 +197,35 @@ public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegi 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")); + BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> { + FeignClientFactoryBean factoryBean = new FeignClientFactoryBean(); + factoryBean.setBeanFactory(beanFactory); + factoryBean.setUrl(getUrl(beanFactory, attributes)); + factoryBean.setPath(getPath(beanFactory, attributes)); + factoryBean.setName(name); + factoryBean.setContextId(contextId); + factoryBean.setType(clazz); + 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(); @@ -235,6 +254,10 @@ private void validate(Map attributes) { } /* 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 +265,42 @@ private void validate(Map attributes) { 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 060ce1204..f5ed0b155 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 @@ -22,6 +22,7 @@ import feign.Feign; import feign.Logger; import feign.RequestInterceptor; +import org.junit.Ignore; import org.junit.Test; import org.junit.runner.RunWith; @@ -47,9 +48,10 @@ "feign.client.config.default.loggerLevel=full", "feign.client.config.default.requestInterceptors[0]=org.springframework.cloud.openfeign.FeignClientUsingPropertiesTests.FooRequestInterceptor", "feign.client.config.default.requestInterceptors[1]=org.springframework.cloud.openfeign.FeignClientUsingPropertiesTests.BarRequestInterceptor" }) +@Ignore("I don't know how to test this since the factory bean is no longer created") 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; From 624ad040aaad8dfd4be8742c4076c1b98785d53a Mon Sep 17 00:00:00 2001 From: Marcin Grzejszczak Date: Wed, 13 Jan 2021 10:02:57 +0100 Subject: [PATCH 2/2] Added tests --- .../openfeign/FeignClientsRegistrar.java | 11 +++++----- .../FeignClientUsingConfigurerTest.java | 21 ++++++++++--------- 2 files changed, 17 insertions(+), 15 deletions(-) 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 22139d0a4..cd9042658 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 @@ -202,14 +202,14 @@ private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMeta ? (ConfigurableBeanFactory) registry : null; String contextId = getContextId(beanFactory, attributes); String name = getName(attributes); + FeignClientFactoryBean factoryBean = new FeignClientFactoryBean(); + factoryBean.setBeanFactory(beanFactory); + factoryBean.setName(name); + factoryBean.setContextId(contextId); + factoryBean.setType(clazz); BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> { - FeignClientFactoryBean factoryBean = new FeignClientFactoryBean(); - factoryBean.setBeanFactory(beanFactory); factoryBean.setUrl(getUrl(beanFactory, attributes)); factoryBean.setPath(getPath(beanFactory, attributes)); - factoryBean.setName(name); - factoryBean.setContextId(contextId); - factoryBean.setType(clazz); factoryBean.setDecode404(Boolean.parseBoolean(String.valueOf(attributes.get("decode404")))); Object fallback = attributes.get("fallback"); if (fallback != null) { @@ -230,6 +230,7 @@ private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMeta 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"); 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 f5ed0b155..e342a1b5a 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 @@ -22,15 +22,14 @@ import feign.Feign; import feign.Logger; import feign.RequestInterceptor; -import org.junit.Ignore; 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; @@ -48,21 +47,21 @@ "feign.client.config.default.loggerLevel=full", "feign.client.config.default.requestInterceptors[0]=org.springframework.cloud.openfeign.FeignClientUsingPropertiesTests.FooRequestInterceptor", "feign.client.config.default.requestInterceptors[1]=org.springframework.cloud.openfeign.FeignClientUsingPropertiesTests.BarRequestInterceptor" }) -@Ignore("I don't know how to test this since the factory bean is no longer created") public class 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"); @@ -79,8 +78,9 @@ private Object getBuilderValue(Feign.Builder builder, String member) { @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"); @@ -91,8 +91,9 @@ public void testNoInheritFeignClient() { @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);