一個成熟的微服務集羣,內部調用必然依賴一個好的RPC框架,好比:基於http協議的feign,基於私有tcp協議的dubbo。本文內容介紹feign。java
若是不使用rpc框架,那麼調用服務須要走http的話,配置請求head、body,而後才能發起請求。得到響應體後,還需解析等操做,十分繁瑣。git
Feign是一個http請求調用的輕量級框架,能夠以Java接口註解的方式調用Http請求。Feign經過處理註解,將請求模板化,當實際調用的時候,傳入參數,根據參數再應用到請求上,進而轉化成真正的請求,封裝了http調用流程。github
feign底層基於http協議,適應絕大部份內外部API調用的應用場景,而且SpringCloud對feign已經有了比較好的封裝。使用上能夠依賴於SpringCloud封裝過的feign:spring
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
Feign在默認狀況下使用的是JDK原生的URLConnection發送HTTP請求,沒有鏈接池,可是對每一個地址會保持一個長鏈接,即利用HTTP的
persistence connection。建議替換爲Apache HttpClient,做爲底層的http client包,從而獲取鏈接池、超時時間等與性能息息相關的控制能力:服務器
<dependency> <groupId>io.github.openfeign</groupId> <artifactId>feign-httpclient</artifactId> </dependency>
在配置文件中啓用ApacheHttpClient:app
feign.httpclient.enabled=true
FeignClient參數:負載均衡
public @interface FeignClient { @AliasFor("name") String value() default ""; /** @deprecated */ @Deprecated String serviceId() default ""; String contextId() default ""; // 指定FeignClient的名稱 @AliasFor("value") String name() default ""; String qualifier() default ""; // 全路徑地址或hostname,http或https可選 String url() default ""; // 當發生http 404錯誤時,若是該字段位true,會調用decoder進行解碼,不然拋出FeignException boolean decode404() default false; // Feign配置類,能夠自定義Feign的LogLevel Class<?>[] configuration() default {}; // 容錯的處理類,當調用遠程接口失敗或超時時,會調用對應接口的容錯邏輯 Class<?> fallback() default void.class; // 工廠類,用於生成fallback類實例,經過這個屬性咱們能夠實現每一個接口通用的容錯邏輯,減小重複的代碼 Class<?> fallbackFactory() default void.class; // 定義當前FeignClient的統一前綴,相似於controller類上的requestMapping String path() default ""; boolean primary() default true; }
使用Feign涉及兩個註解:@EnableFeignClients,用來開啓Feign;@FeignClient,標記要用Feign來攔截的請求接口。框架
啓動配置上檢查是否有@EnableFeignClients註解,若是有該註解,則開啓包掃描,掃描被@FeignClient註解的接口。掃描出該註解後,
經過beanDefinition注入到IOC容器中,方便後續被調用使用。tcp
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(FeignClientsRegistrar.class) public @interface EnableFeignClients { String[] value() default {}; String[] basePackages() default {}; Class<?>[] basePackageClasses() default {}; Class<?>[] defaultConfiguration() default {}; Class<?>[] clients() default {}; }
@EnableFeignClients 是關於註解掃描的配置,使用了@Import(FeignClientsRegistrar.class)。在spring context處理過程當中,這個Import會在解析Configuration的時候當作提供了其餘的bean definition的擴展,Spring經過調用其registerBeanDefinitions方法來獲取其提供的bean definition。ide
class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware { @Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { registerDefaultConfiguration(metadata, registry); registerFeignClients(metadata, registry); } }
FeignClientsRegistrar裏重寫了spring裏ImportBeanDefinitionRegistrar接口的registerBeanDefinitions方法。也就是在啓動時,處理了EnableFeignClients註解後,registry裏面會多出一些關於Feign的BeanDefinition。
ReflectiveFeign內部使用了jdk的動態代理爲目標接口生成了一個動態代理類,這裏會生成一個InvocationHandler統一的方法攔截器,同時爲接口的每一個方法生成一個SynchronousMethodHandler攔截器,並解析方法上的元數據,生成一個http請求模板RequestTemplate。
public class ReflectiveFeign extends Feign { @Override public <T> T newInstance(Target<T> target) { 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); T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[] {target.type()}, handler); for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) { defaultMethodHandler.bindTo(proxy); } return proxy; } }
Feign內置了一個重試器,當HTTP請求出現IO異常時,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) { try { retryer.continueOrPropagate(e); } catch (RetryableException th) { Throwable cause = th.getCause(); if (propagationPolicy == UNWRAP && cause != null) { throw cause; } else { throw th; } } if (logLevel != Logger.Level.NONE) { logger.logRetry(metadata.configKey(), logLevel); } continue; } } } }
Feign真正發送HTTP請求是委託給feign.Client來作的:
public interface Client { Response execute(Request request, Options options) throws IOException; class Default implements Client { @Override public Response execute(Request request, Options options) throws IOException { HttpURLConnection connection = convertAndSend(request, options); return convertResponse(connection, request); } } }
默認底層經過JDK的java.net.HttpURLConnection實現了feign.Client接口類。在每次發送請求的時候,都會建立新的HttpURLConnection連接,這樣的話默認狀況下Feign的性能不好,通常擴展該接口,好比使用Apache的HttpClient或者OkHttp3等基於鏈接池的高性能Http客戶端。
注意:SynchronousMethodHandler並非直接完成遠程URL的請求,而是經過負載均衡機制,定位到合適的遠程server服務器,而後再完成真正的遠程URL請求。即:SynchronousMethodHandler實例的client成員,其實際不是feign.Client.Default類型,而是LoadBalancerFeignClient客戶端負載均衡類型。
Feign框架比較小巧,在處理請求轉換和消息解析的過程當中,基本上沒什麼時間消耗。真正影響性能的,是處理Http請求的環節。能夠從這個方面着手分析系統的性能提高點。