1. 程式人生 > >Spring Cloud Alibaba Sentinel 整合 Feign 的設計實現

Spring Cloud Alibaba Sentinel 整合 Feign 的設計實現

作者 | Spring Cloud Alibaba 高階開發工程師洛夜
來自公眾號阿里巴巴中介軟體投稿

前段時間 Hystrix 宣佈不再維護之後(Hystrix 停止開發。。。Spring Cloud 何去何從?),Feign 作為一個跟 Hystrix 強依賴的元件,必然會有所擔心後續的使用。

作為 Spring Cloud Alibaba 體系中的熔斷器 Sentinel,Sentinel 目前整合了 Feign,本文對整合過程做一次總結,歡迎大家討論和使用。

Feign 是什麼?

Feign 是一個 Java 實現的 Http 客戶端,用於簡化 Restful 呼叫。

Feign 跟 OkHttp、HttpClient 這種客戶端實現理念不一樣。Feign 強調介面的定義,介面中的一個方法對應一個 Http 請求,呼叫方法即傳送一個 Http 請求;OkHttp 或 HttpClient 以過程式的方式傳送 Http 請求。Feign 底層傳送請求的實現可以跟 OkHttp 或 HttpClient 整合。

要想整合 Feign,首先要了解 Feign 的使用以及執行過程,然後看 Sentinel 如何整合進去。

Feign 的使用

需要兩個步驟:

1、使用 @EnableFeignClients 註解開啟 Feign 功能

@SpringBootApplication
@EnableFeignClients // 開啟 Feign 功能
public class MyApplication {
  ...
}

@EnableFeignClients 屬性介紹:

value:String[] 包路徑。比如 org.my.pkg,會掃描這個包路徑下帶有 @FeignClient 註解的類並處理;

basePackages:String[] 跟 value 屬性作用一致;

basePackageClasses:Class<?>[] 跟 basePackages 作用一致,basePackages 是個 String 陣列,而 basePackageClasses 是個 Class 陣列,用於掃描這些類對應的 package;

defaultConfiguration:Class<?>[] 預設的配置類,對於所有的 Feign Client,這些配置類裡的配置都會對它們生效,可以在配置類裡構造 feign.codec.Decoder

, feign.codec.Encoderfeign.Contract 等bean;

clients:Class<?>[] 表示 @FeignClient; 註解修飾的類集合,如果指定了該屬性,那麼掃描功能相關的屬性就是失效。比如 value、basePackages 和 basePackageClasses;

2、使用 @FeignClient 註解修飾介面,這樣會基於跟介面生成代理類

@FeignClient(name = "service-provider")
public interface EchoService {
  @RequestMapping(value = "/echo/{str}", method = RequestMethod.GET)
  String echo(@PathVariable("str") String str);
}

只要確保這個被 @FeignClient 註解修飾到的介面能被 @EnableFeignClients 註解掃描到,就會基於 java.lang.reflect.Proxy 根據這個介面生成一個代理類。

生成代理類之後,會被注入到 ApplicationContext 中,直接 AutoWired 就能使用,使用的時候呼叫 echo 方法就相當於是發起一個 Restful 請求。

@FeignClient 屬性介紹:

value:String 服務名。比如 service-provider, http://service-provider。比如 EchoService 中如果配置了 value=service-provider,那麼呼叫 echo 方法的 url 為 http://service-provider/echo;如果配置了 value=https://service-provider,那麼呼叫 echo 方法的 url 為 https://service-provider/divide

serviceId:String 該屬性已過期,但還能用。作用跟 value 一致
name:String 跟 value 屬性作用一致

qualifier:String 給 FeignClient 設定 @Qualifier 註解

url:String 絕對路徑,用於替換服務名。優先順序比服務名高。比如 EchoService 中如果配置了 url=aaa,那麼呼叫 echo 方法的 url 為 http://aaa/echo;如果配置了 url=https://aaa,那麼呼叫 echo 方法的 url 為 https://aaa/divide

decode404:boolean 預設是 false,表示對於一個 http status code 為 404 的請求是否需要進行 decode,預設不進行 decode,當成一個異常處理。設定為true之後,遇到 404 的 response 還是會解析 body

configuration:Class<?>[] 跟 @EnableFeignClients 註解的 defaultConfiguration 屬性作用一致,但是這個對於單個 FeignClient 的配置,而 @EnableFeignClients 裡的 defaultConfiguration 屬性是作用域全域性的,針對所有的 FeignClient

