首页 文章资讯内容详情

SpringCloud学习笔记(六、SpringCloud Netflix Feign)

2026-06-01 4 花语

本文内容纲要:

-目录: -Feign简介: -Feign应用: -Ribbon饥饿模式 -Feign源码分析 -@FeignClient代理对象 -拿到元数据后封装请求,并生成代理对象 -通过代理对象执行请求

目录:

Feign简介 Feign应用 Ribbon饥饿模式 Feign源码分析

Feign简介:

Feign是一款Netflix开源的声明式、模板化的http客户端,它可以更加便捷、优雅的调用httpapi;SpringCloud对Netflix的feign进行了增强,使其支持spring并整合了ribbon、eureka以提供负载均衡的http调用。

Feign应用:

1、引入openfeign依赖

1<dependency> 2<groupId>org.springframework.cloud</groupId> 3<artifactId>spring-cloud-starter-openfeign</artifactId> 4</dependency>

2、启动类加上feign注解(需要eureka的支持,所以此模块首先需要为eureka客户端)

第一种,针对类扫描feignapi:@EnableFeignClients(clients={Xxx1.class,Xxx2.class})。 第二种,针对包扫描feignapi:@EnableFeignClients(basePackages={"com.xxx.xxx"})

3、定义feignapi:只需与模块API保持一致就可以了。

模块API:

1@RestController 2@RequestMapping("/ad") 3publicclassAdApi{ 4 5@GetMapping("/getUserAd/{account}") 6publicStringgetUserAd(@PathVariable(name="account")Stringaccount){ 7return"这是"+account+"的广告"; 8} 9}

feignapi:

1@FeignClient(name="ad-model") 2publicinterfaceAdRemoteService{ 3 4@GetMapping("/ad/getUserAd/{account}") 5StringgetUserAd(@PathVariable(name="account")Stringaccount); 6}

4、调用

调用方式很简单,就像调用方法一样就可以了

