Loading...
墨滴

lyq

2021/11/05  阅读:32  主题:默认主题

Spring Cloud OpenFeign源码解析

本篇基于Spring Cloud Hoxton.SR9


前言

如果您在看阅读的时候感觉吃力,请自行补充基础知识,该系列只是带领大家走读源码,通过读源码,弄懂背后实现的原理以及一些扩展点,以便更好的辅助日常工作的开发。

如果您在看阅读的时候感觉吃力,请自行补充基础知识,该系列只是带领大家走读源码,通过读源码,弄懂背后实现的原理以及一些扩展点,以便更好的辅助日常工作的开发。

如果您在看阅读的时候感觉吃力,请自行补充基础知识,该系列只是带领大家走读源码,通过读源码,弄懂背后实现的原理以及一些扩展点,以便更好的辅助日常工作的开发。 重要的事情说三遍!!!

由于之前的排版有问题,代码不便于阅读,所以经过了重新排版。本篇默认读者已经知道OpenFeign是做什么的,如果不知到,请自行百度。另外如果阅读完本文觉得有些吃力,请提前学习SpringBoot自动装配基础知识。废话不多说,直接上干货。

基本用法

  1. 引入依赖
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>
  1. 开启feign
@EnableFeignClients
  1. 定义接口
@FeignClient("nacos-discovery-provider-sample"// 指向服务提供者应用
public interface EchoService {

    @GetMapping("/echo/{message}")
    String echo(@PathVariable("message") String message);
}

源码分析

配置解析阶段

@EnableFeignClients

@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.TYPE)
@Documented
@Import(FeignClientsRegistrar.class)
public @interface EnableFeignClients 
{
}

通过import注解导入了FeignClientsRegistrar配置类,那么进入这个类中,一看详情。

FeignClientsRegistrar

这个类实现了ImportBeanDefinitionRegistrar接口,那么就需要重写registerBeanDefinitions方法。如下:

@Override
public void registerBeanDefinitions(AnnotationMetadata metadata,
  BeanDefinitionRegistry registry)
 
{
 registerDefaultConfiguration(metadata, registry);
 registerFeignClients(metadata, registry);
}

这个方法做了2两件事情:

  1. 注册全局配置,如果EnableFeignClients注解上配置了defaultConfiguration属性
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"));
  }
 }
  1. 注册FeignClients
public void registerFeignClients(AnnotationMetadata metadata,
   BeanDefinitionRegistry registry)
 
{
  ClassPathScanningCandidateComponentProvider scanner = getScanner();
  scanner.setResourceLoader(this.resourceLoader);

  Set<String> basePackages;

  Map<String, Object> attrs = metadata
    .getAnnotationAttributes(EnableFeignClients.class.getName());
  AnnotationTypeFilter annotationTypeFilter = new AnnotationTypeFilter(
    FeignClient.class);
  //省略部分代码
  ......
  for (String basePackage : basePackages) {
   Set<BeanDefinition> candidateComponents = scanner
     .findCandidateComponents(basePackage);
   for (BeanDefinition candidateComponent : candidateComponents) {
    if (candidateComponent instanceof AnnotatedBeanDefinition) {
     // verify annotated class is an interface
     AnnotatedBeanDefinition beanDefinition = (AnnotatedBeanDefinition) candidateComponent;
     AnnotationMetadata annotationMetadata = beanDefinition.getMetadata();
     Assert.isTrue(annotationMetadata.isInterface(),
       "@FeignClient can only be specified on an interface");

     Map<String, Object> attributes = annotationMetadata
       .getAnnotationAttributes(
         FeignClient.class.getCanonicalName());

     String name = getClientName(attributes);
     registerClientConfiguration(registry, name,
       attributes.get("configuration"));

     registerFeignClient(registry, annotationMetadata, attributes);
    }
   }
  }
 }

这个方法会扫描类路径上标记@FeignClient注解的接口,根据@EnableFeignClients注解上的配置,并循环注册FeignClient。 进入registerFeignClient方法

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

可以看到,这个方法就是将FeignClient注解上的属性信息,封装到BeanDefinition中,并注册到Spring容器中。但是在这个方法中,有一个关键信息,就是真实注册的是FeignClientFactoryBean,它实现了FactoryBean接口,表明这是一个工厂bean,用于创建代理Bean,真正执行的逻辑是FactoryBeangetObject方法。至此,FeignClient的配置解析阶段就完成了。下面进入FeignClientFactoryBean,看看在这个类中都做了什么。

运行阶段

FeignClientFactoryBean

  1. 核心方法getObject
@Override
public Object getObject() throws Exception {
 return getTarget();
}

当我们在业务代码中通过@Autowire依赖注入或者通过getBean依赖查找时,此方法会被调用。内部会调用getTarget方法,那么进入这个方法一探究竟。 2. getTarget

