【微服务专题】深入理解与实践微服务架构(六)之整合Openfeign服务调用

1,827 阅读25分钟

我报名参加金石计划1期挑战——瓜分10万奖池,这是我的第6篇文章,点击查看活动详情

默认集成的Http客户端工具是RestTemplate,Feign框架是对RestTemplate框架的封装(2019年Feign停止维护后开源升级版本为OpenFeign)

什么是服务调用

服务调用,即一个独立的服务调用另一个独立服务,此过程可以分为服务调用者、服务提供者。在微服务中存在服务调用的概念,因为注册中心的存在,将拆分出来的多个服务组成了一个以注册中心为中心的分布式系统;除注册中心外,进行服务发现的节点之间相互调用的过程则称之为服务调用。因为注册中心实现了DNS服务器的功能,因此,服务调用者可以直接通过服务名来调用,注册中心会将服务名解析为真实的IP地址。因此,一般服务调用组件也可以默认实现负载均衡的效果,例如feign/openfeign就默认集成了ribbon负载均衡器。

服务调用的几大实现

在Spring Cloud生态中服务调用有RestTemplateOpenFeign这两种常见的方式,当然还有停止维护的Feign客户端和针对未来的响应式请求的Spring WebClient框架,以及HttpClientOKHttpRetrofit等第三方Http客户端,甚至还有JDK原生自带的 HttpURLConnection客户端请求工具。

下面是这几种Http客户端的区别:

  • HttpURLConnection:java.net包下的原生http请求工具,首次出现了统一资源定位器连接器的概念。
  • HttpClient:原为Apache HttpClient,于JDK 9引入JDK中(JDK 11正式可用),取代了JDK更早期的HttpUrlConnection类。
  • OKHttp:采用异步IO网络模型,性能极佳;客户端对象可重复使用,功能丰富,高度可配置。
  • Retrofit:基于OKHttp的封装框架。
  • RestTemplate:在Spring3中引入的Http客户端工具。RestTemplate只是对其它Rest客户端的一个封装,本身并没有自己的实现。Spring Boot 2.0之前RestTestTemplate的默认实现是HttpClient,2.+为OKHttp3,其实这种说法并不完全正确,如果没有引入这些客户端的jar包,其默认实现是HttpURLConnection(集成了URLConnection),这是JDK自带的REST客户端实现。
  • WebClient:在Spring5中引入的非阻塞式Reactive Http客户端,可能会作为未来RestTemplate弃用的替代方案(官方源码介绍)。
  • Feign:Spring Cloud下的一个轻量级Http客户端组件,内置Ribbon负载均衡组件,但目前Netiflix已停止维护。
  • OpenFeign:在Feign的基础上支持了SpringMVC的注解,以JDK动态代理的方式,通过IOC容器注入请求到调用实现者。

Feign与OpenFeign的区别

区别

FeginOpenFegin
Feign是Spring Cloud组件中的一个轻量级RESTful的HTTP服务客户端 Feign内置了Ribbon,用来做客户端负载均衡,去调用服务注册中心的服务。Feign的使用方式是:使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务OpenFeign是Spring Cloud 在Feign的基础上支持了SpringMVC的注解,如@RequesMapping等等。OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。
org.springframework.cloud spring-cloud-starter-feignorg.springframework.cloud spring-cloud-starter-openfeign
  • 他们底层都是内置了Ribbon,去调用注册中心的服务。
  • Feign是Netflix公司写的,是SpringCloud组件中的一个轻量级RESTful的HTTP服务客户端,是SpringCloud中的第一代负载均衡客户端。
  • OpenFeign是SpringCloud自己研发的,在Feign的基础上支持了Spring MVC的注解,如@RequesMapping等等。是SpringCloud中的第二代负载均衡客户端。
  • Feign本身不支持Spring MVC的注解,使用Feign的注解定义接口,调用这个接口,就可以调用服务注册中心的服务
  • OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务

OpenFeign实现原理

img

总结:通过@EnableFeignClinets注解将Feign客户端接口注入到Spring容器,用来解析配置类,根据配置从容器获取到一个Feign.Builder,然后再从容器中获取每个组件,填充到Feign.Builder中,最后通过Feign.Builder的build方法来构造动态代理对象。

image-20220517132557366

此外,查看源码依赖结构,发现openfeign依赖于spring-boot-starter-aop;所以,就很好理解为什么openfeign的实现原理是动态代理了。

创建服务调用子模块

这里我们主要集成spring cloud生态中服务调用的常用的两大框架:RestTemplate和OpenFeign。

创建子模块service-invocate-openfeign:

因为RestTemplate属于spring自带的http请求客户端框架,因此没有另外新建一个模块,而是放到了openfeign子模块项目中一起集成。

image-20220504040112734

集成RestTemplate客户端

最原始的调用方式 -- RestTemplate

SpringBoot的Web依赖默认就集成了RestTemplate客户端,因此无需引入第三方依赖。另外,因为2.x版本实现为OKHttp框架,性能极佳;引入OKHttp的jar包依赖后,即可使用OKHttp进行异步网络请求。

