Retrofit 如何将 Java 接口转换为 HTTP 请求?其动态代理机制是如何实现的?

解读

面试官问的是“底层原理”,不是“怎么用”。国内大厂一面常问“Retrofit 怎么用注解配置”,二面/三面就会追问“接口都没实现类,怎么就发出请求了”。答不出动态代理、注解解析、ServiceMethod 缓存三件套,基本会被判定为“只会调 API”。因此答案必须体现:

  1. 运行时到底谁帮我们把接口方法变成 Call;
  2. 这个“代理对象”如何与 OkHttp 衔接;
  3. 为什么反射只发生在第一次,后续性能几乎无损耗;
  4. 结合国内网络环境(HTTPS 证书校验、网关统一加签、渠道包多域名)说明扩展点。

知识点

  1. Java 动态代理:Proxy.newProxyInstance 生成接口实现,InvocationHandler 统一拦截;
  2. Retrofit 内部三段式:
    a. 解析阶段——RequestFactory 解析方法注解、参数注解,构建 okhttp3.Request 模板;
    b. 缓存阶段——ServiceMethod 对象作为 key 缓存在 ConcurrentHashMap,避免二次解析;
    c. 执行阶段——InvocationHandler 把入参填到模板,生成 Call,默认走 OkHttpCall,再包一层 ExecutorCallbackCall 切线程;
  3. 注解分类:方法级(GET、POST、Headers、Url)、参数级(Path、Query、Field、Body、HeaderMap)、标记级(FormUrlEncoded、Multipart);
  4. 转换器机制:Converter.Factory 把 RequestBody/ResponseBody 和 Java 对象互转,国内常用 GsonConverterFactory、FastJsonConverterFactory,可自定义加密/解密 Converter;
  5. CallAdapter 机制:把默认 Call<T> 转成 RxJava、Coroutines、LiveData 等,国内项目常用 RxJava2CallAdapterFactory;
  6. 性能优化:反射只发生在 ServiceMethod 构建阶段,后续直接 method.invoke → ServiceMethod.invoke,无反射开销;
  7. 国内扩展:
    • 多域名切换——拦截器里动态替换 baseUrl;
    • 统一加签——自定义拦截器读取 RequestBody 二次加密;
    • 证书强校验——OkHttp 的 CertificatePinner;
    • 渠道包——Gradle 脚本根据 buildVariant 注入不同 baseUrl。

答案

Retrofit 在 create() 阶段通过 Proxy.newProxyInstance 为接口生成一个运行时代理对象。当业务代码调用接口方法时,所有请求都会进入 InvocationHandler#invoke。
步骤拆解:

  1. 第一次调用某方法时,Retrofit 会构建 ServiceMethod:
    a. RequestFactory 解析方法注解(GET、POST、Headers 等)和参数注解(Path、Query、Body 等),生成一个不带实参的 okhttp3.Request 模板;
    b. 根据返回类型选择 CallAdapter,根据注解中 @Body 类型选择 Converter;
    c. 把解析结果封装成 ServiceMethod 并缓存到 ConcurrentHashMap,key 为 Method 对象。
  2. 后续同一方法再次调用,直接命中缓存,无反射开销。
  3. invoke 时把实参填充到模板,生成真正的 okhttp3.Request,交给 OkHttpCall(实现了 retrofit2.Call),OkHttpCall 内部用 OkHttp 的 RealCall 执行网络 IO。
  4. 如果配置了 CallbackExecutor(Android 默认 MainThreadExecutor),结果会切回主线程再回调。
  5. 整个流程中,动态代理只做“接口→ServiceMethod→OkHttpCall”的转发,真正的网络、线程切换、数据转换全部委托给 OkHttp 和插件化的 Converter/CallAdapter,因此 Retrofit 本身非常轻量。

一句话总结:Retrofit 通过动态代理把“接口方法调用”变成“ServiceMethod 缓存 + 模板填充”,最终生成 OkHttp 的 Request,完成 HTTP 请求,全程只在首次解析阶段使用反射,后续零反射,性能等同于直接调 OkHttp。

拓展思考

  1. 如果公司网关要求“对所有 POST 体做 RSA+AES 混合加密”,如何零侵入实现?
    答:自定义 Converter.Factory,在 requestBodyConverter 里把原始 JSON 加密后返回新的 RequestBody;解密同理在 responseBodyConverter 里做,业务层无感知。
  2. 接口返回类型想直接拿到 Kotlin Coroutines 的 Deferred,需要做什么?
    答:引入 retrofit2-kotlin-coroutines-adapter,或自定义 CallAdapter,把 Call<T> 转成 Deferred<T>,内部用 suspendCancellableCoroutine 包装。
  3. 线上遇到“部分 4G 用户首次请求延迟 200 ms+”,如何定位是 DNS 还是 Retrofit 层?
    答:在 OkHttp 层加 EventListener,打印 dnsStart/dnsEnd、connectionAcquired、requestHeadersStart 时间戳;若 DNS 耗时占比高,可接入 HttpDns 或阿里云的 DNS 解析缓存。
  4. 国内厂商 ROM 对后台网络限制越来越严,如何在 Retrofit 层统一降级缓存?
    答:自定义 Call.Factory,在 invoke 里先判断网络状态,无网时直接返回本地缓存的 Response,有网再走正常流程,与 Cache-Control 头互补,实现“离线优先”。