fix: 修复内存泄漏问题,缓存和复用 Retrofit/OkHttpClient 实例

问题:
- 每次调用 createClobApi、createGammaApi 等方法都创建新的 OkHttpClient 和 Retrofit 实例
- OkHttpClient 包含连接池、线程池等资源,导致内存不断增长
- 运行几小时后内存从 400MB 涨到 1GB+

解决方案:
- 为不需要认证的 API 创建共享的 OkHttpClient 实例
- Gamma API、Data API、GitHub API、不带认证的 CLOB API 使用单例客户端
- 带认证的 CLOB API 按钱包地址缓存(每个账户一个客户端)
- RPC API 按 RPC URL 缓存
- Builder Relayer API 按 relayerUrl 缓存
- 添加 @PreDestroy 方法清理缓存

效果:
- 大幅减少内存占用,避免内存泄漏
- 复用连接池和线程池,提高性能
- 内存占用将保持稳定,不再持续增长
This commit is contained in:
WrBug
2026-01-01 22:07:24 +08:00
parent fe2db11b75
commit 3d05b13298
@@ -23,10 +23,14 @@ import org.springframework.stereotype.Component
import retrofit2.Retrofit
import retrofit2.converter.gson.GsonConverterFactory
import java.io.IOException
import java.util.concurrent.ConcurrentHashMap
import jakarta.annotation.PreDestroy
/**
* Retrofit 客户端工厂
* 用于创建带认证的 Polymarket CLOB API 客户端和 Ethereum RPC API 客户端
*
* 注意:为了避免内存泄漏,本类会缓存和复用客户端实例
*/
@Component
class RetrofitFactory(
@@ -37,8 +41,98 @@ class RetrofitFactory(
private val gson: Gson
) {
private val logger = LoggerFactory.getLogger(RetrofitFactory::class.java)
// 共享的 OkHttpClient(用于不需要认证的 API
private val sharedOkHttpClient: OkHttpClient by lazy {
createClient().build()
}
// 共享的 OkHttpClient(用于需要跟随重定向的 API)
private val sharedOkHttpClientWithRedirect: OkHttpClient by lazy {
createClient()
.followRedirects(true)
.followSslRedirects(true)
.build()
}
// 缓存 Gamma API 客户端(单例)
private val gammaApi: PolymarketGammaApi by lazy {
val baseUrl = if (gammaBaseUrl.endsWith("/")) {
gammaBaseUrl.dropLast(1)
} else {
gammaBaseUrl
}
Retrofit.Builder()
.baseUrl("$baseUrl/")
.client(sharedOkHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
.create(PolymarketGammaApi::class.java)
}
// 缓存 Data API 客户端(单例)
private val dataApi: PolymarketDataApi by lazy {
val baseUrl = "https://data-api.polymarket.com"
Retrofit.Builder()
.baseUrl("$baseUrl/")
.client(sharedOkHttpClientWithRedirect)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
.create(PolymarketDataApi::class.java)
}
// 缓存 GitHub API 客户端(单例)
private val githubApi: GitHubApi by lazy {
val baseUrl = "https://api.github.com"
// 添加拦截器,设置 Accept 头以获取 reactions 数据
val githubInterceptor = object : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder()
.header("Accept", "application/vnd.github+json")
.build()
return chain.proceed(request)
}
}
val okHttpClient = createClient()
.addInterceptor(githubInterceptor)
.build()
Retrofit.Builder()
.baseUrl("$baseUrl/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
.create(GitHubApi::class.java)
}
// 缓存不带认证的 CLOB API 客户端(单例)
private val clobApiWithoutAuth: PolymarketClobApi by lazy {
Retrofit.Builder()
.baseUrl(clobBaseUrl)
.client(sharedOkHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
.create(PolymarketClobApi::class.java)
}
// 缓存带认证的 CLOB API 客户端:walletAddress -> PolymarketClobApi
// 注意:每个账户使用不同的 API Key,需要不同的客户端
private val clobApiCache = ConcurrentHashMap<String, PolymarketClobApi>()
// 缓存 RPC API 客户端:rpcUrl -> EthereumRpcApi
private val rpcApiCache = ConcurrentHashMap<String, EthereumRpcApi>()
// 缓存 Builder Relayer API 客户端:relayerUrl -> BuilderRelayerApi
private val builderRelayerApiCache = ConcurrentHashMap<String, BuilderRelayerApi>()
/**
* 创建带认证的 Polymarket CLOB API 客户端
* 按钱包地址缓存,避免重复创建
* @param apiKey API Key
* @param apiSecret API Secret
* @param apiPassphrase API Passphrase
@@ -51,57 +145,46 @@ class RetrofitFactory(
apiPassphrase: String,
walletAddress: String
): PolymarketClobApi {
val authInterceptor = PolymarketAuthInterceptor(apiKey, apiSecret, apiPassphrase, walletAddress)
// 添加响应日志拦截器,用于调试 JSON 解析错误
val responseLoggingInterceptor = ResponseLoggingInterceptor()
val okHttpClient = createClient()
.addInterceptor(authInterceptor)
.addInterceptor(responseLoggingInterceptor)
.build()
return Retrofit.Builder()
.baseUrl(clobBaseUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
.create(PolymarketClobApi::class.java)
// 使用钱包地址作为缓存键(每个账户使用不同的 API Key)
return clobApiCache.computeIfAbsent(walletAddress) {
val authInterceptor = PolymarketAuthInterceptor(apiKey, apiSecret, apiPassphrase, walletAddress)
// 添加响应日志拦截器,用于调试 JSON 解析错误
val responseLoggingInterceptor = ResponseLoggingInterceptor()
val okHttpClient = createClient()
.addInterceptor(authInterceptor)
.addInterceptor(responseLoggingInterceptor)
.build()
Retrofit.Builder()
.baseUrl(clobBaseUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
.create(PolymarketClobApi::class.java)
}
}
/**
* 创建不带认证的 Polymarket CLOB API 客户端
* 用于不需要认证的查询接口
* @return PolymarketClobApi 客户端
* @return PolymarketClobApi 客户端(单例)
*/
fun createClobApiWithoutAuth(): PolymarketClobApi {
// 添加响应日志拦截器,用于调试 JSON 解析错误
val responseLoggingInterceptor = ResponseLoggingInterceptor()
val okHttpClient = createClient()
.addInterceptor(responseLoggingInterceptor)
.build()
return Retrofit.Builder()
.baseUrl(clobBaseUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
.create(PolymarketClobApi::class.java)
return clobApiWithoutAuth
}
/**
* 创建 Ethereum RPC API 客户端
* 使用固定的 baseUrl,通过拦截器动态替换为实际的 RPC URL
* 如果 RPC 不可用,将抛出异常
* 按 RPC URL 缓存,避免重复创建
* @param rpcUrl RPC 节点 URL
* @return EthereumRpcApi 客户端
* @throws IllegalArgumentException 如果 RPC URL 无效或不可用
*/
fun createEthereumRpcApi(rpcUrl: String): EthereumRpcApi {
// 使用固定的 baseUrlRetrofit 要求 baseUrl 必须以 / 结尾)
val fixedBaseUrl = "https://polyrpc.polyhermes/"
// 确保实际的 RPC URL 以 / 结尾
val actualRpcUrl = if (rpcUrl.endsWith("/")) {
rpcUrl
@@ -109,22 +192,28 @@ class RetrofitFactory(
"$rpcUrl/"
}
// 验证 RPC 是否可用
validateRpcAvailability(actualRpcUrl)
// 创建 URL 替换拦截器
val urlReplaceInterceptor = RpcUrlReplaceInterceptor(fixedBaseUrl, actualRpcUrl)
val okHttpClient = createClient()
.addInterceptor(urlReplaceInterceptor)
.build()
return Retrofit.Builder()
.baseUrl(fixedBaseUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
.create(EthereumRpcApi::class.java)
// 使用 RPC URL 作为缓存键
return rpcApiCache.computeIfAbsent(actualRpcUrl) {
// 验证 RPC 是否可用(仅在新创建时验证)
validateRpcAvailability(actualRpcUrl)
// 使用固定的 baseUrlRetrofit 要求 baseUrl 必须以 / 结尾)
val fixedBaseUrl = "https://polyrpc.polyhermes/"
// 创建 URL 替换拦截器
val urlReplaceInterceptor = RpcUrlReplaceInterceptor(fixedBaseUrl, actualRpcUrl)
val okHttpClient = createClient()
.addInterceptor(urlReplaceInterceptor)
.build()
Retrofit.Builder()
.baseUrl(fixedBaseUrl)
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
.create(EthereumRpcApi::class.java)
}
}
/**
@@ -134,8 +223,6 @@ class RetrofitFactory(
* @throws IllegalArgumentException 如果 RPC 不可用
*/
private fun validateRpcAvailability(rpcUrl: String) {
val logger = LoggerFactory.getLogger(RetrofitFactory::class.java)
try {
// 解析 URL
val httpUrl = rpcUrl.toHttpUrlOrNull()
@@ -203,46 +290,24 @@ class RetrofitFactory(
/**
* 创建 Polymarket Gamma API 客户端
* Gamma API 是公开 API,不需要认证
* @return PolymarketGammaApi 客户端
* @return PolymarketGammaApi 客户端(单例)
*/
fun createGammaApi(): PolymarketGammaApi {
val baseUrl = if (gammaBaseUrl.endsWith("/")) {
gammaBaseUrl.dropLast(1)
} else {
gammaBaseUrl
}
val okHttpClient = createClient().build()
return Retrofit.Builder()
.baseUrl("$baseUrl/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
.create(PolymarketGammaApi::class.java)
return gammaApi
}
/**
* 创建 Polymarket Data API 客户端
* Data API 是公开 API,不需要认证
* @return PolymarketDataApi 客户端
* @return PolymarketDataApi 客户端(单例)
*/
fun createDataApi(): PolymarketDataApi {
val baseUrl = "https://data-api.polymarket.com"
val okHttpClient = createClient()
.followRedirects(true)
.followSslRedirects(true)
.build()
return Retrofit.Builder()
.baseUrl("$baseUrl/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
.create(PolymarketDataApi::class.java)
return dataApi
}
/**
* 创建 Builder Relayer API 客户端
* 按 relayerUrl 缓存,避免重复创建
* @param relayerUrl Builder Relayer URL
* @param apiKey Builder API Key
* @param secret Builder Secret
@@ -261,49 +326,61 @@ class RetrofitFactory(
relayerUrl
}
// 添加 Builder 认证拦截器
val builderAuthInterceptor = BuilderAuthInterceptor(apiKey, secret, passphrase)
val okHttpClient = createClient()
.addInterceptor(builderAuthInterceptor)
.build()
return Retrofit.Builder()
.baseUrl("$baseUrl/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
.create(BuilderRelayerApi::class.java)
// 使用 baseUrl 作为缓存键(注意:如果 API Key 变化,需要清理缓存)
return builderRelayerApiCache.computeIfAbsent(baseUrl) {
// 添加 Builder 认证拦截器
val builderAuthInterceptor = BuilderAuthInterceptor(apiKey, secret, passphrase)
val okHttpClient = createClient()
.addInterceptor(builderAuthInterceptor)
.build()
Retrofit.Builder()
.baseUrl("$baseUrl/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
.create(BuilderRelayerApi::class.java)
}
}
/**
* 创建 GitHub API 客户端
* GitHub API 是公开 API,不需要认证(但建议使用 token 提高速率限制)
* 添加 Accept 头以获取 reactions 数据
* @return GitHubApi 客户端
* @return GitHubApi 客户端(单例)
*/
fun createGitHubApi(): GitHubApi {
val baseUrl = "https://api.github.com"
// 添加拦截器,设置 Accept 头以获取 reactions 数据
val githubInterceptor = object : Interceptor {
override fun intercept(chain: Interceptor.Chain): Response {
val request = chain.request().newBuilder()
.header("Accept", "application/vnd.github+json")
.build()
return chain.proceed(request)
}
}
val okHttpClient = createClient()
.addInterceptor(githubInterceptor)
.build()
return Retrofit.Builder()
.baseUrl("$baseUrl/")
.client(okHttpClient)
.addConverterFactory(GsonConverterFactory.create(gson))
.build()
.create(GitHubApi::class.java)
return githubApi
}
/**
* 清理缓存(用于测试或配置变更时)
*/
@PreDestroy
fun destroy() {
logger.info("清理 RetrofitFactory 缓存")
clobApiCache.clear()
rpcApiCache.clear()
builderRelayerApiCache.clear()
}
/**
* 清理指定钱包地址的 CLOB API 缓存
* 用于 API Key 变更时
*/
fun clearClobApiCache(walletAddress: String) {
clobApiCache.remove(walletAddress)
logger.debug("已清理 CLOB API 缓存: $walletAddress")
}
/**
* 清理指定 RPC URL 的 RPC API 缓存
* 用于 RPC 节点变更时
*/
fun clearRpcApiCache(rpcUrl: String) {
val actualRpcUrl = if (rpcUrl.endsWith("/")) rpcUrl else "$rpcUrl/"
rpcApiCache.remove(actualRpcUrl)
logger.debug("已清理 RPC API 缓存: $actualRpcUrl")
}
}
@@ -392,4 +469,3 @@ class ResponseLoggingInterceptor : Interceptor {
return response
}
}