Feign 使用 okhttp3 的正确姿势

本贴最后更新于 1077 天前,其中的信息可能已经时移世易

首先来吐槽一波(╯‵□′)╯︵┻━┻

本来呢,我是想加一层 feign 的 interceptor 处理 feign 里 request 请求的返回信息的,比如只提取 ResponseBody 中的 data 啥的。后来想起来 feign 默认使用的是 jdk 的 HttpURLConnection,而且 feign 本身是支持替换 okhttp 的,于是打算搞起~

可是,百毒到的都是什么鬼啊,按照别人写的文档配好,各种问题,什么 springboot 注解不行啦,需要使用 feign 默认注解,什么负载均衡失效啦,无语。(╯‵□′)╯︵┻━┻

最后,还是自己操刀,从源码看吧。

遂有此文~

百毒,淦!

首先来写一个简单的 interceptor

不需要这个的跳过看下一段吧~

//这个interceptor的目的是提取返回body中的data,并转化为json
@Component
public class OkHttpResponseInterceptor implements Interceptor {
    @Override
    public Response intercept(Chain chain) throws IOException {
        Request request = chain.request();
        Response response = chain.proceed(request);

        if (HttpHeaders.hasBody(response)) {
            if (response.code() == 200) {
                ResponseBody responseBody = response.body();
                if (responseBody != null
                        && responseBody.contentLength() != 0
                        && Objects.requireNonNull(responseBody.contentType()).type()
                        .equals(MediaType.APPLICATION_JSON_VALUE)) {
                    String str = responseBody.string();
                    JSONObject json = JSONObject.parseObject(JSON.toJSON(str));
                    String data = json.getString("data");
                    if (StringUtils.isNotBlank(data)) {
                        ResponseBody body = ResponseBody.create(okhttp3.MediaType.get(MediaType.APPLICATION_JSON_VALUE), data);
                        return response.newBuilder().body(body).build();
                    }
                }
            }
        }
        return response;
    }
}

嗯,这样,interceptor 就定义好了,接下来就是配置 feign 了~

配置 Feign

关于 maven 依赖修改啥的我就不说了,如果使用的 spring-cloud-openfeign-dependencies,应该会包含 okhttp 的依赖。

  1. 修改 application.yml

    feign:
      hystrix:
        enabled: true
      okhttp:
        enabled: true
      httpclient:
        connectionTimeout: 30000
      client:
        config:
          default:
            readTimeout: 30000
    
  2. 添加 FeignOkHttpConfig.java

    @Configuration
    @ConditionalOnClass(Feign.class)
    @AutoConfigureBefore(FeignLoadBalancerAutoConfiguration.class)
    public class FeignOkHttpConfig {
    
        @Autowired
        private OkHttpResponseInterceptor okHttpResponseInterceptor;
    
        private okhttp3.OkHttpClient okHttpClient;
    
        @Bean
        @ConditionalOnMissingBean(ConnectionPool.class)
        public ConnectionPool httpClientConnectionPool(
                FeignHttpClientProperties httpClientProperties,
                OkHttpClientConnectionPoolFactory connectionPoolFactory) {
            Integer maxTotalConnections = httpClientProperties.getMaxConnections();
            Long timeToLive = httpClientProperties.getTimeToLive();
            TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
            return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
        }
    
        @Bean
        @ConditionalOnMissingBean(okhttp3.OkHttpClient.class)
        public okhttp3.OkHttpClient okHttpClient(OkHttpClientFactory httpClientFactory,
                                                 ConnectionPool connectionPool,
                                                 FeignClientProperties feignClientProperties,
                                                 FeignHttpClientProperties feignHttpClientProperties) {
            FeignClientProperties.FeignClientConfiguration defaultConfig = feignClientProperties.getConfig().get("default");
            int connectTimeout = feignHttpClientProperties.getConnectionTimeout();
            int readTimeout = defaultConfig.getReadTimeout();
            boolean disableSslValidation = feignHttpClientProperties.isDisableSslValidation();
            boolean followRedirects = feignHttpClientProperties.isFollowRedirects();
            this.okHttpClient = httpClientFactory.createBuilder(disableSslValidation)
                    .readTimeout(readTimeout, TimeUnit.MILLISECONDS)
                    .connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
                    .followRedirects(followRedirects)
                    .connectionPool(connectionPool)
                    .addInterceptor(okHttpResponseInterceptor)
                    .build();
            return this.okHttpClient;
        }
    
        @PreDestroy
        public void destroy() {
            if (this.okHttpClient != null) {
                this.okHttpClient.dispatcher().executorService().shutdown();
                this.okHttpClient.connectionPool().evictAll();
            }
        }
    }
    

