Spring Cloud Feign详解

Feign是什么

feign是在SpringCloud微服务框架下,实现微服务间相互调用的开发利器,官网介绍为:https://docs.spring.io/spring-cloud-openfeign/docs/2.2.6.RELEASE/reference/html/ 在这个地址下可以看到详细的feign的介绍,基本上可以跟着这个教程走一遍。

feign是什么?作为英语单词,他的意思是假装、佯装,他假装自己是一个客户端,而底层是通过http请求进行通信的。这与传统的客户端(带驱动程序的如mysql-jdbc-connector)是不一样的,但概念上类似,只是通信方式上有些差别。

代码地址

本次演示的代码放到gitee上了。 feign演示代码地址,可以直接检出测试,也可以fork后自己随意修改。

由于feign是用于微服务间的调用的,所以在这里,需要有一个注册中心,我选择的是nacos注册中心,nacos选用的服务端版本是2.1.0,具体可以从这个地方下载:https://github.com/alibaba/nacos/releases/tag/2.1.0 如果打不开的话多刷新几次,如果还是打不开,可以来我的云盘下载:链接: https://pan.baidu.com/s/11BDue7WvzAIiswlIch9bcw 提取码: 8j2s。

启动nacos

nacos的官网地址:https://nacos.io/zh-cn/,详细的用法暂且不表,启动方式为:

在 nacos-server-2.1.0\nacos\bin 目录下,打开cmd,执行 startup.cmd -m standalone,启动nacos服务,界面显示如下表示启动成功:

启动后,访问nacos地址:

http://localhost:8848/nacos,登录用户名密码为: nacos/nacos,可以看到如下界面:

启动服务提供者provider

服务提供者需要添加依赖:

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>

其版本号由依赖管理控制:

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>com.alibaba.cloud</groupId>
                <artifactId>spring-cloud-alibaba-dependencies</artifactId>
                <version>2.1.0.RELEASE</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

稍后我们会分析其代码及原理。

服务启动类:

@SpringBootApplication
public class ProviderApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(ProviderApplication.class, args);
    }
    
}

服务只加了一个controller,用于简单测试:

@RestController
public class HelloController {
    
    @RequestMapping("/provider/hello")
    public String hello(@RequestParam("name") String name){
        return "from provider:"+name;
    }
    
}

服务配置参数文件:

server:
  port: 8081
spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
  application.name: provider

直接运行ProviderApplication启动服务,启动后,在浏览器访问:http://localhost:8081/provider/hello?name=1 可以看到如下输出:

启动服务消费者consumer

消费者服务,添加nacos依赖和feign依赖:

        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
        </dependency>

(当然spring-boot-starter-web依赖是必须加的,具体可参考资料地址里的gitee仓库)。

启动类:

@SpringBootApplication
@EnableFeignClients("demo.consumer")
public class ConsumerApplication {
    
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApplication.class, args);
    }
    
}

消费者的controller:

@RestController
public class ConsumerController {
    @Autowired
    private ProviderFeignClient providerFeignClient;
    
    @RequestMapping(path="/consumer/hello")
    public String consumer(@RequestParam("name") String name){
        return "from consumer:"+name;
    }
    
    @RequestMapping("/consumer/provider")
    public String consumerProvider(@RequestParam("name") String name){
        String hello = providerFeignClient.hello(name);
        return "from consumer,"+hello;
    }
}

feign接口定义:

@FeignClient("provider")
public interface ProviderFeignClient {
    
    @RequestMapping("/provider/hello")
    public String hello(@RequestParam("name") String name);
    
}

服务配置文件:

server:
  port: 8082
spring:
  cloud:
    nacos:
      discovery:
        server-addr: 127.0.0.1:8848
  application.name: consumer

直接运行ConsumerApplication启动消费者服务。

consumer服务启动后,测试controller自身接口,访问地址 http://localhost:8082/consumer/hello?name=1 可以看到如下输出。

查看服务状态

provider服务和consumer服务都启动后,可以在nacos上查看服务注册情况。

