1. 为什么需要三剑客组合?
在Spring Boot应用开发中,拦截器、过滤器和AOP这三种技术经常让开发者感到困惑——它们看起来都能实现相似的功能,比如对请求进行预处理、后处理或者权限校验。但实际上面向的问题域和适用场景有着本质区别。
我经历过一个典型的线上事故:某个电商平台的促销接口在秒杀活动时崩溃,事后排查发现是因为所有请求都直接打到业务逻辑层,既没有做限流控制,也没有记录完整的调用链路。如果当时合理使用了过滤器做限流、拦截器记录基础日志、AOP处理业务日志,问题完全可以避免。
过滤器(Filter)是Servlet规范的一部分,工作在Web容器层面。它的特点是作用范围广,能处理所有进入容器的请求,包括静态资源。但正因为如此,它无法获取Spring的上下文信息。我在处理跨域问题时就会优先选择过滤器,因为需要拦截所有OPTIONS请求。
拦截器(Interceptor)是Spring MVC的组件,能够访问Spring的上下文和Handler信息。它更适合处理与业务相关的预处理,比如登录状态校验。最近在重构一个老系统时,我将原本放在过滤器的权限校验迁移到了拦截器,因为需要读取数据库中的权限配置,过滤器无法直接注入Spring Bean。
AOP(面向切面编程)则更加灵活,它不局限于HTTP请求,可以拦截方法调用。我常用AOP来处理一些横切关注点,比如数据库事务管理、性能监控等。在微服务架构中,AOP特别适合实现分布式链路追踪,因为可以统一处理所有微服务间的调用。
2. 登录鉴权的实现方案对比
2.1 基于过滤器的JWT校验
在网关层做认证是最常见的场景。下面是一个完整的JWT校验过滤器实现:
java复制public class JwtFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
throws IOException, ServletException {
HttpServletRequest req = (HttpServletRequest) request;
String token = req.getHeader("Authorization");
try {
if (token != null && JwtUtil.verify(token)) {
chain.doFilter(request, response);
} else {
sendError(response, "Invalid token");
}
} catch (Exception e) {
sendError(response, "Token verification failed");
}
}
private void sendError(ServletResponse response, String message) {
// 错误响应处理
}
}
关键点:过滤器要注册为最先执行的,通常设置Order为HIGHEST_PRECEDENCE
2.2 拦截器实现角色权限控制
拦截器可以获取HandlerMethod信息,适合做细粒度控制:
java复制public class AuthInterceptor implements HandlerInterceptor {
@Autowired
private RoleService roleService;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (handler instanceof HandlerMethod) {
HandlerMethod method = (HandlerMethod)handler;
RequiredRole annotation = method.getMethodAnnotation(RequiredRole.class);
if (annotation != null) {
String role = annotation.value();
return roleService.checkRole(request.getHeader("userId"), role);
}
}
return true;
}
}
2.3 AOP实现方法级权限校验
对于Service层的方法权限校验,AOP是最佳选择:
java复制@Aspect
@Component
public class PermissionAspect {
@Around("@annotation(requiredPermission)")
public Object checkPermission(ProceedingJoinPoint joinPoint, RequiredPermission requiredPermission) {
String permission = requiredPermission.value();
// 从ThreadLocal获取当前用户
if (!PermissionChecker.hasPermission(CurrentUser.get(), permission)) {
throw new PermissionDeniedException();
}
return joinPoint.proceed();
}
}
三种方案对比:
| 方案 | 适用层级 | 优点 | 缺点 |
|---|---|---|---|
| 过滤器 | Web容器层 | 性能最好,覆盖所有请求 | 无法使用Spring功能 |
| 拦截器 | MVC层 | 可获取Handler信息 | 仅对Controller有效 |
| AOP | 方法调用层 | 最灵活,不限于HTTP请求 | 对性能影响较大 |
3. 接口限流的实战方案
3.1 过滤器实现全局限流
使用Guava的RateLimiter做全局限流:
java复制public class RateLimitFilter implements Filter {
private final RateLimiter limiter = RateLimiter.create(1000); // 1000请求/秒
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
if (!limiter.tryAcquire()) {
((HttpServletResponse)response).setStatus(429);
return;
}
chain.doFilter(request, response);
}
}
3.2 拦截器实现API级限流
结合Redis实现分布式限流:
java复制public class ApiRateInterceptor implements HandlerInterceptor {
@Autowired
private RedisTemplate<String, String> redisTemplate;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
String apiKey = request.getRequestURI();
String userKey = request.getHeader("userId");
String redisKey = "rate:" + apiKey + ":" + userKey;
Long count = redisTemplate.opsForValue().increment(redisKey);
if (count == 1) {
redisTemplate.expire(redisKey, 1, TimeUnit.MINUTES);
}
if (count > 100) { // 每分钟100次
response.setStatus(429);
return false;
}
return true;
}
}
3.3 AOP实现方法级限流
使用注解实现灵活控制:
java复制@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface RateLimit {
int value() default 100;
TimeUnit unit() default TimeUnit.MINUTES;
}
@Aspect
@Component
public class RateLimitAspect {
private final Map<String, RateLimiter> limiters = new ConcurrentHashMap<>();
@Around("@annotation(limit)")
public Object limit(ProceedingJoinPoint pjp, RateLimit limit) throws Throwable {
String key = pjp.getSignature().toLongString();
RateLimiter limiter = limiters.computeIfAbsent(key,
k -> RateLimiter.create(limit.value() / limit.unit().toSeconds(1)));
if (!limiter.tryAcquire()) {
throw new RateLimitExceededException();
}
return pjp.proceed();
}
}
经验:生产环境推荐使用Redis+Lua脚本实现分布式限流,避免单机限流的不均衡问题
4. 全链路日志追踪的实现
4.1 过滤器生成TraceID
java复制public class TraceFilter implements Filter {
@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) {
String traceId = UUID.randomUUID().toString();
MDC.put("traceId", traceId);
((HttpServletResponse)response).setHeader("X-Trace-Id", traceId);
try {
chain.doFilter(request, response);
} finally {
MDC.remove("traceId");
}
}
}
4.2 拦截器记录请求信息
java复制public class LogInterceptor implements HandlerInterceptor {
private static final Logger logger = LoggerFactory.getLogger(LogInterceptor.class);
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
logger.info("Request {} {} from {}", request.getMethod(),
request.getRequestURI(), request.getRemoteAddr());
request.setAttribute("startTime", System.currentTimeMillis());
return true;
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response,
Object handler, Exception ex) {
long duration = System.currentTimeMillis() - (long)request.getAttribute("startTime");
logger.info("Completed {} in {}ms with status {}",
request.getRequestURI(), duration, response.getStatus());
}
}
4.3 AOP实现方法调用追踪
java复制@Aspect
@Component
public class TraceAspect {
@Around("execution(* com.example..*.*(..))")
public Object trace(ProceedingJoinPoint pjp) throws Throwable {
String method = pjp.getSignature().toShortString();
Logger logger = LoggerFactory.getLogger(pjp.getTarget().getClass());
logger.trace("Entering {}", method);
try {
return pjp.proceed();
} finally {
logger.trace("Exiting {}", method);
}
}
}
日志收集建议方案:
- 使用Logback的MDC功能传递traceId
- ELK(Elasticsearch+Logstash+Kibana)搭建日志平台
- 关键日志字段:
- traceId:全链路唯一标识
- spanId:调用层级关系
- serviceName:服务名称
- timestamp:精确到毫秒
5. 生产环境最佳实践
5.1 执行顺序优化
三者的执行顺序对性能有重要影响:
- 过滤器链(FilterChain)
- 拦截器preHandle(按注册顺序)
- Controller方法
- 拦截器postHandle(逆序)
- 拦截器afterCompletion(逆序)
- 过滤器Chain.doFilter后续逻辑
- AOP环绕通知(根据切面定义顺序)
建议配置:
java复制@Configuration
public class WebConfig implements WebMvcConfigurer {
@Override
public void addInterceptors(InterceptorRegistry registry) {
// 先添加的先执行preHandle,后执行postHandle/afterCompletion
registry.addInterceptor(new LogInterceptor()).order(1);
registry.addInterceptor(new AuthInterceptor()).order(2);
}
}
@Bean
public FilterRegistrationBean<RateLimitFilter> rateLimitFilter() {
FilterRegistrationBean<RateLimitFilter> bean = new FilterRegistrationBean<>();
bean.setFilter(new RateLimitFilter());
bean.setOrder(Ordered.HIGHEST_PRECEDENCE); // 最先执行
return bean;
}
5.2 性能调优建议
-
过滤器层:
- 简单校验(如JWT签名)放在这里
- 避免在过滤器中做数据库操作
-
拦截器层:
- 权限校验缓存用户角色信息
- 耗时操作考虑异步处理
-
AOP层:
- 谨慎使用@Around,尽量用@Before/@After
- 避免在切面中处理大量数据
5.3 常见问题排查
问题1:过滤器不生效
- 检查是否被@ComponentScan扫描到
- 确认FilterRegistrationBean的urlPatterns配置正确
- 查看order值是否被其他过滤器覆盖
问题2:拦截器preHandle返回true但请求未到达Controller
- 检查是否有更高优先级的拦截器返回false
- 确认没有抛出未捕获的异常
问题3:AOP切面不执行
- 确认切点表达式匹配目标方法
- 检查是否在同一个Bean内调用(自调用问题)
- 确认没有其他切面通过Order覆盖了当前切面
我在实际项目中最深刻的教训是:曾经因为一个权限拦截器的order值设置不当,导致登录接口本身被拦截。这个bug在测试环境没有发现,因为测试账号恰好有所有权限。上线后普通用户完全无法登录,造成了严重事故。现在我会严格遵守以下原则:
- 登录接口完全绕过权限校验
- 关键过滤器/拦截器有单元测试验证
- 生产环境部署前用不同权限账号完整测试
