Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a42b4e4b43 | ||
|
|
793842a9ac | ||
|
|
ec4c935384 | ||
|
|
c672d205d7 | ||
|
|
4453424bdb | ||
|
|
db2d07500d | ||
|
|
6b29a1f7df | ||
|
|
252f118bba | ||
|
|
bbef08205d | ||
|
|
0799f3dd7c | ||
|
|
25b8202991 | ||
|
|
5025285c5b | ||
|
|
7ef5c0ce96 | ||
|
|
5f932b0158 | ||
|
|
f5461821a1 | ||
|
|
14914b4c97 | ||
|
|
1b41e39740 | ||
|
|
6bfb1edcf1 | ||
|
|
54a090e77b | ||
|
|
8bc9a0eb0a | ||
|
|
01a3898e83 | ||
|
|
147ef8c863 | ||
|
|
286360cf7b | ||
|
|
14eb8a7bb1 | ||
|
|
5011703eb9 | ||
|
|
a79d82584e | ||
|
|
c508f68b85 | ||
|
|
ae3e3efb26 | ||
|
|
01f101a910 | ||
|
|
69a03ffdb8 | ||
|
|
2cf8b55316 | ||
|
|
eb30c13129 | ||
|
|
258e8aee95 | ||
|
|
ad21ff2276 | ||
|
|
ecfba4b6e9 | ||
|
|
c060e07115 | ||
|
|
e58b52ce05 | ||
|
|
e32b10b60f | ||
|
|
63a524843d | ||
|
|
a697fd274b | ||
|
|
9336c1d7c5 | ||
|
|
3e7e74c5bf | ||
|
|
86551041d9 | ||
|
|
35a5b8d614 | ||
|
|
7047439624 | ||
|
|
5e61666f7c | ||
|
|
e3810f1586 | ||
|
|
9bdda59fbb | ||
|
|
e257ec31ff | ||
|
|
246805815f | ||
|
|
89fb98daf7 | ||
|
|
15054fa37e | ||
|
|
1a7917e2bb | ||
|
|
49d3830276 | ||
|
|
d419826ca0 | ||
|
|
b231783e84 | ||
|
|
64d3f05e70 | ||
|
|
a7c804db63 | ||
|
|
4d021fd3f8 | ||
|
|
a8bcda0b5b | ||
|
|
4120ca2bc6 | ||
|
|
29295425f3 | ||
|
|
ef9ded650f | ||
|
|
3c25b010c3 | ||
|
|
1b42820c3a | ||
|
|
e31defb2dd | ||
|
|
f4ca971253 | ||
|
|
f9a8dd13f2 | ||
|
|
f6fdd63cef | ||
|
|
410d0f29a3 | ||
|
|
82de713d06 | ||
|
|
a8a2a0155f | ||
|
|
5e455ed14b | ||
|
|
a3584a9cc8 | ||
|
|
cf4fa54ac8 | ||
|
|
ffb17a4552 | ||
|
|
93327fcace | ||
|
|
20e2548a5d | ||
|
|
d93defe1c1 | ||
|
|
bc6c3b7bf2 | ||
|
|
db8d10a511 | ||
|
|
f410385237 | ||
|
|
6649c86be1 | ||
|
|
3f0c2850ee | ||
|
|
479e1ab743 | ||
|
|
77a1367796 | ||
|
|
89f8942dff |
@@ -28,6 +28,8 @@ image:https://api.codacy.com/project/badge/Grade/97b04c4e609c4b4f86b415e4437a648
|
||||
:project-full-name: Spring Cloud OpenFeign
|
||||
:all: {asterisk}{asterisk}
|
||||
|
||||
:core_path: {project-root}/spring-cloud-openfeign-core
|
||||
|
||||
This project provides OpenFeign integrations for Spring Boot apps through autoconfiguration
|
||||
and binding to the Spring Environment and other Spring programming model idioms.
|
||||
|
||||
@@ -314,3 +316,7 @@ Go to `File` -> `Settings` -> `Other settings` -> `Checkstyle`. There click on t
|
||||
- `checkstyle.additional.suppressions.file` - this variable corresponds to suppressions in your local project. E.g. you're working on `spring-cloud-contract`. Then point to the `project-root/src/checkstyle/checkstyle-suppressions.xml` folder. Example for `spring-cloud-contract` would be: `/home/username/spring-cloud-contract/src/checkstyle/checkstyle-suppressions.xml`.
|
||||
|
||||
IMPORTANT: Remember to set the `Scan Scope` to `All sources` since we apply checkstyle rules for production and test sources.
|
||||
|
||||
== License
|
||||
|
||||
The project license file is available https://raw.githubusercontent.com/spring-cloud/spring-cloud-openfeign/master/LICENSE.txt[here].
|
||||
|
||||
@@ -1,5 +0,0 @@
|
||||
# Security Policy
|
||||
|
||||
## Reporting a Vulnerability
|
||||
|
||||
To report security vulnerabilities, please go to https://pivotal.io/security.
|
||||
+1
-1
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-openfeign</artifactId>
|
||||
<version>3.0.0-M4</version>
|
||||
<version>3.0.1</version>
|
||||
</parent>
|
||||
<artifactId>spring-cloud-openfeign-docs</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
@@ -19,3 +19,7 @@ include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/maste
|
||||
== Contributing
|
||||
|
||||
include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/contributing.adoc[]
|
||||
|
||||
== License
|
||||
|
||||
The project license file is available https://raw.githubusercontent.com/spring-cloud/spring-cloud-openfeign/master/LICENSE.txt[here].
|
||||
|
||||
@@ -14,3 +14,5 @@
|
||||
:sc-ext: java
|
||||
:project-full-name: Spring Cloud OpenFeign
|
||||
:all: {asterisk}{asterisk}
|
||||
|
||||
:core_path: {project-root}/spring-cloud-openfeign-core
|
||||
|
||||
@@ -1,24 +1,25 @@
|
||||
|===
|
||||
|Name | Default | Description
|
||||
|
||||
|feign.circuitbreaker.enabled | `false` | If true, an OpenFeign client will be wrapped with a Spring Cloud CircuitBreaker circuit breaker.
|
||||
|feign.client.config | |
|
||||
|feign.client.default-config | default |
|
||||
|feign.client.default-to-properties | true |
|
||||
|feign.compression.request.enabled | false | Enables the request sent by Feign to be compressed.
|
||||
|feign.compression.request.mime-types | [text/xml, application/xml, application/json] | The list of supported mime types.
|
||||
|feign.compression.request.min-request-size | 2048 | The minimum threshold content size.
|
||||
|feign.compression.response.enabled | false | Enables the response from Feign to be compressed.
|
||||
|feign.compression.response.useGzipDecoder | false | Enables the default gzip decoder to be used.
|
||||
|feign.httpclient.connection-timeout | 2000 |
|
||||
|feign.httpclient.connection-timer-repeat | 3000 |
|
||||
|feign.httpclient.disable-ssl-validation | false |
|
||||
|feign.httpclient.enabled | true | Enables the use of the Apache HTTP Client by Feign.
|
||||
|feign.httpclient.follow-redirects | true |
|
||||
|feign.httpclient.max-connections | 200 |
|
||||
|feign.httpclient.max-connections-per-route | 50 |
|
||||
|feign.httpclient.time-to-live | 900 |
|
||||
|feign.client.decode-slash | `true` | Feign clients do not encode slash `/` characters by default. To change this behavior, set the `decodeSlash` to `false`.
|
||||
|feign.client.default-config | `default` |
|
||||
|feign.client.default-to-properties | `true` |
|
||||
|feign.compression.request.enabled | `false` | Enables the request sent by Feign to be compressed.
|
||||
|feign.compression.request.mime-types | `[text/xml, application/xml, application/json]` | The list of supported mime types.
|
||||
|feign.compression.request.min-request-size | `2048` | The minimum threshold content size.
|
||||
|feign.compression.response.enabled | `false` | Enables the response from Feign to be compressed.
|
||||
|feign.compression.response.useGzipDecoder | `false` | Enables the default gzip decoder to be used.
|
||||
|feign.httpclient.connection-timeout | `2000` |
|
||||
|feign.httpclient.connection-timer-repeat | `3000` |
|
||||
|feign.httpclient.disable-ssl-validation | `false` |
|
||||
|feign.httpclient.enabled | `true` | Enables the use of the Apache HTTP Client by Feign.
|
||||
|feign.httpclient.follow-redirects | `true` |
|
||||
|feign.httpclient.max-connections | `200` |
|
||||
|feign.httpclient.max-connections-per-route | `50` |
|
||||
|feign.httpclient.time-to-live | `900` |
|
||||
|feign.httpclient.time-to-live-unit | |
|
||||
|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.
|
||||
|feign.okhttp.enabled | `false` | Enables the use of the OK HTTP Client by Feign.
|
||||
|
||||
|===
|
||||
@@ -24,3 +24,18 @@ public class WebApplication {
|
||||
}
|
||||
|
||||
```
|
||||
|
||||
## Contributing
|
||||
|
||||
We welcome contributions. You can read more on how to contribute to the project https://github.com/spring-cloud/spring-cloud-openfeign/blob/master/README.adoc#3-contributing[here].
|
||||
|
||||
## Community Support
|
||||
|
||||
* You can report issues through https://github.com/spring-cloud/spring-cloud-openfeign/issues[Github].
|
||||
* We monitor https://stackoverflow.com/[StackOverflow] for questions with the `spring-cloud-feign` tag.
|
||||
* You can contact our team at https://gitter.im/spring-cloud/spring-cloud[Gitter].
|
||||
|
||||
## Commercial Support
|
||||
|
||||
Commercial Support is provided as part of the https://spring.io/support[VMware Spring Runtime] offering.
|
||||
|
||||
|
||||
@@ -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, as well as Spring Cloud LoadBalancer to provide a load-balanced http client when using Feign.
|
||||
Spring Cloud integrates 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
|
||||
@@ -68,6 +68,8 @@ it will resolve the service in the Eureka service registry. If you
|
||||
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].
|
||||
|
||||
[[spring-cloud-feign-overriding-defaults]]
|
||||
=== Overriding Feign Defaults
|
||||
|
||||
@@ -90,8 +92,6 @@ 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.
|
||||
@@ -114,7 +114,7 @@ Spring Cloud OpenFeign provides the following beans by default for feign (`BeanT
|
||||
* `Encoder` feignEncoder: `SpringEncoder`
|
||||
* `Logger` feignLogger: `Slf4jLogger`
|
||||
* `Contract` feignContract: `SpringMvcContract`
|
||||
* `Feign.Builder` feignBuilder: `HystrixFeign.Builder`
|
||||
* `Feign.Builder` feignBuilder: `FeignCircuitBreaker.Builder`
|
||||
* `Client` feignClient: 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.
|
||||
|
||||
@@ -171,6 +171,10 @@ feign:
|
||||
loggerLevel: full
|
||||
errorDecoder: com.example.SimpleErrorDecoder
|
||||
retryer: com.example.SimpleRetryer
|
||||
defaultQueryParameters:
|
||||
query: queryValue
|
||||
defaultRequestHeaders:
|
||||
header: headerValue
|
||||
requestInterceptors:
|
||||
- com.example.FooRequestInterceptor
|
||||
- com.example.BarRequestInterceptor
|
||||
@@ -184,6 +188,8 @@ Default configurations can be specified in the `@EnableFeignClients` attribute `
|
||||
|
||||
If you prefer using configuration properties to configured all `@FeignClient`, you can create configuration properties with `default` feign name.
|
||||
|
||||
You can use `feign.client.config.feignName.defaultQueryParameters` and `feign.client.config.feignName.defaultRequestHeaders` to specify query parameters and headers that will be sent with every request of the client named `feignName`.
|
||||
|
||||
application.yml
|
||||
[source,yaml]
|
||||
----
|
||||
@@ -200,26 +206,6 @@ If we create both `@Configuration` bean and configuration properties, configurat
|
||||
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
|
||||
@@ -264,6 +250,18 @@ public FeignClientConfigurer feignClientConfigurer() {
|
||||
}
|
||||
----
|
||||
|
||||
TIP: By default, Feign clients do not encode slash `/` characters. You can change this behaviour, by setting the value of `feign.client.decodeSlash` to `false`.
|
||||
|
||||
[[timeout-handling]]
|
||||
=== Timeout Handling
|
||||
|
||||
We can configure timeouts on both the default and the named client. OpenFeign works with two timeout parameters:
|
||||
|
||||
- `connectTimeout` prevents blocking the caller due to the long server processing time.
|
||||
- `readTimeout` is applied from the time of connection establishment and is triggered when returning the response takes too long.
|
||||
|
||||
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.
|
||||
|
||||
=== Creating Feign Clients Manually
|
||||
|
||||
In some cases it might be necessary to customize your Feign Clients in a way that is not
|
||||
@@ -312,12 +310,12 @@ 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
|
||||
[[spring-cloud-feign-circuitbreaker]]
|
||||
=== Feign Spring Cloud CircuitBreaker 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()`).
|
||||
If Spring Cloud CircuitBreaker is on the classpath and `feign.circuitbreaker.enabled=true`, Feign will wrap all methods with a circuit breaker.
|
||||
|
||||
To disable Hystrix support on a per-client basis create a vanilla `Feign.Builder` with the "prototype" scope, e.g.:
|
||||
To disable Spring Cloud CircuitBreaker support on a per-client basis create a vanilla `Feign.Builder` with the "prototype" scope, e.g.:
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
@@ -331,60 +329,28 @@ public class FooConfiguration {
|
||||
}
|
||||
----
|
||||
|
||||
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.
|
||||
The circuit breaker name follows this pattern `<feignClientName>_<calledMethod>`. When calling a `@FeignClient` with name `foo` and the called interface method is `bar` then the circuit breaker name will be `foo_bar`.
|
||||
|
||||
[[spring-cloud-feign-hystrix-fallback]]
|
||||
=== Feign Hystrix Fallbacks
|
||||
[[spring-cloud-feign-circuitbreaker-fallback]]
|
||||
=== Feign Spring Cloud CircuitBreaker Fallbacks
|
||||
|
||||
Hystrix 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 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.
|
||||
|
||||
[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");
|
||||
}
|
||||
}
|
||||
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 = "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());
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
include::{core_path}/src/test/java/org/springframework/cloud/openfeign/circuitbreaker/CircuitBreakerTests.java[tags=client_with_fallback_factory, indent=0]
|
||||
----
|
||||
|
||||
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`.
|
||||
|
||||
=== Feign and `@Primary`
|
||||
|
||||
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.
|
||||
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.
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
@@ -583,7 +549,7 @@ public interface DemoTemplate {
|
||||
----
|
||||
|
||||
=== 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.
|
||||
|
||||
@@ -602,7 +568,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.
|
||||
|
||||
@@ -617,6 +583,15 @@ To work around this problem you can use an `ObjectProvider` when autowiring your
|
||||
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]
|
||||
----
|
||||
feign.autoconfiguration.jackson.enabled=true
|
||||
----
|
||||
|
||||
== Configuration properties
|
||||
|
||||
To see the list of all Sleuth related configuration properties please check link:appendix.html[the Appendix page].
|
||||
|
||||
@@ -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.0-M4</version>
|
||||
<version>3.0.1</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.0-M4</version>
|
||||
<version>3.0.1</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
<scm>
|
||||
@@ -25,8 +25,8 @@
|
||||
</scm>
|
||||
<properties>
|
||||
<main.basedir>${basedir}</main.basedir>
|
||||
<jackson.version>2.11.2</jackson.version>
|
||||
<spring-cloud-commons.version>3.0.0-M4</spring-cloud-commons.version>
|
||||
<jackson.version>2.11.3</jackson.version>
|
||||
<spring-cloud-commons.version>3.0.1</spring-cloud-commons.version>
|
||||
|
||||
<!-- Plugin versions -->
|
||||
<maven-eclipse-plugin.version>2.10</maven-eclipse-plugin.version>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-openfeign</artifactId>
|
||||
<version>3.0.0-M4</version>
|
||||
<version>3.0.1</version>
|
||||
<relativePath>..</relativePath> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<artifactId>spring-cloud-openfeign-core</artifactId>
|
||||
@@ -119,6 +119,11 @@
|
||||
<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>
|
||||
@@ -168,7 +173,7 @@
|
||||
<dependency>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java</artifactId>
|
||||
<version>3.13.0</version>
|
||||
<version>3.14.0</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import static feign.Util.checkNotNull;
|
||||
|
||||
/**
|
||||
* Used to control the fallback given its cause.
|
||||
*
|
||||
* Ex.
|
||||
*
|
||||
* <pre>
|
||||
* {@code
|
||||
* // This instance will be invoked if there are errors of any kind.
|
||||
* FallbackFactory<GitHub> fallbackFactory = cause -> (owner, repo) -> {
|
||||
* if (cause instanceof FeignException && ((FeignException) cause).status() == 403) {
|
||||
* return Collections.emptyList();
|
||||
* } else {
|
||||
* return Arrays.asList("yogi");
|
||||
* }
|
||||
* };
|
||||
*
|
||||
* GitHub github = FeignCircuitBreaker.builder()
|
||||
* ...
|
||||
* .target(GitHub.class, "https://api.github.com", fallbackFactory);
|
||||
* }
|
||||
* </pre>
|
||||
*
|
||||
* @param <T> the feign interface type
|
||||
*/
|
||||
public interface FallbackFactory<T> {
|
||||
|
||||
/**
|
||||
* Returns an instance of the fallback appropriate for the given cause.
|
||||
* @param cause cause of an exception.
|
||||
* @return fallback
|
||||
*/
|
||||
T create(Throwable cause);
|
||||
|
||||
final class Default<T> implements FallbackFactory<T> {
|
||||
|
||||
final Log logger;
|
||||
|
||||
final T constant;
|
||||
|
||||
public Default(T constant) {
|
||||
this(constant, LogFactory.getLog(Default.class));
|
||||
}
|
||||
|
||||
Default(T constant, Log logger) {
|
||||
this.constant = checkNotNull(constant, "fallback");
|
||||
this.logger = checkNotNull(logger, "logger");
|
||||
}
|
||||
|
||||
@Override
|
||||
public T create(Throwable cause) {
|
||||
if (logger.isTraceEnabled()) {
|
||||
logger.trace("fallback due to: " + cause.getMessage(), cause);
|
||||
}
|
||||
return constant;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return constant.toString();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+79
-2
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
@@ -24,11 +25,15 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.annotation.PreDestroy;
|
||||
|
||||
import com.fasterxml.jackson.databind.Module;
|
||||
import feign.Client;
|
||||
import feign.Feign;
|
||||
import feign.RequestInterceptor;
|
||||
import feign.httpclient.ApacheHttpClient;
|
||||
import feign.okhttp.OkHttpClient;
|
||||
import okhttp3.ConnectionPool;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.http.client.HttpClient;
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.config.RegistryBuilder;
|
||||
@@ -36,24 +41,35 @@ 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.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.actuator.HasFeatures;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||
import org.springframework.cloud.commons.httpclient.ApacheHttpClientConnectionManagerFactory;
|
||||
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.FeignHttpClientProperties;
|
||||
import org.springframework.cloud.openfeign.support.PageJacksonModule;
|
||||
import org.springframework.cloud.openfeign.support.SortJacksonModule;
|
||||
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.security.oauth2.client.OAuth2ClientContext;
|
||||
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
* @author Julien Roy
|
||||
* @author Grzegorz Poznachowski
|
||||
* @author Nikita Konev
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(Feign.class)
|
||||
@@ -61,6 +77,8 @@ import org.springframework.context.annotation.Import;
|
||||
@Import(DefaultGzipDecoderConfiguration.class)
|
||||
public class FeignAutoConfiguration {
|
||||
|
||||
private static final Log LOG = LogFactory.getLog(FeignAutoConfiguration.class);
|
||||
|
||||
@Autowired(required = false)
|
||||
private List<FeignClientSpecification> configurations = new ArrayList<>();
|
||||
|
||||
@@ -76,7 +94,24 @@ public class FeignAutoConfiguration {
|
||||
return context;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(PageJacksonModule.class)
|
||||
@ConditionalOnClass(name = "org.springframework.data.domain.Page")
|
||||
@ConditionalOnProperty(value = "feign.autoconfiguration.jackson.enabled", havingValue = "true")
|
||||
public Module pageJacksonModule() {
|
||||
return new PageJacksonModule();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(SortJacksonModule.class)
|
||||
@ConditionalOnClass(name = "org.springframework.data.domain.Sort")
|
||||
@ConditionalOnProperty(value = "feign.autoconfiguration.jackson.enabled", havingValue = "true")
|
||||
public Module sortModule() {
|
||||
return new SortJacksonModule();
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Conditional(FeignCircuitBreakerDisabledConditions.class)
|
||||
protected static class DefaultFeignTargeterConfiguration {
|
||||
|
||||
@Bean
|
||||
@@ -87,6 +122,26 @@ public class FeignAutoConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(CircuitBreaker.class)
|
||||
@ConditionalOnProperty("feign.circuitbreaker.enabled")
|
||||
protected static class CircuitBreakerPresentFeignTargeterConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(CircuitBreakerFactory.class)
|
||||
public Targeter defaultFeignTargeter() {
|
||||
return new DefaultTargeter();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnBean(CircuitBreakerFactory.class)
|
||||
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 FeignRibbonClientAutoConfiguration
|
||||
@@ -142,10 +197,17 @@ public class FeignAutoConfiguration {
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void destroy() throws Exception {
|
||||
public void destroy() {
|
||||
this.connectionManagerTimer.cancel();
|
||||
if (this.httpClient != null) {
|
||||
this.httpClient.close();
|
||||
try {
|
||||
this.httpClient.close();
|
||||
}
|
||||
catch (IOException e) {
|
||||
if (LOG.isErrorEnabled()) {
|
||||
LOG.error("Could not correctly close httpClient.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -197,4 +259,19 @@ public class FeignAutoConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(OAuth2ClientContext.class)
|
||||
@ConditionalOnProperty("feign.oauth2.enabled")
|
||||
protected static class Oauth2FeignConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(OAuth2FeignRequestInterceptor.class)
|
||||
@ConditionalOnBean({ OAuth2ClientContext.class, OAuth2ProtectedResourceDetails.class })
|
||||
public RequestInterceptor oauth2FeignRequestInterceptor(OAuth2ClientContext oAuth2ClientContext,
|
||||
OAuth2ProtectedResourceDetails resource) {
|
||||
return new OAuth2FeignRequestInterceptor(oAuth2ClientContext, resource);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
import feign.Feign;
|
||||
import feign.Target;
|
||||
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||
|
||||
/**
|
||||
* Allows Feign interfaces to work with {@link CircuitBreaker}.
|
||||
*
|
||||
* @author Marcin Grzejszczak
|
||||
* @since 3.0.0
|
||||
*/
|
||||
public final class FeignCircuitBreaker {
|
||||
|
||||
private FeignCircuitBreaker() {
|
||||
throw new IllegalStateException("Don't instantiate a utility class");
|
||||
}
|
||||
|
||||
/**
|
||||
* @return builder for Feign CircuitBreaker integration
|
||||
*/
|
||||
public static Builder builder() {
|
||||
return new Builder();
|
||||
}
|
||||
|
||||
/**
|
||||
* Builder for Feign CircuitBreaker integration.
|
||||
*/
|
||||
public static final class Builder extends Feign.Builder {
|
||||
|
||||
private CircuitBreakerFactory circuitBreakerFactory;
|
||||
|
||||
private String feignClientName;
|
||||
|
||||
Builder circuitBreakerFactory(CircuitBreakerFactory circuitBreakerFactory) {
|
||||
this.circuitBreakerFactory = circuitBreakerFactory;
|
||||
return this;
|
||||
}
|
||||
|
||||
Builder feignClientName(String feignClientName) {
|
||||
this.feignClientName = feignClientName;
|
||||
return this;
|
||||
}
|
||||
|
||||
public <T> T target(Target<T> target, T fallback) {
|
||||
return build(fallback != null ? new FallbackFactory.Default<T>(fallback) : null).newInstance(target);
|
||||
}
|
||||
|
||||
public <T> T target(Target<T> target, FallbackFactory<? extends T> fallbackFactory) {
|
||||
return build(fallbackFactory).newInstance(target);
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T target(Target<T> target) {
|
||||
return build(null).newInstance(target);
|
||||
}
|
||||
|
||||
public Feign build(final FallbackFactory<?> nullableFallbackFactory) {
|
||||
super.invocationHandlerFactory((target, dispatch) -> new FeignCircuitBreakerInvocationHandler(
|
||||
circuitBreakerFactory, feignClientName, target, dispatch, nullableFallbackFactory));
|
||||
return super.build();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* 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;
|
||||
|
||||
class FeignCircuitBreakerDisabledConditions extends AnyNestedCondition {
|
||||
|
||||
FeignCircuitBreakerDisabledConditions() {
|
||||
super(ConfigurationPhase.PARSE_CONFIGURATION);
|
||||
}
|
||||
|
||||
@ConditionalOnMissingClass("org.springframework.cloud.client.circuitbreaker.CircuitBreaker")
|
||||
static class CircuitBreakerClassMissing {
|
||||
|
||||
}
|
||||
|
||||
@ConditionalOnProperty(value = "feign.circuitbreaker.enabled", havingValue = "false", matchIfMissing = true)
|
||||
static class CircuitBreakerDisabled {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+147
@@ -0,0 +1,147 @@
|
||||
/*
|
||||
* 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 java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.lang.reflect.Proxy;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import feign.InvocationHandlerFactory;
|
||||
import feign.Target;
|
||||
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||
|
||||
import static feign.Util.checkNotNull;
|
||||
|
||||
class FeignCircuitBreakerInvocationHandler implements InvocationHandler {
|
||||
|
||||
private final CircuitBreakerFactory factory;
|
||||
|
||||
private final String feignClientName;
|
||||
|
||||
private final Target<?> target;
|
||||
|
||||
private final Map<Method, InvocationHandlerFactory.MethodHandler> dispatch;
|
||||
|
||||
private final FallbackFactory<?> nullableFallbackFactory;
|
||||
|
||||
private final Map<Method, Method> fallbackMethodMap;
|
||||
|
||||
FeignCircuitBreakerInvocationHandler(CircuitBreakerFactory factory, String feignClientName, Target<?> target,
|
||||
Map<Method, InvocationHandlerFactory.MethodHandler> dispatch, FallbackFactory<?> nullableFallbackFactory) {
|
||||
this.factory = factory;
|
||||
this.feignClientName = feignClientName;
|
||||
this.target = checkNotNull(target, "target");
|
||||
this.dispatch = checkNotNull(dispatch, "dispatch");
|
||||
this.fallbackMethodMap = toFallbackMethod(dispatch);
|
||||
this.nullableFallbackFactory = nullableFallbackFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
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;
|
||||
return equals(otherHandler);
|
||||
}
|
||||
catch (IllegalArgumentException e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
else if ("hashCode".equals(method.getName())) {
|
||||
return hashCode();
|
||||
}
|
||||
else if ("toString".equals(method.getName())) {
|
||||
return toString();
|
||||
}
|
||||
String circuitName = this.feignClientName + "_" + method.getName();
|
||||
CircuitBreaker circuitBreaker = this.factory.create(circuitName);
|
||||
Supplier<Object> supplier = asSupplier(method, args);
|
||||
if (this.nullableFallbackFactory != null) {
|
||||
Function<Throwable, Object> fallbackFunction = throwable -> {
|
||||
Object fallback = this.nullableFallbackFactory.create(throwable);
|
||||
try {
|
||||
return this.fallbackMethodMap.get(method).invoke(fallback, args);
|
||||
}
|
||||
catch (Exception e) {
|
||||
throw new IllegalStateException(e);
|
||||
}
|
||||
};
|
||||
return circuitBreaker.run(supplier, fallbackFunction);
|
||||
}
|
||||
return circuitBreaker.run(supplier);
|
||||
}
|
||||
|
||||
private Supplier<Object> asSupplier(final Method method, final Object[] args) {
|
||||
return () -> {
|
||||
try {
|
||||
return this.dispatch.get(method).invoke(args);
|
||||
}
|
||||
catch (RuntimeException throwable) {
|
||||
throw throwable;
|
||||
}
|
||||
catch (Throwable throwable) {
|
||||
throw new RuntimeException(throwable);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* If the method param of InvocationHandler.invoke is not accessible, i.e in a
|
||||
* package-private interface, the fallback call will cause of access restrictions. But
|
||||
* methods in dispatch are copied methods. So setting access to dispatch method
|
||||
* doesn't take effect to the method in InvocationHandler.invoke. Use map to store a
|
||||
* copy of method to invoke the fallback to bypass this and reducing the count of
|
||||
* reflection calls.
|
||||
* @return cached methods map for fallback invoking
|
||||
*/
|
||||
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);
|
||||
result.put(method, method);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object obj) {
|
||||
if (obj instanceof FeignCircuitBreakerInvocationHandler) {
|
||||
FeignCircuitBreakerInvocationHandler other = (FeignCircuitBreakerInvocationHandler) obj;
|
||||
return this.target.equals(other.target);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return this.target.hashCode();
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return this.target.toString();
|
||||
}
|
||||
|
||||
}
|
||||
+87
@@ -0,0 +1,87 @@
|
||||
/*
|
||||
* 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 org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
class FeignCircuitBreakerTargeter implements Targeter {
|
||||
|
||||
private final CircuitBreakerFactory circuitBreakerFactory;
|
||||
|
||||
FeignCircuitBreakerTargeter(CircuitBreakerFactory circuitBreakerFactory) {
|
||||
this.circuitBreakerFactory = circuitBreakerFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
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();
|
||||
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 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);
|
||||
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());
|
||||
return builder(feignClientName, 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 FeignCircuitBreaker.Builder builder(String feignClientName, FeignCircuitBreaker.Builder builder) {
|
||||
return builder.circuitBreakerFactory(this.circuitBreakerFactory).feignClientName(feignClientName);
|
||||
}
|
||||
|
||||
}
|
||||
+1
-9
@@ -49,14 +49,6 @@ 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.
|
||||
@@ -108,7 +100,7 @@ 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;
|
||||
|
||||
+40
-11
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
@@ -36,6 +38,8 @@ import feign.codec.ErrorDecoder;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
@@ -54,8 +58,11 @@ import org.springframework.util.StringUtils;
|
||||
* @author Gregor Zurowski
|
||||
* @author Matt King
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Ilia Ilinykh
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
public class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
|
||||
public class FeignClientFactoryBean
|
||||
implements FactoryBean<Object>, InitializingBean, ApplicationContextAware, BeanFactoryAware {
|
||||
|
||||
/***********************************
|
||||
* WARNING! Nothing in this class should be @Autowired. It causes NPEs because of some
|
||||
@@ -78,6 +85,8 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
private BeanFactory beanFactory;
|
||||
|
||||
private Class<?> fallback = void.class;
|
||||
|
||||
private Class<?> fallbackFactory = void.class;
|
||||
@@ -122,7 +131,8 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
||||
}
|
||||
|
||||
protected void configureFeign(FeignContext context, Feign.Builder builder) {
|
||||
FeignClientProperties properties = applicationContext.getBean(FeignClientProperties.class);
|
||||
FeignClientProperties properties = beanFactory != null ? beanFactory.getBean(FeignClientProperties.class)
|
||||
: applicationContext.getBean(FeignClientProperties.class);
|
||||
|
||||
FeignClientConfigurer feignClientConfigurer = getOptional(context, FeignClientConfigurer.class);
|
||||
setInheritParentContext(feignClientConfigurer.inheritParentConfiguration());
|
||||
@@ -173,7 +183,9 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
||||
Map<String, RequestInterceptor> requestInterceptors = getInheritedAwareInstances(context,
|
||||
RequestInterceptor.class);
|
||||
if (requestInterceptors != null) {
|
||||
builder.requestInterceptors(requestInterceptors.values());
|
||||
List<RequestInterceptor> interceptors = new ArrayList<>(requestInterceptors.values());
|
||||
AnnotationAwareOrderComparator.sort(interceptors);
|
||||
builder.requestInterceptors(interceptors);
|
||||
}
|
||||
QueryMapEncoder queryMapEncoder = getInheritedAwareOptional(context, QueryMapEncoder.class);
|
||||
if (queryMapEncoder != null) {
|
||||
@@ -233,6 +245,14 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
||||
builder.encoder(getOrInstantiate(config.getEncoder()));
|
||||
}
|
||||
|
||||
if (Objects.nonNull(config.getDefaultRequestHeaders())) {
|
||||
builder.requestInterceptor(requestTemplate -> requestTemplate.headers(config.getDefaultRequestHeaders()));
|
||||
}
|
||||
|
||||
if (Objects.nonNull(config.getDefaultQueryParameters())) {
|
||||
builder.requestInterceptor(requestTemplate -> requestTemplate.queries(config.getDefaultQueryParameters()));
|
||||
}
|
||||
|
||||
if (Objects.nonNull(config.getDecoder())) {
|
||||
builder.decoder(getOrInstantiate(config.getDecoder()));
|
||||
}
|
||||
@@ -248,7 +268,7 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
||||
|
||||
private <T> T getOrInstantiate(Class<T> tClass) {
|
||||
try {
|
||||
return applicationContext.getBean(tClass);
|
||||
return beanFactory != null ? beanFactory.getBean(tClass) : applicationContext.getBean(tClass);
|
||||
}
|
||||
catch (NoSuchBeanDefinitionException e) {
|
||||
return BeanUtils.instantiateClass(tClass);
|
||||
@@ -308,7 +328,8 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
||||
* information
|
||||
*/
|
||||
<T> T getTarget() {
|
||||
FeignContext context = applicationContext.getBean(FeignContext.class);
|
||||
FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class)
|
||||
: applicationContext.getBean(FeignContext.class);
|
||||
Feign.Builder builder = feign(context);
|
||||
|
||||
if (!StringUtils.hasText(url)) {
|
||||
@@ -423,7 +444,8 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext context) throws BeansException {
|
||||
this.applicationContext = context;
|
||||
applicationContext = context;
|
||||
beanFactory = context;
|
||||
}
|
||||
|
||||
public Class<?> getFallback() {
|
||||
@@ -451,7 +473,8 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
||||
return false;
|
||||
}
|
||||
FeignClientFactoryBean that = (FeignClientFactoryBean) o;
|
||||
return Objects.equals(applicationContext, that.applicationContext) && decode404 == that.decode404
|
||||
return Objects.equals(applicationContext, that.applicationContext)
|
||||
&& Objects.equals(beanFactory, that.beanFactory) && decode404 == that.decode404
|
||||
&& inheritParentContext == that.inheritParentContext && Objects.equals(fallback, that.fallback)
|
||||
&& Objects.equals(fallbackFactory, that.fallbackFactory) && Objects.equals(name, that.name)
|
||||
&& Objects.equals(path, that.path) && Objects.equals(type, that.type) && Objects.equals(url, that.url);
|
||||
@@ -459,8 +482,8 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(applicationContext, decode404, inheritParentContext, fallback, fallbackFactory, name, path,
|
||||
type, url);
|
||||
return Objects.hash(applicationContext, beanFactory, decode404, inheritParentContext, fallback, fallbackFactory,
|
||||
name, path, type, url);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -469,8 +492,14 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
||||
.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("fallback=").append(fallback).append(", ").append("fallbackFactory=")
|
||||
.append(fallbackFactory).append("}").toString();
|
||||
.append(", ").append("beanFactory=").append(beanFactory).append(", ").append("fallback=")
|
||||
.append(fallback).append(", ").append("fallbackFactory=").append(fallbackFactory).append("}")
|
||||
.toString();
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||
this.beanFactory = beanFactory;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+65
-26
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
@@ -34,6 +35,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* @author Eko Kurniawan Khannedy
|
||||
* @author Ilia Ilinykh
|
||||
* @author Ram Anaswara
|
||||
*/
|
||||
@ConfigurationProperties("feign.client")
|
||||
public class FeignClientProperties {
|
||||
@@ -44,8 +47,14 @@ public class FeignClientProperties {
|
||||
|
||||
private Map<String, FeignClientConfiguration> config = new HashMap<>();
|
||||
|
||||
/**
|
||||
* Feign clients do not encode slash `/` characters by default. To change this
|
||||
* behavior, set the `decodeSlash` to `false`.
|
||||
*/
|
||||
private boolean decodeSlash = true;
|
||||
|
||||
public boolean isDefaultToProperties() {
|
||||
return this.defaultToProperties;
|
||||
return defaultToProperties;
|
||||
}
|
||||
|
||||
public void setDefaultToProperties(boolean defaultToProperties) {
|
||||
@@ -53,7 +62,7 @@ public class FeignClientProperties {
|
||||
}
|
||||
|
||||
public String getDefaultConfig() {
|
||||
return this.defaultConfig;
|
||||
return defaultConfig;
|
||||
}
|
||||
|
||||
public void setDefaultConfig(String defaultConfig) {
|
||||
@@ -61,13 +70,21 @@ public class FeignClientProperties {
|
||||
}
|
||||
|
||||
public Map<String, FeignClientConfiguration> getConfig() {
|
||||
return this.config;
|
||||
return config;
|
||||
}
|
||||
|
||||
public void setConfig(Map<String, FeignClientConfiguration> config) {
|
||||
this.config = config;
|
||||
}
|
||||
|
||||
public boolean isDecodeSlash() {
|
||||
return decodeSlash;
|
||||
}
|
||||
|
||||
public void setDecodeSlash(boolean decodeSlash) {
|
||||
this.decodeSlash = decodeSlash;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
@@ -77,13 +94,13 @@ public class FeignClientProperties {
|
||||
return false;
|
||||
}
|
||||
FeignClientProperties that = (FeignClientProperties) o;
|
||||
return this.defaultToProperties == that.defaultToProperties
|
||||
&& Objects.equals(this.defaultConfig, that.defaultConfig) && Objects.equals(this.config, that.config);
|
||||
return defaultToProperties == that.defaultToProperties && Objects.equals(defaultConfig, that.defaultConfig)
|
||||
&& Objects.equals(config, that.config) && Objects.equals(decodeSlash, that.decodeSlash);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.defaultToProperties, this.defaultConfig, this.config);
|
||||
return Objects.hash(defaultToProperties, defaultConfig, config, decodeSlash);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -103,6 +120,10 @@ public class FeignClientProperties {
|
||||
|
||||
private List<Class<RequestInterceptor>> requestInterceptors;
|
||||
|
||||
private Map<String, Collection<String>> defaultRequestHeaders;
|
||||
|
||||
private Map<String, Collection<String>> defaultQueryParameters;
|
||||
|
||||
private Boolean decode404;
|
||||
|
||||
private Class<Decoder> decoder;
|
||||
@@ -114,7 +135,7 @@ public class FeignClientProperties {
|
||||
private ExceptionPropagationPolicy exceptionPropagationPolicy;
|
||||
|
||||
public Logger.Level getLoggerLevel() {
|
||||
return this.loggerLevel;
|
||||
return loggerLevel;
|
||||
}
|
||||
|
||||
public void setLoggerLevel(Logger.Level loggerLevel) {
|
||||
@@ -122,7 +143,7 @@ public class FeignClientProperties {
|
||||
}
|
||||
|
||||
public Integer getConnectTimeout() {
|
||||
return this.connectTimeout;
|
||||
return connectTimeout;
|
||||
}
|
||||
|
||||
public void setConnectTimeout(Integer connectTimeout) {
|
||||
@@ -130,7 +151,7 @@ public class FeignClientProperties {
|
||||
}
|
||||
|
||||
public Integer getReadTimeout() {
|
||||
return this.readTimeout;
|
||||
return readTimeout;
|
||||
}
|
||||
|
||||
public void setReadTimeout(Integer readTimeout) {
|
||||
@@ -138,7 +159,7 @@ public class FeignClientProperties {
|
||||
}
|
||||
|
||||
public Class<Retryer> getRetryer() {
|
||||
return this.retryer;
|
||||
return retryer;
|
||||
}
|
||||
|
||||
public void setRetryer(Class<Retryer> retryer) {
|
||||
@@ -146,7 +167,7 @@ public class FeignClientProperties {
|
||||
}
|
||||
|
||||
public Class<ErrorDecoder> getErrorDecoder() {
|
||||
return this.errorDecoder;
|
||||
return errorDecoder;
|
||||
}
|
||||
|
||||
public void setErrorDecoder(Class<ErrorDecoder> errorDecoder) {
|
||||
@@ -154,15 +175,31 @@ public class FeignClientProperties {
|
||||
}
|
||||
|
||||
public List<Class<RequestInterceptor>> getRequestInterceptors() {
|
||||
return this.requestInterceptors;
|
||||
return requestInterceptors;
|
||||
}
|
||||
|
||||
public void setRequestInterceptors(List<Class<RequestInterceptor>> requestInterceptors) {
|
||||
this.requestInterceptors = requestInterceptors;
|
||||
}
|
||||
|
||||
public Map<String, Collection<String>> getDefaultRequestHeaders() {
|
||||
return defaultRequestHeaders;
|
||||
}
|
||||
|
||||
public void setDefaultRequestHeaders(Map<String, Collection<String>> defaultRequestHeaders) {
|
||||
this.defaultRequestHeaders = defaultRequestHeaders;
|
||||
}
|
||||
|
||||
public Map<String, Collection<String>> getDefaultQueryParameters() {
|
||||
return defaultQueryParameters;
|
||||
}
|
||||
|
||||
public void setDefaultQueryParameters(Map<String, Collection<String>> defaultQueryParameters) {
|
||||
this.defaultQueryParameters = defaultQueryParameters;
|
||||
}
|
||||
|
||||
public Boolean getDecode404() {
|
||||
return this.decode404;
|
||||
return decode404;
|
||||
}
|
||||
|
||||
public void setDecode404(Boolean decode404) {
|
||||
@@ -170,7 +207,7 @@ public class FeignClientProperties {
|
||||
}
|
||||
|
||||
public Class<Decoder> getDecoder() {
|
||||
return this.decoder;
|
||||
return decoder;
|
||||
}
|
||||
|
||||
public void setDecoder(Class<Decoder> decoder) {
|
||||
@@ -178,7 +215,7 @@ public class FeignClientProperties {
|
||||
}
|
||||
|
||||
public Class<Encoder> getEncoder() {
|
||||
return this.encoder;
|
||||
return encoder;
|
||||
}
|
||||
|
||||
public void setEncoder(Class<Encoder> encoder) {
|
||||
@@ -186,7 +223,7 @@ public class FeignClientProperties {
|
||||
}
|
||||
|
||||
public Class<Contract> getContract() {
|
||||
return this.contract;
|
||||
return contract;
|
||||
}
|
||||
|
||||
public void setContract(Class<Contract> contract) {
|
||||
@@ -210,20 +247,22 @@ public class FeignClientProperties {
|
||||
return false;
|
||||
}
|
||||
FeignClientConfiguration that = (FeignClientConfiguration) o;
|
||||
return this.loggerLevel == that.loggerLevel && Objects.equals(this.connectTimeout, that.connectTimeout)
|
||||
&& Objects.equals(this.readTimeout, that.readTimeout) && Objects.equals(this.retryer, that.retryer)
|
||||
&& Objects.equals(this.errorDecoder, that.errorDecoder)
|
||||
&& Objects.equals(this.requestInterceptors, that.requestInterceptors)
|
||||
&& Objects.equals(this.decode404, that.decode404) && Objects.equals(this.encoder, that.encoder)
|
||||
&& Objects.equals(this.decoder, that.decoder) && Objects.equals(this.contract, that.contract)
|
||||
&& Objects.equals(this.exceptionPropagationPolicy, that.exceptionPropagationPolicy);
|
||||
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(defaultRequestHeaders, that.defaultRequestHeaders)
|
||||
&& Objects.equals(defaultQueryParameters, that.defaultQueryParameters);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.loggerLevel, this.connectTimeout, this.readTimeout, this.retryer,
|
||||
this.errorDecoder, this.requestInterceptors, this.decode404, this.encoder, this.decoder,
|
||||
this.contract, this.exceptionPropagationPolicy);
|
||||
return Objects.hash(loggerLevel, connectTimeout, readTimeout, retryer, errorDecoder, requestInterceptors,
|
||||
decode404, encoder, decoder, contract, exceptionPropagationPolicy, defaultQueryParameters,
|
||||
defaultRequestHeaders);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+45
-23
@@ -19,7 +19,6 @@ package org.springframework.cloud.openfeign;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.databind.Module;
|
||||
import feign.Contract;
|
||||
import feign.Feign;
|
||||
import feign.Logger;
|
||||
@@ -33,21 +32,24 @@ import feign.optionals.OptionalDecoder;
|
||||
import org.springframework.beans.factory.ObjectFactory;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.autoconfigure.data.web.SpringDataWebProperties;
|
||||
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||
import org.springframework.cloud.openfeign.clientconfig.FeignClientConfigurer;
|
||||
import org.springframework.cloud.openfeign.support.AbstractFormWriter;
|
||||
import org.springframework.cloud.openfeign.support.PageJacksonModule;
|
||||
import org.springframework.cloud.openfeign.support.PageableSpringEncoder;
|
||||
import org.springframework.cloud.openfeign.support.ResponseEntityDecoder;
|
||||
import org.springframework.cloud.openfeign.support.SortJacksonModule;
|
||||
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;
|
||||
@@ -79,6 +81,9 @@ public class FeignClientsConfiguration {
|
||||
@Autowired(required = false)
|
||||
private SpringDataWebProperties springDataWebProperties;
|
||||
|
||||
@Autowired(required = false)
|
||||
private FeignClientProperties feignClientProperties;
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public Decoder feignDecoder() {
|
||||
@@ -109,7 +114,8 @@ public class FeignClientsConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public Contract feignContract(ConversionService feignConversionService) {
|
||||
return new SpringMvcContract(this.parameterProcessors, feignConversionService);
|
||||
boolean decodeSlash = feignClientProperties == null || feignClientProperties.isDecodeSlash();
|
||||
return new SpringMvcContract(this.parameterProcessors, feignConversionService, decodeSlash);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -127,31 +133,12 @@ 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() {
|
||||
return new DefaultFeignLoggerFactory(this.logger);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnClass(name = "org.springframework.data.domain.Page")
|
||||
public Module pageJacksonModule() {
|
||||
return new PageJacksonModule();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnClass(name = "org.springframework.data.domain.Page")
|
||||
public Module sortModule() {
|
||||
return new SortJacksonModule();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(FeignClientConfigurer.class)
|
||||
public FeignClientConfigurer feignClientConfigurer() {
|
||||
@@ -181,4 +168,39 @@ public class FeignClientsConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@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")
|
||||
protected static class CircuitBreakerPresentFeignBuilderConfiguration {
|
||||
|
||||
@Bean
|
||||
@Scope("prototype")
|
||||
@ConditionalOnMissingBean({ Feign.Builder.class, CircuitBreakerFactory.class })
|
||||
public Feign.Builder defaultFeignBuilder(Retryer retryer) {
|
||||
return Feign.builder().retryer(retryer);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@Scope("prototype")
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnBean(CircuitBreakerFactory.class)
|
||||
public Feign.Builder circuitBreakerFeignBuilder() {
|
||||
return FeignCircuitBreaker.builder();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+76
-96
@@ -16,21 +16,23 @@
|
||||
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.util.Arrays;
|
||||
import java.util.HashSet;
|
||||
import java.util.List;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
|
||||
import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.BeanDefinitionHolder;
|
||||
import org.springframework.beans.factory.config.BeanExpressionContext;
|
||||
import org.springframework.beans.factory.config.BeanExpressionResolver;
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.beans.factory.support.AbstractBeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
|
||||
@@ -43,12 +45,7 @@ import org.springframework.core.annotation.AnnotationAttributes;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.io.ResourceLoader;
|
||||
import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.core.type.ClassMetadata;
|
||||
import org.springframework.core.type.classreading.MetadataReader;
|
||||
import org.springframework.core.type.classreading.MetadataReaderFactory;
|
||||
import org.springframework.core.type.filter.AbstractClassTestingTypeFilter;
|
||||
import org.springframework.core.type.filter.AnnotationTypeFilter;
|
||||
import org.springframework.core.type.filter.TypeFilter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
@@ -58,6 +55,8 @@ import org.springframework.util.StringUtils;
|
||||
* @author Jakub Narloch
|
||||
* @author Venil Noronha
|
||||
* @author Gang Li
|
||||
* @author Michal Domagala
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
|
||||
|
||||
@@ -158,53 +157,39 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
||||
}
|
||||
|
||||
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
|
||||
ClassPathScanningCandidateComponentProvider scanner = getScanner();
|
||||
scanner.setResourceLoader(this.resourceLoader);
|
||||
|
||||
Set<String> basePackages;
|
||||
|
||||
LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
|
||||
Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
|
||||
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(FeignClient.class);
|
||||
final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients");
|
||||
if (clients == null || clients.length == 0) {
|
||||
scanner.addIncludeFilter(annotationTypeFilter);
|
||||
basePackages = getBasePackages(metadata);
|
||||
ClassPathScanningCandidateComponentProvider scanner = getScanner();
|
||||
scanner.setResourceLoader(this.resourceLoader);
|
||||
scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));
|
||||
Set<String> basePackages = getBasePackages(metadata);
|
||||
for (String basePackage : basePackages) {
|
||||
candidateComponents.addAll(scanner.findCandidateComponents(basePackage));
|
||||
}
|
||||
}
|
||||
else {
|
||||
final Set<String> clientClasses = new HashSet<>();
|
||||
basePackages = new HashSet<>();
|
||||
for (Class<?> clazz : clients) {
|
||||
basePackages.add(ClassUtils.getPackageName(clazz));
|
||||
clientClasses.add(clazz.getCanonicalName());
|
||||
candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
|
||||
}
|
||||
AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() {
|
||||
@Override
|
||||
protected boolean match(ClassMetadata metadata) {
|
||||
String cleaned = metadata.getClassName().replaceAll("\\$", ".");
|
||||
return clientClasses.contains(cleaned);
|
||||
}
|
||||
};
|
||||
scanner.addIncludeFilter(new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter)));
|
||||
}
|
||||
|
||||
for (String basePackage : basePackages) {
|
||||
Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);
|
||||
for (BeanDefinition candidateComponent : candidateComponents) {
|
||||
if (candidateComponent instanceof AnnotatedBeanDefinition) {
|
||||
// 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");
|
||||
for (BeanDefinition candidateComponent : candidateComponents) {
|
||||
if (candidateComponent instanceof AnnotatedBeanDefinition) {
|
||||
// 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");
|
||||
|
||||
Map<String, Object> attributes = annotationMetadata
|
||||
.getAnnotationAttributes(FeignClient.class.getCanonicalName());
|
||||
Map<String, Object> attributes = annotationMetadata
|
||||
.getAnnotationAttributes(FeignClient.class.getCanonicalName());
|
||||
|
||||
String name = getClientName(attributes);
|
||||
registerClientConfiguration(registry, name, attributes.get("configuration"));
|
||||
String name = getClientName(attributes);
|
||||
registerClientConfiguration(registry, name, attributes.get("configuration"));
|
||||
|
||||
registerFeignClient(registry, annotationMetadata, attributes);
|
||||
}
|
||||
registerFeignClient(registry, annotationMetadata, attributes);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -212,23 +197,40 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
||||
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata,
|
||||
Map<String, Object> attributes) {
|
||||
String className = annotationMetadata.getClassName();
|
||||
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
|
||||
validate(attributes);
|
||||
definition.addPropertyValue("url", getUrl(attributes));
|
||||
definition.addPropertyValue("path", getPath(attributes));
|
||||
Class clazz = ClassUtils.resolveClassName(className, null);
|
||||
ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
|
||||
? (ConfigurableBeanFactory) registry : null;
|
||||
String contextId = getContextId(beanFactory, attributes);
|
||||
String name = getName(attributes);
|
||||
definition.addPropertyValue("name", name);
|
||||
String contextId = getContextId(attributes);
|
||||
definition.addPropertyValue("contextId", contextId);
|
||||
definition.addPropertyValue("type", className);
|
||||
definition.addPropertyValue("decode404", attributes.get("decode404"));
|
||||
definition.addPropertyValue("fallback", attributes.get("fallback"));
|
||||
definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
|
||||
FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
|
||||
factoryBean.setBeanFactory(beanFactory);
|
||||
factoryBean.setName(name);
|
||||
factoryBean.setContextId(contextId);
|
||||
factoryBean.setType(clazz);
|
||||
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {
|
||||
factoryBean.setUrl(getUrl(beanFactory, attributes));
|
||||
factoryBean.setPath(getPath(beanFactory, attributes));
|
||||
factoryBean.setDecode404(Boolean.parseBoolean(String.valueOf(attributes.get("decode404"))));
|
||||
Object fallback = attributes.get("fallback");
|
||||
if (fallback != null) {
|
||||
factoryBean.setFallback(fallback instanceof Class ? (Class<?>) fallback
|
||||
: ClassUtils.resolveClassName(fallback.toString(), null));
|
||||
}
|
||||
Object fallbackFactory = attributes.get("fallbackFactory");
|
||||
if (fallbackFactory != null) {
|
||||
factoryBean.setFallbackFactory(fallbackFactory instanceof Class ? (Class<?>) fallbackFactory
|
||||
: ClassUtils.resolveClassName(fallbackFactory.toString(), null));
|
||||
}
|
||||
return factoryBean.getObject();
|
||||
});
|
||||
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
|
||||
definition.setLazyInit(true);
|
||||
validate(attributes);
|
||||
|
||||
String alias = contextId + "FeignClient";
|
||||
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
|
||||
beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
|
||||
beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean);
|
||||
|
||||
// has a default, won't be null
|
||||
boolean primary = (Boolean) attributes.get("primary");
|
||||
@@ -253,6 +255,10 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
||||
}
|
||||
|
||||
/* for testing */ String getName(Map<String, Object> attributes) {
|
||||
return getName(null, attributes);
|
||||
}
|
||||
|
||||
String getName(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {
|
||||
String name = (String) attributes.get("serviceId");
|
||||
if (!StringUtils.hasText(name)) {
|
||||
name = (String) attributes.get("name");
|
||||
@@ -260,34 +266,42 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
||||
if (!StringUtils.hasText(name)) {
|
||||
name = (String) attributes.get("value");
|
||||
}
|
||||
name = resolve(name);
|
||||
name = resolve(beanFactory, name);
|
||||
return getName(name);
|
||||
}
|
||||
|
||||
private String getContextId(Map<String, Object> attributes) {
|
||||
private String getContextId(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {
|
||||
String contextId = (String) attributes.get("contextId");
|
||||
if (!StringUtils.hasText(contextId)) {
|
||||
return getName(attributes);
|
||||
}
|
||||
|
||||
contextId = resolve(contextId);
|
||||
contextId = resolve(beanFactory, contextId);
|
||||
return getName(contextId);
|
||||
}
|
||||
|
||||
private String resolve(String value) {
|
||||
private String resolve(ConfigurableBeanFactory beanFactory, String value) {
|
||||
if (StringUtils.hasText(value)) {
|
||||
return this.environment.resolvePlaceholders(value);
|
||||
if (beanFactory == null) {
|
||||
return this.environment.resolvePlaceholders(value);
|
||||
}
|
||||
BeanExpressionResolver resolver = beanFactory.getBeanExpressionResolver();
|
||||
String resolved = beanFactory.resolveEmbeddedValue(value);
|
||||
if (resolver == null) {
|
||||
return resolved;
|
||||
}
|
||||
return String.valueOf(resolver.evaluate(resolved, new BeanExpressionContext(beanFactory, null)));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private String getUrl(Map<String, Object> attributes) {
|
||||
String url = resolve((String) attributes.get("url"));
|
||||
private String getUrl(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {
|
||||
String url = resolve(beanFactory, (String) attributes.get("url"));
|
||||
return getUrl(url);
|
||||
}
|
||||
|
||||
private String getPath(Map<String, Object> attributes) {
|
||||
String path = resolve((String) attributes.get("path"));
|
||||
private String getPath(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {
|
||||
String path = resolve(beanFactory, (String) attributes.get("path"));
|
||||
return getPath(path);
|
||||
}
|
||||
|
||||
@@ -377,38 +391,4 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
/**
|
||||
* Helper class to create a {@link TypeFilter} that matches if all the delegates
|
||||
* match.
|
||||
*
|
||||
* @author Oliver Gierke
|
||||
*/
|
||||
private static class AllTypeFilter implements TypeFilter {
|
||||
|
||||
private final List<TypeFilter> delegates;
|
||||
|
||||
/**
|
||||
* Creates a new {@link AllTypeFilter} to match if all the given delegates match.
|
||||
* @param delegates must not be {@literal null}.
|
||||
*/
|
||||
AllTypeFilter(List<TypeFilter> delegates) {
|
||||
Assert.notNull(delegates, "This argument is required, it must not be null");
|
||||
this.delegates = delegates;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory)
|
||||
throws IOException {
|
||||
|
||||
for (TypeFilter filter : this.delegates) {
|
||||
if (!filter.match(metadataReader, metadataReaderFactory)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+14
-2
@@ -16,11 +16,14 @@
|
||||
|
||||
package org.springframework.cloud.openfeign.clientconfig;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Timer;
|
||||
import java.util.TimerTask;
|
||||
|
||||
import javax.annotation.PreDestroy;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.apache.http.client.config.RequestConfig;
|
||||
import org.apache.http.config.RegistryBuilder;
|
||||
import org.apache.http.conn.HttpClientConnectionManager;
|
||||
@@ -48,6 +51,8 @@ import org.springframework.context.annotation.Configuration;
|
||||
@ConditionalOnMissingBean(CloseableHttpClient.class)
|
||||
public class HttpClientFeignConfiguration {
|
||||
|
||||
private static final Log LOG = LogFactory.getLog(HttpClientFeignConfiguration.class);
|
||||
|
||||
private final Timer connectionManagerTimer = new Timer("FeignApacheHttpClientConfiguration.connectionManagerTimer",
|
||||
true);
|
||||
|
||||
@@ -103,10 +108,17 @@ public class HttpClientFeignConfiguration {
|
||||
}
|
||||
|
||||
@PreDestroy
|
||||
public void destroy() throws Exception {
|
||||
public void destroy() {
|
||||
this.connectionManagerTimer.cancel();
|
||||
if (this.httpClient != null) {
|
||||
this.httpClient.close();
|
||||
try {
|
||||
this.httpClient.close();
|
||||
}
|
||||
catch (IOException e) {
|
||||
if (LOG.isErrorEnabled()) {
|
||||
LOG.error("Could not correctly close httpClient.");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+9
-5
@@ -25,7 +25,8 @@ 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.reactive.LoadBalancerProperties;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
|
||||
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -44,8 +45,10 @@ class DefaultFeignLoadBalancerConfiguration {
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@Conditional(OnRetryNotEnabledCondition.class)
|
||||
public Client feignClient(LoadBalancerClient loadBalancerClient, LoadBalancerProperties properties) {
|
||||
return new FeignBlockingLoadBalancerClient(new Client.Default(null, null), loadBalancerClient, properties);
|
||||
public Client feignClient(LoadBalancerClient loadBalancerClient, LoadBalancerProperties properties,
|
||||
LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||
return new FeignBlockingLoadBalancerClient(new Client.Default(null, null), loadBalancerClient, properties,
|
||||
loadBalancerClientFactory);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -55,9 +58,10 @@ class DefaultFeignLoadBalancerConfiguration {
|
||||
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true",
|
||||
matchIfMissing = true)
|
||||
public Client feignRetryClient(LoadBalancerClient loadBalancerClient,
|
||||
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties) {
|
||||
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties,
|
||||
LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||
return new RetryableFeignBlockingLoadBalancerClient(new Client.Default(null, null), loadBalancerClient,
|
||||
loadBalancedRetryFactory, properties);
|
||||
loadBalancedRetryFactory, properties, loadBalancerClientFactory);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+36
-7
@@ -19,6 +19,7 @@ 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;
|
||||
@@ -27,13 +28,22 @@ 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.DefaultRequestContext;
|
||||
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
|
||||
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
|
||||
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.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.
|
||||
@@ -41,6 +51,7 @@ import org.springframework.util.Assert;
|
||||
* @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);
|
||||
@@ -51,11 +62,14 @@ public class FeignBlockingLoadBalancerClient implements Client {
|
||||
|
||||
private final LoadBalancerProperties properties;
|
||||
|
||||
private final LoadBalancerClientFactory loadBalancerClientFactory;
|
||||
|
||||
public FeignBlockingLoadBalancerClient(Client delegate, LoadBalancerClient loadBalancerClient,
|
||||
LoadBalancerProperties properties) {
|
||||
LoadBalancerProperties properties, LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||
this.delegate = delegate;
|
||||
this.loadBalancerClient = loadBalancerClient;
|
||||
this.properties = properties;
|
||||
this.loadBalancerClientFactory = loadBalancerClientFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -64,21 +78,36 @@ public class FeignBlockingLoadBalancerClient implements Client {
|
||||
String serviceId = originalUri.getHost();
|
||||
Assert.state(serviceId != null, "Request URI does not contain a valid hostname: " + originalUri);
|
||||
String hint = getHint(serviceId);
|
||||
DefaultRequest<DefaultRequestContext> lbRequest = new DefaultRequest<>(
|
||||
new DefaultRequestContext(request, hint));
|
||||
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);
|
||||
if (instance == null) {
|
||||
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())
|
||||
.body(message, StandardCharsets.UTF_8).build();
|
||||
}
|
||||
String reconstructedUrl = loadBalancerClient.reconstructURI(instance, originalUri).toString();
|
||||
Request newRequest = Request.create(request.httpMethod(), reconstructedUrl, request.headers(), request.body(),
|
||||
Request newRequest = buildRequest(request, reconstructedUrl);
|
||||
return executeWithLoadBalancerLifecycleProcessing(delegate, options, newRequest, lbRequest, lbResponse,
|
||||
supportedLifecycleProcessors);
|
||||
}
|
||||
|
||||
protected Request buildRequest(Request request, String reconstructedUrl) {
|
||||
return Request.create(request.httpMethod(), reconstructedUrl, request.headers(), request.body(),
|
||||
request.charset(), request.requestTemplate());
|
||||
return delegate.execute(newRequest, options);
|
||||
}
|
||||
|
||||
// Visible for Sleuth instrumentation
|
||||
|
||||
+6
-1
@@ -19,11 +19,15 @@ package org.springframework.cloud.openfeign.loadbalancer;
|
||||
import feign.Client;
|
||||
import feign.Feign;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
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.openfeign.FeignAutoConfiguration;
|
||||
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -37,8 +41,9 @@ import org.springframework.context.annotation.Import;
|
||||
* @since 2.2.0
|
||||
*/
|
||||
@ConditionalOnClass(Feign.class)
|
||||
@ConditionalOnBean(LoadBalancerClient.class)
|
||||
@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
|
||||
@AutoConfigureBefore(FeignAutoConfiguration.class)
|
||||
@AutoConfigureAfter({ BlockingLoadBalancerClientAutoConfiguration.class, LoadBalancerAutoConfiguration.class })
|
||||
@EnableConfigurationProperties(FeignHttpClientProperties.class)
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
// Order is important here, last should be the default, first should be optional
|
||||
|
||||
+8
-6
@@ -27,7 +27,8 @@ 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.reactive.LoadBalancerProperties;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
|
||||
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||
import org.springframework.cloud.openfeign.clientconfig.HttpClientFeignConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
@@ -43,7 +44,7 @@ import org.springframework.context.annotation.Import;
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(ApacheHttpClient.class)
|
||||
@ConditionalOnBean(LoadBalancerClient.class)
|
||||
@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
|
||||
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
|
||||
@Import(HttpClientFeignConfiguration.class)
|
||||
@EnableConfigurationProperties(LoadBalancerProperties.class)
|
||||
@@ -53,9 +54,9 @@ class HttpClientFeignLoadBalancerConfiguration {
|
||||
@ConditionalOnMissingBean
|
||||
@Conditional(OnRetryNotEnabledCondition.class)
|
||||
public Client feignClient(LoadBalancerClient loadBalancerClient, HttpClient httpClient,
|
||||
LoadBalancerProperties properties) {
|
||||
LoadBalancerProperties properties, LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||
ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
|
||||
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, properties);
|
||||
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, properties, loadBalancerClientFactory);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -65,10 +66,11 @@ class HttpClientFeignLoadBalancerConfiguration {
|
||||
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true",
|
||||
matchIfMissing = true)
|
||||
public Client feignRetryClient(LoadBalancerClient loadBalancerClient, HttpClient httpClient,
|
||||
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties) {
|
||||
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties,
|
||||
LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||
ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
|
||||
return new RetryableFeignBlockingLoadBalancerClient(delegate, loadBalancerClient, loadBalancedRetryFactory,
|
||||
properties);
|
||||
properties, loadBalancerClientFactory);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+95
@@ -0,0 +1,95 @@
|
||||
/*
|
||||
* 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);
|
||||
}
|
||||
|
||||
}
|
||||
+8
-6
@@ -26,7 +26,8 @@ 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.reactive.LoadBalancerProperties;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
|
||||
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||
import org.springframework.cloud.openfeign.clientconfig.OkHttpFeignConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
@@ -43,7 +44,7 @@ import org.springframework.context.annotation.Import;
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(OkHttpClient.class)
|
||||
@ConditionalOnProperty("feign.okhttp.enabled")
|
||||
@ConditionalOnBean(LoadBalancerClient.class)
|
||||
@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
|
||||
@Import(OkHttpFeignConfiguration.class)
|
||||
@EnableConfigurationProperties(LoadBalancerProperties.class)
|
||||
class OkHttpFeignLoadBalancerConfiguration {
|
||||
@@ -52,9 +53,9 @@ class OkHttpFeignLoadBalancerConfiguration {
|
||||
@ConditionalOnMissingBean
|
||||
@Conditional(OnRetryNotEnabledCondition.class)
|
||||
public Client feignClient(okhttp3.OkHttpClient okHttpClient, LoadBalancerClient loadBalancerClient,
|
||||
LoadBalancerProperties properties) {
|
||||
LoadBalancerProperties properties, LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||
OkHttpClient delegate = new OkHttpClient(okHttpClient);
|
||||
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, properties);
|
||||
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, properties, loadBalancerClientFactory);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -64,10 +65,11 @@ class OkHttpFeignLoadBalancerConfiguration {
|
||||
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true",
|
||||
matchIfMissing = true)
|
||||
public Client feignRetryClient(LoadBalancerClient loadBalancerClient, okhttp3.OkHttpClient okHttpClient,
|
||||
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties) {
|
||||
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties,
|
||||
LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||
OkHttpClient delegate = new OkHttpClient(okHttpClient);
|
||||
return new RetryableFeignBlockingLoadBalancerClient(delegate, loadBalancerClient, loadBalancedRetryFactory,
|
||||
properties);
|
||||
properties, loadBalancerClientFactory);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+55
-20
@@ -23,6 +23,7 @@ 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;
|
||||
@@ -31,16 +32,22 @@ 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.client.loadbalancer.reactive.LoadBalancerProperties;
|
||||
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
import org.springframework.http.HttpRequest;
|
||||
@@ -51,6 +58,8 @@ 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
|
||||
* load-balanced with Spring Cloud LoadBalancer.
|
||||
@@ -58,6 +67,7 @@ import org.springframework.util.Assert;
|
||||
* @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);
|
||||
@@ -70,12 +80,16 @@ public class RetryableFeignBlockingLoadBalancerClient implements Client {
|
||||
|
||||
private final LoadBalancerProperties properties;
|
||||
|
||||
private final LoadBalancerClientFactory loadBalancerClientFactory;
|
||||
|
||||
public RetryableFeignBlockingLoadBalancerClient(Client delegate, LoadBalancerClient loadBalancerClient,
|
||||
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties) {
|
||||
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties,
|
||||
LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||
this.delegate = delegate;
|
||||
this.loadBalancerClient = loadBalancerClient;
|
||||
this.loadBalancedRetryFactory = loadBalancedRetryFactory;
|
||||
this.properties = properties;
|
||||
this.loadBalancerClientFactory = loadBalancerClientFactory;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -88,6 +102,14 @@ public class RetryableFeignBlockingLoadBalancerClient implements Client {
|
||||
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) {
|
||||
@@ -99,33 +121,41 @@ public class RetryableFeignBlockingLoadBalancerClient implements Client {
|
||||
+ "Reattempting service instance selection");
|
||||
}
|
||||
ServiceInstance previousServiceInstance = lbContext.getPreviousServiceInstance();
|
||||
String hint = getHint(serviceId);
|
||||
DefaultRequest<RetryableRequestContext> lbRequest = new DefaultRequest<>(
|
||||
new RetryableRequestContext(previousServiceInstance, request, hint));
|
||||
serviceInstance = loadBalancerClient.choose(serviceId, lbRequest);
|
||||
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", serviceInstance));
|
||||
LOG.debug(String.format("Selected service instance: %s", retrievedServiceInstance));
|
||||
}
|
||||
lbContext.setServiceInstance(serviceInstance);
|
||||
lbContext.setServiceInstance(retrievedServiceInstance);
|
||||
}
|
||||
if (serviceInstance != null) {
|
||||
|
||||
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",
|
||||
serviceInstance));
|
||||
retrievedServiceInstance));
|
||||
}
|
||||
String reconstructedUrl = loadBalancerClient.reconstructURI(serviceInstance, originalUri)
|
||||
String reconstructedUrl = loadBalancerClient.reconstructURI(retrievedServiceInstance, originalUri)
|
||||
.toString();
|
||||
feignRequest = Request.create(request.httpMethod(), reconstructedUrl, request.headers(),
|
||||
request.body(), request.charset(), request.requestTemplate());
|
||||
feignRequest = buildRequest(request, reconstructedUrl);
|
||||
}
|
||||
}
|
||||
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);
|
||||
org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse = new DefaultResponse(
|
||||
retrievedServiceInstance);
|
||||
Response response = LoadBalancerUtils.executeWithLoadBalancerLifecycleProcessing(delegate, options,
|
||||
feignRequest, lbRequest, lbResponse, supportedLifecycleProcessors,
|
||||
retrievedServiceInstance != null);
|
||||
int responseStatus = response.status();
|
||||
if (retryPolicy != null && retryPolicy.retryableStatusCode(responseStatus)) {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
@@ -143,6 +173,11 @@ 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());
|
||||
}
|
||||
|
||||
private RetryTemplate buildRetryTemplate(String serviceId, Request request, LoadBalancedRetryPolicy retryPolicy) {
|
||||
RetryTemplate retryTemplate = new RetryTemplate();
|
||||
BackOffPolicy backOffPolicy = this.loadBalancedRetryFactory.createBackOffPolicy(serviceId);
|
||||
|
||||
+182
@@ -0,0 +1,182 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
}
|
||||
+4
-4
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
* 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.
|
||||
@@ -26,16 +26,16 @@ import feign.Response;
|
||||
import feign.codec.Decoder;
|
||||
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
/**
|
||||
* Decoder adds compatibility for Spring MVC's ResponseEntity to any other decoder via
|
||||
* composition.
|
||||
*
|
||||
* @author chad jaros
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
public class ResponseEntityDecoder implements Decoder {
|
||||
|
||||
@@ -80,7 +80,7 @@ public class ResponseEntityDecoder implements Decoder {
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> ResponseEntity<T> createResponse(Object instance, Response response) {
|
||||
|
||||
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
for (String key : response.headers().keySet()) {
|
||||
headers.put(key, new LinkedList<>(response.headers().get(key)));
|
||||
}
|
||||
|
||||
+15
-19
@@ -78,6 +78,7 @@ import static org.springframework.core.annotation.AnnotatedElementUtils.findMerg
|
||||
* @author Aaron Whiteside
|
||||
* @author Artyom Romanenko
|
||||
* @author Darren Foong
|
||||
* @author Ram Anaswara
|
||||
*/
|
||||
public class SpringMvcContract extends Contract.BaseContract implements ResourceLoaderAware {
|
||||
|
||||
@@ -101,6 +102,8 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
|
||||
|
||||
private ResourceLoader resourceLoader = new DefaultResourceLoader();
|
||||
|
||||
private boolean decodeSlash;
|
||||
|
||||
public SpringMvcContract() {
|
||||
this(Collections.emptyList());
|
||||
}
|
||||
@@ -111,6 +114,11 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
|
||||
|
||||
public SpringMvcContract(List<AnnotatedParameterProcessor> annotatedParameterProcessors,
|
||||
ConversionService conversionService) {
|
||||
this(annotatedParameterProcessors, conversionService, true);
|
||||
}
|
||||
|
||||
public SpringMvcContract(List<AnnotatedParameterProcessor> annotatedParameterProcessors,
|
||||
ConversionService conversionService, boolean decodeSlash) {
|
||||
Assert.notNull(annotatedParameterProcessors, "Parameter processors can not be null.");
|
||||
Assert.notNull(conversionService, "ConversionService can not be null.");
|
||||
|
||||
@@ -120,6 +128,7 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
|
||||
annotatedArgumentProcessors = toAnnotatedArgumentProcessorMap(processors);
|
||||
this.conversionService = conversionService;
|
||||
convertingExpanderFactory = new ConvertingExpanderFactory(conversionService);
|
||||
this.decodeSlash = decodeSlash;
|
||||
}
|
||||
|
||||
private static TypeDescriptor createTypeDescriptor(Method method, int paramIndex) {
|
||||
@@ -171,6 +180,9 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
|
||||
pathValue = "/" + pathValue;
|
||||
}
|
||||
data.template().uri(pathValue);
|
||||
if (data.template().decodeSlash() != decodeSlash) {
|
||||
data.template().decodeSlash(decodeSlash);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -232,6 +244,9 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
|
||||
pathValue = "/" + pathValue;
|
||||
}
|
||||
data.template().uri(pathValue, true);
|
||||
if (data.template().decodeSlash() != decodeSlash) {
|
||||
data.template().decodeSlash(decodeSlash);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -386,25 +401,6 @@ 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;
|
||||
|
||||
+2
-2
@@ -3,9 +3,9 @@
|
||||
],
|
||||
"properties": [
|
||||
{
|
||||
"name": "feign.hystrix.enabled",
|
||||
"name": "feign.circuitbreaker.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "If true, an OpenFeign client will be wrapped with a Hystrix circuit breaker.",
|
||||
"description": "If true, an OpenFeign client will be wrapped with a Spring Cloud CircuitBreaker circuit breaker.",
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
|
||||
+1
-1
@@ -29,7 +29,7 @@ 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.reactive.LoadBalancerProperties;
|
||||
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;
|
||||
|
||||
+12
-9
@@ -26,10 +26,10 @@ import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.openfeign.clientconfig.FeignClientConfigurer;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
@@ -49,18 +49,19 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
"feign.client.config.default.requestInterceptors[1]=org.springframework.cloud.openfeign.FeignClientUsingPropertiesTests.BarRequestInterceptor" })
|
||||
public class FeignClientUsingConfigurerTest {
|
||||
|
||||
private static final String BEAN_NAME_PREFIX = "&org.springframework.cloud.openfeign.FeignClientUsingConfigurerTest$";
|
||||
private static final String BEAN_NAME_PREFIX = "org.springframework.cloud.openfeign.FeignClientUsingConfigurerTest$";
|
||||
|
||||
@Autowired
|
||||
private ApplicationContext applicationContext;
|
||||
private ConfigurableListableBeanFactory beanFactory;
|
||||
|
||||
@Autowired
|
||||
private FeignContext context;
|
||||
|
||||
@Test
|
||||
public void testFeignClient() {
|
||||
FeignClientFactoryBean factoryBean = (FeignClientFactoryBean) applicationContext
|
||||
.getBean(BEAN_NAME_PREFIX + "TestFeignClient");
|
||||
FeignClientFactoryBean factoryBean = (FeignClientFactoryBean) beanFactory
|
||||
.getBeanDefinition(BEAN_NAME_PREFIX + "TestFeignClient")
|
||||
.getAttribute("feignClientsRegistrarFactoryBean");
|
||||
Feign.Builder builder = factoryBean.feign(context);
|
||||
|
||||
List<RequestInterceptor> interceptors = (List) getBuilderValue(builder, "requestInterceptors");
|
||||
@@ -77,8 +78,9 @@ public class FeignClientUsingConfigurerTest {
|
||||
|
||||
@Test
|
||||
public void testNoInheritFeignClient() {
|
||||
FeignClientFactoryBean factoryBean = (FeignClientFactoryBean) applicationContext
|
||||
.getBean(BEAN_NAME_PREFIX + "NoInheritFeignClient");
|
||||
FeignClientFactoryBean factoryBean = (FeignClientFactoryBean) beanFactory
|
||||
.getBeanDefinition(BEAN_NAME_PREFIX + "NoInheritFeignClient")
|
||||
.getAttribute("feignClientsRegistrarFactoryBean");
|
||||
Feign.Builder builder = factoryBean.feign(context);
|
||||
|
||||
List<RequestInterceptor> interceptors = (List) getBuilderValue(builder, "requestInterceptors");
|
||||
@@ -89,8 +91,9 @@ public class FeignClientUsingConfigurerTest {
|
||||
|
||||
@Test
|
||||
public void testNoInheritFeignClient_ignoreProperties() {
|
||||
FeignClientFactoryBean factoryBean = (FeignClientFactoryBean) applicationContext
|
||||
.getBean(BEAN_NAME_PREFIX + "NoInheritFeignClient");
|
||||
FeignClientFactoryBean factoryBean = (FeignClientFactoryBean) beanFactory
|
||||
.getBeanDefinition(BEAN_NAME_PREFIX + "NoInheritFeignClient")
|
||||
.getAttribute("feignClientsRegistrarFactoryBean");
|
||||
Feign.Builder builder = factoryBean.feign(context);
|
||||
|
||||
assertThat(getBuilderValue(builder, "logLevel")).as("log level not set").isEqualTo(Logger.Level.HEADERS);
|
||||
|
||||
+80
-7
@@ -22,9 +22,15 @@ import java.lang.reflect.Proxy;
|
||||
import java.lang.reflect.Type;
|
||||
import java.net.SocketTimeoutException;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
@@ -55,8 +61,11 @@ import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -66,6 +75,7 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
|
||||
/**
|
||||
* @author Eko Kurniawan Khannedy
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Ilia Ilinykh
|
||||
*/
|
||||
@SuppressWarnings("FieldMayBeFinal")
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@@ -91,6 +101,10 @@ public class FeignClientUsingPropertiesTests {
|
||||
|
||||
private FeignClientFactoryBean formFactoryBean;
|
||||
|
||||
private FeignClientFactoryBean defaultHeadersAndQuerySingleParamsFeignClientFactoryBean;
|
||||
|
||||
private FeignClientFactoryBean defaultHeadersAndQueryMultipleParamsFeignClientFactoryBean;
|
||||
|
||||
public FeignClientUsingPropertiesTests() {
|
||||
fooFactoryBean = new FeignClientFactoryBean();
|
||||
fooFactoryBean.setContextId("foo");
|
||||
@@ -107,6 +121,14 @@ public class FeignClientUsingPropertiesTests {
|
||||
formFactoryBean = new FeignClientFactoryBean();
|
||||
formFactoryBean.setContextId("form");
|
||||
formFactoryBean.setType(FeignClientFactoryBean.class);
|
||||
|
||||
this.defaultHeadersAndQuerySingleParamsFeignClientFactoryBean = new FeignClientFactoryBean();
|
||||
this.defaultHeadersAndQuerySingleParamsFeignClientFactoryBean.setContextId("singleValue");
|
||||
this.defaultHeadersAndQuerySingleParamsFeignClientFactoryBean.setType(FeignClientFactoryBean.class);
|
||||
|
||||
this.defaultHeadersAndQueryMultipleParamsFeignClientFactoryBean = new FeignClientFactoryBean();
|
||||
this.defaultHeadersAndQueryMultipleParamsFeignClientFactoryBean.setContextId("multipleValue");
|
||||
this.defaultHeadersAndQueryMultipleParamsFeignClientFactoryBean.setType(FeignClientFactoryBean.class);
|
||||
}
|
||||
|
||||
public FooClient fooClient() {
|
||||
@@ -154,6 +176,30 @@ public class FeignClientUsingPropertiesTests {
|
||||
assertThat(response).isEqualTo("Data");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSingleValue() {
|
||||
List<String> response = singleValueClient().singleValue();
|
||||
assertThat(response).isEqualTo(Arrays.asList("header", "parameter"));
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testMultipleValue() {
|
||||
List<String> response = multipleValueClient().multipleValue();
|
||||
assertThat(response).isEqualTo(Arrays.asList("header1", "header2", "parameter1", "parameter2"));
|
||||
}
|
||||
|
||||
public SingleValueClient singleValueClient() {
|
||||
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)
|
||||
.target(MultipleValueClient.class, "http://localhost:" + this.port);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readTimeoutShouldWorkWhenConnectTimeoutNotSet() {
|
||||
FeignClientFactoryBean readTimeoutFactoryBean = new FeignClientFactoryBean();
|
||||
@@ -196,21 +242,21 @@ public class FeignClientUsingPropertiesTests {
|
||||
|
||||
protected interface FooClient {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/foo")
|
||||
@GetMapping(path = "/foo")
|
||||
String foo();
|
||||
|
||||
}
|
||||
|
||||
protected interface BarClient {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/bar")
|
||||
@GetMapping(path = "/bar")
|
||||
String bar();
|
||||
|
||||
}
|
||||
|
||||
protected interface UnwrapClient {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/bar") // intentionally /bar
|
||||
@GetMapping(path = "/bar") // intentionally /bar
|
||||
String unwrap() throws IOException;
|
||||
|
||||
}
|
||||
@@ -223,6 +269,20 @@ public class FeignClientUsingPropertiesTests {
|
||||
|
||||
}
|
||||
|
||||
protected interface SingleValueClient {
|
||||
|
||||
@GetMapping(path = "/singleValue")
|
||||
List<String> singleValue();
|
||||
|
||||
}
|
||||
|
||||
protected interface MultipleValueClient {
|
||||
|
||||
@GetMapping(path = "/multipleValue")
|
||||
List<String> multipleValue();
|
||||
|
||||
}
|
||||
|
||||
protected interface TimeoutClient {
|
||||
|
||||
@GetMapping("/timeouts")
|
||||
@@ -246,18 +306,31 @@ public class FeignClientUsingPropertiesTests {
|
||||
}
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/bar")
|
||||
@GetMapping(path = "/bar")
|
||||
public String bar() throws InterruptedException {
|
||||
Thread.sleep(2000L);
|
||||
TimeUnit.SECONDS.sleep(2);
|
||||
return "OK";
|
||||
}
|
||||
|
||||
@RequestMapping(value = "/form", method = RequestMethod.POST,
|
||||
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
@PostMapping(path = "/form", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
public String form(HttpServletRequest request) {
|
||||
return request.getParameter("form");
|
||||
}
|
||||
|
||||
@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());
|
||||
}
|
||||
|
||||
@GetMapping(path = "/multipleValue")
|
||||
public List<String> multipleValue(@RequestHeader List<String> multipleValueHeaders,
|
||||
@RequestParam List<String> multipleValueParameters) {
|
||||
return Stream.of(multipleValueHeaders, multipleValueParameters).flatMap(Collection::stream)
|
||||
.collect(Collectors.toList());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class FooRequestInterceptor implements RequestInterceptor {
|
||||
|
||||
+20
@@ -20,6 +20,7 @@ import java.util.Collections;
|
||||
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -28,10 +29,12 @@ import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
* @author Gang Li
|
||||
* @author Michal Domagala
|
||||
*/
|
||||
public class FeignClientsRegistrarTests {
|
||||
|
||||
@@ -89,6 +92,17 @@ public class FeignClientsRegistrarTests {
|
||||
new AnnotationConfigApplicationContext(FallbackFactoryTestConfig.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldPassSubLevelFeignClient() {
|
||||
AnnotationConfigApplicationContext config = new AnnotationConfigApplicationContext();
|
||||
((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")
|
||||
.doesNotThrowAnyException();
|
||||
|
||||
}
|
||||
|
||||
@FeignClient(name = "fallbackTestClient", url = "http://localhost:8080/", fallback = FallbackClient.class)
|
||||
protected interface FallbackClient {
|
||||
|
||||
@@ -120,4 +134,10 @@ public class FeignClientsRegistrarTests {
|
||||
|
||||
}
|
||||
|
||||
@EnableFeignClients(clients = { org.springframework.cloud.openfeign.feignclientsregistrar.TopLevelClient.class,
|
||||
org.springframework.cloud.openfeign.feignclientsregistrar.sub.SubLevelClient.class })
|
||||
protected static class TopLevelSubLevelTestConfig {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@SpringBootTest(classes = FeignHttpClientUrlTests.TestConfig.class, webEnvironment = DEFINED_PORT,
|
||||
value = { "spring.application.name=feignclienturltest", "feign.hystrix.enabled=false",
|
||||
value = { "spring.application.name=feignclienturltest", "feign.circuitbreaker.enabled=false",
|
||||
"feign.okhttp.enabled=false", "spring.cloud.loadbalancer.retry.enabled=false" })
|
||||
@DirtiesContext
|
||||
class FeignHttpClientUrlTests {
|
||||
|
||||
+19
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
* 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.
|
||||
@@ -32,7 +32,9 @@ import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
@@ -45,6 +47,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringBootTest(classes = SpringDecoderTests.Application.class, webEnvironment = WebEnvironment.RANDOM_PORT,
|
||||
@@ -142,6 +145,13 @@ public class SpringDecoderTests extends FeignClientFactoryBean {
|
||||
assertThat(response.getBody()).as("response body was not null").isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
// Issue: https://github.com/spring-cloud/spring-cloud-openfeign/issues/456
|
||||
public void testResponseEntityHeaders() {
|
||||
ResponseEntity<String> response = testClient().getContentType();
|
||||
assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
|
||||
}
|
||||
|
||||
protected interface TestClient {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/helloresponse")
|
||||
@@ -165,6 +175,9 @@ public class SpringDecoderTests extends FeignClientFactoryBean {
|
||||
@GetMapping("/helloWildcard")
|
||||
ResponseEntity<?> getWildcard();
|
||||
|
||||
@GetMapping(path = "/contentType", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
ResponseEntity<String> getContentType();
|
||||
|
||||
}
|
||||
|
||||
public static class Hello {
|
||||
@@ -252,6 +265,11 @@ public class SpringDecoderTests extends FeignClientFactoryBean {
|
||||
return ResponseEntity.ok(new Hello("wildcard"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<String> getContentType() {
|
||||
return ResponseEntity.ok().header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).body("test");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+242
@@ -0,0 +1,242 @@
|
||||
/*
|
||||
* 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.circuitbreaker;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||
import org.springframework.cloud.client.circuitbreaker.ConfigBuilder;
|
||||
import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.cloud.openfeign.FallbackFactory;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.util.SocketUtils;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringBootTest(classes = CircuitBreakerTests.Application.class, webEnvironment = WebEnvironment.DEFINED_PORT, value = {
|
||||
"spring.application.name=springcircuittest", "spring.jmx.enabled=false", "feign.circuitbreaker.enabled=true" })
|
||||
@DirtiesContext
|
||||
public class CircuitBreakerTests {
|
||||
|
||||
@Autowired
|
||||
MyCircuitBreaker myCircuitBreaker;
|
||||
|
||||
@Autowired
|
||||
TestClient testClient;
|
||||
|
||||
@Autowired
|
||||
TestClientWithFactory testClientWithFactory;
|
||||
|
||||
@LocalServerPort
|
||||
private int port = 0;
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeClass() {
|
||||
System.setProperty("server.port", String.valueOf(SocketUtils.findAvailableTcpPort()));
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void afterClass() {
|
||||
System.clearProperty("server.port");
|
||||
}
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
this.myCircuitBreaker.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleTypeWithFallback() {
|
||||
Hello hello = testClient.getHello();
|
||||
|
||||
assertThat(hello).as("hello was null").isNotNull();
|
||||
assertThat(hello).as("first hello didn't match").isEqualTo(new Hello("hello world 1"));
|
||||
assertThat(myCircuitBreaker.runWasCalled).as("Circuit Breaker was called").isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test404WithFallback() {
|
||||
assertThat(testClient.getException()).isEqualTo("Fixed response");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleTypeWithFallbackFactory() {
|
||||
Hello hello = testClientWithFactory.getHello();
|
||||
|
||||
assertThat(hello).as("hello was null").isNotNull();
|
||||
assertThat(hello).as("first hello didn't match").isEqualTo(new Hello("hello world 1"));
|
||||
assertThat(myCircuitBreaker.runWasCalled).as("Circuit Breaker was called").isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test404WithFallbackFactory() {
|
||||
assertThat(testClientWithFactory.getException()).isEqualTo("Fixed response");
|
||||
}
|
||||
|
||||
// tag::client_with_fallback[]
|
||||
@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";
|
||||
}
|
||||
|
||||
}
|
||||
// end::client_with_fallback[]
|
||||
|
||||
// tag::client_with_fallback_factory[]
|
||||
@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";
|
||||
}
|
||||
|
||||
}
|
||||
// end::client_with_fallback_factory[]
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableAutoConfiguration
|
||||
@RestController
|
||||
@EnableFeignClients(clients = { TestClient.class, TestClientWithFactory.class })
|
||||
@Import(NoSecurityConfiguration.class)
|
||||
protected static class Application implements TestClient {
|
||||
|
||||
static final Log log = LogFactory.getLog(Application.class);
|
||||
|
||||
@Bean
|
||||
MyCircuitBreaker myCircuitBreaker() {
|
||||
return new MyCircuitBreaker();
|
||||
}
|
||||
|
||||
@Bean
|
||||
CircuitBreakerFactory circuitBreakerFactory(MyCircuitBreaker myCircuitBreaker) {
|
||||
return new CircuitBreakerFactory() {
|
||||
@Override
|
||||
public CircuitBreaker create(String id) {
|
||||
log.info("Creating a circuit breaker with id [" + id + "]");
|
||||
return myCircuitBreaker;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ConfigBuilder configBuilder(String id) {
|
||||
return Object::new;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureDefault(Function defaultConfiguration) {
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Hello getHello() {
|
||||
return new Hello("hello world 1");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getException() {
|
||||
throw new IllegalStateException("BOOM!");
|
||||
}
|
||||
|
||||
@Bean
|
||||
Fallback fallback() {
|
||||
return new Fallback();
|
||||
}
|
||||
|
||||
@Bean
|
||||
TestFallbackFactory testFallbackFactory() {
|
||||
return new TestFallbackFactory();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+155
@@ -0,0 +1,155 @@
|
||||
/*
|
||||
* 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.circuitbreaker;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||
import org.springframework.cloud.client.circuitbreaker.ConfigBuilder;
|
||||
import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
|
||||
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.util.SocketUtils;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Tests for Feign calls with CircuitBreaker, without fallbacks.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@SpringBootTest(classes = CircuitBreakerWithNoFallbackTests.Application.class,
|
||||
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT,
|
||||
value = { "spring.application.name=springcircuittest", "spring.jmx.enabled=false",
|
||||
"feign.circuitbreaker.enabled=true" })
|
||||
@DirtiesContext
|
||||
public class CircuitBreakerWithNoFallbackTests {
|
||||
|
||||
@Autowired
|
||||
MyCircuitBreaker myCircuitBreaker;
|
||||
|
||||
@Autowired
|
||||
CircuitBreakerTestClient testClient;
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeClass() {
|
||||
System.setProperty("server.port", String.valueOf(SocketUtils.findAvailableTcpPort()));
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void afterClass() {
|
||||
System.clearProperty("server.port");
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
this.myCircuitBreaker.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleTypeWithFallback() {
|
||||
Hello hello = testClient.getHello();
|
||||
|
||||
assertThat(hello).as("hello was null").isNotNull();
|
||||
assertThat(hello).as("first hello didn't match").isEqualTo(new Hello("hello world 1"));
|
||||
assertThat(myCircuitBreaker.runWasCalled).as("Circuit Breaker was called").isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test404WithoutFallback() {
|
||||
assertThatThrownBy(() -> testClient.getException()).isInstanceOf(NoFallbackAvailableException.class);
|
||||
}
|
||||
|
||||
@FeignClient(name = "test", url = "http://localhost:${server.port}/")
|
||||
protected interface CircuitBreakerTestClient {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
Hello getHello();
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hellonotfound")
|
||||
String getException();
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableAutoConfiguration
|
||||
@RestController
|
||||
@EnableFeignClients(clients = { CircuitBreakerTestClient.class })
|
||||
@Import(NoSecurityConfiguration.class)
|
||||
protected static class Application implements CircuitBreakerTestClient {
|
||||
|
||||
static final Log log = LogFactory.getLog(Application.class);
|
||||
|
||||
@Bean
|
||||
MyCircuitBreaker myCircuitBreaker() {
|
||||
return new MyCircuitBreaker();
|
||||
}
|
||||
|
||||
@Bean
|
||||
CircuitBreakerFactory circuitBreakerFactory(MyCircuitBreaker myCircuitBreaker) {
|
||||
return new CircuitBreakerFactory() {
|
||||
@Override
|
||||
public CircuitBreaker create(String id) {
|
||||
log.info("Creating a circuit breaker with id [" + id + "]");
|
||||
return myCircuitBreaker;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ConfigBuilder configBuilder(String id) {
|
||||
return Object::new;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureDefault(Function defaultConfiguration) {
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Hello getHello() {
|
||||
return new Hello("hello world 1");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getException() {
|
||||
throw new IllegalStateException("BOOM!");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* 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.circuitbreaker;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
class Hello {
|
||||
|
||||
private String message;
|
||||
|
||||
public Hello() {
|
||||
}
|
||||
|
||||
public Hello(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return this.message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Hello that = (Hello) o;
|
||||
return Objects.equals(this.message, that.message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.message);
|
||||
}
|
||||
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* 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.circuitbreaker;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
|
||||
import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException;
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
class MyCircuitBreaker implements CircuitBreaker {
|
||||
|
||||
AtomicBoolean runWasCalled = new AtomicBoolean();
|
||||
|
||||
@Override
|
||||
public <T> T run(Supplier<T> toRun) {
|
||||
try {
|
||||
this.runWasCalled.set(true);
|
||||
return toRun.get();
|
||||
}
|
||||
catch (Throwable throwable) {
|
||||
throw new NoFallbackAvailableException("No fallback available.", throwable);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T run(Supplier<T> toRun, Function<Throwable, T> fallback) {
|
||||
try {
|
||||
return run(toRun);
|
||||
}
|
||||
catch (Throwable throwable) {
|
||||
return fallback.apply(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
this.runWasCalled.set(false);
|
||||
}
|
||||
|
||||
}
|
||||
+5
-3
@@ -25,8 +25,10 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
|
||||
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
|
||||
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.cloud.openfeign.encoding.app.client.InvoiceClient;
|
||||
import org.springframework.cloud.openfeign.encoding.app.domain.Invoice;
|
||||
@@ -34,7 +36,6 @@ import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
@@ -86,8 +87,9 @@ public class FeignAcceptEncodingTests {
|
||||
private int port = 0;
|
||||
|
||||
@Bean
|
||||
public ServiceInstanceListSupplier staticServiceInstanceListSupplier(Environment env) {
|
||||
return ServiceInstanceListSupplier.fixed(env).instance(port, "local").build();
|
||||
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
|
||||
return ServiceInstanceListSuppliers.from("local",
|
||||
new DefaultServiceInstance("local-1", "local", "localhost", port, false));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
-6
@@ -25,8 +25,10 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
|
||||
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
|
||||
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.cloud.openfeign.encoding.app.client.InvoiceClient;
|
||||
import org.springframework.cloud.openfeign.encoding.app.domain.Invoice;
|
||||
@@ -34,7 +36,6 @@ import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
@@ -48,9 +49,7 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
|
||||
* @author Jakub Narloch
|
||||
*/
|
||||
@SpringBootTest(classes = FeignContentEncodingTests.Application.class, webEnvironment = RANDOM_PORT,
|
||||
value = { "feign.compression.request.enabled=true",
|
||||
"hystrix.command.default.execution.isolation.strategy=SEMAPHORE",
|
||||
"ribbon.OkToRetryOnAllOperations=false" })
|
||||
value = { "feign.compression.request.enabled=true", "ribbon.OkToRetryOnAllOperations=false" })
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
public class FeignContentEncodingTests {
|
||||
|
||||
@@ -89,8 +88,9 @@ public class FeignContentEncodingTests {
|
||||
private int port = 0;
|
||||
|
||||
@Bean
|
||||
public ServiceInstanceListSupplier staticServiceInstanceListSupplier(Environment env) {
|
||||
return ServiceInstanceListSupplier.fixed(env).instance(port, "local").build();
|
||||
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
|
||||
return ServiceInstanceListSuppliers.from("local",
|
||||
new DefaultServiceInstance("local-1", "local", "localhost", port, false));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+7
-8
@@ -26,17 +26,17 @@ import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
|
||||
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
|
||||
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
|
||||
import org.springframework.cloud.openfeign.encoding.app.client.InvoiceClient;
|
||||
import org.springframework.cloud.openfeign.encoding.app.domain.Invoice;
|
||||
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
@@ -55,9 +55,7 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
|
||||
* @author Charlie Mordant.
|
||||
*/
|
||||
@SpringBootTest(classes = FeignPageableEncodingTests.Application.class, webEnvironment = RANDOM_PORT,
|
||||
value = { "feign.compression.request.enabled=true",
|
||||
"hystrix.command.default.execution.isolation.strategy=SEMAPHORE",
|
||||
"ribbon.OkToRetryOnAllOperations=false" })
|
||||
value = { "feign.compression.request.enabled=true", "feign.autoconfiguration.jackson.enabled=true" })
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
public class FeignPageableEncodingTests {
|
||||
|
||||
@@ -93,7 +91,7 @@ public class FeignPageableEncodingTests {
|
||||
@SpringBootApplication(scanBasePackages = "org.springframework.cloud.openfeign.encoding.app",
|
||||
exclude = { RepositoryRestMvcAutoConfiguration.class })
|
||||
@EnableSpringDataWebSupport
|
||||
@Import({ NoSecurityConfiguration.class, FeignClientsConfiguration.class })
|
||||
@Import({ NoSecurityConfiguration.class })
|
||||
public static class Application {
|
||||
|
||||
}
|
||||
@@ -105,8 +103,9 @@ public class FeignPageableEncodingTests {
|
||||
private int port = 0;
|
||||
|
||||
@Bean
|
||||
public ServiceInstanceListSupplier staticServiceInstanceListSupplier(Environment env) {
|
||||
return ServiceInstanceListSupplier.fixed(env).instance(port, "local").build();
|
||||
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
|
||||
return ServiceInstanceListSuppliers.from("local",
|
||||
new DefaultServiceInstance("local-1", "local", "localhost", port, false));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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.feignclientsregistrar;
|
||||
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
|
||||
/**
|
||||
* @author Michal Domagala
|
||||
*/
|
||||
|
||||
@FeignClient("top-level")
|
||||
public interface TopLevelClient {
|
||||
|
||||
}
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
/*
|
||||
* 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.feignclientsregistrar.sub;
|
||||
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
|
||||
/**
|
||||
* @author Michal Domagala
|
||||
*/
|
||||
|
||||
@FeignClient("sub-level")
|
||||
public interface SubLevelClient {
|
||||
|
||||
}
|
||||
+5
-3
@@ -19,13 +19,14 @@ package org.springframework.cloud.openfeign.hateoas.app;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration;
|
||||
import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
|
||||
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
|
||||
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.env.Environment;
|
||||
|
||||
/**
|
||||
* Test HATEOAS application.
|
||||
@@ -49,8 +50,9 @@ class LocalHalClientConfiguration {
|
||||
private int port = 0;
|
||||
|
||||
@Bean
|
||||
public ServiceInstanceListSupplier staticServiceInstanceListSupplier(Environment env) {
|
||||
return ServiceInstanceListSupplier.fixed(env).instance(port, "local").build();
|
||||
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
|
||||
return ServiceInstanceListSuppliers.from("local",
|
||||
new DefaultServiceInstance("local-1", "local", "localhost", port, false));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -71,7 +71,7 @@ public class FeignClientValidationTests {
|
||||
@EnableFeignClients(clients = NameAndServiceIdConfiguration.Client.class)
|
||||
protected static class NameAndServiceIdConfiguration {
|
||||
|
||||
@FeignClient(name = "bar", serviceId = "foo")
|
||||
@FeignClient(name = "bar")
|
||||
interface Client {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/")
|
||||
|
||||
+100
-2
@@ -23,6 +23,8 @@ import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import feign.Client;
|
||||
import feign.Request;
|
||||
@@ -34,8 +36,13 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
|
||||
import org.springframework.cloud.client.loadbalancer.CompletionContext;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle;
|
||||
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.blocking.client.BlockingLoadBalancerClient;
|
||||
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -66,8 +73,12 @@ class FeignBlockingLoadBalancerClientTests {
|
||||
|
||||
private BlockingLoadBalancerClient loadBalancerClient = mock(BlockingLoadBalancerClient.class);
|
||||
|
||||
private final LoadBalancerClientFactory loadBalancerClientFactory = mock(LoadBalancerClientFactory.class);
|
||||
|
||||
private final LoadBalancerProperties loadBalancerProperties = new LoadBalancerProperties();
|
||||
|
||||
private FeignBlockingLoadBalancerClient feignBlockingLoadBalancerClient = new FeignBlockingLoadBalancerClient(
|
||||
delegate, loadBalancerClient, new LoadBalancerProperties());
|
||||
delegate, loadBalancerClient, loadBalancerProperties, loadBalancerClientFactory);
|
||||
|
||||
@Test
|
||||
void shouldExtractServiceIdFromRequestUrl() throws IOException {
|
||||
@@ -121,6 +132,41 @@ class FeignBlockingLoadBalancerClientTests {
|
||||
assertThat(new String(actualRequest.body())).isEqualTo("hello");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExecuteLoadBalancerLifecycleCallbacks() throws IOException {
|
||||
Request request = testRequest();
|
||||
when(delegate.execute(any(), any())).thenReturn(Response.builder().status(200).request(request).build());
|
||||
Request.Options options = new Request.Options();
|
||||
String url = "http://127.0.0.1/path";
|
||||
ServiceInstance serviceInstance = new DefaultServiceInstance("test-1", "test", "test-host", 8888, false);
|
||||
when(loadBalancerClient.choose(eq("test"), any())).thenReturn(serviceInstance);
|
||||
when(loadBalancerClient.reconstructURI(serviceInstance, URI.create("http://test/path")))
|
||||
.thenReturn(URI.create(url));
|
||||
String callbackTestHint = "callbackTestHint";
|
||||
loadBalancerProperties.getHint().put("test", callbackTestHint);
|
||||
Map<String, LoadBalancerLifecycle> loadBalancerLifecycleBeans = new HashMap<>();
|
||||
loadBalancerLifecycleBeans.put("loadBalancerLifecycle", new TestLoadBalancerLifecycle());
|
||||
loadBalancerLifecycleBeans.put("anotherLoadBalancerLifecycle", new AnotherLoadBalancerLifecycle());
|
||||
when(loadBalancerClientFactory.getInstances("test", LoadBalancerLifecycle.class))
|
||||
.thenReturn(loadBalancerLifecycleBeans);
|
||||
|
||||
feignBlockingLoadBalancerClient.execute(request, options);
|
||||
|
||||
Collection<org.springframework.cloud.client.loadbalancer.Request<RequestDataContext>> lifecycleLogRequests = ((TestLoadBalancerLifecycle) loadBalancerLifecycleBeans
|
||||
.get("loadBalancerLifecycle")).getStartLog().values();
|
||||
Collection<org.springframework.cloud.client.loadbalancer.Request<RequestDataContext>> lifecycleLogStartedRequests = ((TestLoadBalancerLifecycle) loadBalancerLifecycleBeans
|
||||
.get("loadBalancerLifecycle")).getStartRequestLog().values();
|
||||
Collection<CompletionContext<ResponseData, ServiceInstance, RequestDataContext>> anotherLifecycleLogRequests = ((AnotherLoadBalancerLifecycle) loadBalancerLifecycleBeans
|
||||
.get("anotherLoadBalancerLifecycle")).getCompleteLog().values();
|
||||
assertThat(lifecycleLogRequests).extracting(lbRequest -> lbRequest.getContext().getHint())
|
||||
.contains(callbackTestHint);
|
||||
assertThat(lifecycleLogStartedRequests).extracting(lbRequest -> lbRequest.getContext().getHint())
|
||||
.contains(callbackTestHint);
|
||||
assertThat(anotherLifecycleLogRequests)
|
||||
.extracting(completionContext -> completionContext.getClientResponse().getHttpStatus())
|
||||
.contains(HttpStatus.OK);
|
||||
}
|
||||
|
||||
private Request testRequest() {
|
||||
return testRequest("test");
|
||||
}
|
||||
@@ -137,4 +183,56 @@ class FeignBlockingLoadBalancerClientTests {
|
||||
|
||||
}
|
||||
|
||||
protected static class TestLoadBalancerLifecycle
|
||||
implements LoadBalancerLifecycle<RequestDataContext, ResponseData, ServiceInstance> {
|
||||
|
||||
final Map<String, org.springframework.cloud.client.loadbalancer.Request<RequestDataContext>> startLog = new ConcurrentHashMap<>();
|
||||
|
||||
final Map<String, org.springframework.cloud.client.loadbalancer.Request<RequestDataContext>> startRequestLog = new ConcurrentHashMap<>();
|
||||
|
||||
final Map<String, CompletionContext<ResponseData, ServiceInstance, RequestDataContext>> completeLog = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void onStart(org.springframework.cloud.client.loadbalancer.Request<RequestDataContext> request) {
|
||||
startLog.put(getName() + UUID.randomUUID(), request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartRequest(org.springframework.cloud.client.loadbalancer.Request<RequestDataContext> request,
|
||||
org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse) {
|
||||
startRequestLog.put(getName() + UUID.randomUUID(), request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(CompletionContext<ResponseData, ServiceInstance, RequestDataContext> completionContext) {
|
||||
completeLog.put(getName() + UUID.randomUUID(), completionContext);
|
||||
}
|
||||
|
||||
Map<String, org.springframework.cloud.client.loadbalancer.Request<RequestDataContext>> getStartLog() {
|
||||
return startLog;
|
||||
}
|
||||
|
||||
Map<String, CompletionContext<ResponseData, ServiceInstance, RequestDataContext>> getCompleteLog() {
|
||||
return completeLog;
|
||||
}
|
||||
|
||||
Map<String, org.springframework.cloud.client.loadbalancer.Request<RequestDataContext>> getStartRequestLog() {
|
||||
return startRequestLog;
|
||||
}
|
||||
|
||||
protected String getName() {
|
||||
return this.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected static class AnotherLoadBalancerLifecycle extends TestLoadBalancerLifecycle {
|
||||
|
||||
@Override
|
||||
protected String getName() {
|
||||
return this.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+103
-2
@@ -23,6 +23,8 @@ import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
import feign.Client;
|
||||
import feign.Request;
|
||||
@@ -35,11 +37,17 @@ import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.client.ServiceInstance;
|
||||
import org.springframework.cloud.client.loadbalancer.CompletionContext;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
|
||||
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycle;
|
||||
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.loadbalancer.blocking.client.BlockingLoadBalancerClient;
|
||||
import org.springframework.cloud.loadbalancer.blocking.retry.BlockingLoadBalancedRetryPolicy;
|
||||
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -70,10 +78,12 @@ class RetryableFeignBlockingLoadBalancerClientTests {
|
||||
|
||||
private BlockingLoadBalancerClient loadBalancerClient = mock(BlockingLoadBalancerClient.class);
|
||||
|
||||
private final LoadBalancerClientFactory loadBalancerClientFactory = mock(LoadBalancerClientFactory.class);
|
||||
|
||||
private LoadBalancerProperties properties = new LoadBalancerProperties();
|
||||
|
||||
private RetryableFeignBlockingLoadBalancerClient feignBlockingLoadBalancerClient = new RetryableFeignBlockingLoadBalancerClient(
|
||||
delegate, loadBalancerClient, retryFactory, properties);
|
||||
delegate, loadBalancerClient, retryFactory, properties, loadBalancerClientFactory);
|
||||
|
||||
private ServiceInstance serviceInstance = new DefaultServiceInstance("test-a", "test", "testhost", 80, false);
|
||||
|
||||
@@ -165,6 +175,42 @@ class RetryableFeignBlockingLoadBalancerClientTests {
|
||||
assertThat(new String(actualRequest.body())).isEqualTo("hello");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExecuteLoadBalancerLifecycleCallbacks() throws IOException {
|
||||
Request request = testRequest();
|
||||
Request.Options options = new Request.Options();
|
||||
String url = "http://127.0.0.1/path";
|
||||
ServiceInstance serviceInstance = new DefaultServiceInstance("test-1", "test", "test-host", 8888, false);
|
||||
when(loadBalancerClient.choose(eq("test"), any())).thenReturn(serviceInstance);
|
||||
when(loadBalancerClient.reconstructURI(serviceInstance, URI.create("http://test/path")))
|
||||
.thenReturn(URI.create(url));
|
||||
Response response = testResponse(200);
|
||||
when(delegate.execute(any(), any())).thenReturn(response);
|
||||
String callbackTestHint = "callbackTestHint";
|
||||
properties.getHint().put("test", callbackTestHint);
|
||||
Map<String, LoadBalancerLifecycle> loadBalancerLifecycleBeans = new HashMap<>();
|
||||
loadBalancerLifecycleBeans.put("loadBalancerLifecycle", new TestLoadBalancerLifecycle());
|
||||
loadBalancerLifecycleBeans.put("anotherLoadBalancerLifecycle", new AnotherLoadBalancerLifecycle());
|
||||
when(loadBalancerClientFactory.getInstances("test", LoadBalancerLifecycle.class))
|
||||
.thenReturn(loadBalancerLifecycleBeans);
|
||||
|
||||
feignBlockingLoadBalancerClient.execute(request, options);
|
||||
|
||||
Collection<org.springframework.cloud.client.loadbalancer.Request<RetryableRequestContext>> lifecycleLogRequests = ((TestLoadBalancerLifecycle) loadBalancerLifecycleBeans
|
||||
.get("loadBalancerLifecycle")).getStartLog().values();
|
||||
Collection<org.springframework.cloud.client.loadbalancer.Request<RetryableRequestContext>> lifecycleLogStartedRequests = ((TestLoadBalancerLifecycle) loadBalancerLifecycleBeans
|
||||
.get("loadBalancerLifecycle")).getStartRequestLog().values();
|
||||
Collection<CompletionContext<ResponseData, ServiceInstance, RetryableRequestContext>> anotherLifecycleLogRequests = ((AnotherLoadBalancerLifecycle) loadBalancerLifecycleBeans
|
||||
.get("anotherLoadBalancerLifecycle")).getCompleteLog().values();
|
||||
assertThat(lifecycleLogRequests).extracting(lbRequest -> lbRequest.getContext().getHint())
|
||||
.contains(callbackTestHint);
|
||||
assertThat(lifecycleLogStartedRequests).extracting(lbRequest -> lbRequest.getContext().getHint())
|
||||
.contains(callbackTestHint);
|
||||
assertThat(anotherLifecycleLogRequests)
|
||||
.extracting(completionContext -> completionContext.getClientResponse().getHttpStatus())
|
||||
.contains(HttpStatus.OK);
|
||||
}
|
||||
|
||||
private Request testRequest() {
|
||||
return testRequest("test");
|
||||
}
|
||||
@@ -181,4 +227,59 @@ class RetryableFeignBlockingLoadBalancerClientTests {
|
||||
|
||||
}
|
||||
|
||||
protected static class TestLoadBalancerLifecycle
|
||||
implements LoadBalancerLifecycle<RetryableRequestContext, ResponseData, ServiceInstance> {
|
||||
|
||||
final Map<String, org.springframework.cloud.client.loadbalancer.Request<RetryableRequestContext>> startLog = new ConcurrentHashMap<>();
|
||||
|
||||
final Map<String, org.springframework.cloud.client.loadbalancer.Request<RetryableRequestContext>> startRequestLog = new ConcurrentHashMap<>();
|
||||
|
||||
final Map<String, CompletionContext<ResponseData, ServiceInstance, RetryableRequestContext>> completeLog = new ConcurrentHashMap<>();
|
||||
|
||||
@Override
|
||||
public void onStart(org.springframework.cloud.client.loadbalancer.Request<RetryableRequestContext> request) {
|
||||
startLog.put(getName() + UUID.randomUUID(), request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onStartRequest(
|
||||
org.springframework.cloud.client.loadbalancer.Request<RetryableRequestContext> request,
|
||||
org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse) {
|
||||
startRequestLog.put(getName() + UUID.randomUUID(), request);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onComplete(
|
||||
CompletionContext<ResponseData, ServiceInstance, RetryableRequestContext> completionContext) {
|
||||
completeLog.put(getName() + UUID.randomUUID(), completionContext);
|
||||
}
|
||||
|
||||
Map<String, org.springframework.cloud.client.loadbalancer.Request<RetryableRequestContext>> getStartLog() {
|
||||
return startLog;
|
||||
}
|
||||
|
||||
Map<String, CompletionContext<ResponseData, ServiceInstance, RetryableRequestContext>> getCompleteLog() {
|
||||
return completeLog;
|
||||
}
|
||||
|
||||
Map<String, org.springframework.cloud.client.loadbalancer.Request<RetryableRequestContext>> getStartRequestLog() {
|
||||
return startRequestLog;
|
||||
}
|
||||
|
||||
protected String getName() {
|
||||
return this.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
protected static class AnotherLoadBalancerLifecycle
|
||||
extends RetryableFeignBlockingLoadBalancerClientTests.TestLoadBalancerLifecycle {
|
||||
|
||||
@Override
|
||||
protected String getName() {
|
||||
return this.getClass().getSimpleName();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+65
@@ -0,0 +1,65 @@
|
||||
/*
|
||||
* Copyright 2015-2015 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 org.springframework.security.access.AccessDeniedException;
|
||||
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
|
||||
import org.springframework.security.oauth2.client.resource.UserApprovalRequiredException;
|
||||
import org.springframework.security.oauth2.client.resource.UserRedirectRequiredException;
|
||||
import org.springframework.security.oauth2.client.token.AccessTokenProvider;
|
||||
import org.springframework.security.oauth2.client.token.AccessTokenRequest;
|
||||
import org.springframework.security.oauth2.common.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
|
||||
|
||||
/**
|
||||
* Mocks the access token provider
|
||||
*
|
||||
* @author Mihhail Verhovtsov
|
||||
*/
|
||||
public class MockAccessTokenProvider implements AccessTokenProvider {
|
||||
|
||||
private OAuth2AccessToken token;
|
||||
|
||||
public MockAccessTokenProvider(OAuth2AccessToken token) {
|
||||
this.token = token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2AccessToken obtainAccessToken(OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails,
|
||||
AccessTokenRequest accessTokenRequest)
|
||||
throws UserRedirectRequiredException, UserApprovalRequiredException, AccessDeniedException {
|
||||
return token;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsResource(OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails) {
|
||||
return true;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2AccessToken refreshAccessToken(OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails,
|
||||
OAuth2RefreshToken oAuth2RefreshToken, AccessTokenRequest accessTokenRequest)
|
||||
throws UserRedirectRequiredException {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supportsRefresh(OAuth2ProtectedResourceDetails oAuth2ProtectedResourceDetails) {
|
||||
return false;
|
||||
}
|
||||
|
||||
}
|
||||
+79
@@ -0,0 +1,79 @@
|
||||
/*
|
||||
* Copyright 2015-2015 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.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import org.springframework.security.oauth2.common.OAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.common.OAuth2RefreshToken;
|
||||
|
||||
/**
|
||||
* Mocks the OAuth2 access token
|
||||
*
|
||||
* @author Mihhail Verhovtsov
|
||||
*/
|
||||
public class MockOAuth2AccessToken implements OAuth2AccessToken {
|
||||
|
||||
private String value;
|
||||
|
||||
public MockOAuth2AccessToken(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Map<String, Object> getAdditionalInformation() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Set<String> getScope() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2RefreshToken getRefreshToken() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getTokenType() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean isExpired() {
|
||||
return false;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Date getExpiration() {
|
||||
return null;
|
||||
}
|
||||
|
||||
@Override
|
||||
public int getExpiresIn() {
|
||||
return 0;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getValue() {
|
||||
return value;
|
||||
}
|
||||
|
||||
}
|
||||
+67
@@ -0,0 +1,67 @@
|
||||
/*
|
||||
* Copyright 2015-2015 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.HashMap;
|
||||
|
||||
import org.springframework.security.oauth2.client.OAuth2ClientContext;
|
||||
import org.springframework.security.oauth2.client.token.AccessTokenRequest;
|
||||
import org.springframework.security.oauth2.client.token.DefaultAccessTokenRequest;
|
||||
import org.springframework.security.oauth2.common.DefaultOAuth2AccessToken;
|
||||
import org.springframework.security.oauth2.common.OAuth2AccessToken;
|
||||
|
||||
/**
|
||||
* Mocks the current client context
|
||||
*
|
||||
* @author João Pedro Evangelista
|
||||
*/
|
||||
final class MockOAuth2ClientContext implements OAuth2ClientContext {
|
||||
|
||||
private final String value;
|
||||
|
||||
MockOAuth2ClientContext(String value) {
|
||||
this.value = value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OAuth2AccessToken getAccessToken() {
|
||||
return new DefaultOAuth2AccessToken(value);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setAccessToken(OAuth2AccessToken accessToken) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public AccessTokenRequest getAccessTokenRequest() {
|
||||
DefaultAccessTokenRequest tokenRequest = new DefaultAccessTokenRequest(new HashMap<String, String[]>());
|
||||
tokenRequest.setExistingToken(new DefaultOAuth2AccessToken(value));
|
||||
return tokenRequest;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setPreservedState(String stateKey, Object preservedState) {
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public Object removePreservedState(String stateKey) {
|
||||
return null;
|
||||
}
|
||||
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* 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.Collection;
|
||||
import java.util.Map;
|
||||
|
||||
import feign.Request.HttpMethod;
|
||||
import feign.RequestTemplate;
|
||||
import org.junit.Assert;
|
||||
import org.junit.Before;
|
||||
import org.junit.Test;
|
||||
|
||||
import org.springframework.security.oauth2.client.DefaultOAuth2ClientContext;
|
||||
import org.springframework.security.oauth2.client.OAuth2ClientContext;
|
||||
import org.springframework.security.oauth2.client.resource.BaseOAuth2ProtectedResourceDetails;
|
||||
import org.springframework.security.oauth2.client.resource.OAuth2AccessDeniedException;
|
||||
import org.springframework.security.oauth2.client.token.AccessTokenRequest;
|
||||
import org.springframework.security.oauth2.common.OAuth2AccessToken;
|
||||
|
||||
import static org.hamcrest.Matchers.contains;
|
||||
import static org.hamcrest.Matchers.hasSize;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* @author João Pedro Evangelista
|
||||
* @author Tim Ysewyn
|
||||
*/
|
||||
public class OAuth2FeignRequestInterceptorTests {
|
||||
|
||||
private OAuth2FeignRequestInterceptor oAuth2FeignRequestInterceptor;
|
||||
|
||||
private RequestTemplate requestTemplate;
|
||||
|
||||
@Before
|
||||
public void setUp() {
|
||||
oAuth2FeignRequestInterceptor = new OAuth2FeignRequestInterceptor(new MockOAuth2ClientContext("Fancy"),
|
||||
new BaseOAuth2ProtectedResourceDetails());
|
||||
requestTemplate = new RequestTemplate().method(HttpMethod.GET);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void applyAuthorizationHeader() {
|
||||
oAuth2FeignRequestInterceptor.apply(requestTemplate);
|
||||
Map<String, Collection<String>> headers = requestTemplate.headers();
|
||||
Assert.assertTrue("RequestTemplate must have a Authorization header", headers.containsKey("Authorization"));
|
||||
Assert.assertThat("Authorization must have a extract of Fancy", headers.get("Authorization"),
|
||||
contains("Bearer Fancy"));
|
||||
}
|
||||
|
||||
@Test(expected = OAuth2AccessDeniedException.class)
|
||||
public void tryToAcquireToken() {
|
||||
oAuth2FeignRequestInterceptor = new OAuth2FeignRequestInterceptor(new DefaultOAuth2ClientContext(),
|
||||
new BaseOAuth2ProtectedResourceDetails());
|
||||
OAuth2AccessToken oAuth2AccessToken = oAuth2FeignRequestInterceptor.getToken();
|
||||
Assert.assertTrue(oAuth2AccessToken.getValue() + " Must be null", oAuth2AccessToken.getValue() == null);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void configureAccessTokenProvider() {
|
||||
OAuth2AccessToken mockedToken = new MockOAuth2AccessToken("MOCKED_TOKEN");
|
||||
oAuth2FeignRequestInterceptor.setAccessTokenProvider(new MockAccessTokenProvider(mockedToken));
|
||||
Assert.assertEquals("Should return same mocked token instance", mockedToken,
|
||||
oAuth2FeignRequestInterceptor.acquireAccessToken());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void applyAuthorizationHeaderOnlyOnce() {
|
||||
OAuth2ClientContext oAuth2ClientContext = mock(OAuth2ClientContext.class);
|
||||
when(oAuth2ClientContext.getAccessToken()).thenReturn(new MockOAuth2AccessToken("MOCKED_TOKEN"));
|
||||
|
||||
OAuth2FeignRequestInterceptor oAuth2FeignRequestInterceptor = new OAuth2FeignRequestInterceptor(
|
||||
oAuth2ClientContext, new BaseOAuth2ProtectedResourceDetails());
|
||||
|
||||
oAuth2FeignRequestInterceptor.apply(requestTemplate);
|
||||
|
||||
// First idempotent call failed, retry mechanism kicks in, and token has expired
|
||||
// in the meantime
|
||||
|
||||
OAuth2AccessToken expiredAccessToken = mock(OAuth2AccessToken.class);
|
||||
when(expiredAccessToken.isExpired()).thenReturn(true);
|
||||
when(oAuth2ClientContext.getAccessToken()).thenReturn(expiredAccessToken);
|
||||
AccessTokenRequest accessTokenRequest = mock(AccessTokenRequest.class);
|
||||
when(oAuth2ClientContext.getAccessTokenRequest()).thenReturn(accessTokenRequest);
|
||||
OAuth2AccessToken newToken = new MockOAuth2AccessToken("Fancy");
|
||||
oAuth2FeignRequestInterceptor.setAccessTokenProvider(new MockAccessTokenProvider(newToken));
|
||||
|
||||
oAuth2FeignRequestInterceptor.apply(requestTemplate);
|
||||
|
||||
Map<String, Collection<String>> headers = requestTemplate.headers();
|
||||
Assert.assertTrue("RequestTemplate must have a Authorization header", headers.containsKey("Authorization"));
|
||||
Assert.assertThat("Authorization must have a extract of Fancy", headers.get("Authorization"), hasSize(1));
|
||||
Assert.assertThat("Authorization must have a extract of Fancy", headers.get("Authorization"),
|
||||
contains("Bearer Fancy"));
|
||||
}
|
||||
|
||||
}
|
||||
+112
@@ -0,0 +1,112 @@
|
||||
/*
|
||||
* 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 java.nio.charset.Charset;
|
||||
|
||||
import feign.Response;
|
||||
import feign.codec.Decoder;
|
||||
import feign.codec.Encoder;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.util.SocketUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* Abstract class for the integration tests for {@link SpringMvcContract}.
|
||||
*
|
||||
* @author Ram Anaswara
|
||||
*/
|
||||
public class AbstractSpringMvcContractIntegrationTests {
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeClass() {
|
||||
System.setProperty("server.port", String.valueOf(SocketUtils.findAvailableTcpPort()));
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void afterClass() {
|
||||
System.clearProperty("server.port");
|
||||
}
|
||||
|
||||
protected String getUrlQueryParam(Response response) {
|
||||
return response.request().requestTemplate().queries().get("url").stream().findFirst()
|
||||
.orElseThrow(IllegalStateException::new);
|
||||
}
|
||||
|
||||
@FeignClient(name = "test", url = "http://localhost:${server.port}/",
|
||||
configuration = NoCodecsFeignConfiguration.class)
|
||||
interface TestClient {
|
||||
|
||||
@PostMapping("/test")
|
||||
Object sendMessage(@RequestBody String message, @RequestHeader(HttpHeaders.CONTENT_TYPE) String acceptHeader);
|
||||
|
||||
@GetMapping("/get")
|
||||
Object getMessage(@RequestParam String url);
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableFeignClients(clients = TestClient.class)
|
||||
@EnableAutoConfiguration
|
||||
@RestController
|
||||
@Import(NoSecurityConfiguration.class)
|
||||
protected static class Config {
|
||||
|
||||
@PostMapping("/test")
|
||||
Object sendMessage(@RequestBody String message, @RequestHeader(HttpHeaders.CONTENT_TYPE) String acceptHeader) {
|
||||
return message;
|
||||
}
|
||||
|
||||
@GetMapping("/get")
|
||||
Object getMessage(@RequestParam String url) {
|
||||
return url;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// Avoid feign.codec.EncodeException - this feature works for users that override
|
||||
// Encoder
|
||||
protected static class NoCodecsFeignConfiguration {
|
||||
|
||||
@Bean
|
||||
public Decoder decoder() {
|
||||
return (response, type) -> response;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Encoder encoder() {
|
||||
return (object, bodyType, request) -> request.body(object.toString().getBytes(), Charset.defaultCharset());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+25
-15
@@ -16,47 +16,41 @@
|
||||
|
||||
package org.springframework.cloud.openfeign.support;
|
||||
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
|
||||
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
|
||||
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
|
||||
import org.springframework.cloud.openfeign.CollectionFormat;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.data.domain.Pageable;
|
||||
import org.springframework.data.domain.Sort;
|
||||
import org.springframework.util.SocketUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
|
||||
|
||||
/**
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@SpringBootTest(classes = PageableSupportTest.Config.class, webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
|
||||
@SpringBootTest(classes = PageableSupportTest.Config.class, webEnvironment = RANDOM_PORT)
|
||||
public class PageableSupportTest {
|
||||
|
||||
@Autowired
|
||||
private PageableFeignClient feignClient;
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeClass() {
|
||||
System.setProperty("server.port", String.valueOf(SocketUtils.findAvailableTcpPort()));
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void afterClass() {
|
||||
System.clearProperty("server.port");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldProperlyFormatPageable() {
|
||||
String direction = feignClient.performRequest(PageRequest.of(1, 10, Sort.by(Sort.Order.desc("property"))));
|
||||
@@ -64,7 +58,7 @@ public class PageableSupportTest {
|
||||
assertThat(direction).isEqualTo("DESC");
|
||||
}
|
||||
|
||||
@FeignClient(name = "pageable", url = "http://localhost:${server.port}/")
|
||||
@FeignClient("pageable")
|
||||
protected interface PageableFeignClient {
|
||||
|
||||
@CollectionFormat(feign.CollectionFormat.CSV)
|
||||
@@ -79,6 +73,7 @@ public class PageableSupportTest {
|
||||
@RestController
|
||||
@EnableFeignClients(clients = PageableFeignClient.class)
|
||||
@Import(NoSecurityConfiguration.class)
|
||||
@LoadBalancerClient(name = "pageable", configuration = LocalClientConfiguration.class)
|
||||
protected static class Config {
|
||||
|
||||
@GetMapping(path = "/page")
|
||||
@@ -88,4 +83,19 @@ public class PageableSupportTest {
|
||||
|
||||
}
|
||||
|
||||
// Load balancer with fixed server list for "local" pointing to localhost
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
static class LocalClientConfiguration {
|
||||
|
||||
@LocalServerPort
|
||||
private int port = 0;
|
||||
|
||||
@Bean
|
||||
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
|
||||
return ServiceInstanceListSuppliers.from("pageable",
|
||||
new DefaultServiceInstance("pageable-1", "pageable", "localhost", port, false));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+11
-68
@@ -16,96 +16,39 @@
|
||||
|
||||
package org.springframework.cloud.openfeign.support;
|
||||
|
||||
import java.nio.charset.Charset;
|
||||
|
||||
import feign.codec.Decoder;
|
||||
import feign.codec.Encoder;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import feign.Response;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.util.SocketUtils;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link SpringMvcContract}
|
||||
* Integration tests for {@link SpringMvcContract}.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Ram Anaswara
|
||||
*/
|
||||
@SpringBootTest(classes = SpringMvcContractIntegrationTests.Config.class,
|
||||
@SpringBootTest(classes = AbstractSpringMvcContractIntegrationTests.Config.class,
|
||||
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT)
|
||||
public class SpringMvcContractIntegrationTests {
|
||||
public class SpringMvcContractIntegrationTests extends AbstractSpringMvcContractIntegrationTests {
|
||||
|
||||
@Autowired
|
||||
private TestClient client;
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeClass() {
|
||||
System.setProperty("server.port", String.valueOf(SocketUtils.findAvailableTcpPort()));
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void afterClass() {
|
||||
System.clearProperty("server.port");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void shouldNotThrowInvalidMediaTypeExceptionWhenContentTypeTemplateUsed() {
|
||||
assertThatCode(() -> client.sendMessage("test", "text/markdown")).doesNotThrowAnyException();
|
||||
}
|
||||
|
||||
@FeignClient(name = "test", url = "http://localhost:${server.port}/",
|
||||
configuration = NoCodecsFeignConfiguration.class)
|
||||
interface TestClient {
|
||||
|
||||
@PostMapping("/test")
|
||||
Object sendMessage(@RequestBody String message, @RequestHeader(HttpHeaders.CONTENT_TYPE) String acceptHeader);
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableFeignClients(clients = TestClient.class)
|
||||
@EnableAutoConfiguration
|
||||
@RestController
|
||||
@Import(NoSecurityConfiguration.class)
|
||||
protected static class Config {
|
||||
|
||||
@PostMapping("/test")
|
||||
Object sendMessage(@RequestBody String message, @RequestHeader(HttpHeaders.CONTENT_TYPE) String acceptHeader) {
|
||||
return message;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// avoid feign.codec.EncodeException - this feature works for users that override
|
||||
// Encoder
|
||||
protected static class NoCodecsFeignConfiguration {
|
||||
|
||||
@Bean
|
||||
public Decoder decoder() {
|
||||
return (response, type) -> response;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Encoder encoder() {
|
||||
return (object, bodyType, request) -> request.body(object.toString().getBytes(), Charset.defaultCharset());
|
||||
}
|
||||
@Test
|
||||
public void feignClientShouldPreserveSlash() {
|
||||
Response response = (Response) client.getMessage("https://www.google.com");
|
||||
|
||||
String urlQueryParam = getUrlQueryParam(response);
|
||||
assertThat(urlQueryParam).isEqualTo("https%3A//www.google.com");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+47
@@ -0,0 +1,47 @@
|
||||
/*
|
||||
* 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 feign.Response;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Integration tests for {@link SpringMvcContract}.
|
||||
*
|
||||
* @author Ram Anaswara
|
||||
*/
|
||||
@SpringBootTest(classes = SpringMvcContractSlashEncodingIntegrationTests.Config.class,
|
||||
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT, properties = { "feign.client.decodeSlash=false" })
|
||||
public class SpringMvcContractSlashEncodingIntegrationTests extends AbstractSpringMvcContractIntegrationTests {
|
||||
|
||||
@Autowired
|
||||
private TestClient client;
|
||||
|
||||
@Test
|
||||
public void feignClientShouldNotDecodeEncodedSlash() {
|
||||
Response response = (Response) client.getMessage("https://www.google.com");
|
||||
|
||||
String urlQueryParam = getUrlQueryParam(response);
|
||||
assertThat(urlQueryParam).isEqualTo("https%3A%2F%2Fwww.google.com");
|
||||
}
|
||||
|
||||
}
|
||||
+64
-6
@@ -114,11 +114,7 @@ public class SpringMvcContractTests {
|
||||
|
||||
@Before
|
||||
public void setup() {
|
||||
FormattingConversionServiceFactoryBean conversionServiceFactoryBean = new FormattingConversionServiceFactoryBean();
|
||||
conversionServiceFactoryBean.afterPropertiesSet();
|
||||
ConversionService conversionService = conversionServiceFactoryBean.getObject();
|
||||
|
||||
contract = new SpringMvcContract(Collections.emptyList(), conversionService);
|
||||
contract = new SpringMvcContract(Collections.emptyList(), getConversionService());
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -130,6 +126,19 @@ public class SpringMvcContractTests {
|
||||
assertThat(data.template().method()).isEqualTo("GET");
|
||||
assertThat(data.template().headers().get("Accept").iterator().next())
|
||||
.isEqualTo(MediaType.APPLICATION_JSON_VALUE);
|
||||
assertThat(data.template().decodeSlash()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessAnnotationOnMethod_Simple_SlashEncoded() throws Exception {
|
||||
contract = new SpringMvcContract(Collections.emptyList(), getConversionService(), false);
|
||||
|
||||
Method method = TestTemplate_Simple.class.getDeclaredMethod("getTest", String.class);
|
||||
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
|
||||
|
||||
assertThat(data.template().url()).isEqualTo("/test/{id}");
|
||||
|
||||
assertThat(data.template().decodeSlash()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -180,6 +189,19 @@ public class SpringMvcContractTests {
|
||||
assertThat(data.template().method()).isEqualTo("GET");
|
||||
|
||||
assertThat(data.indexToName().get(0).iterator().next()).isEqualTo("classId");
|
||||
assertThat(data.template().decodeSlash()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessAnnotations_Class_AnnotationsGetAllTests_EncodeSlash() throws Exception {
|
||||
contract = new SpringMvcContract(Collections.emptyList(), getConversionService(), false);
|
||||
|
||||
Method method = TestTemplate_Class_Annotations.class.getDeclaredMethod("getAllTests", String.class);
|
||||
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
|
||||
|
||||
assertThat(data.template().url()).isEqualTo("/prepend/{classId}");
|
||||
|
||||
assertThat(data.template().decodeSlash()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -193,8 +215,25 @@ public class SpringMvcContractTests {
|
||||
|
||||
assertThat(data.template().url()).isEqualTo(extendedData.template().url());
|
||||
assertThat(data.template().method()).isEqualTo(extendedData.template().method());
|
||||
|
||||
assertThat(data.indexToName().get(0).iterator().next()).isEqualTo(data.indexToName().get(0).iterator().next());
|
||||
assertThat(data.indexToName().get(0).iterator().next()).isEqualTo(data.indexToName().get(0).iterator().next());
|
||||
assertThat(data.template().decodeSlash()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessAnnotations_ExtendedInterface_EncodeSlash() throws Exception {
|
||||
contract = new SpringMvcContract(Collections.emptyList(), getConversionService(), false);
|
||||
|
||||
Method extendedMethod = TestTemplate_Extended.class.getMethod("getAllTests", String.class);
|
||||
MethodMetadata extendedData = contract.parseAndValidateMetadata(extendedMethod.getDeclaringClass(),
|
||||
extendedMethod);
|
||||
|
||||
Method method = TestTemplate_Class_Annotations.class.getDeclaredMethod("getAllTests", String.class);
|
||||
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
|
||||
|
||||
assertThat(data.template().url()).isEqualTo(extendedData.template().url());
|
||||
assertThat(data.template().method()).isEqualTo(extendedData.template().method());
|
||||
assertThat(data.template().decodeSlash()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -343,6 +382,19 @@ public class SpringMvcContractTests {
|
||||
assertThat(data.template().method()).isEqualTo("GET");
|
||||
assertThat(data.template().headers().get("Accept").iterator().next())
|
||||
.isEqualTo(MediaType.APPLICATION_JSON_VALUE);
|
||||
assertThat(data.template().decodeSlash()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessAnnotations_Advanced3_DecodeSlashFlagNotModified() throws Exception {
|
||||
contract = new SpringMvcContract(Collections.emptyList(), getConversionService(), false);
|
||||
|
||||
Method method = TestTemplate_Simple.class.getDeclaredMethod("getTest");
|
||||
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
|
||||
|
||||
assertThat(data.template().url()).isEqualTo("/");
|
||||
|
||||
assertThat(data.template().decodeSlash()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -524,6 +576,12 @@ public class SpringMvcContractTests {
|
||||
assertThat(data.formParams()).contains("file", "id");
|
||||
}
|
||||
|
||||
private ConversionService getConversionService() {
|
||||
FormattingConversionServiceFactoryBean conversionServiceFactoryBean = new FormattingConversionServiceFactoryBean();
|
||||
conversionServiceFactoryBean.afterPropertiesSet();
|
||||
return conversionServiceFactoryBean.getObject();
|
||||
}
|
||||
|
||||
public interface TestTemplate_Simple {
|
||||
|
||||
@RequestMapping(value = "/test/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
|
||||
+1
-1
@@ -108,7 +108,7 @@ class ApacheHttpClientConfigurationTests {
|
||||
@EnableFeignClients(clients = { ApacheHttpClientConfigurationTestApp.FooClient.class })
|
||||
static class ApacheHttpClientConfigurationTestApp {
|
||||
|
||||
@FeignClient(name = "foo", serviceId = "foo")
|
||||
@FeignClient(name = "foo")
|
||||
interface FooClient {
|
||||
|
||||
}
|
||||
|
||||
-68
@@ -1,68 +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.test;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||
import org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration;
|
||||
import org.springframework.cloud.client.discovery.noop.NoopDiscoveryClientAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.Ordered;
|
||||
import org.springframework.core.annotation.Order;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
|
||||
import org.springframework.security.core.userdetails.User;
|
||||
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Import({ NoopDiscoveryClientAutoConfiguration.class })
|
||||
@AutoConfigureBefore(SecurityAutoConfiguration.class)
|
||||
public class TestAutoConfiguration {
|
||||
|
||||
public static final String USER = "user";
|
||||
|
||||
public static final String PASSWORD = "{noop}password";
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@Order(Ordered.HIGHEST_PRECEDENCE)
|
||||
protected static class TestSecurityConfiguration extends WebSecurityConfigurerAdapter {
|
||||
|
||||
TestSecurityConfiguration() {
|
||||
super(true);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public UserDetailsService userDetailsService() {
|
||||
InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
|
||||
manager.createUser(User.withUsername(USER).password(PASSWORD).roles("USER").build());
|
||||
return manager;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected void configure(HttpSecurity http) throws Exception {
|
||||
// super.configure(http);
|
||||
http.antMatcher("/proxy-username").httpBasic().and().authorizeRequests().antMatchers("/**").permitAll();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+5
-3
@@ -26,14 +26,15 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
|
||||
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
|
||||
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -155,8 +156,9 @@ public class FeignClientNotPrimaryTests {
|
||||
private int port = 0;
|
||||
|
||||
@Bean
|
||||
public ServiceInstanceListSupplier staticServiceInstanceListSupplier(Environment env) {
|
||||
return ServiceInstanceListSupplier.fixed(env).instance(port, "localapp").build();
|
||||
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
|
||||
return ServiceInstanceListSuppliers.from("local",
|
||||
new DefaultServiceInstance("local-1", "local", "localhost", port, false));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
-4
@@ -26,9 +26,11 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
|
||||
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
|
||||
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
|
||||
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient;
|
||||
@@ -36,7 +38,6 @@ import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
@@ -54,7 +55,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@SpringBootTest(classes = FeignHttpClientTests.Application.class, webEnvironment = WebEnvironment.RANDOM_PORT,
|
||||
value = { "spring.application.name=feignclienttest", "feign.hystrix.enabled=false",
|
||||
value = { "spring.application.name=feignclienttest", "feign.circuitbreaker.enabled=false",
|
||||
"feign.okhttp.enabled=false", "spring.cloud.loadbalancer.retry.enabled=false" })
|
||||
@DirtiesContext
|
||||
class FeignHttpClientTests {
|
||||
@@ -243,8 +244,9 @@ class FeignHttpClientTests {
|
||||
private int port = 0;
|
||||
|
||||
@Bean
|
||||
public ServiceInstanceListSupplier staticServiceInstanceListSupplier(Environment env) {
|
||||
return ServiceInstanceListSupplier.fixed(env).instance(port, "local").build();
|
||||
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
|
||||
return ServiceInstanceListSuppliers.from("local",
|
||||
new DefaultServiceInstance("local-1", "local", "localhost", port, false));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
-4
@@ -26,9 +26,11 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
|
||||
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
|
||||
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
|
||||
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient;
|
||||
@@ -36,7 +38,6 @@ import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
@@ -53,7 +54,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@SpringBootTest(classes = FeignOkHttpTests.Application.class, webEnvironment = WebEnvironment.RANDOM_PORT,
|
||||
value = { "spring.application.name=feignclienttest", "feign.hystrix.enabled=false",
|
||||
value = { "spring.application.name=feignclienttest", "feign.circuitbreaker.enabled=false",
|
||||
"feign.httpclient.enabled=false", "feign.okhttp.enabled=true",
|
||||
"spring.cloud.httpclientfactories.ok.enabled=true", "spring.cloud.loadbalancer.retry.enabled=false" })
|
||||
@DirtiesContext
|
||||
@@ -245,8 +246,9 @@ class FeignOkHttpTests {
|
||||
private int port = 0;
|
||||
|
||||
@Bean
|
||||
public ServiceInstanceListSupplier staticServiceInstanceListSupplier(Environment env) {
|
||||
return ServiceInstanceListSupplier.fixed(env).instance(port, "local").build();
|
||||
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
|
||||
return ServiceInstanceListSuppliers.from("local",
|
||||
new DefaultServiceInstance("local-1", "local", "localhost", port, false));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
-4
@@ -26,15 +26,16 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
|
||||
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
|
||||
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
@@ -50,7 +51,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
@SpringBootTest(classes = IterableParameterTests.Application.class, webEnvironment = WebEnvironment.RANDOM_PORT,
|
||||
value = { "spring.application.name=iterableparametertest",
|
||||
"logging.level.org.springframework.cloud.openfeign.valid=DEBUG", "feign.httpclient.enabled=false",
|
||||
"feign.okhttp.enabled=false", "feign.hystrix.enabled=false" })
|
||||
"feign.okhttp.enabled=false", "feign.circuitbreaker.enabled=false" })
|
||||
@DirtiesContext
|
||||
public class IterableParameterTests {
|
||||
|
||||
@@ -94,8 +95,9 @@ public class IterableParameterTests {
|
||||
private int port = 0;
|
||||
|
||||
@Bean
|
||||
public ServiceInstanceListSupplier staticServiceInstanceListSupplier(Environment env) {
|
||||
return ServiceInstanceListSupplier.fixed(env).instance(port, "localapp").build();
|
||||
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
|
||||
return ServiceInstanceListSuppliers.from("local",
|
||||
new DefaultServiceInstance("local-1", "local", "localhost", port, false));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+6
-4
@@ -49,9 +49,11 @@ import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
|
||||
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
|
||||
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
|
||||
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.cloud.openfeign.FeignFormatterRegistrar;
|
||||
@@ -62,7 +64,6 @@ import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.format.Formatter;
|
||||
import org.springframework.format.FormatterRegistry;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
@@ -96,7 +97,7 @@ import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
@SpringBootTest(classes = ValidFeignClientTests.Application.class, webEnvironment = WebEnvironment.RANDOM_PORT,
|
||||
value = { "spring.application.name=feignclienttest",
|
||||
"logging.level.org.springframework.cloud.openfeign.valid=DEBUG", "feign.httpclient.enabled=false",
|
||||
"feign.okhttp.enabled=false", "feign.hystrix.enabled=true",
|
||||
"feign.okhttp.enabled=false", "feign.circuitbreaker.enabled=true",
|
||||
"spring.cloud.loadbalancer.retry.enabled=false" })
|
||||
@DirtiesContext
|
||||
class ValidFeignClientTests {
|
||||
@@ -808,8 +809,9 @@ class ValidFeignClientTests {
|
||||
private int port = 0;
|
||||
|
||||
@Bean
|
||||
public ServiceInstanceListSupplier staticServiceInstanceListSupplier(Environment env) {
|
||||
return ServiceInstanceListSupplier.fixed(env).instance(port, "local").build();
|
||||
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
|
||||
return ServiceInstanceListSuppliers.from("local",
|
||||
new DefaultServiceInstance("local-1", "local", "localhost", port, false));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
-3
@@ -23,15 +23,16 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
|
||||
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
|
||||
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
|
||||
import org.springframework.cloud.openfeign.testclients.TestClient;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -84,8 +85,9 @@ public class FeignClientEnvVarTests {
|
||||
private int port = 0;
|
||||
|
||||
@Bean
|
||||
public ServiceInstanceListSupplier staticServiceInstanceListSupplier(Environment env) {
|
||||
return ServiceInstanceListSupplier.fixed(env).instance(port, "localapp").build();
|
||||
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
|
||||
return ServiceInstanceListSuppliers.from("localapp",
|
||||
new DefaultServiceInstance("localapp-1", "localapp", "localhost", port, false));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
-3
@@ -25,15 +25,16 @@ import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.cloud.client.DefaultServiceInstance;
|
||||
import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClients;
|
||||
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
|
||||
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -118,8 +119,9 @@ public class FeignClientScanningTests {
|
||||
private int port = 0;
|
||||
|
||||
@Bean
|
||||
public ServiceInstanceListSupplier staticServiceInstanceListSupplier(Environment env) {
|
||||
return ServiceInstanceListSupplier.fixed(env).instance(port, "local").build();
|
||||
public ServiceInstanceListSupplier staticServiceInstanceListSupplier() {
|
||||
return ServiceInstanceListSuppliers.from("local",
|
||||
new DefaultServiceInstance("local-1", "local", "localhost", port, false));
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
|
||||
org.springframework.cloud.openfeign.test.TestAutoConfiguration
|
||||
@@ -15,7 +15,6 @@ eureka:
|
||||
fetchRegistry: false
|
||||
#error:
|
||||
# path: /myerror
|
||||
hystrix.command.default.execution.isolation.thread.timeoutInMilliseconds: 60000
|
||||
ribbon:
|
||||
ConnectTimeout: 3001
|
||||
ReadTimeout: 60001
|
||||
@@ -30,8 +29,6 @@ badClients:
|
||||
endpoints:
|
||||
health:
|
||||
sensitive: false
|
||||
hystrix:
|
||||
shareSecurityContext: true
|
||||
feignClient:
|
||||
localappName: localapp
|
||||
methodLevelRequestMappingPath: /hello2
|
||||
|
||||
@@ -10,6 +10,10 @@ feign.client.config.default.retryer=org.springframework.cloud.openfeign.FeignCli
|
||||
feign.client.config.default.decode404=true
|
||||
feign.client.config.foo.requestInterceptors[0]=org.springframework.cloud.openfeign.FeignClientUsingPropertiesTests.FooRequestInterceptor
|
||||
feign.client.config.foo.requestInterceptors[1]=org.springframework.cloud.openfeign.FeignClientUsingPropertiesTests.BarRequestInterceptor
|
||||
feign.client.config.singleValue.defaultRequestHeaders[singleValueHeaders]=header
|
||||
feign.client.config.singleValue.defaultQueryParameters[singleValueParameters]=parameter
|
||||
feign.client.config.multipleValue.defaultRequestHeaders[multipleValueHeaders]=header1,header2
|
||||
feign.client.config.multipleValue.defaultQueryParameters[multipleValueParameters]=parameter1,parameter2
|
||||
feign.client.config.bar.connectTimeout=1000
|
||||
feign.client.config.bar.readTimeout=1000
|
||||
feign.client.config.form.encoder=org.springframework.cloud.openfeign.FeignClientUsingPropertiesTests.FormEncoder
|
||||
|
||||
@@ -6,20 +6,26 @@
|
||||
<parent>
|
||||
<artifactId>spring-cloud-dependencies-parent</artifactId>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<version>3.0.0-M4</version>
|
||||
<version>3.0.1</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
<artifactId>spring-cloud-openfeign-dependencies</artifactId>
|
||||
<version>3.0.0-M4</version>
|
||||
<version>3.0.1</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>spring-cloud-openfeign-dependencies</name>
|
||||
<description>Spring Cloud OpenFeign Dependencies</description>
|
||||
<properties>
|
||||
<feign.version>10.10.1</feign.version>
|
||||
<feign-form.version>3.8.0</feign-form.version>
|
||||
<spring-security-oauth2-autoconfigure.version>2.1.2.RELEASE</spring-security-oauth2-autoconfigure.version>
|
||||
</properties>
|
||||
<dependencyManagement>
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security.oauth.boot</groupId>
|
||||
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
|
||||
<version>${spring-security-oauth2-autoconfigure.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-openfeign-core</artifactId>
|
||||
@@ -50,11 +56,6 @@
|
||||
<artifactId>feign-httpclient</artifactId>
|
||||
<version>${feign.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.openfeign</groupId>
|
||||
<artifactId>feign-hystrix</artifactId>
|
||||
<version>${feign.version}</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.openfeign</groupId>
|
||||
<artifactId>feign-okhttp</artifactId>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-openfeign</artifactId>
|
||||
<version>3.0.0-M4</version>
|
||||
<version>3.0.1</version>
|
||||
<relativePath>..</relativePath>
|
||||
</parent>
|
||||
<artifactId>spring-cloud-starter-openfeign</artifactId>
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
<suppress files=".*LoadBalancerFeignClientOverrideTests.*" checks="LineLengthCheck" />
|
||||
<suppress files=".*ProtobufSpringEncoderTest.*" checks="LineLengthCheck" />
|
||||
<suppress files=".*ProtobufTest.*" checks="LineLengthCheck" />
|
||||
<suppress files=".*Hello.*" checks="RedundantModifier"/>
|
||||
<!-- Important -->
|
||||
<suppress files=".*HttpEncoding.*" checks="InterfaceIsTypeCheck" />
|
||||
</suppressions>
|
||||
|
||||
Reference in New Issue
Block a user