Compare commits
172
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bba08931d | ||
|
|
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 | ||
|
|
e32b10b60f |
@@ -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,
|
||||
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
|
||||
follow the guidelines below.
|
||||
|
||||
@@ -17,7 +17,7 @@ given the ability to merge pull requests.
|
||||
|
||||
## Code of Conduct
|
||||
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.
|
||||
|
||||
## 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
|
||||
you can import formatter settings using 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.
|
||||
* 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
|
||||
@@ -38,7 +38,7 @@ added after the original pull request but before a merge.
|
||||
than cosmetic changes).
|
||||
* 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.
|
||||
* 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).
|
||||
* 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
|
||||
|
||||
@@ -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
|
||||
+8
-18
@@ -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"]
|
||||
|
||||
@@ -66,23 +66,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
|
||||
source control.
|
||||
|
||||
For hints on how to build the project look in `.travis.yml` if there
|
||||
is one. There should be a "script" and maybe "install" command. Also
|
||||
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 (i.e. Redis) for testing generally
|
||||
require that a local instance of [Docker](https://www.docker.com/get-started) is installed and running.
|
||||
|
||||
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
|
||||
|
||||
@@ -316,3 +302,7 @@ Go to `File` -> `Settings` -> `Other settings` -> `Checkstyle`. There click on t
|
||||
- `checkstyle.additional.suppressions.file` - this variable corresponds to suppressions in your local project. E.g. you're working on `spring-cloud-contract`. Then point to the `project-root/src/checkstyle/checkstyle-suppressions.xml` folder. Example for `spring-cloud-contract` would be: `/home/username/spring-cloud-contract/src/checkstyle/checkstyle-suppressions.xml`.
|
||||
|
||||
IMPORTANT: Remember to set the `Scan Scope` to `All sources` since we apply checkstyle rules for production and test sources.
|
||||
|
||||
== License
|
||||
|
||||
The project license file is available https://raw.githubusercontent.com/spring-cloud/spring-cloud-openfeign/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>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-openfeign</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<version>3.0.7-SNAPSHOT</version>
|
||||
</parent>
|
||||
<artifactId>spring-cloud-openfeign-docs</artifactId>
|
||||
<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"]
|
||||
|
||||
@@ -14,8 +14,12 @@ include::intro.adoc[]
|
||||
|
||||
== 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
|
||||
|
||||
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].
|
||||
|
||||
@@ -1,25 +1,35 @@
|
||||
|===
|
||||
|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.decode-slash | `true` | Feign clients do not encode slash `/` characters by default. To change this behavior, set the `decodeSlash` to `false`.
|
||||
|feign.client.default-config | `default` |
|
||||
|feign.client.default-to-properties | `true` |
|
||||
|feign.client.refresh-enabled | `false` | Enables options value refresh capability for Feign.
|
||||
|feign.compression.request.enabled | `false` | Enables the request sent by Feign to be compressed.
|
||||
|feign.compression.request.mime-types | `[text/xml, application/xml, application/json]` | The list of supported mime types.
|
||||
|feign.compression.request.min-request-size | `2048` | The minimum threshold content size.
|
||||
|feign.compression.response.enabled | `false` | Enables the response from Feign to be compressed.
|
||||
|feign.compression.response.useGzipDecoder | `false` | Enables the default gzip decoder to be used.
|
||||
|feign.encoder.charset-from-content-type | `false` | Indicates whether the charset should be derived from the {@code Content-Type} header.
|
||||
|feign.httpclient.connection-timeout | `2000` |
|
||||
|feign.httpclient.connection-timer-repeat | `3000` |
|
||||
|feign.httpclient.disable-ssl-validation | `false` |
|
||||
|feign.httpclient.enabled | `true` | Enables the use of the Apache HTTP Client by Feign.
|
||||
|feign.httpclient.follow-redirects | `true` |
|
||||
|feign.httpclient.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.metrics.enabled | `true` | Enables metrics capability for 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() {
|
||||
# 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
|
||||
# If there is a branch already passed will reuse it - otherwise will try to find it
|
||||
CURRENT_BRANCH=${BRANCH}
|
||||
@@ -134,8 +134,8 @@ function add_docs_from_target() {
|
||||
|
||||
# Copies the docs by using the retrieved properties from Maven build
|
||||
function copy_docs_for_current_version() {
|
||||
if [[ "${CURRENT_BRANCH}" == "master" ]] ; then
|
||||
echo -e "Current branch is master - will copy the current docs only to the root folder"
|
||||
if [[ "${CURRENT_BRANCH}" == "main" ]] ; then
|
||||
echo -e "Current branch is main - will copy the current docs only to the root folder"
|
||||
for f in docs/target/generated-docs/*; do
|
||||
file=${f#docs/target/generated-docs/*}
|
||||
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"
|
||||
|
||||
# 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!
|
||||
###################################################################
|
||||
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:
|
||||
|
||||
- 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
|
||||
and docs are copied there
|
||||
- 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
|
||||
stash_changes
|
||||
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.
|
||||
Feign also supports pluggable encoders and decoders.
|
||||
Spring Cloud adds support for Spring MVC annotations and for using the same `HttpMessageConverters` used by default in Spring Web.
|
||||
Spring Cloud integrates Eureka, as well as Spring Cloud LoadBalancer to provide a load-balanced http client when using Feign.
|
||||
Spring Cloud integrates Eureka, Spring Cloud CircuitBreaker, as well as Spring Cloud LoadBalancer to provide a load-balanced http client when using Feign.
|
||||
|
||||
[[netflix-feign-starter]]
|
||||
=== How to Include Feign
|
||||
@@ -32,9 +32,9 @@ Example spring boot app
|
||||
@EnableFeignClients
|
||||
public class Application {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(Application.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
@@ -44,32 +44,37 @@ public class Application {
|
||||
----
|
||||
@FeignClient("stores")
|
||||
public interface StoreClient {
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/stores")
|
||||
List<Store> getStores();
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/stores")
|
||||
List<Store> getStores();
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/stores")
|
||||
Page<Store> getStores(Pageable pageable);
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/stores")
|
||||
Page<Store> getStores(Pageable pageable);
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
|
||||
Store update(@PathVariable("storeId") Long storeId, Store store);
|
||||
@RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
|
||||
Store update(@PathVariable("storeId") Long storeId, Store store);
|
||||
}
|
||||
----
|
||||
|
||||
In the `@FeignClient` annotation the String value ("stores" above) is an arbitrary client name, which is used to create a https://github.com/spring-cloud/spring-cloud-commons/blob/master/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/blocking/client/BlockingLoadBalancerClient.java[Spring Cloud LoadBalancer client].
|
||||
In the `@FeignClient` annotation the String value ("stores" above) is an arbitrary client name, which is used to create 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
|
||||
(absolute value or just a hostname). The name of the bean in the
|
||||
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.
|
||||
|
||||
The load-balancer client above will want to discover the physical addresses
|
||||
for the "stores" service. If your application is a Eureka client then
|
||||
it will resolve the service in the Eureka service registry. If you
|
||||
don't want to use Eureka, you can simply configure a list of servers
|
||||
in your external configuration using https://cloud.spring.io/spring-cloud-static/spring-cloud-commons/current/reference/html/#simplediscoveryclient[`SimpleDiscoveryClient`].
|
||||
don't want to use Eureka, you can configure a list of servers
|
||||
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]]
|
||||
=== Overriding Feign Defaults
|
||||
|
||||
@@ -84,7 +89,7 @@ Spring Cloud lets you take full control of the feign client by declaring additio
|
||||
----
|
||||
@FeignClient(name = "stores", configuration = FooConfiguration.class)
|
||||
public interface StoreClient {
|
||||
//..
|
||||
//..
|
||||
}
|
||||
----
|
||||
|
||||
@@ -104,7 +109,7 @@ Placeholders are supported in the `name` and `url` attributes.
|
||||
----
|
||||
@FeignClient(name = "${feign.name}", url = "${feign.url}")
|
||||
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`)
|
||||
* `Encoder` feignEncoder: `SpringEncoder`
|
||||
* `Logger` feignLogger: `Slf4jLogger`
|
||||
* `MicrometerCapability` micrometerCapability: If `feign-micrometer` is on the classpath and `MeterRegistry` is available
|
||||
* `Contract` feignContract: `SpringMvcContract`
|
||||
* `Feign.Builder` feignBuilder: `FeignCircuitBreaker.Builder`
|
||||
* `Client` feignClient: if Spring Cloud LoadBalancer is in the classpath, `FeignBlockingLoadBalancerClient` is used.
|
||||
If none of them is in the classpath, the default feign client is used.
|
||||
* `Client` feignClient: If Spring Cloud LoadBalancer is on the classpath, `FeignBlockingLoadBalancerClient` is used.
|
||||
If none of them is on the classpath, the default feign client is used.
|
||||
|
||||
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.
|
||||
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.
|
||||
The OkHttpClient and ApacheHttpClient and ApacheHC5 feign clients can be used by setting `feign.okhttp.enabled` or `feign.httpclient.enabled` or `feign.httpclient.hc5.enabled` to `true`, respectively, and having them on the classpath.
|
||||
You can customize the HTTP client used by providing a bean of either `org.apache.http.impl.client.CloseableHttpClient` when using Apache or `okhttp3.OkHttpClient` when using OK HTTP or `org.apache.hc.client5.http.impl.classic.CloseableHttpClient` when using Apache HC5.
|
||||
|
||||
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>`
|
||||
* `SetterFactory`
|
||||
* `QueryMapEncoder`
|
||||
* `Capability` (`MicrometerCapability` is provided by default)
|
||||
|
||||
A bean of `Retryer.NEVER_RETRY` with the type `Retryer` is created by default, which will disable retrying.
|
||||
Notice this retrying behavior is different from the Feign default one, where it will automatically retry IOExceptions,
|
||||
@@ -143,15 +150,15 @@ Creating a bean of one of those type and placing it in a `@FeignClient` configur
|
||||
----
|
||||
@Configuration
|
||||
public class FooConfiguration {
|
||||
@Bean
|
||||
public Contract feignContract() {
|
||||
return new feign.Contract.Default();
|
||||
}
|
||||
@Bean
|
||||
public Contract feignContract() {
|
||||
return new feign.Contract.Default();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
|
||||
return new BasicAuthRequestInterceptor("user", "password");
|
||||
}
|
||||
@Bean
|
||||
public BasicAuthRequestInterceptor basicAuthRequestInterceptor() {
|
||||
return new BasicAuthRequestInterceptor("user", "password");
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
@@ -163,25 +170,29 @@ application.yml
|
||||
[source,yaml]
|
||||
----
|
||||
feign:
|
||||
client:
|
||||
config:
|
||||
feignName:
|
||||
connectTimeout: 5000
|
||||
readTimeout: 5000
|
||||
loggerLevel: full
|
||||
errorDecoder: com.example.SimpleErrorDecoder
|
||||
retryer: com.example.SimpleRetryer
|
||||
defaultQueryParameters:
|
||||
query: queryValue
|
||||
defaultRequestHeaders:
|
||||
header: headerValue
|
||||
requestInterceptors:
|
||||
- com.example.FooRequestInterceptor
|
||||
- com.example.BarRequestInterceptor
|
||||
decode404: false
|
||||
encoder: com.example.SimpleEncoder
|
||||
decoder: com.example.SimpleDecoder
|
||||
contract: com.example.SimpleContract
|
||||
client:
|
||||
config:
|
||||
feignName:
|
||||
connectTimeout: 5000
|
||||
readTimeout: 5000
|
||||
loggerLevel: full
|
||||
errorDecoder: com.example.SimpleErrorDecoder
|
||||
retryer: com.example.SimpleRetryer
|
||||
defaultQueryParameters:
|
||||
query: queryValue
|
||||
defaultRequestHeaders:
|
||||
header: headerValue
|
||||
requestInterceptors:
|
||||
- com.example.FooRequestInterceptor
|
||||
- com.example.BarRequestInterceptor
|
||||
decode404: false
|
||||
encoder: com.example.SimpleEncoder
|
||||
decoder: com.example.SimpleDecoder
|
||||
contract: com.example.SimpleContract
|
||||
capabilities:
|
||||
- com.example.FooCapability
|
||||
- com.example.BarCapability
|
||||
metrics.enabled: false
|
||||
----
|
||||
|
||||
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.
|
||||
@@ -194,12 +205,12 @@ application.yml
|
||||
[source,yaml]
|
||||
----
|
||||
feign:
|
||||
client:
|
||||
config:
|
||||
default:
|
||||
connectTimeout: 5000
|
||||
readTimeout: 5000
|
||||
loggerLevel: basic
|
||||
client:
|
||||
config:
|
||||
default:
|
||||
connectTimeout: 5000
|
||||
readTimeout: 5000
|
||||
loggerLevel: basic
|
||||
----
|
||||
|
||||
If we create both `@Configuration` bean and configuration properties, configuration properties will win.
|
||||
@@ -215,7 +226,7 @@ collision of these configuration beans.
|
||||
----
|
||||
@FeignClient(contextId = "fooClient", name = "stores", configuration = FooConfiguration.class)
|
||||
public interface FooClient {
|
||||
//..
|
||||
//..
|
||||
}
|
||||
----
|
||||
|
||||
@@ -223,7 +234,7 @@ public interface FooClient {
|
||||
----
|
||||
@FeignClient(contextId = "barClient", name = "stores", configuration = BarConfiguration.class)
|
||||
public interface BarClient {
|
||||
//..
|
||||
//..
|
||||
}
|
||||
----
|
||||
|
||||
@@ -252,6 +263,12 @@ 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
|
||||
|
||||
@@ -279,12 +296,13 @@ class FooController {
|
||||
|
||||
private FooClient adminClient;
|
||||
|
||||
@Autowired
|
||||
public FooController(Decoder decoder, Encoder encoder, Client client, Contract contract) {
|
||||
@Autowired
|
||||
public FooController(Client client, Encoder encoder, Decoder decoder, Contract contract, MicrometerCapability micrometerCapability) {
|
||||
this.fooClient = Feign.builder().client(client)
|
||||
.encoder(encoder)
|
||||
.decoder(decoder)
|
||||
.contract(contract)
|
||||
.addCapability(micrometerCapability)
|
||||
.requestInterceptor(new BasicAuthRequestInterceptor("user", "user"))
|
||||
.target(FooClient.class, "https://PROD-SVC");
|
||||
|
||||
@@ -292,9 +310,10 @@ class FooController {
|
||||
.encoder(encoder)
|
||||
.decoder(decoder)
|
||||
.contract(contract)
|
||||
.addCapability(micrometerCapability)
|
||||
.requestInterceptor(new BasicAuthRequestInterceptor("admin", "admin"))
|
||||
.target(FooClient.class, "https://PROD-SVC");
|
||||
}
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
@@ -321,7 +340,7 @@ To disable Spring Cloud CircuitBreaker support on a per-client basis create a va
|
||||
----
|
||||
@Configuration
|
||||
public class FooConfiguration {
|
||||
@Bean
|
||||
@Bean
|
||||
@Scope("prototype")
|
||||
public Feign.Builder feignBuilder() {
|
||||
return Feign.builder();
|
||||
@@ -329,23 +348,99 @@ public class FooConfiguration {
|
||||
}
|
||||
----
|
||||
|
||||
The circuit breaker name follows this pattern `<feignClientName>_<calledMethod>`. When calling a `@FeignClient` with name `foo` and the called interface method is `bar` then the circuit breaker name will be `foo_bar`.
|
||||
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()`.
|
||||
|
||||
NOTE: As of 2020.0.2, the circuit breaker name pattern has changed from `<feignClientName>_<calledMethod>`.
|
||||
Using `CircuitBreakerNameResolver` introduced in 2020.0.4, circuit breaker names can retain the old pattern.
|
||||
|
||||
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 they circuit is open or there is an error. To enable fallbacks for a given `@FeignClient` set the `fallback` attribute to the class name that implements the fallback. You also need to declare your implementation as a Spring bean.
|
||||
Spring Cloud CircuitBreaker supports the notion of a fallback: a default code path that is executed when the circuit is open or there is an error. To enable fallbacks for a given `@FeignClient` set the `fallback` attribute to the class name that implements the fallback. You also need to declare your implementation as a Spring bean.
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::{core_path}/src/test/java/org/springframework/cloud/openfeign/circuitbreaker/CirciutBreakerTests.java[tags=client_with_fallback, indent=0]
|
||||
@FeignClient(name = "test", url = "http://localhost:${server.port}/", fallback = Fallback.class)
|
||||
protected interface TestClient {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
Hello getHello();
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hellonotfound")
|
||||
String getException();
|
||||
|
||||
}
|
||||
|
||||
@Component
|
||||
static class Fallback implements TestClient {
|
||||
|
||||
@Override
|
||||
public Hello getHello() {
|
||||
throw new NoFallbackAvailableException("Boom!", new RuntimeException());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getException() {
|
||||
return "Fixed response";
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
If one needs access to the cause that made the fallback trigger, one can use the `fallbackFactory` attribute inside `@FeignClient`.
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
include::{core_path}/src/test/java/org/springframework/cloud/openfeign/circuitbreaker/CirciutBreakerTests.java[tags=client_with_fallback_factory, indent=0]
|
||||
@FeignClient(name = "testClientWithFactory", url = "http://localhost:${server.port}/",
|
||||
fallbackFactory = TestFallbackFactory.class)
|
||||
protected interface TestClientWithFactory {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
Hello getHello();
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hellonotfound")
|
||||
String getException();
|
||||
|
||||
}
|
||||
|
||||
@Component
|
||||
static class TestFallbackFactory implements FallbackFactory<FallbackWithFactory> {
|
||||
|
||||
@Override
|
||||
public FallbackWithFactory create(Throwable cause) {
|
||||
return new FallbackWithFactory();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
static class FallbackWithFactory implements TestClientWithFactory {
|
||||
|
||||
@Override
|
||||
public Hello getHello() {
|
||||
throw new NoFallbackAvailableException("Boom!", new RuntimeException());
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getException() {
|
||||
return "Fixed response";
|
||||
}
|
||||
|
||||
}
|
||||
----
|
||||
|
||||
=== Feign and `@Primary`
|
||||
@@ -371,8 +466,8 @@ This allows grouping common operations into convenient base interfaces.
|
||||
----
|
||||
public interface UserService {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value ="/users/{id}")
|
||||
User getUser(@PathVariable("id") long id);
|
||||
@RequestMapping(method = RequestMethod.GET, value ="/users/{id}")
|
||||
User getUser(@PathVariable("id") long id);
|
||||
}
|
||||
----
|
||||
|
||||
@@ -396,10 +491,7 @@ public interface UserClient extends UserService {
|
||||
}
|
||||
----
|
||||
|
||||
NOTE: It is generally not advisable to share an interface between a
|
||||
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).
|
||||
WARNING: `@FeignClient` interfaces should not be shared between server and client and annotating `@FeignClient` interfaces with `@RequestMapping` on class level is no longer supported.
|
||||
|
||||
=== Feign request/response compression
|
||||
|
||||
@@ -455,10 +547,63 @@ For example, the following would set the `Logger.Level` to `FULL`:
|
||||
----
|
||||
@Configuration
|
||||
public class FooConfiguration {
|
||||
@Bean
|
||||
Logger.Level feignLoggerLevel() {
|
||||
return Logger.Level.FULL;
|
||||
}
|
||||
@Bean
|
||||
Logger.Level feignLoggerLevel() {
|
||||
return Logger.Level.FULL;
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
=== Feign Capability support
|
||||
|
||||
The Feign capabilities expose core Feign components so that these components can be modified. For example, the capabilities can take the `Client`, _decorate_ it, and give the decorated instance back to Feign.
|
||||
The support for metrics libraries is a good real-life example for this. See <<feign-metrics>>.
|
||||
|
||||
Creating one or more `Capability` beans and placing them in a `@FeignClient` configuration lets you register them and modify the behavior of the involved client.
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
@Configuration
|
||||
public class FooConfiguration {
|
||||
@Bean
|
||||
Capability customCapability() {
|
||||
return new CustomCapability();
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
=== Feign metrics
|
||||
|
||||
If all of the following conditions are true, a `MicrometerCapability` bean is created and registered so that your Feign client publishes metrics to Micrometer:
|
||||
|
||||
* `feign-micrometer` is on the classpath
|
||||
* A `MeterRegistry` bean is available
|
||||
* feign metrics properties are set to `true` (by default)
|
||||
- `feign.metrics.enabled=true` (for all clients)
|
||||
- `feign.client.config.feignName.metrics.enabled=true` (for a single client)
|
||||
|
||||
NOTE: If your application already uses Micrometer, enabling metrics is as simple as putting `feign-micrometer` onto your classpath.
|
||||
|
||||
You can also disable the feature by either:
|
||||
|
||||
* excluding `feign-micrometer` from your classpath
|
||||
* setting one of the feign metrics properties to `false`
|
||||
- `feign.metrics.enabled=false`
|
||||
- `feign.client.config.feignName.metrics.enabled=false`
|
||||
|
||||
NOTE: `feign.metrics.enabled=false` disables metrics support for *all* Feign clients regardless of the value of the client-level flags: `feign.client.config.feignName.metrics.enabled`.
|
||||
If you want to enable or disable merics per client, don't set `feign.metrics.enabled` and use `feign.client.config.feignName.metrics.enabled`.
|
||||
|
||||
You can also customize the `MicrometerCapability` by registering your own bean:
|
||||
|
||||
[source,java,indent=0]
|
||||
----
|
||||
@Configuration
|
||||
public class FooConfiguration {
|
||||
@Bean
|
||||
public MicrometerCapability micrometerCapability(MeterRegistry meterRegistry) {
|
||||
return new MicrometerCapability(meterRegistry);
|
||||
}
|
||||
}
|
||||
----
|
||||
|
||||
@@ -477,10 +622,10 @@ For example, the `Params` class defines parameters `param1` and `param2`:
|
||||
----
|
||||
// Params.java
|
||||
public class Params {
|
||||
private String param1;
|
||||
private String param2;
|
||||
private String param1;
|
||||
private String param2;
|
||||
|
||||
// [Getters and setters omitted for brevity]
|
||||
// [Getters and setters omitted for brevity]
|
||||
}
|
||||
----
|
||||
|
||||
@@ -491,8 +636,8 @@ The following feign client uses the `Params` class by using the `@SpringQueryMap
|
||||
@FeignClient("demo")
|
||||
public interface DemoTemplate {
|
||||
|
||||
@GetMapping(path = "/demo")
|
||||
String demoEndpoint(@SpringQueryMap Params params);
|
||||
@GetMapping(path = "/demo")
|
||||
String demoEndpoint(@SpringQueryMap Params params);
|
||||
}
|
||||
----
|
||||
|
||||
@@ -513,8 +658,8 @@ and deserialize HATEOAS representation models: https://docs.spring.io/spring-hat
|
||||
@FeignClient("demo")
|
||||
public interface DemoTemplate {
|
||||
|
||||
@GetMapping(path = "/stores")
|
||||
CollectionModel<Store> getStores();
|
||||
@GetMapping(path = "/stores")
|
||||
CollectionModel<Store> getStores();
|
||||
}
|
||||
----
|
||||
|
||||
@@ -543,13 +688,13 @@ Note that both variable name and the path segment placeholder are called `matrix
|
||||
@FeignClient("demo")
|
||||
public interface DemoTemplate {
|
||||
|
||||
@GetMapping(path = "/stores")
|
||||
CollectionModel<Store> getStores();
|
||||
@GetMapping(path = "/stores")
|
||||
CollectionModel<Store> getStores();
|
||||
}
|
||||
----
|
||||
|
||||
=== Feign `CollectionFormat` support
|
||||
We support `feign.CollectionFormat` by providing the `@CollectionFormat` annotation. You can annotate a Feign client method with it by passing the desired `feign.CollectionFormat` as annotation value.
|
||||
We support `feign.CollectionFormat` by providing the `@CollectionFormat` annotation.You can annotate a Feign client method with it by passing the desired `feign.CollectionFormat` as annotation value.
|
||||
|
||||
In the following example, the `CSV` format is used instead of the default `EXPLODED` to process the method.
|
||||
|
||||
@@ -568,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.
|
||||
|
||||
=== 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.
|
||||
|
||||
@@ -583,6 +728,26 @@ To work around this problem you can use an `ObjectProvider` when autowiring your
|
||||
ObjectProvider<TestFeginClient> testFeginClient;
|
||||
----
|
||||
|
||||
=== Spring Data Support
|
||||
|
||||
You may consider enabling Jackson Modules for the support `org.springframework.data.domain.Page` and `org.springframework.data.domain.Sort` decoding.
|
||||
|
||||
[source,java]
|
||||
----
|
||||
feign.autoconfiguration.jackson.enabled=true
|
||||
----
|
||||
|
||||
=== 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
|
||||
|
||||
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">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<artifactId>spring-cloud-openfeign</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<version>3.0.7-SNAPSHOT</version>
|
||||
<packaging>pom</packaging>
|
||||
<name>Spring Cloud OpenFeign</name>
|
||||
<description>Spring Cloud OpenFeign</description>
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-build</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<version>3.0.5</version>
|
||||
<relativePath/>
|
||||
</parent>
|
||||
<scm>
|
||||
@@ -26,7 +26,7 @@
|
||||
<properties>
|
||||
<main.basedir>${basedir}</main.basedir>
|
||||
<jackson.version>2.11.3</jackson.version>
|
||||
<spring-cloud-commons.version>3.0.0</spring-cloud-commons.version>
|
||||
<spring-cloud-commons.version>3.0.6-SNAPSHOT</spring-cloud-commons.version>
|
||||
|
||||
<!-- Plugin versions -->
|
||||
<maven-eclipse-plugin.version>2.10</maven-eclipse-plugin.version>
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
<parent>
|
||||
<groupId>org.springframework.cloud</groupId>
|
||||
<artifactId>spring-cloud-openfeign</artifactId>
|
||||
<version>3.0.0</version>
|
||||
<version>3.0.7-SNAPSHOT</version>
|
||||
<relativePath>..</relativePath> <!-- lookup parent from repository -->
|
||||
</parent>
|
||||
<artifactId>spring-cloud-openfeign-core</artifactId>
|
||||
@@ -99,11 +99,21 @@
|
||||
<artifactId>feign-slf4j</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.openfeign</groupId>
|
||||
<artifactId>feign-micrometer</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.openfeign</groupId>
|
||||
<artifactId>feign-httpclient</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.openfeign</groupId>
|
||||
<artifactId>feign-hc5</artifactId>
|
||||
<optional>true</optional>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.github.openfeign</groupId>
|
||||
<artifactId>feign-okhttp</artifactId>
|
||||
@@ -173,13 +183,13 @@
|
||||
<dependency>
|
||||
<groupId>com.google.protobuf</groupId>
|
||||
<artifactId>protobuf-java</artifactId>
|
||||
<version>3.14.0</version>
|
||||
<version>3.19.1</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>io.vavr</groupId>
|
||||
<artifactId>vavr</artifactId>
|
||||
<version>0.10.3</version>
|
||||
<version>0.10.4</version>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
|
||||
+34
@@ -0,0 +1,34 @@
|
||||
/*
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import 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);
|
||||
|
||||
}
|
||||
+77
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,6 +17,7 @@
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import java.util.Timer;
|
||||
@@ -25,9 +26,12 @@ import java.util.concurrent.TimeUnit;
|
||||
|
||||
import javax.annotation.PreDestroy;
|
||||
|
||||
import com.fasterxml.jackson.databind.Module;
|
||||
import feign.Client;
|
||||
import feign.Feign;
|
||||
import feign.RequestInterceptor;
|
||||
import feign.Target;
|
||||
import feign.hc5.ApacheHttp5Client;
|
||||
import feign.httpclient.ApacheHttpClient;
|
||||
import feign.okhttp.OkHttpClient;
|
||||
import okhttp3.ConnectionPool;
|
||||
@@ -40,6 +44,7 @@ import org.apache.http.conn.HttpClientConnectionManager;
|
||||
import org.apache.http.impl.client.CloseableHttpClient;
|
||||
|
||||
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.ConditionalOnMissingBean;
|
||||
@@ -54,21 +59,34 @@ import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFa
|
||||
import org.springframework.cloud.commons.httpclient.OkHttpClientFactory;
|
||||
import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptor;
|
||||
import org.springframework.cloud.openfeign.support.DefaultGzipDecoderConfiguration;
|
||||
import org.springframework.cloud.openfeign.support.FeignEncoderProperties;
|
||||
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
|
||||
import org.springframework.cloud.openfeign.support.PageJacksonModule;
|
||||
import org.springframework.cloud.openfeign.support.SortJacksonModule;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.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 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)
|
||||
@ConditionalOnClass(Feign.class)
|
||||
@EnableConfigurationProperties({ FeignClientProperties.class, FeignHttpClientProperties.class })
|
||||
@EnableConfigurationProperties({ FeignClientProperties.class, FeignHttpClientProperties.class,
|
||||
FeignEncoderProperties.class })
|
||||
@Import(DefaultGzipDecoderConfiguration.class)
|
||||
public class FeignAutoConfiguration {
|
||||
|
||||
@@ -89,6 +107,25 @@ public class FeignAutoConfiguration {
|
||||
return context;
|
||||
}
|
||||
|
||||
@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 {
|
||||
@@ -103,7 +140,7 @@ public class FeignAutoConfiguration {
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(CircuitBreaker.class)
|
||||
@ConditionalOnProperty("feign.circuitbreaker.enabled")
|
||||
@ConditionalOnProperty(value = "feign.circuitbreaker.enabled", havingValue = "true")
|
||||
protected static class CircuitBreakerPresentFeignTargeterConfiguration {
|
||||
|
||||
@Bean
|
||||
@@ -112,23 +149,42 @@ public class FeignAutoConfiguration {
|
||||
return new DefaultTargeter();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(CircuitBreakerNameResolver.class)
|
||||
public CircuitBreakerNameResolver circuitBreakerNameResolver() {
|
||||
return new DefaultCircuitBreakerNameResolver();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnBean(CircuitBreakerFactory.class)
|
||||
public Targeter circuitBreakerFeignTargeter(CircuitBreakerFactory circuitBreakerFactory) {
|
||||
return new FeignCircuitBreakerTargeter(circuitBreakerFactory);
|
||||
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
|
||||
// SC loadbalancer is not on the class path.
|
||||
// see corresponding configurations in FeignRibbonClientAutoConfiguration
|
||||
// see corresponding configurations in FeignLoadBalancerAutoConfiguration
|
||||
// for load-balanced clients.
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(ApacheHttpClient.class)
|
||||
@ConditionalOnMissingBean(CloseableHttpClient.class)
|
||||
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
|
||||
@Conditional(HttpClient5DisabledConditions.class)
|
||||
protected static class HttpClientFeignConfiguration {
|
||||
|
||||
private final Timer connectionManagerTimer = new Timer(
|
||||
@@ -238,6 +294,21 @@ 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")
|
||||
|
||||
+25
-13
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,12 +16,7 @@
|
||||
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import java.lang.reflect.InvocationHandler;
|
||||
import java.lang.reflect.Method;
|
||||
import java.util.Map;
|
||||
|
||||
import feign.Feign;
|
||||
import feign.InvocationHandlerFactory;
|
||||
import feign.Target;
|
||||
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
|
||||
@@ -31,6 +26,8 @@ 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 {
|
||||
@@ -55,6 +52,10 @@ public final class FeignCircuitBreaker {
|
||||
|
||||
private String feignClientName;
|
||||
|
||||
private boolean circuitBreakerGroupEnabled;
|
||||
|
||||
private CircuitBreakerNameResolver circuitBreakerNameResolver;
|
||||
|
||||
Builder circuitBreakerFactory(CircuitBreakerFactory circuitBreakerFactory) {
|
||||
this.circuitBreakerFactory = circuitBreakerFactory;
|
||||
return this;
|
||||
@@ -65,6 +66,16 @@ public final class FeignCircuitBreaker {
|
||||
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);
|
||||
}
|
||||
@@ -73,14 +84,15 @@ public final class FeignCircuitBreaker {
|
||||
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(new InvocationHandlerFactory() {
|
||||
@Override
|
||||
public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {
|
||||
return new FeignCircuitBreakerInvocationHandler(circuitBreakerFactory, feignClientName, target,
|
||||
dispatch, nullableFallbackFactory);
|
||||
}
|
||||
});
|
||||
super.invocationHandlerFactory((target, dispatch) -> new FeignCircuitBreakerInvocationHandler(
|
||||
circuitBreakerFactory, feignClientName, target, dispatch, nullableFallbackFactory,
|
||||
circuitBreakerGroupEnabled, circuitBreakerNameResolver));
|
||||
return super.build();
|
||||
}
|
||||
|
||||
|
||||
+17
-4
@@ -29,6 +29,8 @@ import feign.Target;
|
||||
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||
import org.springframework.web.context.request.RequestAttributes;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
|
||||
import static feign.Util.checkNotNull;
|
||||
|
||||
@@ -46,14 +48,21 @@ class FeignCircuitBreakerInvocationHandler implements InvocationHandler {
|
||||
|
||||
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) {
|
||||
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
|
||||
@@ -75,8 +84,10 @@ class FeignCircuitBreakerInvocationHandler implements InvocationHandler {
|
||||
else if ("toString".equals(method.getName())) {
|
||||
return toString();
|
||||
}
|
||||
String circuitName = this.feignClientName + "_" + method.getName();
|
||||
CircuitBreaker circuitBreaker = this.factory.create(circuitName);
|
||||
|
||||
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 -> {
|
||||
@@ -94,9 +105,11 @@ class FeignCircuitBreakerInvocationHandler implements InvocationHandler {
|
||||
}
|
||||
|
||||
private Supplier<Object> asSupplier(final Method method, final Object[] args) {
|
||||
final RequestAttributes requestAttributes = RequestContextHolder.getRequestAttributes();
|
||||
return () -> {
|
||||
try {
|
||||
return this.dispatch.get(method).invoke(args);
|
||||
RequestContextHolder.setRequestAttributes(requestAttributes);
|
||||
return dispatch.get(method).invoke(args);
|
||||
}
|
||||
catch (RuntimeException throwable) {
|
||||
throw throwable;
|
||||
|
||||
+11
-2
@@ -27,8 +27,15 @@ class FeignCircuitBreakerTargeter implements Targeter {
|
||||
|
||||
private final CircuitBreakerFactory circuitBreakerFactory;
|
||||
|
||||
FeignCircuitBreakerTargeter(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
|
||||
@@ -81,7 +88,9 @@ class FeignCircuitBreakerTargeter implements Targeter {
|
||||
}
|
||||
|
||||
private FeignCircuitBreaker.Builder builder(String feignClientName, FeignCircuitBreaker.Builder builder) {
|
||||
return builder.circuitBreakerFactory(this.circuitBreakerFactory).feignClientName(feignClientName);
|
||||
return builder.circuitBreakerFactory(circuitBreakerFactory).feignClientName(feignClientName)
|
||||
.circuitBreakerGroupEnabled(circuitBreakerGroupEnabled)
|
||||
.circuitBreakerNameResolver(circuitBreakerNameResolver);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+21
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -33,6 +33,7 @@ import org.springframework.core.annotation.AliasFor;
|
||||
*
|
||||
* @author Spencer Gibb
|
||||
* @author Venil Noronha
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@Target(ElementType.TYPE)
|
||||
@Retention(RetentionPolicy.RUNTIME)
|
||||
@@ -65,9 +66,28 @@ public @interface FeignClient {
|
||||
|
||||
/**
|
||||
* @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 "";
|
||||
|
||||
/**
|
||||
* @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).
|
||||
*/
|
||||
|
||||
+15
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import feign.Feign;
|
||||
|
||||
import org.springframework.context.ApplicationContext;
|
||||
|
||||
/**
|
||||
@@ -26,6 +28,7 @@ import org.springframework.context.ApplicationContext;
|
||||
*
|
||||
* @author Sven Döring
|
||||
* @author Matt King
|
||||
* @author Sam Kruglov
|
||||
*/
|
||||
public class FeignClientBuilder {
|
||||
|
||||
@@ -76,6 +79,18 @@ public class FeignClientBuilder {
|
||||
return this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies a {@link FeignBuilderCustomizer} to the underlying
|
||||
* {@link Feign.Builder}. May be called multiple times.
|
||||
* @param customizer applied in the same order as supplied here after applying
|
||||
* customizers found in the context.
|
||||
* @return the {@link Builder} with the customizer added
|
||||
*/
|
||||
public Builder<T> customize(final FeignBuilderCustomizer customizer) {
|
||||
this.feignClientFactoryBean.addCustomizer(customizer);
|
||||
return this;
|
||||
}
|
||||
|
||||
public Builder<T> contextId(final String contextId) {
|
||||
this.feignClientFactoryBean.setContextId(contextId);
|
||||
return this;
|
||||
|
||||
+136
-23
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -17,11 +17,13 @@
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import feign.Capability;
|
||||
import feign.Client;
|
||||
import feign.Contract;
|
||||
import feign.ExceptionPropagationPolicy;
|
||||
@@ -35,14 +37,19 @@ import feign.Target.HardCodedTarget;
|
||||
import feign.codec.Decoder;
|
||||
import feign.codec.Encoder;
|
||||
import feign.codec.ErrorDecoder;
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
|
||||
import org.springframework.beans.BeanUtils;
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.BeanFactory;
|
||||
import org.springframework.beans.factory.BeanFactoryAware;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.InitializingBean;
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.cloud.openfeign.clientconfig.FeignClientConfigurer;
|
||||
import org.springframework.cloud.openfeign.loadbalancer.FeignBlockingLoadBalancerClient;
|
||||
import org.springframework.cloud.openfeign.loadbalancer.RetryableFeignBlockingLoadBalancerClient;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.ApplicationContextAware;
|
||||
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
|
||||
@@ -57,14 +64,21 @@ import org.springframework.util.StringUtils;
|
||||
* @author Matt King
|
||||
* @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
|
||||
* lifecycle race condition.
|
||||
***********************************/
|
||||
|
||||
private static Log LOG = LogFactory.getLog(FeignClientFactoryBean.class);
|
||||
|
||||
private Class<?> type;
|
||||
|
||||
private String name;
|
||||
@@ -81,6 +95,8 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
||||
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
private BeanFactory beanFactory;
|
||||
|
||||
private Class<?> fallback = void.class;
|
||||
|
||||
private Class<?> fallbackFactory = void.class;
|
||||
@@ -89,6 +105,12 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
||||
|
||||
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
|
||||
public void afterPropertiesSet() {
|
||||
Assert.hasText(contextId, "Context id must be set");
|
||||
@@ -122,10 +144,12 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
||||
customizerMap.values().stream().sorted(AnnotationAwareOrderComparator.INSTANCE)
|
||||
.forEach(feignBuilderCustomizer -> feignBuilderCustomizer.customize(builder));
|
||||
}
|
||||
additionalCustomizers.forEach(customizer -> customizer.customize(builder));
|
||||
}
|
||||
|
||||
protected void configureFeign(FeignContext context, Feign.Builder builder) {
|
||||
FeignClientProperties properties = applicationContext.getBean(FeignClientProperties.class);
|
||||
FeignClientProperties properties = beanFactory != null ? beanFactory.getBean(FeignClientProperties.class)
|
||||
: applicationContext.getBean(FeignClientProperties.class);
|
||||
|
||||
FeignClientConfigurer feignClientConfigurer = getOptional(context, FeignClientConfigurer.class);
|
||||
setInheritParentContext(feignClientConfigurer.inheritParentConfiguration());
|
||||
@@ -168,10 +192,15 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
||||
}
|
||||
}
|
||||
Request.Options options = getInheritedAwareOptional(context, Request.Options.class);
|
||||
if (options == null) {
|
||||
options = getOptionsByName(context, contextId);
|
||||
}
|
||||
|
||||
if (options != null) {
|
||||
builder.options(options);
|
||||
readTimeoutMillis = options.readTimeoutMillis();
|
||||
connectTimeoutMillis = options.connectTimeoutMillis();
|
||||
followRedirects = options.isFollowRedirects();
|
||||
}
|
||||
Map<String, RequestInterceptor> requestInterceptors = getInheritedAwareInstances(context,
|
||||
RequestInterceptor.class);
|
||||
@@ -192,6 +221,12 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
||||
if (exceptionPropagationPolicy != null) {
|
||||
builder.exceptionPropagationPolicy(exceptionPropagationPolicy);
|
||||
}
|
||||
|
||||
Map<String, Capability> capabilities = getInheritedAwareInstances(context, Capability.class);
|
||||
if (capabilities != null) {
|
||||
capabilities.values().stream().sorted(AnnotationAwareOrderComparator.INSTANCE)
|
||||
.forEach(builder::addCapability);
|
||||
}
|
||||
}
|
||||
|
||||
protected void configureUsingProperties(FeignClientProperties.FeignClientConfiguration config,
|
||||
@@ -204,11 +239,15 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
||||
builder.logLevel(config.getLoggerLevel());
|
||||
}
|
||||
|
||||
connectTimeoutMillis = config.getConnectTimeout() != null ? config.getConnectTimeout() : connectTimeoutMillis;
|
||||
readTimeoutMillis = config.getReadTimeout() != null ? config.getReadTimeout() : readTimeoutMillis;
|
||||
if (!refreshableClient) {
|
||||
connectTimeoutMillis = config.getConnectTimeout() != null ? config.getConnectTimeout()
|
||||
: connectTimeoutMillis;
|
||||
readTimeoutMillis = config.getReadTimeout() != null ? config.getReadTimeout() : readTimeoutMillis;
|
||||
followRedirects = config.isFollowRedirects() != null ? config.isFollowRedirects() : followRedirects;
|
||||
|
||||
builder.options(new Request.Options(connectTimeoutMillis, TimeUnit.MILLISECONDS, readTimeoutMillis,
|
||||
TimeUnit.MILLISECONDS, true));
|
||||
builder.options(new Request.Options(connectTimeoutMillis, TimeUnit.MILLISECONDS, readTimeoutMillis,
|
||||
TimeUnit.MILLISECONDS, followRedirects));
|
||||
}
|
||||
|
||||
if (config.getRetryer() != null) {
|
||||
Retryer retryer = getOrInstantiate(config.getRetryer());
|
||||
@@ -238,13 +277,8 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
||||
builder.encoder(getOrInstantiate(config.getEncoder()));
|
||||
}
|
||||
|
||||
if (Objects.nonNull(config.getDefaultRequestHeaders())) {
|
||||
builder.requestInterceptor(requestTemplate -> requestTemplate.headers(config.getDefaultRequestHeaders()));
|
||||
}
|
||||
|
||||
if (Objects.nonNull(config.getDefaultQueryParameters())) {
|
||||
builder.requestInterceptor(requestTemplate -> requestTemplate.queries(config.getDefaultQueryParameters()));
|
||||
}
|
||||
addDefaultRequestHeaders(config, builder);
|
||||
addDefaultQueryParams(config, builder);
|
||||
|
||||
if (Objects.nonNull(config.getDecoder())) {
|
||||
builder.decoder(getOrInstantiate(config.getDecoder()));
|
||||
@@ -257,11 +291,44 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
||||
if (Objects.nonNull(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) {
|
||||
try {
|
||||
return applicationContext.getBean(tClass);
|
||||
return beanFactory != null ? beanFactory.getBean(tClass) : applicationContext.getBean(tClass);
|
||||
}
|
||||
catch (NoSuchBeanDefinitionException e) {
|
||||
return BeanUtils.instantiateClass(tClass);
|
||||
@@ -310,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?");
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
public Object getObject() {
|
||||
return getTarget();
|
||||
@@ -321,10 +402,15 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
||||
* information
|
||||
*/
|
||||
<T> T getTarget() {
|
||||
FeignContext context = applicationContext.getBean(FeignContext.class);
|
||||
FeignContext context = beanFactory != null ? beanFactory.getBean(FeignContext.class)
|
||||
: applicationContext.getBean(FeignContext.class);
|
||||
Feign.Builder builder = feign(context);
|
||||
|
||||
if (!StringUtils.hasText(url)) {
|
||||
|
||||
if (LOG.isInfoEnabled()) {
|
||||
LOG.info("For '" + name + "' URL not provided. Will try picking an instance via load-balancing.");
|
||||
}
|
||||
if (!name.startsWith("http")) {
|
||||
url = "http://" + name;
|
||||
}
|
||||
@@ -345,6 +431,11 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
||||
// but Spring Cloud LoadBalancer is on the classpath, so unwrap
|
||||
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);
|
||||
}
|
||||
Targeter targeter = get(context, Targeter.class);
|
||||
@@ -430,13 +521,18 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
||||
this.inheritParentContext = inheritParentContext;
|
||||
}
|
||||
|
||||
public void addCustomizer(FeignBuilderCustomizer customizer) {
|
||||
additionalCustomizers.add(customizer);
|
||||
}
|
||||
|
||||
public ApplicationContext getApplicationContext() {
|
||||
return applicationContext;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void setApplicationContext(ApplicationContext context) throws BeansException {
|
||||
this.applicationContext = context;
|
||||
applicationContext = context;
|
||||
beanFactory = context;
|
||||
}
|
||||
|
||||
public Class<?> getFallback() {
|
||||
@@ -455,6 +551,10 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
||||
this.fallbackFactory = fallbackFactory;
|
||||
}
|
||||
|
||||
public void setRefreshableClient(boolean refreshableClient) {
|
||||
this.refreshableClient = refreshableClient;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
@@ -464,16 +564,21 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
||||
return false;
|
||||
}
|
||||
FeignClientFactoryBean that = (FeignClientFactoryBean) o;
|
||||
return Objects.equals(applicationContext, that.applicationContext) && decode404 == that.decode404
|
||||
return Objects.equals(applicationContext, that.applicationContext)
|
||||
&& Objects.equals(beanFactory, that.beanFactory) && decode404 == that.decode404
|
||||
&& inheritParentContext == that.inheritParentContext && Objects.equals(fallback, that.fallback)
|
||||
&& Objects.equals(fallbackFactory, that.fallbackFactory) && Objects.equals(name, that.name)
|
||||
&& Objects.equals(path, that.path) && Objects.equals(type, that.type) && Objects.equals(url, that.url);
|
||||
&& 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
|
||||
public int hashCode() {
|
||||
return Objects.hash(applicationContext, decode404, inheritParentContext, fallback, fallbackFactory, name, path,
|
||||
type, url);
|
||||
return Objects.hash(applicationContext, beanFactory, decode404, inheritParentContext, fallback, fallbackFactory,
|
||||
name, path, type, url, readTimeoutMillis, connectTimeoutMillis, followRedirects, refreshableClient);
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -482,8 +587,16 @@ public class FeignClientFactoryBean implements FactoryBean<Object>, Initializing
|
||||
.append(name).append("', ").append("url='").append(url).append("', ").append("path='").append(path)
|
||||
.append("', ").append("decode404=").append(decode404).append(", ").append("inheritParentContext=")
|
||||
.append(inheritParentContext).append(", ").append("applicationContext=").append(applicationContext)
|
||||
.append(", ").append("fallback=").append(fallback).append(", ").append("fallbackFactory=")
|
||||
.append(fallbackFactory).append("}").toString();
|
||||
.append(", ").append("beanFactory=").append(beanFactory).append(", ").append("fallback=")
|
||||
.append(fallback).append(", ").append("fallbackFactory=").append(fallbackFactory).append("}")
|
||||
.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-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.springframework.context.annotation.Condition;
|
||||
import org.springframework.context.annotation.ConditionContext;
|
||||
import org.springframework.core.type.AnnotatedTypeMetadata;
|
||||
|
||||
/**
|
||||
* @author Jonatan Ivanov
|
||||
*/
|
||||
class FeignClientMetricsEnabledCondition implements Condition {
|
||||
|
||||
@Override
|
||||
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
|
||||
FeignClientProperties feignClientProperties = context.getBeanFactory()
|
||||
.getBeanProvider(FeignClientProperties.class).getIfAvailable();
|
||||
if (feignClientProperties != null) {
|
||||
Map<String, FeignClientProperties.FeignClientConfiguration> feignClientConfigMap = feignClientProperties
|
||||
.getConfig();
|
||||
if (feignClientConfigMap != null) {
|
||||
FeignClientProperties.FeignClientConfiguration feignClientConfig = feignClientConfigMap
|
||||
.get(context.getEnvironment().getProperty("feign.client.name"));
|
||||
if (feignClientConfig != null) {
|
||||
FeignClientProperties.MetricsProperties metrics = feignClientConfig.getMetrics();
|
||||
if (metrics != null && metrics.getEnabled() != null) {
|
||||
return metrics.getEnabled();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
+73
-3
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -22,6 +22,7 @@ import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
|
||||
import feign.Capability;
|
||||
import feign.Contract;
|
||||
import feign.ExceptionPropagationPolicy;
|
||||
import feign.Logger;
|
||||
@@ -37,6 +38,8 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
* @author Eko Kurniawan Khannedy
|
||||
* @author Ilia Ilinykh
|
||||
* @author Ram Anaswara
|
||||
* @author Jonatan Ivanov
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@ConfigurationProperties("feign.client")
|
||||
public class FeignClientProperties {
|
||||
@@ -134,6 +137,12 @@ public class FeignClientProperties {
|
||||
|
||||
private ExceptionPropagationPolicy exceptionPropagationPolicy;
|
||||
|
||||
private List<Class<Capability>> capabilities;
|
||||
|
||||
private MetricsProperties metrics;
|
||||
|
||||
private Boolean followRedirects;
|
||||
|
||||
public Logger.Level getLoggerLevel() {
|
||||
return loggerLevel;
|
||||
}
|
||||
@@ -238,6 +247,30 @@ public class FeignClientProperties {
|
||||
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
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
@@ -255,14 +288,51 @@ public class FeignClientProperties {
|
||||
&& Objects.equals(decoder, that.decoder) && Objects.equals(contract, that.contract)
|
||||
&& Objects.equals(exceptionPropagationPolicy, that.exceptionPropagationPolicy)
|
||||
&& Objects.equals(defaultRequestHeaders, that.defaultRequestHeaders)
|
||||
&& Objects.equals(defaultQueryParameters, that.defaultQueryParameters);
|
||||
&& Objects.equals(defaultQueryParameters, that.defaultQueryParameters)
|
||||
&& Objects.equals(capabilities, that.capabilities) && Objects.equals(metrics, that.metrics)
|
||||
&& Objects.equals(followRedirects, that.followRedirects);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(loggerLevel, connectTimeout, readTimeout, retryer, errorDecoder, requestInterceptors,
|
||||
decode404, encoder, decoder, contract, exceptionPropagationPolicy, defaultQueryParameters,
|
||||
defaultRequestHeaders);
|
||||
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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+40
-21
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,16 +19,18 @@ package org.springframework.cloud.openfeign;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
|
||||
import com.fasterxml.jackson.databind.Module;
|
||||
import feign.Contract;
|
||||
import feign.Feign;
|
||||
import feign.Logger;
|
||||
import feign.QueryMapEncoder;
|
||||
import feign.Retryer;
|
||||
import feign.codec.Decoder;
|
||||
import feign.codec.Encoder;
|
||||
import feign.form.MultipartFormContentProcessor;
|
||||
import feign.form.spring.SpringFormEncoder;
|
||||
import feign.micrometer.MicrometerCapability;
|
||||
import feign.optionals.OptionalDecoder;
|
||||
import io.micrometer.core.instrument.MeterRegistry;
|
||||
|
||||
import org.springframework.beans.factory.ObjectFactory;
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
@@ -44,10 +46,10 @@ import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||
import org.springframework.cloud.openfeign.clientconfig.FeignClientConfigurer;
|
||||
import org.springframework.cloud.openfeign.support.AbstractFormWriter;
|
||||
import org.springframework.cloud.openfeign.support.PageJacksonModule;
|
||||
import org.springframework.cloud.openfeign.support.FeignEncoderProperties;
|
||||
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.SortJacksonModule;
|
||||
import org.springframework.cloud.openfeign.support.SpringDecoder;
|
||||
import org.springframework.cloud.openfeign.support.SpringEncoder;
|
||||
import org.springframework.cloud.openfeign.support.SpringMvcContract;
|
||||
@@ -65,6 +67,9 @@ import static feign.form.ContentType.MULTIPART;
|
||||
* @author Dave Syer
|
||||
* @author Venil Noronha
|
||||
* @author Darren Foong
|
||||
* @author Jonatan Ivanov
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Hyeonmin Park
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
public class FeignClientsConfiguration {
|
||||
@@ -87,6 +92,9 @@ public class FeignClientsConfiguration {
|
||||
@Autowired(required = false)
|
||||
private FeignClientProperties feignClientProperties;
|
||||
|
||||
@Autowired(required = false)
|
||||
private FeignEncoderProperties encoderProperties;
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public Decoder feignDecoder() {
|
||||
@@ -97,14 +105,14 @@ public class FeignClientsConfiguration {
|
||||
@ConditionalOnMissingBean
|
||||
@ConditionalOnMissingClass("org.springframework.data.domain.Pageable")
|
||||
public Encoder feignEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider) {
|
||||
return springEncoder(formWriterProvider);
|
||||
return springEncoder(formWriterProvider, encoderProperties);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnClass(name = "org.springframework.data.domain.Pageable")
|
||||
@ConditionalOnMissingBean
|
||||
public Encoder feignEncoderPageable(ObjectProvider<AbstractFormWriter> formWriterProvider) {
|
||||
PageableSpringEncoder encoder = new PageableSpringEncoder(springEncoder(formWriterProvider));
|
||||
PageableSpringEncoder encoder = new PageableSpringEncoder(springEncoder(formWriterProvider, encoderProperties));
|
||||
|
||||
if (springDataWebProperties != null) {
|
||||
encoder.setPageParameter(springDataWebProperties.getPageable().getPageParameter());
|
||||
@@ -114,6 +122,13 @@ public class FeignClientsConfiguration {
|
||||
return encoder;
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnClass(name = "org.springframework.data.domain.Pageable")
|
||||
@ConditionalOnMissingBean
|
||||
public QueryMapEncoder feignQueryMapEncoderPageable() {
|
||||
return new PageableSpringQueryMapEncoder();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public Contract feignContract(ConversionService feignConversionService) {
|
||||
@@ -142,18 +157,6 @@ public class FeignClientsConfiguration {
|
||||
return new DefaultFeignLoggerFactory(this.logger);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnClass(name = "org.springframework.data.domain.Page")
|
||||
public Module pageJacksonModule() {
|
||||
return new PageJacksonModule();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnClass(name = "org.springframework.data.domain.Page")
|
||||
public Module sortModule() {
|
||||
return new SortJacksonModule();
|
||||
}
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean(FeignClientConfigurer.class)
|
||||
public FeignClientConfigurer feignClientConfigurer() {
|
||||
@@ -161,14 +164,15 @@ public class FeignClientsConfiguration {
|
||||
};
|
||||
}
|
||||
|
||||
private Encoder springEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider) {
|
||||
private Encoder springEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider,
|
||||
FeignEncoderProperties encoderProperties) {
|
||||
AbstractFormWriter formWriter = formWriterProvider.getIfAvailable();
|
||||
|
||||
if (formWriter != null) {
|
||||
return new SpringEncoder(new SpringPojoFormEncoder(formWriter), this.messageConverters);
|
||||
return new SpringEncoder(new SpringPojoFormEncoder(formWriter), this.messageConverters, encoderProperties);
|
||||
}
|
||||
else {
|
||||
return new SpringEncoder(new SpringFormEncoder(), this.messageConverters);
|
||||
return new SpringEncoder(new SpringFormEncoder(), this.messageConverters, encoderProperties);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -218,4 +222,19 @@ public class FeignClientsConfiguration {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnBean(type = "io.micrometer.core.instrument.MeterRegistry")
|
||||
@ConditionalOnClass(name = "feign.micrometer.MicrometerCapability")
|
||||
@ConditionalOnProperty(name = "feign.metrics.enabled", matchIfMissing = true)
|
||||
@Conditional(FeignClientMetricsEnabledCondition.class)
|
||||
protected static class MetricsConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public MicrometerCapability micrometerCapability(MeterRegistry meterRegistry) {
|
||||
return new MicrometerCapability(meterRegistry);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+107
-27
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -20,16 +20,26 @@ import java.net.MalformedURLException;
|
||||
import java.net.URI;
|
||||
import java.net.URISyntaxException;
|
||||
import java.net.URL;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.HashSet;
|
||||
import java.util.LinkedHashSet;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
|
||||
import feign.Request;
|
||||
|
||||
import org.springframework.aop.scope.ScopedProxyUtils;
|
||||
import org.springframework.beans.factory.FactoryBean;
|
||||
import org.springframework.beans.factory.annotation.AnnotatedBeanDefinition;
|
||||
import org.springframework.beans.factory.annotation.AnnotatedGenericBeanDefinition;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.beans.factory.config.BeanDefinitionHolder;
|
||||
import org.springframework.beans.factory.config.BeanExpressionContext;
|
||||
import org.springframework.beans.factory.config.BeanExpressionResolver;
|
||||
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
|
||||
import org.springframework.beans.factory.support.AbstractBeanDefinition;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
|
||||
import org.springframework.beans.factory.support.BeanDefinitionReaderUtils;
|
||||
@@ -45,6 +55,7 @@ import org.springframework.core.type.AnnotationMetadata;
|
||||
import org.springframework.core.type.filter.AnnotationTypeFilter;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.ClassUtils;
|
||||
import org.springframework.util.ObjectUtils;
|
||||
import org.springframework.util.StringUtils;
|
||||
|
||||
/**
|
||||
@@ -53,6 +64,9 @@ import org.springframework.util.StringUtils;
|
||||
* @author Venil Noronha
|
||||
* @author Gang Li
|
||||
* @author Michal Domagala
|
||||
* @author Marcin Grzejszczak
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Jasbir Singh
|
||||
*/
|
||||
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
|
||||
|
||||
@@ -156,7 +170,6 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
||||
|
||||
LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();
|
||||
Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());
|
||||
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(FeignClient.class);
|
||||
final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients");
|
||||
if (clients == null || clients.length == 0) {
|
||||
ClassPathScanningCandidateComponentProvider scanner = getScanner();
|
||||
@@ -194,36 +207,55 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
||||
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata,
|
||||
Map<String, Object> attributes) {
|
||||
String className = annotationMetadata.getClassName();
|
||||
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);
|
||||
validate(attributes);
|
||||
definition.addPropertyValue("url", getUrl(attributes));
|
||||
definition.addPropertyValue("path", getPath(attributes));
|
||||
Class clazz = ClassUtils.resolveClassName(className, null);
|
||||
ConfigurableBeanFactory beanFactory = registry instanceof ConfigurableBeanFactory
|
||||
? (ConfigurableBeanFactory) registry : null;
|
||||
String contextId = getContextId(beanFactory, attributes);
|
||||
String name = getName(attributes);
|
||||
definition.addPropertyValue("name", name);
|
||||
String contextId = getContextId(attributes);
|
||||
definition.addPropertyValue("contextId", contextId);
|
||||
definition.addPropertyValue("type", className);
|
||||
definition.addPropertyValue("decode404", attributes.get("decode404"));
|
||||
definition.addPropertyValue("fallback", attributes.get("fallback"));
|
||||
definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
|
||||
FeignClientFactoryBean factoryBean = new FeignClientFactoryBean();
|
||||
factoryBean.setBeanFactory(beanFactory);
|
||||
factoryBean.setName(name);
|
||||
factoryBean.setContextId(contextId);
|
||||
factoryBean.setType(clazz);
|
||||
factoryBean.setRefreshableClient(isClientRefreshEnabled());
|
||||
BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(clazz, () -> {
|
||||
factoryBean.setUrl(getUrl(beanFactory, attributes));
|
||||
factoryBean.setPath(getPath(beanFactory, attributes));
|
||||
factoryBean.setDecode404(Boolean.parseBoolean(String.valueOf(attributes.get("decode404"))));
|
||||
Object fallback = attributes.get("fallback");
|
||||
if (fallback != null) {
|
||||
factoryBean.setFallback(fallback instanceof Class ? (Class<?>) fallback
|
||||
: ClassUtils.resolveClassName(fallback.toString(), null));
|
||||
}
|
||||
Object fallbackFactory = attributes.get("fallbackFactory");
|
||||
if (fallbackFactory != null) {
|
||||
factoryBean.setFallbackFactory(fallbackFactory instanceof Class ? (Class<?>) fallbackFactory
|
||||
: ClassUtils.resolveClassName(fallbackFactory.toString(), null));
|
||||
}
|
||||
return factoryBean.getObject();
|
||||
});
|
||||
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
|
||||
definition.setLazyInit(true);
|
||||
validate(attributes);
|
||||
|
||||
String alias = contextId + "FeignClient";
|
||||
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
|
||||
beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);
|
||||
beanDefinition.setAttribute("feignClientsRegistrarFactoryBean", factoryBean);
|
||||
|
||||
// has a default, won't be null
|
||||
boolean primary = (Boolean) attributes.get("primary");
|
||||
|
||||
beanDefinition.setPrimary(primary);
|
||||
|
||||
String qualifier = getQualifier(attributes);
|
||||
if (StringUtils.hasText(qualifier)) {
|
||||
alias = qualifier;
|
||||
String[] qualifiers = getQualifiers(attributes);
|
||||
if (ObjectUtils.isEmpty(qualifiers)) {
|
||||
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);
|
||||
|
||||
registerOptionsBeanDefinition(registry, contextId);
|
||||
}
|
||||
|
||||
private void validate(Map<String, Object> attributes) {
|
||||
@@ -235,6 +267,10 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
||||
}
|
||||
|
||||
/* for testing */ String getName(Map<String, Object> attributes) {
|
||||
return getName(null, attributes);
|
||||
}
|
||||
|
||||
String getName(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {
|
||||
String name = (String) attributes.get("serviceId");
|
||||
if (!StringUtils.hasText(name)) {
|
||||
name = (String) attributes.get("name");
|
||||
@@ -242,34 +278,42 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
||||
if (!StringUtils.hasText(name)) {
|
||||
name = (String) attributes.get("value");
|
||||
}
|
||||
name = resolve(name);
|
||||
name = resolve(beanFactory, name);
|
||||
return getName(name);
|
||||
}
|
||||
|
||||
private String getContextId(Map<String, Object> attributes) {
|
||||
private String getContextId(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {
|
||||
String contextId = (String) attributes.get("contextId");
|
||||
if (!StringUtils.hasText(contextId)) {
|
||||
return getName(attributes);
|
||||
}
|
||||
|
||||
contextId = resolve(contextId);
|
||||
contextId = resolve(beanFactory, contextId);
|
||||
return getName(contextId);
|
||||
}
|
||||
|
||||
private String resolve(String value) {
|
||||
private String resolve(ConfigurableBeanFactory beanFactory, String value) {
|
||||
if (StringUtils.hasText(value)) {
|
||||
return this.environment.resolvePlaceholders(value);
|
||||
if (beanFactory == null) {
|
||||
return this.environment.resolvePlaceholders(value);
|
||||
}
|
||||
BeanExpressionResolver resolver = beanFactory.getBeanExpressionResolver();
|
||||
String resolved = beanFactory.resolveEmbeddedValue(value);
|
||||
if (resolver == null) {
|
||||
return resolved;
|
||||
}
|
||||
return String.valueOf(resolver.evaluate(resolved, new BeanExpressionContext(beanFactory, null)));
|
||||
}
|
||||
return value;
|
||||
}
|
||||
|
||||
private String getUrl(Map<String, Object> attributes) {
|
||||
String url = resolve((String) attributes.get("url"));
|
||||
private String getUrl(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {
|
||||
String url = resolve(beanFactory, (String) attributes.get("url"));
|
||||
return getUrl(url);
|
||||
}
|
||||
|
||||
private String getPath(Map<String, Object> attributes) {
|
||||
String path = resolve((String) attributes.get("path"));
|
||||
private String getPath(ConfigurableBeanFactory beanFactory, Map<String, Object> attributes) {
|
||||
String path = resolve(beanFactory, (String) attributes.get("path"));
|
||||
return getPath(path);
|
||||
}
|
||||
|
||||
@@ -324,6 +368,18 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
||||
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) {
|
||||
if (client == null) {
|
||||
return null;
|
||||
@@ -359,4 +415,28 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
|
||||
this.environment = environment;
|
||||
}
|
||||
|
||||
/**
|
||||
* This method is meant to create {@link Request.Options} beans definition with
|
||||
* refreshScope.
|
||||
* @param registry spring bean definition registry
|
||||
* @param contextId name of feign client
|
||||
*/
|
||||
private void registerOptionsBeanDefinition(BeanDefinitionRegistry registry, String contextId) {
|
||||
if (isClientRefreshEnabled()) {
|
||||
String beanName = Request.Options.class.getCanonicalName() + "-" + contextId;
|
||||
BeanDefinitionBuilder definitionBuilder = BeanDefinitionBuilder
|
||||
.genericBeanDefinition(OptionsFactoryBean.class);
|
||||
definitionBuilder.setScope("refresh");
|
||||
definitionBuilder.addPropertyValue("contextId", contextId);
|
||||
BeanDefinitionHolder definitionHolder = new BeanDefinitionHolder(definitionBuilder.getBeanDefinition(),
|
||||
beanName);
|
||||
definitionHolder = ScopedProxyUtils.createScopedProxy(definitionHolder, registry, true);
|
||||
BeanDefinitionReaderUtils.registerBeanDefinition(definitionHolder, registry);
|
||||
}
|
||||
}
|
||||
|
||||
private boolean isClientRefreshEnabled() {
|
||||
return environment.getProperty("feign.client.refresh-enabled", Boolean.class, false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+5
@@ -30,6 +30,7 @@ import org.springframework.lang.Nullable;
|
||||
* @author Spencer Gibb
|
||||
* @author Dave Syer
|
||||
* @author Matt King
|
||||
* @author Jasbir Singh
|
||||
*/
|
||||
public class FeignContext extends NamedContextFactory<FeignClientSpecification> {
|
||||
|
||||
@@ -52,4 +53,8 @@ public class FeignContext extends NamedContextFactory<FeignClientSpecification>
|
||||
return getContext(name).getBeansOfType(type);
|
||||
}
|
||||
|
||||
public <T> T getInstance(String contextName, String beanName, Class<T> type) {
|
||||
return getContext(contextName).getBean(beanName, type);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+42
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import 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-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import java.util.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);
|
||||
}
|
||||
|
||||
}
|
||||
+2
-2
@@ -72,8 +72,8 @@ public class MatrixVariableParameterProcessor implements AnnotatedParameterProce
|
||||
private String expandMap(Object object) {
|
||||
Map<String, Object> paramMap = (Map) object;
|
||||
|
||||
return paramMap.keySet().stream().map(key -> ";" + key + "=" + paramMap.get(key).toString())
|
||||
.collect(Collectors.joining());
|
||||
return paramMap.keySet().stream().filter(key -> paramMap.get(key) != null)
|
||||
.map(key -> ";" + key + "=" + paramMap.get(key).toString()).collect(Collectors.joining());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+146
@@ -0,0 +1,146 @@
|
||||
/*
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign.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;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+8
-35
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2016-2020 the original author or authors.
|
||||
* Copyright 2016-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,75 +16,48 @@
|
||||
|
||||
package org.springframework.cloud.openfeign.hateoas;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
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.ConditionalOnMissingBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnWebApplication;
|
||||
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.jackson.JacksonAutoConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.hateoas.RepresentationModel;
|
||||
import org.springframework.hateoas.mediatype.MessageResolver;
|
||||
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.mediatype.hal.HalMediaTypeConfiguration;
|
||||
import org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter;
|
||||
|
||||
import static org.springframework.hateoas.MediaTypes.HAL_JSON;
|
||||
|
||||
/**
|
||||
* @author Hector Espert
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnWebApplication
|
||||
@ConditionalOnClass(RepresentationModel.class)
|
||||
@AutoConfigureAfter({ JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,
|
||||
RepositoryRestMvcAutoConfiguration.class })
|
||||
@AutoConfigureBefore(HypermediaAutoConfiguration.class)
|
||||
public class FeignHalAutoConfiguration {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnBean(HalMediaTypeConfiguration.class)
|
||||
@ConditionalOnMissingBean
|
||||
public TypeConstrainedMappingJackson2HttpMessageConverter halJacksonHttpMessageConverter(
|
||||
ObjectProvider<ObjectMapper> objectMapper, ObjectProvider<HalConfiguration> halConfiguration,
|
||||
ObjectProvider<MessageResolver> messageResolver, ObjectProvider<CurieProvider> curieProvider,
|
||||
ObjectProvider<LinkRelationProvider> linkRelationProvider) {
|
||||
|
||||
ObjectProvider<ObjectMapper> objectMapper, HalMediaTypeConfiguration halConfiguration) {
|
||||
ObjectMapper mapper = objectMapper.getIfAvailable(ObjectMapper::new).copy();
|
||||
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
halConfiguration.configureObjectMapper(mapper);
|
||||
TypeConstrainedMappingJackson2HttpMessageConverter converter = new TypeConstrainedMappingJackson2HttpMessageConverter(
|
||||
RepresentationModel.class);
|
||||
converter.setSupportedMediaTypes(Arrays.asList(HAL_JSON));
|
||||
converter.setSupportedMediaTypes(Collections.singletonList(HAL_JSON));
|
||||
converter.setObjectMapper(mapper);
|
||||
return converter;
|
||||
}
|
||||
|
||||
+7
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,11 +19,14 @@ package org.springframework.cloud.openfeign.loadbalancer;
|
||||
import feign.Client;
|
||||
import feign.Feign;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
|
||||
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnBean;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration;
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerClient;
|
||||
import org.springframework.cloud.loadbalancer.config.BlockingLoadBalancerClientAutoConfiguration;
|
||||
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
|
||||
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
|
||||
@@ -35,18 +38,20 @@ import org.springframework.context.annotation.Import;
|
||||
* of {@link Client}.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Nguyen Ky Thanh
|
||||
* @since 2.2.0
|
||||
*/
|
||||
@ConditionalOnClass(Feign.class)
|
||||
@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
|
||||
@AutoConfigureBefore(FeignAutoConfiguration.class)
|
||||
@AutoConfigureAfter({ BlockingLoadBalancerClientAutoConfiguration.class, LoadBalancerAutoConfiguration.class })
|
||||
@EnableConfigurationProperties(FeignHttpClientProperties.class)
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
// Order is important here, last should be the default, first should be optional
|
||||
// see
|
||||
// https://github.com/spring-cloud/spring-cloud-netflix/issues/2086#issuecomment-316281653
|
||||
@Import({ HttpClientFeignLoadBalancerConfiguration.class, OkHttpFeignLoadBalancerConfiguration.class,
|
||||
DefaultFeignLoadBalancerConfiguration.class })
|
||||
HttpClient5FeignLoadBalancerConfiguration.class, DefaultFeignLoadBalancerConfiguration.class })
|
||||
public class FeignLoadBalancerAutoConfiguration {
|
||||
|
||||
}
|
||||
|
||||
+75
@@ -0,0 +1,75 @@
|
||||
/*
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign.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);
|
||||
}
|
||||
|
||||
}
|
||||
+4
-1
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -29,6 +29,7 @@ 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.HttpClient5DisabledConditions;
|
||||
import org.springframework.cloud.openfeign.clientconfig.HttpClientFeignConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Conditional;
|
||||
@@ -40,12 +41,14 @@ import org.springframework.context.annotation.Import;
|
||||
* that uses {@link ApacheHttpClient} under the hood.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Nguyen Ky Thanh
|
||||
* @since 2.2.0
|
||||
*/
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@ConditionalOnClass(ApacheHttpClient.class)
|
||||
@ConditionalOnBean({ LoadBalancerClient.class, LoadBalancerClientFactory.class })
|
||||
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
|
||||
@Conditional(HttpClient5DisabledConditions.class)
|
||||
@Import(HttpClientFeignConfiguration.class)
|
||||
@EnableConfigurationProperties(LoadBalancerProperties.class)
|
||||
class HttpClientFeignLoadBalancerConfiguration {
|
||||
|
||||
+46
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign.loadbalancer;
|
||||
|
||||
import java.io.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;
|
||||
}
|
||||
|
||||
}
|
||||
+6
-3
@@ -46,7 +46,6 @@ import org.springframework.cloud.client.loadbalancer.LoadBalancerLifecycleValida
|
||||
import org.springframework.cloud.client.loadbalancer.LoadBalancerProperties;
|
||||
import org.springframework.cloud.client.loadbalancer.ResponseData;
|
||||
import org.springframework.cloud.client.loadbalancer.RetryableRequestContext;
|
||||
import org.springframework.cloud.client.loadbalancer.RetryableStatusCodeException;
|
||||
import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpMethod;
|
||||
@@ -57,6 +56,7 @@ import org.springframework.retry.backoff.NoBackOffPolicy;
|
||||
import org.springframework.retry.policy.NeverRetryPolicy;
|
||||
import org.springframework.retry.support.RetryTemplate;
|
||||
import org.springframework.util.Assert;
|
||||
import org.springframework.util.StreamUtils;
|
||||
|
||||
import static org.springframework.cloud.openfeign.loadbalancer.LoadBalancerUtils.buildRequestData;
|
||||
|
||||
@@ -70,7 +70,7 @@ import static org.springframework.cloud.openfeign.loadbalancer.LoadBalancerUtils
|
||||
@SuppressWarnings({ "rawtypes", "unchecked" })
|
||||
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;
|
||||
|
||||
@@ -161,8 +161,11 @@ public class RetryableFeignBlockingLoadBalancerClient implements Client {
|
||||
if (LOG.isDebugEnabled()) {
|
||||
LOG.debug(String.format("Retrying on status code: %d", responseStatus));
|
||||
}
|
||||
byte[] byteArray = response.body() == null ? new byte[] {}
|
||||
: StreamUtils.copyToByteArray(response.body().asInputStream());
|
||||
response.close();
|
||||
throw new RetryableStatusCodeException(serviceId, responseStatus, response, URI.create(request.url()));
|
||||
throw new LoadBalancerResponseStatusCodeException(serviceId, response, byteArray,
|
||||
URI.create(request.url()));
|
||||
}
|
||||
return response;
|
||||
}, new LoadBalancedRecoveryCallback<Response, Response>() {
|
||||
|
||||
+44
@@ -0,0 +1,44 @@
|
||||
/*
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign.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-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -22,6 +22,7 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
|
||||
/**
|
||||
* @author Ryan Baxter
|
||||
* @author Nguyen Ky Thanh
|
||||
*/
|
||||
@ConfigurationProperties(prefix = "feign.httpclient")
|
||||
public class FeignHttpClientProperties {
|
||||
@@ -82,6 +83,11 @@ public class FeignHttpClientProperties {
|
||||
|
||||
private int connectionTimerRepeat = DEFAULT_CONNECTION_TIMER_REPEAT;
|
||||
|
||||
/**
|
||||
* Apache HttpClient5 additional properties.
|
||||
*/
|
||||
private Hc5Properties hc5 = new Hc5Properties();
|
||||
|
||||
public int getConnectionTimerRepeat() {
|
||||
return this.connectionTimerRepeat;
|
||||
}
|
||||
@@ -146,4 +152,124 @@ public class FeignHttpClientProperties {
|
||||
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
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+32
-11
@@ -20,6 +20,7 @@ import java.util.Iterator;
|
||||
import java.util.List;
|
||||
import java.util.function.Function;
|
||||
|
||||
import com.fasterxml.jackson.annotation.JsonAlias;
|
||||
import com.fasterxml.jackson.annotation.JsonIgnore;
|
||||
import com.fasterxml.jackson.annotation.JsonProperty;
|
||||
import com.fasterxml.jackson.core.Version;
|
||||
@@ -36,6 +37,9 @@ import org.springframework.data.domain.Sort;
|
||||
* This Jackson module provides support to deserialize Spring {@link Page} objects.
|
||||
*
|
||||
* @author Pascal Büttiker
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Pedro Mendes
|
||||
* @author Nikita Konev
|
||||
*/
|
||||
public class PageJacksonModule extends Module {
|
||||
|
||||
@@ -64,20 +68,25 @@ public class PageJacksonModule extends Module {
|
||||
private final Page<T> delegate;
|
||||
|
||||
SimplePageImpl(@JsonProperty("content") List<T> content, @JsonProperty("number") int number,
|
||||
@JsonProperty("size") int size, @JsonProperty("totalElements") long totalElements,
|
||||
@JsonProperty("size") int size, @JsonProperty("totalElements") @JsonAlias({ "total-elements",
|
||||
"total_elements", "totalelements", "TotalElements" }) long totalElements,
|
||||
@JsonProperty("sort") Sort sort) {
|
||||
PageRequest pageRequest;
|
||||
if (sort != null) {
|
||||
pageRequest = PageRequest.of(number, size, sort);
|
||||
if (size > 0) {
|
||||
PageRequest pageRequest;
|
||||
if (sort != null) {
|
||||
pageRequest = PageRequest.of(number, size, sort);
|
||||
}
|
||||
else {
|
||||
pageRequest = PageRequest.of(number, size);
|
||||
}
|
||||
delegate = new PageImpl<>(content, pageRequest, totalElements);
|
||||
}
|
||||
else {
|
||||
pageRequest = PageRequest.of(number, size);
|
||||
delegate = new PageImpl<>(content);
|
||||
}
|
||||
delegate = new PageImpl<>(content, pageRequest, totalElements);
|
||||
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public int getTotalPages() {
|
||||
return delegate.getTotalPages();
|
||||
@@ -101,7 +110,7 @@ public class PageJacksonModule extends Module {
|
||||
return delegate.getSize();
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public int getNumberOfElements() {
|
||||
return delegate.getNumberOfElements();
|
||||
@@ -125,13 +134,13 @@ public class PageJacksonModule extends Module {
|
||||
return delegate.getSort();
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public boolean isFirst() {
|
||||
return delegate.isFirst();
|
||||
}
|
||||
|
||||
@JsonProperty
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public boolean isLast() {
|
||||
return delegate.isLast();
|
||||
@@ -173,6 +182,18 @@ public class PageJacksonModule extends Module {
|
||||
return delegate.iterator();
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public Pageable getPageable() {
|
||||
return delegate.getPageable();
|
||||
}
|
||||
|
||||
@JsonIgnore
|
||||
@Override
|
||||
public boolean isEmpty() {
|
||||
return delegate.isEmpty();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -116,7 +116,7 @@ public class PageableSpringEncoder implements Encoder {
|
||||
}
|
||||
}
|
||||
for (Sort.Order order : sort) {
|
||||
sortQueries.add(order.getProperty() + "," + order.getDirection());
|
||||
sortQueries.add(order.getProperty() + "%2C" + order.getDirection());
|
||||
}
|
||||
if (!sortQueries.isEmpty()) {
|
||||
template.query(sortParameter, sortQueries);
|
||||
|
||||
+80
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign.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-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -26,16 +26,16 @@ import feign.Response;
|
||||
import feign.codec.Decoder;
|
||||
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.util.LinkedMultiValueMap;
|
||||
import org.springframework.util.MultiValueMap;
|
||||
|
||||
/**
|
||||
* Decoder adds compatibility for Spring MVC's ResponseEntity to any other decoder via
|
||||
* composition.
|
||||
*
|
||||
* @author chad jaros
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
public class ResponseEntityDecoder implements Decoder {
|
||||
|
||||
@@ -80,7 +80,7 @@ public class ResponseEntityDecoder implements Decoder {
|
||||
@SuppressWarnings("unchecked")
|
||||
private <T> ResponseEntity<T> createResponse(Object instance, Response response) {
|
||||
|
||||
MultiValueMap<String, String> headers = new LinkedMultiValueMap<>();
|
||||
HttpHeaders headers = new HttpHeaders();
|
||||
for (String key : response.headers().keySet()) {
|
||||
headers.put(key, new LinkedList<>(response.headers().get(key)));
|
||||
}
|
||||
|
||||
+103
-55
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -22,10 +22,11 @@ import java.io.OutputStream;
|
||||
import java.lang.reflect.Type;
|
||||
import java.nio.charset.Charset;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Arrays;
|
||||
import java.util.Collection;
|
||||
import java.util.Objects;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import feign.Request;
|
||||
import feign.RequestTemplate;
|
||||
import feign.codec.EncodeException;
|
||||
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.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
|
||||
@@ -55,7 +60,10 @@ import static org.springframework.cloud.openfeign.support.FeignUtils.getHttpHead
|
||||
* @author Ahmad Mozafarnia
|
||||
* @author Aaron Whiteside
|
||||
* @author Darren Foong
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Can Bezmen
|
||||
*/
|
||||
@SuppressWarnings("rawtypes")
|
||||
public class SpringEncoder implements Encoder {
|
||||
|
||||
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 FeignEncoderProperties encoderProperties;
|
||||
|
||||
public SpringEncoder(ObjectFactory<HttpMessageConverters> messageConverters) {
|
||||
this.springFormEncoder = new SpringFormEncoder();
|
||||
this.messageConverters = messageConverters;
|
||||
this(new SpringFormEncoder(), 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.messageConverters = messageConverters;
|
||||
this.encoderProperties = encoderProperties;
|
||||
}
|
||||
|
||||
@Override
|
||||
@@ -86,8 +101,8 @@ public class SpringEncoder implements Encoder {
|
||||
requestContentType = MediaType.valueOf(type);
|
||||
}
|
||||
|
||||
if (Objects.equals(requestContentType, MediaType.MULTIPART_FORM_DATA)) {
|
||||
this.springFormEncoder.encode(requestBody, bodyType, request);
|
||||
if (isFormRelatedContentType(requestContentType)) {
|
||||
springFormEncoder.encode(requestBody, bodyType, request);
|
||||
return;
|
||||
}
|
||||
else {
|
||||
@@ -96,54 +111,67 @@ public class SpringEncoder implements Encoder {
|
||||
+ "should be specified as MediaType.MULTIPART_FORM_DATA_VALUE");
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
encodeWithMessageConverter(requestBody, bodyType, request, requestContentType);
|
||||
}
|
||||
}
|
||||
|
||||
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")
|
||||
private FeignOutputMessage checkAndWrite(Object body, MediaType contentType, HttpMessageConverter converter,
|
||||
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 ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
|
||||
@@ -192,21 +240,21 @@ public class SpringEncoder implements Encoder {
|
||||
private final HttpHeaders httpHeaders;
|
||||
|
||||
private FeignOutputMessage(RequestTemplate request) {
|
||||
this.httpHeaders = getHttpHeaders(request.headers());
|
||||
httpHeaders = getHttpHeaders(request.headers());
|
||||
}
|
||||
|
||||
@Override
|
||||
public OutputStream getBody() throws IOException {
|
||||
return this.outputStream;
|
||||
return outputStream;
|
||||
}
|
||||
|
||||
@Override
|
||||
public HttpHeaders getHeaders() {
|
||||
return this.httpHeaders;
|
||||
return httpHeaders;
|
||||
}
|
||||
|
||||
public ByteArrayOutputStream getOutputStream() {
|
||||
return this.outputStream;
|
||||
return outputStream;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+10
-35
@@ -35,6 +35,8 @@ import feign.Feign;
|
||||
import feign.MethodMetadata;
|
||||
import feign.Param;
|
||||
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.CollectionFormat;
|
||||
@@ -82,6 +84,8 @@ import static org.springframework.core.annotation.AnnotatedElementUtils.findMerg
|
||||
*/
|
||||
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 CONTENT_TYPE = "Content-Type";
|
||||
@@ -169,47 +173,18 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
|
||||
|
||||
@Override
|
||||
protected void processAnnotationOnClass(MethodMetadata data, Class<?> clz) {
|
||||
if (clz.getInterfaces().length == 0) {
|
||||
RequestMapping classAnnotation = findMergedAnnotation(clz, RequestMapping.class);
|
||||
if (classAnnotation != null) {
|
||||
// Prepend path from class annotation if specified
|
||||
if (classAnnotation.value().length > 0) {
|
||||
String pathValue = emptyToNull(classAnnotation.value()[0]);
|
||||
pathValue = resolve(pathValue);
|
||||
if (!pathValue.startsWith("/")) {
|
||||
pathValue = "/" + pathValue;
|
||||
}
|
||||
data.template().uri(pathValue);
|
||||
if (data.template().decodeSlash() != decodeSlash) {
|
||||
data.template().decodeSlash(decodeSlash);
|
||||
}
|
||||
}
|
||||
}
|
||||
RequestMapping classAnnotation = findMergedAnnotation(clz, RequestMapping.class);
|
||||
if (classAnnotation != null) {
|
||||
LOG.error("Cannot process class: " + clz.getName()
|
||||
+ ". @RequestMapping annotation is not allowed on @FeignClient interfaces.");
|
||||
throw new IllegalArgumentException("@RequestMapping annotation not allowed on @FeignClient interfaces");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public MethodMetadata parseAndValidateMetadata(Class<?> targetType, Method method) {
|
||||
processedMethods.put(Feign.configKey(targetType, method), method);
|
||||
MethodMetadata md = 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;
|
||||
return super.parseAndValidateMetadata(targetType, method);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
+30
@@ -2,18 +2,36 @@
|
||||
"groups": [
|
||||
],
|
||||
"properties": [
|
||||
{
|
||||
"name": "feign.autoconfiguration.jackson.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
"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"
|
||||
},
|
||||
{
|
||||
"name": "feign.httpclient.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Enables the use of the Apache HTTP Client by Feign.",
|
||||
"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",
|
||||
"type": "java.lang.Boolean",
|
||||
@@ -37,6 +55,18 @@
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Enables the request sent by Feign to be compressed.",
|
||||
"defaultValue": "false"
|
||||
},
|
||||
{
|
||||
"name": "feign.metrics.enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Enables metrics capability for Feign.",
|
||||
"defaultValue": "true"
|
||||
},
|
||||
{
|
||||
"name": "feign.client.refresh-enabled",
|
||||
"type": "java.lang.Boolean",
|
||||
"description": "Enables options value refresh capability for Feign.",
|
||||
"defaultValue": "false"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
+111
@@ -0,0 +1,111 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import java.lang.reflect.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();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+19
@@ -33,6 +33,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Matt King
|
||||
* @author Sam Kruglov
|
||||
*/
|
||||
public class FeignBuilderCustomizerTests {
|
||||
|
||||
@@ -74,6 +75,24 @@ public class FeignBuilderCustomizerTests {
|
||||
context.close();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBuildCustomizerOrderedWithAdditional() {
|
||||
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
|
||||
FeignBuilderCustomizerTests.SampleConfiguration3.class);
|
||||
|
||||
FeignClientFactoryBean clientFactoryBean = context.getBean(FeignClientFactoryBean.class);
|
||||
clientFactoryBean.addCustomizer(builder -> builder.logLevel(Logger.Level.BASIC));
|
||||
clientFactoryBean.addCustomizer(Feign.Builder::doNotCloseAfterDecode);
|
||||
FeignContext feignContext = context.getBean(FeignContext.class);
|
||||
|
||||
Feign.Builder builder = clientFactoryBean.feign(feignContext);
|
||||
assertFeignBuilderField(builder, "logLevel", Logger.Level.BASIC);
|
||||
assertFeignBuilderField(builder, "decode404", true);
|
||||
assertFeignBuilderField(builder, "closeAfterDecode", false);
|
||||
|
||||
context.close();
|
||||
}
|
||||
|
||||
private static FeignClientFactoryBean defaultFeignClientFactoryBean() {
|
||||
FeignClientFactoryBean feignClientFactoryBean = new FeignClientFactoryBean();
|
||||
feignClientFactoryBean.setContextId("test");
|
||||
|
||||
+15
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -24,6 +24,7 @@ import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
import feign.Feign;
|
||||
import org.hamcrest.Matchers;
|
||||
import org.junit.Before;
|
||||
import org.junit.Rule;
|
||||
@@ -39,6 +40,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Sven Döring
|
||||
* @author Sam Kruglov
|
||||
*/
|
||||
public class FeignClientBuilderTests {
|
||||
|
||||
@@ -56,6 +58,12 @@ public class FeignClientBuilderTests {
|
||||
|
||||
private static void assertFactoryBeanField(final FeignClientBuilder.Builder builder, final String fieldName,
|
||||
final Object expectedValue) {
|
||||
final Object value = getFactoryBeanField(builder, fieldName);
|
||||
assertThat(value).as("Expected value for the field '" + fieldName + "':").isEqualTo(expectedValue);
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static <T> T getFactoryBeanField(final FeignClientBuilder.Builder builder, final String fieldName) {
|
||||
final Field factoryBeanField = ReflectionUtils.findField(FeignClientBuilder.Builder.class,
|
||||
"feignClientFactoryBean");
|
||||
ReflectionUtils.makeAccessible(factoryBeanField);
|
||||
@@ -64,8 +72,7 @@ public class FeignClientBuilderTests {
|
||||
|
||||
final Field field = ReflectionUtils.findField(FeignClientFactoryBean.class, fieldName);
|
||||
ReflectionUtils.makeAccessible(field);
|
||||
final Object value = ReflectionUtils.getField(field, factoryBean);
|
||||
assertThat(value).as("Expected value for the field '" + fieldName + "':").isEqualTo(expectedValue);
|
||||
return (T) ReflectionUtils.getField(field, factoryBean);
|
||||
}
|
||||
|
||||
@Before
|
||||
@@ -80,8 +87,8 @@ public class FeignClientBuilderTests {
|
||||
for (final Method method : FeignClient.class.getMethods()) {
|
||||
methodNames.add(method.getName());
|
||||
}
|
||||
methodNames.removeAll(Arrays.asList("annotationType", "value", "serviceId", "qualifier", "configuration",
|
||||
"primary", "equals", "hashCode", "toString"));
|
||||
methodNames.removeAll(Arrays.asList("annotationType", "value", "serviceId", "qualifier", "qualifiers",
|
||||
"configuration", "primary", "equals", "hashCode", "toString"));
|
||||
Collections.sort(methodNames);
|
||||
// If this safety check fails the Builder has to be updated.
|
||||
// (1) Either a field was removed from the FeignClient annotation and so it has to
|
||||
@@ -136,7 +143,7 @@ public class FeignClientBuilderTests {
|
||||
// when:
|
||||
final FeignClientBuilder.Builder builder = this.feignClientBuilder
|
||||
.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:
|
||||
assertFactoryBeanField(builder, "applicationContext", this.applicationContext);
|
||||
@@ -148,6 +155,8 @@ public class FeignClientBuilderTests {
|
||||
assertFactoryBeanField(builder, "url", "http://Url/");
|
||||
assertFactoryBeanField(builder, "path", "/Path");
|
||||
assertFactoryBeanField(builder, "decode404", true);
|
||||
List<FeignBuilderCustomizer> additionalCustomizers = getFactoryBeanField(builder, "additionalCustomizers");
|
||||
assertThat(additionalCustomizers).hasSize(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
||||
+140
@@ -0,0 +1,140 @@
|
||||
/*
|
||||
* Copyright 2021-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
import feign.Capability;
|
||||
import feign.Contract;
|
||||
import feign.ExceptionPropagationPolicy;
|
||||
import feign.Logger;
|
||||
import feign.RequestInterceptor;
|
||||
import feign.Retryer;
|
||||
import feign.codec.Decoder;
|
||||
import feign.codec.Encoder;
|
||||
import feign.codec.ErrorDecoder;
|
||||
import org.assertj.core.util.Lists;
|
||||
import org.assertj.core.util.Maps;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsAndHashCodeConsistency;
|
||||
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsConsistency;
|
||||
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsReflexivity;
|
||||
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsSymmetricity;
|
||||
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsTransitivity;
|
||||
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertHashCodeConsistency;
|
||||
|
||||
/**
|
||||
* @author Jonatan Ivanov
|
||||
*/
|
||||
class FeignClientConfigurationTests {
|
||||
|
||||
@Test
|
||||
void shouldDefaultToValuesWhenFieldsNotSet() {
|
||||
FeignClientProperties.FeignClientConfiguration config = new FeignClientProperties.FeignClientConfiguration();
|
||||
|
||||
assertThat(config.getLoggerLevel()).isNull();
|
||||
assertThat(config.getConnectTimeout()).isNull();
|
||||
assertThat(config.getReadTimeout()).isNull();
|
||||
assertThat(config.getRetryer()).isNull();
|
||||
assertThat(config.getErrorDecoder()).isNull();
|
||||
assertThat(config.getRequestInterceptors()).isNull();
|
||||
assertThat(config.getDefaultRequestHeaders()).isNull();
|
||||
assertThat(config.getDefaultQueryParameters()).isNull();
|
||||
assertThat(config.getDecode404()).isNull();
|
||||
assertThat(config.getDecoder()).isNull();
|
||||
assertThat(config.getEncoder()).isNull();
|
||||
assertThat(config.getContract()).isNull();
|
||||
assertThat(config.getExceptionPropagationPolicy()).isNull();
|
||||
assertThat(config.getCapabilities()).isNull();
|
||||
assertThat(config.getMetrics()).isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnValuesWhenSet() {
|
||||
FeignClientProperties.FeignClientConfiguration config = new FeignClientProperties.FeignClientConfiguration();
|
||||
config.setLoggerLevel(Logger.Level.FULL);
|
||||
config.setConnectTimeout(21);
|
||||
config.setReadTimeout(42);
|
||||
config.setRetryer(Retryer.class);
|
||||
config.setErrorDecoder(ErrorDecoder.class);
|
||||
List<Class<RequestInterceptor>> requestInterceptors = Lists.list(RequestInterceptor.class);
|
||||
config.setRequestInterceptors(requestInterceptors);
|
||||
Map<String, Collection<String>> defaultRequestHeaders = Maps.newHashMap("default", Lists.emptyList());
|
||||
config.setDefaultRequestHeaders(defaultRequestHeaders);
|
||||
Map<String, Collection<String>> defaultQueryParameters = Maps.newHashMap("default", Lists.emptyList());
|
||||
config.setDefaultQueryParameters(defaultQueryParameters);
|
||||
config.setDecode404(true);
|
||||
config.setDecoder(Decoder.class);
|
||||
config.setEncoder(Encoder.class);
|
||||
config.setContract(Contract.class);
|
||||
config.setExceptionPropagationPolicy(ExceptionPropagationPolicy.UNWRAP);
|
||||
List<Class<Capability>> capabilities = Lists.list(Capability.class);
|
||||
config.setCapabilities(capabilities);
|
||||
FeignClientProperties.MetricsProperties metrics = new FeignClientProperties.MetricsProperties();
|
||||
config.setMetrics(metrics);
|
||||
|
||||
assertThat(config.getLoggerLevel()).isSameAs(Logger.Level.FULL);
|
||||
assertThat(config.getConnectTimeout()).isEqualTo(21);
|
||||
assertThat(config.getReadTimeout()).isEqualTo(42);
|
||||
assertThat(config.getRetryer()).isSameAs(Retryer.class);
|
||||
assertThat(config.getErrorDecoder()).isSameAs(ErrorDecoder.class);
|
||||
assertThat(config.getRequestInterceptors()).isSameAs(requestInterceptors);
|
||||
assertThat(config.getDefaultRequestHeaders()).isSameAs(defaultRequestHeaders);
|
||||
assertThat(config.getDefaultQueryParameters()).isSameAs(defaultQueryParameters);
|
||||
assertThat(config.getDecode404()).isTrue();
|
||||
assertThat(config.getDecoder()).isSameAs(Decoder.class);
|
||||
assertThat(config.getEncoder()).isSameAs(Encoder.class);
|
||||
assertThat(config.getContract()).isSameAs(Contract.class);
|
||||
assertThat(config.getExceptionPropagationPolicy()).isSameAs(ExceptionPropagationPolicy.UNWRAP);
|
||||
assertThat(config.getCapabilities()).isSameAs(capabilities);
|
||||
assertThat(config.getMetrics()).isSameAs(metrics);
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanity-checks equals and hashCode contracts but does not check every variation of
|
||||
* the fields.
|
||||
*/
|
||||
@Test
|
||||
void shouldHaveSomewhatValidEqualsAndHashCode() {
|
||||
FeignClientProperties.FeignClientConfiguration configOne = new FeignClientProperties.FeignClientConfiguration();
|
||||
FeignClientProperties.FeignClientConfiguration configTwo = new FeignClientProperties.FeignClientConfiguration();
|
||||
FeignClientProperties.FeignClientConfiguration configThree = new FeignClientProperties.FeignClientConfiguration();
|
||||
FeignClientProperties.FeignClientConfiguration differentConfig = new FeignClientProperties.FeignClientConfiguration();
|
||||
differentConfig.setDecode404(true);
|
||||
|
||||
assertEqualsReflexivity(configOne);
|
||||
|
||||
assertEqualsSymmetricity(configOne, configTwo);
|
||||
assertEqualsSymmetricity(configOne, differentConfig);
|
||||
assertEqualsSymmetricity(configOne, 42);
|
||||
|
||||
assertEqualsTransitivity(configOne, configTwo, configThree);
|
||||
|
||||
assertEqualsConsistency(configOne, configTwo);
|
||||
assertEqualsConsistency(configOne, differentConfig);
|
||||
assertEqualsConsistency(configOne, 42);
|
||||
assertEqualsConsistency(configOne, null);
|
||||
|
||||
assertHashCodeConsistency(configOne);
|
||||
assertEqualsAndHashCodeConsistency(configOne, configTwo);
|
||||
}
|
||||
|
||||
}
|
||||
+120
@@ -0,0 +1,120 @@
|
||||
/*
|
||||
* Copyright 2021-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import feign.Capability;
|
||||
import feign.Contract;
|
||||
import feign.RequestLine;
|
||||
import feign.micrometer.MicrometerCapability;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.web.bind.annotation.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-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import feign.Capability;
|
||||
import feign.Contract;
|
||||
import feign.RequestLine;
|
||||
import feign.micrometer.MicrometerCapability;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.ActiveProfiles;
|
||||
import org.springframework.web.bind.annotation.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 {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+10
-8
@@ -25,8 +25,9 @@ import feign.InvocationHandlerFactory;
|
||||
import feign.RequestLine;
|
||||
import feign.Response;
|
||||
import feign.codec.ErrorDecoder;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.condition.DisabledForJreRange;
|
||||
import org.junit.jupiter.api.condition.JRE;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
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.Configuration;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Michael Cramer
|
||||
* @author Jonatan Ivanov
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringBootTest(classes = FeignClientErrorDecoderTests.TestConfiguration.class)
|
||||
@DirtiesContext
|
||||
public class FeignClientErrorDecoderTests {
|
||||
@@ -72,12 +71,14 @@ public class FeignClientErrorDecoderTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisabledForJreRange(min = JRE.JAVA_16)
|
||||
public void useConfiguredErrorDecoderWhenAlsoErrorDecoderFactoryIsAvailable() {
|
||||
Object errorDecoder = getErrorDecoderFromClient(this.foo);
|
||||
assertThat(errorDecoder).isInstanceOf(ErrorDecoder.Default.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisabledForJreRange(min = JRE.JAVA_16)
|
||||
public void useErrorDecoderFromErrorDecoderFactory() {
|
||||
Object errorDecoder = getErrorDecoderFromClient(this.bar);
|
||||
assertThat(errorDecoder).isInstanceOf(ErrorDecoderImpl.class);
|
||||
@@ -85,7 +86,8 @@ public class FeignClientErrorDecoderTests {
|
||||
|
||||
@SuppressWarnings({ "unchecked", "ConstantConditions" })
|
||||
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
|
||||
.getField(invocationHandler, "dispatch");
|
||||
Method key = new ArrayList<>(dispatch.keySet()).get(0);
|
||||
@@ -130,7 +132,7 @@ public class FeignClientErrorDecoderTests {
|
||||
@FeignClient(name = "bar", url = "http://bar", configuration = BarConfiguration.class)
|
||||
interface BarClient {
|
||||
|
||||
@RequestMapping(value = "/", method = RequestMethod.GET)
|
||||
@GetMapping("/")
|
||||
String get();
|
||||
|
||||
}
|
||||
|
||||
+6
-4
@@ -25,7 +25,9 @@ import java.util.Map;
|
||||
|
||||
import feign.Client;
|
||||
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.runner.ApplicationContextRunner;
|
||||
@@ -37,10 +39,9 @@ import org.springframework.context.annotation.AnnotationConfigApplicationContext
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
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.springframework.web.bind.annotation.RequestMethod.GET;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
@@ -66,6 +67,7 @@ public class FeignClientFactoryTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisabledForJreRange(min = JRE.JAVA_16)
|
||||
public void shouldRedirectToDelegateWhenUrlSet() {
|
||||
new ApplicationContextRunner().withUserConfiguration(TestConfig.class).run(this::defaultClientUsed);
|
||||
}
|
||||
@@ -87,7 +89,7 @@ public class FeignClientFactoryTests {
|
||||
|
||||
interface TestType {
|
||||
|
||||
@RequestMapping(value = "/", method = GET)
|
||||
@GetMapping("/")
|
||||
String hello();
|
||||
|
||||
}
|
||||
|
||||
+225
@@ -0,0 +1,225 @@
|
||||
/*
|
||||
* Copyright 2021-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import java.util.HashMap;
|
||||
|
||||
import org.assertj.core.util.Maps;
|
||||
import org.junit.jupiter.api.AfterEach;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.junit.jupiter.MockitoExtension;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.context.annotation.ConditionContext;
|
||||
import org.springframework.core.env.Environment;
|
||||
import org.springframework.core.type.AnnotatedTypeMetadata;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.mockito.Mockito.mock;
|
||||
import static org.mockito.Mockito.never;
|
||||
import static org.mockito.Mockito.verify;
|
||||
import static org.mockito.Mockito.when;
|
||||
|
||||
/**
|
||||
* @author Jonatan Ivanov
|
||||
*/
|
||||
@ExtendWith({ MockitoExtension.class })
|
||||
class FeignClientMetricsEnabledConditionTests {
|
||||
|
||||
@Mock
|
||||
private ConditionContext context;
|
||||
|
||||
@Mock
|
||||
private AnnotatedTypeMetadata metadata;
|
||||
|
||||
@Mock
|
||||
private ConfigurableListableBeanFactory beanFactory;
|
||||
|
||||
@Mock
|
||||
private ObjectProvider<FeignClientProperties> beanProvider;
|
||||
|
||||
@Mock
|
||||
private Environment environment;
|
||||
|
||||
private final FeignClientMetricsEnabledCondition condition = new FeignClientMetricsEnabledCondition();
|
||||
|
||||
@BeforeEach
|
||||
void setUp() {
|
||||
when(context.getBeanFactory()).thenReturn(beanFactory);
|
||||
when(beanFactory.getBeanProvider(FeignClientProperties.class)).thenReturn(beanProvider);
|
||||
}
|
||||
|
||||
@AfterEach
|
||||
void tearDown() {
|
||||
verify(context).getBeanFactory();
|
||||
verify(beanFactory).getBeanProvider(FeignClientProperties.class);
|
||||
verify(beanProvider).getIfAvailable();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchWhenFeignClientPropertiesBeanIsMissing() {
|
||||
when(beanProvider.getIfAvailable()).thenReturn(null);
|
||||
|
||||
assertThat(condition.matches(context, metadata)).isTrue();
|
||||
verify(environment, never()).getProperty("feign.client.name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchWhenConfigMapIsMissing() {
|
||||
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
|
||||
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
|
||||
when(feignClientProperties.getConfig()).thenReturn(null);
|
||||
|
||||
assertThat(condition.matches(context, metadata)).isTrue();
|
||||
verify(environment, never()).getProperty("feign.client.name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchWhenConfigMapDoesNotContainTheConfig() {
|
||||
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
|
||||
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
|
||||
when(context.getEnvironment()).thenReturn(environment);
|
||||
when(environment.getProperty("feign.client.name")).thenReturn("foo");
|
||||
when(feignClientProperties.getConfig()).thenReturn(new HashMap<>());
|
||||
|
||||
assertThat(condition.matches(context, metadata)).isTrue();
|
||||
verify(environment).getProperty("feign.client.name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchWhenClientNameIsNull() {
|
||||
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
|
||||
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
|
||||
when(context.getEnvironment()).thenReturn(environment);
|
||||
when(environment.getProperty("feign.client.name")).thenReturn(null);
|
||||
when(feignClientProperties.getConfig()).thenReturn(new HashMap<>());
|
||||
|
||||
assertThat(condition.matches(context, metadata)).isTrue();
|
||||
verify(environment).getProperty("feign.client.name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchWhenClientNameIsEmpty() {
|
||||
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
|
||||
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
|
||||
when(context.getEnvironment()).thenReturn(environment);
|
||||
when(environment.getProperty("feign.client.name")).thenReturn("");
|
||||
when(feignClientProperties.getConfig()).thenReturn(new HashMap<>());
|
||||
|
||||
assertThat(condition.matches(context, metadata)).isTrue();
|
||||
|
||||
verify(environment).getProperty("feign.client.name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchWhenConfigMapContainsNullConfig() {
|
||||
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
|
||||
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
|
||||
when(context.getEnvironment()).thenReturn(environment);
|
||||
when(environment.getProperty("feign.client.name")).thenReturn("foo");
|
||||
when(feignClientProperties.getConfig()).thenReturn(Maps.newHashMap("foo", null));
|
||||
|
||||
assertThat(condition.matches(context, metadata)).isTrue();
|
||||
verify(environment).getProperty("feign.client.name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchWhenMetricsConfigurationIsMissing() {
|
||||
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
|
||||
FeignClientProperties.FeignClientConfiguration feignClientConfig = mock(
|
||||
FeignClientProperties.FeignClientConfiguration.class);
|
||||
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
|
||||
when(context.getEnvironment()).thenReturn(environment);
|
||||
when(environment.getProperty("feign.client.name")).thenReturn("foo");
|
||||
when(feignClientProperties.getConfig()).thenReturn(Maps.newHashMap("foo", feignClientConfig));
|
||||
when(feignClientConfig.getMetrics()).thenReturn(null);
|
||||
|
||||
assertThat(condition.matches(context, metadata)).isTrue();
|
||||
verify(environment).getProperty("feign.client.name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchWhenEnabledFlagIsNotSet() {
|
||||
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
|
||||
FeignClientProperties.FeignClientConfiguration feignClientConfig = mock(
|
||||
FeignClientProperties.FeignClientConfiguration.class);
|
||||
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
|
||||
when(context.getEnvironment()).thenReturn(environment);
|
||||
when(environment.getProperty("feign.client.name")).thenReturn("foo");
|
||||
when(feignClientProperties.getConfig()).thenReturn(Maps.newHashMap("foo", feignClientConfig));
|
||||
when(feignClientConfig.getMetrics()).thenReturn(new FeignClientProperties.MetricsProperties());
|
||||
|
||||
assertThat(condition.matches(context, metadata)).isTrue();
|
||||
verify(environment).getProperty("feign.client.name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchWhenEnabledFlagIsNull() {
|
||||
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
|
||||
FeignClientProperties.FeignClientConfiguration feignClientConfig = mock(
|
||||
FeignClientProperties.FeignClientConfiguration.class);
|
||||
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
|
||||
when(context.getEnvironment()).thenReturn(environment);
|
||||
when(environment.getProperty("feign.client.name")).thenReturn("foo");
|
||||
when(feignClientProperties.getConfig()).thenReturn(Maps.newHashMap("foo", feignClientConfig));
|
||||
FeignClientProperties.MetricsProperties metricsProperties = new FeignClientProperties.MetricsProperties();
|
||||
metricsProperties.setEnabled(null);
|
||||
when(feignClientConfig.getMetrics()).thenReturn(metricsProperties);
|
||||
|
||||
assertThat(condition.matches(context, metadata)).isTrue();
|
||||
verify(environment).getProperty("feign.client.name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldMatchWhenMetricsConfigurationIsEnabled() {
|
||||
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
|
||||
FeignClientProperties.FeignClientConfiguration feignClientConfig = mock(
|
||||
FeignClientProperties.FeignClientConfiguration.class);
|
||||
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
|
||||
when(context.getEnvironment()).thenReturn(environment);
|
||||
when(environment.getProperty("feign.client.name")).thenReturn("foo");
|
||||
when(feignClientProperties.getConfig()).thenReturn(Maps.newHashMap("foo", feignClientConfig));
|
||||
FeignClientProperties.MetricsProperties metricsProperties = new FeignClientProperties.MetricsProperties();
|
||||
metricsProperties.setEnabled(true);
|
||||
when(feignClientConfig.getMetrics()).thenReturn(metricsProperties);
|
||||
|
||||
assertThat(condition.matches(context, metadata)).isTrue();
|
||||
verify(environment).getProperty("feign.client.name");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotMatchWhenMetricsConfigurationIsEnabled() {
|
||||
FeignClientProperties feignClientProperties = mock(FeignClientProperties.class);
|
||||
FeignClientProperties.FeignClientConfiguration feignClientConfig = mock(
|
||||
FeignClientProperties.FeignClientConfiguration.class);
|
||||
when(beanProvider.getIfAvailable()).thenReturn(feignClientProperties);
|
||||
when(context.getEnvironment()).thenReturn(environment);
|
||||
when(environment.getProperty("feign.client.name")).thenReturn("foo");
|
||||
when(feignClientProperties.getConfig()).thenReturn(Maps.newHashMap("foo", feignClientConfig));
|
||||
FeignClientProperties.MetricsProperties metricsProperties = new FeignClientProperties.MetricsProperties();
|
||||
metricsProperties.setEnabled(false);
|
||||
when(feignClientConfig.getMetrics()).thenReturn(metricsProperties);
|
||||
|
||||
assertThat(condition.matches(context, metadata)).isFalse();
|
||||
verify(environment).getProperty("feign.client.name");
|
||||
}
|
||||
|
||||
}
|
||||
+46
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,10 @@
|
||||
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import feign.Capability;
|
||||
import feign.Contract;
|
||||
import feign.ExceptionPropagationPolicy;
|
||||
import feign.Logger;
|
||||
@@ -28,6 +32,7 @@ import feign.auth.BasicAuthRequestInterceptor;
|
||||
import feign.codec.Decoder;
|
||||
import feign.codec.Encoder;
|
||||
import feign.codec.ErrorDecoder;
|
||||
import feign.micrometer.MicrometerCapability;
|
||||
import feign.optionals.OptionalDecoder;
|
||||
import feign.querymap.BeanQueryMapEncoder;
|
||||
import feign.slf4j.Slf4jLogger;
|
||||
@@ -41,13 +46,14 @@ import org.springframework.cloud.openfeign.support.SpringMvcContract;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
* @author Jonatan Ivanov
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@SpringBootTest(classes = FeignClientOverrideDefaultsTests.TestConfiguration.class)
|
||||
@DirtiesContext
|
||||
@@ -116,6 +122,7 @@ class FeignClientOverrideDefaultsTests {
|
||||
Request.Options options = context.getInstance("bar", Request.Options.class);
|
||||
assertThat(options.connectTimeoutMillis()).isEqualTo(1);
|
||||
assertThat(options.readTimeoutMillis()).isEqualTo(1);
|
||||
assertThat(options.isFollowRedirects()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -137,6 +144,22 @@ class FeignClientOverrideDefaultsTests {
|
||||
.containsValues(ExceptionPropagationPolicy.UNWRAP);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldOverrideMicrometerCapability() {
|
||||
assertThat(context.getInstance("foo", MicrometerCapability.class))
|
||||
.isExactlyInstanceOf(TestMicrometerCapability.class);
|
||||
Map<String, Capability> fooCapabilities = context.getInstances("foo", Capability.class);
|
||||
assertThat(fooCapabilities).hasSize(1);
|
||||
assertThat(fooCapabilities.get("micrometerCapability")).isExactlyInstanceOf(TestMicrometerCapability.class);
|
||||
|
||||
assertThat(context.getInstance("bar", MicrometerCapability.class))
|
||||
.isExactlyInstanceOf(TestMicrometerCapability.class);
|
||||
Map<String, Capability> barCapabilities = context.getInstances("bar", Capability.class);
|
||||
assertThat(barCapabilities).hasSize(2);
|
||||
assertThat(barCapabilities.get("micrometerCapability")).isExactlyInstanceOf(TestMicrometerCapability.class);
|
||||
assertThat(barCapabilities.get("noOpCapability")).isExactlyInstanceOf(NoOpCapability.class);
|
||||
}
|
||||
|
||||
@FeignClient(name = "foo", url = "https://foo", configuration = FooConfiguration.class)
|
||||
interface FooClient {
|
||||
|
||||
@@ -148,7 +171,7 @@ class FeignClientOverrideDefaultsTests {
|
||||
@FeignClient(name = "bar", url = "https://bar", configuration = BarConfiguration.class)
|
||||
interface BarClient {
|
||||
|
||||
@RequestMapping(value = "/", method = RequestMethod.GET)
|
||||
@GetMapping("/")
|
||||
String get();
|
||||
|
||||
}
|
||||
@@ -164,6 +187,11 @@ class FeignClientOverrideDefaultsTests {
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
MicrometerCapability micrometerCapability() {
|
||||
return new TestMicrometerCapability();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class FooConfiguration {
|
||||
@@ -214,7 +242,7 @@ class FeignClientOverrideDefaultsTests {
|
||||
|
||||
@Bean
|
||||
Request.Options feignRequestOptions() {
|
||||
return new Request.Options(1, 1);
|
||||
return new Request.Options(1, TimeUnit.MILLISECONDS, 1, TimeUnit.MILLISECONDS, false);
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -232,6 +260,19 @@ class FeignClientOverrideDefaultsTests {
|
||||
return ExceptionPropagationPolicy.UNWRAP;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Capability noOpCapability() {
|
||||
return new NoOpCapability();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
private static class TestMicrometerCapability extends feign.micrometer.MicrometerCapability {
|
||||
|
||||
}
|
||||
|
||||
private static class NoOpCapability implements Capability {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+90
@@ -0,0 +1,90 @@
|
||||
/*
|
||||
* Copyright 2021-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
import org.assertj.core.util.Maps;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsAndHashCodeConsistency;
|
||||
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsConsistency;
|
||||
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsReflexivity;
|
||||
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsSymmetricity;
|
||||
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertEqualsTransitivity;
|
||||
import static org.springframework.cloud.openfeign.test.EqualsAndHashCodeAssert.assertHashCodeConsistency;
|
||||
|
||||
/**
|
||||
* @author Jonatan Ivanov
|
||||
*/
|
||||
class FeignClientPropertiesTests {
|
||||
|
||||
@Test
|
||||
void shouldDefaultToValuesWhenFieldsNotSet() {
|
||||
FeignClientProperties properties = new FeignClientProperties();
|
||||
assertThat(properties.isDefaultToProperties()).isTrue();
|
||||
assertThat(properties.getDefaultConfig()).isEqualTo("default");
|
||||
assertThat(properties.getConfig()).isEmpty();
|
||||
assertThat(properties.isDecodeSlash()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldReturnValuesWhenSet() {
|
||||
FeignClientProperties properties = new FeignClientProperties();
|
||||
properties.setDefaultToProperties(false);
|
||||
properties.setDefaultConfig("custom");
|
||||
Map<String, FeignClientProperties.FeignClientConfiguration> configMap = Maps.newHashMap("foo", null);
|
||||
properties.setConfig(configMap);
|
||||
properties.setDecodeSlash(false);
|
||||
|
||||
assertThat(properties.isDefaultToProperties()).isFalse();
|
||||
assertThat(properties.getDefaultConfig()).isEqualTo("custom");
|
||||
assertThat(properties.getConfig()).isSameAs(configMap);
|
||||
assertThat(properties.isDecodeSlash()).isFalse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanity-checks equals and hashCode contracts but does not check every variation of
|
||||
* the fields.
|
||||
*/
|
||||
@Test
|
||||
void shouldHaveSomewhatValidEqualsAndHashCode() {
|
||||
FeignClientProperties propsOne = new FeignClientProperties();
|
||||
FeignClientProperties propsTwo = new FeignClientProperties();
|
||||
FeignClientProperties propsThree = new FeignClientProperties();
|
||||
FeignClientProperties differentProps = new FeignClientProperties();
|
||||
differentProps.setDecodeSlash(false);
|
||||
|
||||
assertEqualsReflexivity(propsOne);
|
||||
|
||||
assertEqualsSymmetricity(propsOne, propsTwo);
|
||||
assertEqualsSymmetricity(propsOne, differentProps);
|
||||
assertEqualsSymmetricity(propsOne, 42);
|
||||
|
||||
assertEqualsTransitivity(propsOne, propsTwo, propsThree);
|
||||
|
||||
assertEqualsConsistency(propsOne, propsTwo);
|
||||
assertEqualsConsistency(propsOne, differentProps);
|
||||
assertEqualsConsistency(propsOne, 42);
|
||||
assertEqualsConsistency(propsOne, null);
|
||||
|
||||
assertHashCodeConsistency(propsOne);
|
||||
assertEqualsAndHashCodeConsistency(propsOne, propsTwo);
|
||||
}
|
||||
|
||||
}
|
||||
+41
-10
@@ -19,17 +19,19 @@ package org.springframework.cloud.openfeign;
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.List;
|
||||
|
||||
import feign.Capability;
|
||||
import feign.Feign;
|
||||
import feign.Logger;
|
||||
import feign.RequestInterceptor;
|
||||
import feign.micrometer.MicrometerCapability;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.openfeign.clientconfig.FeignClientConfigurer;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
@@ -40,6 +42,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author matt king
|
||||
* @author Jonatan Ivanov
|
||||
*/
|
||||
@DirtiesContext
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@@ -49,23 +52,28 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
"feign.client.config.default.requestInterceptors[1]=org.springframework.cloud.openfeign.FeignClientUsingPropertiesTests.BarRequestInterceptor" })
|
||||
public class FeignClientUsingConfigurerTest {
|
||||
|
||||
private static final String BEAN_NAME_PREFIX = "&org.springframework.cloud.openfeign.FeignClientUsingConfigurerTest$";
|
||||
private static final String BEAN_NAME_PREFIX = "org.springframework.cloud.openfeign.FeignClientUsingConfigurerTest$";
|
||||
|
||||
@Autowired
|
||||
private ApplicationContext applicationContext;
|
||||
private ConfigurableListableBeanFactory beanFactory;
|
||||
|
||||
@Autowired
|
||||
private FeignContext context;
|
||||
|
||||
@Test
|
||||
public void testFeignClient() {
|
||||
FeignClientFactoryBean factoryBean = (FeignClientFactoryBean) applicationContext
|
||||
.getBean(BEAN_NAME_PREFIX + "TestFeignClient");
|
||||
FeignClientFactoryBean factoryBean = (FeignClientFactoryBean) beanFactory
|
||||
.getBeanDefinition(BEAN_NAME_PREFIX + "TestFeignClient")
|
||||
.getAttribute("feignClientsRegistrarFactoryBean");
|
||||
Feign.Builder builder = factoryBean.feign(context);
|
||||
|
||||
List<RequestInterceptor> interceptors = (List) getBuilderValue(builder, "requestInterceptors");
|
||||
assertThat(interceptors.size()).as("interceptors not set").isEqualTo(3);
|
||||
assertThat(getBuilderValue(builder, "logLevel")).as("log level not set").isEqualTo(Logger.Level.FULL);
|
||||
|
||||
List<Capability> capabilities = (List) getBuilderValue(builder, "capabilities");
|
||||
assertThat(capabilities).hasSize(2).hasAtLeastOneElementOfType(NoOpCapability.class)
|
||||
.hasAtLeastOneElementOfType(MicrometerCapability.class);
|
||||
}
|
||||
|
||||
private Object getBuilderValue(Feign.Builder builder, String member) {
|
||||
@@ -77,23 +85,32 @@ public class FeignClientUsingConfigurerTest {
|
||||
|
||||
@Test
|
||||
public void testNoInheritFeignClient() {
|
||||
FeignClientFactoryBean factoryBean = (FeignClientFactoryBean) applicationContext
|
||||
.getBean(BEAN_NAME_PREFIX + "NoInheritFeignClient");
|
||||
FeignClientFactoryBean factoryBean = (FeignClientFactoryBean) beanFactory
|
||||
.getBeanDefinition(BEAN_NAME_PREFIX + "NoInheritFeignClient")
|
||||
.getAttribute("feignClientsRegistrarFactoryBean");
|
||||
Feign.Builder builder = factoryBean.feign(context);
|
||||
|
||||
List<RequestInterceptor> interceptors = (List) getBuilderValue(builder, "requestInterceptors");
|
||||
|
||||
assertThat(interceptors).as("interceptors not set").isEmpty();
|
||||
assertThat(factoryBean.isInheritParentContext()).as("is inheriting from parent configuration").isFalse();
|
||||
|
||||
List<Capability> capabilities = (List) getBuilderValue(builder, "capabilities");
|
||||
assertThat(capabilities).hasSize(2).hasAtLeastOneElementOfType(NoOpCapability.class)
|
||||
.hasAtLeastOneElementOfType(MicrometerCapability.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoInheritFeignClient_ignoreProperties() {
|
||||
FeignClientFactoryBean factoryBean = (FeignClientFactoryBean) applicationContext
|
||||
.getBean(BEAN_NAME_PREFIX + "NoInheritFeignClient");
|
||||
FeignClientFactoryBean factoryBean = (FeignClientFactoryBean) beanFactory
|
||||
.getBeanDefinition(BEAN_NAME_PREFIX + "NoInheritFeignClient")
|
||||
.getAttribute("feignClientsRegistrarFactoryBean");
|
||||
Feign.Builder builder = factoryBean.feign(context);
|
||||
|
||||
assertThat(getBuilderValue(builder, "logLevel")).as("log level not set").isEqualTo(Logger.Level.HEADERS);
|
||||
|
||||
List<Capability> capabilities = (List) getBuilderValue(builder, "capabilities");
|
||||
assertThat(capabilities).hasSize(2).hasAtLeastOneElementOfType(NoOpCapability.class)
|
||||
.hasAtLeastOneElementOfType(MicrometerCapability.class);
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@@ -107,6 +124,11 @@ public class FeignClientUsingConfigurerTest {
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
public NoOpCapability noOpCapability() {
|
||||
return new NoOpCapability();
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class NoInheritConfiguration {
|
||||
@@ -116,6 +138,11 @@ public class FeignClientUsingConfigurerTest {
|
||||
return Logger.Level.HEADERS;
|
||||
}
|
||||
|
||||
@Bean
|
||||
public NoOpCapability noOpCapability() {
|
||||
return new NoOpCapability();
|
||||
}
|
||||
|
||||
@Bean
|
||||
public FeignClientConfigurer feignClientConfigurer() {
|
||||
return new FeignClientConfigurer() {
|
||||
@@ -140,4 +167,8 @@ public class FeignClientUsingConfigurerTest {
|
||||
|
||||
}
|
||||
|
||||
private static class NoOpCapability implements Capability {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+51
-18
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -34,6 +34,8 @@ import java.util.stream.Stream;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import feign.Capability;
|
||||
import feign.Feign;
|
||||
import feign.InvocationHandlerFactory;
|
||||
import feign.Request;
|
||||
import feign.RequestInterceptor;
|
||||
@@ -43,8 +45,10 @@ import feign.Retryer;
|
||||
import feign.codec.EncodeException;
|
||||
import feign.codec.Encoder;
|
||||
import feign.codec.ErrorDecoder;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
import feign.micrometer.MicrometerCapability;
|
||||
import org.junit.jupiter.api.Test;
|
||||
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.Value;
|
||||
@@ -58,27 +62,24 @@ import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.test.util.ReflectionTestUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.fail;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
|
||||
|
||||
/**
|
||||
* @author Eko Kurniawan Khannedy
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Ilia Ilinykh
|
||||
* @author Jonatan Ivanov
|
||||
*/
|
||||
@SuppressWarnings("FieldMayBeFinal")
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringBootTest(classes = FeignClientUsingPropertiesTests.Application.class, webEnvironment = RANDOM_PORT)
|
||||
@TestPropertySource("classpath:feign-properties.properties")
|
||||
@DirtiesContext
|
||||
@@ -157,16 +158,14 @@ public class FeignClientUsingPropertiesTests {
|
||||
assertThat(response).isEqualTo("OK");
|
||||
}
|
||||
|
||||
@Test(expected = RetryableException.class)
|
||||
@Test
|
||||
public void testBar() {
|
||||
barClient().bar();
|
||||
fail("it should timeout");
|
||||
assertThatThrownBy(() -> barClient().bar()).isInstanceOf(RetryableException.class);
|
||||
}
|
||||
|
||||
@Test(expected = SocketTimeoutException.class)
|
||||
@Test
|
||||
public void testUnwrap() throws Exception {
|
||||
unwrapClient().unwrap();
|
||||
fail("it should timeout");
|
||||
assertThatThrownBy(() -> unwrapClient().unwrap()).isInstanceOf(SocketTimeoutException.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -201,6 +200,7 @@ public class FeignClientUsingPropertiesTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisabledForJreRange(min = JRE.JAVA_16)
|
||||
public void readTimeoutShouldWorkWhenConnectTimeoutNotSet() {
|
||||
FeignClientFactoryBean readTimeoutFactoryBean = new FeignClientFactoryBean();
|
||||
readTimeoutFactoryBean.setContextId("readTimeout");
|
||||
@@ -217,6 +217,7 @@ public class FeignClientUsingPropertiesTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisabledForJreRange(min = JRE.JAVA_16)
|
||||
public void connectTimeoutShouldWorkWhenReadTimeoutNotSet() {
|
||||
FeignClientFactoryBean readTimeoutFactoryBean = new FeignClientFactoryBean();
|
||||
readTimeoutFactoryBean.setContextId("connectTimeout");
|
||||
@@ -232,8 +233,37 @@ public class FeignClientUsingPropertiesTests {
|
||||
assertThat(options.readTimeoutMillis()).isEqualTo(5000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void clientShouldContainCapabilities() {
|
||||
fooFactoryBean.setApplicationContext(applicationContext);
|
||||
Feign.Builder feignBuilder = fooFactoryBean.feign(context);
|
||||
FooClient fooClient = feignBuilder.target(FooClient.class, "http://localhost:" + port);
|
||||
|
||||
String response = fooClient.foo();
|
||||
assertThat(response).isEqualTo("OK");
|
||||
List<Capability> capabilities = (List) ReflectionTestUtils.getField(feignBuilder, "capabilities");
|
||||
assertThat(capabilities).hasSize(2).hasAtLeastOneElementOfType(NoOpCapability.class)
|
||||
.hasAtLeastOneElementOfType(MicrometerCapability.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
@DisabledForJreRange(min = JRE.JAVA_16)
|
||||
public void shouldSetFollowRedirects() {
|
||||
FeignClientFactoryBean testFactoryBean = new FeignClientFactoryBean();
|
||||
testFactoryBean.setContextId("test");
|
||||
testFactoryBean.setType(FeignClientFactoryBean.class);
|
||||
testFactoryBean.setApplicationContext(applicationContext);
|
||||
|
||||
TimeoutClient client = testFactoryBean.feign(context).target(TimeoutClient.class, "http://localhost:" + port);
|
||||
|
||||
Request.Options options = getRequestOptions((Proxy) client);
|
||||
|
||||
assertThat(options.isFollowRedirects()).isFalse();
|
||||
}
|
||||
|
||||
private Request.Options getRequestOptions(Proxy 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
|
||||
.getField(Objects.requireNonNull(invocationHandler), "dispatch");
|
||||
Method key = new ArrayList<>(dispatch.keySet()).get(0);
|
||||
@@ -263,8 +293,7 @@ public class FeignClientUsingPropertiesTests {
|
||||
|
||||
protected interface FormClient {
|
||||
|
||||
@RequestMapping(value = "/form", method = RequestMethod.POST,
|
||||
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
@PostMapping(value = "/form", consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
|
||||
String form(Map<String, String> form);
|
||||
|
||||
}
|
||||
@@ -296,7 +325,7 @@ public class FeignClientUsingPropertiesTests {
|
||||
@Import(NoSecurityConfiguration.class)
|
||||
protected static class Application {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/foo")
|
||||
@GetMapping("/foo")
|
||||
public String foo(HttpServletRequest request) throws IllegalAccessException {
|
||||
if ("Foo".equals(request.getHeader("Foo")) && "Bar".equals(request.getHeader("Bar"))) {
|
||||
return "OK";
|
||||
@@ -385,4 +414,8 @@ public class FeignClientUsingPropertiesTests {
|
||||
|
||||
}
|
||||
|
||||
public static class NoOpCapability implements Capability {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+185
@@ -0,0 +1,185 @@
|
||||
/*
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import feign.Request;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.config.BeanDefinition;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.context.scope.refresh.RefreshScope;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.TestPropertySource;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.context.support.GenericWebApplicationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Jasbir Singh
|
||||
*/
|
||||
@SpringBootTest
|
||||
@TestPropertySource("classpath:feign-refreshable-properties.properties")
|
||||
@DirtiesContext
|
||||
public class FeignClientWithRefreshableOptionsTest {
|
||||
|
||||
@Autowired
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Autowired
|
||||
private RefreshScope refreshScope;
|
||||
|
||||
@Autowired
|
||||
private Application.RefreshableClient refreshableClient;
|
||||
|
||||
@Autowired
|
||||
private Application.ReadTimeoutClient readTimeoutClient;
|
||||
|
||||
@Autowired
|
||||
private Application.ConnectTimeoutClient connectTimeoutClient;
|
||||
|
||||
@Autowired
|
||||
private Application.OverrideOptionsClient overrideOptionsClient;
|
||||
|
||||
@Autowired
|
||||
private FeignClientProperties clientProperties;
|
||||
|
||||
@Test
|
||||
public void overridedOptionsBeanShouldBePresentInsteadOfRefreshable() {
|
||||
OptionsTestClient.OptionsResponseForTests options = overrideOptionsClient.override();
|
||||
assertConnectionAndReadTimeout(options, 1, 1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void refreshScopeBeanDefinitionShouldBePresent() {
|
||||
BeanDefinition beanDefinition = ((GenericWebApplicationContext) applicationContext)
|
||||
.getBeanDefinition(Request.Options.class.getCanonicalName() + "-" + "refreshableClient");
|
||||
BeanDefinition originBeanDefinition = beanDefinition.getOriginatingBeanDefinition();
|
||||
assertThat(originBeanDefinition.getBeanClassName()).isEqualTo(OptionsFactoryBean.class.getCanonicalName());
|
||||
assertThat(originBeanDefinition.getScope()).isEqualTo("refresh");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void withConfigDefaultConnectTimeoutAndReadTimeout() {
|
||||
OptionsTestClient.OptionsResponseForTests options = refreshableClient.refreshable();
|
||||
assertConnectionAndReadTimeout(options, 5000, 5000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void readTimeoutShouldWorkWhenConnectTimeoutNotSet() {
|
||||
OptionsTestClient.OptionsResponseForTests options = readTimeoutClient.readTimeout();
|
||||
assertConnectionAndReadTimeout(options, 5000, 2000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void connectTimeoutShouldWorkWhenReadTimeoutNotSet() {
|
||||
OptionsTestClient.OptionsResponseForTests options = connectTimeoutClient.connectTimeout();
|
||||
assertConnectionAndReadTimeout(options, 2000, 5000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void connectTimeoutShouldNotChangeWithoutContextRefresh() {
|
||||
OptionsTestClient.OptionsResponseForTests options = connectTimeoutClient.connectTimeout();
|
||||
assertConnectionAndReadTimeout(options, 2000, 5000);
|
||||
|
||||
clientProperties.getConfig().get("connectTimeout").setConnectTimeout(5000);
|
||||
options = connectTimeoutClient.connectTimeout();
|
||||
assertConnectionAndReadTimeout(options, 2000, 5000);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void connectTimeoutShouldChangeAfterContextRefresh() {
|
||||
OptionsTestClient.OptionsResponseForTests options = connectTimeoutClient.connectTimeout();
|
||||
assertConnectionAndReadTimeout(options, 2000, 5000);
|
||||
|
||||
clientProperties.getConfig().get("connectTimeout").setConnectTimeout(5000);
|
||||
refreshScope.refreshAll();
|
||||
options = connectTimeoutClient.connectTimeout();
|
||||
assertConnectionAndReadTimeout(options, 5000, 5000);
|
||||
}
|
||||
|
||||
private void assertConnectionAndReadTimeout(OptionsTestClient.OptionsResponseForTests options,
|
||||
int expectedConnectTimeoutInMillis, int expectedReadTimeoutInMillis) {
|
||||
assertThat(options.getConnectTimeout()).isEqualTo(expectedConnectTimeoutInMillis);
|
||||
assertThat(options.getReadTimeout()).isEqualTo(expectedReadTimeoutInMillis);
|
||||
}
|
||||
|
||||
@Configuration
|
||||
@EnableAutoConfiguration
|
||||
@EnableConfigurationProperties(FeignClientProperties.class)
|
||||
@EnableFeignClients(clients = { Application.OverrideOptionsClient.class, Application.RefreshableClient.class,
|
||||
Application.ReadTimeoutClient.class, Application.ConnectTimeoutClient.class })
|
||||
protected static class Application {
|
||||
|
||||
@Bean
|
||||
OptionsTestClient client() {
|
||||
return new OptionsTestClient();
|
||||
}
|
||||
|
||||
@FeignClient(name = "overrideOptionsClient", configuration = OverrideConfig.class)
|
||||
protected interface OverrideOptionsClient {
|
||||
|
||||
@GetMapping("/override")
|
||||
OptionsTestClient.OptionsResponseForTests override();
|
||||
|
||||
}
|
||||
|
||||
@FeignClient(name = "refreshableClient")
|
||||
protected interface RefreshableClient {
|
||||
|
||||
@GetMapping("/refreshable")
|
||||
OptionsTestClient.OptionsResponseForTests refreshable();
|
||||
|
||||
}
|
||||
|
||||
@FeignClient(name = "readTimeout")
|
||||
protected interface ReadTimeoutClient {
|
||||
|
||||
@GetMapping("/readTimeout")
|
||||
OptionsTestClient.OptionsResponseForTests readTimeout();
|
||||
|
||||
}
|
||||
|
||||
@FeignClient(name = "connectTimeout")
|
||||
protected interface ConnectTimeoutClient {
|
||||
|
||||
@GetMapping("/connectTimeout")
|
||||
OptionsTestClient.OptionsResponseForTests connectTimeout();
|
||||
|
||||
}
|
||||
|
||||
@Configuration
|
||||
protected class OverrideConfig {
|
||||
|
||||
@Bean
|
||||
public Request.Options options() {
|
||||
return new Request.Options(1, TimeUnit.MILLISECONDS, 1, TimeUnit.MILLISECONDS, true);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+130
@@ -0,0 +1,130 @@
|
||||
/*
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
|
||||
/**
|
||||
* Tests for {@link FeignClientsRegistrar}.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@SpringBootTest(classes = FeignClientsRegistrarIntegrationTests.QualifiersTestConfig.class)
|
||||
class FeignClientsRegistrarIntegrationTests {
|
||||
|
||||
@Autowired
|
||||
ConfigurableApplicationContext context;
|
||||
|
||||
@Test
|
||||
void shouldUseQualifiersIfPresent() {
|
||||
assertThat(context.getBean("qualifier1")).isNotNull();
|
||||
assertThat(context.getBean("qualifier2")).isNotNull();
|
||||
assertThatExceptionOfType(NoSuchBeanDefinitionException.class).isThrownBy(() -> context.getBean("qualifier3"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseQualifierIfQualifiersArrayNotPresent() {
|
||||
assertThat(context.getBean("qualifier4")).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseDefaultQualifierWhenNonePresent() {
|
||||
assertThat(context.getBean("noQualifiersFeignClient")).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseQualifierWhenEmptyQualifiers() {
|
||||
assertThat(context.getBean("test1")).isNotNull();
|
||||
assertThatExceptionOfType(NoSuchBeanDefinitionException.class)
|
||||
.isThrownBy(() -> context.getBean("emptyQualifiersFeignClient"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseQualifierWhenWhitespaceQualifiers() {
|
||||
assertThat(context.getBean("test2")).isNotNull();
|
||||
assertThatExceptionOfType(NoSuchBeanDefinitionException.class)
|
||||
.isThrownBy(() -> context.getBean("whitespaceQualifiersFeignClient"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseDefaultQualifierWhenEmptyQualifiers() {
|
||||
assertThat(context.getBean("emptyQualifiersNoQualifierFeignClient")).isNotNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldUseDefaultQualifierWhenWhitespaceQualifiers() {
|
||||
assertThat(context.getBean("whitespaceQualifiersNoQualifierFeignClient")).isNotNull();
|
||||
}
|
||||
|
||||
@FeignClient(name = "qualifiersClient", qualifiers = { "qualifier1", "qualifier2" }, qualifier = "qualifier3")
|
||||
protected interface QualifiersClient {
|
||||
|
||||
}
|
||||
|
||||
@FeignClient(name = "qualifierClient", qualifier = "qualifier4")
|
||||
protected interface QualifierClient {
|
||||
|
||||
}
|
||||
|
||||
@FeignClient(name = "noQualifiers")
|
||||
protected interface NoQualifiersClient {
|
||||
|
||||
}
|
||||
|
||||
@FeignClient(name = "emptyQualifiers", qualifier = "test1", qualifiers = {})
|
||||
protected interface EmptyQualifiersClient {
|
||||
|
||||
}
|
||||
|
||||
@FeignClient(name = "whitespaceQualifiers", qualifier = "test2", qualifiers = { " " })
|
||||
protected interface WhitespaceQualifiersClient {
|
||||
|
||||
}
|
||||
|
||||
@FeignClient(name = "emptyQualifiersNoQualifier", qualifiers = {})
|
||||
protected interface EmptyQualifiersNoQualifierClient {
|
||||
|
||||
}
|
||||
|
||||
@FeignClient(name = "whitespaceQualifiersNoQualifier", qualifiers = { " " })
|
||||
protected interface WhitespaceQualifiersNoQualifierClient {
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableAutoConfiguration
|
||||
@Import(NoSecurityConfiguration.class)
|
||||
@EnableFeignClients(clients = { QualifiersClient.class, QualifierClient.class, NoQualifiersClient.class,
|
||||
EmptyQualifiersClient.class, WhitespaceQualifiersClient.class, EmptyQualifiersNoQualifierClient.class,
|
||||
WhitespaceQualifiersNoQualifierClient.class })
|
||||
protected static class QualifiersTestConfig {
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+5
-7
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -25,8 +25,7 @@ import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.mock.env.MockEnvironment;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatCode;
|
||||
@@ -79,7 +78,7 @@ public class FeignClientsRegistrarTests {
|
||||
private String testGetName(String name) {
|
||||
FeignClientsRegistrar registrar = new FeignClientsRegistrar();
|
||||
registrar.setEnvironment(new MockEnvironment());
|
||||
return registrar.getName(Collections.<String, Object>singletonMap("name", name));
|
||||
return registrar.getName(Collections.singletonMap("name", name));
|
||||
}
|
||||
|
||||
@Test(expected = IllegalArgumentException.class)
|
||||
@@ -100,13 +99,12 @@ public class FeignClientsRegistrarTests {
|
||||
assertThatCode(() -> config.refresh())
|
||||
.as("Case https://github.com/spring-cloud/spring-cloud-openfeign/issues/331 should be solved")
|
||||
.doesNotThrowAnyException();
|
||||
|
||||
}
|
||||
|
||||
@FeignClient(name = "fallbackTestClient", url = "http://localhost:8080/", fallback = FallbackClient.class)
|
||||
protected interface FallbackClient {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
@GetMapping("/hello")
|
||||
String fallbackTest();
|
||||
|
||||
}
|
||||
@@ -115,7 +113,7 @@ public class FeignClientsRegistrarTests {
|
||||
fallbackFactory = FallbackFactoryClient.class)
|
||||
protected interface FallbackFactoryClient {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
@GetMapping("/hello")
|
||||
String fallbackFactoryTest();
|
||||
|
||||
}
|
||||
|
||||
+143
@@ -0,0 +1,143 @@
|
||||
/*
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import feign.Client;
|
||||
import feign.hc5.ApacheHttp5Client;
|
||||
import feign.httpclient.ApacheHttpClient;
|
||||
import org.apache.hc.client5.http.impl.classic.CloseableHttpClient;
|
||||
import org.apache.hc.client5.http.impl.io.PoolingHttpClientConnectionManager;
|
||||
import org.apache.hc.client5.http.io.HttpClientConnectionManager;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.boot.WebApplicationType;
|
||||
import org.springframework.boot.builder.SpringApplicationBuilder;
|
||||
import org.springframework.cloud.commons.httpclient.HttpClientConfiguration;
|
||||
import org.springframework.cloud.test.ClassPathExclusions;
|
||||
import org.springframework.cloud.test.ModifiedClassPathRunner;
|
||||
import org.springframework.context.ConfigurableApplicationContext;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Nguyen Ky Thanh
|
||||
*/
|
||||
public class FeignHttpClient5ConfigurationTests {
|
||||
|
||||
private static void verifyHc4BeansAvailable(ConfigurableApplicationContext context) {
|
||||
org.apache.http.impl.client.CloseableHttpClient httpClient4 = context
|
||||
.getBean(org.apache.http.impl.client.CloseableHttpClient.class);
|
||||
assertThat(httpClient4).isNotNull();
|
||||
org.apache.http.conn.HttpClientConnectionManager connectionManager4 = context
|
||||
.getBean(org.apache.http.conn.HttpClientConnectionManager.class);
|
||||
assertThat(connectionManager4).isInstanceOf(org.apache.http.impl.conn.PoolingHttpClientConnectionManager.class);
|
||||
Client client = context.getBean(Client.class);
|
||||
assertThat(client).isInstanceOf(ApacheHttpClient.class);
|
||||
}
|
||||
|
||||
private static void verifyHc5BeansAvailable(ConfigurableApplicationContext context) {
|
||||
CloseableHttpClient httpClient = context.getBean(CloseableHttpClient.class);
|
||||
assertThat(httpClient).isNotNull();
|
||||
HttpClientConnectionManager connectionManager = context.getBean(HttpClientConnectionManager.class);
|
||||
assertThat(connectionManager).isInstanceOf(PoolingHttpClientConnectionManager.class);
|
||||
Client client = context.getBean(Client.class);
|
||||
assertThat(client).isInstanceOf(ApacheHttp5Client.class);
|
||||
}
|
||||
|
||||
@RunWith(ModifiedClassPathRunner.class)
|
||||
@ClassPathExclusions("ribbon-loadbalancer-{version:\\d.*}.jar")
|
||||
public static class WithoutLoadBalancerInClasspath {
|
||||
|
||||
@Test
|
||||
public void verifyHttpClient5AutoConfig() {
|
||||
ConfigurableApplicationContext context = new SpringApplicationBuilder()
|
||||
.properties("feign.httpclient.hc5.enabled=true", "feign.httpclient.enabled=false")
|
||||
.web(WebApplicationType.NONE).sources(HttpClientConfiguration.class, FeignAutoConfiguration.class)
|
||||
.run();
|
||||
|
||||
verifyHc5BeansAvailable(context);
|
||||
|
||||
if (context != null) {
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hc5ShouldWinIfTheBothVersionsAvailable() {
|
||||
ConfigurableApplicationContext context = new SpringApplicationBuilder()
|
||||
.properties("feign.httpclient.hc5.enabled=true", "feign.httpclient.enabled=true")
|
||||
.web(WebApplicationType.NONE).sources(HttpClientConfiguration.class, FeignAutoConfiguration.class)
|
||||
.run();
|
||||
|
||||
Client client = context.getBean(Client.class);
|
||||
assertThat(client).isInstanceOf(ApacheHttp5Client.class);
|
||||
|
||||
if (context != null) {
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hc4ShouldBeTheDefaultIfHc5NotEnabled() {
|
||||
ConfigurableApplicationContext context = new SpringApplicationBuilder()
|
||||
.properties("feign.httpclient.hc5.enabled=false", "feign.httpclient.enabled=true")
|
||||
.web(WebApplicationType.NONE).sources(HttpClientConfiguration.class, FeignAutoConfiguration.class)
|
||||
.run();
|
||||
|
||||
verifyHc4BeansAvailable(context);
|
||||
|
||||
if (context != null) {
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RunWith(ModifiedClassPathRunner.class)
|
||||
@ClassPathExclusions({ "ribbon-loadbalancer-{version:\\d.*}.jar", "feign-hc5-{version:\\d.*}.jar",
|
||||
"httpclient5-{version:\\d.*}.jar", "httpcore5-{version:\\d.*}.jar", "httpcore5-h2-{version:\\d.*}.jar" })
|
||||
public static class WithoutLoadBalancerAndHc5InClasspath {
|
||||
|
||||
@Test
|
||||
public void hc4ShouldWinEvenHc5ConfigEnabled() {
|
||||
ConfigurableApplicationContext context = new SpringApplicationBuilder()
|
||||
.properties("feign.httpclient.hc5.enabled=true").web(WebApplicationType.NONE)
|
||||
.sources(HttpClientConfiguration.class, FeignAutoConfiguration.class).run();
|
||||
|
||||
verifyHc4BeansAvailable(context);
|
||||
|
||||
if (context != null) {
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void hc4ShouldBeTheDefault() {
|
||||
ConfigurableApplicationContext context = new SpringApplicationBuilder().web(WebApplicationType.NONE)
|
||||
.sources(HttpClientConfiguration.class, FeignAutoConfiguration.class).run();
|
||||
|
||||
verifyHc4BeansAvailable(context);
|
||||
|
||||
if (context != null) {
|
||||
context.close();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+12
-4
@@ -66,10 +66,18 @@ public class FeignHttpClientConfigurationTests {
|
||||
|
||||
@Test
|
||||
public void disableSslTest() throws Exception {
|
||||
HttpClientConnectionManager connectionManager = this.context.getBean(HttpClientConnectionManager.class);
|
||||
Lookup<ConnectionSocketFactory> socketFactoryRegistry = getConnectionSocketFactoryLookup(connectionManager);
|
||||
assertThat(socketFactoryRegistry.lookup("https")).isNotNull();
|
||||
assertThat(this.getX509TrustManager(socketFactoryRegistry).getAcceptedIssuers()).isNull();
|
||||
try {
|
||||
HttpClientConnectionManager connectionManager = this.context.getBean(HttpClientConnectionManager.class);
|
||||
Lookup<ConnectionSocketFactory> socketFactoryRegistry = getConnectionSocketFactoryLookup(connectionManager);
|
||||
assertThat(socketFactoryRegistry.lookup("https")).isNotNull();
|
||||
assertThat(this.getX509TrustManager(socketFactoryRegistry).getAcceptedIssuers()).isNull();
|
||||
}
|
||||
catch (RuntimeException e) {
|
||||
// FIXME: java 16 need junit 5 compatible modified classpath extension
|
||||
if (e.getMessage() == null || !e.getMessage().startsWith("Unable to make field private final")) {
|
||||
ReflectionUtils.rethrowRuntimeException(e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private Lookup<ConnectionSocketFactory> getConnectionSocketFactoryLookup(
|
||||
|
||||
+11
-11
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -37,8 +37,7 @@ import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.SocketUtils;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -46,6 +45,7 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@SpringBootTest(classes = FeignHttpClientUrlTests.TestConfig.class, webEnvironment = DEFINED_PORT,
|
||||
value = { "spring.application.name=feignclienturltest", "feign.circuitbreaker.enabled=false",
|
||||
@@ -65,13 +65,13 @@ class FeignHttpClientUrlTests {
|
||||
private BeanUrlClient beanClient;
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeClass() {
|
||||
static void beforeClass() {
|
||||
port = SocketUtils.findAvailableTcpPort();
|
||||
System.setProperty("server.port", String.valueOf(port));
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void afterClass() {
|
||||
static void afterClass() {
|
||||
System.clearProperty("server.port");
|
||||
}
|
||||
|
||||
@@ -91,7 +91,7 @@ class FeignHttpClientUrlTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testBeanUrlNoProtocol() {
|
||||
void testBeanUrlNoProtocol() {
|
||||
Hello hello = beanClientNoProtocol.getHello();
|
||||
assertThat(hello).as("hello was null").isNotNull();
|
||||
assertThat(hello).as("first hello didn't match").isEqualTo(new Hello("hello world 1"));
|
||||
@@ -101,7 +101,7 @@ class FeignHttpClientUrlTests {
|
||||
@FeignClient(name = "localappurl", url = "http://localhost:${server.port}/")
|
||||
protected interface UrlClient {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
@GetMapping("/hello")
|
||||
Hello getHello();
|
||||
|
||||
}
|
||||
@@ -109,7 +109,7 @@ class FeignHttpClientUrlTests {
|
||||
@FeignClient(name = "beanappurl", url = "#{SERVER_URL}path")
|
||||
protected interface BeanUrlClient {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
@GetMapping("/hello")
|
||||
Hello getHello();
|
||||
|
||||
}
|
||||
@@ -117,7 +117,7 @@ class FeignHttpClientUrlTests {
|
||||
@FeignClient(name = "beanappurlnoprotocol", url = "#{SERVER_URL_NO_PROTOCOL}path")
|
||||
protected interface BeanUrlClientNoProtocol {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
@GetMapping("/hello")
|
||||
Hello getHello();
|
||||
|
||||
}
|
||||
@@ -129,12 +129,12 @@ class FeignHttpClientUrlTests {
|
||||
@Import(NoSecurityConfiguration.class)
|
||||
protected static class TestConfig {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
@GetMapping("/hello")
|
||||
public Hello getHello() {
|
||||
return new Hello("hello world 1");
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/path/hello")
|
||||
@GetMapping("/path/hello")
|
||||
public Hello getHelloWithPath() {
|
||||
return getHello();
|
||||
}
|
||||
|
||||
+211
@@ -0,0 +1,211 @@
|
||||
/*
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import java.lang.reflect.Field;
|
||||
import java.util.Objects;
|
||||
|
||||
import feign.Client;
|
||||
import feign.Feign;
|
||||
import feign.Target;
|
||||
import feign.httpclient.ApacheHttpClient;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.util.ReflectionUtils;
|
||||
import org.springframework.util.SocketUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@SpringBootTest(classes = FeignHttpClientUrlTestsWithRetryableLoadBalancer.TestConfig.class,
|
||||
webEnvironment = DEFINED_PORT,
|
||||
value = { "spring.application.name=feignclienturlwithretryableloadbalancertest", "feign.hystrix.enabled=false",
|
||||
"feign.okhttp.enabled=false", "spring.cloud.loadbalancer.ribbon.enabled=false" })
|
||||
@DirtiesContext
|
||||
class FeignHttpClientUrlTestsWithRetryableLoadBalancer {
|
||||
|
||||
static int port;
|
||||
|
||||
@Autowired
|
||||
BeanUrlClientNoProtocol beanClientNoProtocol;
|
||||
|
||||
@Autowired
|
||||
private UrlClient urlClient;
|
||||
|
||||
@Autowired
|
||||
private BeanUrlClient beanClient;
|
||||
|
||||
@BeforeAll
|
||||
static void beforeClass() {
|
||||
port = SocketUtils.findAvailableTcpPort();
|
||||
System.setProperty("server.port", String.valueOf(port));
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
static void afterClass() {
|
||||
System.clearProperty("server.port");
|
||||
}
|
||||
|
||||
@Test
|
||||
void testUrlHttpClient() {
|
||||
assertThat(urlClient).as("UrlClient was null").isNotNull();
|
||||
Hello hello = urlClient.getHello();
|
||||
assertThat(hello).as("hello was null").isNotNull();
|
||||
assertThat(hello).as("first hello didn't match").isEqualTo(new Hello("hello world 1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBeanUrl() {
|
||||
Hello hello = beanClient.getHello();
|
||||
assertThat(hello).as("hello was null").isNotNull();
|
||||
assertThat(hello).as("first hello didn't match").isEqualTo(new Hello("hello world 1"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void testBeanUrlNoProtocol() {
|
||||
Hello hello = beanClientNoProtocol.getHello();
|
||||
assertThat(hello).as("hello was null").isNotNull();
|
||||
assertThat(hello).as("first hello didn't match").isEqualTo(new Hello("hello world 1"));
|
||||
}
|
||||
|
||||
// this tests that
|
||||
@FeignClient(name = "localappurl", url = "http://localhost:${server.port}/")
|
||||
protected interface UrlClient {
|
||||
|
||||
@GetMapping("/hello")
|
||||
Hello getHello();
|
||||
|
||||
}
|
||||
|
||||
@FeignClient(name = "beanappurl", url = "#{SERVER_URL}path")
|
||||
protected interface BeanUrlClient {
|
||||
|
||||
@GetMapping("/hello")
|
||||
Hello getHello();
|
||||
|
||||
}
|
||||
|
||||
@FeignClient(name = "beanappurlnoprotocol", url = "#{SERVER_URL_NO_PROTOCOL}path")
|
||||
protected interface BeanUrlClientNoProtocol {
|
||||
|
||||
@GetMapping("/hello")
|
||||
Hello getHello();
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableAutoConfiguration
|
||||
@RestController
|
||||
@EnableFeignClients(clients = { UrlClient.class, BeanUrlClient.class, BeanUrlClientNoProtocol.class })
|
||||
@Import(NoSecurityConfiguration.class)
|
||||
protected static class TestConfig {
|
||||
|
||||
@GetMapping("/hello")
|
||||
public Hello getHello() {
|
||||
return new Hello("hello world 1");
|
||||
}
|
||||
|
||||
@GetMapping("/path/hello")
|
||||
public Hello getHelloWithPath() {
|
||||
return getHello();
|
||||
}
|
||||
|
||||
@Bean(name = "SERVER_URL")
|
||||
public String serverUrl() {
|
||||
return "http://localhost:" + port + "/";
|
||||
}
|
||||
|
||||
@Bean(name = "SERVER_URL_NO_PROTOCOL")
|
||||
public String serverUrlNoProtocol() {
|
||||
return "localhost:" + port + "/";
|
||||
}
|
||||
|
||||
@Bean
|
||||
public Targeter feignTargeter() {
|
||||
return new Targeter() {
|
||||
@Override
|
||||
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
|
||||
Target.HardCodedTarget<T> target) {
|
||||
Field field = ReflectionUtils.findField(Feign.Builder.class, "client");
|
||||
ReflectionUtils.makeAccessible(field);
|
||||
Client client = (Client) ReflectionUtils.getField(field, feign);
|
||||
if (target.name().equals("localappurl")) {
|
||||
assertThat(client).isInstanceOf(ApacheHttpClient.class).as("client was wrong type");
|
||||
}
|
||||
return feign.target(target);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Hello {
|
||||
|
||||
private String message;
|
||||
|
||||
Hello() {
|
||||
|
||||
}
|
||||
|
||||
Hello(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Hello that = (Hello) o;
|
||||
return Objects.equals(message, that.message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+76
@@ -0,0 +1,76 @@
|
||||
/*
|
||||
* Copyright 2021-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import 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 MetricsPropertiesTests {
|
||||
|
||||
@Test
|
||||
void shouldBeEnabledByDefault() {
|
||||
FeignClientProperties.MetricsProperties properties = new FeignClientProperties.MetricsProperties();
|
||||
assertThat(properties.getEnabled()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldBeDisabledWhenSet() {
|
||||
FeignClientProperties.MetricsProperties properties = new FeignClientProperties.MetricsProperties();
|
||||
properties.setEnabled(false);
|
||||
assertThat(properties.getEnabled()).isFalse();
|
||||
}
|
||||
|
||||
/**
|
||||
* Sanity-checks equals and hashCode contracts but does not check every variation of
|
||||
* the fields.
|
||||
*/
|
||||
@Test
|
||||
void shouldHaveSomewhatValidEqualsAndHashCode() {
|
||||
FeignClientProperties.MetricsProperties propertyOne = new FeignClientProperties.MetricsProperties();
|
||||
FeignClientProperties.MetricsProperties propertyTwo = new FeignClientProperties.MetricsProperties();
|
||||
FeignClientProperties.MetricsProperties propertyThree = new FeignClientProperties.MetricsProperties();
|
||||
FeignClientProperties.MetricsProperties differentProperty = new FeignClientProperties.MetricsProperties();
|
||||
differentProperty.setEnabled(false);
|
||||
|
||||
assertEqualsReflexivity(propertyOne);
|
||||
|
||||
assertEqualsSymmetricity(propertyOne, propertyTwo);
|
||||
assertEqualsSymmetricity(propertyOne, differentProperty);
|
||||
assertEqualsSymmetricity(propertyOne, 42);
|
||||
|
||||
assertEqualsTransitivity(propertyOne, propertyTwo, propertyThree);
|
||||
|
||||
assertEqualsConsistency(propertyOne, propertyTwo);
|
||||
assertEqualsConsistency(propertyOne, differentProperty);
|
||||
assertEqualsConsistency(propertyOne, 42);
|
||||
assertEqualsConsistency(propertyOne, null);
|
||||
|
||||
assertHashCodeConsistency(propertyOne);
|
||||
assertEqualsAndHashCodeConsistency(propertyOne, propertyTwo);
|
||||
}
|
||||
|
||||
}
|
||||
+113
@@ -0,0 +1,113 @@
|
||||
/*
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.DeserializationFeature;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
|
||||
import feign.Client;
|
||||
import feign.Request;
|
||||
import feign.Response;
|
||||
|
||||
/**
|
||||
* @author Jasbir Singh
|
||||
*/
|
||||
public class OptionsTestClient implements Client {
|
||||
|
||||
private static ObjectMapper mapper;
|
||||
|
||||
static {
|
||||
mapper = new ObjectMapper();
|
||||
mapper.registerModule(new JavaTimeModule()).configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public Response execute(Request request, Request.Options options) throws IOException {
|
||||
return Response.builder().status(200).request(request).headers(headers()).body(prepareResponse(options))
|
||||
.build();
|
||||
}
|
||||
|
||||
private Map<String, Collection<String>> headers() {
|
||||
Map<String, Collection<String>> headers = new LinkedHashMap<>();
|
||||
headers.put("Content-Type", Collections.singletonList("application/json"));
|
||||
return headers;
|
||||
}
|
||||
|
||||
private byte[] prepareResponse(Request.Options options) {
|
||||
try {
|
||||
|
||||
OptionsResponseForTests response = new OptionsResponseForTests(options.connectTimeoutMillis(),
|
||||
TimeUnit.MILLISECONDS, options.readTimeoutMillis(), TimeUnit.MILLISECONDS);
|
||||
return mapper.writeValueAsString(response).getBytes();
|
||||
}
|
||||
catch (JsonProcessingException e) {
|
||||
throw new RuntimeException(e);
|
||||
}
|
||||
}
|
||||
|
||||
static class OptionsResponseForTests {
|
||||
|
||||
private long connectTimeout;
|
||||
|
||||
private TimeUnit connectTimeoutUnit;
|
||||
|
||||
private long readTimeout;
|
||||
|
||||
private TimeUnit readTimeoutUnit;
|
||||
|
||||
OptionsResponseForTests(long connectTimeout, TimeUnit connectTimeoutUnit, long readTimeout,
|
||||
TimeUnit readTimeoutUnit) {
|
||||
this.connectTimeout = connectTimeout;
|
||||
this.connectTimeoutUnit = connectTimeoutUnit;
|
||||
this.readTimeout = readTimeout;
|
||||
this.readTimeoutUnit = readTimeoutUnit;
|
||||
}
|
||||
|
||||
public long getConnectTimeout() {
|
||||
return connectTimeout;
|
||||
}
|
||||
|
||||
public TimeUnit getConnectTimeoutUnit() {
|
||||
return connectTimeoutUnit;
|
||||
}
|
||||
|
||||
public long getReadTimeout() {
|
||||
return readTimeout;
|
||||
}
|
||||
|
||||
public TimeUnit getReadTimeoutUnit() {
|
||||
return readTimeoutUnit;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "OptionsResponseForTests{" + "connectTimeout=" + connectTimeout + ", connectTimeoutUnit="
|
||||
+ connectTimeoutUnit + ", readTimeout=" + readTimeout + ", readTimeoutUnit=" + readTimeoutUnit
|
||||
+ '}';
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+25
-9
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -32,19 +32,20 @@ import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringBootTest(classes = SpringDecoderTests.Application.class, webEnvironment = WebEnvironment.RANDOM_PORT,
|
||||
@@ -142,29 +143,39 @@ public class SpringDecoderTests extends FeignClientFactoryBean {
|
||||
assertThat(response.getBody()).as("response body was not null").isNull();
|
||||
}
|
||||
|
||||
@Test
|
||||
// Issue: https://github.com/spring-cloud/spring-cloud-openfeign/issues/456
|
||||
public void testResponseEntityHeaders() {
|
||||
ResponseEntity<String> response = testClient().getContentType();
|
||||
assertThat(response.getHeaders().getContentType()).isEqualTo(MediaType.APPLICATION_JSON);
|
||||
}
|
||||
|
||||
protected interface TestClient {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/helloresponse")
|
||||
@GetMapping("/helloresponse")
|
||||
ResponseEntity<Hello> getHelloResponse();
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hellovoid")
|
||||
@GetMapping("/hellovoid")
|
||||
ResponseEntity<Void> getHelloVoid();
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
@GetMapping("/hello")
|
||||
Hello getHello();
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hellos")
|
||||
@GetMapping("/hellos")
|
||||
List<Hello> getHellos();
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hellostrings")
|
||||
@GetMapping("/hellostrings")
|
||||
List<String> getHelloStrings();
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hellonotfound")
|
||||
@GetMapping("/hellonotfound")
|
||||
ResponseEntity<String> getNotFound();
|
||||
|
||||
@GetMapping("/helloWildcard")
|
||||
ResponseEntity<?> getWildcard();
|
||||
|
||||
@GetMapping(path = "/contentType", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
ResponseEntity<String> getContentType();
|
||||
|
||||
}
|
||||
|
||||
public static class Hello {
|
||||
@@ -252,6 +263,11 @@ public class SpringDecoderTests extends FeignClientFactoryBean {
|
||||
return ResponseEntity.ok(new Hello("wildcard"));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResponseEntity<String> getContentType() {
|
||||
return ResponseEntity.ok().header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE).body("test");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+2
-3
@@ -39,8 +39,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -122,7 +121,7 @@ public class BeansFeignClientTests {
|
||||
return feignClientBuilder.forType(TestClient.class, "builderapp").build();
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
@GetMapping("/hello")
|
||||
public Hello getHello() {
|
||||
return new Hello("hello world 1");
|
||||
}
|
||||
|
||||
+2
-3
@@ -19,14 +19,13 @@ package org.springframework.cloud.openfeign.beans;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.cloud.openfeign.beans.BeansFeignClientTests.Hello;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Primary
|
||||
@FeignClient("localapp")
|
||||
public interface TestClient {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
@GetMapping("/hello")
|
||||
Hello getHello();
|
||||
|
||||
}
|
||||
|
||||
+2
-3
@@ -18,13 +18,12 @@ package org.springframework.cloud.openfeign.beans.extra;
|
||||
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.cloud.openfeign.beans.BeansFeignClientTests.Hello;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@FeignClient(value = "otherapp", qualifier = "uniquequalifier")
|
||||
public interface TestClient {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
@GetMapping("/hello")
|
||||
Hello getHello();
|
||||
|
||||
}
|
||||
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign.circuitbreaker;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.concurrent.ExecutorService;
|
||||
import java.util.concurrent.Executors;
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
|
||||
|
||||
/**
|
||||
* Asynchronous circuit breaker.
|
||||
*
|
||||
* @author John Niang
|
||||
*/
|
||||
class AsyncCircuitBreaker implements CircuitBreaker {
|
||||
|
||||
final Duration timeout;
|
||||
|
||||
final ExecutorService executorService;
|
||||
|
||||
AsyncCircuitBreaker(Duration timeout) {
|
||||
this(timeout, Executors.newCachedThreadPool());
|
||||
}
|
||||
|
||||
AsyncCircuitBreaker(Duration timeout, ExecutorService executorService) {
|
||||
this.timeout = timeout;
|
||||
this.executorService = executorService;
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T run(Supplier<T> toRun, Function<Throwable, T> fallback) {
|
||||
Future<T> future = executorService.submit(toRun::get);
|
||||
try {
|
||||
return future.get(timeout.toMillis(), TimeUnit.MILLISECONDS);
|
||||
}
|
||||
catch (Throwable t) {
|
||||
return fallback.apply(t);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
+177
@@ -0,0 +1,177 @@
|
||||
/*
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign.circuitbreaker;
|
||||
|
||||
import java.time.Duration;
|
||||
import java.util.Objects;
|
||||
import java.util.UUID;
|
||||
import java.util.function.Function;
|
||||
|
||||
import javax.servlet.http.HttpServletRequest;
|
||||
|
||||
import feign.RequestInterceptor;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||
import org.springframework.cloud.client.circuitbreaker.ConfigBuilder;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.HttpHeaders;
|
||||
import org.springframework.test.web.servlet.MockMvc;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.context.request.RequestContextHolder;
|
||||
import org.springframework.web.context.request.ServletRequestAttributes;
|
||||
|
||||
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
|
||||
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultHandlers.print;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
|
||||
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
|
||||
|
||||
/**
|
||||
* Tests for asynchronous circuit breaker.
|
||||
*
|
||||
* @author John Niang
|
||||
*/
|
||||
@SpringBootTest(classes = AsyncCircuitBreakerTest.Application.class, webEnvironment = RANDOM_PORT,
|
||||
properties = "feign.circuitbreaker.enabled=true")
|
||||
@AutoConfigureMockMvc
|
||||
class AsyncCircuitBreakerTest {
|
||||
|
||||
@Autowired
|
||||
MockMvc mvc;
|
||||
|
||||
@Test
|
||||
void shouldWorkNormally() throws Exception {
|
||||
mvc.perform(get("/hello/proxy")).andDo(print()).andExpect(status().isOk())
|
||||
.andExpect(content().string("openfeign"));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldNotProxyAnyHeadersWithoutHeaderSet() throws Exception {
|
||||
mvc.perform(get("/headers/" + HttpHeaders.AUTHORIZATION + "/proxy")).andDo(print()).andExpect(status().isOk())
|
||||
.andExpect(content().string(""));
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldProxyHeaderWhenHeaderSet() throws Exception {
|
||||
String authorization = UUID.randomUUID().toString();
|
||||
mvc.perform(get("/headers/" + HttpHeaders.AUTHORIZATION + "/proxy").header(HttpHeaders.AUTHORIZATION,
|
||||
authorization)).andDo(print()).andExpect(status().isOk()).andExpect(content().string(authorization));
|
||||
}
|
||||
|
||||
@EnableAutoConfiguration
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableFeignClients(clients = { TestClient.class })
|
||||
@Import({ NoSecurityConfiguration.class, TestController.class })
|
||||
static class Application {
|
||||
|
||||
@Bean
|
||||
CircuitBreakerFactory<Duration, ConfigBuilder<Duration>> circuitBreakerFactory() {
|
||||
return new CircuitBreakerFactory<Duration, ConfigBuilder<Duration>>() {
|
||||
|
||||
Function<String, Duration> defaultConfiguration = id -> Duration.ofMillis(1000);
|
||||
|
||||
@Override
|
||||
public CircuitBreaker create(String id) {
|
||||
Duration timeout = super.getConfigurations().computeIfAbsent(id, defaultConfiguration);
|
||||
return new AsyncCircuitBreaker(timeout);
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ConfigBuilder<Duration> configBuilder(String id) {
|
||||
return () -> Duration.ofMillis(100);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureDefault(Function<String, Duration> defaultConfiguration) {
|
||||
this.defaultConfiguration = defaultConfiguration;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Bean
|
||||
RequestInterceptor proxyHeaderRequestInterceptor() {
|
||||
return template -> {
|
||||
ServletRequestAttributes requestAttributes = (ServletRequestAttributes) RequestContextHolder
|
||||
.getRequestAttributes();
|
||||
String authorization = Objects.requireNonNull(requestAttributes).getRequest()
|
||||
.getHeader(HttpHeaders.AUTHORIZATION);
|
||||
if (authorization != null) {
|
||||
// proxy authorization header
|
||||
template.header(HttpHeaders.AUTHORIZATION, authorization);
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@RestController
|
||||
static class TestController {
|
||||
|
||||
final ObjectProvider<TestClient> testClient;
|
||||
|
||||
TestController(ObjectProvider<TestClient> testClient) {
|
||||
this.testClient = testClient;
|
||||
}
|
||||
|
||||
@GetMapping("/hello")
|
||||
String hello() {
|
||||
return "openfeign";
|
||||
}
|
||||
|
||||
@GetMapping("/hello/proxy")
|
||||
String helloProxy() {
|
||||
return testClient.getObject().hello();
|
||||
}
|
||||
|
||||
@GetMapping("/headers/{headerName}")
|
||||
String header(HttpServletRequest request, @PathVariable String headerName) {
|
||||
return request.getHeader(headerName);
|
||||
}
|
||||
|
||||
@GetMapping("/headers/{headerName}/proxy")
|
||||
String headerProxy(@PathVariable String headerName) {
|
||||
return testClient.getObject().header(headerName);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@FeignClient(name = "async-circuit-breaker-test", url = "http://localhost:${local.server.port}")
|
||||
interface TestClient {
|
||||
|
||||
@GetMapping("/hello")
|
||||
String hello();
|
||||
|
||||
@GetMapping("/headers/{headerName}")
|
||||
String header(@PathVariable String headerName);
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+8
-84
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,10 +16,7 @@
|
||||
|
||||
package org.springframework.cloud.openfeign.circuitbreaker;
|
||||
|
||||
import java.util.Objects;
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
@@ -33,7 +30,6 @@ import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.boot.web.server.LocalServerPort;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||
import org.springframework.cloud.client.circuitbreaker.ConfigBuilder;
|
||||
@@ -49,8 +45,7 @@ import org.springframework.stereotype.Component;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.util.SocketUtils;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -59,10 +54,10 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
* @author Spencer Gibb
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringBootTest(classes = CirciutBreakerTests.Application.class, webEnvironment = WebEnvironment.DEFINED_PORT, value = {
|
||||
@SpringBootTest(classes = CircuitBreakerTests.Application.class, webEnvironment = WebEnvironment.DEFINED_PORT, value = {
|
||||
"spring.application.name=springcircuittest", "spring.jmx.enabled=false", "feign.circuitbreaker.enabled=true" })
|
||||
@DirtiesContext
|
||||
public class CirciutBreakerTests {
|
||||
public class CircuitBreakerTests {
|
||||
|
||||
@Autowired
|
||||
MyCircuitBreaker myCircuitBreaker;
|
||||
@@ -73,9 +68,6 @@ public class CirciutBreakerTests {
|
||||
@Autowired
|
||||
TestClientWithFactory testClientWithFactory;
|
||||
|
||||
@LocalServerPort
|
||||
private int port = 0;
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeClass() {
|
||||
System.setProperty("server.port", String.valueOf(SocketUtils.findAvailableTcpPort()));
|
||||
@@ -119,14 +111,13 @@ public class CirciutBreakerTests {
|
||||
assertThat(testClientWithFactory.getException()).isEqualTo("Fixed response");
|
||||
}
|
||||
|
||||
// tag::client_with_fallback[]
|
||||
@FeignClient(name = "test", url = "http://localhost:${server.port}/", fallback = Fallback.class)
|
||||
protected interface TestClient {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
@GetMapping("/hello")
|
||||
Hello getHello();
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hellonotfound")
|
||||
@GetMapping("/hellonotfound")
|
||||
String getException();
|
||||
|
||||
}
|
||||
@@ -145,17 +136,15 @@ public class CirciutBreakerTests {
|
||||
}
|
||||
|
||||
}
|
||||
// end::client_with_fallback[]
|
||||
|
||||
// tag::client_with_fallback_factory[]
|
||||
@FeignClient(name = "testClientWithFactory", url = "http://localhost:${server.port}/",
|
||||
fallbackFactory = TestFallbackFactory.class)
|
||||
protected interface TestClientWithFactory {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
@GetMapping("/hello")
|
||||
Hello getHello();
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hellonotfound")
|
||||
@GetMapping("/hellonotfound")
|
||||
String getException();
|
||||
|
||||
}
|
||||
@@ -182,45 +171,6 @@ public class CirciutBreakerTests {
|
||||
return "Fixed response";
|
||||
}
|
||||
|
||||
}
|
||||
// end::client_with_fallback_factory[]
|
||||
|
||||
public static class Hello {
|
||||
|
||||
private String message;
|
||||
|
||||
public Hello() {
|
||||
}
|
||||
|
||||
public Hello(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return this.message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Hello that = (Hello) o;
|
||||
return Objects.equals(this.message, that.message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.message);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@@ -280,30 +230,4 @@ public class CirciutBreakerTests {
|
||||
|
||||
}
|
||||
|
||||
static class MyCircuitBreaker implements CircuitBreaker {
|
||||
|
||||
AtomicBoolean runWasCalled = new AtomicBoolean();
|
||||
|
||||
@Override
|
||||
public <T> T run(Supplier<T> toRun) {
|
||||
this.runWasCalled.set(true);
|
||||
return toRun.get();
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T run(Supplier<T> toRun, Function<Throwable, T> fallback) {
|
||||
try {
|
||||
return run(toRun);
|
||||
}
|
||||
catch (Throwable throwable) {
|
||||
return fallback.apply(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
this.runWasCalled.set(false);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+154
@@ -0,0 +1,154 @@
|
||||
/*
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign.circuitbreaker;
|
||||
|
||||
import java.util.function.Function;
|
||||
|
||||
import org.apache.commons.logging.Log;
|
||||
import org.apache.commons.logging.LogFactory;
|
||||
import org.junit.jupiter.api.AfterAll;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreakerFactory;
|
||||
import org.springframework.cloud.client.circuitbreaker.ConfigBuilder;
|
||||
import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.util.SocketUtils;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatThrownBy;
|
||||
|
||||
/**
|
||||
* Tests for Feign calls with CircuitBreaker, without fallbacks.
|
||||
*
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@SpringBootTest(classes = CircuitBreakerWithNoFallbackTests.Application.class,
|
||||
webEnvironment = SpringBootTest.WebEnvironment.DEFINED_PORT,
|
||||
value = { "spring.application.name=springcircuittest", "spring.jmx.enabled=false",
|
||||
"feign.circuitbreaker.enabled=true" })
|
||||
@DirtiesContext
|
||||
public class CircuitBreakerWithNoFallbackTests {
|
||||
|
||||
@Autowired
|
||||
MyCircuitBreaker myCircuitBreaker;
|
||||
|
||||
@Autowired
|
||||
CircuitBreakerTestClient testClient;
|
||||
|
||||
@BeforeAll
|
||||
public static void beforeClass() {
|
||||
System.setProperty("server.port", String.valueOf(SocketUtils.findAvailableTcpPort()));
|
||||
}
|
||||
|
||||
@AfterAll
|
||||
public static void afterClass() {
|
||||
System.clearProperty("server.port");
|
||||
}
|
||||
|
||||
@BeforeEach
|
||||
public void setup() {
|
||||
this.myCircuitBreaker.clear();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSimpleTypeWithFallback() {
|
||||
Hello hello = testClient.getHello();
|
||||
|
||||
assertThat(hello).as("hello was null").isNotNull();
|
||||
assertThat(hello).as("first hello didn't match").isEqualTo(new Hello("hello world 1"));
|
||||
assertThat(myCircuitBreaker.runWasCalled).as("Circuit Breaker was called").isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void test404WithoutFallback() {
|
||||
assertThatThrownBy(() -> testClient.getException()).isInstanceOf(NoFallbackAvailableException.class);
|
||||
}
|
||||
|
||||
@FeignClient(name = "test", url = "http://localhost:${server.port}/")
|
||||
protected interface CircuitBreakerTestClient {
|
||||
|
||||
@GetMapping("/hello")
|
||||
Hello getHello();
|
||||
|
||||
@GetMapping("/hellonotfound")
|
||||
String getException();
|
||||
|
||||
}
|
||||
|
||||
@Configuration(proxyBeanMethods = false)
|
||||
@EnableAutoConfiguration
|
||||
@RestController
|
||||
@EnableFeignClients(clients = { CircuitBreakerTestClient.class })
|
||||
@Import(NoSecurityConfiguration.class)
|
||||
protected static class Application implements CircuitBreakerTestClient {
|
||||
|
||||
static final Log log = LogFactory.getLog(Application.class);
|
||||
|
||||
@Bean
|
||||
MyCircuitBreaker myCircuitBreaker() {
|
||||
return new MyCircuitBreaker();
|
||||
}
|
||||
|
||||
@Bean
|
||||
CircuitBreakerFactory circuitBreakerFactory(MyCircuitBreaker myCircuitBreaker) {
|
||||
return new CircuitBreakerFactory() {
|
||||
@Override
|
||||
public CircuitBreaker create(String id) {
|
||||
log.info("Creating a circuit breaker with id [" + id + "]");
|
||||
return myCircuitBreaker;
|
||||
}
|
||||
|
||||
@Override
|
||||
protected ConfigBuilder configBuilder(String id) {
|
||||
return Object::new;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void configureDefault(Function defaultConfiguration) {
|
||||
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@Override
|
||||
public Hello getHello() {
|
||||
return new Hello("hello world 1");
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getException() {
|
||||
throw new IllegalStateException("BOOM!");
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
+60
@@ -0,0 +1,60 @@
|
||||
/*
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign.circuitbreaker;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
*/
|
||||
class Hello {
|
||||
|
||||
private String message;
|
||||
|
||||
public Hello() {
|
||||
}
|
||||
|
||||
public Hello(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
public String getMessage() {
|
||||
return this.message;
|
||||
}
|
||||
|
||||
public void setMessage(String message) {
|
||||
this.message = message;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object o) {
|
||||
if (this == o) {
|
||||
return true;
|
||||
}
|
||||
if (o == null || getClass() != o.getClass()) {
|
||||
return false;
|
||||
}
|
||||
Hello that = (Hello) o;
|
||||
return Objects.equals(this.message, that.message);
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
return Objects.hash(this.message);
|
||||
}
|
||||
|
||||
}
|
||||
+59
@@ -0,0 +1,59 @@
|
||||
/*
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign.circuitbreaker;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
import java.util.function.Function;
|
||||
import java.util.function.Supplier;
|
||||
|
||||
import org.springframework.cloud.client.circuitbreaker.CircuitBreaker;
|
||||
import org.springframework.cloud.client.circuitbreaker.NoFallbackAvailableException;
|
||||
|
||||
/**
|
||||
* @author Marcin Grzejszczak
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
class MyCircuitBreaker implements CircuitBreaker {
|
||||
|
||||
AtomicBoolean runWasCalled = new AtomicBoolean();
|
||||
|
||||
@Override
|
||||
public <T> T run(Supplier<T> toRun) {
|
||||
try {
|
||||
this.runWasCalled.set(true);
|
||||
return toRun.get();
|
||||
}
|
||||
catch (Throwable throwable) {
|
||||
throw new NoFallbackAvailableException("No fallback available.", throwable);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public <T> T run(Supplier<T> toRun, Function<Throwable, T> fallback) {
|
||||
try {
|
||||
return run(toRun);
|
||||
}
|
||||
catch (Throwable throwable) {
|
||||
return fallback.apply(throwable);
|
||||
}
|
||||
}
|
||||
|
||||
public void clear() {
|
||||
this.runWasCalled.set(false);
|
||||
}
|
||||
|
||||
}
|
||||
+185
-5
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,7 @@
|
||||
|
||||
package org.springframework.cloud.openfeign.encoding;
|
||||
|
||||
import java.util.List;
|
||||
import java.util.Optional;
|
||||
|
||||
import org.junit.Test;
|
||||
@@ -31,7 +32,6 @@ import org.springframework.cloud.loadbalancer.annotation.LoadBalancerClient;
|
||||
import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier;
|
||||
import org.springframework.cloud.loadbalancer.support.ServiceInstanceListSuppliers;
|
||||
import org.springframework.cloud.openfeign.EnableFeignClients;
|
||||
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
|
||||
import org.springframework.cloud.openfeign.encoding.app.client.InvoiceClient;
|
||||
import org.springframework.cloud.openfeign.encoding.app.domain.Invoice;
|
||||
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
|
||||
@@ -54,9 +54,10 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
|
||||
* Tests the pagination encoding.
|
||||
*
|
||||
* @author Charlie Mordant.
|
||||
* @author Hyeonmin Park
|
||||
*/
|
||||
@SpringBootTest(classes = FeignPageableEncodingTests.Application.class, webEnvironment = RANDOM_PORT,
|
||||
value = { "feign.compression.request.enabled=true", "ribbon.OkToRetryOnAllOperations=false" })
|
||||
value = { "feign.compression.request.enabled=true", "feign.autoconfiguration.jackson.enabled=true" })
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
public class FeignPageableEncodingTests {
|
||||
|
||||
@@ -65,7 +66,6 @@ public class FeignPageableEncodingTests {
|
||||
|
||||
@Test
|
||||
public void testPageable() {
|
||||
|
||||
// given
|
||||
Pageable pageable = PageRequest.of(0, 10, Sort.Direction.ASC, "sortProperty");
|
||||
|
||||
@@ -84,7 +84,187 @@ public class FeignPageableEncodingTests {
|
||||
assertThat(order.getDirection()).isEqualTo(Sort.Direction.ASC);
|
||||
assertThat(order.getProperty()).isEqualTo("sortProperty");
|
||||
}
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPageableWithDescDirection() {
|
||||
// given
|
||||
Pageable pageable = PageRequest.of(0, 10, Sort.Direction.DESC, "sortProperty");
|
||||
|
||||
// when
|
||||
final ResponseEntity<Page<Invoice>> response = this.invoiceClient.getInvoicesPaged(pageable);
|
||||
|
||||
// then
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(response.getBody()).isNotNull();
|
||||
assertThat(pageable.getPageSize()).isEqualTo(response.getBody().getSize());
|
||||
|
||||
Sort sort = response.getBody().getPageable().getSort();
|
||||
assertThat(sort).hasSize(1);
|
||||
assertThat(sort.get()).hasSize(1);
|
||||
|
||||
Optional<Sort.Order> optionalOrder = sort.get().findFirst();
|
||||
assertThat(optionalOrder.isPresent()).isTrue();
|
||||
|
||||
Sort.Order order = optionalOrder.get();
|
||||
assertThat(order.getDirection()).isEqualTo(Sort.Direction.DESC);
|
||||
assertThat(order.getProperty()).isEqualTo("sortProperty");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPageableWithMultipleSort() {
|
||||
// given
|
||||
Pageable pageable = PageRequest.of(0, 10,
|
||||
Sort.by(Sort.Order.desc("sortProperty1"), Sort.Order.asc("sortProperty2")));
|
||||
|
||||
// when
|
||||
final ResponseEntity<Page<Invoice>> response = this.invoiceClient.getInvoicesPaged(pageable);
|
||||
|
||||
// then
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(response.getBody()).isNotNull();
|
||||
assertThat(pageable.getPageSize()).isEqualTo(response.getBody().getSize());
|
||||
|
||||
Sort sort = response.getBody().getPageable().getSort();
|
||||
assertThat(sort).hasSize(2);
|
||||
|
||||
List<Sort.Order> orderList = sort.toList();
|
||||
assertThat(orderList).hasSize(2);
|
||||
|
||||
Sort.Order firstOrder = orderList.get(0);
|
||||
assertThat(firstOrder.getDirection()).isEqualTo(Sort.Direction.DESC);
|
||||
assertThat(firstOrder.getProperty()).isEqualTo("sortProperty1");
|
||||
|
||||
Sort.Order secondOrder = orderList.get(1);
|
||||
assertThat(secondOrder.getDirection()).isEqualTo(Sort.Direction.ASC);
|
||||
assertThat(secondOrder.getProperty()).isEqualTo("sortProperty2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPageableWithoutSort() {
|
||||
// given
|
||||
Pageable pageable = PageRequest.of(0, 10);
|
||||
|
||||
// when
|
||||
final ResponseEntity<Page<Invoice>> response = this.invoiceClient.getInvoicesPaged(pageable);
|
||||
|
||||
// then
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(response.getBody()).isNotNull();
|
||||
assertThat(pageable.getPageSize()).isEqualTo(response.getBody().getSize());
|
||||
assertThat(response.getBody().getPageable().getSort().isSorted()).isFalse();
|
||||
|
||||
List<Invoice> invoiceList = response.getBody().getContent();
|
||||
assertThat(invoiceList).hasSizeGreaterThanOrEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPageableWithoutSortWithBody() {
|
||||
// given
|
||||
Pageable pageable = PageRequest.of(0, 10);
|
||||
|
||||
// when
|
||||
final ResponseEntity<Page<Invoice>> response = this.invoiceClient.getInvoicesPagedWithBody(pageable,
|
||||
"InvoiceTitleFromBody");
|
||||
|
||||
// then
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(response.getBody()).isNotNull();
|
||||
assertThat(pageable.getPageSize()).isEqualTo(response.getBody().getSize());
|
||||
|
||||
List<Invoice> invoiceList = response.getBody().getContent();
|
||||
assertThat(invoiceList).hasSizeGreaterThanOrEqualTo(1);
|
||||
|
||||
Invoice firstInvoice = invoiceList.get(0);
|
||||
assertThat(firstInvoice.getTitle()).startsWith("InvoiceTitleFromBody");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testPageableWithBody() {
|
||||
// given
|
||||
Pageable pageable = PageRequest.of(0, 10,
|
||||
Sort.by(Sort.Order.desc("sortProperty1"), Sort.Order.asc("sortProperty2")));
|
||||
|
||||
// when
|
||||
final ResponseEntity<Page<Invoice>> response = this.invoiceClient.getInvoicesPagedWithBody(pageable,
|
||||
"InvoiceTitleFromBody");
|
||||
|
||||
// then
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(response.getBody()).isNotNull();
|
||||
assertThat(pageable.getPageSize()).isEqualTo(response.getBody().getSize());
|
||||
|
||||
List<Invoice> invoiceList = response.getBody().getContent();
|
||||
assertThat(invoiceList).hasSizeGreaterThanOrEqualTo(1);
|
||||
|
||||
Invoice firstInvoice = invoiceList.get(0);
|
||||
assertThat(firstInvoice.getTitle()).startsWith("InvoiceTitleFromBody");
|
||||
|
||||
Sort sort = response.getBody().getPageable().getSort();
|
||||
assertThat(sort).hasSize(2);
|
||||
|
||||
List<Sort.Order> orderList = sort.toList();
|
||||
assertThat(orderList).hasSize(2);
|
||||
|
||||
Sort.Order firstOrder = orderList.get(0);
|
||||
assertThat(firstOrder.getDirection()).isEqualTo(Sort.Direction.DESC);
|
||||
assertThat(firstOrder.getProperty()).isEqualTo("sortProperty1");
|
||||
|
||||
Sort.Order secondOrder = orderList.get(1);
|
||||
assertThat(secondOrder.getDirection()).isEqualTo(Sort.Direction.ASC);
|
||||
assertThat(secondOrder.getProperty()).isEqualTo("sortProperty2");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUnpagedWithBody() {
|
||||
// given
|
||||
Pageable unpaged = Pageable.unpaged();
|
||||
|
||||
// when
|
||||
final ResponseEntity<Page<Invoice>> response = this.invoiceClient.getInvoicesPagedWithBody(unpaged,
|
||||
"InvoiceTitleFromBody");
|
||||
|
||||
// then
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(response.getBody()).isNotNull();
|
||||
|
||||
List<Invoice> invoiceList = response.getBody().getContent();
|
||||
assertThat(invoiceList).hasSizeGreaterThanOrEqualTo(1);
|
||||
|
||||
Invoice firstInvoice = invoiceList.get(0);
|
||||
assertThat(firstInvoice.getTitle()).startsWith("InvoiceTitleFromBody");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testSortWithBody() {
|
||||
// given
|
||||
Sort sort = Sort.by(Sort.Order.desc("amount"));
|
||||
|
||||
// when
|
||||
final ResponseEntity<Page<Invoice>> response = this.invoiceClient.getInvoicesSortedWithBody(sort,
|
||||
"InvoiceTitleFromBody");
|
||||
|
||||
// then
|
||||
assertThat(response).isNotNull();
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
|
||||
assertThat(response.getBody()).isNotNull();
|
||||
assertThat(sort).isEqualTo(response.getBody().getSort());
|
||||
|
||||
List<Invoice> invoiceList = response.getBody().getContent();
|
||||
assertThat(invoiceList).hasSizeGreaterThanOrEqualTo(1);
|
||||
|
||||
Invoice firstInvoice = invoiceList.get(0);
|
||||
assertThat(firstInvoice.getTitle()).startsWith("InvoiceTitleFromBody");
|
||||
|
||||
for (int ind = 0; ind < invoiceList.size() - 1; ind++) {
|
||||
assertThat(invoiceList.get(ind).getAmount()).isGreaterThanOrEqualTo(invoiceList.get(ind + 1).getAmount());
|
||||
}
|
||||
}
|
||||
|
||||
@EnableFeignClients(clients = InvoiceClient.class)
|
||||
@@ -92,7 +272,7 @@ public class FeignPageableEncodingTests {
|
||||
@SpringBootApplication(scanBasePackages = "org.springframework.cloud.openfeign.encoding.app",
|
||||
exclude = { RepositoryRestMvcAutoConfiguration.class })
|
||||
@EnableSpringDataWebSupport
|
||||
@Import({ NoSecurityConfiguration.class, FeignClientsConfiguration.class })
|
||||
@Import({ NoSecurityConfiguration.class })
|
||||
public static class Application {
|
||||
|
||||
}
|
||||
|
||||
+18
-5
@@ -19,28 +19,41 @@ package org.springframework.cloud.openfeign.encoding.app.client;
|
||||
import java.util.List;
|
||||
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.cloud.openfeign.SpringQueryMap;
|
||||
import org.springframework.cloud.openfeign.encoding.app.domain.Invoice;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
|
||||
/**
|
||||
* Simple Feign client for retrieving the invoice list.
|
||||
*
|
||||
* @author Jakub Narloch
|
||||
* @author Hyeonmin Park
|
||||
*/
|
||||
@FeignClient("local")
|
||||
public interface InvoiceClient {
|
||||
|
||||
@RequestMapping(value = "invoicesPaged", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@GetMapping(value = "invoicesPaged", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
ResponseEntity<Page<Invoice>> getInvoicesPaged(org.springframework.data.domain.Pageable pageable);
|
||||
|
||||
@RequestMapping(value = "invoices", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@PostMapping(value = "invoicesPagedWithBody", consumes = MediaType.APPLICATION_JSON_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
ResponseEntity<Page<Invoice>> getInvoicesPagedWithBody(
|
||||
@SpringQueryMap org.springframework.data.domain.Pageable pageable, @RequestBody String titlePrefix);
|
||||
|
||||
@PostMapping(value = "invoicesSortedWithBody", consumes = MediaType.APPLICATION_JSON_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
ResponseEntity<Page<Invoice>> getInvoicesSortedWithBody(@SpringQueryMap org.springframework.data.domain.Sort sort,
|
||||
@RequestBody String titlePrefix);
|
||||
|
||||
@GetMapping(value = "invoices", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
ResponseEntity<List<Invoice>> getInvoices();
|
||||
|
||||
@RequestMapping(value = "invoices", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE,
|
||||
@PostMapping(value = "invoices", consumes = MediaType.APPLICATION_JSON_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
ResponseEntity<List<Invoice>> saveInvoices(List<Invoice> invoices);
|
||||
|
||||
|
||||
+63
-9
@@ -18,54 +18,108 @@ package org.springframework.cloud.openfeign.encoding.app.resource;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Comparator;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
|
||||
import org.springframework.cloud.openfeign.encoding.app.domain.Invoice;
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* An sample REST controller, that potentially returns large response - used for testing.
|
||||
*
|
||||
* @author Jakub Narloch
|
||||
* @author Hyeonmin Park
|
||||
*/
|
||||
@RestController
|
||||
public class InvoiceResource {
|
||||
|
||||
@RequestMapping(value = "invoices", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@GetMapping(value = "invoices", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<List<Invoice>> getInvoices() {
|
||||
|
||||
return ResponseEntity.ok(createInvoiceList(100));
|
||||
return ResponseEntity.ok(createInvoiceList(null, 100, null));
|
||||
}
|
||||
|
||||
@RequestMapping(value = "invoices", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE,
|
||||
@PostMapping(value = "invoices", consumes = MediaType.APPLICATION_JSON_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
ResponseEntity<List<Invoice>> saveInvoices(@RequestBody List<Invoice> invoices) {
|
||||
|
||||
return ResponseEntity.ok(invoices);
|
||||
}
|
||||
|
||||
@RequestMapping(value = "invoicesPaged", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@GetMapping(value = "invoicesPaged", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<Page<Invoice>> getInvoicesPaged(org.springframework.data.domain.Pageable pageable) {
|
||||
Page<Invoice> page = new PageImpl<>(createInvoiceList(pageable.getPageSize()), pageable, 100);
|
||||
Page<Invoice> page = new PageImpl<>(createInvoiceList(null, pageable.getPageSize(), pageable.getSort()),
|
||||
pageable, 100);
|
||||
return ResponseEntity.ok(page);
|
||||
}
|
||||
|
||||
private List<Invoice> createInvoiceList(int count) {
|
||||
@PostMapping(value = "invoicesPagedWithBody", consumes = MediaType.APPLICATION_JSON_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<Page<Invoice>> getInvoicesPagedWithBody(org.springframework.data.domain.Pageable pageable,
|
||||
@RequestBody String titlePrefix) {
|
||||
Page<Invoice> page = new PageImpl<>(createInvoiceList(titlePrefix, pageable.getPageSize(), pageable.getSort()),
|
||||
pageable, 100);
|
||||
return ResponseEntity.ok(page);
|
||||
}
|
||||
|
||||
@PostMapping(value = "invoicesSortedWithBody", consumes = MediaType.APPLICATION_JSON_VALUE,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public ResponseEntity<Page<Invoice>> getInvoicesSortedWithBody(org.springframework.data.domain.Sort sort,
|
||||
@RequestBody String titlePrefix) {
|
||||
Page<Invoice> page = new PageImpl<>(createInvoiceList(titlePrefix, 100, sort), PageRequest.of(0, 100, sort),
|
||||
100);
|
||||
return ResponseEntity.ok(page);
|
||||
}
|
||||
|
||||
private List<Invoice> createInvoiceList(String titlePrefix, int count, org.springframework.data.domain.Sort sort) {
|
||||
if (titlePrefix == null) {
|
||||
titlePrefix = "Invoice";
|
||||
}
|
||||
final List<Invoice> invoices = new ArrayList<>();
|
||||
for (int ind = 0; ind < count; ind++) {
|
||||
final Invoice invoice = new Invoice();
|
||||
invoice.setTitle("Invoice " + (ind + 1));
|
||||
invoice.setTitle(titlePrefix + " " + (ind + 1));
|
||||
invoice.setAmount(new BigDecimal(String.format(Locale.US, "%.2f", Math.random() * 1000)));
|
||||
invoices.add(invoice);
|
||||
}
|
||||
if (sort != null) {
|
||||
Comparator<Invoice> comparatorForSort = null;
|
||||
for (org.springframework.data.domain.Sort.Order order : sort) {
|
||||
Comparator<Invoice> comparatorForOrder;
|
||||
if (order.getProperty().equals("title")) {
|
||||
comparatorForOrder = Comparator.comparing(Invoice::getTitle);
|
||||
}
|
||||
else if (order.getProperty().equals("amount")) {
|
||||
comparatorForOrder = Comparator.comparing(Invoice::getAmount);
|
||||
}
|
||||
else {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (order.isDescending()) {
|
||||
comparatorForOrder = comparatorForOrder.reversed();
|
||||
}
|
||||
|
||||
if (comparatorForSort == null) {
|
||||
comparatorForSort = comparatorForOrder;
|
||||
}
|
||||
else {
|
||||
comparatorForSort = comparatorForSort.thenComparing(comparatorForOrder);
|
||||
}
|
||||
}
|
||||
if (comparatorForSort != null) {
|
||||
invoices.sort(comparatorForSort);
|
||||
}
|
||||
}
|
||||
return invoices;
|
||||
}
|
||||
|
||||
|
||||
+2
-7
@@ -20,7 +20,6 @@ import feign.RequestTemplate;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.ObjectFactory;
|
||||
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
|
||||
import org.springframework.cloud.openfeign.support.SpringEncoder;
|
||||
@@ -41,12 +40,8 @@ public class ProtobufNotInClasspathTest {
|
||||
|
||||
@Test
|
||||
public void testEncodeWhenProtobufNotInClasspath() {
|
||||
ObjectFactory<HttpMessageConverters> converters = new ObjectFactory<HttpMessageConverters>() {
|
||||
@Override
|
||||
public HttpMessageConverters getObject() throws BeansException {
|
||||
return new HttpMessageConverters(new StringHttpMessageConverter());
|
||||
}
|
||||
};
|
||||
ObjectFactory<HttpMessageConverters> converters = () -> new HttpMessageConverters(
|
||||
new StringHttpMessageConverter());
|
||||
RequestTemplate requestTemplate = new RequestTemplate();
|
||||
requestTemplate.method(POST);
|
||||
new SpringEncoder(converters).encode("a=b", String.class, requestTemplate);
|
||||
|
||||
+8
-17
@@ -40,11 +40,9 @@ import org.junit.runner.RunWith;
|
||||
import org.mockito.ArgumentMatchers;
|
||||
import org.mockito.BDDMockito;
|
||||
import org.mockito.Mock;
|
||||
import org.mockito.invocation.InvocationOnMock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
import org.mockito.stubbing.Answer;
|
||||
|
||||
import org.springframework.beans.BeansException;
|
||||
import org.springframework.beans.factory.ObjectFactory;
|
||||
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
|
||||
import org.springframework.cloud.openfeign.support.SpringEncoder;
|
||||
@@ -72,7 +70,7 @@ public class ProtobufSpringEncoderTest {
|
||||
.setMsg("Erlang/OTP 最初是爱立信为开发电信设备系统设计的编程语言平台," + "电信设备(路由器、接入网关、…)典型设计是通过背板连接主控板卡与多块业务板卡的分布式系统。").build();
|
||||
|
||||
@Test
|
||||
public void testProtobuf() throws IOException, URISyntaxException {
|
||||
public void testProtobuf() throws IOException {
|
||||
// protobuf convert to request by feign and ProtobufHttpMessageConverter
|
||||
RequestTemplate requestTemplate = newRequestTemplate();
|
||||
newEncoder().encode(this.request, Request.class, requestTemplate);
|
||||
@@ -108,12 +106,8 @@ public class ProtobufSpringEncoderTest {
|
||||
}
|
||||
|
||||
private SpringEncoder newEncoder() {
|
||||
ObjectFactory<HttpMessageConverters> converters = new ObjectFactory<HttpMessageConverters>() {
|
||||
@Override
|
||||
public HttpMessageConverters getObject() throws BeansException {
|
||||
return new HttpMessageConverters(new ProtobufHttpMessageConverter());
|
||||
}
|
||||
};
|
||||
ObjectFactory<HttpMessageConverters> converters = () -> new HttpMessageConverters(
|
||||
new ProtobufHttpMessageConverter());
|
||||
return new SpringEncoder(converters);
|
||||
}
|
||||
|
||||
@@ -123,15 +117,12 @@ public class ProtobufSpringEncoderTest {
|
||||
return requestTemplate;
|
||||
}
|
||||
|
||||
private HttpEntity toApacheHttpEntity(RequestTemplate requestTemplate) throws IOException, URISyntaxException {
|
||||
private HttpEntity toApacheHttpEntity(RequestTemplate requestTemplate) throws IOException {
|
||||
final List<HttpUriRequest> request = new ArrayList<>(1);
|
||||
BDDMockito.given(this.httpClient.execute(ArgumentMatchers.<HttpUriRequest>any()))
|
||||
.will(new Answer<HttpResponse>() {
|
||||
@Override
|
||||
public HttpResponse answer(InvocationOnMock invocationOnMock) throws Throwable {
|
||||
request.add((HttpUriRequest) invocationOnMock.getArguments()[0]);
|
||||
return new BasicHttpResponse(new BasicStatusLine(new ProtocolVersion("http", 1, 1), 200, null));
|
||||
}
|
||||
BDDMockito.given(this.httpClient.execute(ArgumentMatchers.any()))
|
||||
.will((Answer<HttpResponse>) invocationOnMock -> {
|
||||
request.add((HttpUriRequest) invocationOnMock.getArguments()[0]);
|
||||
return new BasicHttpResponse(new BasicStatusLine(new ProtocolVersion("http", 1, 1), 200, null));
|
||||
});
|
||||
new ApacheHttpClient(this.httpClient).execute(requestTemplate.resolve(new HashMap<>()).request(),
|
||||
new feign.Request.Options());
|
||||
|
||||
+14
-11
@@ -16,7 +16,7 @@
|
||||
|
||||
package org.springframework.cloud.openfeign.hateoas;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.Test;
|
||||
@@ -26,9 +26,11 @@ import org.mockito.Mock;
|
||||
import org.mockito.junit.MockitoJUnitRunner;
|
||||
|
||||
import org.springframework.beans.factory.ObjectProvider;
|
||||
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
|
||||
import org.springframework.hateoas.mediatype.MessageResolver;
|
||||
import org.springframework.hateoas.mediatype.hal.CurieProvider;
|
||||
import org.springframework.hateoas.mediatype.hal.HalConfiguration;
|
||||
import org.springframework.hateoas.mediatype.hal.HalMediaTypeConfiguration;
|
||||
import org.springframework.hateoas.mediatype.hal.Jackson2HalModule;
|
||||
import org.springframework.hateoas.server.LinkRelationProvider;
|
||||
import org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter;
|
||||
@@ -41,24 +43,25 @@ import static org.springframework.hateoas.MediaTypes.HAL_JSON;
|
||||
|
||||
/**
|
||||
* @author Hector Espert
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@RunWith(MockitoJUnitRunner.class)
|
||||
public class FeignHalAutoConfigurationTests {
|
||||
|
||||
@Mock
|
||||
private ObjectProvider<ObjectMapper> objectMapper;
|
||||
|
||||
@Mock
|
||||
private ObjectProvider<HalConfiguration> halConfiguration;
|
||||
|
||||
@Mock
|
||||
private ObjectProvider<LinkRelationProvider> relProvider;
|
||||
private ObjectProvider<ObjectMapper> objectMapper;
|
||||
|
||||
@Mock
|
||||
private LinkRelationProvider relProvider;
|
||||
|
||||
@Mock
|
||||
private ObjectProvider<CurieProvider> curieProvider;
|
||||
|
||||
@Mock
|
||||
private ObjectProvider<MessageResolver> messageResolver;
|
||||
private MessageResolver messageResolver;
|
||||
|
||||
@InjectMocks
|
||||
private FeignHalAutoConfiguration feignHalAutoConfiguration;
|
||||
@@ -69,17 +72,17 @@ public class FeignHalAutoConfigurationTests {
|
||||
when(objectMapper.getIfAvailable(any())).thenReturn(mapper);
|
||||
|
||||
when(halConfiguration.getIfAvailable(any())).thenReturn(mock(HalConfiguration.class));
|
||||
when(relProvider.getIfAvailable()).thenReturn(mock(LinkRelationProvider.class));
|
||||
when(curieProvider.getIfAvailable(any())).thenReturn(mock(CurieProvider.class));
|
||||
when(messageResolver.getIfAvailable()).thenReturn(mock(MessageResolver.class));
|
||||
|
||||
HalMediaTypeConfiguration halMediaTypeConfiguration = new HalMediaTypeConfiguration(relProvider, curieProvider,
|
||||
halConfiguration, messageResolver, new DefaultListableBeanFactory());
|
||||
|
||||
TypeConstrainedMappingJackson2HttpMessageConverter converter = feignHalAutoConfiguration
|
||||
.halJacksonHttpMessageConverter(objectMapper, halConfiguration, messageResolver, curieProvider,
|
||||
relProvider);
|
||||
.halJacksonHttpMessageConverter(objectMapper, halMediaTypeConfiguration);
|
||||
|
||||
assertThat(converter).isNotNull();
|
||||
assertThat(converter.getObjectMapper()).isNotNull();
|
||||
assertThat(converter.getSupportedMediaTypes()).isEqualTo(Arrays.asList(HAL_JSON));
|
||||
assertThat(converter.getSupportedMediaTypes()).isEqualTo(Collections.singletonList(HAL_JSON));
|
||||
|
||||
assertThat(Jackson2HalModule.isAlreadyRegisteredIn(converter.getObjectMapper())).isTrue();
|
||||
}
|
||||
|
||||
+5
-6
@@ -28,8 +28,7 @@ import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -74,7 +73,7 @@ public class FeignClientValidationTests {
|
||||
@FeignClient(name = "bar")
|
||||
interface Client {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/")
|
||||
@GetMapping("/")
|
||||
String get();
|
||||
|
||||
}
|
||||
@@ -90,7 +89,7 @@ public class FeignClientValidationTests {
|
||||
@FeignClient(contextId = "foo", name = "bar")
|
||||
interface FooClient {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/")
|
||||
@GetMapping("/")
|
||||
String get();
|
||||
|
||||
}
|
||||
@@ -98,7 +97,7 @@ public class FeignClientValidationTests {
|
||||
@FeignClient(name = "bar")
|
||||
interface BarClient {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/")
|
||||
@GetMapping("/")
|
||||
String get();
|
||||
|
||||
}
|
||||
@@ -113,7 +112,7 @@ public class FeignClientValidationTests {
|
||||
@FeignClient("foo_bar")
|
||||
interface Client {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/")
|
||||
@GetMapping("/")
|
||||
String get();
|
||||
|
||||
}
|
||||
|
||||
+1
-1
@@ -63,7 +63,7 @@ import static org.mockito.Mockito.when;
|
||||
* {@link FeignBlockingLoadBalancerClient} and its delegates.
|
||||
*
|
||||
* @see <a href=
|
||||
* "https://github.com/spring-cloud/spring-cloud-commons/blob/master/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/blocking/client/BlockingLoadBalancerClientTests.java">BlockingLoadBalancerClientTests</a>
|
||||
* "https://github.com/spring-cloud/spring-cloud-commons/blob/main/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/blocking/client/BlockingLoadBalancerClientTests.java">BlockingLoadBalancerClientTests</a>
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
|
||||
+41
-6
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -19,6 +19,7 @@ package org.springframework.cloud.openfeign.loadbalancer;
|
||||
import java.util.Map;
|
||||
|
||||
import feign.Client;
|
||||
import feign.hc5.ApacheHttp5Client;
|
||||
import feign.httpclient.ApacheHttpClient;
|
||||
import feign.okhttp.OkHttpClient;
|
||||
import org.junit.jupiter.api.Test;
|
||||
@@ -35,6 +36,7 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Nguyen Ky Thanh
|
||||
*/
|
||||
class FeignLoadBalancerAutoConfigurationTests {
|
||||
|
||||
@@ -61,29 +63,62 @@ class FeignLoadBalancerAutoConfigurationTests {
|
||||
assertLoadBalanced(context, OkHttpClient.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInstantiateHttpFeignClient5WhenEnabled() {
|
||||
ConfigurableApplicationContext context = initContext("feign.httpclient.enabled=false",
|
||||
"feign.okhttp.enabled=false", "feign.httpclient.hc5.enabled=true",
|
||||
"spring.cloud.loadbalancer.retry.enabled=false");
|
||||
assertThatOneBeanPresent(context, BlockingLoadBalancerClient.class);
|
||||
assertLoadBalanced(context, ApacheHttp5Client.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInstantiateHttpFeignClient5WhenBothHttpClientAndHttpClient5Enabled() {
|
||||
ConfigurableApplicationContext context = initContext("feign.httpclient.enabled=true",
|
||||
"feign.okhttp.enabled=false", "feign.httpclient.hc5.enabled=true",
|
||||
"spring.cloud.loadbalancer.retry.enabled=false");
|
||||
assertThatOneBeanPresent(context, BlockingLoadBalancerClient.class);
|
||||
assertLoadBalanced(context, ApacheHttp5Client.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInstantiateRetryableDefaultFeignBlockingLoadBalancerClientWhenHttpClientDisabled() {
|
||||
ConfigurableApplicationContext context = initContext("spring.cloud.loadbalancer.ribbon.enabled=false",
|
||||
"feign.httpclient.enabled=false");
|
||||
ConfigurableApplicationContext context = initContext("feign.httpclient.enabled=false");
|
||||
assertThatOneBeanPresent(context, BlockingLoadBalancerClient.class);
|
||||
assertLoadBalancedWithRetries(context, Client.Default.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInstantiateRetryableHttpFeignClientWhenEnabled() {
|
||||
ConfigurableApplicationContext context = initContext("spring.cloud.loadbalancer.ribbon.enabled=false");
|
||||
ConfigurableApplicationContext context = initContext();
|
||||
assertThatOneBeanPresent(context, BlockingLoadBalancerClient.class);
|
||||
assertLoadBalancedWithRetries(context, ApacheHttpClient.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInstantiateRetryableOkHttpFeignClientWhenEnabled() {
|
||||
ConfigurableApplicationContext context = initContext("spring.cloud.loadbalancer.ribbon.enabled=false",
|
||||
"feign.httpclient.enabled=false", "feign.okhttp.enabled=true");
|
||||
ConfigurableApplicationContext context = initContext("feign.httpclient.enabled=false",
|
||||
"feign.okhttp.enabled=true");
|
||||
assertThatOneBeanPresent(context, BlockingLoadBalancerClient.class);
|
||||
assertLoadBalancedWithRetries(context, OkHttpClient.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInstantiateRetryableHttpFeignClient5WhenEnabled() {
|
||||
ConfigurableApplicationContext context = initContext("feign.httpclient.enabled=false",
|
||||
"feign.okhttp.enabled=false", "feign.httpclient.hc5.enabled=true");
|
||||
assertThatOneBeanPresent(context, BlockingLoadBalancerClient.class);
|
||||
assertLoadBalancedWithRetries(context, ApacheHttp5Client.class);
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldInstantiateRetryableHttpFeignClient5WhenBothHttpClientAndHttpClient5Enabled() {
|
||||
ConfigurableApplicationContext context = initContext("feign.httpclient.enabled=true",
|
||||
"feign.okhttp.enabled=false", "feign.httpclient.hc5.enabled=true");
|
||||
assertThatOneBeanPresent(context, BlockingLoadBalancerClient.class);
|
||||
assertLoadBalancedWithRetries(context, ApacheHttp5Client.class);
|
||||
}
|
||||
|
||||
private ConfigurableApplicationContext initContext(String... properties) {
|
||||
return new SpringApplicationBuilder().web(WebApplicationType.NONE).properties(properties)
|
||||
.sources(HttpClientConfiguration.class, LoadBalancerAutoConfiguration.class,
|
||||
|
||||
+28
-1
@@ -16,7 +16,10 @@
|
||||
|
||||
package org.springframework.cloud.openfeign.loadbalancer;
|
||||
|
||||
import java.io.BufferedInputStream;
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.Collection;
|
||||
@@ -29,6 +32,7 @@ import java.util.concurrent.ConcurrentHashMap;
|
||||
import feign.Client;
|
||||
import feign.Request;
|
||||
import feign.Response;
|
||||
import org.apache.commons.io.IOUtils;
|
||||
import org.junit.jupiter.api.BeforeEach;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.api.extension.ExtendWith;
|
||||
@@ -66,7 +70,7 @@ import static org.mockito.Mockito.when;
|
||||
* {@link RetryableFeignBlockingLoadBalancerClient} and its delegates.
|
||||
*
|
||||
* @see <a href=
|
||||
* "https://github.com/spring-cloud/spring-cloud-commons/blob/master/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/blocking/client/BlockingLoadBalancerClientTests.java">BlockingLoadBalancerClientTests</a>
|
||||
* "https://github.com/spring-cloud/spring-cloud-commons/blob/main/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/blocking/client/BlockingLoadBalancerClientTests.java">BlockingLoadBalancerClientTests</a>
|
||||
* @author Olga Maciaszek-Sharma
|
||||
*/
|
||||
@ExtendWith(MockitoExtension.class)
|
||||
@@ -117,6 +121,13 @@ class RetryableFeignBlockingLoadBalancerClientTests {
|
||||
return Response.builder().request(testRequest()).status(status).build();
|
||||
}
|
||||
|
||||
private Response testResponse(int status, String body) {
|
||||
// ByteArrayInputStream ignores close() and must be wrapped
|
||||
InputStream reallyCloseable = new BufferedInputStream(
|
||||
new ByteArrayInputStream(body.getBytes(StandardCharsets.UTF_8)));
|
||||
return Response.builder().request(testRequest()).status(status).body(reallyCloseable, null).build();
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExecuteOriginalRequestIfInstanceNotFound() throws IOException {
|
||||
Request request = testRequest();
|
||||
@@ -148,6 +159,22 @@ class RetryableFeignBlockingLoadBalancerClientTests {
|
||||
verify(delegate, times(2)).execute(any(), any());
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldExposeResponseBodyOnRetry() throws IOException {
|
||||
properties.getRetry().getRetryableStatusCodes().add(503);
|
||||
Request request = testRequest();
|
||||
when(delegate.execute(any(), any())).thenReturn(testResponse(503, "foo"), testResponse(503, "foo"));
|
||||
when(retryFactory.createRetryPolicy(any(), eq(loadBalancerClient)))
|
||||
.thenReturn(new BlockingLoadBalancedRetryPolicy(properties));
|
||||
when(loadBalancerClient.reconstructURI(serviceInstance, URI.create("http://test/path")))
|
||||
.thenReturn(URI.create("http://testhost:80/path"));
|
||||
|
||||
Response response = feignBlockingLoadBalancerClient.execute(request, new Request.Options());
|
||||
|
||||
String bodyContent = IOUtils.toString(response.body().asReader(StandardCharsets.UTF_8));
|
||||
assertThat(bodyContent).isEqualTo("foo");
|
||||
}
|
||||
|
||||
@Test
|
||||
void shouldPassCorrectRequestToDelegate() throws IOException {
|
||||
Request request = testRequest();
|
||||
|
||||
+20
-2
@@ -1,5 +1,5 @@
|
||||
/*
|
||||
* Copyright 2013-2020 the original author or authors.
|
||||
* Copyright 2013-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
@@ -16,6 +16,8 @@
|
||||
|
||||
package org.springframework.cloud.openfeign.support;
|
||||
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import org.junit.After;
|
||||
import org.junit.Test;
|
||||
import org.junit.runner.RunWith;
|
||||
@@ -23,6 +25,8 @@ import org.junit.runner.RunWith;
|
||||
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.boot.test.util.TestPropertyValues;
|
||||
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties.Hc5Properties.PoolConcurrencyPolicy;
|
||||
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties.Hc5Properties.PoolReusePolicy;
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
@@ -30,9 +34,12 @@ import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit4.SpringRunner;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.cloud.openfeign.support.FeignHttpClientProperties.Hc5Properties.DEFAULT_SOCKET_TIMEOUT;
|
||||
import static org.springframework.cloud.openfeign.support.FeignHttpClientProperties.Hc5Properties.DEFAULT_SOCKET_TIMEOUT_UNIT;
|
||||
|
||||
/**
|
||||
* @author Ryan Baxter
|
||||
* @author Nguyen Ky Thanh
|
||||
*/
|
||||
@RunWith(SpringRunner.class)
|
||||
@DirtiesContext
|
||||
@@ -59,6 +66,10 @@ public class FeignHttpClientPropertiesTests {
|
||||
assertThat(getProperties().isDisableSslValidation())
|
||||
.isEqualTo(FeignHttpClientProperties.DEFAULT_DISABLE_SSL_VALIDATION);
|
||||
assertThat(getProperties().isFollowRedirects()).isEqualTo(FeignHttpClientProperties.DEFAULT_FOLLOW_REDIRECTS);
|
||||
assertThat(getProperties().getHc5().getPoolConcurrencyPolicy()).isEqualTo(PoolConcurrencyPolicy.STRICT);
|
||||
assertThat(getProperties().getHc5().getPoolReusePolicy()).isEqualTo(PoolReusePolicy.FIFO);
|
||||
assertThat(getProperties().getHc5().getSocketTimeout()).isEqualTo(DEFAULT_SOCKET_TIMEOUT);
|
||||
assertThat(getProperties().getHc5().getSocketTimeoutUnit()).isEqualTo(DEFAULT_SOCKET_TIMEOUT_UNIT);
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -66,7 +77,10 @@ public class FeignHttpClientPropertiesTests {
|
||||
TestPropertyValues
|
||||
.of("feign.httpclient.maxConnections=2", "feign.httpclient.connectionTimeout=2",
|
||||
"feign.httpclient.maxConnectionsPerRoute=2", "feign.httpclient.timeToLive=2",
|
||||
"feign.httpclient.disableSslValidation=true", "feign.httpclient.followRedirects=false")
|
||||
"feign.httpclient.disableSslValidation=true", "feign.httpclient.followRedirects=false",
|
||||
"feign.httpclient.disableSslValidation=true", "feign.httpclient.followRedirects=false",
|
||||
"feign.httpclient.hc5.poolConcurrencyPolicy=lax", "feign.httpclient.hc5.poolReusePolicy=lifo",
|
||||
"feign.httpclient.hc5.socketTimeout=200", "feign.httpclient.hc5.socketTimeoutUnit=milliseconds")
|
||||
.applyTo(this.context);
|
||||
setupContext();
|
||||
assertThat(getProperties().getMaxConnections()).isEqualTo(2);
|
||||
@@ -75,6 +89,10 @@ public class FeignHttpClientPropertiesTests {
|
||||
assertThat(getProperties().getTimeToLive()).isEqualTo(2L);
|
||||
assertThat(getProperties().isDisableSslValidation()).isTrue();
|
||||
assertThat(getProperties().isFollowRedirects()).isFalse();
|
||||
assertThat(getProperties().getHc5().getPoolConcurrencyPolicy()).isEqualTo(PoolConcurrencyPolicy.LAX);
|
||||
assertThat(getProperties().getHc5().getPoolReusePolicy()).isEqualTo(PoolReusePolicy.LIFO);
|
||||
assertThat(getProperties().getHc5().getSocketTimeout()).isEqualTo(200);
|
||||
assertThat(getProperties().getHc5().getSocketTimeoutUnit()).isEqualTo(TimeUnit.MILLISECONDS);
|
||||
}
|
||||
|
||||
private void setupContext() {
|
||||
|
||||
+52
-3
@@ -16,17 +16,28 @@
|
||||
|
||||
package org.springframework.cloud.openfeign.support;
|
||||
|
||||
import java.util.ArrayList;
|
||||
|
||||
import com.fasterxml.jackson.core.JsonProcessingException;
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import org.junit.jupiter.api.BeforeAll;
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.junit.jupiter.params.ParameterizedTest;
|
||||
import org.junit.jupiter.params.provider.ValueSource;
|
||||
|
||||
import org.springframework.data.domain.Page;
|
||||
import org.springframework.data.domain.PageImpl;
|
||||
import org.springframework.data.domain.PageRequest;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* Test for {@link PageJacksonModule}.
|
||||
*
|
||||
* @author Ruben Vervaeke
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Pedro Mendes
|
||||
* @author Nikita Konev
|
||||
*/
|
||||
public class PageJacksonModuleTests {
|
||||
|
||||
@@ -36,12 +47,14 @@ public class PageJacksonModuleTests {
|
||||
public static void initialize() {
|
||||
objectMapper = new ObjectMapper();
|
||||
objectMapper.registerModule(new PageJacksonModule());
|
||||
objectMapper.registerModule(new SortJacksonModule());
|
||||
}
|
||||
|
||||
@Test
|
||||
public void deserializePage() throws JsonProcessingException {
|
||||
@ParameterizedTest
|
||||
@ValueSource(strings = { "totalElements", "total-elements", "total_elements", "totalelements", "TotalElements" })
|
||||
public void deserializePage(String totalElements) throws JsonProcessingException {
|
||||
// Given
|
||||
String pageJson = "{\"content\":[\"A name\"], \"number\":1, \"size\":2, \"totalElements\": 3}";
|
||||
String pageJson = "{\"content\":[\"A name\"], \"number\":1, \"size\":2, \"" + totalElements + "\": 3}";
|
||||
// When
|
||||
Page<?> result = objectMapper.readValue(pageJson, Page.class);
|
||||
// Then
|
||||
@@ -53,4 +66,40 @@ public class PageJacksonModuleTests {
|
||||
assertThat(result.getPageable().getPageNumber()).isEqualTo(1);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serializeAndDeserializeEmpty() throws JsonProcessingException {
|
||||
// Given
|
||||
PageImpl<Object> objects = new PageImpl<>(new ArrayList<>());
|
||||
String pageJson = objectMapper.writeValueAsString(objects);
|
||||
// When
|
||||
Page<?> result = objectMapper.readValue(pageJson, Page.class);
|
||||
// Then
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getTotalElements()).isEqualTo(0);
|
||||
assertThat(result.getContent()).hasSize(0);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void serializeAndDeserializeFilledMultiple() throws JsonProcessingException {
|
||||
// Given
|
||||
ArrayList<Object> strings0 = new ArrayList<>();
|
||||
strings0.add("first element");
|
||||
strings0.add("second element");
|
||||
PageImpl<Object> objects = new PageImpl<>(strings0, PageRequest.of(6, 2), 100);
|
||||
assertThat(objects.getContent()).hasSize(2);
|
||||
assertThat(objects.getPageable().getPageSize()).isEqualTo(2);
|
||||
|
||||
String pageJson = objectMapper.writeValueAsString(objects);
|
||||
// When
|
||||
Page<?> result = objectMapper.readValue(pageJson, Page.class);
|
||||
// Then
|
||||
assertThat(result).isNotNull();
|
||||
assertThat(result.getTotalElements()).isEqualTo(100);
|
||||
assertThat(result.getContent()).hasSize(2);
|
||||
assertThat(result.getContent().get(0)).isEqualTo("first element");
|
||||
assertThat(result.getContent().get(1)).isEqualTo("second element");
|
||||
assertThat(result.getPageable().getPageSize()).isEqualTo(2);
|
||||
assertThat(result.getPageable().getPageNumber()).isEqualTo(6);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+51
@@ -36,9 +36,11 @@ import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.cloud.openfeign.FeignContext;
|
||||
import org.springframework.cloud.openfeign.encoding.HttpEncoding;
|
||||
import org.springframework.context.ApplicationContext;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.core.ParameterizedTypeReference;
|
||||
import org.springframework.core.io.Resource;
|
||||
import org.springframework.http.HttpInputMessage;
|
||||
import org.springframework.http.HttpOutputMessage;
|
||||
import org.springframework.http.MediaType;
|
||||
@@ -47,6 +49,7 @@ import org.springframework.http.converter.GenericHttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageConverter;
|
||||
import org.springframework.http.converter.HttpMessageNotReadableException;
|
||||
import org.springframework.http.converter.HttpMessageNotWritableException;
|
||||
import org.springframework.http.converter.ResourceHttpMessageConverter;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
@@ -57,13 +60,16 @@ import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.springframework.http.HttpHeaders.ACCEPT;
|
||||
import static org.springframework.http.HttpHeaders.CONTENT_LENGTH;
|
||||
import static org.springframework.http.HttpHeaders.CONTENT_TYPE;
|
||||
import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE;
|
||||
import static org.springframework.http.MediaType.APPLICATION_OCTET_STREAM_VALUE;
|
||||
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;
|
||||
import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
* @author Olga Maciaszek-Sharma
|
||||
* @author Ahmad Mozafarnia
|
||||
* @author Can Bezmen
|
||||
*/
|
||||
@RunWith(SpringJUnit4ClassRunner.class)
|
||||
@SpringBootTest(classes = SpringEncoderTests.Application.class, webEnvironment = WebEnvironment.RANDOM_PORT,
|
||||
@@ -78,6 +84,9 @@ public class SpringEncoderTests {
|
||||
@Qualifier("myHttpMessageConverter")
|
||||
private HttpMessageConverter<?> myConverter;
|
||||
|
||||
@Autowired
|
||||
private ApplicationContext applicationContext;
|
||||
|
||||
@Autowired
|
||||
@Qualifier("myGenericHttpMessageConverter")
|
||||
private GenericHttpMessageConverter<?> myGenericConverter;
|
||||
@@ -172,6 +181,43 @@ public class SpringEncoderTests {
|
||||
assertThat(new String(request.requestBody().asBytes())).as("Body content cannot be decoded").contains("hi");
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testFromURLEncodedValue() {
|
||||
Encoder encoder = context.getInstance("formUrlEncoded", Encoder.class);
|
||||
assertThat(encoder).isNotNull();
|
||||
RequestTemplate request = new RequestTemplate();
|
||||
request.header(CONTENT_TYPE, APPLICATION_FORM_URLENCODED_VALUE);
|
||||
String body = "test";
|
||||
encoder.encode(body, String.class, request);
|
||||
assertThat(new String(request.requestBody().asBytes())).as("Body content cannot be decoded").contains(body);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testNoCharsetForBinaryFiles() {
|
||||
Encoder encoder = context.getInstance("test", Encoder.class);
|
||||
assertThat(encoder).isNotNull();
|
||||
RequestTemplate request = new RequestTemplate();
|
||||
request.header(CONTENT_TYPE, APPLICATION_OCTET_STREAM_VALUE);
|
||||
Resource resource = applicationContext.getResource("classpath:dummy.pdf");
|
||||
|
||||
encoder.encode(resource, Resource.class, request);
|
||||
|
||||
assertThat(request.requestBody().getEncoding()).isEmpty();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testUTF8CharsetForTextFiles() {
|
||||
Encoder encoder = context.getInstance("test", Encoder.class);
|
||||
assertThat(encoder).isNotNull();
|
||||
RequestTemplate request = new RequestTemplate();
|
||||
request.header(CONTENT_TYPE, TEXT_PLAIN_VALUE);
|
||||
String test = "test";
|
||||
|
||||
encoder.encode(test, String.class, request);
|
||||
|
||||
assertThat(request.requestBody().getEncoding().get().name()).isEqualTo("UTF-8");
|
||||
}
|
||||
|
||||
protected interface TestClient {
|
||||
|
||||
}
|
||||
@@ -200,6 +246,11 @@ public class SpringEncoderTests {
|
||||
return new MyHttpMessageConverter();
|
||||
}
|
||||
|
||||
@Bean
|
||||
ResourceHttpMessageConverter resourceHttpMessageConverter() {
|
||||
return new ResourceHttpMessageConverter();
|
||||
}
|
||||
|
||||
@Bean
|
||||
GenericHttpMessageConverter<?> myGenericHttpMessageConverter() {
|
||||
return new MyGenericHttpMessageConverter();
|
||||
|
||||
+44
-71
@@ -50,6 +50,7 @@ import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.MatrixVariable;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -62,6 +63,7 @@ import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY;
|
||||
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE;
|
||||
import static feign.CollectionFormat.SSV;
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
|
||||
import static org.junit.Assume.assumeTrue;
|
||||
|
||||
/**
|
||||
@@ -168,16 +170,12 @@ public class SpringMvcContractTests {
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessAnnotations_Class_AnnotationsGetSpecificTest() throws Exception {
|
||||
Method method = TestTemplate_Class_Annotations.class.getDeclaredMethod("getSpecificTest", String.class,
|
||||
String.class);
|
||||
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
|
||||
|
||||
assertThat(data.template().url()).isEqualTo("/prepend/{classId}/test/{testId}");
|
||||
assertThat(data.template().method()).isEqualTo("GET");
|
||||
|
||||
assertThat(data.indexToName().get(0).iterator().next()).isEqualTo("classId");
|
||||
assertThat(data.indexToName().get(1).iterator().next()).isEqualTo("testId");
|
||||
public void testProcessAnnotations_Class_Annotations_RequestMapping() {
|
||||
assertThatIllegalArgumentException().isThrownBy(() -> {
|
||||
Method method = TestTemplate_Class_RequestMapping.class.getDeclaredMethod("getSpecificTest", String.class,
|
||||
String.class);
|
||||
contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
|
||||
});
|
||||
}
|
||||
|
||||
@Test
|
||||
@@ -185,25 +183,13 @@ public class SpringMvcContractTests {
|
||||
Method method = TestTemplate_Class_Annotations.class.getDeclaredMethod("getAllTests", String.class);
|
||||
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
|
||||
|
||||
assertThat(data.template().url()).isEqualTo("/prepend/{classId}");
|
||||
assertThat(data.template().url()).isEqualTo("/");
|
||||
assertThat(data.template().method()).isEqualTo("GET");
|
||||
|
||||
assertThat(data.indexToName().get(0).iterator().next()).isEqualTo("classId");
|
||||
assertThat(data.template().decodeSlash()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessAnnotations_Class_AnnotationsGetAllTests_EncodeSlash() throws Exception {
|
||||
contract = new SpringMvcContract(Collections.emptyList(), getConversionService(), false);
|
||||
|
||||
Method method = TestTemplate_Class_Annotations.class.getDeclaredMethod("getAllTests", String.class);
|
||||
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
|
||||
|
||||
assertThat(data.template().url()).isEqualTo("/prepend/{classId}");
|
||||
|
||||
assertThat(data.template().decodeSlash()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessAnnotations_ExtendedInterface() throws Exception {
|
||||
Method extendedMethod = TestTemplate_Extended.class.getMethod("getAllTests", String.class);
|
||||
@@ -220,22 +206,6 @@ public class SpringMvcContractTests {
|
||||
assertThat(data.template().decodeSlash()).isTrue();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessAnnotations_ExtendedInterface_EncodeSlash() throws Exception {
|
||||
contract = new SpringMvcContract(Collections.emptyList(), getConversionService(), false);
|
||||
|
||||
Method extendedMethod = TestTemplate_Extended.class.getMethod("getAllTests", String.class);
|
||||
MethodMetadata extendedData = contract.parseAndValidateMetadata(extendedMethod.getDeclaringClass(),
|
||||
extendedMethod);
|
||||
|
||||
Method method = TestTemplate_Class_Annotations.class.getDeclaredMethod("getAllTests", String.class);
|
||||
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
|
||||
|
||||
assertThat(data.template().url()).isEqualTo(extendedData.template().url());
|
||||
assertThat(data.template().method()).isEqualTo(extendedData.template().method());
|
||||
assertThat(data.template().decodeSlash()).isFalse();
|
||||
}
|
||||
|
||||
@Test
|
||||
public void testProcessAnnotations_SimplePost() throws Exception {
|
||||
Method method = TestTemplate_Simple.class.getDeclaredMethod("postTest", TestObject.class);
|
||||
@@ -266,7 +236,7 @@ public class SpringMvcContractTests {
|
||||
Integer.class);
|
||||
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
|
||||
|
||||
assertThat(data.template().url()).isEqualTo("/advanced/test/{id}?amount=" + "{amount}");
|
||||
assertThat(data.template().url()).isEqualTo("/test/{id}?amount=" + "{amount}");
|
||||
assertThat(data.template().method()).isEqualTo("PUT");
|
||||
assertThat(data.template().headers().get("Accept").iterator().next())
|
||||
.isEqualTo(MediaType.APPLICATION_JSON_VALUE);
|
||||
@@ -296,7 +266,7 @@ public class SpringMvcContractTests {
|
||||
Integer.class);
|
||||
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
|
||||
|
||||
assertThat(data.template().url()).isEqualTo("/advanced/test/{id}?amount=" + "{amount}");
|
||||
assertThat(data.template().url()).isEqualTo("/test/{id}?amount=" + "{amount}");
|
||||
assertThat(data.template().method()).isEqualTo("PUT");
|
||||
assertThat(data.template().headers().get("Accept").iterator().next())
|
||||
.isEqualTo(MediaType.APPLICATION_JSON_VALUE);
|
||||
@@ -315,7 +285,7 @@ public class SpringMvcContractTests {
|
||||
Method method = TestTemplate_Advanced.class.getDeclaredMethod("getTest2", String.class, Integer.class);
|
||||
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
|
||||
|
||||
assertThat(data.template().url()).isEqualTo("/advanced/test2?amount=" + "{amount}");
|
||||
assertThat(data.template().url()).isEqualTo("/test2?amount=" + "{amount}");
|
||||
assertThat(data.template().method()).isEqualTo("PUT");
|
||||
assertThat(data.template().headers().get("Accept").iterator().next())
|
||||
.isEqualTo(MediaType.APPLICATION_JSON_VALUE);
|
||||
@@ -367,7 +337,7 @@ public class SpringMvcContractTests {
|
||||
Method method = TestTemplate_Advanced.class.getDeclaredMethod("getTest");
|
||||
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
|
||||
|
||||
assertThat(data.template().url()).isEqualTo("/advanced");
|
||||
assertThat(data.template().url()).isEqualTo("/");
|
||||
assertThat(data.template().method()).isEqualTo("GET");
|
||||
assertThat(data.template().headers().get("Accept").iterator().next())
|
||||
.isEqualTo(MediaType.APPLICATION_JSON_VALUE);
|
||||
@@ -459,7 +429,7 @@ public class SpringMvcContractTests {
|
||||
|
||||
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
|
||||
|
||||
assertThat(data.template().url()).isEqualTo("/advanced/testfallback/{id}?amount=" + "{amount}");
|
||||
assertThat(data.template().url()).isEqualTo("/testfallback/{id}?amount=" + "{amount}");
|
||||
assertThat(data.template().method()).isEqualTo("PUT");
|
||||
assertThat(data.template().headers().get("Accept").iterator().next())
|
||||
.isEqualTo(MediaType.APPLICATION_JSON_VALUE);
|
||||
@@ -587,13 +557,13 @@ public class SpringMvcContractTests {
|
||||
@RequestMapping(value = "/test/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
ResponseEntity<TestObject> getTest(@PathVariable("id") String id);
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
TestObject getTest();
|
||||
|
||||
@GetMapping(value = "/test/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
ResponseEntity<TestObject> getMappingTest(@PathVariable("id") String id);
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@PostMapping(produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
TestObject postTest(@RequestBody TestObject object);
|
||||
|
||||
@PostMapping(produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@@ -602,11 +572,18 @@ public class SpringMvcContractTests {
|
||||
}
|
||||
|
||||
@RequestMapping("/prepend/{classId}")
|
||||
public interface TestTemplate_Class_Annotations {
|
||||
public interface TestTemplate_Class_RequestMapping {
|
||||
|
||||
@RequestMapping(value = "/test/{testId}", method = RequestMethod.GET)
|
||||
TestObject getSpecificTest(@PathVariable("classId") String classId, @PathVariable("testId") String testId);
|
||||
|
||||
}
|
||||
|
||||
public interface TestTemplate_Class_Annotations {
|
||||
|
||||
@GetMapping("/test/{testId}")
|
||||
TestObject getSpecificTest(@PathVariable("classId") String classId, @PathVariable("testId") String testId);
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET)
|
||||
TestObject getAllTests(@PathVariable("classId") String classId);
|
||||
|
||||
@@ -618,47 +595,46 @@ public class SpringMvcContractTests {
|
||||
|
||||
public interface TestTemplate_Headers {
|
||||
|
||||
@RequestMapping(value = "/test/{id}", method = RequestMethod.GET, headers = "X-Foo=bar")
|
||||
@GetMapping(value = "/test/{id}", headers = "X-Foo=bar")
|
||||
ResponseEntity<TestObject> getTest(@PathVariable("id") String id);
|
||||
|
||||
}
|
||||
|
||||
public interface TestTemplate_HeadersWithoutValues {
|
||||
|
||||
@RequestMapping(value = "/test/{id}", method = RequestMethod.GET,
|
||||
headers = { "X-Foo", "!X-Bar", "X-Baz!=fooBar" })
|
||||
@GetMapping(value = "/test/{id}", headers = { "X-Foo", "!X-Bar", "X-Baz!=fooBar" })
|
||||
ResponseEntity<TestObject> getTest(@PathVariable("id") String id);
|
||||
|
||||
}
|
||||
|
||||
public interface TestTemplate_ListParams {
|
||||
|
||||
@RequestMapping(value = "/test", method = RequestMethod.GET)
|
||||
@GetMapping("/test")
|
||||
ResponseEntity<TestObject> getTest(@RequestParam("id") List<String> id);
|
||||
|
||||
}
|
||||
|
||||
public interface TestTemplate_ListParamsWithoutName {
|
||||
|
||||
@RequestMapping(value = "/test", method = RequestMethod.GET)
|
||||
@GetMapping("/test")
|
||||
ResponseEntity<TestObject> getTest(@RequestParam List<String> id);
|
||||
|
||||
}
|
||||
|
||||
public interface TestTemplate_MapParams {
|
||||
|
||||
@RequestMapping(value = "/test", method = RequestMethod.GET)
|
||||
@GetMapping("/test")
|
||||
ResponseEntity<TestObject> getTest(@RequestParam Map<String, String> params);
|
||||
|
||||
}
|
||||
|
||||
public interface TestTemplate_HeaderMap {
|
||||
|
||||
@RequestMapping(path = "/headerMap")
|
||||
@GetMapping("/headerMap")
|
||||
String headerMap(@RequestHeader MultiValueMap<String, String> headerMap,
|
||||
@RequestHeader(name = "aHeader") String aHeader);
|
||||
|
||||
@RequestMapping(path = "/headerMapMoreThanOnce")
|
||||
@GetMapping("/headerMapMoreThanOnce")
|
||||
String headerMapMoreThanOnce(@RequestHeader MultiValueMap<String, String> headerMap1,
|
||||
@RequestHeader MultiValueMap<String, String> headerMap2);
|
||||
|
||||
@@ -666,42 +642,40 @@ public class SpringMvcContractTests {
|
||||
|
||||
public interface TestTemplate_QueryMap {
|
||||
|
||||
@RequestMapping(path = "/queryMap")
|
||||
@GetMapping("/queryMap")
|
||||
String queryMap(@RequestParam MultiValueMap<String, String> queryMap,
|
||||
@RequestParam(name = "aParam") String aParam);
|
||||
|
||||
@RequestMapping(path = "/queryMapMoreThanOnce")
|
||||
@GetMapping("/queryMapMoreThanOnce")
|
||||
String queryMapMoreThanOnce(@RequestParam MultiValueMap<String, String> queryMap1,
|
||||
@RequestParam MultiValueMap<String, String> queryMap2);
|
||||
|
||||
@RequestMapping(path = "/queryMapObject")
|
||||
@GetMapping("/queryMapObject")
|
||||
String queryMapObject(@SpringQueryMap TestObject queryMap, @RequestParam(name = "aParam") String aParam);
|
||||
|
||||
}
|
||||
|
||||
public interface TestTemplate_RequestPart {
|
||||
|
||||
@RequestMapping(path = "/requestPart", method = RequestMethod.POST,
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@PostMapping(path = "/requestPart", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
void requestWithMultipleParts(@RequestPart("file") MultipartFile file, @RequestPart("id") String identifier);
|
||||
|
||||
}
|
||||
|
||||
public interface TestTemplate_MatrixVariable {
|
||||
|
||||
@RequestMapping(path = "/matrixVariable/{params}")
|
||||
@GetMapping("/matrixVariable/{params}")
|
||||
String matrixVariable(@MatrixVariable("params") Map<String, Object> params);
|
||||
|
||||
@RequestMapping(path = "/matrixVariableObject/{param}")
|
||||
@GetMapping("/matrixVariableObject/{param}")
|
||||
String matrixVariableObject(@MatrixVariable("param") Object object);
|
||||
|
||||
@RequestMapping(path = "/matrixVariable/{params}")
|
||||
@GetMapping("/matrixVariable/{params}")
|
||||
String matrixVariableNotNamed(@MatrixVariable Map<String, Object> params);
|
||||
|
||||
}
|
||||
|
||||
@JsonAutoDetect
|
||||
@RequestMapping("/advanced")
|
||||
public interface TestTemplate_Advanced {
|
||||
|
||||
@CollectionFormat(SSV)
|
||||
@@ -709,21 +683,20 @@ public class SpringMvcContractTests {
|
||||
ResponseEntity<TestObject> getWithCollectionFormat();
|
||||
|
||||
@ExceptionHandler
|
||||
@RequestMapping(path = "/test/{id}", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@PutMapping(path = "/test/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
ResponseEntity<TestObject> getTest(@RequestHeader("Authorization") String auth, @PathVariable("id") String id,
|
||||
@RequestParam("amount") Integer amount);
|
||||
|
||||
@RequestMapping(path = "/test2", method = RequestMethod.PUT, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@PutMapping(path = "/test2", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
ResponseEntity<TestObject> getTest2(@RequestHeader(name = "Authorization") String auth,
|
||||
@RequestParam(name = "amount") Integer amount);
|
||||
|
||||
@ExceptionHandler
|
||||
@RequestMapping(path = "/testfallback/{id}", method = RequestMethod.PUT,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@PutMapping(path = "/testfallback/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
ResponseEntity<TestObject> getTestFallback(@RequestHeader String Authorization, @PathVariable String id,
|
||||
@RequestParam Integer amount);
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@GetMapping(produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
TestObject getTest();
|
||||
|
||||
@GetMapping(produces = "application/json")
|
||||
@@ -735,7 +708,7 @@ public class SpringMvcContractTests {
|
||||
|
||||
String CUSTOM_PATTERN = "dd-MM-yyyy HH:mm";
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET)
|
||||
@GetMapping
|
||||
String getTest(@RequestParam(name = "localDateTime") @DateTimeFormat(
|
||||
pattern = CUSTOM_PATTERN) LocalDateTime localDateTime);
|
||||
|
||||
@@ -745,7 +718,7 @@ public class SpringMvcContractTests {
|
||||
|
||||
String CUSTOM_PATTERN = "$###,###.###";
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET)
|
||||
@GetMapping
|
||||
String getTest(@RequestParam("amount") @NumberFormat(pattern = CUSTOM_PATTERN) BigDecimal amount);
|
||||
|
||||
}
|
||||
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
/*
|
||||
* Copyright 2021-2021 the original author or authors.
|
||||
*
|
||||
* Licensed under the Apache License, Version 2.0 (the "License");
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
*
|
||||
* https://www.apache.org/licenses/LICENSE-2.0
|
||||
*
|
||||
* Unless required by applicable law or agreed to in writing, software
|
||||
* distributed under the License is distributed on an "AS IS" BASIS,
|
||||
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
* See the License for the specific language governing permissions and
|
||||
* limitations under the License.
|
||||
*/
|
||||
|
||||
package org.springframework.cloud.openfeign.test;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* A few assertions to sanity-check equals and hashCode contracts:
|
||||
* https://docs.oracle.com/javase/8/docs/api/java/lang/Object.html See
|
||||
* {@link Object#equals(Object)} and {@link Object#hashCode()}.
|
||||
*
|
||||
* @author Jonatan Ivanov
|
||||
*/
|
||||
public class EqualsAndHashCodeAssert {
|
||||
|
||||
/**
|
||||
* Checks if equals is reflexive: for any non-null reference value x, x.equals(x)
|
||||
* should return true.
|
||||
* @param object the reference object to check
|
||||
*/
|
||||
public static void assertEqualsReflexivity(Object object) {
|
||||
assertThat(object.equals(object)).isTrue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if equals is symmetric: for any non-null reference values x and y,
|
||||
* x.equals(y) should return true if and only if y.equals(x) returns true The user of
|
||||
* this method should call this at least twice: once with objects that are equal and
|
||||
* once with objects that are not.
|
||||
* @param objectOne a reference object to check
|
||||
* @param objectTwo another reference object to check
|
||||
*/
|
||||
public static void assertEqualsSymmetricity(Object objectOne, Object objectTwo) {
|
||||
assertThat(objectOne.equals(objectTwo)).isEqualTo(objectTwo.equals(objectOne));
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if equals is transitive: for any non-null reference values x, y, and z, if
|
||||
* x.equals(y) returns true and y.equals(z) returns true, then x.equals(z) should
|
||||
* return true.
|
||||
* @param objectOne a reference object to check
|
||||
* @param objectTwo another reference object to check
|
||||
* @param objectThree and the third reference object to check
|
||||
*/
|
||||
public static void assertEqualsTransitivity(Object objectOne, Object objectTwo, Object objectThree) {
|
||||
assertThat(objectOne.equals(objectTwo)).isTrue();
|
||||
assertThat(objectTwo.equals(objectThree)).isTrue();
|
||||
assertThat(objectOne.equals(objectThree)).isTrue();
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to check if equals is consistent: for any non-null reference values x and y,
|
||||
* multiple invocations of x.equals(y) consistently return true or consistently return
|
||||
* false. The user of this method should call this at least twice: once with objects
|
||||
* that are equal and once with objects that are not.
|
||||
* @param objectOne a reference object to check
|
||||
* @param objectTwo another reference object to check
|
||||
*/
|
||||
public static void assertEqualsConsistency(Object objectOne, Object objectTwo) {
|
||||
boolean equality = objectOne.equals(objectTwo);
|
||||
for (int i = 0; i < 100; i++) {
|
||||
assertThat(objectOne.equals(objectTwo)).isEqualTo(equality);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Tries to check if hashCode is consistent: whenever it is invoked on the same object
|
||||
* more than once during an execution of a Java application, the hashCode method must
|
||||
* consistently return the same integer.
|
||||
* @param object the reference object to check
|
||||
*/
|
||||
public static void assertHashCodeConsistency(Object object) {
|
||||
int hashCode = object.hashCode();
|
||||
for (int i = 0; i < 100; i++) {
|
||||
assertThat(object.hashCode()).isEqualTo(hashCode);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if equals and hashCode are consistent to each other: if two objects are
|
||||
* equal according to the equals method, then calling the hashCode method on each of
|
||||
* the two objects must produce the same integer result.
|
||||
* @param objectOne a reference object to check
|
||||
* @param objectTwo another reference object to check
|
||||
*/
|
||||
public static void assertEqualsAndHashCodeConsistency(Object objectOne, Object objectTwo) {
|
||||
assertThat(objectOne.equals(objectTwo)).isTrue();
|
||||
assertThat(objectOne.hashCode()).isEqualTo(objectTwo.hashCode());
|
||||
}
|
||||
|
||||
}
|
||||
+2
-3
@@ -17,8 +17,7 @@
|
||||
package org.springframework.cloud.openfeign.testclients;
|
||||
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
/**
|
||||
* @author Ryan Baxter
|
||||
@@ -26,7 +25,7 @@ import org.springframework.web.bind.annotation.RequestMethod;
|
||||
@FeignClient(name = "localapp")
|
||||
public interface TestClient {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
@GetMapping("/hello")
|
||||
String getHello();
|
||||
|
||||
}
|
||||
|
||||
+3
-4
@@ -37,8 +37,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Primary;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -83,7 +82,7 @@ public class FeignClientNotPrimaryTests {
|
||||
@FeignClient(name = "localapp", primary = false)
|
||||
protected interface TestClient {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/hello")
|
||||
@GetMapping("/hello")
|
||||
Hello getHello();
|
||||
|
||||
}
|
||||
@@ -101,7 +100,7 @@ public class FeignClientNotPrimaryTests {
|
||||
return new PrimaryTestClient();
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/hello")
|
||||
@GetMapping("/hello")
|
||||
public Hello getHello() {
|
||||
return new Hello(HELLO_WORLD_1);
|
||||
}
|
||||
|
||||
+4
-5
@@ -27,8 +27,7 @@ import org.springframework.cloud.openfeign.loadbalancer.FeignLoadBalancerAutoCon
|
||||
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
@@ -70,7 +69,7 @@ public class FeignClientValidationTests {
|
||||
@FeignClient(name = "example", url = "https://example.com")
|
||||
interface Client {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/")
|
||||
@GetMapping("/")
|
||||
@Deprecated
|
||||
String get();
|
||||
|
||||
@@ -86,7 +85,7 @@ public class FeignClientValidationTests {
|
||||
@FeignClient(name = "example", url = "${feignClient.url:https://example.com}")
|
||||
interface Client {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/")
|
||||
@GetMapping("/")
|
||||
@Deprecated
|
||||
String get();
|
||||
|
||||
@@ -102,7 +101,7 @@ public class FeignClientValidationTests {
|
||||
@FeignClient("foo")
|
||||
interface Client {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/")
|
||||
@GetMapping("/")
|
||||
@Deprecated
|
||||
String get();
|
||||
|
||||
|
||||
+7
-7
@@ -41,11 +41,11 @@ import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -107,17 +107,17 @@ class FeignHttpClientTests {
|
||||
|
||||
protected interface BaseTestClient {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@GetMapping(value = "/hello", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
Hello getHello();
|
||||
|
||||
@RequestMapping(method = RequestMethod.PATCH, value = "/hellop", consumes = "application/json")
|
||||
@PatchMapping(value = "/hellop", consumes = "application/json")
|
||||
ResponseEntity<Void> patchHello(Hello hello);
|
||||
|
||||
}
|
||||
|
||||
protected interface UserService {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/users/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
@GetMapping(value = "/users/{id}", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
User getUser(@PathVariable("id") long id);
|
||||
|
||||
}
|
||||
@@ -136,12 +136,12 @@ class FeignHttpClientTests {
|
||||
@Import(NoSecurityConfiguration.class)
|
||||
protected static class Application implements UserService {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
@GetMapping("/hello")
|
||||
public Hello getHello() {
|
||||
return new Hello("hello world 1");
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.PATCH, value = "/hellop")
|
||||
@PatchMapping("/hellop")
|
||||
public ResponseEntity<Void> patchHello(@RequestBody Hello hello,
|
||||
@RequestHeader("Content-Length") int contentLength) {
|
||||
if (contentLength <= 0) {
|
||||
|
||||
+7
-7
@@ -40,11 +40,11 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PatchMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -107,17 +107,17 @@ class FeignOkHttpTests {
|
||||
|
||||
protected interface BaseTestClient {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
@GetMapping("/hello")
|
||||
Hello getHello();
|
||||
|
||||
@RequestMapping(method = RequestMethod.PATCH, value = "/hellop", consumes = "application/json")
|
||||
@PatchMapping(value = "/hellop", consumes = "application/json")
|
||||
ResponseEntity<Void> patchHello(Hello hello);
|
||||
|
||||
}
|
||||
|
||||
protected interface UserService {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/users/{id}")
|
||||
@GetMapping("/users/{id}")
|
||||
User getUser(@PathVariable("id") long id);
|
||||
|
||||
}
|
||||
@@ -138,12 +138,12 @@ class FeignOkHttpTests {
|
||||
@Import(NoSecurityConfiguration.class)
|
||||
protected static class Application implements UserService {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
@GetMapping("/hello")
|
||||
public Hello getHello() {
|
||||
return new Hello("hello world 1");
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.PATCH, value = "/hellop")
|
||||
@PatchMapping("/hellop")
|
||||
public ResponseEntity<Void> patchHello(@RequestBody Hello hello,
|
||||
@RequestHeader("Content-Length") int contentLength) {
|
||||
if (contentLength <= 0) {
|
||||
|
||||
+82
-97
@@ -38,7 +38,6 @@ import javax.servlet.http.Part;
|
||||
import feign.Client;
|
||||
import feign.Logger;
|
||||
import feign.RequestInterceptor;
|
||||
import feign.RequestTemplate;
|
||||
import feign.codec.EncodeException;
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
@@ -65,7 +64,6 @@ import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.format.Formatter;
|
||||
import org.springframework.format.FormatterRegistry;
|
||||
import org.springframework.format.annotation.DateTimeFormat;
|
||||
import org.springframework.http.HttpEntity;
|
||||
import org.springframework.http.HttpStatus;
|
||||
@@ -73,6 +71,8 @@ import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.mock.web.MockMultipartFile;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestHeader;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
@@ -84,6 +84,9 @@ import org.springframework.web.multipart.MultipartFile;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
|
||||
import static org.springframework.http.MediaType.APPLICATION_FORM_URLENCODED_VALUE;
|
||||
import static org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE;
|
||||
import static org.springframework.http.MediaType.TEXT_PLAIN_VALUE;
|
||||
|
||||
/**
|
||||
* @author Spencer Gibb
|
||||
@@ -277,6 +280,13 @@ class ValidFeignClientTests {
|
||||
assertThat(testClient.getToString(args)).isEqualTo(expectedResult);
|
||||
}
|
||||
|
||||
@Test
|
||||
void testFormURLEncoded() {
|
||||
Hello hello = new Hello(HELLO_WORLD_1);
|
||||
Hello response = testClient.postFormUrlEncoded(hello);
|
||||
assertThat(response).isEqualTo(hello);
|
||||
}
|
||||
|
||||
@Test
|
||||
void namedFeignClientWorks() {
|
||||
assertThat(namedFeignClient).as("namedFeignClient was null").isNotNull();
|
||||
@@ -382,52 +392,42 @@ class ValidFeignClientTests {
|
||||
@FeignClient(name = "localapp8")
|
||||
protected interface MultipartClient {
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST, path = "/singlePart",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
@PostMapping(path = "/singlePart", consumes = MULTIPART_FORM_DATA_VALUE, produces = TEXT_PLAIN_VALUE)
|
||||
String singlePart(@RequestPart("hello") String hello);
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST, path = "/singlePojoPart",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
@PostMapping(path = "/singlePojoPart", consumes = MULTIPART_FORM_DATA_VALUE, produces = TEXT_PLAIN_VALUE)
|
||||
String singlePojoPart(@RequestPart("hello") Hello hello);
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST, path = "/multipart",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
@PostMapping(path = "/multipart", consumes = MULTIPART_FORM_DATA_VALUE, produces = TEXT_PLAIN_VALUE)
|
||||
String multipart(@RequestPart("hello") String hello, @RequestPart("world") String world,
|
||||
@RequestPart("file") MultipartFile file);
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST, path = "/multipartPojo",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
@PostMapping(path = "/multipartPojo", consumes = MULTIPART_FORM_DATA_VALUE, produces = TEXT_PLAIN_VALUE)
|
||||
String multipartPojo(@RequestPart("hello") String hello, @RequestPart("world") String world,
|
||||
@RequestPart("pojo1") Hello pojo1, @RequestPart("pojo2") Hello pojo2,
|
||||
@RequestPart("file") MultipartFile file);
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST, path = "/multipartNames",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
@PostMapping(path = "/multipartNames", consumes = MULTIPART_FORM_DATA_VALUE, produces = TEXT_PLAIN_VALUE)
|
||||
String requestPartListOfMultipartFilesReturnsPartNames(@RequestPart("files") List<MultipartFile> files);
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST, path = "/multipartFilenames",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
@PostMapping(path = "/multipartFilenames", consumes = MULTIPART_FORM_DATA_VALUE, produces = TEXT_PLAIN_VALUE)
|
||||
String requestPartListOfMultipartFilesReturnsFileNames(@RequestPart("files") List<MultipartFile> files);
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST, path = "/multipartPojosFiles",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
@PostMapping(path = "/multipartPojosFiles", consumes = MULTIPART_FORM_DATA_VALUE, produces = TEXT_PLAIN_VALUE)
|
||||
String requestPartListOfPojosAndListOfMultipartFiles(@RequestPart("pojos") List<Hello> pojos,
|
||||
@RequestPart("files") List<MultipartFile> files);
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST, path = "/multipartNames",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
@PostMapping(path = "/multipartNames", consumes = MULTIPART_FORM_DATA_VALUE, produces = TEXT_PLAIN_VALUE)
|
||||
String requestBodyListOfMultipartFiles(@RequestBody List<MultipartFile> files);
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST, path = "/multipartNames",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
@PostMapping(path = "/multipartNames", consumes = MULTIPART_FORM_DATA_VALUE, produces = TEXT_PLAIN_VALUE)
|
||||
String requestBodySingleMultipartFile(@RequestBody MultipartFile file);
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST, path = "/multipartNames",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
@PostMapping(path = "/multipartNames", consumes = MULTIPART_FORM_DATA_VALUE, produces = TEXT_PLAIN_VALUE)
|
||||
String requestBodyMap(@RequestBody Map<String, ?> form);
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST, path = "/invalid",
|
||||
consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
@PostMapping(path = "/invalid", consumes = MediaType.APPLICATION_OCTET_STREAM_VALUE,
|
||||
produces = TEXT_PLAIN_VALUE)
|
||||
String invalid(@RequestBody MultipartFile file);
|
||||
|
||||
}
|
||||
@@ -435,36 +435,36 @@ class ValidFeignClientTests {
|
||||
@FeignClient(name = "localapp", configuration = TestClientConfig.class)
|
||||
protected interface TestClient {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/hello")
|
||||
@GetMapping("/hello")
|
||||
Hello getHello();
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/hello")
|
||||
@GetMapping("/hello")
|
||||
Optional<Hello> getOptionalHello();
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "${feignClient.methodLevelRequestMappingPath}")
|
||||
@GetMapping("${feignClient.methodLevelRequestMappingPath}")
|
||||
Hello getHelloUsingPropertyPlaceHolder();
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/hellos")
|
||||
@GetMapping("/hellos")
|
||||
List<Hello> getHellos();
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/hellostrings")
|
||||
@GetMapping("/hellostrings")
|
||||
List<String> getHelloStrings();
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/helloheaders")
|
||||
@GetMapping("/helloheaders")
|
||||
List<String> getHelloHeaders();
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/helloheadersplaceholders",
|
||||
@GetMapping(path = "/helloheadersplaceholders",
|
||||
headers = "myPlaceholderHeader=${feignClient.myPlaceholderHeader}")
|
||||
String getHelloHeadersPlaceholders();
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/helloparams")
|
||||
@GetMapping("/helloparams")
|
||||
List<String> getParams(@RequestParam("params") List<String> params);
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/formattedparams")
|
||||
@GetMapping("/formattedparams")
|
||||
List<LocalDate> getFormattedParams(
|
||||
@RequestParam("params") @DateTimeFormat(pattern = "dd-MM-yyyy") List<LocalDate> params);
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/noContent")
|
||||
@GetMapping("/noContent")
|
||||
ResponseEntity<Void> noContent();
|
||||
|
||||
@RequestMapping(method = RequestMethod.HEAD, path = "/head")
|
||||
@@ -477,21 +477,24 @@ class ValidFeignClientTests {
|
||||
produces = "application/vnd.io.spring.cloud.test.v1+json", path = "/complex")
|
||||
String moreComplexContentType(String body);
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/tostring")
|
||||
@GetMapping("/tostring")
|
||||
String getToString(@RequestParam("arg") Arg arg);
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/tostring2")
|
||||
@GetMapping("/tostring2")
|
||||
String getToString(@RequestParam("arg") OtherArg arg);
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/tostringcollection")
|
||||
@GetMapping("/tostringcollection")
|
||||
Collection<String> getToString(@RequestParam("arg") Collection<OtherArg> args);
|
||||
|
||||
@PostMapping(path = "/form-urlencoded", consumes = APPLICATION_FORM_URLENCODED_VALUE)
|
||||
Hello postFormUrlEncoded(Hello hello);
|
||||
|
||||
}
|
||||
|
||||
@FeignClient(name = "localapp1")
|
||||
protected interface TestClientServiceId {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/hello")
|
||||
@GetMapping("/hello")
|
||||
Hello getHello();
|
||||
|
||||
}
|
||||
@@ -499,10 +502,10 @@ class ValidFeignClientTests {
|
||||
@FeignClient(name = "localapp2", decode404 = true)
|
||||
protected interface DecodingTestClient {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/notFound")
|
||||
@GetMapping("/notFound")
|
||||
ResponseEntity<String> notFound();
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/notFound")
|
||||
@GetMapping("/notFound")
|
||||
Optional<String> optional();
|
||||
|
||||
}
|
||||
@@ -526,22 +529,12 @@ class ValidFeignClientTests {
|
||||
|
||||
@Bean
|
||||
public RequestInterceptor interceptor1() {
|
||||
return new RequestInterceptor() {
|
||||
@Override
|
||||
public void apply(RequestTemplate template) {
|
||||
template.header(MYHEADER1, "myheader1value");
|
||||
}
|
||||
};
|
||||
return template -> template.header(MYHEADER1, "myheader1value");
|
||||
}
|
||||
|
||||
@Bean
|
||||
public RequestInterceptor interceptor2() {
|
||||
return new RequestInterceptor() {
|
||||
@Override
|
||||
public void apply(RequestTemplate template) {
|
||||
template.header(MYHEADER2, "myheader2value");
|
||||
}
|
||||
};
|
||||
return template -> template.header(MYHEADER2, "myheader2value");
|
||||
}
|
||||
|
||||
}
|
||||
@@ -570,27 +563,21 @@ class ValidFeignClientTests {
|
||||
|
||||
@Bean
|
||||
FeignFormatterRegistrar feignFormatterRegistrar() {
|
||||
return new FeignFormatterRegistrar() {
|
||||
return registry -> registry.addFormatter(new Formatter<OtherArg>() {
|
||||
|
||||
@Override
|
||||
public void registerFormatters(FormatterRegistry registry) {
|
||||
registry.addFormatter(new Formatter<OtherArg>() {
|
||||
|
||||
@Override
|
||||
public String print(OtherArg object, Locale locale) {
|
||||
if ("foo".equals(object.value)) {
|
||||
return "bar";
|
||||
}
|
||||
return object.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public OtherArg parse(String text, Locale locale) throws ParseException {
|
||||
return new OtherArg(text);
|
||||
}
|
||||
});
|
||||
public String print(OtherArg object, Locale locale) {
|
||||
if ("foo".equals(object.value)) {
|
||||
return "bar";
|
||||
}
|
||||
return object.value;
|
||||
}
|
||||
};
|
||||
|
||||
@Override
|
||||
public OtherArg parse(String text, Locale locale) throws ParseException {
|
||||
return new OtherArg(text);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
@Bean
|
||||
@@ -598,23 +585,23 @@ class ValidFeignClientTests {
|
||||
return new JsonFormWriter();
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/hello")
|
||||
@GetMapping("/hello")
|
||||
public Hello getHello() {
|
||||
return new Hello(HELLO_WORLD_1);
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/hello2")
|
||||
@GetMapping("/hello2")
|
||||
public Hello getHello2() {
|
||||
return new Hello(OI_TERRA_2);
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/hellos")
|
||||
@GetMapping("/hellos")
|
||||
public List<Hello> getHellos() {
|
||||
ArrayList<Hello> hellos = getHelloList();
|
||||
return hellos;
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/hellostrings")
|
||||
@GetMapping("/hellostrings")
|
||||
public List<String> getHelloStrings() {
|
||||
ArrayList<String> hellos = new ArrayList<>();
|
||||
hellos.add(HELLO_WORLD_1);
|
||||
@@ -622,7 +609,7 @@ class ValidFeignClientTests {
|
||||
return hellos;
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/helloheaders")
|
||||
@GetMapping("/helloheaders")
|
||||
public List<String> getHelloHeaders(@RequestHeader(MYHEADER1) String myheader1,
|
||||
@RequestHeader(MYHEADER2) String myheader2) {
|
||||
ArrayList<String> headers = new ArrayList<>();
|
||||
@@ -631,23 +618,23 @@ class ValidFeignClientTests {
|
||||
return headers;
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/helloheadersplaceholders")
|
||||
@GetMapping("/helloheadersplaceholders")
|
||||
public String getHelloHeadersPlaceholders(@RequestHeader("myPlaceholderHeader") String myPlaceholderHeader) {
|
||||
return myPlaceholderHeader;
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/helloparams")
|
||||
@GetMapping("/helloparams")
|
||||
public List<String> getParams(@RequestParam("params") List<String> params) {
|
||||
return params;
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/formattedparams")
|
||||
@GetMapping("/formattedparams")
|
||||
public List<LocalDate> getFormattedParams(
|
||||
@RequestParam("params") @DateTimeFormat(pattern = "dd-MM-yyyy") List<LocalDate> params) {
|
||||
return params;
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/noContent")
|
||||
@GetMapping("/noContent")
|
||||
ResponseEntity<Void> noContent() {
|
||||
return ResponseEntity.noContent().build();
|
||||
}
|
||||
@@ -662,7 +649,7 @@ class ValidFeignClientTests {
|
||||
throw new RuntimeException("always fails");
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/notFound")
|
||||
@GetMapping("/notFound")
|
||||
ResponseEntity<String> notFound() {
|
||||
return ResponseEntity.status(HttpStatus.NOT_FOUND).body((String) null);
|
||||
}
|
||||
@@ -676,17 +663,17 @@ class ValidFeignClientTests {
|
||||
return body;
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/tostring")
|
||||
@GetMapping("/tostring")
|
||||
String getToString(@RequestParam("arg") Arg arg) {
|
||||
return arg.toString();
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/tostring2")
|
||||
@GetMapping("/tostring2")
|
||||
String getToString(@RequestParam("arg") OtherArg arg) {
|
||||
return arg.value;
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, path = "/tostringcollection")
|
||||
@GetMapping("/tostringcollection")
|
||||
Collection<String> getToString(@RequestParam("arg") Collection<OtherArg> args) {
|
||||
List<String> result = new ArrayList<>();
|
||||
for (OtherArg arg : args) {
|
||||
@@ -695,47 +682,40 @@ class ValidFeignClientTests {
|
||||
return result;
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST, path = "/singlePart",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
@PostMapping(path = "/singlePart", consumes = MULTIPART_FORM_DATA_VALUE, produces = TEXT_PLAIN_VALUE)
|
||||
String singlePart(@RequestPart("hello") String hello) {
|
||||
return hello;
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST, path = "/singlePojoPart",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
@PostMapping(path = "/singlePojoPart", consumes = MULTIPART_FORM_DATA_VALUE, produces = TEXT_PLAIN_VALUE)
|
||||
String singlePojoPart(@RequestPart("hello") Hello hello) {
|
||||
return hello.getMessage();
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST, path = "/multipart",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
@PostMapping(path = "/multipart", consumes = MULTIPART_FORM_DATA_VALUE, produces = TEXT_PLAIN_VALUE)
|
||||
String multipart(@RequestPart("hello") String hello, @RequestPart("world") String world,
|
||||
@RequestPart("file") MultipartFile file) {
|
||||
return hello + world + file.getOriginalFilename();
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST, path = "/multipartPojo",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
@PostMapping(path = "/multipartPojo", consumes = MULTIPART_FORM_DATA_VALUE, produces = TEXT_PLAIN_VALUE)
|
||||
String multipartPojo(@RequestPart("hello") String hello, @RequestPart("world") String world,
|
||||
@RequestPart("pojo1") Hello pojo1, @RequestPart("pojo2") Hello pojo2,
|
||||
@RequestPart("file") MultipartFile file) {
|
||||
return hello + world + pojo1.getMessage() + pojo2.getMessage() + file.getOriginalFilename();
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST, path = "/multipartNames",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
@PostMapping(path = "/multipartNames", consumes = MULTIPART_FORM_DATA_VALUE, produces = TEXT_PLAIN_VALUE)
|
||||
String multipartNames(HttpServletRequest request) throws Exception {
|
||||
return request.getParts().stream().map(Part::getName).collect(Collectors.joining(","));
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST, path = "/multipartFilenames",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
@PostMapping(path = "/multipartFilenames", consumes = MULTIPART_FORM_DATA_VALUE, produces = TEXT_PLAIN_VALUE)
|
||||
String multipartFilenames(HttpServletRequest request) throws Exception {
|
||||
return request.getParts().stream().map(Part::getSubmittedFileName).collect(Collectors.joining(","));
|
||||
}
|
||||
|
||||
@RequestMapping(method = RequestMethod.POST, path = "/multipartPojosFiles",
|
||||
consumes = MediaType.MULTIPART_FORM_DATA_VALUE, produces = MediaType.TEXT_PLAIN_VALUE)
|
||||
@PostMapping(path = "/multipartPojosFiles", consumes = MULTIPART_FORM_DATA_VALUE, produces = TEXT_PLAIN_VALUE)
|
||||
String requestPartListOfPojosAndListOfMultipartFiles(@RequestPart("pojos") List<Hello> pojos,
|
||||
@RequestPart("files") List<MultipartFile> files) {
|
||||
StringBuilder result = new StringBuilder();
|
||||
@@ -751,6 +731,11 @@ class ValidFeignClientTests {
|
||||
return result.toString();
|
||||
}
|
||||
|
||||
@PostMapping(path = "/form-urlencoded", consumes = APPLICATION_FORM_URLENCODED_VALUE)
|
||||
Hello postFormUrlEncoded(Hello hello) {
|
||||
return hello;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
public static class Hello {
|
||||
|
||||
+2
-3
@@ -35,8 +35,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -70,7 +69,7 @@ public class FeignClientEnvVarTests {
|
||||
@Import(NoSecurityConfiguration.class)
|
||||
protected static class Application {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
@GetMapping("/hello")
|
||||
public String getHello() {
|
||||
return "hello world 1";
|
||||
}
|
||||
|
||||
+4
-5
@@ -37,8 +37,7 @@ import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.context.annotation.Import;
|
||||
import org.springframework.test.annotation.DirtiesContext;
|
||||
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
@@ -83,7 +82,7 @@ public class FeignClientScanningTests {
|
||||
@FeignClient("localapp123")
|
||||
protected interface TestClient {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
@GetMapping("/hello")
|
||||
String getHello();
|
||||
|
||||
}
|
||||
@@ -91,7 +90,7 @@ public class FeignClientScanningTests {
|
||||
@FeignClient("${feignClient.localappName}")
|
||||
protected interface TestClientByKey {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
@GetMapping("/hello")
|
||||
String getHello();
|
||||
|
||||
}
|
||||
@@ -104,7 +103,7 @@ public class FeignClientScanningTests {
|
||||
@Import(NoSecurityConfiguration.class)
|
||||
protected static class Application {
|
||||
|
||||
@RequestMapping(method = RequestMethod.GET, value = "/hello")
|
||||
@GetMapping("/hello")
|
||||
public String getHello() {
|
||||
return "hello world 1";
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user