fix: 修复HTTP连接资源泄露和Token缓存线程安全问题

- 修复HTTP连接资源管理问题
  * 所有HTTP请求方法使用try-with-resources确保InputStream自动关闭
  * 添加EntityUtils.consume确保HTTP响应实体被完全消费
  * 引入必要的Apache HttpClient工具类

- 修复Token缓存线程安全问题
  * 使用computeIfAbsent确保首次创建token的原子性操作
  * 实现双重检查锁定机制避免重复获取token
  * 提取createNewTokenInfo方法提高代码可读性和复用性

- 性能和稳定性提升
  * 消除HTTP连接泄露风险,提高连接池利用率
  * 解决多线程环境下的token竞争问题
  * 减少重复token请求,提升高并发场景下系统稳定性

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
yanlongqi
2025-11-22 15:33:01 +08:00
parent 1c00f7eaee
commit 08a56f782a
3 changed files with 58 additions and 33 deletions

View File

@@ -142,14 +142,36 @@ public class JHRequestExecution {
if (StringUtils.isBlank(username)) { if (StringUtils.isBlank(username)) {
throw new ArgsException("用户名称不能为空!"); throw new ArgsException("用户名称不能为空!");
} }
TokenInfo tokenInfo = TOKEN_INFO_MAP.get(username);
// 防止因为服务器时间的问题二导致token不可用可以通过此配置提前获取token // 防止因为服务器时间的问题二导致token不可用可以通过此配置提前获取token
int tokenEffectiveTime = (tokenTimeout - tokenResidueTime) * 60 * 1000; int tokenEffectiveTime = (tokenTimeout - tokenResidueTime) * 60 * 1000;
// 如果是强制获取用户令牌为空、用户令牌不存在、用户令牌过期等,则获取令牌 // 使用computeIfAbsent确保原子性操作避免重复获取token
boolean isGetToken = isForceGetToken || tokenInfo == null || System.currentTimeMillis() - tokenInfo.getCurrentTimestamp() > tokenEffectiveTime; TokenInfo tokenInfo = TOKEN_INFO_MAP.computeIfAbsent(username, this::createNewTokenInfo);
if (isGetToken) {
// 检查token是否过期如果过期则创建新token
long currentTime = System.currentTimeMillis();
if (isForceGetToken || currentTime - tokenInfo.getCurrentTimestamp() > tokenEffectiveTime) {
synchronized (this) {
// 双重检查锁定,确保在同步块中再次检查
tokenInfo = TOKEN_INFO_MAP.get(username);
if (tokenInfo == null || currentTime - tokenInfo.getCurrentTimestamp() > tokenEffectiveTime || isForceGetToken) {
tokenInfo = createNewTokenInfo(username);
TOKEN_INFO_MAP.put(username, tokenInfo);
}
}
}
return tokenInfo.getToken();
}
/**
* 创建新的TokenInfo
*
* @param username 用户名
* @return 新的TokenInfo
*/
private TokenInfo createNewTokenInfo(String username) {
Map<String, Object> params = new HashMap<>(2); Map<String, Object> params = new HashMap<>(2);
params.put("timeout", tokenTimeout); params.put("timeout", tokenTimeout);
String currentTimeMillis = getCurrentTimeMillis(); String currentTimeMillis = getCurrentTimeMillis();
@@ -164,13 +186,12 @@ public class JHRequestExecution {
} catch (Exception e) { } catch (Exception e) {
throw new ClientException("AES加密失败失败原因" + e.getMessage(), e); throw new ClientException("AES加密失败失败原因" + e.getMessage(), e);
} }
tokenInfo = new TokenInfo();
TokenInfo tokenInfo = new TokenInfo();
tokenInfo.setUserName(username); tokenInfo.setUserName(username);
tokenInfo.setToken(requestToken(params)); tokenInfo.setToken(requestToken(params));
tokenInfo.setCurrentTimestamp(System.currentTimeMillis()); tokenInfo.setCurrentTimestamp(System.currentTimeMillis());
TOKEN_INFO_MAP.put(username, tokenInfo); return tokenInfo;
}
return tokenInfo.getToken();
} }
/** /**

View File

@@ -87,8 +87,7 @@ public class JHApiClient {
if (StringUtils.isBlank(path)) { if (StringUtils.isBlank(path)) {
throw new ArgsException("url不能为空"); throw new ArgsException("url不能为空");
} }
try { try (InputStream content = apiHttpClient.get(getUrl(path), headers)) {
InputStream content = apiHttpClient.get(getUrl(path), headers);
return mapper.readValue(content, type); return mapper.readValue(content, type);
} catch (IOException e) { } catch (IOException e) {
throw new ClientException(e.getMessage(), e); throw new ClientException(e.getMessage(), e);
@@ -179,8 +178,9 @@ public class JHApiClient {
if (body != null) { if (body != null) {
bodyStr = mapper.writeValueAsString(body); bodyStr = mapper.writeValueAsString(body);
} }
InputStream content = apiHttpClient.post(getUrl(path), bodyStr, headers); try (InputStream content = apiHttpClient.post(getUrl(path), bodyStr, headers)) {
return mapper.readValue(content, type); return mapper.readValue(content, type);
}
} catch (IOException e) { } catch (IOException e) {
throw new ClientException(e.getMessage(), e); throw new ClientException(e.getMessage(), e);
} }
@@ -208,8 +208,9 @@ public class JHApiClient {
if (body != null) { if (body != null) {
bodyStr = mapper.writeValueAsString(body); bodyStr = mapper.writeValueAsString(body);
} }
InputStream content = apiHttpClient.put(getUrl(path), bodyStr, headers); try (InputStream content = apiHttpClient.put(getUrl(path), bodyStr, headers)) {
return mapper.readValue(content, type); return mapper.readValue(content, type);
}
} catch (IOException e) { } catch (IOException e) {
throw new ClientException(e.getMessage(), e); throw new ClientException(e.getMessage(), e);
} }
@@ -272,8 +273,7 @@ public class JHApiClient {
if (StringUtils.isBlank(path)) { if (StringUtils.isBlank(path)) {
throw new ArgsException("path不能为空"); throw new ArgsException("path不能为空");
} }
try { try (InputStream content = apiHttpClient.delete(getUrl(path), headers)) {
InputStream content = apiHttpClient.delete(getUrl(path), headers);
return mapper.readValue(content, type); return mapper.readValue(content, type);
} catch (IOException e) { } catch (IOException e) {
throw new ClientException(e.getMessage(), e); throw new ClientException(e.getMessage(), e);
@@ -314,8 +314,9 @@ public class JHApiClient {
throw new ArgsException("path不能为空"); throw new ArgsException("path不能为空");
} }
try { try {
InputStream content = apiHttpClient.upload(getUrl(path), keyName, fileName, is, body, headers); try (InputStream content = apiHttpClient.upload(getUrl(path), keyName, fileName, is, body, headers)) {
return mapper.readValue(content, type); return mapper.readValue(content, type);
}
} catch (IOException e) { } catch (IOException e) {
throw new ClientException(e.getMessage(), e); throw new ClientException(e.getMessage(), e);
} }

View File

@@ -26,6 +26,7 @@ import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.client.HttpClients;
import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager;
import org.apache.http.ssl.SSLContextBuilder; import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.util.EntityUtils;
import java.io.IOException; import java.io.IOException;
import java.io.InputStream; import java.io.InputStream;
@@ -146,7 +147,9 @@ public class JHApiHttpClientImpl implements JHApiHttpClient {
try { try {
HttpResponse response = closeableHttpClient.execute(httpRequest); HttpResponse response = closeableHttpClient.execute(httpRequest);
int statusCode = response.getStatusLine().getStatusCode(); int statusCode = response.getStatusLine().getStatusCode();
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) { if (statusCode != HttpStatus.SC_OK) {
// 确保响应实体被完全消费以释放连接
EntityUtils.consume(response.getEntity());
httpRequest.releaseConnection(); httpRequest.releaseConnection();
throw new ClientException("发送HTTP请求失败请求码" + statusCode, ClientErrorCode.REQUEST_ERROR); throw new ClientException("发送HTTP请求失败请求码" + statusCode, ClientErrorCode.REQUEST_ERROR);
} }