OpenFeign的集成与优化

1,888 阅读4分钟

持续创作,加速成长!这是我参与「掘金日新计划 · 10 月更文挑战」的第5天,点击查看活动详情

背景

最近在做微服务的集成,为了解决各服务间的rpc调用问题,使用到了openFeign,虽然是简单地在springboot项目中集成openFeign,但是里面其实还是有很多需要注意的点,下面就依次列举出来。

依赖

首先我们的springboot版本是2.7.3,spring cloud版本是2021.0.4,下面我们引入openFeign的依赖:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
    <version>3.1.4</version>
</dependency>
<!-- openFeign负载均衡由ribbon改为loadbalancer-->
<!-- 不加这个依赖会报错:No Feign Client for loadBalancing defined. Did you forget to include spring-cloud-starter-loadbalancer? -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-loadbalancer</artifactId>
    <version>3.1.4</version>
</dependency>

使用openFeign

  • 依赖引入后,我们需要使用openFeign的功能,首先需要在启动类上添加@EnableFeignClients注解;

  • 其次,我们需要开始编写FeignClient:

    // api接口,提供给客户端和服务端共用
    public interface WalletApi {
      /**
       * 扣钱
       *
       * @param amount
       * @return
       */
      @PostMapping("/dedutMoney")
      Boolean deductMoney(@RequestParam("amount") long amount);
    }
    ​
    // account代表目标服务名称,需要从注册中心找
    // contextId作为注入到ioc容器中bean的名称,要保持唯一性
    // path对应服务端的@RequestMapping中的内容
    @FeignClient(name = "account", contextId = "walletApi", path = "/wallet")
    public interface WalletApiClient extends WalletApi {
    }
    ​
    // 服务端代码
    @Slf4j
    @RestController
    @RequestMapping("/wallet")
    public class WalletController implements WalletApi {
    ​
      @Override
      public Boolean deductMoney(long amount) {
      }
    }
    

    上面的代码把客户端和服务端抽象出了公共的一个接口,这样的话,我们就可以避免两边都要写requestMapping这一套,也无形之中让生产者和消费者直接达成了共识,避免很多无效沟通。

至此,openFeign的集成就算是告一段落了,项目中就可以正常使用了。下面我们继续openFeign的优化:

链接池

默认的情况下,openFeign使用的上是HttpURLConnection发起请求,具体代码可查看feign.Client.Default类实现,也就是说,openFeign每次需要创建一个新的请求,而不是使用的链接池,所以我们的需要替换掉这个默认的实现,改用一个有链接池的实现。

  • 添加依赖

    我们打算把HttpURLConnection实现替换成okhttp的实现

    <!-- 替换默认的HttpURLConnection,改为okhttp,并添加链接池-->
    <dependency>
        <groupId>io.github.openfeign</groupId>
        <artifactId>feign-okhttp</artifactId>
        <version>11.9.1</version>
    </dependency>
    
  • 通过javaConfig的方式把okhttp的实现引入进来

    import feign.Feign;
    import feign.Logger;
    import feign.okhttp.OkHttpClient;
    import lombok.Getter;
    import lombok.Setter;
    import org.springframework.boot.autoconfigure.AutoConfigureBefore;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
    import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
    import org.springframework.boot.context.properties.ConfigurationProperties;
    import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory;
    import org.springframework.cloud.commons.httpclient.OkHttpClientFactory;
    import org.springframework.cloud.openfeign.FeignAutoConfiguration;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    ​
    import javax.annotation.PreDestroy;
    import java.util.concurrent.TimeUnit;
    ​
    /**
     * @author zouwei
     * @className FeignConfig
     * @date: 2022/9/18 19:12
     * @description:
     */
    @Configuration
    @ConditionalOnClass(Feign.class)
    @AutoConfigureBefore(FeignAutoConfiguration.class)
    public class FeignConfig {
    ​
      @Getter
      @Setter
      @Configuration
      @ConfigurationProperties(prefix = "feign.okhttp")
      @ConditionalOnProperty(name = "feign.okhttp.enabled", havingValue = "true")
      protected static class OkHttpProperties {
        boolean followRedirects = true;
        // 链接超时时间,单位毫秒
        int connectTimeout = 5000;
        boolean disableSslValidation = false;
        // 读超时,单位毫秒
        int readTimeout = 5000;
        // 写超时,单位毫秒
        int writeTimeout = 5000;
        // 是否自动重连
        boolean retryOnConnectionFailure = true;
        // 最大空闲链接
        int maxIdleConnections = 10;
        // 默认保持5分钟
        long keepAliveDuration = 1000 * 60 * 5L;
      }
    ​
      /**
       * 配置okhttp以及对应的链接池
       */
      @Configuration(
          proxyBeanMethods = false
      )
      @ConditionalOnClass({OkHttpClient.class})
      @ConditionalOnMissingBean({okhttp3.OkHttpClient.class})
      @ConditionalOnProperty({"feign.okhttp.enabled"})
      protected static class OkHttpFeignConfiguration {
        private okhttp3.OkHttpClient okHttpClient;
    ​
        protected OkHttpFeignConfiguration() {
        }
    ​
        @Bean
        public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory, OkHttpProperties properties, OkHttpClientConnectionPoolFactory connectionPoolFactory) {
          this.okHttpClient = httpClientFactory.createBuilder(properties.isDisableSslValidation())
              // 链接超时时间
              .connectTimeout(properties.getConnectTimeout(), TimeUnit.MILLISECONDS)
              // 是否禁用重定向
              .followRedirects(properties.isFollowRedirects())
              //设置读超时
              .readTimeout(properties.getReadTimeout(), TimeUnit.MILLISECONDS)
              //设置写超时
              .writeTimeout(properties.getWriteTimeout(), TimeUnit.MILLISECONDS)
              // 链接失败是否重试
              .retryOnConnectionFailure(properties.isRetryOnConnectionFailure())
              //链接池
              .connectionPool(connectionPoolFactory.create(properties.getMaxIdleConnections(), properties.getKeepAliveDuration(), TimeUnit.MILLISECONDS))
              .build();
          return this.okHttpClient;
        }
    ​
        @PreDestroy
        public void destroy() {
          if (this.okHttpClient != null) {
            this.okHttpClient.dispatcher().executorService().shutdown();
            this.okHttpClient.connectionPool().evictAll();
          }
        }
      }
    }
    
  • 更改配置

    feign:
      # 不使用httpclient,改用okhttp
      httpclient:
        enabled: false
      okhttp:
        enabled: true
        # 是否禁用重定向
        follow-redirects: true
        connect-timeout: 5000
        # 链接失败是否重试
        retry-on-connection-failure: true
        read-timeout: 5000
        write-timeout: 5000
        # 最大空闲数量
        max-idle-connections: 5
        # 生存时间
        keep-alive-duration: 15000
    

