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:
@@ -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 {
|
||||||
// 使用固定的 baseUrl(Retrofit 要求 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)
|
|
||||||
|
// 使用固定的 baseUrl(Retrofit 要求 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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user