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。