异步记录第三方接口调用日志的优雅实现(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有成熟的处理消息丢失的方案)。

posted @ 2023-12-19 12:46  Ashe|||^_^  阅读(451)  评论(0编辑  收藏  举报