Android 多 BaseUrl 动态切换策略(结合 ServiceManager 实现)

在 Android 开发中,如何优雅地实现多环境切换和动态域名管理?本文将深入探讨基于动态代理和 Retrofit 实例池的多 BaseUrl 解决方案,结合 ServiceManager 实现灵活高效的 URL 调度机制。

一、需求背景与痛点分析

1.1 常见场景

  • 多环境切换:开发(DEV)、测试(TEST)、预发布(STAGING)、生产(PROD)环境
  • 多域名管理:用户服务、支付服务、消息服务等使用不同域名
  • 动态降级:主域名失败时自动切换到备份域名
  • A/B 测试:不同用户群体使用不同的服务端点

1.2 传统方案痛点

  • 编译时锁定:通过 BuildConfig 区分环境,需重新编译才能切换
  • 全局单例限制:Retrofit 单例无法支持多域名
  • 代码侵入性强:URL 硬编码在接口定义中
  • 灵活性不足:运行时无法动态调整域名

二、核心架构设计

graph TD A[ServiceManager] -->|获取服务| B[动态代理] B -->|请求时| C[URL决策器] C -->|返回BaseUrl| D[Retrofit实例池] D -->|返回Retrofit实例| B B -->|执行请求| E[网络服务] F[环境切换] -->|更新| C G[配置中心] -->|提供规则| C

三、完整实现方案

3.1 依赖配置

gradle 复制代码
// 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 环境管理模块

kotlin 复制代码
// 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: EnvType
        get() = 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 决策器(核心)

kotlin 复制代码
// 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. 获取当前环境的基础URL
        val 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 实例池

kotlin 复制代码
// 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 动态服务代理

kotlin 复制代码
// ServiceProxy.kt
class ServiceProxy<T>(private val serviceClass: Class<T>) : InvocationHandler {
    
    // 方法级缓存:Key为方法签名+BaseUrl
    private 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. 获取当前BaseUrl
        val 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 统一入口

kotlin 复制代码
// 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 = env
        EnvConfig.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 定义服务接口

kotlin 复制代码
// 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 中使用

kotlin 复制代码
// 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 环境切换功能实现

kotlin 复制代码
// EnvSwitchActivity.kt
class EnvSwitchActivity : AppCompatActivity() {
    
    private lateinit var binding: ActivityEnvSwitchBinding
    
    override 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_dev
                EnvType.TEST -> R.id.radio_test
                EnvType.STAGING -> R.id.radio_staging
                EnvType.PROD -> R.id.radio_prod
            }
        )
        
        // 切换监听
        binding.radioGroup.setOnCheckedChangeListener { group, checkedId ->
            val env = when (checkedId) {
                R.id.radio_dev -> EnvType.DEV
                R.id.radio_test -> EnvType.TEST
                R.id.radio_staging -> EnvType.STAGING
                else -> 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 覆盖

kotlin 复制代码
// 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 智能故障转移

kotlin 复制代码
// FailoverInterceptor.kt
class FailoverInterceptor : Interceptor {
    
    // 域名故障状态记录
    private val failureRecords = ConcurrentHashMap<String, Long>()
    private val lock = ReentrantLock()
    
    // 故障冷却时间(5分钟)
    private val COOL_DOWN_PERIOD = 5 * 60 * 1000L
    
    override 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 e
            
            lock.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 null
        
        return 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] ?: 0
        if (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 性能优化策略

kotlin 复制代码
// 优化后的 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()))
        }
        
        // 获取BaseUrl
        val 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 区分 ⭐⭐⭐⭐ ⭐⭐ ⭐⭐
接口注解方式 ⚠️(部分) ⭐⭐⭐ ⭐⭐⭐ ⭐⭐⭐
拦截器动态替换 ⚠️(有限) ⭐⭐ ⭐⭐⭐⭐
本方案 ⭐⭐⭐⭐ ⭐⭐

方案优势总结

  1. 完全解耦:业务代码与网络配置分离
  2. 动态生效:环境切换无需重启应用
  3. 高性能:多级缓存减少资源开销
  4. 扩展性强:支持复杂域名策略和故障转移
  5. 维护简单:配置集中管理,修改成本低

七、关键点总结

  1. 动态代理机制:在方法调用时动态确定 BaseUrl
  2. 实例池管理:复用 Retrofit 和 Service 实例
  3. 策略模式应用:URL 决策器支持复杂规则
  4. 缓存优化:多级缓存减少反射开销
  5. 监听机制:环境变化通知系统组件
  6. 故障转移:智能域名降级提升可用性

八、最佳实践建议

  1. 环境切换权限:生产环境屏蔽开发环境切换功能
  2. 域名配置中心化:从后端获取域名配置,实现动态更新
  3. 性能监控:添加网络请求监控和性能统计
  4. 缓存策略优化:根据业务场景调整缓存失效策略
  5. 单元测试覆盖:重点测试 URL 决策和环境切换逻辑
kotlin 复制代码
// 单元测试示例
class UrlDeciderTest {
    
    @Test
    fun `test user service url in dev env`() {
        EnvConfig.currentEnv = EnvType.DEV
        val url = UrlDecider.getBaseUrl(UserService::class.java)
        assertEquals("https://dev.api.example.com/user/v1", url)
    }
    
    @Test
    fun `test pay service url in prod env`() {
        EnvConfig.currentEnv = EnvType.PROD
        val url = UrlDecider.getBaseUrl(PayService::class.java)
        assertEquals("https://pay.thirdparty.com", url)
    }
}
相关推荐
积跬步DEV4 小时前
Android 获取签名 keystore 的 SHA1和MD5值
android
陈旭金-小金子6 小时前
发现 Kotlin MultiPlatform 的一点小变化
android·开发语言·kotlin
二流小码农8 小时前
鸿蒙开发:DevEcoStudio中的代码提取
android·ios·harmonyos
江湖有缘9 小时前
使用obsutil工具在OBS上完成基本的数据存取【玩转华为云】
android·java·华为云
移动开发者1号10 小时前
Kotlin实现文件上传进度监听:RequestBody封装详解
android·kotlin
AJi13 小时前
Android音视频框架探索(三):系统播放器MediaPlayer的创建流程
android·ffmpeg·音视频开发
柿蒂14 小时前
WorkManager 任务链详解:优雅处理云相册上传队列
android
alexhilton14 小时前
使用用例(Use Case)以让Android代码更简洁
android·kotlin·android jetpack
峥嵘life14 小时前
Android xml的Preference设置visibility=“gone“ 无效分析解决
android·xml