拦截器和切面(AOP)

一、核心概念:先搞懂 "是什么"

1. 面向切面编程(AOP):一种编程思想

AOP 的核心是抽离通用的 "横切逻辑"(比如日志记录、权限校验、事务管理、性能监控),让这些逻辑与核心业务代码分离,避免重复编码。

可以用生活例子理解:

  • 核心业务:餐厅做菜(炒菜、炖汤)
  • 横切逻辑:洗菜、洗碗(所有菜品都需要,但不属于 "做菜" 核心)
  • AOP 就是把 "洗菜 / 洗碗" 抽出来,专门有人负责,厨师只专注做菜。

AOP 的核心术语(新手必记):

术语 通俗解释
切面(Aspect) 封装横切逻辑的类(比如 "日志切面")
连接点(JoinPoint) 程序执行的某个点(比如方法调用、异常抛出)
切入点(Pointcut) 匹配连接点的规则(比如 "拦截所有 controller 的方法")
通知(Advice) 切面在连接点执行的动作(前置、后置、环绕等)
2. 拦截器:AOP 思想的一种具体实现

拦截器是基于 "拦截" 机制实现 AOP 的工具,通常用于特定场景(比如 HTTP 请求拦截、方法调用拦截)。不同框架有不同的拦截器实现:

  • Spring MVC:HandlerInterceptor(拦截 HTTP 请求)
  • MyBatis:Interceptor(拦截 SQL 执行)
  • Java 动态代理:手动实现拦截逻辑

简单说:AOP 是思想,拦截器是实现 AOP 的 "工具" 之一(其他工具还有过滤器、AspectJ、动态代理等)。

二、实战代码:Spring Boot 中快速实现

下面用 Spring Boot 分别实现 "请求拦截器" 和 "AOP 切面",帮你直观理解。

场景 1:实现 HTTP 请求拦截器(拦截所有接口请求)

需求:拦截所有 /api 开头的请求,记录请求 URL 和执行时间。

步骤 1:自定义拦截器类
java 复制代码
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

// 自定义请求拦截器
public class RequestLogInterceptor implements HandlerInterceptor {
    private static final Logger log = LoggerFactory.getLogger(RequestLogInterceptor.class);
    // 存储请求开始时间
    private ThreadLocal<Long> startTime = new ThreadLocal<>();

    // 1. 前置处理:请求到达控制器前执行
    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        startTime.set(System.currentTimeMillis());
        log.info("请求开始 | URL: {} | 请求方式: {}", request.getRequestURL(), request.getMethod());
        // 返回true:放行请求;返回false:拦截请求(比如权限不足时)
        return true;
    }

    // 2. 后置处理:控制器执行完、视图渲染前执行
    @Override
    public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
        long costTime = System.currentTimeMillis() - startTime.get();
        log.info("请求结束 | URL: {} | 耗时: {}ms", request.getRequestURL(), costTime);
        // 移除ThreadLocal,避免内存泄漏
        startTime.remove();
    }
}
步骤 2:配置拦截器(注册到 Spring)
java 复制代码
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration // 标记为配置类
public class WebInterceptorConfig implements WebMvcConfigurer {

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        // 注册自定义拦截器,并指定拦截的URL规则
        registry.addInterceptor(new RequestLogInterceptor())
                .addPathPatterns("/api/**") // 拦截/api开头的请求
                .excludePathPatterns("/api/login"); // 排除登录请求
    }
}
场景 2:实现 AOP 切面(拦截指定方法)

需求:拦截所有 Service 层的方法,记录方法入参、出参和执行时间。

步骤 1:引入 AOP 依赖(pom.xml)
java 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
步骤 2:编写 AOP 切面类
java 复制代码
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

// 1. @Aspect:标记为切面类;@Component:注册到Spring容器
@Aspect
@Component
public class ServiceLogAspect {
    private static final Logger log = LoggerFactory.getLogger(ServiceLogAspect.class);

    // 2. 定义切入点:匹配所有com.example.demo.service包下的所有方法
    @Pointcut("execution(* com.example.demo.service.*.*(..))")
    public void servicePointcut() {}

    // 3. 环绕通知:方法执行前后都能处理(最常用的通知类型)
    @Around("servicePointcut()")
    public Object logServiceMethod(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取方法名和入参
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        
        // 前置:记录方法开始执行
        log.info("Service方法开始 | 方法名: {} | 入参: {}", methodName, args);
        
        long startTime = System.currentTimeMillis();
        // 执行原方法(核心业务逻辑)
        Object result = joinPoint.proceed();
        long costTime = System.currentTimeMillis() - startTime;
        
        // 后置:记录方法执行结果
        log.info("Service方法结束 | 方法名: {} | 出参: {} | 耗时: {}ms", methodName, result, costTime);
        
        return result;
    }
}

三、拦截器 vs AOP:核心区别(新手必分清)