这样我们就把openFeign的请求发送改造成链接池了,避免了每次请求都创建HttpURLConnection对象;

开启请求压缩功能

为了更好地减少请求发送的时间,我们可以针对请求数据进行压缩处理,openFeign也内置了压缩功能,不过需要我们自己开启:

feign:
  # 开启压缩功能
  compression:
    request:
      enabled: true
      mime-types: text/xml,application/xml,application/json
      min-request-size: 2048
    response:
      enabled: true

配置超时时间

我们还可以给指定的FeignClient指定对应的超时时间,因为并不是所有的服务超时时间都是统一的,有些特殊的业务场景需要针对性地设置超时时间:


feign:
  client:
    config:
      # 设置超时,囊括了okhttp的超时,okhttp属于真正执行的超时,openFeign属于服务间的超时
      # 设置全局超时时间
      default:
        connectTimeout: 2000
        readTimeout: 5000
      # 针对特定contextId设置超时时间
      walletApi:
        connectTimeout: 1000
        readTimeout: 2000

添加LoadBalancerCacheManager

在项目启动过程中,会出现警告:LoadBalancerCacheManager not available, returning delegate without caching.,说明LoadBalancerCacheManager没有开启,我们需要加入以下依赖并开启缓存:

<!-- 解决项目启动警告:LoadBalancerCacheManager not available, returning delegate without caching.-->
<!-- 如果注册中心有自己的缓存,那么就可以禁用loadbalancer的缓存-->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
    <version>3.1.1</version>
</dependency>
<!-- 解决项目启动警告:Spring Cloud LoadBalancer is currently working with the default cache. You can switch to using Caffeine cache, by adding it and org.springframework.cache.caffeine.CaffeineCacheManager to the classpath.-->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context-support</artifactId>
</dependency>

另外,我们还需要通过配置开启缓存:

spring:
  cloud:
    loadbalancer:
      cache:
        # 开启缓存,如果注册中心有自己的缓存,那么就可以禁用loadbalancer的缓存
        enabled: true
        # 过期时间10s
        ttl: 10
        # 容量256M
        capacity: 256
        caffeine:
          #          initialCapacity=[integer]: sets Caffeine.initialCapacity.
          #          maximumSize=[long]: sets Caffeine.maximumSize.
          #          maximumWeight=[long]: sets Caffeine.maximumWeight.
          #          expireAfterAccess=[duration]: sets Caffeine.expireAfterAccess(long, java.util.concurrent.TimeUnit).
          #          expireAfterWrite=[duration]: sets Caffeine.expireAfterWrite(long, java.util.concurrent.TimeUnit).
          #          refreshAfterWrite=[duration]: sets Caffeine.refreshAfterWrite(long, java.util.concurrent.TimeUnit).
          #          weakKeys: sets Caffeine.weakKeys().
          #          weakValues: sets Caffeine.weakValues().
          #          softValues: sets Caffeine.softValues().
          #          recordStats: sets Caffeine.recordStats().
          #         initialCapacity初始化键值对的数量
          spec: initialCapacity=500,expireAfterWrite=5s

至此,我们基本上已经把OpenFeign的几个优化点列举完毕,感兴趣的小伙伴也可以自己尝试一下。