1. 添加Nacos和Web依赖

添加nacos服务发现和web依赖,用于作为Nacos的客户端进行服务发现和引入RestTemplate所需的web依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spring-cloud-alibaba-starter</artifactId>
        <groupId>com.deepinsea</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>
​
    <artifactId>service-consumer-openfeign</artifactId>
​
    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
    </properties>
​
    <dependencies>
        <!-- 包含resttemplate依赖 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!--nacos service discovery client依赖-->
        <dependency>
            <groupId>com.alibaba.cloud</groupId>
            <artifactId>spring-cloud-starter-alibaba-nacos-discovery</artifactId>
            <version>2021.0.1.0</version>
        </dependency>
    </dependencies>
</project>

2. 主启动类服务发现声明

编写主启动类src/main/java/com/deepinsea/ServiceConsumerOpenFeignApplication.java

package com.deepinsea;
​
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
​
/**
 * @author deepinsea
 * @date 2022/5/4
 * 服务消费者主启动类
 */
@SpringBootApplication
@EnableDiscoveryClient
public class ServiceConsumerOpenFeignApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceConsumerOpenFeignApplication.class, args);
    }
}

3. 编写服务消费者配置文件

server:
  # 服务运行端口
  port: 9020
spring:
  application:
    # 服务名称
    name: service-consumer-openfeign
  cloud:
    nacos:
      discovery:
        # 服务注册地址
        server-addr: localhost:8848
        # Nacos认证信息
        username: nacos
        password: nacos
        # 注册到 nacos 的指定 namespace,默认为 public
        namespace: public
    inetutils:
      # 延长超时时间为10s
      timeout-seconds: 10

4. 创建RestTemplate配置类

这里使用配置类的方式注入spring容器的原因是:依赖注入的思想,即某客户类只依赖于服务类的一个接口,而不依赖于具体服务类。也可以直接实例化调用,但是工程大了以后统一配置维护麻烦。

package com.deepinsea.config;
​
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
​
/**
 * @author deepinsea
 * @date 2022/5/4
 */
@Configuration
public class RestTemplateConfig {
    // 将RestTemplate注册到容器
    @Bean
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

后面可以通过一个配置类将所有使用RestTemplate进行调用的Bean都与@LoadBalanced绑定,进行服务名调用。

5. 创建服务调用控制器

下面采用直接实例化RestTemplate的方式和注入的方式,分别测试调用提供服务的接口:

package com.deepinsea.controller;
​
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
​
/**
 * @author deepinsea
 * @date 2022/5/4
 */
@RestController
@RequestMapping("/consumer-resttemplate")
public class RestTemplateController {
    @Autowired
    private RestTemplate restTemplate;
​
    private static final String url = "http://localhost:9010/provider-nacos/hello";
​
    // 直接实例化RestTemplate的方式调用
    @GetMapping("/test")// 调用者的接口什么类型都可以,一般采用较为安全的post方法,这里测试方便采用GET方法
    public String test() {
        RestTemplate restTemplate = new RestTemplate();
        String result = restTemplate.getForObject(url, String.class);
        return "实例化的方式调用成功,调用结果为:" + result;
    }
​
    // 注入RestTemplate的方式调用
    @GetMapping
    public String testHello(){
        String result = restTemplate.getForObject(url, String.class);
        return "注入的方式调用成功,调用结果为:" + result;
    }
​
}

6. 测试服务调用

点击"▶"启动 service-consumer-openfeign 子模块项目,可以看到项目正常启动起来了:

注意:因为这里服务运行在不同的端口,所以可以同时启动;但如果都运行在8080的两个模块,则需要开启配置项🪒 Allow parallel run ,才不会出现端口冲突。

先启动服务提供者,再启动服务消费者,可以看到两个项目都正常启动了:

image-20220504052757794

查看Nacos控制台,发现两个服务都成功注册到Nacos注册中心了:

image-20220504052855118

使用curl命令或者postman测试:

C:\Users\deepinsea>curl http://localhost:9020/consumer-resttemplate/test
实例化的方式调用成功,调用结果为:hi, this is service-provider-nacos!
C:\Users\deepinsea>curl -X POST -d '' http://localhost:9020/consumer-resttemplate/testHello
注入的方式调用成功,调用结果为:hi, this is service-provider-nacos!
# curl命令中存在-d参数时,-X POST参数可以省略
C:\Users\deepinsea>curl -d '' http://localhost:9020/consumer-resttemplate/testHello
注入的方式调用成功,调用结果为:hi, this is service-provider-nacos!

到此,基于RestTemplate的服务调用已经完成。

7. 服务名称的服务调用

这部分功能实现等同于负载均衡功能

使用服务名称进行调用时,需要利用注册中心将服务名解析为真实的host地址(即DNS功能),因此需要引入负载均衡功能。

在Spring Cloud微服务应用体系中,远程调用都应负载均衡。我们在使用RestTemplate作为远程调用客户端的时候,开启负载均衡极其简单:一个@LoadBalanced注解即可开启

在定义RestTemplate的时候,可以增加 @LoadBalanced 注解实现负载均衡。而在真正调用服务接口的时候,原来host部分是通过手工拼接ip和端口的,直接采用服务名的时候来写请求路径即可。在真正调用的时候,Spring Cloud会将请求拦截下来,然后通过负载均衡器选出节点,并替换服务名部分为具体的ip和端口,从而实现基于服务名的负载均衡调用。

使用服务名称调用时,通常开启@LoadBalanced注解。

添加LoadBalancer依赖

这里需要导入spring-cloud-starter-LoadBalancer依赖,如果不导入的话运行后调用会报错500错误。

需要注意的是nacos自从2020版本之后不再整合的是Netflix,也就没有ribbon了,它之所以报错是因为,你使用了负载均衡算法,但是没有ribbon了,它不知道该使用哪个服务。

        <!-- LoadBalancer负载均衡依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-LoadBalancer</artifactId>
        </dependency>

RestTemplate配置类开启负载均衡

在之前创建的RestTemplateConfig方法中添加@LoadBalanced注解到方法上即可

package com.deepinsea.config;
​
import org.springframework.cloud.client.LoadBalancer.LoadBalanced;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.client.RestTemplate;
​
/**
 * @author deepinsea
 * @date 2022/5/4
 */
@Configuration
public class RestTemplateConfig {
​
    @Bean
    @LoadBalanced // 标注此注解后,RestTemplate就具有了客户端负载均衡能力
    public RestTemplate restTemplate(){
        return new RestTemplate();
    }
}

注意:如果调用时是名称时开启,基于ip+端口时关闭。因为开启@LoadBalanced后,IP+端口的方式调用服务会失效。

解决方案:在注册的时候可以注册两个不同的RestTemplate

 @Bean   
 @LoadBalanced  
 public RestTemplate restTemplate() {      
  return new RestTemplate();   
 }  
​
@Bean    
public RestTemplate commRestTemplate() { 
       RestTemplate restTemplate = new RestTemplate();     
       restTemplate.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));  
       return restTemplate;   
 }

上面的添加了UTF编码设置,防止中文乱码。

调用接口注入方式:

    @Autowired  
    private RestTemplate commRestTemplate;

创建基于名称的服务调用接口

package com.deepinsea.controller;
​
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
​
/**
 * @author deepinsea
 * @date 2022/5/4
 */
@RestController
@RequestMapping("/consumer-resttemplate")
public class RestTemplateLoadBalancedController {
​
    @Autowired
    private RestTemplate restTemplate;
​
    private static final String urlName = "http://service-provider-nacos/provider-nacos/hello";
​
    @PostMapping("/testLoad")
    public String testLoadBalanced(){
        String result = restTemplate.getForObject(urlName, String.class);
        return "服务名称的方式调用成功,调用结果为:" + result;
    }
}

测试服务调用接口

C:\Users\deepinsea>curl -d '' http://localhost:9020/consumer-resttemplate/testLoad
服务名称的方式调用成功,调用结果为:hi, this is service-provider-nacos!

到此,RestTemplate的基于IP+端口和服务名称的服务调用方式以及测试集成完成,还添加了spring cloud LoadBalancer负载均衡依赖。

下一步,集成另一个服务调用客户端框架OpenFeign。

集成OpenFeign客户端

其实在SpringWeb里面,已经原生支持了 RestTemplate。通过RestTemplate去调用微服务,同一个服务提供者的名称可能分布在不同的Controller里,或者一个Controller里有几个不同的微服务提供者,这会较难去管理。接口变动的时候我们可能会修改多处,Spring Cloud 提供OpenFeign来解决这个问题。

Feign 是 Netflix 开发的声明式、模板化的 HTTP客户端, Feign 可以帮助我们更快捷、优雅地调用 HTTP API。

常见的几种远程调用方案

  • RestTemplate + Ribbon 每次发起远程服务调用时,都需要填写远程目标地址,还要配置各种参数,非常麻烦。
  • Feign 是一个轻量级的 Restful HTTP 客户端,内嵌了 Ribbon 作为客户端的负载均衡。面向接口编程,使用时只需要定义一个接口并加上@FeignClient注解,非常方便。
  • OpenFeign 是 Feign 的增强版,是Spring Cloud 2.0以后Feign停止维护的开源版本。对 Feign 进一步封装,支持 Spring MVC 的标准注解和HttpMessageConverts。

为什么使用Feign?

Feign 的首要目标就是减少HTTP 调用的复杂性。在微服务调用的场景中,我们调用很多时候都是基于HTTP协议的服务,如果服务调用只使用提供 HTTP调用服务的 OkHttp框架(e.g. Apache HttpComponnets、HttpURLConnection HTTP Client 等),我们需要关注哪些问题呢?

在这里插入图片描述

