springclound 之feign原理源码解析一篇就够了

springclound 之feign原理源码解析及使用详解

目录

feign作用

feign是一款基于注解和动态代理实现的声明式restful http客户端,它提供了类似retrofit、 jaxrs的api和使用方式,但他更加简洁、扩展性也很强,能根据需求扩展http请求各个阶段的处理,使用一系列模板简化了这些请求操作,例如头部、请求参数、参数拼装、结果处理、错误处理等。

feign基本用法

首先,理解feign注解使用方式及作用。

注解使用方式作用
@RequestLineMethod定义http请求行,如GET api/a.配合{param}和@Param注解可以实现占位符的功能可以用接口方法的参数动态赋值,按照名字赋值
@Param方法参数用于@reqeustline或者其他具有{}占位符号的注解表达式指定的变量使用@Param的值进行替换
@Header类型或者方法用于指定请求携带的头部字段,用于接口时该类所有的方法都会携带该请求头部,用于方法是只会对该api的请求使用该头部。
@QueryMap参数用map或者pojo对象填充请求参数
@HeaderMap参数用map或者pojo对象填充请求header
@Body方法定义一个模板表达式可以包含{}占位符,使用接口方法的@Param注解的值填充占位符

接着,我们看一个简单的实例,以下是官方提供的示例。

public interface GitHub {
  
  @RequestLine("GET /repos/{owner}/{repo}/contributors")
  List<Contributor> getContributors(@Param("owner") String owner, @Param("repo") String repository);
  
  class Contributor {
    String login;
    int contributions;
  }
}
public class MyApp {
  public static void main(String[] args) {
    GitHub github = Feign.builder()
                         .decoder(new GsonDecoder())
                         .target(GitHub.class, "https://api.github.com");
    
  /**
  *@Param注解的参数会按照名字填充{owner}{repo}
  HousonCao/Hystrix
  */
    github.contributors("HousonCao", "Hystrix");
  }
}

注意:

  1. request参数的@Param的值默认会被encoded,header不会被encoded
  2. 相同名字的param都会被赋相同值,不管是header,reqeustline还是body
  3. 没有被解析的参数名字将被忽略,头部将被清除
  4. @body注解的需要有content-Type头部

以上是基本的feign的使用,是不是很简洁方便。
接下来我们一起来看下feign内部是怎么实现。

feign源码解析

解决什么问题

思考一个问题:如果要自己实现基于现有的okhttp或者apache httpclient封装一套易用的api,需要怎么做,为了回答这个问题,先看普通的okhttp发送的http请求时怎么样的。

package okhttp;

import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.ObjectMapper;
import okhttp3.*;

import java.io.IOException;
import java.util.HashMap;
import java.util.Map;

public class OkhttpRawDemo {
    public static void main(String[] args) throws IOException {
        try {
            Object o = doPost("http://www.baidu.com", new HashMap<>(), new HashMap<>());
        } catch (JsonMappingException e) {
            e.printStackTrace();
        }
    }
	/**用于发送post请求的方法*/
    public static Object doPost(String url, Map<String, String> headers, Map<String, String> params) throws IOException, JsonMappingException {
        
        OkHttpClient okHttpClient = new OkHttpClient.Builder().build();
        FormBody.Builder formBody = new FormBody.Builder();
        for (Map.Entry<String,String> e :
                params.entrySet()) {
            formBody.add(e.getKey(), e.getValue());
        }
        FormBody body = formBody.build();
        Request request =  new Request.Builder()
                .url(url)
                .headers(Headers.of(headers))
                .post(body)
                .build();
        Response response = okHttpClient.newCall(request).execute();
        String resppnseString = response.body().string();
        System.out.println(resppnseString);
        return new ObjectMapper().readValue(resppnseString,Object.class);
    }
}


通常我们会定义一个工具方法来简化这些模板代码,但是上面简单的例子有以下的问题:

  1. 不同接口的请求格式是千变万化的,可能另一个接口需要上传文件等等,如何能减少这种方法的定义,就是怎么把代码变得更抽象的更通用?api如何更易用?
  2. 当需要替换掉client或者增加功能时,需要大改特改怎么实现好的扩展性?

问题1抽象,很明显,每个请求需要构建一下几个部分:

  1. 请求行
  2. 请求头
  3. 请求主体
  4. 响应

问题1,面对不同的接口做到通用和简单明了的接口,答案是使用声明式的注解配置请求的各个组成。
问题2,要能根据不同的接口注解配置动态组装出不同的增强功能的代理对象来实现对接口请求,达到发送http请求的目的,答案是使用动态代理.

理解这几点要看feign就很简单了,其实其他框架也是这个思路,工具解决什么问题,怎么解决的带着问题去看源码会更有目的性。

从调用时序解析Feign源码

