微服務RPC框架-Feign


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

一、What?

如果不使用rpc框架,那么調用服務需要走http的話,配置請求head、body,然后才能發起請求。獲得響應體后,還需解析等操作,十分繁瑣。

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

二、How?

feign底層基於http協議,適應絕大部分內外部API調用的應用場景,並且SpringCloud對feign已經有了比較好的封裝。使用上可以依賴於SpringCloud封裝過的feign:

<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:

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

三、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來攔截的請求接口。

1、啟用

啟動配置上檢查是否有@EnableFeignClients注解,如果有該注解,則開啟包掃描,掃描被@FeignClient注解的接口。掃描出該注解后,
通過beanDefinition注入到IOC容器中,方便后續被調用使用。

@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。

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。

2、發起請求

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客戶端負載均衡類型。

3、性能分析

Feign框架比較小巧,在處理請求轉換和消息解析的過程中,基本上沒什么時間消耗。真正影響性能的,是處理Http請求的環節。可以從這個方面着手分析系統的性能提升點。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM