restTemplate是springboot自带的http请求客户端,里面包装了HttpClient,是在SpringBoot项目中进行http请求常用的方式,本篇文章主要是讲解怎么进行restTemplate的长连接。
一、普通情况下的RestTemplate配置
@Bean
public RestTemplate AchiementTemplateLongConnection(RestTemplateBuilder builder) {
return builder.build();
}
二、长连接的配置
下面说下怎么进行RestTemplate的配置
@Bean
public RestTemplate AchiementTemplateLongConnection(RestTemplateBuilder builder) {
return builder.requestFactory(()->httpComponentsClientHttpRequestFactory())
.build();
}
@Bean
public HttpComponentsClientHttpRequestFactory httpComponentsClientHttpRequestFactory() {
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setHttpClient(httpClient(poolingHttpClientConnectionManager(),requestConfig()));
return factory;
}
@Bean
public RequestConfig requestConfig() {
RequestConfig result = RequestConfig.custom()
.setConnectionRequestTimeout(0)
.setConnectTimeout(CONNECTION_TIMEOUT)
.setSocketTimeout(CONNECTION_TIMEOUT)
.build();
return result;
}
@Bean
public CloseableHttpClient httpClient(PoolingHttpClientConnectionManager poolingHttpClientConnectionManager, RequestConfig requestConfig) {
CloseableHttpClient result = HttpClientBuilder
.create()
.setConnectionManager(poolingHttpClientConnectionManager)
.setDefaultRequestConfig(requestConfig)
.setRetryHandler(new AchivmentGetHttpRequestRetryHandler(1,true))
.build();
return result;
}
@Bean
public PoolingHttpClientConnectionManager poolingHttpClientConnectionManager() {
PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();
connectionManager.setValidateAfterInactivity(2000);
return connectionManager;
}
下面对几个方法返回的类进行解释下:
- httpClient:http请求的客户端
- RequestConfig:httpClient请求的配置包括超时时间等
- PoolingHttpClientConnectionManager:设置连接池,节省了创建连接的开销
三、长连接中需要注意的问题
试想一个问题,长连接是有时间的,不管是8小时还是2小时,当连接断开了,怎么重连是需要处理的,有两种方式可以进行处理:
- 连接之前先校验下连接是否通着,mysql的长连接就是通过ping命令去校验的
- 做重试,RestTemplate就是这么做的,捕获IOException,然后进行重试,因为之前断开的连接在第一次失败后,被销毁了,后面再一次重试即可成功了。
==注:==这里要特别说明下,httpComponents包里面并没有那么傻,自己断开的连接自己肯定可以感知的,但是当你是客户端,而服务端单方面断开连接的时候,就感知不到了。
最开始我是在检查,我代码中已经设置的检验,为何还是检验不出来呢,我设置检验的代码如下:
connectionManager.setValidateAfterInactivity(2000);
为何检验的代码没有生效呢?原因在这里:BHttpConnectionBase类的方法
@Override
public boolean isStale() {
if (!isOpen()) {
return true;
}
try {
final int bytesRead = fillInputBuffer(1);
return bytesRead < 0;
} catch (final SocketTimeoutException ex) {
//如果是SocketTimeoutException
return false;
} catch (final IOException ex) {
return true;
}
}
我们看下当服务端关闭连接的时候,客户端报什么异常:
java.net.SocketInputStream.socketRead0(Native Method)抛出了timeoutException
正好是返回连接没有问题,就是说这个检验是无效的。
那这个时候只能重试了:
在RestTemplate中使用了DefaultHttpRequestRetryHandler类进行重试,里面有一个retryRequest方法就是用来判断是否重试的。里面有这么一段代码
if (handleAsIdempotent(request)) {
// Retry if the request is considered idempotent
return true;
}
这段代码是判断是否有消息体,如果有消息体则不能重试,这是为了避免如果是插入或者修改的方法,不能轻易重试,否则会有风险。但是如果你的查询请求是post,那么也不会重试,可以自己实现一个retryHandler解决此问题。
四、最后
我贴上mysql的ping逻辑,在此方法中:
com.mysql.jdbc.ConnectionImpl#execSQL(com.mysql.jdbc.StatementImpl, java.lang.String, int, com.mysql.jdbc.Buffer, int, int, boolean, java.lang.String, com.mysql.jdbc.Field[], boolean)
public ResultSetInternalMethods execSQL(StatementImpl callingStatement, String sql, int maxRows, Buffer packet, int resultSetType, int resultSetConcurrency,
boolean streamResults, String catalog, Field[] cachedMetadata, boolean isBatch) throws SQLException {
synchronized (getConnectionMutex()) {
//
// Fall-back if the master is back online if we've issued queriesBeforeRetryMaster queries since we failed over
//
long queryStartTime = 0;
int endOfQueryPacketPosition = 0;
if (packet != null) {
endOfQueryPacketPosition = packet.getPosition();
}
if (getGatherPerformanceMetrics()) {
queryStartTime = System.currentTimeMillis();
}
this.lastQueryFinishedTime = 0; // we're busy!
if ((getHighAvailability()) && (this.autoCommit || getAutoReconnectForPools()) && this.needsPing && !isBatch) {
try {
pingInternal(false, 0);
this.needsPing = false;
} catch (Exception Ex) {
createNewIO(true);
}
}