可以清楚看到两个服务都注册上了,后面即可以进行调用测试了。

测试feign接口

注意上面在consumer服务里的ConsumerController里面有一个方法:

    @RequestMapping("/consumer/provider")
    public String consumerProvider(@RequestParam("name") String name){
        String hello = providerFeignClient.hello(name);
        return "from consumer,"+hello;
    }

这个方法就用到了我们本次要说的重点:feign接口,他注入了feign接口,对provider服务进行调用,下面测试这个接口。

在浏览器中访问地址:http://localhost:8082/consumer/provider?name=1 可以看到如下返回:

可以看出,成功实现了consumer服务队provider服务的调用。下面我们进行深入分析。

分析调用流程

分析调用,有一个技巧,那就是:断点调试,包括任何的问题、任何的框架,归根结底就是调试来学习的。包括自己在工作中编写的代码,也通过调试来发现问题,分析流程。

那么在哪里加断点呢?这很重要,这里也是有技巧的,一般就是加在调用入口处,或者说,就是在用户编写的代码里找入口断点(当然,在对框架比较熟悉之后,可以在任何自己关心的地方加断点,但思路不变,仍然是对未知区域的入口处加断点)。

加入口断点

说了这么多,那么feign调用,应该在哪里加断点呢?ProviderFeignClient本身只是个接口,没法加断点,但我们就是要调试这个接口,所以我们是对ConsumerController加断点。

    @RequestMapping("/consumer/provider")
    public String consumerProvider(@RequestParam("name") String name){
        String hello = providerFeignClient.hello(name); // 在这个地方加断点
        return "from consumer,"+hello;
    }

仍然访问地址 http://localhost:8082/consumer/provider?name=1 进入设置的断点:

然后按F5进入方法内(IDEA下为F7单步,后面无特别说明,按照Eclipse的默认快捷键来操作)。

进入Proxy代理回调类

可以看到是进入到了FeignInvocationHandler#invoke()方法,ConsumerController调用的providerFeignClient是一个jdk动态代理$Proxy对象,看FeignInvocationHandler也确实实现了Proxy里的重要接口类:java.lang.reflect.InvocationHandler。下面看下FeignInvocationHandler的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();
      }

      return dispatch.get(method).invoke(args); // 在这里放下一个断点
    }

这里很好理解,一般我们自己给接口编写Proxy代理也是这样的思路,根据方法名进行相应的处理,这里先排除掉Object的常用方法,然后从dispatch这个map中获取method对应的处理MethodHandler,进行调用。至于这里dispatch是怎么来的,暂且不管。

在dispatch.get.invoke()的地方单步进入,进入如下所示调用栈:

可以看到这里进入的是SynchronousMethodHandler.invoke()方法,他实现了MethodHandler接口。

调用MethodHandler

MethodHandler简单理解,就是在ProviderFeign里每一个接口方法对应的处理类,为什么一个方法一个?因为每个方法都有差异,可能是GET POST,url不同,请求参数不同,返回类型不同等等。

