聊到 Retrofit 的源码解析,我已经在前面写了四篇:从基础用法、源码调用链梳理、注解机制分析,到请求执行的完整流程。基本上“一个接口方法是如何变成一次真实网络请求”的脉络已经清楚了。这第五篇我想换个角度聊,把注意力从“流程”转向“结构”,系统拆一拆 Retrofit 这个开源网络库在设计层面用到的那些设计模式。
其实刚接触源码那阵子,我也抱着“找模式”的心态去一行行扫代码,结果越扫越乱:代理、适配器、工厂、建造者……每个名词都认识,但就是串不起来。后来才意识到,设计模式在 Retrofit 里从来不是硬贴上去的装饰品,而是每一个核心机制自然长出来的产物。动态代理解决了“接口即API”的开发体验,适配器解决了返回类型不可控的问题,转换器解决了数据格式和核心框架的耦合,Builder 则是配置项爆炸后的必然选择。
这篇文章适合这样几类人读:已经把 Retrofit 用得挺熟、但一直没想明白它内部长什么样的朋友;读过前面几篇、想更进一步理解代码结构的同学;以及正打算在业务代码里“偷师”一些设计思路的开发者。我会直接从源码中出现的模式切入,一边拆代码,一边讲清楚“为什么偏偏是它”。
1. Retrofit 中的设计模式全景:先看见森林,再研究树木
1.1 一句话看懂 Retrofit 的工作方式
Retrofit 做的事情,用一句话讲就是:把你声明的一个 Java 接口,翻译成对底层网络请求库的调用,再把返回结果按你想要的形态交给你。
这里有两层翻译。第一层是“从接口方法到请求描述”,这一层靠的是动态代理加注解解析;第二层是“从底层请求对象到上层返回类型”,这一层靠的是适配器(CallAdapter)和转换器(Converter)。而把这些零件全部组装起来的骨架,则大量依赖 Builder、工厂方法这类创建型模式来降低零件之间的耦合。
所以你会发现一个很有意思的现象:Retrofit 里几乎没有一个设计模式是孤立的。动态代理在工作时,需要调用 ServiceMethod 的解析逻辑,而 ServiceMethod 里面躺着 CallAdapter 和 Converter;Builder 建造出来的 Retrofit 对象,又把这些 Factory 通过平台单例统一管理。模式之间互相配合,才形成了这个库完整的扩展能力。
1.2 模式清单与职责对照表
先把整篇文章要讲的内容摆到桌面上,这样后续分析起来有明确坐标。
| 设计模式 | 出现位置 | 解决的核心问题 |
|---|---|---|
| 动态代理 | create()方法 | 让调用方只写接口就能触发请求,避免为每个接口生成手写实现类 |
| 建造者模式 | Retrofit.Builder | 参数多、可选参数多,防止构造函数难以阅读和扩展 |
| 外观模式 | Retrofit 对象本身 | 对上层屏蔽 OkHttp 的线程调度、连接池、重试等细节 |
| 适配器模式 | CallAdapter | 把统一的Call对象适配成 RxJava、协程等不同返回形态 |
| 策略模式 | Converter | 序列化/反序列化算法可替换,比如 Gson 换 Moshi |
| 工厂模式 | CallAdapter.Factory、Converter.Factory | 解耦适配器与转换器的创建过程,让用户自由插入扩展 |
| 单例模式 | Platform.get() | 避免重复检测运行平台,全局共享平台相关配置 |
这张表是这篇文章的地图。下面几节我会把前面几项单独剖开,并把容易混淆的点专门拿出来对比。
1.3 看源码时的一个心态建议
读设计模式,最忌讳的是“为了找模式而找模式”。你真的去翻 Retrofit 源码,会发现很多地方它只是长得像某种模式,或者只用了模式的一部分。比如有人会说 Retrofit 里也用到了装饰器模式,因为某些 Converter 会额外包装一层,但 Retrofit 本身并没有刻意去实现装饰器。这种地方不需要硬扣。
带着“这个类为什么长这样”的问题去读,比带着“这是什么模式”的问题去读要有效得多。模式是结果,不是原因。
2. 动态代理模式:Retrofit 的发动机藏在一行代码里
2.1 create() 方法是怎么把接口变成请求的
动态代理是 Retrofit 整个库最核心的一环,也是最容易让新手读源码时卡住的地方。核心代码其实只有一段:
public <T> T create(final Class<T> service) { // 1. 校验传入的 class 必须是一个接口 validateServiceInterface(service); // 2. 通过 JDK 动态代理生成接口的代理对象 return (T) Proxy.newProxyInstance( service.getClassLoader(), new Class<?>[]{service}, new InvocationHandler() { private final Platform platform = Platform.get(); @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { // 代理 Object 类的方法时直接转发,避免死循环 if (method.getDeclaringClass() == Object.class) { return method.invoke(this, args); } // 这是最关键的调用入口,把接口方法转换成请求 ServiceMethod<Object, Object> serviceMethod = loadServiceMethod(method); OkHttpCall<Object, Object> okHttpCall = new OkHttpCall<>(serviceMethod, args); return serviceMethod.callAdapter.adapt(okHttpCall); } }); }第一次看这段代码的人通常会疑惑:接口没有实现类,Proxy.newProxyInstance到底生成了什么?
可以把它理解成一个“转发器”。当我们调用api.getUser("123")时,因为api是代理对象,这个调用不会进入任何真实实现类,而是被InvocationHandler.invoke截获。method就是getUser这个方法对象,args就是"123"这个参数。接下来,loadServiceMethod会把方法的注解、参数注解全部解析成一份“请求说明书”,然后交给OkHttpCall去真正执行。
这里有一个很关键的细节:validateServiceInterface里会检查这个接口是否继承了其他接口,如果有额外的接口继承,会直接抛出异常。原因是 Retrofit 希望接口声明足够“干净”,这样解析注解时不会因为继承链而产生歧义。
2.2 代理流程拆解:从方法调用到 Call 对象
动态代理之后,完整的调用链是这样的:
- 调用方执行代理对象上的方法。
invoke里根据method从缓存中取ServiceMethod;第一次调用时创建并解析注解、参数、返回类型。- 用
ServiceMethod和实际参数args构建OkHttpCall。 - 通过
CallAdapter.adapt()把OkHttpCall包装成调用方期望的返回类型。 - 如果是同步请求,直接返回结果;如果是异步请求,返回一个带回调的包装对象。
我在前几篇里详细拆过ServiceMethod的构造过程,这里只强调一个点:ServiceMethod是线程安全的,创建之后可以被多个请求共享;而OkHttpCall是每次调用都新建的,因为它包含一次请求的完整参数状态。动态代理在这里的价值,就是让接口方法到请求对象之间的映射自动化了。
2.3 如果不用动态代理,会怎么样
理解了动态代理的价值,最好的方式是做一个反事实实验:如果 Retrofit 不用动态代理,开发者需要做什么?
常规做法是为每个 API 接口写一个实现类,每个方法里手动解析注解、拼接 URL、构建请求。这样有两个致命问题:一是一百个接口就要维护一百个模板代码几乎一样的类,代码量爆炸;二是新加一个接口时,很容易漏掉某些注解处理逻辑,出问题只能靠运行时慢慢找。
动态代理把“接口定义”和“逻辑实现”彻底分开。有接口就有代理,方法的数量不影响核心框架的设计,开发者体验也保持在“只写声明”这个极简状态。这种做法的另一层好处是,接口成了模块之间的契约。调用方看到的永远是接口,不关心底层实现;而底层实现也可以在不改接口的前提下替换,扩展性非常强。
3. 构建者模式与外观模式:把复杂配置和底层网络栈都“藏”起来
3.1 Retrofit.Builder 是如何收拢参数的
Retrofit 的构造方式在开源库中极具代表性,人人都写过这样一段代码:
Retrofit retrofit = new Retrofit.Builder() .baseUrl("https://api.example.com/") .client(new OkHttpClient()) .addConverterFactory(GsonConverterFactory.create()) .addCallAdapterFactory(RxJava2CallAdapterFactory.create()) .build();Builder的存在看起来顺理成章,但它并不是单纯为了“链式调用好看”。让我们看看build()方法里做了什么:
public Retrofit build() { if (baseUrl == null) { throw new IllegalStateException("Base URL required."); } okhttp3.Call.Factory callFactory = this.callFactory; if (callFactory == null) { callFactory = new OkHttpClient(); } Executor callbackExecutor = this.callbackExecutor; if (callbackExecutor == null) { callbackExecutor = platform.defaultCallbackExecutor(); } List<CallAdapter.Factory> callAdapterFactories = new ArrayList<>(this.callAdapterFactories); callAdapterFactories.addAll(platform.defaultCallAdapterFactories(callbackExecutor)); List<Converter.Factory> converterFactories = new ArrayList<>(1 + this.converterFactories.size() + platform.defaultConverterFactoriesSize()); // 补充默认转换工厂 converterFactories.add(new BuiltInConverters()); converterFactories.addAll(this.converterFactories); converterFactories.addAll(platform.defaultConverterFactories()); return new Retrofit(...); }这里透露出 Builder 的真正价值:它负责把“用户可能遗漏的配置”用默认值补齐,把必填项在构建时立刻检查,而不是等到第一次发请求时才报错。baseUrl必填、callFactory默认新建OkHttpClient、callbackExecutor默认用主线程执行器,这些规则全部集中在build()里,既好读又好维护。
3.2 为什么配置类几乎都选择 Builder
如果你写过一个参数特别多的类,一定遇到过这样的痛苦:构造函数维护六七个参数,每个参数可能为 null,调用方根本分不清哪个是必填哪个是选填。用重载组合?三四个参数已经能整出一堆重载方法,扩展到十个参数时基本没法看。
Builder 模式解决的不只是“参数很多”,而是“有些参数必填、有些参数选填、某些参数之间还有隐含依赖”的问题。比如 Retrofit 里,如果你不指定callFactory,它会默认创建OkHttpClient;如果你指定了,它就不会重复创建。这种逻辑放在构造函数里会非常笨重,放在 Builder 里就非常自然。
另外一个值得学习的地方是 Builder 的校验时机。很多团队写 Builder 只把它当“setter 的链式写法”,真正的校验却放到使用阶段才做,这样其实没有享受 Builder 的全部好处。Retrofit 的做法是在build()阶段把所有必填项与默认值处理完,构造出来的对象一定是完整可用的,这种思路可以直接借鉴到业务配置类中。
3.3 外观模式:Retrofit 作为门面,替你挡住了 OkHttp
说完 Builder,再往深一层看:我们拿着构建好的 Retrofit 对象发请求时,其实是在和一个“门面”打交道。Retrofit 对外暴露的接口非常简洁:create()、newBuilder(),以及内部的callFactory、callbackExecutor、baseUrl等,但背后真正干活的是 OkHttp。
这里就是典型的外观模式:上层调用者不需要关心 OkHttp 的连接池怎么管理、线程池怎么调度、重试拦截器怎么挂载,只需要说一句“我要请求这个接口,返回这个类型”,剩下的复杂度全部被 Retrofit 挡住。
外观模式和代理模式在表面上有点像,但意图完全不同。代理模式控制访问、增加额外逻辑;外观模式统一简化入口。Retrofit 不是 OkHttp 的代理,而是 OkHttp 的“前台接待员”。我见过不少初学者把这一点搞混,等到下一节我会专门把这类容易混淆的点挑出来聊清楚。
4. 适配器模式与工厂模式:让返回类型和序列化方案彻底解耦
4.1 CallAdapter 到底在适配什么
看 Retrofit 源码时,CallAdapter可能是除了动态代理之外第二个值得停下来细品的接口。它的定义非常短:
public interface CallAdapter<R, T> { Type responseType(); T adapt(Call<R> call); }responseType()返回的是服务器响应体解析后的目标类型,比如User;adapt()接收一个Call<R>,返回一个期望类型T。
这玩意的核心价值在于:Retrofit 的核心请求流程只需要维护一个Call对象,至于这个Call最终是变成同步返回值、RxJava 的Observable、协程的挂起函数,还是 Guava 的ListenableFuture,全部交给CallAdapter去适配。
举一个最直观的例子。默认情况下,你写接口方法返回Call<User>,Retrofit 的DefaultCallAdapterFactory会直接把原Call返回;如果你引入 RxJava 并配置了RxJava2CallAdapterFactory,方法返回类型可以写成Observable<User>,这时候底层OkHttpCall会被包装成Observable。上层调用方式完全不同,但请求执行、参数解析、返回数据反序列化这部分逻辑一行都不用改。
这带来的好处是巨大的:第三方可以针对自己的异步体系写适配器,而不需要改动 Retrofit 本身。
4.2 Converter:策略模式的一场优雅实践
Converter和CallAdapter经常被搞混,但它俩解决的问题完全不在一个维度。看接口定义更清楚:
public interface Converter<F, T> { T convert(F value) throws IOException; abstract class Factory { public abstract Converter<F, T> responseBodyConverter( Type type, Annotation[] annotations, Retrofit retrofit); public abstract Converter<F, T> requestBodyConverter( Type type, Annotation[] parameterAnnotations, Annotation[] methodAnnotations, Retrofit retrofit); } }Converter管的是“怎么把 HTTP 的请求体、响应体转换成 Java 对象”。用 Gson 就用GsonConverterFactory,用 Moshi 就换MoshiConverterFactory,用 Protobuf 就再换一个。核心框架不关心你用哪种 JSON 库,只要实现框架规定的Converter接口就行。
这其实就是策略模式:定义一组算法(序列化/反序列化),每种算法封装成独立的类,运行时动态切换。Retrofit 在构建时把用户传入的所有Converter.Factory串成一个列表,当需要解析某个类型时,依次询问每个工厂“能不能处理这个类型”,第一个能处理的工厂负责产出对应的Converter。
这种设计的妙处在于,数据格式不再与代码逻辑耦合。同一个Retrofit对象,只换一个 Converter 工厂,就能让整个 App 的数据协议从 JSON 切到 XML,代价非常小。
4.3 Factory 是工厂模式的骨架
不管是CallAdapter.Factory还是Converter.Factory,背后都有一套工厂模式的骨架在支撑。Factory本身不直接创建用户要的那个对象,而是定义了一个统一入口,让各个实现类去决定具体创建什么。
以CallAdapter.Factory为例,它可以拿到接口方法的返回类型returnType、方法上的注解annotations、以及当前的Retrofit实例。有了这些信息,不同的Factory就能做不同的判断:RxJava 的工厂看到返回类型是Observable<T>就返回对应的适配器;协程的工厂看到最后一个参数是Continuation就创建挂起函数的适配器;谁都不认识这个返回类型,就返回 null,让列表里的下一个工厂去尝试。
这种“责任链+工厂方法”的组合,本质上是一个高度可扩展的插槽机制。用户代码可以通过addCallAdapterFactory往链上挂自定义工厂,从而实现任意返回类型,这个过程完全没有侵入核心代码。
到这里可以提炼一句:Retrofit 的核心框架其实短小精悍,剩下的全是扩展点。而扩展点能保持优雅的核心原因,就是工厂模式把“对象创建”这件事从业务逻辑里彻底剥离了。
5. 读源码最容易掉进去的几个误区
5.1 误区一:动态代理是在代理 OkHttp
这个误解我见过太多次。一些文章会把 Retrofit 的动态代理解释成“代理了 OkHttp 的调用”,听起来顺耳,实际上不准确。动态代理的对象是开发者定义的接口,代理的入口是接口方法调用;而 OkHttp 是底层真正执行网络请求的,它本身并没有被 Retrofit 的InvocationHandler拦截。
正确的理解是:动态代理把“接口方法”这一层抽象掉了,让调用方感觉不到实现类的存在。OkHttp 在这里只是被OkHttpCall调用,两者不是代理关系,而是组合关系。
5.2 误区二:CallAdapter 与 Converter 分不清
不少朋友面试时被问“CallAdapter 和 Converter 有什么区别”,第一反应都是“两个都是用来处理返回数据的吧”。这个答案很容易被追问到底。
CallAdapter处理的是调用形态,决定“请求之后拿到的容器是什么”,比如Call、Observable、suspend;Converter处理的是数据格式,决定“容器里面的内容怎么反序列化”,比如 JSON 字符串变User对象。
可以这样记:CallAdapter管外层包装,Converter管内层解析。外层是同步是异步、是响应式还是挂起函数,跟数据格式没有任何关系;同样,数据格式用 Gson 还是 Moshi,也可以独立于外层容器单独切换。
5.3 误区三:把“使用了模式”当成“实现了模式”
有些读者看完源码会写“Retrofit 使用了装饰器模式、观察者模式”之类的结论,但真拿源码去核对时,往往找不到对应的接口和实现。
比如观察者模式,Retrofit 的异步回调是通过Callback.enqueue实现的,但这只是借助了 OkHttp 的异步回调机制,Retrofit 本身并没有订阅/发布模型。再比如装饰器模式,某些 Converter 确实会叠加额外处理逻辑,但这是为了满足接口组合,并不是刻意实现装饰器。
做源码分析时,分清“直接实现”和“间接涉及”很重要。否则容易把知识讲串,也容易在面试中一问到细节就露馅。我自己的原则是:只讲源码里存在明确结构和明确意图的模式,模糊地带宁可一笔带过,也不要强行往经典模式上贴标签。
6. 从 Retrofit 的设计模式中能搬到业务里的实战套路
6.1 场景一:用 Builder 管理多环境配置
Android 开发里经常遇到这个问题:项目有开发、测试、生产三套环境,每套环境的 baseUrl、日志开关、超时时间都不一样,于是不少人写了一个AppConfig类,里面一堆 public 静态变量,到处都能改。
参照 Retrofit.Builder 的做法,可以建一个EnvironmentConfig.Builder,把环境名、baseUrl、日志开关设为必填项,把超时时间、重试次数设为选填项,并在build()阶段完成默认值填充和参数合法性校验。这样整体配置不仅可读性强,还能保证运行时拿到的对象一定是完整可用的。
6.2 场景二:用策略模式切换加解密方案
业务中经常遇到加解密方案需要切换的情况。比如接口协议从明文改成 AES,再改成 RSA+AES 混合;如果用 if-else 到处判断,每次改动都要牵动一串调用方。
模仿 Converter 的思路,定义一个加密策略接口,不同实现类放在一起,运行时通过一个工厂类根据协议版本返回对应实现。调用方永远面向策略接口,不关心具体算法。这样既符合开闭原则,也方便做单元测试:测试时注入一个“不加密”的假实现即可。
6.3 场景三:用适配器模式统一回调形态
业务层经常踩到另一个坑:异步任务有时候用回调,有时候返回Future,有时候用协程,代码风格很不统一。
定义一个简单的适配接口:
public interface TaskAdapter<R, T> { T adapt(Task<R> task); }每个业务场景自己实现适配逻辑,比如把回调风格的Task适配成协程风格的suspend结果。这样做的好处是,底层任务执行逻辑不需要改,上层调用方式却可以按场景灵活变化。那种“底层随手换,上层无感知”的效果,我在实践里觉得特别受用,也正是从 Retrofit 的CallAdapter上偷师来的。
最后再说点个人体会。Retrofit 里的设计模式真正打动我的,不是哪个模式本身多精彩,而是每个模式都被用在了“最合适”的位置上:动态代理换来了接口声明式编程,Builder 换来了配置的可读性,Adapter 和 Converter 换来了生态扩展能力。模式从来不是背出来的,而是从需求里长出来的。以后你读源码,不妨先问一个问题:作者在这里遇到的最大麻烦是什么?想清楚这个,很多设计模式不用刻意认,你自己就能推导出来。