微服務RPC框架-Feign

    一個成熟的微服務集羣,內部調用必然依賴一個好的RPC框架,好比:基於http協議的feign,基於私有tcp協議的dubbo。本文內容介紹feign。java

1、What?

若是不使用rpc框架,那麼調用服務須要走http的話,配置請求head、body,而後才能發起請求。得到響應體後,還需解析等操做,十分繁瑣。git

Feign是一個http請求調用的輕量級框架,能夠以Java接口註解的方式調用Http請求。Feign經過處理註解,將請求模板化,當實際調用的時候,傳入參數,根據參數再應用到請求上,進而轉化成真正的請求,封裝了http調用流程。github

2、How?

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;
}

3、Why?

  • 啓動時,程序會進行包掃描,掃描全部包下全部@FeignClient註解的類,並將這些類注入到spring的IOC容器中。
  • 當定義的Feign中的接口被調用時,經過JDK的動態代理來生成RequestTemplate。RequestTemplate中包含請求的全部信息,如請求參數,請求URL等。
  • RequestTemplate聲場Request,而後將Request交給client處理,client默認是JDK的HTTPUrlConnection,也能夠是OKhttp、Apache的HTTPClient等。
  • 最後client封裝成LoadBaLanceClient,結合ribbon負載均衡地發起調用。

使用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請求的環節。能夠從這個方面着手分析系統的性能提高點。

相關文章
相關標籤/搜索