fallback:Class<?> 預設值是 void.class,表示 fallback 類,需要實現 FeignClient 對應的介面,當呼叫方法發生異常的時候會呼叫這個 Fallback 類對應的 FeignClient 介面方法。

如果配置了 fallback 屬性,那麼會把這個 Fallback 類包裝在一個預設的 FallbackFactory 實現類 FallbackFactory.Default 上,而不使用 fallbackFactory 屬性對應的 FallbackFactory 實現類

fallbackFactory:Class<?> 預設值是 void.class,表示生產 fallback 類的 Factory,可以實現 feign.hystrix.FallbackFactory 介面,FallbackFactory 內部會針對一個 Throwable 異常返回一個 Fallback 類進行 fallback 操作

path:String 請求路徑。 在服務名或 url 與 requestPath 之間

primary:boolean 預設是 true,表示當前這個 FeignClient 生成的 bean 是否是 primary。

所以如果在 ApplicationContext中存在一個實現 EchoService 介面的 Bean,但是注入的時候並不會使用該Bean,因為 FeignClient 生成的 Bean 是 primary

Feign 的執行過程

瞭解了 Feign 的使用之後,接下來我們來看 Feign 構造一個 Client 的過程。

@EnableFeignClients 註解可以看到,入口在該註解上的 FeignClientsRegistrar 類上,整個鏈路是這樣的:

Feign.png

從這個鏈路上我們可以得到幾個資訊:

1.@FeignClient 註解修飾的介面最終會被轉換成 FeignClientFactoryBean 這個 FactoryBeanFactoryBean內部的 getObject 方法最終會返回一個 Proxy

2.在構造 Proxy 的過程中會根據 org.springframework.cloud.openfeign.Targeter 介面的 target 方法去構造。如果啟動了hystrix開關(feign.hystrix.enabled=true),會使用 HystrixTargeter,否則使用預設的 DefaultTargeter

3.Targeter 內部構造 Proxy 的過程中會使用 feign.Feign.Builder 去呼叫它的 build 方法構造 feign.Feign 例項(預設只有一個子類 ReflectiveFeign)。

如果啟動了 hystrix 開關(feign.hystrix.enabled=true),會使用 feign.hystrix.HystrixFeign.Builder,否則使用預設的feign.Feign.Builder

4.構造出 feign.Feign 例項之後,呼叫 newInstance 方法返回一個 Proxy

簡單看下這個 newInstance 方法內部的邏輯:

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)));
      }
    }
    // 使用 InvocationHandlerFactory 根據介面的方法資訊和 target 物件構造 InvocationHandler
    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;
  }

這裡的 InvocationHandlerFactory 是通過構造 Feign 的時候傳入的:

  • 使用原生的 DefaultTargeter: 那麼會使用 feign.InvocationHandlerFactory.Default 這個 factory,並且構造出來的 InvocationHandlerfeign.ReflectiveFeign.FeignInvocationHandler

  • 使用 hystrix 的 HystrixTargeter: 那麼會在feign.hystrix.HystrixFeign.Builder#build(feign.hystrix.FallbackFactory<?>) 方法中呼叫父類的 invocationHandlerFactory 方法傳入一個匿名的 InvocationHandlerFactory 實現類,該類內部構造出的 InvocationHandlerHystrixInvocationHandler

Sentinel 整合 Feign

理解了 Feign 的執行過程之後,Sentinel 想要整合 Feign,可以參考 Hystrix 的實現:

1.❌ 實現 Targeter 介面 SentinelTargeter很不幸,Targeter 這個介面屬於包級別的介面,在外部包中無法使用,這個 Targeter 無法使用。沒關係,我們可以沿用預設的HystrixTargeter(實際上會用 DefaultTargeter,下文 Note 有解釋)

2.✅ FeignClientFactoryBean 內部構造 Targeterfeign.Feign.Builder 的時候,都會從 FeignContext 中獲取。所以我們沿用預設的 DefaultTargeter 的時候,內部使用的 feign.Feign.Builder 可控,而且這個 Builder 不是包級別的類,可在外部使用

  • 建立 SentinelFeign.Builder 繼承 feign.Feign.Builder ,用來構造 Feign

  • SentinelFeign.Builder 內部需要獲取 FeignClientFactoryBean 中的屬性進行處理,比如獲取 fallback, name, fallbackFactory

