Feign — это средство связывания клиента Java с HTTP, созданное на основе Retrofit, JAXRS-2.0 и WebSocket. Первой целью Фейна было упрощение единой привязки Denominator к HTTP API независимо от ReSTfulness.
Feign использует такие инструменты, как Jersey и CXF, для написания клиентов Java для служб ReST или SOAP. Более того, Feign позволяет вам писать собственный код поверх http-библиотек, таких как Apache HC. Feign подключает ваш код к API-интерфейсам HTTP с минимальными накладными расходами и кодом с помощью настраиваемых декодеров и обработки ошибок, которые можно записать в любой текстовый API-интерфейс HTTP.
Feign работает путем обработки аннотаций в шаблонный запрос. Аргументы применяются к этим шаблонам непосредственно перед выводом. Хотя Feign ограничен поддержкой текстовых API, он значительно упрощает системные аспекты, такие как воспроизведение запросов. Более того, зная это, Feign упрощает модульное тестирование ваших конверсий.
Feign 10.x и более поздние версии созданы на Java 8 и должны работать на Java 9, 10 и 11. Если вам нужна совместимость с JDK 6, используйте Feign 9.x.
Это карта с текущими ключевыми функциями, предоставленными feign:
Упрощение API- клиентов
Logger
Logger
, чтобы он был ближе к таким платформам, как SLF4J, обеспечивающим общую ментальную модель ведения журналов в Feign. Эта модель будет использоваться самой Feign повсюду и даст более четкое представление о том, как будет использоваться Logger
.Retry
рефакторинг APIRetry
для поддержки условий, заданных пользователем, и лучшего контроля над политиками отсрочки. Это может привести к несовместимым с обратной совместимостью критическим изменениям. 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" );
// Fetch and print a list of the contributors to this library.
List < Contributor > contributors = github . contributors ( "OpenFeign" , "feign" );
for ( Contributor contributor : contributors ) {
System . out . println ( contributor . login + " (" + contributor . contributions + ")" );
}
}
}
Аннотации Feign определяют Contract
между интерфейсом и тем, как должен работать базовый клиент. Контракт Feign по умолчанию определяет следующие аннотации:
Аннотация | Цель интерфейса | Использование |
---|---|---|
@RequestLine | Метод | Определяет HttpMethod и UriTemplate для запроса. Expressions , значения, заключенные в фигурные скобки {expression} разрешаются с использованием соответствующих аннотированных параметров @Param . |
@Param | Параметр | Определяет переменную шаблона, значение которой будет использоваться для разрешения соответствующего шаблона Expression по имени, указанному в качестве значения аннотации. Если значение отсутствует, оно попытается получить имя из имени параметра метода байт-кода (если код был скомпилирован с флагом -parameters ). |
@Headers | Метод, Тип | Определяет HeaderTemplate ; вариант UriTemplate . который использует аннотированные значения @Param для разрешения соответствующих Expressions . При использовании в Type шаблон будет применяться к каждому запросу. При использовании в Method шаблон будет применяться только к аннотированному методу. |
@QueryMap | Параметр | Определяет Map пар имя-значение (POJO) для расширения в строку запроса. |
@HeaderMap | Параметр | Определяет Map пар имя-значение для расширения в Http Headers |
@Body | Метод | Определяет Template , аналогичный UriTemplate и HeaderTemplate , который использует аннотированные значения @Param для разрешения соответствующих Expressions . |
Переопределение строки запроса
Если необходимо направить запрос на другой хост, отличный от того, который был указан при создании клиента 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 );
Expressions
Feign представляют собой простые строковые выражения (уровень 1), как определено шаблоном URI — RFC 6570. Expressions
расширяются с использованием соответствующих параметров аннотированного метода 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" );
/* The owner and repository parameters will be used to expand the owner and repo expressions
* defined in the RequestLine.
*
* the resulting uri will be https://api.github.com/repos/OpenFeign/feign/contributors
*/
github . contributors ( "OpenFeign" , "feign" );
}
}
Выражения должны быть заключены в фигурные скобки {}
и могут содержать шаблоны регулярных выражений, разделенные двоеточием :
для ограничения разрешенных значений. owner
примера должен быть в алфавитном порядке. {owner:[a-zA-Z]*}
Шаблоны RequestLine
и QueryMap
соответствуют шаблону URI — спецификации RFC 6570 для шаблонов уровня 1, которая определяет следующее:
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 6570, раздел 3.2.7.
Неопределенные выражения — это выражения, в которых значением выражения является явное значение null
или значение не указано. В соответствии с шаблоном URI (RFC 6570) для выражения можно указать пустое значение. Когда Feign разрешает выражение, он сначала определяет, определено ли значение; если да, то параметр запроса останется. Если выражение не определено, параметр запроса удаляется. Полную разбивку смотрите ниже.
Пустая строка
public void test () {
Map < String , Object > parameters = new LinkedHashMap <>();
parameters . put ( "param" , "" );
this . demoClient . test ( parameters );
}
Результат
http://localhost:8080/test?param=
Отсутствующий
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
@RequestLine
значениеfalse
.
А что насчет плюса?
+
Согласно спецификации URI, знак
+
разрешен как в сегментах пути, так и в сегментах запроса URI, однако обработка символа в запросе может быть противоречивой. В некоторых устаревших системах+
эквивалентен пробелу. Фейн использует подход современных систем, где символ+
не должен обозначать пробел и явно кодируется как%2B
при обнаружении в строке запроса.Если вы хотите использовать
+
в качестве пробела, используйте литералсимвол или закодируйте значение напрямую как
%20
Аннотация @Param
имеет дополнительный expander
свойств, позволяющий полностью контролировать раскрытие отдельного параметра. Свойство expander
должно ссылаться на класс, реализующий интерфейс Expander
:
public interface Expander {
String expand ( Object value );
}
Результат этого метода соответствует тем же правилам, изложенным выше. Если результат равен null
или является пустой строкой, значение опускается. Если значение не закодировано в формате pct, оно будет. Дополнительные примеры см. в разделе Пользовательское расширение @Param.
Шаблоны Headers
и HeaderMap
следуют тем же правилам, что и расширение параметров запроса, со следующими изменениями:
Примеры см. в разделе «Заголовки».
Примечание о параметрах
@Param
и их именах :Все выражения с одинаковым именем, независимо от их позиции в
@RequestLine
,@QueryMap
,@BodyTemplate
или@Headers
, будут преобразованы в одно и то же значение. В следующем примере значениеcontentType
будет использоваться для разрешения выражения заголовка и пути:public interface ContentService { @ RequestLine ( "GET /api/documents/{contentType}" ) @ Headers ( "Accept: {contentType}" ) String getDocumentByType ( @ Param ( "contentType" ) String type ); }Помните об этом при разработке интерфейсов.
Шаблоны Body
следуют тем же правилам, что и расширение параметров запроса, со следующими изменениями:
Encoder
до размещения в теле запроса.Content-Type
. Примеры см. в разделе «Шаблоны тела». Feign имеет несколько аспектов, которые можно настроить.
В простых случаях вы можете использовать Feign.builder()
для создания интерфейса API с вашими пользовательскими компонентами.
Для настройки запроса вы можете использовать options(Request.Options options)
в target()
чтобы установить ConnectTimeout, ConnectTimeoutUnit, readTimeout, readTimeoutUnit, FollowRedirects.
Например:
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 > {
/* implementation of a Target */
}
}
Feign включает примеры клиентов GitHub и Wikipedia. Проект знаменателя также можно использовать для Feign на практике. В частности, посмотрите на пример демона.
Feign намерен хорошо работать с другими инструментами с открытым исходным кодом. Модули можно интегрировать с вашими любимыми проектами!
Gson включает в себя кодировщик и декодер, которые можно использовать с API JSON.
Добавьте 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" );
}
}
Джексон включает в себя кодировщик и декодер, которые можно использовать с API JSON.
Добавьте 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 включает в себя кодировщик и декодер, которые можно использовать с API JSON. Добавьте MoshiEncoder
и/или MoshiDecoder
в ваш Feign.Builder
следующим образом:
GitHub github = Feign . builder ()
. encoder ( new MoshiEncoder ())
. decoder ( new MoshiDecoder ())
. target ( GitHub . class , "https://api.github.com" );
SaxDecoder позволяет декодировать XML способом, совместимым с обычной JVM, а также со средой Android.
Вот пример настройки анализа ответов Sax:
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 возвращаются в ответ с кодами ошибок http (4xx, 5xx,...)
fastjson2 включает в себя кодировщик и декодер, которые можно использовать с API JSON.
Добавьте Fastjson2Encoder
и/или Fastjson2Decoder
в ваш Feign.Builder
следующим образом:
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 в свой путь к классам. Затем настройте 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" );
}
}
RibbonClient переопределяет разрешение URL-адресов клиента Feign, добавляя возможности интеллектуальной маршрутизации и устойчивости, предоставляемые Ribbon.
Для интеграции необходимо передать имя ленточного клиента в качестве хостовой части URL-адреса, например myAppProd
.
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 Java11, который реализует HTTP/2.
Чтобы использовать новый клиент HTTP/2 с 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 в свой путь к классам. Затем используйте конструктор HystrixFeign
:
public class Example {
public static void main ( String [] args ) {
MyService api = HystrixFeign . builder (). target ( MyService . class , "https://myAppProd" );
}
}
SLF4JModule позволяет направлять журналирование Feign на SLF4J, что позволяет вам легко использовать серверную часть журнала по вашему выбору (Logback, Log4J и т. д.).
Чтобы использовать SLF4J с Feign, добавьте в свой путь к классам модуль SLF4J и привязку SLF4J по вашему выбору. Затем настройте Feign для использования Slf4jLogger:
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
, вам необходимо настроить Decoder
не по умолчанию.
Вот как настроить декодирование JSON (с использованием расширения feign-gson
):
public class Example {
public static void main ( String [] args ) {
GitHub github = Feign . builder ()
. decoder ( new GsonDecoder ())
. target ( GitHub . class , "https://api.github.com" );
}
}
Если вам необходимо предварительно обработать ответ перед его передачей в декодер, вы можете использовать метод компоновщика mapAndDecode
. Пример варианта использования касается API, который обслуживает только jsonp. Возможно, вам придется развернуть jsonp перед отправкой его в декодер Json по вашему выбору:
public class Example {
public static void main ( String [] args ) {
JsonpApi jsonpApi = Feign . builder ()
. mapAndDecode (( response , type ) -> jsopUnwrap ( response , type ), new GsonDecoder ())
. target ( JsonpApi . class , "https://some-jsonp-api.com" );
}
}
Если какие-либо методы вашего интерфейса возвращают тип Stream
, вам необходимо настроить StreamDecoder
.
Вот как настроить декодер потока без декодера делегата:
public class Example {
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" );
}
}
Вот как настроить декодер Stream с декодером делегата:
public class Example {
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 ();
}, ( r , t ) -> "this is delegate decoder" ))
. 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 Example {
public static void main ( String [] args ) {
client . login ( "{ " user_name " : " denominator " , " password " : " secret " }" );
}
}
Настроив Encoder
, вы можете отправить типизированное тело запроса. Вот пример использования расширения 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 Example {
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
.
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 curly braces must be escaped!
@ 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 с помощью аннотации @Headers
.
@ 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- мета-*" или "x-goog-meta-*"), параметр Map можно пометить с помощью HeaderMap
для создания запроса, который использует содержимое карты в качестве параметров заголовка.
public interface Api {
@ RequestLine ( "POST /" )
void post ( @ HeaderMap Map < String , Object > headerMap );
}
Эти подходы определяют записи заголовка как часть API и не требуют каких-либо настроек при создании клиента Feign.
Чтобы настроить заголовки для каждого метода запроса в Target, можно использовать RequestInterceptor. RequestInterceptors могут использоваться совместно между экземплярами Target и, как ожидается, будут потокобезопасными. RequestInterceptors применяются ко всем методам запроса в Target.
Если вам нужна настройка каждого метода, требуется настраиваемая цель, поскольку 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: 0
при выполнении запроса с пустым телом, системное свойство sun.net.http.allowRestrictedHeaders
должно быть установлено в true
В противном случае заголовок Content-Length
не будет добавлен.
Во многих случаях API для службы следуют тем же соглашениям. Feign поддерживает этот шаблон через интерфейсы с одним наследованием.
Рассмотрим пример:
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
.
static class ForwardedForInterceptor implements RequestInterceptor {
@ Override public void apply ( RequestTemplate template ) {
template . header ( "X-Forwarded-For" , "origin.host.com" );
}
}
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
.
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}" (порядок включенных параметров запроса не гарантируется, и, как обычно, если какое-либо значение равно нулю, оно будет оставлено).
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 кодировщик по умолчанию использует отражение для проверки предоставленных полей объектов, чтобы расширить значения объектов в строку запроса. Если вы предпочитаете, чтобы строка запроса была построена с использованием методов получения и установки, как определено в API Java Beans, используйте 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.
public class Example {
public static void main ( String [] args ) {
var github = Feign . builder ()
. decoder ( new GsonDecoder ())
. retryer ( new MyRetryer ( 100 , 3 ))
. errorDecoder ( new MyErrorDecoder ())
. target ( Github . class , "https://api.github.com" );
var contributors = github . contributors ( "foo" , "bar" , "invalid_token" );
for ( var contributor : contributors ) {
System . out . println ( contributor . login + " " + contributor . contributions );
}
}
static class MyErrorDecoder implements ErrorDecoder {
private final ErrorDecoder defaultErrorDecoder = new Default ();
@ Override
public Exception decode ( String methodKey , Response response ) {
// wrapper 401 to RetryableException in order to retry
if ( response . status () == 401 ) {
return new RetryableException ( response . status (), response . reason (), response . request (). httpMethod (), null , response . request ());
}
return defaultErrorDecoder . decode ( methodKey , response );
}
}
static class MyRetryer implements Retryer {
private final long period ;
private final int maxAttempts ;
private int attempt = 1 ;
public MyRetryer ( long period , int maxAttempts ) {
this . period = period ;
this . maxAttempts = maxAttempts ;
}
@ Override
public void continueOrPropagate ( RetryableException e ) {
if (++ attempt > maxAttempts ) {
throw e ;
}
if ( e . status () == 401 ) {
// remove Authorization first, otherwise Feign will add a new Authorization header
// cause github responses a 400 bad request
e . request (). requestTemplate (). removeHeader ( "Authorization" );
e . request (). requestTemplate (). header ( "Authorization" , "Bearer " + getNewToken ());
try {
Thread . sleep ( period );
} catch ( InterruptedException ex ) {
throw e ;
}
} else {
throw e ;
}
}
// Access an external api to obtain new token
// In this example, we can simply return a fixed token to demonstrate how Retryer works
private String getNewToken () {
return "newToken" ;
}
@ Override
public Retryer clone () {
return new MyRetryer ( period , maxAttempts );
}
}
Retryer
отвечает за определение необходимости повторной попытки, возвращая true
или false
из метода continueOrPropagate(RetryableException e);
Экземпляр Retryer
будет создан для каждого выполнения Client
, что позволит вам при желании поддерживать состояние между каждым запросом.
Если повторная попытка окажется неудачной, будет выброшено последнее RetryException
. Чтобы указать первоначальную причину, которая привела к неудачной повторной попытке, создайте клиент Feign с опциейExceptionPropagationPolicy exceptionPropagationPolicy()
.
Если вам нужно рассматривать то, что в противном случае было бы ошибкой, как успех, и возвращать результат, а не генерировать исключение, вы можете использовать ResponseInterceptor
.
В качестве примера Feign включает простой RedirectionInterceptor
, который можно использовать для извлечения заголовка местоположения из ответов перенаправления.
public interface Api {
// returns a 302 response
@ RequestLine ( "GET /location" )
String location ();
}
public class MyApp {
public static void main ( String [] args ) {
// Configure the HTTP client to ignore redirection
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 не собирает никаких показателей.
Но можно добавить возможности сбора метрик к любому притворному клиенту.
Metric Capabilities предоставляют первоклассный API-интерфейс Metrics, к которому пользователи могут подключиться, чтобы получить представление о жизненном цикле запроса/ответа.
Примечание о модулях метрик :
Все метрические интеграции встроены в отдельные модули и недоступны в модуле
feign-core
. Вам нужно будет добавить их в свои зависимости.
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");
// metrics will be available from this point onwards
}
}
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");
// metrics will be available from this point onwards
}
}
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");
// metrics will be available from this point onwards
}
}
Интерфейсы, на которые нацелен 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 );
default List < Repo > repos ( String owner ) {
return repos ( owner , "full_name" );
}
/**
* Lists all contributors for all repos owned by a user.
*/
default List < Contributor > contributors ( String user ) {
MergingContributorList contributors = new MergingContributorList ();
for ( Repo repo : this . repos ( owner )) {
contributors . addAll ( this . contributors ( user , repo . getName ()));
}
return contributors . mergeResult ();
}
static GitHub connect () {
return Feign . builder ()
. decoder ( new GsonDecoder ())
. target ( GitHub . class , "https://api.github.com" );
}
}
CompletableFuture
В Feign 10.8 представлен новый построитель AsyncFeign
, который позволяет методам возвращать экземпляры CompletableFuture
.
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" );
// Fetch and print a list of the contributors to this library.
CompletableFuture < List < Contributor >> contributors = github . contributors ( "OpenFeign" , "feign" );
for ( Contributor contributor : contributors . get ( 1 , TimeUnit . SECONDS )) {
System . out . println ( contributor . login + " (" + contributor . contributions + ")" );
}
}
}
Первоначальная реализация включает 2 асинхронных клиента:
AsyncClient.Default
AsyncApacheHttp5Client
Хранение всех фиктивных библиотек в одной и той же версии важно, чтобы избежать несовместимых двоичных файлов. При использовании внешних зависимостей может быть сложно убедиться, что присутствует только одна версия.
Имея это в виду, feign build генерирует модуль feign-bom
, который блокирует версии для всех модулей feign-*
.
Спецификация — это специальный файл POM, в котором группируются версии зависимостей, которые заведомо действительны и протестированы для совместной работы. Это облегчит разработчикам необходимость проверять совместимость различных версий и уменьшит вероятность несоответствия версий.
Вот один из примеров того, как выглядит файл feign BOM.
< 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 .
Включите зависимость в ваше приложение:
Мейвен :
< dependencies >
...
< dependency >
< groupId >io.github.openfeign.form</ groupId >
< artifactId >feign-form</ artifactId >
< version >4.0.0</ version >
</ dependency >
...
</ dependencies >
Градл :
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.* до версии 2.0.3.RELEASE и использует 10.* после. В любом случае, у зависимости уже есть подходящая версия feign-form
, см. pom зависимости, поэтому вам не нужно указывать ее отдельно;
spring-cloud-starter-feign
— устаревшая зависимость, которая всегда использует версии OpenFeign
9.* .
Добавьте FormEncoder
в свой Feign.Builder
следующим образом:
SomeApi github = Feign . builder ()
. encoder ( new FormEncoder ())
. target ( SomeApi . class , "http://api.some.org" );
Более того, вы можете украсить существующий кодировщик, например 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
.
interface SomeApi {
@ RequestLine ( "POST /authorization" )
@ Headers ( "Content-Type: application/x-www-form-urlencoded" )
void authorization ( @ Param ( "email" ) String email , @ Param ( "password" ) String password );
// Group all parameters within a POJO
@ RequestLine ( "POST /user" )
@ Headers ( "Content-Type: application/x-www-form-urlencoded" )
void addUser ( User user );
class User {
Integer id ;
String name ;
}
}
interface SomeApi {
// File parameter
@ RequestLine ( "POST /send_photo" )
@ Headers ( "Content-Type: multipart/form-data" )
void sendPhoto ( @ Param ( "is_public" ) Boolean isPublic , @ Param ( "photo" ) File photo );
// byte[] parameter
@ RequestLine ( "POST /send_photo" )
@ Headers ( "Content-Type: multipart/form-data" )
void sendPhoto ( @ Param ( "is_public" ) Boolean isPublic , @ Param ( "photo" ) byte [] photo );
// FormData parameter
@ RequestLine ( "POST /send_photo" )
@ Headers ( "Content-Type: multipart/form-data" )
void sendPhoto ( @ Param ( "is_public" ) Boolean isPublic , @ Param ( "photo" ) FormData photo );
// Group all parameters within a 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
будет использовать расширение File для определения Content-Type
;byte[]
будет использовать application/octet-stream
в качестве Content-Type
;FormData
будет использовать Content-Type
и fileName
FormData
; FormData
— это пользовательский объект, который обертывает byte[]
и определяет Content-Type
и fileName
следующим образом:
FormData formData = new FormData ( "image/png" , "filename.png" , myDataAsByteArray );
someApi . sendPhoto ( true , formData );
Вы также можете использовать Form Encoder с Spring MultipartFile
и @FeignClient
.
Включите зависимости в файл pom.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, которая обрабатывает анализ составного ответа. Части данных тела хранятся в памяти в виде массивов байтов.
Чтобы использовать эту функцию, включите 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 ;
}
});
}
}
}