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

目录:

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

Feign简介:

Feign是一款Netflix开源的声明式模板化http客户端,它可以更加便捷、优雅的调用http api;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客户端)

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

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

模块API:

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

feign api:

1 @FeignClient(name = "ad-model")
2 public interface AdRemoteService {
3 
4     @GetMapping("/ad/getUserAd/{account}")
5     String getUserAd(@PathVariable(name = "account") String account);
6 }

4、调用

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

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

Ribbon饥饿模式

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

1 # 开启ribbon的饥饿加载模式
2 ribbon.eager-load.enabled=true
3 # 指定需要饥饿加载模式的客户端服务名(多个服务以逗号分隔)
4 ribbon.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")
 3 public class UserApi {
 4 
 5     @GetMapping(value = "/get/{name}")
 6     public UserModel getUserByName(@PathVariable("name") String userName) {
 7         return Xxx.get(userName);
 8     }
 9     
10 }
11 
12 @FeignClient(name = "user-provider")
13 public interface ProviderService {
14 
15     @GetMapping(value = "/userProvider/get/{name}")
16     UserModel getUserByName(@PathVariable("name") String userName);
17 
18 }

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

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

1 @FeignClient(name = "user-provider", path = "/userProvider")
2 public interface ProviderService {
3 
4     @GetMapping(value = "/get/{name}")
5     UserModel getUserByName(@PathVariable("name") String userName);
6 
7 }

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

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

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

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

1 class FeignClientFactoryBean implements FactoryBean<Object>, InitializingBean,
2         ApplicationContextAware {}

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

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

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

 1 @Override
 2 public Object getObject() throws Exception {
 3     FeignContext context = applicationContext.getBean(FeignContext.class);
 4     Feign.Builder builder = feign(context);
 5 
 6     if (!StringUtils.hasText(this.url)) {
 7         String url;
 8         // 如果name未指定http则会加上
 9         if (!this.name.startsWith("http")) {
10             url = "http://" + this.name;
11         }
12         else {
13             url = this.name;
14         }
15         // 为接口加上调用路径的前缀
16         url += cleanPath();
17         return loadBalance(builder, context, new HardCodedTarget<>(this.type,
18                 this.name, url));
19     }
20     // 如果有置顶url并且url不是以http开头,同样的会加上http://
21     if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
22         this.url = "http://" + this.url;
23     }
24     // 为接口加上调用路径的前缀
25     String url = this.url + cleanPath();
26     // 获取操作客户端
27     Client client = getOptional(context, Client.class);
28     if (client != null) {
29         if (client instanceof LoadBalancerFeignClient) {
30             // not lod balancing because we have a url,
31             // but ribbon is on the classpath, so unwrap
32             client = ((LoadBalancerFeignClient)client).getDelegate();
33         }
34         builder.client(client);
35     }
36     // 获取目标源并调用target来创建代理类,获取Feign示例对象
37     Targeter targeter = get(context, Targeter.class);
38     return targeter.target(this, builder, context, new HardCodedTarget<>(
39             this.type, this.name, url));
40 }
 1 protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
 2             HardCodedTarget<T> target) {
 3     Client client = getOptional(context, Client.class);
 4     if (client != null) {
 5         builder.client(client);
 6         Targeter targeter = get(context, Targeter.class);
 7         return targeter.target(this, builder, context, target);
 8     }
 9 
10     throw new IllegalStateException(
11             "No Feign Client for loadBalancing defined. Did you forget to include spring-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里来的,new HardCodedTarget<>(this.type, this.name, url)

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

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

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

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

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

1 InvocationHandler handler = factory.create(target, methodToHandler);
2 T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[]{target.type()}, handler);

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

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

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

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

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

 1 public Map<String, MethodHandler> apply(Target key) {
 2   // 解析并校验方法的元数据
 3   List<MethodMetadata> metadata = contract.parseAndValidatateMetadata(key.type());
 4   Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>();
 5   // 遍历元数据,获取方法签名与MethodHandler的映射关系
 6   for (MethodMetadata md : metadata) {
 7     BuildTemplateByResolvingArgs buildTemplate;
 8     if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
 9       buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder);
10     } else if (md.bodyIndex() != null) {
11       buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder);
12     } else {
13       buildTemplate = new BuildTemplateByResolvingArgs(md);
14     }
15     result.put(md.configKey(),
16                factory.create(key, md, buildTemplate, options, decoder, errorDecoder));
17   }
18   return result;
19 }
20 
21 @Override
22 public List<MethodMetadata> parseAndValidatateMetadata(Class<?> targetType) {
23   checkState(targetType.getTypeParameters().length == 0, "Parameterized types unsupported: %s",
24              targetType.getSimpleName());
25   checkState(targetType.getInterfaces().length <= 1, "Only single inheritance supported: %s",
26              targetType.getSimpleName());
27   if (targetType.getInterfaces().length == 1) {
28     checkState(targetType.getInterfaces()[0].getInterfaces().length == 0,
29                "Only single-level inheritance supported: %s",
30                targetType.getSimpleName());
31   }
32   Map<String, MethodMetadata> result = new LinkedHashMap<String, MethodMetadata>();
33   for (Method method : targetType.getMethods()) {
34     if (method.getDeclaringClass() == Object.class ||
35         (method.getModifiers() & Modifier.STATIC) != 0 ||
36         Util.isDefault(method)) {
37       continue;
38     }
39     // 根据Method以及Target信息,为方法创建MethodMetadata对象
40     MethodMetadata metadata = parseAndValidateMetadata(targetType, method);
41     checkState(!result.containsKey(metadata.configKey()), "Overrides unsupported: %s",
42                metadata.configKey());
43     result.put(metadata.configKey(), metadata);
44   }
45   return new ArrayList<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(String userName)的签名为ProviderService#getUserByName(String),也就是类名#方法名(参数列表)。

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

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

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

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

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

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

通过代理对象执行请求

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

 1 static final class Default implements InvocationHandlerFactory {
 2   @Override
 3   public InvocationHandler create(Target target, Map<Method, MethodHandler> dispatch) {
 4     return new ReflectiveFeign.FeignInvocationHandler(target, dispatch);
 5   }
 6 }
 7 
 8 @Override
 9 public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
10   if ("equals".equals(method.getName())) {
11     try {
12       Object
13           otherHandler =
14           args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
15       return equals(otherHandler);
16     } catch (IllegalArgumentException e) {
17       return false;
18     }
19   } else if ("hashCode".equals(method.getName())) {
20     return hashCode();
21   } else if ("toString".equals(method.getName())) {
22     return toString();
23   }
24   return dispatch.get(method).invoke(args);
25 }
26 
27 @Override
28 public Object invoke(Object[] argv) throws Throwable {
29   // 拿到请求参数,创建请求模板
30   RequestTemplate template = buildTemplateFromArgs.create(argv);
31   Retryer retryer = this.retryer.clone();
32   while (true) {
33     try {
34       // 将RequestTemplate转为Request,来真真发起http请求
35       return executeAndDecode(template);
36     } catch (RetryableException e) {
37       retryer.continueOrPropagate(e);
38       if (logLevel != Logger.Level.NONE) {
39         logger.logRetry(metadata.configKey(), logLevel);
40       }
41       continue;
42     }
43   }
44 }
45   
46 @Override
47 public Object invoke(Object[] argv) throws Throwable {
48   if(handle == null) {
49     throw new IllegalStateException("Default method handler invoked before proxy has been bound.");
50   }
51   return handle.invokeWithArguments(argv);
52 }

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

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

posted @ 2019-10-13 21:57  被猪附身的人  阅读(2158)  评论(0编辑  收藏  举报