常用的Feign客户端实现类,大致如下:

  • Client.Default类默认的 feign.Client 客户端实现类,内部使用HttpURLConnnection 完成HTTP URL请求处理;
  • ApacheHttpClient类:内部使用 Apache httpclient 开源组件完成HTTP URL请求处理的feign.Client 客户端实现类;
  • OkHttpClient类:内部使用 OkHttp3 开源组件完成HTTP URL请求异步线程池化处理的feign.Client 客户端实现类。
  • LoadBalancerFeignClient类:这是一个特殊的 feign.Client 客户端实现类。内部先使用 Ribbon负载均衡算法计算server服务器,然后使用包装的 delegate 客户端实例,去完成 HTTP URL请求处理。

Feign 在启动的时候,有两个与feign.Client 客户端实例相关的自动配置类,根据多种条件组合,去创建不同类型的 客户端Spring IOC容器实例。

1. 添加OpenFeign依赖

在之前创建的服务调用子模块service-consumer-openfeign的pom.xml文件中,添加以下依赖:

新版openfeign框架不再集成LoadBalancer依赖,因此需要手动添加

        <!--需要集成loadbalancer依赖实现服务名称调用 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-loadbalancer</artifactId>
        </dependency>
        <!-- openfeign远程调用依赖 -->
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-openfeign</artifactId>
            <exclusions>
                <!-- 排除commons-io依赖冲突 -->
                <exclusion>
                    <artifactId>commons-io</artifactId>
                    <groupId>commons-io</groupId>
                </exclusion>
            </exclusions>
        </dependency>

注意:这里的nacos-discovery最新版本是是2021.0.1.0,2021.1是和2.2.5.RELEASE等Hoxtonl并列命名的版本(需要手动添加loadbalancer依赖),因此可能会误导我们认为后者是最新版本。

2. 启动类启用OpenFeign客户端

package com.deepinsea;
​
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.client.discovery.EnableDiscoveryClient;
import org.springframework.cloud.openfeign.EnableFeignClients;
​
/**
 * @author deepinsea
 * @date 2022/5/4
 * 服务消费者主启动类
 */
@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class ServiceConsumerOpenFeignApplication {
    public static void main(String[] args) {
        SpringApplication.run(ServiceConsumerOpenFeignApplication.class, args);
    }
}

3. 创建FeignClient服务接口

写一个接口,用来调用指定的微服务(可以由接口提供方写好供调用的SDK接口,我们直接调用)

package com.deepinsea.service;
​
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
​
/**
 * @author deepinsea
 * @date 2022/5/5
 * @FeignClient 声明当前接口为服务调用客户端  
 */
@FeignClient(value = "service-provider-nacos") //调用的服务名
public interface FeignClientService {
​
    @GetMapping("/provider-nacos/hello")
    String hello();
}

@FeignClient参数解析:

  • name:指定FeignClient的名称,如果项目使用了Ribbon,name属性会作为微服务的名称,用于服务发现
  • url: url一般用于调试,可以手动指定@FeignClient调用的地址
  • decode404:当发生http 404错误时,如果该字段位true,会调用decoder进行解码,否则抛出FeignException
  • configuration: Feign配置类,可以自定义Feign的Encoder、Decoder、LogLevel、Contract
  • fallback: 定义容错的处理类,当调用远程接口失败或超时时,会调用对应接口的容错逻辑,fallback指定的类必须实现@FeignClient标记的接口
  • fallbackFactory: 工厂类,用于生成fallback类示例,通过这个属性我们可以实现每个接口通用的容错逻辑,减少重复的代码
  • path: 定义当前FeignClient的统一前缀

4. 创建服务调用Controller

通过spring将服务调用客户端的接口注入控制器,直接调用客户端接口的方法,即可直接调用到服务提供者的接口。

package com.deepinsea.controller;
​
import com.deepinsea.service.FeignClientService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
​
/**
 * @author deepinsea
 * @date 2022/5/5
 * 
 */
@RestController
@RequestMapping("/consumer-openfeign")
public class OpenFeignController {
​
    // 注入OpenFeign服务调用客户端
    @Autowired
    private FeignClientService feignClientService;
​
    // 通过服务调用客户端,调用服务提供者的接口
    @PostMapping("/hello")
    public String test(){
        return feignClientService.hello();
    }
}

5. 测试服务调用

同样, 先启动service-provider-nacos子模块项目,再点击"▶"启动 service-consumer-openfeign 子模块项目:

image-20220505021311629

正常启动,然后查看Nacos控制台:

image-20220505021346557

可以看到服务提供者和服务调用者都成功注册到Nacos上了。

使用curl命令或者postman测试:

C:\Users\deepinsea>curl -d '' http://localhost:9020/consumer-openfeign/hello
hi, this is service-provider-nacos!
# ''/""都可以
C:\Users\deepinsea>curl -d "" http://localhost:9020/consumer-openfeign/hello
hi, this is service-provider-nacos!

测试成功,成功调用服务提供者接口!

OpenFeign日志配置

