SpringCloud OpenFeign(二)

上接:

SpringCloud OpenFeign_钱多多_qdd的博客-CSDN博客

前言

源码图:

一、Bean的动态装载

在spring系列里bean的动态装载是很常见的,感兴趣可以看看我的这篇文章:

Bean的动态装载_钱多多_qdd的博客-CSDN博客_动态加载bean

二、@FeignClient解析

从下面这个类开始切入,这个注解开启了FeignClient的解析过程。

@EnableFeignClients(basePackages = "com.gupaoedu.example.clients")

2.1 FeignClientsRegistrar

  • registerDefaultConfiguration 方法内部从 SpringBoot 启动类上检查是否有@EnableFeignClients, 有该注解的话, 则完成 Feign 框架相关的一些配置内容注册
  • registerFeignClients 方法内部从 classpath 中, 扫描获得 @FeignClient 修饰的类, 将类的内容解析为 BeanDefinition , 最终通过调用 Spring 框架中的
    BeanDefinitionReaderUtils.resgisterBeanDefinition 将解析处理过的 FeignClient
    BeanDeifinition 添加到 spring 容器中
@Override
	public void registerBeanDefinitions(AnnotationMetadata metadata,
			BeanDefinitionRegistry registry) {
        //注册@EnableFeignClients中定义defaultConfiguration属性下的类,包装成FeignClientSpecification,注册到Spring容器。
		registerDefaultConfiguration(metadata, registry);
        //在@FeignClient中有一个属性:configuration,这个属性是表示各个FeignClient自定义的配置类,后面也会通过调用registerClientConfiguration方法来注册成FeignClientSpecification到容器。
        //所以,这里可以完全理解在@EnableFeignClients中配置的是做为兜底的配置,在各个@FeignClient配置的就是自定义的情况。
		registerFeignClients(metadata, registry);
	}

 这里面需要重点分析的就是 registerFeignClients 方法,这个方法主要是扫描类路径下所有的
@FeignClient注解,然后进行动态Bean的注入。它最终会调用 registerFeignClient 方法。

由上图我们可以看到此方法大概有5个步骤:

  1. 获取@EnableFeignClient的相关属性;
  2. 获取服务端类所在的路径basePackages(@EnableFeignClient("xx.xx.xx")配置的)。后续会遍历这些路径,并找到@FeignClient注解的类,将相关的类注册;
  3. 扫描basePackage下的所有类;
  4. 获取@FeignClient注解的相关属性;
  5. 注册;

下面我们来着重看第5步注册:

private void registerFeignClient(BeanDefinitionRegistry registry,
			AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
		String className = annotationMetadata.getClassName();
		BeanDefinitionBuilder definition = BeanDefinitionBuilder
				.genericBeanDefinition(FeignClientFactoryBean.class);
		validate(attributes);
		definition.addPropertyValue("url", getUrl(attributes));
		definition.addPropertyValue("path", getPath(attributes));
		String name = getName(attributes);
		definition.addPropertyValue("name", name);
		String contextId = getContextId(attributes);
		definition.addPropertyValue("contextId", contextId);
		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 = contextId + "FeignClient";
		AbstractBeanDefinition beanDefinition = definition.getBeanDefinition();
		beanDefinition.setAttribute(FactoryBean.OBJECT_TYPE_ATTRIBUTE, className);

		// has a default, won't be null
		boolean primary = (Boolean) attributes.get("primary");

		beanDefinition.setPrimary(primary);

		String qualifier = getQualifier(attributes);
		if (StringUtils.hasText(qualifier)) {
			alias = qualifier;
		}

		BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
				new String[] { alias });
		BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
	}

在这个方法中,就是去组装BeanDefinition,也就是Bean的定义,然后注册到Spring IOC容器。

我们关注一下,BeanDefinitionBuilder是用来构建一个BeanDefinition的,它是通过
genericBeanDefinition 来构建的,并且传入了一个FeignClientFactoryBean的类,代码如下。
我们可以发现,FeignClient被动态注册成了一个FactoryBean,之前听过tom老师的源码的同学应该知道什么是FactoryBean吧。

