Feign聲明式REST調用
OpenFeign是Netflix 開發的聲明式、模板化的HTTP請求客戶端,可以更加便捷、優雅地調用http api。
OpenFeign會根據帶有注解的函數信息構建出網絡請求的模板,在發送網絡請求之前,OpenFeign會將函數的參數值設置到這些請求模板中
feign主要是構建微服務消費端
只要使用OpenFeign提供的注解修飾定義網絡請求的接口類,就可以使用該接口的實例發送RESTful的網絡請求
還可以集成Ribbon和Hystrix,提供負載均衡和斷路器。
Feign
- 英文表意為“假裝,偽裝,變形”, 是一個 Http 請求調用的輕量級框架,可以以 Java 接口注解的方式調用 Http 請求
- 而不用像 Java 中通過封裝 HTTP 請求報文的方式直接調用
- 通過處理注解,將請求模板化,當實際調用的時候,傳入參數,根據參數再應用到請求上,進而轉化成真正的請求
- 這種請求相對而言比較直觀,Feign 封裝 了HTTP 調用流程,面向接口編程
項目安排
api-passenger-feign
service-valuation
<!-- 引入feign依賴 ,用來實現接口偽裝 --> <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-openfeign</artifactId> </dependency>
添加接口,注解
- 一般一個服務提供者,寫一個interface
- 此處由於結合了eureka,所以name是 虛擬主機名,默認服務名,請求時 會將它解析成注冊表中的服務。
- 不結合eureka,就是自定義一個client名字。就用url屬性指定 服務器列表。url=“http://ip:port/”
- 此時的name作用就是創建負載均衡器,也可以添加@RequestMapping
@FeignClient(name = "service-valuation") public interface ServiceForecast { @RequestMapping(value = "/forecast/single",method = RequestMethod.POST) public ResponseResult<ForecastResponse> forecast(@RequestBody ForecastRequest forecastRequest); }
啟動類
@EnableFeignClients
@EnableFeignClients就像是一個開關,只有使用了該注解,OpenFeign相關的組件和配置機制才會生效。
@EnableFeignClients還可以對OpenFeign相關組件進行自定義配置
調用
@Autowired private ServiceForecast serviceForecast; @PostMapping("/forecast") public ResponseResult<ForecastResponse> forecast(@RequestBody ForecastRequest forecastRequest) { ResponseResult<ForecastResponse> result = serviceForecast.forecast(forecastRequest); return ResponseResult.success(result.getData()); }
會向service-valuation服務的接口:/forecast/single 發送請求
自定義feign配置
feign的默認配置類是:org.springframework.cloud.openfeign.FeignClientsConfiguration
默認定義了feign使用的編碼器,解碼器等
允許使用@FeignClient的configuration的屬性自定義Feign配置
自定義的配置優先級高於上面的FeignClientsConfiguration
通過權限的例子,學習feign的自定義配置
服務提供者。上述例子開放service-valuation的權限 后,訪問。
<!-- 安全認證 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
@Configuration @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { @Override protected void configure(HttpSecurity http) throws Exception { // 關閉csrf http.csrf().disable(); // 表示所有的訪問都必須認證,認證處理后才可以正常進行 http.httpBasic().and().authorizeRequests().anyRequest().fullyAuthenticated(); // 所有的rest服務一定要設置為無狀態,以提升操作效率和性能 http.sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS); } }
spring:
security:
user:
name: root
password: root
有如下兩種方式:
1. 自定義配置類。
2. 增加攔截器。
自定義配置
public class FeignAuthConfiguration { @Bean public BasicAuthRequestInterceptor basicAuthRequestInterceptor() { return new BasicAuthRequestInterceptor("root", "root"); } }
在feign上加配置
@FeignClient(name = "service-valuation",configuration = FeignAuthConfiguration.class);
如果在配置類上添加了@Configuration注解,並且該類在@ComponentScan所掃描的包中
那么該類中的配置信息就會被所有的@FeignClient共享
最佳實踐是:不指定@Configuration注解(或者指定configuration,用注解忽略)
而是手動
@FeignClient(name = "service-valuation",configuration = FeignAuthConfiguration.class)
攔截器
import feign.RequestInterceptor; import feign.RequestTemplate; public class MyBasicAuthRequestInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { // TODO Auto-generated method stub template.header("Authorization", "Basic cm9vdDpyb290"); } }
feign:
client:
config:
service-valuation:
request-interceptors:
- com.online.taxi.passenger.feign.interceptor.MyBasicAuthRequestInterceptor
報錯401
屬性定義
說明用屬性而不是用屬性中的configuration。
public class MyBasicAuthRequestInterceptor implements RequestInterceptor { @Override public void apply(RequestTemplate template) { // TODO Auto-generated method stub template.header("Authorization", "Basic cm9vdDpyb290"); } }
配置文件
feign:
client:
config:
service-valuation:
request-interceptors:
- com.online.taxi.passenger.feign.interceptor.MyBasicAuthRequestInterceptor
再次訪問Ok
擴展
指定服務名稱配置
feign:
client:
config:
service-valuation:
connect-timeout: 5000
read-timeout: 5000
logger-level: full
通用配置
feign:
client:
config:
default:
connect-timeout: 5000
read-timeout: 5000
logger-level: full
屬性配置比Java代碼優先級高。也可通過配置設置java代碼優先級高。
feign:
client:
default-to-properties: false
feign在方法上可以設置:@RequestMapping,@ResponseBody
方法中的參數可以設置:@RequestBody等等,Spring MVC中的注解
推薦使用yml配置方式,在yml中按 代碼提示鍵,可以看到所有配置
Feign繼承
- 編寫通用服務接口A,接口方法上寫@RequestMapping(),此接口用於 feign
- 服務提供者 實現上面接口A
- 服務消費者的feign client接口 繼承A
common組件(不推薦,降低耦合度)
package com.online.taxi.common.interactor; import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestMethod; import com.online.taxi.common.dto.ResponseResult; import com.online.taxi.common.dto.order.ForecastRequest; import com.online.taxi.common.dto.order.ForecastResponse; public interface CommonServiceForecast { @RequestMapping(value = "/forecast/single",method = RequestMethod.POST) public ResponseResult<ForecastResponse> forecast(@RequestBody ForecastRequest forecastRequest); } // 提供者 @RestController public class ServiceForecastController implements CommonServiceForecast { @Override @PostMapping("/forecast") public ResponseResult<ForecastResponse> forecast(@RequestBody ForecastRequest forecastRequest) { // 業務邏輯 return null; } } 消費者 @FeignClient(name = "service-valuation") public interface ServiceForecast extends CommonServiceForecast { }
這樣服務端和客戶端就耦合了,但會方便編碼
Feign壓縮
開啟壓縮可以有效節約網絡資源,但是會增加CPU壓力,建議把最小壓縮的文檔大小適度調大一點,進行gzip壓縮
feign:
compression:
request:
enabled: true
response: #設置返回值后,接受參數要改一下。
enabled: true
點注解進去,看看默認值
org.springframework.cloud.openfeign.encoding /** * The list of supported mime types. */ private String[] mimeTypes = new String[] { "text/xml", "application/xml", "application/json" }; /** * The minimum threshold content size. */ private int minRequestSize = 2048; 單位是B。
也可以選擇性的進行某種類型的壓縮
feign:
compression:
request:
enabled: true
mime-types: text/xml
min-request-size: 2048\
org.springframework.cloud.openfeign.encoding.FeignContentGzipEncodingInterceptor
方法 判斷內容是否超過配置的大小
private boolean contentLengthExceedThreshold(Collection<String> contentLength) { try { if (contentLength == null || contentLength.size() != 1) { return false; } final String strLen = contentLength.iterator().next(); final long length = Long.parseLong(strLen); return length > getProperties().getMinRequestSize(); }catch (NumberFormatException ex) { return false; } }
在HTTP協議中,有Content-Length的詳細解讀
參考文章:https://www.cnblogs.com/YC-L/p/14472311.html#2754049707
Content-Length用於描述HTTP消息實體的傳輸長度the transfer-length of the message-body
在HTTP協議中,消息實體長度和消息實體的傳輸長度是有區別
比如說gzip壓縮下,消息實體長度是壓縮前的長度,消息實體的傳輸長度是gzip壓縮后的長度
一般不需要設置壓縮,如果系統流量浪費比較多,可以考慮一下。
Feign日志
4種日志類型
- none: 不記錄任何日志,默認值
- basic: 僅記錄請求方法,url,響應狀態碼,執行時間
- headers:在basic基礎上,記錄header信息
- full: 記錄請求和響應的header,body,元數據
feign:
client:
config:
service-valuation:
logger-level: basic
//上面的logger-level只對下面的debug級別日志做出響應
logging:
level:
com.online.taxi.passenger.feign.ServiceForecast: debug
Feign構造多參數請求
GET多參數請求
- 接口方法種使用 方法(@RequestParam("id") long id)
- 用map,方法(@RequestParam Map<String , Object> map)
POST多參數請求
- 用bean,方法(@RequestBody User bean)
Feign執行流程
- 主程序入口添加@EnableFeignClients注解開啟對Feign Client掃描加載處理
- 根據Feign Client的開發規范,定義接口並加@FeignClient注解
- 當程序啟動時,會進行包掃描,掃描所有@FeignClient注解的類,並將這些信息注入Spring IoC容器中
- 當定義的Feign接口中的方法被調用時,通過JDK的代理方式,來生成具體的RequestTemplate
- 當生成代理時,Feign會為每個接口方法創建一個RequestTemplate對象
- 該對象封裝了HTTP請求需要的全部信息,如請求參數名、請求方法等信息都在這個過程中確定
- 由RequestTemplate生成Request,把這個Request交給client處理,這里指的Client可以是JDK原生的URLConnection、Apache的Http Client,也可以是Okhttp
- 最后Client被封裝到LoadBalanceClient類,這個類結合Ribbon負載均衡發起服務之間的調用
兩大流程
程序啟動時
- 接口的bean實例時如何初始化的
- 被@FeignClient修飾的接口類
- 構建Bean
網絡調用時
- 調用上面類的方法時如何發送網絡請求
核心組件
- FeignClientFactoryBean是創建@FeignClient修飾的接口類Bean實例的工廠類
- FeignContext是配置組件的上下文環境,保存着相關組件的不同實例
- 這些實例由不同的FeignConfiguration配置類構造出來
SynchronousMethodHandler是MethodHandler的子類,可以在FeignClient相應方法被調用時發送網絡請求,然后再將請求響應轉化為函數返回值進行輸出
流程
- 啟動時會首先進行相關的BeanDefinition的動態注冊
- 然后當Spring容器注入相關實例時會進行實例初始化
- 最后當feign接口類實例函數調用時會發送網絡請求
入口
自動注入
org.springframework.boot.autoconfigure.EnableAutoConfiguration=\
org.springframework.cloud.openfeign.ribbon.FeignRibbonClientAutoConfiguration,\
org.springframework.cloud.openfeign.FeignAutoConfiguration,\
org.springframework.cloud.openfeign.encoding.FeignAcceptGzipEncodingAutoConfiguration,\
org.springframework.cloud.openfeign.encoding.FeignContentGzipEncodingAutoConfiguration
開關
從開關開始@EnableFeignClients
@EnableFeignClients有三個作用
一是引入FeignClientsRegistrar
@Import(FeignClientsRegistrar.class)
在@Import注解的參數中可以填寫類名,例如@Import(Abc.class),根據類Abc的不同類型
spring容器有以下四種處理方式
- 如果Abc類實現了ImportSelector接口,spring容器就會實例化Abc類,並且調用其selectImports方法
- DeferredImportSelector是ImportSelector的子類,如果Abc類實現了DeferredImportSelector接口,spring容器就會實例化Abc類
- 並且調用其selectImports方法,和ImportSelector的實例不同的是
- DeferredImportSelector的實例的selectImports方法調用時機晚於ImportSelector的實例
- 要等到@Configuration注解中相關的業務全部都處理完了才會調用(具體邏輯在ConfigurationClassParser.processDeferredImportSelectors方法中)
- 如果Abc類實現了ImportBeanDefinitionRegistrar接口,spring容器就會實例化Abc類,並且調用其registerBeanDefinitions方法
- 如果Abc沒有實現ImportSelector、DeferredImportSelector、ImportBeanDefinitionRegistrar等其中的任何一個,spring容器就會實例化Abc類
我們此時的FeignClientsRegistrar,屬於第三種情況
二,是指定掃描FeignClient的包信息
- 就是指定FeignClient接口類所在的包名
- value(),basePackages(),basePackageClasses() ,默認都為空,如果要指定,可以在注解中加
三,指定FeignClient接口類的自定義配置類
- defaultConfiguration()
- 看注釋:默認是:FeignClientsConfiguration,
- clients(),羅列被@FeignClient修飾的類
FeignClientsRegistrar
- 上面提到的org.springframework.cloud.openfeign.FeignClientsRegistrar implements ImportBeanDefinitionRegistrar
- FeignClientsRegistrar是ImportBeanDefinitionRegistrar的子類,Spring用ImportBeanDefinitionRegistrar來動態注冊BeanDefinition
- OpenFeign通過FeignClientsRegistrar也能實現動態注冊beanfefinition的功能
- 處理@FeignClient修飾的FeignClient接口類,將這些接口類的BeanDefinition注冊到Spring容器中
- 這樣就可以使用@Autowired等方式來自動裝載這些FeignClient接口類的Bean實例
BeanDefinition
Spring使用BeanDefinition來描述bean
BeanDefinitionBuilder是Builder模式的應用。通過這個類我們可以方便的構建BeanDefinition的實例對象
建造者模式:https://www.runoob.com/design-pattern/builder-pattern.html
其實就是將Bean的定義信息存儲到這個BeanDefinition相應的屬性中,后面對Bean的操作就直接對BeanDefinition進行
例如拿到這個BeanDefinition后,可以根據里面的類名、構造函數、構造函數參數,使用反射進行對象創建
打斷點可以,看到啟動的時候執行到這個方法
FeignClientsRegistrar.class
@Override public void registerBeanDefinitions(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { // 從開關EnableFeignClients的屬性值來構建Feign的自定義Configuration進行注冊。看其代碼的第一句。 registerDefaultConfiguration(metadata, registry); // 注冊被@FeignClient的修飾的接口類的信息。 registerFeignClients(metadata, registry); }
兩個功能
- 注冊@EnableFeignClients提供的自定義配置類中的相關bean
- 此時的配置類是被 @Configuration注解修飾的配置類,它會提供一系列組裝FeignClient的各類組件實例,比如Decoder、Encoder等。
- 根據@EnableFeignClients提供的包信息掃描@FeignClient修飾的接口類,並注冊
registerDefaultConfiguration方法
點第一個方法進去,registerDefaultConfiguration
private void registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { // 獲取@EnableFeignClients中屬性鍵值對。 Map<String, Object> defaultAttrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName(), true); // 如果@EnableFeignClients,注解中有屬性,並且包含defaultConfiguration,則進入此邏輯。 if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) { String name; if (metadata.hasEnclosingClass()) { name = "default." + metadata.getEnclosingClassName(); } else { name = "default." + metadata.getClassName(); } registerClientConfiguration(registry, name, defaultAttrs.get("defaultConfiguration")); } }
name是:default.com.online.taxi.passenger.ApiPassengerApplication
點進去registerClientConfiguration,此方法進行BeanDefinitionRegistry注冊
private void registerClientConfiguration(BeanDefinitionRegistry registry, Object name, Object configuration) { // 先生成beanDefinition。 BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(FeignClientSpecification.class); builder.addConstructorArgValue(name); builder.addConstructorArgValue(configuration); // 注冊beandefinition registry.registerBeanDefinition(name + "." + FeignClientSpecification.class.getSimpleName(),builder.getBeanDefinition()); }
上面方法第一個參數:BeanDefinitionRegistry是Spring框架中用於動態注冊BeanDefinition信息的接口
調用其registerBeanDefinition方法可以將BeanDefinition注冊到Spring容器中
此方法第一個參數是beanName,name屬性就是注冊BeanDefinition的名稱(default.com.online.taxi.passenger.ApiPassengerApplication)
FeignClientSpecification
class FeignClientSpecification implements NamedContextFactory.Specification
FeignClientSpecification持有自定義配置類提供的組件實例,供OpenFeign使用
Spring Cloud框架使用NamedContextFactory創建一系列的運行上下文(ApplicationContext)
- 來讓對應的Specification在這些上下文中創建實例對象
- 這樣使得各個上下文中的實例對象相互獨立,互不影響,可以方便地通過子上下文管理一系列不同的實例對象
- 意思就是:此處的FeignClientSpecification持有的自定義配置類的組件在feign的上下文中和其他上下文獨立
- feign組件就是feign的組件,和其他組件區分開
NamedContextFactory有三個功能
- 一是創建AnnotationConfigApplicationContext子上下文
- 二是在子上下文中創建並獲取Bean實例
- 三是當子上下文消亡時清除其中的Bean實例(通過其父類DisposableBean的destory實現)
- 我們看NamedContextFactory的實現類有:FeignContext
- 構造方法中有:super(FeignClientsConfiguration.class, "feign", "feign.client.name");
- 可以看出FeignContext存儲了各類 openFeign的組件實例
此時我們發現一個類FeignContext
而FeignContext組件實例是通過:FeignAutoConfiguration自動配置的
我們看到在org.springframework.cloud.openfeign.FeignAutoConfiguration中,定義了一個bean
@Bean public FeignContext feignContext() { FeignContext context = new FeignContext(); // 此時將上面注冊的FeignClientSpecification設置到feignContext的configuration中。 context.setConfigurations(this.configurations); return context; }
看構造函數
public FeignContext() { super(FeignClientsConfiguration.class, "feign", "feign.client.name"); }
發現了上面所說開關中的默認配置FeignClientsConfiguration類
上面就是:將@EnableFeignClients注解中的自定義配置注冊到spring中
registerFeignClients
第二個方法,注冊feignclient接口的beanDefinition
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { ClassPathScanningCandidateComponentProvider scanner = getScanner(); scanner.setResourceLoader(this.resourceLoader); Set<String> basePackages; Map<String, Object> attrs = metadata.getAnnotationAttributes(EnableFeignClients.class.getName()); }
注冊被@FeignClient的修飾的接口類的信息
AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(FeignClient.class);
此方法中有兩個for循環
for (String basePackage : basePackages) { Set<BeanDefinition> candidateComponents = scanner.findCandidateComponents(basePackage); for (BeanDefinition candidateComponent : candidateComponents) { // coding } }
第一層循環是 項目主包,第二層循環是循環所有@FeignClient注解修飾的接口
找出來后注冊到spring,beandefinition。
FeignClientsRegistrar的registerBeanDefinitions方法主要做了兩個事情
- 一是注冊@EnableFeignClients提供的自定義配置類中的相關Bean實例
- 二是注冊@FeignClient注解修飾的FeignCleint接口類,然后進行Bean實例注冊
@EnableFeignClients的自定義配置類是被@Configuration注解修飾的配置類
它會提供一系列組裝FeignClient的各類組件實例
這些組件包括:Client、Targeter、Decoder、Encoder和Contract等
實例初始化
上面講了BeanDefinition注冊。下面進行實例初始化
在spring-cloud-openfeign-core-2.1.2.RELEASE中
org.springframework.cloud.openfeign.FeignClientFactoryBean
Spring容器通過調用它的getObject來獲取對應的bean實例
此時的實例是指被@FeignClient修飾的接口類的實例
點getTarget方法進去。
每個feignclient的實例都通過此工廠類,獲取對應的實例。
Client client = getOptional(context, Client.class);獲取client對象。
org.springframework.cloud.openfeign.Targeter有兩個實現類:DefaultTargeter和HystrixTargeter
DefaultTargeter
class DefaultTargeter implements Targeter { @Override public <T> T target(FeignClientFactoryBean factory, Feign.Builder feign, FeignContext context, Target.HardCodedTarget<T> target) { return feign.target(target); } }
其中:Feign.Builder feign
負責生成被@FeignClient修飾的接口類實例
通過Java的反射機制,生成實例
當feignclient的方法被調用時
InvocationHandler的回調函數會被調用,在回調函數中發送網絡請求
public <T> T target(Target<T> target) { return build().newInstance(target); } public Feign build() { SynchronousMethodHandler.Factory synchronousMethodHandlerFactory = new SynchronousMethodHandler.Factory(client, retryer,ΩrequestInterceptors, logger,logLevel, decode404, closeAfterDecode, propagationPolicy); ParseHandlersByName handlersByName = new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder, errorDecoder, synchronousMethodHandlerFactory); return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder); }
ReflectiveFeign有個newInstance方法,2個功能
- 掃描feignclient接口類的所有函數,生成對應的Handler
- 用Proxy生成feignclient的實例對象
@SuppressWarnings("unchecked") @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; }
此方法中apply方法作用
- 通過Contract的parseAndValidatateMetadata方法獲得了接口類中所有方法的元數據
- 這些信息中包含了每個方法所對應的網絡請求信息
- 比如說請求的路徑(path)、參數(params)、頭部(headers)和body
- 接下來apply方法會為每個方法生成一個MethodHandler
- 此方法中factory.create作用:創建接口類的實例
- 然后通過bindTo將InvocationHandler綁定到接口類實例上,用於處理函數調用
函數調用
在配置和實例生成結束之后,就可以直接使用FeignClient接口類的實例,調用它的函數來發送網絡請求
在調用其函數的過程中,由於設置了MethodHandler,所以最終函數調用會執行SynchronousMethodHandler的invoke方法
在該方法中,OpenFeign會將函數的實際參數值與之前生成的RequestTemplate進行結合,然后發送網絡請求
feign.SynchronousMethodHandler方法中
@Override public Object invoke(Object[] argv) throws Throwable { // 生成請求類似於:GET /uri HTTP/1.1 // argv:[BaseOrder(startLatitude=labore et laboris eiusmod, startLongitude=ut cupidatat, endLatitude=sit sint111, endLongitude=Excepteur Lorem reprehend)] // template: // POST /forecast/single HTTP/1.1 // Content-Length: 148 // Content-Type: application/json;charset=UTF-8 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; } }
構建RequestTemplate,用RequestTemplate.Factory.create,構建url,queryMap,headerMap等
executeAndDecode點進去,有一句:response = client.execute(request, options);
- 此時的client,就是具體發送請求的client
- 此時發送完請求后,還會將結果封裝成Response
feign和ribbon結合的源碼
打斷點到feign.SynchronousMethodHandler的invoke第一行
feign.SynchronousMethodHandler
里面有executeAndDecode
-
此代碼主要功能:構建request數據,然后通過request和options去通過LoadBalancerFeignClient.execute()方法去獲得返回值
Object executeAndDecode(RequestTemplate template) throws Throwable { // 構建request對象,類似於:GET /uri HTTP/1.1 // request: // POST http://service-valuation/forecast/single HTTP/1.1 // Authorization: Basic cm9vdDpyb290 // Content-Length: 148 // Content-Type: application/json;charset=UTF-8 // {"startLatitude":"labore et laboris eiusmod","startLongitude":"ut cupidatat","endLatitude":"sit sint111","endLongitude":"Excepteur Lorem reprehend"} Request request = targetRequest(template); if (logLevel != Logger.Level.NONE) { logger.logRequest(metadata.configKey(), logLevel, request); } Response response; long start = System.nanoTime(); try { // 這個client就是之前構建的LoadBalancerFeignClient,是Client的實現類LoadBalancerFeignClient。 response = client.execute(request, options); } catch (IOException e) { if (logLevel != Logger.Level.NONE) { logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start)); } throw errorExecuting(request, e); } long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); boolean shouldClose = true; try { if (logLevel != Logger.Level.NONE) { response = logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime); } if (Response.class == metadata.returnType()) { if (response.body() == null) { return response; } if (response.body().length() == null || response.body().length() > MAX_RESPONSE_BUFFER_SIZE) { shouldClose = false; return response; } // Ensure the response body is disconnected byte[] bodyData = Util.toByteArray(response.body().asInputStream()); return response.toBuilder().body(bodyData).build(); } if (response.status() >= 200 && response.status() < 300) { if (void.class == metadata.returnType()) { return null; } else { Object result = decode(response); shouldClose = closeAfterDecode; return result; } } else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) { Object result = decode(response); shouldClose = closeAfterDecode; return result; } else { throw errorDecoder.decode(metadata.configKey(), response); } } catch (IOException e) { if (logLevel != Logger.Level.NONE) { logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime); } throw errorReading(request, response, e); } finally { if (shouldClose) { ensureClosed(response.body()); } } }
F5點進去execute方法
TraceLoadBalancerFeignClient.execute)走了這一行
response = super.execute(request, options);
實際就是org.springframework.cloud.openfeign.ribbon.execute
@Override public Response execute(Request request, Request.Options options) throws IOException { try { // asUri: http://service-valuation/forecast/single URI asUri = URI.create(request.url()); // clientName:service-valuation String clientName = asUri.getHost(); // uriWithoutHost:http:///forecast/single URI uriWithoutHost = cleanUrl(request.url(), clientName); FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest( this.delegate, request, uriWithoutHost); IClientConfig requestConfig = getClientConfig(options, clientName); // 真正執行負載均衡的地方: return lbClient(clientName).executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse(); }catch (ClientException e) { IOException io = findIOException(e); if (io != null) { throw io; } throw new RuntimeException(e); } }
F5 進入 lbClient()
private FeignLoadBalancer lbClient(String clientName) { return this.lbClientFactory.create(clientName); } public FeignLoadBalancer create(String clientName) { FeignLoadBalancer client = this.cache.get(clientName); if (client != null) { return client; } IClientConfig config = this.factory.getClientConfig(clientName); // 獲取Ribbon ILoadBalancer信息,鼠標放到lb上,發現:我們自己配置的com.netflix.loadbalancer.RandomRule@498bbb15 ILoadBalancer lb = this.factory.getLoadBalancer(clientName); ServerIntrospector serverIntrospector = this.factory.getInstance(clientName, ServerIntrospector.class); client = this.loadBalancedRetryFactory != null ? new RetryableFeignLoadBalancer(lb, config, serverIntrospector, this.loadBalancedRetryFactory) : new FeignLoadBalancer(lb, config, serverIntrospector); this.cache.put(clientName, client); return client; }
F7繼續斷點
return lbClient(clientName).executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
F5進入executeWithLoadBalancer,AbstractLoadBalancerAwareClient的下面方法:
public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException { LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig); try { return command.submit( new ServerOperation<T>() { @Override public Observable<T> call(Server server) { URI finalUri = reconstructURIWithServer(server, request.getUri()); S requestForServer = (S) request.replaceUri(finalUri); try { return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig)); } catch (Exception e) { return Observable.error(e); } } }).toBlocking().single(); } catch (Exception e) { Throwable t = e.getCause(); if (t instanceof ClientException) { throw (ClientException) t; } else { throw new ClientException(e); } } }
打斷點到:com.netflix.loadbalancer.reactive.LoadBalancerCommand的
public Observable<T> submit(final ServerOperation<T> operation) { final ExecutionInfoContext context = new ExecutionInfoContext();
看這行代碼: (server == null ? selectServer() : Observable.just(server))
進入selectServer()。
執行到(打斷點到此行 F8)Server server = loadBalancerContext.getServerFromLoadBalancer(loadBalancerURI, loadBalancerKey);
打斷點:com.netflix.loadbalancer.LoadBalancerContext
行
public Server getServerFromLoadBalancer(@Nullable URI original, @Nullable Object loadBalancerKey) throws ClientException {
String host = null;
打斷點: ILoadBalancer lb = getLoadBalancer();
打斷點:Server svc = lb.chooseServer(loadBalancerKey);
終於看到ribbon的東西了
進入chooseServer
進入if (!ENABLED.get() || getLoadBalancerStats().getAvailableZones().size() <= 1) {
logger.debug("Zone aware logic disabled or there is only one zone");
return super.chooseServer(key);
}
來到:com.netflix.loadbalancer.BaseLoadBalancer
public Server chooseServer(Object key) {
看到了return rule.choose(key);
feign在調用其他微服務接口前,會去請求該微服務的相關信息(地址、端口等),並做一些初始化操作由於默認的懶加載特性,導致了在第一次調用時,出現超時的情況
ribbon:
eager-load:
enabled: true
clients:
- SERVICE-SMS
配置ribbon立即加載,此處需要注意的是,光配置立即加載是不生效的,還要配置客戶端列表.
總結
- feign的使用。
- feign的獨立使用。(大家課下實踐,feignClient(name="",url="http://ip:port/xxx"))
- feign和ribbon結合。(配置負載均衡的地方)
- 原理,源碼。
- 繼承,壓縮,日志(方便開發)。
RestTemplate,自由,更貼近httpclient,方便調用別的第三方的http服務
feign,更面向對象一些,更優雅一些