javascript
SpringCloud OpenFeign 远程HTTP服务调用用法与原理
????????在 openFeign 未出現前,Spring 提供了 RestTemplate 作為遠程服務調用的客戶端,提供了多種便捷訪問遠程 Http 服務的方法,能夠大大提高客戶端的編寫效率。由于文章內容會使用到 RestTemplate,所以這里就簡單說下。
????????一講到服務調用,我們肯定會聯想到服務的路由與負載均衡,那么我們接下來就先介紹兩種客戶端的服務負載均衡組件:LoadBalancerClient 與 Ribbon
一、SpringCloud的客戶端負載均衡:
1、客戶端負載均衡:
????????負載均衡分為客戶端負載均衡和服務端負載均衡,它們之間的區別在于:服務清單所在的位置。
????????我們通常說的負載均衡都是服務端的負載均衡,其中可以分為硬件的負載均衡和軟件負載均衡:硬件的負載均衡就是在服務器節點之間安裝用于負載均衡的設備,比如F5;軟件負載均衡則是在服務器上安裝一些具有負載均衡功能的模塊或軟件來完成請求分發的工作,比如nginx。服務端的負載均衡會在服務端維護一個服務清單,然后通過心跳檢測來剔除故障節點以保證服務清單中的節點都正常可用。
????????客戶端負載均衡指客戶端都維護著自己要訪問的服務端實例清單,而這些服務端清單來自服務注冊中心。客戶端負載均衡也需要心跳檢測維護清單服務的健康性,只不過這個工作要和服務注冊中心配合完成。
2、LoadBalancerClient:
????????LoadBalancerClient 是 SpringCloud 提供的一種負載均衡客戶端,LoadBalancerClient 在初始化時會通過 Eureka Client 向 Eureka 服務端獲取所有服務實例的注冊信息并緩存在本地,并且每10秒向 EurekaClient 發送“ ping ”,來判斷服務的可用性。如果服務的可用性發生了改變或者服務數量和之前的不一致,則更新或者重新拉取。最后,在得到服務注冊列表信息后,ILoadBalancer 根據 IRule 的策略進行負載均衡(默認策略為輪詢)。
????????當使用 LoadBalancerClient 進行遠程調用的負載均衡時,LoadBalancerClient 先通過目標服務名在本地服務注冊清單中獲取服務提供方的某一個實例,比如訂單服務需要訪問商品服務,商品服務有3個節點,LoadBalancerClient 會通過 choose() 方法獲取到3個節點中的一個服務,拿到服務的信息之后取出服務IP信息,就可以得到完整的想要訪問的IP地址和接口,最后通過 RestTempate 訪問商品服務。
2.1、springboot + LoadBalancerClient 負載均衡調用:
注:本案例需要 springboot 提前整合 nacos 作為注冊中心,但是 nacos 并非本文的重點,此處就不重點介紹,對 nacos 注冊中心有疑惑的讀者請移步這篇文章:Nacos注冊中心的部署與用法詳細介紹
2.1.1、服務提供方代碼:
//nacos注冊中心的服務名:cloud-producer-server//兩數求和@PostMapping ("getSum")public String getSum(@RequestParam (value = "num1") Integer num1, @RequestParam (value = "num2") Integer num2){return "兩數求和結果=" + (num1 + num2);}2.1.2、服務消費方代碼:
(1)指定服務,通過 LoadBalancerClient 自動獲取某個服務實例與請求地址
@Component public class LoadBalancerUtil {// 注入LoadBalancerClient@AutowiredLoadBalancerClient loadBalancerClient;/*** 通過 LoadBalancer 獲取提供服務的host與ip*/public String getService(String serviceId){//獲取實例服務中的某一個服務ServiceInstance instance = loadBalancerClient.choose(serviceId);//獲取服務的ip地址和端口號String host = instance.getHost();int port = instance.getPort();//格式化最終的訪問地址return String.format("http://%s:%s", host, port);} }(2)通過 RestTemplate 請求遠程服務地址并接收返回值:
@RestController @RequestMapping (value = "api/invoke") public class InvokeController { @Autowiredprivate LoadBalancerUtil loadBalancerUtil;/*** 使用 SpringCloud 的負載均衡策略組件 LoadBalancerClient 進行遠程服務調用*/@GetMapping ("getByLoadBalancer")public String getByLoadBalancer(Integer num1, Integer num2){String hostAndIp = loadBalancerUtil.getService("cloud-producer-server");//打印服務的請求地址與端口,方便測試負載功能System.out.println(hostAndIp);String url = hostAndIp + "/cloud-producer-server/getSum";MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();params.add("num1", num1);params.add("num2", num2);RestTemplate restTemplate = new RestTemplate();String result = restTemplate.postForObject(url, params, String.class);return result;} }????????多次訪問服務消費方的 api/invoke/getByLoadBalancer 接口,并且通過打印出來的 hostAndIp 信息,可以看出 LoadBalancerClient 是輪詢調用服務提供方的,這也是 LoadBalancerClient 的默認負載均衡策略
2.2、LoadBalancerClient 原理:
LoadBalancerClient 的原理請讀者移步這篇文章:SpringCloud LoadBalancerClient 負載均衡原理
3、ribbon:
????????Ribbon 負載組件的內部就是集成了 LoadBalancerClient 負載均衡客戶端,所以 Ribbon 負載均衡的原理本質也跟上面介紹的 LoadBalancerClient 原理一致,負載均衡器 Ribbon 默認會通過 Eureka Client 向 Eureka 服務端的服務注冊列表中獲取服務的信息,并緩存一份在本地 JVM 中,根據緩存的服務注冊列表信息,可以通過 LoadBalancerClient 來選擇不同的服務實例,從而實現負載均衡。
????????基本用法就是注入一個 RestTemplate,并使用 @LoadBalance 注解標注 RestTemplate,從而使 RestTemplate 具備負載均衡的能力。當 Spring 容器啟動時,使用 @LoadBalanced 注解修飾的 RestTemplate 會被添加攔截器 LoadBalancerInterceptor,攔截器會攔截 RestTemplate 發送的請求,轉而執行 LoadBalancerInterceptor 中的 intercept() 方法,并在 intercept() 方法中使用 LoadBalancerClient 處理請求,從而達到負載均衡的目的。
????????那么 RestTemplate 添加 @LoadBalanced 注解后,為什么會被攔截呢?這是因為 LoadBalancerAutoConfiguration 類維護了一個被 @LoadBalanced 修飾的 RestTemplate 列表,在初始化過程中,通過調用 customizer.customize(restTemplate) 方法為 RestTemplate 添加了 LoadBalancerInterceptor 攔截器,該攔截器中的方法將遠程服務調用的方法交給了 LoadBalancerClient 去處理,從而達到了負載均衡的目的。
3.1、springboot + Ribbon 負載均衡調用:
通過 Spring Cloud Ribbon 的封裝,我們在微服務架構中使用客戶端負載均衡非常簡單,只需要兩步:
- ① 服務提供者啟動服務實例并注冊到服務注冊中心
- ② 服務消費者直接使用被 @LoadBalanced 注解修飾的 RestTemplate 來實現面向服務的接口調用
3.1.1、服務提供方代碼:
//nacos注冊中心的服務名:cloud-producer-server//兩數求和@PostMapping ("getSum")public String getSum(@RequestParam (value = "num1") Integer num1, @RequestParam (value = "num2") Integer num2){return "兩數求和結果=" + (num1 + num2);}3.1.2、服務消費方代碼:
(1)使用 @LoadBalanced 注解修飾的 RestTemplate:
@LoadBalanced 注解用于開啟負載均衡,標記 RestTemplate 使用 LoadBalancerClient 配置
import org.springframework.cloud.client.loadbalancer.LoadBalanced; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.client.RestTemplate;@Configuration public class RestConfig {/*** 創建restTemplate對象。* LoadBalanced注解表示賦予restTemplate使用Ribbon的負載均衡的能力(一定要加上注解,否則無法遠程調用)*/@Bean@LoadBalancedpublic RestTemplate restTemplate(){return new RestTemplate();} }(2)通過 RestTemplate 請求遠程服務地址并接收返回值
@RestController @RequestMapping (value = "api/invoke") public class InvokeController {@Autowiredprivate RestTemplate restTemplate;/*** 使用 RestTemplate 進行遠程服務調用,并且使用 Ribbon 進行負載均衡*/@ApiOperation (value = "RestTemplate", notes = "使用RestTemplate進行遠程服務調用,并使用Ribbon進行負載均衡")@GetMapping ("getByRestTemplate")public String getByRestTemplate(Integer num1, Integer num2){//第一個cloud-producer-server代表在nacos注冊中心中的服務名,第二個cloud-producer-server代表contextPath配置的項目路徑String url = "http://cloud-producer-server/cloud-producer-server/getSum";MultiValueMap<String, Object> params = new LinkedMultiValueMap<>();params.add("num1", num1);params.add("num2", num2);//通過服務名的方式調用遠程服務(非ip端口)return restTemplate.postForObject(url, params, String.class);} }????????默認情況下,Ribbon 也是使用輪詢作為負載均衡策略,那么處理輪詢策略,Ribbon 還有哪些負載均衡策略呢?
3.2、Ribbon 的七種負載均衡策略:
????????我們打開 com.netflix.loadbalancer.IRule 接口,該接口的實現類主要用于定義負載均衡策略,我們找到它所有的實現類,如下:
?(1)隨機策略 RandomRule:隨機數選擇服務列表中的服務節點Server,如果當前節點不可用,則進入下一輪隨機策略,直到選到可用服務節點為止
(2)輪詢策略 RoundRobinRule:按照接收的請求順序,逐一分配到不同的后端服務器
(3)重試策略 RetryRule:在選定的負載均衡策略機上重試機制,在一個配置時間段內當選擇Server不成功,則一直嘗試使用 subRule 的方式選擇一個可用的server;
(4)可用過濾策略 PredicateBaseRule:過濾掉連接失敗 和 高并發連接 的服務節點,然后從健康的服務節點中以線性輪詢的方式選出一個節點返回
(5)響應時間權重策略 WeightedRespinseTimeRule:根據服務器的響應時間分配一個權重weight,響應時間越長,weight越小,被選中的可能性越低。主要通過后臺線程定期地從 status 里面讀取平均響應時間,為每個 server 計算一個 weight
(6)并發量最小可用策略 BestAvailableRule:選擇一個并發量最小的服務節點 server。ServerStats 的 activeRequestCount 屬性記錄了 server 的并發量,輪詢所有的server,選擇其中 activeRequestCount 最小的那個server,就是并發量最小的服務節點。該策略的優點是可以充分考慮每臺服務節點的負載,把請求打到負載壓力最小的服務節點上。但是缺點是需要輪詢所有的服務節點,如果集群數量太大,那么就會比較耗時。
(7)區域權重策略 ZoneAvoidanceRule:綜合判斷 server 所在區域的性能 和 server 的可用性,使用 ZoneAvoidancePredicate 和 AvailabilityPredicate 來判斷是否選擇某個server,前一個判斷判定一個zone的運行性能是否可用,剔除不可用的zone(的所有server),AvailabilityPredicate 用于過濾掉連接數過多的Server。
二、什么是openFeign:
????????微服務架構中,由于對服務粒度的拆分致使服務數量變多,而作為 Web 服務的調用端方,除了需要熟悉各種 Http 客戶端,比如 okHttp、HttpClient 組件的使用,而且還要顯式地序列化和反序列化請求和響應內容,從而導致出現很多樣板代碼,開發起來很痛苦。為了解決這個問題,Feign 誕生了,那么 Feign 是什么呢?
????????Feign 就是一個 Http 客戶端的模板,目標是減少 HTTP API 的復雜性,希望能將 HTTP 遠程服務調用做到像 RPC 一樣易用。Feign 集成 RestTemplate、Ribbon 實現了客戶端的負載均衡的 Http 調用,并對原調用方式進行了封裝,使得開發者不必手動使用 RestTemplate 調用服務,而是聲明一個接口,并在這個接口中標注一個注解即可完成服務調用,這樣更加符合面向接口編程的宗旨,客戶端在調用服務端時也不需要再關注請求的方式、地址以及是 forObject 還是 forEntity,結構更加明了,耦合也更低,簡化了開發。但 Feign 已經停止迭代了,所以本篇文章我們也不過多的介紹,而在 Feign 的基礎上,又衍生出了 openFeign,那么 openFeign 又是什么呢?
????????openFeign 在 Feign 的基礎上支持了 SpringMVC 的注解,如 @RequestMapping 等。OpenFeign 的 @FeignClient 可以解析 SpringMVC 的 @RequestMapping 注解下的接口,并通過動態代理的方式產生實現類,實現類中做負載均衡并調用其他服務。
????????總的就是,openFeign 作為微服務架構下服務間調用的解決方案,是一種聲明式、模板化的 HTTP 的模板,使 HTTP 請求就像調用本地方法一樣,通過 openFeign 可以替代基于 RestTemplate 的遠程服務調用,并且默認集成了 Ribbon 進行負載均衡。
openFeign 與 Ribbon 的聯系:https://www.cnblogs.com/unchain/p/13405814.html
三、Springboot 整合 openFeign:
?注:本案例需要 springboot 提前整合 nacos 作為注冊中心,但是 nacos 并非本文的重點,此處就不重點介紹,對 nacos 注冊中心有疑惑的讀者請移步這篇文章:Nacos注冊中心的部署與用法詳細介紹
1、創建服務提供者 provider:
(1)項目配置:
# 服務在nacos中的服務名 spring.application.name = openFeign-provider# nacos注冊中心配置 nacos.url = 120.76.129.106:80 nacos.namespace = 856a40d7-6548-4494-bdb9-c44491865f63 spring.cloud.nacos.discovery.server-addr = ${nacos.url} spring.cloud.nacos.discovery.namespace = ${nacos.namespace} spring.cloud.nacos.discovery.register-enabled = true注意:此處的 spring.application.name 指定的名稱將會在 openFeign 接口調用中使用。
2、創建服務消費者 consumer:
(1)引入 openFeign 相關依賴
<!-- 引入openFeign進行遠程服務調用 --><dependency><groupId>org.springframework.cloud</groupId><artifactId>spring-cloud-starter-openfeign</artifactId></dependency>(2)開啟 openFeign 功能:
在 Springboot 應用的主啟動類上使用注解 @EnableFeignClients 開啟 openFeign 功能,如下:
import org.mybatis.spring.annotation.MapperScan; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.discovery.EnableDiscoveryClient; import org.springframework.cloud.openfeign.EnableFeignClients;/*** SpringBoot啟動類:* EnableFeignClients:啟動OpenFeign客戶端* EnableDiscoveryClient:啟動服務發現*/ @EnableFeignClients @EnableDiscoveryClient @SpringBootApplication @MapperScan(basePackages = "com.eebbk.*.dao") public class ConsumerApplication {public static void main(String[] args){SpringApplication.run(ConsumerApplication.class, args);} }(3)新建 FeignClient 接口:
新建一個 openFeign 接口,使用 @FeignClient 注解標注,如下:
@FeignClient(value = "openFeign-provider") public interface OpenFeignService { }注意:注解 @FeignClient 中的 value 屬性指定了服務提供者在 nacos 注冊中心的服務名
(4)新建 Controller 調試:
新建一個controller用來調試接口,直接調用openFeign的接口,如下:
@RestController @RequestMapping("/openfeign") public class OpenFeignController { }好了,至此一個openFeign的微服務就搭建好了,并未實現具體的功能,下面一點點實現。
3、openFeign 的傳參:
????????開發中接口傳參的方式有很多,但是在 openFeign 中的傳參是有一定規則的,下面詳細介紹四種常見的傳參方式。
3.1、傳遞JSON數據:
provider 接口中 JSON 傳參方法如下:
@RestController @RequestMapping("/openfeign/provider") public class OpenFeignProviderController {@PostMapping("/order2")public Order createOrder2(@RequestBody Order order){return order;} }consumer消費者openFeign代碼如下:
@FeignClient(value = "openFeign-provider") public interface OpenFeignService {/*** 參數默認是@RequestBody標注的,這里的@RequestBody可以不填* 方法名稱任意*/@PostMapping("/openfeign/provider/order2")Order createOrder2(@RequestBody Order order); }注意:openFeign 默認的傳參方式就是JSON傳參(@RequestBody),因此定義接口的時候可以不用@RequestBody注解標注,不過為了規范,一般都填上。
3.2、POJO表單傳參:
provider服務提供者代碼如下:
@RestController @RequestMapping("/openfeign/provider") public class OpenFeignProviderController {@PostMapping("/order1")public Order createOrder1(Order order){return order;} }consumer消費者openFeign代碼如下:
@FeignClient(value = "openFeign-provider") public interface OpenFeignService {/*** 如果通過POJO表單傳參的,使用@SpringQueryMap標注*/@PostMapping("/openfeign/provider/order1")Order createOrder1(@SpringQueryMap Order order); }3.3、URL中攜帶參數:
此種方式針對restful方式中的GET請求,也是比較常用請求方式。
provider服務提供者代碼如下:
@RestController @RequestMapping("/openfeign/provider") public class OpenFeignProviderController {@GetMapping("/test/{id}")public String test(@PathVariable("id")Integer id){return "accept one msg id="+id; }consumer消費者openFeign接口如下:
@FeignClient(value = "openFeign-provider") public interface OpenFeignService {@GetMapping("/openfeign/provider/test/{id}")String get(@PathVariable("id")Integer id); }使用注解 @PathVariable 接收url中的占位符,這種方式很好理解。
3.4、普通表單參數:
此種方式傳參不建議使用,但是也有很多開發在用。
provider服務提供者代碼如下:
@RestController @RequestMapping("/openfeign/provider") public class OpenFeignProviderController {@PostMapping("/test2")public String test2(String id,String name){return MessageFormat.format("accept on msg id={0},name={1}",id,name);} }consumer消費者openFeign接口傳參如下:
@FeignClient(value = "openFeign-provider") public interface OpenFeignService {/*** 必須要@RequestParam注解標注,且value屬性必須填上參數名* 方法參數名可以任意,但是@RequestParam注解中的value屬性必須和provider中的參數名相同*/@PostMapping("/openfeign/provider/test2")String test(@RequestParam("id") String arg1,@RequestParam("name") String arg2); }4、設置超時時間:
想要理解超時處理,先看一個例子:我將provider服務接口睡眠3秒鐘,接口如下:
@PostMapping("/test2") public String test2(String id,String name) throws InterruptedException {Thread.sleep(3000);return MessageFormat.format("accept on msg id={0},name={1}",id,name); }此時,我們調用consumer的openFeign接口返回結果如下圖的超時異常:
?????????openFeign 其實是有默認的超時時間的,默認分別是連接超時時間 10秒、讀超時時間 60秒,源碼在 feign.Request.Options#Options() 這個方法中,如下圖:
????????那么為什么我們只設置了睡眠3秒就報超時呢?其實 openFeign 集成了 Ribbon,Ribbon 的默認超時連接時間、讀超時時間都是是1秒,源碼在 org.springframework.cloud.openfeign.ribbon.FeignLoadBalancer#execute() 方法中,如下圖:
????????源碼大致意思:如果openFeign沒有設置對應得超時時間,那么將會采用Ribbon的默認超時時間。理解了超時設置的原理,由之產生兩種方案也是很明了了,如下:
- 設置openFeign的超時時間
- 設置Ribbon的超時時間
4.1、設置Ribbon的超時時間(不推薦)
ribbon:# 值的是建立鏈接所用的時間,適用于網絡狀況正常的情況下, 兩端鏈接所用的時間ReadTimeout: 5000# 指的是建立鏈接后從服務器讀取可用資源所用的時間ConectTimeout: 50004.2、設置Ribbon的超時時間
openFeign設置超時時間非常簡單,只需要在配置文件中配置,如下:
feign:client:config:## default 設置的全局超時時間,指定服務名稱可以設置單個服務的超時時間default:connectTimeout: 5000readTimeout: 5000????????default設置的是全局超時時間,對所有的openFeign接口服務都生效,但是正常的業務邏輯中可能有其實 openFeign 接口的調用需要單獨配置一個超時時間,比如下面我們就單獨給 serviceC 這個服務單獨配置了一個超時時間,單個配置的超時時間將會覆蓋全局配置:
feign:client:config:## default 設置的全局超時時間,指定服務名稱可以設置單個服務的超時時間default:connectTimeout: 5000readTimeout: 5000## 為serviceC這個服務單獨配置超時時間serviceC:connectTimeout: 30000readTimeout: 300005、替換的 HTTP 客戶端:
????????openFeign 默認使用的是 JDK 原生的 URLConnection 發送 HTTP 請求,沒有連接池,但是對每個地址會保持一個長連接,即利用 HTTP 的 persistence connection。在生產環境中,通常不使用默認的 http client,通常有兩種選擇:使用 ApacheHttpClient 或者 OkHttp,兩者各有千秋,下面我們演示下如何使用 ApacheHttpClient 替換原生的 http client
5.1、添加ApacheHttpClient依賴:
<!-- 使用 Apache HttpClient 替換 Feign原生httpclient--><dependency><groupId>org.apache.httpcomponents</groupId><artifactId>httpclient</artifactId></dependency><dependency><groupId>io.github.openfeign</groupId><artifactId>feign-httpclient</artifactId></dependency>????????為什么要添加上面的依賴呢?從源碼中不難看出,請看org.springframework.cloud.openfeign.FeignAutoConfiguration.HttpClientFeignConfiguration 這個類,代碼如下: ?
????????上述紅色框中的生成條件,其中的 @ConditionalOnClass(ApacheHttpClient.class),必須要有 ApacheHttpClient 這個類才會生效,并且 feign.httpclient.enabled 這個配置要設置為 true。
5.2、配置文件中開啟:
在配置文件中要配置開啟,代碼如下:
feign:client:httpclient:# 開啟 Http Clientenabled: true5.3、如何驗證?
????????其實很簡單,在 feign.SynchronousMethodHandler#executeAndDecode() 這個方法中可以清楚的看出調用哪個client,如下圖:
上圖中可以看到最終調用的是 ApacheHttpClient。
6、開啟日志增強:
????????openFeign 雖然提供了日志增強功能,但默認是不顯示任何日志的,不過開發者在調試階段可以自己配置日志的級別。
openFeign 的日志級別如下:
- NONE:默認的,不顯示任何日志;
- BASIC:僅記錄請求方法、URL、響應狀態碼及執行時間;
- HEADERS:除了BASIC中定義的信息之外,還有請求和響應的頭信息;
- FULL:除了HEADERS中定義的信息之外,還有請求和響應的正文及元數據。
配置起來也很簡單,步驟如下:
6.1、配置類中配置日志級別
需要自定義一個配置類,在其中設置日志級別,如下:
注意:這里的logger是feign包里的。
6.2、yaml文件中設置接口日志級別:
logging:level:cn.myjszl.service: debug????????這里的 cn.myjszl.service 是 openFeign 接口所在的包名,當然你也可以配置一個特定的openFeign接口。
6.3、效果演示
????????上述步驟將日志設置成了 FULL,此時發出請求,日志效果如下圖:
日志中詳細的打印出了請求頭、請求體的內容。
7、通訊優化:
在講如何優化之前先來看一下GZIP 壓縮算法
7.1、GZIP壓縮算法:
????????gzip是一種數據格式,采用deflate算法壓縮數據;當GZIP算法壓縮到一個純文本數據時,效果是非常明顯的,大約可以減少70%以上的數據大小。
????????網絡數據經過壓縮后實際上降低了網絡傳輸的字節數,最明顯的好處就是可以加快網頁加載的速度。網頁加載速度加快的好處不言而喻,除了節省流量,改善用戶的瀏覽體驗外,另一個潛在的好處是GZIP與搜索引擎的抓取工具有著更好的關系。例如 Google就可以通過直接讀取GZIP文件來比普通手工抓取更快地檢索網頁。
GZIP壓縮傳輸的原理如下圖:
按照上圖拆解出的步驟如下:
- 客戶端向服務器請求頭中帶有:Accept-Encoding:gzip,deflate 字段,向服務器表示,客戶端支持的壓縮格式(gzip或者deflate),如果不發送該消息頭,服務器是不會壓縮的。
- 服務端在收到請求之后,如果發現請求頭中含有 Accept-Encoding 字段,并且支持該類型的壓縮,就對響應報文壓縮之后返回給客戶端,并且攜帶 Content-Encoding:gzip 消息頭,表示響應報文是根據該格式壓縮過的。
- 客戶端接收到響應之后,先判斷是否有 Content-Encoding 消息頭,如果有,按該格式解壓報文。否則按正常報文處理。
7.2、openFeign開啟GZIP壓縮:
????????openFeign支持請求/響應開啟GZIP壓縮,整體的流程如下圖:
????????上圖中涉及到GZIP傳輸的只有兩塊,分別是 Application client -> Application Service、 Application Service->Application client。
????????注意:openFeign支持的GZIP僅僅是在openFeign接口的請求和響應,即openFeign消費者調用服務提供者的接口。
????????openFeign開啟GZIP步驟也是很簡單,只需要在配置文件中開啟如下配置:
feign:## 開啟壓縮compression:request:enabled: true## 開啟壓縮的閾值,單位字節,默認2048,即是2k,這里為了演示效果設置成10字節min-request-size: 10mime-types: text/xml,application/xml,application/jsonresponse:enabled: true上述配置完成之后,發出請求,可以清楚看到請求頭中已經攜帶了GZIP壓縮,如下圖:
四、OpenFeign 的原理:
文章該部分轉自:https://blog.csdn.net/manzhizhen/article/details/110013311
? ? ? ? Feign 只是對 HTTP 調用組件進行了易用性封裝,底層還是使用我們常見的 OkHttp、HttpClient 等組件,你瞧:
????????Feign 的目標之一就讓這些 HTTP 客戶端更好用,使用方式更統一,更像RPC。要想了解 Spring Cloud OpenFeign 整體實現原理,我們需要回答如下四個問題:
- (1)@FeignClient 如何根據接口生成實現(代理)類的?
- (2)生成的實現(代理)類是如何適配各種HTTP組件的?
- (3)生成的實現(代理)類如何實現HTTP請求應答序列化和反序列化的?
- (4)生成的實現(代理)類是如何注入到Spring容器中的?
接下來,我們通過解讀源碼方式,逐一解答上述問題。
1、@FeignClient 如何根據接口生成實現類的?
? ? ? ? Fegin 使用的是 JDK 動態代理技術來生成實現類的,因此 Feign 的使用必須要有接口。但還有一個小問題,我們回看上面提到的 OpenFeignService?接口,里面有多個方法,每個方法有 @RequestMapping ,意味著這些方法可以映射到不同的遠端HTTP路徑,所以給整個?OpenFeignService?接口做代理時,代理類的方法必須知道對應到哪個遠端HTTP路徑,雖然我們可以在 java.lang.reflect.InvocationHandler#invoke 的方法入參 Method 中去解析 @RequestMapping 拿url,但需要注意的是,大多數開源框架很忌諱在運行時高頻使用JDK的反射,因為這樣非常影響執行效率,Dubbo 的 Provider 端也不是用反射來調用本地方法的,所以在 Feign 使用 JDK動態代理技術時,需要提前將接口(例如 OpenFeignService)帶 @RequestMapping 方法解析出來。為了探究這塊的具體實現,我們移步原生 Feign 的 feign-core 包的核心類 ReflectiveFeign:
package feign;import feign.InvocationHandlerFactory.MethodHandler;import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; import java.util.LinkedHashMap; import java.util.Map;import static feign.Util.checkNotNull;public class ReflectiveFeign extends Feign {private final InvocationHandlerFactory factory;/*** creates an api binding to the {@code target}. As this invokes reflection, care should be taken* to cache the result.* 注意:這里我們隱藏了大部分非核心的代碼*/@Overridepublic <T> T newInstance(Target<T> target) {// 將@FeignClient的接口類所有帶@RequestMapping方法解析出來,map的key為方法簽名,MethodHander為包裝過的方法調用HanderMap<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();// 根據nameToHandler來組裝methodToHandlerfor (Method method : target.type().getMethods()) {methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method)));}// 這里通過一個InvocationHandler工廠來參見JDK動態代理中的InvocationHandler(既下面的FeignInvocationHandler)InvocationHandler handler = factory.create(target, methodToHandler);// 創建JDK動態代理生成代理類,這個類在Spring Cloud OpenFeign中會被注冊到Spring容器中T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),new Class<?>[]{target.type()}, handler);return proxy;}/*** Feign專用FeignInvocationHandler*/static class FeignInvocationHandler implements InvocationHandler {private final Target target;// 這里保持的是我們在newInstance解析出來的@RequestMapping(在原生Feign中是@RequestLine)方法和方法處理器的映射關系private final Map<Method, MethodHandler> dispatch;FeignInvocationHandler(Target target, Map<Method, MethodHandler> dispatch) {this.target = checkNotNull(target, "target");this.dispatch = checkNotNull(dispatch, "dispatch for %s", target);}@Overridepublic Object invoke(Object proxy, Method method, Object[] args) throws Throwable {// 常規的用于AOP的動態代理會選擇調用target的method方法,但我們這里由于沒有自定義的接口實現類,所以直接調用我們包裝過的對應MethodHandlerreturn dispatch.get(method).invoke(args);}} }這里順便補充下?MethodHandler 接口的定義:
package feign;import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.util.Map;/*** Controls reflective method dispatch.*/ public interface InvocationHandlerFactory {InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch);/*** Like {@link InvocationHandler#invoke(Object, java.lang.reflect.Method, Object[])}, except for a* single method.*/interface MethodHandler {Object invoke(Object[] argv) throws Throwable;}static final class Default implements InvocationHandlerFactory {@Overridepublic InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {return new ReflectiveFeign.FeignInvocationHandler(target, dispatch);}} }????????看 ReflectiveFeign 的類名我們就知道它和反射有關,ReflectiveFeign 借助于JDK動態代理,根據我們的業務接口生成對應的代理類,這個代理類會根據調用的方法來直接找到已經提前準備好的對應的 MethodHandler,直接調用即可完成Feign的使命,根據上面的使用方法,我們不難猜到 MethodHandler 里面有HTTP調用的相關信息(這些信息之前是在接口方法定義的 @RequestMapping 或 @RequestLine 之中),而且 MethodHandler#invoke 會完成真正的HTTP調用并將結果反序列化成原接口方法的返回值對象。
2、生成的實現(代理)類是如何適配各種HTTP組件的?
????????這個問題應該由 Feign 來回答,而不是 Spring Cloud OpenFeign,Feign 的 feign-core 模塊中有一個 Client 接口,專門用來給各個HTTP組件提供接入接口,我們看其定義:
package feign;import feign.Request.Options;import java.io.IOException;/*** Submits HTTP {@link Request requests}. Implementations are expected to be thread-safe.* 注意,為了展現方便,我們裁剪了部分代碼*/ public interface Client {/*** Executes a request against its {@link Request#url() url} and returns a 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; }????????各個 HTTP客戶端組件的適配模塊(例如feign-okhttp、feign-httpclient等)只需要實現該接口就可以和 Feign 打通,而在原生的 Feign 中,選擇何種HTTP組件是自己選擇的,比如我們想使用OkHttpClient,在Consumer端可以這樣:
public class Example {public static void main(String[] args) {String response = Feign.builder().client(new OkHttpClient()).target(ProviderDemoService.class, "https://xxxx");} }????????Spring Cloud 遵循?Spring Boot 的“約定優于配置”的原則,通過條件注解,實現了通過當前項目的依賴包決定使用哪個 HTTP 組件,詳見 Spring Cloud OpenFeign 中的 org.springframework.cloud.openfeign.FeignAutoConfiguration:
@Configuration(proxyBeanMethods = false) @ConditionalOnClass(Feign.class) @EnableConfigurationProperties({FeignClientProperties.class, FeignHttpClientProperties.class}) @Import(DefaultGzipDecoderConfiguration.class) public class FeignAutoConfiguration {@Configuration(proxyBeanMethods = false)@ConditionalOnClass(ApacheHttpClient.class)@ConditionalOnMissingBean(CloseableHttpClient.class)@ConditionalOnProperty(value = "feign.httpclient.enabled", matchIfMissing = true)protected static class HttpClientFeignConfiguration {private final Timer connectionManagerTimer = new Timer("FeignApacheHttpClientConfiguration.connectionManagerTimer", true);@Autowired(required = false)private RegistryBuilder registryBuilder;private CloseableHttpClient httpClient;@Bean@ConditionalOnMissingBean(HttpClientConnectionManager.class)public HttpClientConnectionManager connectionManager(ApacheHttpClientConnectionManagerFactory connectionManagerFactory,FeignHttpClientProperties httpClientProperties) {// 略}@Beanpublic CloseableHttpClient httpClient(ApacheHttpClientFactory httpClientFactory,HttpClientConnectionManager httpClientConnectionManager,FeignHttpClientProperties httpClientProperties) {// 略}@Bean@ConditionalOnMissingBean(Client.class)public Client feignClient(HttpClient httpClient) {return new ApacheHttpClient(httpClient);}}@Configuration(proxyBeanMethods = false)@ConditionalOnClass(OkHttpClient.class)@ConditionalOnMissingBean(okhttp3.OkHttpClient.class)@ConditionalOnProperty("feign.okhttp.enabled")protected static class OkHttpFeignConfiguration {private okhttp3.OkHttpClient okHttpClient;@Bean@ConditionalOnMissingBean(ConnectionPool.class)public ConnectionPool httpClientConnectionPool(FeignHttpClientProperties httpClientProperties,OkHttpClientConnectionPoolFactory connectionPoolFactory) {Integer maxTotalConnections = httpClientProperties.getMaxConnections();Long timeToLive = httpClientProperties.getTimeToLive();TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);}@Beanpublic okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory, ConnectionPool connectionPool,FeignHttpClientProperties httpClientProperties) {// 略}@Bean@ConditionalOnMissingBean(Client.class)public Client feignClient(okhttp3.OkHttpClient client) {return new OkHttpClient(client);}} }????????從上面各種復雜的條件注解來看,如果我們項目中引入了 feign-httpclient 包(即ApacheHttpClient),并且配置了“feign.httpclient.enable”的值為 true 時,那么就會使用HttpClient,其他的HTTP組件也是類似的方式來判斷和加載。
3、生成的實現(代理)類如何實現HTTP請求應答序列化和反序列化的?
原生的Feign允許你添加額外的解碼器,官方給出了Consumer的例子:
public class Example {public static void main(String[] args) {// 這里假定ProviderDemoService中有一個返回MyResponse的方法MyResponse response = Feign.builder().decoder(new GsonDecoder()).client(new OkHttpClient()).target(ProviderDemoService.class, "https://xxxx");} }為了能做到這一點,原生Feign提供了?Decoder?和?Encoder?兩個接口(本文我們只重點關注解碼部分):
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}.** @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; }public interface Encoder {/*** Converts objects to an appropriate representation in the template.** @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; }????????換成 Spring Cloud OpenFeign 的話,就得和Spring的Web體系打通了,這里就不得不提一個構造類即?FeignClientsConfiguration:
// 注意:為了演示方便,對其進行了代碼裁剪 @Configuration(proxyBeanMethods = false) public class FeignClientsConfiguration {@Autowired// 這里將Spring Web的消息轉換器機制注入進來private ObjectFactory<HttpMessageConverters> messageConverters;@Bean@ConditionalOnMissingBean// 構造解碼Decoder的Spring Beanpublic Decoder feignDecoder() {// 這里的SpringDecoder實現了Feign的Decoder接口,并且將Spring Web的消息轉換器設置到SpringDecoder來使用return new OptionalDecoder(new ResponseEntityDecoder(new SpringDecoder(this.messageConverters)));}@Bean@ConditionalOnMissingBean// 構造編碼Encoder的Spring Beanpublic Encoder feignEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider) {return springEncoder(formWriterProvider);}private Encoder springEncoder(ObjectProvider<AbstractFormWriter> formWriterProvider) {AbstractFormWriter formWriter = formWriterProvider.getIfAvailable();if (formWriter != null) {return new SpringEncoder(new SpringPojoFormEncoder(formWriter), this.messageConverters);}else {return new SpringEncoder(new SpringFormEncoder(), this.messageConverters);}} }那我們看看?SpringDecoder?拿到Spring Web的解碼器后如何使用:
// 注意:裁剪了部分代碼 public class SpringDecoder implements Decoder {private ObjectFactory<HttpMessageConverters> messageConverters;public SpringDecoder(ObjectFactory<HttpMessageConverters> messageConverters) {this.messageConverters = messageConverters;}@Overridepublic Object decode(final Response response, Type type) throws IOException, FeignException {if (type instanceof Class || type instanceof ParameterizedType || type instanceof WildcardType) {HttpMessageConverterExtractor<?> extractor = new HttpMessageConverterExtractor(type,this.messageConverters.getObject().getConverters());// 直接使用了。。。return extractor.extractData(new FeignResponseAdapter(response));}throw new DecodeException(response.status(), "type is not an instance of Class or ParameterizedType: " + type,response.request());} }到此為止,相信你對編解碼這塊已經有一定的了解。
4、生成的實現(代理)類是如何注入到Spring容器中的?
????????Spring Cloud OpenFeign 如何將動態生成的代理類和Spring容器打通?還記得我們前面說的 @EnableFeignClients 嗎?這時需要我們在使用 Spring Cloud OpenFeign 時顯式的在一個能被 Spring 容器掃到并加載的類上使用的,@EnableFeignClients 的定義如下:
@Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Documented @Import(FeignClientsRegistrar.class) public @interface EnableFeignClients {// 注解內容省略 }????????就是這里的 @Import 提前加載 Spring Bean 的方式,觸發了 FeignClientRegistrar 的初始化,而 FeignClientRegistrar 由于實現了 ImportBeanDefinitionRegistrar 接口,我們知道在處理 @Configuration 類時可以通過 Import 注冊其他 Spring Bean 定義的能力,而前面說過,我們還不知道哪些接口使用了 @FeignClient,所以在 FeignClientRegistrar 中我們需要做的就是掃描某些路徑(該路徑由配置Spring掃描路徑包括@EnableFeignClients中配置的路徑)的接口類,識別對應的 @FeignClient ,給這些接口類創建代理對象。而為了把這些代理對象注入到Spring 容器中,所以還得借助 FactoryBean 的能力。我們先看下 ImportBeanDefinitionRegistrar 的實現:
// 注意:裁剪了大量代碼 class FeignClientsRegistrar implements ImportBeanDefinitionRegistrar, ResourceLoaderAware, EnvironmentAware {private ResourceLoader resourceLoader;private Environment environment;@Overridepublic void setResourceLoader(ResourceLoader resourceLoader) {this.resourceLoader = resourceLoader;}@Overridepublic void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {// 獲取 @EnableFeignClients 上的相關屬性并用這些屬性做一些基本配置Bean的注冊registerDefaultConfiguration(metadata, registry);// 注冊BeanregisterFeignClients(metadata, registry);}private void registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {// 略}public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) {LinkedHashSet<BeanDefinition> candidateComponents = new LinkedHashSet<>();Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName());final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients");if (clients == null || clients.length == 0) {// 獲取包路徑下的掃描器ClassPathScanningCandidateComponentProvider scanner = getScanner();scanner.setResourceLoader(this.resourceLoader);scanner.addIncludeFilter(new AnnotationTypeFilter(FeignClient.class));Set<String> basePackages = getBasePackages(metadata);for (String basePackage : basePackages) {// 將所有 @FeignClient 的接口的BeanDefinition拿到candidateComponents.addAll(scanner.findCandidateComponents(basePackage));}} else {for (Class<?> clazz : clients) {candidateComponents.add(new AnnotatedGenericBeanDefinition(clazz));}}for (BeanDefinition candidateComponent : candidateComponents) {if (candidateComponent instanceof AnnotatedBeanDefinition) {AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();// 對,這里要求必須是接口Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface");Map<String, Object> attributes = annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName());// 根據這些屬性和接口來注冊FeignClient BeanregisterFeignClient(registry, annotationMetadata, attributes);}}}private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata,Map<String, Object> attributes) {String className = annotationMetadata.getClassName();// 使用FactoryBean,將Bean的具體生成過程收攏到FeignClientFactoryBean之中BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);definition.addPropertyValue("type", className);definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE);AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[]{alias});// 將這個使用了 @FeignClient 的接口的工廠Bean的 BeanDefinition 注冊到Spring容器中BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);} }????????可以看出,關鍵邏輯又回到 FeignClientFactoryBean 拿到業務接口、@EnableFeignClient 和 @FeignClient 的數據后如何去構造代理類了,而 FeignClientFactoryBean 內部其實使用的是原生 Feign 的 API 來構建代理對象。
參考文章:openFeign奪命連環9問,這誰受得了?
總結
以上是生活随笔為你收集整理的SpringCloud OpenFeign 远程HTTP服务调用用法与原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Nacos配置中心用法详细介绍
- 下一篇: Java虚拟机:垃圾回收机制与垃圾收集器