Skip to content

Commit

Permalink
Lazy openfeign bean registration (#455)
Browse files Browse the repository at this point in the history
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.
  • Loading branch information
marcingrzejszczak committed Jan 13, 2021
1 parent 69a03ff commit 01f101a
Show file tree
Hide file tree
Showing 3 changed files with 83 additions and 34 deletions.
Expand Up @@ -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;
Expand All @@ -58,7 +60,8 @@
* @author Olga Maciaszek-Sharma
* @author Ilia Ilinykh
*/
public class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
public class FeignClientFactoryBean
implements FactoryBean<Object>, InitializingBean, ApplicationContextAware, BeanFactoryAware {

/***********************************
* WARNING! Nothing in this class should be @Autowired. It causes NPEs because of some
Expand All @@ -81,6 +84,8 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing

private ApplicationContext applicationContext;

private BeanFactory beanFactory;

private Class<?> fallback = void.class;

private Class<?> fallbackFactory = void.class;
Expand Down Expand Up @@ -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());
Expand Down Expand Up @@ -261,7 +267,7 @@ protected void configureUsingProperties(FeignClientProperties.FeignClientConfigu

private <T> T getOrInstantiate(Class<T> tClass) {
try {
return applicationContext.getBean(tClass);
return beanFactory != null ? beanFactory.getBean(tClass) : applicationContext.getBean(tClass);
}
catch (NoSuchBeanDefinitionException e) {
return BeanUtils.instantiateClass(tClass);
Expand Down Expand Up @@ -321,7 +327,8 @@ public Object getObject() {
* information
*/
<T> 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)) {
Expand Down Expand Up @@ -437,6 +444,7 @@ public ApplicationContext getApplicationContext() {
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
this.applicationContext = context;
this.beanFactory = context;
}

public Class<?> getFallback() {
Expand Down Expand Up @@ -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);
Expand All @@ -486,4 +495,9 @@ public String toString() {
.append(fallbackFactory).append("}").toString();
}

@Override
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
this.beanFactory = beanFactory;
}

}
Expand Up @@ -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;
Expand Down Expand Up @@ -194,23 +197,40 @@ public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegi
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata,
Map<String, Object> 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");
Expand All @@ -235,41 +255,53 @@ private void validate(Map<String, Object> attributes) {
}

/* for testing */ String getName(Map<String, Object> attributes) {
return getName(null, attributes);
}

String getName(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {
String name = (String) attributes.get("serviceId");
if (!StringUtils.hasText(name)) {
name = (String) attributes.get("name");
}
if (!StringUtils.hasText(name)) {
name = (String) attributes.get("value");
}
name = resolve(name);
name = resolve(beanFactory, name);
return getName(name);
}

private String getContextId(Map<String, Object> attributes) {
private String getContextId(ConfigurableBeanFactory beanFactory, Map<String, Object> 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<String, Object> attributes) {
String url = resolve((String) attributes.get("url"));
private String getUrl(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {
String url = resolve(beanFactory, (String) attributes.get("url"));
return getUrl(url);
}

private String getPath(Map<String, Object> attributes) {
String path = resolve((String) attributes.get("path"));
private String getPath(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {
String path = resolve(beanFactory, (String) attributes.get("path"));
return getPath(path);
}

Expand Down
Expand Up @@ -26,10 +26,10 @@
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;
Expand All @@ -49,18 +49,19 @@
"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<RequestInterceptor> interceptors = (List) getBuilderValue(builder, "requestInterceptors");
Expand All @@ -77,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<RequestInterceptor> interceptors = (List) getBuilderValue(builder, "requestInterceptors");
Expand All @@ -89,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);
Expand Down

0 comments on commit 01f101a

Please sign in to comment.