mirror of
https://github.com/yanlongqi/jhinno-openapi-java-sdk.git
synced 2026-03-22 06:15:10 +08:00
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:
@@ -142,37 +142,58 @@ 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) {
|
|
||||||
Map<String, Object> params = new HashMap<>(2);
|
// 检查token是否过期,如果过期则创建新token
|
||||||
params.put("timeout", tokenTimeout);
|
long currentTime = System.currentTimeMillis();
|
||||||
String currentTimeMillis = getCurrentTimeMillis();
|
if (isForceGetToken || currentTime - tokenInfo.getCurrentTimestamp() > tokenEffectiveTime) {
|
||||||
String beforeEncryption = String.format(CommonConstant.TokenUserFormat, username, currentTimeMillis);
|
synchronized (this) {
|
||||||
try {
|
// 双重检查锁定,确保在同步块中再次检查
|
||||||
SecretKeySpec secretKey = new SecretKeySpec(
|
tokenInfo = TOKEN_INFO_MAP.get(username);
|
||||||
CommonConstant.DEFAULT_AES_KEY.getBytes(StandardCharsets.UTF_8), CommonConstant.AES_ALGORITHM);
|
if (tokenInfo == null || currentTime - tokenInfo.getCurrentTimestamp() > tokenEffectiveTime || isForceGetToken) {
|
||||||
Cipher cipher = Cipher.getInstance(CommonConstant.AES_ECB_PADDING);
|
tokenInfo = createNewTokenInfo(username);
|
||||||
cipher.init(Cipher.ENCRYPT_MODE, secretKey);
|
TOKEN_INFO_MAP.put(username, tokenInfo);
|
||||||
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 = new TokenInfo();
|
|
||||||
tokenInfo.setUserName(username);
|
|
||||||
tokenInfo.setToken(requestToken(params));
|
|
||||||
tokenInfo.setCurrentTimestamp(System.currentTimeMillis());
|
|
||||||
TOKEN_INFO_MAP.put(username, tokenInfo);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return tokenInfo.getToken();
|
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;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 获得当前的时间
|
* 获得当前的时间
|
||||||
*
|
*
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user