一个类型安全的 HTTP 客户端 — 核心是动态代理 + 注解解析 + OkHttp,把 HTTP 请求变成 Kotlin/Java 接口调用
| 维度 | OkHttp | Retrofit |
|---|---|---|
| 定位 | 通用 HTTP 客户端 | 类型安全的 HTTP 客户端(上层封装) |
| 发起请求的方式 | 手动构造 Request→ Call→ execute() |
定义接口方法 → 直接调用 → 自动发请求 |
| 响应处理 | 手动解析 ResponseBody |
自动转换为声明类型的对象(通过 Converter) |
| 底层引擎 | 自己就是引擎(拦截器、连接池、缓存) | 底层复用 OkHttp(可通过 .client()传入自定义 OkHttpClient) |
.client(okHttpClient)传入自定义的 OkHttpClient 来实现。Retrofit 不替代 OkHttp,而是在 OkHttp 之上加了一层"接口化"的抽象。
用 Kotlin 写一个最简单的例子,感受 Retrofit 是怎么把接口方法变成 HTTP 请求的:
// 1. 定义接口 — 这就是你的 API 契约
interface GithubService {
@GET("users/{user}/repos")
fun listRepos(@Path("user") user: String): Call<List<Repo>>
}
// 2. 使用 — 接口方法调用自动变成 HTTP 请求
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
setContentView(R.layout.activity_main)
val retrofit = Retrofit.Builder()
.baseUrl("https://api.github.com/") // 注意是 https
.addConverterFactory(GsonConverterFactory.create()) // ← 必须添加!否则无法转换响应体
.build()
val service = retrofit.create(GithubService::class.java)
val repos: Call<List<Repo>> = service.listRepos("octocat")
// ↑ 这一行触发了什么?→ 进入动态代理的 invoke()
}
retrofit.create(GithubService::class.java)——它返回的不是你写的 GithubService 实现类,而是一个运行时动态生成的代理对象。当你调用 service.listRepos("octocat")时,这个调用被代理拦截,转化为 HTTP 请求发出去。
// 接口定义 — 返回 Call<T>
interface ApiService {
@GET("users/{user}/repos")
fun listRepos(@Path("user") user: String): Call<List<Repo>>
}
// 使用 — 异步回调
val call = service.listRepos("octocat")
call.enqueue(object : Callback<List<Repo>> { // 注意是 Callback,不是 CallBack
override fun onResponse(call: Call<List<Repo>>, response: Response<List<Repo>>) {
val data = response.body()
println("Response: ${data?.get(0)?.name}") // 注意空安全
}
override fun onFailure(call: Call<List<Repo>>, t: Throwable) {
val error = t.message
}
})
// 同步请求(不能在 Android 主线程!)
// val response = repos.execute()
execute()是同步方法,会阻塞当前线程,不能在 Android 主线程使用,否则会抛 NetworkOnMainThreadException。一般只用 enqueue()做异步请求。
// 接口定义 — 加上 suspend 关键字
interface ApiService {
@GET("users/{user}/repos")
suspend fun listRepos(@Path("user") user: String): Response<List<Repo>>
}
// 使用 — 在协程作用域中调用,写法像同步但实际是异步
viewModelScope.launch {
try {
val response = service.listRepos("octocat")
val data = response.body()
} catch (e: Exception) {
val error = e.message
}
}
try-catch,比回调中的 onFailure更自然call.cancel()当你调用 retrofit.create(ApiService.class)时,Retrofit 内部做了三件事:验证接口 → 创建动态代理 → 在 invoke 中转发方法调用。
public <T> T create(final Class<T> service) {
// 步骤1:验证服务接口是否合法
validateServiceInterface(service);
// 步骤2:通过动态代理创建接口的实现对象
return (T) Proxy.newProxyInstance(
service.getClassLoader(), // 类加载器
new Class<?>[] { service }, // 要实现的接口数组(这里只有一个元素:service)
new InvocationHandler() { // 步骤3:所有接口方法调用都会进这个 invoke
private final Object[] emptyArgs = new Object[0];
@Override
public @Nullable Object invoke(Object proxy, Method method, @Nullable Object[] args)
throws Throwable {
// 如果调的是 Object 的方法(toString/equals/hashCode),直接走正常调用
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args);
}
args = args != null ? args : emptyArgs;
Reflection reflection = Platform.reflection;
// 如果是 default 方法,走 default 方法的调用逻辑
// 否则:解析方法注解 → 构建 HTTP 请求 → 执行
return reflection.isDefaultMethod(method)
? reflection.invokeDefaultMethod(method, service, proxy, args)
: loadServiceMethod(service, method).invoke(proxy, args);
}
});
}
// 这里传入的 service 参数其实就是你调用 retrofit.create(ApiService::class.java) 传入的接口类
private void validateServiceInterface(Class<?> service) {
// 验证1:必须是接口,不能是类或抽象类
if (!service.isInterface()) {
throw new IllegalArgumentException("API declarations must be interfaces.");
}
// 验证2:接口和它所继承的所有父接口都不能有泛型参数
// 例如 interface Api<T> { ... } 是不允许的
Deque<Class<?>> check = new ArrayDeque<>(1);
check.add(service);
while (!check.isEmpty()) {
Class<?> candidate = check.removeFirst();
if (candidate.getTypeParameters().length != 0) {
StringBuilder message =
new StringBuilder("Type parameters are unsupported on ")
.append(candidate.getName());
if (candidate != service) {
message.append(" which is an interface of ").append(service.getName());
}
throw new IllegalArgumentException(message.toString());
}
// 递归检查父接口
Collections.addAll(check, candidate.getInterfaces());
}
// 如果开启了预加载(validateEagerly),则立即解析所有方法
if (validateEagerly) {
Reflection reflection = Platform.reflection;
for (Method method : service.getDeclaredMethods()) {
if (!reflection.isDefaultMethod(method)
&& !Modifier.isStatic(method.getModifiers())
&& !method.isSynthetic()) {
loadServiceMethod(service, method); // 预解析并缓存
}
}
}
}
这个方法的职责很纯粹:
validateEagerly = true时,在创建代理对象时就解析所有方法,方便及早发现注解错误// invoke 中的这段代码
if (method.getDeclaringClass() == Object.class) {
return method.invoke(this, args); // this = InvocationHandler 实例
}
这段代码的作用是:当调用代理对象的 Object 类方法(如 toString()、equals()、hashCode())时,不走 Retrofit 的网络请求逻辑,而是直接由 InvocationHandler 实例本身来处理这些方法调用。
method.invoke(this, args)而不是 method.invoke(proxy, args)?proxy是代理对象本身,如果你调用 proxy.toString(),它又会进入 invoke(),形成一个无限递归直到栈溢出。而 this指向的是 InvocationHandler 实例(一个普通对象),调用它的 toString()不会触发代理逻辑。
| 概念 | 一句话解释 | 在 Retrofit 中的角色 |
|---|---|---|
| 代理(Proxy) | 创建一个类,这个类实现了你给的接口,代理那些接口方法的实现 | retrofit.create()返回的 service 对象就是代理 |
| 动态代理(Dynamic Proxy) | 代理类在运行时动态生成,不是在编译时写好的 | 生成的代理类在 .java 源码中不存在,是 JVM 运行时生成的字节码 |
interface ApiService { @GET("...") fun get(): Call<T> }
Proxy.newProxyInstance()+ InvocationHandler+ ServiceMethod
OkHttpClient、拦截器、连接池、缓存
每一层各司其职:
既然代理对象的每个方法都只是转发给 handler.invoke(),那为什么不直接调用 handler.invoke()呢?为什么还要绕一圈通过 Proxy.newProxyInstance()生成代理对象?
答案是:Proxy 对象不是可有可无的中间人,它承担着四个不可替代的关键角色。
没有 Proxy 的写法(每次都要手动反射):
// 调用方必须自己处理反射,丑陋且不安全
InvocationHandler handler = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// 解析注解、构建请求、执行...
return executeHttpRequest(method, args);
}
};
// 每次调用都要手动反射!
Method method = GithubService.class.getMethod("listRepos", String.class);
Object result = handler.invoke(null, method, new Object[]{"octocat"});
// 问题1: 没有类型安全 — 编译器不检查方法名、参数类型
// 问题2: 没有 IDE 自动补全 — 你只能手写方法名字符串
// 问题3: 必须显式处理反射异常 — try-catch 满天飞
// 问题4: 暴露了底层实现 — 调用方知道 InvocationHandler 的存在
有 Proxy 的写法(Retrofit 的实际做法):
// create() 返回的代理对象实现了 GithubService 接口
val service: GithubService = retrofit.create(GithubService::class.java)
// 调用方像调用普通对象一样自然
val repos: Call<List<Repo>> = service.listRepos("octocat")
// 优势1: 类型安全 — 编译器检查方法名和参数
// 优势2: IDE 自动补全 — 输入 service. 就能看到所有 API 方法
// 优势3: 完全透明 — 调用者不知道背后有 InvocationHandler
// 优势4: 可以传递给任何需要 GithubService 类型的地方
| 价值 | 说明 | 没有 Proxy 的话 |
|---|---|---|
| 1 类型安全与接口一致性 | 代理对象实现了 GithubService接口,可以安全地传递给任何需要该类型的地方。编译器会检查方法名、参数类型。 |
调用方必须手动反射,方法名写错——编译期发现不了,运行时报错 |
| 2 隐藏底层转发细节 | 调用者完全无感知:只是调用了普通接口方法,不知道背后有 InvocationHandler在干活。 |
调用方必须显式构造 Method对象、处理反射异常,代码非常丑陋 |
| 3 标准设计模式 | Proxy.newProxyInstance是 Java 官方提供的统一机制。InvocationHandler是"策略",代理对象是"外壳"——两者配合:外壳保证接口契约,策略提供具体实现。 |
每个框架都要自己发明一套"接口 → 实现"的机制,不一致、不好维护 |
| 4 多接口支持 | 一个代理对象可以同时实现多个接口(new Class>[] { ApiA.class, ApiB.class }) |
InvocationHandler只是一个普通对象,不具备多接口类型 |
InvocationHandler这个通用处理器能够以特定接口的形态被调用,从而兼顾了灵活性与类型安全。InvocationHandler是"策略",代理对象是"外壳"——外壳保证接口契约,策略提供具体实现。
同一个接口方法会被调用成千上万次(比如 listRepos()每次下拉刷新都会调),但方法的注解信息(@GET、@Path、@Query)在编译后就不会变了。如果每次调用都重新解析一遍注解,就是白白浪费 CPU。
所以 Retrofit 用了一个 serviceMethodCache(本质是 ConcurrentHashMap),每个方法的解析结果只做一次,之后直接复用。
ServiceMethod<?> loadServiceMethod(Class<?> service, Method method) {
while (true) {
// 第一步:先从缓存里查
Object lookup = serviceMethodCache.get(method);
// 情况A:缓存里已经是解析好的 ServiceMethod → 直接返回(最快路径)
if (lookup instanceof ServiceMethod<?>) {
return (ServiceMethod<?>) lookup;
}
// 情况B:缓存里啥也没有 → 当前线程负责解析
if (lookup == null) {
Object lock = new Object();
synchronized (lock) {
// putIfAbsent:塞一个"锁对象"占坑,告诉其他线程"我正在解析"
lookup = serviceMethodCache.putIfAbsent(method, lock);
if (lookup == null) {
// 成功占坑!开始解析注解
ServiceMethod<Object> result;
try {
result = ServiceMethod.parseAnnotations(this, service, method);
} catch (Throwable e) {
// 解析失败 → 把占坑的锁移除,让其他线程有机会重试
serviceMethodCache.remove(method);
throw e;
}
// 解析完成 → 把结果替换掉占坑的锁
serviceMethodCache.put(method, result);
return result;
}
// 如果 putIfAbsent 返回了别人的锁 → 走下面的等待逻辑
}
}
// 情况C:缓存里是别人放的锁对象 → 说明另一个线程正在解析,等待它完成
synchronized (lookup) {
Object result = serviceMethodCache.get(method);
if (result == null) {
// 另一个线程解析失败了,当前线程重试
continue;
}
return (ServiceMethod<?>) result;
}
}
}
ConcurrentHashMap实现了一个无锁读 + 轻量级同步写的缓存。读操作完全无锁(get),只有写操作(putIfAbsent)需要同步,且同步粒度控制在单个方法级别(每个方法一把锁),不同方法之间不互相阻塞。
这个设计非常精巧:
$Proxy0),它实现了你的接口,所有方法调用都被转发到 InvocationHandler.invoke()。loadServiceMethod用 ConcurrentHashMap实现了无锁读 + 方法级同步写的缓存策略。| 组件 | 职责 | 类比 |
|---|---|---|
| 动态代理 | 拦截接口方法调用,转发到 InvocationHandler | 接线员 — 你拨一个号码(调方法),它帮你转接到正确的人 |
| 注解解析 | 把 @GET/@POST/@Path/@Query 翻译成 OkHttp Request | 翻译官 — 把你的"声明式描述"翻译成底层的 HTTP 构造指令 |
| OkHttp | 执行实际的 Socket 通信(TCP 连接、TLS 握手、HTTP 请求) | 快递员 — 真正把包裹送到目的地的人 |
| Converter | 请求体序列化 + 响应体反序列化(JSON ↔ 对象) | 包装工 — 把物品装进包裹 / 把包裹拆开取物品 |
| CallAdapter | 把底层 Call 适配成目标返回类型(Call / suspend / Flow / Observable) | 转接头 — 一个插头适配不同规格的插座 |
@Url、@HeaderMap等特殊注解