初步理解 AOP
AOP(Aspect-Oriented Programming,面向切面编程)是对 OOP(面向对象编程)的补充。它要解决的核心问题是:把那些散落在各个业务方法中的重复逻辑(如日志、事务、权限、缓存)抽取出来,集中管理,让业务代码只关注业务本身。
一、为什么需要 AOP
假设你写了一个用户注册方法:
publicvoidregister(Useruser){log.info("开始注册用户:"+user.getName());// 日志longstart=System.currentTimeMillis();// 性能监控if(!currentUser.hasPermission("user:add")){// 权限校验thrownewPermissionDeniedException();}userDao.save(user);// 真正的业务longcost=System.currentTimeMillis()-start;// 性能监控log.info("注册完成,耗时:"+cost+"ms");// 日志}真正的业务只有userDao.save(user)一行,其他都是“横切关注点”。如果每个方法都这样写,代码会变得臃肿、重复、难以维护。AOP 就是把这些横切逻辑从业务方法中抽离出去,统一放到“切面”里。
二、AOP 的核心概念
| 概念 | 说明 | 类比 |
|---|---|---|
| 切面(Aspect) | 横切关注点的模块化,包含切点和通知 | 一个日志处理类 |
| 连接点(Join Point) | 程序执行中可以插入切面的点,通常是方法调用 | 每个可以被拦截的方法 |
| 切点(Pointcut) | 匹配连接点的表达式,定义“在哪里”织入 | execution(* com.example.service.*.*(..)) |
| 通知(Advice) | 在切点上执行的具体动作,定义“做什么” | log.info("方法执行") |
| 织入(Weaving) | 把切面应用到目标对象并创建代理的过程 | Spring 运行时生成代理对象 |
| 目标对象(Target) | 被切面增强的原始业务对象 | UserServiceImpl实例 |
| 代理(Proxy) | 织入后生成的代理对象,包含增强逻辑 | $Proxy12或 CGLIB 子类 |
一句话:切点定义在哪里拦截,通知定义拦截后做什么,切面把它们组合在一起,通过代理织入到目标对象。
三、五种通知类型
| 通知 | 注解 | 执行时机 |
|---|---|---|
| 前置通知 | @Before | 目标方法执行前 |
| 后置通知 | @After | 目标方法执行后(无论是否异常) |
| 返回通知 | @AfterReturning | 目标方法正常返回后 |
| 异常通知 | @AfterThrowing | 目标方法抛出异常后 |
| 环绕通知 | @Around | 目标方法执行前后,最强大 |
@Aspect@ComponentpublicclassLogAspect{@Before("execution(* com.example.service.*.*(..))")publicvoidbefore(JoinPointjp){System.out.println("执行前:"+jp.getSignature().getName());}@AfterReturning(pointcut="execution(* com.example.service.*.*(..))",returning="result")publicvoidafterReturning(JoinPointjp,Objectresult){System.out.println("返回结果:"+result);}@Around("execution(* com.example.service.*.*(..))")publicObjectaround(ProceedingJoinPointpjp)throwsThrowable{longstart=System.currentTimeMillis();Objectresult=pjp.proceed();// 执行目标方法longcost=System.currentTimeMillis()-start;System.out.println("耗时:"+cost+"ms");returnresult;}}四、AOP 的实现方式
Spring AOP 底层使用动态代理:
- 目标类实现了接口 → 默认使用 JDK 动态代理。
- 目标类没有接口 → 使用 CGLIB 动态代理。
- Spring Boot 默认强制使用 CGLIB。
代理对象在运行时生成,拦截目标方法调用,在调用前后插入通知逻辑。
// 伪代码:代理对象内部publicclassUserServiceProxyimplementsUserService{privateUserServicetarget;publicvoidregister(Useruser){// 前置通知log.info("开始注册");// 调用目标方法target.register(user);// 后置通知log.info("注册完成");}}五、AOP 能做什么
| 场景 | 说明 |
|---|---|
| 日志记录 | 统一记录方法调用日志 |
| 事务管理 | @Transactional自动开启/提交/回滚事务 |
| 权限校验 | 方法执行前检查用户权限 |
| 性能监控 | 统计方法执行耗时 |
| 缓存 | @Cacheable先查缓存,未命中才执行方法 |
| 异常处理 | 统一捕获异常并记录 |
六、快速上手示例
1. 引入依赖(Spring Boot)
<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-aop</artifactId></dependency>2. 定义切面
@Aspect@ComponentpublicclassTimeAspect{@Around("execution(* com.example.service.*.*(..))")publicObjectmeasureTime(ProceedingJoinPointpjp)throwsThrowable{longstart=System.currentTimeMillis();Objectresult=pjp.proceed();longcost=System.currentTimeMillis()-start;System.out.println(pjp.getSignature().getName()+" 耗时:"+cost+"ms");returnresult;}}3. 调用 Service 方法
@ServicepublicclassUserService{publicvoidregister(Useruser){System.out.println("注册用户:"+user.getName());}}执行userService.register(user)时,控制台会输出:
注册用户:张三 register 耗时:0ms业务代码中没有任何日志或监控代码,增强逻辑完全由切面处理。
七、注意事项
内部调用失效:同一个类中方法 A 调用方法 B,B 的增强不会生效。因为内部调用走的是原始对象,不是代理对象。
@ServicepublicclassUserService{publicvoidmethodA(){methodB();// 不经过代理,增强失效}@TransactionalpublicvoidmethodB(){}}切点表达式要准确:execution(* com.example.service.*.*(..))只匹配service包下的类。写错包名或方法签名会导致增强不生效。
不要滥用 AOP:AOP 适合处理横切关注点,不适合替代正常的业务逻辑。过度使用会让代码难以追踪。
八、总结
AOP 的核心思想是把横切关注点从业务代码中分离出来,通过代理在运行时织入。它让业务方法保持纯净,同时获得日志、事务、权限等通用能力。
| 关键点 | 说明 |
|---|---|
| 解决的问题 | 横切逻辑重复、耦合高 |
| 核心概念 | 切面、切点、通知、连接点、织入、代理 |
| 通知类型 | @Before、@After、@AfterReturning、@AfterThrowing、@Around |
| 底层实现 | JDK 动态代理 / CGLIB |
| 典型应用 | 日志、事务、权限、缓存、监控 |
| 注意事项 | 内部调用失效、切点表达式准确 |
理解 AOP,就理解了 Spring 为什么能在不修改业务代码的情况下,为方法添加事务、缓存、日志等能力。这是 Spring 框架最核心的设计思想之一。