从可插拔拦截器出发:自定义、注入 Spring Boot、到生效路径的完整实践(Demo 版)
1. 拦截器要解决什么问题?
把签名、打点、鉴权、灰度、审计等横切逻辑从业务请求中抽离;
低侵入接入任意 OkHttpClient;
能通过运行时配置(如开关、密钥、目标地址等)动态生效;
支持“是否命中某个条件才生效”的灰度能力。
2. 自定义一个 OkHttp Interceptor
下方仅为示例:拦截器在命中某个“特征”时,将请求“包一层”再转发;未命中则直通。
public final class DemoInterceptor implements Interceptor {
private final DemoRuntimeSwitch runtime; // 运行时开关 & 配置
public DemoInterceptor(DemoRuntimeSwitch runtime) {this.runtime = runtime;}
@Overridepublic Response intercept(Chain chain) throws IOException {Request original = chain.request();
// 1) 未开启或配置不完整:放行if (!runtime.isOn() || !runtime.isConfigReady()) {return chain.proceed(original);}
// 2) 命中某个触发条件(Demo:URL 上带 trigger=true)boolean hit = "true".equalsIgnoreCase(original.url().queryParameter("trigger"));if (!hit) return chain.proceed(original);
// 3) 包装:示意将原请求信息转成一个 JSON 文本String wrapped = DemoJson.wrap(original); // {method, path, headers, query, body}
// 4) 生成一个示意“签名”字符串(真实场景请用更安全算法与协议)String sign = DemoSign.simple(runtime.getAppKey(), runtime.getSecret(), wrapped);
// 5) 构造“第二跳”请求(Demo:POST 表单提交到 runtime.getSecondHopUrl())Request secondHop = new Request.Builder().url(runtime.getSecondHopUrl()).post(new FormBody.Builder().add("appKey", runtime.getAppKey()).add("payload", wrapped).add("sign", sign).build()).build();
try {return chain.proceed(secondHop);} catch (Exception ex) {// 6) 兜底:返回一个 Demo 错误响应(生产可返回统一错误结构)return DemoResponse.errorJson(500, "second-hop failed: " + ex.getMessage());}}
}
要点回顾:
触发条件外置,真实可换成 token、Header、白名单等;
直通优先:未命中一律放行,降低对主链路影响;
二跳独立配置:URL、超时、重试策略等在配置中统一管理。
3. 运行时配置开关(Demo)
//只展示结构与意图
public final class DemoRuntimeSwitch {private volatile boolean on;private volatile String appKey;private volatile String secret;private volatile String secondHopUrl;
public boolean isOn() { return on; }public boolean isConfigReady() {return appKey != null && !appKey.isEmpty() &&secret != null && !secret.isEmpty() &&secondHopUrl != null && !secondHopUrl.isEmpty();}
// 可由配置中心/管理接口更新public String getAppKey() { return appKey; }public String getSecret() { return secret; }public String getSecondHopUrl() { return secondHopUrl; }
}
实际可用
AtomicReference
持有完整配置快照;更新方式可来自配置中心/Redis/管理接口,做到秒级生效;
配置校验应集中在一个地方(例如
normalizeAndValidate()
)。
4. 在 Spring Boot 中注入拦截器(Demo)
方式 A:直接注册为 Bean
@Configuration
public class DemoInterceptorConfig {
@Beanpublic DemoRuntimeSwitch demoRuntimeSwitch() { return new DemoRuntimeSwitch(); }
@Beanpublic Interceptor demoInterceptor(DemoRuntimeSwitch runtime) {return new DemoInterceptor(runtime);}
}
方式 B:自动收集所有拦截器,再统一挂载(可插拔)
// 一个简单的“桥”,把容器中的拦截器收集起来,统一挂载到 OkHttp
public final class DemoInterceptorBridge {private static final CopyOnWriteArrayList<Interceptor> LIST = new CopyOnWriteArrayList<>();public static void registerAll(Collection<Interceptor> its) { LIST.addAll(its); }public static void attachAll(OkHttpClient.Builder b){for (Interceptor it : LIST) {boolean exists = b.interceptors().stream().anyMatch(x -> x.getClass().getName().equals(it.getClass().getName()));if (!exists) b.addInterceptor(it);}}
}
@Configuration
public class DemoAutoCollectConfig {@Beanpublic ApplicationRunner collectInterceptors(List<Interceptor> interceptors){return args -> DemoInterceptorBridge.registerAll(interceptors);}
}
这样,业务侧无需感知有哪些拦截器,统一通过“桥”完成补挂,真正做到可插拔。
5. 让拦截器在 OkHttp 中真正生效(Demo)
public final class DemoOkHttpFactory {public static OkHttpClient newClient(){OkHttpClient.Builder b = new OkHttpClient.Builder().connectTimeout(Duration.ofSeconds(5)).readTimeout(Duration.ofSeconds(10));
// 统一补挂所有已注册的拦截器DemoInterceptorBridge.attachAll(b);return b.build();}
}
生效路径
Spring 启动:扫描并注册所有
Interceptor
Bean → 交给DemoInterceptorBridge
;构建客户端:使用
DemoOkHttpFactory.newClient()
获取OkHttpClient
;发起请求:OkHttp 按拦截器顺序依次调用
intercept()
→ 命中条件的拦截器执行逻辑,否则放行;运行期:通过
DemoRuntimeSwitch
更新开关与参数,无重启生效。
6. 端到端流程示意
7. 最佳实践
直通优先:非命中条件一律放行,降低链路抖动;
独立配置:把“是否启用”“目标地址”“签名策略”都放到运行时配置中;
安全策略:签名/加密算法做成可替换的策略接口;敏感字段统一脱敏;
顺序管理:有依赖关系的拦截器(例如“鉴权 → 访问日志”)要明确注册顺序;
测试清单:命中/未命中、配置缺失、远端超时/5xx、请求体不可重复读取等场景;
可观测:埋点 traceId、耗时、命中率、错误码分布,日志注意截断与采样。
8. Demo 模板
//自定义拦截器骨架(最小实现)public class MyDemoInterceptor implements Interceptor {@Override public Response intercept(Chain chain) throws IOException {Request req = chain.request();// 前置:校验/打点/改写(按需)Response resp = chain.proceed(req);// 后置:统计/日志(按需)return resp;}
}
//Spring 注入 + 统一补挂@Configuration
public class MyDemoOkHttpConfig {@Bean public Interceptor myDemoInterceptor(){ return new MyDemoInterceptor(); }@Bean public OkHttpClient httpClient(){OkHttpClient.Builder b = new OkHttpClient.Builder();DemoInterceptorBridge.attachAll(b);return b.build();}
}
//运行时开关(示意)public final class MySwitch {private final AtomicBoolean on = new AtomicBoolean(false);public boolean isOn(){ return on.get(); }public void setOn(boolean v){ on.set(v); }
}