异步记录第三方接口调用日志的优雅实现(HttpClient+装饰者模式+异步线程池/MQ)
对于第三方接口调用日志这个功能,笔者在工作中曾见过以下两种方式:
Restemplate+装饰者模式+MQ实现
网关监控 + Feign拦截器 + 观察者模式实现
其中观察者模式的实现是我最为佩服的设计(站在了一种很新的角度来解决问题),个人认为以上两种实现都显得略过臃肿,应该简化设计,让异步记录的实现更加简洁优雅,因此产生了这样的构思。
为什么选择HttpClient而不是RestTemplate?
其一是因为RestTemplate默认情况下是基于Java原生HTTP的支持,在性能上HttpClient表现更优秀。
其二是RestTemplate在一些版本较老的SpringBoot中存在较多bug,不够稳定。
其三是HttpClient拥有更广泛的适用性。
pom.xml引入依赖
(choose version) 参考 MVN REPOSITORY
<!-- https://mvnrepository.com/artifact/org.apache.httpcomponents.client5/httpclient5 -->
<dependency>
<groupId>org.apache.httpcomponents.client5</groupId>
<artifactId>httpclient5</artifactId>
<version>5.3</version>
</dependency>
异步线程池的实现可参考我的另一篇博客
/**
* DecoratedHttpClient
* 特性:异步记录第三方接口调用日志
*/
@Slf4j
public class DecoratedHttpClient {
private HttpClient httpClient;
public DecoratedHttpClient(){
this.httpClient = HttpClients.createDefault();
}
/**
* 自定义的HttpClient
*/
public DecoratedHttpClient(HttpClient httpClient){
this.httpClient = httpClient;
}
public String callApi(HttpUriRequest httpUriRequest) throws Exception{
String requestBody = null;
try {
if(httpUriRequest instanceof HttpEntityEnclosingRequest){
requestBody = EntityUtils.toString(((HttpEntityEnclosingRequest)httpUriRequest).getEntity());
}
HttpResponse httpResponse = httpClient.execute(httpUriRequest);
String responseBody = EntityUtils.toString(httpResponse);
// TODO 最好推送MQ来实现高可用(因为线程池队列长度始终是有限的/LinkedBlockingQueue无界队列的话也会有内存泄漏的风险,此设计不适用于高并发场景)
ThreadPoolSington.getInstance().submitTask(
new LogRecordAsyncTask(httpUriRequest, requestBody, responseBody, null)
);
return apiResponse;
} catch(Exception e){
ThreadPoolSington.getInstance().submitTask(
new LogRecordAsyncTask(httpUriRequest, requestBody, null, e)
);
throw e;
}
}
}
/**
* 注入至IOC,方便随时使用
*/
@Configuration
public class IocBeanConf{
/**
* 跳过SSL证书验证
*/
@Bean
public HttpClient httpClient() throws KeyStoreException, NoSuchAlgorithmException, KeyManagementException {
return HttpClients.custom()
.setSSLSocketFactory(
new SSLConnectionSocketFactory(SSLContextBuilder.create().loadTrustMaterial(TrustAllStrategy.INSTANCE).build()),
NoopHostnameVerifier.INSTANCE)
)
.build();
}
@Bean
@DependsOn("httpClient")
public DecoratedHttpClient decoratedHttpClient(){
return new DecoratedHttpClient(httpClient);
}
}
/**
* 异步日志任务
*/
@Slf4j
public class LogRecordAsyncTask implents Runnable {
private final HttpUriRequest httpUriRequest;
private final Header[] requestHeaders;
private final String requestBody;
private final String responseBody;
private final Exception exception;
private final Date date;
public LogRecordAsyncTask(HttpUriRequest httpUriRequest, String requestBody, String responseBody, Exception exception){
this.httpUriRequest = httpUriRequest;
this.requestHeaders = httpUriRequest.getAllHeaders();
this.requestBody = requestBody;
this.responseBody = responseBody;
this.exception = exception;
this.date = new Date();
}
@Override
public void run() {
// TODO 持久化日志至数据库
log.info("httpUriRequest ---> {}", this.httpUriRequest);
log.info("requestHeaders ---> {}", Arrays.toString(this.requestHeaders));
log.info("requestBody ---> {}", this.requestBody);
log.info("responseBody ---> {}", this.responseBody);
log.info("exception ---> {}", this.exception);
}
}
从代码中可以看到我的设计,记录了HttpUriRequest,requestHeaders,requestBody,responseBody,异常,调用时间(异步执行,因此入库时间和调用时间不一致),可以更好的监控第三方接口的调用情况。
在run方法中你甚至还可以拓展实时报警机制,可参考我的另一篇博客
改成推送MQ的方式,应该将线程池那里的代码改为调用一个异步注解@Async修饰的方法,方法内部则是发送MQ,避免发送MQ的行为阻塞调用第三方接口的线程?
当然,还可以继续给DecoratedHttpClient做增强,比如记录调用者的username(根据Token),ip,执行该调用的服务器节点Host等信息。
后记:考虑到线程池异步记录日志,可能会发生——程序重启时丢失线程池中的任务,因此建议大家在具体编码的时候,尽量考虑使用MQ来完成异步记录日志(MQ有成熟的处理消息丢失的方案)。