From 7ef5c0ce96f323c1edddf4d23899fc201a9b098d 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. Fixes gh-441. # Conflicts: # spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientFactoryBean.java # spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientsRegistrar.java --- .../openfeign/FeignClientFactoryBean.java | 37 ++++++--- .../openfeign/FeignClientsRegistrar.java | 83 ++++++++++++++----- .../FeignClientUsingConfigurerTest.java | 21 +++-- 3 files changed, 101 insertions(+), 40 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 7d729072..cff06615 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,9 +60,10 @@ import org.springframework.util.StringUtils; * @author Matt King * @author Olga Maciaszek-Sharma * @author Ilia Ilinykh + * @author Marcin Grzejszczak */ -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 @@ -83,6 +86,8 @@ class FeignClientFactoryBean private ApplicationContext applicationContext; + private BeanFactory beanFactory; + private Class fallback = void.class; private Class fallbackFactory = void.class; @@ -129,8 +134,9 @@ class FeignClientFactoryBean } 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); @@ -283,7 +289,8 @@ class FeignClientFactoryBean 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); @@ -336,7 +343,7 @@ class FeignClientFactoryBean } @Override - public Object getObject() throws Exception { + public Object getObject() { return getTarget(); } @@ -346,7 +353,9 @@ class FeignClientFactoryBean * information */ T getTarget() { - FeignContext context = applicationContext.getBean(FeignContext.class); + FeignContext context = beanFactory != null + ? beanFactory.getBean(FeignContext.class) + : applicationContext.getBean(FeignContext.class); Feign.Builder builder = feign(context); if (!StringUtils.hasText(url)) { @@ -468,7 +477,8 @@ class FeignClientFactoryBean @Override public void setApplicationContext(ApplicationContext context) throws BeansException { - this.applicationContext = context; + applicationContext = context; + beanFactory = context; } public Class getFallback() { @@ -497,6 +507,7 @@ class FeignClientFactoryBean } FeignClientFactoryBean that = (FeignClientFactoryBean) o; return Objects.equals(applicationContext, that.applicationContext) + && Objects.equals(beanFactory, that.beanFactory) && decode404 == that.decode404 && inheritParentContext == that.inheritParentContext && Objects.equals(fallback, that.fallback) @@ -507,8 +518,8 @@ class FeignClientFactoryBean @Override public int hashCode() { - return Objects.hash(applicationContext, decode404, inheritParentContext, fallback, - fallbackFactory, name, path, type, url); + return Objects.hash(applicationContext, beanFactory, decode404, + inheritParentContext, fallback, fallbackFactory, name, path, type, url); } @Override @@ -519,9 +530,15 @@ class FeignClientFactoryBean .append("decode404=").append(decode404).append(", ") .append("inheritParentContext=").append(inheritParentContext).append(", ") .append("applicationContext=").append(applicationContext).append(", ") + .append("beanFactory=").append(beanFactory).append(", ") .append("fallback=").append(fallback).append(", ") .append("fallbackFactory=").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 1b09c59e..fc88d077 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; @@ -53,6 +56,7 @@ import org.springframework.util.StringUtils; * @author Venil Noronha * @author Gang Li * @author Michal Domagala + * @author Marcin Grzejszczak */ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware { @@ -204,24 +208,45 @@ class FeignClientsRegistrar 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"); @@ -247,6 +272,10 @@ class FeignClientsRegistrar } /* 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"); @@ -254,34 +283,46 @@ class FeignClientsRegistrar 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 5b82ddf0..34ed5cfd 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, @@ -79,8 +80,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, @@ -93,8 +95,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")