使用Feign實現聲明式Rest調用
文章目錄
Feign是一個http請求調用的輕量級框架。使用Feign,可以直接以Java接口注解的方式發送Http請求,而不需要在Java中通過封裝HTTP工具類來發送請求。
Feign源碼地址:Feign
Feign封裝了Http 請求調用流程,實現了申明式Http接口調用。
使用Feign的方式調用遠程服務,服務消費者與生產者不需要實現同一個接口,可以做到消費者與生產者代碼上的完全解耦。就代碼耦合而言,Feign與jdk提供的rmi和阿里的dubbo有所差異,后者進行遠程服務調用需要實現共同的api。另外服務消費者一方只需要關注FeignClient配置即可,不需要關注具體Http Request的實現,所以說Feign最終目的是將Java Http客戶端調用過程變得簡單。
從角色職能劃分,Feign提供http調用服務流程如下:
Feign是一個偽java客戶端,Feign不做任何的請求處理。Feign通過處理注解生成Request模板,從而簡化了Http API的開發,開發人員可以使用注解的方式定制Request API模板。在發送Http Request請求之前,Feign通過處理注解的方式替換掉Request模板中的參數,生成真正的Request,并交給Java Http客戶端處理。
綜合來講,發送一個Http請求,Feign做了兩件事情:
?1、Http請求處理流程封裝,包含:請求行、請求頭、請求體、響應;
2、選擇Http Client發送請求。
我們在通過源碼去理解Feign原理的時候,不妨帶著這兩個問題,從源碼中理解,Feign是如何處理這兩個問題的,這樣對于我們理解Feign會有所幫助。
? 我們可以把Feign處理Http請求的基本流程分為兩個部分,第一部分是初始化階段,進行Proxy和MethodHandler的創建,第二部分則是具體請求處理流程。
初始化流程基本如下:
Feign初始化過程基本分為兩個部分:
1.ReflectiveFeign根據指定的Contract為每一個方法創建了一個SynchronousMethodHandler;
2.基于動態代理,為Target接口創建了一個proxy對象,同時定義一個統一的InvocationHandler用于請求處理,將請求分發到指定的SynchronousMethodHandler處理。
Request處理過程基本如下:
Feign封裝了整個Request的處理過程,按照請求順序,如下:
1.具體方法處理類SynchronousMethodHandler創建請求模板;
2.對Request請求進行預處理,編碼;
3.將Request交給client去執行處理,若有攔截器先執行攔截器;
4.返回結果處理,解碼;
5.返回結果最終轉化為javaBean交付給具體方法處理類SynchronousMethodHandler。
在@EnableFeignClients標簽中,import了FeignClientsRegistrar,通過FeignClientsRegistrar的registerBeanDefinitions方法完成了FeignClient的Bean的注入。程序啟動時,會檢查是否有@EnableFeignClients注解,如果有,則會執行FeignClientsRegistrar的registerBeanDefinitions方法。其中registerBeanDefinitions代碼如下:
@OverridepublicvoidregisterBeanDefinitions(AnnotationMetadata metadata,BeanDefinitionRegistry registry){//掃描EnableFeignClients標簽里配置的信息,注冊到beanDefinitionNames中。registerDefaultConfiguration(metadata,registry);registerFeignClients(metadata,registry);}
其中registerFeignClients完成了對FeignClient的注冊,代碼如下:
publicvoidregisterFeignClients(AnnotationMetadata metadata,BeanDefinitionRegistry registry){ClassPathScanningCandidateComponentProvider scanner=getScanner();scanner.setResourceLoader(this.resourceLoader);Set<String>basePackages;Map<String,Object>attrs=metadata.getAnnotationAttributes(EnableFeignClients.class.getName());// 定義Filter規則,用于scanner過濾AnnotationTypeFilter annotationTypeFilter=newAnnotationTypeFilter(FeignClient.class);// 獲取EnableFeignClients注解中的clients屬性的值finalClass<?>[]clients=attrs==null?null:(Class<?>[])attrs.get("clients");if(clients==null||clients.length==0){scanner.addIncludeFilter(annotationTypeFilter);basePackages=getBasePackages(metadata);}else{// @EnableFeignClients提供了clients屬性用于指定掃描的clients// 存在指定的clients,依次將client所在的package添加到basePackages...// 省略代碼}for(String basePackage:basePackages){// 從basePackage中掃描到的FeignClientSet<BeanDefinition>candidateComponents=scanner.findCandidateComponents(basePackage);for(BeanDefinition candidateComponent:candidateComponents){if(candidateComponentinstanceofAnnotatedBeanDefinition){AnnotatedBeanDefinition beanDefinition=(AnnotatedBeanDefinition)candidateComponent;AnnotationMetadata annotationMetadata=beanDefinition.getMetadata();// 注意,@FeignClient只能標注在接口上Assert.isTrue(annotationMetadata.isInterface(),"@FeignClient can only be specified on an interface");Map<String,Object>attributes=annotationMetadata.getAnnotationAttributes(FeignClient.class.getCanonicalName());String name=getClientName(attributes);/**
? ? ? ? * 關鍵地方:Feign子容器概念:
? ? ? ? * 在注入FeignAutoConfiguration類的時候,注入了一個FeignContext對象,這個就是Feign的子容器。
? ? ? ? * 這里面裝了List<FeignClientSpecification>對象,FeignClientSpecification對象的實質就是在@feignClient上配置的configuration指定對象的值
? ? ? ? * 這個地方比較關鍵,主要是因為后期對feign客戶端的編碼解碼會用到自定義的類
? ? ? ? */registerClientConfiguration(registry,name,attributes.get("configuration"));// 注冊feignClientregisterFeignClient(registry,annotationMetadata,attributes);}}}}
? 大致邏輯如下:
? 1.獲取EnableFeignClients注解的相關屬性;
? 2.定義按照FeignClient注解過濾的過濾器annotationTypeFilter;
? 3.根據注解和定義的過濾規則確定掃描范圍basePackages,basePackages默認是啟動類的同級目錄,若EnableFeignClients指定了clients,則basePackages是clients指定的每一個類的同級目錄的集合;
? 4.掃描basePackages中FeignClient,依次注入到Spring容器中。
? 從上文中,我們可以了解到在registerBeanDefinitions是方法中完成了FeignClient的Bean注入,那么registerBeanDefinitions這個方法又是在上面時候執行的呢?我們不妨進一步探索一下,跟著Spring的源碼走下去,看過源碼的人都會直接看到AbstractApplicationContext#refresh()方法,整體整理一下代碼:
@Overridepublicvoidrefresh()throwsBeansException,IllegalStateException{synchronized(this.startupShutdownMonitor){// Prepare this context for refreshing.prepareRefresh();// 掃描本項目里面的java文件,把bean對象封裝成BeanDefinitiaon對象,//然后調用DefaultListableBeanFactory#registerBeanDefinition()方法把beanName放到DefaultListableBeanFactory 的 List<String> beanDefinitionNames 中去ConfigurableListableBeanFactory beanFactory=obtainFreshBeanFactory();// Prepare the bean factory for use in this context.prepareBeanFactory(beanFactory);try{postProcessBeanFactory(beanFactory);// 在這里調用到FeignClientsRegistrar對象的registerBeanDefinitions()方法invokeBeanFactoryPostProcessors(beanFactory);//從DefaultListableBeanFactory里面的beanDefinitionNames中找到所有實現了BeanPostProcessor接口的方法,如果有排序進行排序后放到list中registerBeanPostProcessors(beanFactory);//Spring的國際化initMessageSource();initApplicationEventMulticaster();// Initialize other special beans in specific context subclasses.onRefresh();registerListeners();// Spring的IOC、ID處理。Spring的AOP。事務都是在IOC完成之后調用了BeanPostProcessor#postProcessBeforeInitialization()和postProcessBeforeInitialization()方法,AOP(事務)就是在這里處理的finishBeanFactoryInitialization(beanFactory);// 執行完之后調用實現了所有LifecycleProcessor接口的類的onRefresh()方法,同時調用所有觀察了ApplicationEvent接口的事件(觀察者模式)finishRefresh();}catch(BeansExceptionex){// 找到所有實現了DisposableBean接口的方法,調用了destroy()方法,這就是bean的銷毀destroyBeans();// Reset 'active' flag.cancelRefresh(ex);throwex;}finally{resetCommonCaches();}}}
? 根據上面整理的代碼發現,FeignClientsRegistrar#registerBeanDefinitions()方法是在掃描完bean之后,只放了一個beanname的情況下, 并沒有進行IOC注冊的時候調用的,這就是Spring動態擴展Bean。另外,實現BeanDefinitionRegistryPostProcessor接口的所有方法也會在這里調用下postProcessBeanDefinitionRegistry()方法。
? 總結一下:
? 我們平時工作和學習中,留心的話,不難發現:spring作為整合專家,在整合其它框架時存在一個基本套路:1.自定義三方注解、2.定義注冊器Registrar,掃描注解標準類注入到spring的IoC容器中。Feign正是其中之一,對這方面比較感興趣的話,不妨去深入研究一下spring自定義注解和spring-bean。
? 注入BeanDefinition之后, ReflectiveFeign內部使用了jdk的動態代理為目標接口生成了一個代理類,這里會生成一個InvocationHandler統一的方法處理器,同時為接口的每個方法生成一個SynchronousMethodHandler攔截。
? 下面圍繞兩個方面講述:
? 1、如何創建代理,創建的是誰的代理;
? 2、請求是怎么分發到具體的SynchronousMethodHandler方法處理器的。
? ReflectiveFeign#newInstance代碼如下:
public<T>TnewInstance(Target<T>target){// 為每一個method創建一個MethodHandlerMap<String,MethodHandler>nameToHandler=targetToHandlersByName.apply(target);// method 容器,key為targetd的Method Map<Method,MethodHandler>methodToHandler=newLinkedHashMap<Method,MethodHandler>();List<DefaultMethodHandler>defaultMethodHandlers=newLinkedList<DefaultMethodHandler>();for(Method method:target.type().getMethods()){if(method.getDeclaringClass()==Object.class){continue;}elseif(Util.isDefault(method)){DefaultMethodHandler handler=newDefaultMethodHandler(method);defaultMethodHandlers.add(handler);methodToHandler.put(method,handler);}else{methodToHandler.put(method,nameToHandler.get(Feign.configKey(target.type(),method)));}}// 創建InvocationHandler,總創建InvocationHandler,最終會分到具體的SynchronousMethodHandlerInvocationHandler handler=factory.create(target,methodToHandler);// 創建代理對象,經過源碼追蹤可以了解到target即為@FeignClient注解標注的接口T proxy=(T)Proxy.newProxyInstance(target.type().getClassLoader(),newClass<?>[]{target.type()},handler);for(DefaultMethodHandler defaultMethodHandler:defaultMethodHandlers){defaultMethodHandler.bindTo(proxy);}returnproxy;}
? 上面代碼中targetToHandlersByName.apply(target),根據contract協議為每一個method創建了一個MethodHandler,具體實現類是SynchronousMethodHandler,代碼如下:
publicMap<String,MethodHandler>apply(Target key){List<MethodMetadata>metadata=contract.parseAndValidatateMetadata(key.type());Map<String,MethodHandler>result=newLinkedHashMap<String,MethodHandler>();for(MethodMetadata md:metadata){BuildTemplateByResolvingArgs buildTemplate;···// 省略代碼result.put(md.configKey(),factory.create(key,md,buildTemplate,options,decoder,errorDecoder));}returnresult;}
? 進入factory.create可以發現,實際上創建的MethodHandler即為SynchronousMethodHandler,代碼如下:
publicMethodHandlercreate(Target<?>target,MethodMetadata md,RequestTemplate.Factory buildTemplateFromArgs,Options options,Decoder decoder,ErrorDecoder errorDecoder){returnnewSynchronousMethodHandler(target,client,retryer,requestInterceptors,logger,logLevel,md,buildTemplateFromArgs,options,decoder,errorDecoder,decode404);}
? 另外,在創建InvocationHandler的時候,我們發現傳入的參數是methodToHandler,從上文中可知,其中key為method,value為SynchronousMethodHandler對象。繼續代碼跟進,可以發現創建的InvocationHandler實際上就是FeignInvocationHandler,并且將methodToHandler賦值給了dispatch,代碼如下:
publicInvocationHandlercreate(Target target,Map<Method,MethodHandler>dispatch){returnnewReflectiveFeign.FeignInvocationHandler(target,dispatch);}
FeignInvocationHandler(Target target,Map<Method,MethodHandler>dispatch){this.target=checkNotNull(target,"target");this.dispatch=checkNotNull(dispatch,"dispatch for %s",target);}
? 總結一下:
? 1.ReflectiveFeign內部使用了jdk的動態代理為目標接口(@FeignClient注解標注的接口)生成了一個代理類,并且生成統一的InvocationHandler;
? 2.為每一個Method創建SynchronousMethodHandler,并將方法及放入dispatch方法容器中,其中key為Method,保證key的唯一性,value為具體的SynchronousMethodHandler。那么在后面的接口調用中,則可以通過具體的Method獲取到具體的SynchronousMethodHandler了。
? 根據上文創建代理部分可知,當調用Feign Client接口里面的方法時,該方法會被FeignInvocationHandler攔截,并且調用invoke方法,在invoke方法中,完成了分發到指定的SynchronousMethodHandler處理的動作,代碼如下:
publicObjectinvoke(Object proxy,Method method,Object[]args)throwsThrowable{···// 省略代碼returndispatch.get(method).invoke(args);}
? SynchronousMethodHandler處理請求時,根據傳入參數生成RequestTemplate對象,該對象即為請求模板,代碼如下:
@OverridepublicObjectinvoke(Object[]argv)throwsThrowable{// 根據Target接口中的方法注解,創建請求模板RequestTemplate template=buildTemplateFromArgs.create(argv);// 獲取重試策略,默認不重試Retryer retryer=this.retryer.clone();while(true){try{returnexecuteAndDecode(template);}catch(RetryableExceptione){// 請求異常重試retryer.continueOrPropagate(e);if(logLevel!=Logger.Level.NONE){logger.logRetry(metadata.configKey(),logLevel);}continue;}}}
在 executeAndDecode()方法中,通過RequestTemplate創建Request請求對象,然后用Http Client執行request,即通過Http Client進行Http請求獲取結果,代碼如下:
ObjectexecuteAndDecode(RequestTemplate template)throwsThrowable{Request request=targetRequest(template);···// 省略代碼,參數編碼// client發送request請求response=client.execute(request,options);···// 省略代碼,response解碼}
此處,進入 targetRequest()方法,發現執行了一些列的攔截器,代碼如下:
RequesttargetRequest(RequestTemplate template){// 執行攔截器for(RequestInterceptor interceptor:requestInterceptors){interceptor.apply(template);}// 生成Request并返回returntarget.apply(newRequestTemplate(template));}
? 總結一下調用步驟:
? 1.以method為key,獲取到具體的SynchronousMethodHandler;
? 2.創建請求模板;
? 3.獲取重試策略;
? 4.執行攔截器;
? 5.創建Request;
? 6.參數編碼;
? 7.發送請求;
? 8.response解碼;
? 9.請求異常,執行重試策略。
從SynchronousMethodHandler的invoke方法中可以看到,聲明了一個重試器Retryer,在請求執行失敗后會根據重試策略進行請求重試,調用Retryer的continueOrPropagate方法。從FeignClientsConfiguration代碼中可以看到默認定義的Retryer是不進行重試的,因為continueOrPropagate方法直接拋出了異常,代碼如下:
@ConfigurationpublicclassFeignClientsConfiguration{···// 省略代碼@Bean@ConditionalOnMissingBeanpublicRetryerfeignRetryer(){returnRetryer.NEVER_RETRY;}···// 省略代碼}
publicinterfaceRetryerextendsCloneable{···// 省略代碼Retryer NEVER_RETRY=newRetryer(){// 重試方法,直接返回異常,不重試@OverridepublicvoidcontinueOrPropagate(RetryableException e){throwe;}@OverridepublicRetryerclone(){returnthis;}};···// 省略代碼}
? 所以說,如果需要具有重試功能,可以重新定義一個Retryer覆蓋默認的即可,Feign也默認提供Retryer.Default的重試策略,可以定義好重試參數后直接使用,不需要拓展重試策略了。
??注意:
? spring-cloud-feign之所以默認Retryer.NEVER_RETRY,即不重試,是因為spring-cloud-feign整合了ribbon,ribbon也有重試策略,如果fegin也開啟重試策略,容易造成混亂。如果feign單獨使用的情況下,建議定義一下重試策略。
? 看到這里,其實還有一個疑問,執行Http請求使用的Client是什么時候初始化的,整體一下,提供兩個點思考方向:
? 1.發送http請求的client工具類是怎么集成進去的;
? 2.Feign是怎么實現負載均衡的。
? 先看第1個問題:
? Feign默認集成了3種Http調用工具,分布為:ApacheHttpClient、OkHttpClient、HttpURLConnection。默認情況下使用的是HttpURLConnection,當引入ApacheHttpClient依賴時,client即為ApacheHttpClient,想切換為OkHttpClient,只需要將依賴替換為OkHttpClient即可。相關加載原理可以查看FeignAutoConfiguration類中對于ApacheHttpClient和OkHttpClient的加載條件。
? 對于性能有要求的項目中,建議不要使用HttpURLConnection,可以使用OkHttpClient或者ApacheHttpClient,對這3個工具類性能有興趣的同學,可以深入了解一下,做一下對比。
? 接下來說一下第2個問題:
? 用過Feign的同學可能知道,spring-cloud-feign是支持負載均衡的,而第1個問題中提到的3種http工具類本身是不支持負載均衡的。那么,Feign是怎么保證初始化在內存中的client能夠進行負載均衡的呢?這里的client有兩個實現類,分別是Client.Defaut和LoadBalancerFeignClient,而默認值是Client.Defaut。繼續閱讀源碼,發現FeignClientFactoryBean中的loadBalance方法會重置client,程序啟動時,從這里使用LoadBalancerFeignClient的實例覆蓋了默認的Client.Defaut,代碼如下:
classFeignClientFactoryBeanimplementsFactoryBean<Object>,InitializingBean,ApplicationContextAware{···// 省略代碼@OverridepublicObjectgetObject()throwsException{···// 省略代碼if(!StringUtils.hasText(this.url)){···// 省略代碼returnloadBalance(builder,context,newHardCodedTarget<>(this.type,this.name,url));}···// 省略代碼}···// 省略代碼protected<T>TloadBalance(Feign.Builder builder,FeignContext context,HardCodedTarget<T>target){Client client=getOptional(context,Client.class);if(client!=null){// 重置client,設置為loadBalanceClientbuilder.client(client);Targeter targeter=get(context,Targeter.class);returntargeter.target(this,builder,context,target);}···// 省略代碼}···// 省略代碼}
? 總結一下:
? Client的注冊使用了動態注入的方式,其實現邏輯是根據FeignClient是否配置了指定的url,如果沒有配置url則使用負載均衡策略,配置了url,則直接使用url綁定的服務。我們平時編碼直接使用@Service注解,而這種方式是靜態注入。
用戶下單;
通過資金賬號查詢資產;
下單時對用戶等級做身份認證,給高等級的機構用戶提供快速下單渠道。
? 新建一個Spring Boot的Moudle工程,命名traderServer-1,滿足需求中的相關接口,controller中代碼如下:
@RestController@RequestMapping("/trade")publicclassTradeController{@RequestMapping(value="/queryFund")publicStringqueryFund(String account){return"tradeServer-1,賬戶余額:1,000,000";}@RequestMapping(value="/order")@ResponseBodypublicStringorderJSON(String stock,Double price,Double count){return"tradeServer-1,下單成功";}@RequestMapping(value="/orderJSON")@ResponseBodypublicStringorderJSON(@RequestBodyJSONObject body){return"tradeServer-1,下單成功";}}
publicinterfaceITradeService{@RequestLine("GET /trade/queryFund?account={account}")StringqueryFund(@Param("account")String account);@RequestLine("POST /trade/order")@Headers("Content-Type: application/json")@Body("%7B\"stock\": \"{stock}\", \"price\": {price}, \"count\":{count}%7D")Stringorder(@Param("stock")String stock,@Param("price")Double price,@Param("count")Double count);}publicclassTestFeign{publicstaticvoidmain(String[]args){ITradeService tradeService=Feign.builder().options(newOptions(2000,6000)).target(ITradeService.class,"http://localhost:2002");String result=tradeService.queryFund("xumiao");System.out.println(result);result=tradeService.order("300033",20.0,1000.0);System.out.println(result);}}
? 從上文中可以看到,Feign支持get和post請求,并且新定義了一套注解,這種方式一定程度上提高了學習成本。Spring對feign進行整合后,對Spring MVC注解做了一定程度上的支持,基本滿足項目中的使用,推薦使用Spring MVC注解。Feign默認的協議規范,如下:
? 新建一個Spring Boot的Moudle工程,命名spring-cloud-feign,在pom文件中加入相關依賴,application.yml文件中增加eureka相關配置啟動類增加@EnableFeignClients注解,開啟Feign Client功能,該程序就具備了Feign功能了。
根據需求,只需要創建一個包含資金查詢和交易下單的接口即可,在接口上加@FeignClient注解來聲明一個Feign Client,其中name為遠程調用其他服務的服務名,本工程中使用eureka作為注冊中心,則name的值即為服務端注冊在eureka中的服務名稱,代碼如下:
@FeignClient(name="trade-server")publicinterfaceITradeService{@RequestMapping(value="/trade/queryFund")StringqueryFund(@RequestParam("account")String account);@RequestMapping(value="/trade/order")Stringorder(@RequestParam("stock")String stock,@RequestParam("price")Double price,@RequestParam("count")Double count);@RequestMapping(value="/trade/orderJSON")StringorderJson(@RequestBodyJSONObject json);}
? 新增相關controller,提供外部調用接口,使用ITradeService進行相關遠程服務的調用,部分代碼如下:
@RestController@RequestMapping("/trade")publicclassTradeController{@AutowiredprivateITradeService tradeService;@RequestMapping(value="/queryFund")publicStringqueryFund(String account){returntradeService.queryFund(account);}...}
至此,已經完成需求中的下單和資金查詢的功能,至于普通用戶和機構客戶的身份認證可以通過攔截器來實現,下文將在講述攔截器的時候進一步實現該功能。瀏覽器中訪問http://localhost:2000/trade/queryFund?account=3302…即可訪問提供的服務了。結果如下:
復制traderServer-1命名為traderServer-2,同時啟動traderServer-1和traderServer-2,發現Feign具備負載均衡功能。因為Feign本身并不支持負載均衡,屬于Ribbon中的內容,有興趣的同學建議去了解一下Ribbon。至此,工程架構圖如下:
? FeignClient默認的配置類為FeignClientsConfiguration,打開這個類,可以發現這個類注入了很多Feign相關的配置Bean,包括Retryer、FeignLoggerFactory、FormattingConversionService等。另外,Decoder、Encoder和Contract這3個類使用@ConditionalOnMissingBean標記,即在沒有Bean注入的情況下,會自動注入默認配置的Bean,部分代碼如下:
@ConfigurationpublicclassFeignClientsConfiguration{···//省略代碼@Bean@ConditionalOnMissingBeanpublicDecoderfeignDecoder(){returnnewOptionalDecoder(newResponseEntityDecoder(newSpringDecoder(this.messageConverters)));}@Bean@ConditionalOnMissingBeanpublicEncoderfeignEncoder(){returnnewSpringEncoder(this.messageConverters);}@Bean@ConditionalOnMissingBeanpublicContractfeignContract(ConversionService feignConversionService){returnnewSpringMvcContract(this.parameterProcessors,feignConversionService);}···//省略代碼}
我們在實際使用中,可根據具體需求覆蓋掉 FeignClientsConfiguration類中默認的配置Bean,從而達到自定義配置的目的。例如在Feign默認的配置在請求失敗后,重試次數為0。現在希望請求失敗后能夠重試,這時寫一個配置FeignConfig類,在該類中注入Retryer的Bean,覆蓋掉默認的Retryer的Bean,并將FeignConfig指定為ITradeService的配置類。
? FeignConfig類代碼如下:
publicclassFeignConfig{@BeanpublicRetryerfeignRetryer(){returnnewRetryer.Default(100,SECONDS.toMillis(1),5);}}
注意:@FeignClient標注的目標接口類中使用的方法注解一定要與Contract契約相匹配。
我們可以寫個例子看一下,不匹配的時候會有什么現象,看下面的例子:
此時,@FeignClient標注接口方法中使用的還是MVC契約的注解,當用Feign原生契約覆蓋默認的MVC契約時,在原工廠中新建一個FeignConfiguration 配置類,代碼如下:
@ConfigurationpublicclassFeignConfiguration@BeanpublicContractfeignContract(){returnnewfeign.Contract.Default();}}
在@Configuration不被注釋并且配置類與啟動類在統計目錄下時,啟動服務,會報一個比較常見的錯誤,如下:
? 報錯原因:
? 當把Contract替換為feign.Contract.Default()后,ITradeService中方法上使用的注解還是基于MVC的spring-web包中的注解,兩種契約出現沖突,所以拋出此異常。同理,當使用MVC契約,接口中使用Feign原生注解時,也會拋出此異常。
? 除上述情況之外,在不加@Configuration時,雖然不會啟動報錯,但是一旦 FeignConfiguration被@FeignClient使用,并且接口被類似@Autowired注解標記,啟動會報同樣的錯,代碼如下:
@FeignClient(name="trade-server",configuration=FeignConfiguration.class)publicinterfaceTradeFeignClient{···// 省略代碼}@RestController@RequestMapping("/trade")publicclassTradeController{@AutowiredprivateTradeFeignClient tradeFeignClient;···// 省略代碼}
? 若將改配置類置于@ComponentScan掃描范圍(默認啟動類同級目錄)之外,此時,可啟動正常。
? 根據這個現象,可以得出一個結論:
? 加了@Configuration注解,那么該類不能存放在主應用程序上下文@ComponentScan所掃描的包中。否則, 該類中的配置的feign.Decoder、feign.Encoder、feign.Contract 等配置就會被所有的@FeignClient共享,一旦Contract契約與注解不匹配時,會出錯,所以最好不要混用。
? Client在執行Http Request之前,會執行相關RequestInterceptor攔截器,而Feign中默認也實現了BasicAuthRequestInterceptor,用于訪問服務時,進行用戶名和密碼的基礎認證,一般與Spring-cloud-security共同使用。同樣,可通過配置類進行攔截器的定義,代碼如下:
publicclassFooConfiguration{@BeanpublicBasicAuthRequestInterceptorbasicAuthRequestlnterceptor(){returnnewBasicAuthRequestInterceptor("organ","123456");}}
增加上述代碼后,引入上述FooConfiguration的FeignClient就具有HttpBasic認證的功能了。
? 我們再回顧上文中第3個需求,對機構和普通用戶做一個身份認證,以便給高等級機構用戶提供一個快速下單通道。對于此處的用戶身份認證,可以采用spring-cloud-security做基礎認證。
? 具體方案如下:
? 首先復制一下trade-server工程,命名為trade-server-auth,引入spring-cloud-security相關依賴,增加security相關配置類,代碼如下
@SuppressWarnings("deprecation")@Configuration@EnableWebSecurity@EnableGlobalMethodSecurity(prePostEnabled=true)// 開啟方法級別保護publicclassSecurityConfigextendsWebSecurityConfigurerAdapter{@BeanpublicPasswordEncoderpasswordEncoder(){returnNoOpPasswordEncoder.getInstance();}/**
? ? * @param authenticationManagerBuilder
? ? * @throws Exception
? ? */@Overrideprotectedvoidconfigure(AuthenticationManagerBuilder auth)throwsException{auth.userDetailsService(userDetailsService()).passwordEncoder(passwordEncoder());}@Overrideprotectedvoidconfigure(HttpSecurity http)throwsException{// 所有的請求,都需要經過HTTP basici人證http.authorizeRequests().anyRequest().authenticated().and().httpBasic();}@BeanpublicUserDetailsServiceuserDetailsService(){InMemoryUserDetailsManager manager=newInMemoryUserDetailsManager(// 機構角色manager.createUser(User.withUsername("organ").password("123456").roles("ORGAN").build());// 個人角色 manager.createUser(User.withUsername("person").password("123456").roles("PERSON").build());returnmanager;}}
? 上述代碼中,約定了organ和person這兩種用戶可以訪問下單服務,所有請求都需要做HttpBasic認證。
? 服務提供方已經做好相關security的基礎認證,服務調用方在調用服務的時候將身份信息傳遞過來即可,服務提供者根據調用者傳入身份信息進行身份認證,機構用戶則可以走機構用戶下單的快速渠道。
? 接下來編寫客戶端相關代碼,復制spring-cloud-feign命名為spring-cloud-feign-auth,新建TradeFooClient類引入上文中的FooConfiguration配置,代碼如下:
@FeignClient(name="trade-server-auth",configuration=FooConfiguration.class)publicinterfaceTradeFooClient{@RequestMapping(value="/trade/queryFund")StringqueryFund(@RequestParam("account")String account);@RequestMapping(value="/trade/orderJson")StringorderJson(@RequestParam(value="stock")String stock,@RequestParam(value="price")Double price,@RequestParam(value="count")Double count);}
? 此時,我們已經基于攔截器的方式,實現了對用戶身份識別,至于上文中第3個需求,機構用戶使用快色渠道下單,只需要在識別身份后做對應的分發即可。
? Fegin本身不支持負載均衡,其整合了Ribbon,通過Ribbon實現負載均衡。
? FeginRibbonClientAutoConfiguration類通過@Import引入了HttpClientFeignLoadBalancedConfiguration、Ok-HttpFeignLoadBalancedConfiguration、DefaultFeignLoadBalancedConfiguration,不同版本可能有差異,但是目的都是為了配置Client的類型,分別為ApacheHttpClient、OkHttp和HttpURLConnection。3個配置類最終向容器注入的都是Client的實現類LoadBalancerFeignClient,即負載均衡客戶端。查看LoadBalancerFeignClient的execute方法,代碼如下:
@OverridepublicResponseexecute(Request request,Request.Options options)throwsIOException{···// 省略代碼returnlbClient(clientName).executeWithLoadBalancer(ribbonRequest,requestConfig).toResponse();···// 省略代碼}
? 其中 executeWithLoadBalancer()方法,即通過負載均衡的方式來執行網絡請求。代碼繼續跟進到LoadBalancerCommand,其中selectServer()方法則為選擇服務進行負載均衡的方法,代碼如下:
privateObservable<Server>selectServer(){returnObservable.create(newOnSubscribe<Server>(){@Overridepublicvoidcall(Subscriber<?superServer>next){try{Server server=loadBalancerContext.getServerFromLoadBalancer(loadBalancerURI,loadBalancerKey);next.onNext(server);next.onCompleted();}catch(Exceptione){next.onError(e);}}});}
由上述代碼可知,負載均衡的服務選擇策略是 loadBalancerContext實現的,是ribbonloadbalancer包中的類。實際上feign本身是沒有負載均衡能力的,spring-cloud-feign整合了ribbon使其具有負載均衡功能。如果需要有效的使用feign的負載均衡功能,建議先熟悉一下ribbon負載均衡的用法。
同時啟動兩個server時,工程架構圖,如下:
? 總的來說,Feign的源碼實現過程如下:
? 1.首先通過@EnableFeignClients注解開啟FeignClient功能,只有這個注解存在,才會在程序啟動時開啟對@FeignClient注解包的掃描。
? 2.根據Feign的規則實現接口、并在接口上面加上@FeignClient注解。
? 3.程序啟動后,會進行包掃描,掃描所有的@FeignClient的注解類,并將這些信息注入IoC容器。
? 4.當接口的方法被調用時,通過JDK的代理類生產具體的RequestTemplate模板對象。
? 5.根據RequestTemplate再生成Http請求的Request對象。
? 6.Request對象交給Client處理,其中Client的網絡請求框架可以是HttpURLConnection、HttpClient和OkHttp。
? 7.最后Client被封裝到LoadBalanceClient類,這個類結合Ribbon做到了負載均衡。
1.《深入理解Srping Cloud 與微服務構建》
2.https://blog.csdn.net/luanlouis/article/details/82821294