Spring AOP(面向切面编程)
定位说明
Spring 系列第 3 篇。核心回答:日志/事务这类横切逻辑怎么不侵入业务——九部分从问题提出到核心概念/术语、实现原理(JDK 动态代理 vs CGLIB)、五种通知类型、切入点表达式、实际应用、执行顺序与最佳实践。前置:动态代理。
第一部分:问题的提出
一、传统方式的困境
想象一个场景:
// 传统方式:业务代码中混入大量横切关注点
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@PostMapping
public void createUser(User user) {
// 日志:记录请求
System.out.println("创建用户请求:" + user);
// 权限检查
if (!hasPermission()) {
throw new SecurityException("无权限");
}
// 性能监控:记录开始时间
long startTime = System.currentTimeMillis();
try {
// 业务逻辑
userService.createUser(user);
// 日志:记录成功
System.out.println("用户创建成功");
} catch (Exception e) {
// 异常处理
System.out.println("用户创建失败:" + e.getMessage());
throw e;
} finally {
// 性能监控:记录耗时
long endTime = System.currentTimeMillis();
System.out.println("耗时:" + (endTime - startTime) + "ms");
}
}
@GetMapping("/{id}")
public User getUser(@PathVariable int id) {
// 又要重复写一遍日志、权限、性能监控的代码...
System.out.println("查询用户请求:" + id);
if (!hasPermission()) {
throw new SecurityException("无权限");
}
long startTime = System.currentTimeMillis();
try {
User user = userService.getUser(id);
System.out.println("用户查询成功");
return user;
} catch (Exception e) {
System.out.println("用户查询失败:" + e.getMessage());
throw e;
} finally {
long endTime = System.currentTimeMillis();
System.out.println("耗时:" + (endTime - startTime) + "ms");
}
}
@DeleteMapping("/{id}")
public void deleteUser(@PathVariable int id) {
// 又要重复写一遍...
}
}
这种方式有什么问题?
- 代码重复 - 日志、权限、性能监控代码在每个方法中重复
- 职责混乱 - 业务逻辑和增强逻辑混在一起
- 难以维护 - 修改增强逻辑需要改动所有方法
- 难以测试 - 无法单独测试业务逻辑
- 耦合度高 - 业务代码依赖于增强逻辑
二、理想的解决方案
我们希望能这样写:
// 理想方式:业务代码只关注业务逻辑
@RestController
@RequestMapping("/api/users")
public class UserController {
@Autowired
private UserService userService;
@PostMapping
public void createUser(User user) {
// 只写业务逻辑,增强逻辑由AOP自动处理
userService.createUser(user);
}
@GetMapping("/{id}")
public User getUser(@PathVariable int id) {
// 只写业务逻辑
return userService.getUser(id);
}
@DeleteMapping("/{id}")
public void deleteUser(@PathVariable int id) {
// 只写业务逻辑
userService.deleteUser(id);
}
}
// 通过AOP自动处理日志、权限、性能监控等
这就是AOP要解决的问题:在不改变原始代码的前提下,为方法自动织入增强逻辑。
第二部分:AOP的核心概念
三、什么是AOP?
3.1 AOP的定义
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,通过在程序运行时动态地将代码切入到指定的位置,实现对横切关注点的统一处理。
┌──────────────────────────────────────────┐
│ AOP的本质 │
├──────────────────────────────────────────┤
│ │
│ 问题: │
│ ├─ 日志、权限、事务等逻辑 │
│ ├─ 与具体业务无关 │
│ ├─ 但所有方法都需要 │
│ └─ 导致代码重复、耦合高 │
│ │
│ 解决方案: │
│ ├─ 将这些逻辑抽离出来 │
│ ├─ 统一成"切面" │
│ ├─ 通过代理模式织入 │
│ └─ 实现"非侵入式编程" │
│ │
│ 核心思想: │
│ └─ "在什么时候,在什么位置, │
│ 做什么事情" │
│ │
└──────────────────────────────────────────┘
3.2 AOP与面向对象编程(OOP)的关系
┌──────────────────────────────────────────┐
│ AOP与OOP的关系 │
├──────────────────────────────────────────┤
│ │
│ OOP(面向对象编程) │
│ ├─ 纵向:类的继承、多态 │
│ ├─ 解决:代码复用、结构清晰 │
│ └─ 特点:纵向扩展 │
│ │
│ AOP(面向切面编程) │
│ ├─ 横向:切面的织入 │
│ ├─ 解决:横切关注点 │
│ └─ 特点:横向扩展 │
│ │
│ 关系: │
│ └─ AOP是OOP的补充和扩展 │
│ OOP处理纵向,AOP处理横向 │
│ │
└──────────────────────────────────────────┘
3.3 AOP的核心优势
┌──────────────────────────────────────────┐
│ AOP的核心优势 │
├──────────────────────────────────────────┤
│ │
│ 1. 解耦 │
│ ├─ 业务逻辑和增强逻辑分离 │
│ ├─ 易于维护和扩展 │
│ └─ 符合单一职责原则 │
│ │
│ 2. 非侵入式编程 │
│ ├─ 不改动原始代码 │
│ ├─ 通过配置织入增强逻辑 │
│ └─ 想加就加,想撕就撕 │
│ │
│ 3. 代码复用 │
│ ├─ 一个切面适用多个方法 │
│ ├─ 减少代码重复 │
│ └─ 易于维护 │
│ │
│ 4. 灵活性 │
│ ├─ 支持多个切面组合 │
│ ├─ 支持切面顺序控制 │
│ └─ 支持条件织入 │
│ │
│ 5. 可维护性 │
│ ├─ 增强逻辑集中管理 │
│ ├─ 易于修改和扩展 │
│ └─ 代码更清晰 │
│ │
└──────────────────────────────────────────┘
第三部分:AOP的核心术语
四、AOP的专业术语
4.1 术语解释
┌──────────────────────────────────────────┐
│ AOP的核心术语 │
├──────────────────────────────────────────┤
│ │
│ 连接点(JoinPoint) │
│ ├─ 定义:所有能被增强的位置 │
│ ├─ 包括:方法、构造器、字段 │
│ └─ 例子:UserService的所有方法 │
│ │
│ 切入点(Pointcut) │
│ ├─ 定义:我们实际选中的位置 │
│ ├─ 包括:特定的方法、包下的所有方法 │
│ └─ 例子:UserService.createUser() │
│ │
│ 通知(Advice) │
│ ├─ 定义:真正的增强逻辑 │
│ ├─ 包括:日志、权限、事务等 │
│ └─ 例子:打印日志的代码 │
│ │
│ 切面(Aspect) │
│ ├─ 定义:通知+切入点的组合 │
│ ├─ 包括:在哪里、什么时候、做什么 │
│ └─ 例子:日志切面 │
│ │
│ 目标对象(Target) │
│ ├─ 定义:被增强的原始对象 │
│ ├─ 包括:UserService实例 │
│ └─ 特点:不知道自己被增强了 │
│ │
│ 代理对象(Proxy) │
│ ├─ 定义:加完增强的对象 │
│ ├─ 包括:由Spring创建的代理类实例 │
│ └─ 特点:对外使用的是这个对象 │
│ │
│ 织入(Weaving) │
│ ├─ 定义:将增强逻辑应用到目标的过程 │
│ ├─ 包括:编译期、类加载期、运行期 │
│ └─ Spring:运行期织入 │
│ │
└──────────────────────────────────────────┘
4.2 术语类比
┌──────────────────────────────────────────┐
│ AOP术语与"加特效"的类比 │
├──────────────────────────────────────────┤
│ │
│ 术语 现实类比 │
│ ───────────────────────────────────── │
│ 连接点 能加特效的动作 │
│ (跳跃、攻击) │
│ │
│ 切入点 选中"攻击"动作 │
│ 加特效 │
│ │
│ 通知 加特效本身 │
│ (发光、爆炸) │
│ │
│ 切面 发光特效应用到 │
│ 攻击动作上 │
│ │
│ 目标对象 没有特效的 │
│ 普通角色 │
│ │
│ 代理对象 看上去很牛的 │
│ 角色 │
│ │
│ 织入 给角色加特效 │
│ 的过程 │
│ │
└──────────────────────────────────────────┘
第四部分:AOP的实现原理
五、Spring AOP的实现方式
5.1 两种代理方式
┌──────────────────────────────────────────┐
│ Spring AOP的两种代理方式 │
├──────────────────────────────────────────┤
│ │
│ JDK动态代理 │
│ ├─ 原理:基于接口实现 │
│ ├─ 前提:目标类必须实现接口 │
│ ├─ 优点:原生支持,无需依赖 │
│ ├─ 缺点:只能代理接口方法 │
│ └─ 性能:反射调用,性能一般 │
│ │
│ CGLIB动态代理 │
│ ├─ 原理:通过继承+字节码增强 │
│ ├─ 前提:目标类可以没有接口 │
│ ├─ 优点:支持代理类,灵活性强 │
│ ├─ 缺点:需要CGLIB依赖 │
│ └─ 性能:字节码调用,性能较好 │
│ │
│ Spring的选择策略: │
│ ├─ Spring Framework:优先JDK │
│ └─ Spring Boot 2.x+:优先CGLIB │
│ │
└──────────────────────────────────────────┘
5.2 JDK动态代理实现AOP
// JDK动态代理实现AOP
// 1. 定义业务接口
public interface UserService {
void createUser(User user);
User getUser(int id);
}
// 2. 实现业务接口
@Service
public class UserServiceImpl implements UserService {
@Override
public void createUser(User user) {
System.out.println("创建用户:" + user.getName());
}
@Override
public User getUser(int id) {
System.out.println("查询用户:" + id);
return new User(id, "Tom");
}
}
// 3. 创建代理对象
public class JDKProxyAOPDemo {
public static void main(String[] args) {
// 创建目标对象
UserService target = new UserServiceImpl();
// 创建代理对象
UserService proxy = (UserService) Proxy.newProxyInstance(
UserService.class.getClassLoader(),
new Class[]{UserService.class},
(proxyObj, method, args) -> {
// 前置增强
System.out.println("=== 方法调用前 ===");
System.out.println("方法名:" + method.getName());
System.out.println("参数:" + Arrays.toString(args));
// 调用原方法
long startTime = System.currentTimeMillis();
Object result = method.invoke(target, args);
long endTime = System.currentTimeMillis();
// 后置增强
System.out.println("=== 方法调用后 ===");
System.out.println("耗时:" + (endTime - startTime) + "ms");
System.out.println("返回值:" + result);
return result;
}
);
// 调用代理对象的方法
proxy.createUser(new User(1, "Tom"));
User user = proxy.getUser(1);
}
}
5.3 CGLIB动态代理实现AOP
// CGLIB动态代理实现AOP
// 1. 定义业务类(无需接口)
@Service
public class UserService {
public void createUser(User user) {
System.out.println("创建用户:" + user.getName());
}
public User getUser(int id) {
System.out.println("查询用户:" + id);
return new User(id, "Tom");
}
}
// 2. 创建代理对象
public class CGLIBProxyAOPDemo {
public static void main(String[] args) {
// 创建目标对象
UserService target = new UserService();
// 创建Enhancer
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(UserService.class);
enhancer.setCallback((MethodInterceptor) (obj, method, args, proxy) -> {
// 前置增强
System.out.println("=== 方法调用前 ===");
System.out.println("方法名:" + method.getName());
System.out.println("参数:" + Arrays.toString(args));
// 调用原方法
long startTime = System.currentTimeMillis();
Object result = proxy.invokeSuper(obj, args);
long endTime = System.currentTimeMillis();
// 后置增强
System.out.println("=== 方法调用后 ===");
System.out.println("耗时:" + (endTime - startTime) + "ms");
System.out.println("返回值:" + result);
return result;
});
// 创建代理对象
UserService proxy = (UserService) enhancer.create();
// 调用代理对象的方法
proxy.createUser(new User(1, "Tom"));
User user = proxy.getUser(1);
}
}
第五部分:AOP的通知类型
六、五种通知类型
6.1 通知类型概览
┌──────────────────────────────────────────┐
│ AOP的五种通知类型 │
├──────────────────────────────────────────┤
│ │
│ @Before(前置通知) │
│ ├─ 执行时机:方法执行前 │
│ ├─ 用途:权限检查、参数验证 │
│ └─ 特点:无法获取返回值 │
│ │
│ @After(后置通知) │
│ ├─ 执行时机:方法执行后(无论异常) │
│ ├─ 用途:资源释放、日志记录 │
│ └─ 特点:总是执行 │
│ │
│ @AfterReturning(返回通知) │
│ ├─ 执行时机:方法成功返回后 │
│ ├─ 用途:处理返回值、缓存 │
│ └─ 特点:可以获取返回值 │
│ │
│ @AfterThrowing(异常通知) │
│ ├─ 执行时机:方法抛异常时 │
│ ├─ 用途:异常处理、告警 │
│ └─ 特点:可以获取异常信息 │
│ │
│ @Around(环绕通知) │
│ ├─ 执行时机:包裹整个方法执行 │
│ ├─ 用途:性能监控、事务管理 │
│ └─ 特点:最灵活,能实现所有通知效果 │
│ │
└──────────────────────────────────────────┘
6.2 通知执行顺序
┌──────────────────────────────────────────┐
│ 通知的执行顺序 │
├──────────────────────────────────────────┤
│ │
│ 正常执行流程: │
│ ├─ @Before(前置) │
│ ├─ 执行原方法 │
│ ├─ @AfterReturning(返回) │
│ ├─ @After(后置) │
│ └─ 返回结果 │
│ │
│ 异常执行流程: │
│ ├─ @Before(前置) │
│ ├─ 执行原方法(抛异常) │
│ ├─ @AfterThrowing(异常) │
│ ├─ @After(后置) │
│ └─ 抛出异常 │
│ │
│ @Around的位置: │
│ └─ 包裹整个流程,最外层 │
│ │
└──────────────────────────────────────────┘
6.3 各通知类型的实现
// 各通知类型的实现
@Aspect
@Component
public class UserServiceAspect {
// 1. 前置通知:方法执行前
@Before("execution(* com.example.service.UserService.*(..))")
public void beforeMethod(JoinPoint joinPoint) {
System.out.println("=== 前置通知 ===");
System.out.println("方法名:" + joinPoint.getSignature().getName());
System.out.println("参数:" + Arrays.toString(joinPoint.getArgs()));
}
// 2. 后置通知:方法执行后(无论异常)
@After("execution(* com.example.service.UserService.*(..))")
public void afterMethod(JoinPoint joinPoint) {
System.out.println("=== 后置通知 ===");
System.out.println("方法执行完成");
}
// 3. 返回通知:方法成功返回后
@AfterReturning(
pointcut = "execution(* com.example.service.UserService.*(..))",
returning = "result"
)
public void afterReturningMethod(JoinPoint joinPoint, Object result) {
System.out.println("=== 返回通知 ===");
System.out.println("返回值:" + result);
}
// 4. 异常通知:方法抛异常时
@AfterThrowing(
pointcut = "execution(* com.example.service.UserService.*(..))",
throwing = "exception"
)
public void afterThrowingMethod(JoinPoint joinPoint, Exception exception) {
System.out.println("=== 异常通知 ===");
System.out.println("异常类型:" + exception.getClass().getName());
System.out.println("异常信息:" + exception.getMessage());
}
// 5. 环绕通知:包裹整个方法执行
@Around("execution(* com.example.service.UserService.*(..))")
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("=== 环绕通知 - 前置 ===");
long startTime = System.currentTimeMillis();
try {
// 执行原方法
Object result = joinPoint.proceed();
System.out.println("=== 环绕通知 - 返回 ===");
System.out.println("返回值:" + result);
return result;
} catch (Exception e) {
System.out.println("=== 环绕通知 - 异常 ===");
System.out.println("异常:" + e.getMessage());
throw e;
} finally {
long endTime = System.currentTimeMillis();
System.out.println("=== 环绕通知 - 后置 ===");
System.out.println("耗时:" + (endTime - startTime) + "ms");
}
}
}
6.4 推荐使用@Around
// 推荐使用@Around,因为它最灵活
@Aspect
@Component
public class BestPracticeAspect {
// ✅ 推荐:使用@Around实现所有通知的效果
@Around("execution(* com.example.service.*.*(..))")
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
// 前置通知
System.out.println("前置:方法执行前");
long startTime = System.currentTimeMillis();
Object result = null;
Exception exception = null;
try {
// 执行原方法
result = joinPoint.proceed();
// 返回通知
System.out.println("返回:" + result);
return result;
} catch (Exception e) {
// 异常通知
exception = e;
System.out.println("异常:" + e.getMessage());
throw e;
} finally {
// 后置通知
long endTime = System.currentTimeMillis();
System.out.println("后置:耗时" + (endTime - startTime) + "ms");
}
}
}
第六部分:切入点表达式
七、切入点表达式详解
7.1 切入点表达式语法
// 切入点表达式语法
// 基本格式:execution(修饰符 返回类型 包名.类名.方法名(参数))
// 1. 匹配所有方法
@Pointcut("execution(* *(..))")
public void allMethods() {}
// 2. 匹配特定包下的所有方法
@Pointcut("execution(* com.example.service.*.*(..))")
public void servicePackage() {}
// 3. 匹配特定类的所有方法
@Pointcut("execution(* com.example.service.UserService.*(..))")
public void userServiceClass() {}
// 4. 匹配特定方法
@Pointcut("execution(* com.example.service.UserService.createUser(..))")
public void createUserMethod() {}
// 5. 匹配特定参数的方法
@Pointcut("execution(* com.example.service.UserService.createUser(com.example.User))")
public void createUserWithUserParam() {}
// 6. 匹配任意参数的方法
@Pointcut("execution(* com.example.service.UserService.*(..))")
public void anyParams() {}
// 7. 匹配特定返回类型的方法
@Pointcut("execution(com.example.User com.example.service.UserService.getUser(..))")
public void returnUserType() {}
// 8. 匹配void返回的方法
@Pointcut("execution(void com.example.service.UserService.*(..))")
public void voidReturn() {}
// 9. 匹配特定注解的方法
@Pointcut("@annotation(com.example.annotation.RequirePermission)")
public void annotatedMethod() {}
// 10. 匹配特定注解的类
@Pointcut("@within(com.example.annotation.Service)")
public void annotatedClass() {}
// 11. 组合切入点
@Pointcut("execution(* com.example.service.*.*(..)) && @annotation(com.example.annotation.Log)")
public void serviceAndAnnotated() {}
7.2 常用切入点表达式
// 常用的切入点表达式
@Aspect
@Component
public class CommonPointcutExpressions {
// 1. 匹配service包下所有类的所有方法
@Pointcut("execution(* com.example.service..*.*(..))")
public void serviceLayer() {}
// 2. 匹配controller包下所有类的所有方法
@Pointcut("execution(* com.example.controller..*.*(..))")
public void controllerLayer() {}
// 3. 匹配所有@Service注解的类
@Pointcut("@within(org.springframework.stereotype.Service)")
public void serviceAnnotation() {}
// 4. 匹配所有@RequestMapping注解的方法
@Pointcut("@annotation(org.springframework.web.bind.annotation.RequestMapping)")
public void requestMappingAnnotation() {}
// 5. 匹配所有public方法
@Pointcut("execution(public * com.example..*.*(..))")
public void publicMethods() {}
// 6. 匹配所有protected方法
@Pointcut("execution(protected * com.example..*.*(..))")
public void protectedMethods() {}
// 7. 匹配所有返回User对象的方法
@Pointcut("execution(com.example.User com.example..*.*(..))")
public void returnUser() {}
// 8. 匹配所有参数为User的方法
@Pointcut("execution(* com.example..*(com.example.User))")
public void paramUser() {}
// 9. 匹配所有参数为User和String的方法
@Pointcut("execution(* com.example..*(com.example.User, String))")
public void paramUserAndString() {}
// 10. 匹配所有参数个数为2的方法
@Pointcut("execution(* com.example..*(*, *))")
public void twoParams() {}
}
第七部分:AOP的实际应用
八、日志切面
// 日志切面实现
@Aspect
@Component
public class LoggingAspect {
private static final Logger logger = LoggerFactory.getLogger(LoggingAspect.class);
@Around("execution(* com.example.service..*.*(..))")
public Object logAround(ProceedingJoinPoint joinPoint) throws Throwable {
// 获取方法信息
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
String className = joinPoint.getTarget().getClass().getSimpleName();
String methodName = signature.getName();
Object[] args = joinPoint.getArgs();
// 记录方法调用前
logger.info("=== 方法调用开始 ===");
logger.info("类名:{}", className);
logger.info("方法名:{}", methodName);
logger.info("参数:{}", Arrays.toString(args));
long startTime = System.currentTimeMillis();
try {
// 执行原方法
Object result = joinPoint.proceed();
// 记录返回值
logger.info("返回值:{}", result);
return result;
} catch (Exception e) {
// 记录异常
logger.error("方法执行异常:{}", e.getMessage(), e);
throw e;
} finally {
// 记录耗时
long endTime = System.currentTimeMillis();
logger.info("耗时:{}ms", (endTime - startTime));
logger.info("=== 方法调用结束 ===");
}
}
}
九、性能监控切面
// 性能监控切面实现
@Aspect
@Component
public class PerformanceMonitoringAspect {
private static final Logger logger = LoggerFactory.getLogger(PerformanceMonitoringAspect.class);
// 定义切入点
@Pointcut("execution(* com.example.service..*.*(..))")
public void serviceLayer() {}
@Around("serviceLayer()")
public Object monitorPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
String methodName = joinPoint.getSignature().getName();
long startTime = System.currentTimeMillis();
try {
return joinPoint.proceed();
} finally {
long endTime = System.currentTimeMillis();
long duration = endTime - startTime;
// 记录耗时
logger.info("方法:{},耗时:{}ms", methodName, duration);
// 如果超过阈值,发送告警
if (duration > 1000) {
logger.warn("方法{}执行过慢,耗时{}ms", methodName, duration);
// 发送告警通知
sendAlert(methodName, duration);
}
}
}
private void sendAlert(String methodName, long duration) {
// 发送告警逻辑
System.out.println("发送告警:方法" + methodName + "执行过慢");
}
}
十、权限检查切面
// 权限检查切面实现
// 1. 定义权限注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequirePermission {
String value();
}
// 2. 定义权限检查切面
@Aspect
@Component
public class PermissionCheckAspect {
@Autowired
private UserService userService;
@Before("@annotation(requirePermission)")
public void checkPermission(JoinPoint joinPoint, RequirePermission requirePermission) {
// 获取当前用户
User currentUser = getCurrentUser();
if (currentUser == null) {
throw new SecurityException("用户未登录");
}
// 检查权限
String requiredPermission = requirePermission.value();
if (!userService.hasPermission(currentUser.getId(), requiredPermission)) {
throw new SecurityException("用户无权限:" + requiredPermission);
}
}
private User getCurrentUser() {
// 从SecurityContext获取当前用户
return null; // 实现细节
}
}
// 3. 使用权限注解
@Service
public class UserService {
@RequirePermission("DELETE_USER")
public void deleteUser(int id) {
System.out.println("删除用户:" + id);
}
@RequirePermission("UPDATE_USER")
public void updateUser(User user) {
System.out.println("更新用户:" + user.getName());
}
}
十一、事务管理切面
// 事务管理切面实现
@Aspect
@Component
public class TransactionAspect {
@Autowired
private TransactionManager transactionManager;
// 匹配所有@Transactional注解的方法
@Around("@annotation(org.springframework.transaction.annotation.Transactional)")
public Object manageTransaction(ProceedingJoinPoint joinPoint) throws Throwable {
// 开启事务
TransactionStatus status = transactionManager.getTransaction(
new DefaultTransactionDefinition()
);
try {
// 执行业务逻辑
Object result = joinPoint.proceed();
// 提交事务
transactionManager.commit(status);
return result;
} catch (Exception e) {
// 回滚事务
transactionManager.rollback(status);
throw e;
}
}
}
// 使用@Transactional注解
@Service
public class OrderService {
@Transactional
public void createOrder(Order order) {
// 业务逻辑
// 如果发生异常,事务自动回滚
}
}
十二、缓存切面
// 缓存切面实现
// 1. 定义缓存注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Cacheable {
String value();
long timeout() default 3600; // 秒
}
// 2. 定义缓存切面
@Aspect
@Component
public class CacheAspect {
private final Map<String, CacheEntry> cache = new ConcurrentHashMap<>();
@Around("@annotation(cacheable)")
public Object cacheMethod(ProceedingJoinPoint joinPoint, Cacheable cacheable) throws Throwable {
// 生成缓存key
String cacheKey = generateCacheKey(joinPoint, cacheable);
// 检查缓存
CacheEntry entry = cache.get(cacheKey);
if (entry != null && !entry.isExpired()) {
System.out.println("从缓存返回:" + cacheKey);
return entry.getValue();
}
// 执行原方法
Object result = joinPoint.proceed();
// 存入缓存
cache.put(cacheKey, new CacheEntry(result, cacheable.timeout()));
return result;
}
private String generateCacheKey(ProceedingJoinPoint joinPoint, Cacheable cacheable) {
String methodName = joinPoint.getSignature().getName();
String args = Arrays.toString(joinPoint.getArgs());
return cacheable.value() + ":" + methodName + ":" + args;
}
static class CacheEntry {
private final Object value;
private final long expireTime;
public CacheEntry(Object value, long timeout) {
this.value = value;
this.expireTime = System.currentTimeMillis() + timeout * 1000;
}
public boolean isExpired() {
return System.currentTimeMillis() > expireTime;
}
public Object getValue() {
return value;
}
}
}
// 3. 使用缓存注解
@Service
public class UserService {
@Cacheable("user")
public User getUserById(int id) {
System.out.println("从数据库查询用户:" + id);
return new User(id, "Tom");
}
}
第八部分:切面的执行顺序
十三、多切面的执行顺序
13.1 使用@Order控制顺序
// 多切面的执行顺序控制
// 1. 权限检查切面(最先执行)
@Aspect
@Component
@Order(1)
public class PermissionAspect {
@Before("execution(* com.example.service.*.*(..))")
public void checkPermission(JoinPoint joinPoint) {
System.out.println("1. 权限检查");
}
}
// 2. 日志切面
@Aspect
@Component
@Order(2)
public class LoggingAspect {
@Before("execution(* com.example.service.*.*(..))")
public void logging(JoinPoint joinPoint) {
System.out.println("2. 记录日志");
}
}
// 3. 性能监控切面(最后执行)
@Aspect
@Component
@Order(3)
public class PerformanceAspect {
@Before("execution(* com.example.service.*.*(..))")
public void monitoring(JoinPoint joinPoint) {
System.out.println("3. 性能监控");
}
}
// 执行顺序:
// 前置通知:1 → 2 → 3
// 执行原方法
// 后置通知:3 → 2 → 1
13.2 切面执行顺序的详细流程
┌──────────────────────────────────────────┐
│ 多切面的执行顺序 │
├──────────────────────────────────────────┤
│ │
│ @Order(1) - PermissionAspect │
│ ├─ @Before:权限检查 │
│ │ │
│ @Order(2) - LoggingAspect │
│ ├─ @Before:记录日志 │
│ │ │
│ @Order(3) - PerformanceAspect │
│ ├─ @Before:性能监控 │
│ │ │
│ ├─ 执行原方法 │
│ │ │
│ @Order(3) - PerformanceAspect │
│ ├─ @After:性能监控后置 │
│ │ │
│ @Order(2) - LoggingAspect │
│ ├─ @After:日志后置 │
│ │ │
│ @Order(1) - PermissionAspect │
│ ├─ @After:权限检查后置 │
│ │
└──────────────────────────────────────────┘
第九部分:AOP的最佳实践
十四、推荐做法
14.1 优先使用 @Around,而不是同时写一堆通知
能用一个
@Around表达的,就不要再拆成@Before/@AfterReturning/@AfterThrowing/@After四连。
@Aspect
@Component
public class GoodAspect {
// ✅ 推荐:使用 @Around 实现所有通知的效果
@Around("execution(* com.example.service.*.*(..))")
public Object aroundMethod(ProceedingJoinPoint joinPoint) throws Throwable {
System.out.println("前置逻辑");
try {
Object result = joinPoint.proceed(); // 执行业务方法
System.out.println("返回逻辑");
return result;
} catch (Exception e) {
System.out.println("异常逻辑");
throw e;
} finally {
System.out.println("后置逻辑");
}
}
}
- 实际上
@Around可以覆盖所有类型通知; - 逻辑集中在一个地方,方便维护、调试;
- 避免多个通知顺序、重复执行的问题。
14.2 使用 @Pointcut 提取公共切入点
不要到处复制粘贴
execution(...),统一抽成@Pointcut,维护成本低很多。
@Aspect
@Component
public class BestPracticeAspect {
// Service 层切点
@Pointcut("execution(* com.example.service..*.*(..))")
public void serviceLayer() {}
// Controller 层切点
@Pointcut("execution(* com.example.controller..*.*(..))")
public void controllerLayer() {}
// 组合切点:Controller + Service
@Pointcut("serviceLayer() || controllerLayer()")
public void allLayers() {}
@Around("serviceLayer()")
public Object monitorService(ProceedingJoinPoint joinPoint) throws Throwable {
// 可以加耗时统计、统一异常转换等
return joinPoint.proceed();
}
@Around("controllerLayer()")
public Object monitorController(ProceedingJoinPoint joinPoint) throws Throwable {
// 可以加统一日志、请求参数记录等
return joinPoint.proceed();
}
}
关键点:
- 把「横切范围」抽象成名字(
serviceLayer、controllerLayer),比一串execution(...)可读多了; - 支持组合切点:
&&、||、!; - 后续包结构、类名改变,只用改一个地方的表达式。
14.3 使用自定义注解定义“语义切点”
比起按包名/类名切,更推荐按“业务语义”切,比如:
@OpLog、@Audit、@RateLimit、@DistributedLock等。
1)定义自定义注解
// 例如:操作日志注解
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface OpLog {
String value() default ""; // 描述:比如「创建订单」「删除用户」
}
2)在业务方法上使用
@Service
public class OrderService {
@OpLog("创建订单")
public void createOrder(String userId, String productId) {
// 业务逻辑...
}
@OpLog("取消订单")
public void cancelOrder(String orderId) {
// 业务逻辑...
}
}
3)编写切面:通过 @annotation 匹配
@Aspect
@Component
public class OpLogAspect {
@Around("@annotation(opLog)")
public Object logOperation(ProceedingJoinPoint joinPoint, OpLog opLog) throws Throwable {
String desc = opLog.value();
String method = joinPoint.getSignature().toShortString();
long start = System.currentTimeMillis();
try {
Object result = joinPoint.proceed();
long cost = System.currentTimeMillis() - start;
System.out.printf("【操作日志】%s - 方法:%s - 耗时:%d ms - 状态:成功%n",
desc, method, cost);
return result;
} catch (Throwable ex) {
long cost = System.currentTimeMillis() - start;
System.out.printf("【操作日志】%s - 方法:%s - 耗时:%d ms - 状态:异常:%s%n",
desc, method, cost, ex.getMessage());
throw ex;
}
}
}
这样做的好处:
- 业务代码上只需要加一个注解,语义非常清晰;
- AOP 切面只关心「带这个注解的方法」,和包结构解耦;
- 方便按“功能”划分切面:日志切面、鉴权切面、限流切面、分布式锁切面……
⬅️ 依赖注入(DI) 🏠 00-Java ➡️ 04-Bean生命周期与三级缓存
💬 评论