背景
最近在做微服务的集成,为了解决各服务间的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的几个优化点列举完毕,感兴趣的小伙伴也可以自己尝试一下。