Android 多 BaseUrl 动态切换策略(结合 ServiceManager 实现)
在 Android 开发中,如何优雅地实现多环境切换和动态域名管理?本文将深入探讨基于动态代理和 Retrofit 实例池的多 BaseUrl 解决方案,结合 ServiceManager 实现灵活高效的 URL 调度机制。
一、需求背景与痛点分析
1.1 常见场景
- 多环境切换:开发(DEV)、测试(TEST)、预发布(STAGING)、生产(PROD)环境
- 多域名管理:用户服务、支付服务、消息服务等使用不同域名
- 动态降级:主域名失败时自动切换到备份域名
- A/B 测试:不同用户群体使用不同的服务端点
1.2 传统方案痛点
- 编译时锁定:通过 BuildConfig 区分环境,需重新编译才能切换
- 全局单例限制:Retrofit 单例无法支持多域名
- 代码侵入性强:URL 硬编码在接口定义中
- 灵活性不足:运行时无法动态调整域名
二、核心架构设计
三、完整实现方案
3.1 依赖配置
// build.gradle (app)
dependencies {implementation 'com.squareup.retrofit2:retrofit:2.9.0'implementation 'com.squareup.retrofit2:converter-gson:2.9.0'implementation 'com.squareup.okhttp3:okhttp:4.12.0'implementation 'com.squareup.okhttp3:logging-interceptor:4.12.0'implementation 'com.tencent:mmkv:1.3.4' // 持久化存储
}
3.2 环境管理模块
// EnvManager.kt
enum class EnvType(val desc: String) {DEV("开发环境"),TEST("测试环境"),STAGING("预发环境"),PROD("生产环境")
}object EnvConfig {private const val KEY_CURRENT_ENV = "current_env"private val mmkv by lazy { MMKV.defaultMMKV() }// 当前环境(默认为生产环境)var currentEnv: EnvTypeget() = EnvType.valueOf(mmkv.getString(KEY_CURRENT_ENV, EnvType.PROD.name) ?: EnvType.PROD.name)set(value) {mmkv.putString(KEY_CURRENT_ENV, value.name)}// 环境切换监听器private val envChangeListeners = mutableListOf<() -> Unit>()fun addEnvChangeListener(listener: () -> Unit) {envChangeListeners.add(listener)}fun notifyEnvChanged() {envChangeListeners.forEach { it.invoke() }}
}
3.3 URL 决策器(核心)
// UrlDecider.kt
object UrlDecider {// 环境基础URL配置private val envBaseMap = mapOf(EnvType.DEV to "https://dev.api.example.com",EnvType.TEST to "https://test.api.example.com",EnvType.STAGING to "https://staging.api.example.com",EnvType.PROD to "https://api.example.com")// 特殊服务独立域名private val specialServiceMap = mapOf("PayService" to "https://pay.thirdparty.com","ChatService" to "https://chat.thirdparty.com")// 服务分组配置private val serviceGroupMap = mapOf("UserService" to ServiceGroup.USER,"OrderService" to ServiceGroup.ORDER)// 分组URL后缀private val groupSuffixMap = mapOf(ServiceGroup.USER to "/user/v1",ServiceGroup.ORDER to "/order/v2")/*** 获取服务的完整BaseUrl* * @param serviceClass 服务接口Class* @return 完整的BaseUrl字符串*/fun getBaseUrl(serviceClass: Class<*>): String {val serviceName = serviceClass.simpleName// 1. 检查是否是特殊服务(固定域名)specialServiceMap[serviceName]?.let { return it }// 2. 获取当前环境的基础URLval baseUrl = envBaseMap[EnvConfig.currentEnv] ?: throw IllegalArgumentException("No base URL configured for current environment")// 3. 获取服务分组后缀val group = serviceGroupMap[serviceName]val suffix = group?.let { groupSuffixMap[it] } ?: ""return "$baseUrl$suffix"}
}enum class ServiceGroup {USER, ORDER, PRODUCT, CONTENT
}
3.4 Retrofit 实例池
// RetrofitPool.kt
object RetrofitPool {private val retrofitMap = ConcurrentHashMap<String, Retrofit>()private val serviceCacheMap = ConcurrentHashMap<Class<*>, Any>()// 公共OkHttpClient配置private val commonClient by lazy {OkHttpClient.Builder().connectTimeout(15, TimeUnit.SECONDS).readTimeout(20, TimeUnit.SECONDS).writeTimeout(20, TimeUnit.SECONDS).addInterceptor(CommonHeadersInterceptor()).addInterceptor(AuthInterceptor()).addInterceptor(LoggingInterceptor().apply {level = if (BuildConfig.DEBUG) Level.BODY else Level.NONE}).build()}/*** 获取Retrofit实例* * @param baseUrl 基础URL* @return Retrofit实例*/fun getRetrofit(baseUrl: String): Retrofit {return retrofitMap.getOrPut(baseUrl) {Retrofit.Builder().baseUrl(baseUrl).client(commonClient).addConverterFactory(GsonConverterFactory.create()).addCallAdapterFactory(CoroutineCallAdapterFactory()).build()}}/*** 获取真实服务实例(带缓存)*/@Suppress("UNCHECKED_CAST")fun <T> getRealService(serviceClass: Class<T>, baseUrl: String): T {return serviceCacheMap.getOrPut(serviceClass) {getRetrofit(baseUrl).create(serviceClass)} as T}/*** 清除所有缓存(环境切换时调用)*/fun clearAll() {retrofitMap.clear()serviceCacheMap.clear()}
}// 公共请求头拦截器
class CommonHeadersInterceptor : Interceptor {override fun intercept(chain: Interceptor.Chain): Response {val request = chain.request().newBuilder().addHeader("Accept", "application/json").addHeader("Content-Type", "application/json").addHeader("X-Env", EnvConfig.currentEnv.name).addHeader("X-Platform", "Android").addHeader("X-Version", BuildConfig.VERSION_NAME).build()return chain.proceed(request)}
}// 认证拦截器
class AuthInterceptor : Interceptor {override fun intercept(chain: Interceptor.Chain): Response {val request = chain.request()val token = AuthManager.getToken() ?: return chain.proceed(request)val newRequest = request.newBuilder().addHeader("Authorization", "Bearer $token").build()return chain.proceed(newRequest)}
}
3.5 动态服务代理
// ServiceProxy.kt
class ServiceProxy<T>(private val serviceClass: Class<T>) : InvocationHandler {// 方法级缓存:Key为方法签名+BaseUrlprivate val methodCache = ConcurrentHashMap<String, Method>()@Throws(Throwable::class)override fun invoke(proxy: Any, method: Method, args: Array<Any>?): Any {// 0. 特殊方法处理if (method.declaringClass == Any::class.java) {return method.invoke(this, *(args ?: emptyArray()))}// 1. 获取当前BaseUrlval baseUrl = UrlDecider.getBaseUrl(serviceClass)// 2. 获取方法签名val methodSignature = generateMethodSignature(method, args)// 3. 获取缓存的方法实例val realMethod = methodCache.getOrPut("$baseUrl|$methodSignature") {// 4. 获取真实服务实例val realService = RetrofitPool.getRealService(serviceClass, baseUrl)// 5. 反射获取真实方法realService::class.java.getMethod(method.name, *(args?.map { it::class.java }?.toTypedArray() ?: emptyArray()))}// 6. 执行真实方法return if (args == null) {realMethod.invoke(realService, emptyArray())} else {realMethod.invoke(realService, *args)}}private fun generateMethodSignature(method: Method, args: Array<Any>?): String {val params = args?.joinToString(",") { it::class.java.simpleName } ?: ""return "${method.name}($params)"}
}
3.6 ServiceManager 统一入口
// ServiceManager.kt
object ServiceManager {// 代理实例缓存private val proxyCache = ConcurrentHashMap<Class<*>, Any>()// 服务实例计数器(用于调试)private val instanceCount = AtomicInteger(0)/*** 获取服务代理实例*/@Suppress("UNCHECKED_CAST")fun <T> getService(serviceClass: Class<T>): T {return proxyCache.getOrPut(serviceClass) {Proxy.newProxyInstance(serviceClass.classLoader,arrayOf(serviceClass),ServiceProxy(serviceClass)).also {instanceCount.incrementAndGet()}} as T}/*** 切换环境*/fun switchEnvironment(env: EnvType) {EnvConfig.currentEnv = envEnvConfig.notifyEnvChanged()RetrofitPool.clearAll()clearProxyCache()}/*** 清除代理缓存(谨慎使用)*/fun clearProxyCache() {proxyCache.clear()}/*** 获取当前服务实例数(调试用)*/fun getInstanceCount() = instanceCount.get()
}// 扩展函数,简化服务获取
inline fun <reified T> ServiceManager.getService(): T {return getService(T::class.java)
}
四、使用示例
4.1 定义服务接口
// UserService.kt
interface UserService {@GET("/profile")suspend fun getProfile(): UserProfile@POST("/update")suspend fun updateProfile(@Body profile: UpdateProfileRequest): ApiResponse<Unit>
}// PayService.kt
interface PayService {@POST("/create")suspend fun createOrder(@Body request: CreateOrderRequest): ApiResponse<Order>@GET("/history")suspend fun getOrderHistory(@Query("page") page: Int,@Query("size") size: Int): ApiResponse<PagedData<Order>>
}
4.2 在 ViewModel 中使用
// UserViewModel.kt
class UserViewModel : ViewModel() {// 获取服务实例private val userService = ServiceManager.getService<UserService>()private val payService = ServiceManager.getService<PayService>()// 用户数据private val _userProfile = MutableStateFlow<UserProfile?>(null)val userProfile: StateFlow<UserProfile?> = _userProfile// 加载用户资料fun loadUserProfile() {viewModelScope.launch {try {val profile = userService.getProfile()_userProfile.value = profile} catch (e: Exception) {// 错误处理}}}// 创建订单fun createOrder(productId: String, amount: Double) {viewModelScope.launch {val request = CreateOrderRequest(productId, amount)val response = payService.createOrder(request)if (response.isSuccess) {// 处理成功逻辑} else {// 处理失败逻辑}}}
}
4.3 环境切换功能实现
// EnvSwitchActivity.kt
class EnvSwitchActivity : AppCompatActivity() {private lateinit var binding: ActivityEnvSwitchBindingoverride fun onCreate(savedInstanceState: Bundle?) {super.onCreate(savedInstanceState)binding = ActivityEnvSwitchBinding.inflate(layoutInflater)setContentView(binding.root)setupEnvRadioGroup()setupDebugInfo()}private fun setupEnvRadioGroup() {// 初始化选中状态binding.radioGroup.check(when (EnvConfig.currentEnv) {EnvType.DEV -> R.id.radio_devEnvType.TEST -> R.id.radio_testEnvType.STAGING -> R.id.radio_stagingEnvType.PROD -> R.id.radio_prod})// 切换监听binding.radioGroup.setOnCheckedChangeListener { group, checkedId ->val env = when (checkedId) {R.id.radio_dev -> EnvType.DEVR.id.radio_test -> EnvType.TESTR.id.radio_staging -> EnvType.STAGINGelse -> EnvType.PROD}if (env != EnvConfig.currentEnv) {ServiceManager.switchEnvironment(env)showToast("已切换到${env.desc}")updateDebugInfo()}}}private fun setupDebugInfo() {updateDebugInfo()// 添加环境变化监听EnvConfig.addEnvChangeListener {runOnUiThread { updateDebugInfo() }}}private fun updateDebugInfo() {binding.tvCurrentEnv.text = "当前环境: ${EnvConfig.currentEnv.desc}"binding.tvInstanceCount.text = "服务实例数: ${ServiceManager.getInstanceCount()}"binding.tvRetrofitCount.text = "Retrofit实例数: ${getRetrofitInstanceCount()}"}private fun showToast(message: String) {Toast.makeText(this, message, Toast.LENGTH_SHORT).show()}
}
五、高级优化策略
5.1 请求级 URL 覆盖
// UrlOverrideInterceptor.kt
class UrlOverrideInterceptor : Interceptor {override fun intercept(chain: Interceptor.Chain): Response {val originalRequest = chain.request()// 检查是否有URL覆盖注解val urlOverride = originalRequest.tag(Invocation::class.java)?.method()?.getAnnotation(UrlOverride::class.java)if (urlOverride != null) {val newUrl = originalRequest.url.newBuilder().host(urlOverride.host).apply {if (urlOverride.path.isNotEmpty()) {encodedPath(urlOverride.path)}}.build()val newRequest = originalRequest.newBuilder().url(newUrl).build()return chain.proceed(newRequest)}return chain.proceed(originalRequest)}
}@Target(AnnotationTarget.FUNCTION)
@Retention(AnnotationRetention.RUNTIME)
annotation class UrlOverride(val host: String,val path: String = ""
)// 使用示例
interface DebugService {@GET("/status")@UrlOverride(host = "debug.internal.com")suspend fun getDebugStatus(): ApiResponse<DebugStatus>
}
5.2 智能故障转移
// FailoverInterceptor.kt
class FailoverInterceptor : Interceptor {// 域名故障状态记录private val failureRecords = ConcurrentHashMap<String, Long>()private val lock = ReentrantLock()// 故障冷却时间(5分钟)private val COOL_DOWN_PERIOD = 5 * 60 * 1000Loverride fun intercept(chain: Interceptor.Chain): Response {val originalRequest = chain.request()val originalUrl = originalRequest.url.toString()try {return chain.proceed(originalRequest)} catch (e: IOException) {// 检查是否可降级val backupUrl = getBackupUrl(originalUrl) ?: throw elock.withLock {// 记录当前域名故障recordFailure(originalUrl)// 创建备份请求val backupRequest = originalRequest.newBuilder().url(backupUrl).build()return chain.proceed(backupRequest)}}}private fun recordFailure(url: String) {val host = getHostFromUrl(url)failureRecords[host] = System.currentTimeMillis()}private fun getBackupUrl(originalUrl: String): String? {val host = getHostFromUrl(originalUrl)val backupHost = getBackupHost(host) ?: return nullreturn originalUrl.replace(host, backupHost)}private fun getHostFromUrl(url: String): String {return Uri.parse(url).host ?: ""}private fun getBackupHost(host: String): String? {// 检查是否在冷却期val failureTime = failureRecords[host] ?: 0if (System.currentTimeMillis() - failureTime < COOL_DOWN_PERIOD) {return null}// 配置主备关系return when (host) {"api.example.com" -> "backup.api.example.com""pay.thirdparty.com" -> "pay-backup.thirdparty.com"else -> null}}
}
5.3 性能优化策略
// 优化后的 ServiceProxy
class OptimizedServiceProxy<T>(private val serviceClass: Class<T>) : InvocationHandler {// 二级缓存:服务实例缓存(按BaseUrl)private val serviceCache = ConcurrentHashMap<String, T>()// 方法缓存(按BaseUrl+方法签名)private val methodCache = ConcurrentHashMap<String, Method>()override fun invoke(proxy: Any, method: Method, args: Array<Any>?): Any {// 特殊方法处理if (method.declaringClass == Any::class.java) {return method.invoke(this, *(args ?: emptyArray()))}// 获取BaseUrlval baseUrl = UrlDecider.getBaseUrl(serviceClass)// 获取或创建服务实例val realService = serviceCache.getOrPut(baseUrl) {RetrofitPool.getRealService(serviceClass, baseUrl)}// 获取方法签名val signature = generateMethodSignature(method, args)val cacheKey = "$baseUrl|$signature"// 获取缓存方法val realMethod = methodCache.getOrPut(cacheKey) {realService::class.java.getMethod(method.name, *(args?.map { it::class.javaObjectType }?.toTypedArray() ?: emptyArray())}// 执行方法return if (args == null) {realMethod.invoke(realService)} else {realMethod.invoke(realService, *args)}}// ... generateMethodSignature 方法同上 ...
}
六、方案对比分析
方案 | 动态切换 | 多域名支持 | 性能 | 代码侵入性 | 维护成本 |
---|---|---|---|---|---|
BuildConfig 区分 | ❌ | ❌ | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐ |
接口注解方式 | ⚠️(部分) | ✅ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
拦截器动态替换 | ✅ | ⚠️(有限) | ⭐⭐ | ⭐ | ⭐⭐⭐⭐ |
本方案 | ✅ | ✅ | ⭐⭐⭐⭐ | ⭐ | ⭐⭐ |
方案优势总结:
- 完全解耦:业务代码与网络配置分离
- 动态生效:环境切换无需重启应用
- 高性能:多级缓存减少资源开销
- 扩展性强:支持复杂域名策略和故障转移
- 维护简单:配置集中管理,修改成本低
七、关键点总结
- 动态代理机制:在方法调用时动态确定 BaseUrl
- 实例池管理:复用 Retrofit 和 Service 实例
- 策略模式应用:URL 决策器支持复杂规则
- 缓存优化:多级缓存减少反射开销
- 监听机制:环境变化通知系统组件
- 故障转移:智能域名降级提升可用性
八、最佳实践建议
- 环境切换权限:生产环境屏蔽开发环境切换功能
- 域名配置中心化:从后端获取域名配置,实现动态更新
- 性能监控:添加网络请求监控和性能统计
- 缓存策略优化:根据业务场景调整缓存失效策略
- 单元测试覆盖:重点测试 URL 决策和环境切换逻辑
// 单元测试示例
class UrlDeciderTest {@Testfun `test user service url in dev env`() {EnvConfig.currentEnv = EnvType.DEVval url = UrlDecider.getBaseUrl(UserService::class.java)assertEquals("https://dev.api.example.com/user/v1", url)}@Testfun `test pay service url in prod env`() {EnvConfig.currentEnv = EnvType.PRODval url = UrlDecider.getBaseUrl(PayService::class.java)assertEquals("https://pay.thirdparty.com", url)}
}