下面分析下SynchronousMethodHandler.invoke()

  @Override
  public Object invoke(Object[] argv) throws Throwable {
    RequestTemplate template = buildTemplateFromArgs.create(argv); // 创建一个template对象
    Options options = findOptions(argv); 
    Retryer retryer = this.retryer.clone();  // 本次feign调用的重试器
    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()方法是核心方法,进入这个方法:

SynchronousMethodHandler.executeAndDecode()这个代码有点长,不过并不复杂,走完这个方法后,feign调用也就结束了。

★调用executeAndDecode

这是处理的主方法,是阅读代码时应该关注的重点。

SynchronousMethodHandler#executeAndDecode()

Object executeAndDecode(RequestTemplate template, Options options) throws Throwable {
    Request request = targetRequest(template); // 应用RequestInterceptor拦截器,通过Feign.Target.apply()得到Request对象。

    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(); // 给响应对象关联上对应的请求对象和requestTemplate,后续可以使用这俩属性
    } 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);

    boolean shouldClose = true;
    try {
      if (logLevel != Logger.Level.NONE) {
        response =
            logger.logAndRebufferResponse(metadata.configKey(), logLevel, response, elapsedTime);
      }
      if (Response.class == metadata.returnType()) { // 如果用户的feign接口方法返回类型恰好就是Feign.Response的话,此时不管response.status()是成功还是失败,直接返回
        if (response.body() == null) {
          return response;
        }
        if (response.body().length() == null ||
            response.body().length() > MAX_RESPONSE_BUFFER_SIZE) {
          shouldClose = false;
          return response;
        }
        // Ensure the response body is disconnected
        byte[] bodyData = Util.toByteArray(response.body().asInputStream());
        return response.toBuilder().body(bodyData).build(); // 设置用户最关心的返回数据并返回,不过注意,正常情况下用户feign接口返回类型不会是Response
      }
      if (response.status() >= 200 && response.status() < 300) { // status状态码2xx表示成功
        if (void.class == metadata.returnType()) { // 空返回值,直接返回
          return null;
        } else {
          Object result = decode(response); // 解码,这里就是调用feign.codec.Decoder的实现类,典型的如ResponseEntityDecoder
          shouldClose = closeAfterDecode;
          return result; // 一般到这就结束了
        }
      } else if (decode404 && response.status() == 404 && void.class != metadata.returnType()) {  // 如果404不认为是报错,在这里正常decode
        Object result = decode(response);
        shouldClose = closeAfterDecode;
        return result;
      } else { // 响应码失败的情况,进入feign的错误处理.一般这里自定义一个ErrorDecoder,读取response对象转义为业务异常,自定义ErrorDecoder里可以使用ResponseEntityDecoder解析response为Map,
        throw errorDecoder.decode(metadata.configKey(), response);
      }
    } catch (IOException e) {
      if (logLevel != Logger.Level.NONE) {
        logger.logIOException(metadata.configKey(), logLevel, e, elapsedTime);
      }
      throw errorReading(request, response, e);
    } finally {
      if (shouldClose) {
        ensureClosed(response.body());
      }
    }
  }

解释一下,targetRequest()之所以能通过RequestTemplate返回Request对象,是因为RequestTemplate就是为产生Request对象而服务的,可以看到RequestTemplate.request()方法,就是用RequestTemplate(请求模板)里的各个参数(请求方法、url、headers、requestBody)等来构造出Request的。

具体调用的细节说明见上方代码注释。下面看一下本方法里的核心方法:

response = client.execute(request, options);

调试可以看到,这里的client是 LoadBalancerFeignClient 实例。

调用feign.Client#execute

调用LoadBalancerFeignClient#execute()方法。

看下这段代码:LoadBalancerFeignClient#execute()

    @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); // 去掉http://provider前缀
            FeignLoadBalancer.RibbonRequest ribbonRequest = new FeignLoadBalancer.RibbonRequest(
                    this.delegate, request, uriWithoutHost); // delegate是feign.Client$Default默认实现

            IClientConfig requestConfig = getClientConfig(options, clientName);
            return lbClient(clientName) // 获取FeignLoadBalancer对象
                    .executeWithLoadBalancer(ribbonRequest, requestConfig).toResponse(); // 动态选择服务实例并发起调用
        }
        catch (ClientException e) {
            IOException io = findIOException(e);
            if (io != null) {
                throw io;
            }
            throw new RuntimeException(e);
        }
    }

这里lbClient()方法返回的对象类型是FeignLoadBalancer,这个是具有一个ribbon的loadbalancer属性的,就是 ILoadBalancer lb,他就是借助ribbon的软负载功能实现按服务名负载均衡的。

lbClient(clientName).executeWithLoadBalancer()这个方法,其实就是看FeignLoadBalancer对象。

FeignLoadBalancer <- AbstractLoadBalancerAwareClient <- LoadBalancerContext

这里调用的是executeWithLoadBalancer()方法,这个方法是在AbstractLoadBalancerAwareClient父类的。