从feign的使用例子看出,实例化feign是使用的builder模式。所以看feign的build()方法是怎么实现的。

  //创建动态代理对象
    public <T> T target(Target<T> target) {
      return build().newInstance(target);
    }

    public Feign build() {
      //根据builder创建动态代理的methodhandler工厂类
      SynchronousMethodHandler.Factory synchronousMethodHandlerFactory =
          new SynchronousMethodHandler.Factory(client, retryer, requestInterceptors, logger,
              logLevel, decode404, closeAfterDecode, propagationPolicy);
      //用于解析接口配置的注解
      ParseHandlersByName handlersByName =
          new ParseHandlersByName(contract, options, encoder, decoder, queryMapEncoder,
              errorDecoder, synchronousMethodHandlerFactory);
      //创建feign实例
      return new ReflectiveFeign(handlersByName, invocationHandlerFactory, queryMapEncoder);
    }

主要是target()和build()方法为入口。
target中的newinstance创建了动态代理对象,其中targetToHandlersByName.apply(target)负责解析所有方法的注解元数据并创建每个方法对应的methodhandler用于代理对象调用

 @SuppressWarnings("unchecked")
    @Override
    public <T> T newInstance(Target<T> target) {
        //解析方法注解
        Map<String, MethodHandler> nameToHandler = targetToHandlersByName.apply(target);
        Map<Method, MethodHandler> methodToHandler = new LinkedHashMap<Method, MethodHandler>();
        List<DefaultMethodHandler> defaultMethodHandlers = new LinkedList<DefaultMethodHandler>();
        //遍历所有方法,对应上解析后的methodhandler
        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)));
            }
        }
        //创建动态代理invocationhandler,FeignInvocationHandler
        InvocationHandler handler = factory.create(target, methodToHandler);
        T proxy = (T) Proxy.newProxyInstance(target.type().getClassLoader(),
                new Class<?>[]{target.type()}, handler);
        //java8 默认方法处理
        for (DefaultMethodHandler defaultMethodHandler : defaultMethodHandlers) {
            defaultMethodHandler.bindTo(proxy);
        }
        return proxy;
    }
 

 //解析接口和方法的元数据,创建methodhandler
 public Map<String, MethodHandler> apply(Target key) {
            //解析注解成metadata,使用Contract解析
            List<MethodMetadata> metadata = contract.parseAndValidatateMetadata(key.type());
            Map<String, MethodHandler> result = new LinkedHashMap<String, MethodHandler>();
            for (MethodMetadata md : metadata) {
                BuildTemplateByResolvingArgs buildTemplate;
                if (!md.formParams().isEmpty() && md.template().bodyTemplate() == null) {
                    //将方法中没有@param的参数以map的形式使用encode到body
                    buildTemplate = new BuildFormEncodedTemplateFromArgs(md, encoder, queryMapEncoder);
                } else if (md.bodyIndex() != null) {
                    //将方法中没有@param的参数以javabean的形式使用encode到body
                    buildTemplate = new BuildEncodedTemplateFromArgs(md, encoder, queryMapEncoder);
                } else {
                    //
                    buildTemplate = new BuildTemplateByResolvingArgs(md, queryMapEncoder);
                }
                //创建并保存method对应的处理器methodhandler
                result.put(md.configKey(),
                        factory.create(key, md, buildTemplate, options, decoder, errorDecoder));
            }
            return result;
        }

apply()中调用了Contract.processAnnotationsOnParameter(),主要的责任是解析api接口和方法的注解,并保存其metadata元数据用于后续构造http请求。详细见注释:

@Override
    protected boolean processAnnotationsOnParameter(MethodMetadata data,
                                                    Annotation[] annotations,
                                                    int paramIndex) {
      boolean isHttpAnnotation = false;
      for (Annotation annotation : annotations) {
        Class<? extends Annotation> annotationType = annotation.annotationType();
        if (annotationType == Param.class) {
          Param paramAnnotation = (Param) annotation;
          String name = paramAnnotation.value();
          checkState(emptyToNull(name) != null, "Param annotation was empty on param %s.",
              paramIndex);
          //该注解在方法签名的第paramIndex个参数上,保存在map中key为位置value是该参数包含的所有注解的元数据
          nameParam(data, name, paramIndex);
          //省略部分代码,均为解析配置代码
      return isHttpAnnotation;
    }

apply()解析完参数之后,使用encoder的encode将其他方法参数装入body,body保存在requestTemplate中。feign的template是用于保存对接口和方法解析后的元数据的抽象,子类有HeaderTemplate、UriTemplate、BodyTemplate、QueryTemplate还有一个requestTemplate由上述类组成。

Contract的以上方法解析了接口和方法中所有的注解,及其值。如果需要扩展新的接口可以继承BaseContract,在其子类解析新的自定义注解即可。

之后会调用encoder将接口方法的没有@Param注解的javabean通过encode塞到http body中,如果有@Param但是没有对应的{}使用该注解的值,也会被传到encoder,但是类型是map。

默认的encdoer实现,也可以使用gson去传json,或者自定义表单提交的encoder;

@Override
    public void encode(Object object, Type bodyType, RequestTemplate template) {
      if (bodyType == String.class) {
        template.body(object.toString());
      } else if (bodyType == byte[].class) {
        template.body((byte[]) object, null);
      } else if (object != null) {
        throw new EncodeException(
            format("%s is not a type supported by this encoder.", object.getClass()));
      }
    }

接着,apply()中创建了methodhandler,这个method是动态代理中的对接口方法进行处理的核心类对象。
下面的代码可以看到,其包含了feign builder所有的成员变量

public MethodHandler create(Target<?> target,
                                MethodMetadata md,
                                RequestTemplate.Factory buildTemplateFromArgs,
                                Options options,
                                Decoder decoder,
                                ErrorDecoder errorDecoder) {
      //创建methodhandler实例
      return new SynchronousMethodHandler(target, client, retryer, requestInterceptors, logger,
          logLevel, md, buildTemplateFromArgs, options, decoder,
          errorDecoder, decode404, closeAfterDecode, propagationPolicy);
    }

methodhandler的核心方法是invoke(),方法名是不是很眼熟,动态代理invacationhandler的invoke最终会调用到这个invoke进行http请求,一下是invacationhandler调用methodhandler的过程

  //代理对象的InvocationHandler 的invoke默认实现
       @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();
            }
			//获取接口对应方法的method对应处理,即发起http请求
            return dispatch.get(method).invoke(args);
        }