好的~ 这样基本上就配置完了。下面我来具体解释一下,这么配置就能生效的原因。(如果不需要负载均衡,可以参考百毒到的配置方案,那个应该也是 ok 的)

解析

  1. 什么条件下配置的 okhttp 才会生效

    首先看 org.springframework.cloud.openfeign.FeignAutoConfiguration

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(OkHttpClient.class)
    @ConditionalOnMissingClass("com.netflix.loadbalancer.ILoadBalancer")
    @ConditionalOnMissingBean(okhttp3.OkHttpClient.class)
    @ConditionalOnProperty("feign.okhttp.enabled")
    protected static class OkHttpFeignConfiguration {
    ...
    }
    

    只有当 okhttp3.OkHttpClient 这个 Bean 不存在时,才会启用 OkHttpFeignConfiguration。

    然而我们在配置中需要修改 interceptor 必然会手动创建这个 Bean,因此我们需要手动添加其他的配置。

    但是,先不要急,因为如果使用负载均衡,这个类还不是关键。

  2. feign 负载均衡 FeignLoadBalancerAutoConfiguration

    @ConditionalOnClass(Feign.class)
    @ConditionalOnBean(BlockingLoadBalancerClient.class)
    @AutoConfigureBefore(FeignAutoConfiguration.class)
    @AutoConfigureAfter(FeignRibbonClientAutoConfiguration.class)
    @EnableConfigurationProperties(FeignHttpClientProperties.class)
    @Configuration(proxyBeanMethods = false)
    // Order is important here, last should be the default, first should be optional
    // see
    // https://github.com/spring-cloud/spring-cloud-netflix/issues/2086#issuecomment-316281653
    @Import({ HttpClientFeignLoadBalancerConfiguration.class,
    		OkHttpFeignLoadBalancerConfiguration.class,
    		DefaultFeignLoadBalancerConfiguration.class })
    public class FeignLoadBalancerAutoConfiguration {
    
    }
    

    不难发现,这个配置加载是在 FeignAutoConfiguration 之前的,因此,这个类对于我们而言更为关键。

    @Import({ HttpClientFeignLoadBalancerConfiguration.class,
    		OkHttpFeignLoadBalancerConfiguration.class,
    		DefaultFeignLoadBalancerConfiguration.class })
    

    通过 Import 注解的信息,我们得知需要查看 OkHttpFeignLoadBalancerConfiguration。

  3. 根据 OkHttpFeignLoadBalancerConfiguration

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnClass(OkHttpClient.class)
    @ConditionalOnProperty("feign.okhttp.enabled")
    @ConditionalOnBean(BlockingLoadBalancerClient.class)
    @Import(OkHttpFeignConfiguration.class)
    class OkHttpFeignLoadBalancerConfiguration {
    
    	@Bean
    	@ConditionalOnMissingBean
    	public Client feignClient(okhttp3.OkHttpClient okHttpClient,
    			BlockingLoadBalancerClient loadBalancerClient) {
    		OkHttpClient delegate = new OkHttpClient(okHttpClient);
    		return new FeignBlockingLoadBalancerClient(delegate, loadBalancerClient);
    	}
    
    }
    

    可以看出,这个配置类只生成了 Client 这个 Bean,对于 FeignAutoConfiguration 中需要的剩下的 Bean 显然是不够的,因此,剩下的内容应该都在

    @Import(OkHttpFeignConfiguration.class)
    

    这个 Import 的配置中,那么我们看看这个配置到底做了啥。

  4. 最后一步了~

    @Configuration(proxyBeanMethods = false)
    @ConditionalOnMissingBean(okhttp3.OkHttpClient.class)
    public class OkHttpFeignConfiguration {
    
    	private okhttp3.OkHttpClient okHttpClient;
    
    	@Bean
    	@ConditionalOnMissingBean(ConnectionPool.class)
    	public ConnectionPool httpClientConnectionPool(
    			FeignHttpClientProperties httpClientProperties,
    			OkHttpClientConnectionPoolFactory connectionPoolFactory) {
    		Integer maxTotalConnections = httpClientProperties.getMaxConnections();
    		Long timeToLive = httpClientProperties.getTimeToLive();
    		TimeUnit ttlUnit = httpClientProperties.getTimeToLiveUnit();
    		return connectionPoolFactory.create(maxTotalConnections, timeToLive, ttlUnit);
    	}
    
    	@Bean
    	public okhttp3.OkHttpClient client(OkHttpClientFactory httpClientFactory,
    			ConnectionPool connectionPool,
    			FeignHttpClientProperties httpClientProperties) {
    		Boolean followRedirects = httpClientProperties.isFollowRedirects();
    		Integer connectTimeout = httpClientProperties.getConnectionTimeout();
    		this.okHttpClient = httpClientFactory
    				.createBuilder(httpClientProperties.isDisableSslValidation())
    				.connectTimeout(connectTimeout, TimeUnit.MILLISECONDS)
    				.followRedirects(followRedirects).connectionPool(connectionPool).build();
    		return this.okHttpClient;
    	}
    
    	@PreDestroy
    	public void destroy() {
    		if (this.okHttpClient != null) {
    			this.okHttpClient.dispatcher().executorService().shutdown();
    			this.okHttpClient.connectionPool().evictAll();
    		}
    	}
    }
    

    很显然,这个类才是我们需要替换的,而且这个配置类的加载条件很简单

    @ConditionalOnMissingBean(okhttp3.OkHttpClient.class)
    

    我们只要自己创建这个 Bean 就可以了。

    那么,我们要做的就是在 FeignLoadBalancerAutoConfiguration 配置类加载之前,生成这个 Bean,并根据 OkHttpFeignConfiguration 生成其他需要的 Bean 就可以了。具体参考 配置 Feign 一节。

  • Feign
    8 引用
  • OkHttp

    OkHttp 是一款 HTTP & HTTP/2 客户端库,专为 Android 和 Java 应用打造。

    16 引用 • 6 回帖 • 53 关注
  • Java

    Java 是一种可以撰写跨平台应用软件的面向对象的程序设计语言,是由 Sun Microsystems 公司于 1995 年 5 月推出的。Java 技术具有卓越的通用性、高效性、平台移植性和安全性。

    3164 引用 • 8206 回帖
  • Spring

    Spring 是一个开源框架,是于 2003 年兴起的一个轻量级的 Java 开发框架,由 Rod Johnson 在其著作《Expert One-On-One J2EE Development and Design》中阐述的部分理念和原型衍生而来。它是为了解决企业应用开发的复杂性而创建的。框架的主要优势之一就是其分层架构,分层架构允许使用者选择使用哪一个组件,同时为 JavaEE 应用程序开发提供集成的框架。

    938 引用 • 1456 回帖 • 171 关注

相关帖子

欢迎来到这里!

我们正在构建一个小众社区,大家在这里相互信任,以平等 • 自由 • 奔放的价值观进行分享交流。最终,希望大家能够找到与自己志同道合的伙伴,共同成长。

注册 关于
请输入回帖内容 ...