AbstractLoadBalancerAwareClient#executeWithLoadBalancer()

    
    public T executeWithLoadBalancer(final S request, final IClientConfig requestConfig) throws ClientException {
        LoadBalancerCommand<T> command = buildLoadBalancerCommand(request, requestConfig); //构建了一个Command对象,这个对象是在ribbon-loadbalancer包下的

        try {
            return command.submit( // 提交一个任务,我们关心的逻辑就在这里面,至于command本身怎么处理的,不用深究
                new ServerOperation<T>() {
                    @Override
                    public Observable<T> call(Server server) {
                        URI finalUri = reconstructURIWithServer(server, request.getUri());
                        S requestForServer = (S) request.replaceUri(finalUri);
                        try {
                            // 这个地方注意下,是真正执行网络请求的地方
                            return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
                        } 
                        catch (Exception e) {
                            return Observable.error(e);
                        }
                    }
                })
                .toBlocking() // 同步等待结果
                .single(); // 获取返回值
        } catch (Exception e) {
            Throwable t = e.getCause();
            if (t instanceof ClientException) {
                throw (ClientException) t;
            } else {
                throw new ClientException(e);
            }
        }
        
    }

LoadBalancerCommand

不用对LoadBalancerCommand有什么疑惑,这就是个普通的对象,他是ribbon基于rxjava做的负载均衡处理关键类,里面用大量的rxjava的api实现了服务负载均衡。只是如果不用rxjava会更好懂一点而已。

LoadBalancerCommand#submit()

public Observable<T> submit(final ServerOperation<T> operation) {
        final ExecutionInfoContext context = new ExecutionInfoContext();
        
        if (listenerInvoker != null) {
            try {
                listenerInvoker.onExecutionStart();
            } catch (AbortExecutionException e) {
                return Observable.error(e);
            }
        }
        // 重试次数是在retryHandler中统一配置的,有兴趣可以自行追溯一下在哪配置
        final int maxRetrysSame = retryHandler.getMaxRetriesOnSameServer();
        final int maxRetrysNext = retryHandler.getMaxRetriesOnNextServer();

        // Use the load balancer
        // 构造Observable对象,先选出server作为Observable的对象流,然后转换为新的Observable
        Observable<T> o =  // 这块用到了rxjava的Observable,具体可不用深究
                (server == null ? selectServer() : Observable.just(server))
                .concatMap(new Func1<Server, Observable<T>>() {
                    @Override
                    // Called for each server being selected
                    // 就是在这里转化了一个新的Observable,因为网络调用是耗时的,相当于进行异步化
                    public Observable<T> call(Server server) {
                        context.setServer(server);
                        final ServerStats stats = loadBalancerContext.getServerStats(server);
                        
                        // Called for each attempt and retry
                        // 这里会进行自动重试,针对maxRetrysSame同一服务实例重试次数
                        Observable<T> o = Observable
                                .just(server)
                                .concatMap(new Func1<Server, Observable<T>>() {
                                    @Override
                                    public Observable<T> call(final Server server) {
                                        context.incAttemptCount();
                                        loadBalancerContext.noteOpenConnection(stats);
                                        
                                        if (listenerInvoker != null) {
                                            try {
                                                listenerInvoker.onStartWithServer(context.toExecutionInfo());
                                            } catch (AbortExecutionException e) {
                                                return Observable.error(e);
                                            }
                                        }
                                        
                                        final Stopwatch tracer = loadBalancerContext.getExecuteTracer().start();
                                        // 注意看这里回调了operation,这个operation就是上一个代码的command.submit()提交的
                                        return operation.call(server).doOnEach(new Observer<T>() {
// 这块省略N行代码,这块代码主要就是
                                        });
                                    }
                                });
                        // 这里的重试针对的是同一个server
                        if (maxRetrysSame > 0) 
                            o = o.retry(retryPolicy(maxRetrysSame, true));
                        return o;
                    }
                });
        // 这里会进行重试,重试下一个服务实例
        if (maxRetrysNext > 0 && server == null) 
            o = o.retry(retryPolicy(maxRetrysNext, false));
        
        return o.onErrorResumeNext(new Func1<Throwable, Observable<T>>() {
            @Override
            public Observable<T> call(Throwable e) {
                if (context.getAttemptCount() > 0) {
                    if (maxRetrysNext > 0 && context.getServerAttemptCount() == (maxRetrysNext + 1)) {
                        e = new ClientException(ClientException.ErrorType.NUMBEROF_RETRIES_NEXTSERVER_EXCEEDED,
                                "Number of retries on next server exceeded max " + maxRetrysNext
                                + " retries, while making a call for: " + context.getServer(), e);
                    }
                    else if (maxRetrysSame > 0 && context.getAttemptCount() == (maxRetrysSame + 1)) {
                        e = new ClientException(ClientException.ErrorType.NUMBEROF_RETRIES_EXEEDED,
                                "Number of retries exceeded max " + maxRetrysSame
                                + " retries, while making a call for: " + context.getServer(), e);
                    }
                }
                if (listenerInvoker != null) {
                    listenerInvoker.onExecutionFailed(e, context.toFinalExecutionInfo());
                }
                return Observable.error(e);
            }
        });
    }

