在 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 区分 | ❌ | ❌ | ⭐⭐⭐⭐ | ⭐⭐ | ⭐⭐ |
接口注解方式 | ⚠️(部分) | ✅ | ⭐⭐⭐ | ⭐⭐⭐ | ⭐⭐⭐ |
拦截器动态替换 | ✅ | ⚠️(有限) | ⭐⭐ | ⭐ | ⭐⭐⭐⭐ |
本方案 | ✅ | ✅ | ⭐⭐⭐⭐ | ⭐ | ⭐⭐ |
方案优势总结:
- 完全解耦:业务代码与网络配置分离
- 动态生效:环境切换无需重启应用
- 高性能:多级缓存减少资源开销
- 扩展性强:支持复杂域名策略和故障转移
- 维护简单:配置集中管理,修改成本低
七、关键点总结
- 动态代理机制:在方法调用时动态确定 BaseUrl
- 实例池管理:复用 Retrofit 和 Service 实例
- 策略模式应用:URL 决策器支持复杂规则
- 缓存优化:多级缓存减少反射开销
- 监听机制:环境变化通知系统组件
- 故障转移:智能域名降级提升可用性
八、最佳实践建议
- 环境切换权限:生产环境屏蔽开发环境切换功能
- 域名配置中心化:从后端获取域名配置,实现动态更新
- 性能监控:添加网络请求监控和性能统计
- 缓存策略优化:根据业务场景调整缓存失效策略
- 单元测试覆盖:重点测试 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)
}
}