Feign คือตัวประสานไคลเอนต์ Java ถึง HTTP ที่ได้รับแรงบันดาลใจจาก Retrofit, JAXRS-2.0 และ WebSocket เป้าหมายแรกของ Feign คือการลดความซับซ้อนของการเชื่อมโยงตัวส่วนกับ HTTP API อย่างสม่ำเสมอโดยไม่คำนึงถึง ReSTfulness
Feign ใช้เครื่องมือเช่น Jersey และ CXF เพื่อเขียนไคลเอ็นต์ Java สำหรับบริการ ReST หรือ SOAP นอกจากนี้ Feign ยังให้คุณเขียนโค้ดของคุณเองบนไลบรารี http เช่น Apache HC Feign เชื่อมต่อโค้ดของคุณกับ http API โดยมีค่าใช้จ่ายและโค้ดน้อยที่สุดผ่านตัวถอดรหัสและการจัดการข้อผิดพลาดที่ปรับแต่งได้ ซึ่งสามารถเขียนลงใน http API แบบข้อความใดก็ได้
Feign ทำงานโดยการประมวลผลคำอธิบายประกอบเป็นคำขอที่เป็นเทมเพลต อาร์กิวเมนต์จะถูกนำไปใช้กับเทมเพลตเหล่านี้ในลักษณะตรงไปตรงมาก่อนเอาต์พุต แม้ว่า Feign จะถูกจำกัดให้รองรับ API แบบข้อความเท่านั้น แต่ก็ช่วยลดความยุ่งยากในด้านต่างๆ ของระบบ เช่น คำขอเล่นซ้ำได้อย่างมาก นอกจากนี้ Feign ยังทำให้การทดสอบหน่วย Conversion ของคุณเป็นเรื่องง่าย
Feign 10.x ขึ้นไปสร้างขึ้นบน Java 8 และควรใช้งานได้บน Java 9, 10 และ 11 สำหรับผู้ที่ต้องการความเข้ากันได้กับ JDK 6 โปรดใช้ Feign 9.x
นี่คือแผนที่ที่มีคุณสมบัติหลักในปัจจุบันโดยแกล้งทำเป็น:
ทำให้ไคลเอนต์ API ง่ายขึ้น
Logger
Logger
API ใหม่เพื่อให้สอดคล้องกับเฟรมเวิร์ก เช่น SLF4J มากขึ้น ซึ่งเป็นโมเดลทางจิตทั่วไปสำหรับการบันทึกภายใน Feign โมเดลนี้จะถูกใช้โดย Feign เองตลอด และให้แนวทางที่ชัดเจนยิ่งขึ้นเกี่ยวกับวิธีการใช้งาน Logger
Retry
แฟกเตอร์ API อีกครั้งRetry
API ใหม่เพื่อรองรับเงื่อนไขที่ผู้ใช้ระบุและควบคุมนโยบายการถอยกลับได้ดีขึ้น ซึ่งอาจส่งผลให้เกิดการเปลี่ยนแปลงที่เข้ากันไม่ได้แบบย้อนกลับ CompletableFuture
Future
และการจัดการผู้ดำเนินการสำหรับวงจรการร้องขอ/การตอบกลับ การนำไปปฏิบัติจะต้องมีการเปลี่ยนแปลงที่เข้ากันไม่ได้แบบย้อนกลับ อย่างไรก็ตาม จำเป็นต้องมีคุณลักษณะนี้ก่อนจึงจะสามารถพิจารณาการดำเนินการเชิงรับได้java.util.concurrent.Flow
ห้องสมุดปลอมมีให้ที่ Maven Central
< dependency >
< groupId >io.github.openfeign</ groupId >
< artifactId >feign-core</ artifactId >
< version >??feign.version??</ version >
</ dependency >
โดยทั่วไปการใช้งานจะมีลักษณะเช่นนี้ ซึ่งเป็นการปรับตัวอย่าง Canonical 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 + ")" );
}
}
}
คำอธิบายประกอบปลอมจะกำหนด Contract
ระหว่างอินเทอร์เฟซและวิธีการทำงานของไคลเอ็นต์พื้นฐาน สัญญาเริ่มต้นของ Feign กำหนดคำอธิบายประกอบต่อไปนี้:
คำอธิบายประกอบ | เป้าหมายอินเทอร์เฟซ | การใช้งาน |
---|---|---|
@RequestLine | วิธี | กำหนด HttpMethod และ UriTemplate สำหรับการร้องขอ Expressions ค่าที่อยู่ในวงเล็บปีกกา {expression} ได้รับการแก้ไขโดยใช้พารามิเตอร์ที่มีคำอธิบายประกอบ @Param ที่สอดคล้องกัน |
@Param | พารามิเตอร์ | กำหนดตัวแปรเทมเพลตซึ่งค่าจะถูกใช้เพื่อแก้ไขเทมเพลต Expression ที่เกี่ยวข้อง ตามชื่อที่ระบุเป็นค่าคำอธิบายประกอบ หากไม่มีค่า จะพยายามรับชื่อจากชื่อพารามิเตอร์วิธี bytecode (หากโค้ดถูกคอมไพล์ด้วยแฟล็ก -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 );
Feign Expressions
แสดงถึง Simple String Expressions (ระดับ 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 อย่างไรก็ตาม การจัดการสัญลักษณ์บนการสืบค้นอาจไม่สอดคล้องกัน ในระบบเดิมบางระบบ เครื่องหมาย+
จะเท่ากับช่องว่าง Feign ใช้แนวทางของระบบสมัยใหม่ โดยที่สัญลักษณ์+
ไม่ควรแสดงถึงช่องว่างและมีการเข้ารหัสอย่างชัดเจนเป็น%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 ตั้งใจที่จะทำงานได้ดีกับเครื่องมือ Open Source อื่นๆ เรายินดีที่จะรวมโมดูลเข้ากับโปรเจ็กต์ที่คุณชื่นชอบ!
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 Module
Moshi มีตัวเข้ารหัสและตัวถอดรหัสที่คุณสามารถใช้กับ JSON API ได้ เพิ่ม 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 Faults ถูกส่งคืนเพื่อตอบสนองต่อรหัส http ข้อผิดพลาด (4xx, 5xx, ...)
fastjson2 มีตัวเข้ารหัสและตัวถอดรหัสที่คุณสามารถใช้กับ JSON API ได้
เพิ่ม 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 ลงใน 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" );
}
}
RibbonClient จะแทนที่ความละเอียด URL ของไคลเอ็นต์ของ Feign โดยเพิ่มการกำหนดเส้นทางอัจฉริยะและความสามารถด้านความยืดหยุ่นที่ Ribbon มอบให้
การรวมระบบกำหนดให้คุณต้องส่งชื่อไคลเอ็นต์ 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 ไปยัง Java11 New HTTP/2 Client ที่ใช้ 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 ให้กับ classpath ของคุณ จากนั้นใช้ตัวสร้าง 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 ที่คุณเลือกเข้ากับ classpath ของคุณ จากนั้นกำหนดค่า 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" );
}
}
ต่อไปนี้เป็นวิธีกำหนดค่าตัวถอดรหัสสตรีมด้วยตัวถอดรหัสผู้รับมอบสิทธิ์:
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- meta-*" หรือ "x-goog-meta-*") พารามิเตอร์แผนที่สามารถใส่คำอธิบายประกอบด้วย HeaderMap
เพื่อสร้างข้อความค้นหาที่ใช้เนื้อหาของแผนที่เป็นพารามิเตอร์ส่วนหัว
public interface Api {
@ RequestLine ( "POST /" )
void post ( @ HeaderMap Map < String , Object > headerMap );
}
วิธีการเหล่านี้ระบุรายการส่วนหัวเป็นส่วนหนึ่งของ API และไม่จำเป็นต้องปรับแต่งใดๆ เมื่อสร้างไคลเอ็นต์ Feign
ในการปรับแต่งส่วนหัวสำหรับแต่ละวิธีการร้องขอบนเป้าหมาย คุณสามารถใช้ RequestInterceptor ได้ RequestInterceptors สามารถแชร์ข้ามอินสแตนซ์เป้าหมายได้ และคาดว่าจะปลอดภัยสำหรับเธรด 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" );
}
}
อีกตัวอย่างทั่วไปของ interceptor คือการตรวจสอบสิทธิ์ เช่น การใช้ 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 );
}
พารามิเตอร์แผนที่สามารถใส่คำอธิบายประกอบด้วย 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 ตัวเข้ารหัสเริ่มต้นจะใช้การสะท้อนเพื่อตรวจสอบวัตถุที่ให้มา ฟิลด์ เพื่อขยายค่าของวัตถุลงในสตริงการสืบค้น หากคุณต้องการให้สร้างสตริงการสืบค้นโดยใช้เมธอด getter และ setter ตามที่กำหนดไว้ใน 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
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()
หากคุณต้องการถือว่าสิ่งที่อาจเป็นข้อผิดพลาดสำเร็จและส่งคืนผลลัพธ์แทนที่จะส่งข้อยกเว้น คุณอาจใช้ 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" );
}
}
ตามค่าเริ่มต้น การแกล้งทำจะไม่รวบรวมเมตริกใดๆ
แต่คุณสามารถเพิ่มความสามารถในการรวบรวมตัวชี้วัดให้กับไคลเอนต์ปลอมๆ ได้
ความสามารถด้านเมตริกมี API เมตริกระดับเฟิร์สคลาสที่ผู้ใช้สามารถใช้เพื่อรับข้อมูลเชิงลึกเกี่ยวกับวงจรคำขอ/การตอบสนอง
หมายเหตุเกี่ยวกับโมดูลเมตริก :
การรวมระบบเมตริกทั้งหมดสร้างขึ้นในโมดูลที่แยกจากกัน และไม่มีให้บริการในโมดูล
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 + ")" );
}
}
}
การใช้งานครั้งแรกประกอบด้วยไคลเอนต์ async 2 ตัว:
AsyncClient.Default
AsyncApacheHttp5Client
การรักษาไลบรารีปลอมทั้งหมดให้เป็นเวอร์ชันเดียวกันถือเป็นสิ่งสำคัญเพื่อหลีกเลี่ยงไบนารีที่เข้ากันไม่ได้ เมื่อใช้การอ้างอิงภายนอก อาจเป็นเรื่องยากที่จะตรวจสอบให้แน่ใจว่ามีเพียงเวอร์ชันเดียวเท่านั้น
ด้วยเหตุนี้ feign build จึงสร้างโมดูลที่เรียกว่า feign-bom
ซึ่งจะล็อคเวอร์ชันของโมดูล feign-*
ทั้งหมด
Bill Of Material เป็นไฟล์ POM พิเศษที่จัดกลุ่มเวอร์ชันการพึ่งพาที่ทราบว่าถูกต้องและทดสอบแล้วว่าทำงานร่วมกันได้ วิธีนี้จะช่วยลดความเจ็บปวดของนักพัฒนาที่ต้องทดสอบความเข้ากันได้ของเวอร์ชันต่างๆ และลดโอกาสที่เวอร์ชันจะไม่ตรงกัน
นี่คือตัวอย่างหนึ่งของลักษณะของไฟล์ 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.* จนถึง v2.0.3.RELEASE และใช้ 10.* หลังจากนั้น อย่างไรก็ตาม การขึ้นต่อกันมีเวอร์ชัน feign-form
ที่เหมาะสมอยู่แล้ว ดูที่ pom การขึ้นต่อกัน ดังนั้นคุณไม่จำเป็นต้องระบุแยกกัน
spring-cloud-starter-feign
เป็นการพึ่งพา ที่เลิกใช้แล้ว และจะใช้เวอร์ชัน 9.* ของ OpenFeign
เสมอ
เพิ่ม 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 ;
}
});
}
}
}