这块代码可以不用深究,因为他用到了大量rxjava的api,比较难以阅读。

对于我们来说,主要关注两个地方:

  1. selectServer()方法选取server实例(ribbon软负载)

  1. AbstractLoadBalancerAwareClient.this.execute()方法执行网络请求

feign不可能直接用服务名进行网络调用,一定是先根据服务名按照某种选取策略,选取一个可用的服务实例后,确定本次请求的实际地址,也就是ip:port,在这个场景下就是provider服务的地址 localhost:8081.然后用传统的网络调用方法进行http调用,并将返回结果处理为接口方法返回值类型。

LoadBalancerCommand#submit()提交的任务,需要再重点说一下:

    new ServerOperation<T>() {
                    @Override
                    public Observable<T> call(Server server) {
                        // 使用server重建url,也就是把http://provider替换为http://localhost:8081
                        URI finalUri = reconstructURIWithServer(server, request.getUri());
                        // 用替换后的url替换request里的url,这里的request就是RibbonRequest对象
                        S requestForServer = (S) request.replaceUri(finalUri);
                        try { // 替换后执行网络调用
                            return Observable.just(AbstractLoadBalancerAwareClient.this.execute(requestForServer, requestConfig));
                        } 
                        catch (Exception e) {
                            return Observable.error(e);
                        }
                    }
                })

AbstractLoadBalancerAwareClient.this.execute()==FeignLoadBalancer#execute()

执行FeignLoadBalancer#execute

FeignLoadBalancer#execute()

    @Override
    public RibbonResponse execute(RibbonRequest request, IClientConfig configOverride)
            throws IOException {        
        // 得到请求的options参数,就是截时间,如连接超时事件、读取超时时间等,作为调用的参数传递
        Request.Options options;
        if (configOverride != null) {
            RibbonProperties override = RibbonProperties.from(configOverride);
            options = new Request.Options(override.connectTimeout(this.connectTimeout),
                    override.readTimeout(this.readTimeout));
        }
        else {
            options = new Request.Options(this.connectTimeout, this.readTimeout);
        }
        // 一切准备就绪,调用request.client()执行网络请求,这里的client()就是在new RibbonRequest()的时候传入的delegate,在这里其实就是个Client$Default实例
        Response response = request.client().execute(request.toRequest(), options);
        return new RibbonResponse(request.getUri(), response);
    }

可以看出最终其实是feign.Client$Default进行的请求,这就来到了咱们最后一个代码分析了。

调用feign.Client$Default

feign.Client$Default

