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))来减少内存分配。
日志拦截器在应用拦截器链中的执行流程 1. 客户端发起 HTTP 请求 2. LoggingInterceptor.intercept() 记录 startTime 并打印请求行 3. chain.proceed(request) 4. 下游拦截器执行 → 实际网络请求 5. 返回到 LoggingInterceptor 计算耗时并打印响应行 6. 返回 Response 给调用方 采集点 request.method() request.url() response.code() + 耗时