Spring 第四天:AOP 面向切面编程与声明式事务管理
前言Spring 有两大核心一个是前几天我们重点攻克的IoC/DI另一个就是今天要深入学习的AOP面向切面编程。还记得那句话吗“AOP 是在不改变原有代码的前提下对其进行功能增强”。听起来很神奇对吧今天我们就来揭开它神秘的面纱还会学到 AOP 最重要的实际应用——声明式事务管理。掌握了事务你的程序才能真正做到数据安全可靠。课前唠一唠你有没有遇到过这种场景——想在每个方法执行前后都加个日志或计时结果发现要改几十个地方今天学完 AOP你可以用几行代码就搞定这件事。你最想用 AOP 解决什么重复代码评论区许个愿说不定今天的案例就能帮你实现。一、AOP 简介1.1 什么是 AOPAOPAspect Oriented Programming面向切面编程是一种编程范式指导开发者如何组织程序结构。我们熟悉的OOP面向对象编程是另一种编程范式两者互为补充。1.2 AOP 的作用在不惊动原始设计的基础上为方法进行功能增强。说白了就是原来有一段代码已经写好了现在想给它加点功能比如打印日志、统计时间但又不想改原有代码AOP 就是干这个的。本质Spring AOP 底层采用的是代理模式后面我们会验证。1.3 AOP 核心概念我们来通过一个场景理解几个关键术语假设BookDaoImpl有save()、update()、delete()、select()四个方法。我们想给update和delete增加“计算万次执行时间”的功能但不改原代码。概念解释举例连接点JoinPoint程序执行过程中能插入增强的任意位置BookDaoImpl中的所有方法切入点Pointcut真正需要增强的方法匹配连接点的式子update()和delete()通知Advice抽取出来的共性功能要增强的内容“计算万次执行时间”的方法通知类定义通知的类MyAdvice类切面Aspect描述通知与切入点的对应关系“在update()和delete()上应用计时的通知”一句话记忆连接点是“地点”切入点是“我选中的地点”通知是“要干的事”切面是“在哪里干什么”。二、AOP 入门案例需求在方法执行前打印当前系统时间不改原代码。2.1 环境准备创建 Maven 项目添加spring-context依赖创建BookDao、BookDaoImpl、SpringConfig配置类和App运行类。项目结构如下src/main/java/com/itheima ├── config/SpringConfig ├── dao/BookDao (接口) ├── dao/impl/BookDaoImpl (实现类) └── App (运行类)2.2 AOP 实现步骤步骤 1导入 AOP 依赖dependencygroupIdorg.aspectj/groupIdartifactIdaspectjweaver/artifactIdversion1.9.4/version/dependency说明spring-context已包含spring-aop这里只需导入 AspectJ 的织入包。Spring 整合 AspectJ 是目前最常用的 AOP 开发方式。步骤 2定义通知类与通知ComponentAspect// 标识这是一个切面类publicclassMyAdvice{Pointcut(execution(void com.itheima.dao.BookDao.update()))privatevoidpt(){}// 切入点定义无参数、无返回值、方法体为空Before(pt())// 绑定通知到切入点在方法执行前运行publicvoidmethod(){System.out.println(System.currentTimeMillis());}}步骤 3在配置类上开启 AOP 注解功能ConfigurationComponentScan(com.itheima)EnableAspectJAutoProxy// 开启注解格式 AOP 功能publicclassSpringConfig{}关键注解速查EnableAspectJAutoProxy开启 AOP 注解支持Aspect声明切面类Pointcut定义切入点Before前置通知三、AOP 工作流程3.1 工作流程四步走Spring 容器启动加载需要增强的类和通知类此时 bean 还未创建读取切入点配置只读取被实际使用的切入点没被通知绑定的切入点忽略初始化 bean匹配切入点匹配失败 → 创建原始对象匹配成功 → 创建代理对象Proxy获取 bean 执行方法拿到的原始对象 → 直接调用拿到的代理对象 → 运行代理逻辑在原始方法前后插入增强3.2 验证容器中的对象是代理还是原始BookDaobookDaoctx.getBean(BookDao.class);System.out.println(bookDao.getClass());// 增强后打印的是代理类未增强是原始类⚠️注意不要直接用System.out.println(bookDao)因为toString()被重写过看不出区别。用getClass()才能看到真实类型。3.3 新增核心概念概念解释目标对象Target被代理的原始对象代理Proxy对目标对象增强后生成的对象包含原始方法 通知逻辑总结Spring AOP 的本质 代理模式。匹配切入点的方法会生成代理对象不匹配的就是原始对象。四、AOP 配置管理4.1 切入点表达式4.1.1 语法格式标准格式动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)execution(publicUsercom.itheima.service.UserService.findById(int))// ↑访问修饰符 ↑返回值 ↑包名 ↑类名 ↑方法名 ↑参数访问修饰符、异常名可以省略写在接口上和写在实现类上都能匹配到调用接口最终走的还是实现类4.1.2 通配符符号含义示例*单个独立的任意符号execution(* com.*.service.*.find*(*))..多个连续的任意符号execution(* com..service.*.*(..))匹配子类类型*Service用得少了解即可4.1.3 书写技巧描述接口不描述实现类避免紧耦合访问修饰符通常省略接口方法都是public查询方法返回值用*增删改用精准类型包名尽量不用..用*做单级匹配接口名用*Service表示业务层模块方法名保留动词get、find名词用*实战套路业务层全部方法 →execution(* com.itheima.service.*Service.*(..))4.2 AOP 通知类型共 5 种通知类型下面通过一张图理解它们的位置方法执行流程 ┌─ 后置通知After成功/异常都执行 ─┐ 前置通知Before→ → 原始方法执行 → 返回后通知AfterReturning正常返回才执行 └─ 异常后通知AfterThrowing抛异常才执行 ─┘ 环绕通知Around 前置 后置能完全控制方法执行通知类型注解执行时机前置通知Before方法执行前后置通知After方法执行后无论是否异常返回后通知AfterReturning正常返回后异常后通知AfterThrowing抛出异常后环绕通知Around前后都可以最强大4.3 环绕通知详解重点环绕通知能实现其他所有通知类型的功能是实际开发中最常用的。Around(pt())publicObjectaround(ProceedingJoinPointpjp)throwsThrowable{System.out.println(around before advice ...);// 前置部分Objectretpjp.proceed();// 调用原始方法System.out.println(around after advice ...);// 后置部分returnret;}环绕通知注意事项重要必须依赖形参ProceedingJoinPoint否则无法调用原始方法如果不调用pjp.proceed()原始方法根本不会执行如果原始方法有返回值通知方法最好返回Object并把pjp.proceed()的返回值return出去原始方法无返回值时通知方法可设为void或Object必须处理Throwable异常⚠️坑点pjp.proceed()有两个重载版本。无参版本会自动传入原始参数有参版本pjp.proceed(args)可以修改参数后传入。后面案例会用到这个特性。4.4 AOP 通知获取数据4.4.1 获取参数非环绕通知在方法签名中加JoinPoint参数Before(pt())publicvoidbefore(JoinPointjp){Object[]argsjp.getArgs();System.out.println(Arrays.toString(args));}环绕通知用ProceedingJoinPoint它是JoinPoint的子类Around(pt())publicObjectaround(ProceedingJoinPointpjp)throwsThrowable{Object[]argspjp.getArgs();// 获取参数args[0]666;// 可以修改参数returnpjp.proceed(args);// 传入修改后的参数}4.4.2 获取返回值环绕通知直接接收pjp.proceed()的返回值返回后通知用returning属性AfterReturning(valuept(),returningret)publicvoidafterReturning(Objectret){System.out.println(afterReturning advice ...ret);}4.4.3 获取异常环绕通知用try-catch捕获抛出异常后通知用throwing属性AfterThrowing(valuept(),throwingt)publicvoidafterThrowing(Throwablet){System.out.println(afterThrowing advice ...t);}五、AOP 总结概念说明AOP 作用不惊动原代码为方法增强功能无侵入式本质代理模式生成代理对象切入点表达式中最实用写法execution(* com.xxx.service.*Service.*(..))最常用通知环绕通知Around环绕通知核心ProceedingJoinPoint.proceed()调用原始方法可获取数据参数getArgs()、返回值ret、异常try-catch六、AOP 事务管理重点事务是 AOP 最重要的实际应用场景。Spring 通过 AOP 实现了声明式事务管理让我们只需要一个注解就能搞定复杂的数据库事务。6.1 为什么需要事务转账案例分析场景Tom 给 Jerry 转账 100 元。Tom 账户减 100Jerry 账户加 100两个操作必须同时成功或同时失败。如果减钱成功、加钱失败100 块就凭空消失了。问题数据层每个操作都有自己独立的事务业务层的 transfer 方法没有事务出现异常时无法统一回滚。解决方案用 Spring 事务管理让 transfer 方法开启一个大事务把减钱和加钱两个操作纳入同一个事务中。6.2 Spring 事务管理三步走步骤 1在需要事务的方法上加TransactionalServicepublicclassAccountServiceImplimplementsAccountService{AutowiredprivateAccountDaoaccountDao;Transactionalpublicvoidtransfer(Stringout,Stringin,Doublemoney){accountDao.outMoney(out,money);// int i 1 / 0; // 出现异常事务会自动回滚accountDao.inMoney(in,money);}}步骤 2配置事务管理器JdbcConfig 类中BeanpublicPlatformTransactionManagertransactionManager(DataSourcedataSource){DataSourceTransactionManagermanagernewDataSourceTransactionManager();manager.setDataSource(dataSource);returnmanager;}因为 MyBatis 底层用的是 JDBC所以这里用DataSourceTransactionManager。步骤 3在配置类上开启事务注解ConfigurationComponentScan(com.itheima)PropertySource(classpath:jdbc.properties)Import({JdbcConfig.class,MybatisConfig.class})EnableTransactionManagement// 开启注解式事务publicclassSpringConfig{}关键注解速查Transactional声明方法需要事务管理EnableTransactionManagement开启事务注解支持6.3 事务角色角色说明对应事务管理员发起事务的一方业务层开启事务的方法如transfer事务协调员加入事务的一方数据层方法如outMoney、inMoney开启 Spring 事务后协调员的事务会加入到管理员的事务中形成一个整体。任何一个环节出异常整个事务都会回滚。6.4 事务属性Transactional有丰富的属性可配置属性作用示例readOnly只读事务查询用true增删改用falseTransactional(readOnly true)timeout超时时间秒-1 为永不超时Transactional(timeout -1)rollbackFor指定哪些异常回滚默认只回滚 RuntimeException 和 ErrorTransactional(rollbackFor {IOException.class})noRollbackFor指定哪些异常不回滚Transactional(noRollbackFor {NullPointerException.class})isolation事务隔离级别Transactional(isolation Isolation.DEFAULT)propagation事务传播行为Transactional(propagation Propagation.REQUIRES_NEW)⚠️重要Spring 默认只对RuntimeException和Error回滚。受检异常如IOException需要手动用rollbackFor指定才会回滚。6.5 事务传播行为Propagation场景转账操作中不管转账是否成功都需要记录日志。但日志操作不能因为转账失败而回滚。解决让日志方法用REQUIRES_NEW传播行为独立开启一个新事务。ServicepublicclassLogServiceImplimplementsLogService{AutowiredprivateLogDaologDao;Transactional(propagationPropagation.REQUIRES_NEW)publicvoidlog(Stringout,Stringin,Doublemoney){logDao.log(转账操作由out到in,金额money);}}传播行为管理员有事务管理员无事务REQUIRED默认加入管理员事务新建事务REQUIRES_NEW新建独立事务新建事务SUPPORTS加入管理员事务无事务运行NOT_SUPPORTED挂起管理员事务无事务运行无事务运行MANDATORY加入管理员事务报错NEVER报错无事务运行NESTED设置回滚点savePoint新建事务日常开发中REQUIRED默认能满足大部分需求REQUIRES_NEW用于像日志这种需要独立事务的场景。七、总结今天我们系统学习了 Spring 的第二个核心大模块——AOP 和声明式事务。模块核心要点AOP 核心概念连接点、切入点、通知、切面、目标对象、代理切入点表达式execution(* com.xxx.service.*Service.*(..))五种通知前置Before、后置After、返回后AfterReturning、异常后AfterThrowing、环绕Around数据获取参数getArgs()、返回值proceed()返回 /returning、异常try-catch/throwing事务管理Transactional 事务管理器 EnableTransactionManagement事务传播REQUIRED默认加入现有事务、REQUIRES_NEW始终新建事务结课小调查今天的内容量不小你最想在项目里试一试的是哪个A. 用环绕通知给所有 Service 方法加日志B. 用 AOP 统一处理参数空格C. 用Transactional管理数据库事务D. 写个独立的日志事务REQUIRES_NEW评论区告诉我你的选择也欢迎分享你在事务管理上踩过的坑我们一起避雷本文为 Spring Framework 第四天授课内容整理。AOP 和事务管理是 Spring 的精髓所在掌握了它们你的 Java 开发能力将迈上一个大台阶。如果觉得有帮助欢迎点赞、收藏、关注我们下节课见