4. 自定义拦截器的基本实现:从 interface 到实战
说到 OkHttp 的拦截器,很多人第一反应就是 Retrofit 配合日志拦截器。但拦截器真正的威力,在于你可以插入任意一层逻辑。今天我们就聊聊怎么自己写一个拦截器。
Interceptor 接口,重写 intercept() 方法,调用 chain.proceed() 把请求往下传。
4.1 拦截器接口长什么样?
OkHttp 的拦截器核心就一个接口——Interceptor。里面只有一个方法:
public interface Interceptor {
Response intercept(Chain chain) throws IOException;
}
嗯,就这么简单。参数 Chain 里藏着请求链,你只需要拿到它的 request() 做修改,然后 chain.proceed(request) 把修改后的请求传下去,最后拿到返回的 Response 就可以处理了。
我记得刚接触 OkHttp 时,一直以为拦截器很复杂。其实说白了,就是个「中间人」——请求经过你手里,你可以改,可以拦,可以记日志,甚至可以重试。
@Nullable 或 @NonNull 注解,方便团队其他人看懂意图。
4.2 一个最简单的自定义拦截器
先上代码,咱们直接搞个给请求加 Header 的拦截器:
public class AuthInterceptor implements Interceptor {
private final String ;
public AuthInterceptor(String ) {
this. = ;
}
@Override
public Response intercept(Chain chain) throws IOException {
// 1. 拿到原始请求
Request originalRequest = chain.request();
// 2. 构建新请求,加个 Authorization Header
Request newRequest = originalRequest.newBuilder()
.header("Authorization", "Bearer " + )
.build();
// 3. 把新请求传下去
Response response = chain.proceed(newRequest);
// 4. 你也可以对响应做点什么
// 比如这里我啥也不改,直接返回
return response;
}
}
这段代码里最关键的就是 chain.proceed()。你想想看,如果不调用它,请求就卡在你这里了,永远不会发出去。我在项目中遇到过某个同事把 proceed() 写在条件分支里,结果某些情况下没执行,导致接口完全没请求——那叫一个纳闷。
intercept() 里对同一个请求调了两次 chain.proceed()。第二次调用会直接抛异常,因为每个请求只能被处理一次。如果你需要重试,一定要用 try-catch 包裹,并且用循环或条件判断。
4.3 chain.proceed() 到底做了什么?
你可能会问:为什么必须调用 chain.proceed()?这个方法的返回值是 Response,但实际上它内部做了这些事:
- 把请求交给下一个个拦截器处理
- 最末尾的拦截器会真正发起网络请求
- 响应会沿着链条返回
说白了,整个拦截器就是个「洋葱模型」。我在实际项目中画过一张图给团队看,这里我用 SVG 画一下核心流程:
从图上可以清楚看到:请求先经过你的自定义拦截器,再经过内置的日志拦截器,最后才真正发出网络请求。响应则原路反向回来。
4.4 自定义拦截器的常见用途
工作中我基本上只用了下面几种场景,但每一种都能撑起一个完整的功能模块:
| 用途 | 说明 | 我在项目中遇到过 |
|---|---|---|
| 自动添加 | 登录后统一在 Header 里带上 | 曾经 app 的 过期后没有刷新机制,导致 401 重试了三次都没用 |
| 请求日志打印 | 调试时打印 url、header、body、耗时 | 有次线上 bug,靠日志拦截器才发现请求参数被某个中间件截断了 |
| 统一错误处理 | 将 5xx 错误包装成业务异常 | 如果不统一处理,每个接口回调里都要写一堆 if 判断 |
| 网络重试 | 当网络波动时自动重试一次 | 注意:重试时要克隆 Request,因为 body 只能消费一次 |
| 添加公共参数 | 比如手机型号、版本号、时间戳 | 我习惯在拦截器里做,比在每个接口手动拼方便多了 |
4.5 写拦截器时容易踩的坑
讲了这么多好处,也该说说坑了。毕竟实战中不只有顺风顺水。我整理了几个常见的「雷区」:
- 忘记调用 chain.proceed() —— 请求卡死,没有任何响应。我见过新人调试半天没反应,结果就是这个原因。
- 重复调用 chain.proceed() —— 会抛
IllegalStateException。如果你需要重试,可以用循环重新构建 Request 再调一次。 - 修改了不可变对象 —— Request 的 body 只能消费一次,如果你把请求体读取了但没保存,后面就没了。解决办法是用
Buffer缓存。 - 拦截器执行顺序搞反 —— 添加拦截器时,
addInterceptor()是应用层拦截器,addNetworkInterceptor()是网络层拦截器。两者的执行时机和影响范围不同。我习惯把 刷新放在应用层,日志放在网络层。
intercept() 方法里加一条日志,打印 Thread.currentThread().getName()。网络层拦截器会在不同的线程里执行,而应用层拦截器通常在你调用线程里。
4.6 实战建议:如何组织你的拦截器
我个人习惯把自定义拦截器按照职责拆分,每个类只做一件事。比如:
AuthInterceptor—— 只处理LoggingInterceptor—— 只打日志RetryInterceptor—— 只负责重试ErrorMappingInterceptor—— 只处理错误码转换
然后在构建 OkHttpClient 时,按顺序添加。顺序很重要:先添加的拦截器会先处理 Request,最后处理 Response。你想想看,如果日志拦截器放在最里面,那么日志里就能看到 被添加后的完整请求,这对调试很有帮助。
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new AuthInterceptor()) // 先处理
.addInterceptor(new LoggingInterceptor()) // 再打日志
.addInterceptor(new RetryInterceptor(3)) // 重试放在更里面
.build();
嗯,到这里你已经掌握了自定义拦截器的核心。剩下就是多写多练,把那些看起来千篇一律的「加 Header」「打日志」变成你自己的工具箱。