diff --git a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc index a1e7ac4ba..e96b32591 100644 --- a/docs/src/main/asciidoc/spring-cloud-openfeign.adoc +++ b/docs/src/main/asciidoc/spring-cloud-openfeign.adoc @@ -78,6 +78,15 @@ TIP: To use `@EnableFeignClients` annotation on `@Configuration`-annotated-class or list them explicitly: `@EnableFeignClients(clients = InventoryServiceFeignClient.class)` +[[attribute-resolution-mode]] +==== Attribute resolution mode + +While creating `Feign` client beans, we resolve the values passed via the `@FeignClient` annotation. As of `4.x`, the values are being resolved eagerly. This is a good solution for most use-cases, and it also allows for AOT support. + +If you need the attributes to be resolved lazily, set the `spring.cloud.openfeign.lazy-attributes-resolution` property value to `true`. + +TIP: For Spring Cloud Contract test integration, lazy attribute resolution should be used. + [[spring-cloud-feign-overriding-defaults]] === Overriding Feign Defaults @@ -905,6 +914,17 @@ The URL provided in the configuration properties remains unused. |=== +=== AOT and Native Image Support + +Spring Cloud OpenFeign supports Spring AOT transformations and native images, however, only with refresh mode disabled, Feign clients refresh disabled (default setting) and <> disabled (default setting). + +WARNING: If you want to run Spring Cloud OpenFeign clients in AOT or native image modes, make sure to set `spring.cloud.refresh.enabled` to `false`. + +TIP: If you want to run Spring Cloud OpenFeign clients in AOT or native image modes, ensure `spring.cloud.openfeign.client.refresh-enabled` has not been set to `true`. + +TIP: If you want to run Spring Cloud OpenFeign clients in AOT or native image modes, ensure `spring.cloud.openfeign.lazy-attributes-resolution` has not been set to `true`. + + == Configuration properties To see the list of all Spring Cloud OpenFeign related configuration properties please check link:appendix.html[the Appendix page]. diff --git a/pom.xml b/pom.xml index 8ec63f351..cc6990002 100644 --- a/pom.xml +++ b/pom.xml @@ -228,7 +228,7 @@ maven-surefire-plugin - ${surefireArgLine} + ${surefireArgLine} --add-opens=java.base/java.net=ALL-UNNAMED diff --git a/spring-cloud-openfeign-core/pom.xml b/spring-cloud-openfeign-core/pom.xml index 8ccb15ab0..ad35b00e6 100644 --- a/spring-cloud-openfeign-core/pom.xml +++ b/spring-cloud-openfeign-core/pom.xml @@ -191,6 +191,11 @@ 2.11.0 test + + org.springframework + spring-core-test + test + org.springframework.security spring-security-oauth2-client diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/DefaultTargeter.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/DefaultTargeter.java index f7789874a..29d9c2154 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/DefaultTargeter.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/DefaultTargeter.java @@ -25,7 +25,7 @@ class DefaultTargeter implements Targeter { @Override - public T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context, + public T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignClientFactory context, Target.HardCodedTarget target) { return feign.target(target); } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java index 820d539f4..cf1906aeb 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java @@ -44,6 +44,10 @@ import org.apache.commons.logging.Log; import org.apache.commons.logging.LogFactory; +import org.springframework.aot.hint.MemberCategory; +import org.springframework.aot.hint.RuntimeHints; +import org.springframework.aot.hint.RuntimeHintsRegistrar; +import org.springframework.aot.hint.TypeReference; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; @@ -55,6 +59,8 @@ import org.springframework.cloud.client.actuator.HasFeatures; import org.springframework.cloud.client.circuitbreaker.CircuitBreaker; import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory; +import org.springframework.cloud.openfeign.aot.FeignChildContextInitializer; +import org.springframework.cloud.openfeign.aot.FeignClientBeanFactoryInitializationAotProcessor; import org.springframework.cloud.openfeign.security.OAuth2AccessTokenInterceptor; import org.springframework.cloud.openfeign.support.FeignEncoderProperties; import org.springframework.cloud.openfeign.support.FeignHttpClientProperties; @@ -64,12 +70,14 @@ import org.springframework.context.annotation.Conditional; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; +import org.springframework.context.support.GenericApplicationContext; import org.springframework.data.domain.Page; import org.springframework.data.domain.Sort; import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager; import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; +import org.springframework.util.ClassUtils; /** * @author Spencer Gibb @@ -102,12 +110,24 @@ public HasFeatures feignFeature() { } @Bean - public FeignContext feignContext() { - FeignContext context = new FeignContext(); + public FeignClientFactory feignContext() { + FeignClientFactory context = new FeignClientFactory(); context.setConfigurations(this.configurations); return context; } + @Bean + static FeignChildContextInitializer feignChildContextInitializer(GenericApplicationContext parentContext, + FeignClientFactory feignClientFactory) { + return new FeignChildContextInitializer(parentContext, feignClientFactory); + } + + @Bean + static FeignClientBeanFactoryInitializationAotProcessor feignClientBeanFactoryInitializationCodeGenerator( + GenericApplicationContext applicationContext, FeignClientFactory feignClientFactory) { + return new FeignClientBeanFactoryInitializationAotProcessor(applicationContext, feignClientFactory); + } + @Bean @ConditionalOnProperty(value = "spring.cloud.openfeign.cache.enabled", matchIfMissing = true) @ConditionalOnBean(CacheInterceptor.class) @@ -358,3 +378,17 @@ public OAuth2AccessTokenInterceptor defaultOAuth2AccessTokenInterceptor( } } + +class FeignHints implements RuntimeHintsRegistrar { + + @Override + public void registerHints(RuntimeHints hints, ClassLoader classLoader) { + if (!ClassUtils.isPresent("feign.Feign", classLoader)) { + return; + } + hints.reflection().registerType(TypeReference.of(FeignClientFactoryBean.class), + hint -> hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, + MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.DECLARED_FIELDS)); + } + +} diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignCircuitBreakerTargeter.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignCircuitBreakerTargeter.java index 0c190658b..047ebb0d2 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignCircuitBreakerTargeter.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignCircuitBreakerTargeter.java @@ -39,7 +39,7 @@ class FeignCircuitBreakerTargeter implements Targeter { } @Override - public T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context, + public T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignClientFactory context, Target.HardCodedTarget target) { if (!(feign instanceof FeignCircuitBreaker.Builder builder)) { return feign.target(target); @@ -56,20 +56,20 @@ public T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignCo return builder(name, builder).target(target); } - private T targetWithFallbackFactory(String feignClientName, FeignContext context, + private T targetWithFallbackFactory(String feignClientName, FeignClientFactory context, Target.HardCodedTarget target, FeignCircuitBreaker.Builder builder, Class fallbackFactoryClass) { FallbackFactory fallbackFactory = (FallbackFactory) getFromContext("fallbackFactory", feignClientName, context, fallbackFactoryClass, FallbackFactory.class); return builder(feignClientName, builder).target(target, fallbackFactory); } - private T targetWithFallback(String feignClientName, FeignContext context, Target.HardCodedTarget target, - FeignCircuitBreaker.Builder builder, Class fallback) { + private T targetWithFallback(String feignClientName, FeignClientFactory context, + Target.HardCodedTarget target, FeignCircuitBreaker.Builder builder, Class fallback) { T fallbackInstance = getFromContext("fallback", feignClientName, context, fallback, target.type()); return builder(feignClientName, builder).target(target, fallbackInstance); } - private T getFromContext(String fallbackMechanism, String feignClientName, FeignContext context, + private T getFromContext(String fallbackMechanism, String feignClientName, FeignClientFactory context, Class beanType, Class targetType) { Object fallbackInstance = context.getInstance(feignClientName, beanType); if (fallbackInstance == null) { diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignContext.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientFactory.java similarity index 61% rename from spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignContext.java rename to spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientFactory.java index 39bdef32f..2ef8ba662 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignContext.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientFactory.java @@ -16,11 +16,14 @@ package org.springframework.cloud.openfeign; +import java.util.HashMap; import java.util.Map; import org.springframework.beans.BeansException; import org.springframework.beans.factory.BeanFactoryUtils; import org.springframework.cloud.context.named.NamedContextFactory; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.support.GenericApplicationContext; import org.springframework.lang.Nullable; /** @@ -31,11 +34,18 @@ * @author Dave Syer * @author Matt King * @author Jasbir Singh + * @author Olga Maciaszek-Sharma */ -public class FeignContext extends NamedContextFactory { +public class FeignClientFactory extends NamedContextFactory { - public FeignContext() { - super(FeignClientsConfiguration.class, "spring.cloud.openfeign", "spring.cloud.openfeign.client.name"); + public FeignClientFactory() { + this(new HashMap<>()); + } + + public FeignClientFactory( + Map> applicationContextInitializers) { + super(FeignClientsConfiguration.class, "spring.cloud.openfeign", "spring.cloud.openfeign.client.name", + applicationContextInitializers); } @Nullable @@ -57,4 +67,14 @@ public T getInstance(String contextName, String beanName, Class type) { return getContext(contextName).getBean(beanName, type); } + @SuppressWarnings("unchecked") + public FeignClientFactory withApplicationContextInitializers(Map applicationContextInitializers) { + Map> convertedInitializers = new HashMap<>(); + applicationContextInitializers.keySet() + .forEach(contextId -> convertedInitializers.put(contextId, + (ApplicationContextInitializer) applicationContextInitializers + .get(contextId))); + return new FeignClientFactory(convertedInitializers); + } + } 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 8677283b1..6ef2aec67 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 @@ -113,13 +113,22 @@ public class FeignClientFactoryBean private final List additionalCustomizers = new ArrayList<>(); + private String[] qualifiers = new String[] {}; + + // For AOT testing + public FeignClientFactoryBean() { + if (LOG.isDebugEnabled()) { + LOG.debug("Creating a FeignClientFactoryBean."); + } + } + @Override public void afterPropertiesSet() { Assert.hasText(contextId, "Context id must be set"); Assert.hasText(name, "Name must be set"); } - protected Feign.Builder feign(FeignContext context) { + protected Feign.Builder feign(FeignClientFactory context) { FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class); Logger logger = loggerFactory.create(type); @@ -137,7 +146,7 @@ protected Feign.Builder feign(FeignContext context) { return builder; } - private void applyBuildCustomizers(FeignContext context, Feign.Builder builder) { + private void applyBuildCustomizers(FeignClientFactory context, Feign.Builder builder) { Map customizerMap = context.getInstances(contextId, FeignBuilderCustomizer.class); @@ -148,7 +157,7 @@ private void applyBuildCustomizers(FeignContext context, Feign.Builder builder) additionalCustomizers.forEach(customizer -> customizer.customize(builder)); } - protected void configureFeign(FeignContext context, Feign.Builder builder) { + protected void configureFeign(FeignClientFactory context, Feign.Builder builder) { FeignClientProperties properties = beanFactory != null ? beanFactory.getBean(FeignClientProperties.class) : applicationContext.getBean(FeignClientProperties.class); @@ -172,7 +181,7 @@ protected void configureFeign(FeignContext context, Feign.Builder builder) { } } - protected void configureUsingConfiguration(FeignContext context, Feign.Builder builder) { + protected void configureUsingConfiguration(FeignClientFactory context, Feign.Builder builder) { Logger.Level level = getInheritedAwareOptional(context, Logger.Level.class); if (level != null) { builder.logLevel(level); @@ -340,7 +349,7 @@ private T getOrInstantiate(Class tClass) { } } - protected T get(FeignContext context, Class type) { + protected T get(FeignClientFactory context, Class type) { T instance = context.getInstance(contextId, type); if (instance == null) { throw new IllegalStateException("No bean found of type " + type + " for " + contextId); @@ -348,11 +357,11 @@ protected T get(FeignContext context, Class type) { return instance; } - protected T getOptional(FeignContext context, Class type) { + protected T getOptional(FeignClientFactory context, Class type) { return context.getInstance(contextId, type); } - protected T getInheritedAwareOptional(FeignContext context, Class type) { + protected T getInheritedAwareOptional(FeignClientFactory context, Class type) { if (inheritParentContext) { return getOptional(context, type); } @@ -361,7 +370,7 @@ protected T getInheritedAwareOptional(FeignContext context, Class type) { } } - protected Map getInheritedAwareInstances(FeignContext context, Class type) { + protected Map getInheritedAwareInstances(FeignClientFactory context, Class type) { if (inheritParentContext) { return context.getInstances(contextId, type); } @@ -370,7 +379,7 @@ protected Map getInheritedAwareInstances(FeignContext context, Cl } } - protected T loadBalance(Feign.Builder builder, FeignContext context, HardCodedTarget target) { + protected T loadBalance(Feign.Builder builder, FeignClientFactory context, HardCodedTarget target) { Client client = getOptional(context, Client.class); if (client != null) { builder.client(client); @@ -389,7 +398,7 @@ protected T loadBalance(Feign.Builder builder, FeignContext context, HardCod * @param contextId name of feign client * @return returns Options found in context */ - protected Request.Options getOptionsByName(FeignContext context, String contextId) { + protected Request.Options getOptionsByName(FeignClientFactory context, String contextId) { if (refreshableClient) { return context.getInstance(contextId, Request.Options.class.getCanonicalName() + "-" + contextId, Request.Options.class); @@ -409,9 +418,9 @@ public Object getObject() { */ @SuppressWarnings("unchecked") T getTarget() { - FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class) - : applicationContext.getBean(FeignContext.class); - Feign.Builder builder = feign(context); + FeignClientFactory feignClientFactory = beanFactory != null ? beanFactory.getBean(FeignClientFactory.class) + : applicationContext.getBean(FeignClientFactory.class); + Feign.Builder builder = feign(feignClientFactory); if (!StringUtils.hasText(url) && !isUrlAvailableInConfig(contextId)) { if (LOG.isInfoEnabled()) { @@ -424,13 +433,13 @@ T getTarget() { url = name; } url += cleanPath(); - return (T) loadBalance(builder, context, new HardCodedTarget<>(type, name, url)); + return (T) loadBalance(builder, feignClientFactory, new HardCodedTarget<>(type, name, url)); } if (StringUtils.hasText(url) && !url.startsWith("http")) { url = "http://" + url; } String url = this.url + cleanPath(); - Client client = getOptional(context, Client.class); + Client client = getOptional(feignClientFactory, Client.class); if (client != null) { if (client instanceof FeignBlockingLoadBalancerClient) { // not load balancing because we have a url, @@ -445,10 +454,10 @@ T getTarget() { builder.client(client); } - applyBuildCustomizers(context, builder); + applyBuildCustomizers(feignClientFactory, builder); - Targeter targeter = get(context, Targeter.class); - return targeter.target(this, builder, context, resolveTarget(context, contextId, url)); + Targeter targeter = get(feignClientFactory, Targeter.class); + return targeter.target(this, builder, feignClientFactory, resolveTarget(feignClientFactory, contextId, url)); } private String cleanPath() { @@ -468,7 +477,7 @@ private String cleanPath() { } @SuppressWarnings({ "unchecked", "rawtypes" }) - private HardCodedTarget resolveTarget(FeignContext context, String contextId, String url) { + private HardCodedTarget resolveTarget(FeignClientFactory context, String contextId, String url) { if (StringUtils.hasText(url)) { return new HardCodedTarget(type, name, url); } @@ -600,6 +609,14 @@ public void setRefreshableClient(boolean refreshableClient) { this.refreshableClient = refreshableClient; } + public String[] getQualifiers() { + return qualifiers; + } + + public void setQualifiers(String[] qualifiers) { + this.qualifiers = qualifiers; + } + @Override public boolean equals(Object o) { if (this == o) { diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientSpecification.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientSpecification.java index 9477d568f..345ad8b01 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientSpecification.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientSpecification.java @@ -24,18 +24,22 @@ /** * @author Dave Syer * @author Gregor Zurowski + * @author Olga Maciaszek-Sharma */ public class FeignClientSpecification implements NamedContextFactory.Specification { private String name; + private String className; + private Class[] configuration; - FeignClientSpecification() { + public FeignClientSpecification() { } - public FeignClientSpecification(String name, Class[] configuration) { + public FeignClientSpecification(String name, String className, Class[] configuration) { this.name = name; + this.className = className; this.configuration = configuration; } @@ -47,6 +51,14 @@ public void setName(String name) { this.name = name; } + public String getClassName() { + return className; + } + + public void setClassName(String className) { + this.className = className; + } + public Class[] getConfiguration() { return this.configuration; } @@ -60,22 +72,24 @@ public boolean equals(Object o) { if (this == o) { return true; } - if (o == null || getClass() != o.getClass()) { + if (!(o instanceof FeignClientSpecification that)) { return false; } - FeignClientSpecification that = (FeignClientSpecification) o; - return Objects.equals(name, that.name) && Arrays.equals(configuration, that.configuration); + return Objects.equals(name, that.name) && Objects.equals(className, that.className) + && Arrays.equals(configuration, that.configuration); } @Override public int hashCode() { - return Objects.hash(name, Arrays.hashCode(configuration)); + int result = Objects.hash(name, className); + result = 31 * result + Arrays.hashCode(configuration); + return result; } @Override public String toString() { - return new StringBuilder("FeignClientSpecification{").append("name='").append(name).append("', ") - .append("configuration=").append(Arrays.toString(configuration)).append("}").toString(); + return "FeignClientSpecification{" + "name='" + name + "', " + "className='" + className + "', " + + "configuration=" + Arrays.toString(configuration) + "}"; } } 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 f423c8016..d0cdde94f 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 @@ -71,7 +71,7 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware { // patterned after Spring Integration IntegrationComponentScanRegistrar - // and RibbonClientsConfigurationRegistgrar + // and RibbonClientsConfigurationRegistrar private ResourceLoader resourceLoader; @@ -162,12 +162,11 @@ private void registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefin else { name = "default." + metadata.getClassName(); } - registerClientConfiguration(registry, name, defaultAttrs.get("defaultConfiguration")); + registerClientConfiguration(registry, name, "default", defaultAttrs.get("defaultConfiguration")); } } public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { - LinkedHashSet candidateComponents = new LinkedHashSet<>(); Map attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName()); final Class[] clients = attrs == null ? null : (Class[]) attrs.get("clients"); @@ -196,20 +195,74 @@ public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegi .getAnnotationAttributes(FeignClient.class.getCanonicalName()); String name = getClientName(attributes); - registerClientConfiguration(registry, name, attributes.get("configuration")); + String className = annotationMetadata.getClassName(); + registerClientConfiguration(registry, name, className, attributes.get("configuration")); registerFeignClient(registry, annotationMetadata, attributes); } } } - @SuppressWarnings("unchecked") private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map attributes) { String className = annotationMetadata.getClassName(); - Class clazz = ClassUtils.resolveClassName(className, null); + if (String.valueOf(false).equals( + environment.getProperty("spring.cloud.openfeign.lazy-attributes-resolution", String.valueOf(false)))) { + eagerlyRegisterFeignClientBeanDefinition(className, attributes, registry); + } + else { + lazilyRegisterFeignClientBeanDefinition(className, attributes, registry); + } + } + + private void eagerlyRegisterFeignClientBeanDefinition(String className, Map attributes, + BeanDefinitionRegistry registry) { + validate(attributes); + BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class); + definition.addPropertyValue("url", getUrl(null, attributes)); + definition.addPropertyValue("path", getPath(null, attributes)); + String name = getName(attributes); + definition.addPropertyValue("name", name); + String contextId = getContextId(null, attributes); + definition.addPropertyValue("contextId", contextId); + definition.addPropertyValue("type", className); + definition.addPropertyValue("dismiss404", Boolean.parseBoolean(String.valueOf(attributes.get("dismiss404")))); + Object fallback = attributes.get("fallback"); + if (fallback != null) { + definition.addPropertyValue("fallback", + (fallback instanceof Class ? fallback : ClassUtils.resolveClassName(fallback.toString(), null))); + } + Object fallbackFactory = attributes.get("fallbackFactory"); + if (fallbackFactory != null) { + definition.addPropertyValue("fallbackFactory", fallbackFactory instanceof Class ? fallbackFactory + : ClassUtils.resolveClassName(fallbackFactory.toString(), null)); + } + definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory")); + definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); + definition.addPropertyValue("refreshableClient", isClientRefreshEnabled()); + String[] qualifiers = getQualifiers(attributes); + if (ObjectUtils.isEmpty(qualifiers)) { + qualifiers = new String[] { contextId + "FeignClient" }; + } + // This is done so that there's a way to retrieve qualifiers while generating AOT + // code + definition.addPropertyValue("qualifiers", qualifiers); + AbstractBeanDefinition beanDefinition = definition.getBeanDefinition(); + beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className); + // has a default, won't be null + boolean primary = (Boolean) attributes.get("primary"); + beanDefinition.setPrimary(primary); + BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers); + BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); + registerRefreshableBeanDefinition(registry, contextId, Request.Options.class, OptionsFactoryBean.class); + registerRefreshableBeanDefinition(registry, contextId, RefreshableUrl.class, RefreshableUrlFactoryBean.class); + } + + private void lazilyRegisterFeignClientBeanDefinition(String className, Map attributes, + BeanDefinitionRegistry registry) { ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory ? (ConfigurableBeanFactory) registry : null; + Class clazz = ClassUtils.resolveClassName(className, null); String contextId = getContextId(beanFactory, attributes); String name = getName(attributes); FeignClientFactoryBean factoryBean = new FeignClientFactoryBean(); @@ -407,9 +460,11 @@ private String getClientName(Map client) { "Either 'name' or 'value' must be provided in @" + FeignClient.class.getSimpleName()); } - private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) { + private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object className, + Object configuration) { BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FeignClientSpecification.class); builder.addConstructorArgValue(name); + builder.addConstructorArgValue(className); builder.addConstructorArgValue(configuration); registry.registerBeanDefinition(name + "." + FeignClientSpecification.class.getSimpleName(), builder.getBeanDefinition()); diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/Targeter.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/Targeter.java index 16aaa52de..40c1e4c1f 100644 --- a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/Targeter.java +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/Targeter.java @@ -24,7 +24,7 @@ */ public interface Targeter { - T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context, + T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignClientFactory context, Target.HardCodedTarget target); } diff --git a/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/aot/FeignChildContextInitializer.java b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/aot/FeignChildContextInitializer.java new file mode 100644 index 000000000..1d3059a08 --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/aot/FeignChildContextInitializer.java @@ -0,0 +1,126 @@ +/* + * Copyright 2022-2022 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.Collection; +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Set; +import java.util.stream.Collectors; + +import javax.lang.model.element.Modifier; + +import org.springframework.aot.generate.GeneratedMethod; +import org.springframework.aot.generate.GenerationContext; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.aot.BeanRegistrationAotContribution; +import org.springframework.beans.factory.aot.BeanRegistrationAotProcessor; +import org.springframework.beans.factory.aot.BeanRegistrationCode; +import org.springframework.beans.factory.support.RegisteredBean; +import org.springframework.cloud.openfeign.FeignClientFactory; +import org.springframework.cloud.openfeign.FeignClientSpecification; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.aot.ApplicationContextAotGenerator; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.javapoet.ClassName; +import org.springframework.util.Assert; + +/** + * A {@link BeanRegistrationAotProcessor} that creates an + * {@link BeanRegistrationAotContribution} for Feign child contexts. + * + * @author Olga Maciaszek-Sharma + * @since 4.0.0 + */ +public class FeignChildContextInitializer implements BeanRegistrationAotProcessor { + + private final ApplicationContext applicationContext; + + private final FeignClientFactory feignClientFactory; + + public FeignChildContextInitializer(ApplicationContext applicationContext, FeignClientFactory feignClientFactory) { + this.applicationContext = applicationContext; + this.feignClientFactory = feignClientFactory; + } + + @Override + public BeanRegistrationAotContribution processAheadOfTime(RegisteredBean registeredBean) { + Assert.isInstanceOf(ConfigurableApplicationContext.class, applicationContext); + ConfigurableApplicationContext context = ((ConfigurableApplicationContext) applicationContext); + BeanFactory applicationBeanFactory = context.getBeanFactory(); + if (!((registeredBean.getBeanClass().equals(FeignClientFactory.class)) + && registeredBean.getBeanFactory().equals(applicationBeanFactory))) { + return null; + } + Set contextIds = new HashSet<>(getContextIdsFromConfig()); + Map childContextAotContributions = contextIds.stream() + .map(contextId -> Map.entry(contextId, buildChildContext(contextId))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + return new AotContribution(childContextAotContributions); + } + + private GenericApplicationContext buildChildContext(String contextId) { + GenericApplicationContext childContext = feignClientFactory.buildContext(contextId); + feignClientFactory.registerBeans(contextId, childContext); + return childContext; + } + + private Collection getContextIdsFromConfig() { + Map configurations = feignClientFactory.getConfigurations(); + return configurations.keySet().stream().filter(key -> !key.startsWith("default.")).collect(Collectors.toSet()); + } + + private static class AotContribution implements BeanRegistrationAotContribution { + + private final Map childContexts; + + AotContribution(Map childContexts) { + this.childContexts = childContexts.entrySet().stream().filter(entry -> entry.getValue() != null) + .map(entry -> Map.entry(entry.getKey(), entry.getValue())) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + @Override + public void applyTo(GenerationContext generationContext, BeanRegistrationCode beanRegistrationCode) { + Map generatedInitializerClassNames = childContexts.entrySet().stream().map(entry -> { + String name = entry.getValue().getDisplayName(); + name = name.replaceAll("[-]", "_"); + GenerationContext childGenerationContext = generationContext.withName(name); + ClassName initializerClassName = new ApplicationContextAotGenerator() + .processAheadOfTime(entry.getValue(), childGenerationContext); + return Map.entry(entry.getKey(), initializerClassName); + }).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + GeneratedMethod postProcessorMethod = beanRegistrationCode.getMethods() + .add("addFeignChildContextInitializer", method -> { + method.addJavadoc("Use AOT child context management initialization") + .addModifiers(Modifier.PRIVATE, Modifier.STATIC) + .addParameter(RegisteredBean.class, "registeredBean") + .addParameter(FeignClientFactory.class, "instance").returns(FeignClientFactory.class) + .addStatement("$T initializers = new $T<>()", Map.class, HashMap.class); + generatedInitializerClassNames.keySet() + .forEach(contextId -> method.addStatement("initializers.put($S, new $L())", contextId, + generatedInitializerClassNames.get(contextId))); + method.addStatement("return instance.withApplicationContextInitializers(initializers)"); + }); + beanRegistrationCode.addInstancePostProcessor(postProcessorMethod.toMethodReference()); + } + + } + +} 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 new file mode 100644 index 000000000..3e599537f --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/aot/FeignClientBeanFactoryInitializationAotProcessor.java @@ -0,0 +1,182 @@ +/* + * Copyright 2022-2022 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.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +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.beans.MutablePropertyValues; +import org.springframework.beans.factory.BeanFactory; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotContribution; +import org.springframework.beans.factory.aot.BeanFactoryInitializationAotProcessor; +import org.springframework.beans.factory.aot.BeanFactoryInitializationCode; +import org.springframework.beans.factory.aot.BeanRegistrationExcludeFilter; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanDefinitionHolder; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.beans.factory.support.AbstractBeanDefinition; +import org.springframework.beans.factory.support.BeanDefinitionBuilder; +import org.springframework.beans.factory.support.BeanDefinitionReaderUtils; +import org.springframework.beans.factory.support.BeanDefinitionRegistry; +import org.springframework.beans.factory.support.DefaultListableBeanFactory; +import org.springframework.beans.factory.support.GenericBeanDefinition; +import org.springframework.beans.factory.support.RegisteredBean; +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.javapoet.MethodSpec; +import org.springframework.util.Assert; +import org.springframework.util.ClassUtils; + +/** + * A {@link BeanFactoryInitializationAotProcessor} that creates an + * {@link BeanFactoryInitializationAotContribution} that registers bean definitions and + * proxy hints for Feign client beans. + * + * @author Olga Maciaszek-Sharma + * @since 4.0.0 + */ +public class FeignClientBeanFactoryInitializationAotProcessor + implements BeanRegistrationExcludeFilter, BeanFactoryInitializationAotProcessor { + + private final GenericApplicationContext context; + + private final Map feignClientBeanDefinitions; + + public FeignClientBeanFactoryInitializationAotProcessor(GenericApplicationContext context, + FeignClientFactory feignClientFactory) { + this.context = context; + this.feignClientBeanDefinitions = getFeignClientBeanDefinitions(feignClientFactory); + } + + @Override + public boolean isExcludedFromAotProcessing(RegisteredBean registeredBean) { + return registeredBean.getBeanClass().equals(FeignClientFactoryBean.class) + || feignClientBeanDefinitions.containsKey(registeredBean.getBeanClass().getName()); + } + + private Map getFeignClientBeanDefinitions(FeignClientFactory feignClientFactory) { + Map configurations = feignClientFactory.getConfigurations(); + return configurations.values().stream().map(FeignClientSpecification::getClassName).filter(Objects::nonNull) + .filter(className -> !className.equals("default")) + .map(className -> Map.entry(className, context.getBeanDefinition(className))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + + @Override + public BeanFactoryInitializationAotContribution processAheadOfTime(ConfigurableListableBeanFactory beanFactory) { + BeanFactory applicationBeanFactory = context.getBeanFactory(); + if (feignClientBeanDefinitions.isEmpty() || !beanFactory.equals(applicationBeanFactory)) { + return null; + } + return new AotContribution(feignClientBeanDefinitions); + } + + private static final class AotContribution implements BeanFactoryInitializationAotContribution { + + private final Map feignClientBeanDefinitions; + + private AotContribution(Map feignClientBeanDefinitions) { + this.feignClientBeanDefinitions = feignClientBeanDefinitions; + } + + @Override + public void applyTo(GenerationContext generationContext, + BeanFactoryInitializationCode beanFactoryInitializationCode) { + ProxyHints proxyHints = generationContext.getRuntimeHints().proxies(); + Set feignClientRegistrationMethods = feignClientBeanDefinitions.values().stream() + .map(beanDefinition -> { + Assert.notNull(beanDefinition, "beanDefinition cannot be null"); + Assert.isInstanceOf(GenericBeanDefinition.class, beanDefinition); + GenericBeanDefinition registeredBeanDefinition = (GenericBeanDefinition) beanDefinition; + 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); + return beanFactoryInitializationCode.getMethods() + .add(buildMethodName(className), method -> generateFeignClientRegistrationMethod(method, + feignClientProperties, registeredBeanDefinition)) + .getName(); + }).collect(Collectors.toSet()); + MethodReference initializerMethod = beanFactoryInitializationCode.getMethods() + .add("initialize", method -> generateInitializerMethod(method, feignClientRegistrationMethods)) + .toMethodReference(); + beanFactoryInitializationCode.addInitializer(initializerMethod); + } + + private String buildMethodName(String clientName) { + return "register" + clientName + "FeignClient"; + } + + private void generateInitializerMethod(MethodSpec.Builder method, Set feignClientRegistrationMethods) { + method.addModifiers(Modifier.PUBLIC); + method.addParameter(DefaultListableBeanFactory.class, "registry"); + feignClientRegistrationMethods.forEach(feignClientRegistrationMethod -> method.addStatement("$N(registry)", + feignClientRegistrationMethod)); + } + + private void generateFeignClientRegistrationMethod(MethodSpec.Builder method, + MutablePropertyValues feignClientPropertyValues, GenericBeanDefinition registeredBeanDefinition) { + Object feignQualifiers = feignClientPropertyValues.get("qualifiers"); + Assert.notNull(feignQualifiers, "Feign qualifiers cannot be null"); + String qualifiers = "{\"" + String.join("\",\"", (String[]) feignQualifiers) + "\"}"; + method.addJavadoc("register Feign Client: $L", feignClientPropertyValues.get("type")) + .addModifiers(Modifier.PUBLIC, Modifier.STATIC) + .addParameter(BeanDefinitionRegistry.class, "registry") + .addStatement("Class clazz = $T.resolveClassName(\"$L\", null)", ClassUtils.class, + feignClientPropertyValues.get("type")) + .addStatement("$T definition = $T.genericBeanDefinition($T.class)", BeanDefinitionBuilder.class, + BeanDefinitionBuilder.class, FeignClientFactoryBean.class) + .addStatement("definition.addPropertyValue(\"name\",\"$L\")", feignClientPropertyValues.get("name")) + .addStatement("definition.addPropertyValue(\"contextId\", \"$L\")", + feignClientPropertyValues.get("contextId")) + .addStatement("definition.addPropertyValue(\"type\", clazz)") + .addStatement("definition.addPropertyValue(\"url\", \"$L\")", feignClientPropertyValues.get("url")) + .addStatement("definition.addPropertyValue(\"path\", \"$L\")", + feignClientPropertyValues.get("path")) + .addStatement("definition.addPropertyValue(\"dismiss404\", $L)", + feignClientPropertyValues.get("dismiss404")) + .addStatement("definition.addPropertyValue(\"fallback\", $T.class)", + feignClientPropertyValues.get("fallback")) + .addStatement("definition.addPropertyValue(\"fallbackFactory\", $T.class)", + feignClientPropertyValues.get("fallbackFactory")) + .addStatement("definition.setAutowireMode($L)", registeredBeanDefinition.getAutowireMode()) + .addStatement("definition.setLazyInit($L)", + registeredBeanDefinition.getLazyInit() != null ? registeredBeanDefinition.getLazyInit() + : false) + .addStatement("$T beanDefinition = definition.getBeanDefinition()", AbstractBeanDefinition.class) + .addStatement("beanDefinition.setAttribute(\"$L\", clazz)", FactoryBean.OBJECT_TYPE_ATTRIBUTE) + .addStatement("beanDefinition.setPrimary($L)", registeredBeanDefinition.isPrimary()) + .addStatement("$T holder = new $T(beanDefinition, \"$L\", new String[]$L)", + BeanDefinitionHolder.class, BeanDefinitionHolder.class, + feignClientPropertyValues.get("type"), qualifiers) + .addStatement("$T.registerBeanDefinition(holder, registry) ", BeanDefinitionReaderUtils.class); + } + + } + +} diff --git a/spring-cloud-openfeign-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json b/spring-cloud-openfeign-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json index 8e90d1bd4..6b9c671d1 100644 --- a/spring-cloud-openfeign-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json +++ b/spring-cloud-openfeign-core/src/main/resources/META-INF/additional-spring-configuration-metadata.json @@ -79,6 +79,12 @@ "type": "java.lang.String", "description": "Provides a clientId to be used with OAuth2.", "defaultValue": "" + }, + { + "name": "spring.cloud.openfeign.lazy-attributes-resolution", + "type": "java.lang.Boolean", + "description": "Switches @FeignClient attributes resolution mode to lazy.", + "defaultValue": "false" } ] } diff --git a/spring-cloud-openfeign-core/src/main/resources/META-INF/spring/aot.factories b/spring-cloud-openfeign-core/src/main/resources/META-INF/spring/aot.factories new file mode 100644 index 000000000..9662b382f --- /dev/null +++ b/spring-cloud-openfeign-core/src/main/resources/META-INF/spring/aot.factories @@ -0,0 +1,2 @@ +org.springframework.aot.hint.RuntimeHintsRegistrar=\ +org.springframework.cloud.openfeign.FeignHints diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/EagerInitFeignClientUsingConfigurerTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/EagerInitFeignClientUsingConfigurerTests.java new file mode 100644 index 000000000..845381088 --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/EagerInitFeignClientUsingConfigurerTests.java @@ -0,0 +1,169 @@ +/* + * Copyright 2013-2022 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; + +import java.lang.reflect.Field; +import java.util.List; + +import feign.Capability; +import feign.Feign; +import feign.Logger; +import feign.RequestInterceptor; +import feign.micrometer.MicrometerObservationCapability; +import org.junit.jupiter.api.Test; + +import org.springframework.beans.factory.annotation.Autowired; +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; +import org.springframework.util.ReflectionUtils; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * @author matt king + * @author Jonatan Ivanov + * @author Olga Maciaszek-Sharma + */ +@DirtiesContext +@SpringBootTest(classes = EagerInitFeignClientUsingConfigurerTests.Application.class, value = { + "spring.cloud.openfeign.client.config.default.loggerLevel=full", + "spring.cloud.openfeign.client.config.default.requestInterceptors[0]=org.springframework.cloud.openfeign.FeignClientUsingPropertiesTests.FooRequestInterceptor", + "spring.cloud.openfeign.client.config.default.requestInterceptors[1]=org.springframework.cloud.openfeign.FeignClientUsingPropertiesTests.BarRequestInterceptor" }) +class EagerInitFeignClientUsingConfigurerTests { + + @Autowired + private ApplicationContext applicationContext; + + @Autowired + private FeignClientFactory context; + + private static final String BEAN_NAME_PREFIX = "&org.springframework.cloud.openfeign.EagerInitFeignClientUsingConfigurerTests$"; + + @Test + public void testFeignClient() { + FeignClientFactoryBean factoryBean = (FeignClientFactoryBean) applicationContext + .getBean(BEAN_NAME_PREFIX + "TestFeignClient"); + Feign.Builder builder = factoryBean.feign(context); + + List interceptors = (List) getBuilderValue(builder, "requestInterceptors"); + assertThat(interceptors.size()).as("interceptors not set").isEqualTo(3); + assertThat(getBuilderValue(builder, "logLevel")).as("log level not set").isEqualTo(Logger.Level.FULL); + + List capabilities = (List) getBuilderValue(builder, "capabilities"); + assertThat(capabilities).hasSize(2).hasAtLeastOneElementOfType(NoOpCapability.class) + .hasAtLeastOneElementOfType(MicrometerObservationCapability.class); + } + + private Object getBuilderValue(Feign.Builder builder, String member) { + Field builderField = ReflectionUtils.findField(Feign.Builder.class, member); + ReflectionUtils.makeAccessible(builderField); + + return ReflectionUtils.getField(builderField, builder); + } + + @Test + public void testNoInheritFeignClient() { + FeignClientFactoryBean factoryBean = (FeignClientFactoryBean) applicationContext + .getBean(BEAN_NAME_PREFIX + "NoInheritFeignClient"); + Feign.Builder builder = factoryBean.feign(context); + + List interceptors = (List) getBuilderValue(builder, "requestInterceptors"); + assertThat(interceptors).as("interceptors not set").isEmpty(); + assertThat(factoryBean.isInheritParentContext()).as("is inheriting from parent configuration").isFalse(); + + List capabilities = (List) getBuilderValue(builder, "capabilities"); + assertThat(capabilities).hasSize(2).hasAtLeastOneElementOfType(NoOpCapability.class) + .hasAtLeastOneElementOfType(MicrometerObservationCapability.class); + } + + @Test + public void testNoInheritFeignClient_ignoreProperties() { + FeignClientFactoryBean factoryBean = (FeignClientFactoryBean) applicationContext + .getBean(BEAN_NAME_PREFIX + "NoInheritFeignClient"); + Feign.Builder builder = factoryBean.feign(context); + + assertThat(getBuilderValue(builder, "logLevel")).as("log level not set").isEqualTo(Logger.Level.HEADERS); + + List capabilities = (List) getBuilderValue(builder, "capabilities"); + assertThat(capabilities).hasSize(2).hasAtLeastOneElementOfType(NoOpCapability.class) + .hasAtLeastOneElementOfType(MicrometerObservationCapability.class); + } + + @EnableAutoConfiguration + @Configuration(proxyBeanMethods = false) + @EnableFeignClients(clients = { TestFeignClient.class, NoInheritFeignClient.class }) + protected static class Application { + + @Bean + public RequestInterceptor requestInterceptor() { + return requestTemplate -> { + }; + } + + @Bean + public NoOpCapability noOpCapability() { + return new NoOpCapability(); + } + + } + + public static class NoInheritConfiguration { + + @Bean + public Logger.Level logLevel() { + return Logger.Level.HEADERS; + } + + @Bean + public NoOpCapability noOpCapability() { + return new NoOpCapability(); + } + + @Bean + public FeignClientConfigurer feignClientConfigurer() { + return new FeignClientConfigurer() { + + @Override + public boolean inheritParentConfiguration() { + return false; + } + }; + + } + + } + + @FeignClient("testFeignClient") + interface TestFeignClient { + + } + + @FeignClient(name = "noInheritFeignClient", configuration = NoInheritConfiguration.class) + interface NoInheritFeignClient { + + } + + private static class NoOpCapability implements Capability { + + } + +} diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/EnableFeignClientsSpringDataTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/EnableFeignClientsSpringDataTests.java index 573e673a8..2cc69bcac 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/EnableFeignClientsSpringDataTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/EnableFeignClientsSpringDataTests.java @@ -34,11 +34,11 @@ class EnableFeignClientsSpringDataTests { @Autowired - private FeignContext feignContext; + private FeignClientFactory feignClientFactory; @Test void encoderDefaultCorrect() { - PageableSpringEncoder.class.cast(this.feignContext.getInstance("foo", Encoder.class)); + PageableSpringEncoder.class.cast(this.feignClientFactory.getInstance("foo", Encoder.class)); } @Configuration(proxyBeanMethods = false) diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignBuilderCustomizerTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignBuilderCustomizerTests.java index d6ed73e6f..4b728d58c 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignBuilderCustomizerTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignBuilderCustomizerTests.java @@ -162,8 +162,8 @@ private static Stream testConfiguration() { protected static class SampleConfiguration2 { @Bean - FeignContext feignContext() { - return new FeignContext(); + FeignClientFactory feignContext() { + return new FeignClientFactory(); } @Bean @@ -198,8 +198,8 @@ Targeter targeter() { protected static class SampleConfiguration3 { @Bean - FeignContext feignContext() { - return new FeignContext(); + FeignClientFactory feignContext() { + return new FeignClientFactory(); } @Bean diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientBuilderTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientBuilderTests.java index e0c3fe1cd..9a212a5a5 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientBuilderTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientBuilderTests.java @@ -158,7 +158,8 @@ void forType_clientFactoryBeanProvided() { @Test void forType_build() { // given: - Mockito.when(this.applicationContext.getBean(FeignContext.class)).thenThrow(new ClosedFileSystemException()); // throw + Mockito.when(this.applicationContext.getBean(FeignClientFactory.class)) + .thenThrow(new ClosedFileSystemException()); // throw // an // unusual // exception diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientDisabledClientLevelFeaturesTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientDisabledClientLevelFeaturesTests.java index abf946f9b..114538f12 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientDisabledClientLevelFeaturesTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientDisabledClientLevelFeaturesTests.java @@ -46,7 +46,7 @@ class FeignClientDisabledClientLevelFeaturesTests { @Autowired - private FeignContext context; + private FeignClientFactory context; @Autowired private FooClient foo; diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientDisabledFeaturesTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientDisabledFeaturesTests.java index 52b056313..00d256ab5 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientDisabledFeaturesTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientDisabledFeaturesTests.java @@ -45,7 +45,7 @@ class FeignClientDisabledFeaturesTests { @Autowired - private FeignContext context; + private FeignClientFactory context; @Autowired private FooClient foo; diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientErrorDecoderTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientErrorDecoderTests.java index 722a61aee..1c7282113 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientErrorDecoderTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientErrorDecoderTests.java @@ -50,7 +50,7 @@ public class FeignClientErrorDecoderTests { @Autowired - private FeignContext context; + private FeignClientFactory context; @Autowired private FooClient foo; diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignContextTest.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientFactoryTest.java similarity index 64% rename from spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignContextTest.java rename to spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientFactoryTest.java index 7345aaae9..d4ed2f051 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignContextTest.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientFactoryTest.java @@ -30,24 +30,24 @@ import static org.assertj.core.api.Assertions.assertThat; -class FeignContextTest { +class FeignClientFactoryTest { @Test void getInstanceWithoutAncestors_verifyNullForMissing() { AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext(); parent.refresh(); - FeignContext feignContext = new FeignContext(); - feignContext.setApplicationContext(parent); - feignContext.setConfigurations(Lists.newArrayList(getSpec("empty", EmptyConfiguration.class))); + FeignClientFactory feignClientFactory = new FeignClientFactory(); + feignClientFactory.setApplicationContext(parent); + feignClientFactory.setConfigurations(Lists.newArrayList(getSpec("empty", null, EmptyConfiguration.class))); - Logger.Level level = feignContext.getInstanceWithoutAncestors("empty", Logger.Level.class); + Logger.Level level = feignClientFactory.getInstanceWithoutAncestors("empty", Logger.Level.class); assertThat(level).as("Logger was not null").isNull(); } - private FeignClientSpecification getSpec(String name, Class configClass) { - return new FeignClientSpecification(name, new Class[] { configClass }); + private FeignClientSpecification getSpec(String name, String className, Class configClass) { + return new FeignClientSpecification(name, className, new Class[] { configClass }); } @Test @@ -55,11 +55,11 @@ void getInstancesWithoutAncestors_verifyEmptyForMissing() { AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext(); parent.refresh(); - FeignContext feignContext = new FeignContext(); - feignContext.setApplicationContext(parent); - feignContext.setConfigurations(Lists.newArrayList(getSpec("empty", EmptyConfiguration.class))); + FeignClientFactory feignClientFactory = new FeignClientFactory(); + feignClientFactory.setApplicationContext(parent); + feignClientFactory.setConfigurations(Lists.newArrayList(getSpec("empty", null, EmptyConfiguration.class))); - Collection interceptors = feignContext + Collection interceptors = feignClientFactory .getInstancesWithoutAncestors("empty", RequestInterceptor.class).values(); assertThat(interceptors).as("Interceptors is not empty").isEmpty(); @@ -70,11 +70,11 @@ void getInstanceWithoutAncestors() { AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext(); parent.refresh(); - FeignContext feignContext = new FeignContext(); - feignContext.setApplicationContext(parent); - feignContext.setConfigurations(Lists.newArrayList(getSpec("demo", DemoConfiguration.class))); + FeignClientFactory feignClientFactory = new FeignClientFactory(); + feignClientFactory.setApplicationContext(parent); + feignClientFactory.setConfigurations(Lists.newArrayList(getSpec("demo", null, DemoConfiguration.class))); - Logger.Level level = feignContext.getInstanceWithoutAncestors("demo", Logger.Level.class); + Logger.Level level = feignClientFactory.getInstanceWithoutAncestors("demo", Logger.Level.class); assertThat(level).isEqualTo(Logger.Level.FULL); } @@ -84,11 +84,11 @@ void getInstancesWithoutAncestors() { AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext(); parent.refresh(); - FeignContext feignContext = new FeignContext(); - feignContext.setApplicationContext(parent); - feignContext.setConfigurations(Lists.newArrayList(getSpec("demo", DemoConfiguration.class))); + FeignClientFactory feignClientFactory = new FeignClientFactory(); + feignClientFactory.setApplicationContext(parent); + feignClientFactory.setConfigurations(Lists.newArrayList(getSpec("demo", null, DemoConfiguration.class))); - Collection interceptors = feignContext + Collection interceptors = feignClientFactory .getInstancesWithoutAncestors("demo", RequestInterceptor.class).values(); assertThat(interceptors.size()).isEqualTo(1); diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientFactoryTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientFactoryTests.java index cc7e7f595..6f01b928b 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientFactoryTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientFactoryTests.java @@ -52,9 +52,10 @@ public class FeignClientFactoryTests { public void testChildContexts() { AnnotationConfigApplicationContext parent = new AnnotationConfigApplicationContext(); parent.refresh(); - FeignContext context = new FeignContext(); + FeignClientFactory context = new FeignClientFactory(); context.setApplicationContext(parent); - context.setConfigurations(Arrays.asList(getSpec("foo", FooConfig.class), getSpec("bar", BarConfig.class))); + context.setConfigurations( + Arrays.asList(getSpec("foo", null, FooConfig.class), getSpec("bar", null, BarConfig.class))); Foo foo = context.getInstance("foo", Foo.class); assertThat(foo).as("foo was null").isNotNull(); @@ -83,8 +84,8 @@ private void defaultClientUsed(AssertableApplicationContext context) { assertThat(client).isInstanceOf(Client.Default.class); } - private FeignClientSpecification getSpec(String name, Class configClass) { - return new FeignClientSpecification(name, new Class[] { configClass }); + private FeignClientSpecification getSpec(String name, String className, Class configClass) { + return new FeignClientSpecification(name, className, new Class[] { configClass }); } interface TestType { @@ -103,11 +104,11 @@ BlockingLoadBalancerClient loadBalancerClient() { } @Bean - FeignContext feignContext() { - FeignContext feignContext = new FeignContext(); - feignContext.setConfigurations(Collections.singletonList( - new FeignClientSpecification("test", new Class[] { LoadBalancerAutoConfiguration.class }))); - return feignContext; + FeignClientFactory feignContext() { + FeignClientFactory feignClientFactory = new FeignClientFactory(); + feignClientFactory.setConfigurations(Collections.singletonList( + new FeignClientSpecification("test", null, new Class[] { LoadBalancerAutoConfiguration.class }))); + return feignClientFactory; } @Bean diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientOverrideDefaultsTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientOverrideDefaultsTests.java index cb0c5363b..477392fed 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientOverrideDefaultsTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientOverrideDefaultsTests.java @@ -63,7 +63,7 @@ class FeignClientOverrideDefaultsTests { @Autowired - private FeignContext context; + private FeignClientFactory context; @Autowired private FooClient foo; diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientUsingPropertiesTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientUsingPropertiesTests.java index 6ff8d4022..7cbb6d982 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientUsingPropertiesTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientUsingPropertiesTests.java @@ -87,7 +87,7 @@ public class FeignClientUsingPropertiesTests { @Autowired - FeignContext context; + FeignClientFactory context; @Autowired private ApplicationContext applicationContext; diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientWithRefreshableOptionsTest.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientWithRefreshableOptionsTest.java index e70277615..4ec14ba11 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientWithRefreshableOptionsTest.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientWithRefreshableOptionsTest.java @@ -67,7 +67,7 @@ public class FeignClientWithRefreshableOptionsTest { private FeignClientProperties clientProperties; @Test - public void overridedOptionsBeanShouldBePresentInsteadOfRefreshable() { + public void overriddenOptionsBeanShouldBePresentInsteadOfRefreshable() { OptionsTestClient.OptionsResponseForTests options = overrideOptionsClient.override(); assertConnectionAndReadTimeout(options, 1, 1); } diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignCompressionTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignCompressionTests.java index 0a71100ff..c133f8dc6 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignCompressionTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignCompressionTests.java @@ -47,8 +47,8 @@ void testInterceptors() { FeignContentGzipEncodingAutoConfiguration.class, FeignAcceptGzipEncodingAutoConfiguration.class)) .run(context -> { - FeignContext feignContext = context.getBean(FeignContext.class); - Map interceptors = feignContext.getInstances("foo", + FeignClientFactory feignClientFactory = context.getBean(FeignClientFactory.class); + Map interceptors = feignClientFactory.getInstances("foo", RequestInterceptor.class); assertThat(interceptors.size()).isEqualTo(2); assertThat(interceptors.get("feignAcceptGzipEncodingInterceptor")) diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignHttpClientUrlTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignHttpClientUrlTests.java index 72ded3f9d..92d204604 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignHttpClientUrlTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignHttpClientUrlTests.java @@ -153,7 +153,7 @@ public String serverUrlNoProtocol() { public Targeter feignTargeter() { return new Targeter() { @Override - public T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context, + public T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignClientFactory context, Target.HardCodedTarget target) { Field field = ReflectionUtils.findField(Feign.Builder.class, "client"); ReflectionUtils.makeAccessible(field); diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignHttpClientUrlWithRetryableLoadBalancerTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignHttpClientUrlWithRetryableLoadBalancerTests.java index 4fca05e0e..233c867c0 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignHttpClientUrlWithRetryableLoadBalancerTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignHttpClientUrlWithRetryableLoadBalancerTests.java @@ -154,7 +154,7 @@ public String serverUrlNoProtocol() { public Targeter feignTargeter() { return new Targeter() { @Override - public T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context, + public T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignClientFactory context, Target.HardCodedTarget target) { Field field = ReflectionUtils.findField(Feign.Builder.class, "client"); ReflectionUtils.makeAccessible(field); diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/GzipDecodingTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/GzipDecodingTests.java index a7496ff21..6ccc6df68 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/GzipDecodingTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/GzipDecodingTests.java @@ -50,7 +50,7 @@ class GzipDecodingTests extends FeignClientFactoryBean { @Autowired - FeignContext context; + FeignClientFactory context; @Value("${local.server.port}") private int port = 0; 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/LazyInitFeignClientUsingConfigurerTests.java similarity index 94% rename from spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientUsingConfigurerTest.java rename to spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/LazyInitFeignClientUsingConfigurerTests.java index da4f2cf8c..76f60c750 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/LazyInitFeignClientUsingConfigurerTests.java @@ -41,21 +41,23 @@ /** * @author matt king * @author Jonatan Ivanov + * @author Olga Maciaszek-Sharma */ @DirtiesContext -@SpringBootTest(classes = FeignClientUsingConfigurerTest.Application.class, value = { +@SpringBootTest(classes = LazyInitFeignClientUsingConfigurerTests.Application.class, value = { + "spring.cloud.openfeign.lazy-attributes-resolution=true", "spring.cloud.openfeign.client.config.default.loggerLevel=full", "spring.cloud.openfeign.client.config.default.requestInterceptors[0]=org.springframework.cloud.openfeign.FeignClientUsingPropertiesTests.FooRequestInterceptor", "spring.cloud.openfeign.client.config.default.requestInterceptors[1]=org.springframework.cloud.openfeign.FeignClientUsingPropertiesTests.BarRequestInterceptor" }) -class FeignClientUsingConfigurerTest { +class LazyInitFeignClientUsingConfigurerTests { - private static final String BEAN_NAME_PREFIX = "org.springframework.cloud.openfeign.FeignClientUsingConfigurerTest$"; + private static final String BEAN_NAME_PREFIX = "org.springframework.cloud.openfeign.LazyInitFeignClientUsingConfigurerTests$"; @Autowired private ConfigurableListableBeanFactory beanFactory; @Autowired - private FeignContext context; + private FeignClientFactory context; @SuppressWarnings({ "unchecked", "rawtypes" }) @Test diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/SpringDecoderTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/SpringDecoderTests.java index b863f763a..bc975bab9 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/SpringDecoderTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/SpringDecoderTests.java @@ -53,7 +53,7 @@ class SpringDecoderTests extends FeignClientFactoryBean { @Autowired - FeignContext context; + FeignClientFactory context; @LocalServerPort private int port = 0; diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/aot/FeignAotTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/aot/FeignAotTests.java new file mode 100644 index 000000000..34c2d8f8a --- /dev/null +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/aot/FeignAotTests.java @@ -0,0 +1,171 @@ +/* + * Copyright 2022-2022 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.net.URL; + +import org.apache.catalina.webresources.TomcatURLStreamHandlerFactory; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; + +import org.springframework.aot.AotDetector; +import org.springframework.aot.test.generate.TestGenerationContext; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.web.servlet.ServletWebServerFactoryAutoConfiguration; +import org.springframework.boot.context.annotation.UserConfigurations; +import org.springframework.boot.test.context.runner.WebApplicationContextRunner; +import org.springframework.boot.test.system.CapturedOutput; +import org.springframework.boot.test.system.OutputCaptureExtension; +import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.boot.web.servlet.context.AnnotationConfigServletWebApplicationContext; +import org.springframework.boot.web.servlet.context.ServletWebServerApplicationContext; +import org.springframework.cloud.openfeign.EnableFeignClients; +import org.springframework.cloud.openfeign.FeignAutoConfiguration; +import org.springframework.cloud.openfeign.FeignClient; +import org.springframework.context.ApplicationContextInitializer; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.aot.ApplicationContextAotGenerator; +import org.springframework.context.support.GenericApplicationContext; +import org.springframework.core.test.tools.CompileWithForkedClassLoader; +import org.springframework.core.test.tools.TestCompiler; +import org.springframework.javapoet.ClassName; +import org.springframework.test.util.ReflectionTestUtils; +import org.springframework.web.bind.annotation.GetMapping; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * AOT processing tests. + * + * @author Olga Maciaszek-Sharma + */ +@ExtendWith(OutputCaptureExtension.class) +public class FeignAotTests { + + private static final Log LOG = LogFactory.getLog(FeignAotTests.class); + + @BeforeEach + @AfterEach + void reset() { + ReflectionTestUtils.setField(TomcatURLStreamHandlerFactory.class, "instance", null); + ReflectionTestUtils.setField(URL.class, "factory", null); + } + + @Test + @CompileWithForkedClassLoader + @SuppressWarnings("unchecked") + void shouldStartFeignChildContextsFromAotContributions(CapturedOutput output) { + WebApplicationContextRunner contextRunner = new WebApplicationContextRunner( + AnnotationConfigServletWebApplicationContext::new) + .withConfiguration(AutoConfigurations.of(ServletWebServerFactoryAutoConfiguration.class, + FeignAutoConfiguration.class)) + .withConfiguration(UserConfigurations.of(TestFeignConfiguration.class)) + .withPropertyValues("logging.level.org.springframework.cloud=DEBUG"); + contextRunner.prepare(context -> { + TestGenerationContext generationContext = new TestGenerationContext(TestTarget.class); + ClassName className = new ApplicationContextAotGenerator().processAheadOfTime( + (GenericApplicationContext) context.getSourceApplicationContext(), generationContext); + generationContext.writeGeneratedContent(); + TestCompiler compiler = TestCompiler.forSystem(); + compiler.with(generationContext).compile(compiled -> { + ServletWebServerApplicationContext freshApplicationContext = new ServletWebServerApplicationContext(); + ApplicationContextInitializer initializer = compiled + .getInstance(ApplicationContextInitializer.class, className.toString()); + initializer.initialize(freshApplicationContext); + assertThat(output).contains("Creating a FeignClientFactoryBean."); + assertThat(output).contains("Refreshing FeignClientFactory-test-with-config", + "Refreshing FeignClientFactory-test"); + assertThat(output).doesNotContain("Instantiating bean from Test custom config", + "Instantiating bean from default custom config"); + TestPropertyValues.of(AotDetector.AOT_ENABLED + "=true") + .applyToSystemProperties(freshApplicationContext::refresh); + assertThat(output).contains("Instantiating bean from Test custom config", + "Instantiating bean from default custom config"); + assertThat(freshApplicationContext.getBean(TestFeignClient.class)).isNotNull(); + assertThat(freshApplicationContext.getBean(TestFeignClientWithConfig.class)).isNotNull(); + }); + }); + } + + static class TestTarget { + + } + + @Configuration(proxyBeanMethods = false) + @EnableFeignClients(clients = { TestFeignClient.class, TestFeignClientWithConfig.class }, + defaultConfiguration = DefaultConfiguration.class) + public static class TestFeignConfiguration { + + @Autowired + TestFeignClient testFeignClient; + + @Autowired + TestFeignClientWithConfig testFeignClientWithConfig; + + } + + public static class TestConfiguration { + + @Bean + TestBean testBean() { + if (LOG.isDebugEnabled()) { + LOG.debug("Instantiating bean from Test custom config"); + } + return new TestBean(); + } + + } + + public static class DefaultConfiguration { + + @Bean + TestBean defaultTestBean() { + if (LOG.isDebugEnabled()) { + LOG.debug("Instantiating bean from default custom config"); + } + return new TestBean(); + } + + } + + public static class TestBean { + + } + + @FeignClient(value = "test", dismiss404 = true, url = "http://example.com") + interface TestFeignClient { + + @GetMapping + void test(); + + } + + @FeignClient(value = "test-with-config", configuration = TestConfiguration.class) + interface TestFeignClientWithConfig { + + @GetMapping + void test(); + + } + +} diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/circuitbreaker/AsyncCircuitBreakerTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/circuitbreaker/AsyncCircuitBreakerTests.java index 43ecb355f..87ae8a0ea 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/circuitbreaker/AsyncCircuitBreakerTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/circuitbreaker/AsyncCircuitBreakerTests.java @@ -65,7 +65,8 @@ * @author John Niang */ @SpringBootTest(classes = AsyncCircuitBreakerTests.Application.class, webEnvironment = RANDOM_PORT, - properties = "spring.cloud.openfeign.circuitbreaker.enabled=true") + properties = { "spring.cloud.openfeign.circuitbreaker.enabled=true", + "spring.cloud.openfeign.lazy-attributes-resolution=true" }) @AutoConfigureMockMvc class AsyncCircuitBreakerTests { diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/PageableEncoderTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/PageableEncoderTests.java index 542fb2cb5..2bbf2710f 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/PageableEncoderTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/PageableEncoderTests.java @@ -22,7 +22,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.openfeign.FeignContext; +import org.springframework.cloud.openfeign.FeignClientFactory; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -51,7 +51,7 @@ class PageableEncoderTests { public static final String SORT_1 = "sort1"; @Autowired - private FeignContext context; + private FeignClientFactory context; protected String getPageParameter() { return "page"; diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/PageableSpringQueryMapEncoderTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/PageableSpringQueryMapEncoderTests.java index 2163e02b3..bb14e7332 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/PageableSpringQueryMapEncoderTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/PageableSpringQueryMapEncoderTests.java @@ -24,7 +24,7 @@ import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; -import org.springframework.cloud.openfeign.FeignContext; +import org.springframework.cloud.openfeign.FeignClientFactory; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; import org.springframework.data.domain.Sort; @@ -52,7 +52,7 @@ class PageableSpringQueryMapEncoderTests { public static final String SORT_1 = "sort1"; @Autowired - private FeignContext context; + private FeignClientFactory context; protected String getPageParameter() { return "page"; diff --git a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringEncoderTests.java b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringEncoderTests.java index df590c102..7e57595a9 100644 --- a/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringEncoderTests.java +++ b/spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/support/SpringEncoderTests.java @@ -34,7 +34,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; -import org.springframework.cloud.openfeign.FeignContext; +import org.springframework.cloud.openfeign.FeignClientFactory; import org.springframework.cloud.openfeign.encoding.HttpEncoding; import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; @@ -77,7 +77,7 @@ class SpringEncoderTests { @Autowired - private FeignContext context; + private FeignClientFactory context; @Autowired @Qualifier("myHttpMessageConverter")