Spring Cloud FengnClient实际上是利用Spring的代理工厂来生成代理类,所以在这里地方才会、把所有的FeignClient的BeanDefinition设置为FeignClientFactoryBean类型,而
FeignClientFactoryBean继承自FactoryBean,它是一个工厂Bean。
在Spring中,FactoryBean是一个工厂Bean,用来创建代理Bean。
工厂 Bean 是一种特殊的 Bean, 对于 Bean 的消费者来说, 他逻辑上是感知不到这个 Bean 是普通的 Bean 还是工厂 Bean, 只是按照正常的获取 Bean 方式去调用, 但工厂bean 最后返回的实例不是工厂Bean 本身, 而是执行工厂 Bean 的 getObject 逻辑返回的示例。

public static BeanDefinitionBuilder genericBeanDefinition(Class<?> beanClass) {
    BeanDefinitionBuilder builder = new BeanDefinitionBuilder(new GenericBeanDefinition());
    builder.beanDefinition.setBeanClass(beanClass);
    return builder;
}

         简单来说,FeignClient标注的这个接口,会通过FeignClientFactoryBean.getObject()这个方法获得一个代理对象。

2.2 FeignClientFactoryBean.getObject 

        getObject调用的是getTarget方法,它从applicationContext取出FeignContext,FeignContext继承了NamedContextFactory,它是用来来统一维护feign中各个feign客户端相互隔离的上下文。

FeignContext注册到容器是在FeignAutoConfiguration上完成的:

在初始化FeignContext时,会把configurations在容器中放入FeignContext中。configurations的来源就是在前面registerFeignClients方法中将@FeignClient的配置configuration。

  1. 接着,构建feign.builder,在构建时会向FeignContext获取配置的Encoder,Decoder等各种信息。
  2. FeignContext在上篇中已经提到会为每个Feign客户端分配了一个容器,它们的父容器就是spring容器配置完Feign.Builder之后,再判断是否需要LoadBalance,如果需要,则通过LoadBalance的方法来设置。
  3. 实际上他们最终调用的是Target.target()方法。 

2.3 loadBalance

生成具备负载均衡能力的feign客户端,为feign客户端构建起绑定负载均衡客户端Client client = (Client)this.getOptional(context, Client.class);从上下文中获取一个Client,默认是LoadBalancerFeignClient。

——绿色②代码。

它是在FeignRibbonClientAutoConfiguration这个自动装配类中,通过Import实现的:

 绿色①代码:

protected <T> T loadBalance(Feign.Builder builder, FeignContext context,
			HardCodedTarget<T> target) {
		Client client = getOptional(context, Client.class);
		if (client != null) {
			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?");
	}

2.4 DefaultTarget.target

 

2.5 ReflectiveFeign.newInstance

        这个方法是用来创建一个动态代理的方法,在生成动态代理之前,会根据Contract协议(协议解析规则,解析接口类的注解信息,解析成内部的MethodHandler的处理方式。

        从实现的代码中可以看到熟悉的Proxy.newProxyInstance方法产生代理类。而这里需要对每个定义的接口方法进行特定的处理实现,所以这里会出现一个MethodHandler的概念,就是对应方法级别的InvocationHandler。

三、接口定义的参数解析

根据FeignClient接口的描述解析出对应的请求数据。

3.1 targetToHandlersByName.apply(target)

还是在ReflectiveFeign.newInstance方法里:

 根据Contract协议规则,解析接口类的注解信息,解析成内部表现:

        targetToHandlersByName.apply(target);会解析接口方法上的注解,从而解析出方法粒度的特定的配置信息,然后生产一个SynchronousMethodHandler 然后需要维护一个<method,MethodHandler>的map,放入InvocationHandler的实现FeignInvocationHandler中。

3.2 SpringMvcContract

        当前Spring Cloud 微服务解决方案中,为了降低学习成本,采用了Spring MVC的部分注解来完成 请求协议解析,也就是说 ,写客户端请求接口和像写服务端代码一样:客户端和服务端可以通过SDK的方式进行约定,客户端只需要引入服务端发布的SDK API,就可以使用面向接口的编码方式对接服务。

扩展:Feign详解4-Contract 源码_zhangyingchengqi的博客-CSDN博客

四、OpenFeign调用过程

        在前面的分析中,我们知道OpenFeign最终返回的是一个#ReflectiveFeign.FeignInvocationHandler的对象。

        那么当客户端发起请求时,会进入到FeignInvocationHandler.invoke方法中,这个大家都知道,它是一个动态代理的实现。

而接着,在invoke方法中,会调用 this.dispatch.get(method)).invoke(args) 。
this.dispatch.get(method) 会返回一个SynchronousMethodHandler,进行拦截处理。

这个方法会根据参数生成完成的RequestTemplate对象,这个对象是Http请求的模版,代码如下:

4.1 动态生成Request

4.2 executeAndDecode

        经过上述的代码,我们已经将restTemplate拼装完成,上面的代码中有一个executeAndDecode() 方法,该方法通过RequestTemplate生成Request请求对象,然后利用Http Client获取response,来获取响应信息。
 

 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;
    }
  }

 4.3 Client.execute