OpenFeign日志实际上是对请求的监控和输出,我们可以通过配置来设置日志的级别,从而打印出我们需要的日志。

OpenFeign的日志级别有:

  • NONE: 默认的,不显示任何日志。
  • BASIC: 仅记录请求方法、URL、响应状态码以及执行时间。
  • HEADERS:除了BASIC 中自定义的信息外,还有请求和响应的信息头。
  • FULL: 除了HEADERS中定义的信息外, 还有请求和响应的正文以及元数据。

相对而言,Feign客户端提供了较为全面的日志,因此图方便的可以直接使用Feign的日志模板。

1. 添加日志配置类

package com.deepinsea.config;
​
import feign.Logger;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
​
/**
 * @author deepinsea
 * @date 2022/5/5
 * FeignClient日志配置类
 */
@Configuration
public class FeignClientLogConfig {
​
    @Bean
    public Logger.Level feignLogConfiguration(){
         return Logger.Level.FULL;
    }
}

2. 在配置文件中设置监听指定路径下的文件日志

logging:
  level:
    # 可以配置具体到接口或指定包的请求日志,以及日志级别
    #com.deepinsea.service.FeignClientService: debug
    com.deepinsea.service: debug

通过curl命令通过openfeign客户端调用服务提供者的接口:

C:\Users\deepinsea>curl -d "" http://localhost:9020/consumer-openfeign/hello
hi, this is service-provider-nacos!

日志输出如下所示:

image-20220505221827688

注意:这里日志配置存在Feign Date类型时间错误问题,正常时间比错误时间早8个小时,典型的时区错误问题。

同时,通过上面的接口日志可知,openfeign的请求调用方式为nio异步非阻塞的方式。

OpenFeign参数调优

  • 1.openfeign开启日志
  • 2.openfeign开启Gzip压缩
  • 3.openfeign替换JDK默认的URLConnection为okhttp
  • 4.openfeign超时设置
  • 5.openfeign使用sentinel进行熔断、降级处理

因为上面以及开启了日志配置(严格来说日志并没有提升性能),并且因此进行其余四项(最后两项在后面的组件集成中配置)调优配置。

1. 替换为OkHttp调用实现

众所周知,在默认情况下openfeign在进行各个子服务之间的调用时,http组件使用的是jdk的HttpURLConnection,没有使用线程池。有两种可选的线程池:HttpClient和OKHttp,比较推荐OKHttp,采用异步调用,性能极佳,请求封装的也非常简单易用。

添加OKHttp3依赖

使用okhttp,能提高qps,可以针对okhttp连接线程池和超时时间进行调优:

        <!-- feign-okhttp(包含okhttp3依赖) -->
        <dependency>
            <groupId>io.github.openfeign</groupId>
            <artifactId>feign-okhttp</artifactId>
        </dependency>

修改配置文件

feign:
  okhttp:
    enabled: true
  httpclient:
    # 关闭httpclient配置
    enabled: false
#    # 最大连接数
#    max-connections: 1000
#    # 每个url的连接数
#    max-connections-per-route: 100

实现原理(如何根据源码找到okhttp的yml文件配置项?)

首先根据idea自动提示可以找到配置项开启okhttp开关(最快速),其次我们也可以在openFeign的自动配置类中找到okhttp的配置项:

方法一(推荐)External Libraries => org.springframework.cloud.openfeign:spring-cloud-openfeign-core:3.3.1.jar => META-INF => spring-configuration-metadata.json

image-20220521153231990

按Ctrl + F搜索okhttp,就可以看到feign.okhttp开头的okhttp配置了:

image-20220521153559148

其次,也可以从源代码的自动配置类中找到okhttp相关的配置项:

方法二External Libraries => org.springframework.cloud.openfeign:spring-cloud-openfeign-core:3.3.1.jar => org => springframework => cloud => openfeign => FeignAutoConfiguration => OkHttpfeignConfiguration

image-20220521153954579