<T> T getTarget() {
  FeignContext context = applicationContext.getBean(FeignContext.class);
  Feign.Builder builder = feign(context);

  if (!StringUtils.hasText(url)) {
   if (!name.startsWith("http")) {
    url = "http://" + name;
   }
   else {
    url = name;
   }
   url += cleanPath();
   return (T) loadBalance(builder, context,
     new HardCodedTarget<>(type, name, url));
  }
  if (StringUtils.hasText(url) && !url.startsWith("http")) {
   url = "http://" + 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<>(type, name, url));
 }

首先从Spring上下文中,获取FeignContext这个Bean,这个bean是在哪里注册的呢?是在FeignAutoConfiguration中注册的。 然后判断url属性是否为空,如果不为空,则生成默认的代理类;如果为空,则走负载均衡,生成带有负载均衡的代理类。那么重点关注loadBalance方法。

  1. loadBalance
protected <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?");
 }

首先调用getOptional方法,这个方法就是根据contextId,获取一个子上下文,然后从这个子上下文中查找Client bean,SpringCloud会为每一个feignClient创建一个子上下文,然后存入以contextId为key的map中,详见NamedContextFactorygetContext方法。此处会返回LoadBalancerFeignClient这个Client。详见:FeignRibbonClientAutoConfiguration会导入相关配置类。 然后会从子上下文中,查找Targeter bean,默认返回的是DefaultTargeter, 最后调用target方法。

  1. DefaultTargeter
class DefaultTargeter implements Targeter {

 @Override
 public <T> target(FeignClientFactoryBean factory, Feign.Builder feign,
   FeignContext context, Target.HardCodedTarget<T> target)
 
{
  return feign.target(target);
 }

}

最终底层调用Feign.Buildertarget方法。进入Feignclass中,看看到底做了什么事情

  1. Feign
public <T> target(Target<T> target) {
    return this.build().newInstance(target);
}

public Feign build() {
    //省略部分代码
    ......
    return new ReflectiveFeign(handlersByName, this.invocationHandlerFactory, this.queryMapEncoder);
}

可以看到最终是通过创建ReflectiveFeign对象,然后调用newInstance方法返回了一个代理对象,通过名字可以发现,底层使用的是java反射创建的。 那么看看ReflectiveFeignnewInstance方法到底做了什么。

  1. ReflectiveFeign
public <T> newInstance(Target<T> target) {
//根据接口类和Contract协议解析方式,解析接口类上的方法和注解,转换成内部的MethodHandler处理方式
        Map<String, MethodHandler> nameToHandler = this.targetToHandlersByName.apply(target);
        Map<Method, MethodHandler> methodToHandler = new LinkedHashMap();
        List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList();
        Method[] var5 = target.type().getMethods();
        int var6 = var5.length;

        for(int var7 = 0; var7 < var6; ++var7) {
            Method method = var5[var7];
            if (method.getDeclaringClass() != Object.class{
                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 = this.factory.create(target, methodToHandler);
        T proxy = Proxy.newProxyInstance(target.type().getClassLoader(), new Class[]{target.type()}, handler);
        Iterator var12 = defaultMethodHandlers.iterator();

        while(var12.hasNext()) {
            DefaultMethodHandler defaultMethodHandler = (DefaultMethodHandler)var12.next();
            defaultMethodHandler.bindTo(proxy);
        }

        return proxy;
    }

见注释。此处nvocationHandler handler = this.factory.create(target, methodToHandler);真实返回的是FeignInvocationHandler,当在自己的业务类中调用feign接口方法时,会调用FeignInvocationHandlerinvoke方法。

  1. ReflectiveFeign.FeignInvocationHandler
@Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
      if ("equals".equals(method.getName())) {
        try {
          Object otherHandler =
              args.length > 0 && args[0] != null ? Proxy.getInvocationHandler(args[0]) : null;
          return equals(otherHandler);
        } catch (IllegalArgumentException e) {
          return false;
        }
      } else if ("hashCode".equals(method.getName())) {
        return hashCode();
      } else if ("toString".equals(method.getName())) {
        return toString();
      }

      return dispatch.get(method).invoke(args);
    }

invoke方法中,会调用 this.dispatch.get(method)).invoke(args)this.dispatch.get(method)会返回一个SynchronousMethodHandler,进行拦截处理。这个方法会根据参数生成完成的RequestTemplate对象,这个对象是Http请求的模版,代码如下。 看SynchronousMethodHandler中的invoke

  1. SynchronousMethodHandler
@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;
      }
    }
  }

上面的代码中有一个 executeAndDecode()方法,该方法通过RequestTemplate生成Request请求对象,然后利用Http Client(默认)获取response,来获取响应信息

Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
    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);
    }
    //省略部分代码
    ......
  }

client.execute(request, options);默认使用HttpURLConnection发起远程调用,这里的client为LoadBalancerFeignClient。那么看看他的execute方法。

  1. LoadBalancerFeignClient
@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);
  }
 }

最终通过Ribbon负载均衡器发起远程调用,具体分析见另一篇关于Ribbon的源码分析。

总结

