Skip to content

Commit 7f66cf3

Browse files
Add AOT and native image support (#794)
1 parent 62f9d76 commit 7f66cf3

38 files changed

+931
-105
lines changed

docs/src/main/asciidoc/spring-cloud-openfeign.adoc

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,15 @@ TIP: To use `@EnableFeignClients` annotation on `@Configuration`-annotated-class
7878
or list them explicitly:
7979
`@EnableFeignClients(clients = InventoryServiceFeignClient.class)`
8080

81+
[[attribute-resolution-mode]]
82+
==== Attribute resolution mode
83+
84+
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.
85+
86+
If you need the attributes to be resolved lazily, set the `spring.cloud.openfeign.lazy-attributes-resolution` property value to `true`.
87+
88+
TIP: For Spring Cloud Contract test integration, lazy attribute resolution should be used.
89+
8190
[[spring-cloud-feign-overriding-defaults]]
8291
=== Overriding Feign Defaults
8392

@@ -905,6 +914,17 @@ The URL provided in the configuration properties remains unused.
905914

906915
|===
907916

917+
=== AOT and Native Image Support
918+
919+
Spring Cloud OpenFeign supports Spring AOT transformations and native images, however, only with refresh mode disabled, Feign clients refresh disabled (default setting) and <<attribute-resolution-mode,lazy `@FeignClient` attribute resolution>> disabled (default setting).
920+
921+
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`.
922+
923+
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`.
924+
925+
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`.
926+
927+
908928
== Configuration properties
909929

910930
To see the list of all Spring Cloud OpenFeign related configuration properties please check link:appendix.html[the Appendix page].

pom.xml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@
228228
<artifactId>maven-surefire-plugin</artifactId>
229229
<configuration>
230230
<!-- Sets the VM argument line used when unit tests are run. -->
231-
<argLine>${surefireArgLine}</argLine>
231+
<argLine>${surefireArgLine} --add-opens=java.base/java.net=ALL-UNNAMED</argLine>
232232
</configuration>
233233
</plugin>
234234
</plugins>

spring-cloud-openfeign-core/pom.xml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,11 @@
191191
<version>2.11.0</version>
192192
<scope>test</scope>
193193
</dependency>
194+
<dependency>
195+
<groupId>org.springframework</groupId>
196+
<artifactId>spring-core-test</artifactId>
197+
<scope>test</scope>
198+
</dependency>
194199
<dependency>
195200
<groupId>org.springframework.security</groupId>
196201
<artifactId>spring-security-oauth2-client</artifactId>

spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/DefaultTargeter.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
class DefaultTargeter implements Targeter {
2626

2727
@Override
28-
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
28+
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignClientFactory context,
2929
Target.HardCodedTarget<T> target) {
3030
return feign.target(target);
3131
}

spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignAutoConfiguration.java

Lines changed: 36 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@
4444
import org.apache.commons.logging.Log;
4545
import org.apache.commons.logging.LogFactory;
4646

47+
import org.springframework.aot.hint.MemberCategory;
48+
import org.springframework.aot.hint.RuntimeHints;
49+
import org.springframework.aot.hint.RuntimeHintsRegistrar;
50+
import org.springframework.aot.hint.TypeReference;
4751
import org.springframework.beans.factory.annotation.Autowired;
4852
import org.springframework.beans.factory.annotation.Value;
4953
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
@@ -55,6 +59,8 @@
5559
import org.springframework.cloud.client.actuator.HasFeatures;
5660
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
5761
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
62+
import org.springframework.cloud.openfeign.aot.FeignChildContextInitializer;
63+
import org.springframework.cloud.openfeign.aot.FeignClientBeanFactoryInitializationAotProcessor;
5864
import org.springframework.cloud.openfeign.security.OAuth2AccessTokenInterceptor;
5965
import org.springframework.cloud.openfeign.support.FeignEncoderProperties;
6066
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
@@ -64,12 +70,14 @@
6470
import org.springframework.context.annotation.Conditional;
6571
import org.springframework.context.annotation.Configuration;
6672
import org.springframework.context.annotation.Import;
73+
import org.springframework.context.support.GenericApplicationContext;
6774
import org.springframework.data.domain.Page;
6875
import org.springframework.data.domain.Sort;
6976
import org.springframework.security.oauth2.client.AuthorizedClientServiceOAuth2AuthorizedClientManager;
7077
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientManager;
7178
import org.springframework.security.oauth2.client.OAuth2AuthorizedClientService;
7279
import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository;
80+
import org.springframework.util.ClassUtils;
7381

7482
/**
7583
* @author Spencer Gibb
@@ -102,12 +110,24 @@ public HasFeatures feignFeature() {
102110
}
103111

104112
@Bean
105-
public FeignContext feignContext() {
106-
FeignContext context = new FeignContext();
113+
public FeignClientFactory feignContext() {
114+
FeignClientFactory context = new FeignClientFactory();
107115
context.setConfigurations(this.configurations);
108116
return context;
109117
}
110118

119+
@Bean
120+
static FeignChildContextInitializer feignChildContextInitializer(GenericApplicationContext parentContext,
121+
FeignClientFactory feignClientFactory) {
122+
return new FeignChildContextInitializer(parentContext, feignClientFactory);
123+
}
124+
125+
@Bean
126+
static FeignClientBeanFactoryInitializationAotProcessor feignClientBeanFactoryInitializationCodeGenerator(
127+
GenericApplicationContext applicationContext, FeignClientFactory feignClientFactory) {
128+
return new FeignClientBeanFactoryInitializationAotProcessor(applicationContext, feignClientFactory);
129+
}
130+
111131
@Bean
112132
@ConditionalOnProperty(value = "spring.cloud.openfeign.cache.enabled", matchIfMissing = true)
113133
@ConditionalOnBean(CacheInterceptor.class)
@@ -358,3 +378,17 @@ public OAuth2AccessTokenInterceptor defaultOAuth2AccessTokenInterceptor(
358378
}
359379

360380
}
381+
382+
class FeignHints implements RuntimeHintsRegistrar {
383+
384+
@Override
385+
public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
386+
if (!ClassUtils.isPresent("feign.Feign", classLoader)) {
387+
return;
388+
}
389+
hints.reflection().registerType(TypeReference.of(FeignClientFactoryBean.class),
390+
hint -> hint.withMembers(MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
391+
MemberCategory.INVOKE_DECLARED_METHODS, MemberCategory.DECLARED_FIELDS));
392+
}
393+
394+
}

spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignCircuitBreakerTargeter.java

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class FeignCircuitBreakerTargeter implements Targeter {
3939
}
4040

4141
@Override
42-
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
42+
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignClientFactory context,
4343
Target.HardCodedTarget<T> target) {
4444
if (!(feign instanceof FeignCircuitBreaker.Builder builder)) {
4545
return feign.target(target);
@@ -56,20 +56,20 @@ public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignCo
5656
return builder(name, builder).target(target);
5757
}
5858

59-
private <T> T targetWithFallbackFactory(String feignClientName, FeignContext context,
59+
private <T> T targetWithFallbackFactory(String feignClientName, FeignClientFactory context,
6060
Target.HardCodedTarget<T> target, FeignCircuitBreaker.Builder builder, Class<?> fallbackFactoryClass) {
6161
FallbackFactory<? extends T> fallbackFactory = (FallbackFactory<? extends T>) getFromContext("fallbackFactory",
6262
feignClientName, context, fallbackFactoryClass, FallbackFactory.class);
6363
return builder(feignClientName, builder).target(target, fallbackFactory);
6464
}
6565

66-
private <T> T targetWithFallback(String feignClientName, FeignContext context, Target.HardCodedTarget<T> target,
67-
FeignCircuitBreaker.Builder builder, Class<?> fallback) {
66+
private <T> T targetWithFallback(String feignClientName, FeignClientFactory context,
67+
Target.HardCodedTarget<T> target, FeignCircuitBreaker.Builder builder, Class<?> fallback) {
6868
T fallbackInstance = getFromContext("fallback", feignClientName, context, fallback, target.type());
6969
return builder(feignClientName, builder).target(target, fallbackInstance);
7070
}
7171

72-
private <T> T getFromContext(String fallbackMechanism, String feignClientName, FeignContext context,
72+
private <T> T getFromContext(String fallbackMechanism, String feignClientName, FeignClientFactory context,
7373
Class<?> beanType, Class<T> targetType) {
7474
Object fallbackInstance = context.getInstance(feignClientName, beanType);
7575
if (fallbackInstance == null) {

spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignContext.java renamed to spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientFactory.java

Lines changed: 23 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -16,11 +16,14 @@
1616

1717
package org.springframework.cloud.openfeign;
1818

19+
import java.util.HashMap;
1920
import java.util.Map;
2021

2122
import org.springframework.beans.BeansException;
2223
import org.springframework.beans.factory.BeanFactoryUtils;
2324
import org.springframework.cloud.context.named.NamedContextFactory;
25+
import org.springframework.context.ApplicationContextInitializer;
26+
import org.springframework.context.support.GenericApplicationContext;
2427
import org.springframework.lang.Nullable;
2528

2629
/**
@@ -31,11 +34,18 @@
3134
* @author Dave Syer
3235
* @author Matt King
3336
* @author Jasbir Singh
37+
* @author Olga Maciaszek-Sharma
3438
*/
35-
public class FeignContext extends NamedContextFactory<FeignClientSpecification> {
39+
public class FeignClientFactory extends NamedContextFactory<FeignClientSpecification> {
3640

37-
public FeignContext() {
38-
super(FeignClientsConfiguration.class, "spring.cloud.openfeign", "spring.cloud.openfeign.client.name");
41+
public FeignClientFactory() {
42+
this(new HashMap<>());
43+
}
44+
45+
public FeignClientFactory(
46+
Map<String, ApplicationContextInitializer<GenericApplicationContext>> applicationContextInitializers) {
47+
super(FeignClientsConfiguration.class, "spring.cloud.openfeign", "spring.cloud.openfeign.client.name",
48+
applicationContextInitializers);
3949
}
4050

4151
@Nullable
@@ -57,4 +67,14 @@ public <T> T getInstance(String contextName, String beanName, Class<T> type) {
5767
return getContext(contextName).getBean(beanName, type);
5868
}
5969

70+
@SuppressWarnings("unchecked")
71+
public FeignClientFactory withApplicationContextInitializers(Map<String, Object> applicationContextInitializers) {
72+
Map<String, ApplicationContextInitializer<GenericApplicationContext>> convertedInitializers = new HashMap<>();
73+
applicationContextInitializers.keySet()
74+
.forEach(contextId -> convertedInitializers.put(contextId,
75+
(ApplicationContextInitializer<GenericApplicationContext>) applicationContextInitializers
76+
.get(contextId)));
77+
return new FeignClientFactory(convertedInitializers);
78+
}
79+
6080
}

spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientFactoryBean.java

Lines changed: 36 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -113,13 +113,22 @@ public class FeignClientFactoryBean
113113

114114
private final List<FeignBuilderCustomizer> additionalCustomizers = new ArrayList<>();
115115

116+
private String[] qualifiers = new String[] {};
117+
118+
// For AOT testing
119+
public FeignClientFactoryBean() {
120+
if (LOG.isDebugEnabled()) {
121+
LOG.debug("Creating a FeignClientFactoryBean.");
122+
}
123+
}
124+
116125
@Override
117126
public void afterPropertiesSet() {
118127
Assert.hasText(contextId, "Context id must be set");
119128
Assert.hasText(name, "Name must be set");
120129
}
121130

122-
protected Feign.Builder feign(FeignContext context) {
131+
protected Feign.Builder feign(FeignClientFactory context) {
123132
FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
124133
Logger logger = loggerFactory.create(type);
125134

@@ -137,7 +146,7 @@ protected Feign.Builder feign(FeignContext context) {
137146
return builder;
138147
}
139148

140-
private void applyBuildCustomizers(FeignContext context, Feign.Builder builder) {
149+
private void applyBuildCustomizers(FeignClientFactory context, Feign.Builder builder) {
141150
Map<String, FeignBuilderCustomizer> customizerMap = context.getInstances(contextId,
142151
FeignBuilderCustomizer.class);
143152

@@ -148,7 +157,7 @@ private void applyBuildCustomizers(FeignContext context, Feign.Builder builder)
148157
additionalCustomizers.forEach(customizer -> customizer.customize(builder));
149158
}
150159

151-
protected void configureFeign(FeignContext context, Feign.Builder builder) {
160+
protected void configureFeign(FeignClientFactory context, Feign.Builder builder) {
152161
FeignClientProperties properties = beanFactory != null ? beanFactory.getBean(FeignClientProperties.class)
153162
: applicationContext.getBean(FeignClientProperties.class);
154163

@@ -172,7 +181,7 @@ protected void configureFeign(FeignContext context, Feign.Builder builder) {
172181
}
173182
}
174183

175-
protected void configureUsingConfiguration(FeignContext context, Feign.Builder builder) {
184+
protected void configureUsingConfiguration(FeignClientFactory context, Feign.Builder builder) {
176185
Logger.Level level = getInheritedAwareOptional(context, Logger.Level.class);
177186
if (level != null) {
178187
builder.logLevel(level);
@@ -340,19 +349,19 @@ private <T> T getOrInstantiate(Class<T> tClass) {
340349
}
341350
}
342351

343-
protected <T> T get(FeignContext context, Class<T> type) {
352+
protected <T> T get(FeignClientFactory context, Class<T> type) {
344353
T instance = context.getInstance(contextId, type);
345354
if (instance == null) {
346355
throw new IllegalStateException("No bean found of type " + type + " for " + contextId);
347356
}
348357
return instance;
349358
}
350359

351-
protected <T> T getOptional(FeignContext context, Class<T> type) {
360+
protected <T> T getOptional(FeignClientFactory context, Class<T> type) {
352361
return context.getInstance(contextId, type);
353362
}
354363

355-
protected <T> T getInheritedAwareOptional(FeignContext context, Class<T> type) {
364+
protected <T> T getInheritedAwareOptional(FeignClientFactory context, Class<T> type) {
356365
if (inheritParentContext) {
357366
return getOptional(context, type);
358367
}
@@ -361,7 +370,7 @@ protected <T> T getInheritedAwareOptional(FeignContext context, Class<T> type) {
361370
}
362371
}
363372

364-
protected <T> Map<String, T> getInheritedAwareInstances(FeignContext context, Class<T> type) {
373+
protected <T> Map<String, T> getInheritedAwareInstances(FeignClientFactory context, Class<T> type) {
365374
if (inheritParentContext) {
366375
return context.getInstances(contextId, type);
367376
}
@@ -370,7 +379,7 @@ protected <T> Map<String, T> getInheritedAwareInstances(FeignContext context, Cl
370379
}
371380
}
372381

373-
protected <T> T loadBalance(Feign.Builder builder, FeignContext context, HardCodedTarget<T> target) {
382+
protected <T> T loadBalance(Feign.Builder builder, FeignClientFactory context, HardCodedTarget<T> target) {
374383
Client client = getOptional(context, Client.class);
375384
if (client != null) {
376385
builder.client(client);
@@ -389,7 +398,7 @@ protected <T> T loadBalance(Feign.Builder builder, FeignContext context, HardCod
389398
* @param contextId name of feign client
390399
* @return returns Options found in context
391400
*/
392-
protected Request.Options getOptionsByName(FeignContext context, String contextId) {
401+
protected Request.Options getOptionsByName(FeignClientFactory context, String contextId) {
393402
if (refreshableClient) {
394403
return context.getInstance(contextId, Request.Options.class.getCanonicalName() + "-" + contextId,
395404
Request.Options.class);
@@ -409,9 +418,9 @@ public Object getObject() {
409418
*/
410419
@SuppressWarnings("unchecked")
411420
<T> T getTarget() {
412-
FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class)
413-
: applicationContext.getBean(FeignContext.class);
414-
Feign.Builder builder = feign(context);
421+
FeignClientFactory feignClientFactory = beanFactory != null ? beanFactory.getBean(FeignClientFactory.class)
422+
: applicationContext.getBean(FeignClientFactory.class);
423+
Feign.Builder builder = feign(feignClientFactory);
415424
if (!StringUtils.hasText(url) && !isUrlAvailableInConfig(contextId)) {
416425

417426
if (LOG.isInfoEnabled()) {
@@ -424,13 +433,13 @@ <T> T getTarget() {
424433
url = name;
425434
}
426435
url += cleanPath();
427-
return (T) loadBalance(builder, context, new HardCodedTarget<>(type, name, url));
436+
return (T) loadBalance(builder, feignClientFactory, new HardCodedTarget<>(type, name, url));
428437
}
429438
if (StringUtils.hasText(url) && !url.startsWith("http")) {
430439
url = "http://" + url;
431440
}
432441
String url = this.url + cleanPath();
433-
Client client = getOptional(context, Client.class);
442+
Client client = getOptional(feignClientFactory, Client.class);
434443
if (client != null) {
435444
if (client instanceof FeignBlockingLoadBalancerClient) {
436445
// not load balancing because we have a url,
@@ -445,10 +454,10 @@ <T> T getTarget() {
445454
builder.client(client);
446455
}
447456

448-
applyBuildCustomizers(context, builder);
457+
applyBuildCustomizers(feignClientFactory, builder);
449458

450-
Targeter targeter = get(context, Targeter.class);
451-
return targeter.target(this, builder, context, resolveTarget(context, contextId, url));
459+
Targeter targeter = get(feignClientFactory, Targeter.class);
460+
return targeter.target(this, builder, feignClientFactory, resolveTarget(feignClientFactory, contextId, url));
452461
}
453462

454463
private String cleanPath() {
@@ -468,7 +477,7 @@ private String cleanPath() {
468477
}
469478

470479
@SuppressWarnings({ "unchecked", "rawtypes" })
471-
private <T> HardCodedTarget<T> resolveTarget(FeignContext context, String contextId, String url) {
480+
private <T> HardCodedTarget<T> resolveTarget(FeignClientFactory context, String contextId, String url) {
472481
if (StringUtils.hasText(url)) {
473482
return new HardCodedTarget(type, name, url);
474483
}
@@ -600,6 +609,14 @@ public void setRefreshableClient(boolean refreshableClient) {
600609
this.refreshableClient = refreshableClient;
601610
}
602611

612+
public String[] getQualifiers() {
613+
return qualifiers;
614+
}
615+
616+
public void setQualifiers(String[] qualifiers) {
617+
this.qualifiers = qualifiers;
618+
}
619+
603620
@Override
604621
public boolean equals(Object o) {
605622
if (this == o) {

0 commit comments

Comments
 (0)