Interceptors That Actually Help: Request Logging and Automatic Bearer-Token Injection

Interceptors That Actually Help: Request Logging and Automatic Bearer-Token Injection

真正有用的拦截器:请求日志记录与自动 Bearer Token 注入

Cross-cutting HTTP concerns — logging, authentication headers, request IDs, metrics — should never be hand-edited into every request. JQuickCurl rides on OkHttp’s mature interceptor pipeline, and because interceptors are wired into the global configuration, a single registration upgrades every curl command in your application. In this post: What an interceptor sees and can do. The built-in JLoggingInterceptor. Writing a request/response logging interceptor. Auto-injecting a Bearer token from your secret store.

HTTP 的横切关注点(如日志记录、认证头、请求 ID、指标监控等)绝不应该手动添加到每一个请求中。JQuickCurl 基于 OkHttp 成熟的拦截器管道构建,由于拦截器被挂载到全局配置中,只需注册一次,即可升级应用中的所有 curl 命令。本文将涵盖:拦截器的作用与能力、内置的 JLoggingInterceptor、编写自定义请求/响应日志拦截器,以及从密钥存储中自动注入 Bearer Token。

The Interceptor Model in One Paragraph

一句话理解拦截器模型

An OkHttp Interceptor wraps a call: it receives the outgoing Request, may modify it, calls chain.proceed(...), and then sees the returned Response. Registering interceptors happens on the global config, so both annotation-mode and XML-mode commands pass through them.

OkHttp 拦截器包装了一个调用:它接收外发的 Request,可以对其进行修改,调用 chain.proceed(...),然后获取返回的 Response。拦截器的注册是在全局配置中完成的,因此无论是注解模式还是 XML 模式的命令都会经过它们。

Interceptor myInterceptor = chain -> {
    Request request = chain.request(); // 外发请求
    Response response = chain.proceed(request); // 执行并获取响应
    return response;
};
JQuickCurlConfig.getInstance().addInterceptor(myInterceptor);

addNetworkInterceptor(...) registers at the network layer (after redirects/retries) when you need to observe the real wire traffic.

当你需要观察真实的底层网络流量时,可以使用 addNetworkInterceptor(...) 在网络层(重定向/重试之后)进行注册。

Built-In Logging: JLoggingInterceptor

内置日志记录:JLoggingInterceptor

Out of the box, the config already registers a JLoggingInterceptor at level ALL — it measures each call and reports the elapsed time (and failures) through its console logger. If you want a quieter default, the same interceptor can be constructed with an explicit level:

开箱即用,配置中已经注册了一个级别为 ALLJLoggingInterceptor —— 它会测量每次调用,并通过控制台记录器报告耗时(及失败情况)。如果你希望默认输出更简洁,可以在构造该拦截器时显式指定级别:

public enum JCurlLevelLog { NONE, BASIC, HEADERS, ALL }

import com.github.paohaijiao.enums.JCurlLevelLog;
import com.github.paohaijiao.interceptor.JLoggingInterceptor;
import com.github.paohaijiao.config.JQuickCurlConfig;

JQuickCurlConfig.getInstance()
    .addInterceptor(new JLoggingInterceptor(JCurlLevelLog.BASIC));

Expect output in the spirit of: the request cost : 132 ms. Choose the noisiest level in development, BASIC in staging, and keep an eye on token redaction (see below).

输出格式类似于:the request cost : 132 ms。建议在开发环境使用最详细的级别,在预发布环境使用 BASIC,并注意对 Token 进行脱敏处理(见下文)。

Writing a Purpose-Built Logging Interceptor

编写自定义日志拦截器

Build one that logs method, URL, status, and duration — and be deliberate about what you don’t log (authorization headers!).

编写一个记录方法、URL、状态码和耗时的拦截器,并谨慎处理不应记录的内容(如 Authorization 请求头!)。

import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;
import okhttp3.ResponseBody;

public class CallLogInterceptor implements Interceptor {
    private static final org.slf4j.Logger log = org.slf4j.LoggerFactory.getLogger(CallLogInterceptor.class);

    @Override
    public Response intercept(Chain chain) throws java.io.IOException {
        Request request = chain.request();
        long start = System.nanoTime();
        Response response = chain.proceed(request);
        long tookMs = java.util.concurrent.TimeUnit.NANOSECONDS.toMillis(
            System.nanoTime() - start);

        String bodyPreview = "";
        if (response.body() != null) {
            bodyPreview = safePreview(response.peekBody(256).string());
        }

        log.info("[http] {} {} -> {} in {} ms body={}", 
            request.method(), request.url(), response.code(), tookMs, bodyPreview);
        return response;
    }

