AOP本质:在不改变原有业务逻辑的情况下增强横切逻辑,横切逻辑代码往往是权限校验代码、⽇志代
码、事务控制代码、性能监控代码。

6.1 AOP术语

Joinpoint(连接点) 它指的是那些可以⽤于把增强代码加⼊到业务主线中的点,那么由上图中我们可
以看出,这些点指的就是⽅法。在⽅法执⾏的前后通过动态代理技术加⼊增强的
代码。在Spring框架AOP思想的技术实现中,也只⽀持⽅法类型的连接点。
Pointcut(切⼊点) 它指的是那些已经把增强代码加⼊到业务主线进来之后的连接点。由上图中,我
们看出表现层 transfer ⽅法就只是连接点,因为判断访问权限的功能并没有对
其增强。
Advice(通知/增强) 它指的是切⾯类中⽤于提供增强功能的⽅法。并且不同的⽅法增强的时机是不⼀
样的。⽐如,开启事务肯定要在业务⽅法执⾏之前执⾏;提交事务要在业务⽅法
正常执⾏之后执⾏,⽽回滚事务要在业务⽅法执⾏产⽣异常之后执⾏等等。那么
这些就是通知的类型。其分类有:前置通知 后置通知 异常通知 最终通知 环绕通
知。
Target(⽬标对象) 它指的是代理的⽬标对象。即被代理对象。
Proxy(代理) 它指的是⼀个类被AOP织⼊增强后,产⽣的代理类。即代理对象。
Weaving(织⼊) 它指的是把增强应⽤到⽬标对象来创建新的代理对象的过程。spring采⽤动态代
理织⼊,⽽AspectJ采⽤编译期织⼊和类装载期织⼊。
Aspect(切⾯) 它指定是增强的代码所关注的⽅⾯,把这些相关的增强代码定义到⼀个类中,这
个类就是切⾯类。例如,事务切⾯,它⾥⾯定义的⽅法就是和事务相关的,像开
启事务,提交事务,回滚事务等等,不会定义其他与事务⽆关的⽅法。我们前⾯
的案例中 TrasnactionManager 就是⼀个切⾯。
  • Aspect切⾯= 切⼊点+增强

    = 切⼊点(锁定⽅法) + ⽅位点(锁定⽅法中的特殊时机)+ 横切逻辑

  • 众多的概念,⽬的就是为了锁定要在哪个地⽅插⼊什么横切逻辑代码

    6.2 AOP的代理选择

  1. CGLIB:当被代理对象没有实现任何接⼝时,Spring会选择CGLIB。
  2. JDK动态代理:当被代理对象实现了接⼝,Spring会选择JDK官⽅的代理技术。
  3. 我们可以通过配置的⽅式,让Spring强制使⽤CGLIB。

    6.3 Spring中AOP的实现

    6.3.1 纯xml模式

  4. 引入依赖 ```xml

    org.springframework spring-aop 5.1.12.RELEASE

org.aspectj aspectjweaver 1.9.4

  1. 2. 配置文件
  2. ```xml
  3. <!--
  4. Spring基于XML的AOP配置前期准备:
  5. 在spring的配置⽂件中加⼊aop的约束
  6. xmlns:aop="http://www.springframework.org/schema/aop"
  7. http://www.springframework.org/schema/aop
  8. https://www.springframework.org/schema/aop/spring-aop.xsd
  9. Spring基于XML的AOP配置步骤:
  10. 第⼀步:把通知Bean交给Spring管理
  11. 第⼆步:使⽤aop:config开始aop的配置
  12. 第三步:使⽤aop:aspect配置切⾯
  13. 第四步:使⽤对应的标签配置通知的类型
  14. ⼊⻔案例采⽤前置通知,标签为aop:before
  15. -->
  16. <!--把通知bean交给spring来管理-->
  17. <bean id="logUtil" class="com.atm.utils.LogUtil"></bean>
  18. <!--开始aop的配置-->
  19. <aop:config>
  20. <!--配置切⾯-->
  21. <aop:aspect id="logAdvice" ref="logUtil">
  22. <!--配置前置通知-->
  23. <aop:before method="printLog" pointcut="execution(public *
  24. com.atm.service.impl.TransferServiceImpl.updateAccountByCardNo(com.atm
  25. .pojo.Account))">
  26. </aop:before>
  27. </aop:aspect>
  28. </aop:config>
  1. 对步骤2中配置的解释
  • 上述配置实现了对 TransferServiceImpl 的 updateAccountByCardNo ⽅法进⾏增强,在其执⾏之前,输出了记录⽇志的语句。
  • pointcut属性写的是一个切入点表达式,切入点表达式书写如下: ```xml
  1. 表达式: 访问修饰符 返回值 包名.包名.包名…类名.方法名(参数列表)
  2. 标准的表达式写法: public void com.example.service.impl.XXXServiceImpl.saveXXX()
  3. 访问修饰符可以省略: void com.example.service.impl.XXXServiceImpl.saveXXX()
  4. 返回值可以使用通配符,表示任意返回值:
    • com.example.service.impl.XXXServiceImpl.saveXXX()
  5. 包名可以使用通配符,表示任意包。但是有几级包,就需要写几个*.:
    • ....XXXServiceImpl.saveXXX()
  6. 包名可以使用..表示当前包及其子包:
    • *..XXXServiceImpl.saveXXX()
  7. 类名和方法名都可以使用*来实现通配
    • ...*()
  8. 参数写法: 基本类型直接写名称 int 引用类型写包名.类名的方式 java.lang.String 可以使用通配符*表示任意类型,但是必须有参数 可以使用..表示有无参数均可,有参数可以是任意类型
  9. 全通配写法
    • ...*(..)
  10. 实际开发中切入点表达式的通常写法(impl包下的所有类的所有方法作为切入点poincut):
    • com.example.service.impl..(..)
  1. 4. 改变代理的方式,强制使用基于子类的CGLIB动态代理
  2. - 使⽤aop:cong标签配置
  3. ```xml
  4. <aop:config proxy-target-class="true">
  • 使⽤aop:aspectj-autoproxy标签配置
    1. <!--此标签是基于XML和注解组合配置AOP时的必备标签,表示Spring开启注解配置AOP的⽀持-->
    2. <aop:aspectj-autoproxy proxy-target-class="true"></aop:aspectj-autoproxy>
  1. 五种通知类型(也就是增强的时机,方法前、方法后等等)
  • 前置通知 ```xml <!— 作⽤:⽤于配置前置通知。 出现位置:它只能出现在aop:aspect标签内部 属性: method:⽤于指定前置通知的⽅法名称 pointcut:⽤于指定切⼊点表达式 pointcut-ref:⽤于指定切⼊点表达式的引⽤

执行时机:前置通知永远都会在切⼊点⽅法(业务核⼼⽅法)执⾏之前执⾏。 细节:前置通知可以获取切⼊点⽅法的参数,并对其进⾏增强。 —>

  1. - 正常执⾏时通知
  2. ```xml
  3. <!--
  4. 作⽤:⽤于配置正常执⾏时通知
  5. 出现位置:它只能出现在aop:aspect标签内部
  6. 属性:
  7. method:⽤于指定后置通知的⽅法名称
  8. pointcut:⽤于指定切⼊点表达式
  9. pointcut-ref:⽤于指定切⼊点表达式的引⽤
  10. -->
  11. <aop:after-returning method="afterReturningPrintLog" pointcut-ref="pt1">
  12. </aop:after-returning>
  • 异常通知

    1. <!--
    2. 作⽤:⽤于配置异常通知。
    3. 出现位置:它只能出现在aop:aspect标签内部
    4. 属性:
    5. method:⽤于指定异常通知的⽅法名称
    6. pointcut:⽤于指定切⼊点表达式
    7. pointcut-ref:⽤于指定切⼊点表达式的引⽤
    8. 执⾏时机:异常通知的执⾏时机是在切⼊点⽅法(业务核⼼⽅法)执⾏产⽣异常之后,异常通知执⾏。如果切
    9. ⼊点⽅法执⾏没有产⽣异常,则异常通知不会执⾏。
    10. 细节:异常通知不仅可以获取切⼊点⽅法执⾏的参数,也可以获取切⼊点⽅法执⾏产⽣的异常信息。
    11. -->
    12. <aop:after-throwing method="afterThrowingPrintLog" pointcut-ref="pt1">
    13. </aop:after-throwing>
  • 最终通知

    1. <!--
    2. 作⽤:⽤于指定最终通知。
    3. 出现位置:它只能出现在aop:aspect标签内部
    4. 属性:
    5. method:⽤于指定最终通知的⽅法名称
    6. pointcut:⽤于指定切⼊点表达式
    7. pointcut-ref:⽤于指定切⼊点表达式的引⽤
    8. 执行时机:最终通知的执⾏时机是在切⼊点⽅法(业务核⼼⽅法)执⾏完成之后,切⼊点⽅法返回之前执⾏。
    9. 换句话说,⽆论切⼊点⽅法执⾏是否产⽣异常,它都会在返回之前执⾏。
    10. 细节:最终通知执⾏时,可以获取到通知⽅法的参数。同时它可以做⼀些清理操作。
    11. -->
    12. <aop:after method="afterPrintLog" pointcut-ref="pt1"></aop:after>
  • 环绕通知 ```xml <!— 作⽤:⽤于配置环绕通知。 出现位置:它只能出现在aop:aspect标签的内部 属性: method:⽤于指定环绕通知的⽅法名称 pointcut:⽤于指定切⼊点表达式 pointcut-ref:⽤于指定切⼊点表达式的引⽤

环绕通知:它是有别于前⾯四种通知类型外的特殊通知。前⾯四种通知(前置,后置,异常和最终) 它们都是指定何时增强的通知类型。⽽环绕通知,它是Spring框架为我们提供的⼀种可以通过编码 的⽅式,控制增强代码何时执⾏的通知类型。它⾥⾯借助的ProceedingJoinPoint接⼝及其实现 类,实现⼿动触发切⼊点⽅法的调⽤。 —>

  1. <a name="eWeSP"></a>
  2. ## 6.3.2 xml加注解的模式
  3. 1. XML 中开启 Spring 对注解 AOP 的⽀持
  4. ```xml
  5. <!--开启spring对注解aop的⽀持-->
  6. <aop:aspectj-autoproxy/>
  1. 示例

    1. @Component
    2. @Aspect // 表面是个切面
    3. public class LogUtil {
    4. @Pointcut("execution(* com.lagou.service.impl.*.*(..))")
    5. public void pointcut(){}
    6. // 前置通知
    7. @Before("pointcut()")
    8. public void beforePrintLog(JoinPoint jp){
    9. Object[] args = jp.getArgs();
    10. System.out.println("前置通知:beforePrintLog,参数是:"+ Arrays.toString(args));
    11. }
    12. // 后置通知
    13. @AfterReturning(value = "pointcut()",returning = "rtValue")
    14. public void afterReturningPrintLog(Object rtValue){
    15. System.out.println("后置通知:afterReturningPrintLog,返回值是:"+rtValue);
    16. }
    17. // 异常通知
    18. @AfterThrowing(value = "pointcut()",throwing = "e")
    19. public void afterThrowingPrintLog(Throwable e){
    20. System.out.println("异常通知:afterThrowingPrintLog,异常是:"+e);
    21. }
    22. // 最终通知,不管增强的方法是否出现异常都会执行,类似finally
    23. @After("pointcut()")
    24. public void afterPrintLog(){
    25. System.out.println("最终通知:afterPrintLog");
    26. }
    27. // 环绕通知
    28. @Around("pointcut()")
    29. public Object aroundPrintLog(ProceedingJoinPoint pjp){
    30. //定义返回值
    31. Object rtValue = null;
    32. try{
    33. //前置通知
    34. System.out.println("前置通知");
    35. //1.获取参数
    36. Object[] args = pjp.getArgs();
    37. //2.执⾏切⼊点⽅法
    38. rtValue = pjp.proceed(args);
    39. //后置通知
    40. System.out.println("后置通知");
    41. }catch (Throwable t){
    42. //异常通知
    43. System.out.println("异常通知");
    44. t.printStackTrace();
    45. }finally {
    46. //最终通知
    47. System.out.println("最终通知");
    48. }
    49. return rtValue;
    50. }
    51. }

    6.3.3 纯注解模式

  2. 在使⽤注解驱动开发aop时,我们要明确的就是,是注解替换掉配置⽂件中的下⾯这⾏配置:

    1. <!--开启spring对注解aop的⽀持-->
    2. <aop:aspectj-autoproxy/>
  3. 在配置类中使⽤如下注解进⾏替换上述配置

    1. @EnableAspectJAutoProxy //开启spring对注解AOP的⽀持
    2. public class SpringConfiguration {
    3. }

    6.4 Spring 声明式事务的⽀持

    编程式事务:在业务代码中添加事务控制代码,这样的事务控制机制就叫做编程式事务
    声明式事务:通过xml或者注解配置的⽅式达到事务控制的⽬的,叫做声明式事务

    6.4.1 事务的四大特性

  4. 原子性:原⼦性是指事务是⼀个不可分割的⼯作单位,事务中的操作要么都发⽣,要么都不发⽣。从操作的⻆度来描述,事务中的各个操作要么都成功要么都失败

  5. 一致性:事务必须使数据库从⼀个⼀致性状态变换到另外⼀个⼀致性状态。例如转账前A有1000,B有1000。转账后A+B也得是2000。⼀致性是从数据的⻆度来说的,(1000,1000) (900,1100),不应该出现(900,1000)
  6. 隔离性:事务的隔离性是多个⽤户并发访问数据库时,数据库为每⼀个⽤户开启的事务,每个事务不能被其他事务的操作数据所⼲扰,多个并发事务之间要相互隔离。⽐如:事务1给员⼯涨⼯资2000,但是事务1尚未被提交,员⼯发起事务2查询⼯资,发现⼯资涨了2000块钱,读到了事务1尚未提交的数据(脏读)
  7. 持久性:持久性是指⼀个事务⼀旦被提交,它对数据库中数据的改变就是永久性的,接下来即使数据库发⽣故障也不应该对其有任何影响。

    6.4.2 事务的隔离级别

  8. 不考虑隔离级别,会出现以下情况(以下情况全是错误的也即为隔离级别在解决事务并发问题):

  • 脏读:⼀个线程中的事务读到了另外⼀个线程中未提交的数据。
  • 不可重复读:⼀个线程中的事务读到了另外⼀个线程中已经提交的update的数据(前后内容不⼀样)
    • 员⼯A发起事务1,查询⼯资,⼯资为1w,此时事务1尚未关闭
    • 财务⼈员发起了事务2,给员⼯A转了2000块钱,并且提交了事务
    • 员⼯A通过事务1再次发起查询请求,发现⼯资为1.2w,原来读出来1w读不到了,叫做不可重复读
  • 虚读(幻读):⼀个线程中的事务读到了另外⼀个线程中已经提交的insert或者delete的数据(前后条数不⼀样)
    • 事务1查询所有⼯资为1w的员⼯的总数,查询出来了10个⼈,此时事务尚未关闭
    • 事务2财务⼈员发起,新来员⼯,⼯资1w,向表中插⼊了2条数据,并且提交了事务
    • 事务1再次查询⼯资为1w的员⼯个数,发现有12个⼈,前后读取的结果条数不一样
  1. 数据库共定义了四种隔离级别: | 名称 | 描述 | 级别 | | —- | —- | —- | | Read uncommitted(读未提交) | 最低级别,可能会发生脏读,不可重复读,虚读(幻读) | 最低 | | Read committed(读已提交) | 可避免脏读情况发⽣。不可重复读和幻读⼀定会发⽣。 | 第三 | | Repeatable read(可重复读) | 可避免脏读、不可重复读情况的发⽣。(幻读有可能发⽣) 该机制下会对要update的⾏进⾏加锁 | 第二 | | Serializable(串⾏化) | 可避免脏读、不可重复读、虚读情况的发⽣。 | 最高 |
  • 注意:级别依次升⾼,效率依次降低
  • MySQL的默认隔离级别是:REPEATABLE READ(可重复读)
  • 查询当前使⽤的隔离级别: select @@tx_isolation
  • 设置MySQL事务的隔离级别: set session transaction isolation level xxx; (设置的是当前mysql连接会话的,并不是永久改变的)

