多 BaseURL 支持:动态切换、多实例管理与请求级覆盖

说实话,多 BaseURL 这个话题,几乎每个中大型项目都会遇到。我记得刚带团队那会儿,有个项目需要同时对接三个不同的后端服务——用户中心、订单系统、推送服务。当时年轻,直接写了三个 Retrofit 实例,代码冗余得让人崩溃。后来踩了不少坑,才慢慢摸索出一套比较优雅的方案。

今天我就把这套经验拆开来讲。你想想看,多 BaseURL 无非就三种场景:

  • 动态切换 BaseURL:同一个接口,根据用户或环境换地址
  • 多 Retrofit 实例管理:不同服务,各自独立配置
  • 请求级 URL 覆盖:单个请求临时换个地址

咱们一个一个来。

1. 动态切换 BaseURL

先说说最常见的场景。比如你的 App 有测试环境和生产环境,用户登录后可能还要根据地区分配不同的 API 地址。这时候,你总不能为每个环境都写一套接口吧?

我的做法是用 OkHttp 的 Interceptor 来动态替换 BaseURL。核心思路是:在请求发出前,拦截它,把 URL 的主机部分换成目标地址。

class DynamicBaseUrlInterceptor : Interceptor {
    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()
        val newUrl = originalRequest.url.newBuilder()
            .host(currentBaseUrl.host)
            .port(currentBaseUrl.port)
            .scheme(currentBaseUrl.scheme)
            .build()
        val newRequest = originalRequest.newBuilder()
            .url(newUrl)
            .build()
        return chain.proceed(newRequest)
    }

    companion object {
        var currentBaseUrl: HttpUrl = HttpUrl.parse("https://default.api.com")!!
    }
}

嗯,这里要注意:currentBaseUrl 必须是全局可变的。我习惯把它放在一个单例的配置类里,方便随时切换。

核心要点:Interceptor 里改 URL,不影响 Retrofit 的注解定义。你依然在接口里写相对路径,Interceptor 帮你补全主机部分。

切换的时候,直接赋值就行:

DynamicBaseUrlInterceptor.currentBaseUrl = HttpUrl.parse("https://new.api.com")

我在项目中遇到过一个问题:切换后,如果 OkHttp 的连接池里还有旧连接的 Keep-Alive,可能会复用旧连接。解决办法是切换时调用 connectionPool.evictAll(),或者干脆为不同环境创建不同的 OkHttpClient。

2. 多 Retrofit 实例管理

如果不同服务的配置差异很大——比如超时时间不同、需要不同的拦截器、甚至不同的序列化方式——那还是老老实实创建多个 Retrofit 实例吧。

但别写死。我建议用 工厂模式 + 依赖注入 来管理。

class RetrofitManager(private val okHttpClients: Map<String, OkHttpClient>) {

    private val retrofitMap = mutableMapOf<String, Retrofit>()

    fun getRetrofit(serviceName: String): Retrofit {
        return retrofitMap.getOrPut(serviceName) {
            Retrofit.Builder()
                .baseUrl(getBaseUrl(serviceName))
                .client(okHttpClients[serviceName] ?: defaultClient)
                .addConverterFactory(GsonConverterFactory.create())
                .addCallAdapterFactory(CoroutineCallAdapterFactory())
                .build()
        }
    }

    private fun getBaseUrl(serviceName: String): String {
        return when (serviceName) {
            "user" -> "https://user.api.com/"
            "order" -> "https://order.api.com/"
            "push" -> "https://push.api.com/"
            else -> throw IllegalArgumentException("Unknown service: $serviceName")
        }
    }
}

这样,每个服务都有自己的 Retrofit 实例,互不干扰。而且通过 getOrPut 实现了懒加载,不会一启动就创建一堆对象。

我的习惯:把 OkHttpClient 也做成 Map,每个服务可以自定义拦截器、超时时间。比如用户服务需要 Token 拦截器,订单服务需要签名拦截器,互不影响。

使用的时候,注入 RetrofitManager,然后:

val userApi = retrofitManager.getRetrofit("user").create(UserApi::class.java)

你看,是不是清晰多了?

3. 请求级别的 URL 覆盖

最后一种场景比较特殊:大部分请求走默认地址,但偶尔某个请求需要临时换个地址。比如上传文件到 CDN,或者调用第三方服务的回调接口。

Retrofit 本身支持 @Url 注解,可以让你在方法参数里直接传完整的 URL。

interface FileApi {
    @POST
    suspend fun uploadFile(@Url url: String, @Body file: RequestBody): Response<UploadResult>
}

调用时:

val result = fileApi.uploadFile("https://cdn.example.com/upload", fileBody)

这样,这个请求就完全绕过了 Retrofit 的 BaseURL,直接访问你指定的地址。

注意@Url 必须放在第一个参数位置,而且不能和 @Path@Query 混用。如果你需要同时传路径参数和 URL,建议把 URL 写在前面,后面跟其他参数。

我曾经犯过一个错:在 @Url 参数后面又加了个 @Path,结果编译不报错,运行时报错说路径冲突。排查了半天才发现是注解顺序的问题。

4. 三种方案的对比与选择

方案 适用场景 优点 缺点
动态切换 BaseURL 同一套接口,地址随环境/用户变化 代码改动小,一个 Retrofit 实例搞定 全局状态,并发切换需加锁
多 Retrofit 实例 不同服务,配置差异大 隔离性好,各自独立配置 实例数量多,管理成本略高
请求级 URL 覆盖 个别请求临时换地址 灵活,不影响全局 每个方法都要加参数,代码冗余

说白了,没有银弹。我个人的建议是:

  • 如果只是切换环境,用动态 BaseURL 最省事。
  • 如果对接多个独立服务,用多实例管理。
  • 如果只是偶尔一两个特殊请求,用 @Url 覆盖。

5. 架构图:多 BaseURL 的核心逻辑

下面这张图,我把三种方案的关系画出来了。你看一眼就明白它们各自的位置。

多 BaseURL 支持架构图 App 业务层 动态切换 BaseURL 多 Retrofit 实例管理 请求级 URL 覆盖 实现方式 OkHttp Interceptor 全局可变 BaseURL 实现方式 工厂模式 + 懒加载 Map<String, Retrofit> 实现方式 @Url 注解 方法参数传完整 URL Retrofit + OkHttp + Coroutine + Flow

从图里能看出来,不管选哪种方案,底层都是 Retrofit + OkHttp 这套组合。区别只在于你从哪个层面去干预 URL 的生成。

6. 避坑指南

最后,分享几个我踩过的坑:

  • BaseURL 末尾必须加斜杠:Retrofit 要求 BaseURL 以 / 结尾,否则拼接路径时会出问题。我见过有人漏掉,结果请求地址变成了 https://api.com/v1/user 而不是 https://api.com/v1/user/,导致 404。
  • Interceptor 里别做耗时操作:动态切换 BaseURL 的 Interceptor 里,不要做网络请求或数据库查询。它运行在 OkHttp 的调度线程里,阻塞会影响所有请求。
  • 多实例注意内存泄漏:每个 Retrofit 实例都持有 OkHttpClient 的引用。如果创建了大量实例又不释放,内存会涨得很快。用 getOrPut 或依赖注入的单例模式来管理。
  • @Url 和 BaseURL 的优先级:如果同时用了 @Url 和 BaseURL,@Url 会完全覆盖 BaseURL。但注意,@Url 必须是完整的 URL(包含 scheme),否则 Retrofit 会尝试用 BaseURL 去补全,结果可能不是你想要的。

一个小技巧:如果你需要同时支持多个 BaseURL,但又不想写多个 Retrofit 实例,可以考虑用 @Header 传递目标地址,然后在 Interceptor 里读取 Header 并替换 URL。这样既灵活又统一。

好了,多 BaseURL 这块就聊到这儿。说白了,没有绝对正确的方案,只有适合你项目的方案。多试试,找到最顺手的那一套就行。

公众号:蓝海资料掘金营,微信deep3321