Feign 的英文表意爲「僞裝,假裝,變形」, 是一個http請求調用的輕量級框架,能夠以Java接口註解的方式調用Http請求,而不用像Java中經過封裝HTTP請求報文的方式直接調用。Feign經過處理註解,將請求模板化,當實際調用的時候,傳入參數,根據參數再應用到請求上,進而轉化成真正的請求,這種請求相對而言比較直觀。
Feign被普遍應用在Spring Cloud 的解決方案中,是學習基於Spring Cloud 微服務架構不可或缺的重要組件。java
封裝了Http調用流程,更適合面向接口化的變成習慣
在服務調用的場景中,咱們常常調用基於Http協議的服務,而咱們常用到的框架可能有HttpURLConnection、Apache HttpComponnets、OkHttp3 、Netty等等,這些框架在基於自身的專一點提供了自身特性。而從角色劃分上來看,他們的職能是一致的提供Http調用服務。具體流程以下:
git
在使用feign 時,會定義對應的接口類,在接口類上使用Http相關的註解,標識HTTP請求參數信息,以下所示:github
interface GitHub { @RequestLine("GET /repos/{owner}/{repo}/contributors") List<Contributor> contributors(@Param("owner") String owner, @Param("repo") String repo); } public static 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"); // 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 底層,經過基於面向接口的動態代理方式生成實現類,將請求調用委託到動態代理實現類,基本原理以下所示:
api
public class ReflectiveFeign extends Feign{ ///省略部分代碼 @Override public <T> T newInstance(Target<T> target) { //根據接口類和Contract協議解析方式,解析接口類上的方法和註解,轉換成內部的MethodHandler處理方式 Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target); Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>(); List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>(); for (Method method : target.type().getMethods()) { if (method.getDeclaringClass() == Object.class) { continue; } else if(Util.isDefault(method)) { DefaultMethodHandler handler = new DefaultMethodHandler(method); defaultMethodHandlers.add(handler); methodToHandler.put(method, handler); } else { methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method))); } } InvocationHandler handler = factory.create(target, methodToHandler); // 基於Proxy.newProxyInstance 爲接口類建立動態實現,將全部的請求轉換給InvocationHandler 處理。 T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler); for(DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) { defaultMethodHandler.bindTo(proxy); } return proxy; } //省略部分代碼
Feign 定義了轉換協議,定義以下:架構
/** * Defines what annotations and values are valid on interfaces. */ public interface Contract { /** * Called to parse the methods in the class that are linked to HTTP requests. * 傳入接口定義,解析成相應的方法內部元數據表示 * @param targetType {@link feign.Target#type() type} of the Feign interface. */ // TODO: break this and correct spelling at some point List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType); }
Feign 默認有一套本身的協議規範,規定了一些註解,能夠映射成對應的Http請求,如官方的一個例子:app
public interface GitHub { @RequestLine("GET /repos/{owner}/{repo}/contributors") List<Contributor> getContributors(@Param("owner") String owner, @Param("repo") String repository); class Contributor { String login; int contributions; } }
上述的例子中,嘗試調用GitHub.getContributors("foo","myrepo")的的時候,會轉換成以下的HTTP請求:框架
GET /repos/foo/myrepo/contributors HOST XXXX.XXX.XXX
Feign 默認的協議規範ide
註解 | 接口Target | 使用說明 |
---|---|---|
@RequestLine |
方法上 | 定義HttpMethod 和 UriTemplate. UriTemplate 中使用{} 包裹的表達式,能夠經過在方法參數上使 用@Param 自動注入 |
@Param |
方法參數 | 定義模板變量,模板變量的值可使用名稱的方式使用模板注入解析 |
@Headers |
類上或者方法上 | 定義頭部模板變量,使用@Param註解提供參數值的注入。若是該註解添加在接口類上,則全部的請求都會攜帶對應的Header信息;若是在方法上,則只會添加到對應的方法請求上 |
@QueryMap |
方法上 | 定義一個鍵值對或者pojo,參數值將會轉換成URL上的查詢字符串上 |
@HeaderMap |
方法上 | 定義一個HeaderMap,與UrlTemplate和HeaderTemplate類型,可使用@Param註解提供參數值 |
具體FeignContract 是如何解析的,不在本文的介紹範圍內,請自行查找。微服務
當前Spring Cloud 微服務解決方案中,爲了下降學習成本,採用了Spring MVC的部分註解來完成 請求協議解析,也就是說 ,寫客戶端請求接口和像寫服務端代碼同樣:客戶端和服務端能夠經過SDK的方式進行約定,客戶端只須要引入服務端發佈的SDK API,就可使用面向接口的編碼方式對接服務:性能
咱們團隊內部就是按照這種思路,結合Spring Boot Starter 的特性,定義了服務端starter,
服務消費者在使用的時候,只須要引入Starter,就能夠調用服務。這個比較適合平臺無關性,接口抽象出來的好處就是能夠根據服務調用實現方式自有切換:
- 能夠基於簡單的Http服務調用;
- 能夠基於Spring Cloud 微服務架構調用;
- 能夠基於Dubbo SOA服務治理
這種模式比較適合在SaSS混合軟件服務的模式下自有切換,根據客戶的硬件能力選擇合適的方式部署,也能夠基於自身的服務集羣部署微服務
固然,目前的Spring MVC的註解並非能夠徹底使用的,有一些註解並不支持,如@GetMapping
,@PutMapping
等,僅支持使用@RequestMapping
等,另外註解繼承性方面也有些問題;具體限制細節,每一個版本能會有些出入,能夠參考上述的代碼實現,比較簡單。
Spring Cloud 沒有基於Spring MVC 所有註解來作Feign 客戶端註解協議解析,我的認爲這個是一個不小的坑。在剛入手Spring Cloud 的時候,就碰到這個問題。後來是深刻代碼才解決的.... 這個應該有人寫了加強類來處理,暫且不表,先MARK一下,是一個開源代碼練手的好機會。
根據傳入的Bean對象和註解信息,從中提取出相應的值,來構造Http Request 對象:
Feign 最終會將請求轉換成Http 消息發送出去,傳入的請求對象最終會解析成消息體,以下所示:
在接口定義上Feign作的比較簡單,抽象出了Encoder 和decoder 接口:
public interface Encoder { /** Type literal for {@code Map<String, ?>}, indicating the object to encode is a form. */ Type MAP_STRING_WILDCARD = Util.MAP_STRING_WILDCARD; /** * Converts objects to an appropriate representation in the template. * 將實體對象轉換成Http請求的消息正文中 * @param object what to encode as the request body. * @param bodyType the type the object should be encoded as. {@link #MAP_STRING_WILDCARD} * indicates form encoding. * @param template the request template to populate. * @throws EncodeException when encoding failed due to a checked exception. */ void encode(Object object, Type bodyType, RequestTemplate template) throws EncodeException; /** * Default implementation of {@code Encoder}. */ class Default implements Encoder { @Override public void encode(Object object, Type bodyType, RequestTemplate template) { if (bodyType == String.class) { template.body(object.toString()); } else if (bodyType == byte[].class) { template.body((byte[]) object, null); } else if (object != null) { throw new EncodeException( format("%s is not a type supported by this encoder.", object.getClass())); } } } }
public interface Decoder { /** * Decodes an http response into an object corresponding to its {@link * java.lang.reflect.Method#getGenericReturnType() generic return type}. If you need to wrap * exceptions, please do so via {@link DecodeException}. * 從Response 中提取Http消息正文,經過接口類聲明的返回類型,消息自動裝配 * @param response the response to decode * @param type {@link java.lang.reflect.Method#getGenericReturnType() generic return type} of * the method corresponding to this {@code response}. * @return instance of {@code type} * @throws IOException will be propagated safely to the caller. * @throws DecodeException when decoding failed due to a checked exception besides IOException. * @throws FeignException when decoding succeeds, but conveys the operation failed. */ Object decode(Response response, Type type) throws IOException, DecodeException, FeignException; /** Default implementation of {@code Decoder}. */ public class Default extends StringDecoder { @Override public Object decode(Response response, Type type) throws IOException { if (response.status() == 404) return Util.emptyValueOf(type); if (response.body() == null) return null; if (byte[].class.equals(type)) { return Util.toByteArray(response.body().asInputStream()); } return super.decode(response, type); } } }
目前Feign 有如下實現:
Encoder/ Decoder 實現 | 說明 |
---|---|
JacksonEncoder,JacksonDecoder | 基於 Jackson 格式的持久化轉換協議 |
GsonEncoder,GsonDecoder | 基於Google GSON 格式的持久化轉換協議 |
SaxEncoder,SaxDecoder | 基於XML 格式的Sax 庫持久化轉換協議 |
JAXBEncoder,JAXBDecoder | 基於XML 格式的JAXB 庫持久化轉換協議 |
ResponseEntityEncoder,ResponseEntityDecoder | Spring MVC 基於 ResponseEntity< T > 返回格式的轉換協議 |
SpringEncoder,SpringDecoder | 基於Spring MVC HttpMessageConverters 一套機制實現的轉換協議 ,應用於Spring Cloud 體系中 |
在請求轉換的過程當中,Feign 抽象出來了攔截器接口,用於用戶自定義對請求的操做:
public interface RequestInterceptor { /** * 能夠在構造RequestTemplate 請求時,增長或者修改Header, Method, Body 等信息 * Called for every request. Add data using methods on the supplied {@link RequestTemplate}. */ void apply(RequestTemplate template); }
好比,若是但願Http消息傳遞過程當中被壓縮,能夠定義一個請求攔截器:
public class FeignAcceptGzipEncodingInterceptor extends BaseRequestInterceptor { /** * Creates new instance of {@link FeignAcceptGzipEncodingInterceptor}. * * @param properties the encoding properties */ protected FeignAcceptGzipEncodingInterceptor(FeignClientEncodingProperties properties) { super(properties); } /** * {@inheritDoc} */ @Override public void apply(RequestTemplate template) { // 在Header 頭部添加相應的數據信息 addHeader(template, HttpEncoding.ACCEPT_ENCODING_HEADER, HttpEncoding.GZIP_ENCODING, HttpEncoding.DEFLATE_ENCODING); } }
在發送和接收請求的時候,Feign定義了統一的日誌門面來輸出日誌信息 , 而且將日誌的輸出定義了四個等級:
級別 | 說明 |
---|---|
NONE | 不作任何記錄 |
BASIC | 只記錄輸出Http 方法名稱、請求URL、返回狀態碼和執行時間 |
HEADERS | 記錄輸出Http 方法名稱、請求URL、返回狀態碼和執行時間 和 Header 信息 |
FULL | 記錄Request 和Response的Header,Body和一些請求元數據 |
public abstract class Logger { protected static String methodTag(String configKey) { return new StringBuilder().append('[').append(configKey.substring(0, configKey.indexOf('('))) .append("] ").toString(); } /** * Override to log requests and responses using your own implementation. Messages will be http * request and response text. * * @param configKey value of {@link Feign#configKey(Class, java.lang.reflect.Method)} * @param format {@link java.util.Formatter format string} * @param args arguments applied to {@code format} */ protected abstract void log(String configKey, String format, Object... args); protected void logRequest(String configKey, Level logLevel, Request request) { log(configKey, "---> %s %s HTTP/1.1", request.method(), request.url()); if (logLevel.ordinal() >= Level.HEADERS.ordinal()) { for (String field : request.headers().keySet()) { for (String value : valuesOrEmpty(request.headers(), field)) { log(configKey, "%s: %s", field, value); } } int bodyLength = 0; if (request.body() != null) { bodyLength = request.body().length; if (logLevel.ordinal() >= Level.FULL.ordinal()) { String bodyText = request.charset() != null ? new String(request.body(), request.charset()) : null; log(configKey, ""); // CRLF log(configKey, "%s", bodyText != null ? bodyText : "Binary data"); } } log(configKey, "---> END HTTP (%s-byte body)", bodyLength); } } protected void logRetry(String configKey, Level logLevel) { log(configKey, "---> RETRYING"); } protected Response logAndRebufferResponse(String configKey, Level logLevel, Response response, long elapsedTime) throws IOException { String reason = response.reason() != null && logLevel.compareTo(Level.NONE) > 0 ? " " + response.reason() : ""; int status = response.status(); log(configKey, "<--- HTTP/1.1 %s%s (%sms)", status, reason, elapsedTime); if (logLevel.ordinal() >= Level.HEADERS.ordinal()) { for (String field : response.headers().keySet()) { for (String value : valuesOrEmpty(response.headers(), field)) { log(configKey, "%s: %s", field, value); } } int bodyLength = 0; if (response.body() != null && !(status == 204 || status == 205)) { // HTTP 204 No Content "...response MUST NOT include a message-body" // HTTP 205 Reset Content "...response MUST NOT include an entity" if (logLevel.ordinal() >= Level.FULL.ordinal()) { log(configKey, ""); // CRLF } byte[] bodyData = Util.toByteArray(response.body().asInputStream()); bodyLength = bodyData.length; if (logLevel.ordinal() >= Level.FULL.ordinal() && bodyLength > 0) { log(configKey, "%s", decodeOrDefault(bodyData, UTF_8, "Binary data")); } log(configKey, "<--- END HTTP (%s-byte body)", bodyLength); return response.toBuilder().body(bodyData).build(); } else { log(configKey, "<--- END HTTP (%s-byte body)", bodyLength); } } return response; } protected IOException logIOException(String configKey, Level logLevel, IOException ioe, long elapsedTime) { log(configKey, "<--- ERROR %s: %s (%sms)", ioe.getClass().getSimpleName(), ioe.getMessage(), elapsedTime); if (logLevel.ordinal() >= Level.FULL.ordinal()) { StringWriter sw = new StringWriter(); ioe.printStackTrace(new PrintWriter(sw)); log(configKey, sw.toString()); log(configKey, "<--- END ERROR"); } return ioe; }
Feign 內置了一個重試器,當HTTP請求出現IO異常時,Feign會有一個最大嘗試次數發送請求,如下是Feign核心
代碼邏輯:
final class SynchronousMethodHandler implements MethodHandler { // 省略部分代碼 @Override public Object invoke(Object[] argv) throws Throwable { //根據輸入參數,構造Http 請求。 RequestTemplate template = buildTemplateFromArgs.create(argv); // 克隆出一份重試器 Retryer retryer = this.retryer.clone(); // 嘗試最大次數,若是中間有結果,直接返回 while (true) { try { return executeAndDecode(template); } catch (RetryableException e) { retryer.continueOrPropagate(e); if (logLevel != Logger.Level.NONE) { logger.logRetry(metadata.configKey(), logLevel); } continue; } } }
重試器有以下幾個控制參數:
重試參數 | 說明 | 默認值 |
---|---|---|
period | foo | 100ms |
maxPeriod | 當請求連續失敗時,重試的時間間隔將按照:long interval = (long) (period * Math.pow(1.5, attempt - 1)); 計算,按照等比例方式延長,可是最大間隔時間爲 maxPeriod, 設置此值可以避免 重試次數過多的狀況下執行週期太長 |
1000ms |
maxAttempts | 最大重試次數 | 5 |
Feign 真正發送HTTP請求是委託給 feign.Client
來作的:
public interface Client { /** * Executes a request against its {@link Request#url() url} and returns a response. * 執行Http請求,並返回Response * @param request safe to replay. * @param options options to apply to this request. * @return connected response, {@link Response.Body} is absent or unread. * @throws IOException on a network error connecting to {@link Request#url()}. */ Response execute(Request request, Options options) throws IOException; }
Feign 默認底層經過JDK 的 java.net.HttpURLConnection
實現了feign.Client
接口類,在每次發送請求的時候,都會建立新的HttpURLConnection 連接,這也就是爲何默認狀況下Feign的性能不好的緣由。能夠經過拓展該接口,使用Apache HttpClient 或者OkHttp3等基於鏈接池的高性能Http客戶端,咱們項目內部使用的就是OkHttp3做爲Http 客戶端。
以下是Feign 的默認實現,供參考:
public static class Default implements Client { private final SSLSocketFactory sslContextFactory; private final HostnameVerifier hostnameVerifier; /** * Null parameters imply platform defaults. */ public Default(SSLSocketFactory sslContextFactory, HostnameVerifier hostnameVerifier) { this.sslContextFactory = sslContextFactory; this.hostnameVerifier = hostnameVerifier; } @Override public Response execute(Request request, Options options) throws IOException { HttpURLConnection connection = convertAndSend(request, options); return convertResponse(connection).toBuilder().request(request).build(); } HttpURLConnection convertAndSend(Request request, Options options) throws IOException { final HttpURLConnection connection = (HttpURLConnection) new URL(request.url()).openConnection(); if (connection instanceof HttpsURLConnection) { HttpsURLConnection sslCon = (HttpsURLConnection) connection; if (sslContextFactory != null) { sslCon.setSSLSocketFactory(sslContextFactory); } if (hostnameVerifier != null) { sslCon.setHostnameVerifier(hostnameVerifier); } } connection.setConnectTimeout(options.connectTimeoutMillis()); connection.setReadTimeout(options.readTimeoutMillis()); connection.setAllowUserInteraction(false); connection.setInstanceFollowRedirects(true); connection.setRequestMethod(request.method()); Collection<String> contentEncodingValues = request.headers().get(CONTENT_ENCODING); boolean gzipEncodedRequest = contentEncodingValues != null && contentEncodingValues.contains(ENCODING_GZIP); boolean deflateEncodedRequest = contentEncodingValues != null && contentEncodingValues.contains(ENCODING_DEFLATE); boolean hasAcceptHeader = false; Integer contentLength = null; for (String field : request.headers().keySet()) { if (field.equalsIgnoreCase("Accept")) { hasAcceptHeader = true; } for (String value : request.headers().get(field)) { if (field.equals(CONTENT_LENGTH)) { if (!gzipEncodedRequest && !deflateEncodedRequest) { contentLength = Integer.valueOf(value); connection.addRequestProperty(field, value); } } else { connection.addRequestProperty(field, value); } } } // Some servers choke on the default accept string. if (!hasAcceptHeader) { connection.addRequestProperty("Accept", "*/*"); } if (request.body() != null) { if (contentLength != null) { connection.setFixedLengthStreamingMode(contentLength); } else { connection.setChunkedStreamingMode(8196); } connection.setDoOutput(true); OutputStream out = connection.getOutputStream(); if (gzipEncodedRequest) { out = new GZIPOutputStream(out); } else if (deflateEncodedRequest) { out = new DeflaterOutputStream(out); } try { out.write(request.body()); } finally { try { out.close(); } catch (IOException suppressed) { // NOPMD } } } return connection; } Response convertResponse(HttpURLConnection connection) throws IOException { int status = connection.getResponseCode(); String reason = connection.getResponseMessage(); if (status < 0) { throw new IOException(format("Invalid status(%s) executing %s %s", status, connection.getRequestMethod(), connection.getURL())); } Map<String, Collection<String>> headers = new LinkedHashMap<String, Collection<String>>(); for (Map.Entry<String, List<String>> field : connection.getHeaderFields().entrySet()) { // response message if (field.getKey() != null) { headers.put(field.getKey(), field.getValue()); } } Integer length = connection.getContentLength(); if (length == -1) { length = null; } InputStream stream; if (status >= 400) { stream = connection.getErrorStream(); } else { stream = connection.getInputStream(); } return Response.builder() .status(status) .reason(reason) .headers(headers) .body(stream, length) .build(); } }
Feign 總體框架很是小巧,在處理請求轉換和消息解析的過程當中,基本上沒什麼時間消耗。真正影響性能的,是處理Http請求的環節。
如上所述,因爲默認狀況下,Feign採用的是JDK的HttpURLConnection
,因此總體性能並不高,剛開始接觸Spring Cloud 的同窗,若是沒注意這些細節,可能會對Spring Cloud 有很大的偏見。
咱們項目內部使用的是OkHttp3 做爲鏈接客戶端。