Compare commits

...
118 Commits
Author SHA1 Message Date
buildmaster 6f4179ed91 Bumping versions to 2.1.6.BUILD-SNAPSHOT after release 2020-01-31 21:53:12 +00:00
buildmaster 553c563836 Going back to snapshots 2020-01-31 21:53:12 +00:00
buildmaster 01f4808035 Update SNAPSHOT to 2.1.5.RELEASE 2020-01-31 21:52:25 +00:00
Ryan Baxter a85294a835 Add documentation about early initialization errors. Fixes #221 2020-01-29 10:57:58 -05:00
Ryan Baxter 73a75fd8b2 Add appendix.adoc. Fixes #255 2020-01-29 10:38:13 -05:00
Olga Maciaszek-SharmaandAhmad Mozafarnia 80d2067134 Merge fix 255 to 21x (#273)
* Fix bug in SpringEncoder (#225)

This commit fixes a bug in the SpringEncoder that causes the bodyType to
be ignored and not passed to generic message converters.

Fixes gh-225

* Fix after cherry-pick.

* Add Ahmad Mozafarnia to authors.

* Refactor tests.

Co-authored-by: Ahmad Mozafarnia <ahmad.mozafarnia@gmail.com>
2020-01-09 17:59:15 +01:00
buildmaster f97cbb9a73 Bumping versions to 2.1.5.BUILD-SNAPSHOT after release 2019-11-18 15:11:21 +00:00
buildmaster de39e67e18 Going back to snapshots 2019-11-18 15:11:21 +00:00
buildmaster 78037a1161 Update SNAPSHOT to 2.1.4.RELEASE 2019-11-18 15:10:50 +00:00
Ryan Baxter f01c46ab2e Use the context id if present in the HystrixTargeter (#247)
* Use the context id if present in the HystrixTargeter.  Fixes #173

* Remove resource class from circleci
2019-11-13 10:41:57 -05:00
Ryan Baxter eecc51e695 Upgrade to OpenFeign 10.4.0. Fixes #220 2019-09-17 17:18:05 -04:00
Olga Maciaszek-Sharma c3fc93268c Remove spring.provides. 2019-09-17 15:47:39 +02:00
buildmaster 4e6c1b2d04 Bumping versions to 2.1.4.BUILD-SNAPSHOT after release 2019-09-09 19:16:42 +00:00
buildmaster d8cdade30f Going back to snapshots 2019-09-09 19:16:42 +00:00
buildmaster b16904f15e Update SNAPSHOT to 2.1.3.RELEASE 2019-09-09 19:15:23 +00:00
Olga Maciaszek-Sharma 96ae8479aa Remove unused jersey dependency. Add dependency version for okhttp. 2019-08-30 16:34:25 +02:00
Glosur 2cdc1aceeb Fix serialization of Pageable.unpaged 2019-07-29 16:49:19 +02:00
Marcin Grzejszczak 9cf06153a8 Added symbolic link of index.adoc 2019-07-29 14:43:13 +02:00
Ryan Baxter 2c73498398 Adding feign-mock to our dependency management 2019-07-24 11:11:25 -04:00
Spencer Gibb 27380898a8 Updates to 2.1.3.BUILD-SNAPSHOT and build to 2.1.7 2019-06-21 12:26:25 -04:00
buildmaster d5031f8618 Bumping versions to 2.1.4.BUILD-SNAPSHOT after release 2019-06-20 17:08:40 +00:00
buildmaster b5539198e6 Going back to snapshots 2019-06-20 17:08:40 +00:00
buildmaster 36d63a1b9f Update SNAPSHOT to 2.1.2.RELEASE 2019-06-20 17:07:59 +00:00
buildmaster c4284683ad Bumping versions to 2.1.3.BUILD-SNAPSHOT after release 2019-06-11 19:20:31 +00:00
buildmaster 21805a9989 Going back to snapshots 2019-06-11 19:20:30 +00:00
buildmaster b1a8917796 Update SNAPSHOT to 2.1.2.RELEASE 2019-06-11 19:19:38 +00:00
Olga Maciaszek-Sharma 15be7111ec Upgrade openfeign to 10.2.3 and feign-form to 3.8.0. (#185)
Fixes gh-143.
2019-05-17 10:53:05 +02:00
Olga Maciaszek-Sharma 35ec2913d9 Fix handling adding multiple parametrs for key. Fixes gh-171. (#172) 2019-04-02 17:55:20 +02:00
Ryan Baxter e04056be09 Merge branch 'spring-operator-polish-urls-remaining-2.1.x' into 2.1.x 2019-03-28 15:57:43 -04:00
Ryan Baxter 05893e57e7 Fixing test failures 2019-03-28 15:57:28 -04:00
Ryan Baxter 8e0fc92c48 Merge branch 'polish-urls-remaining-2.1.x' of https://github.com/spring-operator/spring-cloud-openfeign into spring-operator-polish-urls-remaining-2.1.x 2019-03-28 14:04:30 -04:00
Spring Operator 09f9382a51 URL Cleanup (#163)
This commit updates URLs to prefer the https protocol. Redirects are not followed to avoid accidentally expanding intentionally shortened URLs (i.e. if using a URL shortener).

# Fixed URLs

## Fixed But Review Recommended
These URLs were fixed, but the https status was not OK. However, the https status was the same as the http request or http redirected to an https URL, so they were migrated. Your review is recommended.

* [ ] http://www.puppycrawl.com/dtds/suppressions_1_1.dtd (404) with 1 occurrences migrated to:
  https://www.puppycrawl.com/dtds/suppressions_1_1.dtd ([https](https://www.puppycrawl.com/dtds/suppressions_1_1.dtd) result 404).

## Fixed Success
These URLs were switched to an https URL with a 2xx status. While the status was successful, your review is still recommended.

* [ ] http://maven.apache.org/xsd/maven-4.0.0.xsd with 5 occurrences migrated to:
  https://maven.apache.org/xsd/maven-4.0.0.xsd ([https](https://maven.apache.org/xsd/maven-4.0.0.xsd) result 200).

# Ignored
These URLs were intentionally ignored.

* http://maven.apache.org/POM/4.0.0 with 10 occurrences
* http://www.w3.org/2001/XMLSchema-instance with 5 occurrences
2019-03-28 12:52:33 -04:00
Spring Operator 1347724cdf URL Cleanup
This commit updates URLs to prefer the https protocol. Redirects are not followed to avoid accidentally expanding intentionally shortened URLs (i.e. if using a URL shortener).

# Fixed URLs

## Fixed But Review Recommended
These URLs were fixed, but the https status was not OK. However, the https status was the same as the http request or http redirected to an https URL, so they were migrated. Your review is recommended.

* [ ] http://foo.com:8000/ (ConnectTimeoutException) with 2 occurrences migrated to:
  https://foo.com:8000/ ([https](https://foo.com:8000/) result ConnectTimeoutException).
* [ ] http://foo.com:8000/abc/bcd.json (ConnectTimeoutException) with 1 occurrences migrated to:
  https://foo.com:8000/abc/bcd.json ([https](https://foo.com:8000/abc/bcd.json) result ConnectTimeoutException).
* [ ] http://compose.docker.io/ (UnknownHostException) with 1 occurrences migrated to:
  https://compose.docker.io/ ([https](https://compose.docker.io/) result UnknownHostException).
* [ ] http://tp/abc/bcd.json (UnknownHostException) with 1 occurrences migrated to:
  https://tp/abc/bcd.json ([https](https://tp/abc/bcd.json) result UnknownHostException).

## Fixed Success
These URLs were switched to an https URL with a 2xx status. While the status was successful, your review is still recommended.

* [ ] http://EditorConfig.org with 1 occurrences migrated to:
  https://EditorConfig.org ([https](https://EditorConfig.org) result 200).
* [ ] http://asciidoctor.org with 1 occurrences migrated to:
  https://asciidoctor.org ([https](https://asciidoctor.org) result 200).
* [ ] http://example.com with 2 occurrences migrated to:
  https://example.com ([https](https://example.com) result 200).
* [ ] http://github.com/ with 1 occurrences migrated to:
  https://github.com/ ([https](https://github.com/) result 200).
* [ ] http://projects.spring.io/spring-cloud/ with 1 occurrences migrated to:
  https://projects.spring.io/spring-cloud/ ([https](https://projects.spring.io/spring-cloud/) result 200).
* [ ] http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html with 2 occurrences migrated to:
  https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html ([https](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html) result 200).
* [ ] http://plugins.jetbrains.com/plugin/6546 with 2 occurrences migrated to:
  https://plugins.jetbrains.com/plugin/6546 ([https](https://plugins.jetbrains.com/plugin/6546) result 301).
* [ ] http://raw.github.com/ with 1 occurrences migrated to:
  https://raw.github.com/ ([https](https://raw.github.com/) result 301).
* [ ] http://service.com with 1 occurrences migrated to:
  https://service.com ([https](https://service.com) result 301).
* [ ] http://eclipse.org with 1 occurrences migrated to:
  https://eclipse.org ([https](https://eclipse.org) result 302).
* [ ] http://eclipse.org/m2e/ with 2 occurrences migrated to:
  https://eclipse.org/m2e/ ([https](https://eclipse.org/m2e/) result 302).
* [ ] http://www.springsource.com/developer/sts with 1 occurrences migrated to:
  https://www.springsource.com/developer/sts ([https](https://www.springsource.com/developer/sts) result 302).

# Ignored
These URLs were intentionally ignored.

* http://PROD-SVC with 2 occurrences
* http://Url/ with 1 occurrences
* http://bad_hostname with 1 occurrences
* http://bar with 1 occurrences
* http://bar/ with 1 occurrences
* http://foo with 19 occurrences
* http://foo/ with 4 occurrences
* http://foo/?name=%7bcookie with 1 occurrences
* http://foo:7777/ with 1 occurrences
* http://good-name with 2 occurrences
* http://listener with 6 occurrences
* http://localhost with 11 occurrences
* http://localhost:8080/ with 1 occurrences
* http://localhost:8081/ with 1 occurrences
2019-03-26 03:45:19 -05:00
Ryan Baxter ac45facf04 Updating dependencies 2019-03-25 19:22:39 -04:00
Spring Operator 5a54a612cf URL Cleanup (#158)
This commit updates URLs to prefer the https protocol. Redirects are not followed to avoid accidentally expanding intentionally shortened URLs (i.e. if using a URL shortener).

# Fixed URLs

## Fixed Success
These URLs were switched to an https URL with a 2xx status. While the status was successful, your review is still recommended.

* [ ] http://www.apache.org/licenses/ with 1 occurrences migrated to:
  https://www.apache.org/licenses/ ([https](https://www.apache.org/licenses/) result 200).
* [ ] http://www.apache.org/licenses/LICENSE-2.0 with 114 occurrences migrated to:
  https://www.apache.org/licenses/LICENSE-2.0 ([https](https://www.apache.org/licenses/LICENSE-2.0) result 200).
2019-03-25 09:32:27 -04:00
Marcin Grzejszczak 2633db6bde Polish 2019-03-07 18:23:08 +01:00
Marcin Grzejszczak 2c824ff39c HTTP to HTTPS 2019-03-07 13:52:20 +01:00
buildmaster 6f9e1f63dc Bumping versions to 2.1.2.BUILD-SNAPSHOT after release 2019-03-05 17:48:02 +00:00
buildmaster 7a6c0a42f6 Going back to snapshots 2019-03-05 17:48:02 +00:00
buildmaster d73c61f0f0 Update SNAPSHOT to 2.1.1.RELEASE 2019-03-05 17:47:11 +00:00
Spencer Gibb d01bcdf29c Adds support for Iterable but not Collection parameters.
This was removed as part of #48.

fixes gh-135
2019-03-04 17:48:23 -05:00
Marcin Grzejszczak 47f2850751 ConditionalOnClass on a method should not use a class 2019-02-27 14:48:33 +01:00
Ryan Baxter 702e1c7340 Merge branch 'Tcharl-1604' 2019-02-26 20:24:33 -05:00
Ryan Baxter 4ac6429152 Polishing checkstyle 2019-02-26 20:24:00 -05:00
charliemordant 6e0e63644b checkstyle rules applied to pageable support 2019-02-17 12:43:30 +01:00
charliemordant f8352de786 merge conflicts 2019-02-10 18:16:29 +01:00
Marcin Grzejszczak c8c7452848 Added checkstyle 2019-02-05 18:46:31 +01:00
Olga Maciaszek-Sharma 1f37891ffe Fixes gh-107 (#113)
* Fixes gh-107

* Remove unnecessary assignment.
2019-01-29 20:15:32 +01:00
charliemordant 505a83e6a4 registers PageJacksonModule in FeignClientConfiguration 2019-01-28 13:05:26 +01:00
charliemordant 76dbc7d21f Merge branch 'master' of github.com:spring-cloud/spring-cloud-openfeign into 1604 2019-01-28 09:29:26 +01:00
buildmaster b176e3a958 Bumping versions 2019-01-25 13:32:33 +00:00
buildmaster eb7e4d3082 Bumping versions to 2.1.1.BUILD-SNAPSHOT after release 2019-01-22 18:34:21 +00:00
buildmaster c76b02b8a3 Going back to snapshots 2019-01-22 18:34:20 +00:00
buildmaster c1418bafee Update SNAPSHOT to 2.1.0.RELEASE 2019-01-22 18:33:41 +00:00
Frank Pavageau 67fa03dcd9 Upgrade feign-form to 3.5.0 (#106)
The earlier versions are not compatible with Feign 10.1.0 when using the
form encoder.

Fixes #105
2019-01-18 16:09:46 -05:00
Spencer Gibb 5804d12511 Bumps spring cloud build to 2.1.2.BUILD-SNAPSHOT 2019-01-12 13:05:02 -05:00
charliemordant 23ec2a1c43 License headers 2019-01-07 17:20:44 +01:00
Ryan Baxter d148672e13 Merge remote-tracking branch 'origin/2.0.x' 2019-01-07 09:51:04 -05:00
Ryan Baxter a693c30930 Add null check for retry policy. Fixes #101 2019-01-07 09:49:36 -05:00
Piotr Smolarski e227808b9d Support Multiple Clients Using The Same Service (#90)
* Use serviceId as url target if present.

Fixes gh-67

* Code review fixes and improvements

* Fix test

* Add contextId to override bean name of feign client and its configuration.

* Add documentation

* Add contextId example to documentation
2019-01-07 09:35:48 -05:00
charliemordant 642d3e6961 Spring data pageable support 2018-12-30 01:47:55 +01:00
buildmaster 3051c4c1fd Going back to snapshots 2018-12-20 21:12:44 +00:00
buildmaster fb6676d634 Update SNAPSHOT to 2.1.0.RC3 2018-12-20 21:11:11 +00:00
Ryan Baxter c1bbb9cbb3 Updating API usage for Boot 2.1.x. 2018-12-18 16:18:06 -05:00
Spencer Gibb e65f78f06b Merge branch '2.0.x' 2018-12-18 14:49:49 -05:00
Ryan Baxter 4e7e187323 Fix bug where host contains protocol characters (#97)
Fixes #94
2018-12-18 14:48:31 -05:00
Olga Maciaszek-Sharma a1a45125ea Upgrade openfeign to 10.1.0 (#95)
Upgrade to OpenFeign 10.1.0
2018-12-13 18:03:35 +01:00
Ryan Baxter b00abe7b68 Fixing docs 2018-12-12 12:22:46 -05:00
buildmaster b9cb792820 Going back to snapshots 2018-12-11 21:52:28 +00:00
buildmaster a38ebe40c8 Update SNAPSHOT to 2.1.0.RC2 2018-12-11 21:51:44 +00:00
buildmaster a064530141 Going back to snapshots 2018-12-11 16:38:12 +00:00
buildmaster 61afb5dcd6 Update SNAPSHOT to 2.1.0.RC1 2018-12-11 16:37:40 +00:00
Ryan Baxter 20017afdca Merge remote-tracking branch 'origin/2.0.x' 2018-12-11 10:04:16 -05:00
wzslw 88a6682f46 Update pom.xml (#88)
Remove duplicate dependency
2018-12-11 10:02:58 -05:00
Ryan Baxter f267c2255b Merge remote-tracking branch 'origin/2.0.x' 2018-12-11 09:12:30 -05:00
Ryan Baxter dd7b869a12 Uncommenting assert 2018-12-10 12:30:03 -05:00
Momo d46fb3a92e Add support for feign's QueryMap annotation for Object mapping (#79)
* Add QueryMapParameterProcessor

* Add license to QueryMapParameterProcessor

* Use SpringQueryMap instead of QueryMap. Add test.

* Add documentation and license to SpringQueryMap

* SpringQueryMap docs

* Fix typos
2018-12-06 21:26:36 +01:00
buildmaster cc2fe823c3 Going back to snapshots 2018-11-18 09:42:08 +00:00
buildmaster f1975e5200 Update SNAPSHOT to 2.1.0.M2 2018-11-18 09:41:37 +00:00
buildmaster 5ddd771ed5 Bumping versions to 2.0.3.BUILD-SNAPSHOT after release 2018-10-23 18:11:07 +00:00
buildmaster 9f9c41ab59 Going back to snapshots 2018-10-23 18:11:06 +00:00
buildmaster 4dd68ca2f2 Update SNAPSHOT to 2.0.2.RELEASE 2018-10-23 18:08:54 +00:00
Ryan Baxter af094d0bfc Remove rogue content in docs 2018-10-02 16:41:14 -04:00
Ryan Baxter e85e74e311 Merge remote-tracking branch 'origin/2.0.x' 2018-10-02 13:08:37 -04:00
Ryan Baxter 77cdd40f7d Add missing properties to additional-spring-configuration-metadata. Fixes #61 2018-10-02 13:07:19 -04:00
buildmaster 71675455a3 Going back to snapshots 2018-09-21 21:40:58 +00:00
buildmaster a39e6316b5 Update SNAPSHOT to Greenwich.M1 2018-09-21 21:40:12 +00:00
sdoeringNew 750cb35e6a Add builder for feign clients. (#56)
Utilizes the logic from the registrar and the factory bean. The changes to them have been made minimal invasive.

Fixes gh-42
2018-09-19 09:37:51 -04:00
Kerwin Bryant b512c451fd optimized automatic configuration (#70) 2018-09-19 09:35:01 -04:00
Ryan Baxter da6a51778f Merge branch '2.0.x' of github.com:spring-cloud/spring-cloud-openfeign into 2.0.x 2018-09-12 15:00:42 -04:00
Ryan Baxter 9fc19bb6fe Updating dependencies. Fixes #68 2018-09-12 14:56:33 -04:00
Kerwin Bryant 3e3e486648 Support to set loadBalancerKey (#50)
* Support to override CachingSpringLoadBalancerFactory to customize the FeignLoadBalancer instance.
2018-09-12 14:38:15 -04:00
Spencer Gibb 15bda96e91 Merge branch '2.0.x' 2018-09-05 16:09:38 -04:00
Eugene e0bf63bed1 Added support for MultipartFile (#66)
Fixes gh-62

### Notes
The solution is based on
```
        <dependency>
            <groupId>io.github.openfeign.form</groupId>
            <artifactId>feign-form-spring</artifactId>
            <version>3.3.0</version>
        </dependency>
```

Class `feign.form.spring.SpringFormEncoder` was added inside `org.springframework.cloud.openfeign.support.SpringEncoder` because it's doesn't has some checks:
1. Check of  `bodyType` on null.
`org.springframework.cloud.openfeign.support.SpringEncoder#encode` don't handle situation when `bodyType` is null.

2. No check for content type `multipart/form-data` in header. 
`feign.form.FormEncoder` requires `multipart/form-data` in header for proper processing.

All this checks added and tests are passed.
2018-09-05 16:07:56 -04:00
Nicklas Holm a33fd6d869 Add spring boot configuration processor. Fixes #63 (#64) 2018-09-05 10:01:53 -04:00
Ryan Baxter 06ccb9d1e5 Remove code which was calling FallbackFactory with RuntimeException. Fixes #41 (#58) 2018-08-17 10:23:37 -04:00
Halvdan Hoem Grelland 52bea35160 Support Spring formatting annotations for params (#48)
* Add factory for Param.Expander using ConversionService. Instances use ConversionService and passes annotations (through TypeDescriptor) - ConversionService can now pick up @DateTimeFormat and @NumberFormat and convert the params applying those.
2018-08-15 07:19:24 -04:00
Ryan Baxter d57cea265e Fix invalid URI error when Feign request URI does not end in /. See sc-netflix #3136 2018-08-13 14:26:24 -04:00
Spencer Gibb 7148cce790 Upgrades to openfeign 9.7.0
fixes gh-53
2018-08-06 11:52:45 -04:00
Spencer Gibb 88fae41f02 Updates to work with boot 2.1.0 2018-08-03 15:04:17 -04:00
buildmaster 34cb91bc4c Bumping versions to 2.0.2.BUILD-SNAPSHOT after release 2018-07-31 19:06:45 +00:00
buildmaster 58dc45d230 Going back to snapshots 2018-07-31 19:06:45 +00:00
buildmaster 80af701ccf Update SNAPSHOT to 2.0.1.RELEASE 2018-07-31 19:04:57 +00:00
Ryan Baxter 46bc5da424 Make netflix starters optional. Fixes #35 (#36) 2018-07-25 20:11:00 -04:00
Ryan Baxter 7dcb696ec4 Use OK Http client bean instead of instance variable. Fixes #46 2018-07-24 09:44:45 -04:00
Ryan Baxter 69461d4385 Adding spring boot autoconfigure processor. Fixes #43. 2018-07-09 09:31:35 -04:00
Matt Benson 8364b16b8e expose configuration metadata for property 'feign.hystrix.enabled' (#40) 2018-06-28 07:28:32 -04:00
Ian Hu 6d4686624e Fix typo in comments (#38) 2018-06-26 07:10:51 -04:00
buildmaster 51ad63231d Bumping versions to 2.0.1.BUILD-SNAPSHOT after release 2018-06-18 20:33:23 +00:00
buildmaster 6c09501d77 Going back to snapshots 2018-06-18 20:33:22 +00:00
buildmaster 7e1c91c8b8 Update SNAPSHOT to 2.0.0.RELEASE 2018-06-18 20:31:53 +00:00
Spencer Gibb 674b88b467 Updates to s-c-build 2.0.2.BUILD-SNAPSHOT 2018-06-05 15:42:52 -04:00
Fernando F D Bittencourt a57c416d4a Explaining the return in case of exception. (#32) 2018-06-01 16:44:09 -04:00
Karl Nicholas ffd4290e06 Update package to correct package in two test files. (#30) 2018-05-31 15:58:09 -04:00
Ryan Baxter 804ef91620 Merge branch 'master' of github.com:spring-cloud/spring-cloud-openfeign 2018-05-31 07:54:28 -04:00
Ryan Baxter 48e2f70757 Updating sc-build to 2.0.2.BUILD-SNAPSHOT 2018-05-31 07:54:04 -04:00
Rafael Renan Pacheco 569628689b Format "Creating Feign Clients Manually" snippet (#29) 2018-05-29 11:09:05 -04:00
buildmaster af02a8c692 Going back to snapshots 2018-05-24 23:56:03 +00:00
133 changed files with 8938 additions and 4306 deletions
+1 -2
View File
@@ -9,7 +9,6 @@ jobs:
branches: branches:
ignore: ignore:
- gh-pages # list of branches to ignore - gh-pages # list of branches to ignore
resource_class: large
steps: steps:
- checkout - checkout
- restore_cache: - restore_cache:
@@ -36,4 +35,4 @@ jobs:
destination: artifacts destination: artifacts
- store_test_results: - store_test_results:
path: /junit/ path: /junit/
destination: testartifacts destination: testartifacts
+14
View File
@@ -0,0 +1,14 @@
# EditorConfig is awesome: https://EditorConfig.org
# top-most EditorConfig file
root = true
[*]
indent_style = tab
indent_size = 4
end_of_line = lf
insert_final_newline = true
[*.yml]
indent_style = space
indent_size = 2
+2 -2
View File
@@ -28,7 +28,7 @@ added after the original pull request but before a merge.
you can import formatter settings using the you can import formatter settings using the
`eclipse-code-formatter.xml` file from the `eclipse-code-formatter.xml` file from the
[Spring Cloud Build](https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-dependencies-parent/eclipse-code-formatter.xml) project. If using IntelliJ, you can use the [Spring Cloud Build](https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-dependencies-parent/eclipse-code-formatter.xml) project. If using IntelliJ, you can use the
[Eclipse Code Formatter Plugin](http://plugins.jetbrains.com/plugin/6546) to import the same file. [Eclipse Code Formatter Plugin](https://plugins.jetbrains.com/plugin/6546) to import the same file.
* Make sure all new `.java` files to have a simple Javadoc class comment with at least an * Make sure all new `.java` files to have a simple Javadoc class comment with at least an
`@author` tag identifying you, and preferably at least a paragraph on what the class is `@author` tag identifying you, and preferably at least a paragraph on what the class is
for. for.
@@ -40,6 +40,6 @@ added after the original pull request but before a merge.
* A few unit tests would help a lot as well -- someone has to do it. * A few unit tests would help a lot as well -- someone has to do it.
* If no-one else is using your branch, please rebase it against the current master (or * If no-one else is using your branch, please rebase it against the current master (or
other target branch in the main project). other target branch in the main project).
* When writing a commit message please follow [these conventions](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html), * When writing a commit message please follow [these conventions](https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html),
if you are fixing an existing issue please add `Fixes gh-XXXX` at the end of the commit if you are fixing an existing issue please add `Fixes gh-XXXX` at the end of the commit
message (where XXXX is the issue number). message (where XXXX is the issue number).
+65 -63
View File
@@ -1,66 +1,68 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<settings> <settings>
<servers> <servers>
<server> <server>
<id>repo.spring.io</id> <id>repo.spring.io</id>
<username>${env.CI_DEPLOY_USERNAME}</username> <username>${env.CI_DEPLOY_USERNAME}</username>
<password>${env.CI_DEPLOY_PASSWORD}</password> <password>${env.CI_DEPLOY_PASSWORD}</password>
</server> </server>
</servers> </servers>
<profiles> <profiles>
<profile> <profile>
<!-- <!--
N.B. this profile is only here to support users and IDEs that do not use Maven 3.3. N.B. this profile is only here to support users and IDEs that do not use Maven 3.3.
It isn't needed on the command line if you use the wrapper script (mvnw) or if you use It isn't needed on the command line if you use the wrapper script (mvnw) or if you use
a native Maven with the right version. Eclipse users should points their Maven tooling to a native Maven with the right version. Eclipse users should points their Maven tooling to
this settings file, or copy the profile into their ~/.m2/settings.xml. this settings file, or copy the profile into their ~/.m2/settings.xml.
--> -->
<id>spring</id> <id>spring</id>
<activation><activeByDefault>true</activeByDefault></activation> <activation>
<repositories> <activeByDefault>true</activeByDefault>
<repository> </activation>
<id>spring-snapshots</id> <repositories>
<name>Spring Snapshots</name> <repository>
<url>http://repo.spring.io/libs-snapshot-local</url> <id>spring-snapshots</id>
<snapshots> <name>Spring Snapshots</name>
<enabled>true</enabled> <url>https://repo.spring.io/libs-snapshot-local</url>
</snapshots> <snapshots>
</repository> <enabled>true</enabled>
<repository> </snapshots>
<id>spring-milestones</id> </repository>
<name>Spring Milestones</name> <repository>
<url>http://repo.spring.io/libs-milestone-local</url> <id>spring-milestones</id>
<snapshots> <name>Spring Milestones</name>
<enabled>false</enabled> <url>https://repo.spring.io/libs-milestone-local</url>
</snapshots> <snapshots>
</repository> <enabled>false</enabled>
<repository> </snapshots>
<id>spring-releases</id> </repository>
<name>Spring Releases</name> <repository>
<url>http://repo.spring.io/release</url> <id>spring-releases</id>
<snapshots> <name>Spring Releases</name>
<enabled>false</enabled> <url>https://repo.spring.io/release</url>
</snapshots> <snapshots>
</repository> <enabled>false</enabled>
</repositories> </snapshots>
<pluginRepositories> </repository>
<pluginRepository> </repositories>
<id>spring-snapshots</id> <pluginRepositories>
<name>Spring Snapshots</name> <pluginRepository>
<url>http://repo.spring.io/libs-snapshot-local</url> <id>spring-snapshots</id>
<snapshots> <name>Spring Snapshots</name>
<enabled>true</enabled> <url>https://repo.spring.io/libs-snapshot-local</url>
</snapshots> <snapshots>
</pluginRepository> <enabled>true</enabled>
<pluginRepository> </snapshots>
<id>spring-milestones</id> </pluginRepository>
<name>Spring Milestones</name> <pluginRepository>
<url>http://repo.spring.io/libs-milestone-local</url> <id>spring-milestones</id>
<snapshots> <name>Spring Milestones</name>
<enabled>false</enabled> <url>https://repo.spring.io/libs-milestone-local</url>
</snapshots> <snapshots>
</pluginRepository> <enabled>false</enabled>
</pluginRepositories> </snapshots>
</profile> </pluginRepository>
</profiles> </pluginRepositories>
</profile>
</profiles>
</settings> </settings>
View File
+2 -2
View File
@@ -1,7 +1,7 @@
Apache License Apache License
Version 2.0, January 2004 Version 2.0, January 2004
http://www.apache.org/licenses/ https://www.apache.org/licenses/
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
@@ -193,7 +193,7 @@
you may not use this file except in compliance with the License. you may not use this file except in compliance with the License.
You may obtain a copy of the License at You may obtain a copy of the License at
http://www.apache.org/licenses/LICENSE-2.0 https://www.apache.org/licenses/LICENSE-2.0
Unless required by applicable law or agreed to in writing, software Unless required by applicable law or agreed to in writing, software
distributed under the License is distributed on an "AS IS" BASIS, distributed under the License is distributed on an "AS IS" BASIS,
+148 -9
View File
@@ -1,4 +1,8 @@
// Do not edit this file (e.g. go instead to src/main/asciidoc) ////
DO NOT EDIT THIS FILE. IT WAS GENERATED.
Manual changes to this file will be lost when it is generated again.
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://circleci.com/gh/spring-cloud/spring-cloud-openfeign.svg?style=svg["CircleCI", link="https://circleci.com/gh/spring-cloud/spring-cloud-openfeign"]
@@ -51,7 +55,7 @@ credentials and you already have those.
The projects that require middleware generally include a The projects that require middleware generally include a
`docker-compose.yml`, so consider using `docker-compose.yml`, so consider using
http://compose.docker.io/[Docker Compose] to run the middeware servers https://docs.docker.com/compose/[Docker Compose] to run the middeware servers
in Docker containers. See the README in the in Docker containers. See the README in the
https://github.com/spring-cloud-samples/scripts[scripts demo https://github.com/spring-cloud-samples/scripts[scripts demo
repository] for specific instructions about the common cases of mongo, repository] for specific instructions about the common cases of mongo,
@@ -73,13 +77,13 @@ a modified file in the correct place. Just commit it and push the change.
=== Working with the code === Working with the code
If you don't have an IDE preference we would recommend that you use If you don't have an IDE preference we would recommend that you use
http://www.springsource.com/developer/sts[Spring Tools Suite] or https://www.springsource.com/developer/sts[Spring Tools Suite] or
http://eclipse.org[Eclipse] when working with the code. We use the https://eclipse.org[Eclipse] when working with the code. We use the
http://eclipse.org/m2e/[m2eclipse] eclipse plugin for maven support. Other IDEs and tools https://eclipse.org/m2e/[m2eclipse] eclipse plugin for maven support. Other IDEs and tools
should also work without issue as long as they use Maven 3.3.3 or better. should also work without issue as long as they use Maven 3.3.3 or better.
==== Importing into eclipse with m2eclipse ==== Importing into eclipse with m2eclipse
We recommend the http://eclipse.org/m2e/[m2eclipse] eclipse plugin when working with We recommend the https://eclipse.org/m2e/[m2eclipse] eclipse plugin when working with
eclipse. If you don't already have m2eclipse installed it is available from the "eclipse eclipse. If you don't already have m2eclipse installed it is available from the "eclipse
marketplace". marketplace".
@@ -108,6 +112,8 @@ from the `file` menu.
== Contributing == Contributing
:spring-cloud-build-branch: master
Spring Cloud is released under the non-restrictive Apache 2.0 license, Spring Cloud is released under the non-restrictive Apache 2.0 license,
and follows a very standard Github development process, using Github and follows a very standard Github development process, using Github
tracker for issues and merging pull requests into master. If you want tracker for issues and merging pull requests into master. If you want
@@ -136,7 +142,7 @@ added after the original pull request but before a merge.
`eclipse-code-formatter.xml` file from the `eclipse-code-formatter.xml` file from the
https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-dependencies-parent/eclipse-code-formatter.xml[Spring https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-dependencies-parent/eclipse-code-formatter.xml[Spring
Cloud Build] project. If using IntelliJ, you can use the Cloud Build] project. If using IntelliJ, you can use the
http://plugins.jetbrains.com/plugin/6546[Eclipse Code Formatter https://plugins.jetbrains.com/plugin/6546[Eclipse Code Formatter
Plugin] to import the same file. Plugin] to import the same file.
* Make sure all new `.java` files to have a simple Javadoc class comment with at least an * Make sure all new `.java` files to have a simple Javadoc class comment with at least an
`@author` tag identifying you, and preferably at least a paragraph on what the class is `@author` tag identifying you, and preferably at least a paragraph on what the class is
@@ -149,6 +155,139 @@ added after the original pull request but before a merge.
* A few unit tests would help a lot as well -- someone has to do it. * A few unit tests would help a lot as well -- someone has to do it.
* If no-one else is using your branch, please rebase it against the current master (or * If no-one else is using your branch, please rebase it against the current master (or
other target branch in the main project). other target branch in the main project).
* When writing a commit message please follow http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html[these conventions], * When writing a commit message please follow https://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html[these conventions],
if you are fixing an existing issue please add `Fixes gh-XXXX` at the end of the commit if you are fixing an existing issue please add `Fixes gh-XXXX` at the end of the commit
message (where XXXX is the issue number). message (where XXXX is the issue number).
=== Checkstyle
Spring Cloud Build comes with a set of checkstyle rules. You can find them in the `spring-cloud-build-tools` module. The most notable files under the module are:
.spring-cloud-build-tools/
----
└── src
   ├── checkstyle
   │   └── checkstyle-suppressions.xml <3>
   └── main
   └── resources
   ├── checkstyle-header.txt <2>
   └── checkstyle.xml <1>
----
<1> Default Checkstyle rules
<2> File header setup
<3> Default suppression rules
==== Checkstyle configuration
Checkstyle rules are *disabled by default*. To add checkstyle to your project just define the following properties and plugins.
.pom.xml
----
<properties>
<maven-checkstyle-plugin.failsOnError>true</maven-checkstyle-plugin.failsOnError> <1>
<maven-checkstyle-plugin.failsOnViolation>true
</maven-checkstyle-plugin.failsOnViolation> <2>
<maven-checkstyle-plugin.includeTestSourceDirectory>true
</maven-checkstyle-plugin.includeTestSourceDirectory> <3>
</properties>
<build>
<plugins>
<plugin> <4>
<groupId>io.spring.javaformat</groupId>
<artifactId>spring-javaformat-maven-plugin</artifactId>
</plugin>
<plugin> <5>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
</plugin>
</plugins>
<reporting>
<plugins>
<plugin> <5>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
</plugin>
</plugins>
</reporting>
</build>
----
<1> Fails the build upon Checkstyle errors
<2> Fails the build upon Checkstyle violations
<3> Checkstyle analyzes also the test sources
<4> Add the Spring Java Format plugin that will reformat your code to pass most of the Checkstyle formatting rules
<5> Add checkstyle plugin to your build and reporting phases
If you need to suppress some rules (e.g. line length needs to be longer), then it's enough for you to define a file under `${project.root}/src/checkstyle/checkstyle-suppressions.xml` with your suppressions. Example:
.projectRoot/src/checkstyle/checkstyle-suppresions.xml
----
<?xml version="1.0"?>
<!DOCTYPE suppressions PUBLIC
"-//Puppy Crawl//DTD Suppressions 1.1//EN"
"https://www.puppycrawl.com/dtds/suppressions_1_1.dtd">
<suppressions>
<suppress files=".*ConfigServerApplication\.java" checks="HideUtilityClassConstructor"/>
<suppress files=".*ConfigClientWatch\.java" checks="LineLengthCheck"/>
</suppressions>
----
It's advisable to copy the `${spring-cloud-build.rootFolder}/.editorconfig` and `${spring-cloud-build.rootFolder}/.springformat` to your project. That way, some default formatting rules will be applied. You can do so by running this script:
```bash
$ curl https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/.editorconfig -o .editorconfig
$ touch .springformat
```
=== IDE setup
==== Intellij IDEA
In order to setup Intellij you should import our coding conventions, inspection profiles and set up the checkstyle plugin.
The following files can be found in the https://github.com/spring-cloud/spring-cloud-build/tree/master/spring-cloud-build-tools[Spring Cloud Build] project.
.spring-cloud-build-tools/
----
└── src
   ├── checkstyle
   │   └── checkstyle-suppressions.xml <3>
   └── main
   └── resources
   ├── checkstyle-header.txt <2>
   ├── checkstyle.xml <1>
   └── intellij
      ├── Intellij_Project_Defaults.xml <4>
      └── Intellij_Spring_Boot_Java_Conventions.xml <5>
----
<1> Default Checkstyle rules
<2> File header setup
<3> Default suppression rules
<4> Project defaults for Intellij that apply most of Checkstyle rules
<5> Project style conventions for Intellij that apply most of Checkstyle rules
.Code style
image::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/{spring-cloud-build-branch}/docs/src/main/asciidoc/images/intellij-code-style.png[Code style]
Go to `File` -> `Settings` -> `Editor` -> `Code style`. There click on the icon next to the `Scheme` section. There, click on the `Import Scheme` value and pick the `Intellij IDEA code style XML` option. Import the `spring-cloud-build-tools/src/main/resources/intellij/Intellij_Spring_Boot_Java_Conventions.xml` file.
.Inspection profiles
image::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/{spring-cloud-build-branch}/docs/src/main/asciidoc/images/intellij-inspections.png[Code style]
Go to `File` -> `Settings` -> `Editor` -> `Inspections`. There click on the icon next to the `Profile` section. There, click on the `Import Profile` and import the `spring-cloud-build-tools/src/main/resources/intellij/Intellij_Project_Defaults.xml` file.
.Checkstyle
To have Intellij work with Checkstyle, you have to install the `Checkstyle` plugin. It's advisable to also install the `Assertions2Assertj` to automatically convert the JUnit assertions
image::https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/{spring-cloud-build-branch}/docs/src/main/asciidoc/images/intellij-checkstyle.png[Checkstyle]
Go to `File` -> `Settings` -> `Other settings` -> `Checkstyle`. There click on the `+` icon in the `Configuration file` section. There, you'll have to define where the checkstyle rules should be picked from. In the image above, we've picked the rules from the cloned Spring Cloud Build repository. However, you can point to the Spring Cloud Build's GitHub repository (e.g. for the `checkstyle.xml` : `https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/main/resources/checkstyle.xml`). We need to provide the following variables:
- `checkstyle.header.file` - please point it to the Spring Cloud Build's, `spring-cloud-build-tools/src/main/resources/checkstyle-header.txt` file either in your cloned repo or via the `https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/main/resources/checkstyle-header.txt` URL.
- `checkstyle.suppressions.file` - default suppressions. Please point it to the Spring Cloud Build's, `spring-cloud-build-tools/src/checkstyle/checkstyle-suppressions.xml` file either in your cloned repo or via the `https://raw.githubusercontent.com/spring-cloud/spring-cloud-build/master/spring-cloud-build-tools/src/checkstyle/checkstyle-suppressions.xml` URL.
- `checkstyle.additional.suppressions.file` - this variable corresponds to suppressions in your local project. E.g. you're working on `spring-cloud-contract`. Then point to the `project-root/src/checkstyle/checkstyle-suppressions.xml` folder. Example for `spring-cloud-contract` would be: `/home/username/spring-cloud-contract/src/checkstyle/checkstyle-suppressions.xml`.
IMPORTANT: Remember to set the `Scan Scope` to `All sources` since we apply checkstyle rules for production and test sources.
+2009 -397
View File
File diff suppressed because it is too large Load Diff
+5 -4
View File
@@ -1,11 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.springframework.cloud</groupId> <groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign</artifactId> <artifactId>spring-cloud-openfeign</artifactId>
<version>2.0.0.RC2</version> <version>2.1.6.BUILD-SNAPSHOT</version>
</parent> </parent>
<artifactId>spring-cloud-openfeign-docs</artifactId> <artifactId>spring-cloud-openfeign-docs</artifactId>
<packaging>pom</packaging> <packaging>pom</packaging>
@@ -14,7 +15,7 @@
<properties> <properties>
<docs.main>spring-cloud-openfeign</docs.main> <docs.main>spring-cloud-openfeign</docs.main>
<main.basedir>${basedir}/..</main.basedir> <main.basedir>${basedir}/..</main.basedir>
<docs.whitelisted.branches>2.0.x</docs.whitelisted.branches> <docs.whitelisted.branches>2.0.x,2.1.x</docs.whitelisted.branches>
</properties> </properties>
<build> <build>
<plugins> <plugins>
+14
View File
@@ -0,0 +1,14 @@
:numbered!:
[appendix]
[[common-application-properties]]
== Common application properties
include::_attributes.adoc[]
Various properties can be specified inside your `application.properties` file, inside your `application.yml` file, or as command line switches.
This appendix provides a list of common {project-full-name} properties and references to the underlying classes that consume them.
NOTE: Property contributions can come from additional jar files on your classpath, so you should not consider this an exhaustive list.
Also, you can define your own properties.
include::_configprops.adoc[]
+1
View File
@@ -0,0 +1 @@
spring-cloud-openfeign.adoc
@@ -1,7 +1,7 @@
:github-tag: master :github-tag: master
:github-repo: spring-cloud/spring-cloud-openfeign :github-repo: spring-cloud/spring-cloud-openfeign
:github-raw: http://raw.github.com/{github-repo}/{github-tag} :github-raw: https://raw.github.com/{github-repo}/{github-tag}
:github-code: http://github.com/{github-repo}/tree/{github-tag} :github-code: https://github.com/{github-repo}/tree/{github-tag}
:all: {asterisk}{asterisk} :all: {asterisk}{asterisk}
:nofooter: :nofooter:
:branch: master :branch: master
@@ -21,7 +21,7 @@ https://github.com/Netflix/feign[Feign] is a declarative web service client. It
=== How to Include Feign === How to Include Feign
To include Feign in your project use the starter with group `org.springframework.cloud` To include Feign in your project use the starter with group `org.springframework.cloud`
and artifact id `spring-cloud-starter-openfeign`. See the http://projects.spring.io/spring-cloud/[Spring Cloud Project page] and artifact id `spring-cloud-starter-openfeign`. See the https://projects.spring.io/spring-cloud/[Spring Cloud Project page]
for details on setting up your build system with the current Spring Cloud Release Train. for details on setting up your build system with the current Spring Cloud Release Train.
Example spring boot app Example spring boot app
@@ -73,6 +73,8 @@ in your external configuration (see
A central concept in Spring Cloud's Feign support is that of the named client. Each feign client is part of an ensemble of components that work together to contact a remote server on demand, and the ensemble has a name that you give it as an application developer using the `@FeignClient` annotation. Spring Cloud creates a new ensemble as an A central concept in Spring Cloud's Feign support is that of the named client. Each feign client is part of an ensemble of components that work together to contact a remote server on demand, and the ensemble has a name that you give it as an application developer using the `@FeignClient` annotation. Spring Cloud creates a new ensemble as an
`ApplicationContext` on demand for each named client using `FeignClientsConfiguration`. This contains (amongst other things) an `feign.Decoder`, a `feign.Encoder`, and a `feign.Contract`. `ApplicationContext` on demand for each named client using `FeignClientsConfiguration`. This contains (amongst other things) an `feign.Decoder`, a `feign.Encoder`, and a `feign.Contract`.
It is possible to override the name of that ensemble by using the `contextId`
attribute of the `@FeignClient` annotation.
Spring Cloud lets you take full control of the feign client by declaring additional configuration (on top of the `FeignClientsConfiguration`) using `@FeignClient`. Example: Spring Cloud lets you take full control of the feign client by declaring additional configuration (on top of the `FeignClientsConfiguration`) using `@FeignClient`. Example:
@@ -90,6 +92,10 @@ NOTE: `FooConfiguration` does not need to be annotated with `@Configuration`. Ho
NOTE: The `serviceId` attribute is now deprecated in favor of the `name` attribute. NOTE: The `serviceId` attribute is now deprecated in favor of the `name` attribute.
NOTE: Using `contextId` attribute of the `@FeignClient` annotation in addition to changing the name of
the `ApplicationContext` ensemble, it will override the alias of the client name
and it will be used as part of the name of the configuration bean created for that client.
WARNING: Previously, using the `url` attribute, did not require the `name` attribute. Using `name` is now required. WARNING: Previously, using the `url` attribute, did not require the `name` attribute. Using `name` is now required.
Placeholders are supported in the `name` and `url` attributes. Placeholders are supported in the `name` and `url` attributes.
@@ -206,6 +212,27 @@ hystrix:
strategy: SEMAPHORE strategy: SEMAPHORE
---- ----
If we want to create multiple feign clients with the same name or url
so that they would point to the same server but each with a different custom configuration then
we have to use `contextId` attribute of the `@FeignClient` in order to avoid name
collision of these configuration beans.
[source,java,indent=0]
----
@FeignClient(contextId = "fooClient", name = "stores", configuration = FooConfiguration.class)
public interface FooClient {
//..
}
----
[source,java,indent=0]
----
@FeignClient(contextId = "barClient", name = "stores", configuration = BarConfiguration.class)
public interface BarClient {
//..
}
----
=== Creating Feign Clients Manually === Creating Feign Clients Manually
In some cases it might be necessary to customize your Feign Clients in a way that is not In some cases it might be necessary to customize your Feign Clients in a way that is not
@@ -224,14 +251,14 @@ class FooController {
private FooClient adminClient; private FooClient adminClient;
@Autowired @Autowired
public FooController( public FooController(Decoder decoder, Encoder encoder, Client client, Contract contract) {
Decoder decoder, Encoder encoder, Client client, Contract contract) {
this.fooClient = Feign.builder().client(client) this.fooClient = Feign.builder().client(client)
.encoder(encoder) .encoder(encoder)
.decoder(decoder) .decoder(decoder)
.contract(contract) .contract(contract)
.requestInterceptor(new BasicAuthRequestInterceptor("user", "user")) .requestInterceptor(new BasicAuthRequestInterceptor("user", "user"))
.target(FooClient.class, "http://PROD-SVC"); .target(FooClient.class, "http://PROD-SVC");
this.adminClient = Feign.builder().client(client) this.adminClient = Feign.builder().client(client)
.encoder(encoder) .encoder(encoder)
.decoder(decoder) .decoder(decoder)
@@ -426,11 +453,50 @@ public class FooConfiguration {
} }
} }
---- ----
OtherClass.someMethod(myprop.get());
} === Feign @QueryMap support
The OpenFeign `@QueryMap` annotation provides support for POJOs to be used as
GET parameter maps. Unfortunately, the default OpenFeign QueryMap annotation is
incompatible with Spring because it lacks a `value` property.
Spring Cloud OpenFeign provides an equivalent `@SpringQueryMap` annotation, which
is used to annotate a POJO or Map parameter as a query parameter map.
For example, the `Params` class defines parameters `param1` and `param2`:
[source,java,indent=0]
----
// Params.java
public class Params {
private String param1;
private String param2;
// [Getters and setters omitted for brevity]
} }
stripped). The proxy uses Ribbon to locate an instance to forward to ----
via discovery, and all requests are executed in a
<<hystrix-fallbacks-for-routes, hystrix command>>, so The following feign client uses the `Params` class by using the `@SpringQueryMap` annotation:
failures will show up in Hystrix metrics, and once the circuit is open
the proxy will not try to contact the service. [source,java,indent=0]
----
@FeignClient("demo")
public class DemoTemplate {
@GetMapping(path = "/demo")
String demoEndpoint(@SpringQueryMap Params params);
}
----
=== Troubleshooting
==== Early Initialization Errors
Depending on how you are using your Feign clients you may see initialization errors when starting your application.
To work around this problem you can use an `ObjectProvider` when autowiring your client.
[source,java,indent=0]
----
@Autowired
ObjectProvider<TestFeginClient> testFeginClient;
----
+48 -15
View File
@@ -1,29 +1,33 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<artifactId>spring-cloud-openfeign</artifactId> <artifactId>spring-cloud-openfeign</artifactId>
<version>2.0.0.RC2</version> <version>2.1.6.BUILD-SNAPSHOT</version>
<packaging>pom</packaging> <packaging>pom</packaging>
<name>Spring Cloud OpenFeign</name> <name>Spring Cloud OpenFeign</name>
<description>Spring Cloud OpenFeign</description> <description>Spring Cloud OpenFeign</description>
<parent> <parent>
<groupId>org.springframework.cloud</groupId> <groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-build</artifactId> <artifactId>spring-cloud-build</artifactId>
<version>2.0.1.RELEASE</version> <version>2.1.11.BUILD-SNAPSHOT</version>
<relativePath /> <relativePath/>
</parent> </parent>
<scm> <scm>
<url>https://github.com/spring-cloud/spring-cloud-openfeign</url> <url>https://github.com/spring-cloud/spring-cloud-openfeign</url>
<connection>scm:git:git://github.com/spring-cloud/spring-cloud-openfeign.git</connection> <connection>scm:git:git://github.com/spring-cloud/spring-cloud-openfeign.git
<developerConnection>scm:git:ssh://git@github.com/spring-cloud/spring-cloud-openfeign.git</developerConnection> </connection>
<developerConnection>
scm:git:ssh://git@github.com/spring-cloud/spring-cloud-openfeign.git
</developerConnection>
<tag>HEAD</tag> <tag>HEAD</tag>
</scm> </scm>
<properties> <properties>
<main.basedir>${basedir}</main.basedir> <main.basedir>${basedir}</main.basedir>
<jackson.version>2.7.3</jackson.version> <jackson.version>2.7.3</jackson.version>
<spring-cloud-commons.version>2.0.0.RC2</spring-cloud-commons.version> <spring-cloud-commons.version>2.1.6.BUILD-SNAPSHOT</spring-cloud-commons.version>
<spring-cloud-netflix.version>2.0.0.RC2</spring-cloud-netflix.version> <spring-cloud-netflix.version>2.1.6.BUILD-SNAPSHOT</spring-cloud-netflix.version>
<!-- Plugin versions --> <!-- Plugin versions -->
<maven-compiler-plugin.version>3.6.1</maven-compiler-plugin.version> <maven-compiler-plugin.version>3.6.1</maven-compiler-plugin.version>
@@ -32,8 +36,15 @@
<surefire.plugin.version>2.19.1</surefire.plugin.version> <surefire.plugin.version>2.19.1</surefire.plugin.version>
<sonar.java.coveragePlugin>jacoco</sonar.java.coveragePlugin> <sonar.java.coveragePlugin>jacoco</sonar.java.coveragePlugin>
<sonar.dynamicAnalysis>reuseReports</sonar.dynamicAnalysis> <sonar.dynamicAnalysis>reuseReports</sonar.dynamicAnalysis>
<sonar.jacoco.reportPath>${project.basedir}/../target/jacoco.exec</sonar.jacoco.reportPath> <sonar.jacoco.reportPath>${project.basedir}/../target/jacoco.exec
</sonar.jacoco.reportPath>
<sonar.language>java</sonar.language> <sonar.language>java</sonar.language>
<maven-checkstyle-plugin.failsOnError>true</maven-checkstyle-plugin.failsOnError>
<maven-checkstyle-plugin.failsOnViolation>true
</maven-checkstyle-plugin.failsOnViolation>
<maven-checkstyle-plugin.includeTestSourceDirectory>true
</maven-checkstyle-plugin.includeTestSourceDirectory>
</properties> </properties>
<build> <build>
<plugins> <plugins>
@@ -46,11 +57,13 @@
<additionalConfig> <additionalConfig>
<file> <file>
<name>.settings/org.eclipse.jdt.ui.prefs</name> <name>.settings/org.eclipse.jdt.ui.prefs</name>
<location>${main.basedir}/eclipse/org.eclipse.jdt.ui.prefs</location> <location>${main.basedir}/eclipse/org.eclipse.jdt.ui.prefs
</location>
</file> </file>
<file> <file>
<name>.settings/org.eclipse.jdt.core.prefs</name> <name>.settings/org.eclipse.jdt.core.prefs</name>
<location>${main.basedir}/eclipse/org.eclipse.jdt.core.prefs</location> <location>${main.basedir}/eclipse/org.eclipse.jdt.core.prefs
</location>
</file> </file>
</additionalConfig> </additionalConfig>
</configuration> </configuration>
@@ -63,8 +76,26 @@
<target>1.8</target> <target>1.8</target>
</configuration> </configuration>
</plugin> </plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
</plugin>
<plugin>
<groupId>io.spring.javaformat</groupId>
<artifactId>spring-javaformat-maven-plugin</artifactId>
</plugin>
</plugins> </plugins>
</build> </build>
<reporting>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-checkstyle-plugin</artifactId>
</plugin>
</plugins>
</reporting>
<dependencyManagement> <dependencyManagement>
<dependencies> <dependencies>
<dependency> <dependency>
@@ -106,7 +137,7 @@
<module>spring-cloud-openfeign-core</module> <module>spring-cloud-openfeign-core</module>
<module>spring-cloud-starter-openfeign</module> <module>spring-cloud-starter-openfeign</module>
<module>docs</module> <module>docs</module>
</modules> </modules>
<profiles> <profiles>
<profile> <profile>
<id>spring</id> <id>spring</id>
@@ -184,7 +215,8 @@
</goals> </goals>
<configuration> <configuration>
<propertyName>surefireArgLine</propertyName> <propertyName>surefireArgLine</propertyName>
<destFile>${project.build.directory}/jacoco.exec</destFile> <destFile>${project.build.directory}/jacoco.exec
</destFile>
</configuration> </configuration>
</execution> </execution>
<execution> <execution>
@@ -195,7 +227,8 @@
</goals> </goals>
<configuration> <configuration>
<!-- Sets the path to the file which contains the execution data. --> <!-- Sets the path to the file which contains the execution data. -->
<dataFile>${project.build.directory}/jacoco.exec</dataFile> <dataFile>${project.build.directory}/jacoco.exec
</dataFile>
</configuration> </configuration>
</execution> </execution>
</executions> </executions>
+41 -19
View File
@@ -1,11 +1,12 @@
<?xml version="1.0" encoding="UTF-8"?> <?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" <project xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> xmlns="http://maven.apache.org/POM/4.0.0"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion> <modelVersion>4.0.0</modelVersion>
<parent> <parent>
<groupId>org.springframework.cloud</groupId> <groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-openfeign</artifactId> <artifactId>spring-cloud-openfeign</artifactId>
<version>2.0.0.RC2</version> <version>2.1.6.BUILD-SNAPSHOT</version>
<relativePath>..</relativePath> <!-- lookup parent from repository --> <relativePath>..</relativePath> <!-- lookup parent from repository -->
</parent> </parent>
<artifactId>spring-cloud-openfeign-core</artifactId> <artifactId>spring-cloud-openfeign-core</artifactId>
@@ -93,6 +94,10 @@
<artifactId>feign-core</artifactId> <artifactId>feign-core</artifactId>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form-spring</artifactId>
</dependency>
<dependency> <dependency>
<groupId>io.github.openfeign</groupId> <groupId>io.github.openfeign</groupId>
<artifactId>feign-slf4j</artifactId> <artifactId>feign-slf4j</artifactId>
@@ -154,13 +159,19 @@
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.sun.jersey.contribs</groupId> <groupId>com.squareup.okhttp3</groupId>
<artifactId>jersey-apache-client4</artifactId> <artifactId>okhttp</artifactId>
<version>4.1.0</version>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<dependency> <dependency>
<groupId>com.squareup.okhttp3</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>okhttp</artifactId> <artifactId>spring-boot-autoconfigure-processor</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-configuration-processor</artifactId>
<optional>true</optional> <optional>true</optional>
</dependency> </dependency>
<dependency> <dependency>
@@ -173,6 +184,11 @@
<artifactId>spring-cloud-test-support</artifactId> <artifactId>spring-cloud-test-support</artifactId>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.data</groupId>
<artifactId>spring-data-commons</artifactId>
<optional>true</optional>
</dependency>
<dependency> <dependency>
<groupId>io.projectreactor</groupId> <groupId>io.projectreactor</groupId>
<artifactId>reactor-test</artifactId> <artifactId>reactor-test</artifactId>
@@ -200,22 +216,28 @@
<version>3.4.0</version> <version>3.4.0</version>
<scope>test</scope> <scope>test</scope>
</dependency> </dependency>
<dependency>
<groupId>io.vavr</groupId>
<artifactId>vavr</artifactId>
<version>0.10.0</version>
<scope>test</scope>
</dependency>
</dependencies> </dependencies>
<profiles> <profiles>
<profile> <profile>
<id>java8plus</id> <id>java8plus</id>
<activation> <activation>
<jdk>[1.8,2.0)</jdk> <jdk>[1.8,2.0)</jdk>
</activation> </activation>
<build> <build>
<plugins> <plugins>
<plugin> <plugin>
<groupId>org.apache.maven.plugins</groupId> <groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId> <artifactId>maven-compiler-plugin</artifactId>
<configuration> <configuration>
<compilerArgs> <compilerArgs>
<arg>-parameters</arg> <arg>-parameters</arg>
</compilerArgs> </compilerArgs>
</configuration> </configuration>
</plugin> </plugin>
</plugins> </plugins>
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2017 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -32,20 +32,19 @@ public interface AnnotatedParameterProcessor {
/** /**
* Retrieves the processor supported annotation type. * Retrieves the processor supported annotation type.
*
* @return the annotation type * @return the annotation type
*/ */
Class<? extends Annotation> getAnnotationType(); Class<? extends Annotation> getAnnotationType();
/** /**
* Process the annotated parameter. * Process the annotated parameter.
* * @param context the parameter context
* @param context the parameter context
* @param annotation the annotation instance * @param annotation the annotation instance
* @param method the method that contains the annotation * @param method the method that contains the annotation
* @return whether the parameter is http * @return whether the parameter is http
*/ */
boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method); boolean processArgument(AnnotatedParameterContext context, Annotation annotation,
Method method);
/** /**
* Specifies the parameter context. * Specifies the parameter context.
@@ -56,32 +55,30 @@ public interface AnnotatedParameterProcessor {
/** /**
* Retrieves the method metadata. * Retrieves the method metadata.
*
* @return the method metadata * @return the method metadata
*/ */
MethodMetadata getMethodMetadata(); MethodMetadata getMethodMetadata();
/** /**
* Retrieves the index of the parameter. * Retrieves the index of the parameter.
*
* @return the parameter index * @return the parameter index
*/ */
int getParameterIndex(); int getParameterIndex();
/** /**
* Sets the parameter name. * Sets the parameter name.
*
* @param name the name of the parameter * @param name the name of the parameter
*/ */
void setParameterName(String name); void setParameterName(String name);
/** /**
* Sets the template parameter. * Sets the template parameter.
*
* @param name the template parameter * @param name the template parameter
* @param rest the existing parameter values * @param rest the existing parameter values
* @return parameters * @return parameters
*/ */
Collection<String> setTemplateParameter(String name, Collection<String> rest); Collection<String> setTemplateParameter(String name, Collection<String> rest);
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2016 the original author or authors. * Copyright 2016-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -1,18 +1,17 @@
/* /*
* Copyright 2013-2016 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*
*/ */
package org.springframework.cloud.openfeign; package org.springframework.cloud.openfeign;
@@ -26,8 +25,9 @@ import feign.Target;
class DefaultTargeter implements Targeter { class DefaultTargeter implements Targeter {
@Override @Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context, public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
Target.HardCodedTarget<T> target) { FeignContext context, Target.HardCodedTarget<T> target) {
return feign.target(target); return feign.target(target);
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2015 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -25,10 +25,11 @@ import java.lang.annotation.Target;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
/** /**
* Scans for interfaces that declare they are feign clients (via {@link FeignClient * Scans for interfaces that declare they are feign clients (via
* <code>@FeignClient</code>}). Configures component scanning directives for use with * {@link org.springframework.cloud.openfeign.FeignClient} <code>@FeignClient</code>).
* {@link org.springframework.context.annotation.Configuration * Configures component scanning directives for use with
* <code>@Configuration</code>} classes. * {@link org.springframework.context.annotation.Configuration}
* <code>@Configuration</code> classes.
* *
* @author Spencer Gibb * @author Spencer Gibb
* @author Dave Syer * @author Dave Syer
@@ -55,7 +56,6 @@ public @interface EnableFeignClients {
* <p> * <p>
* Use {@link #basePackageClasses()} for a type-safe alternative to String-based * Use {@link #basePackageClasses()} for a type-safe alternative to String-based
* package names. * package names.
*
* @return the array of 'basePackages'. * @return the array of 'basePackages'.
*/ */
String[] basePackages() default {}; String[] basePackages() default {};
@@ -66,7 +66,6 @@ public @interface EnableFeignClients {
* <p> * <p>
* Consider creating a special no-op marker class or interface in each package that * Consider creating a special no-op marker class or interface in each package that
* serves no purpose other than being referenced by this attribute. * serves no purpose other than being referenced by this attribute.
*
* @return the array of 'basePackageClasses'. * @return the array of 'basePackageClasses'.
*/ */
Class<?>[] basePackageClasses() default {}; Class<?>[] basePackageClasses() default {};
@@ -77,12 +76,15 @@ public @interface EnableFeignClients {
* {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}. * {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}.
* *
* @see FeignClientsConfiguration for the defaults * @see FeignClientsConfiguration for the defaults
* @return list of default configurations
*/ */
Class<?>[] defaultConfiguration() default {}; Class<?>[] defaultConfiguration() default {};
/** /**
* List of classes annotated with @FeignClient. If not empty, disables classpath scanning. * List of classes annotated with @FeignClient. If not empty, disables classpath
* @return * scanning.
* @return list of FeignClient classes
*/ */
Class<?>[] clients() default {}; Class<?>[] clients() default {};
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2015 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -22,11 +22,19 @@ import java.util.Timer;
import java.util.TimerTask; import java.util.TimerTask;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import javax.annotation.PreDestroy;
import feign.Client;
import feign.Feign;
import feign.httpclient.ApacheHttpClient;
import feign.okhttp.OkHttpClient;
import okhttp3.ConnectionPool;
import org.apache.http.client.HttpClient; import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig; import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.RegistryBuilder; import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.HttpClientConnectionManager; import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.CloseableHttpClient;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -42,21 +50,14 @@ import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import feign.Client;
import feign.Feign;
import feign.httpclient.ApacheHttpClient;
import feign.okhttp.OkHttpClient;
import okhttp3.ConnectionPool;
import javax.annotation.PreDestroy;
/** /**
* @author Spencer Gibb * @author Spencer Gibb
* @author Julien Roy * @author Julien Roy
*/ */
@Configuration @Configuration
@ConditionalOnClass(Feign.class) @ConditionalOnClass(Feign.class)
@EnableConfigurationProperties({FeignClientProperties.class, FeignHttpClientProperties.class}) @EnableConfigurationProperties({ FeignClientProperties.class,
FeignHttpClientProperties.class })
public class FeignAutoConfiguration { public class FeignAutoConfiguration {
@Autowired(required = false) @Autowired(required = false)
@@ -77,21 +78,25 @@ public class FeignAutoConfiguration {
@Configuration @Configuration
@ConditionalOnClass(name = "feign.hystrix.HystrixFeign") @ConditionalOnClass(name = "feign.hystrix.HystrixFeign")
protected static class HystrixFeignTargeterConfiguration { protected static class HystrixFeignTargeterConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public Targeter feignTargeter() { public Targeter feignTargeter() {
return new HystrixTargeter(); return new HystrixTargeter();
} }
} }
@Configuration @Configuration
@ConditionalOnMissingClass("feign.hystrix.HystrixFeign") @ConditionalOnMissingClass("feign.hystrix.HystrixFeign")
protected static class DefaultFeignTargeterConfiguration { protected static class DefaultFeignTargeterConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public Targeter feignTargeter() { public Targeter feignTargeter() {
return new DefaultTargeter(); return new DefaultTargeter();
} }
} }
// the following configuration is for alternate feign clients if // the following configuration is for alternate feign clients if
@@ -104,6 +109,7 @@ public class FeignAutoConfiguration {
@ConditionalOnMissingBean(CloseableHttpClient.class) @ConditionalOnMissingBean(CloseableHttpClient.class)
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true) @ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
protected static class HttpClientFeignConfiguration { protected static class HttpClientFeignConfiguration {
private final Timer connectionManagerTimer = new Timer( private final Timer connectionManagerTimer = new Timer(
"FeignApacheHttpClientConfiguration.connectionManagerTimer", true); "FeignApacheHttpClientConfiguration.connectionManagerTimer", true);
@@ -118,10 +124,12 @@ public class FeignAutoConfiguration {
ApacheHttpClientConnectionManagerFactory connectionManagerFactory, ApacheHttpClientConnectionManagerFactory connectionManagerFactory,
FeignHttpClientProperties httpClientProperties) { FeignHttpClientProperties httpClientProperties) {
final HttpClientConnectionManager connectionManager = connectionManagerFactory final HttpClientConnectionManager connectionManager = connectionManagerFactory
.newConnectionManager(httpClientProperties.isDisableSslValidation(), httpClientProperties.getMaxConnections(), .newConnectionManager(httpClientProperties.isDisableSslValidation(),
httpClientProperties.getMaxConnections(),
httpClientProperties.getMaxConnectionsPerRoute(), httpClientProperties.getMaxConnectionsPerRoute(),
httpClientProperties.getTimeToLive(), httpClientProperties.getTimeToLive(),
httpClientProperties.getTimeToLiveUnit(), registryBuilder); httpClientProperties.getTimeToLiveUnit(),
this.registryBuilder);
this.connectionManagerTimer.schedule(new TimerTask() { this.connectionManagerTimer.schedule(new TimerTask() {
@Override @Override
public void run() { public void run() {
@@ -139,9 +147,9 @@ public class FeignAutoConfiguration {
.setConnectTimeout(httpClientProperties.getConnectionTimeout()) .setConnectTimeout(httpClientProperties.getConnectionTimeout())
.setRedirectsEnabled(httpClientProperties.isFollowRedirects()) .setRedirectsEnabled(httpClientProperties.isFollowRedirects())
.build(); .build();
this.httpClient = httpClientFactory.createBuilder(). this.httpClient = httpClientFactory.createBuilder()
setConnectionManager(httpClientConnectionManager). .setConnectionManager(httpClientConnectionManager)
setDefaultRequestConfig(defaultRequestConfig).build(); .setDefaultRequestConfig(defaultRequestConfig).build();
return this.httpClient; return this.httpClient;
} }
@@ -153,26 +161,28 @@ public class FeignAutoConfiguration {
@PreDestroy @PreDestroy
public void destroy() throws Exception { public void destroy() throws Exception {
connectionManagerTimer.cancel(); this.connectionManagerTimer.cancel();
if(httpClient != null) { if (this.httpClient != null) {
httpClient.close(); this.httpClient.close();
} }
} }
} }
@Configuration @Configuration
@ConditionalOnClass(OkHttpClient.class) @ConditionalOnClass(OkHttpClient.class)
@ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer") @ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
@ConditionalOnMissingBean(okhttp3.OkHttpClient.class) @ConditionalOnMissingBean(okhttp3.OkHttpClient.class)
@ConditionalOnProperty(value = "feign.okhttp.enabled") @ConditionalOnProperty("feign.okhttp.enabled")
protected static class OkHttpFeignConfiguration { protected static class OkHttpFeignConfiguration {
private okhttp3.OkHttpClient okHttpClient; private okhttp3.OkHttpClient okHttpClient;
@Bean @Bean
@ConditionalOnMissingBean(ConnectionPool.class) @ConditionalOnMissingBean(ConnectionPool.class)
public ConnectionPool httpClientConnectionPool(FeignHttpClientProperties httpClientProperties, public ConnectionPool httpClientConnectionPool(
OkHttpClientConnectionPoolFactory connectionPoolFactory) { FeignHttpClientProperties httpClientProperties,
OkHttpClientConnectionPoolFactory connectionPoolFactory) {
Integer maxTotalConnections = httpClientProperties.getMaxConnections(); Integer maxTotalConnections = httpClientProperties.getMaxConnections();
Long timeToLive = httpClientProperties.getTimeToLive(); Long timeToLive = httpClientProperties.getTimeToLive();
TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit(); TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
@@ -181,30 +191,32 @@ public class FeignAutoConfiguration {
@Bean @Bean
public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory, public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory,
ConnectionPool connectionPool, FeignHttpClientProperties httpClientProperties) { ConnectionPool connectionPool,
FeignHttpClientProperties httpClientProperties) {
Boolean followRedirects = httpClientProperties.isFollowRedirects(); Boolean followRedirects = httpClientProperties.isFollowRedirects();
Integer connectTimeout = httpClientProperties.getConnectionTimeout(); Integer connectTimeout = httpClientProperties.getConnectionTimeout();
Boolean disableSslValidation = httpClientProperties.isDisableSslValidation(); Boolean disableSslValidation = httpClientProperties.isDisableSslValidation();
this.okHttpClient = httpClientFactory.createBuilder(disableSslValidation). this.okHttpClient = httpClientFactory.createBuilder(disableSslValidation)
connectTimeout(connectTimeout, TimeUnit.MILLISECONDS). .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
followRedirects(followRedirects). .followRedirects(followRedirects).connectionPool(connectionPool)
connectionPool(connectionPool).build(); .build();
return this.okHttpClient; return this.okHttpClient;
} }
@PreDestroy @PreDestroy
public void destroy() { public void destroy() {
if(okHttpClient != null) { if (this.okHttpClient != null) {
okHttpClient.dispatcher().executorService().shutdown(); this.okHttpClient.dispatcher().executorService().shutdown();
okHttpClient.connectionPool().evictAll(); this.okHttpClient.connectionPool().evictAll();
} }
} }
@Bean @Bean
@ConditionalOnMissingBean(Client.class) @ConditionalOnMissingBean(Client.class)
public Client feignClient() { public Client feignClient(okhttp3.OkHttpClient client) {
return new OkHttpClient(this.okHttpClient); return new OkHttpClient(client);
} }
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2016 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -42,36 +42,45 @@ public @interface FeignClient {
* The name of the service with optional protocol prefix. Synonym for {@link #name() * The name of the service with optional protocol prefix. Synonym for {@link #name()
* name}. A name must be specified for all clients, whether or not a url is provided. * name}. A name must be specified for all clients, whether or not a url is provided.
* Can be specified as property key, eg: ${propertyKey}. * Can be specified as property key, eg: ${propertyKey}.
* @return the name of the service with optional protocol prefix
*/ */
@AliasFor("name") @AliasFor("name")
String value() default ""; String value() default "";
/** /**
* The service id with optional protocol prefix. Synonym for {@link #value() value}. * The service id with optional protocol prefix. Synonym for {@link #value() value}.
*
* @deprecated use {@link #name() name} instead * @deprecated use {@link #name() name} instead
* @return the service id with optional protocol prefix
*/ */
@Deprecated @Deprecated
String serviceId() default ""; String serviceId() default "";
/** /**
* The service id with optional protocol prefix. Synonym for {@link #value() value}. * This will be used as the bean name instead of name if present, but will not be used
* as a service id.
* @return bean name instead of name if present
*/
String contextId() default "";
/**
* @return The service id with optional protocol prefix. Synonym for {@link #value()
* value}.
*/ */
@AliasFor("value") @AliasFor("value")
String name() default ""; String name() default "";
/** /**
* Sets the <code>@Qualifier</code> value for the feign client. * @return the <code>@Qualifier</code> value for the feign client.
*/ */
String qualifier() default ""; String qualifier() default "";
/** /**
* An absolute URL or resolvable hostname (the protocol is optional). * @return an absolute URL or resolvable hostname (the protocol is optional).
*/ */
String url() default ""; String url() default "";
/** /**
* Whether 404s should be decoded instead of throwing FeignExceptions * @return whether 404s should be decoded instead of throwing FeignExceptions
*/ */
boolean decode404() default false; boolean decode404() default false;
@@ -81,33 +90,35 @@ public @interface FeignClient {
* {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}. * {@link feign.codec.Decoder}, {@link feign.codec.Encoder}, {@link feign.Contract}.
* *
* @see FeignClientsConfiguration for the defaults * @see FeignClientsConfiguration for the defaults
* @return list of configurations for feign client
*/ */
Class<?>[] configuration() default {}; Class<?>[] configuration() default {};
/** /**
* Fallback class for the specified Feign client interface. The fallback class must * Fallback class for the specified Feign client interface. The fallback class must
* implement the interface annotated by this annotation and be a valid spring bean. * implement the interface annotated by this annotation and be a valid spring bean.
* @return fallback class for the specified Feign client interface
*/ */
Class<?> fallback() default void.class; Class<?> fallback() default void.class;
/** /**
* Define a fallback factory for the specified Feign client interface. The fallback * Define a fallback factory for the specified Feign client interface. The fallback
* factory must produce instances of fallback classes that implement the interface * factory must produce instances of fallback classes that implement the interface
* annotated by {@link FeignClient}. The fallback factory must be a valid spring * annotated by {@link FeignClient}. The fallback factory must be a valid spring bean.
* bean.
* *
* @see feign.hystrix.FallbackFactory for details. * @see feign.hystrix.FallbackFactory for details.
* @return fallback factory for the specified Feign client interface
*/ */
Class<?> fallbackFactory() default void.class; Class<?> fallbackFactory() default void.class;
/** /**
* Path prefix to be used by all method-level mappings. Can be used with or without * @return path prefix to be used by all method-level mappings. Can be used with or
* <code>@RibbonClient</code>. * without <code>@RibbonClient</code>.
*/ */
String path() default ""; String path() default "";
/** /**
* Whether to mark the feign proxy as a primary bean. Defaults to true. * @return whether to mark the feign proxy as a primary bean. Defaults to true.
*/ */
boolean primary() default true; boolean primary() default true;
@@ -0,0 +1,106 @@
/*
* Copyright 2013-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.openfeign;
import org.springframework.context.ApplicationContext;
/**
* A builder for creating Feign clients without using the {@link FeignClient} annotation.
* <p>
* This builder builds the Feign client exactly like it would be created by using the
* {@link FeignClient} annotation.
*
* @author Sven Döring
*/
public class FeignClientBuilder {
private final ApplicationContext applicationContext;
public FeignClientBuilder(final ApplicationContext applicationContext) {
this.applicationContext = applicationContext;
}
public <T> Builder<T> forType(final Class<T> type, final String name) {
return new Builder<>(this.applicationContext, type, name);
}
/**
* Builder of feign targets.
*
* @param <T> type of target
*/
public static final class Builder<T> {
private FeignClientFactoryBean feignClientFactoryBean;
private Builder(final ApplicationContext applicationContext, final Class<T> type,
final String name) {
this.feignClientFactoryBean = new FeignClientFactoryBean();
this.feignClientFactoryBean.setApplicationContext(applicationContext);
this.feignClientFactoryBean.setType(type);
this.feignClientFactoryBean.setName(FeignClientsRegistrar.getName(name));
this.feignClientFactoryBean.setContextId(FeignClientsRegistrar.getName(name));
// preset default values - these values resemble the default values on the
// FeignClient annotation
this.url("").path("").decode404(false).fallback(void.class)
.fallbackFactory(void.class);
}
public Builder url(final String url) {
this.feignClientFactoryBean.setUrl(FeignClientsRegistrar.getUrl(url));
return this;
}
public Builder contextId(final String contextId) {
this.feignClientFactoryBean.setContextId(contextId);
return this;
}
public Builder path(final String path) {
this.feignClientFactoryBean.setPath(FeignClientsRegistrar.getPath(path));
return this;
}
public Builder decode404(final boolean decode404) {
this.feignClientFactoryBean.setDecode404(decode404);
return this;
}
public Builder fallback(final Class<T> fallback) {
FeignClientsRegistrar.validateFallback(fallback);
this.feignClientFactoryBean.setFallback(fallback);
return this;
}
public Builder fallbackFactory(final Class<T> fallbackFactory) {
FeignClientsRegistrar.validateFallbackFactory(fallbackFactory);
this.feignClientFactoryBean.setFallbackFactory(fallbackFactory);
return this;
}
/**
* @param <T> the target type of the Feign client to be created
* @return the created Feign client
*/
public <T> T build() {
return this.feignClientFactoryBean.getTarget();
}
}
}
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2018 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -19,17 +19,6 @@ package org.springframework.cloud.openfeign;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
import feign.Client; import feign.Client;
import feign.Contract; import feign.Contract;
import feign.Feign; import feign.Feign;
@@ -42,16 +31,29 @@ import feign.codec.Decoder;
import feign.codec.Encoder; import feign.codec.Encoder;
import feign.codec.ErrorDecoder; import feign.codec.ErrorDecoder;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.FactoryBean;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.NoSuchBeanDefinitionException;
import org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.Assert;
import org.springframework.util.StringUtils;
/** /**
* @author Spencer Gibb * @author Spencer Gibb
* @author Venil Noronha * @author Venil Noronha
* @author Eko Kurniawan Khannedy * @author Eko Kurniawan Khannedy
* @author Gregor Zurowski * @author Gregor Zurowski
*/ */
class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean, class FeignClientFactoryBean
ApplicationContextAware { implements FactoryBean<Object>, InitializingBean, ApplicationContextAware {
/*********************************** /***********************************
* WARNING! Nothing in this class should be @Autowired. It causes NPEs because of some lifecycle race condition. * WARNING! Nothing in this class should be @Autowired. It causes NPEs because of some
* lifecycle race condition.
***********************************/ ***********************************/
private Class<?> type; private Class<?> type;
@@ -60,6 +62,8 @@ class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
private String url; private String url;
private String contextId;
private String path; private String path;
private boolean decode404; private boolean decode404;
@@ -72,14 +76,10 @@ class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
@Override @Override
public void afterPropertiesSet() throws Exception { public void afterPropertiesSet() throws Exception {
Assert.hasText(this.contextId, "Context id must be set");
Assert.hasText(this.name, "Name must be set"); Assert.hasText(this.name, "Name must be set");
} }
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
this.applicationContext = context;
}
protected Feign.Builder feign(FeignContext context) { protected Feign.Builder feign(FeignContext context) {
FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class); FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class);
Logger logger = loggerFactory.create(this.type); Logger logger = loggerFactory.create(this.type);
@@ -99,23 +99,33 @@ class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
} }
protected void configureFeign(FeignContext context, Feign.Builder builder) { protected void configureFeign(FeignContext context, Feign.Builder builder) {
FeignClientProperties properties = applicationContext.getBean(FeignClientProperties.class); FeignClientProperties properties = this.applicationContext
.getBean(FeignClientProperties.class);
if (properties != null) { if (properties != null) {
if (properties.isDefaultToProperties()) { if (properties.isDefaultToProperties()) {
configureUsingConfiguration(context, builder); configureUsingConfiguration(context, builder);
configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder); configureUsingProperties(
configureUsingProperties(properties.getConfig().get(this.name), builder); properties.getConfig().get(properties.getDefaultConfig()),
} else { builder);
configureUsingProperties(properties.getConfig().get(properties.getDefaultConfig()), builder); configureUsingProperties(properties.getConfig().get(this.contextId),
configureUsingProperties(properties.getConfig().get(this.name), builder); builder);
}
else {
configureUsingProperties(
properties.getConfig().get(properties.getDefaultConfig()),
builder);
configureUsingProperties(properties.getConfig().get(this.contextId),
builder);
configureUsingConfiguration(context, builder); configureUsingConfiguration(context, builder);
} }
} else { }
else {
configureUsingConfiguration(context, builder); configureUsingConfiguration(context, builder);
} }
} }
protected void configureUsingConfiguration(FeignContext context, Feign.Builder builder) { protected void configureUsingConfiguration(FeignContext context,
Feign.Builder builder) {
Logger.Level level = getOptional(context, Logger.Level.class); Logger.Level level = getOptional(context, Logger.Level.class);
if (level != null) { if (level != null) {
builder.logLevel(level); builder.logLevel(level);
@@ -132,18 +142,20 @@ class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
if (options != null) { if (options != null) {
builder.options(options); builder.options(options);
} }
Map<String, RequestInterceptor> requestInterceptors = context.getInstances( Map<String, RequestInterceptor> requestInterceptors = context
this.name, RequestInterceptor.class); .getInstances(this.contextId, RequestInterceptor.class);
if (requestInterceptors != null) { if (requestInterceptors != null) {
builder.requestInterceptors(requestInterceptors.values()); builder.requestInterceptors(requestInterceptors.values());
} }
if (decode404) { if (this.decode404) {
builder.decode404(); builder.decode404();
} }
} }
protected void configureUsingProperties(FeignClientProperties.FeignClientConfiguration config, Feign.Builder builder) { protected void configureUsingProperties(
FeignClientProperties.FeignClientConfiguration config,
Feign.Builder builder) {
if (config == null) { if (config == null) {
return; return;
} }
@@ -153,7 +165,8 @@ class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
} }
if (config.getConnectTimeout() != null && config.getReadTimeout() != null) { if (config.getConnectTimeout() != null && config.getReadTimeout() != null) {
builder.options(new Request.Options(config.getConnectTimeout(), config.getReadTimeout())); builder.options(new Request.Options(config.getConnectTimeout(),
config.getReadTimeout()));
} }
if (config.getRetryer() != null) { if (config.getRetryer() != null) {
@@ -166,7 +179,8 @@ class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
builder.errorDecoder(errorDecoder); builder.errorDecoder(errorDecoder);
} }
if (config.getRequestInterceptors() != null && !config.getRequestInterceptors().isEmpty()) { if (config.getRequestInterceptors() != null
&& !config.getRequestInterceptors().isEmpty()) {
// this will add request interceptor to builder, not replace existing // this will add request interceptor to builder, not replace existing
for (Class<RequestInterceptor> bean : config.getRequestInterceptors()) { for (Class<RequestInterceptor> bean : config.getRequestInterceptors()) {
RequestInterceptor interceptor = getOrInstantiate(bean); RequestInterceptor interceptor = getOrInstantiate(bean);
@@ -195,23 +209,24 @@ class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
private <T> T getOrInstantiate(Class<T> tClass) { private <T> T getOrInstantiate(Class<T> tClass) {
try { try {
return applicationContext.getBean(tClass); return this.applicationContext.getBean(tClass);
} catch (NoSuchBeanDefinitionException e) { }
catch (NoSuchBeanDefinitionException e) {
return BeanUtils.instantiateClass(tClass); return BeanUtils.instantiateClass(tClass);
} }
} }
protected <T> T get(FeignContext context, Class<T> type) { protected <T> T get(FeignContext context, Class<T> type) {
T instance = context.getInstance(this.name, type); T instance = context.getInstance(this.contextId, type);
if (instance == null) { if (instance == null) {
throw new IllegalStateException("No bean found of type " + type + " for " throw new IllegalStateException(
+ this.name); "No bean found of type " + type + " for " + this.contextId);
} }
return instance; return instance;
} }
protected <T> T getOptional(FeignContext context, Class<T> type) { protected <T> T getOptional(FeignContext context, Class<T> type) {
return context.getInstance(this.name, type); return context.getInstance(this.contextId, type);
} }
protected <T> T loadBalance(Feign.Builder builder, FeignContext context, protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
@@ -229,20 +244,28 @@ class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
@Override @Override
public Object getObject() throws Exception { public Object getObject() throws Exception {
FeignContext context = applicationContext.getBean(FeignContext.class); return getTarget();
}
/**
* @param <T> the target type of the Feign client
* @return a {@link Feign} client created with the specified data and the context
* information
*/
<T> T getTarget() {
FeignContext context = this.applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context); Feign.Builder builder = feign(context);
if (!StringUtils.hasText(this.url)) { if (!StringUtils.hasText(this.url)) {
String url;
if (!this.name.startsWith("http")) { if (!this.name.startsWith("http")) {
url = "http://" + this.name; this.url = "http://" + this.name;
} }
else { else {
url = this.name; this.url = this.name;
} }
url += cleanPath(); this.url += cleanPath();
return loadBalance(builder, context, new HardCodedTarget<>(this.type, return (T) loadBalance(builder, context,
this.name, url)); new HardCodedTarget<>(this.type, this.name, this.url));
} }
if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) { if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
this.url = "http://" + this.url; this.url = "http://" + this.url;
@@ -251,15 +274,15 @@ class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
Client client = getOptional(context, Client.class); Client client = getOptional(context, Client.class);
if (client != null) { if (client != null) {
if (client instanceof LoadBalancerFeignClient) { if (client instanceof LoadBalancerFeignClient) {
// not lod balancing because we have a url, // not load balancing because we have a url,
// but ribbon is on the classpath, so unwrap // but ribbon is on the classpath, so unwrap
client = ((LoadBalancerFeignClient)client).getDelegate(); client = ((LoadBalancerFeignClient) client).getDelegate();
} }
builder.client(client); builder.client(client);
} }
Targeter targeter = get(context, Targeter.class); Targeter targeter = get(context, Targeter.class);
return targeter.target(this, builder, context, new HardCodedTarget<>( return (T) targeter.target(this, builder, context,
this.type, this.name, url)); new HardCodedTarget<>(this.type, this.name, url));
} }
private String cleanPath() { private String cleanPath() {
@@ -286,7 +309,7 @@ class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
} }
public Class<?> getType() { public Class<?> getType() {
return type; return this.type;
} }
public void setType(Class<?> type) { public void setType(Class<?> type) {
@@ -294,15 +317,23 @@ class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
} }
public String getName() { public String getName() {
return name; return this.name;
} }
public void setName(String name) { public void setName(String name) {
this.name = name; this.name = name;
} }
public String getContextId() {
return this.contextId;
}
public void setContextId(String contextId) {
this.contextId = contextId;
}
public String getUrl() { public String getUrl() {
return url; return this.url;
} }
public void setUrl(String url) { public void setUrl(String url) {
@@ -310,7 +341,7 @@ class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
} }
public String getPath() { public String getPath() {
return path; return this.path;
} }
public void setPath(String path) { public void setPath(String path) {
@@ -318,7 +349,7 @@ class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
} }
public boolean isDecode404() { public boolean isDecode404() {
return decode404; return this.decode404;
} }
public void setDecode404(boolean decode404) { public void setDecode404(boolean decode404) {
@@ -326,11 +357,16 @@ class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
} }
public ApplicationContext getApplicationContext() { public ApplicationContext getApplicationContext() {
return applicationContext; return this.applicationContext;
}
@Override
public void setApplicationContext(ApplicationContext context) throws BeansException {
this.applicationContext = context;
} }
public Class<?> getFallback() { public Class<?> getFallback() {
return fallback; return this.fallback;
} }
public void setFallback(Class<?> fallback) { public void setFallback(Class<?> fallback) {
@@ -338,7 +374,7 @@ class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
} }
public Class<?> getFallbackFactory() { public Class<?> getFallbackFactory() {
return fallbackFactory; return this.fallbackFactory;
} }
public void setFallbackFactory(Class<?> fallbackFactory) { public void setFallbackFactory(Class<?> fallbackFactory) {
@@ -347,37 +383,39 @@ class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) {
if (o == null || getClass() != o.getClass()) return false; return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
FeignClientFactoryBean that = (FeignClientFactoryBean) o; FeignClientFactoryBean that = (FeignClientFactoryBean) o;
return Objects.equals(applicationContext, that.applicationContext) && return Objects.equals(this.applicationContext, that.applicationContext)
decode404 == that.decode404 && && this.decode404 == that.decode404
Objects.equals(fallback, that.fallback) && && Objects.equals(this.fallback, that.fallback)
Objects.equals(fallbackFactory, that.fallbackFactory) && && Objects.equals(this.fallbackFactory, that.fallbackFactory)
Objects.equals(name, that.name) && && Objects.equals(this.name, that.name)
Objects.equals(path, that.path) && && Objects.equals(this.path, that.path)
Objects.equals(type, that.type) && && Objects.equals(this.type, that.type)
Objects.equals(url, that.url); && Objects.equals(this.url, that.url);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(applicationContext, decode404, fallback, fallbackFactory, return Objects.hash(this.applicationContext, this.decode404, this.fallback,
name, path, type, url); this.fallbackFactory, this.name, this.path, this.type, this.url);
} }
@Override @Override
public String toString() { public String toString() {
return new StringBuilder("FeignClientFactoryBean{") return new StringBuilder("FeignClientFactoryBean{").append("type=")
.append("type=").append(type).append(", ") .append(this.type).append(", ").append("name='").append(this.name)
.append("name='").append(name).append("', ") .append("', ").append("url='").append(this.url).append("', ")
.append("url='").append(url).append("', ") .append("path='").append(this.path).append("', ").append("decode404=")
.append("path='").append(path).append("', ") .append(this.decode404).append(", ").append("applicationContext=")
.append("decode404=").append(decode404).append(", ") .append(this.applicationContext).append(", ").append("fallback=")
.append("applicationContext=").append(applicationContext).append(", ") .append(this.fallback).append(", ").append("fallbackFactory=")
.append("fallback=").append(fallback).append(", ") .append(this.fallbackFactory).append("}").toString();
.append("fallbackFactory=").append(fallbackFactory)
.append("}").toString();
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2017 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,8 +13,14 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.cloud.openfeign; package org.springframework.cloud.openfeign;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import feign.Contract; import feign.Contract;
import feign.Logger; import feign.Logger;
import feign.RequestInterceptor; import feign.RequestInterceptor;
@@ -22,12 +28,8 @@ import feign.Retryer;
import feign.codec.Decoder; import feign.codec.Decoder;
import feign.codec.Encoder; import feign.codec.Encoder;
import feign.codec.ErrorDecoder; import feign.codec.ErrorDecoder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.HashMap; import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.List;
import java.util.Map;
import java.util.Objects;
/** /**
* @author Eko Kurniawan Khannedy * @author Eko Kurniawan Khannedy
@@ -42,7 +44,7 @@ public class FeignClientProperties {
private Map<String, FeignClientConfiguration> config = new HashMap<>(); private Map<String, FeignClientConfiguration> config = new HashMap<>();
public boolean isDefaultToProperties() { public boolean isDefaultToProperties() {
return defaultToProperties; return this.defaultToProperties;
} }
public void setDefaultToProperties(boolean defaultToProperties) { public void setDefaultToProperties(boolean defaultToProperties) {
@@ -50,7 +52,7 @@ public class FeignClientProperties {
} }
public String getDefaultConfig() { public String getDefaultConfig() {
return defaultConfig; return this.defaultConfig;
} }
public void setDefaultConfig(String defaultConfig) { public void setDefaultConfig(String defaultConfig) {
@@ -58,7 +60,7 @@ public class FeignClientProperties {
} }
public Map<String, FeignClientConfiguration> getConfig() { public Map<String, FeignClientConfiguration> getConfig() {
return config; return this.config;
} }
public void setConfig(Map<String, FeignClientConfiguration> config) { public void setConfig(Map<String, FeignClientConfiguration> config) {
@@ -67,19 +69,26 @@ public class FeignClientProperties {
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) {
if (o == null || getClass() != o.getClass()) return false; return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
FeignClientProperties that = (FeignClientProperties) o; FeignClientProperties that = (FeignClientProperties) o;
return defaultToProperties == that.defaultToProperties && return this.defaultToProperties == that.defaultToProperties
Objects.equals(defaultConfig, that.defaultConfig) && && Objects.equals(this.defaultConfig, that.defaultConfig)
Objects.equals(config, that.config); && Objects.equals(this.config, that.config);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(defaultToProperties, defaultConfig, config); return Objects.hash(this.defaultToProperties, this.defaultConfig, this.config);
} }
/**
* Feign client configuration.
*/
public static class FeignClientConfiguration { public static class FeignClientConfiguration {
private Logger.Level loggerLevel; private Logger.Level loggerLevel;
@@ -103,7 +112,7 @@ public class FeignClientProperties {
private Class<Contract> contract; private Class<Contract> contract;
public Logger.Level getLoggerLevel() { public Logger.Level getLoggerLevel() {
return loggerLevel; return this.loggerLevel;
} }
public void setLoggerLevel(Logger.Level loggerLevel) { public void setLoggerLevel(Logger.Level loggerLevel) {
@@ -111,7 +120,7 @@ public class FeignClientProperties {
} }
public Integer getConnectTimeout() { public Integer getConnectTimeout() {
return connectTimeout; return this.connectTimeout;
} }
public void setConnectTimeout(Integer connectTimeout) { public void setConnectTimeout(Integer connectTimeout) {
@@ -119,7 +128,7 @@ public class FeignClientProperties {
} }
public Integer getReadTimeout() { public Integer getReadTimeout() {
return readTimeout; return this.readTimeout;
} }
public void setReadTimeout(Integer readTimeout) { public void setReadTimeout(Integer readTimeout) {
@@ -127,7 +136,7 @@ public class FeignClientProperties {
} }
public Class<Retryer> getRetryer() { public Class<Retryer> getRetryer() {
return retryer; return this.retryer;
} }
public void setRetryer(Class<Retryer> retryer) { public void setRetryer(Class<Retryer> retryer) {
@@ -135,7 +144,7 @@ public class FeignClientProperties {
} }
public Class<ErrorDecoder> getErrorDecoder() { public Class<ErrorDecoder> getErrorDecoder() {
return errorDecoder; return this.errorDecoder;
} }
public void setErrorDecoder(Class<ErrorDecoder> errorDecoder) { public void setErrorDecoder(Class<ErrorDecoder> errorDecoder) {
@@ -143,15 +152,16 @@ public class FeignClientProperties {
} }
public List<Class<RequestInterceptor>> getRequestInterceptors() { public List<Class<RequestInterceptor>> getRequestInterceptors() {
return requestInterceptors; return this.requestInterceptors;
} }
public void setRequestInterceptors(List<Class<RequestInterceptor>> requestInterceptors) { public void setRequestInterceptors(
List<Class<RequestInterceptor>> requestInterceptors) {
this.requestInterceptors = requestInterceptors; this.requestInterceptors = requestInterceptors;
} }
public Boolean getDecode404() { public Boolean getDecode404() {
return decode404; return this.decode404;
} }
public void setDecode404(Boolean decode404) { public void setDecode404(Boolean decode404) {
@@ -159,7 +169,7 @@ public class FeignClientProperties {
} }
public Class<Decoder> getDecoder() { public Class<Decoder> getDecoder() {
return decoder; return this.decoder;
} }
public void setDecoder(Class<Decoder> decoder) { public void setDecoder(Class<Decoder> decoder) {
@@ -167,7 +177,7 @@ public class FeignClientProperties {
} }
public Class<Encoder> getEncoder() { public Class<Encoder> getEncoder() {
return encoder; return this.encoder;
} }
public void setEncoder(Class<Encoder> encoder) { public void setEncoder(Class<Encoder> encoder) {
@@ -175,7 +185,7 @@ public class FeignClientProperties {
} }
public Class<Contract> getContract() { public Class<Contract> getContract() {
return contract; return this.contract;
} }
public void setContract(Class<Contract> contract) { public void setContract(Class<Contract> contract) {
@@ -184,26 +194,32 @@ public class FeignClientProperties {
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) {
if (o == null || getClass() != o.getClass()) return false; return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
FeignClientConfiguration that = (FeignClientConfiguration) o; FeignClientConfiguration that = (FeignClientConfiguration) o;
return loggerLevel == that.loggerLevel && return this.loggerLevel == that.loggerLevel
Objects.equals(connectTimeout, that.connectTimeout) && && Objects.equals(this.connectTimeout, that.connectTimeout)
Objects.equals(readTimeout, that.readTimeout) && && Objects.equals(this.readTimeout, that.readTimeout)
Objects.equals(retryer, that.retryer) && && Objects.equals(this.retryer, that.retryer)
Objects.equals(errorDecoder, that.errorDecoder) && && Objects.equals(this.errorDecoder, that.errorDecoder)
Objects.equals(requestInterceptors, that.requestInterceptors) && && Objects.equals(this.requestInterceptors, that.requestInterceptors)
Objects.equals(decode404, that.decode404) && && Objects.equals(this.decode404, that.decode404)
Objects.equals(encoder, that.encoder) && && Objects.equals(this.encoder, that.encoder)
Objects.equals(decoder, that.decoder) && && Objects.equals(this.decoder, that.decoder)
Objects.equals(contract, that.contract); && Objects.equals(this.contract, that.contract);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(loggerLevel, connectTimeout, readTimeout, retryer, return Objects.hash(this.loggerLevel, this.connectTimeout, this.readTimeout,
errorDecoder, requestInterceptors, decode404, encoder, decoder, contract); this.retryer, this.errorDecoder, this.requestInterceptors,
this.decode404, this.encoder, this.decoder, this.contract);
} }
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2015 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -16,11 +16,11 @@
package org.springframework.cloud.openfeign; package org.springframework.cloud.openfeign;
import org.springframework.cloud.context.named.NamedContextFactory;
import java.util.Arrays; import java.util.Arrays;
import java.util.Objects; import java.util.Objects;
import org.springframework.cloud.context.named.NamedContextFactory;
/** /**
* @author Dave Syer * @author Dave Syer
* @author Gregor Zurowski * @author Gregor Zurowski
@@ -31,15 +31,16 @@ class FeignClientSpecification implements NamedContextFactory.Specification {
private Class<?>[] configuration; private Class<?>[] configuration;
public FeignClientSpecification() {} FeignClientSpecification() {
}
public FeignClientSpecification(String name, Class<?>[] configuration) { FeignClientSpecification(String name, Class<?>[] configuration) {
this.name = name; this.name = name;
this.configuration = configuration; this.configuration = configuration;
} }
public String getName() { public String getName() {
return name; return this.name;
} }
public void setName(String name) { public void setName(String name) {
@@ -47,7 +48,7 @@ class FeignClientSpecification implements NamedContextFactory.Specification {
} }
public Class<?>[] getConfiguration() { public Class<?>[] getConfiguration() {
return configuration; return this.configuration;
} }
public void setConfiguration(Class<?>[] configuration) { public void setConfiguration(Class<?>[] configuration) {
@@ -56,24 +57,27 @@ class FeignClientSpecification implements NamedContextFactory.Specification {
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) {
if (o == null || getClass() != o.getClass()) return false; return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
FeignClientSpecification that = (FeignClientSpecification) o; FeignClientSpecification that = (FeignClientSpecification) o;
return Objects.equals(name, that.name) && return Objects.equals(this.name, that.name)
Arrays.equals(configuration, that.configuration); && Arrays.equals(this.configuration, that.configuration);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(name, configuration); return Objects.hash(this.name, this.configuration);
} }
@Override @Override
public String toString() { public String toString() {
return new StringBuilder("FeignClientSpecification{") return new StringBuilder("FeignClientSpecification{").append("name='")
.append("name='").append(name).append("', ") .append(this.name).append("', ").append("configuration=")
.append("configuration=").append(Arrays.toString(configuration)) .append(Arrays.toString(this.configuration)).append("}").toString();
.append("}").toString();
} }
} }
@@ -1,18 +1,17 @@
/* /*
* Copyright 2013-2017 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*
*/ */
package org.springframework.cloud.openfeign; package org.springframework.cloud.openfeign;
@@ -20,12 +19,26 @@ package org.springframework.cloud.openfeign;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import com.fasterxml.jackson.databind.Module;
import com.netflix.hystrix.HystrixCommand;
import feign.Contract;
import feign.Feign;
import feign.Logger;
import feign.Retryer;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.hystrix.HystrixFeign;
import feign.optionals.OptionalDecoder;
import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.ObjectFactory;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.PageJacksonModule;
import org.springframework.cloud.openfeign.support.PageableSpringEncoder;
import org.springframework.cloud.openfeign.support.ResponseEntityDecoder; import org.springframework.cloud.openfeign.support.ResponseEntityDecoder;
import org.springframework.cloud.openfeign.support.SpringDecoder; import org.springframework.cloud.openfeign.support.SpringDecoder;
import org.springframework.cloud.openfeign.support.SpringEncoder; import org.springframework.cloud.openfeign.support.SpringEncoder;
@@ -37,17 +50,6 @@ import org.springframework.core.convert.ConversionService;
import org.springframework.format.support.DefaultFormattingConversionService; import org.springframework.format.support.DefaultFormattingConversionService;
import org.springframework.format.support.FormattingConversionService; import org.springframework.format.support.FormattingConversionService;
import com.netflix.hystrix.HystrixCommand;
import feign.Contract;
import feign.Feign;
import feign.Logger;
import feign.Retryer;
import feign.codec.Decoder;
import feign.codec.Encoder;
import feign.hystrix.HystrixFeign;
import feign.optionals.OptionalDecoder;
/** /**
* @author Dave Syer * @author Dave Syer
* @author Venil Noronha * @author Venil Noronha
@@ -70,15 +72,24 @@ public class FeignClientsConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public Decoder feignDecoder() { public Decoder feignDecoder() {
return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters))); return new OptionalDecoder(
new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));
} }
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
@ConditionalOnMissingClass("org.springframework.data.domain.Pageable")
public Encoder feignEncoder() { public Encoder feignEncoder() {
return new SpringEncoder(this.messageConverters); return new SpringEncoder(this.messageConverters);
} }
@Bean
@ConditionalOnClass(name = "org.springframework.data.domain.Pageable")
@ConditionalOnMissingBean
public Encoder feignEncoderPageable() {
return new PageableSpringEncoder(new SpringEncoder(this.messageConverters));
}
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public Contract feignContract(ConversionService feignConversionService) { public Contract feignContract(ConversionService feignConversionService) {
@@ -88,24 +99,12 @@ public class FeignClientsConfiguration {
@Bean @Bean
public FormattingConversionService feignConversionService() { public FormattingConversionService feignConversionService() {
FormattingConversionService conversionService = new DefaultFormattingConversionService(); FormattingConversionService conversionService = new DefaultFormattingConversionService();
for (FeignFormatterRegistrar feignFormatterRegistrar : feignFormatterRegistrars) { for (FeignFormatterRegistrar feignFormatterRegistrar : this.feignFormatterRegistrars) {
feignFormatterRegistrar.registerFormatters(conversionService); feignFormatterRegistrar.registerFormatters(conversionService);
} }
return conversionService; return conversionService;
} }
@Configuration
@ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
protected static class HystrixFeignConfiguration {
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "feign.hystrix.enabled")
public Feign.Builder feignHystrixBuilder() {
return HystrixFeign.builder();
}
}
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public Retryer feignRetryer() { public Retryer feignRetryer() {
@@ -122,7 +121,27 @@ public class FeignClientsConfiguration {
@Bean @Bean
@ConditionalOnMissingBean(FeignLoggerFactory.class) @ConditionalOnMissingBean(FeignLoggerFactory.class)
public FeignLoggerFactory feignLoggerFactory() { public FeignLoggerFactory feignLoggerFactory() {
return new DefaultFeignLoggerFactory(logger); return new DefaultFeignLoggerFactory(this.logger);
}
@Bean
@ConditionalOnClass(name = "org.springframework.data.domain.Page")
public Module pageJacksonModule() {
return new PageJacksonModule();
}
@Configuration
@ConditionalOnClass({ HystrixCommand.class, HystrixFeign.class })
protected static class HystrixFeignConfiguration {
@Bean
@Scope("prototype")
@ConditionalOnMissingBean
@ConditionalOnProperty(name = "feign.hystrix.enabled")
public Feign.Builder feignHystrixBuilder() {
return HystrixFeign.builder();
}
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2016 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -58,8 +58,8 @@ import org.springframework.util.StringUtils;
* @author Venil Noronha * @author Venil Noronha
* @author Gang Li * @author Gang Li
*/ */
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, class FeignClientsRegistrar
ResourceLoaderAware, EnvironmentAware { implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {
// patterned after Spring Integration IntegrationComponentScanRegistrar // patterned after Spring Integration IntegrationComponentScanRegistrar
// and RibbonClientsConfigurationRegistgrar // and RibbonClientsConfigurationRegistgrar
@@ -68,7 +68,68 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,
private Environment environment; private Environment environment;
public FeignClientsRegistrar() { FeignClientsRegistrar() {
}
static void validateFallback(final Class clazz) {
Assert.isTrue(!clazz.isInterface(),
"Fallback class must implement the interface annotated by @FeignClient");
}
static void validateFallbackFactory(final Class clazz) {
Assert.isTrue(!clazz.isInterface(), "Fallback factory must produce instances "
+ "of fallback classes that implement the interface annotated by @FeignClient");
}
static String getName(String name) {
if (!StringUtils.hasText(name)) {
return "";
}
String host = null;
try {
String url;
if (!name.startsWith("http://") && !name.startsWith("https://")) {
url = "http://" + name;
}
else {
url = name;
}
host = new URI(url).getHost();
}
catch (URISyntaxException e) {
}
Assert.state(host != null, "Service id not legal hostname (" + name + ")");
return name;
}
static String getUrl(String url) {
if (StringUtils.hasText(url) && !(url.startsWith("#{") && url.contains("}"))) {
if (!url.contains("://")) {
url = "http://" + url;
}
try {
new URL(url);
}
catch (MalformedURLException e) {
throw new IllegalArgumentException(url + " is malformed", e);
}
}
return url;
}
static String getPath(String path) {
if (StringUtils.hasText(path)) {
path = path.trim();
if (!path.startsWith("/")) {
path = "/" + path;
}
if (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
}
return path;
} }
@Override @Override
@@ -171,16 +232,19 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,
definition.addPropertyValue("path", getPath(attributes)); definition.addPropertyValue("path", getPath(attributes));
String name = getName(attributes); String name = getName(attributes);
definition.addPropertyValue("name", name); definition.addPropertyValue("name", name);
String contextId = getContextId(attributes);
definition.addPropertyValue("contextId", contextId);
definition.addPropertyValue("type", className); definition.addPropertyValue("type", className);
definition.addPropertyValue("decode404", attributes.get("decode404")); definition.addPropertyValue("decode404", attributes.get("decode404"));
definition.addPropertyValue("fallback", attributes.get("fallback")); definition.addPropertyValue("fallback", attributes.get("fallback"));
definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory")); definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory"));
definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);
String alias = name + "FeignClient"; String alias = contextId + "FeignClient";
AbstractBeanDefinition beanDefinition = definition.getBeanDefinition(); AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
boolean primary = (Boolean)attributes.get("primary"); // has a default, won't be null boolean primary = (Boolean) attributes.get("primary"); // has a default, won't be
// null
beanDefinition.setPrimary(primary); beanDefinition.setPrimary(primary);
@@ -198,14 +262,8 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,
AnnotationAttributes annotation = AnnotationAttributes.fromMap(attributes); AnnotationAttributes annotation = AnnotationAttributes.fromMap(attributes);
// This blows up if an aliased property is overspecified // This blows up if an aliased property is overspecified
// FIXME annotation.getAliasedString("name", FeignClient.class, null); // FIXME annotation.getAliasedString("name", FeignClient.class, null);
Assert.isTrue( validateFallback(annotation.getClass("fallback"));
!annotation.getClass("fallback").isInterface(), validateFallbackFactory(annotation.getClass("fallbackFactory"));
"Fallback class must implement the interface annotated by @FeignClient"
);
Assert.isTrue(
!annotation.getClass("fallbackFactory").isInterface(),
"Fallback factory must produce instances of fallback classes that implement the interface annotated by @FeignClient"
);
} }
/* for testing */ String getName(Map<String, Object> attributes) { /* for testing */ String getName(Map<String, Object> attributes) {
@@ -217,25 +275,17 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,
name = (String) attributes.get("value"); name = (String) attributes.get("value");
} }
name = resolve(name); name = resolve(name);
if (!StringUtils.hasText(name)) { return getName(name);
return ""; }
private String getContextId(Map<String, Object> attributes) {
String contextId = (String) attributes.get("contextId");
if (!StringUtils.hasText(contextId)) {
return getName(attributes);
} }
String host = null; contextId = resolve(contextId);
try { return getName(contextId);
String url;
if (!name.startsWith("http://") && !name.startsWith("https://")) {
url = "http://" + name;
} else {
url = name;
}
host = new URI(url).getHost();
}
catch (URISyntaxException e) {
}
Assert.state(host != null, "Service id not legal hostname (" + name + ")");
return name;
} }
private String resolve(String value) { private String resolve(String value) {
@@ -247,38 +297,19 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,
private String getUrl(Map<String, Object> attributes) { private String getUrl(Map<String, Object> attributes) {
String url = resolve((String) attributes.get("url")); String url = resolve((String) attributes.get("url"));
if (StringUtils.hasText(url) && !(url.startsWith("#{") && url.contains("}"))) { return getUrl(url);
if (!url.contains("://")) {
url = "http://" + url;
}
try {
new URL(url);
}
catch (MalformedURLException e) {
throw new IllegalArgumentException(url + " is malformed", e);
}
}
return url;
} }
private String getPath(Map<String, Object> attributes) { private String getPath(Map<String, Object> attributes) {
String path = resolve((String) attributes.get("path")); String path = resolve((String) attributes.get("path"));
if (StringUtils.hasText(path)) { return getPath(path);
path = path.trim();
if (!path.startsWith("/")) {
path = "/" + path;
}
if (path.endsWith("/")) {
path = path.substring(0, path.length() - 1);
}
}
return path;
} }
protected ClassPathScanningCandidateComponentProvider getScanner() { protected ClassPathScanningCandidateComponentProvider getScanner() {
return new ClassPathScanningCandidateComponentProvider(false, this.environment) { return new ClassPathScanningCandidateComponentProvider(false, this.environment) {
@Override @Override
protected boolean isCandidateComponent(AnnotatedBeanDefinition beanDefinition) { protected boolean isCandidateComponent(
AnnotatedBeanDefinition beanDefinition) {
boolean isCandidate = false; boolean isCandidate = false;
if (beanDefinition.getMetadata().isIndependent()) { if (beanDefinition.getMetadata().isIndependent()) {
if (!beanDefinition.getMetadata().isAnnotation()) { if (!beanDefinition.getMetadata().isAnnotation()) {
@@ -315,7 +346,7 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,
} }
return basePackages; return basePackages;
} }
private String getQualifier(Map<String, Object> client) { private String getQualifier(Map<String, Object> client) {
if (client == null) { if (client == null) {
return null; return null;
@@ -331,7 +362,10 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,
if (client == null) { if (client == null) {
return null; return null;
} }
String value = (String) client.get("value"); String value = (String) client.get("contextId");
if (!StringUtils.hasText(value)) {
value = (String) client.get("value");
}
if (!StringUtils.hasText(value)) { if (!StringUtils.hasText(value)) {
value = (String) client.get("name"); value = (String) client.get("name");
} }
@@ -374,10 +408,9 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,
/** /**
* Creates a new {@link AllTypeFilter} to match if all the given delegates match. * Creates a new {@link AllTypeFilter} to match if all the given delegates match.
*
* @param delegates must not be {@literal null}. * @param delegates must not be {@literal null}.
*/ */
public AllTypeFilter(List<TypeFilter> delegates) { AllTypeFilter(List<TypeFilter> delegates) {
Assert.notNull(delegates, "This argument is required, it must not be null"); Assert.notNull(delegates, "This argument is required, it must not be null");
this.delegates = delegates; this.delegates = delegates;
} }
@@ -394,5 +427,7 @@ class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar,
return true; return true;
} }
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2015 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2016 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -1,11 +1,11 @@
/* /*
* Copyright 2016 the original author or authors. * Copyright 2016-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -27,10 +27,9 @@ public interface FeignLoggerFactory {
/** /**
* Factory method to provide a {@link Logger} for a given {@link Class}. * Factory method to provide a {@link Logger} for a given {@link Class}.
*
* @param type the {@link Class} for which a {@link Logger} instance is to be created * @param type the {@link Class} for which a {@link Logger} instance is to be created
* @return a {@link Logger} instance * @return a {@link Logger} instance
*/ */
public Logger create(Class<?> type); Logger create(Class<?> type);
} }
@@ -1,30 +1,29 @@
/* /*
* Copyright 2013-2016 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*
*/ */
package org.springframework.cloud.openfeign; package org.springframework.cloud.openfeign;
import org.springframework.util.Assert;
import feign.Feign; import feign.Feign;
import feign.Target; import feign.Target;
import feign.hystrix.FallbackFactory; import feign.hystrix.FallbackFactory;
import feign.hystrix.HystrixFeign; import feign.hystrix.HystrixFeign;
import feign.hystrix.SetterFactory; import feign.hystrix.SetterFactory;
import org.springframework.util.StringUtils;
/** /**
* @author Spencer Gibb * @author Spencer Gibb
* @author Erik Kringen * @author Erik Kringen
@@ -33,79 +32,70 @@ import feign.hystrix.SetterFactory;
class HystrixTargeter implements Targeter { class HystrixTargeter implements Targeter {
@Override @Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context, public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
Target.HardCodedTarget<T> target) { FeignContext context, Target.HardCodedTarget<T> target) {
if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) { if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) {
return feign.target(target); return feign.target(target);
} }
feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign; feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign;
SetterFactory setterFactory = getOptional(factory.getName(), context, String name = StringUtils.isEmpty(factory.getContextId()) ? factory.getName()
SetterFactory.class); : factory.getContextId();
SetterFactory setterFactory = getOptional(name, context, SetterFactory.class);
if (setterFactory != null) { if (setterFactory != null) {
builder.setterFactory(setterFactory); builder.setterFactory(setterFactory);
} }
Class<?> fallback = factory.getFallback(); Class<?> fallback = factory.getFallback();
if (fallback != void.class) { if (fallback != void.class) {
return targetWithFallback(factory.getName(), context, target, builder, fallback); return targetWithFallback(name, context, target, builder, fallback);
} }
Class<?> fallbackFactory = factory.getFallbackFactory(); Class<?> fallbackFactory = factory.getFallbackFactory();
if (fallbackFactory != void.class) { if (fallbackFactory != void.class) {
return targetWithFallbackFactory(factory.getName(), context, target, builder, fallbackFactory); return targetWithFallbackFactory(name, context, target, builder,
fallbackFactory);
} }
return feign.target(target); return feign.target(target);
} }
private <T> T targetWithFallbackFactory(String feignClientName, FeignContext context, private <T> T targetWithFallbackFactory(String feignClientName, FeignContext context,
Target.HardCodedTarget<T> target, Target.HardCodedTarget<T> target, HystrixFeign.Builder builder,
HystrixFeign.Builder builder, Class<?> fallbackFactoryClass) {
Class<?> fallbackFactoryClass) { FallbackFactory<? extends T> fallbackFactory = (FallbackFactory<? extends T>) getFromContext(
FallbackFactory<? extends T> fallbackFactory = (FallbackFactory<? extends T>) "fallbackFactory", feignClientName, context, fallbackFactoryClass,
getFromContext("fallbackFactory", feignClientName, context, fallbackFactoryClass, FallbackFactory.class); FallbackFactory.class);
/* We take a sample fallback from the fallback factory to check if it returns a fallback
that is compatible with the annotated feign interface. */
Object exampleFallback = fallbackFactory.create(new RuntimeException());
Assert.notNull(exampleFallback,
String.format(
"Incompatible fallbackFactory instance for feign client %s. Factory may not produce null!",
feignClientName));
if (!target.type().isAssignableFrom(exampleFallback.getClass())) {
throw new IllegalStateException(
String.format(
"Incompatible fallbackFactory instance for feign client %s. Factory produces instances of '%s', but should produce instances of '%s'",
feignClientName, exampleFallback.getClass(), target.type()));
}
return builder.target(target, fallbackFactory); return builder.target(target, fallbackFactory);
} }
private <T> T targetWithFallback(String feignClientName, FeignContext context, private <T> T targetWithFallback(String feignClientName, FeignContext context,
Target.HardCodedTarget<T> target, Target.HardCodedTarget<T> target, HystrixFeign.Builder builder,
HystrixFeign.Builder builder, Class<?> fallback) { Class<?> fallback) {
T fallbackInstance = getFromContext("fallback", feignClientName, context, fallback, target.type()); T fallbackInstance = getFromContext("fallback", feignClientName, context,
fallback, target.type());
return builder.target(target, fallbackInstance); return builder.target(target, fallbackInstance);
} }
private <T> T getFromContext(String fallbackMechanism, String feignClientName, FeignContext context, private <T> T getFromContext(String fallbackMechanism, String feignClientName,
Class<?> beanType, Class<T> targetType) { FeignContext context, Class<?> beanType, Class<T> targetType) {
Object fallbackInstance = context.getInstance(feignClientName, beanType); Object fallbackInstance = context.getInstance(feignClientName, beanType);
if (fallbackInstance == null) { if (fallbackInstance == null) {
throw new IllegalStateException(String.format( throw new IllegalStateException(String.format(
"No " + fallbackMechanism + " instance of type %s found for feign client %s", "No " + fallbackMechanism
beanType, feignClientName)); + " instance of type %s found for feign client %s",
beanType, feignClientName));
} }
if (!targetType.isAssignableFrom(beanType)) { if (!targetType.isAssignableFrom(beanType)) {
throw new IllegalStateException( throw new IllegalStateException(String.format("Incompatible "
String.format( + fallbackMechanism
"Incompatible " + fallbackMechanism + " instance. Fallback/fallbackFactory of type %s is not assignable to %s for feign client %s", + " instance. Fallback/fallbackFactory of type %s is not assignable to %s for feign client %s",
beanType, targetType, feignClientName)); beanType, targetType, feignClientName));
} }
return (T) fallbackInstance; return (T) fallbackInstance;
} }
private <T> T getOptional(String feignClientName, FeignContext context, private <T> T getOptional(String feignClientName, FeignContext context,
Class<T> beanType) { Class<T> beanType) {
return context.getInstance(feignClientName, beanType); return context.getInstance(feignClientName, beanType);
} }
} }
@@ -0,0 +1,53 @@
/*
* Copyright 2013-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import feign.QueryMap;
import org.springframework.core.annotation.AliasFor;
/**
* Spring MVC equivalent of OpenFeign's {@link feign.QueryMap} parameter annotation.
*
* @author Aram Peres
* @see feign.QueryMap
* @see org.springframework.cloud.openfeign.annotation.QueryMapParameterProcessor
*/
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.PARAMETER })
public @interface SpringQueryMap {
/**
* @see QueryMap#encoded()
* @return alias for {@link #encoded()}.
*/
@AliasFor("encoded")
boolean value() default false;
/**
* @see QueryMap#encoded()
* @return Specifies whether parameter names and values are already encoded.
*/
@AliasFor("value")
boolean encoded() default false;
}
@@ -1,18 +1,17 @@
/* /*
* Copyright 2013-2016 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*
*/ */
package org.springframework.cloud.openfeign; package org.springframework.cloud.openfeign;
@@ -24,6 +23,8 @@ import feign.Target;
* @author Spencer Gibb * @author Spencer Gibb
*/ */
interface Targeter { interface Targeter {
<T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
Target.HardCodedTarget<T> target); <T> T target(FeignClientFactoryBean factory, Feign.Builder feign,
FeignContext context, Target.HardCodedTarget<T> target);
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2015 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -21,11 +21,11 @@ import java.lang.reflect.Method;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
import feign.MethodMetadata;
import org.springframework.cloud.openfeign.AnnotatedParameterProcessor; import org.springframework.cloud.openfeign.AnnotatedParameterProcessor;
import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PathVariable;
import feign.MethodMetadata;
import static feign.Util.checkState; import static feign.Util.checkState;
import static feign.Util.emptyToNull; import static feign.Util.emptyToNull;
@@ -46,10 +46,12 @@ public class PathVariableParameterProcessor implements AnnotatedParameterProcess
} }
@Override @Override
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) { public boolean processArgument(AnnotatedParameterContext context,
Annotation annotation, Method method) {
String name = ANNOTATION.cast(annotation).value(); String name = ANNOTATION.cast(annotation).value();
checkState(emptyToNull(name) != null, checkState(emptyToNull(name) != null,
"PathVariable annotation was empty on param %s.", context.getParameterIndex()); "PathVariable annotation was empty on param %s.",
context.getParameterIndex());
context.setParameterName(name); context.setParameterName(name);
MethodMetadata data = context.getMethodMetadata(); MethodMetadata data = context.getMethodMetadata();
@@ -74,4 +76,5 @@ public class PathVariableParameterProcessor implements AnnotatedParameterProcess
} }
return false; return false;
} }
} }
@@ -0,0 +1,54 @@
/*
* Copyright 2013-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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 feign.MethodMetadata;
import org.springframework.cloud.openfeign.AnnotatedParameterProcessor;
import org.springframework.cloud.openfeign.SpringQueryMap;
/**
* {@link SpringQueryMap} parameter processor.
*
* @author Aram Peres
* @see AnnotatedParameterProcessor
*/
public class QueryMapParameterProcessor implements AnnotatedParameterProcessor {
private static final Class<SpringQueryMap> ANNOTATION = SpringQueryMap.class;
@Override
public Class<? extends Annotation> getAnnotationType() {
return ANNOTATION;
}
@Override
public boolean processArgument(AnnotatedParameterContext context,
Annotation annotation, Method method) {
int paramIndex = context.getParameterIndex();
MethodMetadata metadata = context.getMethodMetadata();
if (metadata.queryMapIndex() == null) {
metadata.queryMapIndex(paramIndex);
metadata.queryMapEncoded(SpringQueryMap.class.cast(annotation).encoded());
}
return true;
}
}
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2015 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -21,11 +21,11 @@ import java.lang.reflect.Method;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
import feign.MethodMetadata;
import org.springframework.cloud.openfeign.AnnotatedParameterProcessor; import org.springframework.cloud.openfeign.AnnotatedParameterProcessor;
import org.springframework.web.bind.annotation.RequestHeader; import org.springframework.web.bind.annotation.RequestHeader;
import feign.MethodMetadata;
import static feign.Util.checkState; import static feign.Util.checkState;
import static feign.Util.emptyToNull; import static feign.Util.emptyToNull;
@@ -46,13 +46,15 @@ public class RequestHeaderParameterProcessor implements AnnotatedParameterProces
} }
@Override @Override
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) { public boolean processArgument(AnnotatedParameterContext context,
Annotation annotation, Method method) {
int parameterIndex = context.getParameterIndex(); int parameterIndex = context.getParameterIndex();
Class<?> parameterType = method.getParameterTypes()[parameterIndex]; Class<?> parameterType = method.getParameterTypes()[parameterIndex];
MethodMetadata data = context.getMethodMetadata(); MethodMetadata data = context.getMethodMetadata();
if (Map.class.isAssignableFrom(parameterType)) { if (Map.class.isAssignableFrom(parameterType)) {
checkState(data.headerMapIndex() == null, "Header map can only be present once."); checkState(data.headerMapIndex() == null,
"Header map can only be present once.");
data.headerMapIndex(parameterIndex); data.headerMapIndex(parameterIndex);
return true; return true;
@@ -63,8 +65,10 @@ public class RequestHeaderParameterProcessor implements AnnotatedParameterProces
"RequestHeader.value() was empty on parameter %s", parameterIndex); "RequestHeader.value() was empty on parameter %s", parameterIndex);
context.setParameterName(name); context.setParameterName(name);
Collection<String> header = context.setTemplateParameter(name, data.template().headers().get(name)); Collection<String> header = context.setTemplateParameter(name,
data.template().headers().get(name));
data.template().header(name, header); data.template().header(name, header);
return true; return true;
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2015 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -21,14 +21,14 @@ import java.lang.reflect.Method;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
import feign.MethodMetadata;
import org.springframework.cloud.openfeign.AnnotatedParameterProcessor; import org.springframework.cloud.openfeign.AnnotatedParameterProcessor;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import static feign.Util.checkState; import static feign.Util.checkState;
import static feign.Util.emptyToNull; import static feign.Util.emptyToNull;
import feign.MethodMetadata;
/** /**
* {@link RequestParam} parameter processor. * {@link RequestParam} parameter processor.
* *
@@ -46,13 +46,15 @@ public class RequestParamParameterProcessor implements AnnotatedParameterProcess
} }
@Override @Override
public boolean processArgument(AnnotatedParameterContext context, Annotation annotation, Method method) { public boolean processArgument(AnnotatedParameterContext context,
Annotation annotation, Method method) {
int parameterIndex = context.getParameterIndex(); int parameterIndex = context.getParameterIndex();
Class<?> parameterType = method.getParameterTypes()[parameterIndex]; Class<?> parameterType = method.getParameterTypes()[parameterIndex];
MethodMetadata data = context.getMethodMetadata(); MethodMetadata data = context.getMethodMetadata();
if (Map.class.isAssignableFrom(parameterType)) { if (Map.class.isAssignableFrom(parameterType)) {
checkState(data.queryMapIndex() == null, "Query map can only be present once."); checkState(data.queryMapIndex() == null,
"Query map can only be present once.");
data.queryMapIndex(parameterIndex); data.queryMapIndex(parameterIndex);
return true; return true;
@@ -61,8 +63,7 @@ public class RequestParamParameterProcessor implements AnnotatedParameterProcess
RequestParam requestParam = ANNOTATION.cast(annotation); RequestParam requestParam = ANNOTATION.cast(annotation);
String name = requestParam.value(); String name = requestParam.value();
checkState(emptyToNull(name) != null, checkState(emptyToNull(name) != null,
"RequestParam.value() was empty on parameter %s", "RequestParam.value() was empty on parameter %s", parameterIndex);
parameterIndex);
context.setParameterName(name); context.setParameterName(name);
Collection<String> query = context.setTemplateParameter(name, Collection<String> query = context.setTemplateParameter(name,
@@ -70,4 +71,5 @@ public class RequestParamParameterProcessor implements AnnotatedParameterProcess
data.template().query(name, query); data.template().query(name, query);
return true; return true;
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2017 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -18,6 +18,7 @@ package org.springframework.cloud.openfeign.encoding;
import feign.RequestInterceptor; import feign.RequestInterceptor;
import feign.RequestTemplate; import feign.RequestTemplate;
import org.springframework.util.Assert; import org.springframework.util.Assert;
/** /**
@@ -34,7 +35,6 @@ public abstract class BaseRequestInterceptor implements RequestInterceptor {
/** /**
* Creates new instance of {@link BaseRequestInterceptor}. * Creates new instance of {@link BaseRequestInterceptor}.
*
* @param properties the encoding properties * @param properties the encoding properties
*/ */
protected BaseRequestInterceptor(FeignClientEncodingProperties properties) { protected BaseRequestInterceptor(FeignClientEncodingProperties properties) {
@@ -44,12 +44,12 @@ public abstract class BaseRequestInterceptor implements RequestInterceptor {
/** /**
* Adds the header if it wasn't yet specified. * Adds the header if it wasn't yet specified.
*
* @param requestTemplate the request * @param requestTemplate the request
* @param name the header name * @param name the header name
* @param values the header values * @param values the header values
*/ */
protected void addHeader(RequestTemplate requestTemplate, String name, String... values) { protected void addHeader(RequestTemplate requestTemplate, String name,
String... values) {
if (!requestTemplate.headers().containsKey(name)) { if (!requestTemplate.headers().containsKey(name)) {
requestTemplate.header(name, values); requestTemplate.header(name, values);
@@ -57,7 +57,7 @@ public abstract class BaseRequestInterceptor implements RequestInterceptor {
} }
protected FeignClientEncodingProperties getProperties() { protected FeignClientEncodingProperties getProperties() {
return properties; return this.properties;
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2017 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -40,14 +40,16 @@ import org.springframework.context.annotation.Configuration;
@ConditionalOnClass(Feign.class) @ConditionalOnClass(Feign.class)
@ConditionalOnBean(Client.class) @ConditionalOnBean(Client.class)
@ConditionalOnProperty(value = "feign.compression.response.enabled", matchIfMissing = false) @ConditionalOnProperty(value = "feign.compression.response.enabled", matchIfMissing = false)
//The OK HTTP client uses "transparent" compression. // The OK HTTP client uses "transparent" compression.
//If the accept-encoding header is present it disable transparent compression // If the accept-encoding header is present it disable transparent compression
@ConditionalOnMissingBean(type = "okhttp3.OkHttpClient") @ConditionalOnMissingBean(type = "okhttp3.OkHttpClient")
@AutoConfigureAfter(FeignAutoConfiguration.class) @AutoConfigureAfter(FeignAutoConfiguration.class)
public class FeignAcceptGzipEncodingAutoConfiguration { public class FeignAcceptGzipEncodingAutoConfiguration {
@Bean @Bean
public FeignAcceptGzipEncodingInterceptor feignAcceptGzipEncodingInterceptor(FeignClientEncodingProperties properties) { public FeignAcceptGzipEncodingInterceptor feignAcceptGzipEncodingInterceptor(
FeignClientEncodingProperties properties) {
return new FeignAcceptGzipEncodingInterceptor(properties); return new FeignAcceptGzipEncodingInterceptor(properties);
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2017 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -19,10 +19,11 @@ package org.springframework.cloud.openfeign.encoding;
import feign.RequestTemplate; import feign.RequestTemplate;
/** /**
* Enables the HTTP response payload compression by specifying the {@code Accept-Encoding} headers. * Enables the HTTP response payload compression by specifying the {@code Accept-Encoding}
* Although this does not yet mean that the requests will be compressed, it requires the remote server * headers. Although this does not yet mean that the requests will be compressed, it
* to understand the header and be configured to compress responses. Still no all responses might be compressed * requires the remote server to understand the header and be configured to compress
* based on the media type matching and other factors like the response content length. * responses. Still no all responses might be compressed based on the media type matching
* and other factors like the response content length.
* *
* @author Jakub Narloch * @author Jakub Narloch
*/ */
@@ -30,10 +31,10 @@ public class FeignAcceptGzipEncodingInterceptor extends BaseRequestInterceptor {
/** /**
* Creates new instance of {@link FeignAcceptGzipEncodingInterceptor}. * Creates new instance of {@link FeignAcceptGzipEncodingInterceptor}.
*
* @param properties the encoding properties * @param properties the encoding properties
*/ */
protected FeignAcceptGzipEncodingInterceptor(FeignClientEncodingProperties properties) { protected FeignAcceptGzipEncodingInterceptor(
FeignClientEncodingProperties properties) {
super(properties); super(properties);
} }
@@ -43,7 +44,8 @@ public class FeignAcceptGzipEncodingInterceptor extends BaseRequestInterceptor {
@Override @Override
public void apply(RequestTemplate template) { public void apply(RequestTemplate template) {
addHeader(template, HttpEncoding.ACCEPT_ENCODING_HEADER, HttpEncoding.GZIP_ENCODING, addHeader(template, HttpEncoding.ACCEPT_ENCODING_HEADER,
HttpEncoding.DEFLATE_ENCODING); HttpEncoding.GZIP_ENCODING, HttpEncoding.DEFLATE_ENCODING);
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2017 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -16,11 +16,11 @@
package org.springframework.cloud.openfeign.encoding; package org.springframework.cloud.openfeign.encoding;
import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.Arrays; import java.util.Arrays;
import java.util.Objects; import java.util.Objects;
import org.springframework.boot.context.properties.ConfigurationProperties;
/** /**
* The Feign encoding properties. * The Feign encoding properties.
* *
@@ -32,7 +32,8 @@ public class FeignClientEncodingProperties {
/** /**
* The list of supported mime types. * The list of supported mime types.
*/ */
private String[] mimeTypes = new String[]{"text/xml", "application/xml", "application/json"}; private String[] mimeTypes = new String[] { "text/xml", "application/xml",
"application/json" };
/** /**
* The minimum threshold content size. * The minimum threshold content size.
@@ -40,7 +41,7 @@ public class FeignClientEncodingProperties {
private int minRequestSize = 2048; private int minRequestSize = 2048;
public String[] getMimeTypes() { public String[] getMimeTypes() {
return mimeTypes; return this.mimeTypes;
} }
public void setMimeTypes(String[] mimeTypes) { public void setMimeTypes(String[] mimeTypes) {
@@ -48,7 +49,7 @@ public class FeignClientEncodingProperties {
} }
public int getMinRequestSize() { public int getMinRequestSize() {
return minRequestSize; return this.minRequestSize;
} }
public void setMinRequestSize(int minRequestSize) { public void setMinRequestSize(int minRequestSize) {
@@ -57,24 +58,28 @@ public class FeignClientEncodingProperties {
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) {
if (o == null || getClass() != o.getClass()) return false; return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
FeignClientEncodingProperties that = (FeignClientEncodingProperties) o; FeignClientEncodingProperties that = (FeignClientEncodingProperties) o;
return Arrays.equals(mimeTypes, that.mimeTypes) && return Arrays.equals(this.mimeTypes, that.mimeTypes)
Objects.equals(minRequestSize, that.minRequestSize); && Objects.equals(this.minRequestSize, that.minRequestSize);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(mimeTypes, minRequestSize); return Objects.hash(this.mimeTypes, this.minRequestSize);
} }
@Override @Override
public String toString() { public String toString() {
return new StringBuilder("FeignClientEncodingProperties{") return new StringBuilder("FeignClientEncodingProperties{").append("mimeTypes=")
.append("mimeTypes=").append(Arrays.toString(mimeTypes)).append(", ") .append(Arrays.toString(this.mimeTypes)).append(", ")
.append("minRequestSize=").append(minRequestSize) .append("minRequestSize=").append(this.minRequestSize).append("}")
.append("}").toString(); .toString();
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2017 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -39,15 +39,17 @@ import org.springframework.context.annotation.Configuration;
@EnableConfigurationProperties(FeignClientEncodingProperties.class) @EnableConfigurationProperties(FeignClientEncodingProperties.class)
@ConditionalOnClass(Feign.class) @ConditionalOnClass(Feign.class)
@ConditionalOnBean(Client.class) @ConditionalOnBean(Client.class)
//The OK HTTP client uses "transparent" compression. // The OK HTTP client uses "transparent" compression.
//If the content-encoding header is present it disable transparent compression // If the content-encoding header is present it disable transparent compression
@ConditionalOnMissingBean(type = "okhttp3.OkHttpClient") @ConditionalOnMissingBean(type = "okhttp3.OkHttpClient")
@ConditionalOnProperty(value = "feign.compression.request.enabled", matchIfMissing = false) @ConditionalOnProperty(value = "feign.compression.request.enabled", matchIfMissing = false)
@AutoConfigureAfter(FeignAutoConfiguration.class) @AutoConfigureAfter(FeignAutoConfiguration.class)
public class FeignContentGzipEncodingAutoConfiguration { public class FeignContentGzipEncodingAutoConfiguration {
@Bean @Bean
public FeignContentGzipEncodingInterceptor feignContentGzipEncodingInterceptor(FeignClientEncodingProperties properties) { public FeignContentGzipEncodingInterceptor feignContentGzipEncodingInterceptor(
FeignClientEncodingProperties properties) {
return new FeignContentGzipEncodingInterceptor(properties); return new FeignContentGzipEncodingInterceptor(properties);
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2017 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -16,13 +16,14 @@
package org.springframework.cloud.openfeign.encoding; package org.springframework.cloud.openfeign.encoding;
import feign.RequestTemplate;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
import feign.RequestTemplate;
/** /**
* Enables the HTTP request payload compression by specifying the {@code Content-Encoding} headers. * Enables the HTTP request payload compression by specifying the {@code Content-Encoding}
* headers.
* *
* @author Jakub Narloch * @author Jakub Narloch
*/ */
@@ -30,10 +31,10 @@ public class FeignContentGzipEncodingInterceptor extends BaseRequestInterceptor
/** /**
* Creates new instance of {@link FeignContentGzipEncodingInterceptor}. * Creates new instance of {@link FeignContentGzipEncodingInterceptor}.
*
* @param properties the encoding properties * @param properties the encoding properties
*/ */
protected FeignContentGzipEncodingInterceptor(FeignClientEncodingProperties properties) { protected FeignContentGzipEncodingInterceptor(
FeignClientEncodingProperties properties) {
super(properties); super(properties);
} }
@@ -44,14 +45,13 @@ public class FeignContentGzipEncodingInterceptor extends BaseRequestInterceptor
public void apply(RequestTemplate template) { public void apply(RequestTemplate template) {
if (requiresCompression(template)) { if (requiresCompression(template)) {
addHeader(template, HttpEncoding.CONTENT_ENCODING_HEADER, HttpEncoding.GZIP_ENCODING, addHeader(template, HttpEncoding.CONTENT_ENCODING_HEADER,
HttpEncoding.DEFLATE_ENCODING); HttpEncoding.GZIP_ENCODING, HttpEncoding.DEFLATE_ENCODING);
} }
} }
/** /**
* Returns whether the request requires GZIP compression. * Returns whether the request requires GZIP compression.
*
* @param template the request template * @param template the request template
* @return true if request requires compression, false otherwise * @return true if request requires compression, false otherwise
*/ */
@@ -64,7 +64,6 @@ public class FeignContentGzipEncodingInterceptor extends BaseRequestInterceptor
/** /**
* Returns whether the request content length exceed configured minimum size. * Returns whether the request content length exceed configured minimum size.
*
* @param contentLength the content length header value * @param contentLength the content length header value
* @return true if length is grater than minimum size, false otherwise * @return true if length is grater than minimum size, false otherwise
*/ */
@@ -78,15 +77,14 @@ public class FeignContentGzipEncodingInterceptor extends BaseRequestInterceptor
final String strLen = contentLength.iterator().next(); final String strLen = contentLength.iterator().next();
final long length = Long.parseLong(strLen); final long length = Long.parseLong(strLen);
return length > getProperties().getMinRequestSize(); return length > getProperties().getMinRequestSize();
} catch (NumberFormatException ex) {
// ignores the exception
} }
return false; catch (NumberFormatException ex) {
return false;
}
} }
/** /**
* Returns whether the content mime types matches the configures mime types. * Returns whether the content mime types matches the configures mime types.
*
* @param contentTypes the content types * @param contentTypes the content types
* @return true if any specified content type matches the request content types * @return true if any specified content type matches the request content types
*/ */
@@ -95,7 +93,8 @@ public class FeignContentGzipEncodingInterceptor extends BaseRequestInterceptor
return false; return false;
} }
if (getProperties().getMimeTypes() == null || getProperties().getMimeTypes().length == 0) { if (getProperties().getMimeTypes() == null
|| getProperties().getMimeTypes().length == 0) {
// no specific mime types has been set - matching everything // no specific mime types has been set - matching everything
return true; return true;
} }
@@ -108,4 +107,5 @@ public class FeignContentGzipEncodingInterceptor extends BaseRequestInterceptor
return false; return false;
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2017 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -52,4 +52,5 @@ public interface HttpEncoding {
* The Deflate encoding. * The Deflate encoding.
*/ */
String DEFLATE_ENCODING = "deflate"; String DEFLATE_ENCODING = "deflate";
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2015 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -18,14 +18,14 @@ package org.springframework.cloud.openfeign.ribbon;
import java.util.Map; import java.util.Map;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.ILoadBalancer;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
import org.springframework.cloud.netflix.ribbon.ServerIntrospector; import org.springframework.cloud.netflix.ribbon.ServerIntrospector;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory; import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.util.ConcurrentReferenceHashMap; import org.springframework.util.ConcurrentReferenceHashMap;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.ILoadBalancer;
/** /**
* Factory for SpringLoadBalancer instances that caches the entries created. * Factory for SpringLoadBalancer instances that caches the entries created.
* *
@@ -36,8 +36,9 @@ import com.netflix.loadbalancer.ILoadBalancer;
*/ */
public class CachingSpringLoadBalancerFactory { public class CachingSpringLoadBalancerFactory {
private final SpringClientFactory factory; protected final SpringClientFactory factory;
private LoadBalancedRetryFactory loadBalancedRetryFactory = null;
protected LoadBalancedRetryFactory loadBalancedRetryFactory = null;
private volatile Map<String, FeignLoadBalancer> cache = new ConcurrentReferenceHashMap<>(); private volatile Map<String, FeignLoadBalancer> cache = new ConcurrentReferenceHashMap<>();
@@ -45,23 +46,27 @@ public class CachingSpringLoadBalancerFactory {
this.factory = factory; this.factory = factory;
} }
public CachingSpringLoadBalancerFactory(SpringClientFactory factory, LoadBalancedRetryFactory loadBalancedRetryPolicyFactory) { public CachingSpringLoadBalancerFactory(SpringClientFactory factory,
LoadBalancedRetryFactory loadBalancedRetryPolicyFactory) {
this.factory = factory; this.factory = factory;
this.loadBalancedRetryFactory = loadBalancedRetryPolicyFactory; this.loadBalancedRetryFactory = loadBalancedRetryPolicyFactory;
} }
public FeignLoadBalancer create(String clientName) { public FeignLoadBalancer create(String clientName) {
FeignLoadBalancer client = this.cache.get(clientName); FeignLoadBalancer client = this.cache.get(clientName);
if(client != null) { if (client != null) {
return client; return client;
} }
IClientConfig config = this.factory.getClientConfig(clientName); IClientConfig config = this.factory.getClientConfig(clientName);
ILoadBalancer lb = this.factory.getLoadBalancer(clientName); ILoadBalancer lb = this.factory.getLoadBalancer(clientName);
ServerIntrospector serverIntrospector = this.factory.getInstance(clientName, ServerIntrospector.class); ServerIntrospector serverIntrospector = this.factory.getInstance(clientName,
client = loadBalancedRetryFactory != null ? new RetryableFeignLoadBalancer(lb, config, serverIntrospector, ServerIntrospector.class);
loadBalancedRetryFactory) : new FeignLoadBalancer(lb, config, serverIntrospector); client = this.loadBalancedRetryFactory != null
? new RetryableFeignLoadBalancer(lb, config, serverIntrospector,
this.loadBalancedRetryFactory)
: new FeignLoadBalancer(lb, config, serverIntrospector);
this.cache.put(clientName, client); this.cache.put(clientName, client);
return client; return client;
} }
} }
@@ -1,23 +1,23 @@
/* /*
* Copyright 2013-2017 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*
*/ */
package org.springframework.cloud.openfeign.ribbon; package org.springframework.cloud.openfeign.ribbon;
import feign.Client; import feign.Client;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory; import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
@@ -28,11 +28,13 @@ import org.springframework.context.annotation.Configuration;
*/ */
@Configuration @Configuration
class DefaultFeignLoadBalancedConfiguration { class DefaultFeignLoadBalancedConfiguration {
@Bean @Bean
@ConditionalOnMissingBean @ConditionalOnMissingBean
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory, public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory) { SpringClientFactory clientFactory) {
return new LoadBalancerFeignClient(new Client.Default(null, null), return new LoadBalancerFeignClient(new Client.Default(null, null), cachingFactory,
cachingFactory, clientFactory); clientFactory);
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2015 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -16,10 +16,6 @@
package org.springframework.cloud.openfeign.ribbon; package org.springframework.cloud.openfeign.ribbon;
import feign.Client;
import feign.Request;
import feign.Response;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import java.util.ArrayList; import java.util.ArrayList;
@@ -29,11 +25,6 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import org.springframework.cloud.netflix.ribbon.RibbonProperties;
import org.springframework.cloud.netflix.ribbon.ServerIntrospector;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import com.netflix.client.AbstractLoadBalancerAwareClient; import com.netflix.client.AbstractLoadBalancerAwareClient;
import com.netflix.client.ClientException; import com.netflix.client.ClientException;
import com.netflix.client.ClientRequest; import com.netflix.client.ClientRequest;
@@ -43,6 +34,15 @@ import com.netflix.client.RetryHandler;
import com.netflix.client.config.IClientConfig; import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.ILoadBalancer; import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server; import com.netflix.loadbalancer.Server;
import feign.Client;
import feign.Request;
import feign.Response;
import org.springframework.cloud.netflix.ribbon.RibbonProperties;
import org.springframework.cloud.netflix.ribbon.ServerIntrospector;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod;
import org.springframework.http.HttpRequest;
import static org.springframework.cloud.netflix.ribbon.RibbonUtils.updateToSecureConnectionIfNeeded; import static org.springframework.cloud.netflix.ribbon.RibbonUtils.updateToSecureConnectionIfNeeded;
@@ -51,18 +51,23 @@ import static org.springframework.cloud.netflix.ribbon.RibbonUtils.updateToSecur
* @author Spencer Gibb * @author Spencer Gibb
* @author Ryan Baxter * @author Ryan Baxter
* @author Tim Ysewyn * @author Tim Ysewyn
* @author Olga Maciaszek-Sharma
*/ */
public class FeignLoadBalancer extends public class FeignLoadBalancer extends
AbstractLoadBalancerAwareClient<FeignLoadBalancer.RibbonRequest, FeignLoadBalancer.RibbonResponse> { AbstractLoadBalancerAwareClient<FeignLoadBalancer.RibbonRequest, FeignLoadBalancer.RibbonResponse> {
private final RibbonProperties ribbon; private final RibbonProperties ribbon;
protected int connectTimeout; protected int connectTimeout;
protected int readTimeout; protected int readTimeout;
protected IClientConfig clientConfig; protected IClientConfig clientConfig;
protected ServerIntrospector serverIntrospector; protected ServerIntrospector serverIntrospector;
public FeignLoadBalancer(ILoadBalancer lb, IClientConfig clientConfig, public FeignLoadBalancer(ILoadBalancer lb, IClientConfig clientConfig,
ServerIntrospector serverIntrospector) { ServerIntrospector serverIntrospector) {
super(lb, clientConfig); super(lb, clientConfig);
this.setRetryHandler(RetryHandler.DEFAULT); this.setRetryHandler(RetryHandler.DEFAULT);
this.clientConfig = clientConfig; this.clientConfig = clientConfig;
@@ -79,8 +84,7 @@ public class FeignLoadBalancer extends
Request.Options options; Request.Options options;
if (configOverride != null) { if (configOverride != null) {
RibbonProperties override = RibbonProperties.from(configOverride); RibbonProperties override = RibbonProperties.from(configOverride);
options = new Request.Options( options = new Request.Options(override.connectTimeout(this.connectTimeout),
override.connectTimeout(this.connectTimeout),
override.readTimeout(this.readTimeout)); override.readTimeout(this.readTimeout));
} }
else { else {
@@ -97,7 +101,7 @@ public class FeignLoadBalancer extends
return new RequestSpecificRetryHandler(true, true, this.getRetryHandler(), return new RequestSpecificRetryHandler(true, true, this.getRetryHandler(),
requestConfig); requestConfig);
} }
if (!request.toRequest().method().equals("GET")) { if (!request.toRequest().httpMethod().name().equals("GET")) {
return new RequestSpecificRetryHandler(true, false, this.getRetryHandler(), return new RequestSpecificRetryHandler(true, false, this.getRetryHandler(),
requestConfig); requestConfig);
} }
@@ -109,16 +113,18 @@ public class FeignLoadBalancer extends
@Override @Override
public URI reconstructURIWithServer(Server server, URI original) { public URI reconstructURIWithServer(Server server, URI original) {
URI uri = updateToSecureConnectionIfNeeded(original, this.clientConfig, this.serverIntrospector, server); URI uri = updateToSecureConnectionIfNeeded(original, this.clientConfig,
this.serverIntrospector, server);
return super.reconstructURIWithServer(server, uri); return super.reconstructURIWithServer(server, uri);
} }
protected static class RibbonRequest extends ClientRequest implements Cloneable { protected static class RibbonRequest extends ClientRequest implements Cloneable {
private final Request request; private final Request request;
private final Client client; private final Client client;
RibbonRequest(Client client, Request request, URI uri) { protected RibbonRequest(Client client, Request request, URI uri) {
this.client = client; this.client = client;
setUri(uri); setUri(uri);
this.request = toRequest(request); this.request = toRequest(request);
@@ -127,7 +133,8 @@ public class FeignLoadBalancer extends
private Request toRequest(Request request) { private Request toRequest(Request request) {
Map<String, Collection<String>> headers = new LinkedHashMap<>( Map<String, Collection<String>> headers = new LinkedHashMap<>(
request.headers()); request.headers());
return Request.create(request.method(),getUri().toASCIIString(),headers,request.body(),request.charset()); return Request.create(request.httpMethod(), getUri().toASCIIString(), headers,
request.requestBody());
} }
Request toRequest() { Request toRequest() {
@@ -142,7 +149,8 @@ public class FeignLoadBalancer extends
return new HttpRequest() { return new HttpRequest() {
@Override @Override
public HttpMethod getMethod() { public HttpMethod getMethod() {
return HttpMethod.resolve(RibbonRequest.this.toRequest().method()); return HttpMethod
.resolve(RibbonRequest.this.toRequest().httpMethod().name());
} }
@Override @Override
@@ -158,8 +166,9 @@ public class FeignLoadBalancer extends
@Override @Override
public HttpHeaders getHeaders() { public HttpHeaders getHeaders() {
Map<String, List<String>> headers = new HashMap<>(); Map<String, List<String>> headers = new HashMap<>();
Map<String, Collection<String>> feignHeaders = RibbonRequest.this.toRequest().headers(); Map<String, Collection<String>> feignHeaders = RibbonRequest.this
for(String key : feignHeaders.keySet()) { .toRequest().headers();
for (String key : feignHeaders.keySet()) {
headers.put(key, new ArrayList<String>(feignHeaders.get(key))); headers.put(key, new ArrayList<String>(feignHeaders.get(key)));
} }
HttpHeaders httpHeaders = new HttpHeaders(); HttpHeaders httpHeaders = new HttpHeaders();
@@ -170,19 +179,28 @@ public class FeignLoadBalancer extends
}; };
} }
public Request getRequest() {
return this.request;
}
public Client getClient() {
return this.client;
}
@Override @Override
public Object clone() { public Object clone() {
return new RibbonRequest(this.client, this.request, getUri()); return new RibbonRequest(this.client, this.request, getUri());
} }
} }
protected static class RibbonResponse implements IResponse { protected static class RibbonResponse implements IResponse {
private final URI uri; private final URI uri;
private final Response response; private final Response response;
RibbonResponse(URI uri, Response response) { protected RibbonResponse(URI uri, Response response) {
this.uri = uri; this.uri = uri;
this.response = response; this.response = response;
} }
@@ -225,4 +243,4 @@ public class FeignLoadBalancer extends
} }
} }
@@ -1,19 +1,17 @@
/* /*
* Copyright 2013-2019 the original author or authors.
* *
* * Copyright 2013-2016 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.
* * Licensed under the Apache License, Version 2.0 (the "License"); * You may obtain a copy of the License at
* * you may not use this file except in compliance with the License.
* * You may obtain a copy of the License at
* *
* * http://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.
* *
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package org.springframework.cloud.openfeign.ribbon; package org.springframework.cloud.openfeign.ribbon;
@@ -34,9 +32,13 @@ import org.springframework.retry.RetryContext;
* @author Ryan Baxter * @author Ryan Baxter
*/ */
public class FeignRetryPolicy extends InterceptorRetryPolicy { public class FeignRetryPolicy extends InterceptorRetryPolicy {
private HttpRequest request; private HttpRequest request;
private String serviceId; private String serviceId;
public FeignRetryPolicy(HttpRequest request, LoadBalancedRetryPolicy policy, ServiceInstanceChooser serviceInstanceChooser, String serviceName) {
public FeignRetryPolicy(HttpRequest request, LoadBalancedRetryPolicy policy,
ServiceInstanceChooser serviceInstanceChooser, String serviceName) {
super(request, policy, serviceInstanceChooser, serviceName); super(request, policy, serviceInstanceChooser, serviceName);
this.request = request; this.request = request;
this.serviceId = serviceName; this.serviceId = serviceName;
@@ -45,17 +47,18 @@ public class FeignRetryPolicy extends InterceptorRetryPolicy {
@Override @Override
public boolean canRetry(RetryContext context) { public boolean canRetry(RetryContext context) {
/* /*
* In InterceptorRetryPolicy.canRetry we ask the LoadBalancer to choose a server if one is not * In InterceptorRetryPolicy.canRetry we ask the LoadBalancer to choose a server
* set in the retry context and then return true. RetryTemplat calls the canRetry method of * if one is not set in the retry context and then return true. RetryTemplat calls
* the policy even on its first execution. So the fact that we didnt have a service instance set * the canRetry method of the policy even on its first execution. So the fact that
* in the RetryContext signaled that it was the first execution and we should return true. * we didnt have a service instance set in the RetryContext signaled that it was
* the first execution and we should return true.
* *
* In the Feign scenario, Feign as actually already queried the load balancer for a service instance * In the Feign scenario, Feign as actually already queried the load balancer for
* and we set that service instance in the context when we call the open method of the policy. So in * a service instance and we set that service instance in the context when we call
* the Feign case we just return true if the retry count is 0 indicating we haven't yet made a failed * the open method of the policy. So in the Feign case we just return true if the
* request. * retry count is 0 indicating we haven't yet made a failed request.
*/ */
if(context.getRetryCount() == 0) { if (context.getRetryCount() == 0) {
return true; return true;
} }
return super.canRetry(context); return super.canRetry(context);
@@ -64,20 +67,25 @@ public class FeignRetryPolicy extends InterceptorRetryPolicy {
@Override @Override
public RetryContext open(RetryContext parent) { public RetryContext open(RetryContext parent) {
/* /*
* With Feign (unlike Ribbon) the request already has the URI for the service instance * With Feign (unlike Ribbon) the request already has the URI for the service
* we are going to make the request to, so extract that information and set the service * instance we are going to make the request to, so extract that information and
* instance in the context. In the Ribbon scenario the URI in the request object still has * set the service instance in the context. In the Ribbon scenario the URI in the
* the service id so we choose and set the service instance later on. * request object still has the service id so we choose and set the service
* instance later on.
*/ */
LoadBalancedRetryContext context = new LoadBalancedRetryContext(parent, this.request); LoadBalancedRetryContext context = new LoadBalancedRetryContext(parent,
context.setServiceInstance(new FeignRetryPolicyServiceInstance(serviceId, request)); this.request);
context.setServiceInstance(
new FeignRetryPolicyServiceInstance(this.serviceId, this.request));
return context; return context;
} }
class FeignRetryPolicyServiceInstance implements ServiceInstance { class FeignRetryPolicyServiceInstance implements ServiceInstance {
private String serviceId; private String serviceId;
private HttpRequest request; private HttpRequest request;
private Map<String, String> metadata; private Map<String, String> metadata;
FeignRetryPolicyServiceInstance(String serviceId, HttpRequest request) { FeignRetryPolicyServiceInstance(String serviceId, HttpRequest request) {
@@ -88,32 +96,34 @@ public class FeignRetryPolicy extends InterceptorRetryPolicy {
@Override @Override
public String getServiceId() { public String getServiceId() {
return serviceId; return this.serviceId;
} }
@Override @Override
public String getHost() { public String getHost() {
return request.getURI().getHost(); return this.request.getURI().getHost();
} }
@Override @Override
public int getPort() { public int getPort() {
return request.getURI().getPort(); return this.request.getURI().getPort();
} }
@Override @Override
public boolean isSecure() { public boolean isSecure() {
return "https".equals(request.getURI().getScheme()); return "https".equals(this.request.getURI().getScheme());
} }
@Override @Override
public URI getUri() { public URI getUri() {
return request.getURI(); return this.request.getURI();
} }
@Override @Override
public Map<String, String> getMetadata() { public Map<String, String> getMetadata() {
return metadata; return this.metadata;
} }
} }
}
}
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2017 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -16,25 +16,24 @@
package org.springframework.cloud.openfeign.ribbon; package org.springframework.cloud.openfeign.ribbon;
import com.netflix.loadbalancer.ILoadBalancer;
import feign.Feign;
import feign.Request;
import org.springframework.boot.autoconfigure.AutoConfigureBefore; import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass;
import org.springframework.boot.context.properties.EnableConfigurationProperties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryFactory;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.cloud.openfeign.FeignAutoConfiguration; import org.springframework.cloud.openfeign.FeignAutoConfiguration;
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties; import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import org.springframework.context.annotation.Primary; import org.springframework.context.annotation.Primary;
import com.netflix.loadbalancer.ILoadBalancer;
import feign.Feign;
import feign.Request;
/** /**
* Autoconfiguration to be activated if Feign is in use and needs to be use Ribbon as a * Autoconfiguration to be activated if Feign is in use and needs to be use Ribbon as a
* load balancer. * load balancer.
@@ -45,8 +44,9 @@ import feign.Request;
@Configuration @Configuration
@AutoConfigureBefore(FeignAutoConfiguration.class) @AutoConfigureBefore(FeignAutoConfiguration.class)
@EnableConfigurationProperties({ FeignHttpClientProperties.class }) @EnableConfigurationProperties({ FeignHttpClientProperties.class })
//Order is important here, last should be the default, first should be optional // Order is important here, last should be the default, first should be optional
// see https://github.com/spring-cloud/spring-cloud-netflix/issues/2086#issuecomment-316281653 // see
// https://github.com/spring-cloud/spring-cloud-netflix/issues/2086#issuecomment-316281653
@Import({ HttpClientFeignLoadBalancedConfiguration.class, @Import({ HttpClientFeignLoadBalancedConfiguration.class,
OkHttpFeignLoadBalancedConfiguration.class, OkHttpFeignLoadBalancedConfiguration.class,
DefaultFeignLoadBalancedConfiguration.class }) DefaultFeignLoadBalancedConfiguration.class })
@@ -54,6 +54,7 @@ public class FeignRibbonClientAutoConfiguration {
@Bean @Bean
@Primary @Primary
@ConditionalOnMissingBean
@ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate") @ConditionalOnMissingClass("org.springframework.retry.support.RetryTemplate")
public CachingSpringLoadBalancerFactory cachingLBClientFactory( public CachingSpringLoadBalancerFactory cachingLBClientFactory(
SpringClientFactory factory) { SpringClientFactory factory) {
@@ -62,10 +63,10 @@ public class FeignRibbonClientAutoConfiguration {
@Bean @Bean
@Primary @Primary
@ConditionalOnMissingBean
@ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate") @ConditionalOnClass(name = "org.springframework.retry.support.RetryTemplate")
public CachingSpringLoadBalancerFactory retryabeCachingLBClientFactory( public CachingSpringLoadBalancerFactory retryabeCachingLBClientFactory(
SpringClientFactory factory, SpringClientFactory factory, LoadBalancedRetryFactory retryFactory) {
LoadBalancedRetryFactory retryFactory) {
return new CachingSpringLoadBalancerFactory(factory, retryFactory); return new CachingSpringLoadBalancerFactory(factory, retryFactory);
} }
@@ -74,4 +75,5 @@ public class FeignRibbonClientAutoConfiguration {
public Request.Options feignRequestOptions() { public Request.Options feignRequestOptions() {
return LoadBalancerFeignClient.DEFAULT_OPTIONS; return LoadBalancerFeignClient.DEFAULT_OPTIONS;
} }
} }
@@ -1,42 +1,43 @@
/* /*
* Copyright 2013-2017 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*
*/ */
package org.springframework.cloud.openfeign.ribbon; package org.springframework.cloud.openfeign.ribbon;
import feign.Client;
import feign.httpclient.ApacheHttpClient;
import java.util.Timer; import java.util.Timer;
import java.util.TimerTask; import java.util.TimerTask;
import javax.annotation.PreDestroy; import javax.annotation.PreDestroy;
import feign.Client;
import feign.httpclient.ApacheHttpClient;
import org.apache.http.client.HttpClient; import org.apache.http.client.HttpClient;
import org.apache.http.client.config.RequestConfig; import org.apache.http.client.config.RequestConfig;
import org.apache.http.config.RegistryBuilder; import org.apache.http.config.RegistryBuilder;
import org.apache.http.conn.HttpClientConnectionManager; import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder; import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.commons.httpclient.ApacheHttpClientConnectionManagerFactory; import org.springframework.cloud.commons.httpclient.ApacheHttpClientConnectionManagerFactory;
import org.springframework.cloud.commons.httpclient.ApacheHttpClientFactory; import org.springframework.cloud.commons.httpclient.ApacheHttpClientFactory;
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory; import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -48,9 +49,18 @@ import org.springframework.context.annotation.Configuration;
@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true) @ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)
class HttpClientFeignLoadBalancedConfiguration { class HttpClientFeignLoadBalancedConfiguration {
@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory, HttpClient httpClient) {
ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
}
@Configuration @Configuration
@ConditionalOnMissingBean(CloseableHttpClient.class) @ConditionalOnMissingBean(CloseableHttpClient.class)
protected static class HttpClientFeignConfiguration { protected static class HttpClientFeignConfiguration {
private final Timer connectionManagerTimer = new Timer( private final Timer connectionManagerTimer = new Timer(
"FeignApacheHttpClientConfiguration.connectionManagerTimer", true); "FeignApacheHttpClientConfiguration.connectionManagerTimer", true);
@@ -65,10 +75,12 @@ class HttpClientFeignLoadBalancedConfiguration {
ApacheHttpClientConnectionManagerFactory connectionManagerFactory, ApacheHttpClientConnectionManagerFactory connectionManagerFactory,
FeignHttpClientProperties httpClientProperties) { FeignHttpClientProperties httpClientProperties) {
final HttpClientConnectionManager connectionManager = connectionManagerFactory final HttpClientConnectionManager connectionManager = connectionManagerFactory
.newConnectionManager(httpClientProperties.isDisableSslValidation(), httpClientProperties.getMaxConnections(), .newConnectionManager(httpClientProperties.isDisableSslValidation(),
httpClientProperties.getMaxConnections(),
httpClientProperties.getMaxConnectionsPerRoute(), httpClientProperties.getMaxConnectionsPerRoute(),
httpClientProperties.getTimeToLive(), httpClientProperties.getTimeToLive(),
httpClientProperties.getTimeToLiveUnit(), registryBuilder); httpClientProperties.getTimeToLiveUnit(),
this.registryBuilder);
this.connectionManagerTimer.schedule(new TimerTask() { this.connectionManagerTimer.schedule(new TimerTask() {
@Override @Override
public void run() { public void run() {
@@ -80,49 +92,47 @@ class HttpClientFeignLoadBalancedConfiguration {
@Bean @Bean
@ConditionalOnProperty(value = "feign.compression.response.enabled", havingValue = "true") @ConditionalOnProperty(value = "feign.compression.response.enabled", havingValue = "true")
public CloseableHttpClient customHttpClient(HttpClientConnectionManager httpClientConnectionManager, public CloseableHttpClient customHttpClient(
FeignHttpClientProperties httpClientProperties) { HttpClientConnectionManager httpClientConnectionManager,
HttpClientBuilder builder = HttpClientBuilder.create().disableCookieManagement().useSystemProperties(); FeignHttpClientProperties httpClientProperties) {
this.httpClient = createClient(builder, httpClientConnectionManager, httpClientProperties); HttpClientBuilder builder = HttpClientBuilder.create()
.disableCookieManagement().useSystemProperties();
this.httpClient = createClient(builder, httpClientConnectionManager,
httpClientProperties);
return this.httpClient; return this.httpClient;
} }
@Bean @Bean
@ConditionalOnProperty(value = "feign.compression.response.enabled", havingValue = "false", matchIfMissing = true) @ConditionalOnProperty(value = "feign.compression.response.enabled", havingValue = "false", matchIfMissing = true)
public CloseableHttpClient httpClient(ApacheHttpClientFactory httpClientFactory, HttpClientConnectionManager httpClientConnectionManager, public CloseableHttpClient httpClient(ApacheHttpClientFactory httpClientFactory,
FeignHttpClientProperties httpClientProperties) { HttpClientConnectionManager httpClientConnectionManager,
this.httpClient = createClient(httpClientFactory.createBuilder(), httpClientConnectionManager, httpClientProperties); FeignHttpClientProperties httpClientProperties) {
this.httpClient = createClient(httpClientFactory.createBuilder(),
httpClientConnectionManager, httpClientProperties);
return this.httpClient; return this.httpClient;
} }
private CloseableHttpClient createClient(HttpClientBuilder builder, HttpClientConnectionManager httpClientConnectionManager, private CloseableHttpClient createClient(HttpClientBuilder builder,
FeignHttpClientProperties httpClientProperties) { HttpClientConnectionManager httpClientConnectionManager,
FeignHttpClientProperties httpClientProperties) {
RequestConfig defaultRequestConfig = RequestConfig.custom() RequestConfig defaultRequestConfig = RequestConfig.custom()
.setConnectTimeout(httpClientProperties.getConnectionTimeout()) .setConnectTimeout(httpClientProperties.getConnectionTimeout())
.setRedirectsEnabled(httpClientProperties.isFollowRedirects()) .setRedirectsEnabled(httpClientProperties.isFollowRedirects())
.build(); .build();
CloseableHttpClient httpClient = builder.setDefaultRequestConfig(defaultRequestConfig). CloseableHttpClient httpClient = builder
setConnectionManager(httpClientConnectionManager).build(); .setDefaultRequestConfig(defaultRequestConfig)
.setConnectionManager(httpClientConnectionManager).build();
return httpClient; return httpClient;
} }
@PreDestroy @PreDestroy
public void destroy() throws Exception { public void destroy() throws Exception {
connectionManagerTimer.cancel(); this.connectionManagerTimer.cancel();
if(httpClient != null) { if (this.httpClient != null) {
httpClient.close(); this.httpClient.close();
} }
} }
} }
@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory, HttpClient httpClient) {
ApacheHttpClient delegate = new ApacheHttpClient(httpClient);
return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
}
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2015 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -19,17 +19,16 @@ package org.springframework.cloud.openfeign.ribbon;
import java.io.IOException; import java.io.IOException;
import java.net.URI; import java.net.URI;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import com.netflix.client.ClientException; import com.netflix.client.ClientException;
import com.netflix.client.config.CommonClientConfigKey; import com.netflix.client.config.CommonClientConfigKey;
import com.netflix.client.config.DefaultClientConfigImpl; import com.netflix.client.config.DefaultClientConfigImpl;
import com.netflix.client.config.IClientConfig; import com.netflix.client.config.IClientConfig;
import feign.Client; import feign.Client;
import feign.Request; import feign.Request;
import feign.Response; import feign.Response;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
/** /**
* @author Dave Syer * @author Dave Syer
* *
@@ -39,17 +38,37 @@ public class LoadBalancerFeignClient implements Client {
static final Request.Options DEFAULT_OPTIONS = new Request.Options(); static final Request.Options DEFAULT_OPTIONS = new Request.Options();
private final Client delegate; private final Client delegate;
private CachingSpringLoadBalancerFactory lbClientFactory; private CachingSpringLoadBalancerFactory lbClientFactory;
private SpringClientFactory clientFactory; private SpringClientFactory clientFactory;
public LoadBalancerFeignClient(Client delegate, public LoadBalancerFeignClient(Client delegate,
CachingSpringLoadBalancerFactory lbClientFactory, CachingSpringLoadBalancerFactory lbClientFactory,
SpringClientFactory clientFactory) { SpringClientFactory clientFactory) {
this.delegate = delegate; this.delegate = delegate;
this.lbClientFactory = lbClientFactory; this.lbClientFactory = lbClientFactory;
this.clientFactory = clientFactory; this.clientFactory = clientFactory;
} }
static URI cleanUrl(String originalUrl, String host) {
String newUrl = originalUrl;
if (originalUrl.startsWith("https://")) {
newUrl = originalUrl.substring(0, 8)
+ originalUrl.substring(8 + host.length());
}
else if (originalUrl.startsWith("http")) {
newUrl = originalUrl.substring(0, 7)
+ originalUrl.substring(7 + host.length());
}
StringBuffer buffer = new StringBuffer(newUrl);
if ((newUrl.startsWith("https://") && newUrl.length() == 8)
|| (newUrl.startsWith("http://") && newUrl.length() == 7)) {
buffer.append("/");
}
return URI.create(buffer.toString());
}
@Override @Override
public Response execute(Request request, Request.Options options) throws IOException { public Response execute(Request request, Request.Options options) throws IOException {
try { try {
@@ -60,8 +79,8 @@ public class LoadBalancerFeignClient implements Client {
this.delegate, request, uriWithoutHost); this.delegate, request, uriWithoutHost);
IClientConfig requestConfig = getClientConfig(options, clientName); IClientConfig requestConfig = getClientConfig(options, clientName);
return lbClient(clientName).executeWithLoadBalancer(ribbonRequest, return lbClient(clientName)
requestConfig).toResponse(); .executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
} }
catch (ClientException e) { catch (ClientException e) {
IOException io = findIOException(e); IOException io = findIOException(e);
@@ -76,7 +95,8 @@ public class LoadBalancerFeignClient implements Client {
IClientConfig requestConfig; IClientConfig requestConfig;
if (options == DEFAULT_OPTIONS) { if (options == DEFAULT_OPTIONS) {
requestConfig = this.clientFactory.getClientConfig(clientName); requestConfig = this.clientFactory.getClientConfig(clientName);
} else { }
else {
requestConfig = new FeignOptionsClientConfig(options); requestConfig = new FeignOptionsClientConfig(options);
} }
return requestConfig; return requestConfig;
@@ -96,17 +116,13 @@ public class LoadBalancerFeignClient implements Client {
return this.delegate; return this.delegate;
} }
static URI cleanUrl(String originalUrl, String host) {
return URI.create(originalUrl.replaceFirst(host, ""));
}
private FeignLoadBalancer lbClient(String clientName) { private FeignLoadBalancer lbClient(String clientName) {
return this.lbClientFactory.create(clientName); return this.lbClientFactory.create(clientName);
} }
static class FeignOptionsClientConfig extends DefaultClientConfigImpl { static class FeignOptionsClientConfig extends DefaultClientConfigImpl {
public FeignOptionsClientConfig(Request.Options options) { FeignOptionsClientConfig(Request.Options options) {
setProperty(CommonClientConfigKey.ConnectTimeout, setProperty(CommonClientConfigKey.ConnectTimeout,
options.connectTimeoutMillis()); options.connectTimeoutMillis());
setProperty(CommonClientConfigKey.ReadTimeout, options.readTimeoutMillis()); setProperty(CommonClientConfigKey.ReadTimeout, options.readTimeoutMillis());
@@ -123,4 +139,5 @@ public class LoadBalancerFeignClient implements Client {
} }
} }
} }
@@ -1,35 +1,36 @@
/* /*
* Copyright 2013-2017 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*
*/ */
package org.springframework.cloud.openfeign.ribbon; package org.springframework.cloud.openfeign.ribbon;
import java.util.concurrent.TimeUnit;
import javax.annotation.PreDestroy;
import feign.Client; import feign.Client;
import feign.okhttp.OkHttpClient; import feign.okhttp.OkHttpClient;
import okhttp3.ConnectionPool; import okhttp3.ConnectionPool;
import java.util.concurrent.TimeUnit;
import javax.annotation.PreDestroy;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory; import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory;
import org.springframework.cloud.commons.httpclient.OkHttpClientFactory; import org.springframework.cloud.commons.httpclient.OkHttpClientFactory;
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
import org.springframework.cloud.netflix.ribbon.SpringClientFactory; import org.springframework.cloud.netflix.ribbon.SpringClientFactory;
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -38,18 +39,28 @@ import org.springframework.context.annotation.Configuration;
*/ */
@Configuration @Configuration
@ConditionalOnClass(OkHttpClient.class) @ConditionalOnClass(OkHttpClient.class)
@ConditionalOnProperty(value = "feign.okhttp.enabled") @ConditionalOnProperty("feign.okhttp.enabled")
class OkHttpFeignLoadBalancedConfiguration { class OkHttpFeignLoadBalancedConfiguration {
@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory, okhttp3.OkHttpClient okHttpClient) {
OkHttpClient delegate = new OkHttpClient(okHttpClient);
return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
}
@Configuration @Configuration
@ConditionalOnMissingBean(okhttp3.OkHttpClient.class) @ConditionalOnMissingBean(okhttp3.OkHttpClient.class)
protected static class OkHttpFeignConfiguration { protected static class OkHttpFeignConfiguration {
private okhttp3.OkHttpClient okHttpClient; private okhttp3.OkHttpClient okHttpClient;
@Bean @Bean
@ConditionalOnMissingBean(ConnectionPool.class) @ConditionalOnMissingBean(ConnectionPool.class)
public ConnectionPool httpClientConnectionPool(FeignHttpClientProperties httpClientProperties, public ConnectionPool httpClientConnectionPool(
OkHttpClientConnectionPoolFactory connectionPoolFactory) { FeignHttpClientProperties httpClientProperties,
OkHttpClientConnectionPoolFactory connectionPoolFactory) {
Integer maxTotalConnections = httpClientProperties.getMaxConnections(); Integer maxTotalConnections = httpClientProperties.getMaxConnections();
Long timeToLive = httpClientProperties.getTimeToLive(); Long timeToLive = httpClientProperties.getTimeToLive();
TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit(); TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
@@ -58,30 +69,26 @@ class OkHttpFeignLoadBalancedConfiguration {
@Bean @Bean
public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory, public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory,
ConnectionPool connectionPool, FeignHttpClientProperties httpClientProperties) { ConnectionPool connectionPool,
FeignHttpClientProperties httpClientProperties) {
Boolean followRedirects = httpClientProperties.isFollowRedirects(); Boolean followRedirects = httpClientProperties.isFollowRedirects();
Integer connectTimeout = httpClientProperties.getConnectionTimeout(); Integer connectTimeout = httpClientProperties.getConnectionTimeout();
this.okHttpClient = httpClientFactory.createBuilder(httpClientProperties.isDisableSslValidation()). this.okHttpClient = httpClientFactory
connectTimeout(connectTimeout, TimeUnit.MILLISECONDS). .createBuilder(httpClientProperties.isDisableSslValidation())
followRedirects(followRedirects). .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
connectionPool(connectionPool).build(); .followRedirects(followRedirects).connectionPool(connectionPool)
.build();
return this.okHttpClient; return this.okHttpClient;
} }
@PreDestroy @PreDestroy
public void destroy() { public void destroy() {
if(okHttpClient != null) { if (this.okHttpClient != null) {
okHttpClient.dispatcher().executorService().shutdown(); this.okHttpClient.dispatcher().executorService().shutdown();
okHttpClient.connectionPool().evictAll(); this.okHttpClient.connectionPool().evictAll();
} }
} }
} }
@Bean
@ConditionalOnMissingBean(Client.class)
public Client feignClient(CachingSpringLoadBalancerFactory cachingFactory,
SpringClientFactory clientFactory, okhttp3.OkHttpClient okHttpClient) {
OkHttpClient delegate = new OkHttpClient(okHttpClient);
return new LoadBalancerFeignClient(delegate, cachingFactory, clientFactory);
}
} }
@@ -1,28 +1,32 @@
/* /*
* Copyright 2013-2019 the original author or authors.
* *
* * Copyright 2013-2016 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.
* * Licensed under the Apache License, Version 2.0 (the "License"); * You may obtain a copy of the License at
* * you may not use this file except in compliance with the License.
* * You may obtain a copy of the License at
* *
* * http://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.
* *
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/ */
package org.springframework.cloud.openfeign.ribbon; package org.springframework.cloud.openfeign.ribbon;
import java.io.IOException;
import java.net.URI;
import com.netflix.client.DefaultLoadBalancerRetryHandler;
import com.netflix.client.RequestSpecificRetryHandler;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import feign.Request; import feign.Request;
import feign.Response; import feign.Response;
import java.io.IOException;
import java.net.URI;
import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.ServiceInstance;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRecoveryCallback; import org.springframework.cloud.client.loadbalancer.LoadBalancedRecoveryCallback;
import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryContext; import org.springframework.cloud.client.loadbalancer.LoadBalancedRetryContext;
@@ -40,71 +44,82 @@ import org.springframework.retry.backoff.NoBackOffPolicy;
import org.springframework.retry.policy.NeverRetryPolicy; import org.springframework.retry.policy.NeverRetryPolicy;
import org.springframework.retry.support.RetryTemplate; import org.springframework.retry.support.RetryTemplate;
import org.springframework.util.StreamUtils; import org.springframework.util.StreamUtils;
import com.netflix.client.DefaultLoadBalancerRetryHandler;
import com.netflix.client.RequestSpecificRetryHandler;
import com.netflix.client.config.IClientConfig;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
/** /**
* A {@link FeignLoadBalancer} that leverages Spring Retry to retry failed requests. * A {@link FeignLoadBalancer} that leverages Spring Retry to retry failed requests.
*
* @author Ryan Baxter * @author Ryan Baxter
* @author Gang Li * @author Gang Li
*/ */
public class RetryableFeignLoadBalancer extends FeignLoadBalancer implements ServiceInstanceChooser { public class RetryableFeignLoadBalancer extends FeignLoadBalancer
implements ServiceInstanceChooser {
private final LoadBalancedRetryFactory loadBalancedRetryFactory; private final LoadBalancedRetryFactory loadBalancedRetryFactory;
public RetryableFeignLoadBalancer(ILoadBalancer lb, IClientConfig clientConfig, ServerIntrospector serverIntrospector, public RetryableFeignLoadBalancer(ILoadBalancer lb, IClientConfig clientConfig,
LoadBalancedRetryFactory loadBalancedRetryFactory) { ServerIntrospector serverIntrospector,
LoadBalancedRetryFactory loadBalancedRetryFactory) {
super(lb, clientConfig, serverIntrospector); super(lb, clientConfig, serverIntrospector);
this.loadBalancedRetryFactory = loadBalancedRetryFactory; this.loadBalancedRetryFactory = loadBalancedRetryFactory;
this.setRetryHandler(new DefaultLoadBalancerRetryHandler(clientConfig)); this.setRetryHandler(new DefaultLoadBalancerRetryHandler(clientConfig));
} }
@Override @Override
public RibbonResponse execute(final RibbonRequest request, IClientConfig configOverride) public RibbonResponse execute(final RibbonRequest request,
throws IOException { IClientConfig configOverride) throws IOException {
final Request.Options options; final Request.Options options;
if (configOverride != null) { if (configOverride != null) {
RibbonProperties ribbon = RibbonProperties.from(configOverride); RibbonProperties ribbon = RibbonProperties.from(configOverride);
options = new Request.Options( options = new Request.Options(ribbon.connectTimeout(this.connectTimeout),
ribbon.connectTimeout(this.connectTimeout),
ribbon.readTimeout(this.readTimeout)); ribbon.readTimeout(this.readTimeout));
} }
else { else {
options = new Request.Options(this.connectTimeout, this.readTimeout); options = new Request.Options(this.connectTimeout, this.readTimeout);
} }
final LoadBalancedRetryPolicy retryPolicy = loadBalancedRetryFactory.createRetryPolicy(this.getClientName(), this); final LoadBalancedRetryPolicy retryPolicy = this.loadBalancedRetryFactory
.createRetryPolicy(this.getClientName(), this);
RetryTemplate retryTemplate = new RetryTemplate(); RetryTemplate retryTemplate = new RetryTemplate();
BackOffPolicy backOffPolicy = loadBalancedRetryFactory.createBackOffPolicy(this.getClientName()); BackOffPolicy backOffPolicy = this.loadBalancedRetryFactory
retryTemplate.setBackOffPolicy(backOffPolicy == null ? new NoBackOffPolicy() : backOffPolicy); .createBackOffPolicy(this.getClientName());
RetryListener[] retryListeners = this.loadBalancedRetryFactory.createRetryListeners(this.getClientName()); retryTemplate.setBackOffPolicy(
backOffPolicy == null ? new NoBackOffPolicy() : backOffPolicy);
RetryListener[] retryListeners = this.loadBalancedRetryFactory
.createRetryListeners(this.getClientName());
if (retryListeners != null && retryListeners.length != 0) { if (retryListeners != null && retryListeners.length != 0) {
retryTemplate.setListeners(retryListeners); retryTemplate.setListeners(retryListeners);
} }
retryTemplate.setRetryPolicy(retryPolicy == null ? new NeverRetryPolicy() retryTemplate.setRetryPolicy(retryPolicy == null ? new NeverRetryPolicy()
: new FeignRetryPolicy(request.toHttpRequest(), retryPolicy, this, this.getClientName())); : new FeignRetryPolicy(request.toHttpRequest(), retryPolicy, this,
this.getClientName()));
return retryTemplate.execute(new RetryCallback<RibbonResponse, IOException>() { return retryTemplate.execute(new RetryCallback<RibbonResponse, IOException>() {
@Override @Override
public RibbonResponse doWithRetry(RetryContext retryContext) throws IOException { public RibbonResponse doWithRetry(RetryContext retryContext)
throws IOException {
Request feignRequest = null; Request feignRequest = null;
//on retries the policy will choose the server and set it in the context // on retries the policy will choose the server and set it in the context
//extract the server and update the request being made // extract the server and update the request being made
if (retryContext instanceof LoadBalancedRetryContext) { if (retryContext instanceof LoadBalancedRetryContext) {
ServiceInstance service = ((LoadBalancedRetryContext) retryContext).getServiceInstance(); ServiceInstance service = ((LoadBalancedRetryContext) retryContext)
.getServiceInstance();
if (service != null) { if (service != null) {
feignRequest = ((RibbonRequest) request.replaceUri(reconstructURIWithServer(new Server(service.getHost(), service.getPort()), request.getUri()))).toRequest(); feignRequest = ((RibbonRequest) request
.replaceUri(reconstructURIWithServer(
new Server(service.getHost(), service.getPort()),
request.getUri()))).toRequest();
} }
} }
if (feignRequest == null) { if (feignRequest == null) {
feignRequest = request.toRequest(); feignRequest = request.toRequest();
} }
Response response = request.client().execute(feignRequest, options); Response response = request.client().execute(feignRequest, options);
if (retryPolicy.retryableStatusCode(response.status())) { if (retryPolicy != null
byte[] byteArray = response.body() == null ? new byte[]{} : StreamUtils.copyToByteArray(response.body().asInputStream()); && retryPolicy.retryableStatusCode(response.status())) {
byte[] byteArray = response.body() == null ? new byte[] {}
: StreamUtils
.copyToByteArray(response.body().asInputStream());
response.close(); response.close();
throw new RibbonResponseStatusCodeException(RetryableFeignLoadBalancer.this.clientName, response, throw new RibbonResponseStatusCodeException(
RetryableFeignLoadBalancer.this.clientName, response,
byteArray, request.getUri()); byteArray, request.getUri());
} }
return new RibbonResponse(request.getUri(), response); return new RibbonResponse(request.getUri(), response);
@@ -120,7 +135,8 @@ public class RetryableFeignLoadBalancer extends FeignLoadBalancer implements Ser
@Override @Override
public RequestSpecificRetryHandler getRequestSpecificRetryHandler( public RequestSpecificRetryHandler getRequestSpecificRetryHandler(
FeignLoadBalancer.RibbonRequest request, IClientConfig requestConfig) { FeignLoadBalancer.RibbonRequest request, IClientConfig requestConfig) {
return new RequestSpecificRetryHandler(false, false, this.getRetryHandler(), requestConfig); return new RequestSpecificRetryHandler(false, false, this.getRetryHandler(),
requestConfig);
} }
@Override @Override
@@ -128,4 +144,5 @@ public class RetryableFeignLoadBalancer extends FeignLoadBalancer implements Ser
return new RibbonLoadBalancerClient.RibbonServer(serviceId, return new RibbonLoadBalancerClient.RibbonServer(serviceId,
this.getLoadBalancer().chooseServer(serviceId)); this.getLoadBalancer().chooseServer(serviceId));
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2018 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -13,33 +13,37 @@
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.cloud.openfeign.ribbon; package org.springframework.cloud.openfeign.ribbon;
import java.io.ByteArrayInputStream;
import java.net.URI;
import feign.Response; import feign.Response;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.net.URI;
import org.springframework.cloud.client.loadbalancer.RetryableStatusCodeException; import org.springframework.cloud.client.loadbalancer.RetryableStatusCodeException;
import org.springframework.util.StreamUtils;
/** /**
* A {@link RetryableStatusCodeException} for {@link Response}s * A {@link RetryableStatusCodeException} for {@link Response}s.
*
* @author Ryan Baxter * @author Ryan Baxter
*/ */
public class RibbonResponseStatusCodeException extends RetryableStatusCodeException { public class RibbonResponseStatusCodeException extends RetryableStatusCodeException {
private Response response;
public RibbonResponseStatusCodeException(String serviceId, Response response, byte[] body, URI uri) { private final Response response;
public RibbonResponseStatusCodeException(String serviceId, Response response,
byte[] body, URI uri) {
super(serviceId, response.status(), response, uri); super(serviceId, response.status(), response, uri);
this.response = Response.builder().body(new ByteArrayInputStream(body), body.length) this.response = Response.builder()
.body(new ByteArrayInputStream(body), body.length)
.headers(response.headers()).reason(response.reason()) .headers(response.headers()).reason(response.reason())
.status(response.status()).request(response.request()).build(); .status(response.status()).request(response.request()).build();
} }
@Override @Override
public Response getResponse() { public Response getResponse() {
return response; return this.response;
} }
} }
@@ -1,3 +1,19 @@
/*
* Copyright 2013-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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; package org.springframework.cloud.openfeign.support;
import com.netflix.hystrix.HystrixCommand; import com.netflix.hystrix.HystrixCommand;
@@ -6,8 +22,11 @@ import com.netflix.hystrix.HystrixThreadPoolKey;
/** /**
* Convenience class for implementing feign fallbacks that return {@link HystrixCommand}. * Convenience class for implementing feign fallbacks that return {@link HystrixCommand}.
* Also useful for return types of {@link rx.Observable} and {@link java.util.concurrent.Future}. * Also useful for return types of {@link rx.Observable} and
* For those return types, just call {@link FallbackCommand#observe()} or {@link FallbackCommand#queue()} respectively. * {@link java.util.concurrent.Future}. For those return types, just call
* {@link FallbackCommand#observe()} or {@link FallbackCommand#queue()} respectively.
*
* @param <T> result type
* @author Spencer Gibb * @author Spencer Gibb
*/ */
public class FallbackCommand<T> extends HystrixCommand<T> { public class FallbackCommand<T> extends HystrixCommand<T> {
@@ -28,17 +47,21 @@ public class FallbackCommand<T> extends HystrixCommand<T> {
this.result = result; this.result = result;
} }
public FallbackCommand(T result, HystrixCommandGroupKey group, int executionIsolationThreadTimeoutInMilliseconds) { public FallbackCommand(T result, HystrixCommandGroupKey group,
int executionIsolationThreadTimeoutInMilliseconds) {
super(group, executionIsolationThreadTimeoutInMilliseconds); super(group, executionIsolationThreadTimeoutInMilliseconds);
this.result = result; this.result = result;
} }
public FallbackCommand(T result, HystrixCommandGroupKey group, HystrixThreadPoolKey threadPool) { public FallbackCommand(T result, HystrixCommandGroupKey group,
HystrixThreadPoolKey threadPool) {
super(group, threadPool); super(group, threadPool);
this.result = result; this.result = result;
} }
public FallbackCommand(T result, HystrixCommandGroupKey group, HystrixThreadPoolKey threadPool, int executionIsolationThreadTimeoutInMilliseconds) { public FallbackCommand(T result, HystrixCommandGroupKey group,
HystrixThreadPoolKey threadPool,
int executionIsolationThreadTimeoutInMilliseconds) {
super(group, threadPool, executionIsolationThreadTimeoutInMilliseconds); super(group, threadPool, executionIsolationThreadTimeoutInMilliseconds);
this.result = result; this.result = result;
} }
@@ -52,4 +75,5 @@ public class FallbackCommand<T> extends HystrixCommand<T> {
protected T run() throws Exception { protected T run() throws Exception {
return this.result; return this.result;
} }
} }
@@ -1,24 +1,23 @@
/* /*
* Copyright 2013-2019 the original author or authors.
* *
* * Copyright 2013-2016 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.
* * Licensed under the Apache License, Version 2.0 (the "License"); * You may obtain a copy of the License at
* * you may not use this file except in compliance with the License.
* * You may obtain a copy of the License at
* *
* * http://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.
* *
* 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; package org.springframework.cloud.openfeign.support;
import java.util.concurrent.TimeUnit; import java.util.concurrent.TimeUnit;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
/** /**
@@ -26,26 +25,65 @@ import org.springframework.boot.context.properties.ConfigurationProperties;
*/ */
@ConfigurationProperties(prefix = "feign.httpclient") @ConfigurationProperties(prefix = "feign.httpclient")
public class FeignHttpClientProperties { public class FeignHttpClientProperties {
/**
* Default value for disabling SSL validation.
*/
public static final boolean DEFAULT_DISABLE_SSL_VALIDATION = false; public static final boolean DEFAULT_DISABLE_SSL_VALIDATION = false;
/**
* Default value for max number od connections.
*/
public static final int DEFAULT_MAX_CONNECTIONS = 200; public static final int DEFAULT_MAX_CONNECTIONS = 200;
/**
* Default value for max number od connections per route.
*/
public static final int DEFAULT_MAX_CONNECTIONS_PER_ROUTE = 50; public static final int DEFAULT_MAX_CONNECTIONS_PER_ROUTE = 50;
/**
* Default value for time to live.
*/
public static final long DEFAULT_TIME_TO_LIVE = 900L; public static final long DEFAULT_TIME_TO_LIVE = 900L;
/**
* Default time to live unit.
*/
public static final TimeUnit DEFAULT_TIME_TO_LIVE_UNIT = TimeUnit.SECONDS; public static final TimeUnit DEFAULT_TIME_TO_LIVE_UNIT = TimeUnit.SECONDS;
/**
* Default value for following redirects.
*/
public static final boolean DEFAULT_FOLLOW_REDIRECTS = true; public static final boolean DEFAULT_FOLLOW_REDIRECTS = true;
/**
* Default value for connection timeout.
*/
public static final int DEFAULT_CONNECTION_TIMEOUT = 2000; public static final int DEFAULT_CONNECTION_TIMEOUT = 2000;
/**
* Default value for connection timer repeat.
*/
public static final int DEFAULT_CONNECTION_TIMER_REPEAT = 3000; public static final int DEFAULT_CONNECTION_TIMER_REPEAT = 3000;
private boolean disableSslValidation = DEFAULT_DISABLE_SSL_VALIDATION; private boolean disableSslValidation = DEFAULT_DISABLE_SSL_VALIDATION;
private int maxConnections = DEFAULT_MAX_CONNECTIONS; private int maxConnections = DEFAULT_MAX_CONNECTIONS;
private int maxConnectionsPerRoute = DEFAULT_MAX_CONNECTIONS_PER_ROUTE; private int maxConnectionsPerRoute = DEFAULT_MAX_CONNECTIONS_PER_ROUTE;
private long timeToLive = DEFAULT_TIME_TO_LIVE; private long timeToLive = DEFAULT_TIME_TO_LIVE;
private TimeUnit timeToLiveUnit = DEFAULT_TIME_TO_LIVE_UNIT; private TimeUnit timeToLiveUnit = DEFAULT_TIME_TO_LIVE_UNIT;
private boolean followRedirects = DEFAULT_FOLLOW_REDIRECTS; private boolean followRedirects = DEFAULT_FOLLOW_REDIRECTS;
private int connectionTimeout = DEFAULT_CONNECTION_TIMEOUT; private int connectionTimeout = DEFAULT_CONNECTION_TIMEOUT;
private int connectionTimerRepeat = DEFAULT_CONNECTION_TIMER_REPEAT; private int connectionTimerRepeat = DEFAULT_CONNECTION_TIMER_REPEAT;
public int getConnectionTimerRepeat() { public int getConnectionTimerRepeat() {
return connectionTimerRepeat; return this.connectionTimerRepeat;
} }
public void setConnectionTimerRepeat(int connectionTimerRepeat) { public void setConnectionTimerRepeat(int connectionTimerRepeat) {
@@ -53,7 +91,7 @@ public class FeignHttpClientProperties {
} }
public boolean isDisableSslValidation() { public boolean isDisableSslValidation() {
return disableSslValidation; return this.disableSslValidation;
} }
public void setDisableSslValidation(boolean disableSslValidation) { public void setDisableSslValidation(boolean disableSslValidation) {
@@ -61,7 +99,7 @@ public class FeignHttpClientProperties {
} }
public int getMaxConnections() { public int getMaxConnections() {
return maxConnections; return this.maxConnections;
} }
public void setMaxConnections(int maxConnections) { public void setMaxConnections(int maxConnections) {
@@ -69,7 +107,7 @@ public class FeignHttpClientProperties {
} }
public int getMaxConnectionsPerRoute() { public int getMaxConnectionsPerRoute() {
return maxConnectionsPerRoute; return this.maxConnectionsPerRoute;
} }
public void setMaxConnectionsPerRoute(int maxConnectionsPerRoute) { public void setMaxConnectionsPerRoute(int maxConnectionsPerRoute) {
@@ -77,7 +115,7 @@ public class FeignHttpClientProperties {
} }
public long getTimeToLive() { public long getTimeToLive() {
return timeToLive; return this.timeToLive;
} }
public void setTimeToLive(long timeToLive) { public void setTimeToLive(long timeToLive) {
@@ -85,7 +123,7 @@ public class FeignHttpClientProperties {
} }
public TimeUnit getTimeToLiveUnit() { public TimeUnit getTimeToLiveUnit() {
return timeToLiveUnit; return this.timeToLiveUnit;
} }
public void setTimeToLiveUnit(TimeUnit timeToLiveUnit) { public void setTimeToLiveUnit(TimeUnit timeToLiveUnit) {
@@ -93,7 +131,7 @@ public class FeignHttpClientProperties {
} }
public boolean isFollowRedirects() { public boolean isFollowRedirects() {
return followRedirects; return this.followRedirects;
} }
public void setFollowRedirects(boolean followRedirects) { public void setFollowRedirects(boolean followRedirects) {
@@ -101,10 +139,11 @@ public class FeignHttpClientProperties {
} }
public int getConnectionTimeout() { public int getConnectionTimeout() {
return connectionTimeout; return this.connectionTimeout;
} }
public void setConnectionTimeout(int connectionTimeout) { public void setConnectionTimeout(int connectionTimeout) {
this.connectionTimeout = connectionTimeout; this.connectionTimeout = connectionTimeout;
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2015 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -24,10 +24,17 @@ import java.util.Map;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import static java.util.Optional.ofNullable;
/** /**
* @author Spencer Gibb * @author Spencer Gibb
* @author Olga Maciaszek-Sharma
*/ */
public class FeignUtils { public final class FeignUtils {
private FeignUtils() {
throw new IllegalStateException("Can't instantiate a utility class");
}
static HttpHeaders getHttpHeaders(Map<String, Collection<String>> headers) { static HttpHeaders getHttpHeaders(Map<String, Collection<String>> headers) {
HttpHeaders httpHeaders = new HttpHeaders(); HttpHeaders httpHeaders = new HttpHeaders();
@@ -47,4 +54,12 @@ public class FeignUtils {
return headers; return headers;
} }
static Collection<String> addTemplateParameter(Collection<String> possiblyNull,
String paramName) {
Collection<String> params = ofNullable(possiblyNull).map(ArrayList::new)
.orElse(new ArrayList<>());
params.add(String.format("{%s}", paramName));
return params;
}
} }
@@ -0,0 +1,171 @@
/*
* Copyright 2013-2018 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.Iterator;
import java.util.List;
import java.util.function.Function;
import com.fasterxml.jackson.annotation.JsonIgnore;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.core.Version;
import com.fasterxml.jackson.databind.Module;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
/**
* This jackson module provides support to deserialize spring {@link Page} objects.
*
* @author Pascal Büttiker
*/
public class PageJacksonModule extends Module {
@Override
public String getModuleName() {
return "PageJacksonModule";
}
@Override
public Version version() {
return new Version(0, 1, 0, "", null, null);
}
@Override
public void setupModule(SetupContext context) {
context.setMixInAnnotations(Page.class, PageMixIn.class);
}
@JsonDeserialize(as = SimplePageImpl.class)
private interface PageMixIn {
}
static class SimplePageImpl<T> implements Page<T> {
private final Page<T> delegate;
SimplePageImpl(@JsonProperty("content") List<T> content,
@JsonProperty("page") int number, @JsonProperty("size") int size,
@JsonProperty("totalElements") long totalElements) {
delegate = new PageImpl<>(content, PageRequest.of(number, size),
totalElements);
}
@JsonProperty
@Override
public int getTotalPages() {
return delegate.getTotalPages();
}
@JsonProperty
@Override
public long getTotalElements() {
return delegate.getTotalElements();
}
@JsonProperty("page")
@Override
public int getNumber() {
return delegate.getNumber();
}
@JsonProperty
@Override
public int getSize() {
return delegate.getSize();
}
@JsonProperty
@Override
public int getNumberOfElements() {
return delegate.getNumberOfElements();
}
@JsonProperty
@Override
public List<T> getContent() {
return delegate.getContent();
}
@JsonProperty
@Override
public boolean hasContent() {
return delegate.hasContent();
}
@JsonIgnore
@Override
public Sort getSort() {
return delegate.getSort();
}
@JsonProperty
@Override
public boolean isFirst() {
return delegate.isFirst();
}
@JsonProperty
@Override
public boolean isLast() {
return delegate.isLast();
}
@JsonIgnore
@Override
public boolean hasNext() {
return delegate.hasNext();
}
@JsonIgnore
@Override
public boolean hasPrevious() {
return delegate.hasPrevious();
}
@JsonIgnore
@Override
public Pageable nextPageable() {
return delegate.nextPageable();
}
@JsonIgnore
@Override
public Pageable previousPageable() {
return delegate.previousPageable();
}
@JsonIgnore
@Override
public <S> Page<S> map(Function<? super T, ? extends S> converter) {
return delegate.map(converter);
}
@JsonIgnore
@Override
public Iterator<T> iterator() {
return delegate.iterator();
}
}
}
@@ -0,0 +1,122 @@
/*
* Copyright 2013-2018 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.lang.reflect.Type;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
import feign.RequestTemplate;
import feign.codec.EncodeException;
import feign.codec.Encoder;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
/**
* Provides support for encoding spring Pageable via composition.
*
* @author Pascal Büttiker
*/
public class PageableSpringEncoder implements Encoder {
private final Encoder delegate;
/**
* Page index parameter name.
*/
private String pageParameter = "page";
/**
* Page size parameter name.
*/
private String sizeParameter = "size";
/**
* Sort parameter name.
*/
private String sortParameter = "sort";
/**
* Creates a new PageableSpringEncoder with the given delegate for fallback. If no
* delegate is provided and this encoder cant handle the request, an EncodeException
* is thrown.
* @param delegate The optional delegate.
*/
public PageableSpringEncoder(Encoder delegate) {
this.delegate = delegate;
}
@Override
public void encode(Object object, Type bodyType, RequestTemplate template)
throws EncodeException {
if (supports(object)) {
if (object instanceof Pageable) {
Pageable pageable = (Pageable) object;
if (pageable.isPaged()) {
template.query(pageParameter, pageable.getPageNumber() + "");
template.query(sizeParameter, pageable.getPageSize() + "");
}
if (pageable.getSort() != null) {
applySort(template, pageable.getSort());
}
}
else if (object instanceof Sort) {
Sort sort = (Sort) object;
applySort(template, sort);
}
}
else {
if (delegate != null) {
delegate.encode(object, bodyType, template);
}
else {
throw new EncodeException(
"PageableSpringEncoder does not support the given object "
+ object.getClass()
+ " and no delegate was provided for fallback!");
}
}
}
private void applySort(RequestTemplate template, Sort sort) {
Collection<String> existingSorts = template.queries().get("sort");
List<String> sortQueries = existingSorts != null ? new ArrayList<>(existingSorts)
: new ArrayList<>();
if (!sortParameter.equals("sort")) {
existingSorts = template.queries().get(sortParameter);
if (existingSorts != null) {
sortQueries.addAll(existingSorts);
}
}
for (Sort.Order order : sort) {
sortQueries.add(order.getProperty() + "," + order.getDirection());
}
if (!sortQueries.isEmpty()) {
template.query(sortParameter, sortQueries);
}
}
protected boolean supports(Object object) {
return object instanceof Pageable || object instanceof Sort;
}
}
@@ -1,3 +1,19 @@
/*
* Copyright 2013-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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; package org.springframework.cloud.openfeign.support;
import java.io.IOException; import java.io.IOException;
@@ -5,20 +21,21 @@ import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.LinkedList; import java.util.LinkedList;
import feign.FeignException;
import feign.Response;
import feign.codec.Decoder;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.util.LinkedMultiValueMap; import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap; import org.springframework.util.MultiValueMap;
import feign.FeignException;
import feign.Response;
import feign.codec.Decoder;
/** /**
* Decoder adds compatibility for Spring MVC's ResponseEntity to any other decoder via * Decoder adds compatibility for Spring MVC's ResponseEntity to any other decoder via
* composition. * composition.
* @author chadjaros *
* @author chad jaros
*/ */
public class ResponseEntityDecoder implements Decoder { public class ResponseEntityDecoder implements Decoder {
@@ -29,12 +46,12 @@ public class ResponseEntityDecoder implements Decoder {
} }
@Override @Override
public Object decode(final Response response, Type type) throws IOException, public Object decode(final Response response, Type type)
FeignException { throws IOException, FeignException {
if (isParameterizeHttpEntity(type)) { if (isParameterizeHttpEntity(type)) {
type = ((ParameterizedType) type).getActualTypeArguments()[0]; type = ((ParameterizedType) type).getActualTypeArguments()[0];
Object decodedObject = decoder.decode(response, type); Object decodedObject = this.decoder.decode(response, type);
return createResponse(decodedObject, response); return createResponse(decodedObject, response);
} }
@@ -42,7 +59,7 @@ public class ResponseEntityDecoder implements Decoder {
return createResponse(null, response); return createResponse(null, response);
} }
else { else {
return decoder.decode(response, type); return this.decoder.decode(response, type);
} }
} }
@@ -69,7 +86,8 @@ public class ResponseEntityDecoder implements Decoder {
headers.put(key, new LinkedList<>(response.headers().get(key))); headers.put(key, new LinkedList<>(response.headers().get(key)));
} }
return new ResponseEntity<>((T) instance, headers, HttpStatus.valueOf(response return new ResponseEntity<>((T) instance, headers,
.status())); HttpStatus.valueOf(response.status()));
} }
}
}
@@ -1,30 +1,32 @@
/* /*
* Copyright 2013-2017 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*
*/ */
package org.springframework.cloud.openfeign.support; package org.springframework.cloud.openfeign.support;
import static org.springframework.cloud.openfeign.support.FeignUtils.getHttpHeaders;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
import java.lang.reflect.ParameterizedType; import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.lang.reflect.WildcardType; import java.lang.reflect.WildcardType;
import feign.FeignException;
import feign.Response;
import feign.codec.DecodeException;
import feign.codec.Decoder;
import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
@@ -32,10 +34,7 @@ import org.springframework.http.HttpStatus;
import org.springframework.http.client.ClientHttpResponse; import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.client.HttpMessageConverterExtractor; import org.springframework.web.client.HttpMessageConverterExtractor;
import feign.FeignException; import static org.springframework.cloud.openfeign.support.FeignUtils.getHttpHeaders;
import feign.Response;
import feign.codec.DecodeException;
import feign.codec.Decoder;
/** /**
* @author Spencer Gibb * @author Spencer Gibb
@@ -59,11 +58,12 @@ public class SpringDecoder implements Decoder {
return extractor.extractData(new FeignResponseAdapter(response)); return extractor.extractData(new FeignResponseAdapter(response));
} }
throw new DecodeException( throw new DecodeException(response.status(),
"type is not an instance of Class or ParameterizedType: " + type); "type is not an instance of Class or ParameterizedType: " + type,
response.request());
} }
private class FeignResponseAdapter implements ClientHttpResponse { private final class FeignResponseAdapter implements ClientHttpResponse {
private final Response response; private final Response response;
@@ -1,18 +1,17 @@
/* /*
* Copyright 2013-2017 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*
*/ */
package org.springframework.cloud.openfeign.support; package org.springframework.cloud.openfeign.support;
@@ -24,33 +23,42 @@ import java.lang.reflect.Type;
import java.nio.charset.Charset; import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.Collection; import java.util.Collection;
import java.util.Objects;
import feign.Request;
import feign.RequestTemplate;
import feign.codec.EncodeException;
import feign.codec.Encoder;
import feign.form.spring.SpringFormEncoder;
import org.apache.commons.logging.Log; import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory; import org.apache.commons.logging.LogFactory;
import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.encoding.HttpEncoding;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpOutputMessage; import org.springframework.http.HttpOutputMessage;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.converter.ByteArrayHttpMessageConverter; import org.springframework.http.converter.ByteArrayHttpMessageConverter;
import org.springframework.http.converter.GenericHttpMessageConverter;
import org.springframework.http.converter.HttpMessageConverter; import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter; import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter;
import org.springframework.web.multipart.MultipartFile;
import feign.RequestTemplate;
import feign.codec.EncodeException;
import feign.codec.Encoder;
import static org.springframework.cloud.openfeign.support.FeignUtils.getHeaders; import static org.springframework.cloud.openfeign.support.FeignUtils.getHeaders;
import static org.springframework.cloud.openfeign.support.FeignUtils.getHttpHeaders; import static org.springframework.cloud.openfeign.support.FeignUtils.getHttpHeaders;
/** /**
* @author Spencer Gibb * @author Spencer Gibb
* @author ScienJus * @author Scien Jus
* @author Ahmad Mozafarnia
*/ */
public class SpringEncoder implements Encoder { public class SpringEncoder implements Encoder {
private static final Log log = LogFactory.getLog(SpringEncoder.class); private static final Log log = LogFactory.getLog(SpringEncoder.class);
private final SpringFormEncoder springFormEncoder = new SpringFormEncoder();
private ObjectFactory<HttpMessageConverters> messageConverters; private ObjectFactory<HttpMessageConverters> messageConverters;
public SpringEncoder(ObjectFactory<HttpMessageConverters> messageConverters) { public SpringEncoder(ObjectFactory<HttpMessageConverters> messageConverters) {
@@ -62,8 +70,8 @@ public class SpringEncoder implements Encoder {
throws EncodeException { throws EncodeException {
// template.body(conversionService.convert(object, String.class)); // template.body(conversionService.convert(object, String.class));
if (requestBody != null) { if (requestBody != null) {
Class<?> requestType = requestBody.getClass(); Collection<String> contentTypes = request.headers()
Collection<String> contentTypes = request.headers().get("Content-Type"); .get(HttpEncoding.CONTENT_TYPE);
MediaType requestContentType = null; MediaType requestContentType = null;
if (contentTypes != null && !contentTypes.isEmpty()) { if (contentTypes != null && !contentTypes.isEmpty()) {
@@ -71,31 +79,37 @@ public class SpringEncoder implements Encoder {
requestContentType = MediaType.valueOf(type); requestContentType = MediaType.valueOf(type);
} }
for (HttpMessageConverter<?> messageConverter : this.messageConverters if (bodyType != null && bodyType.equals(MultipartFile.class)) {
if (Objects.equals(requestContentType, MediaType.MULTIPART_FORM_DATA)) {
this.springFormEncoder.encode(requestBody, bodyType, request);
return;
}
else {
String message = "Content-Type \"" + MediaType.MULTIPART_FORM_DATA
+ "\" not set for request body of type "
+ requestBody.getClass().getSimpleName();
throw new EncodeException(message);
}
}
for (HttpMessageConverter messageConverter : this.messageConverters
.getObject().getConverters()) { .getObject().getConverters()) {
if (messageConverter.canWrite(requestType, requestContentType)) { FeignOutputMessage outputMessage;
if (log.isDebugEnabled()) { try {
if (requestContentType != null) { if (messageConverter instanceof GenericHttpMessageConverter) {
log.debug("Writing [" + requestBody + "] as \"" outputMessage = checkAndWrite(requestBody, bodyType,
+ requestContentType + "\" using [" requestContentType,
+ messageConverter + "]"); (GenericHttpMessageConverter) messageConverter, request);
}
else {
log.debug("Writing [" + requestBody + "] using ["
+ messageConverter + "]");
}
} }
else {
FeignOutputMessage outputMessage = new FeignOutputMessage(request); outputMessage = checkAndWrite(requestBody, requestContentType,
try { messageConverter, request);
@SuppressWarnings("unchecked")
HttpMessageConverter<Object> copy = (HttpMessageConverter<Object>) messageConverter;
copy.write(requestBody, requestContentType, outputMessage);
}
catch (IOException ex) {
throw new EncodeException("Error converting request body", ex);
} }
}
catch (IOException ex) {
throw new EncodeException("Error converting request body", ex);
}
if (outputMessage != null) {
// clear headers // clear headers
request.headers(null); request.headers(null);
// converters can modify headers, so update the request // converters can modify headers, so update the request
@@ -106,18 +120,22 @@ public class SpringEncoder implements Encoder {
Charset charset; Charset charset;
if (messageConverter instanceof ByteArrayHttpMessageConverter) { if (messageConverter instanceof ByteArrayHttpMessageConverter) {
charset = null; charset = null;
} else if (messageConverter instanceof ProtobufHttpMessageConverter && }
ProtobufHttpMessageConverter.PROTOBUF.isCompatibleWith(outputMessage.getHeaders().getContentType())) { else if (messageConverter instanceof ProtobufHttpMessageConverter
&& ProtobufHttpMessageConverter.PROTOBUF.isCompatibleWith(
outputMessage.getHeaders().getContentType())) {
charset = null; charset = null;
} else { }
else {
charset = StandardCharsets.UTF_8; charset = StandardCharsets.UTF_8;
} }
request.body(outputMessage.getOutputStream().toByteArray(), charset); request.body(Request.Body.encoded(
outputMessage.getOutputStream().toByteArray(), charset));
return; return;
} }
} }
String message = "Could not write request: no suitable HttpMessageConverter " String message = "Could not write request: no suitable HttpMessageConverter "
+ "found for request type [" + requestType.getName() + "]"; + "found for request type [" + requestBody.getClass().getName() + "]";
if (requestContentType != null) { if (requestContentType != null) {
message += " and content type [" + requestContentType + "]"; message += " and content type [" + requestContentType + "]";
} }
@@ -125,14 +143,57 @@ public class SpringEncoder implements Encoder {
} }
} }
private class FeignOutputMessage implements HttpOutputMessage { @SuppressWarnings("unchecked")
private FeignOutputMessage checkAndWrite(Object body, MediaType contentType,
HttpMessageConverter converter, RequestTemplate request) throws IOException {
if (converter.canWrite(body.getClass(), contentType)) {
logBeforeWrite(body, contentType, converter);
FeignOutputMessage outputMessage = new FeignOutputMessage(request);
converter.write(body, contentType, outputMessage);
return outputMessage;
}
else {
return null;
}
}
@SuppressWarnings("unchecked")
private FeignOutputMessage checkAndWrite(Object body, Type genericType,
MediaType contentType, GenericHttpMessageConverter converter,
RequestTemplate request) throws IOException {
if (converter.canWrite(genericType, body.getClass(), contentType)) {
logBeforeWrite(body, contentType, converter);
FeignOutputMessage outputMessage = new FeignOutputMessage(request);
converter.write(body, genericType, contentType, outputMessage);
return outputMessage;
}
else {
return null;
}
}
private void logBeforeWrite(Object requestBody, MediaType requestContentType,
HttpMessageConverter messageConverter) {
if (log.isDebugEnabled()) {
if (requestContentType != null) {
log.debug("Writing [" + requestBody + "] as \"" + requestContentType
+ "\" using [" + messageConverter + "]");
}
else {
log.debug(
"Writing [" + requestBody + "] using [" + messageConverter + "]");
}
}
}
private final class FeignOutputMessage implements HttpOutputMessage {
private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); private final ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
private final HttpHeaders httpHeaders; private final HttpHeaders httpHeaders;
private FeignOutputMessage(RequestTemplate request) { private FeignOutputMessage(RequestTemplate request) {
httpHeaders = getHttpHeaders(request.headers()); this.httpHeaders = getHttpHeaders(request.headers());
} }
@Override @Override
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2016 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -18,6 +18,7 @@ package org.springframework.cloud.openfeign.support;
import java.lang.annotation.Annotation; import java.lang.annotation.Annotation;
import java.lang.reflect.Method; import java.lang.reflect.Method;
import java.lang.reflect.Parameter;
import java.lang.reflect.Type; import java.lang.reflect.Type;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.Arrays; import java.util.Arrays;
@@ -28,16 +29,26 @@ import java.util.LinkedHashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import feign.Contract;
import feign.Feign;
import feign.MethodMetadata;
import feign.Param;
import feign.Request;
import org.springframework.cloud.openfeign.AnnotatedParameterProcessor; import org.springframework.cloud.openfeign.AnnotatedParameterProcessor;
import org.springframework.cloud.openfeign.annotation.PathVariableParameterProcessor; import org.springframework.cloud.openfeign.annotation.PathVariableParameterProcessor;
import org.springframework.cloud.openfeign.annotation.QueryMapParameterProcessor;
import org.springframework.cloud.openfeign.annotation.RequestHeaderParameterProcessor; import org.springframework.cloud.openfeign.annotation.RequestHeaderParameterProcessor;
import org.springframework.cloud.openfeign.annotation.RequestParamParameterProcessor; import org.springframework.cloud.openfeign.annotation.RequestParamParameterProcessor;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.ResourceLoaderAware; import org.springframework.context.ResourceLoaderAware;
import org.springframework.core.DefaultParameterNameDiscoverer; import org.springframework.core.DefaultParameterNameDiscoverer;
import org.springframework.core.MethodParameter;
import org.springframework.core.ParameterNameDiscoverer; import org.springframework.core.ParameterNameDiscoverer;
import org.springframework.core.ResolvableType;
import org.springframework.core.annotation.AnnotationUtils; import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.core.convert.ConversionService; import org.springframework.core.convert.ConversionService;
import org.springframework.core.convert.TypeDescriptor;
import org.springframework.core.convert.support.DefaultConversionService; import org.springframework.core.convert.support.DefaultConversionService;
import org.springframework.core.io.DefaultResourceLoader; import org.springframework.core.io.DefaultResourceLoader;
import org.springframework.core.io.ResourceLoader; import org.springframework.core.io.ResourceLoader;
@@ -48,16 +59,15 @@ import org.springframework.web.bind.annotation.RequestMethod;
import static feign.Util.checkState; import static feign.Util.checkState;
import static feign.Util.emptyToNull; import static feign.Util.emptyToNull;
import static org.springframework.cloud.openfeign.support.FeignUtils.addTemplateParameter;
import static org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation; import static org.springframework.core.annotation.AnnotatedElementUtils.findMergedAnnotation;
import feign.Contract;
import feign.Feign;
import feign.MethodMetadata;
import feign.Param;
/** /**
* @author Spencer Gibb * @author Spencer Gibb
* @author Abhijit Sarkar * @author Abhijit Sarkar
* @author Halvdan Hoem Grelland
* @author Aram Peres
* @author Olga Maciaszek-Sharma
*/ */
public class SpringMvcContract extends Contract.BaseContract public class SpringMvcContract extends Contract.BaseContract
implements ResourceLoaderAware { implements ResourceLoaderAware {
@@ -66,13 +76,22 @@ public class SpringMvcContract extends Contract.BaseContract
private static final String CONTENT_TYPE = "Content-Type"; private static final String CONTENT_TYPE = "Content-Type";
private static final TypeDescriptor STRING_TYPE_DESCRIPTOR = TypeDescriptor
.valueOf(String.class);
private static final TypeDescriptor ITERABLE_TYPE_DESCRIPTOR = TypeDescriptor
.valueOf(Iterable.class);
private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer(); private static final ParameterNameDiscoverer PARAMETER_NAME_DISCOVERER = new DefaultParameterNameDiscoverer();
private final Map<Class<? extends Annotation>, AnnotatedParameterProcessor> annotatedArgumentProcessors; private final Map<Class<? extends Annotation>, AnnotatedParameterProcessor> annotatedArgumentProcessors;
private final Map<String, Method> processedMethods = new HashMap<>(); private final Map<String, Method> processedMethods = new HashMap<>();
private final ConversionService conversionService; private final ConversionService conversionService;
private final Param.Expander expander;
private final ConvertingExpanderFactory convertingExpanderFactory;
private ResourceLoader resourceLoader = new DefaultResourceLoader(); private ResourceLoader resourceLoader = new DefaultResourceLoader();
public SpringMvcContract() { public SpringMvcContract() {
@@ -100,7 +119,43 @@ public class SpringMvcContract extends Contract.BaseContract
} }
this.annotatedArgumentProcessors = toAnnotatedArgumentProcessorMap(processors); this.annotatedArgumentProcessors = toAnnotatedArgumentProcessorMap(processors);
this.conversionService = conversionService; this.conversionService = conversionService;
this.expander = new ConvertingExpander(conversionService); this.convertingExpanderFactory = new ConvertingExpanderFactory(conversionService);
}
private static TypeDescriptor createTypeDescriptor(Method method, int paramIndex) {
Parameter parameter = method.getParameters()[paramIndex];
MethodParameter methodParameter = MethodParameter.forParameter(parameter);
TypeDescriptor typeDescriptor = new TypeDescriptor(methodParameter);
// Feign applies the Param.Expander to each element of an Iterable, so in those
// cases we need to provide a TypeDescriptor of the element.
if (typeDescriptor.isAssignableTo(ITERABLE_TYPE_DESCRIPTOR)) {
TypeDescriptor elementTypeDescriptor = getElementTypeDescriptor(
typeDescriptor);
checkState(elementTypeDescriptor != null,
"Could not resolve element type of Iterable type %s. Not declared?",
typeDescriptor);
typeDescriptor = elementTypeDescriptor;
}
return typeDescriptor;
}
private static TypeDescriptor getElementTypeDescriptor(
TypeDescriptor typeDescriptor) {
TypeDescriptor elementTypeDescriptor = typeDescriptor.getElementTypeDescriptor();
// that means it's not a collection but it is iterable, gh-135
if (elementTypeDescriptor == null
&& Iterable.class.isAssignableFrom(typeDescriptor.getType())) {
ResolvableType type = typeDescriptor.getResolvableType().as(Iterable.class)
.getGeneric(0);
if (type.resolve() == null) {
return null;
}
return new TypeDescriptor(type, null, typeDescriptor.getAnnotations());
}
return elementTypeDescriptor;
} }
@Override @Override
@@ -121,7 +176,7 @@ public class SpringMvcContract extends Contract.BaseContract
if (!pathValue.startsWith("/")) { if (!pathValue.startsWith("/")) {
pathValue = "/" + pathValue; pathValue = "/" + pathValue;
} }
data.template().insert(0, pathValue); data.template().uri(pathValue);
} }
} }
} }
@@ -167,7 +222,7 @@ public class SpringMvcContract extends Contract.BaseContract
methods = new RequestMethod[] { RequestMethod.GET }; methods = new RequestMethod[] { RequestMethod.GET };
} }
checkOne(method, methods, "method"); checkOne(method, methods, "method");
data.template().method(methods[0].name()); data.template().method(Request.HttpMethod.valueOf(methods[0].name()));
// path // path
checkAtMostOne(method, methodMapping.value(), "value"); checkAtMostOne(method, methodMapping.value(), "value");
@@ -176,11 +231,10 @@ public class SpringMvcContract extends Contract.BaseContract
if (pathValue != null) { if (pathValue != null) {
pathValue = resolve(pathValue); pathValue = resolve(pathValue);
// Append path from @RequestMapping if value is present on method // Append path from @RequestMapping if value is present on method
if (!pathValue.startsWith("/") if (!pathValue.startsWith("/") && !data.template().path().endsWith("/")) {
&& !data.template().toString().endsWith("/")) {
pathValue = "/" + pathValue; pathValue = "/" + pathValue;
} }
data.template().append(pathValue); data.template().uri(pathValue, true);
} }
} }
@@ -239,10 +293,17 @@ public class SpringMvcContract extends Contract.BaseContract
processParameterAnnotation, method); processParameterAnnotation, method);
} }
} }
if (isHttpAnnotation && data.indexToExpander().get(paramIndex) == null
&& this.conversionService.canConvert( if (isHttpAnnotation && data.indexToExpander().get(paramIndex) == null) {
method.getParameterTypes()[paramIndex], String.class)) { TypeDescriptor typeDescriptor = createTypeDescriptor(method, paramIndex);
data.indexToExpander().put(paramIndex, this.expander); if (this.conversionService.canConvert(typeDescriptor,
STRING_TYPE_DESCRIPTOR)) {
Param.Expander expander = this.convertingExpanderFactory
.getExpander(typeDescriptor);
if (expander != null) {
data.indexToExpander().put(paramIndex, expander);
}
}
} }
return isHttpAnnotation; return isHttpAnnotation;
} }
@@ -275,7 +336,7 @@ public class SpringMvcContract extends Contract.BaseContract
int index = header.indexOf('='); int index = header.indexOf('=');
if (!header.contains("!=") && index >= 0) { if (!header.contains("!=") && index >= 0) {
md.template().header(resolve(header.substring(0, index)), md.template().header(resolve(header.substring(0, index)),
resolve(header.substring(index + 1).trim())); resolve(header.substring(index + 1).trim()));
} }
} }
} }
@@ -297,6 +358,7 @@ public class SpringMvcContract extends Contract.BaseContract
annotatedArgumentResolvers.add(new PathVariableParameterProcessor()); annotatedArgumentResolvers.add(new PathVariableParameterProcessor());
annotatedArgumentResolvers.add(new RequestParamParameterProcessor()); annotatedArgumentResolvers.add(new RequestParamParameterProcessor());
annotatedArgumentResolvers.add(new RequestHeaderParameterProcessor()); annotatedArgumentResolvers.add(new RequestHeaderParameterProcessor());
annotatedArgumentResolvers.add(new QueryMapParameterProcessor());
return annotatedArgumentResolvers; return annotatedArgumentResolvers;
} }
@@ -319,13 +381,51 @@ public class SpringMvcContract extends Contract.BaseContract
parameterAnnotation.annotationType(), null); parameterAnnotation.annotationType(), null);
} }
private boolean shouldAddParameterName(int parameterIndex, Type[] parameterTypes, String[] parameterNames) { private boolean shouldAddParameterName(int parameterIndex, Type[] parameterTypes,
String[] parameterNames) {
// has a parameter name // has a parameter name
return parameterNames != null && parameterNames.length > parameterIndex return parameterNames != null && parameterNames.length > parameterIndex
// has a type // has a type
&& parameterTypes != null && parameterTypes.length > parameterIndex; && parameterTypes != null && parameterTypes.length > parameterIndex;
} }
/**
* @deprecated Not used internally anymore. Will be removed in the future.
*/
@Deprecated
public static class ConvertingExpander implements Param.Expander {
private final ConversionService conversionService;
public ConvertingExpander(ConversionService conversionService) {
this.conversionService = conversionService;
}
@Override
public String expand(Object value) {
return this.conversionService.convert(value, String.class);
}
}
private static class ConvertingExpanderFactory {
private final ConversionService conversionService;
ConvertingExpanderFactory(ConversionService conversionService) {
this.conversionService = conversionService;
}
Param.Expander getExpander(TypeDescriptor typeDescriptor) {
return value -> {
Object converted = this.conversionService.convert(value, typeDescriptor,
STRING_TYPE_DESCRIPTOR);
return (String) converted;
};
}
}
private class SimpleAnnotatedParameterContext private class SimpleAnnotatedParameterContext
implements AnnotatedParameterProcessor.AnnotatedParameterContext { implements AnnotatedParameterProcessor.AnnotatedParameterContext {
@@ -333,7 +433,7 @@ public class SpringMvcContract extends Contract.BaseContract
private final int parameterIndex; private final int parameterIndex;
public SimpleAnnotatedParameterContext(MethodMetadata methodMetadata, SimpleAnnotatedParameterContext(MethodMetadata methodMetadata,
int parameterIndex) { int parameterIndex) {
this.methodMetadata = methodMetadata; this.methodMetadata = methodMetadata;
this.parameterIndex = parameterIndex; this.parameterIndex = parameterIndex;
@@ -357,22 +457,9 @@ public class SpringMvcContract extends Contract.BaseContract
@Override @Override
public Collection<String> setTemplateParameter(String name, public Collection<String> setTemplateParameter(String name,
Collection<String> rest) { Collection<String> rest) {
return addTemplatedParam(rest, name); return addTemplateParameter(rest, name);
}
}
public static class ConvertingExpander implements Param.Expander {
private final ConversionService conversionService;
public ConvertingExpander(ConversionService conversionService) {
this.conversionService = conversionService;
}
@Override
public String expand(Object value) {
return this.conversionService.convert(value, String.class);
} }
} }
} }
@@ -0,0 +1,36 @@
{
"groups": [
],
"properties": [
{
"name": "feign.hystrix.enabled",
"type": "java.lang.Boolean",
"description": "If true, an OpenFeign client will be wrapped with a Hystrix circuit breaker.",
"defaultValue": "false"
},
{
"name": "feign.httpclient.enabled",
"type": "java.lang.Boolean",
"description": "Enables the use of the Apache HTTP Client by Feign.",
"defaultValue": "true"
},
{
"name": "feign.okhttp.enabled",
"type": "java.lang.Boolean",
"description": "Enables the use of the OK HTTP Client by Feign.",
"defaultValue": "false"
},
{
"name": "feign.compression.response.enabled",
"type": "java.lang.Boolean",
"description": "Enables the response from Feign to be compressed.",
"defaultValue": "false"
},
{
"name": "feign.compression.request.enabled",
"type": "java.lang.Boolean",
"description": "Enables the request sent by Feign to be compressed.",
"defaultValue": "false"
}
]
}
@@ -0,0 +1,58 @@
/*
* Copyright 2013-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.openfeign;
import feign.codec.Encoder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.netflix.archaius.ArchaiusAutoConfiguration;
import org.springframework.cloud.openfeign.support.PageableSpringEncoder;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @author Spencer Gibb
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = EnableFeignClientsSpringDataTests.PlainConfiguration.class)
@DirtiesContext
public class EnableFeignClientsSpringDataTests {
@Autowired
private FeignContext feignContext;
@Test
public void encoderDefaultCorrect() {
PageableSpringEncoder.class
.cast(this.feignContext.getInstance("foo", Encoder.class));
}
@Configuration
@Import({ PropertyPlaceholderAutoConfiguration.class, ArchaiusAutoConfiguration.class,
FeignAutoConfiguration.class })
protected static class PlainConfiguration {
}
}
@@ -1,35 +1,21 @@
/* /*
* Copyright 2013-2017 the original author or authors. * Copyright 2013-2015 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*
*/ */
package org.springframework.cloud.openfeign; package org.springframework.cloud.openfeign;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.netflix.archaius.ArchaiusAutoConfiguration;
import org.springframework.cloud.openfeign.support.SpringEncoder;
import org.springframework.cloud.openfeign.support.SpringMvcContract;
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 feign.Contract; import feign.Contract;
import feign.Feign; import feign.Feign;
import feign.Logger; import feign.Logger;
@@ -37,50 +23,74 @@ import feign.codec.Decoder;
import feign.codec.Encoder; import feign.codec.Encoder;
import feign.optionals.OptionalDecoder; import feign.optionals.OptionalDecoder;
import feign.slf4j.Slf4jLogger; import feign.slf4j.Slf4jLogger;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.netflix.archaius.ArchaiusAutoConfiguration;
import org.springframework.cloud.openfeign.support.SpringEncoder;
import org.springframework.cloud.openfeign.support.SpringMvcContract;
import org.springframework.cloud.test.ClassPathExclusions;
import org.springframework.cloud.test.ModifiedClassPathRunner;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
/** /**
* @author Spencer Gibb * @author Spencer Gibb
*/ */
@RunWith(SpringJUnit4ClassRunner.class) @RunWith(ModifiedClassPathRunner.class)
@SpringBootTest(classes = EnableFeignClientsTests.PlainConfiguration.class) @ClassPathExclusions({ "spring-data-commons-*.jar" })
@DirtiesContext
public class EnableFeignClientsTests { public class EnableFeignClientsTests {
@Autowired private ConfigurableApplicationContext context;
private FeignContext feignContext;
@Before
public void setUp() {
context = new SpringApplicationBuilder().web(WebApplicationType.NONE)
.properties("debug=true", "feign.httpclient.enabled=false")
.sources(EnableFeignClientsTests.PlainConfiguration.class).run();
}
@After
public void tearDown() {
if (context != null) {
context.close();
}
}
@Test @Test
public void decoderDefaultCorrect() { public void decoderDefaultCorrect() {
OptionalDecoder.class OptionalDecoder.class.cast(this.context.getBeansOfType(Decoder.class).get(0));
.cast(this.feignContext.getInstance("foo", Decoder.class));
} }
@Test @Test
public void encoderDefaultCorrect() { public void encoderDefaultCorrect() {
SpringEncoder.class.cast(this.feignContext.getInstance("foo", Encoder.class)); SpringEncoder.class.cast(this.context.getBeansOfType(Encoder.class).get(0));
} }
@Test @Test
public void loggerDefaultCorrect() { public void loggerDefaultCorrect() {
Slf4jLogger.class.cast(this.feignContext.getInstance("foo", Logger.class)); Slf4jLogger.class.cast(this.context.getBeansOfType(Logger.class).get(0));
} }
@Test @Test
public void contractDefaultCorrect() { public void contractDefaultCorrect() {
SpringMvcContract.class SpringMvcContract.class.cast(this.context.getBeansOfType(Contract.class).get(0));
.cast(this.feignContext.getInstance("foo", Contract.class));
} }
@Test @Test
public void builderDefaultCorrect() { public void builderDefaultCorrect() {
Feign.Builder.class Feign.Builder.class.cast(this.context.getBeansOfType(Feign.Builder.class).get(0));
.cast(this.feignContext.getInstance("foo", Feign.Builder.class));
} }
@Configuration @Configuration
@Import({ PropertyPlaceholderAutoConfiguration.class, ArchaiusAutoConfiguration.class, @Import({ ArchaiusAutoConfiguration.class, FeignAutoConfiguration.class })
FeignAutoConfiguration.class })
protected static class PlainConfiguration { protected static class PlainConfiguration {
} }
} }
@@ -0,0 +1,162 @@
/*
* Copyright 2013-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.openfeign;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.nio.file.ClosedFileSystemException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import org.hamcrest.Matchers;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.ExpectedException;
import org.mockito.Mockito;
import org.springframework.cloud.openfeign.testclients.TestClient;
import org.springframework.context.ApplicationContext;
import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
/**
* @author Sven Döring
*/
public class FeignClientBuilderTests {
@Rule
public ExpectedException thrown = ExpectedException.none();
private FeignClientBuilder feignClientBuilder;
private ApplicationContext applicationContext;
private static Object getDefaultValueFromFeignClientAnnotation(
final String methodName) {
final Method method = ReflectionUtils.findMethod(FeignClient.class, methodName);
return method.getDefaultValue();
}
private static void assertFactoryBeanField(final FeignClientBuilder.Builder builder,
final String fieldName, final Object expectedValue) {
final Field factoryBeanField = ReflectionUtils
.findField(FeignClientBuilder.Builder.class, "feignClientFactoryBean");
ReflectionUtils.makeAccessible(factoryBeanField);
final FeignClientFactoryBean factoryBean = (FeignClientFactoryBean) ReflectionUtils
.getField(factoryBeanField, builder);
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);
}
@Before
public void setUp() {
this.applicationContext = Mockito.mock(ApplicationContext.class);
this.feignClientBuilder = new FeignClientBuilder(this.applicationContext);
}
@Test
public void safetyCheckForNewFieldsOnTheFeignClientAnnotation() {
final List<String> methodNames = new ArrayList();
for (final Method method : FeignClient.class.getMethods()) {
methodNames.add(method.getName());
}
methodNames.removeAll(
Arrays.asList("annotationType", "value", "serviceId", "qualifier",
"configuration", "primary", "equals", "hashCode", "toString"));
Collections.sort(methodNames);
// If this safety check fails the Builder has to be updated.
// (1) Either a field was removed from the FeignClient annotation and so it has to
// be removed
// on this builder class.
// (2) Or a new field was added and the builder class has to be extended with this
// new field.
assertThat(methodNames).containsExactly("contextId", "decode404", "fallback",
"fallbackFactory", "name", "path", "url");
}
@Test
public void forType_preinitializedBuilder() {
// when:
final FeignClientBuilder.Builder builder = this.feignClientBuilder
.forType(FeignClientBuilderTests.class, "TestClient");
// then:
assertFactoryBeanField(builder, "applicationContext", this.applicationContext);
assertFactoryBeanField(builder, "type", FeignClientBuilderTests.class);
assertFactoryBeanField(builder, "name", "TestClient");
assertFactoryBeanField(builder, "contextId", "TestClient");
// and:
assertFactoryBeanField(builder, "url",
getDefaultValueFromFeignClientAnnotation("url"));
assertFactoryBeanField(builder, "path",
getDefaultValueFromFeignClientAnnotation("path"));
assertFactoryBeanField(builder, "decode404",
getDefaultValueFromFeignClientAnnotation("decode404"));
assertFactoryBeanField(builder, "fallback",
getDefaultValueFromFeignClientAnnotation("fallback"));
assertFactoryBeanField(builder, "fallbackFactory",
getDefaultValueFromFeignClientAnnotation("fallbackFactory"));
}
@Test
public void forType_allFieldsSetOnBuilder() {
// when:
final FeignClientBuilder.Builder builder = this.feignClientBuilder
.forType(FeignClientBuilderTests.class, "TestClient").decode404(true)
.fallback(Object.class).fallbackFactory(Object.class).path("Path/")
.url("Url/");
// then:
assertFactoryBeanField(builder, "applicationContext", this.applicationContext);
assertFactoryBeanField(builder, "type", FeignClientBuilderTests.class);
assertFactoryBeanField(builder, "name", "TestClient");
// and:
assertFactoryBeanField(builder, "url", "http://Url/");
assertFactoryBeanField(builder, "path", "/Path");
assertFactoryBeanField(builder, "decode404", true);
assertFactoryBeanField(builder, "fallback", Object.class);
assertFactoryBeanField(builder, "fallbackFactory", Object.class);
}
@Test
public void forType_build() {
// given:
Mockito.when(this.applicationContext.getBean(FeignContext.class))
.thenThrow(new ClosedFileSystemException()); // throw an unusual exception
// in the
// FeignClientFactoryBean
final FeignClientBuilder.Builder builder = this.feignClientBuilder
.forType(TestClient.class, "TestClient");
// expect: 'the build will fail right after calling build() with the mocked
// unusual exception'
this.thrown.expect(Matchers.isA(ClosedFileSystemException.class));
builder.build();
}
}
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2015 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -16,16 +16,14 @@
package org.springframework.cloud.openfeign; package org.springframework.cloud.openfeign;
import java.util.Arrays;
import org.junit.Test; import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import java.util.Arrays; import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.is;
import static org.hamcrest.Matchers.notNullValue;
import static org.hamcrest.Matchers.nullValue;
import static org.junit.Assert.assertThat;
/** /**
* @author Spencer Gibb * @author Spencer Gibb
@@ -42,33 +40,43 @@ public class FeignClientFactoryTests {
getSpec("bar", BarConfig.class))); getSpec("bar", BarConfig.class)));
Foo foo = context.getInstance("foo", Foo.class); Foo foo = context.getInstance("foo", Foo.class);
assertThat("foo was null", foo, is(notNullValue())); assertThat(foo).as("foo was null").isNotNull();
Bar bar = context.getInstance("bar", Bar.class); Bar bar = context.getInstance("bar", Bar.class);
assertThat("bar was null", bar, is(notNullValue())); assertThat(bar).as("bar was null").isNotNull();
Bar foobar = context.getInstance("foo", Bar.class); Bar foobar = context.getInstance("foo", Bar.class);
assertThat("bar was not null", foobar, is(nullValue())); assertThat(foobar).as("bar was not null").isNull();
} }
private FeignClientSpecification getSpec(String name, Class<?> configClass) { private FeignClientSpecification getSpec(String name, Class<?> configClass) {
return new FeignClientSpecification(name, new Class[]{configClass}); return new FeignClientSpecification(name, new Class[] { configClass });
} }
static class FooConfig { static class FooConfig {
@Bean @Bean
Foo foo() { Foo foo() {
return new Foo(); return new Foo();
} }
} }
static class Foo{}
static class Foo {
}
static class BarConfig { static class BarConfig {
@Bean @Bean
Bar bar() { Bar bar() {
return new Bar(); return new Bar();
} }
} }
static class Bar{}
static class Bar {
}
} }
@@ -1,42 +1,21 @@
/* /*
* Copyright 2013-2017 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*
*/ */
package org.springframework.cloud.openfeign; package org.springframework.cloud.openfeign;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.netflix.archaius.ArchaiusAutoConfiguration;
import org.springframework.cloud.openfeign.support.SpringEncoder;
import org.springframework.cloud.openfeign.support.SpringMvcContract;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import feign.Contract; import feign.Contract;
import feign.Feign; import feign.Feign;
import feign.Logger; import feign.Logger;
@@ -52,6 +31,24 @@ import feign.codec.ErrorDecoder;
import feign.hystrix.HystrixFeign; import feign.hystrix.HystrixFeign;
import feign.optionals.OptionalDecoder; import feign.optionals.OptionalDecoder;
import feign.slf4j.Slf4jLogger; import feign.slf4j.Slf4jLogger;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.context.PropertyPlaceholderAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.netflix.archaius.ArchaiusAutoConfiguration;
import org.springframework.cloud.openfeign.support.PageableSpringEncoder;
import org.springframework.cloud.openfeign.support.SpringMvcContract;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import static org.assertj.core.api.Assertions.assertThat;
/** /**
* @author Spencer Gibb * @author Spencer Gibb
@@ -72,8 +69,8 @@ public class FeignClientOverrideDefaultsTests {
@Test @Test
public void clientsAvailable() { public void clientsAvailable() {
assertNotNull(this.foo); assertThat(this.foo).isNotNull();
assertNotNull(this.bar); assertThat(this.bar).isNotNull();
} }
@Test @Test
@@ -85,7 +82,7 @@ public class FeignClientOverrideDefaultsTests {
@Test @Test
public void overrideEncoder() { public void overrideEncoder() {
Encoder.Default.class.cast(this.context.getInstance("foo", Encoder.class)); Encoder.Default.class.cast(this.context.getInstance("foo", Encoder.class));
SpringEncoder.class.cast(this.context.getInstance("bar", Encoder.class)); PageableSpringEncoder.class.cast(this.context.getInstance("bar", Encoder.class));
} }
@Test @Test
@@ -102,45 +99,62 @@ public class FeignClientOverrideDefaultsTests {
@Test @Test
public void overrideLoggerLevel() { public void overrideLoggerLevel() {
assertNull(this.context.getInstance("foo", Logger.Level.class)); assertThat(this.context.getInstance("foo", Logger.Level.class)).isNull();
assertEquals(Logger.Level.HEADERS, assertThat(this.context.getInstance("bar", Logger.Level.class))
this.context.getInstance("bar", Logger.Level.class)); .isEqualTo(Logger.Level.HEADERS);
} }
@Test @Test
public void overrideRetryer() { public void overrideRetryer() {
assertEquals(Retryer.NEVER_RETRY, this.context.getInstance("foo", Retryer.class)); assertThat(this.context.getInstance("foo", Retryer.class))
.isEqualTo(Retryer.NEVER_RETRY);
Retryer.Default.class.cast(this.context.getInstance("bar", Retryer.class)); Retryer.Default.class.cast(this.context.getInstance("bar", Retryer.class));
} }
@Test @Test
public void overrideErrorDecoder() { public void overrideErrorDecoder() {
assertNull(this.context.getInstance("foo", ErrorDecoder.class)); assertThat(this.context.getInstance("foo", ErrorDecoder.class)).isNull();
ErrorDecoder.Default.class ErrorDecoder.Default.class
.cast(this.context.getInstance("bar", ErrorDecoder.class)); .cast(this.context.getInstance("bar", ErrorDecoder.class));
} }
@Test @Test
public void overrideBuilder() { public void overrideBuilder() {
HystrixFeign.Builder.class.cast(this.context.getInstance("foo", Feign.Builder.class)); HystrixFeign.Builder.class
Feign.Builder.class .cast(this.context.getInstance("foo", Feign.Builder.class));
.cast(this.context.getInstance("bar", Feign.Builder.class)); Feign.Builder.class.cast(this.context.getInstance("bar", Feign.Builder.class));
} }
@Test @Test
public void overrideRequestOptions() { public void overrideRequestOptions() {
assertNull(this.context.getInstance("foo", Request.Options.class)); assertThat(this.context.getInstance("foo", Request.Options.class)).isNull();
Request.Options options = this.context.getInstance("bar", Request.Options.class); Request.Options options = this.context.getInstance("bar", Request.Options.class);
assertEquals(1, options.connectTimeoutMillis()); assertThat(options.connectTimeoutMillis()).isEqualTo(1);
assertEquals(1, options.readTimeoutMillis()); assertThat(options.readTimeoutMillis()).isEqualTo(1);
} }
@Test @Test
public void addRequestInterceptor() { public void addRequestInterceptor() {
assertEquals(1, assertThat(this.context.getInstances("foo", RequestInterceptor.class).size())
this.context.getInstances("foo", RequestInterceptor.class).size()); .isEqualTo(1);
assertEquals(2, assertThat(this.context.getInstances("bar", RequestInterceptor.class).size())
this.context.getInstances("bar", RequestInterceptor.class).size()); .isEqualTo(2);
}
@FeignClient(name = "foo", url = "http://foo", configuration = FooConfiguration.class)
interface FooClient {
@RequestLine("GET /")
String get();
}
@FeignClient(name = "bar", url = "http://bar", configuration = BarConfiguration.class)
interface BarClient {
@RequestMapping(value = "/", method = RequestMethod.GET)
String get();
} }
@Configuration @Configuration
@@ -148,6 +162,7 @@ public class FeignClientOverrideDefaultsTests {
@Import({ PropertyPlaceholderAutoConfiguration.class, ArchaiusAutoConfiguration.class, @Import({ PropertyPlaceholderAutoConfiguration.class, ArchaiusAutoConfiguration.class,
FeignAutoConfiguration.class }) FeignAutoConfiguration.class })
protected static class TestConfiguration { protected static class TestConfiguration {
@Bean @Bean
RequestInterceptor defaultRequestInterceptor() { RequestInterceptor defaultRequestInterceptor() {
return new RequestInterceptor() { return new RequestInterceptor() {
@@ -156,16 +171,11 @@ public class FeignClientOverrideDefaultsTests {
} }
}; };
} }
}
@FeignClient(name = "foo", url = "http://foo", configuration = FooConfiguration.class)
interface FooClient {
@RequestLine("GET /")
String get();
} }
public static class FooConfiguration { public static class FooConfiguration {
@Bean @Bean
public Decoder feignDecoder() { public Decoder feignDecoder() {
return new Decoder.Default(); return new Decoder.Default();
@@ -190,15 +200,11 @@ public class FeignClientOverrideDefaultsTests {
public Feign.Builder feignBuilder() { public Feign.Builder feignBuilder() {
return HystrixFeign.builder(); return HystrixFeign.builder();
} }
}
@FeignClient(name = "bar", url = "http://bar", configuration = BarConfiguration.class)
interface BarClient {
@RequestMapping(value = "/", method = RequestMethod.GET)
String get();
} }
public static class BarConfiguration { public static class BarConfiguration {
@Bean @Bean
Logger.Level feignLevel() { Logger.Level feignLevel() {
return Logger.Level.HEADERS; return Logger.Level.HEADERS;
@@ -223,5 +229,7 @@ public class FeignClientOverrideDefaultsTests {
RequestInterceptor feignRequestInterceptor() { RequestInterceptor feignRequestInterceptor() {
return new BasicAuthRequestInterceptor("user", "pass"); return new BasicAuthRequestInterceptor("user", "pass");
} }
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2018 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -16,6 +16,13 @@
package org.springframework.cloud.openfeign; package org.springframework.cloud.openfeign;
import java.lang.reflect.Type;
import java.util.Collections;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import feign.Request;
import feign.RequestInterceptor; import feign.RequestInterceptor;
import feign.RequestTemplate; import feign.RequestTemplate;
import feign.RetryableException; import feign.RetryableException;
@@ -25,13 +32,15 @@ import feign.codec.Encoder;
import feign.codec.ErrorDecoder; import feign.codec.ErrorDecoder;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext;
@@ -41,19 +50,16 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest; import static java.nio.charset.StandardCharsets.UTF_8;
import java.lang.reflect.Type; import static org.assertj.core.api.Assertions.assertThat;
import java.util.Collections; import static org.assertj.core.api.Assertions.fail;
import java.util.Map; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
/** /**
* @author Eko Kurniawan Khannedy * @author Eko Kurniawan Khannedy
*/ */
@RunWith(SpringJUnit4ClassRunner.class) @RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = FeignClientUsingPropertiesTests.Application.class, webEnvironment = WebEnvironment.RANDOM_PORT) @SpringBootTest(classes = FeignClientUsingPropertiesTests.Application.class, webEnvironment = RANDOM_PORT)
@TestPropertySource("classpath:feign-properties.properties") @TestPropertySource("classpath:feign-properties.properties")
@DirtiesContext @DirtiesContext
public class FeignClientUsingPropertiesTests { public class FeignClientUsingPropertiesTests {
@@ -74,38 +80,41 @@ public class FeignClientUsingPropertiesTests {
private FeignClientFactoryBean formFactoryBean; private FeignClientFactoryBean formFactoryBean;
public FeignClientUsingPropertiesTests() { public FeignClientUsingPropertiesTests() {
fooFactoryBean = new FeignClientFactoryBean(); this.fooFactoryBean = new FeignClientFactoryBean();
fooFactoryBean.setName("foo"); this.fooFactoryBean.setContextId("foo");
fooFactoryBean.setType(FeignClientFactoryBean.class); this.fooFactoryBean.setType(FeignClientFactoryBean.class);
barFactoryBean = new FeignClientFactoryBean(); this.barFactoryBean = new FeignClientFactoryBean();
barFactoryBean.setName("bar"); this.barFactoryBean.setContextId("bar");
barFactoryBean.setType(FeignClientFactoryBean.class); this.barFactoryBean.setType(FeignClientFactoryBean.class);
formFactoryBean = new FeignClientFactoryBean(); this.formFactoryBean = new FeignClientFactoryBean();
formFactoryBean.setName("form"); this.formFactoryBean.setContextId("form");
formFactoryBean.setType(FeignClientFactoryBean.class); this.formFactoryBean.setType(FeignClientFactoryBean.class);
} }
public FooClient fooClient() { public FooClient fooClient() {
fooFactoryBean.setApplicationContext(applicationContext); this.fooFactoryBean.setApplicationContext(this.applicationContext);
return fooFactoryBean.feign(context).target(FooClient.class, "http://localhost:" + this.port); return this.fooFactoryBean.feign(this.context).target(FooClient.class,
"http://localhost:" + this.port);
} }
public BarClient barClient() { public BarClient barClient() {
barFactoryBean.setApplicationContext(applicationContext); this.barFactoryBean.setApplicationContext(this.applicationContext);
return barFactoryBean.feign(context).target(BarClient.class, "http://localhost:" + this.port); return this.barFactoryBean.feign(this.context).target(BarClient.class,
"http://localhost:" + this.port);
} }
public FormClient formClient() { public FormClient formClient() {
formFactoryBean.setApplicationContext(applicationContext); this.formFactoryBean.setApplicationContext(this.applicationContext);
return formFactoryBean.feign(context).target(FormClient.class, "http://localhost:" + this.port); return this.formFactoryBean.feign(this.context).target(FormClient.class,
"http://localhost:" + this.port);
} }
@Test @Test
public void testFoo() { public void testFoo() {
String response = fooClient().foo(); String response = fooClient().foo();
assertEquals("OK", response); assertThat(response).isEqualTo("OK");
} }
@Test(expected = RetryableException.class) @Test(expected = RetryableException.class)
@@ -118,25 +127,26 @@ public class FeignClientUsingPropertiesTests {
public void testForm() { public void testForm() {
Map<String, String> request = Collections.singletonMap("form", "Data"); Map<String, String> request = Collections.singletonMap("form", "Data");
String response = formClient().form(request); String response = formClient().form(request);
assertEquals("Data", response); assertThat(response).isEqualTo("Data");
} }
protected interface FooClient { protected interface FooClient {
@RequestMapping(method = RequestMethod.GET, value = "/foo") @RequestMapping(method = RequestMethod.GET, value = "/foo")
String foo(); String foo();
} }
protected interface BarClient { protected interface BarClient {
@RequestMapping(method = RequestMethod.GET, value = "/bar") @RequestMapping(method = RequestMethod.GET, value = "/bar")
String bar(); String bar();
} }
protected interface FormClient { protected interface FormClient {
@RequestMapping(value = "/form", method = RequestMethod.POST, @RequestMapping(value = "/form", method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
String form(Map<String, String> form); String form(Map<String, String> form);
} }
@@ -144,14 +154,16 @@ public class FeignClientUsingPropertiesTests {
@Configuration @Configuration
@EnableAutoConfiguration @EnableAutoConfiguration
@RestController @RestController
@Import(NoSecurityConfiguration.class)
protected static class Application { protected static class Application {
@RequestMapping(method = RequestMethod.GET, value = "/foo") @RequestMapping(method = RequestMethod.GET, value = "/foo")
public String foo(HttpServletRequest request) throws IllegalAccessException { public String foo(HttpServletRequest request) throws IllegalAccessException {
if ("Foo".equals(request.getHeader("Foo")) && if ("Foo".equals(request.getHeader("Foo"))
"Bar".equals(request.getHeader("Bar"))) { && "Bar".equals(request.getHeader("Bar"))) {
return "OK"; return "OK";
} else { }
else {
throw new IllegalAccessException("It should has Foo and Bar header"); throw new IllegalAccessException("It should has Foo and Bar header");
} }
} }
@@ -162,8 +174,7 @@ public class FeignClientUsingPropertiesTests {
return "OK"; return "OK";
} }
@RequestMapping(value = "/form", method = RequestMethod.POST, @RequestMapping(value = "/form", method = RequestMethod.POST, consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
consumes = MediaType.APPLICATION_FORM_URLENCODED_VALUE)
public String form(HttpServletRequest request) { public String form(HttpServletRequest request) {
return request.getParameter("form"); return request.getParameter("form");
} }
@@ -171,17 +182,21 @@ public class FeignClientUsingPropertiesTests {
} }
public static class FooRequestInterceptor implements RequestInterceptor { public static class FooRequestInterceptor implements RequestInterceptor {
@Override @Override
public void apply(RequestTemplate template) { public void apply(RequestTemplate template) {
template.header("Foo", "Foo"); template.header("Foo", "Foo");
} }
} }
public static class BarRequestInterceptor implements RequestInterceptor { public static class BarRequestInterceptor implements RequestInterceptor {
@Override @Override
public void apply(RequestTemplate template) { public void apply(RequestTemplate template) {
template.header("Bar", "Bar"); template.header("Bar", "Bar");
} }
} }
public static class NoRetryer implements Retryer { public static class NoRetryer implements Retryer {
@@ -195,24 +210,29 @@ public class FeignClientUsingPropertiesTests {
public Retryer clone() { public Retryer clone() {
return this; return this;
} }
} }
public static class DefaultErrorDecoder extends ErrorDecoder.Default { public static class DefaultErrorDecoder extends ErrorDecoder.Default {
} }
public static class FormEncoder implements Encoder { public static class FormEncoder implements Encoder {
@Override @Override
public void encode(Object o, Type type, RequestTemplate requestTemplate) throws EncodeException { public void encode(Object o, Type type, RequestTemplate requestTemplate)
throws EncodeException {
Map<String, String> form = (Map<String, String>) o; Map<String, String> form = (Map<String, String>) o;
StringBuilder builder = new StringBuilder(); StringBuilder builder = new StringBuilder();
form.forEach((key, value) -> { form.forEach((key, value) -> {
builder.append(key + "=" + value + "&"); builder.append(key + "=" + value + "&");
}); });
requestTemplate.header(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_FORM_URLENCODED_VALUE); requestTemplate.header(HttpHeaders.CONTENT_TYPE,
requestTemplate.body(builder.toString()); MediaType.APPLICATION_FORM_URLENCODED_VALUE);
requestTemplate.body(Request.Body.bodyTemplate(builder.toString(), UTF_8));
} }
} }
} }
@@ -1,18 +1,17 @@
/* /*
* Copyright 2013-2016 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*
*/ */
package org.springframework.cloud.openfeign; package org.springframework.cloud.openfeign;
@@ -20,6 +19,7 @@ package org.springframework.cloud.openfeign;
import java.util.Collections; import java.util.Collections;
import org.junit.Test; import org.junit.Test;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
@@ -27,8 +27,7 @@ import org.springframework.mock.env.MockEnvironment;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import static org.hamcrest.Matchers.is; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertThat;
/** /**
* @author Spencer Gibb * @author Spencer Gibb
@@ -41,7 +40,6 @@ public class FeignClientsRegistrarTests {
testGetName("http://bad_hostname"); testGetName("http://bad_hostname");
} }
@Test(expected = IllegalStateException.class) @Test(expected = IllegalStateException.class)
public void badNameHttpsPrefix() { public void badNameHttpsPrefix() {
testGetName("https://bad_hostname"); testGetName("https://bad_hostname");
@@ -60,19 +58,19 @@ public class FeignClientsRegistrarTests {
@Test @Test
public void goodName() { public void goodName() {
String name = testGetName("good-name"); String name = testGetName("good-name");
assertThat("name was wrong", name, is("good-name")); assertThat(name).as("name was wrong").isEqualTo("good-name");
} }
@Test @Test
public void goodNameHttpPrefix() { public void goodNameHttpPrefix() {
String name = testGetName("http://good-name"); String name = testGetName("http://good-name");
assertThat("name was wrong", name, is("http://good-name")); assertThat(name).as("name was wrong").isEqualTo("http://good-name");
} }
@Test @Test
public void goodNameHttpsPrefix() { public void goodNameHttpsPrefix() {
String name = testGetName("https://goodname"); String name = testGetName("https://goodname");
assertThat("name was wrong", name, is("https://goodname")); assertThat(name).as("name was wrong").isEqualTo("https://goodname");
} }
private String testGetName(String name) { private String testGetName(String name) {
@@ -81,7 +79,6 @@ public class FeignClientsRegistrarTests {
return registrar.getName(Collections.<String, Object>singletonMap("name", name)); return registrar.getName(Collections.<String, Object>singletonMap("name", name));
} }
@Test(expected = IllegalArgumentException.class) @Test(expected = IllegalArgumentException.class)
public void testFallback() { public void testFallback() {
new AnnotationConfigApplicationContext(FallbackTestConfig.class); new AnnotationConfigApplicationContext(FallbackTestConfig.class);
@@ -92,30 +89,35 @@ public class FeignClientsRegistrarTests {
new AnnotationConfigApplicationContext(FallbackFactoryTestConfig.class); new AnnotationConfigApplicationContext(FallbackFactoryTestConfig.class);
} }
@Configuration
@EnableAutoConfiguration
@EnableFeignClients(clients = { FeignClientsRegistrarTests.FallbackClient.class})
protected static class FallbackTestConfig {
}
@FeignClient(name = "fallbackTestClient", url = "http://localhost:8080/", fallback = FallbackClient.class) @FeignClient(name = "fallbackTestClient", url = "http://localhost:8080/", fallback = FallbackClient.class)
protected interface FallbackClient { protected interface FallbackClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello") @RequestMapping(method = RequestMethod.GET, value = "/hello")
String fallbackTest(); String fallbackTest();
}
@Configuration
@EnableAutoConfiguration
@EnableFeignClients(clients = { FeignClientsRegistrarTests.FallbackFactoryClient.class})
protected static class FallbackFactoryTestConfig {
} }
@FeignClient(name = "fallbackFactoryTestClient", url = "http://localhost:8081/", fallbackFactory = FallbackFactoryClient.class) @FeignClient(name = "fallbackFactoryTestClient", url = "http://localhost:8081/", fallbackFactory = FallbackFactoryClient.class)
protected interface FallbackFactoryClient { protected interface FallbackFactoryClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello") @RequestMapping(method = RequestMethod.GET, value = "/hello")
String fallbackFactoryTest(); String fallbackFactoryTest();
}
@Configuration
@EnableAutoConfiguration
@EnableFeignClients(clients = { FeignClientsRegistrarTests.FallbackClient.class })
protected static class FallbackTestConfig {
}
@Configuration
@EnableAutoConfiguration
@EnableFeignClients(clients = {
FeignClientsRegistrarTests.FallbackFactoryClient.class })
protected static class FallbackFactoryTestConfig {
} }
} }
@@ -1,26 +1,29 @@
/* /*
* Copyright 2013-2018 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*
*/ */
package org.springframework.cloud.openfeign; package org.springframework.cloud.openfeign;
import java.util.Map; import java.util.Map;
import feign.Client;
import feign.RequestInterceptor;
import feign.httpclient.ApacheHttpClient;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.AutoConfigurations;
import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.boot.test.context.runner.ApplicationContextRunner;
@@ -35,10 +38,6 @@ import org.springframework.cloud.test.ModifiedClassPathRunner;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import feign.Client;
import feign.RequestInterceptor;
import feign.httpclient.ApacheHttpClient;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
/** /**
@@ -90,7 +89,9 @@ public class FeignCompressionTests {
* a bean here of type ApacheHttpClient so that the configuration will be * a bean here of type ApacheHttpClient so that the configuration will be
* loaded correctly. * loaded correctly.
*/ */
return (ApacheHttpClient) client; return (ApacheHttpClient) this.client;
} }
} }
}
}
@@ -1,25 +1,27 @@
/* /*
* Copyright 2013-2018 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*
*/ */
package org.springframework.cloud.openfeign; package org.springframework.cloud.openfeign;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import javax.net.ssl.SSLContextSpi; import javax.net.ssl.SSLContextSpi;
import javax.net.ssl.SSLSocketFactory; import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.X509TrustManager; import javax.net.ssl.X509TrustManager;
import org.apache.http.config.Lookup; import org.apache.http.config.Lookup;
import org.apache.http.conn.HttpClientConnectionManager; import org.apache.http.conn.HttpClientConnectionManager;
import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.socket.ConnectionSocketFactory;
@@ -28,6 +30,8 @@ import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.commons.httpclient.HttpClientConfiguration; import org.springframework.cloud.commons.httpclient.HttpClientConfiguration;
import org.springframework.cloud.test.ClassPathExclusions; import org.springframework.cloud.test.ClassPathExclusions;
@@ -35,8 +39,7 @@ import org.springframework.cloud.test.ModifiedClassPathRunner;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
import static org.junit.Assert.assertNotNull; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertNull;
/** /**
* @author Ryan Baxter * @author Ryan Baxter
@@ -49,35 +52,47 @@ public class FeignHttpClientConfigurationTests {
@Before @Before
public void setUp() { public void setUp() {
context = new SpringApplicationBuilder().properties("debug=true","feign.httpclient.disableSslValidation=true").web(false) this.context = new SpringApplicationBuilder()
.sources(HttpClientConfiguration.class, FeignAutoConfiguration.class).run(); .properties("debug=true", "feign.httpclient.disableSslValidation=true")
.web(WebApplicationType.NONE)
.sources(HttpClientConfiguration.class, FeignAutoConfiguration.class)
.run();
} }
@After @After
public void tearDown() { public void tearDown() {
if(context != null) { if (this.context != null) {
context.close(); this.context.close();
} }
} }
@Test @Test
public void disableSslTest() throws Exception { public void disableSslTest() throws Exception {
HttpClientConnectionManager connectionManager = context.getBean(HttpClientConnectionManager.class); HttpClientConnectionManager connectionManager = this.context
Lookup<ConnectionSocketFactory> socketFactoryRegistry = getConnectionSocketFactoryLookup(connectionManager); .getBean(HttpClientConnectionManager.class);
assertNotNull(socketFactoryRegistry.lookup("https")); Lookup<ConnectionSocketFactory> socketFactoryRegistry = getConnectionSocketFactoryLookup(
assertNull(this.getX509TrustManager(socketFactoryRegistry).getAcceptedIssuers()); connectionManager);
assertThat(socketFactoryRegistry.lookup("https")).isNotNull();
assertThat(this.getX509TrustManager(socketFactoryRegistry).getAcceptedIssuers())
.isNull();
} }
private Lookup<ConnectionSocketFactory> getConnectionSocketFactoryLookup(HttpClientConnectionManager connectionManager) { private Lookup<ConnectionSocketFactory> getConnectionSocketFactoryLookup(
DefaultHttpClientConnectionOperator connectionOperator = (DefaultHttpClientConnectionOperator)this.getField(connectionManager, "connectionOperator"); HttpClientConnectionManager connectionManager) {
return (Lookup)this.getField(connectionOperator, "socketFactoryRegistry"); DefaultHttpClientConnectionOperator connectionOperator = (DefaultHttpClientConnectionOperator) this
.getField(connectionManager, "connectionOperator");
return (Lookup) this.getField(connectionOperator, "socketFactoryRegistry");
} }
private X509TrustManager getX509TrustManager(Lookup<ConnectionSocketFactory> socketFactoryRegistry) { private X509TrustManager getX509TrustManager(
ConnectionSocketFactory connectionSocketFactory = (ConnectionSocketFactory)socketFactoryRegistry.lookup("https"); Lookup<ConnectionSocketFactory> socketFactoryRegistry) {
SSLSocketFactory sslSocketFactory = (SSLSocketFactory)this.getField(connectionSocketFactory, "socketfactory"); ConnectionSocketFactory connectionSocketFactory = (ConnectionSocketFactory) socketFactoryRegistry
SSLContextSpi sslContext = (SSLContextSpi)this.getField(sslSocketFactory, "context"); .lookup("https");
return (X509TrustManager)this.getField(sslContext, "trustManager"); SSLSocketFactory sslSocketFactory = (SSLSocketFactory) this
.getField(connectionSocketFactory, "socketfactory");
SSLContextSpi sslContext = (SSLContextSpi) this.getField(sslSocketFactory,
"context");
return (X509TrustManager) this.getField(sslContext, "trustManager");
} }
protected <T> Object getField(Object target, String name) { protected <T> Object getField(Object target, String name) {
@@ -1,41 +1,40 @@
/* /*
* Copyright 2013-2016 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*
*/ */
package org.springframework.cloud.openfeign; package org.springframework.cloud.openfeign;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.is;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertThat;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import java.util.Objects; import java.util.Objects;
import feign.Client;
import feign.Feign;
import feign.Target;
import feign.httpclient.ApacheHttpClient;
import org.junit.AfterClass; import org.junit.AfterClass;
import org.junit.BeforeClass; import org.junit.BeforeClass;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
@@ -44,16 +43,14 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import feign.Client; import static org.assertj.core.api.Assertions.assertThat;
import feign.Feign; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.DEFINED_PORT;
import feign.Target;
import feign.httpclient.ApacheHttpClient;
/** /**
* @author Spencer Gibb * @author Spencer Gibb
*/ */
@RunWith(SpringRunner.class) @RunWith(SpringRunner.class)
@SpringBootTest(classes = FeignHttpClientUrlTests.TestConfig.class, webEnvironment = WebEnvironment.DEFINED_PORT, value = { @SpringBootTest(classes = FeignHttpClientUrlTests.TestConfig.class, webEnvironment = DEFINED_PORT, value = {
"spring.application.name=feignclienturltest", "feign.hystrix.enabled=false", "spring.application.name=feignclienturltest", "feign.hystrix.enabled=false",
"feign.okhttp.enabled=false" }) "feign.okhttp.enabled=false" })
@DirtiesContext @DirtiesContext
@@ -61,6 +58,15 @@ public class FeignHttpClientUrlTests {
static int port; static int port;
@Autowired
BeanUrlClientNoProtocol beanClientNoProtocol;
@Autowired
private UrlClient urlClient;
@Autowired
private BeanUrlClient beanClient;
@BeforeClass @BeforeClass
public static void beforeClass() { public static void beforeClass() {
port = SocketUtils.findAvailableTcpPort(); port = SocketUtils.findAvailableTcpPort();
@@ -72,37 +78,62 @@ public class FeignHttpClientUrlTests {
System.clearProperty("server.port"); System.clearProperty("server.port");
} }
@Autowired @Test
private UrlClient urlClient; public void testUrlHttpClient() {
assertThat(this.urlClient).as("UrlClient was null").isNotNull();
Hello hello = this.urlClient.getHello();
assertThat(hello).as("hello was null").isNotNull();
assertThat(hello).as("first hello didn't match")
.isEqualTo(new Hello("hello world 1"));
}
@Autowired @Test
private BeanUrlClient beanClient; public void testBeanUrl() {
Hello hello = this.beanClient.getHello();
assertThat(hello).as("hello was null").isNotNull();
assertThat(hello).as("first hello didn't match")
.isEqualTo(new Hello("hello world 1"));
}
@Autowired BeanUrlClientNoProtocol beanClientNoProtocol; @Test
public void testBeanUrlNoProtocol() {
Hello hello = this.beanClientNoProtocol.getHello();
assertThat(hello).as("hello was null").isNotNull();
assertThat(hello).as("first hello didn't match")
.isEqualTo(new Hello("hello world 1"));
}
// this tests that // this tests that
@FeignClient(name = "localappurl", url = "http://localhost:${server.port}/") @FeignClient(name = "localappurl", url = "http://localhost:${server.port}/")
protected interface UrlClient { protected interface UrlClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello") @RequestMapping(method = RequestMethod.GET, value = "/hello")
Hello getHello(); Hello getHello();
} }
@FeignClient(name = "beanappurl", url = "#{SERVER_URL}path") @FeignClient(name = "beanappurl", url = "#{SERVER_URL}path")
protected interface BeanUrlClient { protected interface BeanUrlClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello") @RequestMapping(method = RequestMethod.GET, value = "/hello")
Hello getHello(); Hello getHello();
} }
@FeignClient(name = "beanappurlnoprotocol", url = "#{SERVER_URL_NO_PROTOCOL}path") @FeignClient(name = "beanappurlnoprotocol", url = "#{SERVER_URL_NO_PROTOCOL}path")
protected interface BeanUrlClientNoProtocol { protected interface BeanUrlClientNoProtocol {
@RequestMapping(method = RequestMethod.GET, value = "/hello") @RequestMapping(method = RequestMethod.GET, value = "/hello")
Hello getHello(); Hello getHello();
} }
@Configuration @Configuration
@EnableAutoConfiguration @EnableAutoConfiguration
@RestController @RestController
@EnableFeignClients(clients = { UrlClient.class, BeanUrlClient.class, BeanUrlClientNoProtocol.class }) @EnableFeignClients(clients = { UrlClient.class, BeanUrlClient.class,
BeanUrlClientNoProtocol.class })
@Import(NoSecurityConfiguration.class)
protected static class TestConfig { protected static class TestConfig {
@RequestMapping(method = RequestMethod.GET, value = "/hello") @RequestMapping(method = RequestMethod.GET, value = "/hello")
@@ -115,12 +146,12 @@ public class FeignHttpClientUrlTests {
return getHello(); return getHello();
} }
@Bean(name="SERVER_URL") @Bean(name = "SERVER_URL")
public String serverUrl() { public String serverUrl() {
return "http://localhost:" + port + "/"; return "http://localhost:" + port + "/";
} }
@Bean(name="SERVER_URL_NO_PROTOCOL") @Bean(name = "SERVER_URL_NO_PROTOCOL")
public String serverUrlNoProtocol() { public String serverUrlNoProtocol() {
return "localhost:" + port + "/"; return "localhost:" + port + "/";
} }
@@ -136,8 +167,8 @@ public class FeignHttpClientUrlTests {
ReflectionUtils.makeAccessible(field); ReflectionUtils.makeAccessible(field);
Client client = (Client) ReflectionUtils.getField(field, feign); Client client = (Client) ReflectionUtils.getField(field, feign);
if (target.name().equals("localappurl")) { if (target.name().equals("localappurl")) {
assertThat("client was wrong type", client, assertThat(client).isInstanceOf(ApacheHttpClient.class)
is(instanceOf(ApacheHttpClient.class))); .as("client was wrong type");
} }
return feign.target(target); return feign.target(target);
} }
@@ -146,29 +177,8 @@ public class FeignHttpClientUrlTests {
} }
@Test
public void testUrlHttpClient() {
assertNotNull("UrlClient was null", this.urlClient);
Hello hello = this.urlClient.getHello();
assertNotNull("hello was null", hello);
assertEquals("first hello didn't match", new Hello("hello world 1"), hello);
}
@Test
public void testBeanUrl() {
Hello hello = this.beanClient.getHello();
assertNotNull("hello was null", hello);
assertEquals("first hello didn't match", new Hello("hello world 1"), hello);
}
@Test
public void testBeanUrlNoProtocol() {
Hello hello = this.beanClientNoProtocol.getHello();
assertNotNull("hello was null", hello);
assertEquals("first hello didn't match", new Hello("hello world 1"), hello);
}
public static class Hello { public static class Hello {
private String message; private String message;
public Hello() { public Hello() {
@@ -179,7 +189,7 @@ public class FeignHttpClientUrlTests {
} }
public String getMessage() { public String getMessage() {
return message; return this.message;
} }
public void setMessage(String message) { public void setMessage(String message) {
@@ -188,15 +198,21 @@ public class FeignHttpClientUrlTests {
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) {
if (o == null || getClass() != o.getClass()) return false; return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Hello that = (Hello) o; Hello that = (Hello) o;
return Objects.equals(message, that.message); return Objects.equals(this.message, that.message);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return Objects.hash(message); return Objects.hash(this.message);
} }
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2016 the original author or authors. * Copyright 2016-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -16,9 +16,8 @@
package org.springframework.cloud.openfeign; package org.springframework.cloud.openfeign;
import static org.junit.Assert.assertNotNull; import feign.Logger;
import static org.junit.Assert.assertTrue; import feign.slf4j.Slf4jLogger;
import org.junit.Test; import org.junit.Test;
import org.springframework.context.annotation.AnnotationConfigApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext;
@@ -26,8 +25,7 @@ import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import; import org.springframework.context.annotation.Import;
import feign.Logger; import static org.assertj.core.api.Assertions.assertThat;
import feign.slf4j.Slf4jLogger;
/** /**
* @author Venil Noronha * @author Venil Noronha
@@ -36,12 +34,38 @@ public class FeignLoggerFactoryTests {
@Test @Test
public void testDefaultLogger() { public void testDefaultLogger() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SampleConfiguration1.class); AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
SampleConfiguration1.class);
FeignLoggerFactory loggerFactory = context.getBean(FeignLoggerFactory.class); FeignLoggerFactory loggerFactory = context.getBean(FeignLoggerFactory.class);
assertNotNull(loggerFactory); assertThat(loggerFactory).isNotNull();
Logger logger = loggerFactory.create(Object.class); Logger logger = loggerFactory.create(Object.class);
assertNotNull(logger); assertThat(logger).isNotNull();
assertTrue(logger instanceof Slf4jLogger); assertThat(logger instanceof Slf4jLogger).isTrue();
context.close();
}
@Test
public void testCustomLogger() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
SampleConfiguration2.class);
FeignLoggerFactory loggerFactory = context.getBean(FeignLoggerFactory.class);
assertThat(loggerFactory).isNotNull();
Logger logger = loggerFactory.create(Object.class);
assertThat(logger).isNotNull();
assertThat(logger instanceof LoggerImpl1).isTrue();
context.close();
}
@Test
public void testCustomLoggerFactory() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(
SampleConfiguration3.class);
FeignLoggerFactory loggerFactory = context.getBean(FeignLoggerFactory.class);
assertThat(loggerFactory).isNotNull();
assertThat(loggerFactory instanceof LoggerFactoryImpl).isTrue();
Logger logger = loggerFactory.create(Object.class);
assertThat(logger).isNotNull();
assertThat(logger instanceof LoggerImpl2).isTrue();
context.close(); context.close();
} }
@@ -51,17 +75,6 @@ public class FeignLoggerFactoryTests {
} }
@Test
public void testCustomLogger() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SampleConfiguration2.class);
FeignLoggerFactory loggerFactory = context.getBean(FeignLoggerFactory.class);
assertNotNull(loggerFactory);
Logger logger = loggerFactory.create(Object.class);
assertNotNull(logger);
assertTrue(logger instanceof LoggerImpl1);
context.close();
}
@Configuration @Configuration
@Import(FeignClientsConfiguration.class) @Import(FeignClientsConfiguration.class)
protected static class SampleConfiguration2 { protected static class SampleConfiguration2 {
@@ -82,18 +95,6 @@ public class FeignLoggerFactoryTests {
} }
@Test
public void testCustomLoggerFactory() {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(SampleConfiguration3.class);
FeignLoggerFactory loggerFactory = context.getBean(FeignLoggerFactory.class);
assertNotNull(loggerFactory);
assertTrue(loggerFactory instanceof LoggerFactoryImpl);
Logger logger = loggerFactory.create(Object.class);
assertNotNull(logger);
assertTrue(logger instanceof LoggerImpl2);
context.close();
}
@Configuration @Configuration
@Import(FeignClientsConfiguration.class) @Import(FeignClientsConfiguration.class)
protected static class SampleConfiguration3 { protected static class SampleConfiguration3 {
@@ -1,30 +1,32 @@
/* /*
* Copyright 2013-2018 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and * See the License for the specific language governing permissions and
* limitations under the License. * limitations under the License.
*
*/ */
package org.springframework.cloud.openfeign; package org.springframework.cloud.openfeign;
import okhttp3.OkHttpClient;
import java.lang.reflect.Field; import java.lang.reflect.Field;
import javax.net.ssl.HostnameVerifier; import javax.net.ssl.HostnameVerifier;
import okhttp3.OkHttpClient;
import org.junit.After; import org.junit.After;
import org.junit.Assert;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.commons.httpclient.HttpClientConfiguration; import org.springframework.cloud.commons.httpclient.HttpClientConfiguration;
import org.springframework.cloud.commons.httpclient.OkHttpClientFactory; import org.springframework.cloud.commons.httpclient.OkHttpClientFactory;
@@ -33,6 +35,8 @@ import org.springframework.cloud.test.ModifiedClassPathRunner;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.util.ReflectionUtils; import org.springframework.util.ReflectionUtils;
import static org.assertj.core.api.Assertions.assertThat;
/** /**
* @author Ryan Baxter * @author Ryan Baxter
*/ */
@@ -44,23 +48,29 @@ public class FeignOkHttpConfigurationTests {
@Before @Before
public void setUp() { public void setUp() {
context = new SpringApplicationBuilder().properties("debug=true","feign.httpclient.disableSslValidation=true", this.context = new SpringApplicationBuilder()
"feign.okhttp.enabled=true", "feign.httpclient.enabled=false").web(false) .properties("debug=true", "feign.httpclient.disableSslValidation=true",
.sources(HttpClientConfiguration.class, FeignAutoConfiguration.class).run(); "feign.okhttp.enabled=true", "feign.httpclient.enabled=false")
.web(WebApplicationType.NONE)
.sources(HttpClientConfiguration.class, FeignAutoConfiguration.class)
.run();
} }
@After @After
public void tearDown() { public void tearDown() {
if(context != null) { if (this.context != null) {
context.close(); this.context.close();
} }
} }
@Test @Test
public void disableSslTest() throws Exception { public void disableSslTest() throws Exception {
OkHttpClient httpClient = context.getBean(OkHttpClient.class); OkHttpClient httpClient = this.context.getBean(OkHttpClient.class);
HostnameVerifier hostnameVerifier = (HostnameVerifier)this.getField(httpClient, "hostnameVerifier"); HostnameVerifier hostnameVerifier = (HostnameVerifier) this.getField(httpClient,
Assert.assertTrue(OkHttpClientFactory.TrustAllHostnames.class.isInstance(hostnameVerifier)); "hostnameVerifier");
assertThat(
OkHttpClientFactory.TrustAllHostnames.class.isInstance(hostnameVerifier))
.isTrue();
} }
protected <T> Object getField(Object target, String name) { protected <T> Object getField(Object target, String name) {
@@ -69,4 +79,5 @@ public class FeignOkHttpConfigurationTests {
Object value = ReflectionUtils.getField(field, target); Object value = ReflectionUtils.getField(field, target);
return value; return value;
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2015 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -23,12 +23,15 @@ import java.util.Objects;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext;
@@ -38,10 +41,7 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import static org.junit.Assert.assertEquals; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertNotNull;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
/** /**
* @author Spencer Gibb * @author Spencer Gibb
@@ -60,6 +60,7 @@ public class SpringDecoderTests extends FeignClientFactoryBean {
public SpringDecoderTests() { public SpringDecoderTests() {
setName("test"); setName("test");
setContextId("test");
} }
public TestClient testClient() { public TestClient testClient() {
@@ -69,66 +70,73 @@ public class SpringDecoderTests extends FeignClientFactoryBean {
public TestClient testClient(boolean decode404) { public TestClient testClient(boolean decode404) {
setType(this.getClass()); setType(this.getClass());
setDecode404(decode404); setDecode404(decode404);
return feign(context).target(TestClient.class, "http://localhost:" + this.port); return feign(this.context).target(TestClient.class,
"http://localhost:" + this.port);
} }
@Test @Test
public void testResponseEntity() { public void testResponseEntity() {
ResponseEntity<Hello> response = testClient().getHelloResponse(); ResponseEntity<Hello> response = testClient().getHelloResponse();
assertNotNull("response was null", response); assertThat(response).as("response was null").isNotNull();
assertEquals("wrong status code", HttpStatus.OK, response.getStatusCode()); assertThat(response.getStatusCode()).as("wrong status code")
.isEqualTo(HttpStatus.OK);
Hello hello = response.getBody(); Hello hello = response.getBody();
assertNotNull("hello was null", hello); assertThat(hello).as("hello was null").isNotNull();
assertEquals("first hello didn't match", new Hello("hello world via response"), assertThat(hello).as("first hello didn't match")
hello); .isEqualTo(new Hello("hello world via response"));
} }
@Test @Test
public void testSimpleType() { public void testSimpleType() {
Hello hello = testClient().getHello(); Hello hello = testClient().getHello();
assertNotNull("hello was null", hello); assertThat(hello).as("hello was null").isNotNull();
assertEquals("first hello didn't match", new Hello("hello world 1"), hello); assertThat(hello).as("first hello didn't match")
.isEqualTo(new Hello("hello world 1"));
} }
@Test @Test
public void testUserParameterizedTypeDecode() { public void testUserParameterizedTypeDecode() {
List<Hello> hellos = testClient().getHellos(); List<Hello> hellos = testClient().getHellos();
assertNotNull("hellos was null", hellos); assertThat(hellos).as("hellos was null").isNotNull();
assertEquals("hellos was not the right size", 2, hellos.size()); assertThat(hellos.size()).as("hellos was not the right size").isEqualTo(2);
assertEquals("first hello didn't match", new Hello("hello world 1"), assertThat(hellos.get(0)).as("first hello didn't match")
hellos.get(0)); .isEqualTo(new Hello("hello world 1"));
} }
@Test @Test
public void testSimpleParameterizedTypeDecode() { public void testSimpleParameterizedTypeDecode() {
List<String> hellos = testClient().getHelloStrings(); List<String> hellos = testClient().getHelloStrings();
assertNotNull("hellos was null", hellos); assertThat(hellos).as("hellos was null").isNotNull();
assertEquals("hellos was not the right size", 2, hellos.size()); assertThat(hellos.size()).as("hellos was not the right size").isEqualTo(2);
assertEquals("first hello didn't match", "hello world 1", hellos.get(0)); assertThat(hellos.get(0)).as("first hello didn't match")
.isEqualTo("hello world 1");
} }
@Test @Test
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
public void testWildcardTypeDecode() { public void testWildcardTypeDecode() {
ResponseEntity<?> wildcard = testClient().getWildcard(); ResponseEntity<?> wildcard = testClient().getWildcard();
assertNotNull("wildcard was null", wildcard); assertThat(wildcard).as("wildcard was null").isNotNull();
assertEquals("wrong status code", HttpStatus.OK, wildcard.getStatusCode()); assertThat(wildcard.getStatusCode()).as("wrong status code")
.isEqualTo(HttpStatus.OK);
Object wildcardBody = wildcard.getBody(); Object wildcardBody = wildcard.getBody();
assertNotNull("wildcardBody was null", wildcardBody); assertThat(wildcardBody).as("wildcardBody was null").isNotNull();
assertTrue("wildcard not an instance of Map", wildcardBody instanceof Map); assertThat(wildcardBody instanceof Map).as("wildcard not an instance of Map")
.isTrue();
Map<String, String> hello = (Map<String, String>) wildcardBody; Map<String, String> hello = (Map<String, String>) wildcardBody;
assertEquals("first hello didn't match", "wildcard", hello.get("message")); assertThat(hello.get("message")).as("first hello didn't match")
.isEqualTo("wildcard");
} }
@Test @Test
public void testResponseEntityVoid() { public void testResponseEntityVoid() {
ResponseEntity<Void> response = testClient().getHelloVoid(); ResponseEntity<Void> response = testClient().getHelloVoid();
assertNotNull("response was null", response); assertThat(response).as("response was null").isNotNull();
List<String> headerVals = response.getHeaders().get("X-test-header"); List<String> headerVals = response.getHeaders().get("X-test-header");
assertNotNull("headerVals was null", headerVals); assertThat(headerVals).as("headerVals was null").isNotNull();
assertEquals("headerVals size was wrong", 1, headerVals.size()); assertThat(headerVals.size()).as("headerVals size was wrong").isEqualTo(1);
String header = headerVals.get(0); String header = headerVals.get(0);
assertEquals("header was wrong", "myval", header); assertThat(header).as("header was wrong").isEqualTo("myval");
} }
@Test(expected = RuntimeException.class) @Test(expected = RuntimeException.class)
@@ -139,43 +147,12 @@ public class SpringDecoderTests extends FeignClientFactoryBean {
@Test @Test
public void testDecodes404() { public void testDecodes404() {
final ResponseEntity<String> response = testClient(true).getNotFound(); final ResponseEntity<String> response = testClient(true).getNotFound();
assertNotNull("response was null", response); assertThat(response).as("response was null").isNotNull();
assertNull("response body was not null", response.getBody()); assertThat(response.getBody()).as("response body was not null").isNull();
}
public static class Hello {
private String message;
public Hello() {
}
public Hello(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
public void setMessage(String message) {
this.message = message;
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Hello that = (Hello) o;
return Objects.equals(message, that.message);
}
@Override
public int hashCode() {
return Objects.hash(message);
}
} }
protected interface TestClient { protected interface TestClient {
@RequestMapping(method = RequestMethod.GET, value = "/helloresponse") @RequestMapping(method = RequestMethod.GET, value = "/helloresponse")
ResponseEntity<Hello> getHelloResponse(); ResponseEntity<Hello> getHelloResponse();
@@ -196,11 +173,51 @@ public class SpringDecoderTests extends FeignClientFactoryBean {
@GetMapping("/helloWildcard") @GetMapping("/helloWildcard")
ResponseEntity<?> getWildcard(); ResponseEntity<?> getWildcard();
}
public static class Hello {
private String message;
public Hello() {
}
public Hello(String message) {
this.message = message;
}
public String getMessage() {
return this.message;
}
public void setMessage(String message) {
this.message = message;
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Hello that = (Hello) o;
return Objects.equals(this.message, that.message);
}
@Override
public int hashCode() {
return Objects.hash(this.message);
}
} }
@Configuration @Configuration
@EnableAutoConfiguration @EnableAutoConfiguration
@RestController @RestController
@Import(NoSecurityConfiguration.class)
protected static class Application implements TestClient { protected static class Application implements TestClient {
@Override @Override
@@ -243,7 +260,7 @@ public class SpringDecoderTests extends FeignClientFactoryBean {
public ResponseEntity<?> getWildcard() { public ResponseEntity<?> getWildcard() {
return ResponseEntity.ok(new Hello("wildcard")); return ResponseEntity.ok(new Hello("wildcard"));
} }
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2017 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -14,61 +14,64 @@
* limitations under the License. * limitations under the License.
*/ */
package org.springframework.cloud.openfeign; package org.springframework.cloud.openfeign;
import java.util.Map; import java.util.Map;
import org.junit.After; import org.junit.After;
import org.junit.Before; import org.junit.Before;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.builder.SpringApplicationBuilder; import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration; import org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration;
import org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration;
import org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration;
import org.springframework.cloud.openfeign.ribbon.CachingSpringLoadBalancerFactory; import org.springframework.cloud.openfeign.ribbon.CachingSpringLoadBalancerFactory;
import org.springframework.cloud.openfeign.ribbon.FeignLoadBalancer; import org.springframework.cloud.openfeign.ribbon.FeignLoadBalancer;
import org.springframework.cloud.openfeign.ribbon.FeignRibbonClientAutoConfiguration; import org.springframework.cloud.openfeign.ribbon.FeignRibbonClientAutoConfiguration;
import org.springframework.cloud.openfeign.ribbon.RetryableFeignLoadBalancer; import org.springframework.cloud.openfeign.ribbon.RetryableFeignLoadBalancer;
import org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration;
import org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration;
import org.springframework.cloud.test.ClassPathExclusions; import org.springframework.cloud.test.ClassPathExclusions;
import org.springframework.cloud.test.ModifiedClassPathRunner; import org.springframework.cloud.test.ModifiedClassPathRunner;
import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.ConfigurableApplicationContext;
import static org.hamcrest.MatcherAssert.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.hasSize;
import static org.hamcrest.Matchers.instanceOf;
import static org.hamcrest.Matchers.not;
import static org.hamcrest.core.Is.is;
/** /**
* @author Ryan Baxter * @author Ryan Baxter
*/ */
@RunWith(ModifiedClassPathRunner.class) @RunWith(ModifiedClassPathRunner.class)
@ClassPathExclusions({"spring-retry-*.jar", "spring-boot-starter-aop-*.jar"}) @ClassPathExclusions({ "spring-retry-*.jar", "spring-boot-starter-aop-*.jar" })
public class SpringRetryDisabledTests { public class SpringRetryDisabledTests {
private ConfigurableApplicationContext context; private ConfigurableApplicationContext context;
@Before @Before
public void setUp() { public void setUp() {
context = new SpringApplicationBuilder().web(false) this.context = new SpringApplicationBuilder().web(WebApplicationType.NONE)
.sources(RibbonAutoConfiguration.class, LoadBalancerAutoConfiguration.class, RibbonClientConfiguration.class, .sources(RibbonAutoConfiguration.class,
FeignRibbonClientAutoConfiguration.class).run(); LoadBalancerAutoConfiguration.class,
RibbonClientConfiguration.class,
FeignRibbonClientAutoConfiguration.class)
.run();
} }
@After @After
public void tearDown() { public void tearDown() {
if(context != null) { if (this.context != null) {
context.close(); this.context.close();
} }
} }
@Test @Test
public void testLoadBalancedRetryFactoryBean() throws Exception { public void testLoadBalancedRetryFactoryBean() throws Exception {
Map<String, CachingSpringLoadBalancerFactory> lbFactorys = context.getBeansOfType(CachingSpringLoadBalancerFactory.class); Map<String, CachingSpringLoadBalancerFactory> lbFactorys = this.context
assertThat(lbFactorys.values(), hasSize(1)); .getBeansOfType(CachingSpringLoadBalancerFactory.class);
FeignLoadBalancer lb =lbFactorys.values().iterator().next().create("foo"); assertThat(lbFactorys.values()).hasSize(1);
assertThat(lb, instanceOf(FeignLoadBalancer.class)); FeignLoadBalancer lb = lbFactorys.values().iterator().next().create("foo");
assertThat(lb, is(not(instanceOf(RetryableFeignLoadBalancer.class)))); assertThat(lb).isInstanceOf(FeignLoadBalancer.class);
assertThat(lb).isNotInstanceOf(RetryableFeignLoadBalancer.class);
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2017 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -17,46 +17,49 @@
package org.springframework.cloud.openfeign; package org.springframework.cloud.openfeign;
import java.util.Map; import java.util.Map;
import org.hamcrest.Matchers;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration; import org.springframework.cloud.client.loadbalancer.LoadBalancerAutoConfiguration;
import org.springframework.cloud.commons.httpclient.HttpClientConfiguration; import org.springframework.cloud.commons.httpclient.HttpClientConfiguration;
import org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration;
import org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration;
import org.springframework.cloud.openfeign.ribbon.CachingSpringLoadBalancerFactory; import org.springframework.cloud.openfeign.ribbon.CachingSpringLoadBalancerFactory;
import org.springframework.cloud.openfeign.ribbon.FeignLoadBalancer; import org.springframework.cloud.openfeign.ribbon.FeignLoadBalancer;
import org.springframework.cloud.openfeign.ribbon.FeignRibbonClientAutoConfiguration; import org.springframework.cloud.openfeign.ribbon.FeignRibbonClientAutoConfiguration;
import org.springframework.cloud.openfeign.ribbon.RetryableFeignLoadBalancer; import org.springframework.cloud.openfeign.ribbon.RetryableFeignLoadBalancer;
import org.springframework.cloud.netflix.ribbon.RibbonAutoConfiguration;
import org.springframework.cloud.netflix.ribbon.RibbonClientConfiguration;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware; import org.springframework.context.ApplicationContextAware;
import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import static org.hamcrest.MatcherAssert.assertThat; import static org.assertj.core.api.Assertions.assertThat;
import static org.hamcrest.Matchers.instanceOf;
/** /**
* @author Ryan Baxter * @author Ryan Baxter
*/ */
@RunWith(SpringJUnit4ClassRunner.class) @RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {RibbonAutoConfiguration.class, RibbonClientConfiguration.class, LoadBalancerAutoConfiguration.class, @ContextConfiguration(classes = { RibbonAutoConfiguration.class,
FeignRibbonClientAutoConfiguration.class, HttpClientConfiguration.class}) RibbonClientConfiguration.class, LoadBalancerAutoConfiguration.class,
FeignRibbonClientAutoConfiguration.class, HttpClientConfiguration.class })
public class SpringRetryEnabledTests implements ApplicationContextAware { public class SpringRetryEnabledTests implements ApplicationContextAware {
private ApplicationContext context; private ApplicationContext context;
@Test @Test
public void testLoadBalancedRetryFactoryBean() throws Exception { public void testLoadBalancedRetryFactoryBean() throws Exception {
Map<String, CachingSpringLoadBalancerFactory> lbFactorys = context.getBeansOfType(CachingSpringLoadBalancerFactory.class); Map<String, CachingSpringLoadBalancerFactory> lbFactorys = this.context
assertThat(lbFactorys.values(), Matchers.hasSize(1)); .getBeansOfType(CachingSpringLoadBalancerFactory.class);
FeignLoadBalancer lb =lbFactorys.values().iterator().next().create("foo"); assertThat(lbFactorys.values()).hasSize(1);
assertThat(lb, instanceOf(RetryableFeignLoadBalancer.class)); FeignLoadBalancer lb = lbFactorys.values().iterator().next().create("foo");
assertThat(lb).isInstanceOf(RetryableFeignLoadBalancer.class);
} }
@Override @Override
public void setApplicationContext(ApplicationContext context) throws BeansException { public void setApplicationContext(ApplicationContext context) throws BeansException {
this.context = context; this.context = context;
} }
}
}
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2015 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -23,6 +23,7 @@ import java.util.Objects;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
@@ -31,16 +32,18 @@ import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignClient; import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.FeignClientBuilder;
import org.springframework.context.ApplicationContext; import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import static org.junit.Assert.assertNotNull; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertTrue;
/** /**
* @author Dave Syer * @author Dave Syer
@@ -66,19 +69,69 @@ public class FeignClientTests {
@Autowired @Autowired
private org.springframework.cloud.openfeign.beans.extra.TestClient extraClient; private org.springframework.cloud.openfeign.beans.extra.TestClient extraClient;
@Qualifier("build-by-builder")
@Autowired
private TestClient buildByBuilder;
@Test
public void testAnnotations() {
Map<String, Object> beans = this.context
.getBeansWithAnnotation(FeignClient.class);
assertThat(beans.containsKey(TestClient.class.getName()))
.as("Wrong clients: " + beans).isTrue();
}
@Test
public void testClient() {
assertThat(this.testClient).as("testClient was null").isNotNull();
assertThat(this.extraClient).as("extraClient was null").isNotNull();
assertThat(Proxy.isProxyClass(this.testClient.getClass()))
.as("testClient is not a java Proxy").isTrue();
InvocationHandler invocationHandler = Proxy.getInvocationHandler(this.testClient);
assertThat(invocationHandler).as("invocationHandler was null").isNotNull();
}
@Test
public void extraClient() {
assertThat(this.extraClient).as("extraClient was null").isNotNull();
assertThat(Proxy.isProxyClass(this.extraClient.getClass()))
.as("extraClient is not a java Proxy").isTrue();
InvocationHandler invocationHandler = Proxy
.getInvocationHandler(this.extraClient);
assertThat(invocationHandler).as("invocationHandler was null").isNotNull();
}
@Test
public void buildByBuilder() {
assertThat(this.buildByBuilder).as("buildByBuilder was null").isNotNull();
assertThat(Proxy.isProxyClass(this.buildByBuilder.getClass()))
.as("buildByBuilder is not a java Proxy").isTrue();
InvocationHandler invocationHandler = Proxy
.getInvocationHandler(this.buildByBuilder);
assertThat(invocationHandler).as("invocationHandler was null").isNotNull();
}
@Configuration @Configuration
@EnableAutoConfiguration @EnableAutoConfiguration
@RestController @RestController
@EnableFeignClients @EnableFeignClients
@Import(FeignClientBuilder.class)
protected static class Application { protected static class Application {
@Bean("build-by-builder")
public TestClient buildByBuilder(final FeignClientBuilder feignClientBuilder) {
return feignClientBuilder.forType(TestClient.class, "builderapp").build();
}
@RequestMapping(method = RequestMethod.GET, value = "/hello") @RequestMapping(method = RequestMethod.GET, value = "/hello")
public Hello getHello() { public Hello getHello() {
return new Hello("hello world 1"); return new Hello("hello world 1");
} }
} }
public static class Hello { public static class Hello {
private String message; private String message;
public Hello() { public Hello() {
@@ -89,7 +142,7 @@ public class FeignClientTests {
} }
public String getMessage() { public String getMessage() {
return message; return this.message;
} }
public void setMessage(String message) { public void setMessage(String message) {
@@ -98,38 +151,27 @@ public class FeignClientTests {
@Override @Override
public boolean equals(Object o) { public boolean equals(Object o) {
if (this == o) return true; if (this == o) {
if (o == null || getClass() != o.getClass()) return false; return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
Hello that = (Hello) o; Hello that = (Hello) o;
return Objects.equals(message, that.message); return Objects.equals(this.message, that.message);
} }
@Override @Override
public int hashCode() { public int hashCode() {
return message != null ? message.hashCode() : 0; return this.message != null ? this.message.hashCode() : 0;
} }
}
@Test
public void testAnnnotations() throws Exception {
Map<String, Object> beans = this.context
.getBeansWithAnnotation(FeignClient.class);
assertTrue("Wrong clients: " + beans,
beans.containsKey(TestClient.class.getName()));
}
@Test
public void testClient() {
assertNotNull("testClient was null", this.testClient);
assertNotNull("testClient was null", this.extraClient);
assertTrue("testClient is not a java Proxy",
Proxy.isProxyClass(this.testClient.getClass()));
InvocationHandler invocationHandler = Proxy.getInvocationHandler(this.testClient);
assertNotNull("invocationHandler was null", invocationHandler);
} }
@Configuration @Configuration
public static class TestDefaultFeignConfig { public static class TestDefaultFeignConfig {
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2015 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -18,11 +18,15 @@ package org.springframework.cloud.openfeign.beans;
import org.springframework.cloud.openfeign.FeignClient; import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.beans.FeignClientTests.Hello; import org.springframework.cloud.openfeign.beans.FeignClientTests.Hello;
import org.springframework.context.annotation.Primary;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
@FeignClient(value = "localapp") @Primary
@FeignClient("localapp")
public interface TestClient { public interface TestClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello") @RequestMapping(method = RequestMethod.GET, value = "/hello")
Hello getHello(); Hello getHello();
}
}
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2015 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -23,6 +23,8 @@ import org.springframework.web.bind.annotation.RequestMethod;
@FeignClient(value = "otherapp", qualifier = "uniquequalifier") @FeignClient(value = "otherapp", qualifier = "uniquequalifier")
public interface TestClient { public interface TestClient {
@RequestMapping(method = RequestMethod.GET, value = "/hello") @RequestMapping(method = RequestMethod.GET, value = "/hello")
Hello getHello(); Hello getHello();
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2015 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -22,7 +22,6 @@ import java.util.List;
import com.netflix.loadbalancer.BaseLoadBalancer; import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.ILoadBalancer; import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server; import com.netflix.loadbalancer.Server;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
@@ -30,27 +29,28 @@ import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.encoding.app.client.InvoiceClient; import org.springframework.cloud.openfeign.encoding.app.client.InvoiceClient;
import org.springframework.cloud.openfeign.encoding.app.domain.Invoice; import org.springframework.cloud.openfeign.encoding.app.domain.Invoice;
import org.springframework.cloud.netflix.ribbon.RibbonClient; import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.junit4.SpringRunner;
import static org.junit.Assert.assertEquals; import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.Assert.assertNotNull; import static org.springframework.boot.test.context.SpringBootTest.WebEnvironment.RANDOM_PORT;
/** /**
* Tests the response compression. * Tests the response compression.
* *
* @author Jakub Narloch * @author Jakub Narloch
*/ */
@SpringBootTest(classes = FeignAcceptEncodingTests.Application.class, webEnvironment = WebEnvironment.RANDOM_PORT, value = { @SpringBootTest(classes = FeignAcceptEncodingTests.Application.class, webEnvironment = RANDOM_PORT, value = {
"feign.compression.response.enabled=true" }) "feign.compression.response.enabled=true" })
@RunWith(SpringRunner.class) @RunWith(SpringRunner.class)
@DirtiesContext @DirtiesContext
@@ -66,17 +66,19 @@ public class FeignAcceptEncodingTests {
final ResponseEntity<List<Invoice>> invoices = this.invoiceClient.getInvoices(); final ResponseEntity<List<Invoice>> invoices = this.invoiceClient.getInvoices();
// then // then
assertNotNull(invoices); assertThat(invoices).isNotNull();
assertEquals(HttpStatus.OK, invoices.getStatusCode()); assertThat(invoices.getStatusCode()).isEqualTo(HttpStatus.OK);
assertNotNull(invoices.getBody()); assertThat(invoices.getBody()).isNotNull();
assertEquals(100, invoices.getBody().size()); assertThat(invoices.getBody().size()).isEqualTo(100);
} }
@EnableFeignClients(clients = InvoiceClient.class) @EnableFeignClients(clients = InvoiceClient.class)
@RibbonClient(name = "local", configuration = LocalRibbonClientConfiguration.class) @RibbonClient(name = "local", configuration = LocalRibbonClientConfiguration.class)
@SpringBootApplication(scanBasePackages = "org.springframework.cloud.openfeign.encoding.app") @SpringBootApplication(scanBasePackages = "org.springframework.cloud.openfeign.encoding.app")
@Import(NoSecurityConfiguration.class)
public static class Application { public static class Application {
} }
@Configuration @Configuration
@@ -92,5 +94,7 @@ public class FeignAcceptEncodingTests {
Collections.singletonList(new Server("localhost", this.port))); Collections.singletonList(new Server("localhost", this.port)));
return balancer; return balancer;
} }
} }
}
}
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2015 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -16,39 +16,40 @@
package org.springframework.cloud.openfeign.encoding; package org.springframework.cloud.openfeign.encoding;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNotNull;
import java.util.Collections; import java.util.Collections;
import java.util.List; import java.util.List;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.encoding.app.client.InvoiceClient;
import org.springframework.cloud.openfeign.encoding.app.domain.Invoice;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import com.netflix.loadbalancer.BaseLoadBalancer; import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.ILoadBalancer; import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server; import com.netflix.loadbalancer.Server;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.encoding.app.client.InvoiceClient;
import org.springframework.cloud.openfeign.encoding.app.domain.Invoice;
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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 response compression. * Tests the response compression.
* *
* @author Jakub Narloch * @author Jakub Narloch
*/ */
@SpringBootTest(classes = FeignContentEncodingTests.Application.class, webEnvironment = WebEnvironment.RANDOM_PORT, value = { @SpringBootTest(classes = FeignContentEncodingTests.Application.class, webEnvironment = RANDOM_PORT, value = {
"feign.compression.request.enabled=true", "feign.compression.request.enabled=true",
"hystrix.command.default.execution.isolation.strategy=SEMAPHORE", "hystrix.command.default.execution.isolation.strategy=SEMAPHORE",
"ribbon.OkToRetryOnAllOperations=false" }) "ribbon.OkToRetryOnAllOperations=false" })
@@ -69,17 +70,19 @@ public class FeignContentEncodingTests {
.saveInvoices(invoices); .saveInvoices(invoices);
// then // then
assertNotNull(response); assertThat(response).isNotNull();
assertEquals(HttpStatus.OK, response.getStatusCode()); assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertNotNull(response.getBody()); assertThat(response.getBody()).isNotNull();
assertEquals(invoices.size(), response.getBody().size()); assertThat(response.getBody().size()).isEqualTo(invoices.size());
} }
@EnableFeignClients(clients = InvoiceClient.class) @EnableFeignClients(clients = InvoiceClient.class)
@RibbonClient(name = "local", configuration = LocalRibbonClientConfiguration.class) @RibbonClient(name = "local", configuration = LocalRibbonClientConfiguration.class)
@SpringBootApplication(scanBasePackages = "org.springframework.cloud.openfeign.encoding.app") @SpringBootApplication(scanBasePackages = "org.springframework.cloud.openfeign.encoding.app")
@Import(NoSecurityConfiguration.class)
public static class Application { public static class Application {
} }
@Configuration @Configuration
@@ -95,5 +98,7 @@ public class FeignContentEncodingTests {
Collections.singletonList(new Server("localhost", this.port))); Collections.singletonList(new Server("localhost", this.port)));
return balancer; return balancer;
} }
} }
}
}
@@ -0,0 +1,110 @@
/*
* Copyright 2013-2015 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.cloud.openfeign.encoding;
import java.util.Collections;
import com.netflix.loadbalancer.BaseLoadBalancer;
import com.netflix.loadbalancer.ILoadBalancer;
import com.netflix.loadbalancer.Server;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.FeignClientsConfiguration;
import org.springframework.cloud.openfeign.encoding.app.client.InvoiceClient;
import org.springframework.cloud.openfeign.encoding.app.domain.Invoice;
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.data.web.config.EnableSpringDataWebSupport;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
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.
*
* @author Charlie Mordant.
*/
@SpringBootTest(classes = FeignPageableEncodingTests.Application.class, webEnvironment = RANDOM_PORT, value = {
"feign.compression.request.enabled=true",
"hystrix.command.default.execution.isolation.strategy=SEMAPHORE",
"ribbon.OkToRetryOnAllOperations=false" })
@RunWith(SpringJUnit4ClassRunner.class)
public class FeignPageableEncodingTests {
@Autowired
private InvoiceClient invoiceClient;
@Test
public void testPageable() {
// given
Pageable pageable = PageRequest.of(0, 10, Sort.Direction.ASC, "sortProperty");
// when
final ResponseEntity<Page<Invoice>> response = this.invoiceClient
.getInvoicesPaged(pageable);
// then
assertThat(response).isNotNull();
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(response.getBody()).isNotNull();
assertThat(pageable.getPageSize()).isEqualTo(response.getBody().getSize());
}
@EnableFeignClients(clients = InvoiceClient.class)
@RibbonClient(name = "local", configuration = LocalRibbonClientConfiguration.class)
@SpringBootApplication(scanBasePackages = "org.springframework.cloud.openfeign.encoding.app")
@EnableSpringDataWebSupport
@Import({ NoSecurityConfiguration.class, FeignClientsConfiguration.class })
public static class Application {
}
@Configuration
static class LocalRibbonClientConfiguration {
@Value("${local.server.port}")
private int port = 0;
@Bean
public ILoadBalancer ribbonLoadBalancer() {
BaseLoadBalancer balancer = new BaseLoadBalancer();
balancer.setServersList(
Collections.singletonList(new Server("localhost", this.port)));
return balancer;
}
}
}
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2017 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -16,13 +16,13 @@
package org.springframework.cloud.openfeign.encoding; package org.springframework.cloud.openfeign.encoding;
import org.springframework.cloud.openfeign.encoding.app.domain.Invoice;
import java.math.BigDecimal; import java.math.BigDecimal;
import java.util.ArrayList; import java.util.ArrayList;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import org.springframework.cloud.openfeign.encoding.app.domain.Invoice;
/** /**
* Utility class used for testing. * Utility class used for testing.
* *
@@ -35,9 +35,11 @@ final class Invoices {
for (int ind = 0; ind < count; ind++) { for (int ind = 0; ind < count; ind++) {
final Invoice invoice = new Invoice(); final Invoice invoice = new Invoice();
invoice.setTitle("Invoice " + (ind + 1)); invoice.setTitle("Invoice " + (ind + 1));
invoice.setAmount(new BigDecimal(String.format(Locale.US, "%.2f", Math.random() * 1000))); invoice.setAmount(new BigDecimal(
String.format(Locale.US, "%.2f", Math.random() * 1000)));
invoices.add(invoice); invoices.add(invoice);
} }
return invoices; return invoices;
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2015 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -20,6 +20,7 @@ import java.util.List;
import org.springframework.cloud.openfeign.FeignClient; import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.cloud.openfeign.encoding.app.domain.Invoice; import org.springframework.cloud.openfeign.encoding.app.domain.Invoice;
import org.springframework.data.domain.Page;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMapping;
@@ -33,9 +34,14 @@ import org.springframework.web.bind.annotation.RequestMethod;
@FeignClient("local") @FeignClient("local")
public interface InvoiceClient { public interface InvoiceClient {
@RequestMapping(value = "invoicesPaged", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<Page<Invoice>> getInvoicesPaged(
org.springframework.data.domain.Pageable pageable);
@RequestMapping(value = "invoices", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(value = "invoices", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<List<Invoice>> getInvoices(); ResponseEntity<List<Invoice>> getInvoices();
@RequestMapping(value = "invoices", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE) @RequestMapping(value = "invoices", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<List<Invoice>> saveInvoices(List<Invoice> invoices); ResponseEntity<List<Invoice>> saveInvoices(List<Invoice> invoices);
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2017 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -30,7 +30,7 @@ public class Invoice {
private BigDecimal amount; private BigDecimal amount;
public String getTitle() { public String getTitle() {
return title; return this.title;
} }
public void setTitle(String title) { public void setTitle(String title) {
@@ -38,10 +38,11 @@ public class Invoice {
} }
public BigDecimal getAmount() { public BigDecimal getAmount() {
return amount; return this.amount;
} }
public void setAmount(BigDecimal amount) { public void setAmount(BigDecimal amount) {
this.amount = amount; this.amount = amount;
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2017 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -16,7 +16,14 @@
package org.springframework.cloud.openfeign.encoding.app.resource; package org.springframework.cloud.openfeign.encoding.app.resource;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
import org.springframework.cloud.openfeign.encoding.app.domain.Invoice; import org.springframework.cloud.openfeign.encoding.app.domain.Invoice;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageImpl;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
@@ -24,11 +31,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod; import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
import java.util.Locale;
/** /**
* An sample REST controller, that potentially returns large response - used for testing. * An sample REST controller, that potentially returns large response - used for testing.
* *
@@ -43,21 +45,30 @@ public class InvoiceResource {
return ResponseEntity.ok(createInvoiceList(100)); return ResponseEntity.ok(createInvoiceList(100));
} }
@RequestMapping(value = "invoices", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, @RequestMapping(value = "invoices", method = RequestMethod.POST, consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
produces = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<List<Invoice>> saveInvoices(@RequestBody List<Invoice> invoices) { ResponseEntity<List<Invoice>> saveInvoices(@RequestBody List<Invoice> invoices) {
return ResponseEntity.ok(invoices); return ResponseEntity.ok(invoices);
} }
@RequestMapping(value = "invoicesPaged", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public ResponseEntity<Page<Invoice>> getInvoicesPaged(
org.springframework.data.domain.Pageable pageable) {
Page<Invoice> page = new PageImpl<>(createInvoiceList(pageable.getPageSize()),
pageable, 100);
return ResponseEntity.ok(page);
}
private List<Invoice> createInvoiceList(int count) { private List<Invoice> createInvoiceList(int count) {
final List<Invoice> invoices = new ArrayList<>(); final List<Invoice> invoices = new ArrayList<>();
for (int ind = 0; ind < count; ind++) { for (int ind = 0; ind < count; ind++) {
final Invoice invoice = new Invoice(); final Invoice invoice = new Invoice();
invoice.setTitle("Invoice " + (ind + 1)); invoice.setTitle("Invoice " + (ind + 1));
invoice.setAmount(new BigDecimal(String.format(Locale.US, "%.2f", Math.random() * 1000))); invoice.setAmount(new BigDecimal(
String.format(Locale.US, "%.2f", Math.random() * 1000)));
invoices.add(invoice); invoices.add(invoice);
} }
return invoices; return invoices;
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2012-2013 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -19,6 +19,7 @@ package org.springframework.cloud.openfeign.encoding.proto;
import feign.RequestTemplate; import feign.RequestTemplate;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
@@ -27,6 +28,8 @@ import org.springframework.cloud.test.ClassPathExclusions;
import org.springframework.cloud.test.ModifiedClassPathRunner; import org.springframework.cloud.test.ModifiedClassPathRunner;
import org.springframework.http.converter.StringHttpMessageConverter; import org.springframework.http.converter.StringHttpMessageConverter;
import static feign.Request.HttpMethod.POST;
/** /**
* Test {@link SpringEncoder} when protobuf is not in classpath * Test {@link SpringEncoder} when protobuf is not in classpath
* *
@@ -36,17 +39,17 @@ import org.springframework.http.converter.StringHttpMessageConverter;
@ClassPathExclusions("protobuf-*.jar") @ClassPathExclusions("protobuf-*.jar")
public class ProtobufNotInClasspathTest { public class ProtobufNotInClasspathTest {
@Test @Test
public void testEncodeWhenProtobufNotInClasspath() { public void testEncodeWhenProtobufNotInClasspath() {
ObjectFactory<HttpMessageConverters> converters = new ObjectFactory<HttpMessageConverters>() { ObjectFactory<HttpMessageConverters> converters = new ObjectFactory<HttpMessageConverters>() {
@Override @Override
public HttpMessageConverters getObject() throws BeansException { public HttpMessageConverters getObject() throws BeansException {
return new HttpMessageConverters(new StringHttpMessageConverter()); return new HttpMessageConverters(new StringHttpMessageConverter());
} }
}; };
RequestTemplate requestTemplate = new RequestTemplate(); RequestTemplate requestTemplate = new RequestTemplate();
requestTemplate.method("POST"); requestTemplate.method(POST);
new SpringEncoder(converters).encode("a=b", String.class, requestTemplate); new SpringEncoder(converters).encode("a=b", String.class, requestTemplate);
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2012-2013 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -16,6 +16,14 @@
package org.springframework.cloud.openfeign.encoding.proto; package org.springframework.cloud.openfeign.encoding.proto;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import com.google.protobuf.InvalidProtocolBufferException; import com.google.protobuf.InvalidProtocolBufferException;
import feign.RequestTemplate; import feign.RequestTemplate;
import feign.httpclient.ApacheHttpClient; import feign.httpclient.ApacheHttpClient;
@@ -27,27 +35,25 @@ import org.apache.http.client.methods.HttpEntityEnclosingRequestBase;
import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.methods.HttpUriRequest;
import org.apache.http.message.BasicHttpResponse; import org.apache.http.message.BasicHttpResponse;
import org.apache.http.message.BasicStatusLine; import org.apache.http.message.BasicStatusLine;
import org.junit.Assert;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.mockito.ArgumentMatchers;
import org.mockito.BDDMockito; import org.mockito.BDDMockito;
import org.mockito.Matchers;
import org.mockito.Mock; import org.mockito.Mock;
import org.mockito.invocation.InvocationOnMock; import org.mockito.invocation.InvocationOnMock;
import org.mockito.runners.MockitoJUnitRunner; import org.mockito.junit.MockitoJUnitRunner;
import org.mockito.stubbing.Answer; import org.mockito.stubbing.Answer;
import org.springframework.beans.BeansException; import org.springframework.beans.BeansException;
import org.springframework.beans.factory.ObjectFactory; import org.springframework.beans.factory.ObjectFactory;
import org.springframework.boot.autoconfigure.http.HttpMessageConverters; import org.springframework.boot.autoconfigure.http.HttpMessageConverters;
import org.springframework.cloud.openfeign.support.SpringEncoder; import org.springframework.cloud.openfeign.support.SpringEncoder;
import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter; import org.springframework.http.converter.protobuf.ProtobufHttpMessageConverter;
import java.io.IOException; import static feign.Request.Body.encoded;
import java.io.InputStream; import static feign.Request.HttpMethod.POST;
import java.net.URISyntaxException; import static org.assertj.core.api.Assertions.assertThat;
import java.nio.charset.StandardCharsets; import static org.assertj.core.api.Assertions.fail;
import java.util.ArrayList;
import java.util.List;
/** /**
* Test {@link SpringEncoder} with {@link ProtobufHttpMessageConverter} * Test {@link SpringEncoder} with {@link ProtobufHttpMessageConverter}
@@ -57,83 +63,92 @@ import java.util.List;
@RunWith(MockitoJUnitRunner.class) @RunWith(MockitoJUnitRunner.class)
public class ProtobufSpringEncoderTest { public class ProtobufSpringEncoderTest {
@Mock @Mock
private HttpClient httpClient; private HttpClient httpClient;
// a protobuf object with some content // a protobuf object with some content
private org.springframework.cloud.openfeign.encoding.proto.Request request = org.springframework.cloud.openfeign.encoding.proto.Request.newBuilder() private org.springframework.cloud.openfeign.encoding.proto.Request request = org.springframework.cloud.openfeign.encoding.proto.Request
.setId(1000000) .newBuilder().setId(1000000).setMsg("Erlang/OTP 最初是爱立信为开发电信设备系统设计的编程语言平台,"
.setMsg("Erlang/OTP 最初是爱立信为开发电信设备系统设计的编程语言平台," + + "电信设备(路由器、接入网关、…)典型设计是通过背板连接主控板卡与多块业务板卡的分布式系统。")
"电信设备(路由器、接入网关、…)典型设计是通过背板连接主控板卡与多块业务板卡的分布式系统。") .build();
.build();
@Test @Test
public void testProtobuf() throws IOException, URISyntaxException { public void testProtobuf() throws IOException, URISyntaxException {
// protobuf convert to request by feign and ProtobufHttpMessageConverter // protobuf convert to request by feign and ProtobufHttpMessageConverter
RequestTemplate requestTemplate = newRequestTemplate(); RequestTemplate requestTemplate = newRequestTemplate();
newEncoder().encode(request, Request.class, requestTemplate); newEncoder().encode(this.request, Request.class, requestTemplate);
HttpEntity entity = toApacheHttpEntity(requestTemplate); HttpEntity entity = toApacheHttpEntity(requestTemplate);
byte[] bytes = read(entity.getContent(), (int) entity.getContentLength()); byte[] bytes = read(entity.getContent(), (int) entity.getContentLength());
Assert.assertArrayEquals(bytes, request.toByteArray()); assertThat(this.request.toByteArray()).isEqualTo(bytes);
org.springframework.cloud.openfeign.encoding.proto.Request copy = org.springframework.cloud.openfeign.encoding.proto.Request.parseFrom(bytes); org.springframework.cloud.openfeign.encoding.proto.Request copy = org.springframework.cloud.openfeign.encoding.proto.Request
Assert.assertEquals(request, copy); .parseFrom(bytes);
} assertThat(copy).isEqualTo(this.request);
}
@Test @Test
public void testProtobufWithCharsetWillFail() throws IOException, URISyntaxException { public void testProtobufWithCharsetWillFail() throws IOException, URISyntaxException {
// protobuf convert to request by feign and ProtobufHttpMessageConverter // protobuf convert to request by feign and ProtobufHttpMessageConverter
RequestTemplate requestTemplate = newRequestTemplate(); RequestTemplate requestTemplate = newRequestTemplate();
newEncoder().encode(request, Request.class, requestTemplate); newEncoder().encode(this.request, Request.class, requestTemplate);
// set a charset // set a charset
requestTemplate.body(requestTemplate.body(), StandardCharsets.UTF_8); requestTemplate.body(
HttpEntity entity = toApacheHttpEntity(requestTemplate); encoded(requestTemplate.requestBody().asBytes(), StandardCharsets.UTF_8));
byte[] bytes = read(entity.getContent(), (int) entity.getContentLength()); HttpEntity entity = toApacheHttpEntity(requestTemplate);
byte[] bytes = read(entity.getContent(), (int) entity.getContentLength());
// http request-body is different with original protobuf body // http request-body is different with original protobuf body
Assert.assertNotEquals(bytes.length, request.toByteArray().length); assertThat(this.request.toByteArray().length).isNotEqualTo(bytes.length);
try { try {
org.springframework.cloud.openfeign.encoding.proto.Request copy = org.springframework.cloud.openfeign.encoding.proto.Request.parseFrom(bytes); org.springframework.cloud.openfeign.encoding.proto.Request copy = org.springframework.cloud.openfeign.encoding.proto.Request
Assert.fail("Expected an InvalidProtocolBufferException to be thrown"); .parseFrom(bytes);
} catch (InvalidProtocolBufferException e) { fail("Expected an InvalidProtocolBufferException to be thrown");
// success }
} catch (InvalidProtocolBufferException e) {
} // success
}
}
private SpringEncoder newEncoder() { private SpringEncoder newEncoder() {
ObjectFactory<HttpMessageConverters> converters = new ObjectFactory<HttpMessageConverters>() { ObjectFactory<HttpMessageConverters> converters = new ObjectFactory<HttpMessageConverters>() {
@Override @Override
public HttpMessageConverters getObject() throws BeansException { public HttpMessageConverters getObject() throws BeansException {
return new HttpMessageConverters(new ProtobufHttpMessageConverter()); return new HttpMessageConverters(new ProtobufHttpMessageConverter());
} }
}; };
return new SpringEncoder(converters); return new SpringEncoder(converters);
} }
private RequestTemplate newRequestTemplate() { private RequestTemplate newRequestTemplate() {
RequestTemplate requestTemplate = new RequestTemplate(); RequestTemplate requestTemplate = new RequestTemplate();
requestTemplate.method("POST"); requestTemplate.method(POST);
return requestTemplate; return requestTemplate;
} }
private HttpEntity toApacheHttpEntity(RequestTemplate requestTemplate) throws IOException, URISyntaxException { private HttpEntity toApacheHttpEntity(RequestTemplate requestTemplate)
final List<HttpUriRequest> request = new ArrayList<>(1); throws IOException, URISyntaxException {
BDDMockito.given(httpClient.execute(Matchers.<HttpUriRequest>any())).will(new Answer<HttpResponse>() { final List<HttpUriRequest> request = new ArrayList<>(1);
@Override BDDMockito.given(this.httpClient.execute(ArgumentMatchers.<HttpUriRequest>any()))
public HttpResponse answer(InvocationOnMock invocationOnMock) throws Throwable { .will(new Answer<HttpResponse>() {
request.add((HttpUriRequest) invocationOnMock.getArguments()[0]); @Override
return new BasicHttpResponse(new BasicStatusLine(new ProtocolVersion("http", 1, 1), 200, null)); public HttpResponse answer(InvocationOnMock invocationOnMock)
} throws Throwable {
}); request.add((HttpUriRequest) invocationOnMock.getArguments()[0]);
new ApacheHttpClient(httpClient).execute(requestTemplate.request(), new feign.Request.Options()); return new BasicHttpResponse(new BasicStatusLine(
HttpUriRequest httpUriRequest = request.get(0); new ProtocolVersion("http", 1, 1), 200, null));
return ((HttpEntityEnclosingRequestBase)httpUriRequest).getEntity(); }
} });
new ApacheHttpClient(this.httpClient).execute(
requestTemplate.resolve(new HashMap<>()).request(),
new feign.Request.Options());
HttpUriRequest httpUriRequest = request.get(0);
return ((HttpEntityEnclosingRequestBase) httpUriRequest).getEntity();
}
private byte[] read(InputStream in, int length) throws IOException { private byte[] read(InputStream in, int length) throws IOException {
byte[] bytes = new byte[length]; byte[] bytes = new byte[length];
in.read(bytes); in.read(bytes);
return bytes; return bytes;
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2012-2013 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -20,53 +20,48 @@
package org.springframework.cloud.openfeign.encoding.proto; package org.springframework.cloud.openfeign.encoding.proto;
public final class ProtobufTest { public final class ProtobufTest {
private ProtobufTest() {}
public static void registerAllExtensions(
com.google.protobuf.ExtensionRegistryLite registry) {
}
public static void registerAllExtensions( static final com.google.protobuf.Descriptors.Descriptor internal_static_Request_descriptor;
com.google.protobuf.ExtensionRegistry registry) { static final com.google.protobuf.GeneratedMessageV3.FieldAccessorTable internal_static_Request_fieldAccessorTable;
registerAllExtensions(
(com.google.protobuf.ExtensionRegistryLite) registry);
}
static final com.google.protobuf.Descriptors.Descriptor
internal_static_Request_descriptor;
static final
com.google.protobuf.GeneratedMessageV3.FieldAccessorTable
internal_static_Request_fieldAccessorTable;
public static com.google.protobuf.Descriptors.FileDescriptor private static com.google.protobuf.Descriptors.FileDescriptor descriptor;
getDescriptor() {
return descriptor; static {
} String[] descriptorData = {
private static com.google.protobuf.Descriptors.FileDescriptor "\n\023protobuf_test.proto\"\"\n\007Request\022\n\n\002id\030\001"
descriptor; + " \001(\005\022\013\n\003msg\030\002 \001(\tB\024\n\020feign.httpclientP\001b"
static { + "\006proto3" };
String[] descriptorData = { com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = new com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner() {
"\n\023protobuf_test.proto\"\"\n\007Request\022\n\n\002id\030\001" + public com.google.protobuf.ExtensionRegistry assignDescriptors(
" \001(\005\022\013\n\003msg\030\002 \001(\tB\024\n\020feign.httpclientP\001b" + com.google.protobuf.Descriptors.FileDescriptor root) {
"\006proto3" descriptor = root;
}; return null;
com.google.protobuf.Descriptors.FileDescriptor.InternalDescriptorAssigner assigner = }
new com.google.protobuf.Descriptors.FileDescriptor. InternalDescriptorAssigner() { };
public com.google.protobuf.ExtensionRegistry assignDescriptors( com.google.protobuf.Descriptors.FileDescriptor.internalBuildGeneratedFileFrom(
com.google.protobuf.Descriptors.FileDescriptor root) { descriptorData, new com.google.protobuf.Descriptors.FileDescriptor[] {},
descriptor = root; assigner);
return null; internal_static_Request_descriptor = getDescriptor().getMessageTypes().get(0);
} internal_static_Request_fieldAccessorTable = new com.google.protobuf.GeneratedMessageV3.FieldAccessorTable(
}; internal_static_Request_descriptor, new String[] { "Id", "Msg", });
com.google.protobuf.Descriptors.FileDescriptor }
.internalBuildGeneratedFileFrom(descriptorData,
new com.google.protobuf.Descriptors.FileDescriptor[] { private ProtobufTest() {
}, assigner); }
internal_static_Request_descriptor =
getDescriptor().getMessageTypes().get(0); public static void registerAllExtensions(
internal_static_Request_fieldAccessorTable = new com.google.protobuf.ExtensionRegistryLite registry) {
com.google.protobuf.GeneratedMessageV3.FieldAccessorTable( }
internal_static_Request_descriptor,
new String[] { "Id", "Msg", }); public static void registerAllExtensions(
} com.google.protobuf.ExtensionRegistry registry) {
registerAllExtensions((com.google.protobuf.ExtensionRegistryLite) registry);
}
public static com.google.protobuf.Descriptors.FileDescriptor getDescriptor() {
return descriptor;
}
// @@protoc_insertion_point(outer_class_scope)
// @@protoc_insertion_point(outer_class_scope)
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2012-2013 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -20,21 +20,22 @@
package org.springframework.cloud.openfeign.encoding.proto; package org.springframework.cloud.openfeign.encoding.proto;
public interface RequestOrBuilder extends public interface RequestOrBuilder extends
// @@protoc_insertion_point(interface_extends:Request) // @@protoc_insertion_point(interface_extends:Request)
com.google.protobuf.MessageOrBuilder { com.google.protobuf.MessageOrBuilder {
/** /**
* <code>int32 id = 1;</code> * <code>int32 id = 1;</code>
*/ */
int getId(); int getId();
/**
* <code>string msg = 2;</code>
*/
String getMsg();
/**
* <code>string msg = 2;</code>
*/
com.google.protobuf.ByteString getMsgBytes();
/**
* <code>string msg = 2;</code>
*/
String getMsg();
/**
* <code>string msg = 2;</code>
*/
com.google.protobuf.ByteString
getMsgBytes();
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2016 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -16,17 +16,45 @@
package org.springframework.cloud.openfeign.hystrix.security; package org.springframework.cloud.openfeign.hystrix.security;
import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.cloud.openfeign.EnableFeignClients; import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.cloud.openfeign.hystrix.security.app.CustomConcurrenyStrategy;
import org.springframework.cloud.openfeign.hystrix.security.app.ProxyUsernameController;
import org.springframework.cloud.openfeign.hystrix.security.app.TestInterceptor;
import org.springframework.cloud.openfeign.hystrix.security.app.UsernameClient; import org.springframework.cloud.openfeign.hystrix.security.app.UsernameClient;
import org.springframework.cloud.openfeign.hystrix.security.app.UsernameController;
import org.springframework.cloud.openfeign.test.NoSecurityConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
/** /**
* @author Daniel Lavoie * @author Daniel Lavoie
*/ */
@Configuration @Configuration
@SpringBootApplication @EnableAutoConfiguration
@EnableFeignClients(clients = UsernameClient.class) @EnableFeignClients(clients = UsernameClient.class)
@Import(NoSecurityConfiguration.class)
public class HystrixSecurityApplication { public class HystrixSecurityApplication {
@Bean
public CustomConcurrenyStrategy customConcurrenyStrategy() {
return new CustomConcurrenyStrategy();
}
@Bean
public TestInterceptor testInterceptor() {
return new TestInterceptor();
}
@Bean
public ProxyUsernameController proxyUsernameController() {
return new ProxyUsernameController();
}
@Bean
public UsernameController usernameController() {
return new UsernameController();
}
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2016 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -17,73 +17,61 @@
package org.springframework.cloud.openfeign.hystrix.security; package org.springframework.cloud.openfeign.hystrix.security;
import java.util.Base64; import java.util.Base64;
import org.junit.Assert;
import com.netflix.hystrix.strategy.HystrixPlugins;
import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ServerList;
import org.junit.Test; import org.junit.Test;
import org.junit.runner.RunWith; import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment; import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
import org.springframework.boot.web.server.LocalServerPort; import org.springframework.boot.web.server.LocalServerPort;
import org.springframework.cloud.netflix.hystrix.security.SecurityContextConcurrencyStrategy; import org.springframework.cloud.netflix.hystrix.security.SecurityContextConcurrencyStrategy;
import org.springframework.cloud.netflix.ribbon.RibbonClient;
import org.springframework.cloud.netflix.ribbon.StaticServerList;
import org.springframework.cloud.openfeign.hystrix.security.app.CustomConcurrenyStrategy; import org.springframework.cloud.openfeign.hystrix.security.app.CustomConcurrenyStrategy;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Import;
import org.springframework.http.HttpEntity; import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders; import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpMethod; import org.springframework.http.HttpMethod;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.test.annotation.DirtiesContext; import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.ActiveProfiles; import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.junit4.SpringRunner; import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.web.client.RestTemplate; import org.springframework.web.client.RestTemplate;
import com.netflix.hystrix.strategy.HystrixPlugins;
import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy;
import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThat;
/** /**
* Tests that a secured web service returning values using a feign client properly access * Tests that a secured web service returning values using a feign client properly access
* the security context from a hystrix command. * the security context from a hystrix command.
*
* @author Daniel Lavoie * @author Daniel Lavoie
*/ */
@RunWith(SpringRunner.class) @RunWith(SpringRunner.class)
@DirtiesContext @DirtiesContext
@SpringBootTest(classes = HystrixSecurityApplication.class, webEnvironment = WebEnvironment.RANDOM_PORT, @SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT, properties = {
properties = { "username.ribbon.listOfServers=localhost:${local.server.port}", "feign.hystrix.enabled=true" })
"feign.hystrix.enabled=true"})
@ActiveProfiles("proxysecurity") @ActiveProfiles("proxysecurity")
public class HystrixSecurityTests { public class HystrixSecurityTests {
@Autowired @Autowired
private CustomConcurrenyStrategy customConcurrenyStrategy; private CustomConcurrenyStrategy customConcurrenyStrategy;
@LocalServerPort @LocalServerPort
private String serverPort; private String serverPort;
//TODOO: move to constants in TestAutoConfiguration // TODO: move to constants in TestAutoConfiguration
private String username = "user"; private String username = "user";
private String password = "password"; private String password = "password";
@Test
public void testSecurityConcurrencyStrategyInstalled() {
HystrixConcurrencyStrategy concurrencyStrategy = HystrixPlugins.getInstance().getConcurrencyStrategy();
assertThat(concurrencyStrategy).isInstanceOf(SecurityContextConcurrencyStrategy.class);
}
@Test
public void testFeignHystrixSecurity() {
HttpHeaders headers = HystrixSecurityTests.createBasicAuthHeader(username,
password);
String usernameResult = new RestTemplate()
.exchange("http://localhost:" + serverPort + "/proxy-username",
HttpMethod.GET, new HttpEntity<Void>(headers), String.class)
.getBody();
Assert.assertTrue("Username should have been intercepted by feign interceptor.",
username.equals(usernameResult));
Assert.assertTrue("Custom hook should have been called.",
customConcurrenyStrategy.isHookCalled());
}
public static HttpHeaders createBasicAuthHeader(final String username, public static HttpHeaders createBasicAuthHeader(final String username,
final String password) { final String password) {
return new HttpHeaders() { return new HttpHeaders() {
@@ -97,4 +85,50 @@ public class HystrixSecurityTests {
} }
}; };
} }
@Test
public void testSecurityConcurrencyStrategyInstalled() {
HystrixConcurrencyStrategy concurrencyStrategy = HystrixPlugins.getInstance()
.getConcurrencyStrategy();
assertThat(concurrencyStrategy)
.isInstanceOf(SecurityContextConcurrencyStrategy.class);
}
@Test
public void testFeignHystrixSecurity() {
HttpHeaders headers = createBasicAuthHeader(this.username, this.password);
ResponseEntity<String> entity = new RestTemplate().exchange(
"http://localhost:" + this.serverPort + "/proxy-username", HttpMethod.GET,
new HttpEntity<Void>(headers), String.class);
assertThat(entity.getStatusCode()).isEqualTo(HttpStatus.OK);
assertThat(entity.getBody())
.as("Username should have been intercepted by feign interceptor.")
.isEqualTo(this.username);
assertThat(this.customConcurrenyStrategy.isHookCalled())
.as("Custom hook should have been called.").isTrue();
}
@SpringBootConfiguration
@Import(HystrixSecurityApplication.class)
@RibbonClient(name = "username", configuration = LocalRibbonClientConfiguration.class)
protected static class TestConfig {
}
protected static class LocalRibbonClientConfiguration {
@LocalServerPort
private int port = 0;
@Bean
public ServerList<Server> ribbonServerList() {
return new StaticServerList<>(new Server("localhost", this.port));
}
}
} }
@@ -1,11 +1,27 @@
/*
* Copyright 2013-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* 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.hystrix.security.app; package org.springframework.cloud.openfeign.hystrix.security.app;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import org.springframework.stereotype.Component;
import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy; import com.netflix.hystrix.strategy.concurrency.HystrixConcurrencyStrategy;
@Component
public class CustomConcurrenyStrategy extends HystrixConcurrencyStrategy { public class CustomConcurrenyStrategy extends HystrixConcurrencyStrategy {
private boolean hookCalled; private boolean hookCalled;
@Override @Override
@@ -16,6 +32,7 @@ public class CustomConcurrenyStrategy extends HystrixConcurrencyStrategy {
} }
public boolean isHookCalled() { public boolean isHookCalled() {
return hookCalled; return this.hookCalled;
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2016 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -26,11 +26,13 @@ import org.springframework.web.bind.annotation.RestController;
@RestController @RestController
@RequestMapping("/proxy-username") @RequestMapping("/proxy-username")
public class ProxyUsernameController { public class ProxyUsernameController {
@Autowired @Autowired
private UsernameClient usernameClient; private UsernameClient usernameClient;
@RequestMapping @RequestMapping
public String getUsername() { public String getUsername() {
return usernameClient.getUsername(); return this.usernameClient.getUsername();
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2016 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -20,21 +20,21 @@ import feign.RequestInterceptor;
import feign.RequestTemplate; import feign.RequestTemplate;
import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
/** /**
* This interceptor should be called from an Hyxtrix command execution thread. It is * This interceptor should be called from an Hyxtrix command execution thread. It is
* access the SecurityContext and settings an http header from the authentication details. * access the SecurityContext and settings an http header from the authentication details.
* *
* @author Daniel Lavoie * @author Daniel Lavoie
*/ */
@Component
public class TestInterceptor implements RequestInterceptor { public class TestInterceptor implements RequestInterceptor {
@Override @Override
public void apply(RequestTemplate template) { public void apply(RequestTemplate template) {
if (SecurityContextHolder.getContext().getAuthentication() != null) if (SecurityContextHolder.getContext().getAuthentication() != null) {
template.header("username", template.header("username",
SecurityContextHolder.getContext().getAuthentication().getName()); SecurityContextHolder.getContext().getAuthentication().getName());
}
} }
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2016 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -26,5 +26,6 @@ import org.springframework.web.bind.annotation.RequestMapping;
public interface UsernameClient { public interface UsernameClient {
@RequestMapping("/username") @RequestMapping("/username")
public String getUsername(); String getUsername();
} }
@@ -1,11 +1,11 @@
/* /*
* Copyright 2013-2016 the original author or authors. * Copyright 2013-2019 the original author or authors.
* *
* Licensed under the Apache License, Version 2.0 (the "License"); * Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License. * you may not use this file except in compliance with the License.
* You may obtain a copy of the License at * You may obtain a copy of the License at
* *
* http://www.apache.org/licenses/LICENSE-2.0 * https://www.apache.org/licenses/LICENSE-2.0
* *
* Unless required by applicable law or agreed to in writing, software * Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, * distributed under the License is distributed on an "AS IS" BASIS,
@@ -26,8 +26,10 @@ import org.springframework.web.bind.annotation.RestController;
@RestController @RestController
@RequestMapping("/username") @RequestMapping("/username")
public class UsernameController { public class UsernameController {
@RequestMapping @RequestMapping
public String getUsername(@RequestHeader String username){ public String getUsername(@RequestHeader String username) {
return username; return username;
} }
} }

Some files were not shown because too many files have changed in this diff Show More