拦截器模式:Spring AOP 背后的设计智慧
说实话,拦截器模式是我在项目中用得最多的模式之一。你想想看,一个系统上线后,总得加点日志、做做权限校验、搞搞事务管理吧?这些横切关注点如果散落在业务代码里,那维护起来简直是一场噩梦。拦截器模式就是专门解决这个问题的。
我记得刚做架构那会儿,接手过一个老系统。业务逻辑和日志、权限、事务全混在一起,一个方法几百行,改个需求得翻半天代码。后来我重构时引入了拦截器模式,代码瞬间清爽了。说白了,拦截器就是让你在不修改原有业务代码的前提下,给方法调用前后插入一些额外操作。
什么是拦截器模式?
拦截器模式的核心思想其实很简单:在目标方法执行前后,插入一些预处理和后处理逻辑。它由三个关键角色组成:
- 拦截器接口:定义了拦截行为的契约,比如 before()、after()、around() 等方法
- 目标对象:真正执行业务逻辑的对象
- 代理链:将多个拦截器串联起来,形成一个调用链条
我习惯把拦截器想象成「洋葱皮」。你调用目标方法时,会一层层穿过拦截器,每层都可以在调用前后做点手脚。嗯,这个比喻很形象。
核心要点:拦截器模式实现了关注点分离,让业务逻辑和非业务逻辑解耦。这是 AOP(面向切面编程)的底层实现机制。
拦截器模式的结构
我们先看看拦截器模式的标准类图。我个人习惯用这种图来理清思路:
你看这个图,客户端不直接调用目标对象,而是通过代理链。代理链里维护了一组拦截器,每个拦截器都可以在方法调用前后插入逻辑。这就是 AOP 的底层实现机制。
Java 代码实现
我们先从最基础的拦截器接口开始。我写代码喜欢先定义契约:
// 拦截器接口
public interface Interceptor {
// 前置处理
boolean before(Object target, Method method, Object[] args);
// 后置处理
void after(Object target, Method method, Object[] args, Object result);
// 异常处理
void exception(Object target, Method method, Object[] args, Throwable e);
}
接下来是代理链的实现。这里我用了责任链模式的思想,每个拦截器执行完 before 后,决定是否继续往下走:
public class ProxyChain {
private List<Interceptor> interceptors;
private Object target;
private int currentIndex = 0;
public ProxyChain(Object target, List<Interceptor> interceptors) {
this.target = target;
this.interceptors = interceptors;
}
public Object proceed(Method method, Object[] args) throws Throwable {
// 所有拦截器执行完毕,调用目标方法
if (currentIndex == interceptors.size()) {
return method.invoke(target, args);
}
Interceptor interceptor = interceptors.get(currentIndex++);
// 执行前置处理
boolean proceed = interceptor.before(target, method, args);
if (!proceed) {
return null; // 拦截器阻止了后续执行
}
Object result = null;
try {
// 递归调用下一个拦截器或目标方法
result = proceed(method, args);
} catch (Throwable e) {
interceptor.exception(target, method, args, e);
throw e;
} finally {
interceptor.after(target, method, args, result);
}
return result;
}
}
个人经验:我在实现代理链时,喜欢用递归而不是循环。递归的方式更符合「洋葱模型」的直觉,每个拦截器都能控制调用链的走向。不过要注意递归深度,如果拦截器太多,可以考虑用栈来优化。
写一个具体的拦截器看看效果:
public class LogInterceptor implements Interceptor {
@Override
public boolean before(Object target, Method method, Object[] args) {
System.out.println("[日志] 调用方法: " + method.getName() +
",参数: " + Arrays.toString(args));
return true; // 继续执行
}
@Override
public void after(Object target, Method method, Object[] args, Object result) {
System.out.println("[日志] 方法返回: " + result);
}
@Override
public void exception(Object target, Method method, Object[] args, Throwable e) {
System.err.println("[日志] 方法异常: " + e.getMessage());
}
}
C++ 代码实现
C++ 的实现思路和 Java 类似,但要注意资源管理和模板编程。我个人在 C++ 项目中更喜欢用模板来避免虚函数开销:
#include <iostream>
#include <vector>
#include <functional>
#include <memory>
// 拦截器接口(抽象类)
class Interceptor {
public:
virtual ~Interceptor() = default;
virtual bool before(const std::string& methodName,
const std::vector<std::any>& args) = 0;
virtual void after(const std::string& methodName,
const std::any& result) = 0;
virtual void exception(const std::string& methodName,
const std::exception& e) = 0;
};
// 代理链
class ProxyChain {
private:
std::vector<std::shared_ptr<Interceptor>> interceptors_;
size_t currentIndex_ = 0;
std::function<std::any()> targetMethod_;
public:
ProxyChain(std::function<std::any()> targetMethod,
std::vector<std::shared_ptr<Interceptor>> interceptors)
: targetMethod_(std::move(targetMethod))
, interceptors_(std::move(interceptors)) {}
std::any proceed(const std::string& methodName,
const std::vector<std::any>& args) {
if (currentIndex_ == interceptors_.size()) {
return targetMethod_();
}
auto& interceptor = interceptors_[currentIndex_++];
if (!interceptor->before(methodName, args)) {
return std::any(); // 被拦截
}
std::any result;
try {
result = proceed(methodName, args);
} catch (const std::exception& e) {
interceptor->exception(methodName, e);
throw;
}
interceptor->after(methodName, result);
return result;
}
};
避坑指南:我曾经在 C++ 项目中犯过一个错误——拦截器里抛了异常但没有正确处理,导致整个调用链中断。后来我规定:拦截器的 before() 方法如果返回 false,表示拦截;如果抛出异常,表示严重错误,应该终止调用链并向上传播。这两种语义要区分清楚。
Spring AOP 中的拦截器
说到拦截器模式,就不得不提 Spring AOP。Spring AOP 本质上就是拦截器模式的一种实现。它提供了几个核心概念:
| Spring AOP 概念 | 对应拦截器模式角色 | 说明 |
|---|---|---|
| Advice(通知) | 拦截器 | 定义在目标方法前后执行的逻辑 |
| Pointcut(切点) | 拦截条件 | 决定哪些方法需要被拦截 |
| Advisor(通知器) | 拦截器 + 条件 | 将 Advice 和 Pointcut 组合在一起 |
| AopProxy(代理) | 代理链 | 生成代理对象,管理拦截器链 |
Spring 中常用的拦截器类型有:
- MethodInterceptor:最核心的拦截器接口,通过 Invocation.proceed() 控制调用链
- BeforeAdvice:前置通知,在目标方法前执行
- AfterReturningAdvice:后置通知,在目标方法正常返回后执行
- ThrowsAdvice:异常通知,在目标方法抛出异常时执行
- AroundAdvice:环绕通知,可以完全控制目标方法的执行
我举个例子,Spring 中自定义一个拦截器其实很简单:
@Component
public class PerformanceInterceptor implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
long start = System.currentTimeMillis();
try {
Object result = invocation.proceed(); // 继续调用链
return result;
} finally {
long duration = System.currentTimeMillis() - start;
System.out.println("方法 " + invocation.getMethod().getName()
+ " 执行耗时: " + duration + "ms");
}
}
}
关键理解:Spring AOP 的 MethodInterceptor.invoke() 方法,本质上就是代理链的 proceed() 方法。你调用 invocation.proceed() 时,它会继续执行下一个拦截器,直到最终调用目标方法。这就是「洋葱模型」的核心。
实际项目中的应用场景
我在项目中用过拦截器模式的地方太多了,挑几个典型的说说:
- 日志记录:统一记录方法调用日志,包括入参、出参、耗时
- 权限校验:在方法执行前检查用户是否有权限
- 事务管理:在方法执行前开启事务,执行后提交或回滚
- 缓存处理:在方法执行前检查缓存,执行后更新缓存
- 参数校验:统一校验方法参数是否合法
- 性能监控:统计方法执行时间,上报到监控系统
你想想看,如果没有拦截器模式,这些逻辑就得散落在每个业务方法里。改个日志格式要改几百个方法,那画面太美我不敢看。
我的建议:设计拦截器时,尽量让每个拦截器只做一件事。比如日志拦截器只负责日志,权限拦截器只负责权限。这样组合起来更灵活,也更容易测试。我曾经见过一个拦截器里既写日志又做权限还管事务,那代码简直没法看。
拦截器模式还有一个好处:你可以动态地添加或移除拦截器,而不影响业务代码。比如线上出了性能问题,你可以动态加一个性能监控拦截器,排查完再移除。这种灵活性在大型系统中特别有用。
好了,关于拦截器模式就聊这么多。记住一句话:拦截器模式是 AOP 的基石,而 AOP 是让代码保持干净的利器。下次你写 Spring 的 @Around 注解时,想想背后的拦截器模式,你会对它有更深的理解。