同样查看OkHttpfeignConfiguration自动配置类的源码,搜索okhttp关键词:

    @ConditionalOnClass({OkHttpClient.class}) //当给定的类位于类路径上,则实例化当前Bean(允许优先加载自定义Bean)
    @ConditionalOnMissingBean({okhttp3.OkHttpClient.class}) //当给定的bean不存在时,则实例化当前Bean
    @ConditionalOnProperty({"feign.okhttp.enabled"}) // 控制配置类是否生效
    protected static class OkHttpFeignConfiguration {
        private okhttp3.OkHttpClient okHttpClient;

可以看到配置类是通过@ConditionalOnProperty注解读取okhttp参数,从而生效配置的。

创建Feign-OkHttp自定义配置类

通过重写构造器的bean方法的方式,来实现自定义配置。

创建OkHttpConfig类,手动注入OkHttpClient对象

package com.deepinsea.config;
​
import feign.Feign;
import okhttp3.ConnectionPool;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.cloud.openfeign.FeignAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
​
import java.util.concurrent.TimeUnit;
​
/**
 * @author deepinsea
 * @date 2022/5/18
 * Feign-OkHttp配置类
 */
@Configuration
@ConditionalOnClass(Feign.class) //判断当前classpath下是否存在指定类,若是则将当前的配置装载入spring容器
@AutoConfigureBefore(FeignAutoConfiguration.class) //在指定的自动配置类前加载
public class FeignOkHttpConfig {
​
    /**
     * 配置okhttp与连接池
     * ConnectionPool默认创建5个线程,保持5分钟长连接
     */
    @Bean
    public okhttp3.OkHttpClient okHttpClient(){
        return new okhttp3.OkHttpClient.Builder()
                //设置连接超时
                .connectTimeout(10, TimeUnit.SECONDS)
                //设置读超时
                .readTimeout(10, TimeUnit.SECONDS)
                //设置写超时
                .writeTimeout(10,TimeUnit.SECONDS)
                //是否自动重连
                .retryOnConnectionFailure(true)
                .connectionPool(new ConnectionPool(10 , 5L, TimeUnit.MINUTES))
                // 添加自定义日志拦截器
//                .addInterceptor(new OkHttpLogInterceptor())
                //构建OkHttpClient对象
                .build();
    }
}

注意:@ConditionalOnClass(Feign.class) 与@AutoConfigureBefore(FeignAutoConfiguration.class) 其实可以不用加,因为源码中设置了优先加载自定义配置类;我们这里可以注释掉这两个注解,同样可以生效自定义配置。

添加这两个注解的原因是:①兼容okhttp的高低版本;②保证自定义配置优先加载的稳定性。

    @ConditionalOnClass({OkHttpClient.class}) //当给定的类位于类路径上,则实例化当前Bean(确保了实现了OkHttpClient的客户端依赖存在于类加载路径上{import feign.okhttp.OkHttpClient;} -- 也就是引入了feign-okhttp依赖才生效)
    @ConditionalOnMissingBean({okhttp3.OkHttpClient.class}) //当给定的bean不存在时,则实例化当前Bean(也就是说不管有没有这个bean,都会实例化这个bean,且只实例化一次 -- Bean唯一性校验)
    @ConditionalOnProperty({"feign.okhttp.enabled"}) // 控制配置类是否生效
    protected static class OkHttpFeignConfiguration {
        private okhttp3.OkHttpClient okHttpClient;

由@ConditionalOnMissingBean注解可知:只有当 okhttp3.OkHttpClient 这个 Bean 不存在时,才会启用 OkHttpFeignConfiguration。

然而我们在配置中需要添加日志interceptor必然会重写构造方法,手动创建这个 Bean,因此我们需要手动添加其他的配置。

另外,也可以知道:如果想要自己实现feign的okhttp客户端框架,那么首先需要有外部OkHttpClient的对象(并且需要实现Client接口),然后需要有okhttp3的依赖。满足这两点,就可以自定义feign的okhttp对象框架了

实现原理(如何根据源码自定义okhttp配置类并生效?)

方法一:直接查看兼容feign的feign-okhttp源码包

image-20220522155729005

可以发现,整个源码包只有一个OkHttpClient文件,说明feign-okhttp依赖只做了一件事:构建okhttp3.OkHttpClient对象

image-20220522160613855

可以发现,feign-okhttp的实现方式虽然与openfeign不相同,但是实现思想是相同的:通过建造者模式(建造者模式:使用多个简单的对象一步一步构建一个复杂的对象,区别与工厂模式,建造者模式更加关注与零件装配的顺序)创建OkHttp客户端对象。

然后,我们就知道了:重新构建这个OkHttpClient对象的构造方法,就是我们需要自定义的配置方法。

怎么构建这个OkHttpClient对象呢?

其实很简单,将构造方法拷贝一份到自己的自定义配置类中,然后再修改参数和添加插件方法。

那么,怎么查看构造方法有哪些参数和方法可以修改呢?

直接点开OkHttpClient中构建的okhttp3.OkHttpClient对象,就可以看到完整的参数了:

public class OkHttpClient implements Cloneable, Factory, okhttp3.WebSocket.Factory {
    static final List<Protocol> DEFAULT_PROTOCOLS;
    static final List<ConnectionSpec> DEFAULT_CONNECTION_SPECS;
    final Dispatcher dispatcher;
    @Nullable
    final Proxy proxy;
    final List<Protocol> protocols;
    final List<ConnectionSpec> connectionSpecs;
    final List<Interceptor> interceptors;
    final List<Interceptor> networkInterceptors;
    final okhttp3.EventListener.Factory eventListenerFactory;
    final ProxySelector proxySelector;
    final CookieJar cookieJar;
    @Nullable
    final Cache cache;
    @Nullable
    final InternalCache internalCache;
    final SocketFactory socketFactory;
    final SSLSocketFactory sslSocketFactory;
    final CertificateChainCleaner certificateChainCleaner;
    final HostnameVerifier hostnameVerifier;
    final CertificatePinner certificatePinner;
    final Authenticator proxyAuthenticator;
    final Authenticator authenticator;
    final ConnectionPool connectionPool;
    final Dns dns;
    final boolean followSslRedirects;
    final boolean followRedirects;
    final boolean retryOnConnectionFailure;
    final int callTimeout;
    final int connectTimeout;
    final int readTimeout;
    final int writeTimeout;
    final int pingInterval;

需要配置的参数都在这里,然后也有默认的构造方法(默认参数在默认的构造方法中),参考修改即可。另外,根据官网的介绍,请求日志的实现是拦截器,通过实现okhttp拦截器接口的日志拦截器插件可以注册到构造方法中,从而实现日志的拦截。

方法二:查看spring-cloud-openfeign-core源码包

image-20220524154005608

可以看到有两个自动配置文件,实现方法都是一样的,不过下面的FeignAutoConfiguration多了控制feign-okhttp依赖存在与yml中开启了okhttp配置的验证功能。

我们直接查看上面的OkHttpFeignConfiguration自动配置类:

image-20220524160447134

同样可以看到,整个自动配置类就是做一件事:构建OkHttpClient(这里的是okhttp3依赖下的OkHttpClient)对象

然后我们可以看到OkHttpClient的核心构建方法的一些设计:

  • 采用配置类(FeignHttpClientProperties)存储默认配置,一些的默认参数在配置类中,我们修改的时候去找就行了;
  • 构建的实体类对象(OkHttpClient)包含所有的参数,而配置类中提供的配置并不是所有的配置,所以这就是我们可以自定义配置参数和建造者模式的方法插件的基础;
  • 并且可以看到这个对象的构建采用了建造者模式(一步步建造方法,因此方法和参数可热插拔)构建的对象;
  • 采用了工厂模式(OkHttpClientFactory工厂,位于org.springframework.cloud.commons包下),并且OkHttpClientFactory工厂中采用建造者模式一层层构建对象;
  • 采用了连接池(ConnectionPool),采用了NIO设计思想的RealConnectionPool,通过封装ThreadPoolExecutor线程池来实现;
  • 采用枚举类(Duration)存储固定参数,修改配置时直接指定就行了。

这些都是从OkHttpClient的构建方法中可以了解到的信息,那么哪些对我们是有实际用处的呢?

很明显,默认的一些参数直接从FeignHttpClientProperties中查看即可,其他需要添加的参数和方法从具有完整配置的OkHttpClient实体类中查看即可。最后,通过建造者模式构建到OkHttpClient对象中。

定义插件:创建OkHttpLogInterceptor日志拦截文件

根据官网的描述,实现日志拦截器,需要实现okhttp3.OkHttpClient对象中的Interceptor参数接口。根据OkHttpClient的构建原理可知,日志拦截器也是需要作为一种参数进行构建的,因此我们只需要将自定义的日志拦截器方法构建方法即可。

之前配置的Feign客户端日志是生效的,这里是根据个人需要自定义OkHttp日志拦截器,可以选择相对简洁的日志打印方式

package com.deepinsea.config.interceptor;
​
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
​
import java.io.IOException;
​
/**
 * Created by deepinsea on 2022/5/18.
 */
public class OkHttpLogInterceptor implements Interceptor {
    Logger logger = LoggerFactory.getLogger(OkHttpLogInterceptor.class);
    
    // 重写okhttp3的拦截器
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Response response = chain.proceed(request);
        logger.info("okhttp 发送请求, method: {}, url: {}", request.method(), request.url());
        logger.info("okhttp request body: {}", request.body());
        logger.info("okhttp 接收响应, response body: {}", response.peekBody(1024 * 1024).string());
//        System.out.println(response.headers().names());
        // [Connection, Content-Length, Content-Type, Date, Keep-Alive]
        logger.info("<--- Start HTTP Headers");
        logger.info("Connection: {}", response.headers().get("Connection"));
        logger.info("Content-Length: {}", response.headers().get("Content-Length"));
        logger.info("Content-Type: {}", response.headers().get("Content-Type"));
        logger.info("Date: {}", response.headers().getDate("Date"));
        logger.info("Keep-Alive: {}", response.headers().get("Keep-Alive"));
        logger.info("<--- END HTTP Headers");
//        logger.info("okhttp response headers: \n{}", response.headers().getInstant("Date"));
        return response;
    }
}

参考:添加日志拦截器插件并手动注入加载日志插件的构造器bean(官方日志模板)

package com.deepinsea.config.interceptor;
​
import okhttp3.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
​
import java.io.IOException;
​
/**
 * Created by deepinsea on 2022/5/18.
 */
public class OkHttpLogInterceptor implements Interceptor {
    Logger logger = LoggerFactory.getLogger(OkHttpLogInterceptor.class);
    private OkHttpClient okHttpClient;
​
    @Override
    public Response intercept(Interceptor.Chain chain) throws IOException {
        //这个chain里面包含了request和response,所以你要什么都可以从这里拿
        Request request = chain.request();
        long t1 = System.nanoTime();//请求发起的时间
        logger.info(String.format("发送请求 %s on %s%n%s",
                                  request.url(), chain.connection(), request.headers()));
        Response response = chain.proceed(request);
        long t2 = System.nanoTime();//收到响应的时间
        //这里不能直接使用response.body().string()的方式输出日志
        //因为response.body().string()之后,response中的流会被关闭,程序会报错,我们需要创建出一
        //个新的response给应用层处理
        ResponseBody responseBody = response.peekBody(1024 * 1024);
        logger.info(String.format("接收响应: [%s] %n返回json:【%s】 %.1fms%n%s",
                                  response.request().url(),
                                  responseBody.string(),
                                  (t2 - t1) / 1e6d,
                                  response.headers()));
        return response;
​
        @Bean
        public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory,
                                           ConnectionPool connectionPool,
                                           FeignHttpClientProperties httpClientProperties) {
            Boolean followRedirects = httpClientProperties.isFollowRedirects();
            Integer connectTimeout = httpClientProperties.getConnectionTimeout();
            Boolean disableSslValidation = httpClientProperties.isDisableSslValidation();
            this.okHttpClient = httpClientFactory.createBuilder(disableSslValidation)
                .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
                .followRedirects(followRedirects).connectionPool(connectionPool)
                //这里设置我们自定义的拦截器插件
                .addInterceptor(new OkHttpLogInterceptor())
                .build();
            return this.okHttpClient;
        }
    }

注入插件对象:即上面自定义配置中重写的构造器添加.addInterceptor(new OkHttpLogInterceptor())方法

添加自定义日志拦截器到自定义配置中

    @Bean
    public okhttp3.OkHttpClient okHttpClient(){
        return new okhttp3.OkHttpClient.Builder()
                //设置连接超时
                .connectTimeout(10, TimeUnit.SECONDS)
                //设置读超时
                .readTimeout(10, TimeUnit.SECONDS)
                //设置写超时
                .writeTimeout(10,TimeUnit.SECONDS)
                //是否自动重连
                .retryOnConnectionFailure(true)
                .connectionPool(new ConnectionPool(10 , 5L, TimeUnit.MINUTES))
                // 添加自定义日志拦截器
                .addInterceptor(new OkHttpLogInterceptor())
                //构建OkHttpClient对象
                .build();
    }

我们使用okhttp的日志拦截器后,可以关闭feign的日志配置,两者功能重叠了

 @Bean
    public Logger.Level feignLogConfiguration(){
         return Logger.Level.NONE; // 设置日志级别为NONE,从而关闭openfeign的日志
    }

我们来解析下为什么能实现自定义okhttp的配置,也就是说怎么做才能使okhttp配置生效?

首先,我们需要找到okhttp的源配置类OkHttpFeignConfiguration,查看源码,发现配置文件只做了一件事——初始化OkHttpClient类,因此我们知道了自定义okhttp的配置就是重写okhttp实例化方法。 查看OkHttpClient的实例化方法,不难看出它们的默认参数值都是从FeignHttpClientProperties配置文件中获取的,最后通过建造者模式的Builder建造器将配置类中的参数构建为OkHttpClient对象即可。其他可添加的参数和插件方法,我们可以直接查看okhttp3.OkHttpClient对象来获取;添加到构建方法中,最后都会被作为插件建造为okhttp3.OkHttpClient对象。

到此,成功集成OkHttp线程池到OpenFeign中进行请求。

2. 开启Feign请求响应Gzip压缩

开启压缩可以有效节约网络资源,但是会增加CPU压力,建议把最小压缩的文档大小适度调大一点。

在上面的基础上添加以下代码

feign:
  okhttp:
    enabled: true
  httpclient:
    enabled: false
    # 最大连接数
    max-connections: 1000
    # 每个url的连接数
    max-connections-per-route: 100
  compression:
    request:
      # 开启feign请求GZip压缩
      enabled: true
      # 配置压缩文档类型及最小压缩的文档大小
      mime-types: text/xml,application/xml,application/json
      min-request-size: 2048
    # 开启feign响应压缩
    response:
      enabled: true

点击"▶"启动两个项目,再次调用服务提供接口:

C:\Users\deepinsea>curl -d "" http://localhost:9020/consumer-openfeign/hello
hi, this is service-provider-nacos!

可以看到以及成功使用GZip压缩了请求:

image-20220505232055507

其他

一些openfeign可能存在的bug的解决方案:

    //处理feign远程调用报错:No qualifying bean of type ‘org.springframework.boot.autoconfigure.http.HttpMessage
    @Bean
    @ConditionalOnMissingBean
    public HttpMessageConverters messageConverters(ObjectProvider<HttpMessageConverter<?>> converters) {
        return new HttpMessageConverters(converters.orderedStream().collect(Collectors.toList()));
    }

到此,openfeign的集成和调优就到此告一段落了,下面是集成nacos注册中心的功能!

欢迎点赞,谢谢大佬ヾ(◍°∇°◍)ノ゙