当前位置: 首页 > ds >正文

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

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

一、需求背景与痛点分析

1.1 常见场景

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

1.2 传统方案痛点

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

二、核心架构设计

获取服务
请求时
返回BaseUrl
返回Retrofit实例
执行请求
更新
提供规则
ServiceManager
动态代理
URL决策器
Retrofit实例池
网络服务
环境切换
配置中心

三、完整实现方案

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 区分⭐⭐⭐⭐⭐⭐⭐⭐
接口注解方式⚠️(部分)⭐⭐⭐⭐⭐⭐⭐⭐⭐
拦截器动态替换⚠️(有限)⭐⭐⭐⭐⭐⭐
本方案⭐⭐⭐⭐⭐⭐

方案优势总结

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

七、关键点总结

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

八、最佳实践建议

  1. 环境切换权限:生产环境屏蔽开发环境切换功能
  2. 域名配置中心化:从后端获取域名配置,实现动态更新
  3. 性能监控:添加网络请求监控和性能统计
  4. 缓存策略优化:根据业务场景调整缓存失效策略
  5. 单元测试覆盖:重点测试 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)}
}
http://www.xdnf.cn/news/14550.html

相关文章:

  • 微信小程序使用computed
  • XR-RokidAR-ADB环境搭建
  • 机器学习:开启智能时代的大门
  • Django 5.2.3 构建的图书管理系统
  • SpringCloud Alibaba场景实践(Nacos篇)
  • WSL2 中安装 cuDNN​​ 的完整指南
  • Arduino入门教程:5、按键输入
  • 贝塞尔曲线的切矢量
  • 分割数据集 - 足球运动员分割数据集下载
  • 关于 使用 GPT 自动生成反调试代码详解
  • 手机SIM卡通话中随时插入录音语音片段(Windows方案)
  • NLP语言发展路径分享
  • Good Start/Smilo and Minecraft
  • 大数据集群架构hadoop集群、Hbase集群、zookeeper、kafka、spark、flink、doris、dataease(四)
  • Oracle 逻辑结构与性能优化(上)
  • Softhub软件下载站实战开发(三):平台管理模块实战
  • 第9章:Neo4j集群与高可用性
  • SpringBoot学习day3-SpringBoot注解开发(新闻项目后段基础)
  • Java中的CAS与ABA
  • Leetcode 刷题记录 14 —— 回溯
  • 什么是装饰器?
  • UE5错误 Linux离线状态下错误 circular dependency detected;includes/requires
  • chapter06-针对分类的微调
  • 实战指南:部署MinerU多模态文档解析API与Dify深度集成(实现解析PDF/JPG/PNG)
  • 【RAG文档解析】深度剖析 PDF 解析的痛点与方案
  • springboot集成dubbo
  • LangChain调用本地modelscope下载的Deepseek大模型
  • Python打卡第54天
  • 13分钟讲解主流Linux发行版
  • origin绘制双Y轴柱状图、双Y轴柱状点线图和双Y轴点线图