SpringCloud学习笔记(六、SpringCloud Netflix Feign)
本文内容纲要:
-目录:
-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