很不幸,FeignClientFactoryBean 這個類也是包級別的類。沒關係,我們知道它存在在 ApplicationContext 中的 beanName, 拿到 bean 之後根據反射獲取屬性就行(該過程在初始化的時候進行,不會在呼叫的時候進行,所以不會影響效能)

  • SentinelFeign.Builder 呼叫 build 方法構造 Feign 的過程中,我們不需要實現一個新的 Feign,跟 hystrix 一樣沿用 ReflectiveFeign 即可,在沿用的過程中呼叫父類 feign.Feign.Builder 的一些方法進行改造即可,比如 invocationHandlerFactory 方法設定 InvocationHandlerFactorycontract 的呼叫

3.✅ 跟 hystrix 一樣實現自定義的 InvocationHandler 介面 SentinelInvocationHandler 用來處理方法的呼叫

4.✅ SentinelInvocationHandler 內部使用 Sentinel 進行保護,這個時候涉及到資源名的獲取。SentinelInvocationHandler 內部的 feign.Target 能獲取服務名資訊,feign.InvocationHandlerFactory.MethodHandler 的實現類 feign.SynchronousMethodHandler 能拿到對應的請求路徑資訊。

很不幸,feign.SynchronousMethodHandler 這個類也是包級別的類。沒關係,我們可以自定義一個 feign.Contract 的實現類 SentinelContractHolder 在處理 MethodMetadata 的過程把這些 metadata 儲存下來(feign.Contract 這個介面在 Builder 構造 Feign 的過程中會對方法進行解析並驗證)。

SentinelFeign.Builder 中呼叫 contract 進行設定,SentinelContractHolder 內部儲存一個 Contract 使用委託方式不影響原先的 Contract 過程

Note: spring-cloud-starter-openfeign 依賴內部包含了 feign-hystrix。所以是說預設使用 HystrixTargeter 這個 Targeter ,進入 HystrixTargetertarget 方法內部一看,發現有段邏輯這麼寫的:

@Override
public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context,
                    Target.HardCodedTarget<T> target) {
  if (!(feign instanceof feign.hystrix.HystrixFeign.Builder)) {
    // 如果 Builder 不是 feign.hystrix.HystrixFeign.Builder,使用這個 Builder 進行處理
    // 我們預設構造了 SentinelFeign.Builder 這個 Builder,預設使用 feign-hystrix 依賴也沒有什麼問題
    return feign.target(target);
  }
  feign.hystrix.HystrixFeign.Builder builder = (feign.hystrix.HystrixFeign.Builder) feign;
  ...
}

SentinelInvocationHandler 內部我們對資源名的處理策略是: http方法:protocol://服務名/請求路徑跟引數

比如這個 TestService:

@FeignClient(name = "test-service")
public interface TestService {
  @RequestMapping(value = "/echo/{str}", method = RequestMethod.GET)
  String echo(@PathVariable("str") String str);

  @RequestMapping(value = "/divide", method = RequestMethod.GET)
  String divide(@RequestParam("a") Integer a, @RequestParam("b") Integer b);
}
  • echo 方法對應的資源名:GET:http://test-service/echo/{str}
  • divide 方法對應的資源名:GET:http://test-service/divide

總結

1.Feign 的內部很多類都是 package 級別的,外部 package 無法引用某些類,這個時候只能想辦法繞過去,比如使用反射

2.目前這種實現有風險,萬一哪天 starter 內部使用的 Feign 相關類變成了 package 級別,那麼會改造程式碼。所以把 Sentinel 的實現放到 Feign 裡並給 Feign 官方提 pr 可能更加合適

3.Feign的處理流程還是比較清晰的,只要能夠理解其設計原理,我們就能容易地整合進去

歡迎大家對整合方案進行討論,並能給出不合理的地方,當然能提pr解決不合理的地方就更好了。

Sentinel Starter 整合 Feign 的程式碼目前已經在 github 倉庫上,但是沒未發版。預計月底發版,如果現在就想使用,可以在 pom 中引入 Spring SNAPSHOT 的 repository 或自行下載原始碼進行編譯。

最後再附上一個使用 Nacos 做服務發現和 Sentinel 做限流的 Feign 例子。

https://github.com/spring-cloud-incubator/spring-cloud-alibaba/tree/master/spring-cloud-alibaba-examples/nacos-example/nacos-discovery-example


最後,在Java技術棧微信公眾號後臺回覆:cloud,可獲取棧長整理的一系列 Spring Cloud 教程,目前大量教程還在撰寫中……