(四)使用Feign实现声明式Rest调用

使用Feign实现声明式Rest调用

文章目录

使用Feign实现声明式Rest调用

1.什么是Feign

2.Feign解决了什么问题

3.Feign工作原理

3.1.流程梳理

3.1.1.初始化流程

3.1.2.Request处理过程

3.2.FeignClient注册

3.3.创建代理

3.4.接口调用

3.5.重试策略

3.6.Client动态注入

4.Feign使用示例

4.1.原生Feign使用

4.1.1.需求

4.1.2.创建一个服务端

4.1.3.新建原生Feign客户端

4.2.Spring-cloud-feign用法

4.2.1.新建一个Feign客户端

5.FeignClient配置

5.1.常规配置

5.2.拦截器配置

6.Feign之负载均衡

7.总结

8.参考文献

1.什么是Feign

Feign是一个http请求调用的轻量级框架。使用Feign,可以直接以Java接口注解的方式发送Http请求,而不需要在Java中通过封装HTTP工具类来发送请求。

Feign源码地址:Feign

2.Feign解决了什么问题

Feign封装了Http 请求调用流程,实现了申明式Http接口调用。

使用Feign的方式调用远程服务,服务消费者与生产者不需要实现同一个接口,可以做到消费者与生产者代码上的完全解耦。就代码耦合而言,Feign与jdk提供的rmi和阿里的dubbo有所差异,后者进行远程服务调用需要实现共同的api。另外服务消费者一方只需要关注FeignClient配置即可,不需要关注具体Http Request的实现,所以说Feign最终目的是将Java Http客户端调用过程变得简单。

从角色职能划分,Feign提供http调用服务流程如下:

3.Feign工作原理

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会有所帮助。

3.1.流程梳理

​ 我们可以把Feign处理Http请求的基本流程分为两个部分,第一部分是初始化阶段,进行Proxy和MethodHandler的创建,第二部分则是具体请求处理流程。

3.1.1.初始化流程

初始化流程基本如下:

Feign初始化过程基本分为两个部分:

1.ReflectiveFeign根据指定的Contract为每一个方法创建了一个SynchronousMethodHandler;

2.基于动态代理,为Target接口创建了一个proxy对象,同时定义一个统一的InvocationHandler用于请求处理,将请求分发到指定的SynchronousMethodHandler处理。

3.1.2.Request处理过程

Request处理过程基本如下:

Feign封装了整个Request的处理过程,按照请求顺序,如下:

1.具体方法处理类SynchronousMethodHandler创建请求模板;

2.对Request请求进行预处理,编码;

3.将Request交给client去执行处理,若有拦截器先执行拦截器;

4.返回结果处理,解码;

5.返回结果最终转化为javaBean交付给具体方法处理类SynchronousMethodHandler。

3.2.FeignClient注册

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

3.3.创建代理

​ 注入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了。

3.4.接口调用

​ 根据上文创建代理部分可知,当调用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.请求异常,执行重试策略。

3.5.重试策略

从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单独使用的情况下,建议定义一下重试策略。

3.6.Client动态注入

​ 看到这里,其实还有一个疑问,执行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注解,而这种方式是静态注入。

4.Feign使用示例

4.1.原生Feign使用

4.1.1.需求

用户下单;

通过资金账号查询资产;

下单时对用户等级做身份认证,给高等级的机构用户提供快速下单渠道。

4.1.2.创建一个服务端

​ 新建一个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,下单成功";}}

4.1.3.新建原生Feign客户端

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默认的协议规范,如下:

4.2.Spring-cloud-feign用法

4.2.1.新建一个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。至此,工程架构图如下:

5.FeignClient配置

5.1.常规配置

​ 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契约与注解不匹配时,会出错,所以最好不要混用。

5.2.拦截器配置

​ 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个需求,机构用户使用快色渠道下单,只需要在识别身份后做对应的分发即可。

6.Feign之负载均衡

​ 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时,工程架构图,如下:

7.总结

​ 总的来说,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做到了负载均衡。

8.参考文献

1.《深入理解Srping Cloud 与微服务构建》

2.https://blog.csdn.net/luanlouis/article/details/82821294

3.https://blog.csdn.net/lgq2626/article/details/80392914

4.https://segmentfault.com/a/1190000014981170

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,029评论 6 492
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,395评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,570评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,535评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,650评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,850评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,006评论 3 408
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,747评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,207评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,536评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,683评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,342评论 4 330
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,964评论 3 315
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,772评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,004评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,401评论 2 360
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,566评论 2 349

推荐阅读更多精彩内容

  • Feign是一个声明式的Web服务客户端。这使得Web服务客户端的写入更加方便 要使用Feign创建一个界面并对其...
    咔啡阅读 416评论 0 1
  • Feign是一个声明式的Web服务客户端。这使得Web服务客户端的写入更加方便 要使用Feign创建一个界面并对其...
    咔啡阅读 176评论 0 1
  • Feign是一个声明式的Web服务客户端。这使得Web服务客户端的写入更加方便 要使用Feign创建一个界面并对其...
    咔啡阅读 179评论 0 1
  •   在上一章中,讲解了如何使用RestTemplate来消费服务,如何结合Ribbon在消费服务时做负载均衡。本章...
    CarlosBen阅读 705评论 0 0
  • Spring Cloud是一系列框架的有序集合。它利用Spring Boot的开发便利性巧妙地简化了分布式系统基础...
    程序媛马小兮阅读 409评论 0 0