6.4.3 事务的传播⾏为

  • 事务往往在service层进⾏控制,如果出现service层⽅法A调⽤了另外⼀个service层⽅法B,A和B⽅法本

身都已经被添加了事务控制,那么A调⽤B的时候,就需要进⾏事务的⼀些协商,这就叫做事务的传播⾏
为。

  • A调⽤B,我们站在B的⻆度来观察来定义事务的传播⾏为: | PROPAGATION_REQUIRED | 如果当前没有事务,就新建⼀个事务,如果已经存在⼀个事务中,加⼊到这个事务中。这是最常⻅的选择。 | | —- | —- | | PROPAGATION_SUPPORTS | ⽀持当前事务,如果当前没有事务,就以⾮事务⽅式执⾏。 | | PROPAGATION_MANDATORY | 使⽤当前的事务,如果当前没有事务,就抛出异常。 | | PROPAGATION_REQUIRES_NEW | 新建事务,如果当前存在事务,把当前事务挂起。 | | PROPAGATION_NOT_SUPPORTED | 以⾮事务⽅式执⾏操作,如果当前存在事务,就把当前事务挂起。 | | PROPAGATION_NEVER | 以⾮事务⽅式执⾏,如果当前存在事务,则抛出异常。 | | PROPAGATION_NESTED | 如果当前存在事务,则在嵌套事务内执⾏。如果当前没有事务,则
    执⾏与PROPAGATION_REQUIRED类似的操作。 |

