Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
e8eb5c3d08 | ||
|
|
f3bf53acd6 | ||
|
|
a1115f6044 | ||
|
|
d111204007 | ||
|
|
b4c9567de5 | ||
|
|
8fdb488697 | ||
|
|
7e01355317 | ||
|
|
fc459044b5 | ||
|
|
e321e72e3d | ||
|
|
ae63aae6eb | ||
|
|
10fefea29b | ||
|
|
24d9b08d5c | ||
|
|
d4832788f6 | ||
|
|
ff96850ec0 | ||
|
|
9023cc2dc5 | ||
|
|
6c3b8c257e | ||
|
|
fe43b6c52b | ||
|
|
3e5aa41490 | ||
|
|
3a1d19f74f | ||
|
|
0853ccc1e5 | ||
|
|
13f91320b1 | ||
|
|
7af0efd569 | ||
|
|
70a9002002 | ||
|
|
ad892a23f0 | ||
|
|
c0f314a5f0 | ||
|
|
56dc4e07e4 | ||
|
|
9167752bfb | ||
|
|
127c866616 | ||
|
|
0e6b33f480 | ||
|
|
17d0161faf | ||
|
|
d6783a6f1e | ||
|
|
548e4d55ef | ||
|
|
55ceafae4d | ||
|
|
0c87527ea5 | ||
|
|
a2c473db2a | ||
|
|
ae33802a4b | ||
|
|
c761919bc9 | ||
|
|
998baed0d3 | ||
|
|
4fc80e6b01 | ||
|
|
c101fd2c6e | ||
|
|
71930fd9f0 | ||
|
|
b5283e2f88 | ||
|
|
6aeb1ff676 | ||
|
|
44c63798b0 | ||
|
|
769b38c5c1 | ||
|
|
3c402201ec | ||
|
|
deb150ed88 | ||
|
|
1275875ddb | ||
|
|
c4b9492a9b | ||
|
|
3acf5f9e92 | ||
|
|
e9e38d0274 | ||
|
|
28b50401c5 | ||
|
|
6479db0a54 | ||
|
|
f7e490c466 | ||
|
|
d60cda824d | ||
|
|
190f5a9083 | ||
|
|
1005749eb5 | ||
|
|
6de9fdd3e1 | ||
|
|
df1f982046 | ||
|
|
baf7cf95f2 | ||
|
|
45861dd657 | ||
|
|
00ea88c67f | ||
|
|
d897b18e16 | ||
|
|
8c08204c78 | ||
|
|
1f5bdd4224 | ||
|
|
2f43715f6f | ||
|
|
280291ec93 | ||
|
|
0de9dd614d | ||
|
|
93fcf38c24 | ||
|
|
be929043c3 | ||
|
|
c7316b008a | ||
|
|
989616ebe6 | ||
|
|
58b0c7866c | ||
|
|
421df91a80 | ||
|
|
5aa1b7a529 | ||
|
|
ec1f2ad5bb | ||
|
|
fbcebb372c | ||
|
|
1a00bbf705 | ||
|
|
5baa8c8ed5 | ||
|
|
d0e035b8ff | ||
|
|
1615a5a68c | ||
|
|
0c1ab36fe1 | ||
|
|
0684ee4bc2 | ||
|
|
0b34d89adc | ||
|
|
c33bc490cd | ||
|
|
d738b3b224 | ||
|
|
0c1d281970 | ||
|
|
26fb4deb18 | ||
|
|
0bf0426f60 | ||
|
|
9f9ce39789 | ||
|
|
4454c820cf | ||
|
|
b8e966fe14 | ||
|
|
e47255e79d | ||
|
|
96268d16da | ||
|
|
ac019b4af1 | ||
|
|
4d0caec7ee | ||
|
|
b8aea256da | ||
|
|
f653a6730d | ||
|
|
92f7ba9901 | ||
|
|
b2c208ba5f | ||
|
|
e2a54c1359 | ||
|
|
cdfd3695c2 | ||
|
|
a5412cc872 | ||
|
|
59216f2f82 | ||
|
|
aa01ccb5b2 | ||
|
|
4d827623f0 | ||
|
|
86713156f6 | ||
|
|
5e494aa196 | ||
|
|
9db146761a | ||
|
|
af418a04ac | ||
|
|
bcea218513 | ||
|
|
1c49f88e2b | ||
|
|
3dbaf056ec | ||
|
|
5637465ec8 | ||
|
|
6d7ddfa7bd | ||
|
|
809277ef3c | ||
|
|
7cf7051083 | ||
|
|
61aee7cb21 | ||
|
|
fb3924ac72 | ||
|
|
e3439f38bc | ||
|
|
dbfc255146 | ||
|
|
2a23b6d304 | ||
|
|
33f3841e74 | ||
|
|
6b436c4133 | ||
|
|
4b962e6170 | ||
|
|
b4fb3fe7b4 | ||
|
|
90d78a5d68 | ||
|
|
b8129f52b7 | ||
|
|
dcc45128b9 | ||
|
|
a2b798c31a | ||
|
|
4217a0c542 | ||
|
|
11fd7dc43e | ||
|
|
1dbf9c2d6c | ||
|
|
cf20710cad | ||
|
|
b13711f906 | ||
|
|
c92849e28e | ||
|
|
c6e42de202 | ||
|
|
a205afb88d | ||
|
|
37e1c2fd11 | ||
|
|
ce7a55a484 | ||
|
|
10c6992935 | ||
|
|
e745dd0c7c | ||
|
|
9ac5972600 | ||
|
|
40a2ed5030 | ||
|
|
d87375e835 | ||
|
|
cc9af41dfe | ||
|
|
a3db5927b3 | ||
|
|
9bb8b43d58 | ||
|
|
60558c46ef | ||
|
|
89db04d7d0 | ||
|
|
5d9d99d9ea | ||
|
|
27a3967975 | ||
|
|
0c4c19bfcd | ||
|
|
a789625a98 | ||
|
|
e7d66882ea | ||
|
|
a4155e7653 | ||
|
|
388b141369 | ||
|
|
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 |
@@ -1,38 +0,0 @@
|
|||||||
version: 2
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
docker:
|
|
||||||
- image: springcloud/pipeline-base
|
|
||||||
environment:
|
|
||||||
_JAVA_OPTIONS: "-Xms1024m -Xmx2048m"
|
|
||||||
TERM: dumb
|
|
||||||
branches:
|
|
||||||
ignore:
|
|
||||||
- gh-pages # list of branches to ignore
|
|
||||||
steps:
|
|
||||||
- checkout
|
|
||||||
- restore_cache:
|
|
||||||
key: sc-openfeign-{{ .Branch }}
|
|
||||||
- run:
|
|
||||||
name: "Download dependencies"
|
|
||||||
command: ./mvnw -s .settings.xml -U --fail-never dependency:go-offline || true
|
|
||||||
- save_cache:
|
|
||||||
key: sc-openfeign-{{ .Branch }}
|
|
||||||
paths:
|
|
||||||
- ~/.m2
|
|
||||||
- run:
|
|
||||||
name: "Running build"
|
|
||||||
command: ./mvnw -s .settings.xml clean org.jacoco:jacoco-maven-plugin:prepare-agent install -U -P sonar -nsu --batch-mode -Dmaven.test.redirectTestOutputToFile=true -Dorg.slf4j.simpleLogger.log.org.apache.maven.cli.transfer.Slf4jMavenTransferListener=warn
|
|
||||||
- run:
|
|
||||||
name: "Aggregate test results"
|
|
||||||
when: always
|
|
||||||
command: |
|
|
||||||
mkdir -p $CIRCLE_TEST_REPORTS/junit/
|
|
||||||
find . -type f -regex ".*/target/.*-reports/.*" -exec cp {} $CIRCLE_TEST_REPORTS/junit/ \;
|
|
||||||
bash <(curl -s https://codecov.io/bash)
|
|
||||||
- store_artifacts:
|
|
||||||
path: /junit/
|
|
||||||
destination: artifacts
|
|
||||||
- store_test_results:
|
|
||||||
path: /junit/
|
|
||||||
destination: testartifacts
|
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
Spring Cloud is released under the non-restrictive Apache 2.0 license,
|
Spring Cloud is released under the non-restrictive Apache 2.0 license,
|
||||||
and follows a very standard Github development process, using Github
|
and follows a very standard Github development process, using Github
|
||||||
tracker for issues and merging pull requests into master. If you want
|
tracker for issues and merging pull requests into main. If you want
|
||||||
to contribute even something trivial please do not hesitate, but
|
to contribute even something trivial please do not hesitate, but
|
||||||
follow the guidelines below.
|
follow the guidelines below.
|
||||||
|
|
||||||
@@ -17,7 +17,7 @@ given the ability to merge pull requests.
|
|||||||
|
|
||||||
## Code of Conduct
|
## Code of Conduct
|
||||||
This project adheres to the Contributor Covenant [code of
|
This project adheres to the Contributor Covenant [code of
|
||||||
conduct](https://github.com/spring-cloud/spring-cloud-build/blob/master/docs/src/main/asciidoc/code-of-conduct.adoc). By participating, you are expected to uphold this code. Please report
|
conduct](https://github.com/spring-cloud/spring-cloud-build/blob/main/docs/src/main/asciidoc/code-of-conduct.adoc). By participating, you are expected to uphold this code. Please report
|
||||||
unacceptable behavior to spring-code-of-conduct@pivotal.io.
|
unacceptable behavior to spring-code-of-conduct@pivotal.io.
|
||||||
|
|
||||||
## Code Conventions and Housekeeping
|
## Code Conventions and Housekeeping
|
||||||
@@ -27,7 +27,7 @@ added after the original pull request but before a merge.
|
|||||||
* Use the Spring Framework code format conventions. If you use Eclipse
|
* Use the Spring Framework code format conventions. If you use Eclipse
|
||||||
you can import formatter settings using the
|
you can import formatter settings using the
|
||||||
`eclipse-code-formatter.xml` file from the
|
`eclipse-code-formatter.xml` file from the
|
||||||
[Spring Cloud Build](https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-dependencies-parent/eclipse-code-formatter.xml) project. If using IntelliJ, you can use the
|
[Spring Cloud Build](https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/main/spring-cloud-dependencies-parent/eclipse-code-formatter.xml) project. If using IntelliJ, you can use the
|
||||||
[Eclipse Code Formatter Plugin](https://plugins.jetbrains.com/plugin/6546) to import the same file.
|
[Eclipse Code Formatter Plugin](https://plugins.jetbrains.com/plugin/6546) to import the same file.
|
||||||
* Make sure all new `.java` files to have a simple Javadoc class comment with at least an
|
* Make sure all new `.java` files to have a simple Javadoc class comment with at least an
|
||||||
`@author` tag identifying you, and preferably at least a paragraph on what the class is
|
`@author` tag identifying you, and preferably at least a paragraph on what the class is
|
||||||
@@ -38,7 +38,7 @@ added after the original pull request but before a merge.
|
|||||||
than cosmetic changes).
|
than cosmetic changes).
|
||||||
* Add some Javadocs and, if you change the namespace, some XSD doc elements.
|
* Add some Javadocs and, if you change the namespace, some XSD doc elements.
|
||||||
* A few unit tests would help a lot as well -- someone has to do it.
|
* A few unit tests would help a lot as well -- someone has to do it.
|
||||||
* If no-one else is using your branch, please rebase it against the current master (or
|
* If no-one else is using your branch, please rebase it against the current main (or
|
||||||
other target branch in the main project).
|
other target branch in the main project).
|
||||||
* When writing a commit message please follow [these conventions](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html),
|
* When writing a commit message please follow [these conventions](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html),
|
||||||
if you are fixing an existing issue please add `Fixes gh-XXXX` at the end of the commit
|
if you are fixing an existing issue please add `Fixes gh-XXXX` at the end of the commit
|
||||||
|
|||||||
@@ -0,0 +1,33 @@
|
|||||||
|
# This workflow will build a Java project with Maven
|
||||||
|
# For more information see: https://help.github.com/actions/language-and-framework-guides/building-and-testing-java-with-maven
|
||||||
|
|
||||||
|
name: Build
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches: [ 3.0.x ]
|
||||||
|
pull_request:
|
||||||
|
branches: [ 3.0.x ]
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
strategy:
|
||||||
|
matrix:
|
||||||
|
java: ["8", "11", "16"]
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v2
|
||||||
|
- name: Set up JDK ${{ matrix.java }}
|
||||||
|
uses: actions/setup-java@v2
|
||||||
|
with:
|
||||||
|
distribution: 'zulu'
|
||||||
|
java-version: ${{ matrix.java }}
|
||||||
|
cache: 'maven'
|
||||||
|
- name: Build with Maven
|
||||||
|
run: ./mvnw clean install -B -U -P sonar
|
||||||
|
- uses: codecov/codecov-action@v1
|
||||||
|
with:
|
||||||
|
fail_ci_if_error: true
|
||||||
+63
-19
@@ -5,9 +5,9 @@ Edit the files in the src/main/asciidoc/ directory instead.
|
|||||||
////
|
////
|
||||||
|
|
||||||
|
|
||||||
image:https://circleci.com/gh/spring-cloud/spring-cloud-openfeign.svg?style=svg["CircleCI", link="https://circleci.com/gh/spring-cloud/spring-cloud-openfeign"]
|
image::https://github.com/spring-cloud/spring-cloud-openfeign/workflows/Build/badge.svg?branch=main&style=svg["Build",link="https://github.com/spring-cloud/spring-cloud-openfeign/actions"]
|
||||||
|
|
||||||
image:https://codecov.io/gh/spring-cloud/spring-cloud-openfeign/branch/master/graph/badge.svg["Codecov", link="https://codecov.io/gh/spring-cloud/spring-cloud-openfeign"]
|
image:https://codecov.io/gh/spring-cloud/spring-cloud-openfeign/branch/main/graph/badge.svg["Codecov", link="https://codecov.io/gh/spring-cloud/spring-cloud-openfeign"]
|
||||||
|
|
||||||
image:https://api.codacy.com/project/badge/Grade/97b04c4e609c4b4f86b415e4437a6484["Codacy code quality", link="https://www.codacy.com/app/Spring-Cloud/spring-cloud-openfeign?utm_source=github.com&utm_medium=referral&utm_content=spring-cloud/spring-cloud-openfeign&utm_campaign=Badge_Grade"]
|
image:https://api.codacy.com/project/badge/Grade/97b04c4e609c4b4f86b415e4437a6484["Codacy code quality", link="https://www.codacy.com/app/Spring-Cloud/spring-cloud-openfeign?utm_source=github.com&utm_medium=referral&utm_content=spring-cloud/spring-cloud-openfeign&utm_campaign=Badge_Grade"]
|
||||||
|
|
||||||
@@ -28,6 +28,8 @@ image:https://api.codacy.com/project/badge/Grade/97b04c4e609c4b4f86b415e4437a648
|
|||||||
:project-full-name: Spring Cloud OpenFeign
|
:project-full-name: Spring Cloud OpenFeign
|
||||||
:all: {asterisk}{asterisk}
|
:all: {asterisk}{asterisk}
|
||||||
|
|
||||||
|
:core_path: {project-root}/spring-cloud-openfeign-core
|
||||||
|
|
||||||
This project provides OpenFeign integrations for Spring Boot apps through autoconfiguration
|
This project provides OpenFeign integrations for Spring Boot apps through autoconfiguration
|
||||||
and binding to the Spring Environment and other Spring programming model idioms.
|
and binding to the Spring Environment and other Spring programming model idioms.
|
||||||
|
|
||||||
@@ -38,7 +40,8 @@ and binding to the Spring Environment and other Spring programming model idioms.
|
|||||||
|
|
||||||
== Building
|
== Building
|
||||||
|
|
||||||
:jdkversion: 1.8
|
|
||||||
|
:jdkversion: 17
|
||||||
|
|
||||||
=== Basic Compile and Test
|
=== Basic Compile and Test
|
||||||
|
|
||||||
@@ -64,23 +67,9 @@ the `.mvn` configuration, so if you find you have to do it to make a
|
|||||||
build succeed, please raise a ticket to get the settings added to
|
build succeed, please raise a ticket to get the settings added to
|
||||||
source control.
|
source control.
|
||||||
|
|
||||||
For hints on how to build the project look in `.travis.yml` if there
|
The projects that require middleware (i.e. Redis) for testing generally
|
||||||
is one. There should be a "script" and maybe "install" command. Also
|
require that a local instance of [Docker](https://www.docker.com/get-started) is installed and running.
|
||||||
look at the "services" section to see if any services need to be
|
|
||||||
running locally (e.g. mongo or rabbit). Ignore the git-related bits
|
|
||||||
that you might find in "before_install" since they're related to setting git
|
|
||||||
credentials and you already have those.
|
|
||||||
|
|
||||||
The projects that require middleware generally include a
|
|
||||||
`docker-compose.yml`, so consider using
|
|
||||||
https://docs.docker.com/compose/[Docker Compose] to run the middeware servers
|
|
||||||
in Docker containers. See the README in the
|
|
||||||
https://github.com/spring-cloud-samples/scripts[scripts demo
|
|
||||||
repository] for specific instructions about the common cases of mongo,
|
|
||||||
rabbit and redis.
|
|
||||||
|
|
||||||
NOTE: If all else fails, build with the command from `.travis.yml` (usually
|
|
||||||
`./mvnw install`).
|
|
||||||
|
|
||||||
=== Documentation
|
=== Documentation
|
||||||
|
|
||||||
@@ -314,3 +303,58 @@ 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`.
|
- `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.
|
IMPORTANT: Remember to set the `Scan Scope` to `All sources` since we apply checkstyle rules for production and test sources.
|
||||||
|
|
||||||
|
=== Duplicate Finder
|
||||||
|
|
||||||
|
Spring Cloud Build brings along the `basepom:duplicate-finder-maven-plugin`, that enables flagging duplicate and conflicting classes and resources on the java classpath.
|
||||||
|
|
||||||
|
==== Duplicate Finder configuration
|
||||||
|
|
||||||
|
Duplicate finder is *enabled by default* and will run in the `verify` phase of your Maven build, but it will only take effect in your project if you add the `duplicate-finder-maven-plugin` to the `build` section of the projecst's `pom.xml`.
|
||||||
|
|
||||||
|
.pom.xml
|
||||||
|
[source,xml]
|
||||||
|
----
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.basepom.maven</groupId>
|
||||||
|
<artifactId>duplicate-finder-maven-plugin</artifactId>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
----
|
||||||
|
|
||||||
|
For other properties, we have set defaults as listed in the https://github.com/basepom/duplicate-finder-maven-plugin/wiki[plugin documentation].
|
||||||
|
|
||||||
|
You can easily override them but setting the value of the selected property prefixed with `duplicate-finder-maven-plugin`. For example, set `duplicate-finder-maven-plugin.skip` to `true` in order to skip duplicates check in your build.
|
||||||
|
|
||||||
|
If you need to add `ignoredClassPatterns` or `ignoredResourcePatterns` to your setup, make sure to add them in the plugin configuration section of your project:
|
||||||
|
|
||||||
|
[source,xml]
|
||||||
|
----
|
||||||
|
<build>
|
||||||
|
<plugins>
|
||||||
|
<plugin>
|
||||||
|
<groupId>org.basepom.maven</groupId>
|
||||||
|
<artifactId>duplicate-finder-maven-plugin</artifactId>
|
||||||
|
<configuration>
|
||||||
|
<ignoredClassPatterns>
|
||||||
|
<ignoredClassPattern>org.joda.time.base.BaseDateTime</ignoredClassPattern>
|
||||||
|
<ignoredClassPattern>.*module-info</ignoredClassPattern>
|
||||||
|
</ignoredClassPatterns>
|
||||||
|
<ignoredResourcePatterns>
|
||||||
|
<ignoredResourcePattern>changelog.txt</ignoredResourcePattern>
|
||||||
|
</ignoredResourcePatterns>
|
||||||
|
</configuration>
|
||||||
|
</plugin>
|
||||||
|
</plugins>
|
||||||
|
</build>
|
||||||
|
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
|
||||||
|
== License
|
||||||
|
|
||||||
|
The project license file is available https://raw.githubusercontent.com/spring-cloud/spring-cloud-openfeign/main/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>
|
<parent>
|
||||||
<groupId>org.springframework.cloud</groupId>
|
<groupId>org.springframework.cloud</groupId>
|
||||||
<artifactId>spring-cloud-openfeign</artifactId>
|
<artifactId>spring-cloud-openfeign</artifactId>
|
||||||
<version>3.0.0-M4</version>
|
<version>3.0.8-SNAPSHOT</version>
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>spring-cloud-openfeign-docs</artifactId>
|
<artifactId>spring-cloud-openfeign-docs</artifactId>
|
||||||
<packaging>jar</packaging>
|
<packaging>jar</packaging>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
image:https://circleci.com/gh/spring-cloud/spring-cloud-openfeign.svg?style=svg["CircleCI", link="https://circleci.com/gh/spring-cloud/spring-cloud-openfeign"]
|
image::https://github.com/spring-cloud/spring-cloud-openfeign/workflows/Build/badge.svg?branch=main&style=svg["Build",link="https://github.com/spring-cloud/spring-cloud-openfeign/actions"]
|
||||||
|
|
||||||
image:https://codecov.io/gh/spring-cloud/spring-cloud-openfeign/branch/master/graph/badge.svg["Codecov", link="https://codecov.io/gh/spring-cloud/spring-cloud-openfeign"]
|
image:https://codecov.io/gh/spring-cloud/spring-cloud-openfeign/branch/main/graph/badge.svg["Codecov", link="https://codecov.io/gh/spring-cloud/spring-cloud-openfeign"]
|
||||||
|
|
||||||
image:https://api.codacy.com/project/badge/Grade/97b04c4e609c4b4f86b415e4437a6484["Codacy code quality", link="https://www.codacy.com/app/Spring-Cloud/spring-cloud-openfeign?utm_source=github.com&utm_medium=referral&utm_content=spring-cloud/spring-cloud-openfeign&utm_campaign=Badge_Grade"]
|
image:https://api.codacy.com/project/badge/Grade/97b04c4e609c4b4f86b415e4437a6484["Codacy code quality", link="https://www.codacy.com/app/Spring-Cloud/spring-cloud-openfeign?utm_source=github.com&utm_medium=referral&utm_content=spring-cloud/spring-cloud-openfeign&utm_campaign=Badge_Grade"]
|
||||||
|
|
||||||
@@ -14,8 +14,12 @@ include::intro.adoc[]
|
|||||||
|
|
||||||
== Building
|
== Building
|
||||||
|
|
||||||
include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/building-jdk8.adoc[]
|
include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/main/docs/src/main/asciidoc/building-jdk8.adoc[]
|
||||||
|
|
||||||
== Contributing
|
== Contributing
|
||||||
|
|
||||||
include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/docs/src/main/asciidoc/contributing.adoc[]
|
include::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/main/docs/src/main/asciidoc/contributing.adoc[]
|
||||||
|
|
||||||
|
== License
|
||||||
|
|
||||||
|
The project license file is available https://raw.githubusercontent.com/spring-cloud/spring-cloud-openfeign/main/LICENSE.txt[here].
|
||||||
|
|||||||
@@ -14,3 +14,5 @@
|
|||||||
:sc-ext: java
|
:sc-ext: java
|
||||||
:project-full-name: Spring Cloud OpenFeign
|
:project-full-name: Spring Cloud OpenFeign
|
||||||
:all: {asterisk}{asterisk}
|
:all: {asterisk}{asterisk}
|
||||||
|
|
||||||
|
:core_path: {project-root}/spring-cloud-openfeign-core
|
||||||
|
|||||||
@@ -1,24 +1,35 @@
|
|||||||
|===
|
|===
|
||||||
|Name | Default | Description
|
|Name | Default | Description
|
||||||
|
|
||||||
|
|feign.autoconfiguration.jackson.enabled | `false` | If true, PageJacksonModule and SortJacksonModule bean will be provided for Jackson page decoding.
|
||||||
|
|feign.circuitbreaker.enabled | `false` | If true, an OpenFeign client will be wrapped with a Spring Cloud CircuitBreaker circuit breaker.
|
||||||
|
|feign.circuitbreaker.group.enabled | `false` | If true, an OpenFeign client will be wrapped with a Spring Cloud CircuitBreaker circuit breaker with with group.
|
||||||
|feign.client.config | |
|
|feign.client.config | |
|
||||||
|feign.client.default-config | default |
|
|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-to-properties | true |
|
|feign.client.default-config | `default` |
|
||||||
|feign.compression.request.enabled | false | Enables the request sent by Feign to be compressed.
|
|feign.client.default-to-properties | `true` |
|
||||||
|feign.compression.request.mime-types | [text/xml, application/xml, application/json] | The list of supported mime types.
|
|feign.client.refresh-enabled | `false` | Enables options value refresh capability for Feign.
|
||||||
|feign.compression.request.min-request-size | 2048 | The minimum threshold content size.
|
|feign.compression.request.enabled | `false` | Enables the request sent by Feign to be compressed.
|
||||||
|feign.compression.response.enabled | false | Enables the response from Feign to be compressed.
|
|feign.compression.request.mime-types | `[text/xml, application/xml, application/json]` | The list of supported mime types.
|
||||||
|feign.compression.response.useGzipDecoder | false | Enables the default gzip decoder to be used.
|
|feign.compression.request.min-request-size | `2048` | The minimum threshold content size.
|
||||||
|feign.httpclient.connection-timeout | 2000 |
|
|feign.compression.response.enabled | `false` | Enables the response from Feign to be compressed.
|
||||||
|feign.httpclient.connection-timer-repeat | 3000 |
|
|feign.compression.response.useGzipDecoder | `false` | Enables the default gzip decoder to be used.
|
||||||
|feign.httpclient.disable-ssl-validation | false |
|
|feign.encoder.charset-from-content-type | `false` | Indicates whether the charset should be derived from the {@code Content-Type} header.
|
||||||
|feign.httpclient.enabled | true | Enables the use of the Apache HTTP Client by Feign.
|
|feign.httpclient.connection-timeout | `2000` |
|
||||||
|feign.httpclient.follow-redirects | true |
|
|feign.httpclient.connection-timer-repeat | `3000` |
|
||||||
|feign.httpclient.max-connections | 200 |
|
|feign.httpclient.disable-ssl-validation | `false` |
|
||||||
|feign.httpclient.max-connections-per-route | 50 |
|
|feign.httpclient.enabled | `true` | Enables the use of the Apache HTTP Client by Feign.
|
||||||
|feign.httpclient.time-to-live | 900 |
|
|feign.httpclient.follow-redirects | `true` |
|
||||||
|
|feign.httpclient.hc5.enabled | `false` | Enables the use of the Apache HTTP Client 5 by Feign.
|
||||||
|
|feign.httpclient.hc5.pool-concurrency-policy | | Pool concurrency policies.
|
||||||
|
|feign.httpclient.hc5.pool-reuse-policy | | Pool connection re-use policies.
|
||||||
|
|feign.httpclient.hc5.socket-timeout | `5` | Default value for socket timeout.
|
||||||
|
|feign.httpclient.hc5.socket-timeout-unit | | Default value for socket timeout unit.
|
||||||
|
|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.httpclient.time-to-live-unit | |
|
||||||
|feign.hystrix.enabled | false | If true, an OpenFeign client will be wrapped with a Hystrix circuit breaker.
|
|feign.metrics.enabled | `true` | Enables metrics capability for Feign.
|
||||||
|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.
|
||||||
|
|
||||||
|===
|
|===
|
||||||
@@ -39,7 +39,7 @@ function check_if_anything_to_sync() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function retrieve_current_branch() {
|
function retrieve_current_branch() {
|
||||||
# Code getting the name of the current branch. For master we want to publish as we did until now
|
# Code getting the name of the current branch. For main we want to publish as we did until now
|
||||||
# https://stackoverflow.com/questions/1593051/how-to-programmatically-determine-the-current-checked-out-git-branch
|
# https://stackoverflow.com/questions/1593051/how-to-programmatically-determine-the-current-checked-out-git-branch
|
||||||
# If there is a branch already passed will reuse it - otherwise will try to find it
|
# If there is a branch already passed will reuse it - otherwise will try to find it
|
||||||
CURRENT_BRANCH=${BRANCH}
|
CURRENT_BRANCH=${BRANCH}
|
||||||
@@ -134,8 +134,8 @@ function add_docs_from_target() {
|
|||||||
|
|
||||||
# Copies the docs by using the retrieved properties from Maven build
|
# Copies the docs by using the retrieved properties from Maven build
|
||||||
function copy_docs_for_current_version() {
|
function copy_docs_for_current_version() {
|
||||||
if [[ "${CURRENT_BRANCH}" == "master" ]] ; then
|
if [[ "${CURRENT_BRANCH}" == "main" ]] ; then
|
||||||
echo -e "Current branch is master - will copy the current docs only to the root folder"
|
echo -e "Current branch is main - will copy the current docs only to the root folder"
|
||||||
for f in docs/target/generated-docs/*; do
|
for f in docs/target/generated-docs/*; do
|
||||||
file=${f#docs/target/generated-docs/*}
|
file=${f#docs/target/generated-docs/*}
|
||||||
if ! git ls-files -i -o --exclude-standard --directory | grep -q ^$file$; then
|
if ! git ls-files -i -o --exclude-standard --directory | grep -q ^$file$; then
|
||||||
@@ -216,7 +216,7 @@ function commit_changes_if_applicable() {
|
|||||||
git commit -a -m "Sync docs from ${CURRENT_BRANCH} to gh-pages" && COMMIT_SUCCESSFUL="yes" || echo "Failed to commit changes"
|
git commit -a -m "Sync docs from ${CURRENT_BRANCH} to gh-pages" && COMMIT_SUCCESSFUL="yes" || echo "Failed to commit changes"
|
||||||
|
|
||||||
# Uncomment the following push if you want to auto push to
|
# Uncomment the following push if you want to auto push to
|
||||||
# the gh-pages branch whenever you commit to master locally.
|
# the gh-pages branch whenever you commit to main locally.
|
||||||
# This is a little extreme. Use with care!
|
# This is a little extreme. Use with care!
|
||||||
###################################################################
|
###################################################################
|
||||||
if [[ "${COMMIT_SUCCESSFUL}" == "yes" ]] ; then
|
if [[ "${COMMIT_SUCCESSFUL}" == "yes" ]] ; then
|
||||||
@@ -249,7 +249,7 @@ The idea of this script is to update gh-pages branch with the generated docs. Wi
|
|||||||
the script will work in the following manner:
|
the script will work in the following manner:
|
||||||
|
|
||||||
- if there's no gh-pages / target for docs module then the script ends
|
- if there's no gh-pages / target for docs module then the script ends
|
||||||
- for master branch the generated docs are copied to the root of gh-pages branch
|
- for main branch the generated docs are copied to the root of gh-pages branch
|
||||||
- for any other branch (if that branch is whitelisted) a subfolder with branch name is created
|
- for any other branch (if that branch is whitelisted) a subfolder with branch name is created
|
||||||
and docs are copied there
|
and docs are copied there
|
||||||
- if the version switch is passed (-v) then a tag with (v) prefix will be retrieved and a folder
|
- if the version switch is passed (-v) then a tag with (v) prefix will be retrieved and a folder
|
||||||
@@ -327,4 +327,4 @@ build_docs_if_applicable
|
|||||||
retrieve_doc_properties
|
retrieve_doc_properties
|
||||||
stash_changes
|
stash_changes
|
||||||
add_docs_from_target
|
add_docs_from_target
|
||||||
checkout_previous_branch
|
checkout_previous_branch
|
||||||
|
|||||||
@@ -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/main/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.
|
It has pluggable annotation support including Feign annotations and JAX-RS annotations.
|
||||||
Feign also supports pluggable encoders and decoders.
|
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 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]]
|
[[netflix-feign-starter]]
|
||||||
=== How to Include Feign
|
=== How to Include Feign
|
||||||
@@ -32,9 +32,9 @@ Example spring boot app
|
|||||||
@EnableFeignClients
|
@EnableFeignClients
|
||||||
public class Application {
|
public class Application {
|
||||||
|
|
||||||
public static void main(String[] args) {
|
public static void main(String[] args) {
|
||||||
SpringApplication.run(Application.class, args);
|
SpringApplication.run(Application.class, args);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
----
|
----
|
||||||
@@ -44,29 +44,36 @@ public class Application {
|
|||||||
----
|
----
|
||||||
@FeignClient("stores")
|
@FeignClient("stores")
|
||||||
public interface StoreClient {
|
public interface StoreClient {
|
||||||
@RequestMapping(method = RequestMethod.GET, value = "/stores")
|
@RequestMapping(method = RequestMethod.GET, value = "/stores")
|
||||||
List<Store> getStores();
|
List<Store> getStores();
|
||||||
|
|
||||||
@RequestMapping(method = RequestMethod.GET, value = "/stores")
|
@RequestMapping(method = RequestMethod.GET, value = "/stores")
|
||||||
Page<Store> getStores(Pageable pageable);
|
Page<Store> getStores(Pageable pageable);
|
||||||
|
|
||||||
@RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
|
@RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
|
||||||
Store update(@PathVariable("storeId") Long storeId, Store store);
|
Store update(@PathVariable("storeId") Long storeId, Store store);
|
||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
In the `@FeignClient` annotation the String value ("stores" above) is an arbitrary client name, which is used to create a https://github.com/spring-cloud/spring-cloud-commons/blob/master/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/blocking/client/BlockingLoadBalancerClient.java[Spring Cloud LoadBalancer client].
|
In the `@FeignClient` annotation the String value ("stores" above) is an arbitrary client name, which is used to create a https://github.com/spring-cloud/spring-cloud-commons/blob/main/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/blocking/client/BlockingLoadBalancerClient.java[Spring Cloud LoadBalancer client].
|
||||||
You can also specify a URL using the `url` attribute
|
You can also specify a URL using the `url` attribute
|
||||||
(absolute value or just a hostname). The name of the bean in the
|
(absolute value or just a hostname). The name of the bean in the
|
||||||
application context is the fully qualified name of the interface.
|
application context is the fully qualified name of the interface.
|
||||||
To specify your own alias value you can use the `qualifier` value
|
To specify your own alias value you can use the `qualifiers` value
|
||||||
of the `@FeignClient` annotation.
|
of the `@FeignClient` annotation.
|
||||||
|
|
||||||
The load-balancer client above will want to discover the physical addresses
|
The load-balancer client above will want to discover the physical addresses
|
||||||
for the "stores" service. If your application is a Eureka client then
|
for the "stores" service. If your application is a Eureka client then
|
||||||
it will resolve the service in the Eureka service registry. If you
|
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
|
don't want to use Eureka, you can 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`].
|
in your external configuration using https://docs.spring.io/spring-cloud-commons/docs/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].
|
||||||
|
|
||||||
|
TIP: To use `@EnableFeignClients` annotation on `@Configuration`-annotated-classes, make sure to specify where the clients are located, for example:
|
||||||
|
`@EnableFeignClients(basePackages = "com.example.clients")`
|
||||||
|
or list them explicitly:
|
||||||
|
`@EnableFeignClients(clients = InventoryServiceFeignClient.class)`
|
||||||
|
|
||||||
[[spring-cloud-feign-overriding-defaults]]
|
[[spring-cloud-feign-overriding-defaults]]
|
||||||
=== Overriding Feign Defaults
|
=== Overriding Feign Defaults
|
||||||
@@ -82,7 +89,7 @@ Spring Cloud lets you take full control of the feign client by declaring additio
|
|||||||
----
|
----
|
||||||
@FeignClient(name = "stores", configuration = FooConfiguration.class)
|
@FeignClient(name = "stores", configuration = FooConfiguration.class)
|
||||||
public interface StoreClient {
|
public interface StoreClient {
|
||||||
//..
|
//..
|
||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
@@ -90,8 +97,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: `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
|
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
|
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.
|
and it will be used as part of the name of the configuration bean created for that client.
|
||||||
@@ -104,7 +109,7 @@ Placeholders are supported in the `name` and `url` attributes.
|
|||||||
----
|
----
|
||||||
@FeignClient(name = "${feign.name}", url = "${feign.url}")
|
@FeignClient(name = "${feign.name}", url = "${feign.url}")
|
||||||
public interface StoreClient {
|
public interface StoreClient {
|
||||||
//..
|
//..
|
||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
@@ -113,15 +118,16 @@ Spring Cloud OpenFeign provides the following beans by default for feign (`BeanT
|
|||||||
* `Decoder` feignDecoder: `ResponseEntityDecoder` (which wraps a `SpringDecoder`)
|
* `Decoder` feignDecoder: `ResponseEntityDecoder` (which wraps a `SpringDecoder`)
|
||||||
* `Encoder` feignEncoder: `SpringEncoder`
|
* `Encoder` feignEncoder: `SpringEncoder`
|
||||||
* `Logger` feignLogger: `Slf4jLogger`
|
* `Logger` feignLogger: `Slf4jLogger`
|
||||||
|
* `MicrometerCapability` micrometerCapability: If `feign-micrometer` is on the classpath and `MeterRegistry` is available
|
||||||
* `Contract` feignContract: `SpringMvcContract`
|
* `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.
|
* `Client` feignClient: If Spring Cloud LoadBalancer is on the classpath, `FeignBlockingLoadBalancerClient` is used.
|
||||||
If none of them is in the classpath, the default feign client is used.
|
If none of them is on the classpath, the default feign client is used.
|
||||||
|
|
||||||
NOTE: `spring-cloud-starter-openfeign` supports `spring-cloud-starter-loadbalancer`. However, as is an optional dependency, you need to make sure it been added to your project if you want to use it.
|
NOTE: `spring-cloud-starter-openfeign` supports `spring-cloud-starter-loadbalancer`. However, as is an optional dependency, you need to make sure it been added to your project if you want to use it.
|
||||||
|
|
||||||
The OkHttpClient and ApacheHttpClient feign clients can be used by setting `feign.okhttp.enabled` or `feign.httpclient.enabled` to `true`, respectively, and having them on the classpath.
|
The OkHttpClient and ApacheHttpClient and ApacheHC5 feign clients can be used by setting `feign.okhttp.enabled` or `feign.httpclient.enabled` or `feign.httpclient.hc5.enabled` to `true`, respectively, and having them on the classpath.
|
||||||
You can customize the HTTP client used by providing a bean of either `org.apache.http.impl.client.CloseableHttpClient` when using Apache or `okhttp3.OkHttpClient` when using OK HTTP.
|
You can customize the HTTP client used by providing a bean of either `org.apache.http.impl.client.CloseableHttpClient` when using Apache or `okhttp3.OkHttpClient` when using OK HTTP or `org.apache.hc.client5.http.impl.classic.CloseableHttpClient` when using Apache HC5.
|
||||||
|
|
||||||
Spring Cloud OpenFeign _does not_ provide the following beans by default for feign, but still looks up beans of these types from the application context to create the feign client:
|
Spring Cloud OpenFeign _does not_ provide the following beans by default for feign, but still looks up beans of these types from the application context to create the feign client:
|
||||||
|
|
||||||
@@ -132,6 +138,7 @@ Spring Cloud OpenFeign _does not_ provide the following beans by default for fei
|
|||||||
* `Collection<RequestInterceptor>`
|
* `Collection<RequestInterceptor>`
|
||||||
* `SetterFactory`
|
* `SetterFactory`
|
||||||
* `QueryMapEncoder`
|
* `QueryMapEncoder`
|
||||||
|
* `Capability` (`MicrometerCapability` is provided by default)
|
||||||
|
|
||||||
A bean of `Retryer.NEVER_RETRY` with the type `Retryer` is created by default, which will disable retrying.
|
A bean of `Retryer.NEVER_RETRY` with the type `Retryer` is created by default, which will disable retrying.
|
||||||
Notice this retrying behavior is different from the Feign default one, where it will automatically retry IOExceptions,
|
Notice this retrying behavior is different from the Feign default one, where it will automatically retry IOExceptions,
|
||||||
@@ -143,15 +150,15 @@ Creating a bean of one of those type and placing it in a `@FeignClient` configur
|
|||||||
----
|
----
|
||||||
@Configuration
|
@Configuration
|
||||||
public class FooConfiguration {
|
public class FooConfiguration {
|
||||||
@Bean
|
@Bean
|
||||||
public Contract feignContract() {
|
public Contract feignContract() {
|
||||||
return new feign.Contract.Default();
|
return new feign.Contract.Default();
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
|
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
|
||||||
return new BasicAuthRequestInterceptor("user", "password");
|
return new BasicAuthRequestInterceptor("user", "password");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
@@ -163,63 +170,53 @@ application.yml
|
|||||||
[source,yaml]
|
[source,yaml]
|
||||||
----
|
----
|
||||||
feign:
|
feign:
|
||||||
client:
|
client:
|
||||||
config:
|
config:
|
||||||
feignName:
|
feignName:
|
||||||
connectTimeout: 5000
|
connectTimeout: 5000
|
||||||
readTimeout: 5000
|
readTimeout: 5000
|
||||||
loggerLevel: full
|
loggerLevel: full
|
||||||
errorDecoder: com.example.SimpleErrorDecoder
|
errorDecoder: com.example.SimpleErrorDecoder
|
||||||
retryer: com.example.SimpleRetryer
|
retryer: com.example.SimpleRetryer
|
||||||
requestInterceptors:
|
defaultQueryParameters:
|
||||||
- com.example.FooRequestInterceptor
|
query: queryValue
|
||||||
- com.example.BarRequestInterceptor
|
defaultRequestHeaders:
|
||||||
decode404: false
|
header: headerValue
|
||||||
encoder: com.example.SimpleEncoder
|
requestInterceptors:
|
||||||
decoder: com.example.SimpleDecoder
|
- com.example.FooRequestInterceptor
|
||||||
contract: com.example.SimpleContract
|
- com.example.BarRequestInterceptor
|
||||||
|
decode404: false
|
||||||
|
encoder: com.example.SimpleEncoder
|
||||||
|
decoder: com.example.SimpleDecoder
|
||||||
|
contract: com.example.SimpleContract
|
||||||
|
capabilities:
|
||||||
|
- com.example.FooCapability
|
||||||
|
- com.example.BarCapability
|
||||||
|
metrics.enabled: false
|
||||||
----
|
----
|
||||||
|
|
||||||
Default configurations can be specified in the `@EnableFeignClients` attribute `defaultConfiguration` in a similar manner as described above. The difference is that this configuration will apply to _all_ feign clients.
|
Default configurations can be specified in the `@EnableFeignClients` attribute `defaultConfiguration` in a similar manner as described above. The difference is that this configuration will apply to _all_ feign clients.
|
||||||
|
|
||||||
If you prefer using configuration properties to configured all `@FeignClient`, you can create configuration properties with `default` feign name.
|
If you prefer using configuration properties to configure 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
|
application.yml
|
||||||
[source,yaml]
|
[source,yaml]
|
||||||
----
|
----
|
||||||
feign:
|
feign:
|
||||||
client:
|
client:
|
||||||
config:
|
config:
|
||||||
default:
|
default:
|
||||||
connectTimeout: 5000
|
connectTimeout: 5000
|
||||||
readTimeout: 5000
|
readTimeout: 5000
|
||||||
loggerLevel: basic
|
loggerLevel: basic
|
||||||
----
|
----
|
||||||
|
|
||||||
If we create both `@Configuration` bean and configuration properties, configuration properties will win.
|
If we create both `@Configuration` bean and configuration properties, configuration properties will win.
|
||||||
It will override `@Configuration` values. But if you want to change the priority to `@Configuration`,
|
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`.
|
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
|
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
|
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
|
we have to use `contextId` attribute of the `@FeignClient` in order to avoid name
|
||||||
@@ -229,7 +226,7 @@ collision of these configuration beans.
|
|||||||
----
|
----
|
||||||
@FeignClient(contextId = "fooClient", name = "stores", configuration = FooConfiguration.class)
|
@FeignClient(contextId = "fooClient", name = "stores", configuration = FooConfiguration.class)
|
||||||
public interface FooClient {
|
public interface FooClient {
|
||||||
//..
|
//..
|
||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
@@ -237,7 +234,7 @@ public interface FooClient {
|
|||||||
----
|
----
|
||||||
@FeignClient(contextId = "barClient", name = "stores", configuration = BarConfiguration.class)
|
@FeignClient(contextId = "barClient", name = "stores", configuration = BarConfiguration.class)
|
||||||
public interface BarClient {
|
public interface BarClient {
|
||||||
//..
|
//..
|
||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
@@ -264,6 +261,24 @@ 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`.
|
||||||
|
|
||||||
|
==== `SpringEncoder` configuration
|
||||||
|
|
||||||
|
In the `SpringEncoder` that we provide, we set `null` charset for binary content types and `UTF-8` for all the other ones.
|
||||||
|
|
||||||
|
You can modify this behaviour to derive the charset from the `Content-Type` header charset instead by setting the value of `feign.encoder.charset-from-content-type` to `true`.
|
||||||
|
|
||||||
|
[[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
|
=== Creating Feign Clients Manually
|
||||||
|
|
||||||
In some cases it might be necessary to customize your Feign Clients in a way that is not
|
In some cases it might be necessary to customize your Feign Clients in a way that is not
|
||||||
@@ -281,12 +296,13 @@ class FooController {
|
|||||||
|
|
||||||
private FooClient adminClient;
|
private FooClient adminClient;
|
||||||
|
|
||||||
@Autowired
|
@Autowired
|
||||||
public FooController(Decoder decoder, Encoder encoder, Client client, Contract contract) {
|
public FooController(Client client, Encoder encoder, Decoder decoder, Contract contract, MicrometerCapability micrometerCapability) {
|
||||||
this.fooClient = Feign.builder().client(client)
|
this.fooClient = Feign.builder().client(client)
|
||||||
.encoder(encoder)
|
.encoder(encoder)
|
||||||
.decoder(decoder)
|
.decoder(decoder)
|
||||||
.contract(contract)
|
.contract(contract)
|
||||||
|
.addCapability(micrometerCapability)
|
||||||
.requestInterceptor(new BasicAuthRequestInterceptor("user", "user"))
|
.requestInterceptor(new BasicAuthRequestInterceptor("user", "user"))
|
||||||
.target(FooClient.class, "https://PROD-SVC");
|
.target(FooClient.class, "https://PROD-SVC");
|
||||||
|
|
||||||
@@ -294,9 +310,10 @@ class FooController {
|
|||||||
.encoder(encoder)
|
.encoder(encoder)
|
||||||
.decoder(decoder)
|
.decoder(decoder)
|
||||||
.contract(contract)
|
.contract(contract)
|
||||||
|
.addCapability(micrometerCapability)
|
||||||
.requestInterceptor(new BasicAuthRequestInterceptor("admin", "admin"))
|
.requestInterceptor(new BasicAuthRequestInterceptor("admin", "admin"))
|
||||||
.target(FooClient.class, "https://PROD-SVC");
|
.target(FooClient.class, "https://PROD-SVC");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
@@ -312,18 +329,18 @@ the default Feign native annotations.
|
|||||||
You can also use the `Builder`to configure FeignClient not to inherit beans from the parent context.
|
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`.
|
You can do this by overriding calling `inheritParentContext(false)` on the `Builder`.
|
||||||
|
|
||||||
[[spring-cloud-feign-hystrix]]
|
[[spring-cloud-feign-circuitbreaker]]
|
||||||
=== Feign Hystrix Support
|
=== 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]
|
[source,java,indent=0]
|
||||||
----
|
----
|
||||||
@Configuration
|
@Configuration
|
||||||
public class FooConfiguration {
|
public class FooConfiguration {
|
||||||
@Bean
|
@Bean
|
||||||
@Scope("prototype")
|
@Scope("prototype")
|
||||||
public Feign.Builder feignBuilder() {
|
public Feign.Builder feignBuilder() {
|
||||||
return Feign.builder();
|
return Feign.builder();
|
||||||
@@ -331,60 +348,104 @@ public class FooConfiguration {
|
|||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
WARNING: Prior to the Spring Cloud Dalston release, if Hystrix was on the classpath Feign would have wrapped
|
The circuit breaker name follows this pattern `<feignClientClassName>#<calledMethod>(<parameterTypes>)`. When calling a `@FeignClient` with `FooClient` interface and the called interface method that has no parameters is `bar` then the circuit breaker name will be `FooClient#bar()`.
|
||||||
all methods in a circuit breaker by default. This default behavior was changed in Spring Cloud Dalston in
|
|
||||||
favor for an opt-in approach.
|
|
||||||
|
|
||||||
[[spring-cloud-feign-hystrix-fallback]]
|
NOTE: As of 2020.0.2, the circuit breaker name pattern has changed from `<feignClientName>_<calledMethod>`.
|
||||||
=== Feign Hystrix Fallbacks
|
Using `CircuitBreakerNameResolver` introduced in 2020.0.4, circuit breaker names can retain the old pattern.
|
||||||
|
|
||||||
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.
|
Providing a bean of `CircuitBreakerNameResolver`, you can change the circuit breaker name pattern.
|
||||||
|
[source,java,indent=0]
|
||||||
|
----
|
||||||
|
@Configuration
|
||||||
|
public class FooConfiguration {
|
||||||
|
@Bean
|
||||||
|
public CircuitBreakerNameResolver circuitBreakerNameResolver() {
|
||||||
|
return (String feignClientName, Target<?> target, Method method) -> feignClientName + "_" + method.getName();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
To enable Spring Cloud CircuitBreaker group set the `feign.circuitbreaker.group.enabled` property to `true` (by default `false`).
|
||||||
|
|
||||||
|
|
||||||
|
[[spring-cloud-feign-circuitbreaker-fallback]]
|
||||||
|
=== Feign Spring Cloud CircuitBreaker Fallbacks
|
||||||
|
|
||||||
|
Spring Cloud CircuitBreaker supports the notion of a fallback: a default code path that is executed when the circuit is open or there is an error. To enable fallbacks for a given `@FeignClient` set the `fallback` attribute to the class name that implements the fallback. You also need to declare your implementation as a Spring bean.
|
||||||
|
|
||||||
[source,java,indent=0]
|
[source,java,indent=0]
|
||||||
----
|
----
|
||||||
@FeignClient(name = "hello", fallback = HystrixClientFallback.class)
|
@FeignClient(name = "test", url = "http://localhost:${server.port}/", fallback = Fallback.class)
|
||||||
protected interface HystrixClient {
|
protected interface TestClient {
|
||||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
|
||||||
Hello iFailSometimes();
|
|
||||||
}
|
|
||||||
|
|
||||||
static class HystrixClientFallback implements HystrixClient {
|
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||||
@Override
|
Hello getHello();
|
||||||
public Hello iFailSometimes() {
|
|
||||||
return new Hello("fallback");
|
@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";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
If one needs access to the cause that made the fallback trigger, one can use the `fallbackFactory` attribute inside `@FeignClient`.
|
If one needs access to the cause that made the fallback trigger, one can use the `fallbackFactory` attribute inside `@FeignClient`.
|
||||||
|
|
||||||
[source,java,indent=0]
|
[source,java,indent=0]
|
||||||
----
|
----
|
||||||
@FeignClient(name = "hello", fallbackFactory = HystrixClientFallbackFactory.class)
|
@FeignClient(name = "testClientWithFactory", url = "http://localhost:${server.port}/",
|
||||||
protected interface HystrixClient {
|
fallbackFactory = TestFallbackFactory.class)
|
||||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
protected interface TestClientWithFactory {
|
||||||
Hello iFailSometimes();
|
|
||||||
}
|
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||||
|
Hello getHello();
|
||||||
|
|
||||||
|
@RequestMapping(method = RequestMethod.GET, value = "/hellonotfound")
|
||||||
|
String getException();
|
||||||
|
|
||||||
@Component
|
|
||||||
static class HystrixClientFallbackFactory implements FallbackFactory<HystrixClient> {
|
|
||||||
@Override
|
|
||||||
public HystrixClient create(Throwable cause) {
|
|
||||||
return new HystrixClient() {
|
|
||||||
@Override
|
|
||||||
public Hello iFailSometimes() {
|
|
||||||
return new Hello("fallback; reason was: " + cause.getMessage());
|
|
||||||
}
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
----
|
|
||||||
|
|
||||||
WARNING: There is a limitation with the implementation of fallbacks in Feign and how Hystrix fallbacks work. Fallbacks are currently not supported for methods that return `com.netflix.hystrix.HystrixCommand` and `rx.Observable`.
|
@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";
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
=== Feign and `@Primary`
|
=== 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]
|
[source,java,indent=0]
|
||||||
----
|
----
|
||||||
@@ -405,8 +466,8 @@ This allows grouping common operations into convenient base interfaces.
|
|||||||
----
|
----
|
||||||
public interface UserService {
|
public interface UserService {
|
||||||
|
|
||||||
@RequestMapping(method = RequestMethod.GET, value ="/users/{id}")
|
@RequestMapping(method = RequestMethod.GET, value ="/users/{id}")
|
||||||
User getUser(@PathVariable("id") long id);
|
User getUser(@PathVariable("id") long id);
|
||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
@@ -430,10 +491,7 @@ public interface UserClient extends UserService {
|
|||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
NOTE: It is generally not advisable to share an interface between a
|
WARNING: `@FeignClient` interfaces should not be shared between server and client and annotating `@FeignClient` interfaces with `@RequestMapping` on class level is no longer supported.
|
||||||
server and a client. It introduces tight coupling, and also actually
|
|
||||||
doesn't work with Spring MVC in its current form (method parameter
|
|
||||||
mapping is not inherited).
|
|
||||||
|
|
||||||
=== Feign request/response compression
|
=== Feign request/response compression
|
||||||
|
|
||||||
@@ -489,10 +547,63 @@ For example, the following would set the `Logger.Level` to `FULL`:
|
|||||||
----
|
----
|
||||||
@Configuration
|
@Configuration
|
||||||
public class FooConfiguration {
|
public class FooConfiguration {
|
||||||
@Bean
|
@Bean
|
||||||
Logger.Level feignLoggerLevel() {
|
Logger.Level feignLoggerLevel() {
|
||||||
return Logger.Level.FULL;
|
return Logger.Level.FULL;
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
=== Feign Capability support
|
||||||
|
|
||||||
|
The Feign capabilities expose core Feign components so that these components can be modified. For example, the capabilities can take the `Client`, _decorate_ it, and give the decorated instance back to Feign.
|
||||||
|
The support for metrics libraries is a good real-life example for this. See <<feign-metrics>>.
|
||||||
|
|
||||||
|
Creating one or more `Capability` beans and placing them in a `@FeignClient` configuration lets you register them and modify the behavior of the involved client.
|
||||||
|
|
||||||
|
[source,java,indent=0]
|
||||||
|
----
|
||||||
|
@Configuration
|
||||||
|
public class FooConfiguration {
|
||||||
|
@Bean
|
||||||
|
Capability customCapability() {
|
||||||
|
return new CustomCapability();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
----
|
||||||
|
|
||||||
|
=== Feign metrics
|
||||||
|
|
||||||
|
If all of the following conditions are true, a `MicrometerCapability` bean is created and registered so that your Feign client publishes metrics to Micrometer:
|
||||||
|
|
||||||
|
* `feign-micrometer` is on the classpath
|
||||||
|
* A `MeterRegistry` bean is available
|
||||||
|
* feign metrics properties are set to `true` (by default)
|
||||||
|
- `feign.metrics.enabled=true` (for all clients)
|
||||||
|
- `feign.client.config.feignName.metrics.enabled=true` (for a single client)
|
||||||
|
|
||||||
|
NOTE: If your application already uses Micrometer, enabling metrics is as simple as putting `feign-micrometer` onto your classpath.
|
||||||
|
|
||||||
|
You can also disable the feature by either:
|
||||||
|
|
||||||
|
* excluding `feign-micrometer` from your classpath
|
||||||
|
* setting one of the feign metrics properties to `false`
|
||||||
|
- `feign.metrics.enabled=false`
|
||||||
|
- `feign.client.config.feignName.metrics.enabled=false`
|
||||||
|
|
||||||
|
NOTE: `feign.metrics.enabled=false` disables metrics support for *all* Feign clients regardless of the value of the client-level flags: `feign.client.config.feignName.metrics.enabled`.
|
||||||
|
If you want to enable or disable merics per client, don't set `feign.metrics.enabled` and use `feign.client.config.feignName.metrics.enabled`.
|
||||||
|
|
||||||
|
You can also customize the `MicrometerCapability` by registering your own bean:
|
||||||
|
|
||||||
|
[source,java,indent=0]
|
||||||
|
----
|
||||||
|
@Configuration
|
||||||
|
public class FooConfiguration {
|
||||||
|
@Bean
|
||||||
|
public MicrometerCapability micrometerCapability(MeterRegistry meterRegistry) {
|
||||||
|
return new MicrometerCapability(meterRegistry);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
@@ -511,10 +622,10 @@ For example, the `Params` class defines parameters `param1` and `param2`:
|
|||||||
----
|
----
|
||||||
// Params.java
|
// Params.java
|
||||||
public class Params {
|
public class Params {
|
||||||
private String param1;
|
private String param1;
|
||||||
private String param2;
|
private String param2;
|
||||||
|
|
||||||
// [Getters and setters omitted for brevity]
|
// [Getters and setters omitted for brevity]
|
||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
@@ -525,8 +636,8 @@ The following feign client uses the `Params` class by using the `@SpringQueryMap
|
|||||||
@FeignClient("demo")
|
@FeignClient("demo")
|
||||||
public interface DemoTemplate {
|
public interface DemoTemplate {
|
||||||
|
|
||||||
@GetMapping(path = "/demo")
|
@GetMapping(path = "/demo")
|
||||||
String demoEndpoint(@SpringQueryMap Params params);
|
String demoEndpoint(@SpringQueryMap Params params);
|
||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
@@ -547,8 +658,8 @@ and deserialize HATEOAS representation models: https://docs.spring.io/spring-hat
|
|||||||
@FeignClient("demo")
|
@FeignClient("demo")
|
||||||
public interface DemoTemplate {
|
public interface DemoTemplate {
|
||||||
|
|
||||||
@GetMapping(path = "/stores")
|
@GetMapping(path = "/stores")
|
||||||
CollectionModel<Store> getStores();
|
CollectionModel<Store> getStores();
|
||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
@@ -577,13 +688,13 @@ Note that both variable name and the path segment placeholder are called `matrix
|
|||||||
@FeignClient("demo")
|
@FeignClient("demo")
|
||||||
public interface DemoTemplate {
|
public interface DemoTemplate {
|
||||||
|
|
||||||
@GetMapping(path = "/stores")
|
@GetMapping(path = "/stores")
|
||||||
CollectionModel<Store> getStores();
|
CollectionModel<Store> getStores();
|
||||||
}
|
}
|
||||||
----
|
----
|
||||||
|
|
||||||
=== Feign `CollectionFormat` support
|
=== 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.
|
In the following example, the `CSV` format is used instead of the default `EXPLODED` to process the method.
|
||||||
|
|
||||||
@@ -602,7 +713,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.
|
TIP: Set the `CSV` format while sending `Pageable` as a query parameter in order for it to be encoded correctly.
|
||||||
|
|
||||||
=== Reactive Support
|
=== 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.
|
Until that is done, we recommend using https://github.com/Playtika/feign-reactive[feign-reactive] for Spring WebClient support.
|
||||||
|
|
||||||
@@ -617,6 +728,26 @@ To work around this problem you can use an `ObjectProvider` when autowiring your
|
|||||||
ObjectProvider<TestFeginClient> testFeginClient;
|
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
|
||||||
|
----
|
||||||
|
|
||||||
|
=== Spring `@RefreshScope` Support
|
||||||
|
If Feign client refresh is enabled, each feign client is created with `feign.Request.Options` as a refresh-scoped bean. This means properties such as `connectTimeout` and `readTimeout` can be refreshed against any Feign client instance through `POST /actuator/refresh`.
|
||||||
|
|
||||||
|
By default, refresh behavior in Feign clients is disabled. Use the following property to enable refresh behavior:
|
||||||
|
[source,java]
|
||||||
|
----
|
||||||
|
feign.client.refresh-enabled=true
|
||||||
|
----
|
||||||
|
TIP: DO NOT annotate the `@FeignClient` interface with the `@RefreshScope` annotation.
|
||||||
|
|
||||||
|
|
||||||
== Configuration properties
|
== Configuration properties
|
||||||
|
|
||||||
To see the list of all Sleuth related configuration properties please check link:appendix.html[the Appendix page].
|
To see the list of all Spring Cloud OpenFeign 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">
|
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>
|
<modelVersion>4.0.0</modelVersion>
|
||||||
<artifactId>spring-cloud-openfeign</artifactId>
|
<artifactId>spring-cloud-openfeign</artifactId>
|
||||||
<version>3.0.0-M4</version>
|
<version>3.0.8-SNAPSHOT</version>
|
||||||
<packaging>pom</packaging>
|
<packaging>pom</packaging>
|
||||||
<name>Spring Cloud OpenFeign</name>
|
<name>Spring Cloud OpenFeign</name>
|
||||||
<description>Spring Cloud OpenFeign</description>
|
<description>Spring Cloud OpenFeign</description>
|
||||||
<parent>
|
<parent>
|
||||||
<groupId>org.springframework.cloud</groupId>
|
<groupId>org.springframework.cloud</groupId>
|
||||||
<artifactId>spring-cloud-build</artifactId>
|
<artifactId>spring-cloud-build</artifactId>
|
||||||
<version>3.0.0-M4</version>
|
<version>3.0.5</version>
|
||||||
<relativePath/>
|
<relativePath/>
|
||||||
</parent>
|
</parent>
|
||||||
<scm>
|
<scm>
|
||||||
@@ -25,8 +25,8 @@
|
|||||||
</scm>
|
</scm>
|
||||||
<properties>
|
<properties>
|
||||||
<main.basedir>${basedir}</main.basedir>
|
<main.basedir>${basedir}</main.basedir>
|
||||||
<jackson.version>2.11.2</jackson.version>
|
<jackson.version>2.11.3</jackson.version>
|
||||||
<spring-cloud-commons.version>3.0.0-M4</spring-cloud-commons.version>
|
<spring-cloud-commons.version>3.0.7-SNAPSHOT</spring-cloud-commons.version>
|
||||||
|
|
||||||
<!-- Plugin versions -->
|
<!-- Plugin versions -->
|
||||||
<maven-eclipse-plugin.version>2.10</maven-eclipse-plugin.version>
|
<maven-eclipse-plugin.version>2.10</maven-eclipse-plugin.version>
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
<parent>
|
<parent>
|
||||||
<groupId>org.springframework.cloud</groupId>
|
<groupId>org.springframework.cloud</groupId>
|
||||||
<artifactId>spring-cloud-openfeign</artifactId>
|
<artifactId>spring-cloud-openfeign</artifactId>
|
||||||
<version>3.0.0-M4</version>
|
<version>3.0.8-SNAPSHOT</version>
|
||||||
<relativePath>..</relativePath> <!-- lookup parent from repository -->
|
<relativePath>..</relativePath> <!-- lookup parent from repository -->
|
||||||
</parent>
|
</parent>
|
||||||
<artifactId>spring-cloud-openfeign-core</artifactId>
|
<artifactId>spring-cloud-openfeign-core</artifactId>
|
||||||
@@ -99,11 +99,21 @@
|
|||||||
<artifactId>feign-slf4j</artifactId>
|
<artifactId>feign-slf4j</artifactId>
|
||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.github.openfeign</groupId>
|
||||||
|
<artifactId>feign-micrometer</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.github.openfeign</groupId>
|
<groupId>io.github.openfeign</groupId>
|
||||||
<artifactId>feign-httpclient</artifactId>
|
<artifactId>feign-httpclient</artifactId>
|
||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.github.openfeign</groupId>
|
||||||
|
<artifactId>feign-hc5</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.github.openfeign</groupId>
|
<groupId>io.github.openfeign</groupId>
|
||||||
<artifactId>feign-okhttp</artifactId>
|
<artifactId>feign-okhttp</artifactId>
|
||||||
@@ -119,6 +129,11 @@
|
|||||||
<artifactId>okhttp</artifactId>
|
<artifactId>okhttp</artifactId>
|
||||||
<optional>true</optional>
|
<optional>true</optional>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.security.oauth.boot</groupId>
|
||||||
|
<artifactId>spring-security-oauth2-autoconfigure</artifactId>
|
||||||
|
<optional>true</optional>
|
||||||
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.springframework.boot</groupId>
|
<groupId>org.springframework.boot</groupId>
|
||||||
<artifactId>spring-boot-autoconfigure-processor</artifactId>
|
<artifactId>spring-boot-autoconfigure-processor</artifactId>
|
||||||
@@ -168,13 +183,13 @@
|
|||||||
<dependency>
|
<dependency>
|
||||||
<groupId>com.google.protobuf</groupId>
|
<groupId>com.google.protobuf</groupId>
|
||||||
<artifactId>protobuf-java</artifactId>
|
<artifactId>protobuf-java</artifactId>
|
||||||
<version>3.13.0</version>
|
<version>3.19.1</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>io.vavr</groupId>
|
<groupId>io.vavr</groupId>
|
||||||
<artifactId>vavr</artifactId>
|
<artifactId>vavr</artifactId>
|
||||||
<version>0.10.3</version>
|
<version>0.10.4</version>
|
||||||
<scope>test</scope>
|
<scope>test</scope>
|
||||||
</dependency>
|
</dependency>
|
||||||
<dependency>
|
<dependency>
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+34
@@ -0,0 +1,34 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.cloud.openfeign;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
import feign.Target;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to resolve a circuitbreaker name which will be used in
|
||||||
|
* {@link org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory}.
|
||||||
|
*
|
||||||
|
* @author Kwangyong Kim
|
||||||
|
* @since 2020.0.4
|
||||||
|
*/
|
||||||
|
public interface CircuitBreakerNameResolver {
|
||||||
|
|
||||||
|
String resolveCircuitBreakerName(String feignClientName, Target<?> target, Method method);
|
||||||
|
|
||||||
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016-2020 the original author or authors.
|
* Copyright 2016-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+87
@@ -0,0 +1,87 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.cloud.openfeign;
|
||||||
|
|
||||||
|
import 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+133
-5
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
package org.springframework.cloud.openfeign;
|
package org.springframework.cloud.openfeign;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
|
import java.lang.reflect.Method;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Timer;
|
import java.util.Timer;
|
||||||
@@ -24,11 +26,17 @@ import java.util.concurrent.TimeUnit;
|
|||||||
|
|
||||||
import javax.annotation.PreDestroy;
|
import javax.annotation.PreDestroy;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.databind.Module;
|
||||||
import feign.Client;
|
import feign.Client;
|
||||||
import feign.Feign;
|
import feign.Feign;
|
||||||
|
import feign.RequestInterceptor;
|
||||||
|
import feign.Target;
|
||||||
|
import feign.hc5.ApacheHttp5Client;
|
||||||
import feign.httpclient.ApacheHttpClient;
|
import feign.httpclient.ApacheHttpClient;
|
||||||
import feign.okhttp.OkHttpClient;
|
import feign.okhttp.OkHttpClient;
|
||||||
import okhttp3.ConnectionPool;
|
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.HttpClient;
|
||||||
import org.apache.http.client.config.RequestConfig;
|
import org.apache.http.client.config.RequestConfig;
|
||||||
import org.apache.http.config.RegistryBuilder;
|
import org.apache.http.config.RegistryBuilder;
|
||||||
@@ -36,31 +44,54 @@ import org.apache.http.conn.HttpClientConnectionManager;
|
|||||||
import org.apache.http.impl.client.CloseableHttpClient;
|
import org.apache.http.impl.client.CloseableHttpClient;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.beans.factory.annotation.Value;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.cloud.client.actuator.HasFeatures;
|
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.ApacheHttpClientConnectionManagerFactory;
|
||||||
import org.springframework.cloud.commons.httpclient.ApacheHttpClientFactory;
|
import org.springframework.cloud.commons.httpclient.ApacheHttpClientFactory;
|
||||||
import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory;
|
import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory;
|
||||||
import org.springframework.cloud.commons.httpclient.OkHttpClientFactory;
|
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.DefaultGzipDecoderConfiguration;
|
||||||
|
import org.springframework.cloud.openfeign.support.FeignEncoderProperties;
|
||||||
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
|
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.Bean;
|
||||||
|
import org.springframework.context.annotation.Conditional;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Import;
|
import org.springframework.context.annotation.Import;
|
||||||
|
import org.springframework.data.domain.Page;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
import org.springframework.security.oauth2.client.OAuth2ClientContext;
|
||||||
|
import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResourceDetails;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Spencer Gibb
|
* @author Spencer Gibb
|
||||||
* @author Julien Roy
|
* @author Julien Roy
|
||||||
|
* @author Grzegorz Poznachowski
|
||||||
|
* @author Nikita Konev
|
||||||
|
* @author Tim Peeters
|
||||||
|
* @author Olga Maciaszek-Sharma
|
||||||
|
* @author Nguyen Ky Thanh
|
||||||
|
* @author Andrii Bohutskyi
|
||||||
|
* @author Kwangyong Kim
|
||||||
*/
|
*/
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
@ConditionalOnClass(Feign.class)
|
@ConditionalOnClass(Feign.class)
|
||||||
@EnableConfigurationProperties({ FeignClientProperties.class, FeignHttpClientProperties.class })
|
@EnableConfigurationProperties({ FeignClientProperties.class, FeignHttpClientProperties.class,
|
||||||
|
FeignEncoderProperties.class })
|
||||||
@Import(DefaultGzipDecoderConfiguration.class)
|
@Import(DefaultGzipDecoderConfiguration.class)
|
||||||
public class FeignAutoConfiguration {
|
public class FeignAutoConfiguration {
|
||||||
|
|
||||||
|
private static final Log LOG = LogFactory.getLog(FeignAutoConfiguration.class);
|
||||||
|
|
||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
private List<FeignClientSpecification> configurations = new ArrayList<>();
|
private List<FeignClientSpecification> configurations = new ArrayList<>();
|
||||||
|
|
||||||
@@ -77,6 +108,26 @@ public class FeignAutoConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@ConditionalOnClass({ Module.class, Page.class, Sort.class })
|
||||||
|
@ConditionalOnProperty(value = "feign.autoconfiguration.jackson.enabled", havingValue = "true")
|
||||||
|
protected static class FeignJacksonConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean(PageJacksonModule.class)
|
||||||
|
public PageJacksonModule pageJacksonModule() {
|
||||||
|
return new PageJacksonModule();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean(SortJacksonModule.class)
|
||||||
|
public SortJacksonModule sortModule() {
|
||||||
|
return new SortJacksonModule();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@Conditional(FeignCircuitBreakerDisabledConditions.class)
|
||||||
protected static class DefaultFeignTargeterConfiguration {
|
protected static class DefaultFeignTargeterConfiguration {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@@ -87,14 +138,53 @@ public class FeignAutoConfiguration {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@ConditionalOnClass(CircuitBreaker.class)
|
||||||
|
@ConditionalOnProperty(value = "feign.circuitbreaker.enabled", havingValue = "true")
|
||||||
|
protected static class CircuitBreakerPresentFeignTargeterConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean(CircuitBreakerFactory.class)
|
||||||
|
public Targeter defaultFeignTargeter() {
|
||||||
|
return new DefaultTargeter();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean(CircuitBreakerNameResolver.class)
|
||||||
|
public CircuitBreakerNameResolver circuitBreakerNameResolver() {
|
||||||
|
return new DefaultCircuitBreakerNameResolver();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
@ConditionalOnBean(CircuitBreakerFactory.class)
|
||||||
|
public Targeter circuitBreakerFeignTargeter(CircuitBreakerFactory circuitBreakerFactory,
|
||||||
|
@Value("${feign.circuitbreaker.group.enabled:false}") boolean circuitBreakerGroupEnabled,
|
||||||
|
CircuitBreakerNameResolver circuitBreakerNameResolver) {
|
||||||
|
return new FeignCircuitBreakerTargeter(circuitBreakerFactory, circuitBreakerGroupEnabled,
|
||||||
|
circuitBreakerNameResolver);
|
||||||
|
}
|
||||||
|
|
||||||
|
static class DefaultCircuitBreakerNameResolver implements CircuitBreakerNameResolver {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String resolveCircuitBreakerName(String feignClientName, Target<?> target, Method method) {
|
||||||
|
return Feign.configKey(target.type(), method);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
// the following configuration is for alternate feign clients if
|
// the following configuration is for alternate feign clients if
|
||||||
// SC loadbalancer is not on the class path.
|
// SC loadbalancer is not on the class path.
|
||||||
// see corresponding configurations in FeignRibbonClientAutoConfiguration
|
// see corresponding configurations in FeignLoadBalancerAutoConfiguration
|
||||||
// for load-balanced clients.
|
// for load-balanced clients.
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
@ConditionalOnClass(ApacheHttpClient.class)
|
@ConditionalOnClass(ApacheHttpClient.class)
|
||||||
@ConditionalOnMissingBean(CloseableHttpClient.class)
|
@ConditionalOnMissingBean(CloseableHttpClient.class)
|
||||||
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
|
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
|
||||||
|
@Conditional(HttpClient5DisabledConditions.class)
|
||||||
protected static class HttpClientFeignConfiguration {
|
protected static class HttpClientFeignConfiguration {
|
||||||
|
|
||||||
private final Timer connectionManagerTimer = new Timer(
|
private final Timer connectionManagerTimer = new Timer(
|
||||||
@@ -142,10 +232,17 @@ public class FeignAutoConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PreDestroy
|
@PreDestroy
|
||||||
public void destroy() throws Exception {
|
public void destroy() {
|
||||||
this.connectionManagerTimer.cancel();
|
this.connectionManagerTimer.cancel();
|
||||||
if (this.httpClient != null) {
|
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 +294,35 @@ public class FeignAutoConfiguration {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@ConditionalOnClass(ApacheHttp5Client.class)
|
||||||
|
@ConditionalOnMissingBean(org.apache.hc.client5.http.impl.classic.CloseableHttpClient.class)
|
||||||
|
@ConditionalOnProperty(value = "feign.httpclient.hc5.enabled", havingValue = "true")
|
||||||
|
@Import(org.springframework.cloud.openfeign.clientconfig.HttpClient5FeignConfiguration.class)
|
||||||
|
protected static class HttpClient5FeignConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean(Client.class)
|
||||||
|
public Client feignClient(org.apache.hc.client5.http.impl.classic.CloseableHttpClient httpClient5) {
|
||||||
|
return new ApacheHttp5Client(httpClient5);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@ConditionalOnClass(OAuth2ClientContext.class)
|
||||||
|
@ConditionalOnProperty("feign.oauth2.enabled")
|
||||||
|
@Deprecated // spring-security-oauth2 reached EOL
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.cloud.openfeign;
|
||||||
|
|
||||||
|
import 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
|
||||||
|
* @author Andrii Bohutskyi
|
||||||
|
* @author Kwangyong Kim
|
||||||
|
* @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;
|
||||||
|
|
||||||
|
private boolean circuitBreakerGroupEnabled;
|
||||||
|
|
||||||
|
private CircuitBreakerNameResolver circuitBreakerNameResolver;
|
||||||
|
|
||||||
|
Builder circuitBreakerFactory(CircuitBreakerFactory circuitBreakerFactory) {
|
||||||
|
this.circuitBreakerFactory = circuitBreakerFactory;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
Builder feignClientName(String feignClientName) {
|
||||||
|
this.feignClientName = feignClientName;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
Builder circuitBreakerGroupEnabled(boolean circuitBreakerGroupEnabled) {
|
||||||
|
this.circuitBreakerGroupEnabled = circuitBreakerGroupEnabled;
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
|
Builder circuitBreakerNameResolver(CircuitBreakerNameResolver circuitBreakerNameResolver) {
|
||||||
|
this.circuitBreakerNameResolver = circuitBreakerNameResolver;
|
||||||
|
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,
|
||||||
|
circuitBreakerGroupEnabled, circuitBreakerNameResolver));
|
||||||
|
return super.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.cloud.openfeign;
|
||||||
|
|
||||||
|
import 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 {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+176
@@ -0,0 +1,176 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.cloud.openfeign;
|
||||||
|
|
||||||
|
import java.lang.reflect.InvocationHandler;
|
||||||
|
import java.lang.reflect.InvocationTargetException;
|
||||||
|
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 org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException;
|
||||||
|
import org.springframework.web.context.request.RequestAttributes;
|
||||||
|
import org.springframework.web.context.request.RequestContextHolder;
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
private final boolean circuitBreakerGroupEnabled;
|
||||||
|
|
||||||
|
private final CircuitBreakerNameResolver circuitBreakerNameResolver;
|
||||||
|
|
||||||
|
FeignCircuitBreakerInvocationHandler(CircuitBreakerFactory factory, String feignClientName, Target<?> target,
|
||||||
|
Map<Method, InvocationHandlerFactory.MethodHandler> dispatch, FallbackFactory<?> nullableFallbackFactory,
|
||||||
|
boolean circuitBreakerGroupEnabled, CircuitBreakerNameResolver circuitBreakerNameResolver) {
|
||||||
|
this.factory = factory;
|
||||||
|
this.feignClientName = feignClientName;
|
||||||
|
this.target = checkNotNull(target, "target");
|
||||||
|
this.dispatch = checkNotNull(dispatch, "dispatch");
|
||||||
|
this.fallbackMethodMap = toFallbackMethod(dispatch);
|
||||||
|
this.nullableFallbackFactory = nullableFallbackFactory;
|
||||||
|
this.circuitBreakerGroupEnabled = circuitBreakerGroupEnabled;
|
||||||
|
this.circuitBreakerNameResolver = circuitBreakerNameResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 = circuitBreakerNameResolver.resolveCircuitBreakerName(feignClientName, target, method);
|
||||||
|
CircuitBreaker circuitBreaker = circuitBreakerGroupEnabled ? factory.create(circuitName, feignClientName)
|
||||||
|
: 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 exception) {
|
||||||
|
unwrapAndRethrow(exception);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
return circuitBreaker.run(supplier, fallbackFunction);
|
||||||
|
}
|
||||||
|
return circuitBreaker.run(supplier);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void unwrapAndRethrow(Exception exception) {
|
||||||
|
if (exception instanceof InvocationTargetException || exception instanceof NoFallbackAvailableException) {
|
||||||
|
Throwable underlyingException = exception.getCause();
|
||||||
|
if (underlyingException instanceof RuntimeException) {
|
||||||
|
throw (RuntimeException) underlyingException;
|
||||||
|
}
|
||||||
|
if (underlyingException != null) {
|
||||||
|
throw new IllegalStateException(underlyingException);
|
||||||
|
}
|
||||||
|
throw new IllegalStateException(exception);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private Supplier<Object> asSupplier(final Method method, final Object[] args) {
|
||||||
|
final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
||||||
|
return () -> {
|
||||||
|
try {
|
||||||
|
RequestContextHolder.setRequestAttributes(requestAttributes);
|
||||||
|
return 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+96
@@ -0,0 +1,96 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.cloud.openfeign;
|
||||||
|
|
||||||
|
import 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;
|
||||||
|
|
||||||
|
private final boolean circuitBreakerGroupEnabled;
|
||||||
|
|
||||||
|
private final CircuitBreakerNameResolver circuitBreakerNameResolver;
|
||||||
|
|
||||||
|
FeignCircuitBreakerTargeter(CircuitBreakerFactory circuitBreakerFactory, boolean circuitBreakerGroupEnabled,
|
||||||
|
CircuitBreakerNameResolver circuitBreakerNameResolver) {
|
||||||
|
this.circuitBreakerFactory = circuitBreakerFactory;
|
||||||
|
this.circuitBreakerGroupEnabled = circuitBreakerGroupEnabled;
|
||||||
|
this.circuitBreakerNameResolver = circuitBreakerNameResolver;
|
||||||
|
}
|
||||||
|
|
||||||
|
@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(circuitBreakerFactory).feignClientName(feignClientName)
|
||||||
|
.circuitBreakerGroupEnabled(circuitBreakerGroupEnabled)
|
||||||
|
.circuitBreakerNameResolver(circuitBreakerNameResolver);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+22
-10
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -33,6 +33,7 @@ import org.springframework.core.annotation.AliasFor;
|
|||||||
*
|
*
|
||||||
* @author Spencer Gibb
|
* @author Spencer Gibb
|
||||||
* @author Venil Noronha
|
* @author Venil Noronha
|
||||||
|
* @author Olga Maciaszek-Sharma
|
||||||
*/
|
*/
|
||||||
@Target(ElementType.TYPE)
|
@Target(ElementType.TYPE)
|
||||||
@Retention(RetentionPolicy.RUNTIME)
|
@Retention(RetentionPolicy.RUNTIME)
|
||||||
@@ -49,14 +50,6 @@ public @interface FeignClient {
|
|||||||
@AliasFor("name")
|
@AliasFor("name")
|
||||||
String value() default "";
|
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
|
* This will be used as the bean name instead of name if present, but will not be used
|
||||||
* as a service id.
|
* as a service id.
|
||||||
@@ -73,9 +66,28 @@ public @interface FeignClient {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @return the <code>@Qualifier</code> value for the feign client.
|
* @return the <code>@Qualifier</code> value for the feign client.
|
||||||
|
* @deprecated in favour of {@link #qualifiers()}.
|
||||||
|
*
|
||||||
|
* If both {@link #qualifier()} and {@link #qualifiers()} are present, we will use the
|
||||||
|
* latter, unless the array returned by {@link #qualifiers()} is empty or only
|
||||||
|
* contains <code>null</code> or whitespace values, in which case we'll fall back
|
||||||
|
* first to {@link #qualifier()} and, if that's also not present, to the default =
|
||||||
|
* <code>contextId + "FeignClient"</code>.
|
||||||
*/
|
*/
|
||||||
|
@Deprecated
|
||||||
String qualifier() default "";
|
String qualifier() default "";
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @return the <code>@Qualifiers</code> value for the feign client.
|
||||||
|
*
|
||||||
|
* If both {@link #qualifier()} and {@link #qualifiers()} are present, we will use the
|
||||||
|
* latter, unless the array returned by {@link #qualifiers()} is empty or only
|
||||||
|
* contains <code>null</code> or whitespace values, in which case we'll fall back
|
||||||
|
* first to {@link #qualifier()} and, if that's also not present, to the default =
|
||||||
|
* <code>contextId + "FeignClient"</code>.
|
||||||
|
*/
|
||||||
|
String[] qualifiers() default {};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return an absolute URL or resolvable hostname (the protocol is optional).
|
* @return an absolute URL or resolvable hostname (the protocol is optional).
|
||||||
*/
|
*/
|
||||||
@@ -108,7 +120,7 @@ public @interface FeignClient {
|
|||||||
* factory must produce instances of fallback classes that implement the interface
|
* factory must produce instances of fallback classes that implement the interface
|
||||||
* annotated by {@link FeignClient}. The fallback factory must be a valid spring bean.
|
* 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
|
* @return fallback factory for the specified Feign client interface
|
||||||
*/
|
*/
|
||||||
Class<?> fallbackFactory() default void.class;
|
Class<?> fallbackFactory() default void.class;
|
||||||
|
|||||||
+16
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -16,6 +16,8 @@
|
|||||||
|
|
||||||
package org.springframework.cloud.openfeign;
|
package org.springframework.cloud.openfeign;
|
||||||
|
|
||||||
|
import feign.Feign;
|
||||||
|
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -26,6 +28,7 @@ import org.springframework.context.ApplicationContext;
|
|||||||
*
|
*
|
||||||
* @author Sven Döring
|
* @author Sven Döring
|
||||||
* @author Matt King
|
* @author Matt King
|
||||||
|
* @author Sam Kruglov
|
||||||
*/
|
*/
|
||||||
public class FeignClientBuilder {
|
public class FeignClientBuilder {
|
||||||
|
|
||||||
@@ -76,6 +79,18 @@ public class FeignClientBuilder {
|
|||||||
return this;
|
return this;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Applies a {@link FeignBuilderCustomizer} to the underlying
|
||||||
|
* {@link Feign.Builder}. May be called multiple times.
|
||||||
|
* @param customizer applied in the same order as supplied here after applying
|
||||||
|
* customizers found in the context.
|
||||||
|
* @return the {@link Builder} with the customizer added
|
||||||
|
*/
|
||||||
|
public Builder<T> customize(final FeignBuilderCustomizer customizer) {
|
||||||
|
this.feignClientFactoryBean.addCustomizer(customizer);
|
||||||
|
return this;
|
||||||
|
}
|
||||||
|
|
||||||
public Builder<T> contextId(final String contextId) {
|
public Builder<T> contextId(final String contextId) {
|
||||||
this.feignClientFactoryBean.setContextId(contextId);
|
this.feignClientFactoryBean.setContextId(contextId);
|
||||||
return this;
|
return this;
|
||||||
|
|||||||
+146
-17
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -16,10 +16,14 @@
|
|||||||
|
|
||||||
package org.springframework.cloud.openfeign;
|
package org.springframework.cloud.openfeign;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
import java.util.concurrent.TimeUnit;
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import feign.Capability;
|
||||||
import feign.Client;
|
import feign.Client;
|
||||||
import feign.Contract;
|
import feign.Contract;
|
||||||
import feign.ExceptionPropagationPolicy;
|
import feign.ExceptionPropagationPolicy;
|
||||||
@@ -33,14 +37,19 @@ import feign.Target.HardCodedTarget;
|
|||||||
import feign.codec.Decoder;
|
import feign.codec.Decoder;
|
||||||
import feign.codec.Encoder;
|
import feign.codec.Encoder;
|
||||||
import feign.codec.ErrorDecoder;
|
import feign.codec.ErrorDecoder;
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
|
||||||
import org.springframework.beans.BeanUtils;
|
import org.springframework.beans.BeanUtils;
|
||||||
import org.springframework.beans.BeansException;
|
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.FactoryBean;
|
||||||
import org.springframework.beans.factory.InitializingBean;
|
import org.springframework.beans.factory.InitializingBean;
|
||||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||||
import org.springframework.cloud.openfeign.clientconfig.FeignClientConfigurer;
|
import org.springframework.cloud.openfeign.clientconfig.FeignClientConfigurer;
|
||||||
import org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient;
|
import org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient;
|
||||||
|
import org.springframework.cloud.openfeign.loadbalancer.RetryableFeignBlockingLoadBalancerClient;
|
||||||
import org.springframework.context.ApplicationContext;
|
import org.springframework.context.ApplicationContext;
|
||||||
import org.springframework.context.ApplicationContextAware;
|
import org.springframework.context.ApplicationContextAware;
|
||||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||||
@@ -54,14 +63,22 @@ import org.springframework.util.StringUtils;
|
|||||||
* @author Gregor Zurowski
|
* @author Gregor Zurowski
|
||||||
* @author Matt King
|
* @author Matt King
|
||||||
* @author Olga Maciaszek-Sharma
|
* @author Olga Maciaszek-Sharma
|
||||||
|
* @author Ilia Ilinykh
|
||||||
|
* @author Marcin Grzejszczak
|
||||||
|
* @author Jonatan Ivanov
|
||||||
|
* @author Sam Kruglov
|
||||||
|
* @author Jasbir Singh
|
||||||
*/
|
*/
|
||||||
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
|
* WARNING! Nothing in this class should be @Autowired. It causes NPEs because of some
|
||||||
* lifecycle race condition.
|
* lifecycle race condition.
|
||||||
***********************************/
|
***********************************/
|
||||||
|
|
||||||
|
private static Log LOG = LogFactory.getLog(FeignClientFactoryBean.class);
|
||||||
|
|
||||||
private Class<?> type;
|
private Class<?> type;
|
||||||
|
|
||||||
private String name;
|
private String name;
|
||||||
@@ -78,6 +95,8 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
|||||||
|
|
||||||
private ApplicationContext applicationContext;
|
private ApplicationContext applicationContext;
|
||||||
|
|
||||||
|
private BeanFactory beanFactory;
|
||||||
|
|
||||||
private Class<?> fallback = void.class;
|
private Class<?> fallback = void.class;
|
||||||
|
|
||||||
private Class<?> fallbackFactory = void.class;
|
private Class<?> fallbackFactory = void.class;
|
||||||
@@ -86,6 +105,12 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
|||||||
|
|
||||||
private int connectTimeoutMillis = new Request.Options().connectTimeoutMillis();
|
private int connectTimeoutMillis = new Request.Options().connectTimeoutMillis();
|
||||||
|
|
||||||
|
private boolean followRedirects = new Request.Options().isFollowRedirects();
|
||||||
|
|
||||||
|
private boolean refreshableClient = false;
|
||||||
|
|
||||||
|
private final List<FeignBuilderCustomizer> additionalCustomizers = new ArrayList<>();
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void afterPropertiesSet() {
|
public void afterPropertiesSet() {
|
||||||
Assert.hasText(contextId, "Context id must be set");
|
Assert.hasText(contextId, "Context id must be set");
|
||||||
@@ -119,10 +144,12 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
|||||||
customizerMap.values().stream().sorted(AnnotationAwareOrderComparator.INSTANCE)
|
customizerMap.values().stream().sorted(AnnotationAwareOrderComparator.INSTANCE)
|
||||||
.forEach(feignBuilderCustomizer -> feignBuilderCustomizer.customize(builder));
|
.forEach(feignBuilderCustomizer -> feignBuilderCustomizer.customize(builder));
|
||||||
}
|
}
|
||||||
|
additionalCustomizers.forEach(customizer -> customizer.customize(builder));
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void configureFeign(FeignContext context, Feign.Builder builder) {
|
protected void configureFeign(FeignContext context, Feign.Builder builder) {
|
||||||
FeignClientProperties properties = applicationContext.getBean(FeignClientProperties.class);
|
FeignClientProperties properties = beanFactory != null ? beanFactory.getBean(FeignClientProperties.class)
|
||||||
|
: applicationContext.getBean(FeignClientProperties.class);
|
||||||
|
|
||||||
FeignClientConfigurer feignClientConfigurer = getOptional(context, FeignClientConfigurer.class);
|
FeignClientConfigurer feignClientConfigurer = getOptional(context, FeignClientConfigurer.class);
|
||||||
setInheritParentContext(feignClientConfigurer.inheritParentConfiguration());
|
setInheritParentContext(feignClientConfigurer.inheritParentConfiguration());
|
||||||
@@ -165,15 +192,22 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
Request.Options options = getInheritedAwareOptional(context, Request.Options.class);
|
Request.Options options = getInheritedAwareOptional(context, Request.Options.class);
|
||||||
|
if (options == null) {
|
||||||
|
options = getOptionsByName(context, contextId);
|
||||||
|
}
|
||||||
|
|
||||||
if (options != null) {
|
if (options != null) {
|
||||||
builder.options(options);
|
builder.options(options);
|
||||||
readTimeoutMillis = options.readTimeoutMillis();
|
readTimeoutMillis = options.readTimeoutMillis();
|
||||||
connectTimeoutMillis = options.connectTimeoutMillis();
|
connectTimeoutMillis = options.connectTimeoutMillis();
|
||||||
|
followRedirects = options.isFollowRedirects();
|
||||||
}
|
}
|
||||||
Map<String, RequestInterceptor> requestInterceptors = getInheritedAwareInstances(context,
|
Map<String, RequestInterceptor> requestInterceptors = getInheritedAwareInstances(context,
|
||||||
RequestInterceptor.class);
|
RequestInterceptor.class);
|
||||||
if (requestInterceptors != null) {
|
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);
|
QueryMapEncoder queryMapEncoder = getInheritedAwareOptional(context, QueryMapEncoder.class);
|
||||||
if (queryMapEncoder != null) {
|
if (queryMapEncoder != null) {
|
||||||
@@ -187,6 +221,12 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
|||||||
if (exceptionPropagationPolicy != null) {
|
if (exceptionPropagationPolicy != null) {
|
||||||
builder.exceptionPropagationPolicy(exceptionPropagationPolicy);
|
builder.exceptionPropagationPolicy(exceptionPropagationPolicy);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Map<String, Capability> capabilities = getInheritedAwareInstances(context, Capability.class);
|
||||||
|
if (capabilities != null) {
|
||||||
|
capabilities.values().stream().sorted(AnnotationAwareOrderComparator.INSTANCE)
|
||||||
|
.forEach(builder::addCapability);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
protected void configureUsingProperties(FeignClientProperties.FeignClientConfiguration config,
|
protected void configureUsingProperties(FeignClientProperties.FeignClientConfiguration config,
|
||||||
@@ -199,11 +239,15 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
|||||||
builder.logLevel(config.getLoggerLevel());
|
builder.logLevel(config.getLoggerLevel());
|
||||||
}
|
}
|
||||||
|
|
||||||
connectTimeoutMillis = config.getConnectTimeout() != null ? config.getConnectTimeout() : connectTimeoutMillis;
|
if (!refreshableClient) {
|
||||||
readTimeoutMillis = config.getReadTimeout() != null ? config.getReadTimeout() : readTimeoutMillis;
|
connectTimeoutMillis = config.getConnectTimeout() != null ? config.getConnectTimeout()
|
||||||
|
: connectTimeoutMillis;
|
||||||
|
readTimeoutMillis = config.getReadTimeout() != null ? config.getReadTimeout() : readTimeoutMillis;
|
||||||
|
followRedirects = config.isFollowRedirects() != null ? config.isFollowRedirects() : followRedirects;
|
||||||
|
|
||||||
builder.options(new Request.Options(connectTimeoutMillis, TimeUnit.MILLISECONDS, readTimeoutMillis,
|
builder.options(new Request.Options(connectTimeoutMillis, TimeUnit.MILLISECONDS, readTimeoutMillis,
|
||||||
TimeUnit.MILLISECONDS, true));
|
TimeUnit.MILLISECONDS, followRedirects));
|
||||||
|
}
|
||||||
|
|
||||||
if (config.getRetryer() != null) {
|
if (config.getRetryer() != null) {
|
||||||
Retryer retryer = getOrInstantiate(config.getRetryer());
|
Retryer retryer = getOrInstantiate(config.getRetryer());
|
||||||
@@ -233,6 +277,9 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
|||||||
builder.encoder(getOrInstantiate(config.getEncoder()));
|
builder.encoder(getOrInstantiate(config.getEncoder()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
addDefaultRequestHeaders(config, builder);
|
||||||
|
addDefaultQueryParams(config, builder);
|
||||||
|
|
||||||
if (Objects.nonNull(config.getDecoder())) {
|
if (Objects.nonNull(config.getDecoder())) {
|
||||||
builder.decoder(getOrInstantiate(config.getDecoder()));
|
builder.decoder(getOrInstantiate(config.getDecoder()));
|
||||||
}
|
}
|
||||||
@@ -244,11 +291,44 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
|||||||
if (Objects.nonNull(config.getExceptionPropagationPolicy())) {
|
if (Objects.nonNull(config.getExceptionPropagationPolicy())) {
|
||||||
builder.exceptionPropagationPolicy(config.getExceptionPropagationPolicy());
|
builder.exceptionPropagationPolicy(config.getExceptionPropagationPolicy());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (config.getCapabilities() != null) {
|
||||||
|
config.getCapabilities().stream().map(this::getOrInstantiate).forEach(builder::addCapability);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addDefaultQueryParams(FeignClientProperties.FeignClientConfiguration config, Feign.Builder builder) {
|
||||||
|
Map<String, Collection<String>> defaultQueryParameters = config.getDefaultQueryParameters();
|
||||||
|
if (Objects.nonNull(defaultQueryParameters)) {
|
||||||
|
builder.requestInterceptor(requestTemplate -> {
|
||||||
|
Map<String, Collection<String>> queries = requestTemplate.queries();
|
||||||
|
defaultQueryParameters.keySet().forEach(key -> {
|
||||||
|
if (!queries.containsKey(key)) {
|
||||||
|
requestTemplate.query(key, defaultQueryParameters.get(key));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void addDefaultRequestHeaders(FeignClientProperties.FeignClientConfiguration config,
|
||||||
|
Feign.Builder builder) {
|
||||||
|
Map<String, Collection<String>> defaultRequestHeaders = config.getDefaultRequestHeaders();
|
||||||
|
if (Objects.nonNull(defaultRequestHeaders)) {
|
||||||
|
builder.requestInterceptor(requestTemplate -> {
|
||||||
|
Map<String, Collection<String>> headers = requestTemplate.headers();
|
||||||
|
defaultRequestHeaders.keySet().forEach(key -> {
|
||||||
|
if (!headers.containsKey(key)) {
|
||||||
|
requestTemplate.header(key, defaultRequestHeaders.get(key));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private <T> T getOrInstantiate(Class<T> tClass) {
|
private <T> T getOrInstantiate(Class<T> tClass) {
|
||||||
try {
|
try {
|
||||||
return applicationContext.getBean(tClass);
|
return beanFactory != null ? beanFactory.getBean(tClass) : applicationContext.getBean(tClass);
|
||||||
}
|
}
|
||||||
catch (NoSuchBeanDefinitionException e) {
|
catch (NoSuchBeanDefinitionException e) {
|
||||||
return BeanUtils.instantiateClass(tClass);
|
return BeanUtils.instantiateClass(tClass);
|
||||||
@@ -297,6 +377,20 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
|||||||
"No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-loadbalancer?");
|
"No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-loadbalancer?");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Meant to get Options bean from context with bean name.
|
||||||
|
* @param context context of Feign client
|
||||||
|
* @param contextId name of feign client
|
||||||
|
* @return returns Options found in context
|
||||||
|
*/
|
||||||
|
protected Request.Options getOptionsByName(FeignContext context, String contextId) {
|
||||||
|
if (refreshableClient) {
|
||||||
|
return context.getInstance(contextId, Request.Options.class.getCanonicalName() + "-" + contextId,
|
||||||
|
Request.Options.class);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public Object getObject() {
|
public Object getObject() {
|
||||||
return getTarget();
|
return getTarget();
|
||||||
@@ -308,10 +402,15 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
|||||||
* information
|
* information
|
||||||
*/
|
*/
|
||||||
<T> T getTarget() {
|
<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);
|
Feign.Builder builder = feign(context);
|
||||||
|
|
||||||
if (!StringUtils.hasText(url)) {
|
if (!StringUtils.hasText(url)) {
|
||||||
|
|
||||||
|
if (LOG.isInfoEnabled()) {
|
||||||
|
LOG.info("For '" + name + "' URL not provided. Will try picking an instance via load-balancing.");
|
||||||
|
}
|
||||||
if (!name.startsWith("http")) {
|
if (!name.startsWith("http")) {
|
||||||
url = "http://" + name;
|
url = "http://" + name;
|
||||||
}
|
}
|
||||||
@@ -332,6 +431,11 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
|||||||
// but Spring Cloud LoadBalancer is on the classpath, so unwrap
|
// but Spring Cloud LoadBalancer is on the classpath, so unwrap
|
||||||
client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
|
client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
|
||||||
}
|
}
|
||||||
|
if (client instanceof RetryableFeignBlockingLoadBalancerClient) {
|
||||||
|
// not load balancing because we have a url,
|
||||||
|
// but Spring Cloud LoadBalancer is on the classpath, so unwrap
|
||||||
|
client = ((RetryableFeignBlockingLoadBalancerClient) client).getDelegate();
|
||||||
|
}
|
||||||
builder.client(client);
|
builder.client(client);
|
||||||
}
|
}
|
||||||
Targeter targeter = get(context, Targeter.class);
|
Targeter targeter = get(context, Targeter.class);
|
||||||
@@ -339,6 +443,9 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
|||||||
}
|
}
|
||||||
|
|
||||||
private String cleanPath() {
|
private String cleanPath() {
|
||||||
|
if (path == null) {
|
||||||
|
return "";
|
||||||
|
}
|
||||||
String path = this.path.trim();
|
String path = this.path.trim();
|
||||||
if (StringUtils.hasLength(path)) {
|
if (StringUtils.hasLength(path)) {
|
||||||
if (!path.startsWith("/")) {
|
if (!path.startsWith("/")) {
|
||||||
@@ -417,13 +524,18 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
|||||||
this.inheritParentContext = inheritParentContext;
|
this.inheritParentContext = inheritParentContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void addCustomizer(FeignBuilderCustomizer customizer) {
|
||||||
|
additionalCustomizers.add(customizer);
|
||||||
|
}
|
||||||
|
|
||||||
public ApplicationContext getApplicationContext() {
|
public ApplicationContext getApplicationContext() {
|
||||||
return applicationContext;
|
return applicationContext;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void setApplicationContext(ApplicationContext context) throws BeansException {
|
public void setApplicationContext(ApplicationContext context) throws BeansException {
|
||||||
this.applicationContext = context;
|
applicationContext = context;
|
||||||
|
beanFactory = context;
|
||||||
}
|
}
|
||||||
|
|
||||||
public Class<?> getFallback() {
|
public Class<?> getFallback() {
|
||||||
@@ -442,6 +554,10 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
|||||||
this.fallbackFactory = fallbackFactory;
|
this.fallbackFactory = fallbackFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public void setRefreshableClient(boolean refreshableClient) {
|
||||||
|
this.refreshableClient = refreshableClient;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (this == o) {
|
if (this == o) {
|
||||||
@@ -451,16 +567,21 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
FeignClientFactoryBean that = (FeignClientFactoryBean) o;
|
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)
|
&& inheritParentContext == that.inheritParentContext && Objects.equals(fallback, that.fallback)
|
||||||
&& Objects.equals(fallbackFactory, that.fallbackFactory) && Objects.equals(name, that.name)
|
&& Objects.equals(fallbackFactory, that.fallbackFactory) && Objects.equals(name, that.name)
|
||||||
&& Objects.equals(path, that.path) && Objects.equals(type, that.type) && Objects.equals(url, that.url);
|
&& Objects.equals(path, that.path) && Objects.equals(type, that.type) && Objects.equals(url, that.url)
|
||||||
|
&& Objects.equals(connectTimeoutMillis, that.connectTimeoutMillis)
|
||||||
|
&& Objects.equals(readTimeoutMillis, that.readTimeoutMillis)
|
||||||
|
&& Objects.equals(followRedirects, that.followRedirects)
|
||||||
|
&& Objects.equals(refreshableClient, that.refreshableClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hash(applicationContext, decode404, inheritParentContext, fallback, fallbackFactory, name, path,
|
return Objects.hash(applicationContext, beanFactory, decode404, inheritParentContext, fallback, fallbackFactory,
|
||||||
type, url);
|
name, path, type, url, readTimeoutMillis, connectTimeoutMillis, followRedirects, refreshableClient);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -469,8 +590,16 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
|||||||
.append(name).append("', ").append("url='").append(url).append("', ").append("path='").append(path)
|
.append(name).append("', ").append("url='").append(url).append("', ").append("path='").append(path)
|
||||||
.append("', ").append("decode404=").append(decode404).append(", ").append("inheritParentContext=")
|
.append("', ").append("decode404=").append(decode404).append(", ").append("inheritParentContext=")
|
||||||
.append(inheritParentContext).append(", ").append("applicationContext=").append(applicationContext)
|
.append(inheritParentContext).append(", ").append("applicationContext=").append(applicationContext)
|
||||||
.append(", ").append("fallback=").append(fallback).append(", ").append("fallbackFactory=")
|
.append(", ").append("beanFactory=").append(beanFactory).append(", ").append("fallback=")
|
||||||
.append(fallbackFactory).append("}").toString();
|
.append(fallback).append(", ").append("fallbackFactory=").append(fallbackFactory).append("}")
|
||||||
|
.append("connectTimeoutMillis=").append(connectTimeoutMillis).append("}").append("readTimeoutMillis=")
|
||||||
|
.append(readTimeoutMillis).append("}").append("followRedirects=").append(followRedirects)
|
||||||
|
.append("refreshableClient=").append(refreshableClient).append("}").toString();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setBeanFactory(BeanFactory beanFactory) throws BeansException {
|
||||||
|
this.beanFactory = beanFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+52
@@ -0,0 +1,52 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.cloud.openfeign;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Condition;
|
||||||
|
import org.springframework.context.annotation.ConditionContext;
|
||||||
|
import org.springframework.core.type.AnnotatedTypeMetadata;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Jonatan Ivanov
|
||||||
|
*/
|
||||||
|
class FeignClientMetricsEnabledCondition implements Condition {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
|
||||||
|
FeignClientProperties feignClientProperties = context.getBeanFactory()
|
||||||
|
.getBeanProvider(FeignClientProperties.class).getIfAvailable();
|
||||||
|
if (feignClientProperties != null) {
|
||||||
|
Map<String, FeignClientProperties.FeignClientConfiguration> feignClientConfigMap = feignClientProperties
|
||||||
|
.getConfig();
|
||||||
|
if (feignClientConfigMap != null) {
|
||||||
|
FeignClientProperties.FeignClientConfiguration feignClientConfig = feignClientConfigMap
|
||||||
|
.get(context.getEnvironment().getProperty("feign.client.name"));
|
||||||
|
if (feignClientConfig != null) {
|
||||||
|
FeignClientProperties.MetricsProperties metrics = feignClientConfig.getMetrics();
|
||||||
|
if (metrics != null && metrics.getEnabled() != null) {
|
||||||
|
return metrics.getEnabled();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+136
-27
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -16,11 +16,13 @@
|
|||||||
|
|
||||||
package org.springframework.cloud.openfeign;
|
package org.springframework.cloud.openfeign;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
|
||||||
|
import feign.Capability;
|
||||||
import feign.Contract;
|
import feign.Contract;
|
||||||
import feign.ExceptionPropagationPolicy;
|
import feign.ExceptionPropagationPolicy;
|
||||||
import feign.Logger;
|
import feign.Logger;
|
||||||
@@ -34,6 +36,10 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Eko Kurniawan Khannedy
|
* @author Eko Kurniawan Khannedy
|
||||||
|
* @author Ilia Ilinykh
|
||||||
|
* @author Ram Anaswara
|
||||||
|
* @author Jonatan Ivanov
|
||||||
|
* @author Olga Maciaszek-Sharma
|
||||||
*/
|
*/
|
||||||
@ConfigurationProperties("feign.client")
|
@ConfigurationProperties("feign.client")
|
||||||
public class FeignClientProperties {
|
public class FeignClientProperties {
|
||||||
@@ -44,8 +50,14 @@ public class FeignClientProperties {
|
|||||||
|
|
||||||
private Map<String, FeignClientConfiguration> config = new HashMap<>();
|
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() {
|
public boolean isDefaultToProperties() {
|
||||||
return this.defaultToProperties;
|
return defaultToProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDefaultToProperties(boolean defaultToProperties) {
|
public void setDefaultToProperties(boolean defaultToProperties) {
|
||||||
@@ -53,7 +65,7 @@ public class FeignClientProperties {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public String getDefaultConfig() {
|
public String getDefaultConfig() {
|
||||||
return this.defaultConfig;
|
return defaultConfig;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDefaultConfig(String defaultConfig) {
|
public void setDefaultConfig(String defaultConfig) {
|
||||||
@@ -61,13 +73,21 @@ public class FeignClientProperties {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Map<String, FeignClientConfiguration> getConfig() {
|
public Map<String, FeignClientConfiguration> getConfig() {
|
||||||
return this.config;
|
return config;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setConfig(Map<String, FeignClientConfiguration> config) {
|
public void setConfig(Map<String, FeignClientConfiguration> config) {
|
||||||
this.config = config;
|
this.config = config;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public boolean isDecodeSlash() {
|
||||||
|
return decodeSlash;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setDecodeSlash(boolean decodeSlash) {
|
||||||
|
this.decodeSlash = decodeSlash;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (this == o) {
|
if (this == o) {
|
||||||
@@ -77,13 +97,13 @@ public class FeignClientProperties {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
FeignClientProperties that = (FeignClientProperties) o;
|
FeignClientProperties that = (FeignClientProperties) o;
|
||||||
return this.defaultToProperties == that.defaultToProperties
|
return defaultToProperties == that.defaultToProperties && Objects.equals(defaultConfig, that.defaultConfig)
|
||||||
&& Objects.equals(this.defaultConfig, that.defaultConfig) && Objects.equals(this.config, that.config);
|
&& Objects.equals(config, that.config) && Objects.equals(decodeSlash, that.decodeSlash);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hash(this.defaultToProperties, this.defaultConfig, this.config);
|
return Objects.hash(defaultToProperties, defaultConfig, config, decodeSlash);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -103,6 +123,10 @@ public class FeignClientProperties {
|
|||||||
|
|
||||||
private List<Class<RequestInterceptor>> requestInterceptors;
|
private List<Class<RequestInterceptor>> requestInterceptors;
|
||||||
|
|
||||||
|
private Map<String, Collection<String>> defaultRequestHeaders;
|
||||||
|
|
||||||
|
private Map<String, Collection<String>> defaultQueryParameters;
|
||||||
|
|
||||||
private Boolean decode404;
|
private Boolean decode404;
|
||||||
|
|
||||||
private Class<Decoder> decoder;
|
private Class<Decoder> decoder;
|
||||||
@@ -113,8 +137,14 @@ public class FeignClientProperties {
|
|||||||
|
|
||||||
private ExceptionPropagationPolicy exceptionPropagationPolicy;
|
private ExceptionPropagationPolicy exceptionPropagationPolicy;
|
||||||
|
|
||||||
|
private List<Class<Capability>> capabilities;
|
||||||
|
|
||||||
|
private MetricsProperties metrics;
|
||||||
|
|
||||||
|
private Boolean followRedirects;
|
||||||
|
|
||||||
public Logger.Level getLoggerLevel() {
|
public Logger.Level getLoggerLevel() {
|
||||||
return this.loggerLevel;
|
return loggerLevel;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setLoggerLevel(Logger.Level loggerLevel) {
|
public void setLoggerLevel(Logger.Level loggerLevel) {
|
||||||
@@ -122,7 +152,7 @@ public class FeignClientProperties {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Integer getConnectTimeout() {
|
public Integer getConnectTimeout() {
|
||||||
return this.connectTimeout;
|
return connectTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setConnectTimeout(Integer connectTimeout) {
|
public void setConnectTimeout(Integer connectTimeout) {
|
||||||
@@ -130,7 +160,7 @@ public class FeignClientProperties {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Integer getReadTimeout() {
|
public Integer getReadTimeout() {
|
||||||
return this.readTimeout;
|
return readTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setReadTimeout(Integer readTimeout) {
|
public void setReadTimeout(Integer readTimeout) {
|
||||||
@@ -138,7 +168,7 @@ public class FeignClientProperties {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Class<Retryer> getRetryer() {
|
public Class<Retryer> getRetryer() {
|
||||||
return this.retryer;
|
return retryer;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRetryer(Class<Retryer> retryer) {
|
public void setRetryer(Class<Retryer> retryer) {
|
||||||
@@ -146,7 +176,7 @@ public class FeignClientProperties {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Class<ErrorDecoder> getErrorDecoder() {
|
public Class<ErrorDecoder> getErrorDecoder() {
|
||||||
return this.errorDecoder;
|
return errorDecoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setErrorDecoder(Class<ErrorDecoder> errorDecoder) {
|
public void setErrorDecoder(Class<ErrorDecoder> errorDecoder) {
|
||||||
@@ -154,15 +184,31 @@ public class FeignClientProperties {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public List<Class<RequestInterceptor>> getRequestInterceptors() {
|
public List<Class<RequestInterceptor>> getRequestInterceptors() {
|
||||||
return this.requestInterceptors;
|
return requestInterceptors;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setRequestInterceptors(List<Class<RequestInterceptor>> requestInterceptors) {
|
public void setRequestInterceptors(List<Class<RequestInterceptor>> requestInterceptors) {
|
||||||
this.requestInterceptors = 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() {
|
public Boolean getDecode404() {
|
||||||
return this.decode404;
|
return decode404;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDecode404(Boolean decode404) {
|
public void setDecode404(Boolean decode404) {
|
||||||
@@ -170,7 +216,7 @@ public class FeignClientProperties {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Class<Decoder> getDecoder() {
|
public Class<Decoder> getDecoder() {
|
||||||
return this.decoder;
|
return decoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setDecoder(Class<Decoder> decoder) {
|
public void setDecoder(Class<Decoder> decoder) {
|
||||||
@@ -178,7 +224,7 @@ public class FeignClientProperties {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Class<Encoder> getEncoder() {
|
public Class<Encoder> getEncoder() {
|
||||||
return this.encoder;
|
return encoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setEncoder(Class<Encoder> encoder) {
|
public void setEncoder(Class<Encoder> encoder) {
|
||||||
@@ -186,7 +232,7 @@ public class FeignClientProperties {
|
|||||||
}
|
}
|
||||||
|
|
||||||
public Class<Contract> getContract() {
|
public Class<Contract> getContract() {
|
||||||
return this.contract;
|
return contract;
|
||||||
}
|
}
|
||||||
|
|
||||||
public void setContract(Class<Contract> contract) {
|
public void setContract(Class<Contract> contract) {
|
||||||
@@ -201,6 +247,30 @@ public class FeignClientProperties {
|
|||||||
this.exceptionPropagationPolicy = exceptionPropagationPolicy;
|
this.exceptionPropagationPolicy = exceptionPropagationPolicy;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public List<Class<Capability>> getCapabilities() {
|
||||||
|
return capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCapabilities(List<Class<Capability>> capabilities) {
|
||||||
|
this.capabilities = capabilities;
|
||||||
|
}
|
||||||
|
|
||||||
|
public MetricsProperties getMetrics() {
|
||||||
|
return metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setMetrics(MetricsProperties metrics) {
|
||||||
|
this.metrics = metrics;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Boolean isFollowRedirects() {
|
||||||
|
return followRedirects;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setFollowRedirects(Boolean followRedirects) {
|
||||||
|
this.followRedirects = followRedirects;
|
||||||
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public boolean equals(Object o) {
|
public boolean equals(Object o) {
|
||||||
if (this == o) {
|
if (this == o) {
|
||||||
@@ -210,20 +280,59 @@ public class FeignClientProperties {
|
|||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
FeignClientConfiguration that = (FeignClientConfiguration) o;
|
FeignClientConfiguration that = (FeignClientConfiguration) o;
|
||||||
return this.loggerLevel == that.loggerLevel && Objects.equals(this.connectTimeout, that.connectTimeout)
|
return loggerLevel == that.loggerLevel && Objects.equals(connectTimeout, that.connectTimeout)
|
||||||
&& Objects.equals(this.readTimeout, that.readTimeout) && Objects.equals(this.retryer, that.retryer)
|
&& Objects.equals(readTimeout, that.readTimeout) && Objects.equals(retryer, that.retryer)
|
||||||
&& Objects.equals(this.errorDecoder, that.errorDecoder)
|
&& Objects.equals(errorDecoder, that.errorDecoder)
|
||||||
&& Objects.equals(this.requestInterceptors, that.requestInterceptors)
|
&& Objects.equals(requestInterceptors, that.requestInterceptors)
|
||||||
&& Objects.equals(this.decode404, that.decode404) && Objects.equals(this.encoder, that.encoder)
|
&& Objects.equals(decode404, that.decode404) && Objects.equals(encoder, that.encoder)
|
||||||
&& Objects.equals(this.decoder, that.decoder) && Objects.equals(this.contract, that.contract)
|
&& Objects.equals(decoder, that.decoder) && Objects.equals(contract, that.contract)
|
||||||
&& Objects.equals(this.exceptionPropagationPolicy, that.exceptionPropagationPolicy);
|
&& Objects.equals(exceptionPropagationPolicy, that.exceptionPropagationPolicy)
|
||||||
|
&& Objects.equals(defaultRequestHeaders, that.defaultRequestHeaders)
|
||||||
|
&& Objects.equals(defaultQueryParameters, that.defaultQueryParameters)
|
||||||
|
&& Objects.equals(capabilities, that.capabilities) && Objects.equals(metrics, that.metrics)
|
||||||
|
&& Objects.equals(followRedirects, that.followRedirects);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public int hashCode() {
|
public int hashCode() {
|
||||||
return Objects.hash(this.loggerLevel, this.connectTimeout, this.readTimeout, this.retryer,
|
return Objects.hash(loggerLevel, connectTimeout, readTimeout, retryer, errorDecoder, requestInterceptors,
|
||||||
this.errorDecoder, this.requestInterceptors, this.decode404, this.encoder, this.decoder,
|
decode404, encoder, decoder, contract, exceptionPropagationPolicy, defaultQueryParameters,
|
||||||
this.contract, this.exceptionPropagationPolicy);
|
defaultRequestHeaders, capabilities, metrics, followRedirects);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Metrics configuration for Feign Client.
|
||||||
|
*/
|
||||||
|
public static class MetricsProperties {
|
||||||
|
|
||||||
|
private Boolean enabled = true;
|
||||||
|
|
||||||
|
public Boolean getEnabled() {
|
||||||
|
return enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEnabled(Boolean enabled) {
|
||||||
|
this.enabled = enabled;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object o) {
|
||||||
|
if (this == o) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
if (o == null || getClass() != o.getClass()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
MetricsProperties that = (MetricsProperties) o;
|
||||||
|
return Objects.equals(enabled, that.enabled);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return Objects.hash(enabled);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+85
-29
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -19,35 +19,42 @@ package org.springframework.cloud.openfeign;
|
|||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.Module;
|
|
||||||
import feign.Contract;
|
import feign.Contract;
|
||||||
import feign.Feign;
|
import feign.Feign;
|
||||||
import feign.Logger;
|
import feign.Logger;
|
||||||
|
import feign.QueryMapEncoder;
|
||||||
import feign.Retryer;
|
import feign.Retryer;
|
||||||
import feign.codec.Decoder;
|
import feign.codec.Decoder;
|
||||||
import feign.codec.Encoder;
|
import feign.codec.Encoder;
|
||||||
import feign.form.MultipartFormContentProcessor;
|
import feign.form.MultipartFormContentProcessor;
|
||||||
import feign.form.spring.SpringFormEncoder;
|
import feign.form.spring.SpringFormEncoder;
|
||||||
|
import feign.micrometer.MicrometerCapability;
|
||||||
import feign.optionals.OptionalDecoder;
|
import feign.optionals.OptionalDecoder;
|
||||||
|
import io.micrometer.core.instrument.MeterRegistry;
|
||||||
|
|
||||||
import org.springframework.beans.factory.ObjectFactory;
|
import org.springframework.beans.factory.ObjectFactory;
|
||||||
import org.springframework.beans.factory.ObjectProvider;
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
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.ConditionalOnClass;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
|
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.data.web.SpringDataWebProperties;
|
||||||
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
|
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.clientconfig.FeignClientConfigurer;
|
||||||
import org.springframework.cloud.openfeign.support.AbstractFormWriter;
|
import org.springframework.cloud.openfeign.support.AbstractFormWriter;
|
||||||
import org.springframework.cloud.openfeign.support.PageJacksonModule;
|
import org.springframework.cloud.openfeign.support.FeignEncoderProperties;
|
||||||
import org.springframework.cloud.openfeign.support.PageableSpringEncoder;
|
import org.springframework.cloud.openfeign.support.PageableSpringEncoder;
|
||||||
|
import org.springframework.cloud.openfeign.support.PageableSpringQueryMapEncoder;
|
||||||
import org.springframework.cloud.openfeign.support.ResponseEntityDecoder;
|
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.SpringDecoder;
|
||||||
import org.springframework.cloud.openfeign.support.SpringEncoder;
|
import org.springframework.cloud.openfeign.support.SpringEncoder;
|
||||||
import org.springframework.cloud.openfeign.support.SpringMvcContract;
|
import org.springframework.cloud.openfeign.support.SpringMvcContract;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Conditional;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.context.annotation.Scope;
|
import org.springframework.context.annotation.Scope;
|
||||||
import org.springframework.core.convert.ConversionService;
|
import org.springframework.core.convert.ConversionService;
|
||||||
@@ -60,6 +67,9 @@ import static feign.form.ContentType.MULTIPART;
|
|||||||
* @author Dave Syer
|
* @author Dave Syer
|
||||||
* @author Venil Noronha
|
* @author Venil Noronha
|
||||||
* @author Darren Foong
|
* @author Darren Foong
|
||||||
|
* @author Jonatan Ivanov
|
||||||
|
* @author Olga Maciaszek-Sharma
|
||||||
|
* @author Hyeonmin Park
|
||||||
*/
|
*/
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
public class FeignClientsConfiguration {
|
public class FeignClientsConfiguration {
|
||||||
@@ -79,6 +89,12 @@ public class FeignClientsConfiguration {
|
|||||||
@Autowired(required = false)
|
@Autowired(required = false)
|
||||||
private SpringDataWebProperties springDataWebProperties;
|
private SpringDataWebProperties springDataWebProperties;
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
private FeignClientProperties feignClientProperties;
|
||||||
|
|
||||||
|
@Autowired(required = false)
|
||||||
|
private FeignEncoderProperties encoderProperties;
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean
|
||||||
public Decoder feignDecoder() {
|
public Decoder feignDecoder() {
|
||||||
@@ -89,14 +105,14 @@ public class FeignClientsConfiguration {
|
|||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean
|
||||||
@ConditionalOnMissingClass("org.springframework.data.domain.Pageable")
|
@ConditionalOnMissingClass("org.springframework.data.domain.Pageable")
|
||||||
public Encoder feignEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider) {
|
public Encoder feignEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider) {
|
||||||
return springEncoder(formWriterProvider);
|
return springEncoder(formWriterProvider, encoderProperties);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnClass(name = "org.springframework.data.domain.Pageable")
|
@ConditionalOnClass(name = "org.springframework.data.domain.Pageable")
|
||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean
|
||||||
public Encoder feignEncoderPageable(ObjectProvider<AbstractFormWriter> formWriterProvider) {
|
public Encoder feignEncoderPageable(ObjectProvider<AbstractFormWriter> formWriterProvider) {
|
||||||
PageableSpringEncoder encoder = new PageableSpringEncoder(springEncoder(formWriterProvider));
|
PageableSpringEncoder encoder = new PageableSpringEncoder(springEncoder(formWriterProvider, encoderProperties));
|
||||||
|
|
||||||
if (springDataWebProperties != null) {
|
if (springDataWebProperties != null) {
|
||||||
encoder.setPageParameter(springDataWebProperties.getPageable().getPageParameter());
|
encoder.setPageParameter(springDataWebProperties.getPageable().getPageParameter());
|
||||||
@@ -106,10 +122,18 @@ public class FeignClientsConfiguration {
|
|||||||
return encoder;
|
return encoder;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnClass(name = "org.springframework.data.domain.Pageable")
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public QueryMapEncoder feignQueryMapEncoderPageable() {
|
||||||
|
return new PageableSpringQueryMapEncoder();
|
||||||
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean
|
||||||
public Contract feignContract(ConversionService feignConversionService) {
|
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
|
@Bean
|
||||||
@@ -127,31 +151,12 @@ public class FeignClientsConfiguration {
|
|||||||
return Retryer.NEVER_RETRY;
|
return Retryer.NEVER_RETRY;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
|
||||||
@Scope("prototype")
|
|
||||||
@ConditionalOnMissingBean
|
|
||||||
public Feign.Builder feignBuilder(Retryer retryer) {
|
|
||||||
return Feign.builder().retryer(retryer);
|
|
||||||
}
|
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean(FeignLoggerFactory.class)
|
@ConditionalOnMissingBean(FeignLoggerFactory.class)
|
||||||
public FeignLoggerFactory feignLoggerFactory() {
|
public FeignLoggerFactory feignLoggerFactory() {
|
||||||
return new DefaultFeignLoggerFactory(this.logger);
|
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
|
@Bean
|
||||||
@ConditionalOnMissingBean(FeignClientConfigurer.class)
|
@ConditionalOnMissingBean(FeignClientConfigurer.class)
|
||||||
public FeignClientConfigurer feignClientConfigurer() {
|
public FeignClientConfigurer feignClientConfigurer() {
|
||||||
@@ -159,14 +164,15 @@ public class FeignClientsConfiguration {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
private Encoder springEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider) {
|
private Encoder springEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider,
|
||||||
|
FeignEncoderProperties encoderProperties) {
|
||||||
AbstractFormWriter formWriter = formWriterProvider.getIfAvailable();
|
AbstractFormWriter formWriter = formWriterProvider.getIfAvailable();
|
||||||
|
|
||||||
if (formWriter != null) {
|
if (formWriter != null) {
|
||||||
return new SpringEncoder(new SpringPojoFormEncoder(formWriter), this.messageConverters);
|
return new SpringEncoder(new SpringPojoFormEncoder(formWriter), this.messageConverters, encoderProperties);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
return new SpringEncoder(new SpringFormEncoder(), this.messageConverters);
|
return new SpringEncoder(new SpringFormEncoder(), this.messageConverters, encoderProperties);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -181,4 +187,54 @@ 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();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@ConditionalOnBean(type = "io.micrometer.core.instrument.MeterRegistry")
|
||||||
|
@ConditionalOnClass(name = "feign.micrometer.MicrometerCapability")
|
||||||
|
@ConditionalOnProperty(name = "feign.metrics.enabled", matchIfMissing = true)
|
||||||
|
@Conditional(FeignClientMetricsEnabledCondition.class)
|
||||||
|
protected static class MetricsConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
public MicrometerCapability micrometerCapability(MeterRegistry meterRegistry) {
|
||||||
|
return new MicrometerCapability(meterRegistry);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+126
-94
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -16,21 +16,30 @@
|
|||||||
|
|
||||||
package org.springframework.cloud.openfeign;
|
package org.springframework.cloud.openfeign;
|
||||||
|
|
||||||
import java.io.IOException;
|
|
||||||
import java.net.MalformedURLException;
|
import java.net.MalformedURLException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.net.URISyntaxException;
|
import java.net.URISyntaxException;
|
||||||
import java.net.URL;
|
import java.net.URL;
|
||||||
|
import java.util.ArrayList;
|
||||||
import java.util.Arrays;
|
import java.util.Arrays;
|
||||||
|
import java.util.Collections;
|
||||||
import java.util.HashSet;
|
import java.util.HashSet;
|
||||||
|
import java.util.LinkedHashSet;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.Set;
|
import java.util.Set;
|
||||||
|
|
||||||
|
import feign.Request;
|
||||||
|
|
||||||
|
import org.springframework.aop.scope.ScopedProxyUtils;
|
||||||
import org.springframework.beans.factory.FactoryBean;
|
import org.springframework.beans.factory.FactoryBean;
|
||||||
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
|
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.BeanDefinition;
|
||||||
import org.springframework.beans.factory.config.BeanDefinitionHolder;
|
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.AbstractBeanDefinition;
|
||||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||||
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
|
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
|
||||||
@@ -43,14 +52,10 @@ import org.springframework.core.annotation.AnnotationAttributes;
|
|||||||
import org.springframework.core.env.Environment;
|
import org.springframework.core.env.Environment;
|
||||||
import org.springframework.core.io.ResourceLoader;
|
import org.springframework.core.io.ResourceLoader;
|
||||||
import org.springframework.core.type.AnnotationMetadata;
|
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.AnnotationTypeFilter;
|
||||||
import org.springframework.core.type.filter.TypeFilter;
|
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
import org.springframework.util.ClassUtils;
|
import org.springframework.util.ClassUtils;
|
||||||
|
import org.springframework.util.ObjectUtils;
|
||||||
import org.springframework.util.StringUtils;
|
import org.springframework.util.StringUtils;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -58,6 +63,10 @@ import org.springframework.util.StringUtils;
|
|||||||
* @author Jakub Narloch
|
* @author Jakub Narloch
|
||||||
* @author Venil Noronha
|
* @author Venil Noronha
|
||||||
* @author Gang Li
|
* @author Gang Li
|
||||||
|
* @author Michal Domagala
|
||||||
|
* @author Marcin Grzejszczak
|
||||||
|
* @author Olga Maciaszek-Sharma
|
||||||
|
* @author Jasbir Singh
|
||||||
*/
|
*/
|
||||||
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
|
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
|
||||||
|
|
||||||
@@ -158,53 +167,39 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
|||||||
}
|
}
|
||||||
|
|
||||||
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {
|
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());
|
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");
|
final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients");
|
||||||
if (clients == null || clients.length == 0) {
|
if (clients == null || clients.length == 0) {
|
||||||
scanner.addIncludeFilter(annotationTypeFilter);
|
ClassPathScanningCandidateComponentProvider scanner = getScanner();
|
||||||
basePackages = getBasePackages(metadata);
|
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 {
|
else {
|
||||||
final Set<String> clientClasses = new HashSet<>();
|
|
||||||
basePackages = new HashSet<>();
|
|
||||||
for (Class<?> clazz : clients) {
|
for (Class<?> clazz : clients) {
|
||||||
basePackages.add(ClassUtils.getPackageName(clazz));
|
candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));
|
||||||
clientClasses.add(clazz.getCanonicalName());
|
|
||||||
}
|
}
|
||||||
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) {
|
for (BeanDefinition candidateComponent : candidateComponents) {
|
||||||
Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage);
|
if (candidateComponent instanceof AnnotatedBeanDefinition) {
|
||||||
for (BeanDefinition candidateComponent : candidateComponents) {
|
// verify annotated class is an interface
|
||||||
if (candidateComponent instanceof AnnotatedBeanDefinition) {
|
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
|
||||||
// verify annotated class is an interface
|
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
|
||||||
AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
|
Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface");
|
||||||
AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
|
|
||||||
Assert.isTrue(annotationMetadata.isInterface(),
|
|
||||||
"@FeignClient can only be specified on an interface");
|
|
||||||
|
|
||||||
Map<String, Object> attributes = annotationMetadata
|
Map<String, Object> attributes = annotationMetadata
|
||||||
.getAnnotationAttributes(FeignClient.class.getCanonicalName());
|
.getAnnotationAttributes(FeignClient.class.getCanonicalName());
|
||||||
|
|
||||||
String name = getClientName(attributes);
|
String name = getClientName(attributes);
|
||||||
registerClientConfiguration(registry, name, attributes.get("configuration"));
|
registerClientConfiguration(registry, name, attributes.get("configuration"));
|
||||||
|
|
||||||
registerFeignClient(registry, annotationMetadata, attributes);
|
registerFeignClient(registry, annotationMetadata, attributes);
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -212,36 +207,55 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
|||||||
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata,
|
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata,
|
||||||
Map<String, Object> attributes) {
|
Map<String, Object> attributes) {
|
||||||
String className = annotationMetadata.getClassName();
|
String className = annotationMetadata.getClassName();
|
||||||
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
|
Class clazz = ClassUtils.resolveClassName(className, null);
|
||||||
validate(attributes);
|
ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
|
||||||
definition.addPropertyValue("url", getUrl(attributes));
|
? (ConfigurableBeanFactory) registry : null;
|
||||||
definition.addPropertyValue("path", getPath(attributes));
|
String contextId = getContextId(beanFactory, attributes);
|
||||||
String name = getName(attributes);
|
String name = getName(attributes);
|
||||||
definition.addPropertyValue("name", name);
|
FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
|
||||||
String contextId = getContextId(attributes);
|
factoryBean.setBeanFactory(beanFactory);
|
||||||
definition.addPropertyValue("contextId", contextId);
|
factoryBean.setName(name);
|
||||||
definition.addPropertyValue("type", className);
|
factoryBean.setContextId(contextId);
|
||||||
definition.addPropertyValue("decode404", attributes.get("decode404"));
|
factoryBean.setType(clazz);
|
||||||
definition.addPropertyValue("fallback", attributes.get("fallback"));
|
factoryBean.setRefreshableClient(isClientRefreshEnabled());
|
||||||
definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
|
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.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
|
||||||
|
definition.setLazyInit(true);
|
||||||
|
validate(attributes);
|
||||||
|
|
||||||
String alias = contextId + "FeignClient";
|
|
||||||
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
|
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
|
||||||
beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
|
beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
|
||||||
|
beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean);
|
||||||
|
|
||||||
// has a default, won't be null
|
// has a default, won't be null
|
||||||
boolean primary = (Boolean) attributes.get("primary");
|
boolean primary = (Boolean) attributes.get("primary");
|
||||||
|
|
||||||
beanDefinition.setPrimary(primary);
|
beanDefinition.setPrimary(primary);
|
||||||
|
|
||||||
String qualifier = getQualifier(attributes);
|
String[] qualifiers = getQualifiers(attributes);
|
||||||
if (StringUtils.hasText(qualifier)) {
|
if (ObjectUtils.isEmpty(qualifiers)) {
|
||||||
alias = qualifier;
|
qualifiers = new String[] { contextId + "FeignClient" };
|
||||||
}
|
}
|
||||||
|
|
||||||
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[] { alias });
|
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
|
||||||
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
|
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
|
||||||
|
|
||||||
|
registerOptionsBeanDefinition(registry, contextId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private void validate(Map<String, Object> attributes) {
|
private void validate(Map<String, Object> attributes) {
|
||||||
@@ -253,6 +267,10 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
|||||||
}
|
}
|
||||||
|
|
||||||
/* for testing */ String getName(Map<String, Object> attributes) {
|
/* for testing */ String getName(Map<String, Object> attributes) {
|
||||||
|
return getName(null, attributes);
|
||||||
|
}
|
||||||
|
|
||||||
|
String getName(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {
|
||||||
String name = (String) attributes.get("serviceId");
|
String name = (String) attributes.get("serviceId");
|
||||||
if (!StringUtils.hasText(name)) {
|
if (!StringUtils.hasText(name)) {
|
||||||
name = (String) attributes.get("name");
|
name = (String) attributes.get("name");
|
||||||
@@ -260,34 +278,46 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
|||||||
if (!StringUtils.hasText(name)) {
|
if (!StringUtils.hasText(name)) {
|
||||||
name = (String) attributes.get("value");
|
name = (String) attributes.get("value");
|
||||||
}
|
}
|
||||||
name = resolve(name);
|
name = resolve(beanFactory, name);
|
||||||
return getName(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");
|
String contextId = (String) attributes.get("contextId");
|
||||||
if (!StringUtils.hasText(contextId)) {
|
if (!StringUtils.hasText(contextId)) {
|
||||||
return getName(attributes);
|
return getName(attributes);
|
||||||
}
|
}
|
||||||
|
|
||||||
contextId = resolve(contextId);
|
contextId = resolve(beanFactory, contextId);
|
||||||
return getName(contextId);
|
return getName(contextId);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String resolve(String value) {
|
private String resolve(ConfigurableBeanFactory beanFactory, String value) {
|
||||||
if (StringUtils.hasText(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;
|
||||||
|
}
|
||||||
|
Object evaluateValue = resolver.evaluate(resolved, new BeanExpressionContext(beanFactory, null));
|
||||||
|
if (evaluateValue != null) {
|
||||||
|
return String.valueOf(evaluateValue);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getUrl(Map<String, Object> attributes) {
|
private String getUrl(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {
|
||||||
String url = resolve((String) attributes.get("url"));
|
String url = resolve(beanFactory, (String) attributes.get("url"));
|
||||||
return getUrl(url);
|
return getUrl(url);
|
||||||
}
|
}
|
||||||
|
|
||||||
private String getPath(Map<String, Object> attributes) {
|
private String getPath(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {
|
||||||
String path = resolve((String) attributes.get("path"));
|
String path = resolve(beanFactory, (String) attributes.get("path"));
|
||||||
return getPath(path);
|
return getPath(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -342,6 +372,18 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
|||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private String[] getQualifiers(Map<String, Object> client) {
|
||||||
|
if (client == null) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
List<String> qualifierList = new ArrayList<>(Arrays.asList((String[]) client.get("qualifiers")));
|
||||||
|
qualifierList.removeIf(qualifier -> !StringUtils.hasText(qualifier));
|
||||||
|
if (qualifierList.isEmpty() && getQualifier(client) != null) {
|
||||||
|
qualifierList = Collections.singletonList(getQualifier(client));
|
||||||
|
}
|
||||||
|
return !qualifierList.isEmpty() ? qualifierList.toArray(new String[0]) : null;
|
||||||
|
}
|
||||||
|
|
||||||
private String getClientName(Map<String, Object> client) {
|
private String getClientName(Map<String, Object> client) {
|
||||||
if (client == null) {
|
if (client == null) {
|
||||||
return null;
|
return null;
|
||||||
@@ -378,37 +420,27 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Helper class to create a {@link TypeFilter} that matches if all the delegates
|
* This method is meant to create {@link Request.Options} beans definition with
|
||||||
* match.
|
* refreshScope.
|
||||||
*
|
* @param registry spring bean definition registry
|
||||||
* @author Oliver Gierke
|
* @param contextId name of feign client
|
||||||
*/
|
*/
|
||||||
private static class AllTypeFilter implements TypeFilter {
|
private void registerOptionsBeanDefinition(BeanDefinitionRegistry registry, String contextId) {
|
||||||
|
if (isClientRefreshEnabled()) {
|
||||||
private final List<TypeFilter> delegates;
|
String beanName = Request.Options.class.getCanonicalName() + "-" + contextId;
|
||||||
|
BeanDefinitionBuilder definitionBuilder = BeanDefinitionBuilder
|
||||||
/**
|
.genericBeanDefinition(OptionsFactoryBean.class);
|
||||||
* Creates a new {@link AllTypeFilter} to match if all the given delegates match.
|
definitionBuilder.setScope("refresh");
|
||||||
* @param delegates must not be {@literal null}.
|
definitionBuilder.addPropertyValue("contextId", contextId);
|
||||||
*/
|
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(definitionBuilder.getBeanDefinition(),
|
||||||
AllTypeFilter(List<TypeFilter> delegates) {
|
beanName);
|
||||||
Assert.notNull(delegates, "This argument is required, it must not be null");
|
definitionHolder = ScopedProxyUtils.createScopedProxy(definitionHolder, registry, true);
|
||||||
this.delegates = delegates;
|
BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry);
|
||||||
}
|
|
||||||
|
|
||||||
@Override
|
|
||||||
public boolean match(MetadataReader metadataReader, MetadataReaderFactory metadataReaderFactory)
|
|
||||||
throws IOException {
|
|
||||||
|
|
||||||
for (TypeFilter filter : this.delegates) {
|
|
||||||
if (!filter.match(metadataReader, metadataReaderFactory)) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isClientRefreshEnabled() {
|
||||||
|
return environment.getProperty("feign.client.refresh-enabled", Boolean.class, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+6
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -30,6 +30,7 @@ import org.springframework.lang.Nullable;
|
|||||||
* @author Spencer Gibb
|
* @author Spencer Gibb
|
||||||
* @author Dave Syer
|
* @author Dave Syer
|
||||||
* @author Matt King
|
* @author Matt King
|
||||||
|
* @author Jasbir Singh
|
||||||
*/
|
*/
|
||||||
public class FeignContext extends NamedContextFactory<FeignClientSpecification> {
|
public class FeignContext extends NamedContextFactory<FeignClientSpecification> {
|
||||||
|
|
||||||
@@ -52,4 +53,8 @@ public class FeignContext extends NamedContextFactory<FeignClientSpecification>
|
|||||||
return getContext(name).getBeansOfType(type);
|
return getContext(name).getBeansOfType(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public <T> T getInstance(String contextName, String beanName, Class<T> type) {
|
||||||
|
return getContext(contextName).getBean(beanName, type);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2020-2020 the original author or authors.
|
* Copyright 2020-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016-2020 the original author or authors.
|
* Copyright 2016-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+42
@@ -0,0 +1,42 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.cloud.openfeign;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.condition.AnyNestedCondition;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Nguyen Ky Thanh
|
||||||
|
*/
|
||||||
|
public class HttpClient5DisabledConditions extends AnyNestedCondition {
|
||||||
|
|
||||||
|
public HttpClient5DisabledConditions() {
|
||||||
|
super(ConfigurationPhase.PARSE_CONFIGURATION);
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConditionalOnMissingClass("feign.hc5.ApacheHttp5Client")
|
||||||
|
static class ApacheHttp5ClientClassMissing {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@ConditionalOnProperty(value = "feign.httpclient.hc5.enabled", havingValue = "false", matchIfMissing = true)
|
||||||
|
static class HttpClient5Disabled {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+86
@@ -0,0 +1,86 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.cloud.openfeign;
|
||||||
|
|
||||||
|
import java.util.Objects;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import feign.Request;
|
||||||
|
|
||||||
|
import org.springframework.beans.BeansException;
|
||||||
|
import org.springframework.beans.factory.FactoryBean;
|
||||||
|
import org.springframework.context.ApplicationContext;
|
||||||
|
import org.springframework.context.ApplicationContextAware;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This factory bean is meant to create {@link Request.Options} instance as per the
|
||||||
|
* applicable configurations.
|
||||||
|
*
|
||||||
|
* @author Jasbir Singh
|
||||||
|
*/
|
||||||
|
public class OptionsFactoryBean implements FactoryBean<Request.Options>, ApplicationContextAware {
|
||||||
|
|
||||||
|
private ApplicationContext applicationContext;
|
||||||
|
|
||||||
|
private String contextId;
|
||||||
|
|
||||||
|
private Request.Options options;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Class<?> getObjectType() {
|
||||||
|
return Request.Options.class;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
|
||||||
|
this.applicationContext = applicationContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Request.Options getObject() throws Exception {
|
||||||
|
if (options != null) {
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
options = new Request.Options();
|
||||||
|
FeignClientProperties properties = applicationContext.getBean(FeignClientProperties.class);
|
||||||
|
options = createOptionsWithApplicableValues(properties.getConfig().get(properties.getDefaultConfig()), options);
|
||||||
|
options = createOptionsWithApplicableValues(properties.getConfig().get(contextId), options);
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setContextId(String contextId) {
|
||||||
|
this.contextId = contextId;
|
||||||
|
}
|
||||||
|
|
||||||
|
private Request.Options createOptionsWithApplicableValues(
|
||||||
|
FeignClientProperties.FeignClientConfiguration clientConfiguration, Request.Options options) {
|
||||||
|
if (Objects.isNull(clientConfiguration)) {
|
||||||
|
return options;
|
||||||
|
}
|
||||||
|
|
||||||
|
int connectTimeoutMillis = Objects.nonNull(clientConfiguration.getConnectTimeout())
|
||||||
|
? clientConfiguration.getConnectTimeout() : options.connectTimeoutMillis();
|
||||||
|
int readTimeoutMillis = Objects.nonNull(clientConfiguration.getReadTimeout())
|
||||||
|
? clientConfiguration.getReadTimeout() : options.readTimeoutMillis();
|
||||||
|
boolean followRedirects = Objects.nonNull(clientConfiguration.isFollowRedirects())
|
||||||
|
? clientConfiguration.isFollowRedirects() : options.isFollowRedirects();
|
||||||
|
return new Request.Options(connectTimeoutMillis, TimeUnit.MILLISECONDS, readTimeoutMillis,
|
||||||
|
TimeUnit.MILLISECONDS, followRedirects);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+3
-3
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -72,8 +72,8 @@ public class MatrixVariableParameterProcessor implements AnnotatedParameterProce
|
|||||||
private String expandMap(Object object) {
|
private String expandMap(Object object) {
|
||||||
Map<String, Object> paramMap = (Map) object;
|
Map<String, Object> paramMap = (Map) object;
|
||||||
|
|
||||||
return paramMap.keySet().stream().map(key -> ";" + key + "=" + paramMap.get(key).toString())
|
return paramMap.keySet().stream().filter(key -> paramMap.get(key) != null)
|
||||||
.collect(Collectors.joining());
|
.map(key -> ";" + key + "=" + paramMap.get(key).toString()).collect(Collectors.joining());
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2019 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+146
@@ -0,0 +1,146 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.cloud.openfeign.clientconfig;
|
||||||
|
|
||||||
|
import java.security.KeyManagementException;
|
||||||
|
import java.security.NoSuchAlgorithmException;
|
||||||
|
import java.security.SecureRandom;
|
||||||
|
import java.security.cert.CertificateException;
|
||||||
|
import java.security.cert.X509Certificate;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
import javax.annotation.PreDestroy;
|
||||||
|
import javax.net.ssl.SSLContext;
|
||||||
|
import javax.net.ssl.TrustManager;
|
||||||
|
import javax.net.ssl.X509TrustManager;
|
||||||
|
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
import org.apache.hc.client5.http.config.RequestConfig;
|
||||||
|
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
||||||
|
import org.apache.hc.client5.http.impl.classic.HttpClients;
|
||||||
|
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManagerBuilder;
|
||||||
|
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
|
||||||
|
import org.apache.hc.client5.http.socket.LayeredConnectionSocketFactory;
|
||||||
|
import org.apache.hc.client5.http.ssl.SSLConnectionSocketFactoryBuilder;
|
||||||
|
import org.apache.hc.core5.http.io.SocketConfig;
|
||||||
|
import org.apache.hc.core5.http.ssl.TLS;
|
||||||
|
import org.apache.hc.core5.io.CloseMode;
|
||||||
|
import org.apache.hc.core5.pool.PoolConcurrencyPolicy;
|
||||||
|
import org.apache.hc.core5.pool.PoolReusePolicy;
|
||||||
|
import org.apache.hc.core5.ssl.SSLContexts;
|
||||||
|
import org.apache.hc.core5.util.TimeValue;
|
||||||
|
import org.apache.hc.core5.util.Timeout;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default configuration for {@link CloseableHttpClient}.
|
||||||
|
*
|
||||||
|
* @author Nguyen Ky Thanh
|
||||||
|
*/
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@ConditionalOnMissingBean(CloseableHttpClient.class)
|
||||||
|
public class HttpClient5FeignConfiguration {
|
||||||
|
|
||||||
|
private static final Log LOG = LogFactory.getLog(HttpClient5FeignConfiguration.class);
|
||||||
|
|
||||||
|
private CloseableHttpClient httpClient5;
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean(HttpClientConnectionManager.class)
|
||||||
|
public HttpClientConnectionManager hc5ConnectionManager(FeignHttpClientProperties httpClientProperties) {
|
||||||
|
return PoolingHttpClientConnectionManagerBuilder.create()
|
||||||
|
.setSSLSocketFactory(httpsSSLConnectionSocketFactory(httpClientProperties.isDisableSslValidation()))
|
||||||
|
.setMaxConnTotal(httpClientProperties.getMaxConnections())
|
||||||
|
.setMaxConnPerRoute(httpClientProperties.getMaxConnectionsPerRoute())
|
||||||
|
.setConnPoolPolicy(PoolReusePolicy.valueOf(httpClientProperties.getHc5().getPoolReusePolicy().name()))
|
||||||
|
.setPoolConcurrencyPolicy(
|
||||||
|
PoolConcurrencyPolicy.valueOf(httpClientProperties.getHc5().getPoolConcurrencyPolicy().name()))
|
||||||
|
.setConnectionTimeToLive(
|
||||||
|
TimeValue.of(httpClientProperties.getTimeToLive(), httpClientProperties.getTimeToLiveUnit()))
|
||||||
|
.setDefaultSocketConfig(
|
||||||
|
SocketConfig.custom().setSoTimeout(Timeout.of(httpClientProperties.getHc5().getSocketTimeout(),
|
||||||
|
httpClientProperties.getHc5().getSocketTimeoutUnit())).build())
|
||||||
|
.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public CloseableHttpClient httpClient5(HttpClientConnectionManager connectionManager,
|
||||||
|
FeignHttpClientProperties httpClientProperties) {
|
||||||
|
httpClient5 = HttpClients.custom().disableCookieManagement().useSystemProperties()
|
||||||
|
.setConnectionManager(connectionManager).evictExpiredConnections()
|
||||||
|
.setDefaultRequestConfig(RequestConfig.custom()
|
||||||
|
.setConnectTimeout(
|
||||||
|
Timeout.of(httpClientProperties.getConnectionTimeout(), TimeUnit.MILLISECONDS))
|
||||||
|
.setRedirectsEnabled(httpClientProperties.isFollowRedirects()).build())
|
||||||
|
.build();
|
||||||
|
return httpClient5;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PreDestroy
|
||||||
|
public void destroy() {
|
||||||
|
if (httpClient5 != null) {
|
||||||
|
httpClient5.close(CloseMode.GRACEFUL);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private LayeredConnectionSocketFactory httpsSSLConnectionSocketFactory(boolean isDisableSslValidation) {
|
||||||
|
final SSLConnectionSocketFactoryBuilder sslConnectionSocketFactoryBuilder = SSLConnectionSocketFactoryBuilder
|
||||||
|
.create().setTlsVersions(TLS.V_1_3, TLS.V_1_2);
|
||||||
|
|
||||||
|
if (isDisableSslValidation) {
|
||||||
|
try {
|
||||||
|
final SSLContext sslContext = SSLContext.getInstance("SSL");
|
||||||
|
sslContext.init(null, new TrustManager[] { new DisabledValidationTrustManager() }, new SecureRandom());
|
||||||
|
sslConnectionSocketFactoryBuilder.setSslContext(sslContext);
|
||||||
|
}
|
||||||
|
catch (NoSuchAlgorithmException e) {
|
||||||
|
LOG.warn("Error creating SSLContext", e);
|
||||||
|
}
|
||||||
|
catch (KeyManagementException e) {
|
||||||
|
LOG.warn("Error creating SSLContext", e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
sslConnectionSocketFactoryBuilder.setSslContext(SSLContexts.createSystemDefault());
|
||||||
|
}
|
||||||
|
|
||||||
|
return sslConnectionSocketFactoryBuilder.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
static class DisabledValidationTrustManager implements X509TrustManager {
|
||||||
|
|
||||||
|
DisabledValidationTrustManager() {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void checkClientTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
|
||||||
|
}
|
||||||
|
|
||||||
|
public void checkServerTrusted(X509Certificate[] x509Certificates, String s) throws CertificateException {
|
||||||
|
}
|
||||||
|
|
||||||
|
public X509Certificate[] getAcceptedIssuers() {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+15
-3
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -16,11 +16,14 @@
|
|||||||
|
|
||||||
package org.springframework.cloud.openfeign.clientconfig;
|
package org.springframework.cloud.openfeign.clientconfig;
|
||||||
|
|
||||||
|
import java.io.IOException;
|
||||||
import java.util.Timer;
|
import java.util.Timer;
|
||||||
import java.util.TimerTask;
|
import java.util.TimerTask;
|
||||||
|
|
||||||
import javax.annotation.PreDestroy;
|
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.client.config.RequestConfig;
|
||||||
import org.apache.http.config.RegistryBuilder;
|
import org.apache.http.config.RegistryBuilder;
|
||||||
import org.apache.http.conn.HttpClientConnectionManager;
|
import org.apache.http.conn.HttpClientConnectionManager;
|
||||||
@@ -48,6 +51,8 @@ import org.springframework.context.annotation.Configuration;
|
|||||||
@ConditionalOnMissingBean(CloseableHttpClient.class)
|
@ConditionalOnMissingBean(CloseableHttpClient.class)
|
||||||
public class HttpClientFeignConfiguration {
|
public class HttpClientFeignConfiguration {
|
||||||
|
|
||||||
|
private static final Log LOG = LogFactory.getLog(HttpClientFeignConfiguration.class);
|
||||||
|
|
||||||
private final Timer connectionManagerTimer = new Timer("FeignApacheHttpClientConfiguration.connectionManagerTimer",
|
private final Timer connectionManagerTimer = new Timer("FeignApacheHttpClientConfiguration.connectionManagerTimer",
|
||||||
true);
|
true);
|
||||||
|
|
||||||
@@ -103,10 +108,17 @@ public class HttpClientFeignConfiguration {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@PreDestroy
|
@PreDestroy
|
||||||
public void destroy() throws Exception {
|
public void destroy() {
|
||||||
this.connectionManagerTimer.cancel();
|
this.connectionManagerTimer.cancel();
|
||||||
if (this.httpClient != null) {
|
if (this.httpClient != null) {
|
||||||
this.httpClient.close();
|
try {
|
||||||
|
this.httpClient.close();
|
||||||
|
}
|
||||||
|
catch (IOException e) {
|
||||||
|
if (LOG.isErrorEnabled()) {
|
||||||
|
LOG.error("Could not correctly close httpClient.");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+8
-35
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2016-2020 the original author or authors.
|
* Copyright 2016-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -16,75 +16,48 @@
|
|||||||
|
|
||||||
package org.springframework.cloud.openfeign.hateoas;
|
package org.springframework.cloud.openfeign.hateoas;
|
||||||
|
|
||||||
import java.util.Arrays;
|
|
||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
|
|
||||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
|
||||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||||
|
|
||||||
import org.springframework.beans.factory.ObjectProvider;
|
import org.springframework.beans.factory.ObjectProvider;
|
||||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
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.autoconfigure.condition.ConditionalOnClass;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||||
import org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration;
|
import org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.hateoas.HypermediaAutoConfiguration;
|
|
||||||
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
|
import org.springframework.boot.autoconfigure.http.HttpMessageConvertersAutoConfiguration;
|
||||||
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
|
import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.hateoas.RepresentationModel;
|
import org.springframework.hateoas.RepresentationModel;
|
||||||
import org.springframework.hateoas.mediatype.MessageResolver;
|
import org.springframework.hateoas.mediatype.hal.HalMediaTypeConfiguration;
|
||||||
import org.springframework.hateoas.mediatype.hal.CurieProvider;
|
|
||||||
import org.springframework.hateoas.mediatype.hal.DefaultCurieProvider;
|
|
||||||
import org.springframework.hateoas.mediatype.hal.HalConfiguration;
|
|
||||||
import org.springframework.hateoas.mediatype.hal.Jackson2HalModule;
|
|
||||||
import org.springframework.hateoas.server.LinkRelationProvider;
|
|
||||||
import org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter;
|
import org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter;
|
||||||
|
|
||||||
import static org.springframework.hateoas.MediaTypes.HAL_JSON;
|
import static org.springframework.hateoas.MediaTypes.HAL_JSON;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Hector Espert
|
* @author Hector Espert
|
||||||
|
* @author Olga Maciaszek-Sharma
|
||||||
*/
|
*/
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
@ConditionalOnWebApplication
|
@ConditionalOnWebApplication
|
||||||
@ConditionalOnClass(RepresentationModel.class)
|
@ConditionalOnClass(RepresentationModel.class)
|
||||||
@AutoConfigureAfter({ JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,
|
@AutoConfigureAfter({ JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,
|
||||||
RepositoryRestMvcAutoConfiguration.class })
|
RepositoryRestMvcAutoConfiguration.class })
|
||||||
@AutoConfigureBefore(HypermediaAutoConfiguration.class)
|
|
||||||
public class FeignHalAutoConfiguration {
|
public class FeignHalAutoConfiguration {
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
|
@ConditionalOnBean(HalMediaTypeConfiguration.class)
|
||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean
|
||||||
public TypeConstrainedMappingJackson2HttpMessageConverter halJacksonHttpMessageConverter(
|
public TypeConstrainedMappingJackson2HttpMessageConverter halJacksonHttpMessageConverter(
|
||||||
ObjectProvider<ObjectMapper> objectMapper, ObjectProvider<HalConfiguration> halConfiguration,
|
ObjectProvider<ObjectMapper> objectMapper, HalMediaTypeConfiguration halConfiguration) {
|
||||||
ObjectProvider<MessageResolver> messageResolver, ObjectProvider<CurieProvider> curieProvider,
|
|
||||||
ObjectProvider<LinkRelationProvider> linkRelationProvider) {
|
|
||||||
|
|
||||||
ObjectMapper mapper = objectMapper.getIfAvailable(ObjectMapper::new).copy();
|
ObjectMapper mapper = objectMapper.getIfAvailable(ObjectMapper::new).copy();
|
||||||
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
|
halConfiguration.configureObjectMapper(mapper);
|
||||||
|
|
||||||
HalConfiguration configuration = halConfiguration.getIfAvailable(HalConfiguration::new);
|
|
||||||
|
|
||||||
CurieProvider curieProviderInstance = curieProvider
|
|
||||||
.getIfAvailable(() -> new DefaultCurieProvider(Collections.emptyMap()));
|
|
||||||
|
|
||||||
Jackson2HalModule.HalHandlerInstantiator halHandlerInstantiator = new Jackson2HalModule.HalHandlerInstantiator(
|
|
||||||
linkRelationProvider.getIfAvailable(), curieProviderInstance, messageResolver.getIfAvailable(),
|
|
||||||
configuration);
|
|
||||||
|
|
||||||
mapper.setHandlerInstantiator(halHandlerInstantiator);
|
|
||||||
|
|
||||||
if (!Jackson2HalModule.isAlreadyRegisteredIn(mapper)) {
|
|
||||||
Jackson2HalModule halModule = new Jackson2HalModule();
|
|
||||||
mapper.registerModule(halModule);
|
|
||||||
}
|
|
||||||
|
|
||||||
TypeConstrainedMappingJackson2HttpMessageConverter converter = new TypeConstrainedMappingJackson2HttpMessageConverter(
|
TypeConstrainedMappingJackson2HttpMessageConverter converter = new TypeConstrainedMappingJackson2HttpMessageConverter(
|
||||||
RepresentationModel.class);
|
RepresentationModel.class);
|
||||||
converter.setSupportedMediaTypes(Arrays.asList(HAL_JSON));
|
converter.setSupportedMediaTypes(Collections.singletonList(HAL_JSON));
|
||||||
converter.setObjectMapper(mapper);
|
converter.setObjectMapper(mapper);
|
||||||
return converter;
|
return converter;
|
||||||
}
|
}
|
||||||
|
|||||||
+10
-6
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -25,7 +25,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
|
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
|
||||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
|
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.Bean;
|
||||||
import org.springframework.context.annotation.Conditional;
|
import org.springframework.context.annotation.Conditional;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
@@ -44,8 +45,10 @@ class DefaultFeignLoadBalancerConfiguration {
|
|||||||
@Bean
|
@Bean
|
||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean
|
||||||
@Conditional(OnRetryNotEnabledCondition.class)
|
@Conditional(OnRetryNotEnabledCondition.class)
|
||||||
public Client feignClient(LoadBalancerClient loadBalancerClient, LoadBalancerProperties properties) {
|
public Client feignClient(LoadBalancerClient loadBalancerClient, LoadBalancerProperties properties,
|
||||||
return new FeignBlockingLoadBalancerClient(new Client.Default(null, null), loadBalancerClient, properties);
|
LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||||
|
return new FeignBlockingLoadBalancerClient(new Client.Default(null, null), loadBalancerClient, properties,
|
||||||
|
loadBalancerClientFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@@ -55,9 +58,10 @@ class DefaultFeignLoadBalancerConfiguration {
|
|||||||
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true",
|
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true",
|
||||||
matchIfMissing = true)
|
matchIfMissing = true)
|
||||||
public Client feignRetryClient(LoadBalancerClient loadBalancerClient,
|
public Client feignRetryClient(LoadBalancerClient loadBalancerClient,
|
||||||
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties) {
|
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties,
|
||||||
|
LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||||
return new RetryableFeignBlockingLoadBalancerClient(new Client.Default(null, null), loadBalancerClient,
|
return new RetryableFeignBlockingLoadBalancerClient(new Client.Default(null, null), loadBalancerClient,
|
||||||
loadBalancedRetryFactory, properties);
|
loadBalancedRetryFactory, properties, loadBalancerClientFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-8
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -19,6 +19,7 @@ package org.springframework.cloud.openfeign.loadbalancer;
|
|||||||
import java.io.IOException;
|
import java.io.IOException;
|
||||||
import java.net.URI;
|
import java.net.URI;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import feign.Client;
|
import feign.Client;
|
||||||
import feign.Request;
|
import feign.Request;
|
||||||
@@ -27,13 +28,22 @@ import org.apache.commons.logging.Log;
|
|||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
|
||||||
import org.springframework.cloud.client.ServiceInstance;
|
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.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.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.http.HttpStatus;
|
||||||
import org.springframework.util.Assert;
|
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
|
* A {@link Client} implementation that uses {@link LoadBalancerClient} to select a
|
||||||
* {@link ServiceInstance} to use while resolving the request host.
|
* {@link ServiceInstance} to use while resolving the request host.
|
||||||
@@ -41,6 +51,7 @@ import org.springframework.util.Assert;
|
|||||||
* @author Olga Maciaszek-Sharma
|
* @author Olga Maciaszek-Sharma
|
||||||
* @since 2.2.0
|
* @since 2.2.0
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings({ "unchecked", "rawtypes" })
|
||||||
public class FeignBlockingLoadBalancerClient implements Client {
|
public class FeignBlockingLoadBalancerClient implements Client {
|
||||||
|
|
||||||
private static final Log LOG = LogFactory.getLog(FeignBlockingLoadBalancerClient.class);
|
private static final Log LOG = LogFactory.getLog(FeignBlockingLoadBalancerClient.class);
|
||||||
@@ -51,11 +62,14 @@ public class FeignBlockingLoadBalancerClient implements Client {
|
|||||||
|
|
||||||
private final LoadBalancerProperties properties;
|
private final LoadBalancerProperties properties;
|
||||||
|
|
||||||
|
private final LoadBalancerClientFactory loadBalancerClientFactory;
|
||||||
|
|
||||||
public FeignBlockingLoadBalancerClient(Client delegate, LoadBalancerClient loadBalancerClient,
|
public FeignBlockingLoadBalancerClient(Client delegate, LoadBalancerClient loadBalancerClient,
|
||||||
LoadBalancerProperties properties) {
|
LoadBalancerProperties properties, LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||||
this.delegate = delegate;
|
this.delegate = delegate;
|
||||||
this.loadBalancerClient = loadBalancerClient;
|
this.loadBalancerClient = loadBalancerClient;
|
||||||
this.properties = properties;
|
this.properties = properties;
|
||||||
|
this.loadBalancerClientFactory = loadBalancerClientFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -64,21 +78,36 @@ public class FeignBlockingLoadBalancerClient implements Client {
|
|||||||
String serviceId = originalUri.getHost();
|
String serviceId = originalUri.getHost();
|
||||||
Assert.state(serviceId != null, "Request URI does not contain a valid hostname: " + originalUri);
|
Assert.state(serviceId != null, "Request URI does not contain a valid hostname: " + originalUri);
|
||||||
String hint = getHint(serviceId);
|
String hint = getHint(serviceId);
|
||||||
DefaultRequest<DefaultRequestContext> lbRequest = new DefaultRequest<>(
|
DefaultRequest<RequestDataContext> lbRequest = new DefaultRequest<>(
|
||||||
new DefaultRequestContext(request, hint));
|
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);
|
ServiceInstance instance = loadBalancerClient.choose(serviceId, lbRequest);
|
||||||
|
org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse = new DefaultResponse(
|
||||||
|
instance);
|
||||||
if (instance == null) {
|
if (instance == null) {
|
||||||
String message = "Load balancer does not contain an instance for the service " + serviceId;
|
String message = "Load balancer does not contain an instance for the service " + serviceId;
|
||||||
if (LOG.isWarnEnabled()) {
|
if (LOG.isWarnEnabled()) {
|
||||||
LOG.warn(message);
|
LOG.warn(message);
|
||||||
}
|
}
|
||||||
|
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle
|
||||||
|
.onComplete(new CompletionContext<ResponseData, ServiceInstance, RequestDataContext>(
|
||||||
|
CompletionContext.Status.DISCARD, lbRequest, lbResponse)));
|
||||||
return Response.builder().request(request).status(HttpStatus.SERVICE_UNAVAILABLE.value())
|
return Response.builder().request(request).status(HttpStatus.SERVICE_UNAVAILABLE.value())
|
||||||
.body(message, StandardCharsets.UTF_8).build();
|
.body(message, StandardCharsets.UTF_8).build();
|
||||||
}
|
}
|
||||||
String reconstructedUrl = loadBalancerClient.reconstructURI(instance, originalUri).toString();
|
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());
|
request.charset(), request.requestTemplate());
|
||||||
return delegate.execute(newRequest, options);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Visible for Sleuth instrumentation
|
// Visible for Sleuth instrumentation
|
||||||
|
|||||||
+9
-3
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -19,11 +19,15 @@ package org.springframework.cloud.openfeign.loadbalancer;
|
|||||||
import feign.Client;
|
import feign.Client;
|
||||||
import feign.Feign;
|
import feign.Feign;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
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.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.FeignAutoConfiguration;
|
||||||
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
|
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
@@ -34,18 +38,20 @@ import org.springframework.context.annotation.Import;
|
|||||||
* of {@link Client}.
|
* of {@link Client}.
|
||||||
*
|
*
|
||||||
* @author Olga Maciaszek-Sharma
|
* @author Olga Maciaszek-Sharma
|
||||||
|
* @author Nguyen Ky Thanh
|
||||||
* @since 2.2.0
|
* @since 2.2.0
|
||||||
*/
|
*/
|
||||||
@ConditionalOnClass(Feign.class)
|
@ConditionalOnClass(Feign.class)
|
||||||
@ConditionalOnBean(LoadBalancerClient.class)
|
@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
|
||||||
@AutoConfigureBefore(FeignAutoConfiguration.class)
|
@AutoConfigureBefore(FeignAutoConfiguration.class)
|
||||||
|
@AutoConfigureAfter({ BlockingLoadBalancerClientAutoConfiguration.class, LoadBalancerAutoConfiguration.class })
|
||||||
@EnableConfigurationProperties(FeignHttpClientProperties.class)
|
@EnableConfigurationProperties(FeignHttpClientProperties.class)
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
// Order is important here, last should be the default, first should be optional
|
// Order is important here, last should be the default, first should be optional
|
||||||
// see
|
// see
|
||||||
// https://github.com/spring-cloud/spring-cloud-netflix/issues/2086#issuecomment-316281653
|
// https://github.com/spring-cloud/spring-cloud-netflix/issues/2086#issuecomment-316281653
|
||||||
@Import({ HttpClientFeignLoadBalancerConfiguration.class, OkHttpFeignLoadBalancerConfiguration.class,
|
@Import({ HttpClientFeignLoadBalancerConfiguration.class, OkHttpFeignLoadBalancerConfiguration.class,
|
||||||
DefaultFeignLoadBalancerConfiguration.class })
|
HttpClient5FeignLoadBalancerConfiguration.class, DefaultFeignLoadBalancerConfiguration.class })
|
||||||
public class FeignLoadBalancerAutoConfiguration {
|
public class FeignLoadBalancerAutoConfiguration {
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+75
@@ -0,0 +1,75 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.cloud.openfeign.loadbalancer;
|
||||||
|
|
||||||
|
import feign.Client;
|
||||||
|
import feign.hc5.ApacheHttp5Client;
|
||||||
|
import org.apache.hc.client5.http.classic.HttpClient;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||||
|
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
|
||||||
|
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
|
||||||
|
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
|
||||||
|
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||||
|
import org.springframework.cloud.openfeign.clientconfig.HttpClient5FeignConfiguration;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Conditional;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.context.annotation.Import;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Configuration instantiating a {@link LoadBalancerClient}-based {@link Client} object
|
||||||
|
* that uses {@link ApacheHttp5Client} under the hood.
|
||||||
|
*
|
||||||
|
* @author Nguyen Ky Thanh
|
||||||
|
*/
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@ConditionalOnClass(ApacheHttp5Client.class)
|
||||||
|
@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
|
||||||
|
@ConditionalOnProperty(value = "feign.httpclient.hc5.enabled", havingValue = "true")
|
||||||
|
@Import(HttpClient5FeignConfiguration.class)
|
||||||
|
@EnableConfigurationProperties(LoadBalancerProperties.class)
|
||||||
|
class HttpClient5FeignLoadBalancerConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
@Conditional(OnRetryNotEnabledCondition.class)
|
||||||
|
public Client feignClient(LoadBalancerClient loadBalancerClient, HttpClient httpClient5,
|
||||||
|
LoadBalancerProperties properties, LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||||
|
Client delegate = new ApacheHttp5Client(httpClient5);
|
||||||
|
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, properties, loadBalancerClientFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
@ConditionalOnMissingBean
|
||||||
|
@ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
|
||||||
|
@ConditionalOnBean(LoadBalancedRetryFactory.class)
|
||||||
|
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true",
|
||||||
|
matchIfMissing = true)
|
||||||
|
public Client feignRetryClient(LoadBalancerClient loadBalancerClient, HttpClient httpClient5,
|
||||||
|
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties,
|
||||||
|
LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||||
|
Client delegate = new ApacheHttp5Client(httpClient5);
|
||||||
|
return new RetryableFeignBlockingLoadBalancerClient(delegate, loadBalancerClient, loadBalancedRetryFactory,
|
||||||
|
properties, loadBalancerClientFactory);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+12
-7
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -27,7 +27,9 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
|
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
|
||||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
|
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.HttpClient5DisabledConditions;
|
||||||
import org.springframework.cloud.openfeign.clientconfig.HttpClientFeignConfiguration;
|
import org.springframework.cloud.openfeign.clientconfig.HttpClientFeignConfiguration;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Conditional;
|
import org.springframework.context.annotation.Conditional;
|
||||||
@@ -39,12 +41,14 @@ import org.springframework.context.annotation.Import;
|
|||||||
* that uses {@link ApacheHttpClient} under the hood.
|
* that uses {@link ApacheHttpClient} under the hood.
|
||||||
*
|
*
|
||||||
* @author Olga Maciaszek-Sharma
|
* @author Olga Maciaszek-Sharma
|
||||||
|
* @author Nguyen Ky Thanh
|
||||||
* @since 2.2.0
|
* @since 2.2.0
|
||||||
*/
|
*/
|
||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
@ConditionalOnClass(ApacheHttpClient.class)
|
@ConditionalOnClass(ApacheHttpClient.class)
|
||||||
@ConditionalOnBean(LoadBalancerClient.class)
|
@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
|
||||||
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
|
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
|
||||||
|
@Conditional(HttpClient5DisabledConditions.class)
|
||||||
@Import(HttpClientFeignConfiguration.class)
|
@Import(HttpClientFeignConfiguration.class)
|
||||||
@EnableConfigurationProperties(LoadBalancerProperties.class)
|
@EnableConfigurationProperties(LoadBalancerProperties.class)
|
||||||
class HttpClientFeignLoadBalancerConfiguration {
|
class HttpClientFeignLoadBalancerConfiguration {
|
||||||
@@ -53,9 +57,9 @@ class HttpClientFeignLoadBalancerConfiguration {
|
|||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean
|
||||||
@Conditional(OnRetryNotEnabledCondition.class)
|
@Conditional(OnRetryNotEnabledCondition.class)
|
||||||
public Client feignClient(LoadBalancerClient loadBalancerClient, HttpClient httpClient,
|
public Client feignClient(LoadBalancerClient loadBalancerClient, HttpClient httpClient,
|
||||||
LoadBalancerProperties properties) {
|
LoadBalancerProperties properties, LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||||
ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
|
ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
|
||||||
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, properties);
|
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, properties, loadBalancerClientFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@@ -65,10 +69,11 @@ class HttpClientFeignLoadBalancerConfiguration {
|
|||||||
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true",
|
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true",
|
||||||
matchIfMissing = true)
|
matchIfMissing = true)
|
||||||
public Client feignRetryClient(LoadBalancerClient loadBalancerClient, HttpClient httpClient,
|
public Client feignRetryClient(LoadBalancerClient loadBalancerClient, HttpClient httpClient,
|
||||||
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties) {
|
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties,
|
||||||
|
LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||||
ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
|
ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
|
||||||
return new RetryableFeignBlockingLoadBalancerClient(delegate, loadBalancerClient, loadBalancedRetryFactory,
|
return new RetryableFeignBlockingLoadBalancerClient(delegate, loadBalancerClient, loadBalancedRetryFactory,
|
||||||
properties);
|
properties, loadBalancerClientFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+46
@@ -0,0 +1,46 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.cloud.openfeign.loadbalancer;
|
||||||
|
|
||||||
|
import java.io.ByteArrayInputStream;
|
||||||
|
import java.net.URI;
|
||||||
|
|
||||||
|
import feign.Response;
|
||||||
|
|
||||||
|
import org.springframework.cloud.client.loadbalancer.RetryableStatusCodeException;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link RetryableStatusCodeException} for {@link Response}s.
|
||||||
|
*
|
||||||
|
* @author Ryan Baxter
|
||||||
|
*/
|
||||||
|
public class LoadBalancerResponseStatusCodeException extends RetryableStatusCodeException {
|
||||||
|
|
||||||
|
private final Response response;
|
||||||
|
|
||||||
|
public LoadBalancerResponseStatusCodeException(String serviceId, Response response, byte[] body, URI uri) {
|
||||||
|
super(serviceId, response.status(), response, uri);
|
||||||
|
this.response = Response.builder().body(new ByteArrayInputStream(body), body.length).headers(response.headers())
|
||||||
|
.reason(response.reason()).status(response.status()).request(response.request()).build();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Response getResponse() {
|
||||||
|
return this.response;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+95
@@ -0,0 +1,95 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.cloud.openfeign.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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+9
-7
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -26,7 +26,8 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
|||||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
|
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
|
||||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
|
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.cloud.openfeign.clientconfig.OkHttpFeignConfiguration;
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Conditional;
|
import org.springframework.context.annotation.Conditional;
|
||||||
@@ -43,7 +44,7 @@ import org.springframework.context.annotation.Import;
|
|||||||
@Configuration(proxyBeanMethods = false)
|
@Configuration(proxyBeanMethods = false)
|
||||||
@ConditionalOnClass(OkHttpClient.class)
|
@ConditionalOnClass(OkHttpClient.class)
|
||||||
@ConditionalOnProperty("feign.okhttp.enabled")
|
@ConditionalOnProperty("feign.okhttp.enabled")
|
||||||
@ConditionalOnBean(LoadBalancerClient.class)
|
@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
|
||||||
@Import(OkHttpFeignConfiguration.class)
|
@Import(OkHttpFeignConfiguration.class)
|
||||||
@EnableConfigurationProperties(LoadBalancerProperties.class)
|
@EnableConfigurationProperties(LoadBalancerProperties.class)
|
||||||
class OkHttpFeignLoadBalancerConfiguration {
|
class OkHttpFeignLoadBalancerConfiguration {
|
||||||
@@ -52,9 +53,9 @@ class OkHttpFeignLoadBalancerConfiguration {
|
|||||||
@ConditionalOnMissingBean
|
@ConditionalOnMissingBean
|
||||||
@Conditional(OnRetryNotEnabledCondition.class)
|
@Conditional(OnRetryNotEnabledCondition.class)
|
||||||
public Client feignClient(okhttp3.OkHttpClient okHttpClient, LoadBalancerClient loadBalancerClient,
|
public Client feignClient(okhttp3.OkHttpClient okHttpClient, LoadBalancerClient loadBalancerClient,
|
||||||
LoadBalancerProperties properties) {
|
LoadBalancerProperties properties, LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||||
OkHttpClient delegate = new OkHttpClient(okHttpClient);
|
OkHttpClient delegate = new OkHttpClient(okHttpClient);
|
||||||
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, properties);
|
return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient, properties, loadBalancerClientFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Bean
|
@Bean
|
||||||
@@ -64,10 +65,11 @@ class OkHttpFeignLoadBalancerConfiguration {
|
|||||||
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true",
|
@ConditionalOnProperty(value = "spring.cloud.loadbalancer.retry.enabled", havingValue = "true",
|
||||||
matchIfMissing = true)
|
matchIfMissing = true)
|
||||||
public Client feignRetryClient(LoadBalancerClient loadBalancerClient, okhttp3.OkHttpClient okHttpClient,
|
public Client feignRetryClient(LoadBalancerClient loadBalancerClient, okhttp3.OkHttpClient okHttpClient,
|
||||||
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties) {
|
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties,
|
||||||
|
LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||||
OkHttpClient delegate = new OkHttpClient(okHttpClient);
|
OkHttpClient delegate = new OkHttpClient(okHttpClient);
|
||||||
return new RetryableFeignBlockingLoadBalancerClient(delegate, loadBalancerClient, loadBalancedRetryFactory,
|
return new RetryableFeignBlockingLoadBalancerClient(delegate, loadBalancerClient, loadBalancedRetryFactory,
|
||||||
properties);
|
properties, loadBalancerClientFactory);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+62
-24
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -23,6 +23,7 @@ import java.util.Collection;
|
|||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
|
import java.util.Set;
|
||||||
|
|
||||||
import feign.Client;
|
import feign.Client;
|
||||||
import feign.Request;
|
import feign.Request;
|
||||||
@@ -31,16 +32,21 @@ import org.apache.commons.logging.Log;
|
|||||||
import org.apache.commons.logging.LogFactory;
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
|
||||||
import org.springframework.cloud.client.ServiceInstance;
|
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.DefaultRequest;
|
||||||
|
import org.springframework.cloud.client.loadbalancer.DefaultResponse;
|
||||||
import org.springframework.cloud.client.loadbalancer.InterceptorRetryPolicy;
|
import org.springframework.cloud.client.loadbalancer.InterceptorRetryPolicy;
|
||||||
import org.springframework.cloud.client.loadbalancer.LoadBalancedRecoveryCallback;
|
import org.springframework.cloud.client.loadbalancer.LoadBalancedRecoveryCallback;
|
||||||
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryContext;
|
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryContext;
|
||||||
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
|
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
|
||||||
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicy;
|
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryPolicy;
|
||||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
|
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.RetryableRequestContext;
|
||||||
import org.springframework.cloud.client.loadbalancer.RetryableStatusCodeException;
|
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||||
import org.springframework.cloud.client.loadbalancer.reactive.LoadBalancerProperties;
|
|
||||||
import org.springframework.http.HttpHeaders;
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpMethod;
|
import org.springframework.http.HttpMethod;
|
||||||
import org.springframework.http.HttpRequest;
|
import org.springframework.http.HttpRequest;
|
||||||
@@ -50,6 +56,9 @@ import org.springframework.retry.backoff.NoBackOffPolicy;
|
|||||||
import org.springframework.retry.policy.NeverRetryPolicy;
|
import org.springframework.retry.policy.NeverRetryPolicy;
|
||||||
import org.springframework.retry.support.RetryTemplate;
|
import org.springframework.retry.support.RetryTemplate;
|
||||||
import org.springframework.util.Assert;
|
import org.springframework.util.Assert;
|
||||||
|
import org.springframework.util.StreamUtils;
|
||||||
|
|
||||||
|
import static org.springframework.cloud.openfeign.loadbalancer.LoadBalancerUtils.buildRequestData;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A {@link Client} implementation that provides Spring Retry support for requests
|
* A {@link Client} implementation that provides Spring Retry support for requests
|
||||||
@@ -58,9 +67,10 @@ import org.springframework.util.Assert;
|
|||||||
* @author Olga Maciaszek-Sharma
|
* @author Olga Maciaszek-Sharma
|
||||||
* @since 2.2.6
|
* @since 2.2.6
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||||
public class RetryableFeignBlockingLoadBalancerClient implements Client {
|
public class RetryableFeignBlockingLoadBalancerClient implements Client {
|
||||||
|
|
||||||
private static final Log LOG = LogFactory.getLog(FeignBlockingLoadBalancerClient.class);
|
private static final Log LOG = LogFactory.getLog(RetryableFeignBlockingLoadBalancerClient.class);
|
||||||
|
|
||||||
private final Client delegate;
|
private final Client delegate;
|
||||||
|
|
||||||
@@ -70,12 +80,16 @@ public class RetryableFeignBlockingLoadBalancerClient implements Client {
|
|||||||
|
|
||||||
private final LoadBalancerProperties properties;
|
private final LoadBalancerProperties properties;
|
||||||
|
|
||||||
|
private final LoadBalancerClientFactory loadBalancerClientFactory;
|
||||||
|
|
||||||
public RetryableFeignBlockingLoadBalancerClient(Client delegate, LoadBalancerClient loadBalancerClient,
|
public RetryableFeignBlockingLoadBalancerClient(Client delegate, LoadBalancerClient loadBalancerClient,
|
||||||
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties) {
|
LoadBalancedRetryFactory loadBalancedRetryFactory, LoadBalancerProperties properties,
|
||||||
|
LoadBalancerClientFactory loadBalancerClientFactory) {
|
||||||
this.delegate = delegate;
|
this.delegate = delegate;
|
||||||
this.loadBalancerClient = loadBalancerClient;
|
this.loadBalancerClient = loadBalancerClient;
|
||||||
this.loadBalancedRetryFactory = loadBalancedRetryFactory;
|
this.loadBalancedRetryFactory = loadBalancedRetryFactory;
|
||||||
this.properties = properties;
|
this.properties = properties;
|
||||||
|
this.loadBalancerClientFactory = loadBalancerClientFactory;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -88,6 +102,14 @@ public class RetryableFeignBlockingLoadBalancerClient implements Client {
|
|||||||
RetryTemplate retryTemplate = buildRetryTemplate(serviceId, request, retryPolicy);
|
RetryTemplate retryTemplate = buildRetryTemplate(serviceId, request, retryPolicy);
|
||||||
return retryTemplate.execute(context -> {
|
return retryTemplate.execute(context -> {
|
||||||
Request feignRequest = null;
|
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
|
// On retries the policy will choose the server and set it in the context
|
||||||
// and extract the server and update the request being made
|
// and extract the server and update the request being made
|
||||||
if (context instanceof LoadBalancedRetryContext) {
|
if (context instanceof LoadBalancedRetryContext) {
|
||||||
@@ -99,40 +121,51 @@ public class RetryableFeignBlockingLoadBalancerClient implements Client {
|
|||||||
+ "Reattempting service instance selection");
|
+ "Reattempting service instance selection");
|
||||||
}
|
}
|
||||||
ServiceInstance previousServiceInstance = lbContext.getPreviousServiceInstance();
|
ServiceInstance previousServiceInstance = lbContext.getPreviousServiceInstance();
|
||||||
String hint = getHint(serviceId);
|
lbRequest.getContext().setPreviousServiceInstance(previousServiceInstance);
|
||||||
DefaultRequest<RetryableRequestContext> lbRequest = new DefaultRequest<>(
|
supportedLifecycleProcessors.forEach(lifecycle -> lifecycle.onStart(lbRequest));
|
||||||
new RetryableRequestContext(previousServiceInstance, request, hint));
|
retrievedServiceInstance = loadBalancerClient.choose(serviceId, lbRequest);
|
||||||
serviceInstance = loadBalancerClient.choose(serviceId, lbRequest);
|
|
||||||
if (LOG.isDebugEnabled()) {
|
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()) {
|
if (LOG.isDebugEnabled()) {
|
||||||
LOG.debug(String.format("Using service instance from LoadBalancedRetryContext: %s",
|
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();
|
.toString();
|
||||||
feignRequest = Request.create(request.httpMethod(), reconstructedUrl, request.headers(),
|
feignRequest = buildRequest(request, reconstructedUrl);
|
||||||
request.body(), request.charset(), request.requestTemplate());
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (feignRequest == null) {
|
org.springframework.cloud.client.loadbalancer.Response<ServiceInstance> lbResponse = new DefaultResponse(
|
||||||
if (LOG.isWarnEnabled()) {
|
retrievedServiceInstance);
|
||||||
LOG.warn("Service instance was not resolved, executing the original request");
|
Response response = LoadBalancerUtils.executeWithLoadBalancerLifecycleProcessing(delegate, options,
|
||||||
}
|
feignRequest, lbRequest, lbResponse, supportedLifecycleProcessors,
|
||||||
feignRequest = request;
|
retrievedServiceInstance != null);
|
||||||
}
|
|
||||||
Response response = delegate.execute(feignRequest, options);
|
|
||||||
int responseStatus = response.status();
|
int responseStatus = response.status();
|
||||||
if (retryPolicy != null && retryPolicy.retryableStatusCode(responseStatus)) {
|
if (retryPolicy != null && retryPolicy.retryableStatusCode(responseStatus)) {
|
||||||
if (LOG.isDebugEnabled()) {
|
if (LOG.isDebugEnabled()) {
|
||||||
LOG.debug(String.format("Retrying on status code: %d", responseStatus));
|
LOG.debug(String.format("Retrying on status code: %d", responseStatus));
|
||||||
}
|
}
|
||||||
|
byte[] byteArray = response.body() == null ? new byte[] {}
|
||||||
|
: StreamUtils.copyToByteArray(response.body().asInputStream());
|
||||||
response.close();
|
response.close();
|
||||||
throw new RetryableStatusCodeException(serviceId, responseStatus, response, URI.create(request.url()));
|
throw new LoadBalancerResponseStatusCodeException(serviceId, response, byteArray,
|
||||||
|
URI.create(request.url()));
|
||||||
}
|
}
|
||||||
return response;
|
return response;
|
||||||
}, new LoadBalancedRecoveryCallback<Response, Response>() {
|
}, new LoadBalancedRecoveryCallback<Response, Response>() {
|
||||||
@@ -143,6 +176,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) {
|
private RetryTemplate buildRetryTemplate(String serviceId, Request request, LoadBalancedRetryPolicy retryPolicy) {
|
||||||
RetryTemplate retryTemplate = new RetryTemplate();
|
RetryTemplate retryTemplate = new RetryTemplate();
|
||||||
BackOffPolicy backOffPolicy = this.loadBalancedRetryFactory.createBackOffPolicy(serviceId);
|
BackOffPolicy backOffPolicy = this.loadBalancedRetryFactory.createBackOffPolicy(serviceId);
|
||||||
|
|||||||
+183
@@ -0,0 +1,183 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2015-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.cloud.openfeign.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
|
||||||
|
*/
|
||||||
|
@Deprecated // spring-security-oauth2 reached EOL
|
||||||
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+44
@@ -0,0 +1,44 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.cloud.openfeign.support;
|
||||||
|
|
||||||
|
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Properties for {@link SpringEncoder}.
|
||||||
|
*
|
||||||
|
* @author Olga Maciaszek-Sharma
|
||||||
|
* @since 2.2.8
|
||||||
|
*/
|
||||||
|
@ConfigurationProperties("feign.encoder")
|
||||||
|
public class FeignEncoderProperties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Indicates whether the charset should be derived from the {@code Content-Type}
|
||||||
|
* header.
|
||||||
|
*/
|
||||||
|
private boolean charsetFromContentType = false;
|
||||||
|
|
||||||
|
public boolean isCharsetFromContentType() {
|
||||||
|
return charsetFromContentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCharsetFromContentType(boolean charsetFromContentType) {
|
||||||
|
this.charsetFromContentType = charsetFromContentType;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+127
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -22,6 +22,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Ryan Baxter
|
* @author Ryan Baxter
|
||||||
|
* @author Nguyen Ky Thanh
|
||||||
*/
|
*/
|
||||||
@ConfigurationProperties(prefix = "feign.httpclient")
|
@ConfigurationProperties(prefix = "feign.httpclient")
|
||||||
public class FeignHttpClientProperties {
|
public class FeignHttpClientProperties {
|
||||||
@@ -82,6 +83,11 @@ public class FeignHttpClientProperties {
|
|||||||
|
|
||||||
private int connectionTimerRepeat = DEFAULT_CONNECTION_TIMER_REPEAT;
|
private int connectionTimerRepeat = DEFAULT_CONNECTION_TIMER_REPEAT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apache HttpClient5 additional properties.
|
||||||
|
*/
|
||||||
|
private Hc5Properties hc5 = new Hc5Properties();
|
||||||
|
|
||||||
public int getConnectionTimerRepeat() {
|
public int getConnectionTimerRepeat() {
|
||||||
return this.connectionTimerRepeat;
|
return this.connectionTimerRepeat;
|
||||||
}
|
}
|
||||||
@@ -146,4 +152,124 @@ public class FeignHttpClientProperties {
|
|||||||
this.connectionTimeout = connectionTimeout;
|
this.connectionTimeout = connectionTimeout;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public Hc5Properties getHc5() {
|
||||||
|
return hc5;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setHc5(Hc5Properties hc5) {
|
||||||
|
this.hc5 = hc5;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class Hc5Properties {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default value for pool concurrency policy.
|
||||||
|
*/
|
||||||
|
public static final PoolConcurrencyPolicy DEFAULT_POOL_CONCURRENCY_POLICY = PoolConcurrencyPolicy.STRICT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default value for pool reuse policy.
|
||||||
|
*/
|
||||||
|
public static final PoolReusePolicy DEFAULT_POOL_REUSE_POLICY = PoolReusePolicy.FIFO;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default value for socket timeout.
|
||||||
|
*/
|
||||||
|
public static final int DEFAULT_SOCKET_TIMEOUT = 5;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default value for socket timeout unit.
|
||||||
|
*/
|
||||||
|
public static final TimeUnit DEFAULT_SOCKET_TIMEOUT_UNIT = TimeUnit.SECONDS;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pool concurrency policies.
|
||||||
|
*/
|
||||||
|
private PoolConcurrencyPolicy poolConcurrencyPolicy = DEFAULT_POOL_CONCURRENCY_POLICY;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Pool connection re-use policies.
|
||||||
|
*/
|
||||||
|
private PoolReusePolicy poolReusePolicy = DEFAULT_POOL_REUSE_POLICY;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default value for socket timeout.
|
||||||
|
*/
|
||||||
|
private int socketTimeout = DEFAULT_SOCKET_TIMEOUT;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Default value for socket timeout unit.
|
||||||
|
*/
|
||||||
|
private TimeUnit socketTimeoutUnit = DEFAULT_SOCKET_TIMEOUT_UNIT;
|
||||||
|
|
||||||
|
public PoolConcurrencyPolicy getPoolConcurrencyPolicy() {
|
||||||
|
return this.poolConcurrencyPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPoolConcurrencyPolicy(PoolConcurrencyPolicy poolConcurrencyPolicy) {
|
||||||
|
this.poolConcurrencyPolicy = poolConcurrencyPolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public PoolReusePolicy getPoolReusePolicy() {
|
||||||
|
return poolReusePolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPoolReusePolicy(PoolReusePolicy poolReusePolicy) {
|
||||||
|
this.poolReusePolicy = poolReusePolicy;
|
||||||
|
}
|
||||||
|
|
||||||
|
public TimeUnit getSocketTimeoutUnit() {
|
||||||
|
return socketTimeoutUnit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSocketTimeoutUnit(TimeUnit socketTimeoutUnit) {
|
||||||
|
this.socketTimeoutUnit = socketTimeoutUnit;
|
||||||
|
}
|
||||||
|
|
||||||
|
public int getSocketTimeout() {
|
||||||
|
return socketTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setSocketTimeout(int socketTimeout) {
|
||||||
|
this.socketTimeout = socketTimeout;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enumeration of pool concurrency policies.
|
||||||
|
*/
|
||||||
|
public enum PoolConcurrencyPolicy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Higher concurrency but with lax connection max limit guarantees.
|
||||||
|
*/
|
||||||
|
LAX,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Strict connection max limit guarantees.
|
||||||
|
*/
|
||||||
|
STRICT
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enumeration of pooled connection re-use policies.
|
||||||
|
*/
|
||||||
|
public enum PoolReusePolicy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-use as few connections as possible making it possible for connections to
|
||||||
|
* become idle and expire.
|
||||||
|
*/
|
||||||
|
LIFO,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Re-use all connections equally preventing them from becoming idle and
|
||||||
|
* expiring.
|
||||||
|
*/
|
||||||
|
FIFO
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+46
-8
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -20,7 +20,9 @@ import java.util.Iterator;
|
|||||||
import java.util.List;
|
import java.util.List;
|
||||||
import java.util.function.Function;
|
import java.util.function.Function;
|
||||||
|
|
||||||
|
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||||
|
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
|
||||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||||
import com.fasterxml.jackson.core.Version;
|
import com.fasterxml.jackson.core.Version;
|
||||||
import com.fasterxml.jackson.databind.Module;
|
import com.fasterxml.jackson.databind.Module;
|
||||||
@@ -36,6 +38,9 @@ import org.springframework.data.domain.Sort;
|
|||||||
* This Jackson module provides support to deserialize Spring {@link Page} objects.
|
* This Jackson module provides support to deserialize Spring {@link Page} objects.
|
||||||
*
|
*
|
||||||
* @author Pascal Büttiker
|
* @author Pascal Büttiker
|
||||||
|
* @author Olga Maciaszek-Sharma
|
||||||
|
* @author Pedro Mendes
|
||||||
|
* @author Nikita Konev
|
||||||
*/
|
*/
|
||||||
public class PageJacksonModule extends Module {
|
public class PageJacksonModule extends Module {
|
||||||
|
|
||||||
@@ -55,6 +60,7 @@ public class PageJacksonModule extends Module {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@JsonDeserialize(as = SimplePageImpl.class)
|
@JsonDeserialize(as = SimplePageImpl.class)
|
||||||
|
@JsonIgnoreProperties(ignoreUnknown = true)
|
||||||
private interface PageMixIn {
|
private interface PageMixIn {
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -64,17 +70,22 @@ public class PageJacksonModule extends Module {
|
|||||||
private final Page<T> delegate;
|
private final Page<T> delegate;
|
||||||
|
|
||||||
SimplePageImpl(@JsonProperty("content") List<T> content, @JsonProperty("number") int number,
|
SimplePageImpl(@JsonProperty("content") List<T> content, @JsonProperty("number") int number,
|
||||||
@JsonProperty("size") int size, @JsonProperty("totalElements") long totalElements,
|
@JsonProperty("size") int size, @JsonProperty("totalElements") @JsonAlias({ "total-elements",
|
||||||
|
"total_elements", "totalelements", "TotalElements" }) long totalElements,
|
||||||
@JsonProperty("sort") Sort sort) {
|
@JsonProperty("sort") Sort sort) {
|
||||||
PageRequest pageRequest;
|
if (size > 0) {
|
||||||
if (sort != null) {
|
PageRequest pageRequest;
|
||||||
pageRequest = PageRequest.of(number, size, sort);
|
if (sort != null) {
|
||||||
|
pageRequest = PageRequest.of(number, size, sort);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
pageRequest = PageRequest.of(number, size);
|
||||||
|
}
|
||||||
|
delegate = new PageImpl<>(content, pageRequest, totalElements);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
pageRequest = PageRequest.of(number, size);
|
delegate = new PageImpl<>(content);
|
||||||
}
|
}
|
||||||
delegate = new PageImpl<>(content, pageRequest, totalElements);
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@JsonProperty
|
@JsonProperty
|
||||||
@@ -173,6 +184,33 @@ public class PageJacksonModule extends Module {
|
|||||||
return delegate.iterator();
|
return delegate.iterator();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
@Override
|
||||||
|
public Pageable getPageable() {
|
||||||
|
return delegate.getPageable();
|
||||||
|
}
|
||||||
|
|
||||||
|
@JsonIgnore
|
||||||
|
@Override
|
||||||
|
public boolean isEmpty() {
|
||||||
|
return delegate.isEmpty();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public int hashCode() {
|
||||||
|
return delegate.hashCode();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean equals(Object obj) {
|
||||||
|
return delegate.equals(obj);
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String toString() {
|
||||||
|
return delegate.toString();
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -116,7 +116,7 @@ public class PageableSpringEncoder implements Encoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
for (Sort.Order order : sort) {
|
for (Sort.Order order : sort) {
|
||||||
sortQueries.add(order.getProperty() + "," + order.getDirection());
|
sortQueries.add(order.getProperty() + "%2C" + order.getDirection());
|
||||||
}
|
}
|
||||||
if (!sortQueries.isEmpty()) {
|
if (!sortQueries.isEmpty()) {
|
||||||
template.query(sortParameter, sortQueries);
|
template.query(sortParameter, sortQueries);
|
||||||
|
|||||||
+80
@@ -0,0 +1,80 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.cloud.openfeign.support;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.HashMap;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import feign.querymap.BeanQueryMapEncoder;
|
||||||
|
|
||||||
|
import org.springframework.data.domain.Pageable;
|
||||||
|
import org.springframework.data.domain.Sort;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Provides support for encoding Pageable annotated as
|
||||||
|
* {@link org.springframework.cloud.openfeign.SpringQueryMap}.
|
||||||
|
*
|
||||||
|
* @author Hyeonmin Park
|
||||||
|
* @since 2.2.8
|
||||||
|
*/
|
||||||
|
public class PageableSpringQueryMapEncoder extends BeanQueryMapEncoder {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public Map<String, Object> encode(Object object) {
|
||||||
|
if (supports(object)) {
|
||||||
|
Map<String, Object> queryMap = new HashMap<>();
|
||||||
|
|
||||||
|
if (object instanceof Pageable) {
|
||||||
|
Pageable pageable = (Pageable) object;
|
||||||
|
|
||||||
|
if (pageable.isPaged()) {
|
||||||
|
queryMap.put("page", pageable.getPageNumber());
|
||||||
|
queryMap.put("size", pageable.getPageSize());
|
||||||
|
}
|
||||||
|
|
||||||
|
if (pageable.getSort() != null) {
|
||||||
|
applySort(queryMap, pageable.getSort());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else if (object instanceof Sort) {
|
||||||
|
Sort sort = (Sort) object;
|
||||||
|
applySort(queryMap, sort);
|
||||||
|
}
|
||||||
|
return queryMap;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return super.encode(object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void applySort(Map<String, Object> queryMap, Sort sort) {
|
||||||
|
List<String> sortQueries = new ArrayList<>();
|
||||||
|
for (Sort.Order order : sort) {
|
||||||
|
sortQueries.add(order.getProperty() + "%2C" + order.getDirection());
|
||||||
|
}
|
||||||
|
if (!sortQueries.isEmpty()) {
|
||||||
|
queryMap.put("sort", sortQueries);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
protected boolean supports(Object object) {
|
||||||
|
return object instanceof Pageable || object instanceof Sort;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+4
-4
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with 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 feign.codec.Decoder;
|
||||||
|
|
||||||
import org.springframework.http.HttpEntity;
|
import org.springframework.http.HttpEntity;
|
||||||
|
import org.springframework.http.HttpHeaders;
|
||||||
import org.springframework.http.HttpStatus;
|
import org.springframework.http.HttpStatus;
|
||||||
import org.springframework.http.ResponseEntity;
|
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
|
* Decoder adds compatibility for Spring MVC's ResponseEntity to any other decoder via
|
||||||
* composition.
|
* composition.
|
||||||
*
|
*
|
||||||
* @author chad jaros
|
* @author chad jaros
|
||||||
|
* @author Olga Maciaszek-Sharma
|
||||||
*/
|
*/
|
||||||
public class ResponseEntityDecoder implements Decoder {
|
public class ResponseEntityDecoder implements Decoder {
|
||||||
|
|
||||||
@@ -80,7 +80,7 @@ public class ResponseEntityDecoder implements Decoder {
|
|||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private <T> ResponseEntity<T> createResponse(Object instance, Response response) {
|
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()) {
|
for (String key : response.headers().keySet()) {
|
||||||
headers.put(key, new LinkedList<>(response.headers().get(key)));
|
headers.put(key, new LinkedList<>(response.headers().get(key)));
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+103
-55
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -22,10 +22,11 @@ import java.io.OutputStream;
|
|||||||
import java.lang.reflect.Type;
|
import java.lang.reflect.Type;
|
||||||
import java.nio.charset.Charset;
|
import java.nio.charset.Charset;
|
||||||
import java.nio.charset.StandardCharsets;
|
import java.nio.charset.StandardCharsets;
|
||||||
|
import java.util.Arrays;
|
||||||
import java.util.Collection;
|
import java.util.Collection;
|
||||||
import java.util.Objects;
|
import java.util.Objects;
|
||||||
|
import java.util.stream.Stream;
|
||||||
|
|
||||||
import feign.Request;
|
|
||||||
import feign.RequestTemplate;
|
import feign.RequestTemplate;
|
||||||
import feign.codec.EncodeException;
|
import feign.codec.EncodeException;
|
||||||
import feign.codec.Encoder;
|
import feign.codec.Encoder;
|
||||||
@@ -48,6 +49,10 @@ import org.springframework.web.multipart.MultipartFile;
|
|||||||
|
|
||||||
import static org.springframework.cloud.openfeign.support.FeignUtils.getHeaders;
|
import static org.springframework.cloud.openfeign.support.FeignUtils.getHeaders;
|
||||||
import static org.springframework.cloud.openfeign.support.FeignUtils.getHttpHeaders;
|
import static org.springframework.cloud.openfeign.support.FeignUtils.getHttpHeaders;
|
||||||
|
import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED;
|
||||||
|
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA;
|
||||||
|
import static org.springframework.http.MediaType.MULTIPART_MIXED;
|
||||||
|
import static org.springframework.http.MediaType.MULTIPART_RELATED;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Spencer Gibb
|
* @author Spencer Gibb
|
||||||
@@ -55,7 +60,10 @@ import static org.springframework.cloud.openfeign.support.FeignUtils.getHttpHead
|
|||||||
* @author Ahmad Mozafarnia
|
* @author Ahmad Mozafarnia
|
||||||
* @author Aaron Whiteside
|
* @author Aaron Whiteside
|
||||||
* @author Darren Foong
|
* @author Darren Foong
|
||||||
|
* @author Olga Maciaszek-Sharma
|
||||||
|
* @author Can Bezmen
|
||||||
*/
|
*/
|
||||||
|
@SuppressWarnings("rawtypes")
|
||||||
public class SpringEncoder implements Encoder {
|
public class SpringEncoder implements Encoder {
|
||||||
|
|
||||||
private static final Log log = LogFactory.getLog(SpringEncoder.class);
|
private static final Log log = LogFactory.getLog(SpringEncoder.class);
|
||||||
@@ -64,14 +72,21 @@ public class SpringEncoder implements Encoder {
|
|||||||
|
|
||||||
private final ObjectFactory<HttpMessageConverters> messageConverters;
|
private final ObjectFactory<HttpMessageConverters> messageConverters;
|
||||||
|
|
||||||
|
private final FeignEncoderProperties encoderProperties;
|
||||||
|
|
||||||
public SpringEncoder(ObjectFactory<HttpMessageConverters> messageConverters) {
|
public SpringEncoder(ObjectFactory<HttpMessageConverters> messageConverters) {
|
||||||
this.springFormEncoder = new SpringFormEncoder();
|
this(new SpringFormEncoder(), messageConverters);
|
||||||
this.messageConverters = messageConverters;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public SpringEncoder(SpringFormEncoder springFormEncoder, ObjectFactory<HttpMessageConverters> messageConverters) {
|
public SpringEncoder(SpringFormEncoder springFormEncoder, ObjectFactory<HttpMessageConverters> messageConverters) {
|
||||||
|
this(springFormEncoder, messageConverters, new FeignEncoderProperties());
|
||||||
|
}
|
||||||
|
|
||||||
|
public SpringEncoder(SpringFormEncoder springFormEncoder, ObjectFactory<HttpMessageConverters> messageConverters,
|
||||||
|
FeignEncoderProperties encoderProperties) {
|
||||||
this.springFormEncoder = springFormEncoder;
|
this.springFormEncoder = springFormEncoder;
|
||||||
this.messageConverters = messageConverters;
|
this.messageConverters = messageConverters;
|
||||||
|
this.encoderProperties = encoderProperties;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -86,8 +101,8 @@ public class SpringEncoder implements Encoder {
|
|||||||
requestContentType = MediaType.valueOf(type);
|
requestContentType = MediaType.valueOf(type);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (Objects.equals(requestContentType, MediaType.MULTIPART_FORM_DATA)) {
|
if (isFormRelatedContentType(requestContentType)) {
|
||||||
this.springFormEncoder.encode(requestBody, bodyType, request);
|
springFormEncoder.encode(requestBody, bodyType, request);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
@@ -96,54 +111,67 @@ public class SpringEncoder implements Encoder {
|
|||||||
+ "should be specified as MediaType.MULTIPART_FORM_DATA_VALUE");
|
+ "should be specified as MediaType.MULTIPART_FORM_DATA_VALUE");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
encodeWithMessageConverter(requestBody, bodyType, request, requestContentType);
|
||||||
for (HttpMessageConverter messageConverter : this.messageConverters.getObject().getConverters()) {
|
|
||||||
FeignOutputMessage outputMessage;
|
|
||||||
try {
|
|
||||||
if (messageConverter instanceof GenericHttpMessageConverter) {
|
|
||||||
outputMessage = checkAndWrite(requestBody, bodyType, requestContentType,
|
|
||||||
(GenericHttpMessageConverter) messageConverter, request);
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
outputMessage = checkAndWrite(requestBody, requestContentType, messageConverter, request);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
catch (IOException | HttpMessageConversionException ex) {
|
|
||||||
throw new EncodeException("Error converting request body", ex);
|
|
||||||
}
|
|
||||||
if (outputMessage != null) {
|
|
||||||
// clear headers
|
|
||||||
request.headers(null);
|
|
||||||
// converters can modify headers, so update the request
|
|
||||||
// with the modified headers
|
|
||||||
request.headers(getHeaders(outputMessage.getHeaders()));
|
|
||||||
|
|
||||||
// do not use charset for binary data and protobuf
|
|
||||||
Charset charset;
|
|
||||||
if (messageConverter instanceof ByteArrayHttpMessageConverter) {
|
|
||||||
charset = null;
|
|
||||||
}
|
|
||||||
else if (messageConverter instanceof ProtobufHttpMessageConverter
|
|
||||||
&& ProtobufHttpMessageConverter.PROTOBUF
|
|
||||||
.isCompatibleWith(outputMessage.getHeaders().getContentType())) {
|
|
||||||
charset = null;
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
charset = StandardCharsets.UTF_8;
|
|
||||||
}
|
|
||||||
request.body(Request.Body.encoded(outputMessage.getOutputStream().toByteArray(), charset));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
String message = "Could not write request: no suitable HttpMessageConverter " + "found for request type ["
|
|
||||||
+ requestBody.getClass().getName() + "]";
|
|
||||||
if (requestContentType != null) {
|
|
||||||
message += " and content type [" + requestContentType + "]";
|
|
||||||
}
|
|
||||||
throw new EncodeException(message);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private void encodeWithMessageConverter(Object requestBody, Type bodyType, RequestTemplate request,
|
||||||
|
MediaType requestContentType) {
|
||||||
|
for (HttpMessageConverter messageConverter : this.messageConverters.getObject().getConverters()) {
|
||||||
|
FeignOutputMessage outputMessage;
|
||||||
|
try {
|
||||||
|
if (messageConverter instanceof GenericHttpMessageConverter) {
|
||||||
|
outputMessage = checkAndWrite(requestBody, bodyType, requestContentType,
|
||||||
|
(GenericHttpMessageConverter) messageConverter, request);
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
outputMessage = checkAndWrite(requestBody, requestContentType, messageConverter, request);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (IOException | HttpMessageConversionException ex) {
|
||||||
|
throw new EncodeException("Error converting request body", ex);
|
||||||
|
}
|
||||||
|
if (outputMessage != null) {
|
||||||
|
// clear headers
|
||||||
|
request.headers(null);
|
||||||
|
// converters can modify headers, so update the request
|
||||||
|
// with the modified headers
|
||||||
|
request.headers(getHeaders(outputMessage.getHeaders()));
|
||||||
|
|
||||||
|
// do not use charset for binary data and protobuf
|
||||||
|
Charset charset;
|
||||||
|
|
||||||
|
MediaType contentType = outputMessage.getHeaders().getContentType();
|
||||||
|
Charset charsetFromContentType = contentType != null ? contentType.getCharset() : null;
|
||||||
|
|
||||||
|
if (encoderProperties != null && encoderProperties.isCharsetFromContentType()
|
||||||
|
&& charsetFromContentType != null) {
|
||||||
|
charset = charsetFromContentType;
|
||||||
|
}
|
||||||
|
else if (shouldHaveNullCharset(messageConverter, outputMessage)) {
|
||||||
|
charset = null;
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
charset = StandardCharsets.UTF_8;
|
||||||
|
}
|
||||||
|
request.body(outputMessage.getOutputStream().toByteArray(), charset);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
String message = "Could not write request: no suitable HttpMessageConverter " + "found for request type ["
|
||||||
|
+ requestBody.getClass().getName() + "]";
|
||||||
|
if (requestContentType != null) {
|
||||||
|
message += " and content type [" + requestContentType + "]";
|
||||||
|
}
|
||||||
|
throw new EncodeException(message);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean shouldHaveNullCharset(HttpMessageConverter messageConverter, FeignOutputMessage outputMessage) {
|
||||||
|
return binaryContentType(outputMessage) || messageConverter instanceof ByteArrayHttpMessageConverter
|
||||||
|
|| messageConverter instanceof ProtobufHttpMessageConverter && ProtobufHttpMessageConverter.PROTOBUF
|
||||||
|
.isCompatibleWith(outputMessage.getHeaders().getContentType());
|
||||||
|
}
|
||||||
|
|
||||||
@SuppressWarnings("unchecked")
|
@SuppressWarnings("unchecked")
|
||||||
private FeignOutputMessage checkAndWrite(Object body, MediaType contentType, HttpMessageConverter converter,
|
private FeignOutputMessage checkAndWrite(Object body, MediaType contentType, HttpMessageConverter converter,
|
||||||
RequestTemplate request) throws IOException {
|
RequestTemplate request) throws IOException {
|
||||||
@@ -185,6 +213,26 @@ public class SpringEncoder implements Encoder {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private boolean isFormRelatedContentType(MediaType requestContentType) {
|
||||||
|
return isMultipartType(requestContentType) || isFormUrlEncoded(requestContentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isMultipartType(MediaType requestContentType) {
|
||||||
|
return Arrays.asList(MULTIPART_FORM_DATA, MULTIPART_MIXED, MULTIPART_RELATED).contains(requestContentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean isFormUrlEncoded(MediaType requestContentType) {
|
||||||
|
return Objects.equals(APPLICATION_FORM_URLENCODED, requestContentType);
|
||||||
|
}
|
||||||
|
|
||||||
|
private boolean binaryContentType(FeignOutputMessage outputMessage) {
|
||||||
|
MediaType contentType = outputMessage.getHeaders().getContentType();
|
||||||
|
return contentType == null || Stream
|
||||||
|
.of(MediaType.APPLICATION_CBOR, MediaType.APPLICATION_OCTET_STREAM, MediaType.APPLICATION_PDF,
|
||||||
|
MediaType.IMAGE_GIF, MediaType.IMAGE_JPEG, MediaType.IMAGE_PNG)
|
||||||
|
.anyMatch(mediaType -> mediaType.includes(contentType));
|
||||||
|
}
|
||||||
|
|
||||||
private final class FeignOutputMessage implements HttpOutputMessage {
|
private final class FeignOutputMessage implements HttpOutputMessage {
|
||||||
|
|
||||||
private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||||
@@ -192,21 +240,21 @@ public class SpringEncoder implements Encoder {
|
|||||||
private final HttpHeaders httpHeaders;
|
private final HttpHeaders httpHeaders;
|
||||||
|
|
||||||
private FeignOutputMessage(RequestTemplate request) {
|
private FeignOutputMessage(RequestTemplate request) {
|
||||||
this.httpHeaders = getHttpHeaders(request.headers());
|
httpHeaders = getHttpHeaders(request.headers());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public OutputStream getBody() throws IOException {
|
public OutputStream getBody() throws IOException {
|
||||||
return this.outputStream;
|
return outputStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public HttpHeaders getHeaders() {
|
public HttpHeaders getHeaders() {
|
||||||
return this.httpHeaders;
|
return httpHeaders;
|
||||||
}
|
}
|
||||||
|
|
||||||
public ByteArrayOutputStream getOutputStream() {
|
public ByteArrayOutputStream getOutputStream() {
|
||||||
return this.outputStream;
|
return outputStream;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+23
-52
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -35,6 +35,8 @@ import feign.Feign;
|
|||||||
import feign.MethodMetadata;
|
import feign.MethodMetadata;
|
||||||
import feign.Param;
|
import feign.Param;
|
||||||
import feign.Request;
|
import feign.Request;
|
||||||
|
import org.apache.commons.logging.Log;
|
||||||
|
import org.apache.commons.logging.LogFactory;
|
||||||
|
|
||||||
import org.springframework.cloud.openfeign.AnnotatedParameterProcessor;
|
import org.springframework.cloud.openfeign.AnnotatedParameterProcessor;
|
||||||
import org.springframework.cloud.openfeign.CollectionFormat;
|
import org.springframework.cloud.openfeign.CollectionFormat;
|
||||||
@@ -78,9 +80,12 @@ import static org.springframework.core.annotation.AnnotatedElementUtils.findMerg
|
|||||||
* @author Aaron Whiteside
|
* @author Aaron Whiteside
|
||||||
* @author Artyom Romanenko
|
* @author Artyom Romanenko
|
||||||
* @author Darren Foong
|
* @author Darren Foong
|
||||||
|
* @author Ram Anaswara
|
||||||
*/
|
*/
|
||||||
public class SpringMvcContract extends Contract.BaseContract implements ResourceLoaderAware {
|
public class SpringMvcContract extends Contract.BaseContract implements ResourceLoaderAware {
|
||||||
|
|
||||||
|
private static final Log LOG = LogFactory.getLog(SpringMvcContract.class);
|
||||||
|
|
||||||
private static final String ACCEPT = "Accept";
|
private static final String ACCEPT = "Accept";
|
||||||
|
|
||||||
private static final String CONTENT_TYPE = "Content-Type";
|
private static final String CONTENT_TYPE = "Content-Type";
|
||||||
@@ -101,6 +106,8 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
|
|||||||
|
|
||||||
private ResourceLoader resourceLoader = new DefaultResourceLoader();
|
private ResourceLoader resourceLoader = new DefaultResourceLoader();
|
||||||
|
|
||||||
|
private boolean decodeSlash;
|
||||||
|
|
||||||
public SpringMvcContract() {
|
public SpringMvcContract() {
|
||||||
this(Collections.emptyList());
|
this(Collections.emptyList());
|
||||||
}
|
}
|
||||||
@@ -111,6 +118,11 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
|
|||||||
|
|
||||||
public SpringMvcContract(List<AnnotatedParameterProcessor> annotatedParameterProcessors,
|
public SpringMvcContract(List<AnnotatedParameterProcessor> annotatedParameterProcessors,
|
||||||
ConversionService conversionService) {
|
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(annotatedParameterProcessors, "Parameter processors can not be null.");
|
||||||
Assert.notNull(conversionService, "ConversionService can not be null.");
|
Assert.notNull(conversionService, "ConversionService can not be null.");
|
||||||
|
|
||||||
@@ -120,6 +132,7 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
|
|||||||
annotatedArgumentProcessors = toAnnotatedArgumentProcessorMap(processors);
|
annotatedArgumentProcessors = toAnnotatedArgumentProcessorMap(processors);
|
||||||
this.conversionService = conversionService;
|
this.conversionService = conversionService;
|
||||||
convertingExpanderFactory = new ConvertingExpanderFactory(conversionService);
|
convertingExpanderFactory = new ConvertingExpanderFactory(conversionService);
|
||||||
|
this.decodeSlash = decodeSlash;
|
||||||
}
|
}
|
||||||
|
|
||||||
private static TypeDescriptor createTypeDescriptor(Method method, int paramIndex) {
|
private static TypeDescriptor createTypeDescriptor(Method method, int paramIndex) {
|
||||||
@@ -160,44 +173,18 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
|
|||||||
|
|
||||||
@Override
|
@Override
|
||||||
protected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) {
|
protected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) {
|
||||||
if (clz.getInterfaces().length == 0) {
|
RequestMapping classAnnotation = findMergedAnnotation(clz, RequestMapping.class);
|
||||||
RequestMapping classAnnotation = findMergedAnnotation(clz, RequestMapping.class);
|
if (classAnnotation != null) {
|
||||||
if (classAnnotation != null) {
|
LOG.error("Cannot process class: " + clz.getName()
|
||||||
// Prepend path from class annotation if specified
|
+ ". @RequestMapping annotation is not allowed on @FeignClient interfaces.");
|
||||||
if (classAnnotation.value().length > 0) {
|
throw new IllegalArgumentException("@RequestMapping annotation not allowed on @FeignClient interfaces");
|
||||||
String pathValue = emptyToNull(classAnnotation.value()[0]);
|
|
||||||
pathValue = resolve(pathValue);
|
|
||||||
if (!pathValue.startsWith("/")) {
|
|
||||||
pathValue = "/" + pathValue;
|
|
||||||
}
|
|
||||||
data.template().uri(pathValue);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
|
public MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
|
||||||
processedMethods.put(Feign.configKey(targetType, method), method);
|
processedMethods.put(Feign.configKey(targetType, method), method);
|
||||||
MethodMetadata md = super.parseAndValidateMetadata(targetType, method);
|
return super.parseAndValidateMetadata(targetType, method);
|
||||||
|
|
||||||
RequestMapping classAnnotation = findMergedAnnotation(targetType, RequestMapping.class);
|
|
||||||
if (classAnnotation != null) {
|
|
||||||
// produces - use from class annotation only if method has not specified this
|
|
||||||
if (!md.template().headers().containsKey(ACCEPT)) {
|
|
||||||
parseProduces(md, method, classAnnotation);
|
|
||||||
}
|
|
||||||
|
|
||||||
// consumes -- use from class annotation only if method has not specified this
|
|
||||||
if (!md.template().headers().containsKey(CONTENT_TYPE)) {
|
|
||||||
parseConsumes(md, method, classAnnotation);
|
|
||||||
}
|
|
||||||
|
|
||||||
// headers -- class annotation is inherited to methods, always write these if
|
|
||||||
// present
|
|
||||||
parseHeaders(md, method, classAnnotation);
|
|
||||||
}
|
|
||||||
return md;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
@@ -232,6 +219,9 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
|
|||||||
pathValue = "/" + pathValue;
|
pathValue = "/" + pathValue;
|
||||||
}
|
}
|
||||||
data.template().uri(pathValue, true);
|
data.template().uri(pathValue, true);
|
||||||
|
if (data.template().decodeSlash() != decodeSlash) {
|
||||||
|
data.template().decodeSlash(decodeSlash);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -386,25 +376,6 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
|
|||||||
return false;
|
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 static class ConvertingExpanderFactory {
|
||||||
|
|
||||||
private final ConversionService conversionService;
|
private final ConversionService conversionService;
|
||||||
|
|||||||
+32
-2
@@ -3,9 +3,21 @@
|
|||||||
],
|
],
|
||||||
"properties": [
|
"properties": [
|
||||||
{
|
{
|
||||||
"name": "feign.hystrix.enabled",
|
"name": "feign.autoconfiguration.jackson.enabled",
|
||||||
"type": "java.lang.Boolean",
|
"type": "java.lang.Boolean",
|
||||||
"description": "If true, an OpenFeign client will be wrapped with a Hystrix circuit breaker.",
|
"description": "If true, PageJacksonModule and SortJacksonModule bean will be provided for Jackson page decoding.",
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "feign.circuitbreaker.enabled",
|
||||||
|
"type": "java.lang.Boolean",
|
||||||
|
"description": "If true, an OpenFeign client will be wrapped with a Spring Cloud CircuitBreaker circuit breaker.",
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "feign.circuitbreaker.group.enabled",
|
||||||
|
"type": "java.lang.Boolean",
|
||||||
|
"description": "If true, an OpenFeign client will be wrapped with a Spring Cloud CircuitBreaker circuit breaker with with group.",
|
||||||
"defaultValue": "false"
|
"defaultValue": "false"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
@@ -14,6 +26,12 @@
|
|||||||
"description": "Enables the use of the Apache HTTP Client by Feign.",
|
"description": "Enables the use of the Apache HTTP Client by Feign.",
|
||||||
"defaultValue": "true"
|
"defaultValue": "true"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"name": "feign.httpclient.hc5.enabled",
|
||||||
|
"type": "java.lang.Boolean",
|
||||||
|
"description": "Enables the use of the Apache HTTP Client 5 by Feign.",
|
||||||
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"name": "feign.okhttp.enabled",
|
"name": "feign.okhttp.enabled",
|
||||||
"type": "java.lang.Boolean",
|
"type": "java.lang.Boolean",
|
||||||
@@ -37,6 +55,18 @@
|
|||||||
"type": "java.lang.Boolean",
|
"type": "java.lang.Boolean",
|
||||||
"description": "Enables the request sent by Feign to be compressed.",
|
"description": "Enables the request sent by Feign to be compressed.",
|
||||||
"defaultValue": "false"
|
"defaultValue": "false"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "feign.metrics.enabled",
|
||||||
|
"type": "java.lang.Boolean",
|
||||||
|
"description": "Enables metrics capability for Feign.",
|
||||||
|
"defaultValue": "true"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "feign.client.refresh-enabled",
|
||||||
|
"type": "java.lang.Boolean",
|
||||||
|
"description": "Enables options value refresh capability for Feign.",
|
||||||
|
"defaultValue": "false"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
|
|||||||
+111
@@ -0,0 +1,111 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2013-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.cloud.openfeign;
|
||||||
|
|
||||||
|
import java.lang.reflect.Method;
|
||||||
|
|
||||||
|
import feign.Target;
|
||||||
|
import org.assertj.core.api.Condition;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.boot.autoconfigure.AutoConfigurations;
|
||||||
|
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
||||||
|
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||||
|
import org.springframework.cloud.openfeign.FeignAutoConfiguration.CircuitBreakerPresentFeignTargeterConfiguration.DefaultCircuitBreakerNameResolver;
|
||||||
|
import org.springframework.context.ConfigurableApplicationContext;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.mockito.Mockito.mock;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Tim Peeters
|
||||||
|
* @author Olga Maciaszek-Sharma
|
||||||
|
* @author Andrii Bohutskyi
|
||||||
|
* @author Kwangyong Kim
|
||||||
|
*/
|
||||||
|
class FeignAutoConfigurationTests {
|
||||||
|
|
||||||
|
private final ApplicationContextRunner runner = new ApplicationContextRunner()
|
||||||
|
.withConfiguration(AutoConfigurations.of(FeignAutoConfiguration.class))
|
||||||
|
.withPropertyValues("feign.httpclient.enabled=false");
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldInstantiateDefaultTargeterWhenFeignCircuitBreakerIsDisabled() {
|
||||||
|
runner.withPropertyValues("feign.circuitbreaker.enabled=false")
|
||||||
|
.run(ctx -> assertOnlyOneTargeterPresent(ctx, DefaultTargeter.class));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldInstantiateFeignCircuitBreakerTargeterWhenEnabled() {
|
||||||
|
runner.withBean(CircuitBreakerFactory.class, () -> mock(CircuitBreakerFactory.class))
|
||||||
|
.withPropertyValues("feign.circuitbreaker.enabled=true").run(ctx -> {
|
||||||
|
assertOnlyOneTargeterPresent(ctx, FeignCircuitBreakerTargeter.class);
|
||||||
|
assertThatFeignCircuitBreakerTargeterHasGroupEnabledPropertyWithValue(ctx, false);
|
||||||
|
assertThatFeignCircuitBreakerTargeterHasSameCircuitBreakerNameResolver(ctx,
|
||||||
|
DefaultCircuitBreakerNameResolver.class);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldInstantiateFeignCircuitBreakerTargeterWithEnabledGroup() {
|
||||||
|
runner.withBean(CircuitBreakerFactory.class, () -> mock(CircuitBreakerFactory.class))
|
||||||
|
.withPropertyValues("feign.circuitbreaker.enabled=true")
|
||||||
|
.withPropertyValues("feign.circuitbreaker.group.enabled=true").run(ctx -> {
|
||||||
|
assertOnlyOneTargeterPresent(ctx, FeignCircuitBreakerTargeter.class);
|
||||||
|
assertThatFeignCircuitBreakerTargeterHasGroupEnabledPropertyWithValue(ctx, true);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldInstantiateFeignCircuitBreakerTargeterWhenEnabledWithCustomCircuitBreakerNameResolver() {
|
||||||
|
runner.withBean(CircuitBreakerFactory.class, () -> mock(CircuitBreakerFactory.class))
|
||||||
|
.withBean(CircuitBreakerNameResolver.class, CustomCircuitBreakerNameResolver::new)
|
||||||
|
.withPropertyValues("feign.circuitbreaker.enabled=true").run(ctx -> {
|
||||||
|
assertOnlyOneTargeterPresent(ctx, FeignCircuitBreakerTargeter.class);
|
||||||
|
assertThatFeignCircuitBreakerTargeterHasSameCircuitBreakerNameResolver(ctx,
|
||||||
|
CustomCircuitBreakerNameResolver.class);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertOnlyOneTargeterPresent(ConfigurableApplicationContext ctx, Class<?> beanClass) {
|
||||||
|
assertThat(ctx.getBeansOfType(Targeter.class)).hasSize(1).hasValueSatisfying(new Condition<>(
|
||||||
|
beanClass::isInstance, String.format("Targeter should be an instance of %s", beanClass)));
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertThatFeignCircuitBreakerTargeterHasGroupEnabledPropertyWithValue(
|
||||||
|
ConfigurableApplicationContext ctx, boolean expectedValue) {
|
||||||
|
final FeignCircuitBreakerTargeter bean = ctx.getBean(FeignCircuitBreakerTargeter.class);
|
||||||
|
assertThat(bean).hasFieldOrPropertyWithValue("circuitBreakerGroupEnabled", expectedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
private void assertThatFeignCircuitBreakerTargeterHasSameCircuitBreakerNameResolver(
|
||||||
|
ConfigurableApplicationContext ctx, Class<?> beanClass) {
|
||||||
|
final CircuitBreakerNameResolver bean = ctx.getBean(CircuitBreakerNameResolver.class);
|
||||||
|
assertThat(bean).isExactlyInstanceOf(beanClass);
|
||||||
|
}
|
||||||
|
|
||||||
|
static class CustomCircuitBreakerNameResolver implements CircuitBreakerNameResolver {
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public String resolveCircuitBreakerName(String feignClientName, Target<?> target, Method method) {
|
||||||
|
return feignClientName + "_" + method.getName();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+20
-1
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -33,6 +33,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Matt King
|
* @author Matt King
|
||||||
|
* @author Sam Kruglov
|
||||||
*/
|
*/
|
||||||
public class FeignBuilderCustomizerTests {
|
public class FeignBuilderCustomizerTests {
|
||||||
|
|
||||||
@@ -74,6 +75,24 @@ public class FeignBuilderCustomizerTests {
|
|||||||
context.close();
|
context.close();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
public void testBuildCustomizerOrderedWithAdditional() {
|
||||||
|
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
|
||||||
|
FeignBuilderCustomizerTests.SampleConfiguration3.class);
|
||||||
|
|
||||||
|
FeignClientFactoryBean clientFactoryBean = context.getBean(FeignClientFactoryBean.class);
|
||||||
|
clientFactoryBean.addCustomizer(builder -> builder.logLevel(Logger.Level.BASIC));
|
||||||
|
clientFactoryBean.addCustomizer(Feign.Builder::doNotCloseAfterDecode);
|
||||||
|
FeignContext feignContext = context.getBean(FeignContext.class);
|
||||||
|
|
||||||
|
Feign.Builder builder = clientFactoryBean.feign(feignContext);
|
||||||
|
assertFeignBuilderField(builder, "logLevel", Logger.Level.BASIC);
|
||||||
|
assertFeignBuilderField(builder, "decode404", true);
|
||||||
|
assertFeignBuilderField(builder, "closeAfterDecode", false);
|
||||||
|
|
||||||
|
context.close();
|
||||||
|
}
|
||||||
|
|
||||||
private static FeignClientFactoryBean defaultFeignClientFactoryBean() {
|
private static FeignClientFactoryBean defaultFeignClientFactoryBean() {
|
||||||
FeignClientFactoryBean feignClientFactoryBean = new FeignClientFactoryBean();
|
FeignClientFactoryBean feignClientFactoryBean = new FeignClientFactoryBean();
|
||||||
feignClientFactoryBean.setContextId("test");
|
feignClientFactoryBean.setContextId("test");
|
||||||
|
|||||||
+15
-6
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -24,6 +24,7 @@ import java.util.Arrays;
|
|||||||
import java.util.Collections;
|
import java.util.Collections;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import feign.Feign;
|
||||||
import org.hamcrest.Matchers;
|
import org.hamcrest.Matchers;
|
||||||
import org.junit.Before;
|
import org.junit.Before;
|
||||||
import org.junit.Rule;
|
import org.junit.Rule;
|
||||||
@@ -39,6 +40,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Sven Döring
|
* @author Sven Döring
|
||||||
|
* @author Sam Kruglov
|
||||||
*/
|
*/
|
||||||
public class FeignClientBuilderTests {
|
public class FeignClientBuilderTests {
|
||||||
|
|
||||||
@@ -56,6 +58,12 @@ public class FeignClientBuilderTests {
|
|||||||
|
|
||||||
private static void assertFactoryBeanField(final FeignClientBuilder.Builder builder, final String fieldName,
|
private static void assertFactoryBeanField(final FeignClientBuilder.Builder builder, final String fieldName,
|
||||||
final Object expectedValue) {
|
final Object expectedValue) {
|
||||||
|
final Object value = getFactoryBeanField(builder, fieldName);
|
||||||
|
assertThat(value).as("Expected value for the field '" + fieldName + "':").isEqualTo(expectedValue);
|
||||||
|
}
|
||||||
|
|
||||||
|
@SuppressWarnings("unchecked")
|
||||||
|
private static <T> T getFactoryBeanField(final FeignClientBuilder.Builder builder, final String fieldName) {
|
||||||
final Field factoryBeanField = ReflectionUtils.findField(FeignClientBuilder.Builder.class,
|
final Field factoryBeanField = ReflectionUtils.findField(FeignClientBuilder.Builder.class,
|
||||||
"feignClientFactoryBean");
|
"feignClientFactoryBean");
|
||||||
ReflectionUtils.makeAccessible(factoryBeanField);
|
ReflectionUtils.makeAccessible(factoryBeanField);
|
||||||
@@ -64,8 +72,7 @@ public class FeignClientBuilderTests {
|
|||||||
|
|
||||||
final Field field = ReflectionUtils.findField(FeignClientFactoryBean.class, fieldName);
|
final Field field = ReflectionUtils.findField(FeignClientFactoryBean.class, fieldName);
|
||||||
ReflectionUtils.makeAccessible(field);
|
ReflectionUtils.makeAccessible(field);
|
||||||
final Object value = ReflectionUtils.getField(field, factoryBean);
|
return (T) ReflectionUtils.getField(field, factoryBean);
|
||||||
assertThat(value).as("Expected value for the field '" + fieldName + "':").isEqualTo(expectedValue);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@Before
|
@Before
|
||||||
@@ -80,8 +87,8 @@ public class FeignClientBuilderTests {
|
|||||||
for (final Method method : FeignClient.class.getMethods()) {
|
for (final Method method : FeignClient.class.getMethods()) {
|
||||||
methodNames.add(method.getName());
|
methodNames.add(method.getName());
|
||||||
}
|
}
|
||||||
methodNames.removeAll(Arrays.asList("annotationType", "value", "serviceId", "qualifier", "configuration",
|
methodNames.removeAll(Arrays.asList("annotationType", "value", "serviceId", "qualifier", "qualifiers",
|
||||||
"primary", "equals", "hashCode", "toString"));
|
"configuration", "primary", "equals", "hashCode", "toString"));
|
||||||
Collections.sort(methodNames);
|
Collections.sort(methodNames);
|
||||||
// If this safety check fails the Builder has to be updated.
|
// If this safety check fails the Builder has to be updated.
|
||||||
// (1) Either a field was removed from the FeignClient annotation and so it has to
|
// (1) Either a field was removed from the FeignClient annotation and so it has to
|
||||||
@@ -136,7 +143,7 @@ public class FeignClientBuilderTests {
|
|||||||
// when:
|
// when:
|
||||||
final FeignClientBuilder.Builder builder = this.feignClientBuilder
|
final FeignClientBuilder.Builder builder = this.feignClientBuilder
|
||||||
.forType(TestFeignClient.class, new FeignClientFactoryBean(), "TestClient").decode404(true)
|
.forType(TestFeignClient.class, new FeignClientFactoryBean(), "TestClient").decode404(true)
|
||||||
.path("Path/").url("Url/").contextId("TestContext");
|
.path("Path/").url("Url/").contextId("TestContext").customize(Feign.Builder::doNotCloseAfterDecode);
|
||||||
|
|
||||||
// then:
|
// then:
|
||||||
assertFactoryBeanField(builder, "applicationContext", this.applicationContext);
|
assertFactoryBeanField(builder, "applicationContext", this.applicationContext);
|
||||||
@@ -148,6 +155,8 @@ public class FeignClientBuilderTests {
|
|||||||
assertFactoryBeanField(builder, "url", "http://Url/");
|
assertFactoryBeanField(builder, "url", "http://Url/");
|
||||||
assertFactoryBeanField(builder, "path", "/Path");
|
assertFactoryBeanField(builder, "path", "/Path");
|
||||||
assertFactoryBeanField(builder, "decode404", true);
|
assertFactoryBeanField(builder, "decode404", true);
|
||||||
|
List<FeignBuilderCustomizer> additionalCustomizers = getFactoryBeanField(builder, "additionalCustomizers");
|
||||||
|
assertThat(additionalCustomizers).hasSize(1);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
|||||||
+140
@@ -0,0 +1,140 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.cloud.openfeign;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.List;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import feign.Capability;
|
||||||
|
import feign.Contract;
|
||||||
|
import feign.ExceptionPropagationPolicy;
|
||||||
|
import feign.Logger;
|
||||||
|
import feign.RequestInterceptor;
|
||||||
|
import feign.Retryer;
|
||||||
|
import feign.codec.Decoder;
|
||||||
|
import feign.codec.Encoder;
|
||||||
|
import feign.codec.ErrorDecoder;
|
||||||
|
import org.assertj.core.util.Lists;
|
||||||
|
import org.assertj.core.util.Maps;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsAndHashCodeConsistency;
|
||||||
|
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsConsistency;
|
||||||
|
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsReflexivity;
|
||||||
|
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsSymmetricity;
|
||||||
|
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsTransitivity;
|
||||||
|
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertHashCodeConsistency;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Jonatan Ivanov
|
||||||
|
*/
|
||||||
|
class FeignClientConfigurationTests {
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldDefaultToValuesWhenFieldsNotSet() {
|
||||||
|
FeignClientProperties.FeignClientConfiguration config = new FeignClientProperties.FeignClientConfiguration();
|
||||||
|
|
||||||
|
assertThat(config.getLoggerLevel()).isNull();
|
||||||
|
assertThat(config.getConnectTimeout()).isNull();
|
||||||
|
assertThat(config.getReadTimeout()).isNull();
|
||||||
|
assertThat(config.getRetryer()).isNull();
|
||||||
|
assertThat(config.getErrorDecoder()).isNull();
|
||||||
|
assertThat(config.getRequestInterceptors()).isNull();
|
||||||
|
assertThat(config.getDefaultRequestHeaders()).isNull();
|
||||||
|
assertThat(config.getDefaultQueryParameters()).isNull();
|
||||||
|
assertThat(config.getDecode404()).isNull();
|
||||||
|
assertThat(config.getDecoder()).isNull();
|
||||||
|
assertThat(config.getEncoder()).isNull();
|
||||||
|
assertThat(config.getContract()).isNull();
|
||||||
|
assertThat(config.getExceptionPropagationPolicy()).isNull();
|
||||||
|
assertThat(config.getCapabilities()).isNull();
|
||||||
|
assertThat(config.getMetrics()).isNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void shouldReturnValuesWhenSet() {
|
||||||
|
FeignClientProperties.FeignClientConfiguration config = new FeignClientProperties.FeignClientConfiguration();
|
||||||
|
config.setLoggerLevel(Logger.Level.FULL);
|
||||||
|
config.setConnectTimeout(21);
|
||||||
|
config.setReadTimeout(42);
|
||||||
|
config.setRetryer(Retryer.class);
|
||||||
|
config.setErrorDecoder(ErrorDecoder.class);
|
||||||
|
List<Class<RequestInterceptor>> requestInterceptors = Lists.list(RequestInterceptor.class);
|
||||||
|
config.setRequestInterceptors(requestInterceptors);
|
||||||
|
Map<String, Collection<String>> defaultRequestHeaders = Maps.newHashMap("default", Lists.emptyList());
|
||||||
|
config.setDefaultRequestHeaders(defaultRequestHeaders);
|
||||||
|
Map<String, Collection<String>> defaultQueryParameters = Maps.newHashMap("default", Lists.emptyList());
|
||||||
|
config.setDefaultQueryParameters(defaultQueryParameters);
|
||||||
|
config.setDecode404(true);
|
||||||
|
config.setDecoder(Decoder.class);
|
||||||
|
config.setEncoder(Encoder.class);
|
||||||
|
config.setContract(Contract.class);
|
||||||
|
config.setExceptionPropagationPolicy(ExceptionPropagationPolicy.UNWRAP);
|
||||||
|
List<Class<Capability>> capabilities = Lists.list(Capability.class);
|
||||||
|
config.setCapabilities(capabilities);
|
||||||
|
FeignClientProperties.MetricsProperties metrics = new FeignClientProperties.MetricsProperties();
|
||||||
|
config.setMetrics(metrics);
|
||||||
|
|
||||||
|
assertThat(config.getLoggerLevel()).isSameAs(Logger.Level.FULL);
|
||||||
|
assertThat(config.getConnectTimeout()).isEqualTo(21);
|
||||||
|
assertThat(config.getReadTimeout()).isEqualTo(42);
|
||||||
|
assertThat(config.getRetryer()).isSameAs(Retryer.class);
|
||||||
|
assertThat(config.getErrorDecoder()).isSameAs(ErrorDecoder.class);
|
||||||
|
assertThat(config.getRequestInterceptors()).isSameAs(requestInterceptors);
|
||||||
|
assertThat(config.getDefaultRequestHeaders()).isSameAs(defaultRequestHeaders);
|
||||||
|
assertThat(config.getDefaultQueryParameters()).isSameAs(defaultQueryParameters);
|
||||||
|
assertThat(config.getDecode404()).isTrue();
|
||||||
|
assertThat(config.getDecoder()).isSameAs(Decoder.class);
|
||||||
|
assertThat(config.getEncoder()).isSameAs(Encoder.class);
|
||||||
|
assertThat(config.getContract()).isSameAs(Contract.class);
|
||||||
|
assertThat(config.getExceptionPropagationPolicy()).isSameAs(ExceptionPropagationPolicy.UNWRAP);
|
||||||
|
assertThat(config.getCapabilities()).isSameAs(capabilities);
|
||||||
|
assertThat(config.getMetrics()).isSameAs(metrics);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sanity-checks equals and hashCode contracts but does not check every variation of
|
||||||
|
* the fields.
|
||||||
|
*/
|
||||||
|
@Test
|
||||||
|
void shouldHaveSomewhatValidEqualsAndHashCode() {
|
||||||
|
FeignClientProperties.FeignClientConfiguration configOne = new FeignClientProperties.FeignClientConfiguration();
|
||||||
|
FeignClientProperties.FeignClientConfiguration configTwo = new FeignClientProperties.FeignClientConfiguration();
|
||||||
|
FeignClientProperties.FeignClientConfiguration configThree = new FeignClientProperties.FeignClientConfiguration();
|
||||||
|
FeignClientProperties.FeignClientConfiguration differentConfig = new FeignClientProperties.FeignClientConfiguration();
|
||||||
|
differentConfig.setDecode404(true);
|
||||||
|
|
||||||
|
assertEqualsReflexivity(configOne);
|
||||||
|
|
||||||
|
assertEqualsSymmetricity(configOne, configTwo);
|
||||||
|
assertEqualsSymmetricity(configOne, differentConfig);
|
||||||
|
assertEqualsSymmetricity(configOne, 42);
|
||||||
|
|
||||||
|
assertEqualsTransitivity(configOne, configTwo, configThree);
|
||||||
|
|
||||||
|
assertEqualsConsistency(configOne, configTwo);
|
||||||
|
assertEqualsConsistency(configOne, differentConfig);
|
||||||
|
assertEqualsConsistency(configOne, 42);
|
||||||
|
assertEqualsConsistency(configOne, null);
|
||||||
|
|
||||||
|
assertHashCodeConsistency(configOne);
|
||||||
|
assertEqualsAndHashCodeConsistency(configOne, configTwo);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+120
@@ -0,0 +1,120 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.cloud.openfeign;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import feign.Capability;
|
||||||
|
import feign.Contract;
|
||||||
|
import feign.RequestLine;
|
||||||
|
import feign.micrometer.MicrometerCapability;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.test.annotation.DirtiesContext;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Jonatan Ivanov
|
||||||
|
*/
|
||||||
|
@DirtiesContext
|
||||||
|
@ActiveProfiles("no-foo-metrics")
|
||||||
|
@SpringBootTest(classes = FeignClientDisabledClientLevelFeaturesTests.TestConfiguration.class)
|
||||||
|
class FeignClientDisabledClientLevelFeaturesTests {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FeignContext context;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FooClient foo;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BarClient bar;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void clientsAvailable() {
|
||||||
|
assertThat(foo).isNotNull();
|
||||||
|
assertThat(bar).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void capabilitiesShouldNotBeAvailableWhenDisabled() {
|
||||||
|
assertThat(context.getInstance("foo", MicrometerCapability.class)).isNull();
|
||||||
|
assertThat(context.getInstances("foo", Capability.class)).isEmpty();
|
||||||
|
|
||||||
|
assertThat(context.getInstance("bar", MicrometerCapability.class)).isNotNull();
|
||||||
|
Map<String, Capability> barCapabilities = context.getInstances("bar", Capability.class);
|
||||||
|
assertThat(barCapabilities).hasSize(2);
|
||||||
|
assertThat(barCapabilities.get("micrometerCapability")).isExactlyInstanceOf(MicrometerCapability.class);
|
||||||
|
assertThat(barCapabilities.get("noOpCapability")).isExactlyInstanceOf(NoOpCapability.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FeignClient(name = "foo", url = "https://foo", configuration = FooConfiguration.class)
|
||||||
|
interface FooClient {
|
||||||
|
|
||||||
|
@RequestLine("GET /")
|
||||||
|
String get();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@FeignClient(name = "bar", url = "https://bar", configuration = BarConfiguration.class)
|
||||||
|
interface BarClient {
|
||||||
|
|
||||||
|
@GetMapping("/")
|
||||||
|
String get();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@EnableAutoConfiguration
|
||||||
|
@EnableConfigurationProperties(FeignClientProperties.class)
|
||||||
|
@EnableFeignClients(clients = { FooClient.class, BarClient.class })
|
||||||
|
protected static class TestConfiguration {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class FooConfiguration {
|
||||||
|
|
||||||
|
@Bean // if the feign configuration empty, the context is not able to start
|
||||||
|
public Contract feignContract() {
|
||||||
|
return new Contract.Default();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class BarConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Capability noOpCapability() {
|
||||||
|
return new NoOpCapability();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class NoOpCapability implements Capability {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+119
@@ -0,0 +1,119 @@
|
|||||||
|
/*
|
||||||
|
* Copyright 2021-2022 the original author or authors.
|
||||||
|
*
|
||||||
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
* you may not use this file except in compliance with the License.
|
||||||
|
* You may obtain a copy of the License at
|
||||||
|
*
|
||||||
|
* https://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
*
|
||||||
|
* Unless required by applicable law or agreed to in writing, software
|
||||||
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
* See the License for the specific language governing permissions and
|
||||||
|
* limitations under the License.
|
||||||
|
*/
|
||||||
|
|
||||||
|
package org.springframework.cloud.openfeign;
|
||||||
|
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
import feign.Capability;
|
||||||
|
import feign.Contract;
|
||||||
|
import feign.RequestLine;
|
||||||
|
import feign.micrometer.MicrometerCapability;
|
||||||
|
import org.junit.jupiter.api.Test;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
|
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||||
|
import org.springframework.boot.test.context.SpringBootTest;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.test.annotation.DirtiesContext;
|
||||||
|
import org.springframework.test.context.ActiveProfiles;
|
||||||
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
||||||
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* @author Jonatan Ivanov
|
||||||
|
*/
|
||||||
|
@DirtiesContext
|
||||||
|
@ActiveProfiles("no-metrics")
|
||||||
|
@SpringBootTest(classes = FeignClientDisabledFeaturesTests.TestConfiguration.class)
|
||||||
|
class FeignClientDisabledFeaturesTests {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FeignContext context;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private FooClient foo;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private BarClient bar;
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void clientsAvailable() {
|
||||||
|
assertThat(foo).isNotNull();
|
||||||
|
assertThat(bar).isNotNull();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Test
|
||||||
|
void capabilitiesShouldNotBeAvailable() {
|
||||||
|
assertThat(context.getInstance("foo", MicrometerCapability.class)).isNull();
|
||||||
|
assertThat(context.getInstances("foo", Capability.class)).isEmpty();
|
||||||
|
|
||||||
|
assertThat(context.getInstance("bar", MicrometerCapability.class)).isNull();
|
||||||
|
Map<String, Capability> barCapabilities = context.getInstances("bar", Capability.class);
|
||||||
|
assertThat(barCapabilities).hasSize(1);
|
||||||
|
assertThat(barCapabilities.get("noOpCapability")).isExactlyInstanceOf(NoOpCapability.class);
|
||||||
|
}
|
||||||
|
|
||||||
|
@FeignClient(name = "foo", url = "https://foo", configuration = FooConfiguration.class)
|
||||||
|
interface FooClient {
|
||||||
|
|
||||||
|
@RequestLine("GET /")
|
||||||
|
String get();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@FeignClient(name = "bar", url = "https://bar", configuration = BarConfiguration.class)
|
||||||
|
interface BarClient {
|
||||||
|
|
||||||
|
@GetMapping("/")
|
||||||
|
String get();
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@Configuration(proxyBeanMethods = false)
|
||||||
|
@EnableAutoConfiguration
|
||||||
|
@EnableConfigurationProperties(FeignClientProperties.class)
|
||||||
|
@EnableFeignClients(clients = { FooClient.class, BarClient.class })
|
||||||
|
protected static class TestConfiguration {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class FooConfiguration {
|
||||||
|
|
||||||
|
@Bean // if the feign configuration empty, the context is not able to start
|
||||||
|
public Contract feignContract() {
|
||||||
|
return new Contract.Default();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
public static class BarConfiguration {
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public Capability noOpCapability() {
|
||||||
|
return new NoOpCapability();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
private static class NoOpCapability implements Capability {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
+11
-9
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2020-2020 the original author or authors.
|
* Copyright 2020-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -25,8 +25,9 @@ import feign.InvocationHandlerFactory;
|
|||||||
import feign.RequestLine;
|
import feign.RequestLine;
|
||||||
import feign.Response;
|
import feign.Response;
|
||||||
import feign.codec.ErrorDecoder;
|
import feign.codec.ErrorDecoder;
|
||||||
import org.junit.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
import org.junit.runner.RunWith;
|
import org.junit.jupiter.api.condition.DisabledForJreRange;
|
||||||
|
import org.junit.jupiter.api.condition.JRE;
|
||||||
|
|
||||||
import org.springframework.beans.factory.annotation.Autowired;
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||||
@@ -35,17 +36,15 @@ import org.springframework.cloud.openfeign.support.SpringMvcContract;
|
|||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.test.annotation.DirtiesContext;
|
import org.springframework.test.annotation.DirtiesContext;
|
||||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
|
||||||
import org.springframework.test.util.ReflectionTestUtils;
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
import org.springframework.web.bind.annotation.RequestMethod;
|
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Michael Cramer
|
* @author Michael Cramer
|
||||||
|
* @author Jonatan Ivanov
|
||||||
*/
|
*/
|
||||||
@RunWith(SpringJUnit4ClassRunner.class)
|
|
||||||
@SpringBootTest(classes = FeignClientErrorDecoderTests.TestConfiguration.class)
|
@SpringBootTest(classes = FeignClientErrorDecoderTests.TestConfiguration.class)
|
||||||
@DirtiesContext
|
@DirtiesContext
|
||||||
public class FeignClientErrorDecoderTests {
|
public class FeignClientErrorDecoderTests {
|
||||||
@@ -72,12 +71,14 @@ public class FeignClientErrorDecoderTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@DisabledForJreRange(min = JRE.JAVA_16)
|
||||||
public void useConfiguredErrorDecoderWhenAlsoErrorDecoderFactoryIsAvailable() {
|
public void useConfiguredErrorDecoderWhenAlsoErrorDecoderFactoryIsAvailable() {
|
||||||
Object errorDecoder = getErrorDecoderFromClient(this.foo);
|
Object errorDecoder = getErrorDecoderFromClient(this.foo);
|
||||||
assertThat(errorDecoder).isInstanceOf(ErrorDecoder.Default.class);
|
assertThat(errorDecoder).isInstanceOf(ErrorDecoder.Default.class);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@DisabledForJreRange(min = JRE.JAVA_16)
|
||||||
public void useErrorDecoderFromErrorDecoderFactory() {
|
public void useErrorDecoderFromErrorDecoderFactory() {
|
||||||
Object errorDecoder = getErrorDecoderFromClient(this.bar);
|
Object errorDecoder = getErrorDecoderFromClient(this.bar);
|
||||||
assertThat(errorDecoder).isInstanceOf(ErrorDecoderImpl.class);
|
assertThat(errorDecoder).isInstanceOf(ErrorDecoderImpl.class);
|
||||||
@@ -85,7 +86,8 @@ public class FeignClientErrorDecoderTests {
|
|||||||
|
|
||||||
@SuppressWarnings({ "unchecked", "ConstantConditions" })
|
@SuppressWarnings({ "unchecked", "ConstantConditions" })
|
||||||
private Object getErrorDecoderFromClient(final Object client) {
|
private Object getErrorDecoderFromClient(final Object client) {
|
||||||
Object invocationHandler = ReflectionTestUtils.getField(client, "h");
|
Object invocationHandlerLambda = ReflectionTestUtils.getField(client, "h");
|
||||||
|
Object invocationHandler = ReflectionTestUtils.getField(invocationHandlerLambda, "arg$2");
|
||||||
Map<Method, InvocationHandlerFactory.MethodHandler> dispatch = (Map<Method, InvocationHandlerFactory.MethodHandler>) ReflectionTestUtils
|
Map<Method, InvocationHandlerFactory.MethodHandler> dispatch = (Map<Method, InvocationHandlerFactory.MethodHandler>) ReflectionTestUtils
|
||||||
.getField(invocationHandler, "dispatch");
|
.getField(invocationHandler, "dispatch");
|
||||||
Method key = new ArrayList<>(dispatch.keySet()).get(0);
|
Method key = new ArrayList<>(dispatch.keySet()).get(0);
|
||||||
@@ -130,7 +132,7 @@ public class FeignClientErrorDecoderTests {
|
|||||||
@FeignClient(name = "bar", url = "http://bar", configuration = BarConfiguration.class)
|
@FeignClient(name = "bar", url = "http://bar", configuration = BarConfiguration.class)
|
||||||
interface BarClient {
|
interface BarClient {
|
||||||
|
|
||||||
@RequestMapping(value = "/", method = RequestMethod.GET)
|
@GetMapping("/")
|
||||||
String get();
|
String get();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+8
-6
@@ -1,5 +1,5 @@
|
|||||||
/*
|
/*
|
||||||
* Copyright 2013-2020 the original author or authors.
|
* Copyright 2013-2022 the original author or authors.
|
||||||
*
|
*
|
||||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
* you may not use this file except in compliance with the License.
|
* you may not use this file except in compliance with the License.
|
||||||
@@ -25,11 +25,13 @@ import java.util.Map;
|
|||||||
|
|
||||||
import feign.Client;
|
import feign.Client;
|
||||||
import feign.InvocationHandlerFactory;
|
import feign.InvocationHandlerFactory;
|
||||||
import org.junit.Test;
|
import org.junit.jupiter.api.Test;
|
||||||
|
import org.junit.jupiter.api.condition.DisabledForJreRange;
|
||||||
|
import org.junit.jupiter.api.condition.JRE;
|
||||||
|
|
||||||
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
|
import org.springframework.boot.test.context.assertj.AssertableApplicationContext;
|
||||||
import org.springframework.boot.test.context.runner.ApplicationContextRunner;
|
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.blocking.client.BlockingLoadBalancerClient;
|
||||||
import org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration;
|
import org.springframework.cloud.loadbalancer.config.LoadBalancerAutoConfiguration;
|
||||||
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||||
@@ -37,10 +39,9 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
|
|||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
import org.springframework.test.util.ReflectionTestUtils;
|
import org.springframework.test.util.ReflectionTestUtils;
|
||||||
import org.springframework.web.bind.annotation.RequestMapping;
|
import org.springframework.web.bind.annotation.GetMapping;
|
||||||
|
|
||||||
import static org.assertj.core.api.Assertions.assertThat;
|
import static org.assertj.core.api.Assertions.assertThat;
|
||||||
import static org.springframework.web.bind.annotation.RequestMethod.GET;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @author Spencer Gibb
|
* @author Spencer Gibb
|
||||||
@@ -66,6 +67,7 @@ public class FeignClientFactoryTests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
@Test
|
@Test
|
||||||
|
@DisabledForJreRange(min = JRE.JAVA_16)
|
||||||
public void shouldRedirectToDelegateWhenUrlSet() {
|
public void shouldRedirectToDelegateWhenUrlSet() {
|
||||||
new ApplicationContextRunner().withUserConfiguration(TestConfig.class).run(this::defaultClientUsed);
|
new ApplicationContextRunner().withUserConfiguration(TestConfig.class).run(this::defaultClientUsed);
|
||||||
}
|
}
|
||||||
@@ -87,7 +89,7 @@ public class FeignClientFactoryTests {
|
|||||||
|
|
||||||
interface TestType {
|
interface TestType {
|
||||||
|
|
||||||
@RequestMapping(value = "/", method = GET)
|
@GetMapping("/")
|
||||||
String hello();
|
String hello();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user