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,37 +142,58 @@ public class JHRequestExecution {
if (StringUtils.isBlank(username)) {
throw new ArgsException("用户名称不能为空!");
}
TokenInfo tokenInfo = TOKEN_INFO_MAP.get(username);
// 防止因为服务器时间的问题二导致token不可用可以通过此配置提前获取token
int tokenEffectiveTime = (tokenTimeout - tokenResidueTime) * 60 * 1000;
// 如果是强制获取用户令牌为空、用户令牌不存在、用户令牌过期等,则获取令牌
boolean isGetToken = isForceGetToken || tokenInfo == null || System.currentTimeMillis() - tokenInfo.getCurrentTimestamp() > tokenEffectiveTime;
if (isGetToken) {
Map<String, Object> params = new HashMap<>(2);
params.put("timeout", tokenTimeout);
String currentTimeMillis = getCurrentTimeMillis();
String beforeEncryption = String.format(CommonConstant.TokenUserFormat, username, currentTimeMillis);
try {
SecretKeySpec secretKey = new SecretKeySpec(
CommonConstant.DEFAULT_AES_KEY.getBytes(StandardCharsets.UTF_8), CommonConstant.AES_ALGORITHM);
Cipher cipher = Cipher.getInstance(CommonConstant.AES_ECB_PADDING);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] encryptBytes = cipher.doFinal(beforeEncryption.getBytes(StandardCharsets.UTF_8));
params.put("username", Base64.getEncoder().encodeToString(encryptBytes));
} catch (Exception e) {
throw new ClientException("AES加密失败失败原因" + e.getMessage(), e);
// 使用computeIfAbsent确保原子性操作避免重复获取token
TokenInfo tokenInfo = TOKEN_INFO_MAP.computeIfAbsent(username, this::createNewTokenInfo);
// 检查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);
}
}
tokenInfo = new TokenInfo();
tokenInfo.setUserName(username);
tokenInfo.setToken(requestToken(params));
tokenInfo.setCurrentTimestamp(System.currentTimeMillis());
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);
params.put("timeout", tokenTimeout);
String currentTimeMillis = getCurrentTimeMillis();
String beforeEncryption = String.format(CommonConstant.TokenUserFormat, username, currentTimeMillis);
try {
SecretKeySpec secretKey = new SecretKeySpec(
CommonConstant.DEFAULT_AES_KEY.getBytes(StandardCharsets.UTF_8), CommonConstant.AES_ALGORITHM);
Cipher cipher = Cipher.getInstance(CommonConstant.AES_ECB_PADDING);
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
byte[] encryptBytes = cipher.doFinal(beforeEncryption.getBytes(StandardCharsets.UTF_8));
params.put("username", Base64.getEncoder().encodeToString(encryptBytes));
} catch (Exception e) {
throw new ClientException("AES加密失败失败原因" + e.getMessage(), e);
}
TokenInfo tokenInfo = new TokenInfo();
tokenInfo.setUserName(username);
tokenInfo.setToken(requestToken(params));
tokenInfo.setCurrentTimestamp(System.currentTimeMillis());
return tokenInfo;
}
/**
* 获得当前的时间
*

View File

@@ -87,8 +87,7 @@ public class JHApiClient {
if (StringUtils.isBlank(path)) {
throw new ArgsException("url不能为空");
}
try {
InputStream content = apiHttpClient.get(getUrl(path), headers);
try (InputStream content = apiHttpClient.get(getUrl(path), headers)) {
return mapper.readValue(content, type);
} catch (IOException e) {
throw new ClientException(e.getMessage(), e);
@@ -179,8 +178,9 @@ public class JHApiClient {
if (body != null) {
bodyStr = mapper.writeValueAsString(body);
}
InputStream content = apiHttpClient.post(getUrl(path), bodyStr, headers);
return mapper.readValue(content, type);
try (InputStream content = apiHttpClient.post(getUrl(path), bodyStr, headers)) {
return mapper.readValue(content, type);
}
} catch (IOException e) {
throw new ClientException(e.getMessage(), e);
}
@@ -208,8 +208,9 @@ public class JHApiClient {
if (body != null) {
bodyStr = mapper.writeValueAsString(body);
}
InputStream content = apiHttpClient.put(getUrl(path), bodyStr, headers);
return mapper.readValue(content, type);
try (InputStream content = apiHttpClient.put(getUrl(path), bodyStr, headers)) {
return mapper.readValue(content, type);
}
} catch (IOException e) {
throw new ClientException(e.getMessage(), e);
}
@@ -272,8 +273,7 @@ public class JHApiClient {
if (StringUtils.isBlank(path)) {
throw new ArgsException("path不能为空");
}
try {
InputStream content = apiHttpClient.delete(getUrl(path), headers);
try (InputStream content = apiHttpClient.delete(getUrl(path), headers)) {
return mapper.readValue(content, type);
} catch (IOException e) {
throw new ClientException(e.getMessage(), e);
@@ -314,8 +314,9 @@ public class JHApiClient {
throw new ArgsException("path不能为空");
}
try {
InputStream content = apiHttpClient.upload(getUrl(path), keyName, fileName, is, body, headers);
return mapper.readValue(content, type);
try (InputStream content = apiHttpClient.upload(getUrl(path), keyName, fileName, is, body, headers)) {
return mapper.readValue(content, type);
}
} catch (IOException 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.conn.PoolingHttpClientConnectionManager;
import org.apache.http.ssl.SSLContextBuilder;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.io.InputStream;
@@ -146,7 +147,9 @@ public class JHApiHttpClientImpl implements JHApiHttpClient {
try {
HttpResponse response = closeableHttpClient.execute(httpRequest);
int statusCode = response.getStatusLine().getStatusCode();
if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) {
if (statusCode != HttpStatus.SC_OK) {
// 确保响应实体被完全消费以释放连接
EntityUtils.consume(response.getEntity());
httpRequest.releaseConnection();
throw new ClientException("发送HTTP请求失败请求码" + statusCode, ClientErrorCode.REQUEST_ERROR);
}