diff --git a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/RetrofitFactory.kt b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/RetrofitFactory.kt index 16879dd..f7d5041 100644 --- a/backend/src/main/kotlin/com/wrbug/polymarketbot/util/RetrofitFactory.kt +++ b/backend/src/main/kotlin/com/wrbug/polymarketbot/util/RetrofitFactory.kt @@ -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() + + // 缓存 RPC API 客户端:rpcUrl -> EthereumRpcApi + private val rpcApiCache = ConcurrentHashMap() + + // 缓存 Builder Relayer API 客户端:relayerUrl -> BuilderRelayerApi + private val builderRelayerApiCache = ConcurrentHashMap() + /** * 创建带认证的 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 { - // 使用固定的 baseUrl(Retrofit 要求 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) + + // 使用固定的 baseUrl(Retrofit 要求 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 } } -