目录
- 背景
- 代码实现
- 工具类
- 功能实现
- 模拟使用
- 问题分析与定位
- 解决方案
- 总结
背景
最近遇到一个HttpClient问题,某个接口一直报404错误。该接口使用HttpClient调用其他服务获取数据,为了提高接口调用性能,利用httpclient池化技术来保证请求的数量,并及时关闭无效的请求,在某次版本上线后该接口出现异常。本文使用httpclient-5.2.1版本还原使用场景及分析说明。
代码实现
为了说明问题,本文将以最简洁的代码还原问题。
工具类
池化对象PoolingHttpClientConnectionManager使用单例模式。
/**
* HttpClient工具类
*/
class HttpClientUtils {
static PoolingHttpClientConnectionManager cm;
static {
cm = new PoolingHttpClientConnectionManager();
}
public static CloseableHttpClient getHttpClient() {
return HttpClientBuilder.create().setConnectionManager(cm).build();
}
}
功能实现
出现问题的代码是在某个版本的时候调用了CloseableHttpClient.close()方法释放资源。
@Slf4j
class TestTask implements Runnable {
@Override
public void run() {
CloseableHttpClient client = HttpClientUtils.getHttpClient();
HttpGet get = new HttpGet("https://www.baidu.com");
try {
ClassicHttpResponse execute = client.execute(get, response -> response);
} catch (IOException e) {
log.info("RPC error: {}", e.getMessage());
} finally {
try {
client.close();
} catch (IOException e) {
log.info("Release error: {}", e.getMessage());
}
}
}
}
模拟使用
使用多个线程模拟API接口调用
public class HttpClientTest {
public static void main(String[] args) {
for (int i = 0; i < 10; i++) {
new Thread(new TestTask()).start();
}
}
}
问题分析与定位
检查服务日志发现下图所示错误:
该错误出现的原因是调用CloseableHttpClient.close()方法释放资源的时候将单例的PoolingHttpClientConnectionManager对象关闭了,导致再次使用工具类获取client对象时因为池化对象已被关闭而报错。
通过错误信息在httpclient源码中分析出现问题的原因。
在HttpClientBuilder.build()方法中找到如下代码:
在InternalHttpClient.close()方法中将可关闭对象全部关闭了
代码找到这里,原因已经很明显了,使用HttpClient池化技术的方式有问题。
解决方案
通过HttpClientBuilder.build()中的代码,可以发现connManagerShared为true时池化对象不会被放如可关闭对象列表中。在使用HttpClientBuilder设置单例池化对象时将池化对象设置为共享状态,在调用CloseableHttpClient.close()方法释放资源时才不会将池化对象关闭导致后续连接不能通过池化对象分配连接。正确使用方式如下:
HttpClientBuilder.create().setConnectionManager(cm).setConnectionManagerShared(true).build();
总结
在使用第三方库的时候一定要注意接口说明或者查看对应的源码以避免使用不当而出现故障。