@Override
    public Response execute(Request request, Options options) throws IOException {
      // 这个方法的具体代码就在下面
      HttpURLConnection connection = convertAndSend(request, options);
      return convertResponse(connection, request);
    }
     
    HttpURLConnection convertAndSend(Request request, Options options) throws IOException {
      final URL url = new URL(request.url());
      // 终于露出真面目了,使用的HttpURLConnection进行http通信
      final HttpURLConnection connection = this.getConnection(url);
      if (connection instanceof HttpsURLConnection) {
        HttpsURLConnection sslCon = (HttpsURLConnection) connection;
        if (sslContextFactory != null) {
          sslCon.setSSLSocketFactory(sslContextFactory);
        }
        if (hostnameVerifier != null) {
          sslCon.setHostnameVerifier(hostnameVerifier);
        }
      }
      // 应用上面说到的options
      connection.setConnectTimeout(options.connectTimeoutMillis());
      connection.setReadTimeout(options.readTimeoutMillis());
      connection.setAllowUserInteraction(false);
      connection.setInstanceFollowRedirects(options.isFollowRedirects());
      connection.setRequestMethod(request.httpMethod().name());

      Collection<String> contentEncodingValues = request.headers().get(CONTENT_ENCODING);
      boolean gzipEncodedRequest =
          contentEncodingValues != null && contentEncodingValues.contains(ENCODING_GZIP);
      boolean deflateEncodedRequest =
          contentEncodingValues != null && contentEncodingValues.contains(ENCODING_DEFLATE);

      boolean hasAcceptHeader = false;
      Integer contentLength = null;
      // 添加http请求头
      for (String field : request.headers().keySet()) {
        if (field.equalsIgnoreCase("Accept")) {
          hasAcceptHeader = true;
        }
        for (String value : request.headers().get(field)) {
          if (field.equals(CONTENT_LENGTH)) {
            if (!gzipEncodedRequest && !deflateEncodedRequest) {
              contentLength = Integer.valueOf(value);
              connection.addRequestProperty(field, value);
            }
          } else {
            connection.addRequestProperty(field, value);
          }
        }
      }
      // Some servers choke on the default accept string.
      if (!hasAcceptHeader) {
        connection.addRequestProperty("Accept", "*/*");
      }
      // 有请求体时的处理(如POST方式)
      if (request.requestBody().asBytes() != null) {
        if (contentLength != null) {
          connection.setFixedLengthStreamingMode(contentLength);
        } else {
          connection.setChunkedStreamingMode(8196);
        }
        connection.setDoOutput(true);
        OutputStream out = connection.getOutputStream();
        if (gzipEncodedRequest) {
          out = new GZIPOutputStream(out);
        } else if (deflateEncodedRequest) {
          out = new DeflaterOutputStream(out);
        }
        try {
          // 将请求体写入http输出流
          out.write(request.requestBody().asBytes());
        } finally {
          try {
            out.close();
          } catch (IOException suppressed) { // NOPMD
          }
        }
      }
      // 此时connection已经通信完成,后续解析响应信息
      return connection;
    }
  }

Client$Default#convertResponse()

Response convertResponse(HttpURLConnection connection, Request request) throws IOException {
      // 获取响应码
      int status = connection.getResponseCode();
      String reason = connection.getResponseMessage();

      if (status < 0) {
        throw new IOException(format("Invalid status(%s) executing %s %s", status,
            connection.getRequestMethod(), connection.getURL()));
      }
      // 获取响应头
      Map<String, Collection<String>> headers = new LinkedHashMap<>();
      for (Map.Entry<String, List<String>> field : connection.getHeaderFields().entrySet()) {
        // response message
        if (field.getKey() != null) {
          headers.put(field.getKey(), field.getValue());
        }
      }
      // 获取响应体大小
      Integer length = connection.getContentLength();
      if (length == -1) {
        length = null;
      }
      // 获取响应流,但并未读取流
      InputStream stream;
      if (status >= 400) {
        stream = connection.getErrorStream();
      } else {
        stream = connection.getInputStream();
      }
      // 构建Response对象
      return Response.builder()
          .status(status)
          .reason(reason)
          .headers(headers)
          .request(request)
          .body(stream, length)
          .build();
    }

