Compare commits
12
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c33bc490cd | ||
|
|
d738b3b224 | ||
|
|
0c1d281970 | ||
|
|
0bf0426f60 | ||
|
|
9f9ce39789 | ||
|
|
4d0caec7ee | ||
|
|
92f7ba9901 | ||
|
|
b2c208ba5f | ||
|
|
cdfd3695c2 | ||
|
|
59216f2f82 | ||
|
|
4d827623f0 | ||
|
|
5e494aa196 |
@@ -1,38 +0,0 @@
|
||||
version: 2
|
||||
jobs:
|
||||
build:
|
||||
docker:
|
||||
- image: springcloud/pipeline-base
|
||||
environment:
|
||||
_JAVA_OPTIONS: "-Xms1024m -Xmx2048m"
|
||||
TERM: dumb
|
||||
branches:
|
||||
ignore:
|
||||
- gh-pages # list of branches to ignore
|
||||
steps:
|
||||
- checkout
|
||||
- restore_cache:
|
||||
key: sc-openfeign-{{ .Branch }}
|
||||
- run:
|
||||
name: "Download dependencies"
|
||||
command: ./mvnw -s .settings.xml -U --fail-never dependency:go-offline || true
|
||||
- save_cache:
|
||||
key: sc-openfeign-{{ .Branch }}
|
||||
paths:
|
||||
- ~/.m2
|
||||
- run:
|
||||
name: "Running build"
|
||||
command: ./mvnw -s .settings.xml clean org.jacoco:jacoco-maven-plugin:prepare-agent install -U -P sonar -nsu --batch-mode -Dmaven.test.redirectTestOutputToFile=true -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn
|
||||
- run:
|
||||
name: "Aggregate test results"
|
||||
when: always
|
||||
command: |
|
||||
mkdir -p $CIRCLE_TEST_REPORTS/junit/
|
||||
find . -type f -regex ".*/target/.*-reports/.*" -exec cp {} $CIRCLE_TEST_REPORTS/junit/ \;
|
||||
bash <(curl -s https://codecov.io/bash)
|
||||
- store_artifacts:
|
||||
path: /junit/
|
||||
destination: artifacts
|
||||
- store_test_results:
|
||||
path: /junit/
|
||||
destination: testartifacts
|
||||
@@ -0,0 +1,35 @@
|
||||
# This workflow will build a Java project with Maven
|
||||
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
|
||||
|
||||
name: Build
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [ 2.2.x ]
|
||||
pull_request:
|
||||
branches: [ 2.2.x ]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
strategy:
|
||||
matrix:
|
||||
java: ["8", "11", "16"]
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up JDK 1.8
|
||||
uses: actions/setup-java@v1
|
||||
with:
|
||||
java-version: 1.8
|
||||
- name: Cache local Maven repository
|
||||
uses: actions/cache@v2
|
||||
with:
|
||||
path: ~/.m2/repository
|
||||
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-maven-
|
||||
- name: Build with Maven
|
||||
run: ./mvnw clean install -B -U
|
||||
+1
-1
@@ -5,7 +5,7 @@ Edit the files in the src/main/asciidoc/ directory instead.
|
||||
////
|
||||
|
||||
|
||||
image:https://circleci.com/gh/spring-cloud/spring-cloud-openfeign.svg?style=svg["CircleCI", link="https://circleci.com/gh/spring-cloud/spring-cloud-openfeign"]
|
||||
image::https://github.com/spring-cloud/spring-cloud-openfeign/workflows/Build/badge.svg?branch=2.2.x&style=svg["Build",link="https://github.com/spring-cloud/spring-cloud-openfeign/actions"]
|
||||
|
||||
image:https://codecov.io/gh/spring-cloud/spring-cloud-openfeign/branch/master/graph/badge.svg["Codecov", link="https://codecov.io/gh/spring-cloud/spring-cloud-openfeign"]
|
||||
|
||||
|
||||
+2
-8
@@ -6,10 +6,10 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-openfeign</artifactId>
|
||||
<version>3.0.2</version>
|
||||
<version>2.2.8.RELEASE</version>
|
||||
</parent>
|
||||
<artifactId>spring-cloud-openfeign-docs</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
<packaging>pom</packaging>
|
||||
<name>Spring Cloud OpenFeign Docs</name>
|
||||
<description>Spring Cloud Docs</description>
|
||||
<properties>
|
||||
@@ -24,9 +24,6 @@
|
||||
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
<build>
|
||||
<sourceDirectory>src/main/asciidoc</sourceDirectory>
|
||||
</build>
|
||||
<profiles>
|
||||
<profile>
|
||||
<id>docs</id>
|
||||
@@ -37,11 +34,9 @@
|
||||
<artifactId>git-commit-id-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-dependency-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-resources-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
@@ -53,7 +48,6 @@
|
||||
<artifactId>asciidoctor-maven-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
<groupId>org.apache.maven.plugins</groupId>
|
||||
<artifactId>maven-antrun-plugin</artifactId>
|
||||
</plugin>
|
||||
<plugin>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
image:https://circleci.com/gh/spring-cloud/spring-cloud-openfeign.svg?style=svg["CircleCI", link="https://circleci.com/gh/spring-cloud/spring-cloud-openfeign"]
|
||||
image::https://github.com/spring-cloud/spring-cloud-openfeign/workflows/Build/badge.svg?branch=2.2.x&style=svg["Build",link="https://github.com/spring-cloud/spring-cloud-openfeign/actions"]
|
||||
|
||||
image:https://codecov.io/gh/spring-cloud/spring-cloud-openfeign/branch/master/graph/badge.svg["Codecov", link="https://codecov.io/gh/spring-cloud/spring-cloud-openfeign"]
|
||||
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
|feign.httpclient.max-connections-per-route | `50` |
|
||||
|feign.httpclient.time-to-live | `900` |
|
||||
|feign.httpclient.time-to-live-unit | |
|
||||
|feign.metrics.enabled | `true` | Enables metrics capability for Feign.
|
||||
|feign.hystrix.enabled | `false` | If true, an OpenFeign client will be wrapped with a Hystrix circuit breaker.
|
||||
|feign.okhttp.enabled | `false` | Enables the use of the OK HTTP Client by Feign.
|
||||
|
||||
|===
|
||||
@@ -15,7 +15,7 @@ To use Feign create an interface and annotate it.
|
||||
It has pluggable annotation support including Feign annotations and JAX-RS annotations.
|
||||
Feign also supports pluggable encoders and decoders.
|
||||
Spring Cloud adds support for Spring MVC annotations and for using the same `HttpMessageConverters` used by default in Spring Web.
|
||||
Spring Cloud integrates Eureka, Spring Cloud CircuitBreaker, as well as Spring Cloud LoadBalancer to provide a load-balanced http client when using Feign.
|
||||
Spring Cloud integrates Ribbon and Eureka, Spring Cloud CircuitBreaker, as well as Spring Cloud LoadBalancer to provide a load-balanced http client when using Feign.
|
||||
|
||||
[[netflix-feign-starter]]
|
||||
=== How to Include Feign
|
||||
@@ -32,9 +32,9 @@ Example spring boot app
|
||||
@EnableFeignClients
|
||||
public class Application {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
@@ -44,18 +44,18 @@ public class Application {
|
||||
----
|
||||
@FeignClient("stores")
|
||||
public interface StoreClient {
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/stores")
|
||||
List<Store> getStores();
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/stores")
|
||||
List<Store> getStores();
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/stores")
|
||||
Page<Store> getStores(Pageable pageable);
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/stores")
|
||||
Page<Store> getStores(Pageable pageable);
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
|
||||
Store update(@PathVariable("storeId") Long storeId, Store store);
|
||||
@RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
|
||||
Store update(@PathVariable("storeId") Long storeId, Store store);
|
||||
}
|
||||
----
|
||||
|
||||
In the `@FeignClient` annotation the String value ("stores" above) is an arbitrary client name, which is used to create a https://github.com/spring-cloud/spring-cloud-commons/blob/master/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/blocking/client/BlockingLoadBalancerClient.java[Spring Cloud LoadBalancer client].
|
||||
In the `@FeignClient` annotation the String value ("stores" above) is an arbitrary client name, which is used to create either a https://github.com/Netflix/ribbon[Ribbon] load-balancer (see <<spring-cloud-ribbon,below for details of Ribbon support>> and <<spring-cloud-circuitbreaker,below for details of Spring Cloud CircuitBreaker support>>) or https://github.com/spring-cloud/spring-cloud-commons/blob/master/spring-cloud-commons/src/main/java/org/springframework/cloud/client/loadbalancer/reactive/ReactiveLoadBalancer.java[Spring Cloud LoadBalancer].
|
||||
You can also specify a URL using the `url` attribute
|
||||
(absolute value or just a hostname). The name of the bean in the
|
||||
application context is the fully qualified name of the interface.
|
||||
@@ -65,10 +65,12 @@ of the `@FeignClient` annotation.
|
||||
The load-balancer client above will want to discover the physical addresses
|
||||
for the "stores" service. If your application is a Eureka client then
|
||||
it will resolve the service in the Eureka service registry. If you
|
||||
don't want to use Eureka, you can configure a list of servers
|
||||
don't want to use Eureka, you can simply configure a list of servers
|
||||
in your external configuration using https://cloud.spring.io/spring-cloud-static/spring-cloud-commons/current/reference/html/#simplediscoveryclient[`SimpleDiscoveryClient`].
|
||||
|
||||
Spring Cloud OpenFeign supports all the features available for the blocking mode of Spring Cloud LoadBalancer. You can read more about them in the https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#spring-cloud-loadbalancer[project documentation].
|
||||
WARNING: In order to maintain backward compatibility, is used as the default load-balancer implementation.
|
||||
However, Spring Cloud Netflix Ribbon is now in maintenance mode, so we recommend using Spring Cloud LoadBalancer instead.
|
||||
To do this, set the value of `spring.cloud.loadbalancer.ribbon.enabled` to `false`.
|
||||
|
||||
[[spring-cloud-feign-overriding-defaults]]
|
||||
=== Overriding Feign Defaults
|
||||
@@ -84,7 +86,7 @@ Spring Cloud lets you take full control of the feign client by declaring additio
|
||||
----
|
||||
@FeignClient(name = "stores", configuration = FooConfiguration.class)
|
||||
public interface StoreClient {
|
||||
//..
|
||||
//..
|
||||
}
|
||||
----
|
||||
|
||||
@@ -92,6 +94,8 @@ In this case the client is composed from the components already in `FeignClients
|
||||
|
||||
NOTE: `FooConfiguration` does not need to be annotated with `@Configuration`. However, if it is, then take care to exclude it from any `@ComponentScan` that would otherwise include this configuration as it will become the default source for `feign.Decoder`, `feign.Encoder`, `feign.Contract`, etc., when specified. This can be avoided by putting it in a separate, non-overlapping package from any `@ComponentScan` or `@SpringBootApplication`, or it can be explicitly excluded in `@ComponentScan`.
|
||||
|
||||
NOTE: The `serviceId` attribute is now deprecated in favor of the `name` attribute.
|
||||
|
||||
NOTE: Using `contextId` attribute of the `@FeignClient` annotation in addition to changing the name of
|
||||
the `ApplicationContext` ensemble, it will override the alias of the client name
|
||||
and it will be used as part of the name of the configuration bean created for that client.
|
||||
@@ -104,7 +108,7 @@ Placeholders are supported in the `name` and `url` attributes.
|
||||
----
|
||||
@FeignClient(name = "${feign.name}", url = "${feign.url}")
|
||||
public interface StoreClient {
|
||||
//..
|
||||
//..
|
||||
}
|
||||
----
|
||||
|
||||
@@ -113,13 +117,13 @@ Spring Cloud OpenFeign provides the following beans by default for feign (`BeanT
|
||||
* `Decoder` feignDecoder: `ResponseEntityDecoder` (which wraps a `SpringDecoder`)
|
||||
* `Encoder` feignEncoder: `SpringEncoder`
|
||||
* `Logger` feignLogger: `Slf4jLogger`
|
||||
* `MicrometerCapability` micrometerCapability: If `feign-micrometer` is on the classpath and `MeterRegistry` is available
|
||||
* `Contract` feignContract: `SpringMvcContract`
|
||||
* `Feign.Builder` feignBuilder: `HystrixFeign.Builder`
|
||||
* `Feign.Builder` feignBuilder: `FeignCircuitBreaker.Builder`
|
||||
* `Client` feignClient: If Spring Cloud LoadBalancer is on the classpath, `FeignBlockingLoadBalancerClient` is used.
|
||||
If none of them is on the classpath, the default feign client is used.
|
||||
* `Client` feignClient: if Ribbon is in the classpath and is enabled it is a `LoadBalancerFeignClient`, otherwise if Spring Cloud LoadBalancer is in the classpath, `FeignBlockingLoadBalancerClient` is used.
|
||||
If none of them is in the classpath, the default feign client is used.
|
||||
|
||||
NOTE: `spring-cloud-starter-openfeign` supports `spring-cloud-starter-loadbalancer`. However, as is an optional dependency, you need to make sure it been added to your project if you want to use it.
|
||||
NOTE: `spring-cloud-starter-openfeign` supports both `spring-cloud-starter-netflix-ribbon` and `spring-cloud-starter-loadbalancer`. However, as they are optional dependencies, you need to make sure the one you want to use has been added to your project.
|
||||
|
||||
The OkHttpClient and ApacheHttpClient and ApacheHC5 feign clients can be used by setting `feign.okhttp.enabled` or `feign.httpclient.enabled` or `feign.httpclient.hc5.enabled` to `true`, respectively, and having them on the classpath.
|
||||
You can customize the HTTP client used by providing a bean of either `org.apache.http.impl.client.CloseableHttpClient` when using Apache or `okhttp3.OkHttpClient` when using OK HTTP or `org.apache.hc.client5.http.impl.classic.CloseableHttpClient` when using Apache HC5.
|
||||
@@ -133,7 +137,6 @@ Spring Cloud OpenFeign _does not_ provide the following beans by default for fei
|
||||
* `Collection<RequestInterceptor>`
|
||||
* `SetterFactory`
|
||||
* `QueryMapEncoder`
|
||||
* `Capability` (`MicrometerCapability` is provided by default)
|
||||
|
||||
A bean of `Retryer.NEVER_RETRY` with the type `Retryer` is created by default, which will disable retrying.
|
||||
Notice this retrying behavior is different from the Feign default one, where it will automatically retry IOExceptions,
|
||||
@@ -145,15 +148,15 @@ Creating a bean of one of those type and placing it in a `@FeignClient` configur
|
||||
----
|
||||
@Configuration
|
||||
public class FooConfiguration {
|
||||
@Bean
|
||||
public Contract feignContract() {
|
||||
return new feign.Contract.Default();
|
||||
}
|
||||
@Bean
|
||||
public Contract feignContract() {
|
||||
return new feign.Contract.Default();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
|
||||
return new BasicAuthRequestInterceptor("user", "password");
|
||||
}
|
||||
@Bean
|
||||
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
|
||||
return new BasicAuthRequestInterceptor("user", "password");
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
@@ -165,29 +168,25 @@ application.yml
|
||||
[source,yaml]
|
||||
----
|
||||
feign:
|
||||
client:
|
||||
config:
|
||||
feignName:
|
||||
connectTimeout: 5000
|
||||
readTimeout: 5000
|
||||
loggerLevel: full
|
||||
errorDecoder: com.example.SimpleErrorDecoder
|
||||
retryer: com.example.SimpleRetryer
|
||||
defaultQueryParameters:
|
||||
query: queryValue
|
||||
defaultRequestHeaders:
|
||||
header: headerValue
|
||||
requestInterceptors:
|
||||
- com.example.FooRequestInterceptor
|
||||
- com.example.BarRequestInterceptor
|
||||
decode404: false
|
||||
encoder: com.example.SimpleEncoder
|
||||
decoder: com.example.SimpleDecoder
|
||||
contract: com.example.SimpleContract
|
||||
capabilities:
|
||||
- com.example.FooCapability
|
||||
- com.example.BarCapability
|
||||
metrics.enabled: false
|
||||
client:
|
||||
config:
|
||||
feignName:
|
||||
connectTimeout: 5000
|
||||
readTimeout: 5000
|
||||
loggerLevel: full
|
||||
errorDecoder: com.example.SimpleErrorDecoder
|
||||
retryer: com.example.SimpleRetryer
|
||||
defaultQueryParameters:
|
||||
query: queryValue
|
||||
defaultRequestHeaders:
|
||||
header: headerValue
|
||||
requestInterceptors:
|
||||
- com.example.FooRequestInterceptor
|
||||
- com.example.BarRequestInterceptor
|
||||
decode404: false
|
||||
encoder: com.example.SimpleEncoder
|
||||
decoder: com.example.SimpleDecoder
|
||||
contract: com.example.SimpleContract
|
||||
----
|
||||
|
||||
Default configurations can be specified in the `@EnableFeignClients` attribute `defaultConfiguration` in a similar manner as described above. The difference is that this configuration will apply to _all_ feign clients.
|
||||
@@ -200,18 +199,38 @@ application.yml
|
||||
[source,yaml]
|
||||
----
|
||||
feign:
|
||||
client:
|
||||
config:
|
||||
default:
|
||||
connectTimeout: 5000
|
||||
readTimeout: 5000
|
||||
loggerLevel: basic
|
||||
client:
|
||||
config:
|
||||
default:
|
||||
connectTimeout: 5000
|
||||
readTimeout: 5000
|
||||
loggerLevel: basic
|
||||
----
|
||||
|
||||
If we create both `@Configuration` bean and configuration properties, configuration properties will win.
|
||||
It will override `@Configuration` values. But if you want to change the priority to `@Configuration`,
|
||||
you can change `feign.client.default-to-properties` to `false`.
|
||||
|
||||
NOTE: If you need to use `ThreadLocal` bound variables in your `RequestInterceptor`s you will need to either set the
|
||||
thread isolation strategy for Hystrix to `SEMAPHORE` or disable Hystrix in Feign.
|
||||
|
||||
application.yml
|
||||
[source,yaml]
|
||||
----
|
||||
# To disable Hystrix in Feign
|
||||
feign:
|
||||
hystrix:
|
||||
enabled: false
|
||||
|
||||
# To set thread isolation to SEMAPHORE
|
||||
hystrix:
|
||||
command:
|
||||
default:
|
||||
execution:
|
||||
isolation:
|
||||
strategy: SEMAPHORE
|
||||
----
|
||||
|
||||
If we want to create multiple feign clients with the same name or url
|
||||
so that they would point to the same server but each with a different custom configuration then
|
||||
we have to use `contextId` attribute of the `@FeignClient` in order to avoid name
|
||||
@@ -221,7 +240,7 @@ collision of these configuration beans.
|
||||
----
|
||||
@FeignClient(contextId = "fooClient", name = "stores", configuration = FooConfiguration.class)
|
||||
public interface FooClient {
|
||||
//..
|
||||
//..
|
||||
}
|
||||
----
|
||||
|
||||
@@ -229,7 +248,7 @@ public interface FooClient {
|
||||
----
|
||||
@FeignClient(contextId = "barClient", name = "stores", configuration = BarConfiguration.class)
|
||||
public interface BarClient {
|
||||
//..
|
||||
//..
|
||||
}
|
||||
----
|
||||
|
||||
@@ -274,6 +293,32 @@ We can configure timeouts on both the default and the named client. OpenFeign wo
|
||||
|
||||
NOTE: In case the server is not running or available a packet results in _connection refused_. The communication ends either with an error message or in a fallback. This can happen _before_ the `connectTimeout` if it is set very low. The time taken to perform a lookup and to receive such a packet causes a significant part of this delay. It is subject to change based on the remote host that involves a DNS lookup.
|
||||
|
||||
When Hystrix <<spring-cloud-openfeign#spring-cloud-feign-hystrix,is enabled>>, its timeout configuration link:https://github.com/Netflix/Hystrix/wiki/Configuration#execution.isolation.thread.timeoutInMilliseconds[defaults] to 1000 milliseconds. Hence, it might occur before the client timeout that we configured earlier. Increasing this timeout prevents it from happening.
|
||||
|
||||
[source,yaml]
|
||||
----
|
||||
feign:
|
||||
client:
|
||||
config:
|
||||
default:
|
||||
connectTimeout: 5000
|
||||
readTimeout: 5000
|
||||
hystrix:
|
||||
enabled: true
|
||||
|
||||
hystrix:
|
||||
command:
|
||||
default:
|
||||
execution:
|
||||
timeout:
|
||||
enabled: true
|
||||
isolation:
|
||||
thread:
|
||||
timeoutInMilliseconds: 60000
|
||||
----
|
||||
|
||||
NOTE: When the Hystrix timeout is enabled and its timeout is set longer than that of a feign client, `HystrixTimeoutException` wraps a feign exception. Otherwise, the only difference is the cause of the exception. The purpose of `HystrixTimeoutException` is to wrap any runtime exception that occurs first and throw an instance of itself.
|
||||
|
||||
=== Creating Feign Clients Manually
|
||||
|
||||
In some cases it might be necessary to customize your Feign Clients in a way that is not
|
||||
@@ -291,13 +336,12 @@ class FooController {
|
||||
|
||||
private FooClient adminClient;
|
||||
|
||||
@Autowired
|
||||
public FooController(Client client, Encoder encoder, Decoder decoder, Contract contract, MicrometerCapability micrometerCapability) {
|
||||
@Autowired
|
||||
public FooController(Decoder decoder, Encoder encoder, Client client, Contract contract) {
|
||||
this.fooClient = Feign.builder().client(client)
|
||||
.encoder(encoder)
|
||||
.decoder(decoder)
|
||||
.contract(contract)
|
||||
.addCapability(micrometerCapability)
|
||||
.requestInterceptor(new BasicAuthRequestInterceptor("user", "user"))
|
||||
.target(FooClient.class, "https://PROD-SVC");
|
||||
|
||||
@@ -305,10 +349,9 @@ class FooController {
|
||||
.encoder(encoder)
|
||||
.decoder(decoder)
|
||||
.contract(contract)
|
||||
.addCapability(micrometerCapability)
|
||||
.requestInterceptor(new BasicAuthRequestInterceptor("admin", "admin"))
|
||||
.target(FooClient.class, "https://PROD-SVC");
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
@@ -324,6 +367,76 @@ the default Feign native annotations.
|
||||
You can also use the `Builder`to configure FeignClient not to inherit beans from the parent context.
|
||||
You can do this by overriding calling `inheritParentContext(false)` on the `Builder`.
|
||||
|
||||
[[spring-cloud-feign-hystrix]]
|
||||
=== Feign Hystrix Support
|
||||
|
||||
If Hystrix is on the classpath and `feign.hystrix.enabled=true`, Feign will wrap all methods with a circuit breaker. Returning a `com.netflix.hystrix.HystrixCommand` is also available. This lets you use reactive patterns (with a call to `.toObservable()` or `.observe()` or asynchronous use (with a call to `.queue()`).
|
||||
|
||||
To disable Hystrix support on a per-client basis create a vanilla `Feign.Builder` with the "prototype" scope, e.g.:
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
@Configuration
|
||||
public class FooConfiguration {
|
||||
@Bean
|
||||
@Scope("prototype")
|
||||
public Feign.Builder feignBuilder() {
|
||||
return Feign.builder();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
WARNING: Prior to the Spring Cloud Dalston release, if Hystrix was on the classpath Feign would have wrapped
|
||||
all methods in a circuit breaker by default. This default behavior was changed in Spring Cloud Dalston in
|
||||
favor for an opt-in approach.
|
||||
|
||||
[[spring-cloud-feign-hystrix-fallback]]
|
||||
=== Feign Hystrix Fallbacks
|
||||
|
||||
Hystrix supports the notion of a fallback: a default code path that is executed when the circuit is open or there is an error. To enable fallbacks for a given `@FeignClient` set the `fallback` attribute to the class name that implements the fallback. You also need to declare your implementation as a Spring bean.
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
@FeignClient(name = "hello", fallback = HystrixClientFallback.class)
|
||||
protected interface HystrixClient {
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
Hello iFailSometimes();
|
||||
}
|
||||
|
||||
static class HystrixClientFallback implements HystrixClient {
|
||||
@Override
|
||||
public Hello iFailSometimes() {
|
||||
return new Hello("fallback");
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
If one needs access to the cause that made the fallback trigger, one can use the `fallbackFactory` attribute inside `@FeignClient`.
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
@FeignClient(name = "hello", fallbackFactory = HystrixClientFallbackFactory.class)
|
||||
protected interface HystrixClient {
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
Hello iFailSometimes();
|
||||
}
|
||||
|
||||
@Component
|
||||
static class HystrixClientFallbackFactory implements FallbackFactory<HystrixClient> {
|
||||
@Override
|
||||
public HystrixClient create(Throwable cause) {
|
||||
return new HystrixClient() {
|
||||
@Override
|
||||
public Hello iFailSometimes() {
|
||||
return new Hello("fallback; reason was: " + cause.getMessage());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
WARNING: There is a limitation with the implementation of fallbacks in Feign and how Hystrix fallbacks work. Fallbacks are currently not supported for methods that return `com.netflix.hystrix.HystrixCommand` and `rx.Observable`.
|
||||
|
||||
[[spring-cloud-feign-circuitbreaker]]
|
||||
=== Feign Spring Cloud CircuitBreaker Support
|
||||
|
||||
@@ -335,7 +448,7 @@ To disable Spring Cloud CircuitBreaker support on a per-client basis create a va
|
||||
----
|
||||
@Configuration
|
||||
public class FooConfiguration {
|
||||
@Bean
|
||||
@Bean
|
||||
@Scope("prototype")
|
||||
public Feign.Builder feignBuilder() {
|
||||
return Feign.builder();
|
||||
@@ -348,81 +461,23 @@ The circuit breaker name follows this pattern `<feignClientName>#<calledMethod>`
|
||||
[[spring-cloud-feign-circuitbreaker-fallback]]
|
||||
=== Feign Spring Cloud CircuitBreaker Fallbacks
|
||||
|
||||
Spring Cloud CircuitBreaker supports the notion of a fallback: a default code path that is executed when they circuit is open or there is an error. To enable fallbacks for a given `@FeignClient` set the `fallback` attribute to the class name that implements the fallback. You also need to declare your implementation as a Spring bean.
|
||||
Spring Cloud CircuitBreaker supports the notion of a fallback: a default code path that is executed when the circuit is open or there is an error. To enable fallbacks for a given `@FeignClient` set the `fallback` attribute to the class name that implements the fallback. You also need to declare your implementation as a Spring bean.
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
@FeignClient(name = "test", url = "http://localhost:${server.port}/", fallback = Fallback.class)
|
||||
protected interface TestClient {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
Hello getHello();
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hellonotfound")
|
||||
String getException();
|
||||
|
||||
}
|
||||
|
||||
@Component
|
||||
static class Fallback implements TestClient {
|
||||
|
||||
@Override
|
||||
public Hello getHello() {
|
||||
throw new NoFallbackAvailableException("Boom!", new RuntimeException());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getException() {
|
||||
return "Fixed response";
|
||||
}
|
||||
|
||||
}
|
||||
include::{core_path}/src/test/java/org/springframework/cloud/openfeign/circuitbreaker/CircuitBreakerTests.java[tags=client_with_fallback, indent=0]
|
||||
----
|
||||
|
||||
If one needs access to the cause that made the fallback trigger, one can use the `fallbackFactory` attribute inside `@FeignClient`.
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
@FeignClient(name = "testClientWithFactory", url = "http://localhost:${server.port}/",
|
||||
fallbackFactory = TestFallbackFactory.class)
|
||||
protected interface TestClientWithFactory {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
Hello getHello();
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hellonotfound")
|
||||
String getException();
|
||||
|
||||
}
|
||||
|
||||
@Component
|
||||
static class TestFallbackFactory implements FallbackFactory<FallbackWithFactory> {
|
||||
|
||||
@Override
|
||||
public FallbackWithFactory create(Throwable cause) {
|
||||
return new FallbackWithFactory();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class FallbackWithFactory implements TestClientWithFactory {
|
||||
|
||||
@Override
|
||||
public Hello getHello() {
|
||||
throw new NoFallbackAvailableException("Boom!", new RuntimeException());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getException() {
|
||||
return "Fixed response";
|
||||
}
|
||||
|
||||
}
|
||||
include::{core_path}/src/test/java/org/springframework/cloud/openfeign/circuitbreaker/CircuitBreakerTests.java[tags=client_with_fallback_factory, indent=0]
|
||||
----
|
||||
|
||||
=== Feign and `@Primary`
|
||||
|
||||
When using Feign with Spring Cloud CircuitBreaker fallbacks, there are multiple beans in the `ApplicationContext` of the same type. This will cause `@Autowired` to not work because there isn't exactly one bean, or one marked as primary. To work around this, Spring Cloud OpenFeign marks all Feign instances as `@Primary`, so Spring Framework will know which bean to inject. In some cases, this may not be desirable. To turn off this behavior set the `primary` attribute of `@FeignClient` to false.
|
||||
When using Feign with Hystrix fallbacks, there are multiple beans in the `ApplicationContext` of the same type. This will cause `@Autowired` to not work because there isn't exactly one bean, or one marked as primary. To work around this, Spring Cloud OpenFeign marks all Feign instances as `@Primary`, so Spring Framework will know which bean to inject. In some cases, this may not be desirable. To turn off this behavior set the `primary` attribute of `@FeignClient` to false.
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
@@ -443,8 +498,8 @@ This allows grouping common operations into convenient base interfaces.
|
||||
----
|
||||
public interface UserService {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value ="/users/{id}")
|
||||
User getUser(@PathVariable("id") long id);
|
||||
@RequestMapping(method = RequestMethod.GET, value ="/users/{id}")
|
||||
User getUser(@PathVariable("id") long id);
|
||||
}
|
||||
----
|
||||
|
||||
@@ -527,63 +582,10 @@ For example, the following would set the `Logger.Level` to `FULL`:
|
||||
----
|
||||
@Configuration
|
||||
public class FooConfiguration {
|
||||
@Bean
|
||||
Logger.Level feignLoggerLevel() {
|
||||
return Logger.Level.FULL;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
=== Feign Capability support
|
||||
|
||||
The Feign capabilities expose core Feign components so that these components can be modified. For example, the capabilities can take the `Client`, _decorate_ it, and give the decorated instance back to Feign.
|
||||
The support for metrics libraries is a good real-life example for this. See <<feign-metrics>>.
|
||||
|
||||
Creating one or more `Capability` beans and placing them in a `@FeignClient` configuration lets you register them and modify the behavior of the involved client.
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
@Configuration
|
||||
public class FooConfiguration {
|
||||
@Bean
|
||||
Capability customCapability() {
|
||||
return new CustomCapability();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
=== Feign metrics
|
||||
|
||||
If all of the following conditions are true, a `MicrometerCapability` bean is created and registered so that your Feign client publishes metrics to Micrometer:
|
||||
|
||||
* `feign-micrometer` is on the classpath
|
||||
* A `MeterRegistry` bean is available
|
||||
* feign metrics properties are set to `true` (by default)
|
||||
- `feign.metrics.enabled=true` (for all clients)
|
||||
- `feign.client.config.feignName.metrics.enabled=true` (for a single client)
|
||||
|
||||
NOTE: If your application already uses Micrometer, enabling metrics is as simple as putting `feign-micrometer` onto your classpath.
|
||||
|
||||
You can also disable the feature by either:
|
||||
|
||||
* excluding `feign-micrometer` from your classpath
|
||||
* setting one of the feign metrics properties to `false`
|
||||
- `feign.metrics.enabled=false`
|
||||
- `feign.client.config.feignName.metrics.enabled=false`
|
||||
|
||||
NOTE: `feign.metrics.enabled=false` disables metrics support for *all* Feign clients regardless of the value of the client-level flags: `feign.client.config.feignName.metrics.enabled`.
|
||||
If you want to enable or disable merics per client, don't set `feign.metrics.enabled` and use `feign.client.config.feignName.metrics.enabled`.
|
||||
|
||||
You can also customize the `MicrometerCapability` by registering your own bean:
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
@Configuration
|
||||
public class FooConfiguration {
|
||||
@Bean
|
||||
public MicrometerCapability micrometerCapability(MeterRegistry meterRegistry) {
|
||||
return new MicrometerCapability(meterRegistry);
|
||||
}
|
||||
@Bean
|
||||
Logger.Level feignLoggerLevel() {
|
||||
return Logger.Level.FULL;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
@@ -602,10 +604,10 @@ For example, the `Params` class defines parameters `param1` and `param2`:
|
||||
----
|
||||
// Params.java
|
||||
public class Params {
|
||||
private String param1;
|
||||
private String param2;
|
||||
private String param1;
|
||||
private String param2;
|
||||
|
||||
// [Getters and setters omitted for brevity]
|
||||
// [Getters and setters omitted for brevity]
|
||||
}
|
||||
----
|
||||
|
||||
@@ -616,8 +618,8 @@ The following feign client uses the `Params` class by using the `@SpringQueryMap
|
||||
@FeignClient("demo")
|
||||
public interface DemoTemplate {
|
||||
|
||||
@GetMapping(path = "/demo")
|
||||
String demoEndpoint(@SpringQueryMap Params params);
|
||||
@GetMapping(path = "/demo")
|
||||
String demoEndpoint(@SpringQueryMap Params params);
|
||||
}
|
||||
----
|
||||
|
||||
@@ -638,8 +640,8 @@ and deserialize HATEOAS representation models: https://docs.spring.io/spring-hat
|
||||
@FeignClient("demo")
|
||||
public interface DemoTemplate {
|
||||
|
||||
@GetMapping(path = "/stores")
|
||||
CollectionModel<Store> getStores();
|
||||
@GetMapping(path = "/stores")
|
||||
CollectionModel<Store> getStores();
|
||||
}
|
||||
----
|
||||
|
||||
@@ -668,13 +670,13 @@ Note that both variable name and the path segment placeholder are called `matrix
|
||||
@FeignClient("demo")
|
||||
public interface DemoTemplate {
|
||||
|
||||
@GetMapping(path = "/stores")
|
||||
CollectionModel<Store> getStores();
|
||||
@GetMapping(path = "/stores")
|
||||
CollectionModel<Store> getStores();
|
||||
}
|
||||
----
|
||||
|
||||
=== Feign `CollectionFormat` support
|
||||
We support `feign.CollectionFormat` by providing the `@CollectionFormat` annotation.You can annotate a Feign client method with it by passing the desired `feign.CollectionFormat` as annotation value.
|
||||
We support `feign.CollectionFormat` by providing the `@CollectionFormat` annotation. You can annotate a Feign client method with it by passing the desired `feign.CollectionFormat` as annotation value.
|
||||
|
||||
In the following example, the `CSV` format is used instead of the default `EXPLODED` to process the method.
|
||||
|
||||
@@ -693,7 +695,7 @@ In the following example, the `CSV` format is used instead of the default `EXPLO
|
||||
TIP: Set the `CSV` format while sending `Pageable` as a query parameter in order for it to be encoded correctly.
|
||||
|
||||
=== Reactive Support
|
||||
As the https://github.com/OpenFeign/feign[OpenFeign project] does not currently support reactive clients, such as https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/reactive/function/client/WebClient.html[Spring WebClient], neither does Spring Cloud OpenFeign.We will add support for it here as soon as it becomes available in the core project.
|
||||
As the https://github.com/OpenFeign/feign[OpenFeign project] does not currently support reactive clients, such as https://docs.spring.io/spring/docs/current/javadoc-api/org/springframework/web/reactive/function/client/WebClient.html[Spring WebClient], neither does Spring Cloud OpenFeign. We will add support for it here as soon as it becomes available in the core project.
|
||||
|
||||
Until that is done, we recommend using https://github.com/Playtika/feign-reactive[feign-reactive] for Spring WebClient support.
|
||||
|
||||
@@ -709,7 +711,6 @@ ObjectProvider<TestFeginClient> testFeginClient;
|
||||
----
|
||||
|
||||
=== Spring Data Support
|
||||
|
||||
You may consider enabling Jackson Modules for the support `org.springframework.data.domain.Page` and `org.springframework.data.domain.Sort` decoding.
|
||||
|
||||
[source,java]
|
||||
|
||||
@@ -4,14 +4,14 @@
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>spring-cloud-openfeign</artifactId>
|
||||
<version>3.0.2</version>
|
||||
<version>2.2.8.RELEASE</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>Spring Cloud OpenFeign</name>
|
||||
<description>Spring Cloud OpenFeign</description>
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-build</artifactId>
|
||||
<version>3.0.2</version>
|
||||
<version>2.3.4.RELEASE</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
<scm>
|
||||
@@ -26,7 +26,8 @@
|
||||
<properties>
|
||||
<main.basedir>${basedir}</main.basedir>
|
||||
<jackson.version>2.11.3</jackson.version>
|
||||
<spring-cloud-commons.version>3.0.2</spring-cloud-commons.version>
|
||||
<spring-cloud-commons.version>2.2.8.RELEASE</spring-cloud-commons.version>
|
||||
<spring-cloud-netflix.version>2.2.8.RELEASE</spring-cloud-netflix.version>
|
||||
|
||||
<!-- Plugin versions -->
|
||||
<maven-eclipse-plugin.version>2.10</maven-eclipse-plugin.version>
|
||||
@@ -93,6 +94,13 @@
|
||||
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-netflix-dependencies</artifactId>
|
||||
<version>${spring-cloud-netflix.version}</version>
|
||||
<type>pom</type>
|
||||
<scope>import</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-commons-dependencies</artifactId>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-openfeign</artifactId>
|
||||
<version>3.0.2</version>
|
||||
<version>2.2.8.RELEASE</version>
|
||||
<relativePath>..</relativePath> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<artifactId>spring-cloud-openfeign-core</artifactId>
|
||||
@@ -21,6 +21,10 @@
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-autoconfigure</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-netflix-ribbon</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-actuator</artifactId>
|
||||
@@ -85,6 +89,11 @@
|
||||
<artifactId>spring-cloud-context</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.netflix.ribbon</groupId>
|
||||
<artifactId>ribbon-loadbalancer</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.openfeign</groupId>
|
||||
<artifactId>feign-core</artifactId>
|
||||
@@ -101,7 +110,7 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.openfeign</groupId>
|
||||
<artifactId>feign-micrometer</artifactId>
|
||||
<artifactId>feign-hc5</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -111,7 +120,7 @@
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.openfeign</groupId>
|
||||
<artifactId>feign-hc5</artifactId>
|
||||
<artifactId>feign-hystrix</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
@@ -119,6 +128,36 @@
|
||||
<artifactId>feign-okhttp</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.netflix.hystrix</groupId>
|
||||
<artifactId>hystrix-core</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.netflix.hystrix</groupId>
|
||||
<artifactId>hystrix-serialization</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.netflix.hystrix</groupId>
|
||||
<artifactId>hystrix-metrics-event-stream</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.netflix.hystrix</groupId>
|
||||
<artifactId>hystrix-javanica</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.netflix.ribbon</groupId>
|
||||
<artifactId>ribbon-core</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.netflix.ribbon</groupId>
|
||||
<artifactId>ribbon-httpclient</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.reactivex</groupId>
|
||||
<artifactId>rxjava</artifactId>
|
||||
@@ -129,11 +168,6 @@
|
||||
<artifactId>okhttp</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security.oauth.boot</groupId>
|
||||
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-autoconfigure-processor</artifactId>
|
||||
@@ -149,11 +183,6 @@
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.junit.vintage</groupId>
|
||||
<artifactId>junit-vintage-engine</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-test-support</artifactId>
|
||||
@@ -169,6 +198,16 @@
|
||||
<artifactId>reactor-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.netflix.ribbon</groupId>
|
||||
<artifactId>ribbon</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-rest</artifactId>
|
||||
|
||||
+2
-1
@@ -43,7 +43,8 @@ public interface AnnotatedParameterProcessor {
|
||||
* @param method the method that contains the annotation
|
||||
* @return whether the parameter is http
|
||||
*/
|
||||
boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method);
|
||||
boolean processArgument(AnnotatedParameterContext context, Annotation annotation,
|
||||
Method method);
|
||||
|
||||
/**
|
||||
* Specifies the parameter context.
|
||||
|
||||
+2
-2
@@ -25,8 +25,8 @@ import feign.Target;
|
||||
class DefaultTargeter implements Targeter {
|
||||
|
||||
@Override
|
||||
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
|
||||
Target.HardCodedTarget<T> target) {
|
||||
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
|
||||
FeignContext context, Target.HardCodedTarget<T> target) {
|
||||
return feign.target(target);
|
||||
}
|
||||
|
||||
|
||||
+63
-39
@@ -28,7 +28,6 @@ import javax.annotation.PreDestroy;
|
||||
import com.fasterxml.jackson.databind.Module;
|
||||
import feign.Client;
|
||||
import feign.Feign;
|
||||
import feign.RequestInterceptor;
|
||||
import feign.hc5.ApacheHttp5Client;
|
||||
import feign.httpclient.ApacheHttpClient;
|
||||
import feign.okhttp.OkHttpClient;
|
||||
@@ -42,9 +41,11 @@ import org.apache.http.conn.HttpClientConnectionManager;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.condition.AllNestedConditions;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.client.actuator.HasFeatures;
|
||||
@@ -54,7 +55,6 @@ import org.springframework.cloud.commons.httpclient.ApacheHttpClientConnectionMa
|
||||
import org.springframework.cloud.commons.httpclient.ApacheHttpClientFactory;
|
||||
import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory;
|
||||
import org.springframework.cloud.commons.httpclient.OkHttpClientFactory;
|
||||
import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptor;
|
||||
import org.springframework.cloud.openfeign.support.DefaultGzipDecoderConfiguration;
|
||||
import org.springframework.cloud.openfeign.support.FeignEncoderProperties;
|
||||
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
|
||||
@@ -66,8 +66,6 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.security.oauth2.client.OAuth2ClientContext;
|
||||
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
@@ -80,8 +78,8 @@ import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResour
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(Feign.class)
|
||||
@EnableConfigurationProperties({ FeignClientProperties.class, FeignHttpClientProperties.class,
|
||||
FeignEncoderProperties.class })
|
||||
@EnableConfigurationProperties({ FeignClientProperties.class,
|
||||
FeignHttpClientProperties.class, FeignEncoderProperties.class })
|
||||
@Import(DefaultGzipDecoderConfiguration.class)
|
||||
public class FeignAutoConfiguration {
|
||||
|
||||
@@ -104,7 +102,8 @@ public class FeignAutoConfiguration {
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass({ Module.class, Page.class, Sort.class })
|
||||
@ConditionalOnProperty(value = "feign.autoconfiguration.jackson.enabled", havingValue = "true")
|
||||
@ConditionalOnProperty(value = "feign.autoconfiguration.jackson.enabled",
|
||||
havingValue = "true")
|
||||
protected static class FeignJacksonConfiguration {
|
||||
|
||||
@Bean
|
||||
@@ -122,7 +121,7 @@ public class FeignAutoConfiguration {
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Conditional(FeignCircuitBreakerDisabledConditions.class)
|
||||
@Conditional(DefaultFeignTargeterConditions.class)
|
||||
protected static class DefaultFeignTargeterConfiguration {
|
||||
|
||||
@Bean
|
||||
@@ -133,32 +132,43 @@ public class FeignAutoConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Conditional(FeignCircuitBreakerDisabledConditions.class)
|
||||
@ConditionalOnClass(name = "feign.hystrix.HystrixFeign")
|
||||
@ConditionalOnProperty(value = "feign.hystrix.enabled", havingValue = "true",
|
||||
matchIfMissing = true)
|
||||
protected static class HystrixFeignTargeterConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public Targeter feignTargeter() {
|
||||
return new HystrixTargeter();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(CircuitBreaker.class)
|
||||
@ConditionalOnProperty(value = "feign.circuitbreaker.enabled", havingValue = "true")
|
||||
protected static class CircuitBreakerPresentFeignTargeterConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(CircuitBreakerFactory.class)
|
||||
public Targeter defaultFeignTargeter() {
|
||||
return new DefaultTargeter();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnBean(CircuitBreakerFactory.class)
|
||||
public Targeter circuitBreakerFeignTargeter(CircuitBreakerFactory circuitBreakerFactory) {
|
||||
public Targeter circuitBreakerFeignTargeter(
|
||||
CircuitBreakerFactory circuitBreakerFactory) {
|
||||
return new FeignCircuitBreakerTargeter(circuitBreakerFactory);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// the following configuration is for alternate feign clients if
|
||||
// SC loadbalancer is not on the class path.
|
||||
// see corresponding configurations in FeignLoadBalancerAutoConfiguration
|
||||
// for load-balanced clients.
|
||||
// ribbon is not on the class path.
|
||||
// see corresponding configurations in FeignRibbonClientAutoConfiguration
|
||||
// for load balanced ribbon clients.
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(ApacheHttpClient.class)
|
||||
@ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
|
||||
@ConditionalOnMissingBean(CloseableHttpClient.class)
|
||||
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
|
||||
@Conditional(HttpClient5DisabledConditions.class)
|
||||
@@ -177,10 +187,13 @@ public class FeignAutoConfiguration {
|
||||
public HttpClientConnectionManager connectionManager(
|
||||
ApacheHttpClientConnectionManagerFactory connectionManagerFactory,
|
||||
FeignHttpClientProperties httpClientProperties) {
|
||||
final HttpClientConnectionManager connectionManager = connectionManagerFactory.newConnectionManager(
|
||||
httpClientProperties.isDisableSslValidation(), httpClientProperties.getMaxConnections(),
|
||||
httpClientProperties.getMaxConnectionsPerRoute(), httpClientProperties.getTimeToLive(),
|
||||
httpClientProperties.getTimeToLiveUnit(), this.registryBuilder);
|
||||
final HttpClientConnectionManager connectionManager = connectionManagerFactory
|
||||
.newConnectionManager(httpClientProperties.isDisableSslValidation(),
|
||||
httpClientProperties.getMaxConnections(),
|
||||
httpClientProperties.getMaxConnectionsPerRoute(),
|
||||
httpClientProperties.getTimeToLive(),
|
||||
httpClientProperties.getTimeToLiveUnit(),
|
||||
this.registryBuilder);
|
||||
this.connectionManagerTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -196,8 +209,10 @@ public class FeignAutoConfiguration {
|
||||
FeignHttpClientProperties httpClientProperties) {
|
||||
RequestConfig defaultRequestConfig = RequestConfig.custom()
|
||||
.setConnectTimeout(httpClientProperties.getConnectionTimeout())
|
||||
.setRedirectsEnabled(httpClientProperties.isFollowRedirects()).build();
|
||||
this.httpClient = httpClientFactory.createBuilder().setConnectionManager(httpClientConnectionManager)
|
||||
.setRedirectsEnabled(httpClientProperties.isFollowRedirects())
|
||||
.build();
|
||||
this.httpClient = httpClientFactory.createBuilder()
|
||||
.setConnectionManager(httpClientConnectionManager)
|
||||
.setDefaultRequestConfig(defaultRequestConfig).build();
|
||||
return this.httpClient;
|
||||
}
|
||||
@@ -227,6 +242,7 @@ public class FeignAutoConfiguration {
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(OkHttpClient.class)
|
||||
@ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
|
||||
@ConditionalOnMissingBean(okhttp3.OkHttpClient.class)
|
||||
@ConditionalOnProperty("feign.okhttp.enabled")
|
||||
protected static class OkHttpFeignConfiguration {
|
||||
@@ -235,7 +251,8 @@ public class FeignAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(ConnectionPool.class)
|
||||
public ConnectionPool httpClientConnectionPool(FeignHttpClientProperties httpClientProperties,
|
||||
public ConnectionPool httpClientConnectionPool(
|
||||
FeignHttpClientProperties httpClientProperties,
|
||||
OkHttpClientConnectionPoolFactory connectionPoolFactory) {
|
||||
Integer maxTotalConnections = httpClientProperties.getMaxConnections();
|
||||
Long timeToLive = httpClientProperties.getTimeToLive();
|
||||
@@ -244,14 +261,16 @@ public class FeignAutoConfiguration {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory, ConnectionPool connectionPool,
|
||||
public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory,
|
||||
ConnectionPool connectionPool,
|
||||
FeignHttpClientProperties httpClientProperties) {
|
||||
Boolean followRedirects = httpClientProperties.isFollowRedirects();
|
||||
Integer connectTimeout = httpClientProperties.getConnectionTimeout();
|
||||
Boolean disableSslValidation = httpClientProperties.isDisableSslValidation();
|
||||
this.okHttpClient = httpClientFactory.createBuilder(disableSslValidation)
|
||||
.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS).followRedirects(followRedirects)
|
||||
.connectionPool(connectionPool).build();
|
||||
.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
|
||||
.followRedirects(followRedirects).connectionPool(connectionPool)
|
||||
.build();
|
||||
return this.okHttpClient;
|
||||
}
|
||||
|
||||
@@ -273,6 +292,7 @@ public class FeignAutoConfiguration {
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(ApacheHttp5Client.class)
|
||||
@ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
|
||||
@ConditionalOnMissingBean(org.apache.hc.client5.http.impl.classic.CloseableHttpClient.class)
|
||||
@ConditionalOnProperty(value = "feign.httpclient.hc5.enabled", havingValue = "true")
|
||||
@Import(org.springframework.cloud.openfeign.clientconfig.HttpClient5FeignConfiguration.class)
|
||||
@@ -280,23 +300,27 @@ public class FeignAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(Client.class)
|
||||
public Client feignClient(org.apache.hc.client5.http.impl.classic.CloseableHttpClient httpClient5) {
|
||||
public Client feignClient(
|
||||
org.apache.hc.client5.http.impl.classic.CloseableHttpClient httpClient5) {
|
||||
return new ApacheHttp5Client(httpClient5);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(OAuth2ClientContext.class)
|
||||
@ConditionalOnProperty("feign.oauth2.enabled")
|
||||
protected static class Oauth2FeignConfiguration {
|
||||
static class DefaultFeignTargeterConditions extends AllNestedConditions {
|
||||
|
||||
DefaultFeignTargeterConditions() {
|
||||
super(ConfigurationPhase.PARSE_CONFIGURATION);
|
||||
}
|
||||
|
||||
@Conditional(FeignCircuitBreakerDisabledConditions.class)
|
||||
static class FeignCircuitBreakerDisabled {
|
||||
|
||||
}
|
||||
|
||||
@Conditional(HystrixDisabledConditions.class)
|
||||
static class HystrixDisabled {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(OAuth2FeignRequestInterceptor.class)
|
||||
@ConditionalOnBean({ OAuth2ClientContext.class, OAuth2ProtectedResourceDetails.class })
|
||||
public RequestInterceptor oauth2FeignRequestInterceptor(OAuth2ClientContext oAuth2ClientContext,
|
||||
OAuth2ProtectedResourceDetails resource) {
|
||||
return new OAuth2FeignRequestInterceptor(oAuth2ClientContext, resource);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
* Copyright 2013-2019 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.
|
||||
|
||||
+10
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
* Copyright 2013-2020 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.
|
||||
@@ -61,10 +61,13 @@ public final class FeignCircuitBreaker {
|
||||
}
|
||||
|
||||
public <T> T target(Target<T> target, T fallback) {
|
||||
return build(fallback != null ? new FallbackFactory.Default<T>(fallback) : null).newInstance(target);
|
||||
return build(
|
||||
fallback != null ? new FallbackFactory.Default<T>(fallback) : null)
|
||||
.newInstance(target);
|
||||
}
|
||||
|
||||
public <T> T target(Target<T> target, FallbackFactory<? extends T> fallbackFactory) {
|
||||
public <T> T target(Target<T> target,
|
||||
FallbackFactory<? extends T> fallbackFactory) {
|
||||
return build(fallbackFactory).newInstance(target);
|
||||
}
|
||||
|
||||
@@ -74,8 +77,10 @@ public final class FeignCircuitBreaker {
|
||||
}
|
||||
|
||||
public Feign build(final FallbackFactory<?> nullableFallbackFactory) {
|
||||
super.invocationHandlerFactory((target, dispatch) -> new FeignCircuitBreakerInvocationHandler(
|
||||
circuitBreakerFactory, target, dispatch, nullableFallbackFactory));
|
||||
super.invocationHandlerFactory(
|
||||
(target, dispatch) -> new FeignCircuitBreakerInvocationHandler(
|
||||
circuitBreakerFactory, target, dispatch,
|
||||
nullableFallbackFactory));
|
||||
return super.build();
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -31,7 +31,8 @@ class FeignCircuitBreakerDisabledConditions extends AnyNestedCondition {
|
||||
|
||||
}
|
||||
|
||||
@ConditionalOnProperty(value = "feign.circuitbreaker.enabled", havingValue = "false", matchIfMissing = true)
|
||||
@ConditionalOnProperty(value = "feign.circuitbreaker.enabled", havingValue = "false",
|
||||
matchIfMissing = true)
|
||||
static class CircuitBreakerDisabled {
|
||||
|
||||
}
|
||||
|
||||
+8
-4
@@ -46,7 +46,8 @@ class FeignCircuitBreakerInvocationHandler implements InvocationHandler {
|
||||
private final Map<Method, Method> fallbackMethodMap;
|
||||
|
||||
FeignCircuitBreakerInvocationHandler(CircuitBreakerFactory factory, Target<?> target,
|
||||
Map<Method, InvocationHandlerFactory.MethodHandler> dispatch, FallbackFactory<?> nullableFallbackFactory) {
|
||||
Map<Method, InvocationHandlerFactory.MethodHandler> dispatch,
|
||||
FallbackFactory<?> nullableFallbackFactory) {
|
||||
this.factory = factory;
|
||||
this.target = checkNotNull(target, "target");
|
||||
this.dispatch = checkNotNull(dispatch, "dispatch");
|
||||
@@ -55,12 +56,14 @@ class FeignCircuitBreakerInvocationHandler implements InvocationHandler {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object invoke(final Object proxy, final Method method, final Object[] args) throws Throwable {
|
||||
public Object invoke(final Object proxy, final Method method, final Object[] args)
|
||||
throws Throwable {
|
||||
// early exit if the invoked method is from java.lang.Object
|
||||
// code is the same as ReflectiveFeign.FeignInvocationHandler
|
||||
if ("equals".equals(method.getName())) {
|
||||
try {
|
||||
Object otherHandler = args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
|
||||
Object otherHandler = args.length > 0 && args[0] != null
|
||||
? Proxy.getInvocationHandler(args[0]) : null;
|
||||
return equals(otherHandler);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
@@ -114,7 +117,8 @@ class FeignCircuitBreakerInvocationHandler implements InvocationHandler {
|
||||
* reflection calls.
|
||||
* @return cached methods map for fallback invoking
|
||||
*/
|
||||
static Map<Method, Method> toFallbackMethod(Map<Method, InvocationHandlerFactory.MethodHandler> dispatch) {
|
||||
static Map<Method, Method> toFallbackMethod(
|
||||
Map<Method, InvocationHandlerFactory.MethodHandler> dispatch) {
|
||||
Map<Method, Method> result = new LinkedHashMap<Method, Method>();
|
||||
for (Method method : dispatch.keySet()) {
|
||||
method.setAccessible(true);
|
||||
|
||||
+28
-18
@@ -32,56 +32,66 @@ class FeignCircuitBreakerTargeter implements Targeter {
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
|
||||
Target.HardCodedTarget<T> target) {
|
||||
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
|
||||
FeignContext context, Target.HardCodedTarget<T> target) {
|
||||
if (!(feign instanceof FeignCircuitBreaker.Builder)) {
|
||||
return feign.target(target);
|
||||
}
|
||||
FeignCircuitBreaker.Builder builder = (FeignCircuitBreaker.Builder) feign;
|
||||
String name = !StringUtils.hasText(factory.getContextId()) ? factory.getName() : factory.getContextId();
|
||||
String name = !StringUtils.hasText(factory.getContextId()) ? factory.getName()
|
||||
: factory.getContextId();
|
||||
Class<?> fallback = factory.getFallback();
|
||||
if (fallback != void.class) {
|
||||
return targetWithFallback(name, context, target, builder, fallback);
|
||||
}
|
||||
Class<?> fallbackFactory = factory.getFallbackFactory();
|
||||
if (fallbackFactory != void.class) {
|
||||
return targetWithFallbackFactory(name, context, target, builder, fallbackFactory);
|
||||
return targetWithFallbackFactory(name, context, target, builder,
|
||||
fallbackFactory);
|
||||
}
|
||||
return builder(name, builder).target(target);
|
||||
}
|
||||
|
||||
private <T> T targetWithFallbackFactory(String feignClientName, FeignContext context,
|
||||
Target.HardCodedTarget<T> target, FeignCircuitBreaker.Builder builder, Class<?> fallbackFactoryClass) {
|
||||
FallbackFactory<? extends T> fallbackFactory = (FallbackFactory<? extends T>) getFromContext("fallbackFactory",
|
||||
feignClientName, context, fallbackFactoryClass, FallbackFactory.class);
|
||||
Target.HardCodedTarget<T> target, FeignCircuitBreaker.Builder builder,
|
||||
Class<?> fallbackFactoryClass) {
|
||||
FallbackFactory<? extends T> fallbackFactory = (FallbackFactory<? extends T>) getFromContext(
|
||||
"fallbackFactory", feignClientName, context, fallbackFactoryClass,
|
||||
FallbackFactory.class);
|
||||
return builder(feignClientName, builder).target(target, fallbackFactory);
|
||||
}
|
||||
|
||||
private <T> T targetWithFallback(String feignClientName, FeignContext context, Target.HardCodedTarget<T> target,
|
||||
FeignCircuitBreaker.Builder builder, Class<?> fallback) {
|
||||
T fallbackInstance = getFromContext("fallback", feignClientName, context, fallback, target.type());
|
||||
private <T> T targetWithFallback(String feignClientName, FeignContext context,
|
||||
Target.HardCodedTarget<T> target, FeignCircuitBreaker.Builder builder,
|
||||
Class<?> fallback) {
|
||||
T fallbackInstance = getFromContext("fallback", feignClientName, context,
|
||||
fallback, target.type());
|
||||
return builder(feignClientName, builder).target(target, fallbackInstance);
|
||||
}
|
||||
|
||||
private <T> T getFromContext(String fallbackMechanism, String feignClientName, FeignContext context,
|
||||
Class<?> beanType, Class<T> targetType) {
|
||||
private <T> T getFromContext(String fallbackMechanism, String feignClientName,
|
||||
FeignContext context, Class<?> beanType, Class<T> targetType) {
|
||||
Object fallbackInstance = context.getInstance(feignClientName, beanType);
|
||||
if (fallbackInstance == null) {
|
||||
throw new IllegalStateException(
|
||||
String.format("No " + fallbackMechanism + " instance of type %s found for feign client %s",
|
||||
beanType, feignClientName));
|
||||
throw new IllegalStateException(String.format(
|
||||
"No " + fallbackMechanism
|
||||
+ " instance of type %s found for feign client %s",
|
||||
beanType, feignClientName));
|
||||
}
|
||||
|
||||
if (!targetType.isAssignableFrom(beanType)) {
|
||||
throw new IllegalStateException(String.format("Incompatible " + fallbackMechanism
|
||||
throw new IllegalStateException(String.format("Incompatible "
|
||||
+ fallbackMechanism
|
||||
+ " instance. Fallback/fallbackFactory of type %s is not assignable to %s for feign client %s",
|
||||
beanType, targetType, feignClientName));
|
||||
}
|
||||
return (T) fallbackInstance;
|
||||
}
|
||||
|
||||
private FeignCircuitBreaker.Builder builder(String feignClientName, FeignCircuitBreaker.Builder builder) {
|
||||
return builder.circuitBreakerFactory(this.circuitBreakerFactory).feignClientName(feignClientName);
|
||||
private FeignCircuitBreaker.Builder builder(String feignClientName,
|
||||
FeignCircuitBreaker.Builder builder) {
|
||||
return builder.circuitBreakerFactory(this.circuitBreakerFactory)
|
||||
.feignClientName(feignClientName);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+14
-4
@@ -27,9 +27,9 @@ import org.springframework.core.annotation.AliasFor;
|
||||
|
||||
/**
|
||||
* Annotation for interfaces declaring that a REST client with that interface should be
|
||||
* created (e.g. for autowiring into another component). If SC LoadBalancer is available
|
||||
* it will be used to load balance the backend requests, and the load balancer can be
|
||||
* configured using the same name (i.e. value) as the feign client.
|
||||
* created (e.g. for autowiring into another component). If ribbon is available it will be
|
||||
* used to load balance the backend requests, and the load balancer can be configured
|
||||
* using a <code>@RibbonClient</code> with the same name (i.e. value) as the feign client.
|
||||
*
|
||||
* @author Spencer Gibb
|
||||
* @author Venil Noronha
|
||||
@@ -50,6 +50,14 @@ public @interface FeignClient {
|
||||
@AliasFor("name")
|
||||
String value() default "";
|
||||
|
||||
/**
|
||||
* The service id with optional protocol prefix. Synonym for {@link #value() value}.
|
||||
* @deprecated use {@link #name() name} instead
|
||||
* @return the service id with optional protocol prefix
|
||||
*/
|
||||
@Deprecated
|
||||
String serviceId() default "";
|
||||
|
||||
/**
|
||||
* This will be used as the bean name instead of name if present, but will not be used
|
||||
* as a service id.
|
||||
@@ -120,13 +128,15 @@ public @interface FeignClient {
|
||||
* factory must produce instances of fallback classes that implement the interface
|
||||
* annotated by {@link FeignClient}. The fallback factory must be a valid spring bean.
|
||||
*
|
||||
* @see feign.hystrix.FallbackFactory for details.
|
||||
* @see FallbackFactory for details.
|
||||
* @return fallback factory for the specified Feign client interface
|
||||
*/
|
||||
Class<?> fallbackFactory() default void.class;
|
||||
|
||||
/**
|
||||
* @return path prefix to be used by all method-level mappings.
|
||||
* @return path prefix to be used by all method-level mappings. Can be used with or
|
||||
* without <code>@RibbonClient</code>.
|
||||
*/
|
||||
String path() default "";
|
||||
|
||||
|
||||
+26
-12
@@ -16,6 +16,9 @@
|
||||
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import feign.Feign;
|
||||
import feign.hystrix.FallbackFactory;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
/**
|
||||
@@ -26,6 +29,7 @@ import org.springframework.context.ApplicationContext;
|
||||
*
|
||||
* @author Sven Döring
|
||||
* @author Matt King
|
||||
* @author Sam Kruglov
|
||||
*/
|
||||
public class FeignClientBuilder {
|
||||
|
||||
@@ -39,11 +43,6 @@ public class FeignClientBuilder {
|
||||
return new Builder<>(this.applicationContext, type, name);
|
||||
}
|
||||
|
||||
public <T> Builder<T> forType(final Class<T> type, final FeignClientFactoryBean clientFactoryBean,
|
||||
final String name) {
|
||||
return new Builder<>(this.applicationContext, clientFactoryBean, type, name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder of feign targets.
|
||||
*
|
||||
@@ -53,13 +52,9 @@ public class FeignClientBuilder {
|
||||
|
||||
private FeignClientFactoryBean feignClientFactoryBean;
|
||||
|
||||
private Builder(final ApplicationContext applicationContext, final Class<T> type, final String name) {
|
||||
this(applicationContext, new FeignClientFactoryBean(), type, name);
|
||||
}
|
||||
|
||||
private Builder(final ApplicationContext applicationContext, final FeignClientFactoryBean clientFactoryBean,
|
||||
final Class<T> type, final String name) {
|
||||
this.feignClientFactoryBean = clientFactoryBean;
|
||||
private Builder(final ApplicationContext applicationContext, final Class<T> type,
|
||||
final String name) {
|
||||
this.feignClientFactoryBean = new FeignClientFactoryBean();
|
||||
|
||||
this.feignClientFactoryBean.setApplicationContext(applicationContext);
|
||||
this.feignClientFactoryBean.setType(type);
|
||||
@@ -76,6 +71,18 @@ public class FeignClientBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a {@link FeignBuilderCustomizer} to the underlying
|
||||
* {@link Feign.Builder}. May be called multiple times.
|
||||
* @param customizer applied in the same order as supplied here after applying
|
||||
* customizers found in the context.
|
||||
* @return the {@link Builder} with the customizer added
|
||||
*/
|
||||
public Builder<T> customize(final FeignBuilderCustomizer customizer) {
|
||||
this.feignClientFactoryBean.addCustomizer(customizer);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<T> contextId(final String contextId) {
|
||||
this.feignClientFactoryBean.setContextId(contextId);
|
||||
return this;
|
||||
@@ -102,6 +109,13 @@ public class FeignClientBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<T> fallbackFactory(
|
||||
final Class<? extends FallbackFactory<? extends T>> fallbackFactory) {
|
||||
FeignClientsRegistrar.validateFallbackFactory(fallbackFactory);
|
||||
this.feignClientFactoryBean.setFallbackFactory(fallbackFactory);
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return the created Feign client
|
||||
*/
|
||||
|
||||
+114
-64
@@ -22,7 +22,6 @@ import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import feign.Capability;
|
||||
import feign.Client;
|
||||
import feign.Contract;
|
||||
import feign.ExceptionPropagationPolicy;
|
||||
@@ -36,6 +35,8 @@ import feign.Target.HardCodedTarget;
|
||||
import feign.codec.Decoder;
|
||||
import feign.codec.Encoder;
|
||||
import feign.codec.ErrorDecoder;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.BeansException;
|
||||
@@ -47,6 +48,7 @@ import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.cloud.openfeign.clientconfig.FeignClientConfigurer;
|
||||
import org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient;
|
||||
import org.springframework.cloud.openfeign.loadbalancer.RetryableFeignBlockingLoadBalancerClient;
|
||||
import org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
@@ -62,16 +64,18 @@ import org.springframework.util.StringUtils;
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Ilia Ilinykh
|
||||
* @author Marcin Grzejszczak
|
||||
* @author Jonatan Ivanov
|
||||
* @author Sam Kruglov
|
||||
*/
|
||||
public class FeignClientFactoryBean
|
||||
implements FactoryBean<Object>, InitializingBean, ApplicationContextAware, BeanFactoryAware {
|
||||
public class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
|
||||
ApplicationContextAware, BeanFactoryAware {
|
||||
|
||||
/***********************************
|
||||
* WARNING! Nothing in this class should be @Autowired. It causes NPEs because of some
|
||||
* lifecycle race condition.
|
||||
***********************************/
|
||||
|
||||
private static Log LOG = LogFactory.getLog(FeignClientFactoryBean.class);
|
||||
|
||||
private Class<?> type;
|
||||
|
||||
private String name;
|
||||
@@ -100,6 +104,8 @@ public class FeignClientFactoryBean
|
||||
|
||||
private boolean followRedirects = new Request.Options().isFollowRedirects();
|
||||
|
||||
private List<FeignBuilderCustomizer> additionalCustomizers = new ArrayList<>();
|
||||
|
||||
@Override
|
||||
public void afterPropertiesSet() {
|
||||
Assert.hasText(contextId, "Context id must be set");
|
||||
@@ -126,30 +132,39 @@ public class FeignClientFactoryBean
|
||||
}
|
||||
|
||||
private void applyBuildCustomizers(FeignContext context, Feign.Builder builder) {
|
||||
Map<String, FeignBuilderCustomizer> customizerMap = context.getInstances(contextId,
|
||||
FeignBuilderCustomizer.class);
|
||||
Map<String, FeignBuilderCustomizer> customizerMap = context
|
||||
.getInstances(contextId, FeignBuilderCustomizer.class);
|
||||
|
||||
if (customizerMap != null) {
|
||||
customizerMap.values().stream().sorted(AnnotationAwareOrderComparator.INSTANCE)
|
||||
.forEach(feignBuilderCustomizer -> feignBuilderCustomizer.customize(builder));
|
||||
customizerMap.values().stream()
|
||||
.sorted(AnnotationAwareOrderComparator.INSTANCE)
|
||||
.forEach(feignBuilderCustomizer -> feignBuilderCustomizer
|
||||
.customize(builder));
|
||||
}
|
||||
additionalCustomizers.forEach(customizer -> customizer.customize(builder));
|
||||
}
|
||||
|
||||
protected void configureFeign(FeignContext context, Feign.Builder builder) {
|
||||
FeignClientProperties properties = beanFactory != null ? beanFactory.getBean(FeignClientProperties.class)
|
||||
FeignClientProperties properties = beanFactory != null
|
||||
? beanFactory.getBean(FeignClientProperties.class)
|
||||
: applicationContext.getBean(FeignClientProperties.class);
|
||||
|
||||
FeignClientConfigurer feignClientConfigurer = getOptional(context, FeignClientConfigurer.class);
|
||||
FeignClientConfigurer feignClientConfigurer = getOptional(context,
|
||||
FeignClientConfigurer.class);
|
||||
setInheritParentContext(feignClientConfigurer.inheritParentConfiguration());
|
||||
|
||||
if (properties != null && inheritParentContext) {
|
||||
if (properties.isDefaultToProperties()) {
|
||||
configureUsingConfiguration(context, builder);
|
||||
configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder);
|
||||
configureUsingProperties(
|
||||
properties.getConfig().get(properties.getDefaultConfig()),
|
||||
builder);
|
||||
configureUsingProperties(properties.getConfig().get(contextId), builder);
|
||||
}
|
||||
else {
|
||||
configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder);
|
||||
configureUsingProperties(
|
||||
properties.getConfig().get(properties.getDefaultConfig()),
|
||||
builder);
|
||||
configureUsingProperties(properties.getConfig().get(contextId), builder);
|
||||
configureUsingConfiguration(context, builder);
|
||||
}
|
||||
@@ -159,7 +174,8 @@ public class FeignClientFactoryBean
|
||||
}
|
||||
}
|
||||
|
||||
protected void configureUsingConfiguration(FeignContext context, Feign.Builder builder) {
|
||||
protected void configureUsingConfiguration(FeignContext context,
|
||||
Feign.Builder builder) {
|
||||
Logger.Level level = getInheritedAwareOptional(context, Logger.Level.class);
|
||||
if (level != null) {
|
||||
builder.logLevel(level);
|
||||
@@ -168,52 +184,52 @@ public class FeignClientFactoryBean
|
||||
if (retryer != null) {
|
||||
builder.retryer(retryer);
|
||||
}
|
||||
ErrorDecoder errorDecoder = getInheritedAwareOptional(context, ErrorDecoder.class);
|
||||
ErrorDecoder errorDecoder = getInheritedAwareOptional(context,
|
||||
ErrorDecoder.class);
|
||||
if (errorDecoder != null) {
|
||||
builder.errorDecoder(errorDecoder);
|
||||
}
|
||||
else {
|
||||
FeignErrorDecoderFactory errorDecoderFactory = getOptional(context, FeignErrorDecoderFactory.class);
|
||||
FeignErrorDecoderFactory errorDecoderFactory = getOptional(context,
|
||||
FeignErrorDecoderFactory.class);
|
||||
if (errorDecoderFactory != null) {
|
||||
ErrorDecoder factoryErrorDecoder = errorDecoderFactory.create(type);
|
||||
builder.errorDecoder(factoryErrorDecoder);
|
||||
}
|
||||
}
|
||||
Request.Options options = getInheritedAwareOptional(context, Request.Options.class);
|
||||
Request.Options options = getInheritedAwareOptional(context,
|
||||
Request.Options.class);
|
||||
if (options != null) {
|
||||
builder.options(options);
|
||||
readTimeoutMillis = options.readTimeoutMillis();
|
||||
connectTimeoutMillis = options.connectTimeoutMillis();
|
||||
followRedirects = options.isFollowRedirects();
|
||||
}
|
||||
Map<String, RequestInterceptor> requestInterceptors = getInheritedAwareInstances(context,
|
||||
RequestInterceptor.class);
|
||||
Map<String, RequestInterceptor> requestInterceptors = getInheritedAwareInstances(
|
||||
context, RequestInterceptor.class);
|
||||
if (requestInterceptors != null) {
|
||||
List<RequestInterceptor> interceptors = new ArrayList<>(requestInterceptors.values());
|
||||
List<RequestInterceptor> interceptors = new ArrayList<>(
|
||||
requestInterceptors.values());
|
||||
AnnotationAwareOrderComparator.sort(interceptors);
|
||||
builder.requestInterceptors(interceptors);
|
||||
}
|
||||
QueryMapEncoder queryMapEncoder = getInheritedAwareOptional(context, QueryMapEncoder.class);
|
||||
QueryMapEncoder queryMapEncoder = getInheritedAwareOptional(context,
|
||||
QueryMapEncoder.class);
|
||||
if (queryMapEncoder != null) {
|
||||
builder.queryMapEncoder(queryMapEncoder);
|
||||
}
|
||||
if (decode404) {
|
||||
builder.decode404();
|
||||
}
|
||||
ExceptionPropagationPolicy exceptionPropagationPolicy = getInheritedAwareOptional(context,
|
||||
ExceptionPropagationPolicy.class);
|
||||
ExceptionPropagationPolicy exceptionPropagationPolicy = getInheritedAwareOptional(
|
||||
context, ExceptionPropagationPolicy.class);
|
||||
if (exceptionPropagationPolicy != null) {
|
||||
builder.exceptionPropagationPolicy(exceptionPropagationPolicy);
|
||||
}
|
||||
|
||||
Map<String, Capability> capabilities = getInheritedAwareInstances(context, Capability.class);
|
||||
if (capabilities != null) {
|
||||
capabilities.values().stream().sorted(AnnotationAwareOrderComparator.INSTANCE)
|
||||
.forEach(builder::addCapability);
|
||||
}
|
||||
}
|
||||
|
||||
protected void configureUsingProperties(FeignClientProperties.FeignClientConfiguration config,
|
||||
protected void configureUsingProperties(
|
||||
FeignClientProperties.FeignClientConfiguration config,
|
||||
Feign.Builder builder) {
|
||||
if (config == null) {
|
||||
return;
|
||||
@@ -223,12 +239,15 @@ public class FeignClientFactoryBean
|
||||
builder.logLevel(config.getLoggerLevel());
|
||||
}
|
||||
|
||||
connectTimeoutMillis = config.getConnectTimeout() != null ? config.getConnectTimeout() : connectTimeoutMillis;
|
||||
readTimeoutMillis = config.getReadTimeout() != null ? config.getReadTimeout() : readTimeoutMillis;
|
||||
followRedirects = config.isFollowRedirects() != null ? config.isFollowRedirects() : followRedirects;
|
||||
connectTimeoutMillis = config.getConnectTimeout() != null
|
||||
? config.getConnectTimeout() : connectTimeoutMillis;
|
||||
readTimeoutMillis = config.getReadTimeout() != null ? config.getReadTimeout()
|
||||
: readTimeoutMillis;
|
||||
followRedirects = config.isFollowRedirects() != null ? config.isFollowRedirects()
|
||||
: followRedirects;
|
||||
|
||||
builder.options(new Request.Options(connectTimeoutMillis, TimeUnit.MILLISECONDS, readTimeoutMillis,
|
||||
TimeUnit.MILLISECONDS, followRedirects));
|
||||
builder.options(new Request.Options(connectTimeoutMillis, TimeUnit.MILLISECONDS,
|
||||
readTimeoutMillis, TimeUnit.MILLISECONDS, followRedirects));
|
||||
|
||||
if (config.getRetryer() != null) {
|
||||
Retryer retryer = getOrInstantiate(config.getRetryer());
|
||||
@@ -240,7 +259,8 @@ public class FeignClientFactoryBean
|
||||
builder.errorDecoder(errorDecoder);
|
||||
}
|
||||
|
||||
if (config.getRequestInterceptors() != null && !config.getRequestInterceptors().isEmpty()) {
|
||||
if (config.getRequestInterceptors() != null
|
||||
&& !config.getRequestInterceptors().isEmpty()) {
|
||||
// this will add request interceptor to builder, not replace existing
|
||||
for (Class<RequestInterceptor> bean : config.getRequestInterceptors()) {
|
||||
RequestInterceptor interceptor = getOrInstantiate(bean);
|
||||
@@ -259,11 +279,13 @@ public class FeignClientFactoryBean
|
||||
}
|
||||
|
||||
if (Objects.nonNull(config.getDefaultRequestHeaders())) {
|
||||
builder.requestInterceptor(requestTemplate -> requestTemplate.headers(config.getDefaultRequestHeaders()));
|
||||
builder.requestInterceptor(requestTemplate -> requestTemplate
|
||||
.headers(config.getDefaultRequestHeaders()));
|
||||
}
|
||||
|
||||
if (Objects.nonNull(config.getDefaultQueryParameters())) {
|
||||
builder.requestInterceptor(requestTemplate -> requestTemplate.queries(config.getDefaultQueryParameters()));
|
||||
builder.requestInterceptor(requestTemplate -> requestTemplate
|
||||
.queries(config.getDefaultQueryParameters()));
|
||||
}
|
||||
|
||||
if (Objects.nonNull(config.getDecoder())) {
|
||||
@@ -277,15 +299,12 @@ public class FeignClientFactoryBean
|
||||
if (Objects.nonNull(config.getExceptionPropagationPolicy())) {
|
||||
builder.exceptionPropagationPolicy(config.getExceptionPropagationPolicy());
|
||||
}
|
||||
|
||||
if (config.getCapabilities() != null) {
|
||||
config.getCapabilities().stream().map(this::getOrInstantiate).forEach(builder::addCapability);
|
||||
}
|
||||
}
|
||||
|
||||
private <T> T getOrInstantiate(Class<T> tClass) {
|
||||
try {
|
||||
return beanFactory != null ? beanFactory.getBean(tClass) : applicationContext.getBean(tClass);
|
||||
return beanFactory != null ? beanFactory.getBean(tClass)
|
||||
: applicationContext.getBean(tClass);
|
||||
}
|
||||
catch (NoSuchBeanDefinitionException e) {
|
||||
return BeanUtils.instantiateClass(tClass);
|
||||
@@ -295,7 +314,8 @@ public class FeignClientFactoryBean
|
||||
protected <T> T get(FeignContext context, Class<T> type) {
|
||||
T instance = context.getInstance(contextId, type);
|
||||
if (instance == null) {
|
||||
throw new IllegalStateException("No bean found of type " + type + " for " + contextId);
|
||||
throw new IllegalStateException(
|
||||
"No bean found of type " + type + " for " + contextId);
|
||||
}
|
||||
return instance;
|
||||
}
|
||||
@@ -313,7 +333,8 @@ public class FeignClientFactoryBean
|
||||
}
|
||||
}
|
||||
|
||||
protected <T> Map<String, T> getInheritedAwareInstances(FeignContext context, Class<T> type) {
|
||||
protected <T> Map<String, T> getInheritedAwareInstances(FeignContext context,
|
||||
Class<T> type) {
|
||||
if (inheritParentContext) {
|
||||
return context.getInstances(contextId, type);
|
||||
}
|
||||
@@ -322,7 +343,8 @@ public class FeignClientFactoryBean
|
||||
}
|
||||
}
|
||||
|
||||
protected <T> T loadBalance(Feign.Builder builder, FeignContext context, HardCodedTarget<T> target) {
|
||||
protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
|
||||
HardCodedTarget<T> target) {
|
||||
Client client = getOptional(context, Client.class);
|
||||
if (client != null) {
|
||||
builder.client(client);
|
||||
@@ -331,7 +353,7 @@ public class FeignClientFactoryBean
|
||||
}
|
||||
|
||||
throw new IllegalStateException(
|
||||
"No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-loadbalancer?");
|
||||
"No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon or spring-cloud-starter-loadbalancer?");
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -345,11 +367,19 @@ public class FeignClientFactoryBean
|
||||
* information
|
||||
*/
|
||||
<T> T getTarget() {
|
||||
FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class)
|
||||
FeignContext context = beanFactory != null
|
||||
? beanFactory.getBean(FeignContext.class)
|
||||
: applicationContext.getBean(FeignContext.class);
|
||||
Feign.Builder builder = feign(context);
|
||||
|
||||
if (!StringUtils.hasText(url)) {
|
||||
if (url != null && LOG.isWarnEnabled()) {
|
||||
LOG.warn(
|
||||
"The provided URL is empty. Will try picking an instance via load-balancing.");
|
||||
}
|
||||
else if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("URL not provided. Will use LoadBalancer.");
|
||||
}
|
||||
if (!name.startsWith("http")) {
|
||||
url = "http://" + name;
|
||||
}
|
||||
@@ -357,7 +387,8 @@ public class FeignClientFactoryBean
|
||||
url = name;
|
||||
}
|
||||
url += cleanPath();
|
||||
return (T) loadBalance(builder, context, new HardCodedTarget<>(type, name, url));
|
||||
return (T) loadBalance(builder, context,
|
||||
new HardCodedTarget<>(type, name, url));
|
||||
}
|
||||
if (StringUtils.hasText(url) && !url.startsWith("http")) {
|
||||
url = "http://" + url;
|
||||
@@ -365,6 +396,11 @@ public class FeignClientFactoryBean
|
||||
String url = this.url + cleanPath();
|
||||
Client client = getOptional(context, Client.class);
|
||||
if (client != null) {
|
||||
if (client instanceof LoadBalancerFeignClient) {
|
||||
// not load balancing because we have a url,
|
||||
// but ribbon is on the classpath, so unwrap
|
||||
client = ((LoadBalancerFeignClient) client).getDelegate();
|
||||
}
|
||||
if (client instanceof FeignBlockingLoadBalancerClient) {
|
||||
// not load balancing because we have a url,
|
||||
// but Spring Cloud LoadBalancer is on the classpath, so unwrap
|
||||
@@ -373,12 +409,14 @@ public class FeignClientFactoryBean
|
||||
if (client instanceof RetryableFeignBlockingLoadBalancerClient) {
|
||||
// not load balancing because we have a url,
|
||||
// but Spring Cloud LoadBalancer is on the classpath, so unwrap
|
||||
client = ((RetryableFeignBlockingLoadBalancerClient) client).getDelegate();
|
||||
client = ((RetryableFeignBlockingLoadBalancerClient) client)
|
||||
.getDelegate();
|
||||
}
|
||||
builder.client(client);
|
||||
}
|
||||
Targeter targeter = get(context, Targeter.class);
|
||||
return (T) targeter.target(this, builder, context, new HardCodedTarget<>(type, name, url));
|
||||
return (T) targeter.target(this, builder, context,
|
||||
new HardCodedTarget<>(type, name, url));
|
||||
}
|
||||
|
||||
private String cleanPath() {
|
||||
@@ -460,6 +498,10 @@ public class FeignClientFactoryBean
|
||||
this.inheritParentContext = inheritParentContext;
|
||||
}
|
||||
|
||||
public void addCustomizer(FeignBuilderCustomizer customizer) {
|
||||
additionalCustomizers.add(customizer);
|
||||
}
|
||||
|
||||
public ApplicationContext getApplicationContext() {
|
||||
return applicationContext;
|
||||
}
|
||||
@@ -496,10 +538,13 @@ public class FeignClientFactoryBean
|
||||
}
|
||||
FeignClientFactoryBean that = (FeignClientFactoryBean) o;
|
||||
return Objects.equals(applicationContext, that.applicationContext)
|
||||
&& Objects.equals(beanFactory, that.beanFactory) && decode404 == that.decode404
|
||||
&& inheritParentContext == that.inheritParentContext && Objects.equals(fallback, that.fallback)
|
||||
&& Objects.equals(fallbackFactory, that.fallbackFactory) && Objects.equals(name, that.name)
|
||||
&& Objects.equals(path, that.path) && Objects.equals(type, that.type) && Objects.equals(url, that.url)
|
||||
&& Objects.equals(beanFactory, that.beanFactory)
|
||||
&& decode404 == that.decode404
|
||||
&& inheritParentContext == that.inheritParentContext
|
||||
&& Objects.equals(fallback, that.fallback)
|
||||
&& Objects.equals(fallbackFactory, that.fallbackFactory)
|
||||
&& Objects.equals(name, that.name) && Objects.equals(path, that.path)
|
||||
&& Objects.equals(type, that.type) && Objects.equals(url, that.url)
|
||||
&& Objects.equals(connectTimeoutMillis, that.connectTimeoutMillis)
|
||||
&& Objects.equals(readTimeoutMillis, that.readTimeoutMillis)
|
||||
&& Objects.equals(followRedirects, that.followRedirects);
|
||||
@@ -507,20 +552,25 @@ public class FeignClientFactoryBean
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(applicationContext, beanFactory, decode404, inheritParentContext, fallback, fallbackFactory,
|
||||
name, path, type, url, readTimeoutMillis, connectTimeoutMillis, followRedirects);
|
||||
return Objects.hash(applicationContext, beanFactory, decode404,
|
||||
inheritParentContext, fallback, fallbackFactory, name, path, type, url,
|
||||
readTimeoutMillis, connectTimeoutMillis, followRedirects);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringBuilder("FeignClientFactoryBean{").append("type=").append(type).append(", ").append("name='")
|
||||
.append(name).append("', ").append("url='").append(url).append("', ").append("path='").append(path)
|
||||
.append("', ").append("decode404=").append(decode404).append(", ").append("inheritParentContext=")
|
||||
.append(inheritParentContext).append(", ").append("applicationContext=").append(applicationContext)
|
||||
.append(", ").append("beanFactory=").append(beanFactory).append(", ").append("fallback=")
|
||||
.append(fallback).append(", ").append("fallbackFactory=").append(fallbackFactory).append("}")
|
||||
.append("connectTimeoutMillis=").append(connectTimeoutMillis).append("}").append("readTimeoutMillis=")
|
||||
.append(readTimeoutMillis).append("}").append("followRedirects=").append(followRedirects).append("}")
|
||||
return new StringBuilder("FeignClientFactoryBean{").append("type=").append(type)
|
||||
.append(", ").append("name='").append(name).append("', ").append("url='")
|
||||
.append(url).append("', ").append("path='").append(path).append("', ")
|
||||
.append("decode404=").append(decode404).append(", ")
|
||||
.append("inheritParentContext=").append(inheritParentContext).append(", ")
|
||||
.append("applicationContext=").append(applicationContext).append(", ")
|
||||
.append("beanFactory=").append(beanFactory).append(", ")
|
||||
.append("fallback=").append(fallback).append(", ")
|
||||
.append("fallbackFactory=").append(fallbackFactory).append("}")
|
||||
.append("connectTimeoutMillis=").append(connectTimeoutMillis).append("}")
|
||||
.append("readTimeoutMillis=").append(readTimeoutMillis).append("}")
|
||||
.append("followRedirects=").append(followRedirects).append("}")
|
||||
.toString();
|
||||
}
|
||||
|
||||
|
||||
-52
@@ -1,52 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021-2021 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.util.Map;
|
||||
|
||||
import org.springframework.context.annotation.Condition;
|
||||
import org.springframework.context.annotation.ConditionContext;
|
||||
import org.springframework.core.type.AnnotatedTypeMetadata;
|
||||
|
||||
/**
|
||||
* @author Jonatan Ivanov
|
||||
*/
|
||||
class FeignClientMetricsEnabledCondition implements Condition {
|
||||
|
||||
@Override
|
||||
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
|
||||
FeignClientProperties feignClientProperties = context.getBeanFactory()
|
||||
.getBeanProvider(FeignClientProperties.class).getIfAvailable();
|
||||
if (feignClientProperties != null) {
|
||||
Map<String, FeignClientProperties.FeignClientConfiguration> feignClientConfigMap = feignClientProperties
|
||||
.getConfig();
|
||||
if (feignClientConfigMap != null) {
|
||||
FeignClientProperties.FeignClientConfiguration feignClientConfig = feignClientConfigMap
|
||||
.get(context.getEnvironment().getProperty("feign.client.name"));
|
||||
if (feignClientConfig != null) {
|
||||
FeignClientProperties.MetricsProperties metrics = feignClientConfig.getMetrics();
|
||||
if (metrics != null && metrics.getEnabled() != null) {
|
||||
return metrics.getEnabled();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
+26
-72
@@ -22,7 +22,6 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import feign.Capability;
|
||||
import feign.Contract;
|
||||
import feign.ExceptionPropagationPolicy;
|
||||
import feign.Logger;
|
||||
@@ -38,7 +37,6 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
* @author Eko Kurniawan Khannedy
|
||||
* @author Ilia Ilinykh
|
||||
* @author Ram Anaswara
|
||||
* @author Jonatan Ivanov
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@ConfigurationProperties("feign.client")
|
||||
@@ -97,8 +95,10 @@ public class FeignClientProperties {
|
||||
return false;
|
||||
}
|
||||
FeignClientProperties that = (FeignClientProperties) o;
|
||||
return defaultToProperties == that.defaultToProperties && Objects.equals(defaultConfig, that.defaultConfig)
|
||||
&& Objects.equals(config, that.config) && Objects.equals(decodeSlash, that.decodeSlash);
|
||||
return defaultToProperties == that.defaultToProperties
|
||||
&& Objects.equals(defaultConfig, that.defaultConfig)
|
||||
&& Objects.equals(config, that.config)
|
||||
&& Objects.equals(decodeSlash, that.decodeSlash);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -137,10 +137,6 @@ public class FeignClientProperties {
|
||||
|
||||
private ExceptionPropagationPolicy exceptionPropagationPolicy;
|
||||
|
||||
private List<Class<Capability>> capabilities;
|
||||
|
||||
private MetricsProperties metrics;
|
||||
|
||||
private Boolean followRedirects;
|
||||
|
||||
public Logger.Level getLoggerLevel() {
|
||||
@@ -187,7 +183,8 @@ public class FeignClientProperties {
|
||||
return requestInterceptors;
|
||||
}
|
||||
|
||||
public void setRequestInterceptors(List<Class<RequestInterceptor>> requestInterceptors) {
|
||||
public void setRequestInterceptors(
|
||||
List<Class<RequestInterceptor>> requestInterceptors) {
|
||||
this.requestInterceptors = requestInterceptors;
|
||||
}
|
||||
|
||||
@@ -195,7 +192,8 @@ public class FeignClientProperties {
|
||||
return defaultRequestHeaders;
|
||||
}
|
||||
|
||||
public void setDefaultRequestHeaders(Map<String, Collection<String>> defaultRequestHeaders) {
|
||||
public void setDefaultRequestHeaders(
|
||||
Map<String, Collection<String>> defaultRequestHeaders) {
|
||||
this.defaultRequestHeaders = defaultRequestHeaders;
|
||||
}
|
||||
|
||||
@@ -203,7 +201,8 @@ public class FeignClientProperties {
|
||||
return defaultQueryParameters;
|
||||
}
|
||||
|
||||
public void setDefaultQueryParameters(Map<String, Collection<String>> defaultQueryParameters) {
|
||||
public void setDefaultQueryParameters(
|
||||
Map<String, Collection<String>> defaultQueryParameters) {
|
||||
this.defaultQueryParameters = defaultQueryParameters;
|
||||
}
|
||||
|
||||
@@ -243,26 +242,11 @@ public class FeignClientProperties {
|
||||
return exceptionPropagationPolicy;
|
||||
}
|
||||
|
||||
public void setExceptionPropagationPolicy(ExceptionPropagationPolicy exceptionPropagationPolicy) {
|
||||
public void setExceptionPropagationPolicy(
|
||||
ExceptionPropagationPolicy exceptionPropagationPolicy) {
|
||||
this.exceptionPropagationPolicy = exceptionPropagationPolicy;
|
||||
}
|
||||
|
||||
public List<Class<Capability>> getCapabilities() {
|
||||
return capabilities;
|
||||
}
|
||||
|
||||
public void setCapabilities(List<Class<Capability>> capabilities) {
|
||||
this.capabilities = capabilities;
|
||||
}
|
||||
|
||||
public MetricsProperties getMetrics() {
|
||||
return metrics;
|
||||
}
|
||||
|
||||
public void setMetrics(MetricsProperties metrics) {
|
||||
this.metrics = metrics;
|
||||
}
|
||||
|
||||
public Boolean isFollowRedirects() {
|
||||
return followRedirects;
|
||||
}
|
||||
@@ -280,59 +264,29 @@ public class FeignClientProperties {
|
||||
return false;
|
||||
}
|
||||
FeignClientConfiguration that = (FeignClientConfiguration) o;
|
||||
return loggerLevel == that.loggerLevel && Objects.equals(connectTimeout, that.connectTimeout)
|
||||
&& Objects.equals(readTimeout, that.readTimeout) && Objects.equals(retryer, that.retryer)
|
||||
return loggerLevel == that.loggerLevel
|
||||
&& Objects.equals(connectTimeout, that.connectTimeout)
|
||||
&& Objects.equals(readTimeout, that.readTimeout)
|
||||
&& Objects.equals(retryer, that.retryer)
|
||||
&& Objects.equals(errorDecoder, that.errorDecoder)
|
||||
&& Objects.equals(requestInterceptors, that.requestInterceptors)
|
||||
&& Objects.equals(decode404, that.decode404) && Objects.equals(encoder, that.encoder)
|
||||
&& Objects.equals(decoder, that.decoder) && Objects.equals(contract, that.contract)
|
||||
&& Objects.equals(exceptionPropagationPolicy, that.exceptionPropagationPolicy)
|
||||
&& Objects.equals(decode404, that.decode404)
|
||||
&& Objects.equals(encoder, that.encoder)
|
||||
&& Objects.equals(decoder, that.decoder)
|
||||
&& Objects.equals(contract, that.contract)
|
||||
&& Objects.equals(exceptionPropagationPolicy,
|
||||
that.exceptionPropagationPolicy)
|
||||
&& Objects.equals(defaultRequestHeaders, that.defaultRequestHeaders)
|
||||
&& Objects.equals(defaultQueryParameters, that.defaultQueryParameters)
|
||||
&& Objects.equals(capabilities, that.capabilities) && Objects.equals(metrics, that.metrics)
|
||||
&& Objects.equals(followRedirects, that.followRedirects);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(loggerLevel, connectTimeout, readTimeout, retryer, errorDecoder, requestInterceptors,
|
||||
decode404, encoder, decoder, contract, exceptionPropagationPolicy, defaultQueryParameters,
|
||||
defaultRequestHeaders, capabilities, metrics, followRedirects);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Metrics configuration for Feign Client.
|
||||
*/
|
||||
public static class MetricsProperties {
|
||||
|
||||
private Boolean enabled = true;
|
||||
|
||||
public Boolean getEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(Boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
MetricsProperties that = (MetricsProperties) o;
|
||||
return Objects.equals(enabled, that.enabled);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(enabled);
|
||||
return Objects.hash(loggerLevel, connectTimeout, readTimeout, retryer,
|
||||
errorDecoder, requestInterceptors, decode404, encoder, decoder,
|
||||
contract, exceptionPropagationPolicy, defaultQueryParameters,
|
||||
defaultRequestHeaders, followRedirects);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
-3
@@ -64,7 +64,8 @@ class FeignClientSpecification implements NamedContextFactory.Specification {
|
||||
return false;
|
||||
}
|
||||
FeignClientSpecification that = (FeignClientSpecification) o;
|
||||
return Objects.equals(this.name, that.name) && Arrays.equals(this.configuration, that.configuration);
|
||||
return Objects.equals(this.name, that.name)
|
||||
&& Arrays.equals(this.configuration, that.configuration);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -74,8 +75,9 @@ class FeignClientSpecification implements NamedContextFactory.Specification {
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringBuilder("FeignClientSpecification{").append("name='").append(this.name).append("', ")
|
||||
.append("configuration=").append(Arrays.toString(this.configuration)).append("}").toString();
|
||||
return new StringBuilder("FeignClientSpecification{").append("name='")
|
||||
.append(this.name).append("', ").append("configuration=")
|
||||
.append(Arrays.toString(this.configuration)).append("}").toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+45
-43
@@ -19,6 +19,7 @@ package org.springframework.cloud.openfeign;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.netflix.hystrix.HystrixCommand;
|
||||
import feign.Contract;
|
||||
import feign.Feign;
|
||||
import feign.Logger;
|
||||
@@ -28,9 +29,8 @@ import feign.codec.Decoder;
|
||||
import feign.codec.Encoder;
|
||||
import feign.form.MultipartFormContentProcessor;
|
||||
import feign.form.spring.SpringFormEncoder;
|
||||
import feign.micrometer.MicrometerCapability;
|
||||
import feign.hystrix.HystrixFeign;
|
||||
import feign.optionals.OptionalDecoder;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
|
||||
import org.springframework.beans.factory.ObjectFactory;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
@@ -54,7 +54,6 @@ import org.springframework.cloud.openfeign.support.SpringDecoder;
|
||||
import org.springframework.cloud.openfeign.support.SpringEncoder;
|
||||
import org.springframework.cloud.openfeign.support.SpringMvcContract;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Scope;
|
||||
import org.springframework.core.convert.ConversionService;
|
||||
@@ -67,7 +66,6 @@ import static feign.form.ContentType.MULTIPART;
|
||||
* @author Dave Syer
|
||||
* @author Venil Noronha
|
||||
* @author Darren Foong
|
||||
* @author Jonatan Ivanov
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Hyeonmin Park
|
||||
*/
|
||||
@@ -98,7 +96,8 @@ public class FeignClientsConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public Decoder feignDecoder() {
|
||||
return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
|
||||
return new OptionalDecoder(
|
||||
new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -111,13 +110,18 @@ public class FeignClientsConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnClass(name = "org.springframework.data.domain.Pageable")
|
||||
@ConditionalOnMissingBean
|
||||
public Encoder feignEncoderPageable(ObjectProvider<AbstractFormWriter> formWriterProvider) {
|
||||
PageableSpringEncoder encoder = new PageableSpringEncoder(springEncoder(formWriterProvider, encoderProperties));
|
||||
public Encoder feignEncoderPageable(
|
||||
ObjectProvider<AbstractFormWriter> formWriterProvider) {
|
||||
PageableSpringEncoder encoder = new PageableSpringEncoder(
|
||||
springEncoder(formWriterProvider, encoderProperties));
|
||||
|
||||
if (springDataWebProperties != null) {
|
||||
encoder.setPageParameter(springDataWebProperties.getPageable().getPageParameter());
|
||||
encoder.setSizeParameter(springDataWebProperties.getPageable().getSizeParameter());
|
||||
encoder.setSortParameter(springDataWebProperties.getSort().getSortParameter());
|
||||
encoder.setPageParameter(
|
||||
springDataWebProperties.getPageable().getPageParameter());
|
||||
encoder.setSizeParameter(
|
||||
springDataWebProperties.getPageable().getSizeParameter());
|
||||
encoder.setSortParameter(
|
||||
springDataWebProperties.getSort().getSortParameter());
|
||||
}
|
||||
return encoder;
|
||||
}
|
||||
@@ -132,8 +136,10 @@ public class FeignClientsConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public Contract feignContract(ConversionService feignConversionService) {
|
||||
boolean decodeSlash = feignClientProperties == null || feignClientProperties.isDecodeSlash();
|
||||
return new SpringMvcContract(this.parameterProcessors, feignConversionService, decodeSlash);
|
||||
boolean decodeSlash = feignClientProperties == null
|
||||
|| feignClientProperties.isDecodeSlash();
|
||||
return new SpringMvcContract(this.parameterProcessors, feignConversionService,
|
||||
decodeSlash);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -151,6 +157,13 @@ public class FeignClientsConfiguration {
|
||||
return Retryer.NEVER_RETRY;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Scope("prototype")
|
||||
@ConditionalOnMissingBean
|
||||
public Feign.Builder feignBuilder(Retryer retryer) {
|
||||
return Feign.builder().retryer(retryer);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(FeignLoggerFactory.class)
|
||||
public FeignLoggerFactory feignLoggerFactory() {
|
||||
@@ -169,37 +182,41 @@ public class FeignClientsConfiguration {
|
||||
AbstractFormWriter formWriter = formWriterProvider.getIfAvailable();
|
||||
|
||||
if (formWriter != null) {
|
||||
return new SpringEncoder(new SpringPojoFormEncoder(formWriter), this.messageConverters, encoderProperties);
|
||||
return new SpringEncoder(new SpringPojoFormEncoder(formWriter),
|
||||
this.messageConverters, encoderProperties);
|
||||
}
|
||||
else {
|
||||
return new SpringEncoder(new SpringFormEncoder(), this.messageConverters, encoderProperties);
|
||||
return new SpringEncoder(new SpringFormEncoder(), this.messageConverters,
|
||||
encoderProperties);
|
||||
}
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
|
||||
protected static class HystrixFeignConfiguration {
|
||||
|
||||
@Bean
|
||||
@Scope("prototype")
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnProperty(name = "feign.hystrix.enabled")
|
||||
public Feign.Builder feignHystrixBuilder() {
|
||||
return HystrixFeign.builder();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class SpringPojoFormEncoder extends SpringFormEncoder {
|
||||
|
||||
SpringPojoFormEncoder(AbstractFormWriter formWriter) {
|
||||
super();
|
||||
|
||||
MultipartFormContentProcessor processor = (MultipartFormContentProcessor) getContentProcessor(MULTIPART);
|
||||
MultipartFormContentProcessor processor = (MultipartFormContentProcessor) getContentProcessor(
|
||||
MULTIPART);
|
||||
processor.addFirstWriter(formWriter);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Conditional(FeignCircuitBreakerDisabledConditions.class)
|
||||
protected static class DefaultFeignBuilderConfiguration {
|
||||
|
||||
@Bean
|
||||
@Scope("prototype")
|
||||
@ConditionalOnMissingBean
|
||||
public Feign.Builder feignBuilder(Retryer retryer) {
|
||||
return Feign.builder().retryer(retryer);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(CircuitBreaker.class)
|
||||
@ConditionalOnProperty("feign.circuitbreaker.enabled")
|
||||
@@ -222,19 +239,4 @@ public class FeignClientsConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnBean(type = "io.micrometer.core.instrument.MeterRegistry")
|
||||
@ConditionalOnClass(name = "feign.micrometer.MicrometerCapability")
|
||||
@ConditionalOnProperty(name = "feign.metrics.enabled", matchIfMissing = true)
|
||||
@Conditional(FeignClientMetricsEnabledCondition.class)
|
||||
protected static class MetricsConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public MicrometerCapability micrometerCapability(MeterRegistry meterRegistry) {
|
||||
return new MicrometerCapability(meterRegistry);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+69
-42
@@ -64,7 +64,8 @@ import org.springframework.util.StringUtils;
|
||||
* @author Marcin Grzejszczak
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
|
||||
class FeignClientsRegistrar
|
||||
implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
|
||||
|
||||
// patterned after Spring Integration IntegrationComponentScanRegistrar
|
||||
// and RibbonClientsConfigurationRegistgrar
|
||||
@@ -77,7 +78,8 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
||||
}
|
||||
|
||||
static void validateFallback(final Class clazz) {
|
||||
Assert.isTrue(!clazz.isInterface(), "Fallback class must implement the interface annotated by @FeignClient");
|
||||
Assert.isTrue(!clazz.isInterface(),
|
||||
"Fallback class must implement the interface annotated by @FeignClient");
|
||||
}
|
||||
|
||||
static void validateFallbackFactory(final Class clazz) {
|
||||
@@ -142,13 +144,16 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
||||
}
|
||||
|
||||
@Override
|
||||
public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
|
||||
public void registerBeanDefinitions(AnnotationMetadata metadata,
|
||||
BeanDefinitionRegistry registry) {
|
||||
registerDefaultConfiguration(metadata, registry);
|
||||
registerFeignClients(metadata, registry);
|
||||
}
|
||||
|
||||
private void registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
|
||||
Map<String, Object> defaultAttrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName(), true);
|
||||
private void registerDefaultConfiguration(AnnotationMetadata metadata,
|
||||
BeanDefinitionRegistry registry) {
|
||||
Map<String, Object> defaultAttrs = metadata
|
||||
.getAnnotationAttributes(EnableFeignClients.class.getName(), true);
|
||||
|
||||
if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) {
|
||||
String name;
|
||||
@@ -158,15 +163,19 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
||||
else {
|
||||
name = "default." + metadata.getClassName();
|
||||
}
|
||||
registerClientConfiguration(registry, name, defaultAttrs.get("defaultConfiguration"));
|
||||
registerClientConfiguration(registry, name,
|
||||
defaultAttrs.get("defaultConfiguration"));
|
||||
}
|
||||
}
|
||||
|
||||
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
|
||||
public void registerFeignClients(AnnotationMetadata metadata,
|
||||
BeanDefinitionRegistry registry) {
|
||||
|
||||
LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
|
||||
Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
|
||||
final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients");
|
||||
Map<String, Object> attrs = metadata
|
||||
.getAnnotationAttributes(EnableFeignClients.class.getName());
|
||||
final Class<?>[] clients = attrs == null ? null
|
||||
: (Class<?>[]) attrs.get("clients");
|
||||
if (clients == null || clients.length == 0) {
|
||||
ClassPathScanningCandidateComponentProvider scanner = getScanner();
|
||||
scanner.setResourceLoader(this.resourceLoader);
|
||||
@@ -187,21 +196,23 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
||||
// verify annotated class is an interface
|
||||
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
|
||||
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
|
||||
Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface");
|
||||
Assert.isTrue(annotationMetadata.isInterface(),
|
||||
"@FeignClient can only be specified on an interface");
|
||||
|
||||
Map<String, Object> attributes = annotationMetadata
|
||||
.getAnnotationAttributes(FeignClient.class.getCanonicalName());
|
||||
|
||||
String name = getClientName(attributes);
|
||||
registerClientConfiguration(registry, name, attributes.get("configuration"));
|
||||
registerClientConfiguration(registry, name,
|
||||
attributes.get("configuration"));
|
||||
|
||||
registerFeignClient(registry, annotationMetadata, attributes);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata,
|
||||
Map<String, Object> attributes) {
|
||||
private void registerFeignClient(BeanDefinitionRegistry registry,
|
||||
AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
|
||||
String className = annotationMetadata.getClassName();
|
||||
Class clazz = ClassUtils.resolveClassName(className, null);
|
||||
ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
|
||||
@@ -213,22 +224,27 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
||||
factoryBean.setName(name);
|
||||
factoryBean.setContextId(contextId);
|
||||
factoryBean.setType(clazz);
|
||||
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {
|
||||
factoryBean.setUrl(getUrl(beanFactory, attributes));
|
||||
factoryBean.setPath(getPath(beanFactory, attributes));
|
||||
factoryBean.setDecode404(Boolean.parseBoolean(String.valueOf(attributes.get("decode404"))));
|
||||
Object fallback = attributes.get("fallback");
|
||||
if (fallback != null) {
|
||||
factoryBean.setFallback(fallback instanceof Class ? (Class<?>) fallback
|
||||
: ClassUtils.resolveClassName(fallback.toString(), null));
|
||||
}
|
||||
Object fallbackFactory = attributes.get("fallbackFactory");
|
||||
if (fallbackFactory != null) {
|
||||
factoryBean.setFallbackFactory(fallbackFactory instanceof Class ? (Class<?>) fallbackFactory
|
||||
: ClassUtils.resolveClassName(fallbackFactory.toString(), null));
|
||||
}
|
||||
return factoryBean.getObject();
|
||||
});
|
||||
BeanDefinitionBuilder definition = BeanDefinitionBuilder
|
||||
.genericBeanDefinition(clazz, () -> {
|
||||
factoryBean.setUrl(getUrl(beanFactory, attributes));
|
||||
factoryBean.setPath(getPath(beanFactory, attributes));
|
||||
factoryBean.setDecode404(Boolean
|
||||
.parseBoolean(String.valueOf(attributes.get("decode404"))));
|
||||
Object fallback = attributes.get("fallback");
|
||||
if (fallback != null) {
|
||||
factoryBean.setFallback(fallback instanceof Class
|
||||
? (Class<?>) fallback
|
||||
: ClassUtils.resolveClassName(fallback.toString(), null));
|
||||
}
|
||||
Object fallbackFactory = attributes.get("fallbackFactory");
|
||||
if (fallbackFactory != null) {
|
||||
factoryBean.setFallbackFactory(fallbackFactory instanceof Class
|
||||
? (Class<?>) fallbackFactory
|
||||
: ClassUtils.resolveClassName(fallbackFactory.toString(),
|
||||
null));
|
||||
}
|
||||
return factoryBean.getObject();
|
||||
});
|
||||
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
|
||||
definition.setLazyInit(true);
|
||||
validate(attributes);
|
||||
@@ -247,7 +263,8 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
||||
qualifiers = new String[] { contextId + "FeignClient" };
|
||||
}
|
||||
|
||||
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
|
||||
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
|
||||
qualifiers);
|
||||
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
|
||||
}
|
||||
|
||||
@@ -275,7 +292,8 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
||||
return getName(name);
|
||||
}
|
||||
|
||||
private String getContextId(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {
|
||||
private String getContextId(ConfigurableBeanFactory beanFactory,
|
||||
Map<String, Object> attributes) {
|
||||
String contextId = (String) attributes.get("contextId");
|
||||
if (!StringUtils.hasText(contextId)) {
|
||||
return getName(attributes);
|
||||
@@ -295,17 +313,20 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
||||
if (resolver == null) {
|
||||
return resolved;
|
||||
}
|
||||
return String.valueOf(resolver.evaluate(resolved, new BeanExpressionContext(beanFactory, null)));
|
||||
return String.valueOf(resolver.evaluate(resolved,
|
||||
new BeanExpressionContext(beanFactory, null)));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private String getUrl(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {
|
||||
private String getUrl(ConfigurableBeanFactory beanFactory,
|
||||
Map<String, Object> attributes) {
|
||||
String url = resolve(beanFactory, (String) attributes.get("url"));
|
||||
return getUrl(url);
|
||||
}
|
||||
|
||||
private String getPath(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {
|
||||
private String getPath(ConfigurableBeanFactory beanFactory,
|
||||
Map<String, Object> attributes) {
|
||||
String path = resolve(beanFactory, (String) attributes.get("path"));
|
||||
return getPath(path);
|
||||
}
|
||||
@@ -313,7 +334,8 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
||||
protected ClassPathScanningCandidateComponentProvider getScanner() {
|
||||
return new ClassPathScanningCandidateComponentProvider(false, this.environment) {
|
||||
@Override
|
||||
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) {
|
||||
protected boolean isCandidateComponent(
|
||||
AnnotatedBeanDefinition beanDefinition) {
|
||||
boolean isCandidate = false;
|
||||
if (beanDefinition.getMetadata().isIndependent()) {
|
||||
if (!beanDefinition.getMetadata().isAnnotation()) {
|
||||
@@ -345,7 +367,8 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
||||
}
|
||||
|
||||
if (basePackages.isEmpty()) {
|
||||
basePackages.add(ClassUtils.getPackageName(importingClassMetadata.getClassName()));
|
||||
basePackages.add(
|
||||
ClassUtils.getPackageName(importingClassMetadata.getClassName()));
|
||||
}
|
||||
return basePackages;
|
||||
}
|
||||
@@ -365,7 +388,8 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
||||
if (client == null) {
|
||||
return null;
|
||||
}
|
||||
List<String> qualifierList = new ArrayList<>(Arrays.asList((String[]) client.get("qualifiers")));
|
||||
List<String> qualifierList = new ArrayList<>(
|
||||
Arrays.asList((String[]) client.get("qualifiers")));
|
||||
qualifierList.removeIf(qualifier -> !StringUtils.hasText(qualifier));
|
||||
if (qualifierList.isEmpty() && getQualifier(client) != null) {
|
||||
qualifierList = Collections.singletonList(getQualifier(client));
|
||||
@@ -391,15 +415,18 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
||||
return value;
|
||||
}
|
||||
|
||||
throw new IllegalStateException(
|
||||
"Either 'name' or 'value' must be provided in @" + FeignClient.class.getSimpleName());
|
||||
throw new IllegalStateException("Either 'name' or 'value' must be provided in @"
|
||||
+ FeignClient.class.getSimpleName());
|
||||
}
|
||||
|
||||
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) {
|
||||
BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FeignClientSpecification.class);
|
||||
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name,
|
||||
Object configuration) {
|
||||
BeanDefinitionBuilder builder = BeanDefinitionBuilder
|
||||
.genericBeanDefinition(FeignClientSpecification.class);
|
||||
builder.addConstructorArgValue(name);
|
||||
builder.addConstructorArgValue(configuration);
|
||||
registry.registerBeanDefinition(name + "." + FeignClientSpecification.class.getSimpleName(),
|
||||
registry.registerBeanDefinition(
|
||||
name + "." + FeignClientSpecification.class.getSimpleName(),
|
||||
builder.getBeanDefinition());
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -34,7 +34,8 @@ public class HttpClient5DisabledConditions extends AnyNestedCondition {
|
||||
|
||||
}
|
||||
|
||||
@ConditionalOnProperty(value = "feign.httpclient.hc5.enabled", havingValue = "false", matchIfMissing = true)
|
||||
@ConditionalOnProperty(value = "feign.httpclient.hc5.enabled", havingValue = "false",
|
||||
matchIfMissing = true)
|
||||
static class HttpClient5Disabled {
|
||||
|
||||
}
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2013-2020 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 org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
|
||||
/**
|
||||
* @author Tim Peeters
|
||||
*/
|
||||
class HystrixDisabledConditions extends AnyNestedCondition {
|
||||
|
||||
HystrixDisabledConditions() {
|
||||
super(ConfigurationPhase.PARSE_CONFIGURATION);
|
||||
}
|
||||
|
||||
@ConditionalOnMissingClass("feign.hystrix.HystrixFeign")
|
||||
static class HystrixFeignClassMissing {
|
||||
|
||||
}
|
||||
|
||||
@ConditionalOnProperty(value = "feign.hystrix.enabled", havingValue = "false")
|
||||
static class HystrixFeignDisabled {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+101
@@ -0,0 +1,101 @@
|
||||
/*
|
||||
* Copyright 2013-2020 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 feign.Feign;
|
||||
import feign.Target;
|
||||
import feign.hystrix.FallbackFactory;
|
||||
import feign.hystrix.HystrixFeign;
|
||||
import feign.hystrix.SetterFactory;
|
||||
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
* @author Erik Kringen
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
class HystrixTargeter implements Targeter {
|
||||
|
||||
@Override
|
||||
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
|
||||
FeignContext context, Target.HardCodedTarget<T> target) {
|
||||
if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) {
|
||||
return feign.target(target);
|
||||
}
|
||||
feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign;
|
||||
String name = StringUtils.isEmpty(factory.getContextId()) ? factory.getName()
|
||||
: factory.getContextId();
|
||||
SetterFactory setterFactory = getOptional(name, context, SetterFactory.class);
|
||||
if (setterFactory != null) {
|
||||
builder.setterFactory(setterFactory);
|
||||
}
|
||||
Class<?> fallback = factory.getFallback();
|
||||
if (fallback != void.class) {
|
||||
return targetWithFallback(name, context, target, builder, fallback);
|
||||
}
|
||||
Class<?> fallbackFactory = factory.getFallbackFactory();
|
||||
if (fallbackFactory != void.class) {
|
||||
return targetWithFallbackFactory(name, context, target, builder,
|
||||
fallbackFactory);
|
||||
}
|
||||
|
||||
return feign.target(target);
|
||||
}
|
||||
|
||||
private <T> T targetWithFallbackFactory(String feignClientName, FeignContext context,
|
||||
Target.HardCodedTarget<T> target, HystrixFeign.Builder builder,
|
||||
Class<?> fallbackFactoryClass) {
|
||||
FallbackFactory<? extends T> fallbackFactory = (FallbackFactory<? extends T>) getFromContext(
|
||||
"fallbackFactory", feignClientName, context, fallbackFactoryClass,
|
||||
FallbackFactory.class);
|
||||
return builder.target(target, fallbackFactory);
|
||||
}
|
||||
|
||||
private <T> T targetWithFallback(String feignClientName, FeignContext context,
|
||||
Target.HardCodedTarget<T> target, HystrixFeign.Builder builder,
|
||||
Class<?> fallback) {
|
||||
T fallbackInstance = getFromContext("fallback", feignClientName, context,
|
||||
fallback, target.type());
|
||||
return builder.target(target, fallbackInstance);
|
||||
}
|
||||
|
||||
private <T> T getFromContext(String fallbackMechanism, String feignClientName,
|
||||
FeignContext context, Class<?> beanType, Class<T> targetType) {
|
||||
Object fallbackInstance = context.getInstance(feignClientName, beanType);
|
||||
if (fallbackInstance == null) {
|
||||
throw new IllegalStateException(String.format(
|
||||
"No " + fallbackMechanism
|
||||
+ " instance of type %s found for feign client %s",
|
||||
beanType, feignClientName));
|
||||
}
|
||||
|
||||
if (!targetType.isAssignableFrom(beanType)) {
|
||||
throw new IllegalStateException(String.format("Incompatible "
|
||||
+ fallbackMechanism
|
||||
+ " instance. Fallback/fallbackFactory of type %s is not assignable to %s for feign client %s",
|
||||
beanType, targetType, feignClientName));
|
||||
}
|
||||
return (T) fallbackInstance;
|
||||
}
|
||||
|
||||
private <T> T getOptional(String feignClientName, FeignContext context,
|
||||
Class<T> beanType) {
|
||||
return context.getInstance(feignClientName, beanType);
|
||||
}
|
||||
|
||||
}
|
||||
+3
-3
@@ -22,9 +22,9 @@ import feign.Target;
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
public interface Targeter {
|
||||
interface Targeter {
|
||||
|
||||
<T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
|
||||
Target.HardCodedTarget<T> target);
|
||||
<T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
|
||||
FeignContext context, Target.HardCodedTarget<T> target);
|
||||
|
||||
}
|
||||
|
||||
+8
-4
@@ -48,13 +48,15 @@ public class MatrixVariableParameterProcessor implements AnnotatedParameterProce
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
|
||||
public boolean processArgument(AnnotatedParameterContext context,
|
||||
Annotation annotation, Method method) {
|
||||
int parameterIndex = context.getParameterIndex();
|
||||
Class<?> parameterType = method.getParameterTypes()[parameterIndex];
|
||||
MethodMetadata data = context.getMethodMetadata();
|
||||
String name = ANNOTATION.cast(annotation).value();
|
||||
|
||||
checkState(emptyToNull(name) != null, "MatrixVariable annotation was empty on param %s.",
|
||||
checkState(emptyToNull(name) != null,
|
||||
"MatrixVariable annotation was empty on param %s.",
|
||||
context.getParameterIndex());
|
||||
|
||||
context.setParameterName(name);
|
||||
@@ -63,7 +65,8 @@ public class MatrixVariableParameterProcessor implements AnnotatedParameterProce
|
||||
data.indexToExpander().put(parameterIndex, this::expandMap);
|
||||
}
|
||||
else {
|
||||
data.indexToExpander().put(parameterIndex, object -> ";" + name + "=" + object.toString());
|
||||
data.indexToExpander().put(parameterIndex,
|
||||
object -> ";" + name + "=" + object.toString());
|
||||
}
|
||||
|
||||
return true;
|
||||
@@ -72,7 +75,8 @@ public class MatrixVariableParameterProcessor implements AnnotatedParameterProce
|
||||
private String expandMap(Object object) {
|
||||
Map<String, Object> paramMap = (Map) object;
|
||||
|
||||
return paramMap.keySet().stream().map(key -> ";" + key + "=" + paramMap.get(key).toString())
|
||||
return paramMap.keySet().stream()
|
||||
.map(key -> ";" + key + "=" + paramMap.get(key).toString())
|
||||
.collect(Collectors.joining());
|
||||
}
|
||||
|
||||
|
||||
+6
-3
@@ -46,15 +46,18 @@ public class PathVariableParameterProcessor implements AnnotatedParameterProcess
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
|
||||
public boolean processArgument(AnnotatedParameterContext context,
|
||||
Annotation annotation, Method method) {
|
||||
String name = ANNOTATION.cast(annotation).value();
|
||||
checkState(emptyToNull(name) != null, "PathVariable annotation was empty on param %s.",
|
||||
checkState(emptyToNull(name) != null,
|
||||
"PathVariable annotation was empty on param %s.",
|
||||
context.getParameterIndex());
|
||||
context.setParameterName(name);
|
||||
|
||||
MethodMetadata data = context.getMethodMetadata();
|
||||
String varName = '{' + name + '}';
|
||||
if (!data.template().url().contains(varName) && !searchMapValues(data.template().queries(), varName)
|
||||
if (!data.template().url().contains(varName)
|
||||
&& !searchMapValues(data.template().queries(), varName)
|
||||
&& !searchMapValues(data.template().headers(), varName)) {
|
||||
data.formParams().add(name);
|
||||
}
|
||||
|
||||
+2
-1
@@ -40,7 +40,8 @@ public class QueryMapParameterProcessor implements AnnotatedParameterProcessor {
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
|
||||
public boolean processArgument(AnnotatedParameterContext context,
|
||||
Annotation annotation, Method method) {
|
||||
int paramIndex = context.getParameterIndex();
|
||||
MethodMetadata metadata = context.getMethodMetadata();
|
||||
if (metadata.queryMapIndex() == null) {
|
||||
|
||||
+8
-4
@@ -46,23 +46,27 @@ public class RequestHeaderParameterProcessor implements AnnotatedParameterProces
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
|
||||
public boolean processArgument(AnnotatedParameterContext context,
|
||||
Annotation annotation, Method method) {
|
||||
int parameterIndex = context.getParameterIndex();
|
||||
Class<?> parameterType = method.getParameterTypes()[parameterIndex];
|
||||
MethodMetadata data = context.getMethodMetadata();
|
||||
|
||||
if (Map.class.isAssignableFrom(parameterType)) {
|
||||
checkState(data.headerMapIndex() == null, "Header map can only be present once.");
|
||||
checkState(data.headerMapIndex() == null,
|
||||
"Header map can only be present once.");
|
||||
data.headerMapIndex(parameterIndex);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
String name = ANNOTATION.cast(annotation).value();
|
||||
checkState(emptyToNull(name) != null, "RequestHeader.value() was empty on parameter %s", parameterIndex);
|
||||
checkState(emptyToNull(name) != null,
|
||||
"RequestHeader.value() was empty on parameter %s", parameterIndex);
|
||||
context.setParameterName(name);
|
||||
|
||||
Collection<String> header = context.setTemplateParameter(name, data.template().headers().get(name));
|
||||
Collection<String> header = context.setTemplateParameter(name,
|
||||
data.template().headers().get(name));
|
||||
data.template().header(name, header);
|
||||
return true;
|
||||
}
|
||||
|
||||
+8
-4
@@ -46,13 +46,15 @@ public class RequestParamParameterProcessor implements AnnotatedParameterProcess
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
|
||||
public boolean processArgument(AnnotatedParameterContext context,
|
||||
Annotation annotation, Method method) {
|
||||
int parameterIndex = context.getParameterIndex();
|
||||
Class<?> parameterType = method.getParameterTypes()[parameterIndex];
|
||||
MethodMetadata data = context.getMethodMetadata();
|
||||
|
||||
if (Map.class.isAssignableFrom(parameterType)) {
|
||||
checkState(data.queryMapIndex() == null, "Query map can only be present once.");
|
||||
checkState(data.queryMapIndex() == null,
|
||||
"Query map can only be present once.");
|
||||
data.queryMapIndex(parameterIndex);
|
||||
|
||||
return true;
|
||||
@@ -60,10 +62,12 @@ public class RequestParamParameterProcessor implements AnnotatedParameterProcess
|
||||
|
||||
RequestParam requestParam = ANNOTATION.cast(annotation);
|
||||
String name = requestParam.value();
|
||||
checkState(emptyToNull(name) != null, "RequestParam.value() was empty on parameter %s", parameterIndex);
|
||||
checkState(emptyToNull(name) != null,
|
||||
"RequestParam.value() was empty on parameter %s", parameterIndex);
|
||||
context.setParameterName(name);
|
||||
|
||||
Collection<String> query = context.setTemplateParameter(name, data.template().queries().get(name));
|
||||
Collection<String> query = context.setTemplateParameter(name,
|
||||
data.template().queries().get(name));
|
||||
data.template().query(name, query);
|
||||
return true;
|
||||
}
|
||||
|
||||
+6
-3
@@ -44,16 +44,19 @@ public class RequestPartParameterProcessor implements AnnotatedParameterProcesso
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
|
||||
public boolean processArgument(AnnotatedParameterContext context,
|
||||
Annotation annotation, Method method) {
|
||||
int parameterIndex = context.getParameterIndex();
|
||||
MethodMetadata data = context.getMethodMetadata();
|
||||
|
||||
String name = ANNOTATION.cast(annotation).value();
|
||||
checkState(emptyToNull(name) != null, "RequestPart.value() was empty on parameter %s", parameterIndex);
|
||||
checkState(emptyToNull(name) != null,
|
||||
"RequestPart.value() was empty on parameter %s", parameterIndex);
|
||||
context.setParameterName(name);
|
||||
|
||||
data.formParams().add(name);
|
||||
Collection<String> names = context.setTemplateParameter(name, data.indexToName().get(parameterIndex));
|
||||
Collection<String> names = context.setTemplateParameter(name,
|
||||
data.indexToName().get(parameterIndex));
|
||||
data.indexToName().put(parameterIndex, names);
|
||||
return true;
|
||||
}
|
||||
|
||||
+34
-18
@@ -66,19 +66,25 @@ public class HttpClient5FeignConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(HttpClientConnectionManager.class)
|
||||
public HttpClientConnectionManager hc5ConnectionManager(FeignHttpClientProperties httpClientProperties) {
|
||||
public HttpClientConnectionManager hc5ConnectionManager(
|
||||
FeignHttpClientProperties httpClientProperties) {
|
||||
return PoolingHttpClientConnectionManagerBuilder.create()
|
||||
.setSSLSocketFactory(httpsSSLConnectionSocketFactory(httpClientProperties.isDisableSslValidation()))
|
||||
.setSSLSocketFactory(httpsSSLConnectionSocketFactory(
|
||||
httpClientProperties.isDisableSslValidation()))
|
||||
.setMaxConnTotal(httpClientProperties.getMaxConnections())
|
||||
.setMaxConnPerRoute(httpClientProperties.getMaxConnectionsPerRoute())
|
||||
.setConnPoolPolicy(PoolReusePolicy.valueOf(httpClientProperties.getHc5().getPoolReusePolicy().name()))
|
||||
.setPoolConcurrencyPolicy(
|
||||
PoolConcurrencyPolicy.valueOf(httpClientProperties.getHc5().getPoolConcurrencyPolicy().name()))
|
||||
.setConnPoolPolicy(PoolReusePolicy.valueOf(
|
||||
httpClientProperties.getHc5().getPoolReusePolicy().name()))
|
||||
.setPoolConcurrencyPolicy(PoolConcurrencyPolicy.valueOf(
|
||||
httpClientProperties.getHc5().getPoolConcurrencyPolicy().name()))
|
||||
.setConnectionTimeToLive(
|
||||
TimeValue.of(httpClientProperties.getTimeToLive(), httpClientProperties.getTimeToLiveUnit()))
|
||||
.setDefaultSocketConfig(
|
||||
SocketConfig.custom().setSoTimeout(Timeout.of(httpClientProperties.getHc5().getSocketTimeout(),
|
||||
httpClientProperties.getHc5().getSocketTimeoutUnit())).build())
|
||||
TimeValue.of(httpClientProperties.getTimeToLive(),
|
||||
httpClientProperties.getTimeToLiveUnit()))
|
||||
.setDefaultSocketConfig(SocketConfig.custom()
|
||||
.setSoTimeout(Timeout.of(
|
||||
httpClientProperties.getHc5().getSocketTimeout(),
|
||||
httpClientProperties.getHc5().getSocketTimeoutUnit()))
|
||||
.build())
|
||||
.build();
|
||||
}
|
||||
|
||||
@@ -87,10 +93,14 @@ public class HttpClient5FeignConfiguration {
|
||||
FeignHttpClientProperties httpClientProperties) {
|
||||
httpClient5 = HttpClients.custom().disableCookieManagement().useSystemProperties()
|
||||
.setConnectionManager(connectionManager).evictExpiredConnections()
|
||||
.setDefaultRequestConfig(RequestConfig.custom()
|
||||
.setConnectTimeout(
|
||||
Timeout.of(httpClientProperties.getConnectionTimeout(), TimeUnit.MILLISECONDS))
|
||||
.setRedirectsEnabled(httpClientProperties.isFollowRedirects()).build())
|
||||
.setDefaultRequestConfig(
|
||||
RequestConfig.custom()
|
||||
.setConnectTimeout(Timeout.of(
|
||||
httpClientProperties.getConnectionTimeout(),
|
||||
TimeUnit.MILLISECONDS))
|
||||
.setRedirectsEnabled(
|
||||
httpClientProperties.isFollowRedirects())
|
||||
.build())
|
||||
.build();
|
||||
return httpClient5;
|
||||
}
|
||||
@@ -102,14 +112,17 @@ public class HttpClient5FeignConfiguration {
|
||||
}
|
||||
}
|
||||
|
||||
private LayeredConnectionSocketFactory httpsSSLConnectionSocketFactory(boolean isDisableSslValidation) {
|
||||
private LayeredConnectionSocketFactory httpsSSLConnectionSocketFactory(
|
||||
boolean isDisableSslValidation) {
|
||||
final SSLConnectionSocketFactoryBuilder sslConnectionSocketFactoryBuilder = SSLConnectionSocketFactoryBuilder
|
||||
.create().setTlsVersions(TLS.V_1_3, TLS.V_1_2);
|
||||
|
||||
if (isDisableSslValidation) {
|
||||
try {
|
||||
final SSLContext sslContext = SSLContext.getInstance("SSL");
|
||||
sslContext.init(null, new TrustManager[] { new DisabledValidationTrustManager() }, new SecureRandom());
|
||||
sslContext.init(null,
|
||||
new TrustManager[] { new DisabledValidationTrustManager() },
|
||||
new SecureRandom());
|
||||
sslConnectionSocketFactoryBuilder.setSslContext(sslContext);
|
||||
}
|
||||
catch (NoSuchAlgorithmException e) {
|
||||
@@ -120,7 +133,8 @@ public class HttpClient5FeignConfiguration {
|
||||
}
|
||||
}
|
||||
else {
|
||||
sslConnectionSocketFactoryBuilder.setSslContext(SSLContexts.createSystemDefault());
|
||||
sslConnectionSocketFactoryBuilder
|
||||
.setSslContext(SSLContexts.createSystemDefault());
|
||||
}
|
||||
|
||||
return sslConnectionSocketFactoryBuilder.build();
|
||||
@@ -131,10 +145,12 @@ public class HttpClient5FeignConfiguration {
|
||||
DisabledValidationTrustManager() {
|
||||
}
|
||||
|
||||
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
|
||||
public void checkClientTrusted(X509Certificate[] x509Certificates, String s)
|
||||
throws CertificateException {
|
||||
}
|
||||
|
||||
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
|
||||
public void checkServerTrusted(X509Certificate[] x509Certificates, String s)
|
||||
throws CertificateException {
|
||||
}
|
||||
|
||||
public X509Certificate[] getAcceptedIssuers() {
|
||||
|
||||
+30
-20
@@ -53,8 +53,8 @@ public class HttpClientFeignConfiguration {
|
||||
|
||||
private static final Log LOG = LogFactory.getLog(HttpClientFeignConfiguration.class);
|
||||
|
||||
private final Timer connectionManagerTimer = new Timer("FeignApacheHttpClientConfiguration.connectionManagerTimer",
|
||||
true);
|
||||
private final Timer connectionManagerTimer = new Timer(
|
||||
"FeignApacheHttpClientConfiguration.connectionManagerTimer", true);
|
||||
|
||||
private CloseableHttpClient httpClient;
|
||||
|
||||
@@ -66,10 +66,12 @@ public class HttpClientFeignConfiguration {
|
||||
public HttpClientConnectionManager connectionManager(
|
||||
ApacheHttpClientConnectionManagerFactory connectionManagerFactory,
|
||||
FeignHttpClientProperties httpClientProperties) {
|
||||
final HttpClientConnectionManager connectionManager = connectionManagerFactory.newConnectionManager(
|
||||
httpClientProperties.isDisableSslValidation(), httpClientProperties.getMaxConnections(),
|
||||
httpClientProperties.getMaxConnectionsPerRoute(), httpClientProperties.getTimeToLive(),
|
||||
httpClientProperties.getTimeToLiveUnit(), this.registryBuilder);
|
||||
final HttpClientConnectionManager connectionManager = connectionManagerFactory
|
||||
.newConnectionManager(httpClientProperties.isDisableSslValidation(),
|
||||
httpClientProperties.getMaxConnections(),
|
||||
httpClientProperties.getMaxConnectionsPerRoute(),
|
||||
httpClientProperties.getTimeToLive(),
|
||||
httpClientProperties.getTimeToLiveUnit(), this.registryBuilder);
|
||||
this.connectionManagerTimer.schedule(new TimerTask() {
|
||||
@Override
|
||||
public void run() {
|
||||
@@ -80,29 +82,37 @@ public class HttpClientFeignConfiguration {
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(value = "feign.compression.response.enabled", havingValue = "true")
|
||||
public CloseableHttpClient customHttpClient(HttpClientConnectionManager httpClientConnectionManager,
|
||||
@ConditionalOnProperty(value = "feign.compression.response.enabled",
|
||||
havingValue = "true")
|
||||
public CloseableHttpClient customHttpClient(
|
||||
HttpClientConnectionManager httpClientConnectionManager,
|
||||
FeignHttpClientProperties httpClientProperties) {
|
||||
HttpClientBuilder builder = HttpClientBuilder.create().disableCookieManagement().useSystemProperties();
|
||||
this.httpClient = createClient(builder, httpClientConnectionManager, httpClientProperties);
|
||||
return this.httpClient;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(value = "feign.compression.response.enabled", havingValue = "false", matchIfMissing = true)
|
||||
public CloseableHttpClient httpClient(ApacheHttpClientFactory httpClientFactory,
|
||||
HttpClientConnectionManager httpClientConnectionManager, FeignHttpClientProperties httpClientProperties) {
|
||||
this.httpClient = createClient(httpClientFactory.createBuilder(), httpClientConnectionManager,
|
||||
HttpClientBuilder builder = HttpClientBuilder.create().disableCookieManagement()
|
||||
.useSystemProperties();
|
||||
this.httpClient = createClient(builder, httpClientConnectionManager,
|
||||
httpClientProperties);
|
||||
return this.httpClient;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnProperty(value = "feign.compression.response.enabled",
|
||||
havingValue = "false", matchIfMissing = true)
|
||||
public CloseableHttpClient httpClient(ApacheHttpClientFactory httpClientFactory,
|
||||
HttpClientConnectionManager httpClientConnectionManager,
|
||||
FeignHttpClientProperties httpClientProperties) {
|
||||
this.httpClient = createClient(httpClientFactory.createBuilder(),
|
||||
httpClientConnectionManager, httpClientProperties);
|
||||
return this.httpClient;
|
||||
}
|
||||
|
||||
private CloseableHttpClient createClient(HttpClientBuilder builder,
|
||||
HttpClientConnectionManager httpClientConnectionManager, FeignHttpClientProperties httpClientProperties) {
|
||||
HttpClientConnectionManager httpClientConnectionManager,
|
||||
FeignHttpClientProperties httpClientProperties) {
|
||||
RequestConfig defaultRequestConfig = RequestConfig.custom()
|
||||
.setConnectTimeout(httpClientProperties.getConnectionTimeout())
|
||||
.setRedirectsEnabled(httpClientProperties.isFollowRedirects()).build();
|
||||
CloseableHttpClient httpClient = builder.setDefaultRequestConfig(defaultRequestConfig)
|
||||
CloseableHttpClient httpClient = builder
|
||||
.setDefaultRequestConfig(defaultRequestConfig)
|
||||
.setConnectionManager(httpClientConnectionManager).build();
|
||||
return httpClient;
|
||||
}
|
||||
|
||||
+8
-5
@@ -46,7 +46,8 @@ public class OkHttpFeignConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(ConnectionPool.class)
|
||||
public ConnectionPool httpClientConnectionPool(FeignHttpClientProperties httpClientProperties,
|
||||
public ConnectionPool httpClientConnectionPool(
|
||||
FeignHttpClientProperties httpClientProperties,
|
||||
OkHttpClientConnectionPoolFactory connectionPoolFactory) {
|
||||
Integer maxTotalConnections = httpClientProperties.getMaxConnections();
|
||||
Long timeToLive = httpClientProperties.getTimeToLive();
|
||||
@@ -55,13 +56,15 @@ public class OkHttpFeignConfiguration {
|
||||
}
|
||||
|
||||
@Bean
|
||||
public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory, ConnectionPool connectionPool,
|
||||
public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory,
|
||||
ConnectionPool connectionPool,
|
||||
FeignHttpClientProperties httpClientProperties) {
|
||||
Boolean followRedirects = httpClientProperties.isFollowRedirects();
|
||||
Integer connectTimeout = httpClientProperties.getConnectionTimeout();
|
||||
this.okHttpClient = httpClientFactory.createBuilder(httpClientProperties.isDisableSslValidation())
|
||||
.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS).followRedirects(followRedirects)
|
||||
.connectionPool(connectionPool).build();
|
||||
this.okHttpClient = httpClientFactory
|
||||
.createBuilder(httpClientProperties.isDisableSslValidation())
|
||||
.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
|
||||
.followRedirects(followRedirects).connectionPool(connectionPool).build();
|
||||
return this.okHttpClient;
|
||||
}
|
||||
|
||||
|
||||
+2
-1
@@ -48,7 +48,8 @@ public abstract class BaseRequestInterceptor implements RequestInterceptor {
|
||||
* @param name the header name
|
||||
* @param values the header values
|
||||
*/
|
||||
protected void addHeader(RequestTemplate requestTemplate, String name, String... values) {
|
||||
protected void addHeader(RequestTemplate requestTemplate, String name,
|
||||
String... values) {
|
||||
|
||||
if (!requestTemplate.headers().containsKey(name)) {
|
||||
requestTemplate.header(name, values);
|
||||
|
||||
+2
-1
@@ -39,7 +39,8 @@ import org.springframework.context.annotation.Configuration;
|
||||
@EnableConfigurationProperties(FeignClientEncodingProperties.class)
|
||||
@ConditionalOnClass(Feign.class)
|
||||
@ConditionalOnBean(Client.class)
|
||||
@ConditionalOnProperty(value = "feign.compression.response.enabled", matchIfMissing = false)
|
||||
@ConditionalOnProperty(value = "feign.compression.response.enabled",
|
||||
matchIfMissing = false)
|
||||
// The OK HTTP client uses "transparent" compression.
|
||||
// If the accept-encoding header is present it disable transparent compression
|
||||
@ConditionalOnMissingBean(type = "okhttp3.OkHttpClient")
|
||||
|
||||
+4
-3
@@ -33,7 +33,8 @@ public class FeignAcceptGzipEncodingInterceptor extends BaseRequestInterceptor {
|
||||
* Creates new instance of {@link FeignAcceptGzipEncodingInterceptor}.
|
||||
* @param properties the encoding properties
|
||||
*/
|
||||
protected FeignAcceptGzipEncodingInterceptor(FeignClientEncodingProperties properties) {
|
||||
protected FeignAcceptGzipEncodingInterceptor(
|
||||
FeignClientEncodingProperties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
@@ -43,8 +44,8 @@ public class FeignAcceptGzipEncodingInterceptor extends BaseRequestInterceptor {
|
||||
@Override
|
||||
public void apply(RequestTemplate template) {
|
||||
|
||||
addHeader(template, HttpEncoding.ACCEPT_ENCODING_HEADER, HttpEncoding.GZIP_ENCODING,
|
||||
HttpEncoding.DEFLATE_ENCODING);
|
||||
addHeader(template, HttpEncoding.ACCEPT_ENCODING_HEADER,
|
||||
HttpEncoding.GZIP_ENCODING, HttpEncoding.DEFLATE_ENCODING);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
-3
@@ -32,7 +32,8 @@ public class FeignClientEncodingProperties {
|
||||
/**
|
||||
* The list of supported mime types.
|
||||
*/
|
||||
private String[] mimeTypes = new String[] { "text/xml", "application/xml", "application/json" };
|
||||
private String[] mimeTypes = new String[] { "text/xml", "application/xml",
|
||||
"application/json" };
|
||||
|
||||
/**
|
||||
* The minimum threshold content size.
|
||||
@@ -76,8 +77,9 @@ public class FeignClientEncodingProperties {
|
||||
@Override
|
||||
public String toString() {
|
||||
return new StringBuilder("FeignClientEncodingProperties{").append("mimeTypes=")
|
||||
.append(Arrays.toString(this.mimeTypes)).append(", ").append("minRequestSize=")
|
||||
.append(this.minRequestSize).append("}").toString();
|
||||
.append(Arrays.toString(this.mimeTypes)).append(", ")
|
||||
.append("minRequestSize=").append(this.minRequestSize).append("}")
|
||||
.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
-4
@@ -33,7 +33,8 @@ public class FeignContentGzipEncodingInterceptor extends BaseRequestInterceptor
|
||||
* Creates new instance of {@link FeignContentGzipEncodingInterceptor}.
|
||||
* @param properties the encoding properties
|
||||
*/
|
||||
protected FeignContentGzipEncodingInterceptor(FeignClientEncodingProperties properties) {
|
||||
protected FeignContentGzipEncodingInterceptor(
|
||||
FeignClientEncodingProperties properties) {
|
||||
super(properties);
|
||||
}
|
||||
|
||||
@@ -44,8 +45,8 @@ public class FeignContentGzipEncodingInterceptor extends BaseRequestInterceptor
|
||||
public void apply(RequestTemplate template) {
|
||||
|
||||
if (requiresCompression(template)) {
|
||||
addHeader(template, HttpEncoding.CONTENT_ENCODING_HEADER, HttpEncoding.GZIP_ENCODING,
|
||||
HttpEncoding.DEFLATE_ENCODING);
|
||||
addHeader(template, HttpEncoding.CONTENT_ENCODING_HEADER,
|
||||
HttpEncoding.GZIP_ENCODING, HttpEncoding.DEFLATE_ENCODING);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -92,7 +93,8 @@ public class FeignContentGzipEncodingInterceptor extends BaseRequestInterceptor
|
||||
return false;
|
||||
}
|
||||
|
||||
if (getProperties().getMimeTypes() == null || getProperties().getMimeTypes().length == 0) {
|
||||
if (getProperties().getMimeTypes() == null
|
||||
|| getProperties().getMimeTypes().length == 0) {
|
||||
// no specific mime types has been set - matching everything
|
||||
return true;
|
||||
}
|
||||
|
||||
+10
-6
@@ -51,7 +51,8 @@ import static org.springframework.hateoas.MediaTypes.HAL_JSON;
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnWebApplication
|
||||
@ConditionalOnClass(RepresentationModel.class)
|
||||
@AutoConfigureAfter({ JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,
|
||||
@AutoConfigureAfter({ JacksonAutoConfiguration.class,
|
||||
HttpMessageConvertersAutoConfiguration.class,
|
||||
RepositoryRestMvcAutoConfiguration.class })
|
||||
@AutoConfigureBefore(HypermediaAutoConfiguration.class)
|
||||
public class FeignHalAutoConfiguration {
|
||||
@@ -59,21 +60,24 @@ public class FeignHalAutoConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public TypeConstrainedMappingJackson2HttpMessageConverter halJacksonHttpMessageConverter(
|
||||
ObjectProvider<ObjectMapper> objectMapper, ObjectProvider<HalConfiguration> halConfiguration,
|
||||
ObjectProvider<MessageResolver> messageResolver, ObjectProvider<CurieProvider> curieProvider,
|
||||
ObjectProvider<ObjectMapper> objectMapper,
|
||||
ObjectProvider<HalConfiguration> halConfiguration,
|
||||
ObjectProvider<MessageResolver> messageResolver,
|
||||
ObjectProvider<CurieProvider> curieProvider,
|
||||
ObjectProvider<LinkRelationProvider> linkRelationProvider) {
|
||||
|
||||
ObjectMapper mapper = objectMapper.getIfAvailable(ObjectMapper::new).copy();
|
||||
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
|
||||
|
||||
HalConfiguration configuration = halConfiguration.getIfAvailable(HalConfiguration::new);
|
||||
HalConfiguration configuration = halConfiguration
|
||||
.getIfAvailable(HalConfiguration::new);
|
||||
|
||||
CurieProvider curieProviderInstance = curieProvider
|
||||
.getIfAvailable(() -> new DefaultCurieProvider(Collections.emptyMap()));
|
||||
|
||||
Jackson2HalModule.HalHandlerInstantiator halHandlerInstantiator = new Jackson2HalModule.HalHandlerInstantiator(
|
||||
linkRelationProvider.getIfAvailable(), curieProviderInstance, messageResolver.getIfAvailable(),
|
||||
configuration);
|
||||
linkRelationProvider.getIfAvailable(), curieProviderInstance,
|
||||
messageResolver.getIfAvailable(), configuration);
|
||||
|
||||
mapper.setHandlerInstantiator(halHandlerInstantiator);
|
||||
|
||||
|
||||
+17
-18
@@ -16,52 +16,51 @@
|
||||
|
||||
package org.springframework.cloud.openfeign.loadbalancer;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import feign.Client;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
|
||||
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
|
||||
/**
|
||||
* Configuration instantiating a {@link LoadBalancerClient}-based {@link Client} object
|
||||
* that uses {@link Client.Default} under the hood.
|
||||
* Configuration instantiating a {@link BlockingLoadBalancerClient}-based {@link Client}
|
||||
* object that uses {@link Client.Default} under the hood.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @since 2.2.0
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableConfigurationProperties(LoadBalancerProperties.class)
|
||||
class DefaultFeignLoadBalancerConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@Conditional(OnRetryNotEnabledCondition.class)
|
||||
public Client feignClient(LoadBalancerClient loadBalancerClient, LoadBalancerProperties properties,
|
||||
LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||
return new FeignBlockingLoadBalancerClient(new Client.Default(null, null), loadBalancerClient, properties,
|
||||
loadBalancerClientFactory);
|
||||
public Client feignClient(BlockingLoadBalancerClient loadBalancerClient) {
|
||||
return new FeignBlockingLoadBalancerClient(new Client.Default(null, null),
|
||||
loadBalancerClient);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
|
||||
@ConditionalOnBean(LoadBalancedRetryFactory.class)
|
||||
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true",
|
||||
matchIfMissing = true)
|
||||
public Client feignRetryClient(LoadBalancerClient loadBalancerClient,
|
||||
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties,
|
||||
LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||
return new RetryableFeignBlockingLoadBalancerClient(new Client.Default(null, null), loadBalancerClient,
|
||||
loadBalancedRetryFactory, properties, loadBalancerClientFactory);
|
||||
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled",
|
||||
havingValue = "true", matchIfMissing = true)
|
||||
public Client feignRetryClient(BlockingLoadBalancerClient loadBalancerClient,
|
||||
List<LoadBalancedRetryFactory> loadBalancedRetryFactories) {
|
||||
AnnotationAwareOrderComparator.sort(loadBalancedRetryFactories);
|
||||
return new RetryableFeignBlockingLoadBalancerClient(
|
||||
new Client.Default(null, null), loadBalancerClient,
|
||||
loadBalancedRetryFactories.get(0));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+20
-55
@@ -19,7 +19,6 @@ package org.springframework.cloud.openfeign.loadbalancer;
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Set;
|
||||
|
||||
import feign.Client;
|
||||
import feign.Request;
|
||||
@@ -28,86 +27,58 @@ import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.loadbalancer.CompletionContext;
|
||||
import org.springframework.cloud.client.loadbalancer.DefaultRequest;
|
||||
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycleValidator;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
|
||||
import org.springframework.cloud.client.loadbalancer.RequestDataContext;
|
||||
import org.springframework.cloud.client.loadbalancer.ResponseData;
|
||||
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import static org.springframework.cloud.openfeign.loadbalancer.LoadBalancerUtils.buildRequestData;
|
||||
import static org.springframework.cloud.openfeign.loadbalancer.LoadBalancerUtils.executeWithLoadBalancerLifecycleProcessing;
|
||||
|
||||
/**
|
||||
* A {@link Client} implementation that uses {@link LoadBalancerClient} to select a
|
||||
* {@link ServiceInstance} to use while resolving the request host.
|
||||
* A {@link Client} implementation that uses {@link BlockingLoadBalancerClient} to select
|
||||
* a {@link ServiceInstance} to use while resolving the request host.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @since 2.2.0
|
||||
*/
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
public class FeignBlockingLoadBalancerClient implements Client {
|
||||
|
||||
private static final Log LOG = LogFactory.getLog(FeignBlockingLoadBalancerClient.class);
|
||||
private static final Log LOG = LogFactory
|
||||
.getLog(FeignBlockingLoadBalancerClient.class);
|
||||
|
||||
private final Client delegate;
|
||||
|
||||
private final LoadBalancerClient loadBalancerClient;
|
||||
private final BlockingLoadBalancerClient loadBalancerClient;
|
||||
|
||||
private final LoadBalancerProperties properties;
|
||||
|
||||
private final LoadBalancerClientFactory loadBalancerClientFactory;
|
||||
|
||||
public FeignBlockingLoadBalancerClient(Client delegate, LoadBalancerClient loadBalancerClient,
|
||||
LoadBalancerProperties properties, LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||
public FeignBlockingLoadBalancerClient(Client delegate,
|
||||
BlockingLoadBalancerClient loadBalancerClient) {
|
||||
this.delegate = delegate;
|
||||
this.loadBalancerClient = loadBalancerClient;
|
||||
this.properties = properties;
|
||||
this.loadBalancerClientFactory = loadBalancerClientFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response execute(Request request, Request.Options options) throws IOException {
|
||||
final URI originalUri = URI.create(request.url());
|
||||
String serviceId = originalUri.getHost();
|
||||
Assert.state(serviceId != null, "Request URI does not contain a valid hostname: " + originalUri);
|
||||
String hint = getHint(serviceId);
|
||||
DefaultRequest<RequestDataContext> lbRequest = new DefaultRequest<>(
|
||||
new RequestDataContext(buildRequestData(request), hint));
|
||||
Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator
|
||||
.getSupportedLifecycleProcessors(
|
||||
loadBalancerClientFactory.getInstances(serviceId, LoadBalancerLifecycle.class),
|
||||
RequestDataContext.class, ResponseData.class, ServiceInstance.class);
|
||||
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
|
||||
ServiceInstance instance = loadBalancerClient.choose(serviceId, lbRequest);
|
||||
org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse = new DefaultResponse(
|
||||
instance);
|
||||
Assert.state(serviceId != null,
|
||||
"Request URI does not contain a valid hostname: " + originalUri);
|
||||
ServiceInstance instance = loadBalancerClient.choose(serviceId);
|
||||
if (instance == null) {
|
||||
String message = "Load balancer does not contain an instance for the service " + serviceId;
|
||||
String message = "Load balancer does not contain an instance for the service "
|
||||
+ serviceId;
|
||||
if (LOG.isWarnEnabled()) {
|
||||
LOG.warn(message);
|
||||
}
|
||||
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
|
||||
.onComplete(new CompletionContext<ResponseData, ServiceInstance, RequestDataContext>(
|
||||
CompletionContext.Status.DISCARD, lbRequest, lbResponse)));
|
||||
return Response.builder().request(request).status(HttpStatus.SERVICE_UNAVAILABLE.value())
|
||||
return Response.builder().request(request)
|
||||
.status(HttpStatus.SERVICE_UNAVAILABLE.value())
|
||||
.body(message, StandardCharsets.UTF_8).build();
|
||||
}
|
||||
String reconstructedUrl = loadBalancerClient.reconstructURI(instance, originalUri).toString();
|
||||
String reconstructedUrl = loadBalancerClient.reconstructURI(instance, originalUri)
|
||||
.toString();
|
||||
Request newRequest = buildRequest(request, reconstructedUrl);
|
||||
return executeWithLoadBalancerLifecycleProcessing(delegate, options, newRequest, lbRequest, lbResponse,
|
||||
supportedLifecycleProcessors);
|
||||
return delegate.execute(newRequest, options);
|
||||
}
|
||||
|
||||
protected Request buildRequest(Request request, String reconstructedUrl) {
|
||||
return Request.create(request.httpMethod(), reconstructedUrl, request.headers(), request.body(),
|
||||
request.charset(), request.requestTemplate());
|
||||
return Request.create(request.httpMethod(), reconstructedUrl, request.headers(),
|
||||
request.body(), request.charset(), request.requestTemplate());
|
||||
}
|
||||
|
||||
// Visible for Sleuth instrumentation
|
||||
@@ -115,10 +86,4 @@ public class FeignBlockingLoadBalancerClient implements Client {
|
||||
return delegate;
|
||||
}
|
||||
|
||||
private String getHint(String serviceId) {
|
||||
String defaultHint = properties.getHint().getOrDefault("default", "default");
|
||||
String hintPropertyValue = properties.getHint().get(serviceId);
|
||||
return hintPropertyValue != null ? hintPropertyValue : defaultHint;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+12
-10
@@ -24,34 +24,36 @@ import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
|
||||
import org.springframework.cloud.loadbalancer.config.BlockingLoadBalancerClientAutoConfiguration;
|
||||
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
|
||||
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
|
||||
import org.springframework.cloud.openfeign.ribbon.FeignRibbonClientAutoConfiguration;
|
||||
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
/**
|
||||
* An autoconfiguration that instantiates {@link LoadBalancerClient}-based implementations
|
||||
* of {@link Client}.
|
||||
* An autoconfiguration that instantiates {@link BlockingLoadBalancerClient}-based
|
||||
* implementations of {@link Client}. In order to use this load-balancing mechanism, the
|
||||
* Ribbon-based implementation has to be disabled by setting
|
||||
* <code>spring.cloud.loadbalancer.ribbon.enabled</code> to <code>true</code>.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Nguyen Ky Thanh
|
||||
* @since 2.2.0
|
||||
*/
|
||||
@ConditionalOnClass(Feign.class)
|
||||
@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
|
||||
@ConditionalOnBean(BlockingLoadBalancerClient.class)
|
||||
@AutoConfigureBefore(FeignAutoConfiguration.class)
|
||||
@AutoConfigureAfter({ BlockingLoadBalancerClientAutoConfiguration.class, LoadBalancerAutoConfiguration.class })
|
||||
@AutoConfigureAfter(FeignRibbonClientAutoConfiguration.class)
|
||||
@EnableConfigurationProperties(FeignHttpClientProperties.class)
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
// Order is important here, last should be the default, first should be optional
|
||||
// see
|
||||
// https://github.com/spring-cloud/spring-cloud-netflix/issues/2086#issuecomment-316281653
|
||||
@Import({ HttpClientFeignLoadBalancerConfiguration.class, OkHttpFeignLoadBalancerConfiguration.class,
|
||||
HttpClient5FeignLoadBalancerConfiguration.class, DefaultFeignLoadBalancerConfiguration.class })
|
||||
@Import({ HttpClientFeignLoadBalancerConfiguration.class,
|
||||
OkHttpFeignLoadBalancerConfiguration.class,
|
||||
HttpClient5FeignLoadBalancerConfiguration.class,
|
||||
DefaultFeignLoadBalancerConfiguration.class })
|
||||
public class FeignLoadBalancerAutoConfiguration {
|
||||
|
||||
}
|
||||
|
||||
+18
-18
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.cloud.openfeign.loadbalancer;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import feign.Client;
|
||||
import feign.hc5.ApacheHttp5Client;
|
||||
import org.apache.hc.client5.http.classic.HttpClient;
|
||||
@@ -24,52 +26,50 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
|
||||
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
|
||||
import org.springframework.cloud.openfeign.clientconfig.HttpClient5FeignConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
|
||||
/**
|
||||
* Configuration instantiating a {@link LoadBalancerClient}-based {@link Client} object
|
||||
* that uses {@link ApacheHttp5Client} under the hood.
|
||||
* Configuration instantiating a {@link BlockingLoadBalancerClient}-based {@link Client}
|
||||
* object that uses {@link ApacheHttp5Client} under the hood.
|
||||
*
|
||||
* @author Nguyen Ky Thanh
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(ApacheHttp5Client.class)
|
||||
@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
|
||||
@ConditionalOnBean(BlockingLoadBalancerClient.class)
|
||||
@ConditionalOnProperty(value = "feign.httpclient.hc5.enabled", havingValue = "true")
|
||||
@Import(HttpClient5FeignConfiguration.class)
|
||||
@EnableConfigurationProperties(LoadBalancerProperties.class)
|
||||
class HttpClient5FeignLoadBalancerConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@Conditional(OnRetryNotEnabledCondition.class)
|
||||
public Client feignClient(LoadBalancerClient loadBalancerClient, HttpClient httpClient5,
|
||||
LoadBalancerProperties properties, LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||
public Client feignClient(BlockingLoadBalancerClient loadBalancerClient,
|
||||
HttpClient httpClient5) {
|
||||
Client delegate = new ApacheHttp5Client(httpClient5);
|
||||
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, properties, loadBalancerClientFactory);
|
||||
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
|
||||
@ConditionalOnBean(LoadBalancedRetryFactory.class)
|
||||
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true",
|
||||
matchIfMissing = true)
|
||||
public Client feignRetryClient(LoadBalancerClient loadBalancerClient, HttpClient httpClient5,
|
||||
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties,
|
||||
LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled",
|
||||
havingValue = "true", matchIfMissing = true)
|
||||
public Client feignRetryClient(BlockingLoadBalancerClient loadBalancerClient,
|
||||
HttpClient httpClient5,
|
||||
List<LoadBalancedRetryFactory> loadBalancedRetryFactories) {
|
||||
AnnotationAwareOrderComparator.sort(loadBalancedRetryFactories);
|
||||
Client delegate = new ApacheHttp5Client(httpClient5);
|
||||
return new RetryableFeignBlockingLoadBalancerClient(delegate, loadBalancerClient, loadBalancedRetryFactory,
|
||||
properties, loadBalancerClientFactory);
|
||||
return new RetryableFeignBlockingLoadBalancerClient(delegate, loadBalancerClient,
|
||||
loadBalancedRetryFactories.get(0));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+18
-18
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.cloud.openfeign.loadbalancer;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import feign.Client;
|
||||
import feign.httpclient.ApacheHttpClient;
|
||||
import org.apache.http.client.HttpClient;
|
||||
@@ -24,21 +26,19 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
|
||||
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
|
||||
import org.springframework.cloud.openfeign.HttpClient5DisabledConditions;
|
||||
import org.springframework.cloud.openfeign.clientconfig.HttpClientFeignConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
|
||||
/**
|
||||
* Configuration instantiating a {@link LoadBalancerClient}-based {@link Client} object
|
||||
* that uses {@link ApacheHttpClient} under the hood.
|
||||
* Configuration instantiating a {@link BlockingLoadBalancerClient}-based {@link Client}
|
||||
* object that uses {@link ApacheHttpClient} under the hood.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Nguyen Ky Thanh
|
||||
@@ -46,34 +46,34 @@ import org.springframework.context.annotation.Import;
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(ApacheHttpClient.class)
|
||||
@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
|
||||
@ConditionalOnBean(BlockingLoadBalancerClient.class)
|
||||
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
|
||||
@Conditional(HttpClient5DisabledConditions.class)
|
||||
@Import(HttpClientFeignConfiguration.class)
|
||||
@EnableConfigurationProperties(LoadBalancerProperties.class)
|
||||
class HttpClientFeignLoadBalancerConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@Conditional(OnRetryNotEnabledCondition.class)
|
||||
public Client feignClient(LoadBalancerClient loadBalancerClient, HttpClient httpClient,
|
||||
LoadBalancerProperties properties, LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||
public Client feignClient(BlockingLoadBalancerClient loadBalancerClient,
|
||||
HttpClient httpClient) {
|
||||
ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
|
||||
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, properties, loadBalancerClientFactory);
|
||||
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
|
||||
@ConditionalOnBean(LoadBalancedRetryFactory.class)
|
||||
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true",
|
||||
matchIfMissing = true)
|
||||
public Client feignRetryClient(LoadBalancerClient loadBalancerClient, HttpClient httpClient,
|
||||
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties,
|
||||
LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled",
|
||||
havingValue = "true", matchIfMissing = true)
|
||||
public Client feignRetryClient(BlockingLoadBalancerClient loadBalancerClient,
|
||||
HttpClient httpClient,
|
||||
List<LoadBalancedRetryFactory> loadBalancedRetryFactories) {
|
||||
AnnotationAwareOrderComparator.sort(loadBalancedRetryFactories);
|
||||
ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
|
||||
return new RetryableFeignBlockingLoadBalancerClient(delegate, loadBalancerClient, loadBalancedRetryFactory,
|
||||
properties, loadBalancerClientFactory);
|
||||
return new RetryableFeignBlockingLoadBalancerClient(delegate, loadBalancerClient,
|
||||
loadBalancedRetryFactories.get(0));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-95
@@ -1,95 +0,0 @@
|
||||
/*
|
||||
* Copyright 2013-2020 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.loadbalancer;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.Set;
|
||||
|
||||
import feign.Client;
|
||||
import feign.Request;
|
||||
import feign.Response;
|
||||
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.loadbalancer.CompletionContext;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle;
|
||||
import org.springframework.cloud.client.loadbalancer.RequestData;
|
||||
import org.springframework.cloud.client.loadbalancer.ResponseData;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpStatus;
|
||||
|
||||
/**
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*
|
||||
* A utility class for handling {@link LoadBalancerLifecycle} calls.
|
||||
*/
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
final class LoadBalancerUtils {
|
||||
|
||||
private LoadBalancerUtils() {
|
||||
throw new IllegalStateException("Can't instantiate a utility class");
|
||||
}
|
||||
|
||||
static Response executeWithLoadBalancerLifecycleProcessing(Client feignClient, Request.Options options,
|
||||
Request feignRequest, org.springframework.cloud.client.loadbalancer.Request lbRequest,
|
||||
org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse,
|
||||
Set<LoadBalancerLifecycle> supportedLifecycleProcessors, boolean loadBalanced) throws IOException {
|
||||
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStartRequest(lbRequest, lbResponse));
|
||||
try {
|
||||
Response response = feignClient.execute(feignRequest, options);
|
||||
if (loadBalanced) {
|
||||
supportedLifecycleProcessors.forEach(
|
||||
lifecycle -> lifecycle.onComplete(new CompletionContext<>(CompletionContext.Status.SUCCESS,
|
||||
lbRequest, lbResponse, buildResponseData(response))));
|
||||
}
|
||||
return response;
|
||||
}
|
||||
catch (Exception exception) {
|
||||
if (loadBalanced) {
|
||||
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onComplete(
|
||||
new CompletionContext<>(CompletionContext.Status.FAILED, exception, lbRequest, lbResponse)));
|
||||
}
|
||||
throw exception;
|
||||
}
|
||||
}
|
||||
|
||||
static ResponseData buildResponseData(Response response) {
|
||||
HttpHeaders responseHeaders = new HttpHeaders();
|
||||
response.headers().forEach((key, value) -> responseHeaders.put(key, new ArrayList<>(value)));
|
||||
return new ResponseData(HttpStatus.resolve(response.status()), responseHeaders, null,
|
||||
buildRequestData(response.request()));
|
||||
}
|
||||
|
||||
static RequestData buildRequestData(Request request) {
|
||||
HttpHeaders requestHeaders = new HttpHeaders();
|
||||
request.headers().forEach((key, value) -> requestHeaders.put(key, new ArrayList<>(value)));
|
||||
return new RequestData(HttpMethod.resolve(request.httpMethod().name()), URI.create(request.url()),
|
||||
requestHeaders, null, new HashMap<>());
|
||||
}
|
||||
|
||||
static Response executeWithLoadBalancerLifecycleProcessing(Client feignClient, Request.Options options,
|
||||
Request feignRequest, org.springframework.cloud.client.loadbalancer.Request lbRequest,
|
||||
org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse,
|
||||
Set<LoadBalancerLifecycle> supportedLifecycleProcessors) throws IOException {
|
||||
return executeWithLoadBalancerLifecycleProcessing(feignClient, options, feignRequest, lbRequest, lbResponse,
|
||||
supportedLifecycleProcessors, true);
|
||||
}
|
||||
|
||||
}
|
||||
+18
-18
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.cloud.openfeign.loadbalancer;
|
||||
|
||||
import java.util.List;
|
||||
|
||||
import feign.Client;
|
||||
import feign.okhttp.OkHttpClient;
|
||||
|
||||
@@ -23,20 +25,18 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
|
||||
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
|
||||
import org.springframework.cloud.openfeign.clientconfig.OkHttpFeignConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
|
||||
/**
|
||||
* Configuration instantiating a {@link LoadBalancerClient}-based {@link Client} object
|
||||
* that uses {@link OkHttpClient} under the hood.
|
||||
* Configuration instantiating a {@link BlockingLoadBalancerClient}-based {@link Client}
|
||||
* object that uses {@link OkHttpClient} under the hood.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @since 2.2.0
|
||||
@@ -44,32 +44,32 @@ import org.springframework.context.annotation.Import;
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(OkHttpClient.class)
|
||||
@ConditionalOnProperty("feign.okhttp.enabled")
|
||||
@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
|
||||
@ConditionalOnBean(BlockingLoadBalancerClient.class)
|
||||
@Import(OkHttpFeignConfiguration.class)
|
||||
@EnableConfigurationProperties(LoadBalancerProperties.class)
|
||||
class OkHttpFeignLoadBalancerConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@Conditional(OnRetryNotEnabledCondition.class)
|
||||
public Client feignClient(okhttp3.OkHttpClient okHttpClient, LoadBalancerClient loadBalancerClient,
|
||||
LoadBalancerProperties properties, LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||
public Client feignClient(okhttp3.OkHttpClient okHttpClient,
|
||||
BlockingLoadBalancerClient loadBalancerClient) {
|
||||
OkHttpClient delegate = new OkHttpClient(okHttpClient);
|
||||
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, properties, loadBalancerClientFactory);
|
||||
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
|
||||
@ConditionalOnBean(LoadBalancedRetryFactory.class)
|
||||
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true",
|
||||
matchIfMissing = true)
|
||||
public Client feignRetryClient(LoadBalancerClient loadBalancerClient, okhttp3.OkHttpClient okHttpClient,
|
||||
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties,
|
||||
LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled",
|
||||
havingValue = "true", matchIfMissing = true)
|
||||
public Client feignRetryClient(BlockingLoadBalancerClient loadBalancerClient,
|
||||
okhttp3.OkHttpClient okHttpClient,
|
||||
List<LoadBalancedRetryFactory> loadBalancedRetryFactories) {
|
||||
AnnotationAwareOrderComparator.sort(loadBalancedRetryFactories);
|
||||
OkHttpClient delegate = new OkHttpClient(okHttpClient);
|
||||
return new RetryableFeignBlockingLoadBalancerClient(delegate, loadBalancerClient, loadBalancedRetryFactory,
|
||||
properties, loadBalancerClientFactory);
|
||||
return new RetryableFeignBlockingLoadBalancerClient(delegate, loadBalancerClient,
|
||||
loadBalancedRetryFactories.get(0));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-1
@@ -47,7 +47,8 @@ public class OnRetryNotEnabledCondition extends AnyNestedCondition {
|
||||
|
||||
}
|
||||
|
||||
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "false")
|
||||
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled",
|
||||
havingValue = "false")
|
||||
static class OnLoadBalancerRetryEnabledCondition {
|
||||
|
||||
}
|
||||
|
||||
+41
-90
@@ -23,7 +23,6 @@ import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import feign.Client;
|
||||
import feign.Request;
|
||||
@@ -32,22 +31,13 @@ import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.loadbalancer.CompletionContext;
|
||||
import org.springframework.cloud.client.loadbalancer.DefaultRequest;
|
||||
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
|
||||
import org.springframework.cloud.client.loadbalancer.InterceptorRetryPolicy;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancedRecoveryCallback;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryContext;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicy;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycleValidator;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
|
||||
import org.springframework.cloud.client.loadbalancer.ResponseData;
|
||||
import org.springframework.cloud.client.loadbalancer.RetryableRequestContext;
|
||||
import org.springframework.cloud.client.loadbalancer.RetryableStatusCodeException;
|
||||
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpRequest;
|
||||
@@ -56,9 +46,6 @@ import org.springframework.retry.backoff.BackOffPolicy;
|
||||
import org.springframework.retry.backoff.NoBackOffPolicy;
|
||||
import org.springframework.retry.policy.NeverRetryPolicy;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.util.Assert;
|
||||
|
||||
import static org.springframework.cloud.openfeign.loadbalancer.LoadBalancerUtils.buildRequestData;
|
||||
|
||||
/**
|
||||
* A {@link Client} implementation that provides Spring Retry support for requests
|
||||
@@ -67,102 +54,67 @@ import static org.springframework.cloud.openfeign.loadbalancer.LoadBalancerUtils
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @since 2.2.6
|
||||
*/
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
public class RetryableFeignBlockingLoadBalancerClient implements Client {
|
||||
|
||||
private static final Log LOG = LogFactory.getLog(FeignBlockingLoadBalancerClient.class);
|
||||
private static final Log LOG = LogFactory
|
||||
.getLog(FeignBlockingLoadBalancerClient.class);
|
||||
|
||||
private final Client delegate;
|
||||
|
||||
private final LoadBalancerClient loadBalancerClient;
|
||||
private final BlockingLoadBalancerClient loadBalancerClient;
|
||||
|
||||
private final LoadBalancedRetryFactory loadBalancedRetryFactory;
|
||||
|
||||
private final LoadBalancerProperties properties;
|
||||
|
||||
private final LoadBalancerClientFactory loadBalancerClientFactory;
|
||||
|
||||
public RetryableFeignBlockingLoadBalancerClient(Client delegate, LoadBalancerClient loadBalancerClient,
|
||||
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties,
|
||||
LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||
public RetryableFeignBlockingLoadBalancerClient(Client delegate,
|
||||
BlockingLoadBalancerClient loadBalancerClient,
|
||||
LoadBalancedRetryFactory loadBalancedRetryFactory) {
|
||||
this.delegate = delegate;
|
||||
this.loadBalancerClient = loadBalancerClient;
|
||||
this.loadBalancedRetryFactory = loadBalancedRetryFactory;
|
||||
this.properties = properties;
|
||||
this.loadBalancerClientFactory = loadBalancerClientFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response execute(Request request, Request.Options options) throws IOException {
|
||||
final URI originalUri = URI.create(request.url());
|
||||
String serviceId = originalUri.getHost();
|
||||
Assert.state(serviceId != null, "Request URI does not contain a valid hostname: " + originalUri);
|
||||
final LoadBalancedRetryPolicy retryPolicy = loadBalancedRetryFactory.createRetryPolicy(serviceId,
|
||||
loadBalancerClient);
|
||||
final LoadBalancedRetryPolicy retryPolicy = loadBalancedRetryFactory
|
||||
.createRetryPolicy(serviceId, loadBalancerClient);
|
||||
RetryTemplate retryTemplate = buildRetryTemplate(serviceId, request, retryPolicy);
|
||||
return retryTemplate.execute(context -> {
|
||||
Request feignRequest = null;
|
||||
ServiceInstance retrievedServiceInstance = null;
|
||||
Set<LoadBalancerLifecycle> supportedLifecycleProcessors = LoadBalancerLifecycleValidator
|
||||
.getSupportedLifecycleProcessors(
|
||||
loadBalancerClientFactory.getInstances(serviceId, LoadBalancerLifecycle.class),
|
||||
RetryableRequestContext.class, ResponseData.class, ServiceInstance.class);
|
||||
String hint = getHint(serviceId);
|
||||
DefaultRequest<RetryableRequestContext> lbRequest = new DefaultRequest<>(
|
||||
new RetryableRequestContext(null, buildRequestData(request), hint));
|
||||
// On retries the policy will choose the server and set it in the context
|
||||
// and extract the server and update the request being made
|
||||
if (context instanceof LoadBalancedRetryContext) {
|
||||
LoadBalancedRetryContext lbContext = (LoadBalancedRetryContext) context;
|
||||
ServiceInstance serviceInstance = lbContext.getServiceInstance();
|
||||
if (serviceInstance == null) {
|
||||
ServiceInstance serviceInstance = ((LoadBalancedRetryContext) context)
|
||||
.getServiceInstance();
|
||||
if (serviceInstance != null) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug("Service instance retrieved from LoadBalancedRetryContext: was null. "
|
||||
+ "Reattempting service instance selection");
|
||||
LOG.debug(String.format(
|
||||
"Using service instance from LoadBalancedRetryContext: %s",
|
||||
serviceInstance));
|
||||
}
|
||||
ServiceInstance previousServiceInstance = lbContext.getPreviousServiceInstance();
|
||||
lbRequest.getContext().setPreviousServiceInstance(previousServiceInstance);
|
||||
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
|
||||
retrievedServiceInstance = loadBalancerClient.choose(serviceId, lbRequest);
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug(String.format("Selected service instance: %s", retrievedServiceInstance));
|
||||
}
|
||||
lbContext.setServiceInstance(retrievedServiceInstance);
|
||||
}
|
||||
|
||||
if (retrievedServiceInstance == null) {
|
||||
if (LOG.isWarnEnabled()) {
|
||||
LOG.warn("Service instance was not resolved, executing the original request");
|
||||
}
|
||||
org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse = new DefaultResponse(
|
||||
retrievedServiceInstance);
|
||||
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
|
||||
.onComplete(new CompletionContext<ResponseData, ServiceInstance, RetryableRequestContext>(
|
||||
CompletionContext.Status.DISCARD, lbRequest, lbResponse)));
|
||||
feignRequest = request;
|
||||
}
|
||||
else {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug(String.format("Using service instance from LoadBalancedRetryContext: %s",
|
||||
retrievedServiceInstance));
|
||||
}
|
||||
String reconstructedUrl = loadBalancerClient.reconstructURI(retrievedServiceInstance, originalUri)
|
||||
.toString();
|
||||
String reconstructedUrl = loadBalancerClient
|
||||
.reconstructURI(serviceInstance, originalUri).toString();
|
||||
feignRequest = buildRequest(request, reconstructedUrl);
|
||||
}
|
||||
}
|
||||
org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse = new DefaultResponse(
|
||||
retrievedServiceInstance);
|
||||
Response response = LoadBalancerUtils.executeWithLoadBalancerLifecycleProcessing(delegate, options,
|
||||
feignRequest, lbRequest, lbResponse, supportedLifecycleProcessors,
|
||||
retrievedServiceInstance != null);
|
||||
if (feignRequest == null) {
|
||||
if (LOG.isWarnEnabled()) {
|
||||
LOG.warn(
|
||||
"Service instance was not resolved, executing the original request");
|
||||
}
|
||||
feignRequest = request;
|
||||
}
|
||||
Response response = delegate.execute(feignRequest, options);
|
||||
int responseStatus = response.status();
|
||||
if (retryPolicy != null && retryPolicy.retryableStatusCode(responseStatus)) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug(String.format("Retrying on status code: %d", responseStatus));
|
||||
LOG.debug(
|
||||
String.format("Retrying on status code: %d", responseStatus));
|
||||
}
|
||||
response.close();
|
||||
throw new RetryableStatusCodeException(serviceId, responseStatus, response, URI.create(request.url()));
|
||||
throw new RetryableStatusCodeException(serviceId, responseStatus,
|
||||
response, URI.create(request.url()));
|
||||
}
|
||||
return response;
|
||||
}, new LoadBalancedRecoveryCallback<Response, Response>() {
|
||||
@@ -174,21 +126,26 @@ public class RetryableFeignBlockingLoadBalancerClient implements Client {
|
||||
}
|
||||
|
||||
protected Request buildRequest(Request request, String reconstructedUrl) {
|
||||
return Request.create(request.httpMethod(), reconstructedUrl, request.headers(), request.body(),
|
||||
request.charset(), request.requestTemplate());
|
||||
return Request.create(request.httpMethod(), reconstructedUrl, request.headers(),
|
||||
request.body(), request.charset(), request.requestTemplate());
|
||||
}
|
||||
|
||||
private RetryTemplate buildRetryTemplate(String serviceId, Request request, LoadBalancedRetryPolicy retryPolicy) {
|
||||
private RetryTemplate buildRetryTemplate(String serviceId, Request request,
|
||||
LoadBalancedRetryPolicy retryPolicy) {
|
||||
RetryTemplate retryTemplate = new RetryTemplate();
|
||||
BackOffPolicy backOffPolicy = this.loadBalancedRetryFactory.createBackOffPolicy(serviceId);
|
||||
retryTemplate.setBackOffPolicy(backOffPolicy == null ? new NoBackOffPolicy() : backOffPolicy);
|
||||
RetryListener[] retryListeners = this.loadBalancedRetryFactory.createRetryListeners(serviceId);
|
||||
BackOffPolicy backOffPolicy = this.loadBalancedRetryFactory
|
||||
.createBackOffPolicy(serviceId);
|
||||
retryTemplate.setBackOffPolicy(
|
||||
backOffPolicy == null ? new NoBackOffPolicy() : backOffPolicy);
|
||||
RetryListener[] retryListeners = this.loadBalancedRetryFactory
|
||||
.createRetryListeners(serviceId);
|
||||
if (retryListeners != null && retryListeners.length != 0) {
|
||||
retryTemplate.setListeners(retryListeners);
|
||||
}
|
||||
|
||||
retryTemplate.setRetryPolicy(retryPolicy == null ? new NeverRetryPolicy()
|
||||
: new InterceptorRetryPolicy(toHttpRequest(request), retryPolicy, loadBalancerClient, serviceId));
|
||||
: new InterceptorRetryPolicy(toHttpRequest(request), retryPolicy,
|
||||
loadBalancerClient, serviceId));
|
||||
return retryTemplate;
|
||||
}
|
||||
|
||||
@@ -228,10 +185,4 @@ public class RetryableFeignBlockingLoadBalancerClient implements Client {
|
||||
};
|
||||
}
|
||||
|
||||
private String getHint(String serviceId) {
|
||||
String defaultHint = properties.getHint().getOrDefault("default", "default");
|
||||
String hintPropertyValue = properties.getHint().get(serviceId);
|
||||
return hintPropertyValue != null ? hintPropertyValue : defaultHint;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+72
@@ -0,0 +1,72 @@
|
||||
/*
|
||||
* Copyright 2013-2020 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.ribbon;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import com.netflix.client.config.IClientConfig;
|
||||
import com.netflix.loadbalancer.ILoadBalancer;
|
||||
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
|
||||
import org.springframework.cloud.netflix.ribbon.ServerIntrospector;
|
||||
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
|
||||
import org.springframework.util.ConcurrentReferenceHashMap;
|
||||
|
||||
/**
|
||||
* Factory for SpringLoadBalancer instances that caches the entries created.
|
||||
*
|
||||
* @author Spencer Gibb
|
||||
* @author Dave Syer
|
||||
* @author Ryan Baxter
|
||||
* @author Gang Li
|
||||
*/
|
||||
public class CachingSpringLoadBalancerFactory {
|
||||
|
||||
protected final SpringClientFactory factory;
|
||||
|
||||
protected LoadBalancedRetryFactory loadBalancedRetryFactory = null;
|
||||
|
||||
private volatile Map<String, FeignLoadBalancer> cache = new ConcurrentReferenceHashMap<>();
|
||||
|
||||
public CachingSpringLoadBalancerFactory(SpringClientFactory factory) {
|
||||
this.factory = factory;
|
||||
}
|
||||
|
||||
public CachingSpringLoadBalancerFactory(SpringClientFactory factory,
|
||||
LoadBalancedRetryFactory loadBalancedRetryPolicyFactory) {
|
||||
this.factory = factory;
|
||||
this.loadBalancedRetryFactory = loadBalancedRetryPolicyFactory;
|
||||
}
|
||||
|
||||
public FeignLoadBalancer create(String clientName) {
|
||||
FeignLoadBalancer client = this.cache.get(clientName);
|
||||
if (client != null) {
|
||||
return client;
|
||||
}
|
||||
IClientConfig config = this.factory.getClientConfig(clientName);
|
||||
ILoadBalancer lb = this.factory.getLoadBalancer(clientName);
|
||||
ServerIntrospector serverIntrospector = this.factory.getInstance(clientName,
|
||||
ServerIntrospector.class);
|
||||
client = this.loadBalancedRetryFactory != null
|
||||
? new RetryableFeignLoadBalancer(lb, config, serverIntrospector,
|
||||
this.loadBalancedRetryFactory)
|
||||
: new FeignLoadBalancer(lb, config, serverIntrospector);
|
||||
this.cache.put(clientName, client);
|
||||
return client;
|
||||
}
|
||||
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright 2013-2020 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.ribbon;
|
||||
|
||||
import feign.Client;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
class DefaultFeignLoadBalancedConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
|
||||
SpringClientFactory clientFactory) {
|
||||
return new LoadBalancerFeignClient(new Client.Default(null, null), cachingFactory,
|
||||
clientFactory);
|
||||
}
|
||||
|
||||
}
|
||||
+251
@@ -0,0 +1,251 @@
|
||||
/*
|
||||
* Copyright 2013-2021 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.ribbon;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.netflix.client.AbstractLoadBalancerAwareClient;
|
||||
import com.netflix.client.ClientRequest;
|
||||
import com.netflix.client.IResponse;
|
||||
import com.netflix.client.RequestSpecificRetryHandler;
|
||||
import com.netflix.client.RetryHandler;
|
||||
import com.netflix.client.config.IClientConfig;
|
||||
import com.netflix.loadbalancer.ILoadBalancer;
|
||||
import com.netflix.loadbalancer.Server;
|
||||
import feign.Client;
|
||||
import feign.Request;
|
||||
import feign.Response;
|
||||
|
||||
import org.springframework.cloud.netflix.ribbon.RibbonProperties;
|
||||
import org.springframework.cloud.netflix.ribbon.ServerIntrospector;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpRequest;
|
||||
|
||||
import static org.springframework.cloud.netflix.ribbon.RibbonUtils.updateToSecureConnectionIfNeeded;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
* @author Spencer Gibb
|
||||
* @author Ryan Baxter
|
||||
* @author Tim Ysewyn
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
public class FeignLoadBalancer extends
|
||||
AbstractLoadBalancerAwareClient<FeignLoadBalancer.RibbonRequest, FeignLoadBalancer.RibbonResponse> {
|
||||
|
||||
private final RibbonProperties ribbon;
|
||||
|
||||
protected int connectTimeout;
|
||||
|
||||
protected int readTimeout;
|
||||
|
||||
protected IClientConfig clientConfig;
|
||||
|
||||
protected ServerIntrospector serverIntrospector;
|
||||
|
||||
protected boolean followRedirects;
|
||||
|
||||
public FeignLoadBalancer(ILoadBalancer lb, IClientConfig clientConfig,
|
||||
ServerIntrospector serverIntrospector) {
|
||||
super(lb, clientConfig);
|
||||
setRetryHandler(RetryHandler.DEFAULT);
|
||||
this.clientConfig = clientConfig;
|
||||
this.ribbon = RibbonProperties.from(clientConfig);
|
||||
RibbonProperties ribbon = this.ribbon;
|
||||
connectTimeout = ribbon.getConnectTimeout();
|
||||
readTimeout = ribbon.getReadTimeout();
|
||||
this.serverIntrospector = serverIntrospector;
|
||||
followRedirects = ribbon.isFollowRedirects();
|
||||
}
|
||||
|
||||
@Override
|
||||
public RibbonResponse execute(RibbonRequest request, IClientConfig configOverride)
|
||||
throws IOException {
|
||||
Request.Options options;
|
||||
if (configOverride != null) {
|
||||
RibbonProperties override = RibbonProperties.from(configOverride);
|
||||
options = new Request.Options(override.connectTimeout(connectTimeout),
|
||||
TimeUnit.MILLISECONDS, override.readTimeout(readTimeout),
|
||||
TimeUnit.MILLISECONDS, override.isFollowRedirects(followRedirects));
|
||||
}
|
||||
else {
|
||||
options = new Request.Options(connectTimeout, TimeUnit.MILLISECONDS,
|
||||
readTimeout, TimeUnit.MILLISECONDS, followRedirects);
|
||||
}
|
||||
Response response = request.client().execute(request.toRequest(), options);
|
||||
return new RibbonResponse(request.getUri(), response);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestSpecificRetryHandler getRequestSpecificRetryHandler(
|
||||
RibbonRequest request, IClientConfig requestConfig) {
|
||||
if (ribbon.isOkToRetryOnAllOperations()) {
|
||||
return new RequestSpecificRetryHandler(true, true, getRetryHandler(),
|
||||
requestConfig);
|
||||
}
|
||||
if (!request.toRequest().httpMethod().name().equals("GET")) {
|
||||
return new RequestSpecificRetryHandler(true, false, getRetryHandler(),
|
||||
requestConfig);
|
||||
}
|
||||
else {
|
||||
return new RequestSpecificRetryHandler(true, true, getRetryHandler(),
|
||||
requestConfig);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI reconstructURIWithServer(Server server, URI original) {
|
||||
URI uri = updateToSecureConnectionIfNeeded(original, clientConfig,
|
||||
serverIntrospector, server);
|
||||
return super.reconstructURIWithServer(server, uri);
|
||||
}
|
||||
|
||||
protected static class RibbonRequest extends ClientRequest implements Cloneable {
|
||||
|
||||
private final Request request;
|
||||
|
||||
private final Client client;
|
||||
|
||||
protected RibbonRequest(Client client, Request request, URI uri) {
|
||||
this.client = client;
|
||||
setUri(uri);
|
||||
this.request = toRequest(request);
|
||||
}
|
||||
|
||||
private Request toRequest(Request request) {
|
||||
Map<String, Collection<String>> headers = new LinkedHashMap<>(
|
||||
request.headers());
|
||||
return Request.create(request.httpMethod(), getUri().toASCIIString(), headers,
|
||||
request.body(), request.charset(), request.requestTemplate());
|
||||
}
|
||||
|
||||
Request toRequest() {
|
||||
return toRequest(request);
|
||||
}
|
||||
|
||||
Client client() {
|
||||
return client;
|
||||
}
|
||||
|
||||
HttpRequest toHttpRequest() {
|
||||
return new HttpRequest() {
|
||||
@Override
|
||||
public HttpMethod getMethod() {
|
||||
return HttpMethod
|
||||
.resolve(RibbonRequest.this.toRequest().httpMethod().name());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getMethodValue() {
|
||||
return getMethod().name();
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getURI() {
|
||||
return RibbonRequest.this.getUri();
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders getHeaders() {
|
||||
Map<String, List<String>> headers = new HashMap<>();
|
||||
Map<String, Collection<String>> feignHeaders = RibbonRequest.this
|
||||
.toRequest().headers();
|
||||
for (String key : feignHeaders.keySet()) {
|
||||
headers.put(key, new ArrayList<>(feignHeaders.get(key)));
|
||||
}
|
||||
HttpHeaders httpHeaders = new HttpHeaders();
|
||||
httpHeaders.putAll(headers);
|
||||
return httpHeaders;
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
public Request getRequest() {
|
||||
return request;
|
||||
}
|
||||
|
||||
public Client getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object clone() {
|
||||
return new RibbonRequest(client, request, getUri());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected static class RibbonResponse implements IResponse {
|
||||
|
||||
private final URI uri;
|
||||
|
||||
private final Response response;
|
||||
|
||||
protected RibbonResponse(URI uri, Response response) {
|
||||
this.uri = uri;
|
||||
this.response = response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object getPayload() {
|
||||
return response.body();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean hasPayload() {
|
||||
return response.body() != null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSuccess() {
|
||||
return response.status() == 200;
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getRequestedURI() {
|
||||
return uri;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Collection<String>> getHeaders() {
|
||||
return response.headers();
|
||||
}
|
||||
|
||||
Response toResponse() {
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void close() throws IOException {
|
||||
if (response != null && response.body() != null) {
|
||||
response.body().close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+129
@@ -0,0 +1,129 @@
|
||||
/*
|
||||
* Copyright 2013-2020 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.ribbon;
|
||||
|
||||
import java.net.URI;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.loadbalancer.InterceptorRetryPolicy;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryContext;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicy;
|
||||
import org.springframework.cloud.client.loadbalancer.ServiceInstanceChooser;
|
||||
import org.springframework.http.HttpRequest;
|
||||
import org.springframework.retry.RetryContext;
|
||||
|
||||
/**
|
||||
* @author Ryan Baxter
|
||||
*/
|
||||
public class FeignRetryPolicy extends InterceptorRetryPolicy {
|
||||
|
||||
private HttpRequest request;
|
||||
|
||||
private String serviceId;
|
||||
|
||||
public FeignRetryPolicy(HttpRequest request, LoadBalancedRetryPolicy policy,
|
||||
ServiceInstanceChooser serviceInstanceChooser, String serviceName) {
|
||||
super(request, policy, serviceInstanceChooser, serviceName);
|
||||
this.request = request;
|
||||
this.serviceId = serviceName;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean canRetry(RetryContext context) {
|
||||
/*
|
||||
* In InterceptorRetryPolicy.canRetry we ask the LoadBalancer to choose a server
|
||||
* if one is not set in the retry context and then return true. RetryTemplat calls
|
||||
* the canRetry method of the policy even on its first execution. So the fact that
|
||||
* we didnt have a service instance set in the RetryContext signaled that it was
|
||||
* the first execution and we should return true.
|
||||
*
|
||||
* In the Feign scenario, Feign as actually already queried the load balancer for
|
||||
* a service instance and we set that service instance in the context when we call
|
||||
* the open method of the policy. So in the Feign case we just return true if the
|
||||
* retry count is 0 indicating we haven't yet made a failed request.
|
||||
*/
|
||||
if (context.getRetryCount() == 0) {
|
||||
return true;
|
||||
}
|
||||
return super.canRetry(context);
|
||||
}
|
||||
|
||||
@Override
|
||||
public RetryContext open(RetryContext parent) {
|
||||
/*
|
||||
* With Feign (unlike Ribbon) the request already has the URI for the service
|
||||
* instance we are going to make the request to, so extract that information and
|
||||
* set the service instance in the context. In the Ribbon scenario the URI in the
|
||||
* request object still has the service id so we choose and set the service
|
||||
* instance later on.
|
||||
*/
|
||||
LoadBalancedRetryContext context = new LoadBalancedRetryContext(parent,
|
||||
this.request);
|
||||
context.setServiceInstance(
|
||||
new FeignRetryPolicyServiceInstance(this.serviceId, this.request));
|
||||
return context;
|
||||
}
|
||||
|
||||
class FeignRetryPolicyServiceInstance implements ServiceInstance {
|
||||
|
||||
private String serviceId;
|
||||
|
||||
private HttpRequest request;
|
||||
|
||||
private Map<String, String> metadata;
|
||||
|
||||
FeignRetryPolicyServiceInstance(String serviceId, HttpRequest request) {
|
||||
this.serviceId = serviceId;
|
||||
this.request = request;
|
||||
this.metadata = new HashMap<>();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getServiceId() {
|
||||
return this.serviceId;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getHost() {
|
||||
return this.request.getURI().getHost();
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getPort() {
|
||||
return this.request.getURI().getPort();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isSecure() {
|
||||
return "https".equals(this.request.getURI().getScheme());
|
||||
}
|
||||
|
||||
@Override
|
||||
public URI getUri() {
|
||||
return this.request.getURI();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, String> getMetadata() {
|
||||
return this.metadata;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+85
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright 2013-2021 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.ribbon;
|
||||
|
||||
import com.netflix.loadbalancer.ILoadBalancer;
|
||||
import feign.Feign;
|
||||
import feign.Request;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
|
||||
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
|
||||
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
|
||||
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
|
||||
/**
|
||||
* Autoconfiguration to be activated if Feign is in use and needs to be use Ribbon as a
|
||||
* load balancer.
|
||||
*
|
||||
* @author Dave Syer
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Nguyen Ky Thanh
|
||||
*/
|
||||
@ConditionalOnClass({ ILoadBalancer.class, Feign.class })
|
||||
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.ribbon.enabled",
|
||||
matchIfMissing = true)
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@AutoConfigureBefore(FeignAutoConfiguration.class)
|
||||
@EnableConfigurationProperties({ FeignHttpClientProperties.class })
|
||||
// Order is important here, last should be the default, first should be optional
|
||||
// see
|
||||
// https://github.com/spring-cloud/spring-cloud-netflix/issues/2086#issuecomment-316281653
|
||||
@Import({ HttpClientFeignLoadBalancedConfiguration.class,
|
||||
OkHttpFeignLoadBalancedConfiguration.class,
|
||||
HttpClient5FeignLoadBalancedConfiguration.class,
|
||||
DefaultFeignLoadBalancedConfiguration.class })
|
||||
public class FeignRibbonClientAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
|
||||
public CachingSpringLoadBalancerFactory cachingLBClientFactory(
|
||||
SpringClientFactory factory) {
|
||||
return new CachingSpringLoadBalancerFactory(factory);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Primary
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
|
||||
public CachingSpringLoadBalancerFactory retryabeCachingLBClientFactory(
|
||||
SpringClientFactory factory, LoadBalancedRetryFactory retryFactory) {
|
||||
return new CachingSpringLoadBalancerFactory(factory, retryFactory);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public Request.Options feignRequestOptions() {
|
||||
return LoadBalancerFeignClient.DEFAULT_OPTIONS;
|
||||
}
|
||||
|
||||
}
|
||||
+52
@@ -0,0 +1,52 @@
|
||||
/*
|
||||
* Copyright 2013-2021 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.ribbon;
|
||||
|
||||
import feign.Client;
|
||||
import feign.hc5.ApacheHttp5Client;
|
||||
import org.apache.hc.client5.http.classic.HttpClient;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
|
||||
import org.springframework.cloud.openfeign.clientconfig.HttpClient5FeignConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
/**
|
||||
* Configuration instantiating a {@link LoadBalancerFeignClient}-based {@link Client}
|
||||
* object that uses {@link ApacheHttp5Client} under the hood.
|
||||
*
|
||||
* @author Nguyen Ky Thanh
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(ApacheHttp5Client.class)
|
||||
@ConditionalOnProperty(value = "feign.httpclient.hc5.enabled", havingValue = "true")
|
||||
@Import(HttpClient5FeignConfiguration.class)
|
||||
class HttpClient5FeignLoadBalancedConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(Client.class)
|
||||
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
|
||||
SpringClientFactory clientFactory, HttpClient httpClient5) {
|
||||
Client delegate = new ApacheHttp5Client(httpClient5);
|
||||
return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
|
||||
}
|
||||
|
||||
}
|
||||
+54
@@ -0,0 +1,54 @@
|
||||
/*
|
||||
* Copyright 2013-2021 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.ribbon;
|
||||
|
||||
import feign.Client;
|
||||
import feign.httpclient.ApacheHttpClient;
|
||||
import org.apache.http.client.HttpClient;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
|
||||
import org.springframework.cloud.openfeign.HttpClient5DisabledConditions;
|
||||
import org.springframework.cloud.openfeign.clientconfig.HttpClientFeignConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Nguyen Ky Thanh
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(ApacheHttpClient.class)
|
||||
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
|
||||
@Conditional(HttpClient5DisabledConditions.class)
|
||||
@Import(HttpClientFeignConfiguration.class)
|
||||
class HttpClientFeignLoadBalancedConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(Client.class)
|
||||
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
|
||||
SpringClientFactory clientFactory, HttpClient httpClient) {
|
||||
ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
|
||||
return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
|
||||
}
|
||||
|
||||
}
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright 2013-2021 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.ribbon;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
|
||||
import com.netflix.client.ClientException;
|
||||
import com.netflix.client.config.CommonClientConfigKey;
|
||||
import com.netflix.client.config.DefaultClientConfigImpl;
|
||||
import com.netflix.client.config.IClientConfig;
|
||||
import feign.Client;
|
||||
import feign.Request;
|
||||
import feign.Response;
|
||||
|
||||
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
|
||||
|
||||
/**
|
||||
* @author Dave Syer
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*
|
||||
*/
|
||||
public class LoadBalancerFeignClient implements Client {
|
||||
|
||||
static final Request.Options DEFAULT_OPTIONS = new Request.Options();
|
||||
|
||||
private final Client delegate;
|
||||
|
||||
private CachingSpringLoadBalancerFactory lbClientFactory;
|
||||
|
||||
private SpringClientFactory clientFactory;
|
||||
|
||||
public LoadBalancerFeignClient(Client delegate,
|
||||
CachingSpringLoadBalancerFactory lbClientFactory,
|
||||
SpringClientFactory clientFactory) {
|
||||
this.delegate = delegate;
|
||||
this.lbClientFactory = lbClientFactory;
|
||||
this.clientFactory = clientFactory;
|
||||
}
|
||||
|
||||
static URI cleanUrl(String originalUrl, String host) {
|
||||
String newUrl = originalUrl;
|
||||
if (originalUrl.startsWith("https://")) {
|
||||
newUrl = originalUrl.substring(0, 8)
|
||||
+ originalUrl.substring(8 + host.length());
|
||||
}
|
||||
else if (originalUrl.startsWith("http")) {
|
||||
newUrl = originalUrl.substring(0, 7)
|
||||
+ originalUrl.substring(7 + host.length());
|
||||
}
|
||||
StringBuffer buffer = new StringBuffer(newUrl);
|
||||
if ((newUrl.startsWith("https://") && newUrl.length() == 8)
|
||||
|| (newUrl.startsWith("http://") && newUrl.length() == 7)) {
|
||||
buffer.append("/");
|
||||
}
|
||||
return URI.create(buffer.toString());
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response execute(Request request, Request.Options options) throws IOException {
|
||||
try {
|
||||
URI asUri = URI.create(request.url());
|
||||
String clientName = asUri.getHost();
|
||||
URI uriWithoutHost = cleanUrl(request.url(), clientName);
|
||||
FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
|
||||
this.delegate, request, uriWithoutHost);
|
||||
|
||||
IClientConfig requestConfig = getClientConfig(options, clientName);
|
||||
return lbClient(clientName)
|
||||
.executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
|
||||
}
|
||||
catch (ClientException e) {
|
||||
IOException io = findIOException(e);
|
||||
if (io != null) {
|
||||
throw io;
|
||||
}
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
IClientConfig getClientConfig(Request.Options options, String clientName) {
|
||||
IClientConfig requestConfig;
|
||||
if (options == DEFAULT_OPTIONS) {
|
||||
requestConfig = this.clientFactory.getClientConfig(clientName);
|
||||
}
|
||||
else {
|
||||
requestConfig = new FeignOptionsClientConfig(options);
|
||||
}
|
||||
return requestConfig;
|
||||
}
|
||||
|
||||
protected IOException findIOException(Throwable t) {
|
||||
if (t == null) {
|
||||
return null;
|
||||
}
|
||||
if (t instanceof IOException) {
|
||||
return (IOException) t;
|
||||
}
|
||||
return findIOException(t.getCause());
|
||||
}
|
||||
|
||||
public Client getDelegate() {
|
||||
return this.delegate;
|
||||
}
|
||||
|
||||
private FeignLoadBalancer lbClient(String clientName) {
|
||||
return this.lbClientFactory.create(clientName);
|
||||
}
|
||||
|
||||
static class FeignOptionsClientConfig extends DefaultClientConfigImpl {
|
||||
|
||||
FeignOptionsClientConfig(Request.Options options) {
|
||||
setProperty(CommonClientConfigKey.ConnectTimeout,
|
||||
options.connectTimeoutMillis());
|
||||
setProperty(CommonClientConfigKey.ReadTimeout, options.readTimeoutMillis());
|
||||
setProperty(CommonClientConfigKey.FollowRedirects,
|
||||
options.isFollowRedirects());
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadProperties(String clientName) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void loadDefaultValues() {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2013-2020 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.ribbon;
|
||||
|
||||
import feign.Client;
|
||||
import feign.okhttp.OkHttpClient;
|
||||
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
|
||||
import org.springframework.cloud.openfeign.clientconfig.OkHttpFeignConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(OkHttpClient.class)
|
||||
@ConditionalOnProperty("feign.okhttp.enabled")
|
||||
@Import(OkHttpFeignConfiguration.class)
|
||||
class OkHttpFeignLoadBalancedConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(Client.class)
|
||||
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
|
||||
SpringClientFactory clientFactory, okhttp3.OkHttpClient okHttpClient) {
|
||||
OkHttpClient delegate = new OkHttpClient(okHttpClient);
|
||||
return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
|
||||
}
|
||||
|
||||
}
|
||||
+141
@@ -0,0 +1,141 @@
|
||||
/*
|
||||
* Copyright 2013-2020 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.ribbon;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.URI;
|
||||
|
||||
import com.netflix.client.DefaultLoadBalancerRetryHandler;
|
||||
import com.netflix.client.RequestSpecificRetryHandler;
|
||||
import com.netflix.client.config.IClientConfig;
|
||||
import com.netflix.loadbalancer.ILoadBalancer;
|
||||
import com.netflix.loadbalancer.Server;
|
||||
import feign.Request;
|
||||
import feign.Response;
|
||||
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancedRecoveryCallback;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryContext;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicy;
|
||||
import org.springframework.cloud.client.loadbalancer.ServiceInstanceChooser;
|
||||
import org.springframework.cloud.netflix.ribbon.RibbonLoadBalancerClient;
|
||||
import org.springframework.cloud.netflix.ribbon.RibbonProperties;
|
||||
import org.springframework.cloud.netflix.ribbon.ServerIntrospector;
|
||||
import org.springframework.retry.RetryListener;
|
||||
import org.springframework.retry.backoff.BackOffPolicy;
|
||||
import org.springframework.retry.backoff.NoBackOffPolicy;
|
||||
import org.springframework.retry.policy.NeverRetryPolicy;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
/**
|
||||
* A {@link FeignLoadBalancer} that leverages Spring Retry to retry failed requests.
|
||||
*
|
||||
* @author Ryan Baxter
|
||||
* @author Gang Li
|
||||
*/
|
||||
public class RetryableFeignLoadBalancer extends FeignLoadBalancer
|
||||
implements ServiceInstanceChooser {
|
||||
|
||||
private final LoadBalancedRetryFactory loadBalancedRetryFactory;
|
||||
|
||||
public RetryableFeignLoadBalancer(ILoadBalancer lb, IClientConfig clientConfig,
|
||||
ServerIntrospector serverIntrospector,
|
||||
LoadBalancedRetryFactory loadBalancedRetryFactory) {
|
||||
super(lb, clientConfig, serverIntrospector);
|
||||
this.loadBalancedRetryFactory = loadBalancedRetryFactory;
|
||||
this.setRetryHandler(new DefaultLoadBalancerRetryHandler(clientConfig));
|
||||
}
|
||||
|
||||
@Override
|
||||
public RibbonResponse execute(final RibbonRequest request,
|
||||
IClientConfig configOverride) throws IOException {
|
||||
final Request.Options options;
|
||||
if (configOverride != null) {
|
||||
RibbonProperties ribbon = RibbonProperties.from(configOverride);
|
||||
options = new Request.Options(ribbon.connectTimeout(this.connectTimeout),
|
||||
ribbon.readTimeout(this.readTimeout));
|
||||
}
|
||||
else {
|
||||
options = new Request.Options(this.connectTimeout, this.readTimeout);
|
||||
}
|
||||
final LoadBalancedRetryPolicy retryPolicy = this.loadBalancedRetryFactory
|
||||
.createRetryPolicy(this.getClientName(), this);
|
||||
RetryTemplate retryTemplate = new RetryTemplate();
|
||||
BackOffPolicy backOffPolicy = this.loadBalancedRetryFactory
|
||||
.createBackOffPolicy(this.getClientName());
|
||||
retryTemplate.setBackOffPolicy(
|
||||
backOffPolicy == null ? new NoBackOffPolicy() : backOffPolicy);
|
||||
RetryListener[] retryListeners = this.loadBalancedRetryFactory
|
||||
.createRetryListeners(this.getClientName());
|
||||
if (retryListeners != null && retryListeners.length != 0) {
|
||||
retryTemplate.setListeners(retryListeners);
|
||||
}
|
||||
retryTemplate.setRetryPolicy(retryPolicy == null ? new NeverRetryPolicy()
|
||||
: new FeignRetryPolicy(request.toHttpRequest(), retryPolicy, this,
|
||||
this.getClientName()));
|
||||
return retryTemplate.execute(retryContext -> {
|
||||
Request feignRequest = null;
|
||||
// on retries the policy will choose the server and set it in the context
|
||||
// extract the server and update the request being made
|
||||
if (retryContext instanceof LoadBalancedRetryContext) {
|
||||
ServiceInstance service = ((LoadBalancedRetryContext) retryContext)
|
||||
.getServiceInstance();
|
||||
if (service != null) {
|
||||
feignRequest = ((RibbonRequest) request
|
||||
.replaceUri(reconstructURIWithServer(
|
||||
new Server(service.getHost(), service.getPort()),
|
||||
request.getUri()))).toRequest();
|
||||
}
|
||||
}
|
||||
if (feignRequest == null) {
|
||||
feignRequest = request.toRequest();
|
||||
}
|
||||
Response response = request.client().execute(feignRequest, options);
|
||||
if (retryPolicy != null
|
||||
&& retryPolicy.retryableStatusCode(response.status())) {
|
||||
byte[] byteArray = response.body() == null ? new byte[] {}
|
||||
: StreamUtils.copyToByteArray(response.body().asInputStream());
|
||||
response.close();
|
||||
throw new RibbonResponseStatusCodeException(
|
||||
RetryableFeignLoadBalancer.this.clientName, response, byteArray,
|
||||
request.getUri());
|
||||
}
|
||||
return new RibbonResponse(request.getUri(), response);
|
||||
}, new LoadBalancedRecoveryCallback<RibbonResponse, Response>() {
|
||||
@Override
|
||||
protected RibbonResponse createResponse(Response response, URI uri) {
|
||||
return new RibbonResponse(uri, response);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Override
|
||||
public RequestSpecificRetryHandler getRequestSpecificRetryHandler(
|
||||
FeignLoadBalancer.RibbonRequest request, IClientConfig requestConfig) {
|
||||
return new RequestSpecificRetryHandler(false, false, this.getRetryHandler(),
|
||||
requestConfig);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ServiceInstance choose(String serviceId) {
|
||||
return new RibbonLoadBalancerClient.RibbonServer(serviceId,
|
||||
this.getLoadBalancer().chooseServer(serviceId));
|
||||
}
|
||||
|
||||
}
|
||||
+49
@@ -0,0 +1,49 @@
|
||||
/*
|
||||
* Copyright 2013-2020 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.ribbon;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.net.URI;
|
||||
|
||||
import feign.Response;
|
||||
|
||||
import org.springframework.cloud.client.loadbalancer.RetryableStatusCodeException;
|
||||
|
||||
/**
|
||||
* A {@link RetryableStatusCodeException} for {@link Response}s.
|
||||
*
|
||||
* @author Ryan Baxter
|
||||
*/
|
||||
public class RibbonResponseStatusCodeException extends RetryableStatusCodeException {
|
||||
|
||||
private final Response response;
|
||||
|
||||
public RibbonResponseStatusCodeException(String serviceId, Response response,
|
||||
byte[] body, URI uri) {
|
||||
super(serviceId, response.status(), response, uri);
|
||||
this.response = Response.builder()
|
||||
.body(new ByteArrayInputStream(body), body.length)
|
||||
.headers(response.headers()).reason(response.reason())
|
||||
.status(response.status()).request(response.request()).build();
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response getResponse() {
|
||||
return this.response;
|
||||
}
|
||||
|
||||
}
|
||||
-182
@@ -1,182 +0,0 @@
|
||||
/*
|
||||
* Copyright 2015-2019 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.security;
|
||||
|
||||
import java.util.Arrays;
|
||||
|
||||
import feign.RequestInterceptor;
|
||||
import feign.RequestTemplate;
|
||||
|
||||
import org.springframework.security.oauth2.client.OAuth2ClientContext;
|
||||
import org.springframework.security.oauth2.client.http.AccessTokenRequiredException;
|
||||
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
|
||||
import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException;
|
||||
import org.springframework.security.oauth2.client.token.AccessTokenProvider;
|
||||
import org.springframework.security.oauth2.client.token.AccessTokenProviderChain;
|
||||
import org.springframework.security.oauth2.client.token.AccessTokenRequest;
|
||||
import org.springframework.security.oauth2.client.token.grant.client.ClientCredentialsAccessTokenProvider;
|
||||
import org.springframework.security.oauth2.client.token.grant.code.AuthorizationCodeAccessTokenProvider;
|
||||
import org.springframework.security.oauth2.client.token.grant.implicit.ImplicitAccessTokenProvider;
|
||||
import org.springframework.security.oauth2.client.token.grant.password.ResourceOwnerPasswordAccessTokenProvider;
|
||||
import org.springframework.security.oauth2.common.OAuth2AccessToken;
|
||||
|
||||
/**
|
||||
* Pre-defined custom RequestInterceptor for Feign Requests. It uses the
|
||||
* {@link OAuth2ClientContext OAuth2ClientContext} provided from the environment and
|
||||
* construct a new header on the request before it is made by Feign.
|
||||
*
|
||||
* @author Joao Pedro Evangelista
|
||||
* @author Tim Ysewyn
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public class OAuth2FeignRequestInterceptor implements RequestInterceptor {
|
||||
|
||||
/**
|
||||
* The name of the token.
|
||||
*/
|
||||
public static final String BEARER = "Bearer";
|
||||
|
||||
/**
|
||||
* The name of the header.
|
||||
*/
|
||||
public static final String AUTHORIZATION = "Authorization";
|
||||
|
||||
private final OAuth2ClientContext oAuth2ClientContext;
|
||||
|
||||
private final OAuth2ProtectedResourceDetails resource;
|
||||
|
||||
private final String tokenType;
|
||||
|
||||
private final String header;
|
||||
|
||||
private AccessTokenProvider accessTokenProvider = new AccessTokenProviderChain(Arrays.<AccessTokenProvider>asList(
|
||||
new AuthorizationCodeAccessTokenProvider(), new ImplicitAccessTokenProvider(),
|
||||
new ResourceOwnerPasswordAccessTokenProvider(), new ClientCredentialsAccessTokenProvider()));
|
||||
|
||||
/**
|
||||
* Default constructor which uses the provided OAuth2ClientContext and Bearer tokens
|
||||
* within Authorization header.
|
||||
* @param oAuth2ClientContext provided context
|
||||
* @param resource type of resource to be accessed
|
||||
*/
|
||||
public OAuth2FeignRequestInterceptor(OAuth2ClientContext oAuth2ClientContext,
|
||||
OAuth2ProtectedResourceDetails resource) {
|
||||
this(oAuth2ClientContext, resource, BEARER, AUTHORIZATION);
|
||||
}
|
||||
|
||||
/**
|
||||
* Fully customizable constructor for changing token type and header name, in cases of
|
||||
* Bearer and Authorization is not the default such as "bearer", "authorization".
|
||||
* @param oAuth2ClientContext current oAuth2 Context
|
||||
* @param resource type of resource to be accessed
|
||||
* @param tokenType type of token e.g. "token", "Bearer"
|
||||
* @param header name of the header e.g. "Authorization", "authorization"
|
||||
*/
|
||||
public OAuth2FeignRequestInterceptor(OAuth2ClientContext oAuth2ClientContext,
|
||||
OAuth2ProtectedResourceDetails resource, String tokenType, String header) {
|
||||
this.oAuth2ClientContext = oAuth2ClientContext;
|
||||
this.resource = resource;
|
||||
this.tokenType = tokenType;
|
||||
this.header = header;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a template with the header of provided name and extracted extract.
|
||||
*
|
||||
* @see RequestInterceptor#apply(RequestTemplate)
|
||||
*/
|
||||
@Override
|
||||
public void apply(RequestTemplate template) {
|
||||
template.header(header); // Clears out the header, no "clear" method available.
|
||||
template.header(header, extract(tokenType));
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the token extract id the access token exists or returning an empty extract
|
||||
* if there is no one on the context it may occasionally causes Unauthorized response
|
||||
* since the token extract is empty.
|
||||
* @param tokenType type name of token
|
||||
* @return token value from context if it exists otherwise empty String
|
||||
*/
|
||||
protected String extract(String tokenType) {
|
||||
OAuth2AccessToken accessToken = getToken();
|
||||
return String.format("%s %s", tokenType, accessToken.getValue());
|
||||
}
|
||||
|
||||
/**
|
||||
* Extract the access token within the request or try to acquire a new one by
|
||||
* delegating it to {@link #acquireAccessToken()}.
|
||||
* @return valid token
|
||||
*/
|
||||
public OAuth2AccessToken getToken() {
|
||||
|
||||
OAuth2AccessToken accessToken = oAuth2ClientContext.getAccessToken();
|
||||
if (accessToken == null || accessToken.isExpired()) {
|
||||
try {
|
||||
accessToken = acquireAccessToken();
|
||||
}
|
||||
catch (UserRedirectRequiredException e) {
|
||||
oAuth2ClientContext.setAccessToken(null);
|
||||
String stateKey = e.getStateKey();
|
||||
if (stateKey != null) {
|
||||
Object stateToPreserve = e.getStateToPreserve();
|
||||
if (stateToPreserve == null) {
|
||||
stateToPreserve = "NONE";
|
||||
}
|
||||
oAuth2ClientContext.setPreservedState(stateKey, stateToPreserve);
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
return accessToken;
|
||||
}
|
||||
|
||||
/**
|
||||
* Try to acquire the token using a access token provider.
|
||||
* @return valid access token
|
||||
* @throws UserRedirectRequiredException in case the user needs to be redirected to an
|
||||
* approval page or login page
|
||||
*/
|
||||
protected OAuth2AccessToken acquireAccessToken() throws UserRedirectRequiredException {
|
||||
AccessTokenRequest tokenRequest = oAuth2ClientContext.getAccessTokenRequest();
|
||||
if (tokenRequest == null) {
|
||||
throw new AccessTokenRequiredException(
|
||||
"Cannot find valid context on request for resource '" + resource.getId() + "'.", resource);
|
||||
}
|
||||
String stateKey = tokenRequest.getStateKey();
|
||||
if (stateKey != null) {
|
||||
tokenRequest.setPreservedState(oAuth2ClientContext.removePreservedState(stateKey));
|
||||
}
|
||||
OAuth2AccessToken existingToken = oAuth2ClientContext.getAccessToken();
|
||||
if (existingToken != null) {
|
||||
oAuth2ClientContext.setAccessToken(existingToken);
|
||||
}
|
||||
OAuth2AccessToken obtainableAccessToken;
|
||||
obtainableAccessToken = accessTokenProvider.obtainAccessToken(resource, tokenRequest);
|
||||
if (obtainableAccessToken == null || obtainableAccessToken.getValue() == null) {
|
||||
throw new IllegalStateException(
|
||||
" Access token provider returned a null token, which is illegal according to the contract.");
|
||||
}
|
||||
oAuth2ClientContext.setAccessToken(obtainableAccessToken);
|
||||
return obtainableAccessToken;
|
||||
}
|
||||
|
||||
public void setAccessTokenProvider(AccessTokenProvider accessTokenProvider) {
|
||||
this.accessTokenProvider = accessTokenProvider;
|
||||
}
|
||||
|
||||
}
|
||||
+7
-4
@@ -29,6 +29,7 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import static feign.form.ContentProcessor.CRLF;
|
||||
import static feign.form.util.PojoUtil.isUserPojo;
|
||||
|
||||
/**
|
||||
* @author Darren Foong
|
||||
@@ -44,10 +45,12 @@ public abstract class AbstractFormWriter extends AbstractWriter {
|
||||
@Override
|
||||
public void write(Output output, String key, Object object) throws EncodeException {
|
||||
try {
|
||||
String string = new StringBuilder().append("Content-Disposition: form-data; name=\"").append(key)
|
||||
.append('"').append(CRLF).append("Content-Type: ").append(getContentType()).append("; charset=")
|
||||
.append(output.getCharset().name()).append(CRLF).append(CRLF).append(writeAsString(object))
|
||||
.toString();
|
||||
String string = new StringBuilder()
|
||||
.append("Content-Disposition: form-data; name=\"").append(key)
|
||||
.append('"').append(CRLF).append("Content-Type: ")
|
||||
.append(getContentType()).append("; charset=")
|
||||
.append(output.getCharset().name()).append(CRLF).append(CRLF)
|
||||
.append(writeAsString(object)).toString();
|
||||
|
||||
output.write(string);
|
||||
}
|
||||
|
||||
+10
-5
@@ -45,15 +45,19 @@ public class DefaultGzipDecoder implements Decoder {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object decode(final Response response, Type type) throws IOException, FeignException {
|
||||
Collection<String> encoding = response.headers().containsKey(HttpEncoding.CONTENT_ENCODING_HEADER)
|
||||
? response.headers().get(HttpEncoding.CONTENT_ENCODING_HEADER) : null;
|
||||
public Object decode(final Response response, Type type)
|
||||
throws IOException, FeignException {
|
||||
Collection<String> encoding = response.headers()
|
||||
.containsKey(HttpEncoding.CONTENT_ENCODING_HEADER)
|
||||
? response.headers().get(HttpEncoding.CONTENT_ENCODING_HEADER)
|
||||
: null;
|
||||
|
||||
if (encoding != null) {
|
||||
if (encoding.contains(HttpEncoding.GZIP_ENCODING)) {
|
||||
String decompressedBody = decompress(response);
|
||||
if (decompressedBody != null) {
|
||||
Response decompressedResponse = response.toBuilder().body(decompressedBody.getBytes()).build();
|
||||
Response decompressedResponse = response.toBuilder()
|
||||
.body(decompressedBody.getBytes()).build();
|
||||
return decoder.decode(decompressedResponse, type);
|
||||
}
|
||||
}
|
||||
@@ -65,7 +69,8 @@ public class DefaultGzipDecoder implements Decoder {
|
||||
if (response.body() == null) {
|
||||
return null;
|
||||
}
|
||||
try (GZIPInputStream gzipInputStream = new GZIPInputStream(response.body().asInputStream());
|
||||
try (GZIPInputStream gzipInputStream = new GZIPInputStream(
|
||||
response.body().asInputStream());
|
||||
BufferedReader reader = new BufferedReader(
|
||||
new InputStreamReader(gzipInputStream, StandardCharsets.UTF_8))) {
|
||||
String outputString = "";
|
||||
|
||||
+4
-3
@@ -43,7 +43,8 @@ public class DefaultGzipDecoderConfiguration {
|
||||
|
||||
private ObjectFactory<HttpMessageConverters> messageConverters;
|
||||
|
||||
public DefaultGzipDecoderConfiguration(ObjectFactory<HttpMessageConverters> messageConverters) {
|
||||
public DefaultGzipDecoderConfiguration(
|
||||
ObjectFactory<HttpMessageConverters> messageConverters) {
|
||||
this.messageConverters = messageConverters;
|
||||
}
|
||||
|
||||
@@ -51,8 +52,8 @@ public class DefaultGzipDecoderConfiguration {
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnProperty("feign.compression.response.useGzipDecoder")
|
||||
public Decoder defaultGzipDecoder() {
|
||||
return new OptionalDecoder(
|
||||
new ResponseEntityDecoder(new DefaultGzipDecoder(new SpringDecoder(messageConverters))));
|
||||
return new OptionalDecoder(new ResponseEntityDecoder(
|
||||
new DefaultGzipDecoder(new SpringDecoder(messageConverters))));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2013-2020 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.support;
|
||||
|
||||
import com.netflix.hystrix.HystrixCommand;
|
||||
import com.netflix.hystrix.HystrixCommandGroupKey;
|
||||
import com.netflix.hystrix.HystrixThreadPoolKey;
|
||||
|
||||
/**
|
||||
* Convenience class for implementing feign fallbacks that return {@link HystrixCommand}.
|
||||
* Also useful for return types of {@link rx.Observable} and
|
||||
* {@link java.util.concurrent.Future}. For those return types, just call
|
||||
* {@link FallbackCommand#observe()} or {@link FallbackCommand#queue()} respectively.
|
||||
*
|
||||
* @param <T> result type
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
public class FallbackCommand<T> extends HystrixCommand<T> {
|
||||
|
||||
private T result;
|
||||
|
||||
public FallbackCommand(T result) {
|
||||
this(result, "fallback");
|
||||
}
|
||||
|
||||
protected FallbackCommand(T result, String groupname) {
|
||||
super(HystrixCommandGroupKey.Factory.asKey(groupname));
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
public FallbackCommand(T result, HystrixCommandGroupKey group) {
|
||||
super(group);
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
public FallbackCommand(T result, HystrixCommandGroupKey group,
|
||||
int executionIsolationThreadTimeoutInMilliseconds) {
|
||||
super(group, executionIsolationThreadTimeoutInMilliseconds);
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
public FallbackCommand(T result, HystrixCommandGroupKey group,
|
||||
HystrixThreadPoolKey threadPool) {
|
||||
super(group, threadPool);
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
public FallbackCommand(T result, HystrixCommandGroupKey group,
|
||||
HystrixThreadPoolKey threadPool,
|
||||
int executionIsolationThreadTimeoutInMilliseconds) {
|
||||
super(group, threadPool, executionIsolationThreadTimeoutInMilliseconds);
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
public FallbackCommand(T result, Setter setter) {
|
||||
super(setter);
|
||||
this.result = result;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected T run() throws Exception {
|
||||
return this.result;
|
||||
}
|
||||
|
||||
}
|
||||
+2
-1
@@ -206,7 +206,8 @@ public class FeignHttpClientProperties {
|
||||
return this.poolConcurrencyPolicy;
|
||||
}
|
||||
|
||||
public void setPoolConcurrencyPolicy(PoolConcurrencyPolicy poolConcurrencyPolicy) {
|
||||
public void setPoolConcurrencyPolicy(
|
||||
PoolConcurrencyPolicy poolConcurrencyPolicy) {
|
||||
this.poolConcurrencyPolicy = poolConcurrencyPolicy;
|
||||
}
|
||||
|
||||
|
||||
+4
-2
@@ -54,8 +54,10 @@ public final class FeignUtils {
|
||||
return headers;
|
||||
}
|
||||
|
||||
static Collection<String> addTemplateParameter(Collection<String> possiblyNull, String paramName) {
|
||||
Collection<String> params = ofNullable(possiblyNull).map(ArrayList::new).orElse(new ArrayList<>());
|
||||
static Collection<String> addTemplateParameter(Collection<String> possiblyNull,
|
||||
String paramName) {
|
||||
Collection<String> params = ofNullable(possiblyNull).map(ArrayList::new)
|
||||
.orElse(new ArrayList<>());
|
||||
params.add(String.format("{%s}", paramName));
|
||||
return params;
|
||||
}
|
||||
|
||||
+5
-3
@@ -65,9 +65,11 @@ public class PageJacksonModule extends Module {
|
||||
|
||||
private final Page<T> delegate;
|
||||
|
||||
SimplePageImpl(@JsonProperty("content") List<T> content, @JsonProperty("number") int number,
|
||||
@JsonProperty("size") int size, @JsonProperty("totalElements") @JsonAlias({ "total-elements",
|
||||
"total_elements", "totalelements", "TotalElements" }) long totalElements,
|
||||
SimplePageImpl(@JsonProperty("content") List<T> content,
|
||||
@JsonProperty("number") int number, @JsonProperty("size") int size,
|
||||
@JsonProperty("totalElements") @JsonAlias({ "total-elements",
|
||||
"total_elements", "totalelements",
|
||||
"TotalElements" }) long totalElements,
|
||||
@JsonProperty("sort") Sort sort) {
|
||||
PageRequest pageRequest;
|
||||
if (sort != null) {
|
||||
|
||||
+8
-4
@@ -75,7 +75,8 @@ public class PageableSpringEncoder implements Encoder {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException {
|
||||
public void encode(Object object, Type bodyType, RequestTemplate template)
|
||||
throws EncodeException {
|
||||
|
||||
if (supports(object)) {
|
||||
if (object instanceof Pageable) {
|
||||
@@ -100,15 +101,18 @@ public class PageableSpringEncoder implements Encoder {
|
||||
delegate.encode(object, bodyType, template);
|
||||
}
|
||||
else {
|
||||
throw new EncodeException("PageableSpringEncoder does not support the given object " + object.getClass()
|
||||
+ " and no delegate was provided for fallback!");
|
||||
throw new EncodeException(
|
||||
"PageableSpringEncoder does not support the given object "
|
||||
+ object.getClass()
|
||||
+ " and no delegate was provided for fallback!");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void applySort(RequestTemplate template, Sort sort) {
|
||||
Collection<String> existingSorts = template.queries().get("sort");
|
||||
List<String> sortQueries = existingSorts != null ? new ArrayList<>(existingSorts) : new ArrayList<>();
|
||||
List<String> sortQueries = existingSorts != null ? new ArrayList<>(existingSorts)
|
||||
: new ArrayList<>();
|
||||
if (!sortParameter.equals("sort")) {
|
||||
existingSorts = template.queries().get(sortParameter);
|
||||
if (existingSorts != null) {
|
||||
|
||||
+4
-2
@@ -46,7 +46,8 @@ public class ResponseEntityDecoder implements Decoder {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object decode(final Response response, Type type) throws IOException, FeignException {
|
||||
public Object decode(final Response response, Type type)
|
||||
throws IOException, FeignException {
|
||||
|
||||
if (isParameterizeHttpEntity(type)) {
|
||||
type = ((ParameterizedType) type).getActualTypeArguments()[0];
|
||||
@@ -85,7 +86,8 @@ public class ResponseEntityDecoder implements Decoder {
|
||||
headers.put(key, new LinkedList<>(response.headers().get(key)));
|
||||
}
|
||||
|
||||
return new ResponseEntity<>((T) instance, headers, HttpStatus.valueOf(response.status()));
|
||||
return new ResponseEntity<>((T) instance, headers,
|
||||
HttpStatus.valueOf(response.status()));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-1
@@ -48,7 +48,8 @@ public class SortJacksonModule extends Module {
|
||||
context.addSerializers(serializers);
|
||||
|
||||
SimpleDeserializers deserializers = new SimpleDeserializers();
|
||||
deserializers.addDeserializer(Sort.class, new SortJsonComponent.SortDeserializer());
|
||||
deserializers.addDeserializer(Sort.class,
|
||||
new SortJsonComponent.SortDeserializer());
|
||||
context.addDeserializers(deserializers);
|
||||
}
|
||||
|
||||
|
||||
+6
-4
@@ -44,7 +44,8 @@ public class SortJsonComponent {
|
||||
public static class SortSerializer extends JsonSerializer<Sort> {
|
||||
|
||||
@Override
|
||||
public void serialize(Sort value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
|
||||
public void serialize(Sort value, JsonGenerator gen,
|
||||
SerializerProvider serializers) throws IOException {
|
||||
gen.writeStartArray();
|
||||
value.iterator().forEachRemaining(v -> {
|
||||
try {
|
||||
@@ -67,14 +68,15 @@ public class SortJsonComponent {
|
||||
public static class SortDeserializer extends JsonDeserializer<Sort> {
|
||||
|
||||
@Override
|
||||
public Sort deserialize(JsonParser jsonParser, DeserializationContext deserializationContext)
|
||||
throws IOException {
|
||||
public Sort deserialize(JsonParser jsonParser,
|
||||
DeserializationContext deserializationContext) throws IOException {
|
||||
TreeNode treeNode = jsonParser.getCodec().readTree(jsonParser);
|
||||
if (treeNode.isArray()) {
|
||||
ArrayNode arrayNode = (ArrayNode) treeNode;
|
||||
List<Sort.Order> orders = new ArrayList<>();
|
||||
for (JsonNode jsonNode : arrayNode) {
|
||||
Sort.Order order = new Sort.Order(Sort.Direction.valueOf(jsonNode.get("direction").textValue()),
|
||||
Sort.Order order = new Sort.Order(
|
||||
Sort.Direction.valueOf(jsonNode.get("direction").textValue()),
|
||||
jsonNode.get("property").textValue());
|
||||
orders.add(order);
|
||||
}
|
||||
|
||||
+8
-5
@@ -48,15 +48,18 @@ public class SpringDecoder implements Decoder {
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object decode(final Response response, Type type) throws IOException, FeignException {
|
||||
if (type instanceof Class || type instanceof ParameterizedType || type instanceof WildcardType) {
|
||||
public Object decode(final Response response, Type type)
|
||||
throws IOException, FeignException {
|
||||
if (type instanceof Class || type instanceof ParameterizedType
|
||||
|| type instanceof WildcardType) {
|
||||
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||
HttpMessageConverterExtractor<?> extractor = new HttpMessageConverterExtractor(type,
|
||||
this.messageConverters.getObject().getConverters());
|
||||
HttpMessageConverterExtractor<?> extractor = new HttpMessageConverterExtractor(
|
||||
type, this.messageConverters.getObject().getConverters());
|
||||
|
||||
return extractor.extractData(new FeignResponseAdapter(response));
|
||||
}
|
||||
throw new DecodeException(response.status(), "type is not an instance of Class or ParameterizedType: " + type,
|
||||
throw new DecodeException(response.status(),
|
||||
"type is not an instance of Class or ParameterizedType: " + type,
|
||||
response.request());
|
||||
}
|
||||
|
||||
|
||||
+48
-31
@@ -72,11 +72,13 @@ public class SpringEncoder implements Encoder {
|
||||
this(new SpringFormEncoder(), messageConverters);
|
||||
}
|
||||
|
||||
public SpringEncoder(SpringFormEncoder springFormEncoder, ObjectFactory<HttpMessageConverters> messageConverters) {
|
||||
public SpringEncoder(SpringFormEncoder springFormEncoder,
|
||||
ObjectFactory<HttpMessageConverters> messageConverters) {
|
||||
this(springFormEncoder, messageConverters, new FeignEncoderProperties());
|
||||
}
|
||||
|
||||
public SpringEncoder(SpringFormEncoder springFormEncoder, ObjectFactory<HttpMessageConverters> messageConverters,
|
||||
public SpringEncoder(SpringFormEncoder springFormEncoder,
|
||||
ObjectFactory<HttpMessageConverters> messageConverters,
|
||||
FeignEncoderProperties encoderProperties) {
|
||||
this.springFormEncoder = springFormEncoder;
|
||||
this.messageConverters = messageConverters;
|
||||
@@ -84,10 +86,12 @@ public class SpringEncoder implements Encoder {
|
||||
}
|
||||
|
||||
@Override
|
||||
public void encode(Object requestBody, Type bodyType, RequestTemplate request) throws EncodeException {
|
||||
public void encode(Object requestBody, Type bodyType, RequestTemplate request)
|
||||
throws EncodeException {
|
||||
// template.body(conversionService.convert(object, String.class));
|
||||
if (requestBody != null) {
|
||||
Collection<String> contentTypes = request.headers().get(HttpEncoding.CONTENT_TYPE);
|
||||
Collection<String> contentTypes = request.headers()
|
||||
.get(HttpEncoding.CONTENT_TYPE);
|
||||
|
||||
MediaType requestContentType = null;
|
||||
if (contentTypes != null && !contentTypes.isEmpty()) {
|
||||
@@ -101,25 +105,30 @@ public class SpringEncoder implements Encoder {
|
||||
}
|
||||
else {
|
||||
if (bodyType == MultipartFile.class) {
|
||||
log.warn("For MultipartFile to be handled correctly, the 'consumes' parameter of @RequestMapping "
|
||||
+ "should be specified as MediaType.MULTIPART_FORM_DATA_VALUE");
|
||||
log.warn(
|
||||
"For MultipartFile to be handled correctly, the 'consumes' parameter of @RequestMapping "
|
||||
+ "should be specified as MediaType.MULTIPART_FORM_DATA_VALUE");
|
||||
}
|
||||
}
|
||||
encodeWithMessageConverter(requestBody, bodyType, request, requestContentType);
|
||||
encodeWithMessageConverter(requestBody, bodyType, request,
|
||||
requestContentType);
|
||||
}
|
||||
}
|
||||
|
||||
private void encodeWithMessageConverter(Object requestBody, Type bodyType, RequestTemplate request,
|
||||
MediaType requestContentType) {
|
||||
for (HttpMessageConverter messageConverter : this.messageConverters.getObject().getConverters()) {
|
||||
private void encodeWithMessageConverter(Object requestBody, Type bodyType,
|
||||
RequestTemplate request, MediaType requestContentType) {
|
||||
for (HttpMessageConverter messageConverter : this.messageConverters.getObject()
|
||||
.getConverters()) {
|
||||
FeignOutputMessage outputMessage;
|
||||
try {
|
||||
if (messageConverter instanceof GenericHttpMessageConverter) {
|
||||
outputMessage = checkAndWrite(requestBody, bodyType, requestContentType,
|
||||
outputMessage = checkAndWrite(requestBody, bodyType,
|
||||
requestContentType,
|
||||
(GenericHttpMessageConverter) messageConverter, request);
|
||||
}
|
||||
else {
|
||||
outputMessage = checkAndWrite(requestBody, requestContentType, messageConverter, request);
|
||||
outputMessage = checkAndWrite(requestBody, requestContentType,
|
||||
messageConverter, request);
|
||||
}
|
||||
}
|
||||
catch (IOException | HttpMessageConversionException ex) {
|
||||
@@ -136,9 +145,11 @@ public class SpringEncoder implements Encoder {
|
||||
Charset charset;
|
||||
|
||||
MediaType contentType = outputMessage.getHeaders().getContentType();
|
||||
Charset charsetFromContentType = contentType != null ? contentType.getCharset() : null;
|
||||
Charset charsetFromContentType = contentType != null
|
||||
? contentType.getCharset() : null;
|
||||
|
||||
if (encoderProperties != null && encoderProperties.isCharsetFromContentType()
|
||||
if (encoderProperties != null
|
||||
&& encoderProperties.isCharsetFromContentType()
|
||||
&& charsetFromContentType != null) {
|
||||
charset = charsetFromContentType;
|
||||
}
|
||||
@@ -152,23 +163,26 @@ public class SpringEncoder implements Encoder {
|
||||
return;
|
||||
}
|
||||
}
|
||||
String message = "Could not write request: no suitable HttpMessageConverter " + "found for request type ["
|
||||
+ requestBody.getClass().getName() + "]";
|
||||
String message = "Could not write request: no suitable HttpMessageConverter "
|
||||
+ "found for request type [" + requestBody.getClass().getName() + "]";
|
||||
if (requestContentType != null) {
|
||||
message += " and content type [" + requestContentType + "]";
|
||||
}
|
||||
throw new EncodeException(message);
|
||||
}
|
||||
|
||||
private boolean shouldHaveNullCharset(HttpMessageConverter messageConverter, FeignOutputMessage outputMessage) {
|
||||
return binaryContentType(outputMessage) || messageConverter instanceof ByteArrayHttpMessageConverter
|
||||
|| messageConverter instanceof ProtobufHttpMessageConverter && ProtobufHttpMessageConverter.PROTOBUF
|
||||
.isCompatibleWith(outputMessage.getHeaders().getContentType());
|
||||
private boolean shouldHaveNullCharset(HttpMessageConverter messageConverter,
|
||||
FeignOutputMessage outputMessage) {
|
||||
return binaryContentType(outputMessage)
|
||||
|| messageConverter instanceof ByteArrayHttpMessageConverter
|
||||
|| messageConverter instanceof ProtobufHttpMessageConverter
|
||||
&& ProtobufHttpMessageConverter.PROTOBUF.isCompatibleWith(
|
||||
outputMessage.getHeaders().getContentType());
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private FeignOutputMessage checkAndWrite(Object body, MediaType contentType, HttpMessageConverter converter,
|
||||
RequestTemplate request) throws IOException {
|
||||
private FeignOutputMessage checkAndWrite(Object body, MediaType contentType,
|
||||
HttpMessageConverter converter, RequestTemplate request) throws IOException {
|
||||
if (converter.canWrite(body.getClass(), contentType)) {
|
||||
logBeforeWrite(body, contentType, converter);
|
||||
FeignOutputMessage outputMessage = new FeignOutputMessage(request);
|
||||
@@ -181,8 +195,9 @@ public class SpringEncoder implements Encoder {
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private FeignOutputMessage checkAndWrite(Object body, Type genericType, MediaType contentType,
|
||||
GenericHttpMessageConverter converter, RequestTemplate request) throws IOException {
|
||||
private FeignOutputMessage checkAndWrite(Object body, Type genericType,
|
||||
MediaType contentType, GenericHttpMessageConverter converter,
|
||||
RequestTemplate request) throws IOException {
|
||||
if (converter.canWrite(genericType, body.getClass(), contentType)) {
|
||||
logBeforeWrite(body, contentType, converter);
|
||||
FeignOutputMessage outputMessage = new FeignOutputMessage(request);
|
||||
@@ -198,25 +213,27 @@ public class SpringEncoder implements Encoder {
|
||||
HttpMessageConverter messageConverter) {
|
||||
if (log.isDebugEnabled()) {
|
||||
if (requestContentType != null) {
|
||||
log.debug("Writing [" + requestBody + "] as \"" + requestContentType + "\" using [" + messageConverter
|
||||
+ "]");
|
||||
log.debug("Writing [" + requestBody + "] as \"" + requestContentType
|
||||
+ "\" using [" + messageConverter + "]");
|
||||
}
|
||||
else {
|
||||
log.debug("Writing [" + requestBody + "] using [" + messageConverter + "]");
|
||||
log.debug(
|
||||
"Writing [" + requestBody + "] using [" + messageConverter + "]");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isMultipartType(MediaType requestContentType) {
|
||||
return Arrays.asList(MediaType.MULTIPART_FORM_DATA, MediaType.MULTIPART_MIXED, MediaType.MULTIPART_RELATED)
|
||||
.contains(requestContentType);
|
||||
return Arrays.asList(MediaType.MULTIPART_FORM_DATA, MediaType.MULTIPART_MIXED,
|
||||
MediaType.MULTIPART_RELATED).contains(requestContentType);
|
||||
}
|
||||
|
||||
private boolean binaryContentType(FeignOutputMessage outputMessage) {
|
||||
MediaType contentType = outputMessage.getHeaders().getContentType();
|
||||
return contentType == null || Stream
|
||||
.of(MediaType.APPLICATION_CBOR, MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_PDF,
|
||||
MediaType.IMAGE_GIF, MediaType.IMAGE_JPEG, MediaType.IMAGE_PNG)
|
||||
.of(MediaType.APPLICATION_CBOR, MediaType.APPLICATION_OCTET_STREAM,
|
||||
MediaType.APPLICATION_PDF, MediaType.IMAGE_GIF,
|
||||
MediaType.IMAGE_JPEG, MediaType.IMAGE_PNG)
|
||||
.anyMatch(mediaType -> mediaType.includes(contentType));
|
||||
}
|
||||
|
||||
|
||||
+108
-49
@@ -80,15 +80,18 @@ import static org.springframework.core.annotation.AnnotatedElementUtils.findMerg
|
||||
* @author Darren Foong
|
||||
* @author Ram Anaswara
|
||||
*/
|
||||
public class SpringMvcContract extends Contract.BaseContract implements ResourceLoaderAware {
|
||||
public class SpringMvcContract extends Contract.BaseContract
|
||||
implements ResourceLoaderAware {
|
||||
|
||||
private static final String ACCEPT = "Accept";
|
||||
|
||||
private static final String CONTENT_TYPE = "Content-Type";
|
||||
|
||||
private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(String.class);
|
||||
private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor
|
||||
.valueOf(String.class);
|
||||
|
||||
private static final TypeDescriptor ITERABLE_TYPE_DESCRIPTOR = TypeDescriptor.valueOf(Iterable.class);
|
||||
private static final TypeDescriptor ITERABLE_TYPE_DESCRIPTOR = TypeDescriptor
|
||||
.valueOf(Iterable.class);
|
||||
|
||||
private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
|
||||
|
||||
@@ -108,18 +111,22 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
|
||||
this(Collections.emptyList());
|
||||
}
|
||||
|
||||
public SpringMvcContract(List<AnnotatedParameterProcessor> annotatedParameterProcessors) {
|
||||
public SpringMvcContract(
|
||||
List<AnnotatedParameterProcessor> annotatedParameterProcessors) {
|
||||
this(annotatedParameterProcessors, new DefaultConversionService());
|
||||
}
|
||||
|
||||
public SpringMvcContract(List<AnnotatedParameterProcessor> annotatedParameterProcessors,
|
||||
public SpringMvcContract(
|
||||
List<AnnotatedParameterProcessor> annotatedParameterProcessors,
|
||||
ConversionService conversionService) {
|
||||
this(annotatedParameterProcessors, conversionService, true);
|
||||
}
|
||||
|
||||
public SpringMvcContract(List<AnnotatedParameterProcessor> annotatedParameterProcessors,
|
||||
public SpringMvcContract(
|
||||
List<AnnotatedParameterProcessor> annotatedParameterProcessors,
|
||||
ConversionService conversionService, boolean decodeSlash) {
|
||||
Assert.notNull(annotatedParameterProcessors, "Parameter processors can not be null.");
|
||||
Assert.notNull(annotatedParameterProcessors,
|
||||
"Parameter processors can not be null.");
|
||||
Assert.notNull(conversionService, "ConversionService can not be null.");
|
||||
|
||||
List<AnnotatedParameterProcessor> processors = getDefaultAnnotatedArgumentsProcessors();
|
||||
@@ -139,21 +146,26 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
|
||||
// Feign applies the Param.Expander to each element of an Iterable, so in those
|
||||
// cases we need to provide a TypeDescriptor of the element.
|
||||
if (typeDescriptor.isAssignableTo(ITERABLE_TYPE_DESCRIPTOR)) {
|
||||
TypeDescriptor elementTypeDescriptor = getElementTypeDescriptor(typeDescriptor);
|
||||
TypeDescriptor elementTypeDescriptor = getElementTypeDescriptor(
|
||||
typeDescriptor);
|
||||
|
||||
checkState(elementTypeDescriptor != null,
|
||||
"Could not resolve element type of Iterable type %s. Not declared?", typeDescriptor);
|
||||
"Could not resolve element type of Iterable type %s. Not declared?",
|
||||
typeDescriptor);
|
||||
|
||||
typeDescriptor = elementTypeDescriptor;
|
||||
}
|
||||
return typeDescriptor;
|
||||
}
|
||||
|
||||
private static TypeDescriptor getElementTypeDescriptor(TypeDescriptor typeDescriptor) {
|
||||
private static TypeDescriptor getElementTypeDescriptor(
|
||||
TypeDescriptor typeDescriptor) {
|
||||
TypeDescriptor elementTypeDescriptor = typeDescriptor.getElementTypeDescriptor();
|
||||
// that means it's not a collection but it is iterable, gh-135
|
||||
if (elementTypeDescriptor == null && Iterable.class.isAssignableFrom(typeDescriptor.getType())) {
|
||||
ResolvableType type = typeDescriptor.getResolvableType().as(Iterable.class).getGeneric(0);
|
||||
if (elementTypeDescriptor == null
|
||||
&& Iterable.class.isAssignableFrom(typeDescriptor.getType())) {
|
||||
ResolvableType type = typeDescriptor.getResolvableType().as(Iterable.class)
|
||||
.getGeneric(0);
|
||||
if (type.resolve() == null) {
|
||||
return null;
|
||||
}
|
||||
@@ -170,7 +182,8 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
|
||||
@Override
|
||||
protected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) {
|
||||
if (clz.getInterfaces().length == 0) {
|
||||
RequestMapping classAnnotation = findMergedAnnotation(clz, RequestMapping.class);
|
||||
RequestMapping classAnnotation = findMergedAnnotation(clz,
|
||||
RequestMapping.class);
|
||||
if (classAnnotation != null) {
|
||||
// Prepend path from class annotation if specified
|
||||
if (classAnnotation.value().length > 0) {
|
||||
@@ -193,7 +206,8 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
|
||||
processedMethods.put(Feign.configKey(targetType, method), method);
|
||||
MethodMetadata md = super.parseAndValidateMetadata(targetType, method);
|
||||
|
||||
RequestMapping classAnnotation = findMergedAnnotation(targetType, RequestMapping.class);
|
||||
RequestMapping classAnnotation = findMergedAnnotation(targetType,
|
||||
RequestMapping.class);
|
||||
if (classAnnotation != null) {
|
||||
// produces - use from class annotation only if method has not specified this
|
||||
if (!md.template().headers().containsKey(ACCEPT)) {
|
||||
@@ -213,14 +227,16 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void processAnnotationOnMethod(MethodMetadata data, Annotation methodAnnotation, Method method) {
|
||||
protected void processAnnotationOnMethod(MethodMetadata data,
|
||||
Annotation methodAnnotation, Method method) {
|
||||
if (CollectionFormat.class.isInstance(methodAnnotation)) {
|
||||
CollectionFormat collectionFormat = findMergedAnnotation(method, CollectionFormat.class);
|
||||
CollectionFormat collectionFormat = findMergedAnnotation(method,
|
||||
CollectionFormat.class);
|
||||
data.template().collectionFormat(collectionFormat.value());
|
||||
}
|
||||
|
||||
if (!RequestMapping.class.isInstance(methodAnnotation)
|
||||
&& !methodAnnotation.annotationType().isAnnotationPresent(RequestMapping.class)) {
|
||||
if (!RequestMapping.class.isInstance(methodAnnotation) && !methodAnnotation
|
||||
.annotationType().isAnnotationPresent(RequestMapping.class)) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -263,29 +279,34 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
|
||||
}
|
||||
|
||||
private String resolve(String value) {
|
||||
if (StringUtils.hasText(value) && resourceLoader instanceof ConfigurableApplicationContext) {
|
||||
return ((ConfigurableApplicationContext) resourceLoader).getEnvironment().resolvePlaceholders(value);
|
||||
if (StringUtils.hasText(value)
|
||||
&& resourceLoader instanceof ConfigurableApplicationContext) {
|
||||
return ((ConfigurableApplicationContext) resourceLoader).getEnvironment()
|
||||
.resolvePlaceholders(value);
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private void checkAtMostOne(Method method, Object[] values, String fieldName) {
|
||||
checkState(values != null && (values.length == 0 || values.length == 1),
|
||||
"Method %s can only contain at most 1 %s field. Found: %s", method.getName(), fieldName,
|
||||
"Method %s can only contain at most 1 %s field. Found: %s",
|
||||
method.getName(), fieldName,
|
||||
values == null ? null : Arrays.asList(values));
|
||||
}
|
||||
|
||||
private void checkOne(Method method, Object[] values, String fieldName) {
|
||||
checkState(values != null && values.length == 1, "Method %s can only contain 1 %s field. Found: %s",
|
||||
method.getName(), fieldName, values == null ? null : Arrays.asList(values));
|
||||
checkState(values != null && values.length == 1,
|
||||
"Method %s can only contain 1 %s field. Found: %s", method.getName(),
|
||||
fieldName, values == null ? null : Arrays.asList(values));
|
||||
}
|
||||
|
||||
@Override
|
||||
protected boolean processAnnotationsOnParameter(MethodMetadata data, Annotation[] annotations, int paramIndex) {
|
||||
protected boolean processAnnotationsOnParameter(MethodMetadata data,
|
||||
Annotation[] annotations, int paramIndex) {
|
||||
boolean isHttpAnnotation = false;
|
||||
|
||||
AnnotatedParameterProcessor.AnnotatedParameterContext context = new SimpleAnnotatedParameterContext(data,
|
||||
paramIndex);
|
||||
AnnotatedParameterProcessor.AnnotatedParameterContext context = new SimpleAnnotatedParameterContext(
|
||||
data, paramIndex);
|
||||
Method method = processedMethods.get(data.configKey());
|
||||
for (Annotation parameterAnnotation : annotations) {
|
||||
AnnotatedParameterProcessor processor = annotatedArgumentProcessors
|
||||
@@ -294,16 +315,19 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
|
||||
Annotation processParameterAnnotation;
|
||||
// synthesize, handling @AliasFor, while falling back to parameter name on
|
||||
// missing String #value():
|
||||
processParameterAnnotation = synthesizeWithMethodParameterNameAsFallbackValue(parameterAnnotation,
|
||||
method, paramIndex);
|
||||
isHttpAnnotation |= processor.processArgument(context, processParameterAnnotation, method);
|
||||
processParameterAnnotation = synthesizeWithMethodParameterNameAsFallbackValue(
|
||||
parameterAnnotation, method, paramIndex);
|
||||
isHttpAnnotation |= processor.processArgument(context,
|
||||
processParameterAnnotation, method);
|
||||
}
|
||||
}
|
||||
|
||||
if (!isMultipartFormData(data) && isHttpAnnotation && data.indexToExpander().get(paramIndex) == null) {
|
||||
if (!isMultipartFormData(data) && isHttpAnnotation
|
||||
&& data.indexToExpander().get(paramIndex) == null) {
|
||||
TypeDescriptor typeDescriptor = createTypeDescriptor(method, paramIndex);
|
||||
if (conversionService.canConvert(typeDescriptor, STRING_TYPE_DESCRIPTOR)) {
|
||||
Param.Expander expander = convertingExpanderFactory.getExpander(typeDescriptor);
|
||||
Param.Expander expander = convertingExpanderFactory
|
||||
.getExpander(typeDescriptor);
|
||||
if (expander != null) {
|
||||
data.indexToExpander().put(paramIndex, expander);
|
||||
}
|
||||
@@ -312,23 +336,28 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
|
||||
return isHttpAnnotation;
|
||||
}
|
||||
|
||||
private void parseProduces(MethodMetadata md, Method method, RequestMapping annotation) {
|
||||
private void parseProduces(MethodMetadata md, Method method,
|
||||
RequestMapping annotation) {
|
||||
String[] serverProduces = annotation.produces();
|
||||
String clientAccepts = serverProduces.length == 0 ? null : emptyToNull(serverProduces[0]);
|
||||
String clientAccepts = serverProduces.length == 0 ? null
|
||||
: emptyToNull(serverProduces[0]);
|
||||
if (clientAccepts != null) {
|
||||
md.template().header(ACCEPT, clientAccepts);
|
||||
}
|
||||
}
|
||||
|
||||
private void parseConsumes(MethodMetadata md, Method method, RequestMapping annotation) {
|
||||
private void parseConsumes(MethodMetadata md, Method method,
|
||||
RequestMapping annotation) {
|
||||
String[] serverConsumes = annotation.consumes();
|
||||
String clientProduces = serverConsumes.length == 0 ? null : emptyToNull(serverConsumes[0]);
|
||||
String clientProduces = serverConsumes.length == 0 ? null
|
||||
: emptyToNull(serverConsumes[0]);
|
||||
if (clientProduces != null) {
|
||||
md.template().header(CONTENT_TYPE, clientProduces);
|
||||
}
|
||||
}
|
||||
|
||||
private void parseHeaders(MethodMetadata md, Method method, RequestMapping annotation) {
|
||||
private void parseHeaders(MethodMetadata md, Method method,
|
||||
RequestMapping annotation) {
|
||||
// TODO: only supports one header value per key
|
||||
if (annotation.headers() != null && annotation.headers().length > 0) {
|
||||
for (String header : annotation.headers()) {
|
||||
@@ -364,21 +393,26 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
|
||||
return annotatedArgumentResolvers;
|
||||
}
|
||||
|
||||
private Annotation synthesizeWithMethodParameterNameAsFallbackValue(Annotation parameterAnnotation, Method method,
|
||||
int parameterIndex) {
|
||||
Map<String, Object> annotationAttributes = AnnotationUtils.getAnnotationAttributes(parameterAnnotation);
|
||||
private Annotation synthesizeWithMethodParameterNameAsFallbackValue(
|
||||
Annotation parameterAnnotation, Method method, int parameterIndex) {
|
||||
Map<String, Object> annotationAttributes = AnnotationUtils
|
||||
.getAnnotationAttributes(parameterAnnotation);
|
||||
Object defaultValue = AnnotationUtils.getDefaultValue(parameterAnnotation);
|
||||
if (defaultValue instanceof String && defaultValue.equals(annotationAttributes.get(AnnotationUtils.VALUE))) {
|
||||
if (defaultValue instanceof String
|
||||
&& defaultValue.equals(annotationAttributes.get(AnnotationUtils.VALUE))) {
|
||||
Type[] parameterTypes = method.getGenericParameterTypes();
|
||||
String[] parameterNames = PARAMETER_NAME_DISCOVERER.getParameterNames(method);
|
||||
if (shouldAddParameterName(parameterIndex, parameterTypes, parameterNames)) {
|
||||
annotationAttributes.put(AnnotationUtils.VALUE, parameterNames[parameterIndex]);
|
||||
annotationAttributes.put(AnnotationUtils.VALUE,
|
||||
parameterNames[parameterIndex]);
|
||||
}
|
||||
}
|
||||
return AnnotationUtils.synthesizeAnnotation(annotationAttributes, parameterAnnotation.annotationType(), null);
|
||||
return AnnotationUtils.synthesizeAnnotation(annotationAttributes,
|
||||
parameterAnnotation.annotationType(), null);
|
||||
}
|
||||
|
||||
private boolean shouldAddParameterName(int parameterIndex, Type[] parameterTypes, String[] parameterNames) {
|
||||
private boolean shouldAddParameterName(int parameterIndex, Type[] parameterTypes,
|
||||
String[] parameterNames) {
|
||||
// has a parameter name
|
||||
return parameterNames != null && parameterNames.length > parameterIndex
|
||||
// has a type
|
||||
@@ -386,12 +420,14 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
|
||||
}
|
||||
|
||||
private boolean isMultipartFormData(MethodMetadata data) {
|
||||
Collection<String> contentTypes = data.template().headers().get(HttpEncoding.CONTENT_TYPE);
|
||||
Collection<String> contentTypes = data.template().headers()
|
||||
.get(HttpEncoding.CONTENT_TYPE);
|
||||
|
||||
if (contentTypes != null && !contentTypes.isEmpty()) {
|
||||
String type = contentTypes.iterator().next();
|
||||
try {
|
||||
return Objects.equals(MediaType.valueOf(type), MediaType.MULTIPART_FORM_DATA);
|
||||
return Objects.equals(MediaType.valueOf(type),
|
||||
MediaType.MULTIPART_FORM_DATA);
|
||||
}
|
||||
catch (InvalidMediaTypeException ignored) {
|
||||
return false;
|
||||
@@ -401,6 +437,25 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* @deprecated Not used internally anymore. Will be removed in the future.
|
||||
*/
|
||||
@Deprecated
|
||||
public static class ConvertingExpander implements Param.Expander {
|
||||
|
||||
private final ConversionService conversionService;
|
||||
|
||||
public ConvertingExpander(ConversionService conversionService) {
|
||||
this.conversionService = conversionService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String expand(Object value) {
|
||||
return conversionService.convert(value, String.class);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class ConvertingExpanderFactory {
|
||||
|
||||
private final ConversionService conversionService;
|
||||
@@ -411,20 +466,23 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
|
||||
|
||||
Param.Expander getExpander(TypeDescriptor typeDescriptor) {
|
||||
return value -> {
|
||||
Object converted = conversionService.convert(value, typeDescriptor, STRING_TYPE_DESCRIPTOR);
|
||||
Object converted = conversionService.convert(value, typeDescriptor,
|
||||
STRING_TYPE_DESCRIPTOR);
|
||||
return (String) converted;
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private class SimpleAnnotatedParameterContext implements AnnotatedParameterProcessor.AnnotatedParameterContext {
|
||||
private class SimpleAnnotatedParameterContext
|
||||
implements AnnotatedParameterProcessor.AnnotatedParameterContext {
|
||||
|
||||
private final MethodMetadata methodMetadata;
|
||||
|
||||
private final int parameterIndex;
|
||||
|
||||
SimpleAnnotatedParameterContext(MethodMetadata methodMetadata, int parameterIndex) {
|
||||
SimpleAnnotatedParameterContext(MethodMetadata methodMetadata,
|
||||
int parameterIndex) {
|
||||
this.methodMetadata = methodMetadata;
|
||||
this.parameterIndex = parameterIndex;
|
||||
}
|
||||
@@ -445,7 +503,8 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
|
||||
}
|
||||
|
||||
@Override
|
||||
public Collection<String> setTemplateParameter(String name, Collection<String> rest) {
|
||||
public Collection<String> setTemplateParameter(String name,
|
||||
Collection<String> rest) {
|
||||
return addTemplateParameter(rest, name);
|
||||
}
|
||||
|
||||
|
||||
+6
-6
@@ -2,6 +2,12 @@
|
||||
"groups": [
|
||||
],
|
||||
"properties": [
|
||||
{
|
||||
"name": "feign.hystrix.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "If true, an OpenFeign client will be wrapped with a Hystrix circuit breaker.",
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"name": "feign.autoconfiguration.jackson.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
@@ -49,12 +55,6 @@
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Enables the request sent by Feign to be compressed.",
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"name": "feign.metrics.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Enables metrics capability for Feign.",
|
||||
"defaultValue": "true"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
org.springframework.cloud.openfeign.ribbon.FeignRibbonClientAutoConfiguration,\
|
||||
org.springframework.cloud.openfeign.hateoas.FeignHalAutoConfiguration,\
|
||||
org.springframework.cloud.openfeign.FeignAutoConfiguration,\
|
||||
org.springframework.cloud.openfeign.encoding.FeignAcceptGzipEncodingAutoConfiguration,\
|
||||
|
||||
+14
-7
@@ -43,8 +43,10 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringBootTest(classes = DefaultGzipDecoderTests.Application.class,
|
||||
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
|
||||
value = { "spring.application.name=defaultGzipDecoderTests", "feign.compression.response.enabled=true",
|
||||
"feign.compression.response.useGzipDecoder=true", "feign.client.config.default.loggerLevel=full",
|
||||
value = { "spring.application.name=defaultGzipDecoderTests",
|
||||
"feign.compression.response.enabled=true",
|
||||
"feign.compression.response.useGzipDecoder=true",
|
||||
"feign.client.config.default.loggerLevel=full",
|
||||
"logging.level.org.springframework.cloud.openfeign=DEBUG" })
|
||||
@DirtiesContext
|
||||
public class DefaultGzipDecoderTests extends FeignClientFactoryBean {
|
||||
@@ -69,17 +71,20 @@ public class DefaultGzipDecoderTests extends FeignClientFactoryBean {
|
||||
public void testBodyDecompress() {
|
||||
ResponseEntity<Hello> response = testClient().getGzipResponse();
|
||||
assertThat(response).as("response was null").isNotNull();
|
||||
assertThat(response.getStatusCode()).as("wrong status code").isEqualTo(HttpStatus.OK);
|
||||
assertThat(response.getStatusCode()).as("wrong status code")
|
||||
.isEqualTo(HttpStatus.OK);
|
||||
Hello hello = response.getBody();
|
||||
assertThat(hello).as("hello was null").isNotNull();
|
||||
assertThat(hello).as("first hello didn't match").isEqualTo(new Hello("hello world via response"));
|
||||
assertThat(hello).as("first hello didn't match")
|
||||
.isEqualTo(new Hello("hello world via response"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNullBodyDecompress() {
|
||||
ResponseEntity<Hello> response = testClient().getNullResponse();
|
||||
assertThat(response).as("response was null").isNotNull();
|
||||
assertThat(response.getStatusCode()).as("wrong status code").isEqualTo(HttpStatus.OK);
|
||||
assertThat(response.getStatusCode()).as("wrong status code")
|
||||
.isEqualTo(HttpStatus.OK);
|
||||
Hello hello = response.getBody();
|
||||
assertThat(hello).as("hello was not null").isNull();
|
||||
assertThat(hello).as("null hello didn't match").isEqualTo(null);
|
||||
@@ -89,10 +94,12 @@ public class DefaultGzipDecoderTests extends FeignClientFactoryBean {
|
||||
public void testCharsetDecompress() {
|
||||
ResponseEntity<Hello> response = testClient().getUtf8Response();
|
||||
assertThat(response).as("response was null").isNotNull();
|
||||
assertThat(response.getStatusCode()).as("wrong status code").isEqualTo(HttpStatus.OK);
|
||||
assertThat(response.getStatusCode()).as("wrong status code")
|
||||
.isEqualTo(HttpStatus.OK);
|
||||
Hello hello = response.getBody();
|
||||
assertThat(hello).as("hello was null").isNotNull();
|
||||
assertThat(hello).as("utf8 hello didn't match").isEqualTo(new Hello("안녕하세요 means Hello in Korean"));
|
||||
assertThat(hello).as("utf8 hello didn't match")
|
||||
.isEqualTo(new Hello("안녕하세요 means Hello in Korean"));
|
||||
}
|
||||
|
||||
private static class Hello {
|
||||
|
||||
+7
-3
@@ -21,10 +21,12 @@ import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.netflix.archaius.ArchaiusAutoConfiguration;
|
||||
import org.springframework.cloud.openfeign.support.PageableSpringEncoder;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
|
||||
@@ -42,11 +44,13 @@ public class EnableFeignClientsSpringDataTests {
|
||||
@Test
|
||||
public void encoderDefaultCorrect() {
|
||||
|
||||
PageableSpringEncoder.class.cast(this.feignContext.getInstance("foo", Encoder.class));
|
||||
PageableSpringEncoder.class
|
||||
.cast(this.feignContext.getInstance("foo", Encoder.class));
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableAutoConfiguration
|
||||
@Import({ PropertyPlaceholderAutoConfiguration.class, ArchaiusAutoConfiguration.class,
|
||||
FeignAutoConfiguration.class })
|
||||
protected static class PlainConfiguration {
|
||||
|
||||
}
|
||||
|
||||
+2
-1
@@ -30,6 +30,7 @@ import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.netflix.archaius.ArchaiusAutoConfiguration;
|
||||
import org.springframework.cloud.openfeign.support.SpringEncoder;
|
||||
import org.springframework.cloud.openfeign.support.SpringMvcContract;
|
||||
import org.springframework.cloud.test.ClassPathExclusions;
|
||||
@@ -87,7 +88,7 @@ public class EnableFeignClientsTests {
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Import({ FeignAutoConfiguration.class })
|
||||
@Import({ ArchaiusAutoConfiguration.class, FeignAutoConfiguration.class })
|
||||
protected static class PlainConfiguration {
|
||||
|
||||
}
|
||||
|
||||
+59
-10
@@ -16,11 +16,14 @@
|
||||
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import feign.hystrix.HystrixFeign;
|
||||
import org.assertj.core.api.Condition;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.FilteredClassLoader;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
|
||||
@@ -29,30 +32,76 @@ import static org.mockito.Mockito.mock;
|
||||
|
||||
/**
|
||||
* @author Tim Peeters
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
class FeignAutoConfigurationTests {
|
||||
|
||||
private final ApplicationContextRunner runner = new ApplicationContextRunner()
|
||||
.withConfiguration(AutoConfigurations.of(FeignAutoConfiguration.class))
|
||||
.withPropertyValues("feign.httpclient.enabled=false");
|
||||
.withConfiguration(AutoConfigurations.of(FeignAutoConfiguration.class));
|
||||
|
||||
@Test
|
||||
void shouldInstantiateDefaultTargeterWhenFeignCircuitBreakerIsDisabled() {
|
||||
runner.withPropertyValues("feign.circuitbreaker.enabled=false")
|
||||
void shouldInstantiateHystrixTargeterToMaintainBackwardsCompatibility() {
|
||||
runner.run(ctx -> assertOnlyOneTargeterPresent(ctx, HystrixTargeter.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInstantiateHystrixTargeterWhenExplicitlyEnabled() {
|
||||
runner.withPropertyValues("feign.hystrix.enabled=true")
|
||||
.run(ctx -> assertOnlyOneTargeterPresent(ctx, HystrixTargeter.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInstantiateDefaultTargeterWhenHystrixIsDisabled() {
|
||||
runner.withPropertyValues("feign.hystrix.enabled=false")
|
||||
.run(ctx -> assertOnlyOneTargeterPresent(ctx, DefaultTargeter.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInstantiateDefaultTargeterWhenHystrixFeignClassIsMissing() {
|
||||
runner.withPropertyValues("feign.hystrix.enabled=true")
|
||||
.withClassLoader(new FilteredClassLoader(HystrixFeign.class))
|
||||
.run(ctx -> assertOnlyOneTargeterPresent(ctx, DefaultTargeter.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInstantiateDefaultTargeterWhenHystrixFeignAndCircuitBreakerClassesAreMissing() {
|
||||
runner.withPropertyValues("feign.hystrix.enabled=true",
|
||||
"feign.circuitbreaker.enabled=true")
|
||||
.withClassLoader(
|
||||
new FilteredClassLoader(HystrixFeign.class, CircuitBreaker.class))
|
||||
.run(ctx -> assertOnlyOneTargeterPresent(ctx, DefaultTargeter.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInstantiateDefaultTargeterWhenHystrixFeignClassIsMissingAndFeignCircuitBreakerIsDisabled() {
|
||||
runner.withClassLoader(new FilteredClassLoader(HystrixFeign.class))
|
||||
.withPropertyValues("feign.circuitbreaker.enabled=false")
|
||||
.run(ctx -> assertOnlyOneTargeterPresent(ctx, DefaultTargeter.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInstantiateFeignCircuitBreakerTargeterWhenEnabled() {
|
||||
runner.withBean(CircuitBreakerFactory.class, () -> mock(CircuitBreakerFactory.class))
|
||||
runner.withBean(CircuitBreakerFactory.class,
|
||||
() -> mock(CircuitBreakerFactory.class))
|
||||
.withPropertyValues("feign.circuitbreaker.enabled=true")
|
||||
.run(ctx -> assertOnlyOneTargeterPresent(ctx, FeignCircuitBreakerTargeter.class));
|
||||
.run(ctx -> assertOnlyOneTargeterPresent(ctx,
|
||||
FeignCircuitBreakerTargeter.class));
|
||||
}
|
||||
|
||||
private void assertOnlyOneTargeterPresent(ConfigurableApplicationContext ctx, Class<?> beanClass) {
|
||||
assertThat(ctx.getBeansOfType(Targeter.class)).hasSize(1).hasValueSatisfying(new Condition<>(
|
||||
beanClass::isInstance, String.format("Targeter should be an instance of %s", beanClass)));
|
||||
@Test
|
||||
void shouldInstantiateFeignCircuitBreakerTargeterWhenBothHystrixAndCircuitBreakerAreEnabled() {
|
||||
runner.withBean(CircuitBreakerFactory.class,
|
||||
() -> mock(CircuitBreakerFactory.class))
|
||||
.withPropertyValues("feign.hystrix.enabled=true",
|
||||
"feign.circuitbreaker.enabled=true")
|
||||
.run(ctx -> assertOnlyOneTargeterPresent(ctx,
|
||||
FeignCircuitBreakerTargeter.class));
|
||||
}
|
||||
|
||||
private void assertOnlyOneTargeterPresent(ConfigurableApplicationContext ctx,
|
||||
Class<?> beanClass) {
|
||||
assertThat(ctx.getBeansOfType(Targeter.class)).hasSize(1)
|
||||
.hasValueSatisfying(new Condition<>(beanClass::isInstance, String
|
||||
.format("Targeter should be an instance of %s", beanClass)));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+29
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
* Copyright 2013-2019 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.
|
||||
@@ -33,6 +33,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Matt King
|
||||
* @author Sam Kruglov
|
||||
*/
|
||||
public class FeignBuilderCustomizerTests {
|
||||
|
||||
@@ -41,7 +42,8 @@ public class FeignBuilderCustomizerTests {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
|
||||
FeignBuilderCustomizerTests.SampleConfiguration2.class);
|
||||
|
||||
FeignClientFactoryBean clientFactoryBean = context.getBean(FeignClientFactoryBean.class);
|
||||
FeignClientFactoryBean clientFactoryBean = context
|
||||
.getBean(FeignClientFactoryBean.class);
|
||||
FeignContext feignContext = context.getBean(FeignContext.class);
|
||||
|
||||
Feign.Builder builder = clientFactoryBean.feign(feignContext);
|
||||
@@ -51,12 +53,14 @@ public class FeignBuilderCustomizerTests {
|
||||
context.close();
|
||||
}
|
||||
|
||||
private void assertFeignBuilderField(Feign.Builder builder, String fieldName, Object expectedValue) {
|
||||
private void assertFeignBuilderField(Feign.Builder builder, String fieldName,
|
||||
Object expectedValue) {
|
||||
Field builderField = ReflectionUtils.findField(Feign.Builder.class, fieldName);
|
||||
ReflectionUtils.makeAccessible(builderField);
|
||||
|
||||
Object value = ReflectionUtils.getField(builderField, builder);
|
||||
assertThat(value).as("Expected value for the field '" + fieldName + "':").isEqualTo(expectedValue);
|
||||
assertThat(value).as("Expected value for the field '" + fieldName + "':")
|
||||
.isEqualTo(expectedValue);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -64,7 +68,8 @@ public class FeignBuilderCustomizerTests {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
|
||||
FeignBuilderCustomizerTests.SampleConfiguration3.class);
|
||||
|
||||
FeignClientFactoryBean clientFactoryBean = context.getBean(FeignClientFactoryBean.class);
|
||||
FeignClientFactoryBean clientFactoryBean = context
|
||||
.getBean(FeignClientFactoryBean.class);
|
||||
FeignContext feignContext = context.getBean(FeignContext.class);
|
||||
|
||||
Feign.Builder builder = clientFactoryBean.feign(feignContext);
|
||||
@@ -74,6 +79,25 @@ public class FeignBuilderCustomizerTests {
|
||||
context.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildCustomizerOrderedWithAdditional() {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
|
||||
FeignBuilderCustomizerTests.SampleConfiguration3.class);
|
||||
|
||||
FeignClientFactoryBean clientFactoryBean = context
|
||||
.getBean(FeignClientFactoryBean.class);
|
||||
clientFactoryBean.addCustomizer(builder -> builder.logLevel(Logger.Level.BASIC));
|
||||
clientFactoryBean.addCustomizer(Feign.Builder::doNotCloseAfterDecode);
|
||||
FeignContext feignContext = context.getBean(FeignContext.class);
|
||||
|
||||
Feign.Builder builder = clientFactoryBean.feign(feignContext);
|
||||
assertFeignBuilderField(builder, "logLevel", Logger.Level.BASIC);
|
||||
assertFeignBuilderField(builder, "decode404", true);
|
||||
assertFeignBuilderField(builder, "closeAfterDecode", false);
|
||||
|
||||
context.close();
|
||||
}
|
||||
|
||||
private static FeignClientFactoryBean defaultFeignClientFactoryBean() {
|
||||
FeignClientFactoryBean feignClientFactoryBean = new FeignClientFactoryBean();
|
||||
feignClientFactoryBean.setContextId("test");
|
||||
|
||||
+77
-50
@@ -24,6 +24,8 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import feign.Feign;
|
||||
import feign.hystrix.FallbackFactory;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
@@ -39,6 +41,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Sven Döring
|
||||
* @author Sam Kruglov
|
||||
*/
|
||||
public class FeignClientBuilderTests {
|
||||
|
||||
@@ -49,23 +52,32 @@ public class FeignClientBuilderTests {
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
private static Object getDefaultValueFromFeignClientAnnotation(final String methodName) {
|
||||
private static Object getDefaultValueFromFeignClientAnnotation(
|
||||
final String methodName) {
|
||||
final Method method = ReflectionUtils.findMethod(FeignClient.class, methodName);
|
||||
return method.getDefaultValue();
|
||||
}
|
||||
|
||||
private static void assertFactoryBeanField(final FeignClientBuilder.Builder builder, final String fieldName,
|
||||
final Object expectedValue) {
|
||||
final Field factoryBeanField = ReflectionUtils.findField(FeignClientBuilder.Builder.class,
|
||||
"feignClientFactoryBean");
|
||||
ReflectionUtils.makeAccessible(factoryBeanField);
|
||||
final FeignClientFactoryBean factoryBean = (FeignClientFactoryBean) ReflectionUtils.getField(factoryBeanField,
|
||||
builder);
|
||||
private static void assertFactoryBeanField(final FeignClientBuilder.Builder builder,
|
||||
final String fieldName, final Object expectedValue) {
|
||||
final Object value = getFactoryBeanField(builder, fieldName);
|
||||
assertThat(value).as("Expected value for the field '" + fieldName + "':")
|
||||
.isEqualTo(expectedValue);
|
||||
}
|
||||
|
||||
final Field field = ReflectionUtils.findField(FeignClientFactoryBean.class, fieldName);
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> T getFactoryBeanField(final FeignClientBuilder.Builder builder,
|
||||
final String fieldName) {
|
||||
final Field factoryBeanField = ReflectionUtils
|
||||
.findField(FeignClientBuilder.Builder.class, "feignClientFactoryBean");
|
||||
ReflectionUtils.makeAccessible(factoryBeanField);
|
||||
final FeignClientFactoryBean factoryBean = (FeignClientFactoryBean) ReflectionUtils
|
||||
.getField(factoryBeanField, builder);
|
||||
|
||||
final Field field = ReflectionUtils.findField(FeignClientFactoryBean.class,
|
||||
fieldName);
|
||||
ReflectionUtils.makeAccessible(field);
|
||||
final Object value = ReflectionUtils.getField(field, factoryBean);
|
||||
assertThat(value).as("Expected value for the field '" + fieldName + "':").isEqualTo(expectedValue);
|
||||
return (T) ReflectionUtils.getField(field, factoryBean);
|
||||
}
|
||||
|
||||
@Before
|
||||
@@ -80,8 +92,9 @@ public class FeignClientBuilderTests {
|
||||
for (final Method method : FeignClient.class.getMethods()) {
|
||||
methodNames.add(method.getName());
|
||||
}
|
||||
methodNames.removeAll(Arrays.asList("annotationType", "value", "serviceId", "qualifier", "qualifiers",
|
||||
"configuration", "primary", "equals", "hashCode", "toString"));
|
||||
methodNames.removeAll(Arrays.asList("annotationType", "value", "serviceId",
|
||||
"qualifier", "qualifiers", "configuration", "primary", "equals",
|
||||
"hashCode", "toString"));
|
||||
Collections.sort(methodNames);
|
||||
// If this safety check fails the Builder has to be updated.
|
||||
// (1) Either a field was removed from the FeignClient annotation and so it has to
|
||||
@@ -89,54 +102,45 @@ public class FeignClientBuilderTests {
|
||||
// on this builder class.
|
||||
// (2) Or a new field was added and the builder class has to be extended with this
|
||||
// new field.
|
||||
assertThat(methodNames).containsExactly("contextId", "decode404", "fallback", "fallbackFactory", "name", "path",
|
||||
"url");
|
||||
assertThat(methodNames).containsExactly("contextId", "decode404", "fallback",
|
||||
"fallbackFactory", "name", "path", "url");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void forType_preinitializedBuilder() {
|
||||
// when:
|
||||
final FeignClientBuilder.Builder builder = this.feignClientBuilder.forType(TestFeignClient.class, "TestClient");
|
||||
final FeignClientBuilder.Builder builder = this.feignClientBuilder
|
||||
.forType(TestFeignClient.class, "TestClient");
|
||||
|
||||
// then:
|
||||
assertFactoryBeanField(builder, "applicationContext", this.applicationContext);
|
||||
assertFactoryBeanField(builder, "type", TestFeignClient.class);
|
||||
assertFactoryBeanField(builder, "name", "TestClient");
|
||||
assertFactoryBeanField(builder, "contextId", "TestClient");
|
||||
assertFactoryBeanField(builder, "inheritParentContext", true);
|
||||
|
||||
// and:
|
||||
assertFactoryBeanField(builder, "url", getDefaultValueFromFeignClientAnnotation("url"));
|
||||
assertFactoryBeanField(builder, "path", getDefaultValueFromFeignClientAnnotation("path"));
|
||||
assertFactoryBeanField(builder, "decode404", getDefaultValueFromFeignClientAnnotation("decode404"));
|
||||
assertFactoryBeanField(builder, "fallback", getDefaultValueFromFeignClientAnnotation("fallback"));
|
||||
assertFactoryBeanField(builder, "fallbackFactory", getDefaultValueFromFeignClientAnnotation("fallbackFactory"));
|
||||
assertFactoryBeanField(builder, "url",
|
||||
getDefaultValueFromFeignClientAnnotation("url"));
|
||||
assertFactoryBeanField(builder, "path",
|
||||
getDefaultValueFromFeignClientAnnotation("path"));
|
||||
assertFactoryBeanField(builder, "decode404",
|
||||
getDefaultValueFromFeignClientAnnotation("decode404"));
|
||||
assertFactoryBeanField(builder, "fallback",
|
||||
getDefaultValueFromFeignClientAnnotation("fallback"));
|
||||
assertFactoryBeanField(builder, "fallbackFactory",
|
||||
getDefaultValueFromFeignClientAnnotation("fallbackFactory"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void forType_allFieldsSetOnBuilder() {
|
||||
// when:
|
||||
final FeignClientBuilder.Builder builder = this.feignClientBuilder.forType(TestFeignClient.class, "TestClient")
|
||||
.decode404(true).url("Url/").path("/Path").contextId("TestContext");
|
||||
|
||||
// then:
|
||||
assertFactoryBeanField(builder, "applicationContext", this.applicationContext);
|
||||
assertFactoryBeanField(builder, "type", TestFeignClient.class);
|
||||
assertFactoryBeanField(builder, "name", "TestClient");
|
||||
assertFactoryBeanField(builder, "contextId", "TestContext");
|
||||
|
||||
// and:
|
||||
assertFactoryBeanField(builder, "url", "http://Url/");
|
||||
assertFactoryBeanField(builder, "path", "/Path");
|
||||
assertFactoryBeanField(builder, "decode404", true);
|
||||
|
||||
}
|
||||
|
||||
@Test
|
||||
public void forType_clientFactoryBeanProvided() {
|
||||
// when:
|
||||
final FeignClientBuilder.Builder builder = this.feignClientBuilder
|
||||
.forType(TestFeignClient.class, new FeignClientFactoryBean(), "TestClient").decode404(true)
|
||||
.path("Path/").url("Url/").contextId("TestContext");
|
||||
.forType(TestFeignClient.class, "TestClient").inheritParentContext(false)
|
||||
.fallback(TestFeignClientFallback.class)
|
||||
.fallbackFactory(TestFeignClientFallbackFactory.class).decode404(true)
|
||||
.url("Url/").path("/Path").contextId("TestContext")
|
||||
.customize(Feign.Builder::doNotCloseAfterDecode);
|
||||
|
||||
// then:
|
||||
assertFactoryBeanField(builder, "applicationContext", this.applicationContext);
|
||||
@@ -148,19 +152,28 @@ public class FeignClientBuilderTests {
|
||||
assertFactoryBeanField(builder, "url", "http://Url/");
|
||||
assertFactoryBeanField(builder, "path", "/Path");
|
||||
assertFactoryBeanField(builder, "decode404", true);
|
||||
assertFactoryBeanField(builder, "inheritParentContext", false);
|
||||
assertFactoryBeanField(builder, "fallback", TestFeignClientFallback.class);
|
||||
assertFactoryBeanField(builder, "fallbackFactory",
|
||||
TestFeignClientFallbackFactory.class);
|
||||
List<FeignBuilderCustomizer> additionalCustomizers = getFactoryBeanField(builder,
|
||||
"additionalCustomizers");
|
||||
assertThat(additionalCustomizers).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void forType_build() {
|
||||
// given:
|
||||
Mockito.when(this.applicationContext.getBean(FeignContext.class)).thenThrow(new ClosedFileSystemException()); // throw
|
||||
// an
|
||||
// unusual
|
||||
// exception
|
||||
// in
|
||||
// the
|
||||
// FeignClientFactoryBean
|
||||
final FeignClientBuilder.Builder builder = this.feignClientBuilder.forType(TestClient.class, "TestClient");
|
||||
Mockito.when(this.applicationContext.getBean(FeignContext.class))
|
||||
.thenThrow(new ClosedFileSystemException()); // throw
|
||||
// an
|
||||
// unusual
|
||||
// exception
|
||||
// in
|
||||
// the
|
||||
// FeignClientFactoryBean
|
||||
final FeignClientBuilder.Builder builder = this.feignClientBuilder
|
||||
.forType(TestClient.class, "TestClient");
|
||||
|
||||
// expect: 'the build will fail right after calling build() with the mocked
|
||||
// unusual exception'
|
||||
@@ -172,4 +185,18 @@ public class FeignClientBuilderTests {
|
||||
|
||||
}
|
||||
|
||||
private class TestFeignClientFallback implements TestFeignClient {
|
||||
|
||||
}
|
||||
|
||||
private class TestFeignClientFallbackFactory
|
||||
implements FallbackFactory<TestFeignClient> {
|
||||
|
||||
@Override
|
||||
public TestFeignClientFallback create(Throwable throwable) {
|
||||
return new TestFeignClientFallback();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-140
@@ -1,140 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021-2021 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.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import feign.Capability;
|
||||
import feign.Contract;
|
||||
import feign.ExceptionPropagationPolicy;
|
||||
import feign.Logger;
|
||||
import feign.RequestInterceptor;
|
||||
import feign.Retryer;
|
||||
import feign.codec.Decoder;
|
||||
import feign.codec.Encoder;
|
||||
import feign.codec.ErrorDecoder;
|
||||
import org.assertj.core.util.Lists;
|
||||
import org.assertj.core.util.Maps;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsAndHashCodeConsistency;
|
||||
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsConsistency;
|
||||
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsReflexivity;
|
||||
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsSymmetricity;
|
||||
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsTransitivity;
|
||||
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertHashCodeConsistency;
|
||||
|
||||
/**
|
||||
* @author Jonatan Ivanov
|
||||
*/
|
||||
class FeignClientConfigurationTests {
|
||||
|
||||
@Test
|
||||
void shouldDefaultToValuesWhenFieldsNotSet() {
|
||||
FeignClientProperties.FeignClientConfiguration config = new FeignClientProperties.FeignClientConfiguration();
|
||||
|
||||
assertThat(config.getLoggerLevel()).isNull();
|
||||
assertThat(config.getConnectTimeout()).isNull();
|
||||
assertThat(config.getReadTimeout()).isNull();
|
||||
assertThat(config.getRetryer()).isNull();
|
||||
assertThat(config.getErrorDecoder()).isNull();
|
||||
assertThat(config.getRequestInterceptors()).isNull();
|
||||
assertThat(config.getDefaultRequestHeaders()).isNull();
|
||||
assertThat(config.getDefaultQueryParameters()).isNull();
|
||||
assertThat(config.getDecode404()).isNull();
|
||||
assertThat(config.getDecoder()).isNull();
|
||||
assertThat(config.getEncoder()).isNull();
|
||||
assertThat(config.getContract()).isNull();
|
||||
assertThat(config.getExceptionPropagationPolicy()).isNull();
|
||||
assertThat(config.getCapabilities()).isNull();
|
||||
assertThat(config.getMetrics()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnValuesWhenSet() {
|
||||
FeignClientProperties.FeignClientConfiguration config = new FeignClientProperties.FeignClientConfiguration();
|
||||
config.setLoggerLevel(Logger.Level.FULL);
|
||||
config.setConnectTimeout(21);
|
||||
config.setReadTimeout(42);
|
||||
config.setRetryer(Retryer.class);
|
||||
config.setErrorDecoder(ErrorDecoder.class);
|
||||
List<Class<RequestInterceptor>> requestInterceptors = Lists.list(RequestInterceptor.class);
|
||||
config.setRequestInterceptors(requestInterceptors);
|
||||
Map<String, Collection<String>> defaultRequestHeaders = Maps.newHashMap("default", Lists.emptyList());
|
||||
config.setDefaultRequestHeaders(defaultRequestHeaders);
|
||||
Map<String, Collection<String>> defaultQueryParameters = Maps.newHashMap("default", Lists.emptyList());
|
||||
config.setDefaultQueryParameters(defaultQueryParameters);
|
||||
config.setDecode404(true);
|
||||
config.setDecoder(Decoder.class);
|
||||
config.setEncoder(Encoder.class);
|
||||
config.setContract(Contract.class);
|
||||
config.setExceptionPropagationPolicy(ExceptionPropagationPolicy.UNWRAP);
|
||||
List<Class<Capability>> capabilities = Lists.list(Capability.class);
|
||||
config.setCapabilities(capabilities);
|
||||
FeignClientProperties.MetricsProperties metrics = new FeignClientProperties.MetricsProperties();
|
||||
config.setMetrics(metrics);
|
||||
|
||||
assertThat(config.getLoggerLevel()).isSameAs(Logger.Level.FULL);
|
||||
assertThat(config.getConnectTimeout()).isEqualTo(21);
|
||||
assertThat(config.getReadTimeout()).isEqualTo(42);
|
||||
assertThat(config.getRetryer()).isSameAs(Retryer.class);
|
||||
assertThat(config.getErrorDecoder()).isSameAs(ErrorDecoder.class);
|
||||
assertThat(config.getRequestInterceptors()).isSameAs(requestInterceptors);
|
||||
assertThat(config.getDefaultRequestHeaders()).isSameAs(defaultRequestHeaders);
|
||||
assertThat(config.getDefaultQueryParameters()).isSameAs(defaultQueryParameters);
|
||||
assertThat(config.getDecode404()).isTrue();
|
||||
assertThat(config.getDecoder()).isSameAs(Decoder.class);
|
||||
assertThat(config.getEncoder()).isSameAs(Encoder.class);
|
||||
assertThat(config.getContract()).isSameAs(Contract.class);
|
||||
assertThat(config.getExceptionPropagationPolicy()).isSameAs(ExceptionPropagationPolicy.UNWRAP);
|
||||
assertThat(config.getCapabilities()).isSameAs(capabilities);
|
||||
assertThat(config.getMetrics()).isSameAs(metrics);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanity-checks equals and hashCode contracts but does not check every variation of
|
||||
* the fields.
|
||||
*/
|
||||
@Test
|
||||
void shouldHaveSomewhatValidEqualsAndHashCode() {
|
||||
FeignClientProperties.FeignClientConfiguration configOne = new FeignClientProperties.FeignClientConfiguration();
|
||||
FeignClientProperties.FeignClientConfiguration configTwo = new FeignClientProperties.FeignClientConfiguration();
|
||||
FeignClientProperties.FeignClientConfiguration configThree = new FeignClientProperties.FeignClientConfiguration();
|
||||
FeignClientProperties.FeignClientConfiguration differentConfig = new FeignClientProperties.FeignClientConfiguration();
|
||||
differentConfig.setDecode404(true);
|
||||
|
||||
assertEqualsReflexivity(configOne);
|
||||
|
||||
assertEqualsSymmetricity(configOne, configTwo);
|
||||
assertEqualsSymmetricity(configOne, differentConfig);
|
||||
assertEqualsSymmetricity(configOne, 42);
|
||||
|
||||
assertEqualsTransitivity(configOne, configTwo, configThree);
|
||||
|
||||
assertEqualsConsistency(configOne, configTwo);
|
||||
assertEqualsConsistency(configOne, differentConfig);
|
||||
assertEqualsConsistency(configOne, 42);
|
||||
assertEqualsConsistency(configOne, null);
|
||||
|
||||
assertHashCodeConsistency(configOne);
|
||||
assertEqualsAndHashCodeConsistency(configOne, configTwo);
|
||||
}
|
||||
|
||||
}
|
||||
-121
@@ -1,121 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021-2021 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.util.Map;
|
||||
|
||||
import feign.Capability;
|
||||
import feign.Contract;
|
||||
import feign.RequestLine;
|
||||
import feign.micrometer.MicrometerCapability;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Jonatan Ivanov
|
||||
*/
|
||||
@DirtiesContext
|
||||
@ActiveProfiles("no-foo-metrics")
|
||||
@SpringBootTest(classes = FeignClientDisabledClientLevelFeaturesTests.TestConfiguration.class)
|
||||
class FeignClientDisabledClientLevelFeaturesTests {
|
||||
|
||||
@Autowired
|
||||
private FeignContext context;
|
||||
|
||||
@Autowired
|
||||
private FooClient foo;
|
||||
|
||||
@Autowired
|
||||
private BarClient bar;
|
||||
|
||||
@Test
|
||||
void clientsAvailable() {
|
||||
assertThat(foo).isNotNull();
|
||||
assertThat(bar).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void capabilitiesShouldNotBeAvailableWhenDisabled() {
|
||||
assertThat(context.getInstance("foo", MicrometerCapability.class)).isNull();
|
||||
assertThat(context.getInstances("foo", Capability.class)).isEmpty();
|
||||
|
||||
assertThat(context.getInstance("bar", MicrometerCapability.class)).isNotNull();
|
||||
Map<String, Capability> barCapabilities = context.getInstances("bar", Capability.class);
|
||||
assertThat(barCapabilities).hasSize(2);
|
||||
assertThat(barCapabilities.get("micrometerCapability")).isExactlyInstanceOf(MicrometerCapability.class);
|
||||
assertThat(barCapabilities.get("noOpCapability")).isExactlyInstanceOf(NoOpCapability.class);
|
||||
}
|
||||
|
||||
@FeignClient(name = "foo", url = "https://foo", configuration = FooConfiguration.class)
|
||||
interface FooClient {
|
||||
|
||||
@RequestLine("GET /")
|
||||
String get();
|
||||
|
||||
}
|
||||
|
||||
@FeignClient(name = "bar", url = "https://bar", configuration = BarConfiguration.class)
|
||||
interface BarClient {
|
||||
|
||||
@RequestMapping(value = "/", method = RequestMethod.GET)
|
||||
String get();
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableAutoConfiguration
|
||||
@EnableConfigurationProperties(FeignClientProperties.class)
|
||||
@EnableFeignClients(clients = { FooClient.class, BarClient.class })
|
||||
protected static class TestConfiguration {
|
||||
|
||||
}
|
||||
|
||||
public static class FooConfiguration {
|
||||
|
||||
@Bean // if the feign configuration empty, the context is not able to start
|
||||
public Contract feignContract() {
|
||||
return new Contract.Default();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class BarConfiguration {
|
||||
|
||||
@Bean
|
||||
public Capability noOpCapability() {
|
||||
return new NoOpCapability();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class NoOpCapability implements Capability {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
-120
@@ -1,120 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021-2021 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.util.Map;
|
||||
|
||||
import feign.Capability;
|
||||
import feign.Contract;
|
||||
import feign.RequestLine;
|
||||
import feign.micrometer.MicrometerCapability;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Jonatan Ivanov
|
||||
*/
|
||||
@DirtiesContext
|
||||
@ActiveProfiles("no-metrics")
|
||||
@SpringBootTest(classes = FeignClientDisabledFeaturesTests.TestConfiguration.class)
|
||||
class FeignClientDisabledFeaturesTests {
|
||||
|
||||
@Autowired
|
||||
private FeignContext context;
|
||||
|
||||
@Autowired
|
||||
private FooClient foo;
|
||||
|
||||
@Autowired
|
||||
private BarClient bar;
|
||||
|
||||
@Test
|
||||
void clientsAvailable() {
|
||||
assertThat(foo).isNotNull();
|
||||
assertThat(bar).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void capabilitiesShouldNotBeAvailable() {
|
||||
assertThat(context.getInstance("foo", MicrometerCapability.class)).isNull();
|
||||
assertThat(context.getInstances("foo", Capability.class)).isEmpty();
|
||||
|
||||
assertThat(context.getInstance("bar", MicrometerCapability.class)).isNull();
|
||||
Map<String, Capability> barCapabilities = context.getInstances("bar", Capability.class);
|
||||
assertThat(barCapabilities).hasSize(1);
|
||||
assertThat(barCapabilities.get("noOpCapability")).isExactlyInstanceOf(NoOpCapability.class);
|
||||
}
|
||||
|
||||
@FeignClient(name = "foo", url = "https://foo", configuration = FooConfiguration.class)
|
||||
interface FooClient {
|
||||
|
||||
@RequestLine("GET /")
|
||||
String get();
|
||||
|
||||
}
|
||||
|
||||
@FeignClient(name = "bar", url = "https://bar", configuration = BarConfiguration.class)
|
||||
interface BarClient {
|
||||
|
||||
@RequestMapping(value = "/", method = RequestMethod.GET)
|
||||
String get();
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableAutoConfiguration
|
||||
@EnableConfigurationProperties(FeignClientProperties.class)
|
||||
@EnableFeignClients(clients = { FooClient.class, BarClient.class })
|
||||
protected static class TestConfiguration {
|
||||
|
||||
}
|
||||
|
||||
public static class FooConfiguration {
|
||||
|
||||
@Bean // if the feign configuration empty, the context is not able to start
|
||||
public Contract feignContract() {
|
||||
return new Contract.Default();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class BarConfiguration {
|
||||
|
||||
@Bean
|
||||
public Capability noOpCapability() {
|
||||
return new NoOpCapability();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class NoOpCapability implements Capability {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+5
-5
@@ -44,7 +44,6 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Michael Cramer
|
||||
* @author Jonatan Ivanov
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringBootTest(classes = FeignClientErrorDecoderTests.TestConfiguration.class)
|
||||
@@ -68,7 +67,8 @@ public class FeignClientErrorDecoderTests {
|
||||
|
||||
@Test
|
||||
public void errorDecoderInConfiguration() {
|
||||
assertThat(this.context.getInstance("foo", ErrorDecoder.class)).isInstanceOf(ErrorDecoder.Default.class);
|
||||
assertThat(this.context.getInstance("foo", ErrorDecoder.class))
|
||||
.isInstanceOf(ErrorDecoder.Default.class);
|
||||
assertThat(this.context.getInstance("bar", ErrorDecoder.class)).isNull();
|
||||
}
|
||||
|
||||
@@ -86,12 +86,12 @@ public class FeignClientErrorDecoderTests {
|
||||
|
||||
@SuppressWarnings({ "unchecked", "ConstantConditions" })
|
||||
private Object getErrorDecoderFromClient(final Object client) {
|
||||
Object invocationHandlerLambda = ReflectionTestUtils.getField(client, "h");
|
||||
Object invocationHandler = ReflectionTestUtils.getField(invocationHandlerLambda, "arg$2");
|
||||
Object invocationHandler = ReflectionTestUtils.getField(client, "h");
|
||||
Map<Method, InvocationHandlerFactory.MethodHandler> dispatch = (Map<Method, InvocationHandlerFactory.MethodHandler>) ReflectionTestUtils
|
||||
.getField(invocationHandler, "dispatch");
|
||||
Method key = new ArrayList<>(dispatch.keySet()).get(0);
|
||||
return ReflectionTestUtils.getField(ReflectionTestUtils.getField(dispatch.get(key), "asyncResponseHandler"),
|
||||
return ReflectionTestUtils.getField(
|
||||
ReflectionTestUtils.getField(dispatch.get(key), "asyncResponseHandler"),
|
||||
"errorDecoder");
|
||||
}
|
||||
|
||||
|
||||
+8
-6
@@ -29,7 +29,6 @@ import org.junit.Test;
|
||||
|
||||
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
|
||||
import org.springframework.cloud.loadbalancer.blocking.client.BlockingLoadBalancerClient;
|
||||
import org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration;
|
||||
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||
@@ -53,7 +52,8 @@ public class FeignClientFactoryTests {
|
||||
parent.refresh();
|
||||
FeignContext context = new FeignContext();
|
||||
context.setApplicationContext(parent);
|
||||
context.setConfigurations(Arrays.asList(getSpec("foo", FooConfig.class), getSpec("bar", BarConfig.class)));
|
||||
context.setConfigurations(Arrays.asList(getSpec("foo", FooConfig.class),
|
||||
getSpec("bar", BarConfig.class)));
|
||||
|
||||
Foo foo = context.getInstance("foo", Foo.class);
|
||||
assertThat(foo).as("foo was null").isNotNull();
|
||||
@@ -67,7 +67,8 @@ public class FeignClientFactoryTests {
|
||||
|
||||
@Test
|
||||
public void shouldRedirectToDelegateWhenUrlSet() {
|
||||
new ApplicationContextRunner().withUserConfiguration(TestConfig.class).run(this::defaultClientUsed);
|
||||
new ApplicationContextRunner().withUserConfiguration(TestConfig.class)
|
||||
.run(this::defaultClientUsed);
|
||||
}
|
||||
|
||||
@SuppressWarnings({ "unchecked", "ConstantConditions" })
|
||||
@@ -97,14 +98,15 @@ public class FeignClientFactoryTests {
|
||||
|
||||
@Bean
|
||||
BlockingLoadBalancerClient loadBalancerClient() {
|
||||
return new BlockingLoadBalancerClient(new LoadBalancerClientFactory(), new LoadBalancerProperties());
|
||||
return new BlockingLoadBalancerClient(new LoadBalancerClientFactory());
|
||||
}
|
||||
|
||||
@Bean
|
||||
FeignContext feignContext() {
|
||||
FeignContext feignContext = new FeignContext();
|
||||
feignContext.setConfigurations(Collections.singletonList(
|
||||
new FeignClientSpecification("test", new Class[] { LoadBalancerAutoConfiguration.class })));
|
||||
feignContext.setConfigurations(
|
||||
Collections.singletonList(new FeignClientSpecification("test",
|
||||
new Class[] { LoadBalancerAutoConfiguration.class })));
|
||||
return feignContext;
|
||||
}
|
||||
|
||||
|
||||
-225
@@ -1,225 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021-2021 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.util.HashMap;
|
||||
|
||||
import org.assertj.core.util.Maps;
|
||||
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.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.context.annotation.ConditionContext;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.type.AnnotatedTypeMetadata;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* @author Jonatan Ivanov
|
||||
*/
|
||||
@ExtendWith({ MockitoExtension.class })
|
||||
class FeignClientMetricsEnabledConditionTests {
|
||||
|
||||
@Mock
|
||||
private ConditionContext context;
|
||||
|
||||
@Mock
|
||||
private AnnotatedTypeMetadata metadata;
|
||||
|
||||
@Mock
|
||||
private ConfigurableListableBeanFactory beanFactory;
|
||||
|
||||
@Mock
|
||||
private ObjectProvider<FeignClientProperties> beanProvider;
|
||||
|
||||
@Mock
|
||||
private Environment environment;
|
||||
|
||||
private final FeignClientMetricsEnabledCondition condition = new FeignClientMetricsEnabledCondition();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
when(context.getBeanFactory()).thenReturn(beanFactory);
|
||||
when(beanFactory.getBeanProvider(FeignClientProperties.class)).thenReturn(beanProvider);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
verify(context).getBeanFactory();
|
||||
verify(beanFactory).getBeanProvider(FeignClientProperties.class);
|
||||
verify(beanProvider).getIfAvailable();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchWhenFeignClientPropertiesBeanIsMissing() {
|
||||
when(beanProvider.getIfAvailable()).thenReturn(null);
|
||||
|
||||
assertThat(condition.matches(context, metadata)).isTrue();
|
||||
verify(environment, never()).getProperty("feign.client.name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchWhenConfigMapIsMissing() {
|
||||
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
|
||||
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
|
||||
when(feignClientProperties.getConfig()).thenReturn(null);
|
||||
|
||||
assertThat(condition.matches(context, metadata)).isTrue();
|
||||
verify(environment, never()).getProperty("feign.client.name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchWhenConfigMapDoesNotContainTheConfig() {
|
||||
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
|
||||
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
|
||||
when(context.getEnvironment()).thenReturn(environment);
|
||||
when(environment.getProperty("feign.client.name")).thenReturn("foo");
|
||||
when(feignClientProperties.getConfig()).thenReturn(new HashMap<>());
|
||||
|
||||
assertThat(condition.matches(context, metadata)).isTrue();
|
||||
verify(environment).getProperty("feign.client.name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchWhenClientNameIsNull() {
|
||||
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
|
||||
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
|
||||
when(context.getEnvironment()).thenReturn(environment);
|
||||
when(environment.getProperty("feign.client.name")).thenReturn(null);
|
||||
when(feignClientProperties.getConfig()).thenReturn(new HashMap<>());
|
||||
|
||||
assertThat(condition.matches(context, metadata)).isTrue();
|
||||
verify(environment).getProperty("feign.client.name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchWhenClientNameIsEmpty() {
|
||||
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
|
||||
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
|
||||
when(context.getEnvironment()).thenReturn(environment);
|
||||
when(environment.getProperty("feign.client.name")).thenReturn("");
|
||||
when(feignClientProperties.getConfig()).thenReturn(new HashMap<>());
|
||||
|
||||
assertThat(condition.matches(context, metadata)).isTrue();
|
||||
|
||||
verify(environment).getProperty("feign.client.name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchWhenConfigMapContainsNullConfig() {
|
||||
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
|
||||
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
|
||||
when(context.getEnvironment()).thenReturn(environment);
|
||||
when(environment.getProperty("feign.client.name")).thenReturn("foo");
|
||||
when(feignClientProperties.getConfig()).thenReturn(Maps.newHashMap("foo", null));
|
||||
|
||||
assertThat(condition.matches(context, metadata)).isTrue();
|
||||
verify(environment).getProperty("feign.client.name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchWhenMetricsConfigurationIsMissing() {
|
||||
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
|
||||
FeignClientProperties.FeignClientConfiguration feignClientConfig = mock(
|
||||
FeignClientProperties.FeignClientConfiguration.class);
|
||||
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
|
||||
when(context.getEnvironment()).thenReturn(environment);
|
||||
when(environment.getProperty("feign.client.name")).thenReturn("foo");
|
||||
when(feignClientProperties.getConfig()).thenReturn(Maps.newHashMap("foo", feignClientConfig));
|
||||
when(feignClientConfig.getMetrics()).thenReturn(null);
|
||||
|
||||
assertThat(condition.matches(context, metadata)).isTrue();
|
||||
verify(environment).getProperty("feign.client.name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchWhenEnabledFlagIsNotSet() {
|
||||
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
|
||||
FeignClientProperties.FeignClientConfiguration feignClientConfig = mock(
|
||||
FeignClientProperties.FeignClientConfiguration.class);
|
||||
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
|
||||
when(context.getEnvironment()).thenReturn(environment);
|
||||
when(environment.getProperty("feign.client.name")).thenReturn("foo");
|
||||
when(feignClientProperties.getConfig()).thenReturn(Maps.newHashMap("foo", feignClientConfig));
|
||||
when(feignClientConfig.getMetrics()).thenReturn(new FeignClientProperties.MetricsProperties());
|
||||
|
||||
assertThat(condition.matches(context, metadata)).isTrue();
|
||||
verify(environment).getProperty("feign.client.name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchWhenEnabledFlagIsNull() {
|
||||
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
|
||||
FeignClientProperties.FeignClientConfiguration feignClientConfig = mock(
|
||||
FeignClientProperties.FeignClientConfiguration.class);
|
||||
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
|
||||
when(context.getEnvironment()).thenReturn(environment);
|
||||
when(environment.getProperty("feign.client.name")).thenReturn("foo");
|
||||
when(feignClientProperties.getConfig()).thenReturn(Maps.newHashMap("foo", feignClientConfig));
|
||||
FeignClientProperties.MetricsProperties metricsProperties = new FeignClientProperties.MetricsProperties();
|
||||
metricsProperties.setEnabled(null);
|
||||
when(feignClientConfig.getMetrics()).thenReturn(metricsProperties);
|
||||
|
||||
assertThat(condition.matches(context, metadata)).isTrue();
|
||||
verify(environment).getProperty("feign.client.name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchWhenMetricsConfigurationIsEnabled() {
|
||||
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
|
||||
FeignClientProperties.FeignClientConfiguration feignClientConfig = mock(
|
||||
FeignClientProperties.FeignClientConfiguration.class);
|
||||
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
|
||||
when(context.getEnvironment()).thenReturn(environment);
|
||||
when(environment.getProperty("feign.client.name")).thenReturn("foo");
|
||||
when(feignClientProperties.getConfig()).thenReturn(Maps.newHashMap("foo", feignClientConfig));
|
||||
FeignClientProperties.MetricsProperties metricsProperties = new FeignClientProperties.MetricsProperties();
|
||||
metricsProperties.setEnabled(true);
|
||||
when(feignClientConfig.getMetrics()).thenReturn(metricsProperties);
|
||||
|
||||
assertThat(condition.matches(context, metadata)).isTrue();
|
||||
verify(environment).getProperty("feign.client.name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotMatchWhenMetricsConfigurationIsEnabled() {
|
||||
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
|
||||
FeignClientProperties.FeignClientConfiguration feignClientConfig = mock(
|
||||
FeignClientProperties.FeignClientConfiguration.class);
|
||||
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
|
||||
when(context.getEnvironment()).thenReturn(environment);
|
||||
when(environment.getProperty("feign.client.name")).thenReturn("foo");
|
||||
when(feignClientProperties.getConfig()).thenReturn(Maps.newHashMap("foo", feignClientConfig));
|
||||
FeignClientProperties.MetricsProperties metricsProperties = new FeignClientProperties.MetricsProperties();
|
||||
metricsProperties.setEnabled(false);
|
||||
when(feignClientConfig.getMetrics()).thenReturn(metricsProperties);
|
||||
|
||||
assertThat(condition.matches(context, metadata)).isFalse();
|
||||
verify(environment).getProperty("feign.client.name");
|
||||
}
|
||||
|
||||
}
|
||||
+36
-49
@@ -16,12 +16,11 @@
|
||||
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import feign.Capability;
|
||||
import feign.Contract;
|
||||
import feign.ExceptionPropagationPolicy;
|
||||
import feign.Feign;
|
||||
import feign.Logger;
|
||||
import feign.QueryMapEncoder;
|
||||
import feign.Request;
|
||||
@@ -32,19 +31,21 @@ import feign.auth.BasicAuthRequestInterceptor;
|
||||
import feign.codec.Decoder;
|
||||
import feign.codec.Encoder;
|
||||
import feign.codec.ErrorDecoder;
|
||||
import feign.micrometer.MicrometerCapability;
|
||||
import feign.hystrix.HystrixFeign;
|
||||
import feign.optionals.OptionalDecoder;
|
||||
import feign.querymap.BeanQueryMapEncoder;
|
||||
import feign.slf4j.Slf4jLogger;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.netflix.archaius.ArchaiusAutoConfiguration;
|
||||
import org.springframework.cloud.openfeign.support.PageableSpringEncoder;
|
||||
import org.springframework.cloud.openfeign.support.SpringMvcContract;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@@ -52,7 +53,6 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
* @author Jonatan Ivanov
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@SpringBootTest(classes = FeignClientOverrideDefaultsTests.TestConfiguration.class)
|
||||
@@ -101,12 +101,14 @@ class FeignClientOverrideDefaultsTests {
|
||||
@Test
|
||||
void overrideLoggerLevel() {
|
||||
assertThat(context.getInstance("foo", Logger.Level.class)).isNull();
|
||||
assertThat(context.getInstance("bar", Logger.Level.class)).isEqualTo(Logger.Level.HEADERS);
|
||||
assertThat(context.getInstance("bar", Logger.Level.class))
|
||||
.isEqualTo(Logger.Level.HEADERS);
|
||||
}
|
||||
|
||||
@Test
|
||||
void overrideRetryer() {
|
||||
assertThat(context.getInstance("foo", Retryer.class)).isEqualTo(Retryer.NEVER_RETRY);
|
||||
assertThat(context.getInstance("foo", Retryer.class))
|
||||
.isEqualTo(Retryer.NEVER_RETRY);
|
||||
Retryer.Default.class.cast(context.getInstance("bar", Retryer.class));
|
||||
}
|
||||
|
||||
@@ -116,6 +118,12 @@ class FeignClientOverrideDefaultsTests {
|
||||
ErrorDecoder.Default.class.cast(context.getInstance("bar", ErrorDecoder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void overrideBuilder() {
|
||||
HystrixFeign.Builder.class.cast(context.getInstance("foo", Feign.Builder.class));
|
||||
Feign.Builder.class.cast(context.getInstance("bar", Feign.Builder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void overrideRequestOptions() {
|
||||
assertThat(context.getInstance("foo", Request.Options.class)).isNull();
|
||||
@@ -127,40 +135,29 @@ class FeignClientOverrideDefaultsTests {
|
||||
|
||||
@Test
|
||||
void overrideQueryMapEncoder() {
|
||||
QueryMapEncoder.Default.class.cast(context.getInstance("foo", QueryMapEncoder.class));
|
||||
QueryMapEncoder.Default.class
|
||||
.cast(context.getInstance("foo", QueryMapEncoder.class));
|
||||
BeanQueryMapEncoder.class.cast(context.getInstance("bar", QueryMapEncoder.class));
|
||||
}
|
||||
|
||||
@Test
|
||||
void addRequestInterceptor() {
|
||||
assertThat(context.getInstances("foo", RequestInterceptor.class).size()).isEqualTo(1);
|
||||
assertThat(context.getInstances("bar", RequestInterceptor.class).size()).isEqualTo(2);
|
||||
assertThat(context.getInstances("foo", RequestInterceptor.class).size())
|
||||
.isEqualTo(1);
|
||||
assertThat(context.getInstances("bar", RequestInterceptor.class).size())
|
||||
.isEqualTo(2);
|
||||
}
|
||||
|
||||
@Test
|
||||
void exceptionPropagationPolicy() {
|
||||
assertThat(context.getInstances("foo", ExceptionPropagationPolicy.class)).isEmpty();
|
||||
assertThat(context.getInstances("foo", ExceptionPropagationPolicy.class))
|
||||
.isEmpty();
|
||||
assertThat(context.getInstances("bar", ExceptionPropagationPolicy.class))
|
||||
.containsValues(ExceptionPropagationPolicy.UNWRAP);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldOverrideMicrometerCapability() {
|
||||
assertThat(context.getInstance("foo", MicrometerCapability.class))
|
||||
.isExactlyInstanceOf(TestMicrometerCapability.class);
|
||||
Map<String, Capability> fooCapabilities = context.getInstances("foo", Capability.class);
|
||||
assertThat(fooCapabilities).hasSize(1);
|
||||
assertThat(fooCapabilities.get("micrometerCapability")).isExactlyInstanceOf(TestMicrometerCapability.class);
|
||||
|
||||
assertThat(context.getInstance("bar", MicrometerCapability.class))
|
||||
.isExactlyInstanceOf(TestMicrometerCapability.class);
|
||||
Map<String, Capability> barCapabilities = context.getInstances("bar", Capability.class);
|
||||
assertThat(barCapabilities).hasSize(2);
|
||||
assertThat(barCapabilities.get("micrometerCapability")).isExactlyInstanceOf(TestMicrometerCapability.class);
|
||||
assertThat(barCapabilities.get("noOpCapability")).isExactlyInstanceOf(NoOpCapability.class);
|
||||
}
|
||||
|
||||
@FeignClient(name = "foo", url = "https://foo", configuration = FooConfiguration.class)
|
||||
@FeignClient(name = "foo", url = "https://foo",
|
||||
configuration = FooConfiguration.class)
|
||||
interface FooClient {
|
||||
|
||||
@RequestLine("GET /")
|
||||
@@ -168,7 +165,8 @@ class FeignClientOverrideDefaultsTests {
|
||||
|
||||
}
|
||||
|
||||
@FeignClient(name = "bar", url = "https://bar", configuration = BarConfiguration.class)
|
||||
@FeignClient(name = "bar", url = "https://bar",
|
||||
configuration = BarConfiguration.class)
|
||||
interface BarClient {
|
||||
|
||||
@GetMapping("/")
|
||||
@@ -178,7 +176,8 @@ class FeignClientOverrideDefaultsTests {
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableFeignClients(clients = { FooClient.class, BarClient.class })
|
||||
@EnableAutoConfiguration
|
||||
@Import({ PropertyPlaceholderAutoConfiguration.class, ArchaiusAutoConfiguration.class,
|
||||
FeignAutoConfiguration.class })
|
||||
protected static class TestConfiguration {
|
||||
|
||||
@Bean
|
||||
@@ -187,11 +186,6 @@ class FeignClientOverrideDefaultsTests {
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
MicrometerCapability micrometerCapability() {
|
||||
return new TestMicrometerCapability();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class FooConfiguration {
|
||||
@@ -216,6 +210,11 @@ class FeignClientOverrideDefaultsTests {
|
||||
return new Contract.Default();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Feign.Builder feignBuilder() {
|
||||
return HystrixFeign.builder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public QueryMapEncoder queryMapEncoder() {
|
||||
return new feign.QueryMapEncoder.Default();
|
||||
@@ -242,7 +241,8 @@ class FeignClientOverrideDefaultsTests {
|
||||
|
||||
@Bean
|
||||
Request.Options feignRequestOptions() {
|
||||
return new Request.Options(1, TimeUnit.MILLISECONDS, 1, TimeUnit.MILLISECONDS, false);
|
||||
return new Request.Options(1, TimeUnit.MILLISECONDS, 1, TimeUnit.MILLISECONDS,
|
||||
false);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -260,19 +260,6 @@ class FeignClientOverrideDefaultsTests {
|
||||
return ExceptionPropagationPolicy.UNWRAP;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Capability noOpCapability() {
|
||||
return new NoOpCapability();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class TestMicrometerCapability extends feign.micrometer.MicrometerCapability {
|
||||
|
||||
}
|
||||
|
||||
private static class NoOpCapability implements Capability {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
-90
@@ -1,90 +0,0 @@
|
||||
/*
|
||||
* Copyright 2021-2021 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.util.Map;
|
||||
|
||||
import org.assertj.core.util.Maps;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsAndHashCodeConsistency;
|
||||
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsConsistency;
|
||||
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsReflexivity;
|
||||
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsSymmetricity;
|
||||
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsTransitivity;
|
||||
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertHashCodeConsistency;
|
||||
|
||||
/**
|
||||
* @author Jonatan Ivanov
|
||||
*/
|
||||
class FeignClientPropertiesTests {
|
||||
|
||||
@Test
|
||||
void shouldDefaultToValuesWhenFieldsNotSet() {
|
||||
FeignClientProperties properties = new FeignClientProperties();
|
||||
assertThat(properties.isDefaultToProperties()).isTrue();
|
||||
assertThat(properties.getDefaultConfig()).isEqualTo("default");
|
||||
assertThat(properties.getConfig()).isEmpty();
|
||||
assertThat(properties.isDecodeSlash()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnValuesWhenSet() {
|
||||
FeignClientProperties properties = new FeignClientProperties();
|
||||
properties.setDefaultToProperties(false);
|
||||
properties.setDefaultConfig("custom");
|
||||
Map<String, FeignClientProperties.FeignClientConfiguration> configMap = Maps.newHashMap("foo", null);
|
||||
properties.setConfig(configMap);
|
||||
properties.setDecodeSlash(false);
|
||||
|
||||
assertThat(properties.isDefaultToProperties()).isFalse();
|
||||
assertThat(properties.getDefaultConfig()).isEqualTo("custom");
|
||||
assertThat(properties.getConfig()).isSameAs(configMap);
|
||||
assertThat(properties.isDecodeSlash()).isFalse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanity-checks equals and hashCode contracts but does not check every variation of
|
||||
* the fields.
|
||||
*/
|
||||
@Test
|
||||
void shouldHaveSomewhatValidEqualsAndHashCode() {
|
||||
FeignClientProperties propsOne = new FeignClientProperties();
|
||||
FeignClientProperties propsTwo = new FeignClientProperties();
|
||||
FeignClientProperties propsThree = new FeignClientProperties();
|
||||
FeignClientProperties differentProps = new FeignClientProperties();
|
||||
differentProps.setDecodeSlash(false);
|
||||
|
||||
assertEqualsReflexivity(propsOne);
|
||||
|
||||
assertEqualsSymmetricity(propsOne, propsTwo);
|
||||
assertEqualsSymmetricity(propsOne, differentProps);
|
||||
assertEqualsSymmetricity(propsOne, 42);
|
||||
|
||||
assertEqualsTransitivity(propsOne, propsTwo, propsThree);
|
||||
|
||||
assertEqualsConsistency(propsOne, propsTwo);
|
||||
assertEqualsConsistency(propsOne, differentProps);
|
||||
assertEqualsConsistency(propsOne, 42);
|
||||
assertEqualsConsistency(propsOne, null);
|
||||
|
||||
assertHashCodeConsistency(propsOne);
|
||||
assertEqualsAndHashCodeConsistency(propsOne, propsTwo);
|
||||
}
|
||||
|
||||
}
|
||||
+13
-35
@@ -19,11 +19,9 @@ 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.MicrometerCapability;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@@ -42,7 +40,6 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author matt king
|
||||
* @author Jonatan Ivanov
|
||||
*/
|
||||
@DirtiesContext
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@@ -67,13 +64,11 @@ public class FeignClientUsingConfigurerTest {
|
||||
.getAttribute("feignClientsRegistrarFactoryBean");
|
||||
Feign.Builder builder = factoryBean.feign(context);
|
||||
|
||||
List<RequestInterceptor> interceptors = (List) getBuilderValue(builder, "requestInterceptors");
|
||||
List<RequestInterceptor> 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<Capability> capabilities = (List) getBuilderValue(builder, "capabilities");
|
||||
assertThat(capabilities).hasSize(2).hasAtLeastOneElementOfType(NoOpCapability.class)
|
||||
.hasAtLeastOneElementOfType(MicrometerCapability.class);
|
||||
assertThat(getBuilderValue(builder, "logLevel")).as("log level not set")
|
||||
.isEqualTo(Logger.Level.FULL);
|
||||
}
|
||||
|
||||
private Object getBuilderValue(Feign.Builder builder, String member) {
|
||||
@@ -90,13 +85,12 @@ public class FeignClientUsingConfigurerTest {
|
||||
.getAttribute("feignClientsRegistrarFactoryBean");
|
||||
Feign.Builder builder = factoryBean.feign(context);
|
||||
|
||||
List<RequestInterceptor> interceptors = (List) getBuilderValue(builder, "requestInterceptors");
|
||||
assertThat(interceptors).as("interceptors not set").isEmpty();
|
||||
assertThat(factoryBean.isInheritParentContext()).as("is inheriting from parent configuration").isFalse();
|
||||
List<RequestInterceptor> interceptors = (List) getBuilderValue(builder,
|
||||
"requestInterceptors");
|
||||
|
||||
List<Capability> capabilities = (List) getBuilderValue(builder, "capabilities");
|
||||
assertThat(capabilities).hasSize(2).hasAtLeastOneElementOfType(NoOpCapability.class)
|
||||
.hasAtLeastOneElementOfType(MicrometerCapability.class);
|
||||
assertThat(interceptors).as("interceptors not set").isEmpty();
|
||||
assertThat(factoryBean.isInheritParentContext())
|
||||
.as("is inheriting from parent configuration").isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -106,11 +100,8 @@ public class FeignClientUsingConfigurerTest {
|
||||
.getAttribute("feignClientsRegistrarFactoryBean");
|
||||
Feign.Builder builder = factoryBean.feign(context);
|
||||
|
||||
assertThat(getBuilderValue(builder, "logLevel")).as("log level not set").isEqualTo(Logger.Level.HEADERS);
|
||||
|
||||
List<Capability> capabilities = (List) getBuilderValue(builder, "capabilities");
|
||||
assertThat(capabilities).hasSize(2).hasAtLeastOneElementOfType(NoOpCapability.class)
|
||||
.hasAtLeastOneElementOfType(MicrometerCapability.class);
|
||||
assertThat(getBuilderValue(builder, "logLevel")).as("log level not set")
|
||||
.isEqualTo(Logger.Level.HEADERS);
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@@ -124,11 +115,6 @@ public class FeignClientUsingConfigurerTest {
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public NoOpCapability noOpCapability() {
|
||||
return new NoOpCapability();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class NoInheritConfiguration {
|
||||
@@ -138,11 +124,6 @@ public class FeignClientUsingConfigurerTest {
|
||||
return Logger.Level.HEADERS;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public NoOpCapability noOpCapability() {
|
||||
return new NoOpCapability();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FeignClientConfigurer feignClientConfigurer() {
|
||||
return new FeignClientConfigurer() {
|
||||
@@ -162,13 +143,10 @@ public class FeignClientUsingConfigurerTest {
|
||||
|
||||
}
|
||||
|
||||
@FeignClient(name = "noInheritFeignClient", configuration = NoInheritConfiguration.class)
|
||||
@FeignClient(name = "noInheritFeignClient",
|
||||
configuration = NoInheritConfiguration.class)
|
||||
interface NoInheritFeignClient {
|
||||
|
||||
}
|
||||
|
||||
private static class NoOpCapability implements Capability {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+51
-52
@@ -34,8 +34,6 @@ import java.util.stream.Stream;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import feign.Capability;
|
||||
import feign.Feign;
|
||||
import feign.InvocationHandlerFactory;
|
||||
import feign.Request;
|
||||
import feign.RequestInterceptor;
|
||||
@@ -45,7 +43,6 @@ import feign.Retryer;
|
||||
import feign.codec.EncodeException;
|
||||
import feign.codec.Encoder;
|
||||
import feign.codec.ErrorDecoder;
|
||||
import feign.micrometer.MicrometerCapability;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
@@ -79,11 +76,11 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
|
||||
* @author Eko Kurniawan Khannedy
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Ilia Ilinykh
|
||||
* @author Jonatan Ivanov
|
||||
*/
|
||||
@SuppressWarnings("FieldMayBeFinal")
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringBootTest(classes = FeignClientUsingPropertiesTests.Application.class, webEnvironment = RANDOM_PORT)
|
||||
@SpringBootTest(classes = FeignClientUsingPropertiesTests.Application.class,
|
||||
webEnvironment = RANDOM_PORT)
|
||||
@TestPropertySource("classpath:feign-properties.properties")
|
||||
@DirtiesContext
|
||||
public class FeignClientUsingPropertiesTests {
|
||||
@@ -127,32 +124,40 @@ public class FeignClientUsingPropertiesTests {
|
||||
formFactoryBean.setType(FeignClientFactoryBean.class);
|
||||
|
||||
this.defaultHeadersAndQuerySingleParamsFeignClientFactoryBean = new FeignClientFactoryBean();
|
||||
this.defaultHeadersAndQuerySingleParamsFeignClientFactoryBean.setContextId("singleValue");
|
||||
this.defaultHeadersAndQuerySingleParamsFeignClientFactoryBean.setType(FeignClientFactoryBean.class);
|
||||
this.defaultHeadersAndQuerySingleParamsFeignClientFactoryBean
|
||||
.setContextId("singleValue");
|
||||
this.defaultHeadersAndQuerySingleParamsFeignClientFactoryBean
|
||||
.setType(FeignClientFactoryBean.class);
|
||||
|
||||
this.defaultHeadersAndQueryMultipleParamsFeignClientFactoryBean = new FeignClientFactoryBean();
|
||||
this.defaultHeadersAndQueryMultipleParamsFeignClientFactoryBean.setContextId("multipleValue");
|
||||
this.defaultHeadersAndQueryMultipleParamsFeignClientFactoryBean.setType(FeignClientFactoryBean.class);
|
||||
this.defaultHeadersAndQueryMultipleParamsFeignClientFactoryBean
|
||||
.setContextId("multipleValue");
|
||||
this.defaultHeadersAndQueryMultipleParamsFeignClientFactoryBean
|
||||
.setType(FeignClientFactoryBean.class);
|
||||
}
|
||||
|
||||
public FooClient fooClient() {
|
||||
fooFactoryBean.setApplicationContext(applicationContext);
|
||||
return fooFactoryBean.feign(context).target(FooClient.class, "http://localhost:" + port);
|
||||
return fooFactoryBean.feign(context).target(FooClient.class,
|
||||
"http://localhost:" + port);
|
||||
}
|
||||
|
||||
public BarClient barClient() {
|
||||
barFactoryBean.setApplicationContext(applicationContext);
|
||||
return barFactoryBean.feign(context).target(BarClient.class, "http://localhost:" + port);
|
||||
return barFactoryBean.feign(context).target(BarClient.class,
|
||||
"http://localhost:" + port);
|
||||
}
|
||||
|
||||
public UnwrapClient unwrapClient() {
|
||||
unwrapFactoryBean.setApplicationContext(applicationContext);
|
||||
return unwrapFactoryBean.feign(context).target(UnwrapClient.class, "http://localhost:" + port);
|
||||
return unwrapFactoryBean.feign(context).target(UnwrapClient.class,
|
||||
"http://localhost:" + port);
|
||||
}
|
||||
|
||||
public FormClient formClient() {
|
||||
formFactoryBean.setApplicationContext(applicationContext);
|
||||
return formFactoryBean.feign(context).target(FormClient.class, "http://localhost:" + port);
|
||||
return formFactoryBean.feign(context).target(FormClient.class,
|
||||
"http://localhost:" + port);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -189,18 +194,23 @@ public class FeignClientUsingPropertiesTests {
|
||||
@Test
|
||||
public void testMultipleValue() {
|
||||
List<String> response = multipleValueClient().multipleValue();
|
||||
assertThat(response).isEqualTo(Arrays.asList("header1", "header2", "parameter1", "parameter2"));
|
||||
assertThat(response).isEqualTo(
|
||||
Arrays.asList("header1", "header2", "parameter1", "parameter2"));
|
||||
}
|
||||
|
||||
public SingleValueClient singleValueClient() {
|
||||
this.defaultHeadersAndQuerySingleParamsFeignClientFactoryBean.setApplicationContext(this.applicationContext);
|
||||
return this.defaultHeadersAndQuerySingleParamsFeignClientFactoryBean.feign(this.context)
|
||||
this.defaultHeadersAndQuerySingleParamsFeignClientFactoryBean
|
||||
.setApplicationContext(this.applicationContext);
|
||||
return this.defaultHeadersAndQuerySingleParamsFeignClientFactoryBean
|
||||
.feign(this.context)
|
||||
.target(SingleValueClient.class, "http://localhost:" + this.port);
|
||||
}
|
||||
|
||||
public MultipleValueClient multipleValueClient() {
|
||||
this.defaultHeadersAndQueryMultipleParamsFeignClientFactoryBean.setApplicationContext(this.applicationContext);
|
||||
return this.defaultHeadersAndQueryMultipleParamsFeignClientFactoryBean.feign(this.context)
|
||||
this.defaultHeadersAndQueryMultipleParamsFeignClientFactoryBean
|
||||
.setApplicationContext(this.applicationContext);
|
||||
return this.defaultHeadersAndQueryMultipleParamsFeignClientFactoryBean
|
||||
.feign(this.context)
|
||||
.target(MultipleValueClient.class, "http://localhost:" + this.port);
|
||||
}
|
||||
|
||||
@@ -211,8 +221,8 @@ public class FeignClientUsingPropertiesTests {
|
||||
readTimeoutFactoryBean.setType(FeignClientFactoryBean.class);
|
||||
readTimeoutFactoryBean.setApplicationContext(applicationContext);
|
||||
|
||||
TimeoutClient client = readTimeoutFactoryBean.feign(context).target(TimeoutClient.class,
|
||||
"http://localhost:" + port);
|
||||
TimeoutClient client = readTimeoutFactoryBean.feign(context)
|
||||
.target(TimeoutClient.class, "http://localhost:" + port);
|
||||
|
||||
Request.Options options = getRequestOptions((Proxy) client);
|
||||
|
||||
@@ -227,8 +237,8 @@ public class FeignClientUsingPropertiesTests {
|
||||
readTimeoutFactoryBean.setType(FeignClientFactoryBean.class);
|
||||
readTimeoutFactoryBean.setApplicationContext(applicationContext);
|
||||
|
||||
TimeoutClient client = readTimeoutFactoryBean.feign(context).target(TimeoutClient.class,
|
||||
"http://localhost:" + port);
|
||||
TimeoutClient client = readTimeoutFactoryBean.feign(context)
|
||||
.target(TimeoutClient.class, "http://localhost:" + port);
|
||||
|
||||
Request.Options options = getRequestOptions((Proxy) client);
|
||||
|
||||
@@ -236,19 +246,6 @@ public class FeignClientUsingPropertiesTests {
|
||||
assertThat(options.readTimeoutMillis()).isEqualTo(5000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clientShouldContainCapabilities() {
|
||||
fooFactoryBean.setApplicationContext(applicationContext);
|
||||
Feign.Builder feignBuilder = fooFactoryBean.feign(context);
|
||||
FooClient fooClient = feignBuilder.target(FooClient.class, "http://localhost:" + port);
|
||||
|
||||
String response = fooClient.foo();
|
||||
assertThat(response).isEqualTo("OK");
|
||||
List<Capability> capabilities = (List) ReflectionTestUtils.getField(feignBuilder, "capabilities");
|
||||
assertThat(capabilities).hasSize(2).hasAtLeastOneElementOfType(NoOpCapability.class)
|
||||
.hasAtLeastOneElementOfType(MicrometerCapability.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldSetFollowRedirects() {
|
||||
FeignClientFactoryBean testFactoryBean = new FeignClientFactoryBean();
|
||||
@@ -256,7 +253,8 @@ public class FeignClientUsingPropertiesTests {
|
||||
testFactoryBean.setType(FeignClientFactoryBean.class);
|
||||
testFactoryBean.setApplicationContext(applicationContext);
|
||||
|
||||
TimeoutClient client = testFactoryBean.feign(context).target(TimeoutClient.class, "http://localhost:" + port);
|
||||
TimeoutClient client = testFactoryBean.feign(context).target(TimeoutClient.class,
|
||||
"http://localhost:" + port);
|
||||
|
||||
Request.Options options = getRequestOptions((Proxy) client);
|
||||
|
||||
@@ -264,12 +262,12 @@ public class FeignClientUsingPropertiesTests {
|
||||
}
|
||||
|
||||
private Request.Options getRequestOptions(Proxy client) {
|
||||
Object invocationHandlerLambda = ReflectionTestUtils.getField(client, "h");
|
||||
Object invocationHandler = ReflectionTestUtils.getField(invocationHandlerLambda, "arg$2");
|
||||
Object invocationHandler = ReflectionTestUtils.getField(client, "h");
|
||||
Map<Method, InvocationHandlerFactory.MethodHandler> dispatch = (Map<Method, InvocationHandlerFactory.MethodHandler>) ReflectionTestUtils
|
||||
.getField(Objects.requireNonNull(invocationHandler), "dispatch");
|
||||
Method key = new ArrayList<>(dispatch.keySet()).get(0);
|
||||
return (Request.Options) ReflectionTestUtils.getField(dispatch.get(key), "options");
|
||||
return (Request.Options) ReflectionTestUtils.getField(dispatch.get(key),
|
||||
"options");
|
||||
}
|
||||
|
||||
protected interface FooClient {
|
||||
@@ -330,7 +328,8 @@ public class FeignClientUsingPropertiesTests {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/foo")
|
||||
public String foo(HttpServletRequest request) throws IllegalAccessException {
|
||||
if ("Foo".equals(request.getHeader("Foo")) && "Bar".equals(request.getHeader("Bar"))) {
|
||||
if ("Foo".equals(request.getHeader("Foo"))
|
||||
&& "Bar".equals(request.getHeader("Bar"))) {
|
||||
return "OK";
|
||||
}
|
||||
else {
|
||||
@@ -344,7 +343,8 @@ public class FeignClientUsingPropertiesTests {
|
||||
return "OK";
|
||||
}
|
||||
|
||||
@PostMapping(path = "/form", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
@PostMapping(path = "/form",
|
||||
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
public String form(HttpServletRequest request) {
|
||||
return request.getParameter("form");
|
||||
}
|
||||
@@ -352,15 +352,16 @@ public class FeignClientUsingPropertiesTests {
|
||||
@GetMapping(path = "/singleValue")
|
||||
public List<String> singleValue(@RequestHeader List<String> singleValueHeaders,
|
||||
@RequestParam List<String> singleValueParameters) {
|
||||
return Stream.of(singleValueHeaders, singleValueParameters).flatMap(Collection::stream)
|
||||
.collect(Collectors.toList());
|
||||
return Stream.of(singleValueHeaders, singleValueParameters)
|
||||
.flatMap(Collection::stream).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
@GetMapping(path = "/multipleValue")
|
||||
public List<String> multipleValue(@RequestHeader List<String> multipleValueHeaders,
|
||||
public List<String> multipleValue(
|
||||
@RequestHeader List<String> multipleValueHeaders,
|
||||
@RequestParam List<String> multipleValueParameters) {
|
||||
return Stream.of(multipleValueHeaders, multipleValueParameters).flatMap(Collection::stream)
|
||||
.collect(Collectors.toList());
|
||||
return Stream.of(multipleValueHeaders, multipleValueParameters)
|
||||
.flatMap(Collection::stream).collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
@@ -404,21 +405,19 @@ public class FeignClientUsingPropertiesTests {
|
||||
public static class FormEncoder implements Encoder {
|
||||
|
||||
@Override
|
||||
public void encode(Object o, Type type, RequestTemplate requestTemplate) throws EncodeException {
|
||||
public void encode(Object o, Type type, RequestTemplate requestTemplate)
|
||||
throws EncodeException {
|
||||
Map<String, String> form = (Map<String, String>) o;
|
||||
StringBuilder builder = new StringBuilder();
|
||||
form.forEach((key, value) -> {
|
||||
builder.append(key + "=" + value + "&");
|
||||
});
|
||||
|
||||
requestTemplate.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE);
|
||||
requestTemplate.header(HttpHeaders.CONTENT_TYPE,
|
||||
MediaType.APPLICATION_FORM_URLENCODED_VALUE);
|
||||
requestTemplate.body(builder.toString());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class NoOpCapability implements Capability {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+11
-6
@@ -35,7 +35,8 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@SpringBootTest(classes = FeignClientsRegistrarIntegrationTests.QualifiersTestConfig.class)
|
||||
@SpringBootTest(
|
||||
classes = FeignClientsRegistrarIntegrationTests.QualifiersTestConfig.class)
|
||||
class FeignClientsRegistrarIntegrationTests {
|
||||
|
||||
@Autowired
|
||||
@@ -45,7 +46,8 @@ class FeignClientsRegistrarIntegrationTests {
|
||||
void shouldUseQualifiersIfPresent() {
|
||||
assertThat(context.getBean("qualifier1")).isNotNull();
|
||||
assertThat(context.getBean("qualifier2")).isNotNull();
|
||||
assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> context.getBean("qualifier3"));
|
||||
assertThatExceptionOfType(NoSuchBeanDefinitionException.class)
|
||||
.isThrownBy(() -> context.getBean("qualifier3"));
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -79,10 +81,12 @@ class FeignClientsRegistrarIntegrationTests {
|
||||
|
||||
@Test
|
||||
void shouldUseDefaultQualifierWhenWhitespaceQualifiers() {
|
||||
assertThat(context.getBean("whitespaceQualifiersNoQualifierFeignClient")).isNotNull();
|
||||
assertThat(context.getBean("whitespaceQualifiersNoQualifierFeignClient"))
|
||||
.isNotNull();
|
||||
}
|
||||
|
||||
@FeignClient(name = "qualifiersClient", qualifiers = { "qualifier1", "qualifier2" }, qualifier = "qualifier3")
|
||||
@FeignClient(name = "qualifiersClient", qualifiers = { "qualifier1", "qualifier2" },
|
||||
qualifier = "qualifier3")
|
||||
protected interface QualifiersClient {
|
||||
|
||||
}
|
||||
@@ -120,8 +124,9 @@ class FeignClientsRegistrarIntegrationTests {
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableAutoConfiguration
|
||||
@Import(NoSecurityConfiguration.class)
|
||||
@EnableFeignClients(clients = { QualifiersClient.class, QualifierClient.class, NoQualifiersClient.class,
|
||||
EmptyQualifiersClient.class, WhitespaceQualifiersClient.class, EmptyQualifiersNoQualifierClient.class,
|
||||
@EnableFeignClients(clients = { QualifiersClient.class, QualifierClient.class,
|
||||
NoQualifiersClient.class, EmptyQualifiersClient.class,
|
||||
WhitespaceQualifiersClient.class, EmptyQualifiersNoQualifierClient.class,
|
||||
WhitespaceQualifiersNoQualifierClient.class })
|
||||
protected static class QualifiersTestConfig {
|
||||
|
||||
|
||||
+12
-6
@@ -22,6 +22,7 @@ import org.junit.Test;
|
||||
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.cloud.openfeign.test.TestAutoConfiguration;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
@@ -95,14 +96,16 @@ public class FeignClientsRegistrarTests {
|
||||
@Test
|
||||
public void shouldPassSubLevelFeignClient() {
|
||||
AnnotationConfigApplicationContext config = new AnnotationConfigApplicationContext();
|
||||
((DefaultListableBeanFactory) config.getBeanFactory()).setAllowBeanDefinitionOverriding(false);
|
||||
((DefaultListableBeanFactory) config.getBeanFactory())
|
||||
.setAllowBeanDefinitionOverriding(false);
|
||||
config.register(TopLevelSubLevelTestConfig.class);
|
||||
assertThatCode(() -> config.refresh())
|
||||
.as("Case https://github.com/spring-cloud/spring-cloud-openfeign/issues/331 should be solved")
|
||||
assertThatCode(() -> config.refresh()).as(
|
||||
"Case https://github.com/spring-cloud/spring-cloud-openfeign/issues/331 should be solved")
|
||||
.doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@FeignClient(name = "fallbackTestClient", url = "http://localhost:8080/", fallback = FallbackClient.class)
|
||||
@FeignClient(name = "fallbackTestClient", url = "http://localhost:8080/",
|
||||
fallback = FallbackClient.class)
|
||||
protected interface FallbackClient {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
@@ -128,13 +131,16 @@ public class FeignClientsRegistrarTests {
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableAutoConfiguration
|
||||
@EnableFeignClients(clients = { FeignClientsRegistrarTests.FallbackFactoryClient.class })
|
||||
@EnableFeignClients(
|
||||
clients = { FeignClientsRegistrarTests.FallbackFactoryClient.class })
|
||||
protected static class FallbackFactoryTestConfig {
|
||||
|
||||
}
|
||||
|
||||
@EnableFeignClients(clients = { org.springframework.cloud.openfeign.feignclientsregistrar.TopLevelClient.class,
|
||||
@EnableFeignClients(clients = {
|
||||
org.springframework.cloud.openfeign.feignclientsregistrar.TopLevelClient.class,
|
||||
org.springframework.cloud.openfeign.feignclientsregistrar.sub.SubLevelClient.class })
|
||||
@EnableAutoConfiguration(exclude = TestAutoConfiguration.class)
|
||||
protected static class TopLevelSubLevelTestConfig {
|
||||
|
||||
}
|
||||
|
||||
+9
-5
@@ -28,6 +28,7 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||
import org.springframework.cloud.commons.httpclient.HttpClientConfiguration;
|
||||
import org.springframework.cloud.netflix.archaius.ArchaiusAutoConfiguration;
|
||||
import org.springframework.cloud.openfeign.encoding.FeignAcceptGzipEncodingAutoConfiguration;
|
||||
import org.springframework.cloud.openfeign.encoding.FeignAcceptGzipEncodingInterceptor;
|
||||
import org.springframework.cloud.openfeign.encoding.FeignContentGzipEncodingAutoConfiguration;
|
||||
@@ -50,15 +51,18 @@ public class FeignCompressionTests {
|
||||
@Test
|
||||
public void testInterceptors() {
|
||||
new ApplicationContextRunner()
|
||||
.withPropertyValues("feign.compression.response.enabled=true", "feign.compression.request.enabled=true",
|
||||
.withPropertyValues("feign.compression.response.enabled=true",
|
||||
"feign.compression.request.enabled=true",
|
||||
"feign.okhttp.enabled=false")
|
||||
.withConfiguration(AutoConfigurations.of(FeignAutoConfiguration.class,
|
||||
FeignContentGzipEncodingAutoConfiguration.class, FeignAcceptGzipEncodingAutoConfiguration.class,
|
||||
.withConfiguration(AutoConfigurations.of(ArchaiusAutoConfiguration.class,
|
||||
FeignAutoConfiguration.class,
|
||||
FeignContentGzipEncodingAutoConfiguration.class,
|
||||
FeignAcceptGzipEncodingAutoConfiguration.class,
|
||||
HttpClientConfiguration.class, PlainConfig.class))
|
||||
.run(context -> {
|
||||
FeignContext feignContext = context.getBean(FeignContext.class);
|
||||
Map<String, RequestInterceptor> interceptors = feignContext.getInstances("foo",
|
||||
RequestInterceptor.class);
|
||||
Map<String, RequestInterceptor> interceptors = feignContext
|
||||
.getInstances("foo", RequestInterceptor.class);
|
||||
assertThat(interceptors.size()).isEqualTo(2);
|
||||
assertThat(interceptors.get("feignAcceptGzipEncodingInterceptor"))
|
||||
.isInstanceOf(FeignAcceptGzipEncodingInterceptor.class);
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user