    private String safePreview(String s) {
        return s == null ? "" : s.replace('\n', ' ').substring(
            0, Math.min(120, s.length()));
    }
}

Register it next to the built-in one: 将其与内置拦截器一起注册:

JQuickCurlConfig.getInstance().addInterceptor(new CallLogInterceptor());

Automatic Bearer-Token Injection

自动 Bearer Token 注入

Tokens expire, rotate, and come from secret managers — they don’t belong in @JCurlCommand strings. One interceptor can stamp every outgoing request with the current token:

Token 会过期、轮换,且通常来自密钥管理器——它们不应该硬编码在 @JCurlCommand 字符串中。通过一个拦截器,可以为每个外发请求自动添加当前 Token:

import okhttp3.Interceptor;
import okhttp3.Request;
import okhttp3.Response;

public class BearerAuthInterceptor implements Interceptor {
    private final java.util.function.Supplier<String> tokenSupplier;

    public BearerAuthInterceptor(java.util.function.Supplier<String> tokenSupplier) {
        this.tokenSupplier = tokenSupplier;
    }

    @Override
    public Response intercept(Chain chain) throws java.io.IOException {
        String token = tokenSupplier.get();
        Request request = chain.request().newBuilder()
            .addHeader("Authorization", "Bearer " + token)
            .build();
        return chain.proceed(request);
    }
}

Because the token is fetched per request via a Supplier, rotating tokens are picked up automatically — no restart, no stale header. Wire it with your vault/environment source:

由于 Token 是通过 Supplier 在每次请求时获取的,因此轮换后的 Token 会被自动捕获——无需重启,也不会出现过期的请求头。将其与你的密钥库或环境变量源连接起来:

import com.github.paohaijiao.config.JQuickCurlConfig;

JQuickCurlConfig.getInstance().addInterceptor(
    new BearerAuthInterceptor(() -> System.getenv("API_TOKEN")));

Now a plain command stays clean: 现在,简单的命令保持整洁:

@JCurlCommand("curl -X GET 'https://api.example.com/me'")
String me(JQuickCurlReq request); // Authorization stamped by interceptor

The same trick works for API keys, X-Correlation-Id, tenant headers, and basic-auth pairs (Post 15).

同样的技巧也适用于 API Key、X-Correlation-Id、租户请求头以及 Basic-Auth 认证对(详见第 15 篇博文)。

Runnable Demo

可运行的演示

import com.github.paohaijiao.anno.JCurlCommand;
import com.github.paohaijiao.config.JQuickCurlConfig;
import com.github.paohaijiao.domain.req.JQuickCurlReq;
import com.github.paohaijiao.executor.JCurlInvoker;

public interface InspectApi {
    @JCurlCommand("curl -X GET 'https://httpbin.org/headers'")
    String headers(JQuickCurlReq request);
}

class InterceptorDemo {
    public static void main(String[] args) {
        JQuickCurlConfig.getInstance().addInterceptor(chain -> {
            okhttp3.Request r = chain.request().newBuilder()
                .addHeader("Authorization", "Bearer demo-token-123")
                .build();
            return chain.proceed(r);
        });

        String echo = JCurlInvoker.createProxy(InspectApi.class)
            .headers(new JQuickCurlReq());
        System.out.println(echo); // "Authorization": "Bearer demo-token-123"
    }
}

Ordering and Pitfalls

顺序与陷阱

  • Interceptors run in registration order (application interceptors before network interceptors). 拦截器按注册顺序运行(应用拦截器先于网络拦截器)。
  • Headers you add in an interceptor are applied on top of whatever the curl string specified. 你在拦截器中添加的请求头会叠加在 curl 字符串指定的请求头之上。
  • Never log or stash raw Authorization values — hash or redact in logs. 切勿记录或存储原始的 Authorization 值——请在日志中进行哈希或脱敏处理。
  • Keep interceptors fast; they run on the request thread. 保持拦截器高效;它们运行在请求线程中。

Summary

总结

Interceptors are the clean seam for concerns that cut across every HTTP call. JQuickCurl comes with timing-oriented logging out of the box and…

拦截器是处理跨越所有 HTTP 调用关注点的最佳切入点。JQuickCurl 开箱即用,提供了基于耗时的日志记录功能,并且……