通过上篇我们了解OpenFeign他也可以完成远程通信,但是它并不是真正义意上的RPC通信,因为他是通过封装代理来实现的,下面和以前一样,知道了怎么用就来看下他是怎么实现的。
一、思考Feign要做的事情
有了ribbon的铺垫现在看OpenFeign应该很清楚的知道,这玩意就是通过注解拿到服务名,然后通过服务名获取服务列表,进行解析和负载最终拼接出一个URI路径进行代理请求,那么他要完成这一系列动作他就要做下面几件事。
- 参数的解析和装载
- 针对指定的FeignClient,生成动态代理
- 针对FeignClient中的方法描述进行解析
- 组装出一个Request对象,发起请求
二、源码分析
看过我写的ribbon的应该清楚,如果想要找到进入源码的入口那么应该要找的是FeignClient,但是FeignClient是在哪里被解析的呢,在应用篇中我在启动类中加了个@EnableFeignClients注解,这 个注解的作用其实就是开启了一个FeignClient的扫描,那么点击启动类的@EnableFeignClients注解看下他是怎么开启FeignClient的扫描的,进去后发现里面有个@Import(FeignClientsRegistrar.class)这个FeignClientsRegistrar跟Bean的动态装载有关
点击进去有个registerBeanDefinitions方法通过名称可以知道是一个Bean的注入方法
下面我写一个简单的例子来描述他是如何实现动态加载的,学FeignClientsRegistrar类 implements ImportBeanDefinitionRegistrar接口并实现registerBeanDefinitions方法
这一步搞完后,定义一个注解,把@EnableFeignClients注解上的注解都抄过来并把@Import注解里面的类改成我们自己定义的类
然后在启动类上用上自定义的注解,那么在启动类时就可以进行一个Bean的动态装载了
通过这个概念已经很清楚源码中FeignClientsRegistrar类的FeignClientsRegistrar是怎么完成Bean的动态加载了
- registerDefaultConfifiguration 方法内部从 SpringBoot 启动类上检查是否有@EnableFeignClients, 有该注解的话, 则完成 Feign 框架相关的一些配置内容注册
- registerFeignClients 方法内部从 classpath 中, 扫描获得 @FeignClient 修饰的类, 将类的内容解析为 BeanDefifinition , 最终通过调用 Spring 框架中的BeanDefifinitionReaderUtils.resgisterBeanDefifinition 将解析处理过的 FeignClientBeanDeififinition 添加到 spring 容器中.
@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
BeanDefinitionRegistry registry) {
//注册默认配置信息,将EnableFeignClients的defaultConfiguration注册到Spring容器中
registerDefaultConfiguration(metadata, registry);
//注册FeignClients(可能有多个),@FeignClient注解的接口注册到Spring容器中。
registerFeignClients(metadata, registry);
}
如果有该注解,则开启包扫描,扫描被@FeignClient注解接口。
private void registerDefaultConfiguration(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { Map<String, Object> defaultAttrs = metadata .getAnnotationAttributes(EnableFeignClients.class.getName(), true); if (defaultAttrs != null && defaultAttrs.containsKey("defaultConfiguration")) { String name; if (metadata.hasEnclosingClass()) { name = "default." + metadata.getEnclosingClassName(); } else { name = "default." + metadata.getClassName(); } registerClientConfiguration(registry, name, defaultAttrs.get("defaultConfiguration")); } }
进入registerFeignClients(metadata, registry);这玩意是干啥的呢,在启动类中的@EnableFeignClients是可以定义多个basePackers的如果定义了多个那就要扫描FeignClients,下面就是扫描处理过程,下面看下是如何注册FeignClient
public void registerFeignClients(AnnotationMetadata metadata, BeanDefinitionRegistry registry) { // ClassPath的条件扫描组件提供者 ClassPathScanningCandidateComponentProvider scanner = getScanner(); // 设置资源加载器 scanner.setResourceLoader(this.resourceLoader); // 要扫描的包(@EnableFeignClients注解上添的那个) Set<String> basePackages; // 获取注解上的配置 Map<String, Object> attrs = metadata .getAnnotationAttributes(EnableFeignClients.class.getName()); // 注解过滤器,设置只过滤出FeignClient注解标识的Bean AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter( FeignClient.class); final Class<?>[] clients = attrs == null ? null : (Class<?>[]) attrs.get("clients"); if (clients == null || clients.length == 0) { // 扫描器设置过滤器 scanner.addIncludeFilter(annotationTypeFilter); // 获取注解的扫描包路径 basePackages = getBasePackages(metadata); } else { final Set<String> clientClasses = new HashSet<>(); basePackages = new HashSet<>(); for (Class<?> clazz : clients) { basePackages.add(ClassUtils.getPackageName(clazz)); clientClasses.add(clazz.getCanonicalName()); } // 只知道是类型过滤器,暂时什么作用还不明白,求解! AbstractClassTestingTypeFilter filter = new AbstractClassTestingTypeFilter() { @Override protected boolean match(ClassMetadata metadata) { // 将类名上的[$]替换成[.] String cleaned = metadata.getClassName().replaceAll("\\$", "."); return clientClasses.contains(cleaned); } }; scanner.addIncludeFilter( new AllTypeFilter(Arrays.asList(filter, annotationTypeFilter))); } for (String basePackage : basePackages) { // 从指定的包中扫描出和规范的BeanDefinition Set<BeanDefinition> candidateComponents = scanner .findCandidateComponents(basePackage); for (BeanDefinition candidateComponent : candidateComponents) { // 扫描的Bean是否是AnnotatedBeanDefinition的子类 // 虽然看不懂AnnotatedBeanDefinition是什么意思,但是顾名思义我觉得是通过注解扫出来的BeanDefinition就是他的子类 if (candidateComponent instanceof AnnotatedBeanDefinition) { // verify annotated class is an interface AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent; // 获取beanDefinition的元数据,你想要的他基本都有 AnnotationMetadata annotationMetadata = beanDefinition.getMetadata(); // 验证@FeignClient修饰的必须是接口 Assert.isTrue(annotationMetadata.isInterface(), "@FeignClient can only be specified on an interface"); // 获取@FeignClient注解的属性 Map<String, Object> attributes = annotationMetadata .getAnnotationAttributes( FeignClient.class.getCanonicalName()); // 获取客户端名称 String name = getClientName(attributes); // 为FeignClient指定配置类 registerClientConfiguration(registry, name, attributes.get("configuration")); // 注册客户端 registerFeignClient(registry, annotationMetadata, attributes); } } } }
有了上面的步骤其实准备工作就做好了,下面就看下注册FeiginClient
具体实现registerFeignClient
方法;点击registerFeignClient(registry, annotationMetadata, attributes);看下做了啥事,这里面的逻辑其实就干了一件事,就是通过BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);注入一个Bean;这个注入的过程中有个比较重要的代码BeanDefinitionBuilder definition = BeanDefinitionBuilder.genericBeanDefinition(FeignClientFactoryBean.class);这是一个构造者,构造一个BeanDefinition,里面把FeignClientFactoryBean.class给传了进去
private void registerFeignClient(BeanDefinitionRegistry registry, AnnotationMetadata annotationMetadata, Map<String, Object> attributes) { // 被@FeignClient修饰的类名,比如 com.xxx.TestFeignClient,是自己编辑的 String className = annotationMetadata.getClassName(); // BeanDefinitionBuilder通过FeignClientFactoryBean这个类来生成BeanDefinition BeanDefinitionBuilder definition = BeanDefinitionBuilder .genericBeanDefinition(FeignClientFactoryBean.class); // 验证fallback和fallbackFactory是不是接口 validate(attributes); // 通过BeanDefinitionBuilder给beanDefinition增加属性 definition.addPropertyValue("url", getUrl(attributes)); definition.addPropertyValue("path", getPath(attributes)); String name = getName(attributes); definition.addPropertyValue("name", name); definition.addPropertyValue("type", className); definition.addPropertyValue("decode404", attributes.get("decode404")); definition.addPropertyValue("fallback", attributes.get("fallback")); definition.addPropertyValue("fallbackFactory", attributes.get("fallbackFactory")); definition.setAutowireMode(AbstractBeanDefinition.AUTOWIRE_BY_TYPE); String alias = name + "FeignClient"; // 用Builder获取实际的BeanDefinition AbstractBeanDefinition beanDefinition = definition.getBeanDefinition(); boolean primary = (Boolean)attributes.get("primary"); // has a default, won't be null beanDefinition.setPrimary(primary); String qualifier = getQualifier(attributes); if (StringUtils.hasText(qualifier)) { alias = qualifier; } // 创建一个Bean定义的持有者 BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className, new String[] { alias }); // 这里就是将Bean注册到Spring容器中 BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry); }
由上面一段代码,FeignClient
客户端注册就此完成。但是上面还有两个重点还没看完
下面进入.genericBeanDefinition(FeignClientFactoryBean.class);看把 FeignClientFactoryBean.class类传进去干嘛,发现注册的Bean就是参数中自己传进来的beanClass,这个传进去的beanClass是工厂Bean
Spring Cloud FengnClient实际上是利用Spring的代理工厂来生成代理类,所以在这里地方才会把所有的FeignClient的BeanDefifinition设置为FeignClientFactoryBean类型,而FeignClientFactoryBean继承自FactoryBean,它是一个工厂Bean。在Spring中,FactoryBean是一个工厂Bean,用来创建代理Bean。工厂 Bean 是一种特殊的 Bean, 对于 Bean 的消费者来说, 他逻辑上是感知不到这个 Bean 是普通的 Bean 还是工厂 Bean, 只是按照正常的获取 Bean 方式去调用, 但工厂bean 最后返回的实例不是工厂Bean 本身, 而是执行工厂 Bean 的 getObject 逻辑返回的示例。
点击这个工厂Bean的FeignClientFactoryBean类中发现里面有个getObject()方法,这个工厂Bean就是通过这个getTarget();返回一个真正的实例
画下时序图
前面说到了在启动时会通过@EnableFeignClients去扫描所有指定路径下的@FeignClient注解声明的一个接口,然后在扫描到以后要去生成一个动态代理的类,这个动态代理的生成就是在调用getObject()时完成 ,而且getObject()又会调用他方法里面的getTarget()去完成这件事,
<T> T getTarget() {
//FeignContext注册到容器是在FeignAutoConfiguration上完成的
//在初始化FeignContext时,会把configurations在容器中放入FeignContext中。configurations的
//来源就是在前面registerFeignClients方法中将@FeignClient的配置configuration。
FeignContext context = this.applicationContext.getBean(FeignContext.class);
Feign.Builder builder = feign(context);//构建Builder对象
//如果url为空,则走负载均衡,生成有负载均衡功能的代理类
if (!StringUtils.hasText(this.url)) {
if (!this.name.startsWith("http")) {
this.url = "http://" + this.name;
}
else {
this.url = this.name;
}
this.url += cleanPath();
return (T) loadBalance(builder, context,
new HardCodedTarget<>(this.type, this.name, this.url));
}
//如果指定了url,则生成默认的代理类
if (StringUtils.hasText(this.url) && !this.url.startsWith("http")) {
this.url = "http://" + this.url;
}
String url = this.url + cleanPath();
Client client = getOptional(context, Client.class);
if (client != null) {
if (client instanceof LoadBalancerFeignClient) {
// not load balancing because we have a url,
// but ribbon is on the classpath, so unwrap
client = ((LoadBalancerFeignClient) client).getDelegate();
}
if (client instanceof FeignBlockingLoadBalancerClient) {
// not load balancing because we have a url,
// but Spring Cloud LoadBalancer is on the classpath, so unwrap
client = ((FeignBlockingLoadBalancerClient) client).getDelegate();
}
builder.client(client);
}//生成默认代理类
Targeter targeter = get(context, Targeter.class);
return (T) targeter.target(this, builder, context,
new HardCodedTarget<>(this.type, this.name, url));
}

上面有段代码Feign.Builder builder = feign(context);是构建Builder对象
protected Feign.Builder feign(FeignContext context) { FeignLoggerFactory loggerFactory = get(context, FeignLoggerFactory.class); Logger logger = loggerFactory.create(this.type); // @formatter:off Feign.Builder builder = get(context, Feign.Builder.class) // required values
//开启日志级别 .logger(logger)
//编码 .encoder(get(context, Encoder.class))
//解码 .decoder(get(context, Decoder.class))
//连接。这个连接用在解析模板的,后面会提 .contract(get(context, Contract.class)); // @formatter:on configureFeign(context, builder); return builder; }
上面的builder构造完后继续向下走,配置完Feign.Builder之后,再判断是否需要LoadBalance,如果需要,则通过loadBalance(builder, context,new HardCodedTarget<>(this.type, this.name, this.url));的方法来设置。实际上他们最终调用的是Target.target()方法。
@Import({ HttpClientFeignLoadBalancedConfiguration.class,
OkHttpFeignLoadBalancedConfiguration.class,
DefaultFeignLoadBalancedConfiguration.class })
protected <T> T loadBalance(Feign.Builder builder, FeignContext context, HardCodedTarget<T> target) {
//针对某一个服务的client Client client = getOptional(context, Client.class); if (client != null) {
//将client设置进去相当于增加了客户端负载均衡解析的机制 builder.client(client); Targeter targeter = get(context, Targeter.class); return targeter.target(this, builder, context, target); } throw new IllegalStateException( "No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-netflix-ribbon?"); }
点击上图的targeter.target(this, builder, context, target);因为熔断准备在后面讲,所以在选tartget的实现时选择DefaultTarget.target

点击feign.target()往下走,走到这里其实就已经到了核心逻辑了,前面不一直说动态代理吗,前面走的都是人生最长的套路,前面自己写的控制层代码通过@Resource注解注入的UserOpenFeign他最终会调用下面的方法返回一个实例,那么下面看下这newInstance()方法做了啥,发现这玩意有两个实现,至于选择哪个就要看build()返回的是什么了,向下看发现build()返回的是ReflectiveFeign,所以选第二个
feign
生成代理是用的反射(生成
ReflectiveFeign
对象)。我们通用的代理有
JDK动态代理
和
CGLIB
两种代理。我们继续看看
Feign
用的是哪种,这个方法是用来创建一个动态代理的方法,在生成动态代理之前,会根据Contract协议(协议解析规则,解析接口类的注解信息,解析成内部的MethodHandler的处理方式。从实现的代码中可以看到熟悉的Proxy.newProxyInstance方法产生代理类。而这里需要对每个定义的接口方法进行特定的处理实现,所以这里会出现一个MethodHandler的概念,就是对应方法级别的InvocationHandler。
public <T> T newInstance(Target<T> target) { //根据接口类和Contract协议解析方式,解析接口类上的方法和注解,转换成内部的MethodHandler处理方式 Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target); Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>(); List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>(); for (Method method : target.type().getMethods()) { if (method.getDeclaringClass() == Object.class) { continue; } else if (Util.isDefault(method)) { DefaultMethodHandler handler = new DefaultMethodHandler(method); defaultMethodHandlers.add(handler); methodToHandler.put(method, handler); } else { methodToHandler.put(method, nameToHandler.get(Feign.configKey(target.type(), method))); } } // 基于Proxy.newProxyInstance 为接口类创建动态实现,将所有的请求转换给InvocationHandler 处理。 InvocationHandler handler = factory.create(target, methodToHandler); T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(), new Class<?>[] {target.type()}, handler); for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) { defaultMethodHandler.bindTo(proxy); } return proxy; }
下面通过debugger验证下,会看到userOpenFeign的返回的是代理类,通过下图可以知道当调用userOpenFeign时他其实是调用ReflectiveFeign中的handler,而通过Debugger发现这个handler是FeginInvocationHandler,
竟然是走了代理那么他一定是走了ReflectiveFeign的代理方法invoke()方法
下面进入OpenFeign的调用过程
@Override public Object invoke(Object[] argv) throws Throwable { RequestTemplate template = buildTemplateFromArgs.create(argv); Options options = findOptions(argv); Retryer retryer = this.retryer.clone(); while (true) { try { return executeAndDecode(template, options); } catch (RetryableException e) { try { retryer.continueOrPropagate(e); } catch (RetryableException th) { Throwable cause = th.getCause(); if (propagationPolicy == UNWRAP && cause != null) { throw cause; } else { throw th; } } if (logLevel != Logger.Level.NONE) { logger.logRetry(metadata.configKey(), logLevel); } continue; } } }
通过Debugger,访问请求路径,可以发现下面这些基本信息都拿到了
Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
//转换为HTTP请求报文 Request request = targetRequest(template); if (logLevel != Logger.Level.NONE) { logger.logRequest(metadata.configKey(), logLevel, request); } Response response; long start = System.nanoTime(); try {
//发起远程通信 response = client.execute(request, options); // ensure the request is set. TODO: remove in Feign 12
//获取返回结果 response = response.toBuilder() .request(request) .requestTemplate(template) .build(); } catch (IOException e) { if (logLevel != Logger.Level.NONE) { logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime(start)); } throw errorExecuting(request, e); } long elapsedTime = TimeUnit.NANOSECONDS.toMillis(System.nanoTime() - start); if (decoder != null) return decoder.decode(response, metadata.returnType()); CompletableFuture<Object> resultFuture = new CompletableFuture<>(); asyncResponseHandler.handleResponse(resultFuture, metadata.configKey(), response, metadata.returnType(), elapsedTime); try { if (!resultFuture.isDone()) throw new IllegalStateException("Response handling not done"); return resultFuture.join(); } catch (CompletionException e) { Throwable cause = e.getCause(); if (cause != null) throw cause; throw e; } }
其中上图Client.execute 就已经拿到了返回结果,默认采用JDK的 HttpURLConnection 发起远程调用。但我们这里用了负载均衡用到的是LoadBalancerFeignClient,到这里的URL还是没有解析
@Override
public Response execute(Request request, Request.Options options) throws IOException {
try {
URI asUri = URI.create(request.url());
String clientName = asUri.getHost();
URI uriWithoutHost = cleanUrl(request.url(), clientName);
FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
this.delegate, request, uriWithoutHost);
IClientConfig requestConfig = getClientConfig(options, clientName);
return lbClient(clientName)
.executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse();
}
catch (ClientException e) {
IOException io = findIOException(e);
if (io != null) {
throw io;
}
throw new RuntimeException(e);
}
}
回退到下图所示位置,其实前面所有动作我们只是看到了InvocationHandler handler = factory.create(target, methodToHandler);这玩意做了啥事情,但是很核心的东西怎么解析的并没有看到。所以回退到下图位置会发现有个东西叫MethodHandler,他会把target传过去解析,解析完后得到MethodHandler,这东西其实主是针对方法接口的解析
,
那么看下他是怎么做的,点击targetToHandlersByName.apply(target);进入 apply方法,targetToHandlersByName.apply(target);会解析接口方法上的注解,从而解析出方法粒度的特定的配置信息,然后生产一个SynchronousMethodHandler 然后需要维护一个<method,MethodHandler>的map,放入InvocationHandler的实现FeignInvocationHandler中。
public Map<String, MethodHandler> apply(Target target) {
//当前的contract默认是SpringMvcContract,因为他默认继承Springmvc的模板解析 List<MethodMetadata> metadata = contract.parseAndValidateMetadata(target.type()); Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>(); for (MethodMetadata md : metadata) { BuildTemplateByResolvingArgs buildTemplate; if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) { buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target); } else if (md.bodyIndex() != null) { buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder, target); } else { buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder, target); } if (md.isIgnored()) { result.put(md.configKey(), args -> { throw new IllegalStateException(md.configKey() + " is not a method handled by feign"); }); } else { result.put(md.configKey(), factory.create(target, md, buildTemplate, options, decoder, errorDecoder)); } } return result; }
通过下面debugger可以发现这个contract可以做到元数据的解析,去拿到目标进行解析,当前Spring Cloud 微服务解决方案中,为了降低学习成本,采用了Spring MVC的部分注解来完成 请求协议解析,也就是说 ,写客户端请求接口和像写服务端代码一样:客户端和服务端可以通过SDK的方式进行约定,客户端只需要引入服务端发布的SDK API,就可以使用面向接口的编码方式对接服务。
再次回到上次退回的地方,经过上面的方法后就得到了一个MethodHandler,就会进入下面进行一个循环,因为我们会拿到多个method,拿到多个后会进行一些处理,然后再传到factory.create(target, methodToHandler);
然后接合前面知识又能神奇发现这玩意是default
补充内容:
前面说过Feign logging,官网中有说明如何调整等级:https://docs.spring.io/spring-cloud-openfeign/docs/2.2.5.RELEASE/reference/html/#feign-logging
下面来验证下,首先自定一个FooConfiguration类
然后按官网要求在配置文件中配置
#设置日志级别,可以在idea控制台上看日志内容,设置的路径是@FeignClient注解的路径 logging.level.com.ghy.demo.UserOpenFeign=DEBUG
启动两个项目,发起请求后查看控制台,可以看到控制能胡详细的请求信息
源码git网址:https://github.com/ljx958720/Spring-Cloud-OpenFeign-.git