# 第05天:Spring AOP基础 ## 📚 今日目标 1. 理解AOP的概念和作用 2. 理解切面、切点、通知的概念 3. 学会创建AOP切面 4. 理解@Around、@Before、@After等注解 --- ## 🎯 知识点1:什么是AOP? ### 生活中的例子 **横切关注点**: - 日志记录:每个方法都要记录日志 - 事务管理:每个方法都要开启/提交事务 - 权限检查:每个方法都要检查权限 **问题**:如果每个方法都写一遍,代码重复,维护困难 **解决方案**:AOP(面向切面编程) - 把横切关注点提取出来 - 在需要的地方自动应用 - 不污染业务代码 ### AOP核心概念 **切面(Aspect)**:横切关注点的模块化(如日志切面、事务切面) **切点(Pointcut)**:匹配哪些方法需要被拦截 **通知(Advice)**:什么时候执行(Before、After、Around) **连接点(JoinPoint)**:方法执行的点 --- ## 🎯 知识点2:AOP通知类型 ### @Before:前置通知 ```java @Before("execution(* com.example.service.*.*(..))") public void before(JoinPoint joinPoint) { System.out.println("方法执行前"); // 可以获取方法参数 Object[] args = joinPoint.getArgs(); } ``` ### @After:后置通知 ```java @After("execution(* com.example.service.*.*(..))") public void after(JoinPoint joinPoint) { System.out.println("方法执行后"); } ``` ### @Around:环绕通知(最重要) ```java @Around("execution(* com.example.service.*.*(..))") public Object around(ProceedingJoinPoint joinPoint) throws Throwable { // 方法执行前 System.out.println("方法执行前"); // 执行原方法 Object result = joinPoint.proceed(); // 方法执行后 System.out.println("方法执行后"); return result; } ``` **为什么用@Around?** - 可以在方法执行前后都处理 - 可以控制是否执行原方法 - 可以修改返回值 --- ## 🛠️ 实践任务:创建路由切面 ### 步骤1:创建DBRouterJoinPoint切面类 在 `src/main/java/cn/bugstack/middleware/db/router/` 目录下创建 `DBRouterJoinPoint.java`: ```java package cn.bugstack.middleware.db.router; import cn.bugstack.middleware.db.router.annotation.DBRouter; import cn.bugstack.middleware.db.router.config.DBRouterConfig; import cn.bugstack.middleware.db.router.strategy.IDBRouterStrategy; import cn.bugstack.middleware.db.router.util.PropertyUtil; 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 java.lang.reflect.Method; /** * 数据库路由切面 * * 拦截带@DBRouter注解的方法,执行路由逻辑 * * @author 小傅哥 */ @Aspect public class DBRouterJoinPoint { private Logger logger = LoggerFactory.getLogger(DBRouterJoinPoint.class); private DBRouterConfig dbRouterConfig; private IDBRouterStrategy dbRouterStrategy; public DBRouterJoinPoint(DBRouterConfig dbRouterConfig, IDBRouterStrategy dbRouterStrategy) { this.dbRouterConfig = dbRouterConfig; this.dbRouterStrategy = dbRouterStrategy; } /** * 定义切点:拦截所有带@DBRouter注解的方法 */ @Pointcut("@annotation(cn.bugstack.middleware.db.router.annotation.DBRouter)") public void aopPoint() { } /** * 环绕通知:在方法执行前后处理路由逻辑 */ @Around("aopPoint() && @annotation(dbRouter)") public Object doRouter(ProceedingJoinPoint jp, DBRouter dbRouter) throws Throwable { String dbKey = dbRouter.key(); if (null == dbKey || dbKey.isEmpty()) { throw new RuntimeException("annotation DBRouter key is null!"); } // 获取路由键的值 String dbKeyAttr = getAttrValue(dbKey, jp.getArgs()); // 执行路由 dbRouterStrategy.doRouter(dbKeyAttr); // 执行原方法 try { return jp.proceed(); } finally { // 清理路由信息 dbRouterStrategy.clear(); } } /** * 获取路由键的值 * * @param attr 路由键字段名(如:userId) * @param args 方法参数数组 * @return 路由键的值 */ public String getAttrValue(String attr, Object[] args) { if (1 == args.length) { // 只有一个参数,直接从这个参数获取 Object arg = args[0]; if (arg instanceof String) { return arg.toString(); } return String.valueOf(PropertyUtil.getProperty(arg, attr)); } // 多个参数,遍历查找包含该属性的对象 for (Object arg : args) { if (arg == null) { continue; } try { Object value = PropertyUtil.getProperty(arg, attr); if (null != value) { return String.valueOf(value); } } catch (Exception e) { // 忽略,继续查找 } } throw new RuntimeException("未找到路由键: " + attr); } /** * 获取方法对象 */ private Method getMethod(JoinPoint jp) throws NoSuchMethodException { Signature sig = jp.getSignature(); MethodSignature methodSignature = (MethodSignature) sig; return jp.getTarget().getClass().getMethod(methodSignature.getName(), methodSignature.getParameterTypes()); } } ``` ### 代码解释 1. **@Aspect**:标识这是一个切面类 2. **@Pointcut**:定义切点,匹配所有带@DBRouter注解的方法 3. **@Around**:环绕通知,可以在方法执行前后处理 4. **ProceedingJoinPoint**: - `jp.getArgs()`:获取方法参数 - `jp.proceed()`:执行原方法 5. **路由逻辑**: - 获取路由键的值 - 执行路由(设置到ThreadLocal) - 执行原方法 - 清理路由信息 --- ## 🎓 知识点拓展 ### 拓展1:切点表达式 **execution表达式**: ```java // 匹配所有public方法 execution(public * *(..)) // 匹配指定包下的所有方法 execution(* com.example.service.*.*(..)) // 匹配指定类的所有方法 execution(* com.example.service.UserService.*(..)) // 匹配指定方法 execution(* com.example.service.UserService.queryUser(..)) ``` **@annotation表达式**: ```java // 匹配所有带@DBRouter注解的方法 @annotation(cn.bugstack.middleware.db.router.annotation.DBRouter) // 匹配所有带指定注解的方法,并获取注解对象 @annotation(dbRouter) ``` **组合表达式**: ```java // 使用 &&、||、! 组合 @Pointcut("execution(* com.example.service.*.*(..)) && @annotation(DBRouter)") ``` ### 拓展2:JoinPoint vs ProceedingJoinPoint **JoinPoint**: - 只能获取信息,不能控制执行 - 用于@Before、@After **ProceedingJoinPoint**: - 继承自JoinPoint - 可以控制是否执行原方法 - 用于@Around ### 拓展3:AOP代理机制 **JDK动态代理**: - 基于接口 - 目标类必须实现接口 **CGLIB代理**: - 基于继承 - 不需要接口 **Spring选择**: - 有接口:JDK动态代理 - 无接口:CGLIB代理 --- ## ✅ 今日检查清单 - [ ] 理解了AOP的概念和作用 - [ ] 理解了切面、切点、通知的概念 - [ ] 创建了DBRouterJoinPoint切面类 - [ ] 理解了@Around通知的使用 - [ ] 理解了ProceedingJoinPoint的用法 - [ ] 完成了拓展阅读 --- ## 🎯 明日预告 明天我们将学习: - Spring Boot自动配置原理 - @Configuration和@Bean - spring.factories文件 --- ## 💡 思考题 1. 为什么用@Around而不是@Before+@After? 2. 如何获取被拦截方法的返回值? 3. AOP的代理机制是什么? --- ## 📚 参考资源 - [Spring AOP官方文档](https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#aop) - [AspectJ切点表达式](https://www.eclipse.org/aspectj/doc/released/adk15notebook/ataspectj-pcadvice.html) - [Spring AOP原理](https://www.baeldung.com/spring-aop)