4. 应用拦截器实战:日志打印拦截器——记录请求URL、方法和耗时
4.1 原理:应用拦截器与日志采集点
在 OkHttp 的拦截器链中,应用拦截器(Application Interceptor) 处于链的最外层位置。它的执行时机最早,在请求被重定向、重试或连接复用之前就会触发;同时它也是最后一个收到最终响应的拦截器。这一特性使得应用拦截器成为全局日志打印的理想场所:无论是原始请求的 URL、方法、头部,还是经过整个网络流程后的最终响应状态码、耗时,都能在此完整采集。
关键特征: 应用拦截器在
addInterceptor() 注册,每个请求无论是否走缓存或发生重定向,都会且仅会经过它一次。
4.2 实现:日志打印拦截器完整代码
创建一个 LoggingInterceptor 类,它实现 Interceptor 接口。核心逻辑是在拦截器的 intercept() 方法中封装计时逻辑,分别打印请求信息与响应信息。
import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import java.io.IOException;
import java.util.logging.Logger;
public class LoggingInterceptor implements Interceptor {
private static final Logger logger = Logger.getLogger(LoggingInterceptor.class.getName());
@Override
public Response intercept(Chain chain) throws IOException {
// 1. 获取原始请求对象
Request request = chain.request();
// 2. 记录请求开始时间戳
long startTime = System.nanoTime();
// 3. 输出请求基本信息
logger.info("--> " + request.method() + " " + request.url());
// 4. 执行下一个拦截器(网络请求)
Response response = chain.proceed(request);
// 5. 计算请求耗时(毫秒)
long elapsedTime = (System.nanoTime() - startTime) / 1_000_000;
// 6. 输出响应基本信息
logger.info("<-- " + response.code() + " " + request.url() + " (" + elapsedTime + "ms)");
// 7. 返回响应,不修改响应体
return response;
}
}
在构建 OkHttpClient 时,通过 addInterceptor() 注册该拦截器实例:
OkHttpClient client = new OkHttpClient.Builder()
.addInterceptor(new LoggingInterceptor())
.build();
4.3 核心 API 与参数说明
| 方法 / 参数 | 说明 |
|---|---|
chain.request() |
获取当前拦截器链中的请求对象,包含 URL、方法、头部、请求体等 |
chain.proceed(request) |
将请求交给链中的下一个拦截器执行,并返回最终响应 |
request.method() |
返回 HTTP 方法字符串,例如 GET、POST、PUT、DELETE |
request.url() |
返回完整的 HttpUrl 对象,包含 scheme、host、port、路径和查询参数 |
response.code() |
返回 HTTP 状态码,例如 200、404、500 |
System.nanoTime() |
高精度计时器,适合测量代码执行耗时,单位纳秒 |
addInterceptor() |
OkHttpClient.Builder 方法,注册应用拦截器(非网络拦截器) |
4.4 调试:日志输出示例
当客户端发起一个 GET 请求到 https://api.example.com/users/1 时,控制台会输出如下日志:
2025-01-20 10:15:23 INFO LoggingInterceptor: --> GET https://api.example.com/users/1
2025-01-20 10:15:24 INFO LoggingInterceptor: <-- 200 https://api.example.com/users/1 (187ms)
当请求失败或返回非 2xx 状态码时,日志同样会清晰记录:
2025-01-20 10:16:01 INFO LoggingInterceptor: --> POST https://api.example.com/upload
2025-01-20 10:16:03 INFO LoggingInterceptor: <-- 500 https://api.example.com/upload (2112ms)
4.5 部署注意事项
- 日志级别控制: 生产环境建议将日志级别设为
FINE或仅记录错误,避免大量日志导致 I/O 性能下降。可以通过外部配置(如LoggingInterceptor构造函数参数)控制级别。 - 敏感信息过滤: 如果请求 URL 或头部包含 、密码等敏感数据,必须在日志输出前做脱敏处理,防止泄露。
- 响应体不打印: 此实现仅打印请求行和响应行,不包含响应体。如需记录响应体,请考虑
networkInterceptors或使用BufferedSource的克隆机制,避免破坏流。
4.6 常见错误与稳定性优化
| 常见错误 / 场景 | 原因 | 解决方案 |
|---|---|---|
| 日志顺序混乱 | 多个线程同时并发请求,日志交错输出 | 在日志消息中加入线程名称或请求 ID 用于区分 |
| 耗时显示为 0 ms | 请求走本地缓存或连接池复用,未真正发起网络 I/O | 使用 networkInterceptors 可观测真实网络耗时 |
| 日志打印过多导致性能下降 | 高并发且日志输出到文件格式串化 | 采用异步日志框架(如 Logback AsyncAppender),或开启日志采样 |
| 拦截器抛异常导致请求中断 | 日志格式化逻辑或 I/O 本身抛出 IOException | 使用 try-catch 包裹所有日志输出,确保不影响主链路 |
性能警告:
System.nanoTime() 调用开销极小(微秒级),但 logger.info() 的字符串拼接在循环中会产生大量临时对象。建议使用日志框架的占位符语法(如 logger.log(Level.INFO, "--> {0} {1}", method, url))来减少内存分配。