Android 架构之网络框架多域名配置<三>

17 阅读2分钟

日常项目中遇到除本公司接口还需要对接其他的三方业务的接口,这就要求网络请求框架需要支持不同BaseUrl域名的动态配置,否则就需要再单独写一套特殊的网络请求.

1.问题

多域名请求网络数据(域名/返回数据结构不可控)

解决方案梳理:

  • 特殊请求单独请求
  • 项目框架支持多域名配置(采取这个方案)

2.项目框架支持多域名配置实现

思路:

配置多域名-->Api配置Header --->拦截器动态判断

开发环境:

  • Kotlin
  • Retrofit
  • Okhttp

2.1 配置动态域名

fun initFramework() {
    val config = NetConfig.Builder()
        .setBaseUrl("https://api.example.com/") // 主的
        .putBaseUrl("google", "https://google.com/") // 演示动态域名
        .putBaseUrl("github", "https://api.github.com/") // 演示动态域名
        .setConnectTimeout(15L)
        .setReadTimeout(20L)
        .setWriteTimeout(20L)
        .setDebugLogsEnabled(true)
        .addDefaultHeader("Global-Version", "1.0.0")
        .build()
        
    NetManager.init(config)
}
package com.wkq.net.core

import com.wkq.net.config.NetConfig
import com.wkq.net.interceptor.HeaderInterceptor

/**
 * 高级网络框架的入口点和配置持有者。
 * 应用必须在初始化时调用 NetManager.init() 以正确配置框架。
 */
object NetManager {
    
    // 网络配置对象
    private var config: NetConfig? = null
    
    // 全局请求头拦截器
    lateinit var headerInterceptor: HeaderInterceptor
        private set

    /**
     * 使用自定义 NetConfig 配置初始化网络框架。
     * 如果多次调用,则记录日志并返回(防止重复初始化)。
     */
    fun init(netConfig: NetConfig) {
        if (config != null) {
            // 已初始化。跳过或抛出错误。
            return
        }
        this.config = netConfig
        
        // 基于默认配置初始化全局 HeaderInterceptor
        headerInterceptor = HeaderInterceptor(netConfig.defaultHeaders).apply {
            // 如果需要,可在此处添加进一步逻辑
        }
    }

    /**
     * 获取当前活动的配置。
     * 如果在 init() 之前使用,将抛出错误。
     */
    fun getConfig(): NetConfig {
        return config ?: throw IllegalStateException("在 Application 中使用网络框架前,必须先调用 NetManager.init() 初始化配置。")
    }
}

2.2 配置Header

interface ThirdPartyService {
    @Headers("${HeaderInterceptor.HEADER_BASE_URL_KEY}:google")
    @GET("search")
    fun searchGoogle(@Query("q") query: String): Call<ResponseBody>

    @Headers("${HeaderInterceptor.HEADER_BASE_URL_KEY}:github")
    @GET("users/{user}/repos")
    fun getGithubRepos(@Path("user") user: String): Call<ResponseBody>

    // 5. 演示返回非 BaseResponse 格式 (例如直接返回 Any 或自定义 Bean)
    @Headers("${HeaderInterceptor.HEADER_BASE_URL_KEY}:google")
    @GET("search")
    fun searchGoogleAny(@Query("q") query: String): Call<Any>
}

2.3 拦截器动态处理Header

package com.wkq.net.interceptor

import com.wkq.net.core.NetManager
import okhttp3.HttpUrl.Companion.toHttpUrlOrNull
import okhttp3.Interceptor
import okhttp3.Response
import java.util.concurrent.ConcurrentHashMap

/**
 * 负责全局将请求头注入到每个 OkHttp 请求中的拦截器。
 * 支持在运行时动态添加或移除请求头。
 * 同时支持通过 "BaseUrl-Key" 请求头动态切换 BaseUrl。
 */
class HeaderInterceptor(defaultHeaders: Map<String, String>) : Interceptor {

    companion object {
        const val HEADER_BASE_URL_KEY = "BaseUrl-Key"
    }

    private val dynamicHeaders = ConcurrentHashMap<String, String>()

    init {
        // 使用 NetConfig 提供的默认请求头初始化
        dynamicHeaders.putAll(defaultHeaders)
    }

    override fun intercept(chain: Interceptor.Chain): Response {
        val originalRequest = chain.request()
        val requestBuilder = originalRequest.newBuilder()

        // 应用所有动态请求头
        dynamicHeaders.forEach { (key, value) ->
            requestBuilder.header(key, value)
        }

        // --- 动态切换 BaseUrl 逻辑 ---
        val domainKey = originalRequest.header(HEADER_BASE_URL_KEY)
        if (!domainKey.isNullOrEmpty()) {
            val baseUrls = NetManager.getConfig().baseUrls
            val newBaseUrl = baseUrls[domainKey]
            if (!newBaseUrl.isNullOrEmpty()) {
                val newHttpUrl = newBaseUrl.toHttpUrlOrNull()
                if (newHttpUrl != null) {
                    val oldHttpUrl = originalRequest.url
                    val finalUrl = oldHttpUrl.newBuilder()
                        .scheme(newHttpUrl.scheme)
                        .host(newHttpUrl.host)
                        .port(newHttpUrl.port)
                        .build()
                    requestBuilder.url(finalUrl)
                }
            }
            // 移除用于切换的辅助 Header,避免发送到服务端
            requestBuilder.removeHeader(HEADER_BASE_URL_KEY)
        }

        return chain.proceed(requestBuilder.build())
    }

    /**
     * 动态添加请求头
     */
    fun addHeader(key: String, value: String) {
        dynamicHeaders[key] = value
    }

    /**
     * 动态移除请求头
     */
    fun removeHeader(key: String) {
        dynamicHeaders.remove(key)
    }

    /**
     * 清空所有请求头
     */
    fun clearHeaders() {
        dynamicHeaders.clear()
    }
}

3:测试网络请求

private fun performRawRequest() {
    binding.tvResult.text = "正在请求 GitHub (Any) ..."
    
    // 使用协程演示 awaitRawResult()
    lifecycleScope.launch {
        demoService.getGithubUserAny().awaitRawResult()
            .onSuccess { data ->
                binding.tvResult.text = """
                    [请求成功]
                    模式: Raw API (awaitRawResult)
                    返回类型: ${data?.javaClass?.simpleName}
                    内容: $data
                """.trimIndent()
            }
            .onError { code, message ->
                binding.tvResult.text = "[请求失败]\n错误码: $code\n消息: $message"
            }
    }
}

总结

配置动态域名 --> 请求配置Header --> 拦截器动态替换域名

注意:

  • 不同域名的数据结构不同
  • 不同域名返回Code需要特殊处理

通过这种方式 就能动态的支持Retrofit的域名的动态配置,方便日常开发过程中网络请求一套代码兼容多个域名的网络请求.