接上文SpringCloud OpenFegin 底层是如何切换HttpClient 说起,今天玩点实际的应用,SpringCloud 全局 Fegin 日志收集。
由于通过编写 yaml配置的方式无法实现HttpClient 全局日志收集的需求,因为他不能配置全局的拦截器。因此我们需要自定义配置
feign:
compression:
response:
enabled: true
httpclient:
connection-timeout: 777
time-to-live: 666
enabled: true
上文已经说过,当我们的项目中引入了HttpClient 的 maven 依赖后,SpringCloud 默认会加载HttpClient 的配置类,如下图。如果项目中自定义了HttpClient,那么优先加载自定义配置,反之加载Cloud 默认的HttpClient 类。
这里温习一下 LoadBalancerFeignClient 类的作用:Cloud 发起网络请求的入口,真正干活的是里面的 delegate。
知道了原理后,我们直接在common 模块中加上自定义的HttpClient 的配置就好了,代码如下,里面需要注意的是,打印日志的时候我们需要对InputStream的副本进行操作,不然后续的业务接受方,将结果序列化成 json 返回给前端,这个过程发现IO流关闭了,就会报IO流关闭的错误。
package com.xxy.system.common.config;
import com.xxy.system.common.interceptor.OkHttpCLientInterceptor;
import lombok.extern.slf4j.Slf4j;
import okhttp3.ConnectionPool;
import okhttp3.OkHttpClient;
import org.apache.http.*;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpRequestWrapper;
import org.apache.http.entity.BasicHttpEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.protocol.HttpContext;
import org.springframework.beans.BeansException;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.cloud.commons.httpclient.OkHttpClientConnectionPoolFactory;
import org.springframework.cloud.commons.httpclient.OkHttpClientFactory;
import org.springframework.cloud.openfeign.support.FeignHttpClientProperties;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.util.StreamUtils;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URI;
import java.net.URISyntaxException;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.TimeUnit;
@Configuration
@Slf4j
public class FeignConfig{
@Bean
public CloseableHttpClient customHttpClient() {
HttpRequestInterceptor requestInterceptor = new HttpRequestInterceptor() {
@Override
public void process(HttpRequest httpRequest, HttpContext httpContext) throws HttpException, IOException {
log.info("URL:{},params:{}", ((HttpRequestWrapper) httpRequest).getOriginal(), StreamUtils.copyToString(((HttpEntityEnclosingRequest) httpRequest).getEntity().getContent(), Charset.defaultCharset()));
}
};
HttpResponseInterceptor responseInterceptor = new HttpResponseInterceptor() {
@Override
public void process(HttpResponse response, HttpContext httpContext) throws HttpException, IOException {
InputStream inputStream = response.getEntity().getContent();
ByteArrayOutputStream baosOutputStream = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len;
while ((len = inputStream.read(buffer)) > -1) {
baosOutputStream.write(buffer, 0, len);
}
baosOutputStream.flush();
InputStream body = new ByteArrayInputStream(baosOutputStream.toByteArray());
log.info(StreamUtils.copyToString(body, StandardCharsets.UTF_8));
inputStream.close();
body.close();
//设置 InputStream 副本,给后续请求
BasicHttpEntity basicHttpEntity = new BasicHttpEntity();
basicHttpEntity.setContent(new ByteArrayInputStream(baosOutputStream.toByteArray()));
response.setEntity(basicHttpEntity);
}
};
CloseableHttpClient build = HttpClients.custom()
.setMaxConnTotal(666)
.addInterceptorFirst(requestInterceptor)
.addInterceptorFirst(responseInterceptor)
.build();
return build;
}
}
对应的 yaml 配置
在org.springframework.cloud.openfeign.ribbon.LoadBalancerFeignClient的构造方法中打一个断点,可以看到里面的属性 maxTotal=666,是我们的自定义的HttpClient,说明配置成功了。
随便发起一个 fegin 请求,入参、请求真实的 url、返回结果通通给打印出来了。
来到org.springframework.cloud.openfeign.clientconfig.HttpClientFeignConfiguration类,HttpClientFeignConfiguration是Cloud 默认的HttpClient 的配置类。查看他上面的条件注解
@ConditionalOnMissingBean({CloseableHttpClient.class}) ,当容器中不存在CloseableHttpClient的BeanDefinition时,那么此配置类生效,反正不生效。所以当我们的项目配置了HttpClient 后,Cloud 默认的HttpClient 配置类就会失效。
这里顺便提一嘴:@ConditionalOnMissingBean 的原理:因为 @ConditionalOnMissingBean 是被自动装配进去的。普通的被@Configuration标注的类,被扫描的时机早于自动装配的类,因此当我们项目中注入自定义的HttpClient 后,@ConditionalOnMissingBean会生效。
feign.compression.response.enabled配置源码位置org.springframework.cloud.openfeign.clientconfig。HttpClientFeignConfiguration。字面意思理解,Cloud 默认是不压缩返回结果,开启了feign.compression.response.enabled=true 后就开启压缩。源码如下图。
过程和如何切换成HttpClient 一样,加入自定义配置即可。
@Bean
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 OkHttpClient okClient(OkHttpClientFactory httpClientFactory, ConnectionPool connectionPool, FeignHttpClientProperties httpClientProperties) {
Boolean followRedirects = httpClientProperties.isFollowRedirects();
Integer connectTimeout = httpClientProperties.getConnectionTimeout();
Boolean disableSslValidation = httpClientProperties.isDisableSslValidation();
return httpClientFactory.createBuilder(disableSslValidation)
.connectTimeout((long) connectTimeout, TimeUnit.MILLISECONDS)
.followRedirects(followRedirects)
.connectionPool(connectionPool)
.readTimeout(666, TimeUnit.SECONDS)
.addInterceptor(new OkHttpCLientInterceptor()) // 自定义请求日志拦截器
.build();
}
加好后debug 看到配置已经成功了,后续打印日志过程在拦截器中实现
okHttpClient 日志拦截器
/**
* fegin 日志打印
*/
@Slf4j
public class OkHttpCLientInterceptor implements Interceptor {
@Override
public Response intercept(Chain chain) throws IOException {
log.info("#request#url:{} #body:{}", chain.request().url(), convertRequest(chain.request()));
Response response = chain.proceed(chain.request());
BufferedSource source = response.body().source();
source.request(Long.MAX_VALUE);
//创建 io 流副本,否则会出现 io 流提前关闭的异常
log.info("#reponse#url:{} #body:{}", chain.request().url(), source.buffer().clone().readString(Charset.forName("UTF-8")));
return response;
}
private String convertRequest(final Request request) {
final Request copy = request.newBuilder().build();
final Buffer buffer = new Buffer();
try {
copy.body().writeTo(buffer);
} catch (IOException e) {
return "something error,when show requestBody";
}
return buffer.readUtf8();
}
}
通过阅读okHttpClient、httpClient的默认创建源码,了解到,如果需要自定义okHttpClient、httpClient时,项目中注入即可。同时打印日志的时候,注意用流副本操作,否则会出现 io 流异常关闭的问题。