1@Autowired 2privateAdRemoteServiceadRemoteService; 3 4@GetMapping("/login/{account}/{password}") 5publicStringlogin(@PathVariableStringaccount,@PathVariableStringpassword){ 6UserDTOuserDTO=USER_INFO.get(account); 7if(userDTO==null){ 8return"FAILED"; 9} 10 11booleanresult=userDTO.getPassword().equalsIgnoreCase(password); 12if(!result){ 13return"FAILED"; 14} 15 16//调用广告接口 17StringadResult=adRemoteService.getUserAd(account); 18System.err.println(adResult); 19 20return"SUCCESS"; 21}

Ribbon饥饿模式

首先我们知道Ribbon默认是懒加载模式,但这样对第一个调用者很不友好(速度比后续调用者慢很多);如果你不想这样做,那么可以通过配置将Ribbon改为饥饿模式。

1#开启ribbon的饥饿加载模式 2ribbon.eager-load.enabled=true 3#指定需要饥饿加载模式的客户端服务名(多个服务以逗号分隔) 4ribbon.eager-load.clients=service-provider1,service-provider2

Feign源码分析

在看源码前首先我们再回顾下feign的使用,feign其实很简单;你只需要定义好与服务提供者一致的方法签名(方法名可以不一样),并在类上加好@FeignClient的注解就可以通过注入的方式像调用方法一样调用其它服务的API了。

啥???只要定义接口就可以了!!!这么简单吗。没错,就是这么简单。

那这个@FeignClient肯定是对原来的接口进行了代理吧,不然怎么可能注入接口就能实现功能了呢。

嗯嗯,顺着这个思路我就先来猜测下feign的实现思路:

被@FeignClient注解的接口会有一个实现的代理类。 被代理的类会再根据类似于的@RequestMapping的注解的元数据(如value,也就是请求url)来封装请求。 最后通过代理对象执行请求。

注:看源码不要死磕哦,先理清大致流程再追细节实现。

@FeignClient代理对象

再看@FeignClient如何生成代理对象前我们先来了解下它的几个属性:

name:模块的名称,eureka解析时使用。 url:配置调用API的绝对路径。 path:调用API的前缀。

name和url很好理解,我只简单说下path。

——————————————————————————————————————————————————————————————————————

定义一个Controller时我们通常会按照模块分类,比如订单相关的模块一般都会在Controller上加上@RequestMapping("/order"),这样调用和订单相关的都需要加上/order的前缀才能正确访问。

而我们@FeignClient下方法就业需要加/order才行,如:

1@RestController 2@RequestMapping("/userProvider") 3publicclassUserApi{ 4 5@GetMapping(value="/get/{name}") 6publicUserModelgetUserByName(@PathVariable("name")StringuserName){ 7returnXxx.get(userName); 8} 9 10} 11 12@FeignClient(name="user-provider") 13publicinterfaceProviderService{ 14 15@GetMapping(value="/userProvider/get/{name}") 16UserModelgetUserByName(@PathVariable("name")StringuserName); 17 18}

此时我们ProviderService中的getUserByName一定要在@GetMapping加上/userProvider前缀,不然就调不到服务。

针对这种情况Spring的开发者替我们想好了解决方案,也就是上面说到的path,像上面这种案例你只需要在@FeignClient中配上path就可以了:

1@FeignClient(name="user-provider",path="/userProvider") 2publicinterfaceProviderService{ 3 4@GetMapping(value="/get/{name}") 5UserModelgetUserByName(@PathVariable("name")StringuserName); 6 7}

——————————————————————————————————————————————————————————————————————

了解了上面的三个属性后我们就可以开始看看@FeignClient的动态代理是如何实现的了。

首先我们还是和往常一样,既然@FeignClient是生成代理,我们就来看看有没有这样一个完成生成代理的类。

通过IDEACtrl+n,我们找到了一个从命名上很相似的类,FeignClientFactoryBean。

1classFeignClientFactoryBeanimplementsFactoryBean<Object>,InitializingBean, 2ApplicationContextAware{}

从类的定义上可以看出它实现了FactoryBean接口,也就表示此bean是一个工厂bean,需要通过执行getObject来才会初始化,才能交给Spring容器管理。

之后大体浏览了一下此类,发现其并未有定义构造函数(说明只有一个默认的无参构造),但私有属性又有一大推,所以猜测应该是在初始化的时候动态分配了这些属性(你也可以查看@FeignClient的调用情况得知其实现在org.springframework.cloud.netflix.feign.FeignClientsRegistrar#registerFeignClients)。

说了这么多,我们还没说getObject方法,(⊙o⊙)…

1@Override 2publicObjectgetObject()throwsException{ 3FeignContextcontext=applicationContext.getBean(FeignContext.class); 4Feign.Builderbuilder=feign(context); 5 6if(!StringUtils.hasText(this.url)){ 7Stringurl; 8//如果name未指定http则会加上 9if(!this.name.startsWith("http")){ 10url="http://"+this.name; 11} 12else{ 13url=this.name; 14} 15//为接口加上调用路径的前缀 16url+=cleanPath(); 17returnloadBalance(builder,context,newHardCodedTarget<>(this.type, 18this.name,url)); 19} 20//如果有置顶url并且url不是以http开头,同样的会加上http:// 21if(StringUtils.hasText(this.url)&&!this.url.startsWith("http")){ 22this.url="http://"+this.url; 23} 24//为接口加上调用路径的前缀 25Stringurl=this.url+cleanPath(); 26//获取操作客户端 27Clientclient=getOptional(context,Client.class); 28if(client!=null){ 29if(clientinstanceofLoadBalancerFeignClient){ 30//notlodbalancingbecausewehaveaurl, 31//butribbonisontheclasspath,sounwrap 32client=((LoadBalancerFeignClient)client).getDelegate(); 33} 34builder.client(client); 35} 36//获取目标源并调用target来创建代理类,获取Feign示例对象 37Targetertargeter=get(context,Targeter.class); 38returntargeter.target(this,builder,context,newHardCodedTarget<>( 39this.type,this.name,url)); 40} 1protected<T>TloadBalance(Feign.Builderbuilder,FeignContextcontext, 2HardCodedTarget<T>target){ 3Clientclient=getOptional(context,Client.class); 4if(client!=null){ 5builder.client(client); 6Targetertargeter=get(context,Targeter.class); 7returntargeter.target(this,builder,context,target); 8} 9 10thrownewIllegalStateException( 11"NoFeignClientforloadBalancingdefined.Didyouforgettoincludespring-cloud-starter-netflix-ribbon?"); 12}

来来来,其实你只要结合上面说到的三个属性以及注释就能大致了解getObject的基本逻辑。

它分为两块,一个是定义了绝对路径,另一个是没有定义绝对路径,也就是上面说到的url属性。如果没有定义呢,我就调用loadBalance来,不然呢就往后执行。

猜测呢可能是有url就指定调用服务,没有呢就会根据注册到eureka的来使用ribbon负载调用(反正也不知道对嘛,先猜呗)。

——————————————————————————————————————————————————————————————————————

反正呢,不管走哪条路都会到target.target。

到这又有两条线,一个是org.springframework.cloud.netflix.feign.DefaultTargeter#target,一个是org.springframework.cloud.netflix.feign.HystrixTargeter#target

反正肯定不是Hystrix的,那我们看看DefaultTarget的呗。

至此@FeignClient的代理对象也快要发现了,我们继续看看。

拿到元数据后封装请求,并生成代理对象

上面走到了org.springframework.cloud.netflix.feign.DefaultTargeter#target,我们再往后点点就能发现宝藏了,坚持下,嘿嘿。

之后你继续跟就会找到宝藏的所在地:feign.ReflectiveFeign#newInstance,其中入参只有一个feign.Target

而根据我们的链路你就能知道这个target是从org.springframework.cloud.netflix.feign.FeignClientFactoryBean#getObject里来的,newHardCodedTarget<>(this.type,this.name,url)

HardCodedTarget的name和url都说过了,而type的初始化逻辑你也能在org.springframework.cloud.netflix.feign.FeignClientsRegistrar#registerFeignClients中得到答案,type=加了@FeignClient注解的类全路径

——————————————————————————————————————————————————————————————————————

上面的入参介绍完后就可以来看看我们的宝藏了:

1@Override 2public<T>TnewInstance(Target<T>target){ 3Map<String,MethodHandler>nameToHandler=targetToHandlersByName.apply(target); 4Map<Method,MethodHandler>methodToHandler=newLinkedHashMap<Method,MethodHandler>(); 5List<DefaultMethodHandler>defaultMethodHandlers=newLinkedList<DefaultMethodHandler>(); 6 7for(Methodmethod:target.type().getMethods()){ 8if(method.getDeclaringClass()==Object.class){ 9continue; 10}elseif(Util.isDefault(method)){ 11DefaultMethodHandlerhandler=newDefaultMethodHandler(method); 12defaultMethodHandlers.add(handler); 13methodToHandler.put(method,handler); 14}else{ 15methodToHandler.put(method,nameToHandler.get(Feign.configKey(target.type(),method))); 16} 17} 18InvocationHandlerhandler=factory.create(target,methodToHandler); 19Tproxy=(T)Proxy.newProxyInstance(target.type().getClassLoader(),newClass<?>[]{target.type()},handler); 20 21for(DefaultMethodHandlerdefaultMethodHandler:defaultMethodHandlers){ 22defaultMethodHandler.bindTo(proxy); 23} 24returnproxy; 25}

是不是看到我们所熟悉的动态代理了,哈哈。

1InvocationHandlerhandler=factory.create(target,methodToHandler); 2Tproxy=(T)Proxy.newProxyInstance(target.type().getClassLoader(),newClass<?>[]{target.type()},handler);

然后捏就是一步步分析这两步所需要的参数从哪来的,怎么来的,来来来我们继续。

target上面已经说了,而handler就是InvocationHander,所以未知的参数就只有methodToHandler,我们看看它咋来的。

emmmmm,粗略的瞅了瞅还是要重头开始看。。。主要是三个map。

1Map<String,MethodHandler>nameToHandler=targetToHandlersByName.apply(target); 2Map<Method,MethodHandler>methodToHandler=newLinkedHashMap<Method,MethodHandler>(); 3List<DefaultMethodHandler>defaultMethodHandlers=newLinkedList<DefaultMethodHandler>();

nameToHandler:方法签名与MethodHandler的映射关系。

1publicMap<String,MethodHandler>apply(Targetkey){ 2//解析并校验方法的元数据 3List<MethodMetadata>metadata=contract.parseAndValidatateMetadata(key.type()); 4Map<String,MethodHandler>result=newLinkedHashMap<String,MethodHandler>(); 5//遍历元数据,获取方法签名与MethodHandler的映射关系 6for(MethodMetadatamd:metadata){ 7BuildTemplateByResolvingArgsbuildTemplate; 8if(!md.formParams().isEmpty()&&md.template().bodyTemplate()==null){ 9buildTemplate=newBuildFormEncodedTemplateFromArgs(md,encoder); 10}elseif(md.bodyIndex()!=null){ 11buildTemplate=newBuildEncodedTemplateFromArgs(md,encoder); 12}else{ 13buildTemplate=newBuildTemplateByResolvingArgs(md); 14} 15result.put(md.configKey(), 16factory.create(key,md,buildTemplate,options,decoder,errorDecoder)); 17} 18returnresult; 19} 20 21@Override 22publicList<MethodMetadata>parseAndValidatateMetadata(Class<?>targetType){ 23checkState(targetType.getTypeParameters().length==0,"Parameterizedtypesunsupported:%s", 24targetType.getSimpleName()); 25checkState(targetType.getInterfaces().length<=1,"Onlysingleinheritancesupported:%s", 26targetType.getSimpleName()); 27if(targetType.getInterfaces().length==1){ 28checkState(targetType.getInterfaces()[0].getInterfaces().length==0, 29"Onlysingle-levelinheritancesupported:%s", 30targetType.getSimpleName()); 31} 32Map<String,MethodMetadata>result=newLinkedHashMap<String,MethodMetadata>(); 33for(Methodmethod:targetType.getMethods()){ 34if(method.getDeclaringClass()==Object.class|| 35(method.getModifiers()&Modifier.STATIC)!=0|| 36Util.isDefault(method)){ 37continue; 38} 39//根据Method以及Target信息,为方法创建MethodMetadata对象 40MethodMetadatametadata=parseAndValidateMetadata(targetType,method); 41checkState(!result.containsKey(metadata.configKey()),"Overridesunsupported:%s", 42metadata.configKey()); 43result.put(metadata.configKey(),metadata); 44} 45returnnewArrayList<MethodMetadata>(result.values()); 46}

主要逻辑就是从feign.Contract.BaseContract#parseAndValidateMetadata构造出MethodMetadata后将MethodMetadata的configKey与MethodHandler关联到map中。

其中MethodMetadata.configKey=方法签名。如,com.jdr.maven.sc.integration.userconsumer.controller.api.ProviderService#getUserByName(StringuserName)的签名为ProviderService#getUserByName(String),也就是类名#方法名(参数列表)。

methodToHandler:Method与MethodHandler的映射关系;defaultMethodHandlers:默认方法的处理。

1for(Methodmethod:target.type().getMethods()){ 2if(method.getDeclaringClass()==Object.class){ 3continue; 4}elseif(Util.isDefault(method)){ 5//如果方法是默认方法页添加到defaultMethodHandlers中 6DefaultMethodHandlerhandler=newDefaultMethodHandler(method); 7defaultMethodHandlers.add(handler); 8methodToHandler.put(method,handler); 9}else{ 10methodToHandler.put(method,nameToHandler.get(Feign.configKey(target.type(),method))); 11} 12} 13 14//没看懂为啥要调这个 15for(DefaultMethodHandlerdefaultMethodHandler:defaultMethodHandlers){ 16defaultMethodHandler.bindTo(proxy); 17}

methodToHandler很好理解,将所有需要代理的方法添加到此map中并进行代理,那为啥还要有个defaultMethodHandlers呢???为什么要区分,没懂!

——————————————————————————————————————————————————————————————————————

至此生成代理对象结束,我们来总结下:

拿到方法签名与MethodHandler的映射,其中MethodHandler是根据Class对象构造的方法数据元MethodMetadata得到的。 根据被@FeignClient标记的接口拿到所有的方法列表,并构造Method与MethodHandler的映射,以及得到默认的方法处理器defaultMethodHandlers。 得到所有方法与之对应的方法处理关系后(methodToHandler)便利用InvocationHandler与Proxy来实现动态代理,构造代理类。

通过代理对象执行请求

代理类生成后是如何执行的呢,到了这步就已经很简单了。我们只要看下**InvocationHandlerhandler=factory.create(target,methodToHandler);**是如何构造出来的就可以了。

1staticfinalclassDefaultimplementsInvocationHandlerFactory{ 2@Override 3publicInvocationHandlercreate(Targettarget,Map<Method,MethodHandler>dispatch){ 4returnnewReflectiveFeign.FeignInvocationHandler(target,dispatch); 5} 6} 7 8@Override 9publicObjectinvoke(Objectproxy,Methodmethod,Object[]args)throwsThrowable{ 10if("equals".equals(method.getName())){ 11try{ 12Object 13otherHandler= 14args.length>0&&args[0]!=null?Proxy.getInvocationHandler(args[0]):null; 15returnequals(otherHandler); 16}catch(IllegalArgumentExceptione){ 17returnfalse; 18} 19}elseif("hashCode".equals(method.getName())){ 20returnhashCode(); 21}elseif("toString".equals(method.getName())){ 22returntoString(); 23} 24returndispatch.get(method).invoke(args); 25} 26 27@Override 28publicObjectinvoke(Object[]argv)throwsThrowable{ 29//拿到请求参数,创建请求模板 30RequestTemplatetemplate=buildTemplateFromArgs.create(argv); 31Retryerretryer=this.retryer.clone(); 32while(true){ 33try{ 34//将RequestTemplate转为Request,来真真发起http请求 35returnexecuteAndDecode(template); 36}catch(RetryableExceptione){ 37retryer.continueOrPropagate(e); 38if(logLevel!=Logger.Level.NONE){ 39logger.logRetry(metadata.configKey(),logLevel); 40} 41continue; 42} 43} 44} 45 46@Override 47publicObjectinvoke(Object[]argv)throwsThrowable{ 48if(handle==null){ 49thrownewIllegalStateException("Defaultmethodhandlerinvokedbeforeproxyhasbeenbound."); 50} 51returnhandle.invokeWithArguments(argv); 52}

——————————————————————————————————————————————————————————————————————

源码分析所获:注解的实现可参考org.springframework.cloud.netflix.feign.FeignClientsRegistrar

本文内容总结:目录:,Feign简介:,Feign应用:,Ribbon饥饿模式,Feign源码分析,@FeignClient代理对象,拿到元数据后封装请求,并生成代理对象,通过代理对象执行请求,

原文链接:https://www.cnblogs.com/bzfsdr/p/11661968.html