一、核心概念:先搞懂 "是什么"
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 |
| 触发时机 | 请求到达控制器前 / 后 | 方法执行前 / 后 / 异常时 / 最终执行 |
总结
- 核心关系:AOP 是 "横切逻辑解耦" 的编程思想,拦截器是实现 AOP 的一种具体手段;
- 使用场景 :拦截请求用拦截器 ,拦截任意方法(Service/Util)用AOP 切面;
- 核心要点 :
- 拦截器核心是实现
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
总结
- 权限拦截 :用拦截器校验请求级的 token,核心是
preHandle返回 false 拦截请求,并自定义响应; - 全局异常处理:用 AOP 环绕通知捕获方法异常,区分业务 / 系统异常,统一日志和响应格式,消除冗余 try-catch;
- 性能监控:用 AOP 的 finally 块确保耗时统计不遗漏,设置阈值触发告警,快速定位慢方法。