到这里就已经服务调用结束了,后面又会回到SynchronousMethodHandler.executeAndDecode()方法的decode环节,就是调用decoder.decode(response, metadata.returnType()),如果返回的是对象就采用json反序列化,如果是普通的string,就直接读取string了。(这里的转换器跟springmvc里的其实是一个东西)。

到这里就是彻底执行完了,从ProviderFeign接口方法调用返回了,最后返回到consumer服务,在浏览器显示。

feign框架的意义

可以看出的是,通过引入feign,他让我们可以用普通接口的方式来进行跨服务的网络调用,这对于开发人员来说能屏蔽通信细节,用起来跟本地接口方法并无差别,不用操心底层如何通信,只要按照feign的要求编写接口、添加注解即可。

为了更好的理解feign,我们来想一下,如果不用feign,让我们自己完全手动实现跨服务调用的话,我们会怎么做?

  1. 确定服务名,比如确定要调用的服务叫provider

  1. 根据服务名,使用注册中心的api获取服务地址列表。(比如注入DiscoveryClient来获取)

  1. 根据某种实例选择策略,选取某个服务实例,作为本次请求的服务地址(本例只起了一个实例)

  1. 确定要调用的服务端url,在这里就是provider服务的url:/provider/hello

  1. 用确定的服务地址,拼接上url,使用喜欢的http通信工具进行通信,比如HttpURLConnection,http-client,okhttp等。

  1. 对http通信后的响应结果,首先判断响应码,如果不是2xx就认为失败,报错处理

  1. 如果响应码是2xx,就读取结果,比如读取到一个json串

  1. 将读取到的结果反序列化为自定义的实体对象,进行后续使用,或就转成一个Map进行使用。

可以看到,除了确定服务名、url地址、返回数据类型之外,其他操作都是重复的,而框架就是为了解决这些重复问题而出现的。下面进行上方步骤与feign的对应。

1 对应于ProviderFeign接口中的@FeignClient("provider")

2和3对应于LoadBalancerCommand#submit()中的selectServer()方法

4对应于ProviderFeign中的@RequestMapping("/provider/hello")

5对应于LoadBalancerCommand#submit()中的reconstructURIWithServer()方法

6对应于SynchronousMethodHandler.executeAndDecode()方法中response.status()的判断

7和8对应于SynchronousMethodHandler.executeAndDecode()中的decode()方法

另外,异常处理feign也考虑到了,我们只需要定义一个ErrorDecoder就可以实现异常捕捉处理。

相信通过这么比较的看,就更能理解feign框架做了什么,更好理解feign的处理调用流程了。

站在实际使用的角度,feign是如何处理我们的请求的(归纳调用流程)?

  1. 我们调用了ProviderFeign.hello()方法

  1. 我们调用的其实是通过FeignClientFactoryBean对接口代理后的动态代理类,在这里就是ReflectiveFeign。

  1. ReflectiveFeign里有一个list对象:dispatch,这里面保存了对ProviderFeign的解析结果,可以理解为url到处理handler的映射集合。

  1. 根据url找到对应的handler,这里是SynchronousMethodHandler(意为同步方法调用处理器),调用他的invoke()方法,实际调用executeAndDecode().

  1. 开始构建请求RequestTemplate对象,确定请求控制选项,重试处理方式等

  1. 从RequestTemplate得到Request对象,通过feign.Client对象执行服务调用,这里的feign.Client实现为LoadBalancerFeignClient,从名字中可以看出,这是具有负载均衡能力的client,另外注意到他有一个delegate代理对象就是Client$Default,是用于确定服务地址后使用的真实通信类。

  1. 把原Request请求转换为RibbonRequest对象,把上面说的delegate对象也设置给他

  1. 获取FeignLoadBalancer对象,这个对象里设置了ILoadBalancer对象,这其实是ribbon里的核心对象,用于负载均衡的。

  1. 调用FeignLoadBalancer的executeWithLoadBalancer()方法进行服务调用

  1. 构建LoadBalancerCommand对象进行rxjava风格的调用,rxjava不用太多理解,重点是看这个command他submit的任务是什么,就是替换url地址,进行调用。

  1. 在上面说的command.submit()的任务执行中,会调用FeignLoadBalancer重载的方法execute(),这个意思就是说服务地址已经确定了,按原计划调用。最后还是用了HttpURLConnection的方式完成了调用。

  1. 调用栈回到第4步SynchronousMethodHandler的executeAndDecode方法,进行Response响应结果的处理,简单说来就是判断http状态码,将响应结果转为ProviderFeign接口方法定义的返回类型,在这里是String,那么就是用StringHttpMessageConverter进行转换。

  1. feign调用结束并返回,用户调用的ProviderFeign#hello()方法返回结果,继续后面的正常代码。