6.4.4 Spring事务中的API

  • PlatformTransactionManager

    1. public interface PlatformTransactionManager {
    2. /**
    3. * 获取事务状态信息
    4. */
    5. TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException;
    6. /**
    7. * 提交事务
    8. */
    9. void commit(TransactionStatus status) throws TransactionException;
    10. /**
    11. * 回滚事务
    12. */
    13. void rollback(TransactionStatus status) throws TransactionException;
    14. }
  • 作用

    • 此接⼝是Spring的事务管理器核⼼接⼝。Spring本身并不⽀持事务实现,只是负责提供标准,应⽤底层⽀持什么样的事务,需要提供具体实现类。此处也是策略模式的具体应⽤。在Spring框架中,也为我们内置了⼀些具体策略,例如:DataSourceTransactionManager , HibernateTransactionManager 等等。( 和 HibernateTransactionManager 事务管理器在 spring-orm-5.1.12.RELEASE.jar 中)
  • Spring JdbcTemplate(数据库操作⼯具)、Mybatis(mybatis-spring.jar) ->DataSourceTransactionManager

    6.4.5 基于xml加注解的配置

  1. xml

    1. <!--配置事务管理器-->
    2. <bean id="transactionManager"
    3. class="org.springframework.jdbc.datasource.DataSourceTransactionManage
    4. r">
    5. <property name="dataSource" ref="dataSource"></property>
    6. </bean>
    7. <!--开启spring对注解事务的⽀持-->
    8. <tx:annotation-driven transaction-manager="transactionManager"/>
  2. 注解

    1. @Transactional(readOnly = true,propagation = Propagation.SUPPORTS)

    6.4.5 纯注解配置

  • Spring基于注解驱动开发的事务控制配置,只需要把 xml 配置部分改为注解实现。只是需要⼀个注解替换掉xml配置⽂件中的 配置。在 Spring 的配置类上添加 @EnableTransactionManagement 注解即可
    1. @EnableTransactionManagement//开启spring注解事务的⽀持
    2. public class SpringConfiguration {
    3. }