函数拦截器是实现无侵入AOP最直接轻量跨平台的方式,通过高阶函数、装饰器、动态代理或编译期织入,在调用链上“卡位”拦截目标函数,实现日志、埋点等横切逻辑剥离。

函数拦截器是实现无侵入 AOP 最直接、轻量且跨平台的方式之一。它不依赖框架(如 Spring 或 .NET DispatchProxy),而是基于 JavaScript/TypeScript 的高阶函数或装饰器,或在 Java/Android 中借助编译期织入(AspectJ)或运行时代理,对目标函数调用进行“拦截—增强—放行”三步控制,从而把日志、埋点、权限、耗时统计等横切逻辑完全剥离出业务代码。
拦截器的核心机制:在调用链上“卡位”
拦截器本质是在目标函数被真正执行前/后/异常时插入一段可复用的逻辑。关键不是“重写函数”,而是“接管调用”。常见实现路径有:
-
运行时包裹:用高阶函数返回代理函数,例如
logInterceptor(fn)返回一个带日志的包装版fn; -
装饰器语法(TS / Babel):用
@Log标记方法,编译时或运行时自动注入拦截逻辑; -
动态代理(Java / .NET):如 Spring AOP 使用 JDK Proxy 或 CGLIB,.NET Core 使用
DispatchProxy,生成目标接口/类的代理实例; - 编译期织入(Android / JVM):通过 AspectJ 在字节码层面插入逻辑,无需运行时代理,性能更高、更彻底。
如何写出一个真正无侵入的拦截器
“无侵入”不是指完全不写标记,而是指不修改原函数体、不侵染业务流程判断、不耦合执行顺序。做到这点需满足三个条件:
- 业务函数保持“裸写”状态——不调用
logStart()、不包 try-catch 做监控; - 拦截行为由外部统一注册或声明,例如通过装饰器、注解、配置表或代理工厂;
- 拦截逻辑与业务逻辑零共享变量、零强引用,仅通过参数(如
JoinPoint)、返回值或上下文(如executionContext)通信。
例如 TypeScript 中一个计时拦截器:
function Timing() {return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const original = descriptor.value;
descriptor.value = function(...args: any[]) {
console.time(`[${propertyKey}]`);
const result = original.apply(this, args);
console.timeEnd(`[${propertyKey}]`);
return result;
};
};
}
拦截器 ≠ 过滤器或 MVC 拦截器
要避免概念混淆:
- Web 过滤器(Filter)作用于 HTTP 请求生命周期,粒度是请求/响应,无法精确到某个 service 方法;
- MVC 拦截器(Interceptor)属于框架层,绑定在 Controller 方法前后,但通常不支持 private 方法、非 Web 层(如 DAO)或跨模块复用;
- AOP 拦截器以函数/方法为单位,可在任意层级(Controller、Service、Utils、甚至第三方 SDK 调用)生效,支持环绕(Around)、前置(Before)、后置(After)等多种通知类型,并能修改参数与返回值。
生产可用的关键设计点
落地时容易忽略但影响长期维护的细节:
-
切点表达式要可读可配:比如
@Around("execution(* com.app.service..*.*(..))")比硬编码类名更可持续; -
支持启用开关:用配置项(如
enableMetrics: true)控制某类拦截器是否激活,便于灰度和降级; - 错误隔离:拦截逻辑内部异常不能中断原函数执行(除非有意做熔断),应 try-catch 并记录错误,而非抛出;
- 上下文透传:如需在日志中打上 traceId,拦截器应从当前执行环境(如 Zone.js、AsyncLocalStorage、ThreadLocal)安全提取,而非依赖参数显式传递。


