@Override
  public Object invoke(Object[] argv) throws Throwable {
    //处理注解字符串中的占位符,querymap和headermap
    RequestTemplate template = buildTemplateFromArgs.create(argv);
    Retryer retryer = this.retryer.clone();
    while (true) {
      try {
        //发起http请求,decode处理结果
        return executeAndDecode(template);
      } 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;
      }
    }
  }
Object executeAndDecode(RequestTemplate template) throws Throwable {
    //执行interceptor,获取target的host
    Request request = targetRequest(template);

    if (logLevel != Logger.Level.NONE) {
      logger.logRequest(metadata.configKey(), logLevel, request);
    }

    Response response;
    long start = System.nanoTime();
    try {
      //发起请求,不同的httpclient实现,注意如果options和okhttp创建时不一样,会创建临时httpclient对象,影响性能
      response = client.execute(request, options);
    } catch (IOException e) {
     //省略错误处理代码   
    }
  }

需要注意的是在使用okhttp作为httpcllient时,如果单个请求配置的超时时间和创建okhttpclient不一样时,会创建临时的httpclient,这里如果并发量较大时是会有性能问题的,每个okhttpclient都会创建线程池,请求队列等对象是比较好资源的操作。并且每个feign的option如果不手动设置会有默认值,连接超时10s,请求超时60s。
看下okhttp的client的execute()实现

@Override
  public feign.Response execute(feign.Request input, feign.Request.Options options)
      throws IOException {
    okhttp3.OkHttpClient requestScoped;
    //判断是否超时时间和okhttpclient创建时一样
    if (delegate.connectTimeoutMillis() != options.connectTimeoutMillis()
        || delegate.readTimeoutMillis() != options.readTimeoutMillis()) {
      //创建临时http对象
      requestScoped = delegate.newBuilder()
          .connectTimeout(options.connectTimeoutMillis(), TimeUnit.MILLISECONDS)
          .readTimeout(options.readTimeoutMillis(), TimeUnit.MILLISECONDS)
          .followRedirects(options.isFollowRedirects())
          .build();
    } else {
      requestScoped = delegate;
    }
    //转为okhttp的request
    Request request = toOkHttpRequest(input);
    Response response = requestScoped.newCall(request).execute();
    //转为feign的response
    return toFeignResponse(response, input).toBuilder().request(input).build();
  }

feign还有其他的一些功能如错误处理、重试机制,官方还有hystrix扩展、Rxjava异步扩展、ribon扩展、httpclient的扩展、coder、encoder扩展等。这都归结于feign的良好扩展性。

总结

feign的调用时序图如下

在这里插入图片描述
总结feign流程就是:解析注解参数-》创建methodHandler-》创建动态代理invocationHandler-》invocationHandler调用methodhandler-》最终调用http client发起请求。

注意:

1.okhttpclient会根据超时时间配置如果每个请求的设置不一样会导致创建多个临时对象

2.retry默认会重试5次,如果不需要重试需要自己设置,feign还是比较简单的,很多组件有默认设置,需要注意这些是否会影响业务

优点:

1.扩展性好,能有很多自定义的扩展点,如rxjava,hystrix等

2.轻量代码量少易学习

3.springclound默认使用的组件

4.api使用简单,简化了http调用api

缺点:

1.组件很多默认的实现有一些坑,需要注意避免

参考

http基础:https://blog.csdn.net/Houson_c/article/details/52266334

feign官方:https://github.com/OpenFeign

  • 4
    点赞
  • 9
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值