Feign — это Java-биндер для HTTP-клиентов, вдохновленный Retrofit, JAXRS-2.0 и WebSocket. Первая цель Feign заключалась в уменьшении сложности привязки Denominator к HTTP-интерфейсам независимо от RESTfulness.
Feign использует инструменты, такие как Jersey и CXF, для написания Java-клиентов для ReST или SOAP-сервисов. Кроме того, Feign позволяет вам писать собственный код поверх библиотек HTTP, таких как Apache HC. Feign соединяет ваш код с HTTP-интерфейсами с минимальными затратами и кодом за счет настраиваемых декодеров и обработки ошибок, которые могут быть написаны для любого текстового HTTP-интерфейса.
Feign 10.x и выше построены на Java 8 и должны работать на Java 9, 10 и 11. Для тех, кто нуждается в совместимости с JDK 6, пожалуйста, используйте Feign 9.x.
Это карта с текущими ключевыми функциями, предоставляемыми Feign:
Logger
Logger
для соответствия фреймворкам, таким как SLF4J, предоставляя общий умственный модель для логирования в Feign. Эта модель будет использоваться Feign самим и предоставит более ясное представление о том, как будет использоваться Logger
.Retry
Retry
для поддержки пользовательских условий и лучшего контроля над политиками отложенного повторного выполнения. Это может привести к несовместимым изменениям, нарушающим обратную совместимостьСреднесрочные задачи - что будет дальше. ⏲CompletableFuture
Future
и управление исполнителями для жизненного цикла запроса/ответа. Реализация потребует несовместимых изменений, нарушающих обратную совместимость. Однако это функциональное требование необходимо для того, чтобы можно было рассмотреть реактивное выполнение.java.util.concurrent.Flow
.Библиотека feign доступна из Maven Central.
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-core</artifactId>
<version>??feign.version??</version>
</dependency>
Использование обычно выглядит так, адаптация канонического примера Retrofit.
interface GitHub {
@RequestLine("GET /repos/{owner}/{repo}/contributors")
List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
``` @RequestLine("POST /repos/{owner}/{repo}/issues")
void createIssue(Issue issue, @Param("owner") String owner, @Param("repo") String repo);
}
public static class Contributor {
String login;
int contributions;
}
public static class Issue {
String title;
String body;
List<String> assignees;
int milestone;
List<String> labels;
}
public class MyApp {
public static void main(String... args) {
GitHub github = Feign.builder()
.decoder(new GsonDecoder())
.target(GitHub.class, "https://api.github.com");
// Получаем и выводим список вкладчиков этого репозитория.
List<Contributor> contributors = github.contributors("OpenFeign", "feign");
for (Contributor contributor : contributors) {
System.out.println(contributor.login + " (" + contributor.contributions + ")");
}
}
}
Contract
между интерфейсом и тем, как должен работать подлежащий клиент.Стандартный контракт Feign определяет следующие аннотации:| Аннотация | Целевой интерфейс | Использование |
|----------------|------------------|---------------|
| @RequestLine
| Метод | Определяет HttpMethod
и UriTemplate
для запроса. Выражения
, значения, заключенные в фигурные скобки {expression}
, разрешаются с помощью соответствующих параметров, аннотированных @Param
. |
| @Param
| Параметр | Определяет переменную шаблона, значение которой будет использовано для разрешения соответствующего шаблона Выражения
, по имени, указанному в качестве значения аннотации. Если значение отсутствует, попытается получить имя из имени параметра метода в байт-коде (если код был скомпилирован с флагом -parameters
). |
| @Headers
| Метод, Тип | Определяет HeaderTemplate
; вариация UriTemplate
, которая использует значения параметров, аннотированных @Param
, для разрешения соответствующих Выражений
. При использовании на Типе
, шаблон будет применен ко всем запросам. При использовании на Методе
, шаблон будет применен только к аннотированному методу. |
| @QueryMap
| Параметр | Определяет Map
пар имя-значение или POJO, для расширения в строку запроса. |
| @HeaderMap
| Параметр | Определяет Map
пар имя-значение, для расширения в Http Headers
. || @Body
| Метод | Определяет Template
, подобный UriTemplate
и HeaderTemplate
, который использует значения параметров, аннотированных @Param
, для разрешения соответствующих выражений
. |> Переопределение строки запроса
Если необходимо направить запрос на другой хост, отличный от того, который был указан при создании клиента Feign, или вы хотите указать целевой хост для каждого запроса, включите параметр
java.net.URI
, и Feign будет использовать это значение как целевую точку запроса.@RequestLine("POST /repos/{owner}/{repo}/issues") void createIssue(URI host, Issue issue, @Param("owner") String owner, @Param("repo") String repo); ```### Шаблоны и выражения
Выражения Feign представляют простые строковые выражения (уровень 1) в соответствии с спецификацией URI Template - RFC 6570. Выражения расширяются с помощью соответствующих параметров метода, аннотированных Param
.
Пример
public interface GitHub {
@RequestLine("GET /repos/{owner}/{repo}/contributors")
List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repository);
class Contributor {
String login;
int contributions;
}
}
public class MyApp {
public static void main(String[] args) {
GitHub github = Feign.builder()
.decoder(new GsonDecoder())
.target(GitHub.class, "https://api.github.com");
/* Параметры owner и repo будут использоваться для расширения выражений owner и repo, определенных в RequestLine.
*
* Результатом будет URI https://api.github.com/repos/OpenFeign/feign/contributors
*/
github.contributors("OpenFeign", "feign");
}
}
Выражения должны быть заключены в фигурные скобки {}
, и могут содержать регулярные выражения, разделенные двоеточием :
, чтобы ограничить разрешенные значения. Пример owner
должен состоять только из букв. {owner:[a-zA-Z]*}
Шаблоны RequestLine
и QueryMap
следуют спецификации URI Template - RFC 6570 для шаблонов уровня 1, которая определяет следующее:
Неразрешенные выражения исключаются.
Все литералы и значения переменных закодированы в формате pct, если они не были уже закодированы или помечены как encoded
с помощью аннотации @Param
.У нас также есть ограниченная поддержка для уровня 3, выражений стиля пути, с следующими ограничениями:
Массивы и списки расширяются по умолчанию.
Поддерживаются только шаблоны с одной переменной.*
Примеры:
{;who} ;who=fred
{;half} ;half=50%25
{;empty} ;empty
{;list} ;list=red;list=green;list=blue
{;map} ;semi=%3B;dot=.;comma=%2C
public interface MatrixService {
@RequestLine("GET /repos{;owners}")
List<Contributor> contributors(@Param("owners") List<String> owners);
class Contributor {
String login;
int contributions;
}
}
Если owners
в приведенном выше примере определен как Matt, Jeff, Susan
, URI расширится до /repos;owners=Matt;owners=Jeff;owners=Susan
Для получения дополнительной информации см. RFC Yöntem 6570, раздел 3.2.7
Неопределенные выражения — это выражения, где значение для выражения явно равно null
или значение не предоставляется.
Согласно URI Template - RFC 6570, возможно предоставить пустое значение
для выражения. Когда Feign разрешает выражение, он сначала определяет, определено ли значение, если значение определено,
то параметр запроса остается. Если выражение неопределено, параметр запроса удаляется. См. ниже полное разъяснение.
Пустая строка
public void test() {
Map<String, Object> parameters = new LinkedHashMap<>();
parameters.put("param", "");
this.demoClient.test(parameters);
}
Результат
http://localhost:8080/test?param=
Исправленный текст:
У нас также есть ограниченная поддержка для уровня 3, выражений стиля пути, с следующими ограничениями:
Примеры:
{;who} ;who=fred
{;half} ;half=50%25
{;empty} ;empty
{;list} ;list=red;list=green;list=blue
{;map} ;semi=%3B;dot=.;comma=%2C
public interface MatrixService {
@RequestLine("GET /repos{;owners}")
List<Contributor> contributors(@Param("owners") List<String> owners);
class Contributor {
String login;
int contributions;
}
}
Если owners
в приведенном выше примере определен как Matt, Jeff, Susan
, URI расширится до /repos;owners=Matt;owners=Jeff;owners=Susan
Для получения дополнительной информации см. RFC 6570, раздел 3.2.7
Неопределенные выражения — это выражения, где значение для выражения явно равно null
или значение не предоставляется.
Согласно URI Template - RFC 6570, возможно предоставить пустое значение
для выражения. Когда Feign разрешает выражение, он сначала определяет, определено ли значение, если значение определено,
то параметр запроса остается. Если выражение неопределено, параметр запроса удаляется. См. ниже полное разъяснение.
Пустая строка
public void test() {
Map<String, Object> parameters = new LinkedHashMap<>();
parameters.put("param", "");
this.demoClient.test(parameters);
}
Результат
http://localhost:8080/test?param=
```*Отсутствующее значение*
```java
public void test() {
Map<String, Object> parameters = new LinkedHashMap<>();
this.demoClient.test(parameters);
}
Результат
http://localhost:8080/test
Неопределенное значение
public void test() {
Map<String, Object> parameters = new LinkedHashMap<>();
parameters.put("param", null);
this.demoClient.test(parameters);
}
Результат
http://localhost:8080/test
Для получения дополнительных примеров см. раздел Расширенное использование.
А что насчет слешей?
/
Шаблоны
@RequestLine
по умолчанию не кодируют символы слеша/
. Чтобы изменить это поведение, установите свойствоdecodeSlash
вfalse
для@RequestLine
.> Что насчет плюса?+
Согласно спецификации URI, символ
+
допустим как в пути, так и в строке запроса URI. Однако обработка этого символа в строке запроса может быть неоднородной. В некоторых устаревших системах символ+
эквивалентен пробелу. Feign следует подходу современных систем, где символ+
не должен представлять пробел и явно кодируется как%2B
при обнаружении в строке запроса.Если вы хотите использовать
+
как пробел, используйте символ пробелаили закодируйте значение напрямую как
%20
##### Пользовательское Расширение
Аннотация @Param
имеет опциональное свойство expander
, которое позволяет полностью контролировать расширение отдельного параметра.
Свойство expander
должно ссылаться на класс, реализующий интерфейс Expander
:```java
public interface Expander {
String expand(Object value);
}
Результат этого метода следует тем же правилам, что и выше. Если результат равен `null` или пустой строке, значение опускается. Если значение не закодировано в percent-encoded, оно будет закодировано. См. [Пользовательское расширение @Param](#custom-param-expansion) для дополнительных примеров.
#### Расширение заголовков запроса
Шаблоны `Headers` и `HeaderMap` следуют тем же правилам, что и [Расширение параметров запроса](#request-parameter-expansion), с некоторыми изменениями:
* Неразрешённые выражения опускаются. Если результат равен пустому значению заголовка, весь заголовок удаляется.
* Не выполняется кодирование в percent-encoded.
См. [Заголовки](#headers) для примеров.
> **Примечание к параметрам @Param и их именам**:
>
> Все выражения с одинаковым именем, независимо от их положения в `@RequestLine`, `@QueryMap`, `@BodyTemplate`, или `@Headers`, будут разрешаться к одному и тому же значению.
> В следующем примере значение `contentType` будет использоваться для разрешения как заголовка, так и выражения пути:
>
> ```java
> public interface ContentService {
> @RequestLine("GET /api/documents/{contentType}")
> @Headers("Accept: {contentType}")
> String getDocumentByType(@Param("contentType") String type);
> }
>```
>
> Имейте это в виду при проектировании ваших интерфейсов.
#### Расширение тела запроса
Шаблоны `Body` следуют тем же правилам, что и [Расширение параметров запроса](#request-parameter-expansion), с некоторыми изменениями:* Неразрешённые выражения опускаются.
* Расширенное значение **не** будет передано через `Encoder` перед тем, как быть добавлено в тело запроса.
* Должен быть указан заголовок `Content-Type`. См. [Шаблоны тела](#body-templates) для примеров.
---
### Пользовательские Настройки
Feign имеет несколько аспектов, которые можно настроить.
Для простых случаев вы можете использовать `Feign.builder()` для создания интерфейса API с вашими пользовательскими компонентами.<br>
Для настройки запроса вы можете использовать `options(Request.Options options)` на `target()` для установки `connectTimeout`, `connectTimeoutUnit`, `readTimeout`, `readTimeoutUnit`, `followRedirects`.<br>
Для примера:
```java
interface Bank {
@RequestLine("POST /account/{id}")
Account getAccountInfo(@Param("id") String id);
}
public class BankService {
public static void main(String[] args) {
Bank bank = Feign.builder()
.decoder(new AccountDecoder())
.options(new Request.Options(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, true))
.target(Bank.class, "https://api.examplebank.com");
}
}
Feign может создавать несколько интерфейсов API. Эти интерфейсы определяются как Target<T>
(по умолчанию HardCodedTarget<T>
), что позволяет для динамического обнаружения и декорирования запросов перед их выполнением.
Например, следующий шаблон может декорировать каждый запрос текущим URL и токеном аутентификации из службы идентификации.
public class CloudService {
public static void main(String[] args) {
CloudDNS cloudDNS = Feign.builder()
.target(new CloudIdentityTarget<CloudDNS>(user, apiKey));
}
``` class CloudIdentityTarget extends Target<CloudDNS> {
/* реализация класса Target */
}
}
Feign включает примеры клиентов для GitHub и Wikipedia. Проект denominator также можно использовать для изучения Feign в действии. В частности, обратите внимание на его пример демона.
Feign предназначен для работы с другими открытыми проектами. Модули приветствуются для интеграции с вашими любимыми проектами!
Gson включает кодировщик и декодер, которые можно использовать с JSON API.
Добавьте GsonEncoder
и/или GsonDecoder
к вашему Feign.Builder
следующим образом:
public class Example {
public static void main(String[] args) {
GsonCodec codec = new GsonCodec();
GitHub github = Feign.builder()
.encoder(new GsonEncoder())
.decoder(new GsonDecoder())
.target(GitHub.class, "https://api.github.com");
}
}
Jackson включает кодировщик и декодер, которые можно использовать с JSON API.
Добавьте JacksonEncoder
и/или JacksonDecoder
к вашему Feign.Builder
следующим образом:
public class Example {
public static void main(String[] args) {
GitHub github = Feign.builder()
.encoder(new JacksonEncoder())
.decoder(new JacksonDecoder())
.target(GitHub.class, "https://api.github.com");
}
}
Для более легкого использования Jackson Jr используйте JacksonJrEncoder
и JacksonJrDecoder
из
модуля Jackson Jr.
Moshi включает кодировщик и декодер, которые можно использовать с JSON API.
Добавьте MoshiEncoder
и/или MoshiDecoder
к вашему Feign.Builder
следующим образом:```java
GitHub github = Feign.builder()
.encoder(new MoshiEncoder())
.decoder(new MoshiDecoder())
.target(GitHub.class, "https://api.github.com");
#### Sax
[SaxDecoder](./sax) позволяет декодировать XML таким образом, чтобы это было совместимо с обычной JVM и также с Android-окружением.
Вот пример конфигурации парсинга ответа Sax:
```java
public class Example {
public static void main(String[] args) {
Api api = Feign.builder()
.decoder(SAXDecoder.builder()
.registerContentHandler(UserIdHandler.class)
.build())
.target(Api.class, "https://apihost");
}
}
JAXB включает в себя кодировщик и декодировщик, которые можно использовать с XML API.
Добавьте JAXBEncoder
и/или JAXBDecoder
к вашему Feign.Builder
следующим образом:
public class Example {
public static void main(String[] args) {
Api api = Feign.builder()
.encoder(new JAXBEncoder())
.decoder(new JAXBDecoder())
.target(Api.class, "https://apihost");
}
}
SOAP включает в себя кодировщик и декодировщик, которые можно использовать с XML API.
Этот модуль добавляет поддержку для кодирования и декодирования объектов SOAP Body через JAXB и SOAPMessage. Он также предоставляет возможности декодирования SOAPFault, упаковывая их в исключение javax.xml.ws.soap.SOAPFaultException
, так что вам нужно будет ловить только SOAPFaultException
, чтобы обрабатывать SOAPFault.
Добавьте SOAPEncoder
и/или SOAPDecoder
к вашему Feign.Builder
следующим образом:
public class Example {
public static void main(String[] args) {
Api api = Feign.builder()
.encoder(new SOAPEncoder(jaxbFactory))
.decoder(new SOAPDecoder(jaxbFactory))
.errorDecoder(new SOAPErrorDecoder())
.target(MyApi.class, "http://api");
}
}
```Примечание: возможно, вам также потребуется добавить `SOAPErrorDecoder`, если SOAP Faults возвращаются в ответе с ошибочными HTTP-кодами (400, 500, ...).#### Fastjson2
[fastjson2](./fastjson2) включает в себя кодировщик и декодировщик, которые можно использовать с API JSON.
Добавьте `Fastjson2Encoder` и/или `Fastjson2Decoder` к вашему `Feign.Builder` следующим образом:
```java
public class Example {
public static void main(String[] args) {
GitHub github = Feign.builder()
.encoder(new Fastjson2Encoder())
.decoder(new Fastjson2Decoder())
.target(GitHub.class, "https://api.github.com");
}
}
JAXRSContract переопределяет обработку аннотаций, чтобы использовать стандартные, предоставляемые спецификацией JAX-RS. Это в настоящее время ориентировано на спецификацию 1.1.
Вот пример выше, переписанный для использования JAX-RS:
interface GitHub {
@GET @Path("/repos/{owner}/{repo}/contributors")
List<Contributor> contributors(@PathParam("owner") String owner, @PathParam("repo") String repo);
}
public class Example {
public static void main(String[] args) {
GitHub github = Feign.builder()
.contract(new JAXRSContract())
.target(GitHub.class, "https://api.github.com");
}
}
OkHttpClient направляет HTTP-запросы Feign к OkHttp, что позволяет использовать SPDY и улучшенное управление сетью.
Чтобы использовать OkHttp с Feign, добавьте модуль OkHttp к вашему classpath. Затем настройте Feign для использования OkHttpClient:
public class Example {
public static void main(String[] args) {
GitHub github = Feign.builder()
.client(new OkHttpClient())
.target(GitHub.class, "https://api.github.com");
}
}
```#### Ribbon
[RibbonClient](./ribbon) переопределяет разрешение URL клиента Feign, добавляя возможности умного маршрутизирования и устойчивости, предоставляемые [Ribbon](https://github.com/Netflix/ribbon).
Интеграция требует передачи имени клиента Ribbon как части хоста URL, например `myAppProd`.
```java
public class Example {
public static void main(String[] args) {
MyService api = Feign.builder()
.client(RibbonClient.create())
.target(MyService.class, "https://myAppProd");
}
}
Http2Client направляет HTTP-запросы Feign к новому HTTP/2 клиенту Java 11 New HTTP/2 Client, который реализует HTTP/2. Чтобы использовать новый HTTP/2 Client с Feign, используйте Java SDK 11. Затем настройте Feign для использования Http2Client:
GitHub github = Feign.builder()
.client(new Http2Client())
.target(GitHub.class, "https://api.github.com");
HystrixFeign настраивает поддержку размыкания цепи, предоставляемую Hystrix.
Чтобы использовать Hystrix с Feign, добавьте модуль Hystrix в ваш classpath. Затем используйте HystrixFeign
builder:
public class Example {
public static void main(String[] args) {
MyService api = HystrixFeign.builder().target(MyService.class, "https://myAppProd");
}
}
SLF4JModule позволяет направлять логирование Feign в SLF4J, позволяя вам легко использовать backend логирования вашего выбора (Logback, Log4J и т.д.).
Чтобы использовать SLF4J с Feign, добавьте как модуль SLF4J, так и привязку SLF4J вашего выбора в ваш classpath. Затем настройте Feign для использования Slf4jLogger
:```java
public class Example {
public static void main(String[] args) {
GitHub github = Feign.builder()
.logger(new Slf4jLogger())
.logLevel(Level.FULL)
.target(GitHub.class, "https://api.github.com");
}
}
### Декодеры
`Feign.builder()` позволяет вам указывать дополнительные настройки, такие как способ декодирования ответа.
Если какие-либо методы в вашем интерфейсе возвращают типы, отличные от `Response`, `String`, `byte[]` или `void`, вам потребуется настроить декодер, отличный от стандартного.
Вот как настроить декодирование JSON (используя расширение `feign-gson`):
```java
public class Example {
public static void main(String[] args) {
GitHub github = Feign.builder()
.decoder(new GsonDecoder())
.target(GitHub.class, "https://api.github.com");
}
}
Если вам нужно предварительно обработать ответ перед передачей его декодеру, вы можете использовать метод mapAndDecode
builder.
Пример использования: работа с API, который предоставляет только jsonp, вам может потребоваться извлечь jsonp перед передачей его в декодер JSON вашего выбора:
public class Example {
public static void main(String[] args) {
JsonpApi jsonpApi = Feign.builder()
.mapAndDecode((response, type) -> jsonpUnwrap(response, type), new GsonDecoder())
.target(JsonpApi.class, "https://some-jsonp-api.com");
}
}
Если какие-либо методы в вашем интерфейсе возвращают тип Stream
, вам потребуется настроить StreamDecoder
.
Вот как настроить декодер Stream без делегирования декодера:```java public class Пример { public static void main(String[] args) { GitHub github = Feign.builder() .decoder(StreamDecoder.create((r, t) -> { BufferedReader bufferedReader = new BufferedReader(r.body().asReader(UTF_8)); return bufferedReader.lines().iterator(); })) .target(GitHub.class, "https://api.github.com"); } }
```java
public class Пример {
public static void main(String[] args) {
GitHub github = Feign.builder()
.decoder(StreamDecoder.create((r, t) -> {
BufferedReader bufferedReader = new BufferedReader(r.body().asReader(StandardCharsets.UTF_8));
return bufferedReader.lines().iterator();
}, (r, t) -> "это делегированный декодер"))
.target(GitHub.class, "https://api.github.com");
}
}
Самый простой способ отправить тело запроса на сервер — это определить метод POST
, имеющий параметр типа String
или byte[]
без каких-либо аннотаций. Вероятно, вам потребуется добавить заголовок Content-Type
.
interface LoginClient {
@RequestLine("POST /")
@Headers("Content-Type: application/json")
void login(String content);
}
public class Пример {
public static void main(String[] args) {
client.login("{\"user_name\": \"denominator\", \"password\": \"secret\"}");
}
}
С настройкой энкодера вы можете отправлять типизированное тело запроса. Вот пример использования расширения feign-gson
:
static class Credentials {
final String user_name;
final String password;
Credentials(String user_name, String password) {
this.user_name = user_name;
this.password = password;
}
}
interface LoginClient {
@RequestLine("POST /")
void login(Credentials creds);
}
public class Пример {
public static void main(String[] args) {
LoginClient client = Feign.builder()
.encoder(new GsonEncoder())
.target(LoginClient.class, "https://foo.com");
client.login(new Credentials("denominator", "secret"));
}
}
Аннотация @Body
указывает на шаблон для раскрытия с использованием параметров, аннотированных @Param
. Вероятно, вам потребуется добавить заголовок Content-Type
.```java
interface LoginClient {
@RequestLine("POST /") @Headers("Content-Type: application/xml") @Body("<login "user_name"="{user_name}" "password"="{password}"/>") void xml(@Param("user_name") String user, @Param("password") String password);
@RequestLine("POST /") @Headers("Content-Type: application/json") // фигурные скобки JSON должны быть экранированы! @Body("%7B"user_name": "{user_name}", "password": "{password}"%7D") void json(@Param("user_name") String user, @Param("password") String password); }
public class Example { public static void main(String[] args) { client.xml("denominator", "secret"); // <login "user_name"="denominator" "password"="secret"/> client.json("denominator", "secret"); // {"user_name": "denominator", "password": "secret"} } }
### Заголовки
Feign поддерживает установку заголовков в запросах как часть API или клиента в зависимости от сценария использования.
#### Установка заголовков с помощью API
В случаях, когда определенные интерфейсы или вызовы всегда должны иметь определенные значения заголовков, имеет смысл определять заголовки как часть API.
Статические заголовки могут быть установлены на интерфейсе API или методе с помощью аннотации `@Headers`.
```java
@Headers("Accept: application/json")
interface BaseApi<V> {
@Headers("Content-Type: application/json")
@RequestLine("PUT /api/{key}")
void put(@Param("key") String key, V value);
}
Методы могут указывать динамическое содержимое для статических заголовков с использованием расширения переменных в @Headers
.
public interface Api {
@RequestLine("POST /")
@Headers("X-Ping: {token}")
void post(@Param("token") String token);
}
```В случаях, когда ключи и значения заголовков динамические, а диапазон возможных ключей неизвестен заранее и может варьироваться между различными вызовами методов в одном и том же API/клиенте (например, пользовательские заголовки метаданных, такие как "x-amz-meta-\*" или "x-goog-meta-\*"), параметр типа `Map` может быть аннотирован `HeaderMap`, чтобы создать запрос, использующий содержимое карты как параметры заголовков.```java
public interface Api {
@RequestLine("POST /")
void post(@HeaderMap Map<String, Object> headerMap);
}
Эти подходы определяют заголовочные записи как часть API и не требуют никаких кастомизаций при построении клиента Feign.
Чтобы кастомизировать заголовки для каждого метода запроса на целевом объекте, можно использовать RequestInterceptor
. RequestInterceptors
могут быть использованы в нескольких экземплярах целевых объектов и должны быть потокобезопасными. RequestInterceptors
применяются ко всем методам запроса на целевом объекте.
Если вам требуется кастомизация для каждого метода, необходимо использовать кастомный целевой объект, так как RequestInterceptor
не имеет доступа к метаданным текущего метода.
Для примера установки заголовков с помощью RequestInterceptor
см. раздел Request Interceptors
.
Заголовки могут быть установлены как часть кастомного Target
.
static class DynamicAuthTokenTarget<T> implements Target<T> {
public DynamicAuthTokenTarget(Class<T> clazz,
UrlAndTokenProvider provider,
ThreadLocal<String> requestIdProvider);
@Override
public Request apply(RequestTemplate input) {
TokenIdAndPublicURL urlAndToken = provider.get();
if (input.url().indexOf("http") != 0) {
input.insert(0, urlAndToken.publicURL);
}
input.header("X-Auth-Token", urlAndToken.tokenId);
input.header("X-Request-ID", requestIdProvider.get());
return input.request();
}
}
public class Example {
public static void main(String[] args) {
Bank bank = Feign.builder()
.target(new DynamicAuthTokenTarget(Bank.class, provider, requestIdProvider));
}
}
```Эти подходы зависят от использования кастомного `RequestInterceptor` или `Target`, который устанавливается на клиенте Feign при его построении и может использоваться как способ установки заголовков для всех вызовов API на основе каждого клиента. Это может быть полезно для таких вещей, как установка токена аутентификации в заголовке всех вызовов API на основе каждого клиента. Методы выполняются при вызове API на потоке, который вызывает API, что позволяет динамически устанавливать заголовки в момент вызова и в контекстно-зависимом режиме — например, можно использовать потокобезопасное хранилище для установки различных значений заголовков в зависимости от вызывающего потока, что может быть полезно для таких вещей, как установка уникальных идентификаторов трассировки для каждого потока.
#### Установка нулевого заголовка Content-Length
Для указания заголовка `Content-Length: 0` при отправке запроса с пустым телом, системная переменная `sun.net.http.allowRestrictedHeaders` должна быть установлена в значение `true`.
Если этого не сделать, заголовок `Content-Length` не будет добавлен.
### Расширенное использование
#### Основные API
В большинстве случаев API для сервиса следуют одному и тому же соглашению. Feign поддерживает это через интерфейсы с одиночным наследованием.
Рассмотрим пример:
```java
interface BaseAPI {
@RequestLine("GET /health")
String health();
``` @RequestLine("GET /all")
List<Entity> all();
}
Вы можете определить и указать конкретный API, наследуя базовые методы.
interface CustomAPI extends BaseAPI {
@RequestLine("GET /custom")
String custom();
}
В большинстве случаев представления ресурсов также являются последовательными. По этой причине поддерживаются параметры типа на базовом интерфейсе API.
@Headers("Accept: application/json")
interface BaseApi<V> {
@RequestLine("GET /api/{key}")
V get(@Param("key") String key);
@RequestLine("GET /api")
List<V> list();
@Headers("Content-Type: application/json")
@RequestLine("PUT /api/{key}")
void put(@Param("key") String key, V value);
}
interface FooApi extends BaseApi<Foo> { }
interface BarApi extends BaseApi<Bar> { }
Вы можете логировать HTTP-сообщения, отправляемые и получаемые от целевого узла, настроив Logger
. Вот самый простой способ сделать это:
public class Example {
public static void main(String[] args) {
GitHub github = Feign.builder()
.decoder(new GsonDecoder())
.logger(new Logger.JavaLogger("GitHub.Logger").appendToFile("logs/http.log"))
.logLevel(Logger.Level.FULL)
.target(GitHub.class, "https://api.github.com");
}
}
Примечание о JavaLogger: Избегайте использования конструктора по умолчанию
JavaLogger()
, он помечен как устаревший и будет удален в ближайшее время.
SLF4JLogger (см. выше) также может быть полезен.
Чтобы фильтровать чувствительную информацию, такую как авторизация или токены, переопределите методы shouldLogRequestHeader
или shouldLogResponseHeader
.#### Запросные интерцепторы
Когда вам нужно изменить все запросы, независимо от их целевого узла, вам следует настроить RequestInterceptor
.
Например, если вы выступаете в роли промежуточного узла, вы можете хотеть передать заголовок X-Forwarded-For
.```java
static class ForwardedForInterceptor implements RequestInterceptor {
@Override public void apply(RequestTemplate template) {
template.header("X-Forwarded-For", "origin.host.com");
}
}``````java
public class Example {
public static void main(String[] args) {
Bank bank = Feign.builder()
.decoder(accountDecoder)
.requestInterceptor(new ForwardedForInterceptor())
.target(Bank.class, "https://api.examplebank.com");
}
}
Еще один распространенный пример интерцептора — это аутентификация, например, использование встроенного `BasicAuthRequestInterceptor`.
```java
public class Example {
public static void main(String[] args) {
Bank bank = Feign.builder()
.decoder(accountDecoder)
.requestInterceptor(new BasicAuthRequestInterceptor(username, password))
.target(Bank.class, "https://api.examplebank.com");
}
}
Параметры, аннотированные с Param
, расширяются на основе их toString
. Указав пользовательский Param.Expander
, пользователи могут контролировать это поведение, например, форматировать даты.
public interface Api {
@RequestLine("GET /?since={date}") Result list(@Param(value = "date", expander = DateToMillis.class) Date date);
}
Параметр типа Map
может быть аннотирован с QueryMap
, чтобы создать запрос, использующий содержимое карты как параметры запроса.
public interface Api {
@RequestLine("GET /find")
V find(@QueryMap Map<String, Object> queryMap);
}
Это также может использоваться для генерации параметров запроса из объекта POJO с помощью QueryMapEncoder
.
public interface Api {
@RequestLine("GET /find")
V find(@QueryMap CustomPojo customPojo);
}
```Когда используется таким образом, без указания пользовательского `QueryMapEncoder`, карта запросов будет генерироваться с использованием имен членов переменных как имен параметров запроса. Вы можете аннотировать конкретное поле `CustomPojo` аннотацией `@Param`, чтобы указать другое имя для параметра запроса. Следующий объект POJO сгенерирует параметры запроса вида "/find?name={name}&number={number}®ion_id={regionId}" (порядок включенных параметров запроса не гарантируется, и как обычно, если любое значение равно null, оно будет опущено).
```java
public class CustomPojo {
private final String name;
private final int number;
@Param("region_id")
private final String regionId;
}
``` public CustomPojo(String name, int number, String regionId) {
this.name = name;
this.number = number;
this.regionId = regionId;
}
}
Чтобы настроить QueryMapEncoder
:
public class Example {
public static void main(String[] args) {
MyApi myApi = Feign.builder()
.queryMapEncoder(new MyCustomQueryMapEncoder())
.target(MyApi.class, "https://api.hostname.com");
}
}
Когда объекты аннотируются с помощью @QueryMap
, стандартный кодировщик использует рефлексию для анализа предоставленных объектов и их полей для расширения значений объектов в строку запроса. Если вы предпочитаете, чтобы строка запроса создавалась с использованием методов доступа и изменения, как определено в Java Beans API, пожалуйста, используйте BeanQueryMapEncoder
.
public class Example {
public static void main(String[] args) {
MyApi myApi = Feign.builder()
.queryMapEncoder(new BeanQueryMapEncoder())
.target(MyApi.class, "https://api.hostname.com");
}
}
Если вам требуется более тонкое управление обработкой неожиданных ответов, экземпляры Feign могут зарегистрировать пользовательский ErrorDecoder
через конструктор.
public class Example {
public static void main(String[] args) {
MyApi myApi = Feign.builder()
.errorDecoder(new MyErrorDecoder())
.target(MyApi.class, "https://api.hostname.com");
}
}
```Все ответы, которые приводят к HTTP-статусу, не входящему в диапазон 2xx, будут запускать метод `decode` класса `ErrorDecoder`, что позволит вам обрабатывать ответ, обёртывать ошибку в пользовательское исключение или выполнять любую дополнительную обработку. Если вы хотите повторить запрос, выбросьте исключение `RetryableException`. Это вызовет зарегистрированный `Retryer`.### Повторная отправка
По умолчанию Feign автоматически повторяет `IOException`-и, независимо от HTTP-метода, рассматривая их как временные сетевые исключения, а также любые `RetryableException`, выброшенные из `ErrorDecoder`. Чтобы настроить это поведение, зарегистрируйте пользовательский экземпляр `Retryer` через конструктор.
В следующем примере показано, как обновлять токен и повторять запрос с использованием `ErrorDecoder` и `Retryer` при получении ответа с кодом 401.
```java
public class Пример {
public static void main(String[] args) {
var github = Feign.builder()
.decoder(new GsonDecoder())
.retryer(new МойПовторитель(100, 3))
.errorDecoder(new МойОбработчикОшибок())
.target(ГитХаб.class, "https://api.github.com");
var вкладчики = github.вкладчики("foo", "bar", "invalid_token");
for (var вкладчик : вкладчики) {
System.out.println(вкладчик.login + " " + вкладчик.вклад);
}
}
static class МойОбработчикОшибок implements ErrorDecoder {
private final ErrorDecoder стандартныйОбработчикОшибок = new Default();
@Override
public Exception decode(String methodKey, Response response) {
// обертка 401 в RetryableException для повторной попытки
if (response.status() == 401) {
return new RetryableException(response.status(), response.reason(), response.request().httpMethod(), null, response.request());
}
return стандартныйОбработчикОшибок.decode(methodKey, response);
}
}
static class МойПовторитель implements Retryer {
private final long период;
private final int максимальноеКоличествоПопыток;
private int попытка = 1;
public МойПовторитель(long период, int максимальноеКоличествоПопыток) {
this.период = период;
this.максимальноеКоличествоПопыток = максимальноеКоличествоПопыток;
}
@Override
public Retryer clone() {
return new МойПовторитель(период, максимальноеКоличествоПопыток);
}
@Override
public void attempt(Retryer.Context context) {
if (попытка >= максимальноеКоличествоПопыток) {
throw new RetryException("Превышено максимальное количество попыток");
}
попытка++;
try {
Thread.sleep(период);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
throw new RetryException("Повторная попытка прервана", e);
}
}
}
}
``````markdown
```java
public class МойПовторитель {
public МойПовторитель(long период, int максимальное_количество_попыток) {
this.период = период;
this.максимальное_количество_попыток = максимальное_количество_попыток;
}
@Override
public void продолжитьИлиПередать(RetryableException e) {
if (++попытка > максимальное_количество_попыток) {
throw e;
}
if (e.status() == 401) {
// удалить Authorization, чтобы Feign не добавлял новый заголовок Authorization
// поскольку GitHub отвечает 400 bad request
e.request().requestTemplate().removeHeader("Authorization");
e.request().requestTemplate().header("Authorization", "Bearer " + получитьНовыйТокен());
try {
Thread.sleep(период);
} catch (InterruptedException ex) {
throw e;
}
} else {
throw e;
}
}
// Получение нового токена из внешнего API
// В этом примере мы можем просто вернуть фиксированный токен, чтобы продемонстрировать работу Retryer
private String получитьНовыйТокен() {
return "newToken";
}
}
}
``````java
@Override
public Retryer клонировать() {
return new МойПовторитель(период, максимальное_количество_попыток);
}
}
Retryer
ы отвечают за определение, следует ли повторить запрос, возвращая либо true
, либо false
из метода continueOrPropagate(RetryableException e)
. Экземпляр Retryer
создается для каждого выполнения Client
, что позволяет поддерживать состояние между каждым запросом, если это необходимо.
```Если повторная попытка определяется как неудачная, последнее исключение RetryException
будет выброшено. Чтобы выбросить первоначальную причину, приведшую к неудачной повторной попытке, настройте свой Feign клиент с помощью опции `exceptionPropagationPolicy()`.
Если вам нужно рассматривать то, что обычно было бы ошибкой, как успешное выполнение и возвращать результат вместо выбрасывания исключения, вы можете использовать ResponseInterceptor
.
В качестве примера Feign включает простой RedirectionInterceptor
, который можно использовать для извлечения заголовка Location
из ответов перенаправления.
public interface Api {
// возвращает ответ Yöntem 302
@RequestLine("GET /location")
String location();
}
public class MyApp {
public static void main(String[] args) {
// Настройте HTTP-клиент для игнорирования перенаправлений
Api api = Feign.builder()
.options(new Options(10, TimeUnit.SECONDS, 60, TimeUnit.SECONDS, false))
.responseInterceptor(new RedirectionInterceptor())
.target(Api.class, "https://redirect.example.com");
}
}
По умолчанию, Feign не собирает никаких метрик.
Однако, возможно добавить возможности сбора метрик для любого клиента Feign.
Возможности метрик предоставляют первоклассный API метрик, который пользователи могут использовать для получения информации о жизненном цикле запросов/ответов.
Примечание о модулях метрик:
Все модули интеграции метрик построены в отдельных модулях и не доступны в модуле
feign-core
. Вам потребуется добавить их в зависимости.#### Dropwizard Metrics 4
public class MyApp {
public static void main(String[] args) {
GitHub github = Feign.builder()
.addCapability(new Metrics4Capability())
.target(GitHub.class, "https://api.github.com");
github.contributors("OpenFeign", "feign");
// метрики будут доступны с этого момента
}
}
public class MyApp {
public static void main(String[] args) {
GitHub github = Feign.builder()
.addCapability(new Metrics5Capability())
.target(GitHub.class, "https://api.github.com");
github.contributors("OpenFeign", "feign");
// метрики будут доступны с этого момента
}
}
public class MyApp {
public static void main(String[] args) {
GitHub github = Feign.builder()
.addCapability(new MicrometerCapability())
.target(GitHub.class, "https://api.github.com");
github.contributors("OpenFeign", "feign");
// метрики будут доступны с этого момента
}
}
Интерфейсы, на которые ориентирован Feign, могут содержать статические методы или методы по умолчанию (если используется Java 8+). Это позволяет клиентам Feign содержать логику, которая не явно определена в подлежащем API. Например, статические методы делают легким указание общих конфигураций сборки клиентов; методы по умолчанию могут использоваться для составления запросов или определения параметров по умолчанию.
interface GitHub {
@RequestLine("GET /repos/{owner}/{repo}/contributors")
List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo);
@RequestLine("GET /users/{username}/repos?sort={sort}")
List<Repo> repos(@Param("username") String owner, @Param("sort") String sort);
}
``````markdown
### Асинхронное выполнение через `CompletableFuture`
Feign 10.8 вводит новый построитель `AsyncFeign`, который позволяет методам возвращать экземпляры `CompletableFuture`.
```java
interface GitHub {
@RequestLine("GET /repos/{owner}/{repo}/contributors")
CompletableFuture<List<Contributor>> contributors(@Param("owner") String owner, @Param("repo") String repo);
}
public class MyApp {
public static void main(String... args) {
GitHub github = AsyncFeign.builder()
.decoder(new GsonDecoder())
.target(GitHub.class, "https://api.github.com");
// Получение и вывод списка вкладчиков этого библиотеки.
CompletableFuture<List<Contributor>> contributors = github.contributors("OpenFeign", "feign");
for (Contributor contributor : contributors.get(1, TimeUnit.SECONDS)) {
System.out.println(contributor.login + " (" + contributor.contributions + ")");
}
}
}
Поддержание всех библиотек Feign на одной версии необходимо для избежания несовместимых бинарников. При использовании внешних зависимостей может быть сложно убедиться, что только одна версия присутствует.
С этой целью, сборка Feign генерирует модуль под названием feign-bom
, который блокирует версии для всех модулей feign-*
.
[Здесь](https://repo1.maven.org/maven2/io/github/openfeign/feign-bom/11.9/feign-bom-11.9.pom) можно посмотреть пример того, как выглядит файл BOM Feign.
#### Использование
```xml
<project>
...
<dependencyManagement>
<dependencies>
<dependency>
<groupId>io.github.openfeign</groupId>
<artifactId>feign-bom</artifactId>
<version>??feign.version??</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>
</project>
Этот модуль добавляет поддержку кодирования форм application/x-www-form-urlencoded и multipart/form-data.
Добавьте зависимость в ваш проект:
Maven:
<dependencies>
...
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form</artifactId>
<version>4.0.0</version>
</dependency>
...
</dependencies>
Gradle:
compile 'io.github.openfeign.form:feign-form:4.0.0'
Расширение feign-form
зависит от OpenFeign
и его конкретных версий:- все выпуски feign-form
до 3.5.0 работают с OpenFeign
9.* версиями;
feign-form
3.5.0, модуль работает с OpenFeign
10.1.0 версиями и выше.ВАЖНО: обратная совместимость отсутствует, и нет никаких гарантий, что версии
feign-form
после 3.5.0 будут работать сOpenFeign
до 10.*.OpenFeign
был переработан в 10-й версии, поэтому лучший подход — использовать самую свежую версиюOpenFeign
иfeign-form
.
Примечания:
spring-cloud-openfeign использует OpenFeign
9.* до v2.0.3.RELEASE и 10.* после. В любом случае, зависимость уже имеет подходящую версию feign-form
, см. dependency pom, поэтому вам не нужно указывать её отдельно;
spring-cloud-starter-feign
— это устаревшая зависимость, и она всегда использует версии OpenFeign
9.*.
Добавьте FormEncoder
в ваш Feign.Builder
следующим образом:
SomeApi github = Feign.builder()
.encoder(new FormEncoder())
.target(SomeApi.class, "http://api.some.org");
Кроме того, вы можете декорировать существующий encoder, например, JsonEncoder, следующим образом:
SomeApi github = Feign.builder()
.encoder(new FormEncoder(new JacksonEncoder()))
.target(SomeApi.class, "http://api.some.org");
И используйте их вместе:
interface SomeApi {
@RequestLine("POST /json")
@Headers("Content-Type: application/json")
void json(Dto dto);
}
``` @RequestLine("POST /form")
@Headers("Content-Type: application/x-www-form-urlencoded")
void from (@Param("field1") String field1, @Param("field2") String[] values);
}
```Вы можете указать два типа формата кодирования с помощью заголовка `Content-Type`.### application/x-www-form-urlencoded
```java
interface SomeApi {
@RequestLine("POST /authorization")
@Headers("Content-Type: application/x-www-form-urlencoded")
void authorization (@Param("email") String email, @Param("password") String password);
// Группировка всех параметров в POJO
@RequestLine("POST /user")
@Headers("Content-Type: application/x-www-form-urlencoded")
void addUser (User user);
class User {
Integer id;
String name;
}
}
interface SomeApi {
// Параметр файла
@RequestLine("POST /send_photo")
@Headers("Content-Type: multipart/form-data")
void sendPhoto (@Param("is_public") Boolean isPublic, @Param("photo") File photo);
// Параметр byte[]
@RequestLine("POST /send_photo")
@Headers("Content-Type: multipart/form-data")
void sendPhoto (@Param("is_public") Boolean isPublic, @Param("photo") byte[] photo);
// Параметр FormData
@RequestLine("POST /send_photo")
@Headers("Content-Type: multipart/form-data")
void sendPhoto (@Param("is_public") Boolean isPublic, @Param("photo") FormData photo);
// Группировка всех параметров в POJO
@RequestLine("POST /send_photo")
@Headers("Content-Type: multipart/form-data")
void sendPhoto (MyPojo pojo);
class MyPojo {
@FormProperty("is_public")
Boolean isPublic;
File photo;
}
}
В приведенном выше примере метод sendPhoto
использует параметр photo
с использованием трех различных поддерживаемых типов.
File
будет использовать расширение файла для определения Content-Type
;byte[]
будет использовать application/octet-stream
как Content-Type
;FormData
будет использовать Content-Type
и fileName
объекта FormData
;FormData
— это пользовательский объект, который обертывает byte[]
и определяет Content-Type
и fileName
следующим образом:```java
FormData formData = new FormData("image/png", "filename.png", myDataAsByteArray);
someApi.sendPhoto(true, formData);
### Поддержка Spring MultipartFile и Spring Cloud Netflix @FeignClient
Вы также можете использовать Form Encoder с Spring `MultipartFile` и `@FeignClient`.
Добавьте зависимости в файл `pom.xml` вашего проекта:
```xml
<dependencies>
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form</artifactId>
<version>4.0.0</version>
</dependency>
<dependency>
<groupId>io.github.openfeign.form</groupId>
<artifactId>feign-form-spring</artifactId>
<version>4.0.0</version>
</dependency>
</dependencies>
@FeignClient(
name = "file-upload-service",
configuration = FileUploadServiceClient.MultipartSupportConfig.class
)
public interface FileUploadServiceClient extends IFileUploadServiceClient {
public class MultipartSupportConfig {
@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
@Bean
public Encoder feignFormEncoder () {
return new SpringFormEncoder(new SpringEncoder(messageConverters));
}
}
}
Или, если вам не требуется стандартный кодировщик Spring:
@FeignClient(
name = "file-upload-service",
configuration = FileUploadServiceClient.MultipartSupportConfig.class
)
public interface FileUploadServiceClient extends IFileUploadServiceClient {
public class MultipartSupportConfig {
@Bean
public Encoder feignFormEncoder () {
return new SpringFormEncoder();
}
}
}
Благодарим tf-haotri-pham за его функцию, которая использует библиотеку Apache commons-fileupload для обработки разбора multipart-ответа. Части данных тела сохраняются в виде массивов байтов в памяти.
Чтобы использовать эту функцию, включите SpringManyMultipartFilesReader
в список конвертеров сообщений для декодера и сделайте так, чтобы клиент Feign возвращал массив MultipartFile
:
@FeignClient(
name = "${feign.name}",
url = "${feign.url}",
configuration = DownloadClient.ClientConfiguration.class
)
public interface DownloadClient {
@RequestMapping("/multipart/download/{fileId}")
MultipartFile[] download(@PathVariable("fileId") String fileId);
class ClientConfiguration {
@Autowired
private ObjectFactory<HttpMessageConverters> messageConverters;
@Bean
public Decoder feignDecoder () {
List<HttpMessageConverter<?>> springConverters =
messageConverters.getObject().getConverters();
List<HttpMessageConverter<?>> decoderConverters =
new ArrayList<HttpMessageConverter<?>>(springConverters.size() + 1);
decoderConverters.addAll(springConverters);
decoderConverters.add(new SpringManyMultipartFilesReader(4096));
HttpMessageConverters httpMessageConverters = new HttpMessageConverters(decoderConverters);
return new SpringDecoder(new ObjectFactory<HttpMessageConverters>() {
@Override
public HttpMessageConverters getObject() {
return httpMessageConverters;
}
});
}
}
}
```
Вы можете оставить комментарий после Вход в систему
Неприемлемый контент может быть отображен здесь и не будет показан на странице. Вы можете проверить и изменить его с помощью соответствующей функции редактирования.
Если вы подтверждаете, что содержание не содержит непристойной лексики/перенаправления на рекламу/насилия/вульгарной порнографии/нарушений/пиратства/ложного/незначительного или незаконного контента, связанного с национальными законами и предписаниями, вы можете нажать «Отправить» для подачи апелляции, и мы обработаем ее как можно скорее.
Комментарии ( 0 )