维度 拦截器 AOP 切面
适用场景 主要拦截请求级(HTTP 请求) 主要拦截方法级(任意方法)
粒度 较粗(按 URL / 控制器拦截) 极细(按方法、包、注解精准拦截)
底层实现 基于 Spring MVC 的拦截机制 基于动态代理(JDK/CGLIB)+ AspectJ
触发时机 请求到达控制器前 / 后 方法执行前 / 后 / 异常时 / 最终执行

总结

  1. 核心关系:AOP 是 "横切逻辑解耦" 的编程思想,拦截器是实现 AOP 的一种具体手段;
  2. 使用场景 :拦截请求用拦截器 ,拦截任意方法(Service/Util)用AOP 切面
  3. 核心要点
    • 拦截器核心是实现HandlerInterceptor并配置拦截规则;
    • AOP 核心是通过@Aspect定义切面,@Pointcut定义拦截规则,@Around等注解定义通知逻辑。

实际开发中最高频的 3 个场景(权限拦截、全局异常处理、性能监控告警)

四、场景 1:基于拦截器的接口权限校验(实战高频)

需求

拦截所有/api/**的请求,校验请求头中token是否有效:

  • 有有效 token → 放行请求;
  • 无 token / 无效 token → 拦截请求,返回标准化的错误 JSON 响应。
实现步骤(完整可运行)
步骤 1:定义统一响应类(规范返回格式)
java 复制代码
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

// 全局统一响应体
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
    // 响应码:200成功,401未授权,500系统异常
    private int code;
    // 响应信息
    private String msg;
    // 响应数据
    private T data;

    // 快捷方法:返回成功响应
    public static <T> Result<T> success(T data) {
        return new Result<>(200, "操作成功", data);
    }

    // 快捷方法:返回失败响应
    public static <T> Result<T> fail(int code, String msg) {
        return new Result<>(code, msg, null);
    }
}
步骤 2:自定义权限拦截器
java 复制代码
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.MediaType;
import org.springframework.web.servlet.HandlerInterceptor;

import java.io.PrintWriter;

public class AuthInterceptor implements HandlerInterceptor {
    private static final Logger log = LoggerFactory.getLogger(AuthInterceptor.class);
    // 模拟有效的token(实际开发中从Redis/数据库查询)
    private static final String VALID_TOKEN = "user_123456_token";
    private final ObjectMapper objectMapper = new ObjectMapper();

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 1. 获取请求头中的token
        String token = request.getHeader("token");
        
        // 2. 校验token
        if (token == null || !token.equals(VALID_TOKEN)) {
            log.warn("权限拦截 | URL: {} | 原因:token无效或为空", request.getRequestURL());
            
            // 3. 拦截请求,返回标准化错误响应
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.setCharacterEncoding("UTF-8");
            Result<Void> failResult = Result.fail(401, "未授权:请先登录并携带有效token");
            // 将响应体写入返回流
            PrintWriter writer = response.getWriter();
            writer.write(objectMapper.writeValueAsString(failResult));
            writer.flush();
            writer.close();
            
            // 返回false:拦截请求,不再继续执行
            return false;
        }
        
        // 4. token有效,放行请求
        log.info("权限校验通过 | URL: {} | token: {}", request.getRequestURL(), token);
        return true;
    }
}
步骤 3:注册拦截器
java 复制代码
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class AuthWebConfig implements WebMvcConfigurer {
    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(new AuthInterceptor())
                .addPathPatterns("/api/**") // 拦截所有/api请求
                .excludePathPatterns("/api/login"); // 排除登录接口(登录不需要token)
    }
}
测试效果
  • 请求头不带 token:返回{"code":401,"msg":"未授权:请先登录并携带有效token","data":null}
  • 请求头带token: user_123456_token:正常访问接口。

五、场景 2:基于 AOP 的全局异常处理(消除冗余 try-catch)

需求

拦截 Service 层所有方法的异常,统一记录异常日志 + 返回标准化错误信息 ,避免在每个 Service 方法中写重复的try-catch

实现步骤
步骤 1:自定义业务异常类(可选,区分业务异常和系统异常)
java 复制代码
// 业务异常(比如参数错误、数据不存在)
public class BusinessException extends RuntimeException {
    private int code; // 自定义异常码

    public BusinessException(int code, String message) {
        super(message);
        this.code = code;
    }

    public int getCode() {
        return code;
    }
}
步骤 2:编写异常处理切面类
java 复制代码
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class ServiceExceptionAspect {
    private static final Logger log = LoggerFactory.getLogger(ServiceExceptionAspect.class);

    // 切入点:匹配所有Service层方法
    @Pointcut("execution(* com.example.demo.service.*.*(..))")
    public void serviceExceptionPointcut() {}

    // 环绕通知:捕获并处理异常
    @Around("serviceExceptionPointcut()")
    public Object handleServiceException(ProceedingJoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        
        try {
            // 执行原Service方法
            return joinPoint.proceed();
        } catch (BusinessException e) {
            // 捕获业务异常:记录日志,返回错误响应
            log.error("Service方法业务异常 | 方法名: {} | 入参: {} | 异常码: {} | 信息: {}", 
                    methodName, args, e.getCode(), e.getMessage());
            return Result.fail(e.getCode(), e.getMessage());
        } catch (Throwable e) {
            // 捕获系统异常:记录日志,返回通用错误
            log.error("Service方法系统异常 | 方法名: {} | 入参: {} | 异常信息: {}", 
                    methodName, args, e.getMessage(), e); // 打印完整堆栈
            return Result.fail(500, "系统内部错误,请稍后重试");
        }
    }
}
测试效果

在 Service 中抛出异常:

java 复制代码
@Service
public class UserService {
    public String getUserById(Long id) {
        if (id == null || id <= 0) {
            // 抛业务异常
            throw new BusinessException(400, "用户ID不能为空且必须大于0");
        }
        // 模拟查询数据库
        if (id == 999L) {
            // 抛系统异常
            throw new NullPointerException("用户数据不存在");
        }
        return "用户" + id;
    }
}
  • 调用getUserById(-1):返回{"code":400,"msg":"用户ID不能为空且必须大于0","data":null}
  • 调用getUserById(999):返回{"code":500,"msg":"系统内部错误,请稍后重试","data":null},且日志中会打印完整异常堆栈。

六、场景 3:基于 AOP 的方法性能监控(带阈值告警)

需求

监控指定方法的执行时间,当耗时超过500ms时,触发告警日志(方便定位性能瓶颈),同时记录所有方法的执行耗时。

实现步骤
java 复制代码
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class PerformanceMonitorAspect {
    private static final Logger log = LoggerFactory.getLogger(PerformanceMonitorAspect.class);
    // 性能阈值:超过500ms则告警
    private static final long PERFORMANCE_THRESHOLD = 500;

    // 切入点:匹配所有Service和Controller层方法
    @Pointcut("execution(* com.example.demo.service.*.*(..)) || execution(* com.example.demo.controller.*.*(..))")
    public void performancePointcut() {}

    @Around("performancePointcut()")
    public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        // 1. 记录方法开始时间
        long startTime = System.currentTimeMillis();
        String fullMethodName = joinPoint.getSignature().toLongString(); // 完整方法名(含包名)
        
        try {
            // 2. 执行原方法
            return joinPoint.proceed();
        } finally {
            // 3. 计算耗时(finally确保无论是否异常都执行)
            long costTime = System.currentTimeMillis() - startTime;
            
            // 4. 耗时超过阈值则告警
            if (costTime > PERFORMANCE_THRESHOLD) {
                log.warn("【性能告警】方法执行超时 | 方法: {} | 耗时: {}ms | 阈值: {}ms",
                        fullMethodName, costTime, PERFORMANCE_THRESHOLD);
            } else {
                log.info("方法执行耗时 | 方法: {} | 耗时: {}ms", fullMethodName, costTime);
            }
        }
    }
}
测试效果

在 Service 中模拟耗时方法:

java 复制代码
@Service
public class OrderService {
    public void queryOrderList() throws InterruptedException {
        // 模拟耗时600ms
        Thread.sleep(600);
    }
}

调用queryOrderList()后,日志会输出:【性能告警】方法执行超时 | 方法: public void com.example.demo.service.OrderService.queryOrderList() | 耗时: 601ms | 阈值: 500ms

总结

  1. 权限拦截 :用拦截器校验请求级的 token,核心是preHandle返回 false 拦截请求,并自定义响应;
  2. 全局异常处理:用 AOP 环绕通知捕获方法异常,区分业务 / 系统异常,统一日志和响应格式,消除冗余 try-catch;
  3. 性能监控:用 AOP 的 finally 块确保耗时统计不遗漏,设置阈值触发告警,快速定位慢方法。
相关推荐
€8113 天前
Java入门级教程26——序列化和反序列化,Redis存储Java对象、查询数据库与实现多消费者消息队列
java·拦截器·序列化和反序列化·数据库查询·redis存储java对象·多消费者消息队列
独断万古他化4 天前
【Spring 核心:AOP】基础到深入:思想、实现方式、切点表达式与自定义注解全梳理
java·spring·spring aop·aop·切面编程
小肖爱笑不爱笑13 天前
登录认证-会话技术、JWT令牌、过滤器Filter、拦截器Interceptor
java·开发语言·过滤器·拦截器·登录认证
while(1){yan}24 天前
SpringAOP
java·开发语言·spring boot·spring·aop
heartbeat..1 个月前
Spring AOP 全面详解(通俗易懂 + 核心知识点 + 完整案例)
java·数据库·spring·aop
清风徐来QCQ1 个月前
SpringMvc(Interceptor,Filter)
过滤器·拦截器
while(1){yan}1 个月前
拦截器(详解)
数据库·spring boot·spring·java-ee·拦截器
小徐敲java1 个月前
使用aop切面springmvc后抛出异常一直捕捉不到异常(抛出异常UndeclaredThrowableException类)
spring·aop
0和1的舞者1 个月前
Spring AOP详解(一)
java·开发语言·前端·spring·aop·面向切面