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