Compare commits

..
128 Commits
Author SHA1 Message Date
buildmaster ae97365b6c Update SNAPSHOT to 3.1.0-M4 2021-10-21 11:34:56 +00:00
Olga MaciaszekSharma fd6c1290fe Merge remote-tracking branch 'origin/3.0.x' 2021-10-21 13:23:05 +02:00
Olga MaciaszekSharma 0e6b33f480 Merge remote-tracking branch 'origin/2.2.x' into 3.0.x 2021-10-21 13:04:57 +02:00
Olga MaciaszekSharma 17d0161faf Reformat. 2021-10-21 12:40:20 +02:00
Olga MaciaszekSharma d6783a6f1e Block clas-level request mapping on Feign clients. 2021-10-21 12:14:19 +02:00
buildmaster 7f03521b0d Bumping versions 2021-10-21 00:32:35 +00:00
Sam Kruglov 21adacbc43 allow @CollectionFormat on class level (#596) (#610) 2021-10-20 15:39:52 +02:00
buildmaster 3de4b98b3d Going back to snapshots 2021-10-20 02:18:15 +00:00
buildmaster a5e66077d6 Update SNAPSHOT to 3.1.0-M3 2021-10-20 02:16:47 +00:00
Olga MaciaszekSharma 02c0a1e0f6 Merge remote-tracking branch 'origin/3.0.x' 2021-10-11 18:47:07 +02:00
Olga MaciaszekSharma 98229d5ece Merge remote-tracking branch 'origin/3.0.x' 2021-10-11 18:45:46 +02:00
Olga MaciaszekSharma 548e4d55ef Revert default circuit breaker name prefix to target.type(). Fixes gh-606. 2021-10-11 18:40:21 +02:00
Olga MaciaszekSharma 9c0dda896f Use new Spring Hateoas API. 2021-10-11 13:24:59 +02:00
buildmaster c6ea224501 Going back to snapshots 2021-10-01 17:07:56 +00:00
buildmaster f5be046f90 Update SNAPSHOT to 3.1.0-M2 2021-10-01 17:06:36 +00:00
buildmaster 9018acfc40 Bumping versions 2021-09-30 00:23:25 +00:00
GongYi 6533389e16 Add a CookieValueParameterProcessor to support @CookieValue (#604) 2021-09-29 14:01:36 +02:00
buildmaster 1ff68c11d2 Bumping versions 2021-09-28 00:22:33 +00:00
buildmaster 55ceafae4d Bumping versions to 3.0.5-SNAPSHOT after release 2021-09-21 14:39:31 +00:00
buildmaster 0c87527ea5 Going back to snapshots 2021-09-21 14:39:31 +00:00
buildmaster a2c473db2a Update SNAPSHOT to 3.0.4 2021-09-21 14:37:38 +00:00
Can Bezmen ae33802a4b FIX gh-601 2021-09-20 15:35:28 +02:00
Olga MaciaszekSharma 8b9254a008 Merge remote-tracking branch 'origin/3.0.x' 2021-09-09 17:59:25 +02:00
Olga MaciaszekSharma c761919bc9 Only add default query params and default headers if not already in template. Fixes gh-590. 2021-09-09 17:59:08 +02:00
Olga MaciaszekSharma 95a93dcc5c Merge remote-tracking branch 'origin/3.0.x' 2021-09-08 18:52:44 +02:00
Olga MaciaszekSharma 998baed0d3 Merge remote-tracking branch 'origin/3.0.x' into 3.0.x 2021-09-08 18:52:18 +02:00
Olga MaciaszekSharma 4fc80e6b01 Update docs. Fixes gh-585. 2021-09-08 18:52:11 +02:00
buildmaster 8dd2c82968 Going back to snapshots 2021-07-30 18:17:15 +00:00
buildmaster f4615d198e Update SNAPSHOT to 3.1.0-M1 2021-07-30 18:15:57 +00:00
spencergibb fae18f2039 migrates from symlink to include 2021-07-30 14:13:10 -04:00
Yanming Zhou ff30485b40 Respect SpringDataWebProperties where possible (#583) 2021-07-28 11:36:54 +02:00
Yanming Zhou a3704f0614 Support regex expression for @PathVariable (#577)
Fix gh-576
2021-07-27 11:54:47 +02:00
Olga MaciaszekSharma 3322f95fe4 Merge remote-tracking branch 'origin/3.0.x' 2021-07-27 11:00:05 +02:00
Kwangyong Kim c101fd2c6e Add CircuitBreakerNameResolver (#575) 2021-07-27 10:59:32 +02:00
Olga MaciaszekSharma 626d0a739b Bump Feign to 11.6. Fixes gh-584. 2021-07-27 10:52:23 +02:00
Olga MaciaszekSharma 8f403f85f2 Merge remote-tracking branch 'origin/3.0.x' 2021-07-26 17:03:58 +02:00
Olga MaciaszekSharma 71930fd9f0 Do not reset RequestAttributes. Fixes gh-572. 2021-07-26 17:03:33 +02:00
Olga MaciaszekSharma 7581b2c27f Fix github workflows setup. 2021-07-26 16:05:39 +02:00
Olga MaciaszekSharma a3ea61a586 Merge remote-tracking branch 'origin/3.0.x' 2021-07-26 16:04:14 +02:00
Olga MaciaszekSharma 1b063edf7e Upgrade Feign to 11.4. Fixes gh-581. 2021-07-26 15:37:05 +02:00
Olga MaciaszekSharma b5283e2f88 Fix Github workflow setup. 2021-07-21 13:21:08 +02:00
Olga MaciaszekSharma f3f9525081 Fix branch name. 2021-07-21 13:20:24 +02:00
Olga MaciaszekSharma a08fd801c2 Remove unnecessary property info. 2021-07-21 13:20:08 +02:00
Olga MaciaszekSharma 183e6ff30a Fix Spring Cloud Build and Spring Cloud Commons versions. 2021-07-21 12:52:28 +02:00
Olga MaciaszekSharma fe91a99984 Fix Spring Cloud Build and Spring Cloud Commons versions. 2021-07-21 12:47:18 +02:00
Olga MaciaszekSharma c2539ac6a5 Merge remote-tracking branch 'origin/3.0.x' 2021-07-21 12:23:35 +02:00
ManasviGoyal 6aeb1ff676 Added Tip (#580) 2021-07-21 12:21:21 +02:00
spencergibb 948aa13cc1 Merge branch '3.1.x' 2021-07-09 14:48:33 -04:00
spencergibb 44c63798b0 rollback to 3.0.x versions 2021-07-09 14:47:28 -04:00
Jonathan SchneiderandModerne 769b38c5c1 Refactor: replace @RequestMapping annotations
Co-authored-by: Moderne <team@moderne.io>
2021-07-09 12:16:27 +02:00
Olga MaciaszekSharma d8cbd16b76 Merge remote-tracking branch 'origin/main' into 3.1.x 2021-07-09 12:03:00 +02:00
Can Bezmen 3c402201ec Codecov for main branch (#554) 2021-07-09 11:51:57 +02:00
buildmaster deb150ed88 Bumping versions 2021-07-09 00:24:22 +00:00
Olga MaciaszekSharma 1275875ddb Reformat. 2021-07-07 18:58:48 +02:00
Olga MaciaszekSharma c4b9492a9b Bumping versions to 2.2.10.BUILD-SNAPSHOT after release
This reverts commit f7e490c466.
2021-07-07 18:46:53 +02:00
Olga MaciaszekSharma 3acf5f9e92 Going back to snapshots
This reverts commit 6479db0a54.
2021-07-07 18:46:20 +02:00
buildmaster e9e38d0274 Bumping versions 2021-07-07 14:07:20 +00:00
Olga MaciaszekSharma 28b50401c5 Revert "Update SNAPSHOT to 2.2.9.RELEASE"
This reverts commit 1005749eb5.
2021-07-07 14:38:12 +02:00
Olga MaciaszekSharma 6479db0a54 Revert "Going back to snapshots"
This reverts commit 190f5a9083.
2021-07-07 14:37:39 +02:00
Olga MaciaszekSharma f7e490c466 Revert "Bumping versions to 2.2.10.BUILD-SNAPSHOT after release"
This reverts commit d60cda824d.
2021-07-07 14:37:03 +02:00
buildmaster d60cda824d Bumping versions to 2.2.10.BUILD-SNAPSHOT after release 2021-07-06 14:53:53 +00:00
buildmaster 190f5a9083 Going back to snapshots 2021-07-06 14:53:53 +00:00
buildmaster 1005749eb5 Update SNAPSHOT to 2.2.9.RELEASE 2021-07-06 14:52:01 +00:00
Olga MaciaszekSharma 6de9fdd3e1 Merge remote-tracking branch 'origin/2.2.x' 2021-07-05 13:19:39 +02:00
Michal Domagala df1f982046 Fix for RetryableFeignBlockingLoadBalancerClient closes stream (#569) 2021-07-05 13:03:06 +02:00
Michal Domagala baf7cf95f2 Simplify Log.info logic (#556) 2021-07-05 12:43:59 +02:00
Olga MaciaszekSharma 45861dd657 Merge remote-tracking branch 'origin/2.2.x'. Refactor. 2021-07-05 12:09:23 +02:00
Olga MaciaszekSharma 00ea88c67f Fix typo. Refactor. 2021-07-05 11:55:41 +02:00
Olga MaciaszekSharma d897b18e16 Add missing author tags. Refactor. 2021-07-05 11:26:06 +02:00
Can Bezmen 8c08204c78 Issue #549
* Fixed: Form URL Encoded bug fixed.

Fixes gh-549.
2021-07-05 11:19:29 +02:00
Olga MaciaszekSharma 1f5bdd4224 Merge remote-tracking branch 'origin/2.2.x' 2021-06-10 16:08:39 +02:00
Olga MaciaszekSharma 2f43715f6f Change log level. Fixes gh-552. 2021-06-10 16:04:17 +02:00
buildmaster 280291ec93 Bumping versions 2021-05-28 00:22:29 +00:00
buildmaster 0de9dd614d Going back to snapshots 2021-05-27 21:20:38 +00:00
buildmaster 93fcf38c24 Update SNAPSHOT to 3.0.3 2021-05-27 21:18:46 +00:00
buildmaster be929043c3 Bumping versions to 3.0.4-SNAPSHOT after release 2021-05-27 08:11:04 +00:00
buildmaster c7316b008a Going back to snapshots 2021-05-27 08:11:04 +00:00
buildmaster 989616ebe6 Update SNAPSHOT to 3.0.3 2021-05-27 08:08:49 +00:00
Olga MaciaszekSharma 58b0c7866c Merge remote-tracking branch 'origin/2.2.x' into main 2021-05-24 16:02:34 +02:00
Nick Smith 421df91a80 Filters out null matrix parameters 2021-05-24 15:58:27 +02:00
Olga Maciaszek-Sharma 1187c3e5b8 Use WebConverters for Hateoas. (#543) 2021-05-14 17:15:31 +02:00
Olga MaciaszekSharma ebe30c8d3c Upgrade SC Build to 3.1.0-SNAPSHOT. 2021-05-13 14:54:10 +02:00
Olga MaciaszekSharma 4ffc61e186 Merge remote-tracking branch 'origin/main' into 3.1.x 2021-05-12 13:57:21 +02:00
Olga MaciaszekSharma 5aa1b7a529 Merge remote-tracking branch 'origin/2.2.x' into main 2021-05-12 13:56:49 +02:00
Olga MaciaszekSharma ec1f2ad5bb Refactor FeignHalConfiguration to use HalMediaTypeConfiguration under the hood. Fixes gh-541. 2021-05-12 13:51:43 +02:00
Olga Maciaszek-Sharma 5f9a827a40 Upgrade feign to 11 (#540) 2021-05-11 15:45:00 +02:00
Olga MaciaszekSharma 80a7102b14 Fix gh actions setup for branch. 2021-05-11 15:10:09 +02:00
Olga MaciaszekSharma 26a0319095 Merge remote-tracking branch 'origin/main' into 3.1.x 2021-05-10 12:31:06 +02:00
Olga MaciaszekSharma fbcebb372c Switch to a different HalInstantiator constructor for Boot 2.5 compatibility. Fixes gh-536. 2021-04-27 16:22:03 +02:00
Olga MaciaszekSharma 7b6a0c2838 Merge remote-tracking branch 'origin/main' into 3.1.x 2021-04-26 19:28:49 +02:00
Olga MaciaszekSharma 1a00bbf705 Merge 2.2.x 2021-04-26 19:28:17 +02:00
Olga MaciaszekSharma 5baa8c8ed5 Remove master references. 2021-04-26 19:24:36 +02:00
Olga MaciaszekSharma d0e035b8ff Switch from master to main. 2021-04-26 17:48:29 +02:00
Olga MaciaszekSharma 1615a5a68c Switch GH actions to main. 2021-04-26 17:36:44 +02:00
Olga MaciaszekSharma 05365b79f9 Switch version to 3.1.0-SNAPSHOT. 2021-04-26 15:47:19 +02:00
Olga MaciaszekSharma 0c1ab36fe1 Merge remote-tracking branch 'origin/2.2.x' 2021-04-22 18:14:37 +02:00
buildmaster 0684ee4bc2 Bumping versions to 2.2.9.BUILD-SNAPSHOT after release 2021-04-21 17:11:52 +00:00
buildmaster 0b34d89adc Going back to snapshots 2021-04-21 17:11:52 +00:00
buildmaster c33bc490cd Update SNAPSHOT to 2.2.8.RELEASE 2021-04-21 17:10:24 +00:00
Olga MaciaszekSharma d738b3b224 Upgrade Feign to 10.12. Fixes gh-530. 2021-04-21 10:23:38 +02:00
buildmaster 0c1d281970 Bumping versions 2021-04-20 21:33:18 +00:00
Olga MaciaszekSharma 26fb4deb18 Merge remote-tracking branch 'origin/2.2.x' 2021-04-20 17:28:31 +02:00
Olga MaciaszekSharma 0bf0426f60 Update GH actions setup. 2021-04-20 16:50:36 +02:00
Leandro Quiroga 9f9ce39789 Fix typo. 2021-04-20 16:50:18 +02:00
buildmaster 4454c820cf Bumping versions 2021-04-20 11:36:51 +00:00
Andrii BohutskyiandAndrii Bohutskyi b8e966fe14 Support circuit breaker group name (#520)
Fixes gh-514

Co-authored-by: Andrii Bohutskyi <abohutskyi@playtika.com>
2021-04-20 11:41:51 +02:00
Jasbir e47255e79d Make feign.Request.Options as refreshable beans inside Feign clients (#526) 2021-04-19 18:27:28 +02:00
Olga MaciaszekSharma 96268d16da Fix checkstyle and formatting. 2021-04-13 15:21:46 +02:00
Olga MaciaszekSharma ac019b4af1 Merge 2.2.x 2021-04-13 15:20:29 +02:00
Olga MaciaszekSharma 4d0caec7ee Fix checkstyle and formatting. 2021-04-13 15:17:00 +02:00
Olga MaciaszekSharma b8aea256da Merge remote-tracking branch 'origin/2.2.x'
# Please enter a commit message to explain why this merge is necessary,
# especially if it merges an updated upstream into a topic branch.
#
# Lines starting with '#' will be ignored, and an empty message aborts
# the commit.
2021-04-13 15:11:13 +02:00
Olga MaciaszekSharma f653a6730d Merge remote-tracking branch 'origin/2.2.x' 2021-04-13 15:08:50 +02:00
Olga MaciaszekSharma 92f7ba9901 Fix GH actions setup 2021-04-13 15:08:27 +02:00
Olga MaciaszekSharma b2c208ba5f Fix GH actions setup 2021-04-13 15:08:00 +02:00
Olga MaciaszekSharma e2a54c1359 Fix GH actions setup 2021-04-13 15:04:10 +02:00
Olga MaciaszekSharma cdfd3695c2 Add GH actions build icon. 2021-04-13 15:03:23 +02:00
Olga MaciaszekSharma a5412cc872 Merge remote-tracking branch 'origin/2.2.x' 2021-04-13 14:54:18 +02:00
Olga MaciaszekSharma 59216f2f82 Switch to building the project with GH actions. 2021-04-13 14:53:48 +02:00
Olga MaciaszekSharma aa01ccb5b2 Merge remote-tracking branch 'origin/2.2.x'
# Conflicts:
#	spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientBuilder.java
#	spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientFactoryBean.java
#	spring-cloud-openfeign-core/src/test/java/org/springframework/cloud/openfeign/FeignClientBuilderTests.java
2021-04-13 13:11:21 +02:00
Sam Kruglov 4d827623f0 Add FeignClientBuilder.Builder#customize(FeignBuilderCustomizer) (#499) (#515) 2021-04-13 12:57:35 +02:00
Olga MaciaszekSharma 86713156f6 Merge remote-tracking branch 'origin/2.2.x'
# Conflicts:
#	spring-cloud-openfeign-core/src/main/java/org/springframework/cloud/openfeign/FeignClientFactoryBean.java
2021-04-12 14:35:43 +02:00
Olga MaciaszekSharma 5e494aa196 Add more logging. Fixes gh-517. 2021-04-12 14:29:45 +02:00
buildmaster 9db146761a Bumping versions 2021-03-30 11:36:40 +00:00
John Niang af418a04ac Expose request attributes for circuit breaker async invocation (#510) 2021-03-30 11:39:07 +02:00
spencergibb bcea218513 Ignore tests that use reflection in jdk 16 2021-03-18 18:52:34 -04:00
buildmaster 1c49f88e2b Bumping versions 2021-03-18 11:36:51 +00:00
buildmaster 3dbaf056ec Bumping versions to 3.0.3-SNAPSHOT after release 2021-03-17 21:46:51 +00:00
buildmaster 5637465ec8 Going back to snapshots 2021-03-17 21:46:50 +00:00
90 changed files with 2145 additions and 844 deletions
-38
View File
@@ -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
+4 -4
View File
@@ -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
+38
View File
@@ -0,0 +1,38 @@
# 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: [ main ]
pull_request:
branches: [ main ]
jobs:
build:
runs-on: ubuntu-latest
strategy:
matrix:
java: ["8", "11", "16"]
steps:
- uses: actions/checkout@v2
- name: Set up JDK 1.8
uses: actions/setup-java@v1
with:
java-version: 1.8
- name: Cache local Maven repository
uses: actions/cache@v2
with:
path: ~/.m2/repository
key: ${{ runner.os }}-maven-${{ hashFiles('**/pom.xml') }}
restore-keys: |
${{ runner.os }}-maven-
- name: Build with Maven
run: ./mvnw clean install -B -U -P sonar
- uses: codecov/codecov-action@v1
with:
fail_ci_if_error: true
+5 -19
View File
@@ -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
@@ -319,4 +305,4 @@ IMPORTANT: Remember to set the `Scan Scope` to `All sources` since we apply chec
== License
The project license file is available https://raw.githubusercontent.com/spring-cloud/spring-cloud-openfeign/master/LICENSE.txt[here].
The project license file is available https://raw.githubusercontent.com/spring-cloud/spring-cloud-openfeign/main/LICENSE.txt[here].
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign</artifactId>
<version>3.0.2</version>
<version>3.1.0-M4</version>
</parent>
<artifactId>spring-cloud-openfeign-docs</artifactId>
<packaging>jar</packaging>
+5 -5
View File
@@ -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,12 +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/master/LICENSE.txt[here].
The project license file is available https://raw.githubusercontent.com/spring-cloud/spring-cloud-openfeign/main/LICENSE.txt[here].
+2 -1
View File
@@ -3,15 +3,16 @@
|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` |
+6 -6
View File
@@ -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
-1
View File
@@ -1 +0,0 @@
spring-cloud-openfeign.adoc
+1
View File
@@ -0,0 +1 @@
include::spring-cloud-openfeign.adoc[]
+1 -1
View File
@@ -27,7 +27,7 @@ public class WebApplication {
## Contributing
We welcome contributions. You can read more on how to contribute to the project https://github.com/spring-cloud/spring-cloud-openfeign/blob/master/README.adoc#3-contributing[here].
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
@@ -52,10 +52,13 @@ public interface StoreClient {
@RequestMapping(method = RequestMethod.POST, value = "/stores/{storeId}", consumes = "application/json")
Store update(@PathVariable("storeId") Long storeId, Store store);
@RequestMapping(method = RequestMethod.DELETE, value = "/stores/{storeId:\\d+}")
void delete(@PathVariable Long storeId);
}
----
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.
@@ -66,10 +69,15 @@ The load-balancer client above will want to discover the physical addresses
for the "stores" service. If your application is a Eureka client then
it will resolve the service in the Eureka service registry. If you
don't want to use Eureka, you can configure a list of servers
in your external configuration using https://cloud.spring.io/spring-cloud-static/spring-cloud-commons/current/reference/html/#simplediscoveryclient[`SimpleDiscoveryClient`].
in your external configuration using https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#simplediscoveryclient[`SimpleDiscoveryClient`].
Spring Cloud OpenFeign supports all the features available for the blocking mode of Spring Cloud LoadBalancer. You can read more about them in the https://docs.spring.io/spring-cloud-commons/docs/current/reference/html/#spring-cloud-loadbalancer[project documentation].
TIP: To use `@EnableFeignClients` annotation on `@Configuration`-annotated-classes, make sure to specify where the clients are located, for example:
`@EnableFeignClients(basePackages = "com.example.clients")`
or list them explicitly:
`@EnableFeignClients(clients = InventoryServiceFeignClient.class)`
[[spring-cloud-feign-overriding-defaults]]
=== Overriding Feign Defaults
@@ -343,12 +351,30 @@ 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]
----
@@ -468,10 +494,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
@@ -495,14 +518,6 @@ feign.compression.request.min-request-size=2048
These properties allow you to be selective about the compressed media types and minimum request threshold length.
For http clients except OkHttpClient, default gzip decoder can be enabled to decode gzip response in UTF-8 encoding:
[source,java]
----
feign.compression.response.enabled=true
feign.compression.response.useGzipDecoder=true
----
=== Feign logging
A logger is created for each Feign client created. By default the name of the logger is the full class name of the interface used to create the Feign client. Feign logging only responds to the `DEBUG` level.
@@ -674,20 +689,21 @@ public interface DemoTemplate {
----
=== Feign `CollectionFormat` support
We support `feign.CollectionFormat` by providing the `@CollectionFormat` annotation.You can annotate a Feign client method with it by passing the desired `feign.CollectionFormat` as annotation value.
We support `feign.CollectionFormat` by providing the `@CollectionFormat` annotation.
You can annotate a Feign client method (or the whole class to affect all methods) 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.
[source,java,indent=0]
----
@FeignClient(name = "demo")
protected interface PageableFeignClient {
protected interface PageableFeignClient {
@CollectionFormat(feign.CollectionFormat.CSV)
@GetMapping(path = "/page")
ResponseEntity performRequest(Pageable page);
@CollectionFormat(feign.CollectionFormat.CSV)
@GetMapping(path = "/page")
ResponseEntity performRequest(Pageable page);
}
}
----
TIP: Set the `CSV` format while sending `Pageable` as a query parameter in order for it to be encoded correctly.
@@ -717,6 +733,17 @@ You may consider enabling Jackson Modules for the support `org.springframework.d
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 Spring Cloud OpenFeign related configuration properties please check link:appendix.html[the Appendix page].
+3 -3
View File
@@ -4,14 +4,14 @@
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-openfeign</artifactId>
<version>3.0.2</version>
<version>3.1.0-M4</version>
<packaging>pom</packaging>
<name>Spring Cloud OpenFeign</name>
<description>Spring Cloud OpenFeign</description>
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-build</artifactId>
<version>3.0.2</version>
<version>3.1.0-M3</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.2</spring-cloud-commons.version>
<spring-cloud-commons.version>3.1.0-M3</spring-cloud-commons.version>
<!-- Plugin versions -->
<maven-eclipse-plugin.version>2.10</maven-eclipse-plugin.version>
+1 -1
View File
@@ -6,7 +6,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign</artifactId>
<version>3.0.2</version>
<version>3.1.0-M4</version>
<relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent>
<artifactId>spring-cloud-openfeign-core</artifactId>
@@ -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);
}
@@ -25,9 +25,10 @@ import java.lang.annotation.Target;
* Indicates which collection format should be used while processing the annotated method.
*
* @author Olga Maciaszek-Sharma
* @author Sam Kruglov
* @see feign.CollectionFormat
*/
@Target(ElementType.METHOD)
@Target({ ElementType.METHOD, ElementType.TYPE })
@Retention(RetentionPolicy.RUNTIME)
public @interface CollectionFormat {
@@ -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;
@@ -29,6 +30,7 @@ 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;
@@ -42,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;
@@ -55,7 +58,6 @@ import org.springframework.cloud.commons.httpclient.ApacheHttpClientFactory;
import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory;
import org.springframework.cloud.commons.httpclient.OkHttpClientFactory;
import org.springframework.cloud.openfeign.security.OAuth2FeignRequestInterceptor;
import org.springframework.cloud.openfeign.support.DefaultGzipDecoderConfiguration;
import org.springframework.cloud.openfeign.support.FeignEncoderProperties;
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
import org.springframework.cloud.openfeign.support.PageJacksonModule;
@@ -77,12 +79,13 @@ import org.springframework.security.oauth2.client.resource.OAuth2ProtectedResour
* @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,
FeignEncoderProperties.class })
@Import(DefaultGzipDecoderConfiguration.class)
public class FeignAutoConfiguration {
private static final Log LOG = LogFactory.getLog(FeignAutoConfiguration.class);
@@ -144,11 +147,29 @@ 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);
}
}
}
@@ -26,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 {
@@ -50,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;
@@ -60,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);
}
@@ -75,7 +91,8 @@ public final class FeignCircuitBreaker {
public Feign build(final FallbackFactory<?> nullableFallbackFactory) {
super.invocationHandlerFactory((target, dispatch) -> new FeignCircuitBreakerInvocationHandler(
circuitBreakerFactory, target, dispatch, nullableFallbackFactory));
circuitBreakerFactory, feignClientName, target, dispatch, nullableFallbackFactory,
circuitBreakerGroupEnabled, circuitBreakerNameResolver));
return super.build();
}
@@ -24,12 +24,13 @@ import java.util.Map;
import java.util.function.Function;
import java.util.function.Supplier;
import feign.Feign;
import feign.InvocationHandlerFactory;
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;
@@ -37,6 +38,8 @@ class FeignCircuitBreakerInvocationHandler implements InvocationHandler {
private final CircuitBreakerFactory factory;
private final String feignClientName;
private final Target<?> target;
private final Map<Method, InvocationHandlerFactory.MethodHandler> dispatch;
@@ -45,13 +48,21 @@ class FeignCircuitBreakerInvocationHandler implements InvocationHandler {
private final Map<Method, Method> fallbackMethodMap;
FeignCircuitBreakerInvocationHandler(CircuitBreakerFactory factory, Target<?> target,
Map<Method, InvocationHandlerFactory.MethodHandler> dispatch, FallbackFactory<?> nullableFallbackFactory) {
private final boolean circuitBreakerGroupEnabled;
private final CircuitBreakerNameResolver circuitBreakerNameResolver;
FeignCircuitBreakerInvocationHandler(CircuitBreakerFactory factory, String feignClientName, Target<?> target,
Map<Method, InvocationHandlerFactory.MethodHandler> dispatch, FallbackFactory<?> nullableFallbackFactory,
boolean circuitBreakerGroupEnabled, CircuitBreakerNameResolver circuitBreakerNameResolver) {
this.factory = factory;
this.feignClientName = feignClientName;
this.target = checkNotNull(target, "target");
this.dispatch = checkNotNull(dispatch, "dispatch");
this.fallbackMethodMap = toFallbackMethod(dispatch);
this.nullableFallbackFactory = nullableFallbackFactory;
this.circuitBreakerGroupEnabled = circuitBreakerGroupEnabled;
this.circuitBreakerNameResolver = circuitBreakerNameResolver;
}
@Override
@@ -73,8 +84,10 @@ class FeignCircuitBreakerInvocationHandler implements InvocationHandler {
else if ("toString".equals(method.getName())) {
return toString();
}
String circuitName = Feign.configKey(target.type(), method);
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 -> {
@@ -92,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;
@@ -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);
}
}
@@ -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;
@@ -17,6 +17,7 @@
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;
@@ -36,6 +37,8 @@ import feign.Target.HardCodedTarget;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
@@ -63,6 +66,8 @@ import org.springframework.util.StringUtils;
* @author Ilia Ilinykh
* @author Marcin Grzejszczak
* @author Jonatan Ivanov
* @author Sam Kruglov
* @author Jasbir Singh
*/
public class FeignClientFactoryBean
implements FactoryBean<Object>, InitializingBean, ApplicationContextAware, BeanFactoryAware {
@@ -72,6 +77,8 @@ public class FeignClientFactoryBean
* lifecycle race condition.
***********************************/
private static Log LOG = LogFactory.getLog(FeignClientFactoryBean.class);
private Class<?> type;
private String name;
@@ -100,6 +107,10 @@ public class FeignClientFactoryBean
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");
@@ -133,6 +144,7 @@ public class FeignClientFactoryBean
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) {
@@ -180,6 +192,10 @@ public class FeignClientFactoryBean
}
}
Request.Options options = getInheritedAwareOptional(context, Request.Options.class);
if (options == null) {
options = getOptionsByName(context, contextId);
}
if (options != null) {
builder.options(options);
readTimeoutMillis = options.readTimeoutMillis();
@@ -223,12 +239,15 @@ public class FeignClientFactoryBean
builder.logLevel(config.getLoggerLevel());
}
connectTimeoutMillis = config.getConnectTimeout() != null ? config.getConnectTimeout() : connectTimeoutMillis;
readTimeoutMillis = config.getReadTimeout() != null ? config.getReadTimeout() : readTimeoutMillis;
followRedirects = config.isFollowRedirects() != null ? config.isFollowRedirects() : followRedirects;
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, followRedirects));
builder.options(new Request.Options(connectTimeoutMillis, TimeUnit.MILLISECONDS, readTimeoutMillis,
TimeUnit.MILLISECONDS, followRedirects));
}
if (config.getRetryer() != null) {
Retryer retryer = getOrInstantiate(config.getRetryer());
@@ -258,13 +277,8 @@ public class FeignClientFactoryBean
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()));
@@ -283,6 +297,35 @@ public class FeignClientFactoryBean
}
}
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 beanFactory != null ? beanFactory.getBean(tClass) : applicationContext.getBean(tClass);
@@ -334,6 +377,20 @@ public class FeignClientFactoryBean
"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();
@@ -350,6 +407,10 @@ public class FeignClientFactoryBean
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;
}
@@ -460,6 +521,10 @@ public class FeignClientFactoryBean
this.inheritParentContext = inheritParentContext;
}
public void addCustomizer(FeignBuilderCustomizer customizer) {
additionalCustomizers.add(customizer);
}
public ApplicationContext getApplicationContext() {
return applicationContext;
}
@@ -486,6 +551,10 @@ public class FeignClientFactoryBean
this.fallbackFactory = fallbackFactory;
}
public void setRefreshableClient(boolean refreshableClient) {
this.refreshableClient = refreshableClient;
}
@Override
public boolean equals(Object o) {
if (this == o) {
@@ -502,13 +571,14 @@ public class FeignClientFactoryBean
&& 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(followRedirects, that.followRedirects)
&& Objects.equals(refreshableClient, that.refreshableClient);
}
@Override
public int hashCode() {
return Objects.hash(applicationContext, beanFactory, decode404, inheritParentContext, fallback, fallbackFactory,
name, path, type, url, readTimeoutMillis, connectTimeoutMillis, followRedirects);
name, path, type, url, readTimeoutMillis, connectTimeoutMillis, followRedirects, refreshableClient);
}
@Override
@@ -520,8 +590,8 @@ public class FeignClientFactoryBean
.append(", ").append("beanFactory=").append(beanFactory).append(", ").append("fallback=")
.append(fallback).append(", ").append("fallbackFactory=").append(fallbackFactory).append("}")
.append("connectTimeoutMillis=").append(connectTimeoutMillis).append("}").append("readTimeoutMillis=")
.append(readTimeoutMillis).append("}").append("followRedirects=").append(followRedirects).append("}")
.toString();
.append(readTimeoutMillis).append("}").append("followRedirects=").append(followRedirects)
.append("refreshableClient=").append(refreshableClient).append("}").toString();
}
@Override
@@ -47,6 +47,7 @@ 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.FeignEncoderProperties;
import org.springframework.cloud.openfeign.support.HttpMessageConverterCustomizer;
import org.springframework.cloud.openfeign.support.PageableSpringEncoder;
import org.springframework.cloud.openfeign.support.PageableSpringQueryMapEncoder;
import org.springframework.cloud.openfeign.support.ResponseEntityDecoder;
@@ -70,6 +71,7 @@ import static feign.form.ContentType.MULTIPART;
* @author Jonatan Ivanov
* @author Olga Maciaszek-Sharma
* @author Hyeonmin Park
* @author Yanming Zhou
*/
@Configuration(proxyBeanMethods = false)
public class FeignClientsConfiguration {
@@ -97,22 +99,25 @@ public class FeignClientsConfiguration {
@Bean
@ConditionalOnMissingBean
public Decoder feignDecoder() {
return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
public Decoder feignDecoder(ObjectProvider<HttpMessageConverterCustomizer> customizers) {
return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(messageConverters, customizers)));
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnMissingClass("org.springframework.data.domain.Pageable")
public Encoder feignEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider) {
return springEncoder(formWriterProvider, encoderProperties);
public Encoder feignEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider,
ObjectProvider<HttpMessageConverterCustomizer> customizers) {
return springEncoder(formWriterProvider, encoderProperties, customizers);
}
@Bean
@ConditionalOnClass(name = "org.springframework.data.domain.Pageable")
@ConditionalOnMissingBean
public Encoder feignEncoderPageable(ObjectProvider<AbstractFormWriter> formWriterProvider) {
PageableSpringEncoder encoder = new PageableSpringEncoder(springEncoder(formWriterProvider, encoderProperties));
public Encoder feignEncoderPageable(ObjectProvider<AbstractFormWriter> formWriterProvider,
ObjectProvider<HttpMessageConverterCustomizer> customizers) {
PageableSpringEncoder encoder = new PageableSpringEncoder(
springEncoder(formWriterProvider, encoderProperties, customizers));
if (springDataWebProperties != null) {
encoder.setPageParameter(springDataWebProperties.getPageable().getPageParameter());
@@ -126,20 +131,26 @@ public class FeignClientsConfiguration {
@ConditionalOnClass(name = "org.springframework.data.domain.Pageable")
@ConditionalOnMissingBean
public QueryMapEncoder feignQueryMapEncoderPageable() {
return new PageableSpringQueryMapEncoder();
PageableSpringQueryMapEncoder queryMapEncoder = new PageableSpringQueryMapEncoder();
if (springDataWebProperties != null) {
queryMapEncoder.setPageParameter(springDataWebProperties.getPageable().getPageParameter());
queryMapEncoder.setSizeParameter(springDataWebProperties.getPageable().getSizeParameter());
queryMapEncoder.setSortParameter(springDataWebProperties.getSort().getSortParameter());
}
return queryMapEncoder;
}
@Bean
@ConditionalOnMissingBean
public Contract feignContract(ConversionService feignConversionService) {
boolean decodeSlash = feignClientProperties == null || feignClientProperties.isDecodeSlash();
return new SpringMvcContract(this.parameterProcessors, feignConversionService, decodeSlash);
return new SpringMvcContract(parameterProcessors, feignConversionService, decodeSlash);
}
@Bean
public FormattingConversionService feignConversionService() {
FormattingConversionService conversionService = new DefaultFormattingConversionService();
for (FeignFormatterRegistrar feignFormatterRegistrar : this.feignFormatterRegistrars) {
for (FeignFormatterRegistrar feignFormatterRegistrar : feignFormatterRegistrars) {
feignFormatterRegistrar.registerFormatters(conversionService);
}
return conversionService;
@@ -154,7 +165,7 @@ public class FeignClientsConfiguration {
@Bean
@ConditionalOnMissingBean(FeignLoggerFactory.class)
public FeignLoggerFactory feignLoggerFactory() {
return new DefaultFeignLoggerFactory(this.logger);
return new DefaultFeignLoggerFactory(logger);
}
@Bean
@@ -165,14 +176,15 @@ public class FeignClientsConfiguration {
}
private Encoder springEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider,
FeignEncoderProperties encoderProperties) {
FeignEncoderProperties encoderProperties, ObjectProvider<HttpMessageConverterCustomizer> customizers) {
AbstractFormWriter formWriter = formWriterProvider.getIfAvailable();
if (formWriter != null) {
return new SpringEncoder(new SpringPojoFormEncoder(formWriter), this.messageConverters, encoderProperties);
return new SpringEncoder(new SpringPojoFormEncoder(formWriter), messageConverters, encoderProperties,
customizers);
}
else {
return new SpringEncoder(new SpringFormEncoder(), this.messageConverters, encoderProperties);
return new SpringEncoder(new SpringFormEncoder(), messageConverters, encoderProperties, customizers);
}
}
@@ -29,6 +29,9 @@ 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;
@@ -63,6 +66,7 @@ import org.springframework.util.StringUtils;
* @author Michal Domagala
* @author Marcin Grzejszczak
* @author Olga Maciaszek-Sharma
* @author Jasbir Singh
*/
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
@@ -213,6 +217,7 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
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));
@@ -249,6 +254,8 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLo
BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, qualifiers);
BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
registerOptionsBeanDefinition(registry, contextId);
}
private void validate(Map<String, Object> attributes) {
@@ -408,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);
}
}
@@ -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);
}
}
@@ -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);
}
}
@@ -0,0 +1,67 @@
/*
* 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.annotation;
import java.lang.annotation.Annotation;
import java.lang.reflect.Method;
import java.util.Arrays;
import feign.MethodMetadata;
import org.springframework.cloud.openfeign.AnnotatedParameterProcessor;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.CookieValue;
import static feign.Util.checkState;
import static feign.Util.emptyToNull;
/**
* @{link CookieValue} annotation processor.
* @author Gong Yi
*
*/
public class CookieValueParameterProcessor implements AnnotatedParameterProcessor {
private static final Class<CookieValue> ANNOTATION = CookieValue.class;
@Override
public Class<? extends Annotation> getAnnotationType() {
return ANNOTATION;
}
@Override
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) {
int parameterIndex = context.getParameterIndex();
MethodMetadata data = context.getMethodMetadata();
CookieValue cookie = ANNOTATION.cast(annotation);
String name = cookie.value().trim();
checkState(emptyToNull(name) != null, "Cookie.name() was empty on parameter %s", parameterIndex);
context.setParameterName(name);
String cookieExpression = data.template().headers().getOrDefault(HttpHeaders.COOKIE, Arrays.asList("")).stream()
.findFirst().orElse("");
if (cookieExpression.length() == 0) {
cookieExpression = String.format("%s={%s}", name, name);
}
else {
cookieExpression += String.format("; %s={%s}", name, name);
}
data.template().removeHeader(HttpHeaders.COOKIE);
data.template().header(HttpHeaders.COOKIE, cookieExpression);
return true;
}
}
@@ -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());
}
}
@@ -34,6 +34,7 @@ import static feign.Util.emptyToNull;
*
* @author Jakub Narloch
* @author Abhijit Sarkar
* @author Yanming Zhou
* @see AnnotatedParameterProcessor
*/
public class PathVariableParameterProcessor implements AnnotatedParameterProcessor {
@@ -54,7 +55,8 @@ public class PathVariableParameterProcessor implements AnnotatedParameterProcess
MethodMetadata data = context.getMethodMetadata();
String varName = '{' + name + '}';
if (!data.template().url().contains(varName) && !searchMapValues(data.template().queries(), varName)
String varNameRegex = ".*\\{" + name + "(:[^}]+)?\\}.*";
if (!data.template().url().matches(varNameRegex) && !searchMapValues(data.template().queries(), varName)
&& !searchMapValues(data.template().headers(), varName)) {
data.formParams().add(name);
}
@@ -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,77 +16,34 @@
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.cloud.openfeign.support.HttpMessageConverterCustomizer;
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.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter;
import static org.springframework.hateoas.MediaTypes.HAL_JSON;
import org.springframework.hateoas.config.HateoasConfiguration;
import org.springframework.hateoas.config.WebConverters;
/**
* @author Hector Espert
* @author Olga Maciaszek-Sharma
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnWebApplication
@ConditionalOnClass(RepresentationModel.class)
@ConditionalOnClass(WebConverters.class)
@AutoConfigureAfter({ JacksonAutoConfiguration.class, HttpMessageConvertersAutoConfiguration.class,
RepositoryRestMvcAutoConfiguration.class })
@AutoConfigureBefore(HypermediaAutoConfiguration.class)
RepositoryRestMvcAutoConfiguration.class, HateoasConfiguration.class })
public class FeignHalAutoConfiguration {
@Bean
@ConditionalOnMissingBean
public TypeConstrainedMappingJackson2HttpMessageConverter halJacksonHttpMessageConverter(
ObjectProvider<ObjectMapper> objectMapper, ObjectProvider<HalConfiguration> halConfiguration,
ObjectProvider<MessageResolver> messageResolver, ObjectProvider<CurieProvider> curieProvider,
ObjectProvider<LinkRelationProvider> linkRelationProvider) {
ObjectMapper mapper = objectMapper.getIfAvailable(ObjectMapper::new).copy();
mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);
HalConfiguration configuration = halConfiguration.getIfAvailable(HalConfiguration::new);
CurieProvider curieProviderInstance = curieProvider
.getIfAvailable(() -> new DefaultCurieProvider(Collections.emptyMap()));
Jackson2HalModule.HalHandlerInstantiator halHandlerInstantiator = new Jackson2HalModule.HalHandlerInstantiator(
linkRelationProvider.getIfAvailable(), curieProviderInstance, messageResolver.getIfAvailable(),
configuration);
mapper.setHandlerInstantiator(halHandlerInstantiator);
if (!Jackson2HalModule.isAlreadyRegisteredIn(mapper)) {
Jackson2HalModule halModule = new Jackson2HalModule();
mapper.registerModule(halModule);
}
TypeConstrainedMappingJackson2HttpMessageConverter converter = new TypeConstrainedMappingJackson2HttpMessageConverter(
RepresentationModel.class);
converter.setSupportedMediaTypes(Arrays.asList(HAL_JSON));
converter.setObjectMapper(mapper);
return converter;
@ConditionalOnBean(WebConverters.class)
HttpMessageConverterCustomizer webConvertersCustomizer(WebConverters webConverters) {
return new WebConvertersCustomizer(webConverters);
}
}
@@ -0,0 +1,41 @@
/*
* 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.
* 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.hateoas;
import java.util.List;
import org.springframework.cloud.openfeign.support.HttpMessageConverterCustomizer;
import org.springframework.hateoas.config.WebConverters;
import org.springframework.http.converter.HttpMessageConverter;
/**
* @author Olga Maciaszek-Sharma
*/
public class WebConvertersCustomizer implements HttpMessageConverterCustomizer {
private final WebConverters webConverters;
public WebConvertersCustomizer(WebConverters webConverters) {
this.webConverters = webConverters;
}
@Override
public void accept(List<HttpMessageConverter<?>> httpMessageConverters) {
webConverters.augmentClient(httpMessageConverters);
}
}
@@ -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;
}
}
@@ -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;
@@ -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>() {
@@ -1,80 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.openfeign.support;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.lang.reflect.Type;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
import java.util.zip.GZIPInputStream;
import feign.FeignException;
import feign.Response;
import feign.codec.Decoder;
import org.springframework.cloud.openfeign.encoding.HttpEncoding;
/**
* When response is compressed as gzip, this decompresses and uses {@link SpringDecoder}
* to decode.
*
* @author Jaesik Kim
*/
public class DefaultGzipDecoder implements Decoder {
private Decoder decoder;
public DefaultGzipDecoder(Decoder decoder) {
this.decoder = decoder;
}
@Override
public Object decode(final Response response, Type type) throws IOException, FeignException {
Collection<String> encoding = response.headers().containsKey(HttpEncoding.CONTENT_ENCODING_HEADER)
? response.headers().get(HttpEncoding.CONTENT_ENCODING_HEADER) : null;
if (encoding != null) {
if (encoding.contains(HttpEncoding.GZIP_ENCODING)) {
String decompressedBody = decompress(response);
if (decompressedBody != null) {
Response decompressedResponse = response.toBuilder().body(decompressedBody.getBytes()).build();
return decoder.decode(decompressedResponse, type);
}
}
}
return decoder.decode(response, type);
}
private String decompress(Response response) throws IOException {
if (response.body() == null) {
return null;
}
try (GZIPInputStream gzipInputStream = new GZIPInputStream(response.body().asInputStream());
BufferedReader reader = new BufferedReader(
new InputStreamReader(gzipInputStream, StandardCharsets.UTF_8))) {
String outputString = "";
String line;
while ((line = reader.readLine()) != null) {
outputString += line;
}
return outputString;
}
}
}
@@ -1,58 +0,0 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.openfeign.support;
import feign.codec.Decoder;
import feign.optionals.OptionalDecoder;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.AutoConfigureAfter;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* Configures Default Gzip Decoder.
*
* @author Jaesik Kim
*/
@Configuration(proxyBeanMethods = false)
@ConditionalOnProperty("feign.compression.response.enabled")
// The OK HTTP client uses "transparent" compression.
// If the accept-encoding header is present, it disables transparent compression
@ConditionalOnMissingBean(type = "okhttp3.OkHttpClient")
@AutoConfigureAfter(FeignAutoConfiguration.class)
public class DefaultGzipDecoderConfiguration {
private ObjectFactory<HttpMessageConverters> messageConverters;
public DefaultGzipDecoderConfiguration(ObjectFactory<HttpMessageConverters> messageConverters) {
this.messageConverters = messageConverters;
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnProperty("feign.compression.response.useGzipDecoder")
public Decoder defaultGzipDecoder() {
return new OptionalDecoder(
new ResponseEntityDecoder(new DefaultGzipDecoder(new SpringDecoder(messageConverters))));
}
}
@@ -0,0 +1,54 @@
/*
* Copyright 2013-2021 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.openfeign.support;
import java.util.function.Consumer;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectProvider;
/**
* @author Olga Maciaszek-Sharma
*/
class EmptyObjectProvider<T> implements ObjectProvider<T> {
@Override
public T getObject(Object... args) throws BeansException {
return null;
}
@Override
public T getIfAvailable() throws BeansException {
return null;
}
@Override
public T getIfUnique() throws BeansException {
return null;
}
@Override
public T getObject() throws BeansException {
return null;
}
@Override
public void forEach(Consumer action) {
// do nothing
}
}
@@ -0,0 +1,33 @@
/*
* 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.
* 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.List;
import java.util.function.Consumer;
import org.springframework.http.converter.HttpMessageConverter;
/**
* Allows customising {@link HttpMessageConverter} objects passed via {@link Consumer}
* parameter.
*
* @author Olga Maciaszek-Sharma
* @since 3.1.0
*/
public interface HttpMessageConverterCustomizer extends Consumer<List<HttpMessageConverter<?>>> {
}
@@ -32,6 +32,7 @@ import org.springframework.data.domain.Sort;
* Provides support for encoding spring Pageable via composition.
*
* @author Pascal Büttiker
* @author Yanming Zhou
*/
public class PageableSpringEncoder implements Encoder {
@@ -82,8 +83,8 @@ public class PageableSpringEncoder implements Encoder {
Pageable pageable = (Pageable) object;
if (pageable.isPaged()) {
template.query(pageParameter, pageable.getPageNumber() + "");
template.query(sizeParameter, pageable.getPageSize() + "");
template.query(pageParameter, String.valueOf(pageable.getPageNumber()));
template.query(sizeParameter, String.valueOf(pageable.getPageSize()));
}
if (pageable.getSort() != null) {
@@ -31,10 +31,38 @@ import org.springframework.data.domain.Sort;
* {@link org.springframework.cloud.openfeign.SpringQueryMap}.
*
* @author Hyeonmin Park
* @author Yanming Zhou
* @since 2.2.8
*/
public class PageableSpringQueryMapEncoder extends BeanQueryMapEncoder {
/**
* Page index parameter name.
*/
private String pageParameter = "page";
/**
* Page size parameter name.
*/
private String sizeParameter = "size";
/**
* Sort parameter name.
*/
private String sortParameter = "sort";
public void setPageParameter(String pageParameter) {
this.pageParameter = pageParameter;
}
public void setSizeParameter(String sizeParameter) {
this.sizeParameter = sizeParameter;
}
public void setSortParameter(String sortParameter) {
this.sortParameter = sortParameter;
}
@Override
public Map<String, Object> encode(Object object) {
if (supports(object)) {
@@ -44,8 +72,8 @@ public class PageableSpringQueryMapEncoder extends BeanQueryMapEncoder {
Pageable pageable = (Pageable) object;
if (pageable.isPaged()) {
queryMap.put("page", pageable.getPageNumber());
queryMap.put("size", pageable.getPageSize());
queryMap.put(pageParameter, pageable.getPageNumber());
queryMap.put(sizeParameter, pageable.getPageSize());
}
if (pageable.getSort() != null) {
@@ -69,7 +97,7 @@ public class PageableSpringQueryMapEncoder extends BeanQueryMapEncoder {
sortQueries.add(order.getProperty() + "%2C" + order.getDirection());
}
if (!sortQueries.isEmpty()) {
queryMap.put("sort", sortQueries);
queryMap.put(sortParameter, sortQueries);
}
}
@@ -21,6 +21,7 @@ import java.io.InputStream;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.WildcardType;
import java.util.List;
import feign.FeignException;
import feign.Response;
@@ -28,31 +29,48 @@ import feign.codec.DecodeException;
import feign.codec.Decoder;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.web.client.HttpMessageConverterExtractor;
import static org.springframework.cloud.openfeign.support.FeignUtils.getHttpHeaders;
/**
* @author Spencer Gibb
* @author Olga Maciaszek-Sharma
*/
public class SpringDecoder implements Decoder {
private ObjectFactory<HttpMessageConverters> messageConverters;
private final ObjectFactory<HttpMessageConverters> messageConverters;
private final ObjectProvider<HttpMessageConverterCustomizer> customizers;
/**
* @deprecated in favour of
* {@link SpringDecoder#SpringDecoder(ObjectFactory, ObjectProvider)}
*/
@Deprecated
public SpringDecoder(ObjectFactory<HttpMessageConverters> messageConverters) {
this(messageConverters, new EmptyObjectProvider<>());
}
public SpringDecoder(ObjectFactory<HttpMessageConverters> messageConverters,
ObjectProvider<HttpMessageConverterCustomizer> customizers) {
this.messageConverters = messageConverters;
this.customizers = customizers;
}
@Override
public Object decode(final Response response, Type type) throws IOException, FeignException {
if (type instanceof Class || type instanceof ParameterizedType || type instanceof WildcardType) {
List<HttpMessageConverter<?>> converters = messageConverters.getObject().getConverters();
customizers.forEach(customizer -> customizer.accept(converters));
@SuppressWarnings({ "unchecked", "rawtypes" })
HttpMessageConverterExtractor<?> extractor = new HttpMessageConverterExtractor(type,
this.messageConverters.getObject().getConverters());
HttpMessageConverterExtractor<?> extractor = new HttpMessageConverterExtractor(type, converters);
return extractor.extractData(new FeignResponseAdapter(response));
}
@@ -70,23 +88,23 @@ public class SpringDecoder implements Decoder {
@Override
public HttpStatus getStatusCode() throws IOException {
return HttpStatus.valueOf(this.response.status());
return HttpStatus.valueOf(response.status());
}
@Override
public int getRawStatusCode() throws IOException {
return this.response.status();
return response.status();
}
@Override
public String getStatusText() throws IOException {
return this.response.reason();
return response.reason();
}
@Override
public void close() {
try {
this.response.body().close();
response.body().close();
}
catch (IOException ex) {
// Ignore exception on close...
@@ -95,12 +113,12 @@ public class SpringDecoder implements Decoder {
@Override
public InputStream getBody() throws IOException {
return this.response.body().asInputStream();
return response.body().asInputStream();
}
@Override
public HttpHeaders getHeaders() {
return getHttpHeaders(this.response.headers());
return getHttpHeaders(response.headers());
}
}
@@ -24,6 +24,8 @@ import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.Arrays;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.stream.Stream;
import feign.RequestTemplate;
@@ -34,6 +36,7 @@ import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.encoding.HttpEncoding;
import org.springframework.http.HttpHeaders;
@@ -48,6 +51,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
@@ -56,6 +63,7 @@ import static org.springframework.cloud.openfeign.support.FeignUtils.getHttpHead
* @author Aaron Whiteside
* @author Darren Foong
* @author Olga Maciaszek-Sharma
* @author Can Bezmen
*/
@SuppressWarnings("rawtypes")
public class SpringEncoder implements Encoder {
@@ -68,19 +76,37 @@ public class SpringEncoder implements Encoder {
private final FeignEncoderProperties encoderProperties;
private final ObjectProvider<HttpMessageConverterCustomizer> customizers;
public SpringEncoder(ObjectFactory<HttpMessageConverters> messageConverters) {
this(new SpringFormEncoder(), messageConverters);
}
/**
* @deprecated in favour of
* {@link SpringEncoder#SpringEncoder(SpringFormEncoder, ObjectFactory, FeignEncoderProperties, ObjectProvider)}
*/
@Deprecated
public SpringEncoder(SpringFormEncoder springFormEncoder, ObjectFactory<HttpMessageConverters> messageConverters) {
this(springFormEncoder, messageConverters, new FeignEncoderProperties());
}
/**
* @deprecated in favour of
* {@link SpringEncoder#SpringEncoder(SpringFormEncoder, ObjectFactory, FeignEncoderProperties, ObjectProvider)}
*/
@Deprecated
public SpringEncoder(SpringFormEncoder springFormEncoder, ObjectFactory<HttpMessageConverters> messageConverters,
FeignEncoderProperties encoderProperties) {
this(springFormEncoder, messageConverters, encoderProperties, new EmptyObjectProvider<>());
}
public SpringEncoder(SpringFormEncoder springFormEncoder, ObjectFactory<HttpMessageConverters> messageConverters,
FeignEncoderProperties encoderProperties, ObjectProvider<HttpMessageConverterCustomizer> customizers) {
this.springFormEncoder = springFormEncoder;
this.messageConverters = messageConverters;
this.encoderProperties = encoderProperties;
this.customizers = customizers;
}
@Override
@@ -95,8 +121,8 @@ public class SpringEncoder implements Encoder {
requestContentType = MediaType.valueOf(type);
}
if (isMultipartType(requestContentType)) {
this.springFormEncoder.encode(requestBody, bodyType, request);
if (isFormRelatedContentType(requestContentType)) {
springFormEncoder.encode(requestBody, bodyType, request);
return;
}
else {
@@ -111,7 +137,9 @@ public class SpringEncoder implements Encoder {
private void encodeWithMessageConverter(Object requestBody, Type bodyType, RequestTemplate request,
MediaType requestContentType) {
for (HttpMessageConverter messageConverter : this.messageConverters.getObject().getConverters()) {
List<HttpMessageConverter<?>> converters = messageConverters.getObject().getConverters();
customizers.forEach(customizer -> customizer.accept(converters));
for (HttpMessageConverter messageConverter : converters) {
FeignOutputMessage outputMessage;
try {
if (messageConverter instanceof GenericHttpMessageConverter) {
@@ -207,9 +235,16 @@ public class SpringEncoder implements Encoder {
}
}
private boolean isFormRelatedContentType(MediaType requestContentType) {
return isMultipartType(requestContentType) || isFormUrlEncoded(requestContentType);
}
private boolean isMultipartType(MediaType requestContentType) {
return Arrays.asList(MediaType.MULTIPART_FORM_DATA, MediaType.MULTIPART_MIXED, MediaType.MULTIPART_RELATED)
.contains(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) {
@@ -227,21 +262,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;
}
}
@@ -35,9 +35,12 @@ 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;
import org.springframework.cloud.openfeign.annotation.CookieValueParameterProcessor;
import org.springframework.cloud.openfeign.annotation.MatrixVariableParameterProcessor;
import org.springframework.cloud.openfeign.annotation.PathVariableParameterProcessor;
import org.springframework.cloud.openfeign.annotation.QueryMapParameterProcessor;
@@ -79,9 +82,12 @@ import static org.springframework.core.annotation.AnnotatedElementUtils.findMerg
* @author Artyom Romanenko
* @author Darren Foong
* @author Ram Anaswara
* @author Sam Kruglov
*/
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 +175,22 @@ 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");
}
CollectionFormat collectionFormat = findMergedAnnotation(clz, CollectionFormat.class);
if (collectionFormat != null) {
data.template().collectionFormat(collectionFormat.value());
}
}
@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
@@ -360,6 +341,7 @@ public class SpringMvcContract extends Contract.BaseContract implements Resource
annotatedArgumentResolvers.add(new RequestHeaderParameterProcessor());
annotatedArgumentResolvers.add(new QueryMapParameterProcessor());
annotatedArgumentResolvers.add(new RequestPartParameterProcessor());
annotatedArgumentResolvers.add(new CookieValueParameterProcessor());
return annotatedArgumentResolvers;
}
@@ -14,6 +14,12 @@
"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",
@@ -38,12 +44,6 @@
"description": "Enables the response from Feign to be compressed.",
"defaultValue": "false"
},
{
"name": "feign.compression.response.useGzipDecoder",
"type": "java.lang.Boolean",
"description": "Enables the default gzip decoder to be used.",
"defaultValue": "false"
},
{
"name": "feign.compression.request.enabled",
"type": "java.lang.Boolean",
@@ -55,6 +55,12 @@
"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"
}
]
}
@@ -16,12 +16,16 @@
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;
@@ -30,6 +34,8 @@ import static org.mockito.Mockito.mock;
/**
* @author Tim Peeters
* @author Olga Maciaszek-Sharma
* @author Andrii Bohutskyi
* @author Kwangyong Kim
*/
class FeignAutoConfigurationTests {
@@ -45,14 +51,61 @@ class FeignAutoConfigurationTests {
@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")
.run(ctx -> assertOnlyOneTargeterPresent(ctx, FeignCircuitBreakerTargeter.class));
.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();
}
}
}
@@ -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");
@@ -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
@@ -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
@@ -32,8 +32,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.GetMapping;
import static org.assertj.core.api.Assertions.assertThat;
@@ -83,7 +82,7 @@ class FeignClientDisabledClientLevelFeaturesTests {
@FeignClient(name = "bar", url = "https://bar", configuration = BarConfiguration.class)
interface BarClient {
@RequestMapping(value = "/", method = RequestMethod.GET)
@GetMapping("/")
String get();
}
@@ -32,8 +32,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.GetMapping;
import static org.assertj.core.api.Assertions.assertThat;
@@ -82,7 +81,7 @@ class FeignClientDisabledFeaturesTests {
@FeignClient(name = "bar", url = "https://bar", configuration = BarConfiguration.class)
interface BarClient {
@RequestMapping(value = "/", method = RequestMethod.GET)
@GetMapping("/")
String get();
}
@@ -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,10 +36,8 @@ 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;
@@ -46,7 +45,6 @@ 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 {
@@ -73,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);
@@ -132,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();
}
@@ -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();
}
@@ -46,8 +46,9 @@ import feign.codec.EncodeException;
import feign.codec.Encoder;
import feign.codec.ErrorDecoder;
import feign.micrometer.MicrometerCapability;
import org.junit.Test;
import org.junit.runner.RunWith;
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;
@@ -61,18 +62,15 @@ 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;
/**
@@ -82,7 +80,6 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
* @author Jonatan Ivanov
*/
@SuppressWarnings("FieldMayBeFinal")
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = FeignClientUsingPropertiesTests.Application.class, webEnvironment = RANDOM_PORT)
@TestPropertySource("classpath:feign-properties.properties")
@DirtiesContext
@@ -161,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
@@ -205,6 +200,7 @@ public class FeignClientUsingPropertiesTests {
}
@Test
@DisabledForJreRange(min = JRE.JAVA_16)
public void readTimeoutShouldWorkWhenConnectTimeoutNotSet() {
FeignClientFactoryBean readTimeoutFactoryBean = new FeignClientFactoryBean();
readTimeoutFactoryBean.setContextId("readTimeout");
@@ -221,6 +217,7 @@ public class FeignClientUsingPropertiesTests {
}
@Test
@DisabledForJreRange(min = JRE.JAVA_16)
public void connectTimeoutShouldWorkWhenReadTimeoutNotSet() {
FeignClientFactoryBean readTimeoutFactoryBean = new FeignClientFactoryBean();
readTimeoutFactoryBean.setContextId("connectTimeout");
@@ -250,6 +247,7 @@ public class FeignClientUsingPropertiesTests {
}
@Test
@DisabledForJreRange(min = JRE.JAVA_16)
public void shouldSetFollowRedirects() {
FeignClientFactoryBean testFactoryBean = new FeignClientFactoryBean();
testFactoryBean.setContextId("test");
@@ -295,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);
}
@@ -328,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";
@@ -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);
}
}
}
}
@@ -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;
@@ -105,7 +104,7 @@ public class FeignClientsRegistrarTests {
@FeignClient(name = "fallbackTestClient", url = "http://localhost:8080/", fallback = FallbackClient.class)
protected interface FallbackClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello")
@GetMapping("/hello")
String fallbackTest();
}
@@ -114,7 +113,7 @@ public class FeignClientsRegistrarTests {
fallbackFactory = FallbackFactoryClient.class)
protected interface FallbackFactoryClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello")
@GetMapping("/hello")
String fallbackFactoryTest();
}
@@ -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(
@@ -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;
@@ -102,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();
}
@@ -110,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();
}
@@ -118,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();
}
@@ -130,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();
}
@@ -38,8 +38,6 @@ 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.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import static org.assertj.core.api.Assertions.assertThat;
@@ -104,7 +102,7 @@ class FeignHttpClientUrlTestsWithRetryableLoadBalancer {
@FeignClient(name = "localappurl", url = "http://localhost:${server.port}/")
protected interface UrlClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello")
@GetMapping("/hello")
Hello getHello();
}
@@ -112,7 +110,7 @@ class FeignHttpClientUrlTestsWithRetryableLoadBalancer {
@FeignClient(name = "beanappurl", url = "#{SERVER_URL}path")
protected interface BeanUrlClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello")
@GetMapping("/hello")
Hello getHello();
}
@@ -120,7 +118,7 @@ class FeignHttpClientUrlTestsWithRetryableLoadBalancer {
@FeignClient(name = "beanappurlnoprotocol", url = "#{SERVER_URL_NO_PROTOCOL}path")
protected interface BeanUrlClientNoProtocol {
@RequestMapping(method = RequestMethod.GET, value = "/hello")
@GetMapping("/hello")
Hello getHello();
}
@@ -39,15 +39,16 @@ import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Jaesik Kim
* @author Olga Maciaszek-Sharma
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = DefaultGzipDecoderTests.Application.class,
@SpringBootTest(classes = GzipDecodingTests.Application.class,
webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT,
value = { "spring.application.name=defaultGzipDecoderTests", "feign.compression.response.enabled=true",
"feign.compression.response.useGzipDecoder=true", "feign.client.config.default.loggerLevel=full",
"feign.client.config.default.loggerLevel=none", "feign.metrics.enabled=false",
"logging.level.org.springframework.cloud.openfeign=DEBUG" })
@DirtiesContext
public class DefaultGzipDecoderTests extends FeignClientFactoryBean {
public class GzipDecodingTests extends FeignClientFactoryBean {
@Autowired
FeignContext context;
@@ -55,7 +56,7 @@ public class DefaultGzipDecoderTests extends FeignClientFactoryBean {
@Value("${local.server.port}")
private int port = 0;
public DefaultGzipDecoderTests() {
public GzipDecodingTests() {
setName("tests");
setContextId("test");
}
@@ -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
+ '}';
}
}
}
@@ -39,8 +39,6 @@ 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;
@@ -154,22 +152,22 @@ public class SpringDecoderTests extends FeignClientFactoryBean {
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")
@@ -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");
}
@@ -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();
}
@@ -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();
}
@@ -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);
}
}
}
@@ -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);
}
}
@@ -45,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;
@@ -115,10 +114,10 @@ public class CircuitBreakerTests {
@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();
}
@@ -142,10 +141,10 @@ public class CircuitBreakerTests {
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();
}
@@ -40,8 +40,7 @@ import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.util.SocketUtils;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import static org.assertj.core.api.Assertions.assertThat;
@@ -97,10 +96,10 @@ public class CircuitBreakerWithNoFallbackTests {
@FeignClient(name = "test", url = "http://localhost:${server.port}/")
protected interface CircuitBreakerTestClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello")
@GetMapping("/hello")
Hello getHello();
@RequestMapping(method = RequestMethod.GET, value = "/hellonotfound")
@GetMapping("/hellonotfound")
String getException();
}
@@ -24,9 +24,9 @@ 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.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;
/**
* Simple Feign client for retrieving the invoice list.
@@ -37,23 +37,23 @@ import org.springframework.web.bind.annotation.RequestMethod;
@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 = "invoicesPagedWithBody", method = RequestMethod.POST,
consumes = MediaType.APPLICATION_JSON_VALUE, 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);
@RequestMapping(value = "invoicesSortedWithBody", method = RequestMethod.POST,
consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@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);
@RequestMapping(value = "invoices", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
@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);
@@ -28,9 +28,9 @@ 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;
/**
@@ -42,28 +42,28 @@ import org.springframework.web.bind.annotation.RestController;
@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(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(null, pageable.getPageSize(), pageable.getSort()),
pageable, 100);
return ResponseEntity.ok(page);
}
@RequestMapping(value = "invoicesPagedWithBody", method = RequestMethod.POST,
consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@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()),
@@ -71,8 +71,8 @@ public class InvoiceResource {
return ResponseEntity.ok(page);
}
@RequestMapping(value = "invoicesSortedWithBody", method = RequestMethod.POST,
consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
@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),
@@ -16,8 +16,8 @@
package org.springframework.cloud.openfeign.hateoas;
import org.junit.Before;
import org.junit.Test;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.autoconfigure.data.rest.RepositoryRestMvcAutoConfiguration;
@@ -27,19 +27,20 @@ import org.springframework.boot.autoconfigure.jackson.JacksonAutoConfiguration;
import org.springframework.boot.test.context.FilteredClassLoader;
import org.springframework.boot.test.context.runner.WebApplicationContextRunner;
import org.springframework.data.rest.webmvc.config.RepositoryRestMvcConfiguration;
import org.springframework.hateoas.RepresentationModel;
import org.springframework.hateoas.config.WebConverters;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Hector Espert
* @author Olga Maciaszek-Sharma
*/
public class FeignHalAutoConfigurationContextTests {
class FeignHalAutoConfigurationContextTests {
private WebApplicationContextRunner contextRunner;
@Before
public void setUp() {
@BeforeEach
void setUp() {
contextRunner = new WebApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(JacksonAutoConfiguration.class,
HttpMessageConvertersAutoConfiguration.class, HypermediaAutoConfiguration.class,
@@ -48,23 +49,18 @@ public class FeignHalAutoConfigurationContextTests {
}
@Test
public void testHalJacksonHttpMessageConverterIsNotLoaded() {
void shouldNotLoadWebConvertersCustomizerWhenNotWebConvertersNotInClasspath() {
FilteredClassLoader filteredClassLoader = new FilteredClassLoader(RepositoryRestMvcConfiguration.class,
RepresentationModel.class);
WebConverters.class);
contextRunner.withClassLoader(filteredClassLoader)
.run(context -> assertThat(context).doesNotHaveBean("halJacksonHttpMessageConverter"));
.run(context -> assertThat(context).doesNotHaveBean("webConvertersCustomizer"));
}
@Test
public void testHalJacksonHttpMessageConverterIsLoaded() {
void shouldLoadWebConvertersCustomizer() {
FilteredClassLoader filteredClassLoader = new FilteredClassLoader(RepositoryRestMvcConfiguration.class);
contextRunner.withClassLoader(filteredClassLoader)
.run(context -> assertThat(context).hasBean("halJacksonHttpMessageConverter"));
}
@Test
public void testHalJacksonHttpMessageConverterIsNotLoadedUseRestDataMessageConverterInstead() {
contextRunner.run(context -> assertThat(context).hasBean("halJacksonHttpMessageConverter"));
.run(context -> assertThat(context).hasBean("webConvertersCustomizer"));
}
}
@@ -1,87 +0,0 @@
/*
* Copyright 2016-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.hateoas;
import java.util.Arrays;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.MockitoJUnitRunner;
import org.springframework.beans.factory.ObjectProvider;
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.Jackson2HalModule;
import org.springframework.hateoas.server.LinkRelationProvider;
import org.springframework.hateoas.server.mvc.TypeConstrainedMappingJackson2HttpMessageConverter;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.when;
import static org.springframework.hateoas.MediaTypes.HAL_JSON;
/**
* @author Hector Espert
*/
@RunWith(MockitoJUnitRunner.class)
public class FeignHalAutoConfigurationTests {
@Mock
private ObjectProvider<ObjectMapper> objectMapper;
@Mock
private ObjectProvider<HalConfiguration> halConfiguration;
@Mock
private ObjectProvider<LinkRelationProvider> relProvider;
@Mock
private ObjectProvider<CurieProvider> curieProvider;
@Mock
private ObjectProvider<MessageResolver> messageResolver;
@InjectMocks
private FeignHalAutoConfiguration feignHalAutoConfiguration;
@Test
public void halJacksonHttpMessageConverter() {
ObjectMapper mapper = new ObjectMapper();
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));
TypeConstrainedMappingJackson2HttpMessageConverter converter = feignHalAutoConfiguration
.halJacksonHttpMessageConverter(objectMapper, halConfiguration, messageResolver, curieProvider,
relProvider);
assertThat(converter).isNotNull();
assertThat(converter.getObjectMapper()).isNotNull();
assertThat(converter.getSupportedMediaTypes()).isEqualTo(Arrays.asList(HAL_JSON));
assertThat(Jackson2HalModule.isAlreadyRegisteredIn(converter.getObjectMapper())).isTrue();
}
}
@@ -35,25 +35,25 @@ public class FeignHalController {
public EntityModel<MarsRover> getEntity() {
MarsRover marsRover = new MarsRover();
marsRover.setName("Sojourner");
Link link = new Link("/entity", "self");
return new EntityModel<>(marsRover, link);
Link link = Link.of("/entity", "self");
return EntityModel.of(marsRover, link);
}
@GetMapping("/collection")
public CollectionModel<MarsRover> getCollection() {
MarsRover marsRover = new MarsRover();
marsRover.setName("Opportunity");
Link link = new Link("/collection", "self");
return new CollectionModel<>(Collections.singleton(marsRover), link);
Link link = Link.of("/collection", "self");
return CollectionModel.of(Collections.singleton(marsRover), link);
}
@GetMapping("/paged")
public CollectionModel<MarsRover> getPaged() {
MarsRover marsRover = new MarsRover();
marsRover.setName("Curiosity");
Link link = new Link("/paged", "self");
Link link = Link.of("/paged", "self");
PagedModel.PageMetadata metadata = new PagedModel.PageMetadata(1, 1, 1);
return new PagedModel<>(Collections.singleton(marsRover), metadata, link);
return PagedModel.of(Collections.singleton(marsRover), metadata, link);
}
}
@@ -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();
}
@@ -16,7 +16,9 @@
package org.springframework.cloud.openfeign.loadbalancer;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.URI;
import java.nio.charset.StandardCharsets;
import java.util.Collection;
@@ -63,7 +65,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)
@@ -105,8 +107,7 @@ class FeignBlockingLoadBalancerClientTests {
Response response = feignBlockingLoadBalancerClient.execute(request, new Request.Options());
assertThat(response.status()).isEqualTo(HttpStatus.SERVICE_UNAVAILABLE.value());
assertThat(response.body().toString())
.isEqualTo("Load balancer does not contain an instance for the service test");
assertThat(read(response)).isEqualTo("Load balancer does not contain an instance for the service test");
}
@Test
@@ -167,6 +168,17 @@ class FeignBlockingLoadBalancerClientTests {
.contains(HttpStatus.OK);
}
private String read(Response response) throws IOException {
BufferedReader reader = new BufferedReader(
new InputStreamReader(response.body().asInputStream(), StandardCharsets.UTF_8));
String outputString = "";
String line;
while ((line = reader.readLine()) != null) {
outputString += line;
}
return outputString;
}
private Request testRequest() {
return testRequest("test");
}
@@ -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();
@@ -37,6 +37,7 @@ import static org.springframework.boot.test.context.SpringBootTest.WebEnvironmen
* Tests the pagination encoding and sorting.
*
* @author Charlie Mordant.
* @author Yanming Zhou
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = SpringEncoderTests.Application.class, webEnvironment = RANDOM_PORT,
@@ -55,6 +56,18 @@ public class PageableEncoderTests {
@Autowired
private FeignContext context;
protected String getPageParameter() {
return "page";
}
protected String getSizeParameter() {
return "size";
}
protected String getSortParameter() {
return "sort";
}
@Test
public void testPaginationAndSortingRequest() {
Encoder encoder = this.context.getInstance("foo", Encoder.class);
@@ -65,11 +78,11 @@ public class PageableEncoderTests {
// Request queries shall contain three entries
assertThat(request.queries()).hasSize(3);
// Request page shall contain page
assertThat(request.queries().get("page")).contains(String.valueOf(PAGE));
assertThat(request.queries().get(getPageParameter())).contains(String.valueOf(PAGE));
// Request size shall contain size
assertThat(request.queries().get("size")).contains(String.valueOf(SIZE));
assertThat(request.queries().get(getSizeParameter())).contains(String.valueOf(SIZE));
// Request sort size shall contain sort entries
assertThat(request.queries().get("sort")).hasSize(2);
assertThat(request.queries().get(getSortParameter())).hasSize(2);
}
private Pageable createPageAndSortRequest() {
@@ -84,11 +97,11 @@ public class PageableEncoderTests {
encoder.encode(createPageAndRequest(), null, request);
assertThat(request.queries().size()).isEqualTo(2);
// Request page shall contain page
assertThat(request.queries().get("page")).contains(String.valueOf(PAGE));
assertThat(request.queries().get(getPageParameter())).contains(String.valueOf(PAGE));
// Request size shall contain size
assertThat(request.queries().get("size")).contains(String.valueOf(SIZE));
assertThat(request.queries().get(getSizeParameter())).contains(String.valueOf(SIZE));
// Request sort size shall contain sort entries
assertThat(request.queries()).doesNotContainKey("sort");
assertThat(request.queries()).doesNotContainKey(getSortParameter());
}
private Pageable createPageAndRequest() {
@@ -105,7 +118,7 @@ public class PageableEncoderTests {
// Request queries shall contain three entries
assertThat(request.queries().size()).isEqualTo(1);
// Request sort size shall contain sort entries
assertThat(request.queries().get("sort")).hasSize(2);
assertThat(request.queries().get(getSortParameter())).hasSize(2);
}
private Sort createSort() {
@@ -0,0 +1,52 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.openfeign.support;
import org.springframework.boot.autoconfigure.data.web.SpringDataWebProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.SpringBootTest;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
/**
* Tests the pagination encoding and sorting.
*
* @author Yanming Zhou
*/
@EnableConfigurationProperties(SpringDataWebProperties.class)
@SpringBootTest(classes = SpringEncoderTests.Application.class, webEnvironment = RANDOM_PORT,
value = { "spring.application.name=springencodertest", "spring.jmx.enabled=false",
"spring.data.web.pageable.pageParameter=pageNo", "spring.data.web.pageable.sizeParameter=pageSize",
"spring.data.web.sort.sortParameter=orderBy" })
public class PageableEncoderWithSpringDataWebTests extends PageableEncoderTests {
@Override
protected String getPageParameter() {
return "pageNo";
}
@Override
protected String getSizeParameter() {
return "pageSize";
}
@Override
protected String getSortParameter() {
return "orderBy";
}
}
@@ -0,0 +1,127 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.openfeign.support;
import java.util.List;
import java.util.Map;
import feign.QueryMapEncoder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.openfeign.FeignContext;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import static org.assertj.core.api.Assertions.assertThat;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
/**
* Tests the pagination encoding and sorting.
*
* @author Yanming Zhou
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = SpringEncoderTests.Application.class, webEnvironment = RANDOM_PORT,
value = { "spring.application.name=springencodertest", "spring.jmx.enabled=false" })
@DirtiesContext
public class PageableSpringQueryMapEncoderTests {
public static final int PAGE = 1;
public static final int SIZE = 10;
public static final String SORT_2 = "sort2";
public static final String SORT_1 = "sort1";
@Autowired
private FeignContext context;
protected String getPageParameter() {
return "page";
}
protected String getSizeParameter() {
return "size";
}
protected String getSortParameter() {
return "sort";
}
@Test
public void testPaginationAndSortingRequest() {
QueryMapEncoder encoder = this.context.getInstance("foo", QueryMapEncoder.class);
assertThat(encoder).isNotNull();
Map<String, Object> map = encoder.encode(createPageAndSortRequest());
assertThat(map).hasSize(3);
assertThat((Integer) map.get(getPageParameter())).isEqualTo(PAGE);
assertThat((Integer) map.get(getSizeParameter())).isEqualTo(SIZE);
assertThat((List<?>) map.get(getSortParameter())).hasSize(2);
}
private Pageable createPageAndSortRequest() {
return PageRequest.of(PAGE, SIZE, Sort.Direction.ASC, SORT_1, SORT_2);
}
@Test
public void testPaginationRequest() {
QueryMapEncoder encoder = this.context.getInstance("foo", QueryMapEncoder.class);
assertThat(encoder).isNotNull();
Map<String, Object> map = encoder.encode(createPageAndRequest());
assertThat(map).hasSize(2);
assertThat((Integer) map.get(getPageParameter())).isEqualTo(PAGE);
assertThat((Integer) map.get(getSizeParameter())).isEqualTo(SIZE);
assertThat(map).doesNotContainKey(getSortParameter());
}
private Pageable createPageAndRequest() {
return PageRequest.of(PAGE, SIZE);
}
@Test
public void testSortingRequest() {
QueryMapEncoder encoder = this.context.getInstance("foo", QueryMapEncoder.class);
assertThat(encoder).isNotNull();
Map<String, Object> map = encoder.encode(createSort());
assertThat(map).hasSize(1);
assertThat((List<?>) map.get(getSortParameter())).hasSize(2);
}
private Sort createSort() {
return Sort.by(SORT_1, SORT_2).ascending();
}
@Test
public void testUnpagedRequest() {
QueryMapEncoder encoder = this.context.getInstance("foo", QueryMapEncoder.class);
assertThat(encoder).isNotNull();
Map<String, Object> map = encoder.encode(Pageable.unpaged());
assertThat(map).isEmpty();
}
}
@@ -0,0 +1,52 @@
/*
* Copyright 2013-2020 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.openfeign.support;
import org.springframework.boot.autoconfigure.data.web.SpringDataWebProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.boot.test.context.SpringBootTest;
import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
/**
* Tests the pagination encoding and sorting.
*
* @author Yanming Zhou
*/
@EnableConfigurationProperties(SpringDataWebProperties.class)
@SpringBootTest(classes = SpringEncoderTests.Application.class, webEnvironment = RANDOM_PORT,
value = { "spring.application.name=springencodertest", "spring.jmx.enabled=false",
"spring.data.web.pageable.pageParameter=pageNo", "spring.data.web.pageable.sizeParameter=pageSize",
"spring.data.web.sort.sortParameter=orderBy" })
public class PageableSpringQueryMapEncoderWithSpringDataWebTests extends PageableSpringQueryMapEncoderTests {
@Override
protected String getPageParameter() {
return "pageNo";
}
@Override
protected String getSizeParameter() {
return "pageSize";
}
@Override
protected String getSortParameter() {
return "orderBy";
}
}
@@ -60,6 +60,7 @@ 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;
@@ -68,6 +69,7 @@ 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,
@@ -179,6 +181,17 @@ 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);
@@ -45,11 +45,13 @@ import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.util.MultiValueMap;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.bind.annotation.CookieValue;
import org.springframework.web.bind.annotation.ExceptionHandler;
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;
@@ -60,8 +62,10 @@ import org.springframework.web.multipart.MultipartFile;
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.ANY;
import static com.fasterxml.jackson.annotation.JsonAutoDetect.Visibility.NONE;
import static feign.CollectionFormat.CSV;
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;
/**
@@ -71,6 +75,7 @@ import static org.junit.Assume.assumeTrue;
* @author Aaron Whiteside
* @author Artyom Romanenko
* @author Olga Maciaszek-Sharma
* @author Sam Kruglov
*/
public class SpringMvcContractTests {
@@ -129,6 +134,16 @@ public class SpringMvcContractTests {
assertThat(data.template().decodeSlash()).isTrue();
}
@Test
public void testProcessAnnotationOnMethod_Simple_RegexPathVariable() throws Exception {
Method method = TestTemplate_Simple.class.getDeclaredMethod("getTestWithDigitalId", String.class);
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
assertThat(data.template().url()).isEqualTo("/test/{id:\\d+}");
assertThat(data.template().method()).isEqualTo("GET");
assertThat(data.formParams()).isEmpty();
}
@Test
public void testProcessAnnotationOnMethod_Simple_SlashEncoded() throws Exception {
contract = new SpringMvcContract(Collections.emptyList(), getConversionService(), false);
@@ -168,16 +183,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 +196,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 +219,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 +249,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);
@@ -290,13 +273,22 @@ public class SpringMvcContractTests {
assertThat(data.template().collectionFormat()).isEqualTo(SSV);
}
@Test
public void processAnnotationOnClass_CollectionFormat() throws NoSuchMethodException {
Method method = TestTemplate_Advanced.class.getDeclaredMethod("getWithoutCollectionFormat");
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
assertThat(data.template().collectionFormat()).isEqualTo(CSV);
}
@Test
public void testProcessAnnotations_Advanced() throws Exception {
Method method = TestTemplate_Advanced.class.getDeclaredMethod("getTest", String.class, String.class,
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 +307,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 +359,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 +451,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);
@@ -576,6 +568,24 @@ public class SpringMvcContractTests {
assertThat(data.formParams()).contains("file", "id");
}
@Test
public void testSingleCookieAnnotation() throws NoSuchMethodException {
Method method = TestTemplate_Cookies.class.getDeclaredMethod("singleCookie", String.class, String.class);
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
assertThat(data.template().headers().get("cookie").iterator().next()).isEqualTo("cookie1={cookie1}");
}
@Test
public void testMultipleCookiesAnnotation() throws NoSuchMethodException {
Method method = TestTemplate_Cookies.class.getDeclaredMethod("multipleCookies", String.class, String.class,
String.class);
MethodMetadata data = contract.parseAndValidateMetadata(method.getDeclaringClass(), method);
assertThat(data.template().headers().get("cookie").iterator().next())
.isEqualTo("cookie1={cookie1}; cookie2={cookie2}");
}
private ConversionService getConversionService() {
FormattingConversionServiceFactoryBean conversionServiceFactoryBean = new FormattingConversionServiceFactoryBean();
conversionServiceFactoryBean.afterPropertiesSet();
@@ -587,13 +597,16 @@ 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("/test/{id:\\d+}")
ResponseEntity<TestObject> getTestWithDigitalId(@PathVariable("id") String id);
@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 +615,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 +638,57 @@ 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_Cookies {
@GetMapping("/test/{id}")
ResponseEntity<TestObject> singleCookie(@PathVariable("id") String id, @CookieValue("cookie1") String cookie1);
@GetMapping("/test/{id}")
ResponseEntity<TestObject> multipleCookies(@PathVariable("id") String id,
@CookieValue("cookie1") String cookie1, @CookieValue("cookie2") String cookie2);
}
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,64 +696,65 @@ 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")
@CollectionFormat(CSV)
public interface TestTemplate_Advanced {
@CollectionFormat(SSV)
@GetMapping
ResponseEntity<TestObject> getWithCollectionFormat();
@GetMapping
ResponseEntity<TestObject> getWithoutCollectionFormat();
@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 +766,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 +776,7 @@ public class SpringMvcContractTests {
String CUSTOM_PATTERN = "$###,###.###";
@RequestMapping(method = RequestMethod.GET)
@GetMapping
String getTest(@RequestParam("amount") @NumberFormat(pattern = CUSTOM_PATTERN) BigDecimal amount);
}
@@ -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();
}
@@ -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);
}
@@ -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();
@@ -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) {
@@ -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) {
@@ -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 {
@@ -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";
}
@@ -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";
}
@@ -0,0 +1,10 @@
# This configuration used by test class FeignClientWithRefreshableOptionsTest
logging.level.org.springframework.cloud.openfeign=debug
feign.client.default-to-properties=true
feign.client.default-config=default
feign.client.refresh-enabled=true
feign.client.config.default.connectTimeout=5000
feign.client.config.default.readTimeout=5000
feign.client.config.default.loggerLevel=full
feign.client.config.connectTimeout.connectTimeout=2000
feign.client.config.readTimeout.readTimeout=2000
+3 -3
View File
@@ -6,16 +6,16 @@
<parent>
<artifactId>spring-cloud-dependencies-parent</artifactId>
<groupId>org.springframework.cloud</groupId>
<version>3.0.2</version>
<version>3.1.0-M3</version>
<relativePath/>
</parent>
<artifactId>spring-cloud-openfeign-dependencies</artifactId>
<version>3.0.2</version>
<version>3.1.0-M4</version>
<packaging>pom</packaging>
<name>spring-cloud-openfeign-dependencies</name>
<description>Spring Cloud OpenFeign Dependencies</description>
<properties>
<feign.version>10.10.1</feign.version>
<feign.version>11.6</feign.version>
<feign-form.version>3.8.0</feign-form.version>
<spring-security-oauth2-autoconfigure.version>2.1.2.RELEASE</spring-security-oauth2-autoconfigure.version>
</properties>
+1 -1
View File
@@ -5,7 +5,7 @@
<parent>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign</artifactId>
<version>3.0.2</version>
<version>3.1.0-M4</version>
<relativePath>..</relativePath>
</parent>
<artifactId>spring-cloud-starter-openfeign</artifactId>