默认采用JDK的 HttpURLConnection 发起远程调用。

  

五、拓展 

Feign源码解析 - 简书

六、 题目

6.1 请简述OpenFeign的底层原理

        openfeign通过包扫描将所有被@FeignClient注解注释的接口扫描出来,并为每个接口注册一个FeignClientFactoryBean实例。FeignClientFactoryBean是一个FactoryBean,当Spring调用FeignClientFactoryBean的getObject方法时,openfeign返回一个Feign生成的动态代理对象,拦截接口的方法执行。

        feign会为代理的接口的每个方法Method都生成一个MethodHandler。

当为接口上的@FeignClient注解的url属性配置服务提供者的url时,其实就是不与Ribbon整合,此时由SynchronousMethodHandler实现接口方法远程同步调用,使用默认的Client实现类Default实例发起http请求。

        当接口上的@FeignClient注解的url属性不配置时,且会走负载均衡逻辑,也就是需要与Ribbon整合使用。这时候不再是使用默认的Client(Default)调用接口,而是使用LoadBalancerFeignClient调用接口,由LoadBalancerFeignClient实现与Ribbon的整合。

 Spring Cloud OpenFeign原理可以理解为:

        通过 @EnableFeignCleints 触发 Spring 应用程序对 classpath 中 @FeignClient 修饰类的扫描解析到 @FeignClient 修饰类后, Feign 框架通过扩展 Spring Bean Deifinition 的注册逻辑, 最终注册一个 FeignClientFacotoryBean 进入 Spring 容器
Spring 容器在初始化其他用到 @FeignClient 接口的类时, 获得的是 FeignClientFacotryBean 产生的一个代理对象 Proxy。
        基于 java 原生的动态代理机制, 针对 Proxy 的调用, 都会被统一转发给 Feign 框架所定义的一个 InvocationHandler , 由该 Handler 完成后续的 HTTP 转换, 发送, 接收, 翻译HTTP响应的工作

请简述OpenFeign的底层原理?
        Feign 能够让我们像调用一个本地 java 方法一样去调用基于 HTTP 协议的 Rest API 接口。 省去了我们编写 HTTP 连接,数据解析获取等一系列繁琐的操作。
Feign 的整体的工作流程:

  1. SpringBoot 应用启动时, 由针对 @EnableFeignClient 这一注解的处理逻辑触发程序扫描 classPath中所有被@FeignClient 注解的类, 这里以 DemoService 为例, 将这些类解析为 BeanDefinition 注册到 Spring 容器中
  2. Sping 容器在为某些用的 Feign 接口的 Bean 注入 DemoService 时, Spring 会尝试从容器中查找 DemoService 的实现类
  3. 由于我们从来没有编写过 DemoService 的实现类, 上面步骤获取到的 DemoService 的实现类必然是 feign 框架通过扩展 spring 的 Bean 处理逻辑, 为 DemoService 创建一个动态接口代理对象, 这里我们将其称为 DemoServiceProxy 注册到spring 容器中。
  4. Spring 最终在使用到 DemoService 的 Bean 中注入了 DemoServiceProxy 这一实例。
  5. 当业务请求真实发生时, 对于 DemoService 的调用被统一转发到了由 Feign 框架实现的 InvocationHandler 中, InvocationHandler 负责将接口中的入参转换为 HTTP 的形式, 发到服务端, 最后再解析 HTTP 响应, 将结果转换为 Java 对象, 予以返回。

Feign 的核心实现原理就是基于接口的动态代理。 

 

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值