通过源码我们了解了Spring Cloud OpenFeign的加载配置创建流程。通过注解@FeignClient@EnableFeignClients注解实现了client的配置声明注册,再通过FeignRibbonClientAutoConfigurationFeignAutoConfiguration类进行自动装配 本文仅对feign源码的主线进行分析,还有很多细节并未介绍,如果读者感兴趣,可以参考本文,自行阅读源码。

补充

Feign的组成

接口 作用 默认值
Feign.Builder Feign的入口 Feign.Builder
Client Feign底层用什么去请求 和Ribbon配合时:LoadBalancerFeignClient不和Ribbon配合时:Fgien.Client.Default
Contract 契约,注解支持 SpringMVCContract
Encoder 编码器 SpringEncoder
Decoder 解码器 ResponseEntityDecoder
Logger 日志管理器 Slf4jLogger
RequestInterceptor 用于为每个请求添加通用逻辑(拦截器,例子:比如想给每个请求都带上heared)

Feign的日志级别

日志级别 打印内容
NONE(默认) 不记录任何日志
BASIC 仅记录请求方法,URL,响应状态代码以及执行时间(适合生产环境)
HEADERS 记录BASIC级别的基础上,记录请求和响应的header
FULL 记录请求和响应header,body和元数据

如何给Feign添加日志级别

局部配置

方式一:代码实现
  1. 编写配置类
public class FeignConfig {
  @Bean
  public Logger.Level Logger() {
      return Logger.Level.FULL;
   }
}

添加Feign配置类,可以添加在主类下,但是不用添加@Configuration。如果添加了@Configuration而且又放在了主类之下,那么就会所有Feign客户端实例共享,同Ribbon配置类一样父子上下文加载冲突;如果一定添加@Configuration,就放在主类加载之外的包。(建议还是不用加@Configuration)

  1. 配置@FeignClient
@FeignClient(name = "alibaba-nacos-discovery-server",configuration = FeignConfig.class)
public interface NacosDiscoveryClientFeign 
{

    @GetMapping("/hello")
    String hello(@RequestParam(name = "name") String name);
}
方式二:配置文件实现
feign:
  client:
    config:
      #要调用的微服务名称
      clientName:
        loggerLevel: FULL

全局配置

方式一:代码实现
@EnableFeignClients(defaultConfiguration = FeignConfig.class)
方式二:配置文件实现
feign:
  client:
    config:
      #将调用的微服务名称改成default就配置成全局的了
      default:
        loggerLevel: FULL

Feign支持的配置项

代码方式支持配置项

配置项 作用
Logger.Level 指定日志级别
Retryer 指定重试策略
ErrorDecoder 指定错误解码器
Request.Options 超时时间
Collection<\RequestInterceptor> 拦截器
SetterFactory 用于设置Hystrix的配置属性,Fgien整合Hystrix才会用

详见FeignClientsConfiguration中配置

配置文件属性支持配置项
feign:
  client:
    config:
      feignName:
        connectTimeout: 5000  # 相当于Request.Optionsn 连接超时时间
        readTimeout: 5000     # 相当于Request.Options 读取超时时间
        loggerLevel: full     # 配置Feign的日志级别,相当于代码配置方式中的Logger
        errorDecoder: com.example.SimpleErrorDecoder  # Feign的错误解码器,相当于代码配置方式中的ErrorDecoder
        retryer: com.example.SimpleRetryer  # 配置重试,相当于代码配置方式中的Retryer
        requestInterceptors: # 配置拦截器,相当于代码配置方式中的RequestInterceptor
          - com.example.FooRequestInterceptor
          - com.example.BarRequestInterceptor
        # 是否对404错误解码
        decode404: false
        encode: com.example.SimpleEncoder
        decoder: com.example.SimpleDecoder
        contract: com.example.SimpleContract

Feign还支持对请求和响应进行GZIP压缩,以提高通信效率, 仅支持Apache HttpClient,详见FeignContentGzipEncodingAutoConfiguration 配置方式如下:

# 配置请求GZIP压缩
feign.compression.request.enabled=true
# 配置响应GZIP压缩
feign.compression.response.enabled=true
# 配置压缩支持的MIME TYPE
feign.compression.request.mime-types=text/xml,application/xml,application/json
# 配置压缩数据大小的下限
feign.compression.request.min-request-size=2048

Feign默认使用HttpUrlConnection进行远程调用,可以通过配置开启HttpClient或OkHttp3,具体详见FeignRibbonClientAutoConfiguration,配置如下:

feign.httpclient.enabled=true
//或
feign.okhttp.enabled=true

并添加相应的依赖即可

<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-okhttp</artifactId>
</dependency>
//或
<dependency>
    <groupId>io.github.openfeign</groupId>
    <artifactId>feign-httpclient</artifactId>
</dependency>

关于Spring Cloud OpenFeign的配置有很多,本文只是列出了部分配置,更多配置请自行阅读源码。

更多原创文章,请扫码关注我的微信公众号
更多原创文章,请扫码关注我的微信公众号

lyq

2021/11/05  阅读:32  主题:默认主题

作者介绍

lyq