Feign如何进行的接口动态代理

这就要说到@EnableFeignClients注解了。点进去看下,可以看到他@Import导入了配置类FeignClientsRegistrar,这个类再看下。(这里仅展示部分关键代码)

    // 向spring容器注册bean定义
    private void registerFeignClient(BeanDefinitionRegistry registry,
            AnnotationMetadata annotationMetadata, Map<String, Object> attributes) {
        String className = annotationMetadata.getClassName();
        // 采用工厂bean的方式,具体为FeignClientFactoryBean这个FactoryBean,所以关注getObject()方法
        BeanDefinitionBuilder definition = BeanDefinitionBuilder
                .genericBeanDefinition(FeignClientFactoryBean.class);
        validate(attributes);
        // 这里添加的一系列参数,都是feign构造代理实例需要的,如服务名、接口类型等
        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();

        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;
        }
        // 注册beanDefinition
        BeanDefinitionHolder holder = new BeanDefinitionHolder(beanDefinition, className,
                new String[] { alias });
        BeanDefinitionReaderUtils.registerBeanDefinition(holder, registry);
    }

因为ProviderFeign只是个接口,所以如果想从spring容器中注入一个ProviderFeign类型的bean,就得注册一个对应类型的bean,这个bean的实现类一般就是个FactoryBean,在getObject()方法里进行接口的动态代理,返回代理后的实例。

如果想深入了解,可以去看下FeignClientFactoryBean#getObject()方法,这就是ProviderFeign的代理类生成的地方。

FeignAutoConfiguration自动配置

这个自动配置类里,注入FeignClientSpecification,可以理解为得到用户在@FeignClient中通过configuration属性设置的FeignClient配置信息,在这里配置的bean作用域为本FeignClient。比如还有个OrderFeign,StorageFeign,对应订单服务、仓库服务,完全可以执行不同的configuration类.

同时FeignAutoConfiguration定义了最重要的对象:FeignContext,就是这里要说到的。他是spring-context的NamedContextFactory实例,至于命名上下文是什么,可以再查阅相关资料,简单理解就是根据名字获取独立上下文,这对于feign来说很重要,因为我们很可能对不同的feign有不同的配置,而这些配置很多情况下就是个标准的bean,如果都放在一个spring容器中,会存在相互污染、定义混乱的情况,所以使用了命名上下文。(对标ribbon里的SpringClientFactory)

FeignAutoConfiguration还定义了一些默认实现,比如feign.Client对象,当client独立上下文中没定义对应的bean时,可以用父容器的(也就是我们当前的spring容器)。

Feign与Ribbon的自动整合

feign和ribbon如何结合的,可以参考在feign框架的意义里说的,第8点,“获取FeignLoadBalancer对象,这个对象里设置了ILoadBalancer对象,这其实是ribbon里的核心对象,用于负载均衡的。”

feign通过对ILoadBalancer对象的使用,实现了负载均衡功能,所有对ribbon的配置仍然可以用,因为feign是站在比较高的层次来用ribbon的api的,对于feign而言,他更关心的也不是确定服务实例地址,而是如何进行url映射、请求参数包装request、响应结果解码,以及异常处理机制等。

本篇到此终于结束,希望对大家有所帮助,能够对大家的调试、理解框架有一点启发。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值