Spring AOP和事务相关知识的学习记录
一、AOP的学习
1.AOP的概念
(1) AOP为Aspect Oriented Programming的缩写,意思为面向切面编程,是通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。
(2)AOP术语的概念定义:
- Aspect(切面): Aspect 声明类似于 Java 中的类声明,在 Aspect 中有一些 Pointcut 以及相应的 * Advice。
- Proxy(代理):向目标对象应用通知之后创建的一个代理对象。
- Joint point(连接点):与切入点匹配的执行点。
- Pointcut(切入点):表示一组 joint point,这些 joint point 或是通过逻辑关系组合起来,它定义了相应的 Advice 将要应用到的地方。
- Advice(增强):Advice 定义了在 Pointcut 里面定义的程序点具体要做的操作,它通过 before、after 和 around 来区别是在每个 joint point 之前、之后还是代替执行的代码。
- Target(目标对象):织入 Advice 的目标对象,被通知对象。
- Weaving(织入):将 Aspect 和其他对象连接起来, 并创建 Adviced object 的过程
2.AOP作用
不通过修改源代码方式,在代码功能里面添加新功能为其增强
3.AOP所需依赖
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.9</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.3</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.8</version>
</dependency>
</dependencies>
4.AOP的Advice(通知/增强)类型
- 前置通知:增强功能到方法执行前
- 后置返回通知:增强功能到方法执行后
- 环绕通知:环绕通知功能比较强大,它可以追加功能到方法执行的前后,这也是比较常用的方式,它可以实现其他四种通知类型的功能
- 异常通知:增强功能到方法抛出异常后,只有方法执行出异常才进行
- 最终通知:增强功能到方法执行后,只有方法正常执行结束后才进行。
5.AOP的使用
(1)在service包下创建类LogService类
package service;
import java.time.LocalDateTime;
import org.springframework.stereotype.Service;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
@Service
public class LogService {
public void writeLog(JoinPoint point) {
System.out.println("开始执行service方法,开始时间"+LocalDateTime.now());
System.out.println("执行service的方法名:"+point.getSignature().getName());
Object[] args = point.getArgs();
System.out.println("用户的参数:");
for(Object object:args) {
System.out.println(object.toString()+" ");
}
}
public void writeEndLog(Object val) {
System.out.println("结束执行service方法,结束时间"+LocalDateTime.now());
System.out.println("返回值是:"+val);
}
public void writeExLog(Exception ex) {
System.out.println("service方法发生异常,异常信息是:"+ex.getMessage());
}
public Object writeAroundLog(ProceedingJoinPoint point) throws Throwable {
System.out.println("开始执行service方法,开始时间"+LocalDateTime.now());
System.out.println("执行service的方法名:"+point.getSignature().getName());
Object[] args = point.getArgs();
Object obj = point.proceed();
System.out.println("结束执行service方法,结束时间"+LocalDateTime.now());
return obj;
}
}
(2)applicationContext.xml配置
<!-- <context:component-scan base-package="service" /> -->
<!-- 开启SpringMVC框架的注解驱动 -->
<mvc:annotation-driven />
<aop:config>
<!-- 定义切点及切点范围 -->
<aop:pointcut expression="execution(* service.*.*(..))" id="pointcut" />
<!-- 定义切面 -->
<aop:aspect ref="logService">
<!-- 定义前置通知 -->
<aop:before pointcut-ref="pointcut" method="writeLog" />
<!-- 定义后置返回通知 -->
<aop:after-returning returning="val" pointcut-ref="pointcut" method="writeEndLog" />
<!-- 定义异常通知 -->
<aop:after-throwing pointcut-ref="pointcut" method="writeExLog" throwing="ex"/>
<!-- 定义环绕通知 -->
<aop:around method="writeAroundLog" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>
6.注解方式使用
(1)开启注解
<!-- 开启注解方法 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
(2)在LogService上添加注解
package service;
import java.time.LocalDateTime;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.AfterThrowing;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Service;
@Service
@Aspect
public class LogService {
@Pointcut("execution(* service.*.*(..))")
public void pointcut() {}
@Before(value="pointcut()")
public void writeLog(JoinPoint point) {
System.out.println("开始执行service方法,开始时间"+LocalDateTime.now());
System.out.println("执行service的方法名:"+point.getSignature().getName());
Object[] args = point.getArgs();
System.out.println("用户的参数:");
for(Object object:args) {
System.out.println(object.toString()+" ");
}
}
@AfterReturning(value="pointcut()",returning="val")
public void writeEndLog(Object val) {
System.out.println("结束执行service方法,结束时间"+LocalDateTime.now());
System.out.println("返回值是:"+val);
}
@AfterThrowing(value="pointcut()",throwing="ex")
public void writeExLog(Exception ex) {
System.out.println("service方法发生异常,异常信息是:"+ex.getMessage());
}
@Around(value = "pointcut()")
public Object writeAroundLog(ProceedingJoinPoint point) throws Throwable {
System.out.println("开始执行service方法,开始时间"+LocalDateTime.now());
System.out.println("执行service的方法名:"+point.getSignature().getName());
Object[] args = point.getArgs();
Object obj = point.proceed();
System.out.println("结束执行service方法,结束时间"+LocalDateTime.now());
return obj;
}
}
8.测试
创建一个Controller类,调用service类的方法
以我测试的为例
在方法前后出现打印,则使用成功,打印内容可以更改
二、Spring事务的学习
1.事务的概念
-
事务(Transaction)是访问并可能更新数据库中各项数据项的一个程序执行单元(unit)。 事务由事务开始(begin transaction)和事务结束(end transaction)之间执行的全体操作组成。
-
事务是一个不可分割的数据库操作序列,也是数据库并发控制的基本单位,其执行的结果必须使数据库从一种一致性状态变到另一种一致性状态。
-
事务结束有两种,事务中的步骤全部成功执行时,提交事务。如果其中一个失败,那么将会发生回滚操作,并且撤销之前的所有操作。也就是说,事务内的语句,要么全部执行成功,要么全部执行失败。
-
事务是恢复和并发控制的基本单位。
-
事务具有四个特征:原子性、一致性、隔离性和持久性。这四个特征通常称为ACID。
2.事务的ACID特征
-
原子性(Atomicity):指事务是一个不可分割的工作单位,整个事务中的所有操作要么全部提交成功,要么全部失败回滚,对于一个事务来说,不可能只执行其中的一部分操作。
-
一致性(Consistency):指事务必须使数据库从一个一致性状态变换到另一个一致性状态。也就是说事务前后数据的完整性必须保持一致。
-
隔离性(Isolation):指一个事务的执行不能有其他事务的干扰,事务的内部操作和使用数据对其他的并发事务是隔离的,互不干扰。
-
持久性(Durability):指一个事务一旦提交,对数据库中数据的改变就是永久性的。此时即使数据库发生故障,修改的数据也不会丢失。接下来其他的操作不会对已经提交了的事务产生影响。
3.脏读、不可重复读、幻读
-
脏读:指在一个事务处理过程里读取了另一个未提交的事务中的数据。
比如在事务 A 修改数据之后提交数据之前,这时另一个事务 B 来读取数据,如果不加控制,事务 B 读取到 A 修改过数据,之后 A 又对数据做了修改再提交,则 B 读到的数据是脏数据。 -
不可重复读:指在数据库访问中,一个事务范围内多次查询却返回了不同的数据值。这是由于在查询间隔中,其他事务修改并提交而引起的。
比如事务 T1 读取某一数据,事务 T2 读取并修改了该数据,T1 为了对读取值进行检验而再次读取该数据,便得到了不同的结果。 -
幻读:指当事务不是独立执行时发生的一种现象。
比如事务 A 在按查询条件读取某个范围的记录时,事务 B 又在该范围内插入了新的满足条件的记录,当事务 A 再次按条件查询记录时,会产生新的满足条件的记录。
4.SQL的4个隔离级别
-
未提交读(Read Uncommitted):一个事务能够读取到别的事务中没有提交的更新数据。事务中的修改,即使没有提交,其他事务也可以看得到。在这种隔离级别下有可能发生脏读,不可重复读和幻读。
-
提交读(Read Committed):事务中的修改只有提交以后才能被其它事务看到。在这种隔离级别下解决了脏读,但是有可能发生不可重复读和幻读。
-
可重复读(Repeated Read):保证了在同一事务中先后执行的多次查询将返回同一结果,看到的每行的记录的结果是一致的,不受其他事务的影响。但是这种级别下有可能发生幻读。
-
可串行化(Serializable):不允许事务并发执行,强制事务串行执行。就是在读取的每一行数据上都加上了锁,读写相互都会阻塞,所以效率很低下。这种隔离级别最高,是最安全的,但是性能最低,不会出现脏读,不可重复读,幻读。
事务隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
read uncommitted | 是 | 是 | 是 |
read committed | 否 | 是 | 是 |
repeatable read | 否 | 否 | 是 |
serializable | 否 | 否 | 否 |
6.spring中隔离规则
(1). @Transactional(isolation = Isolation.DEFAULT)
"使用后端数据库默认的隔离级别 对于MYSQL来说就是可重复读"
(2). @Transactional(isolation = Isolation.READ_UNCOMMITTED)
"是最低的隔离级别,允许读取尚未提交的数据变更(会出现脏读,不可重复读),基本不使用"
(3). @Transactional(isolation = Isolation.READ_COMMITTED)
"允许读取并发事务已经提交的数据(会出现不可重复读和幻读)"
(4). @Transactional(isolation = Isolation.REPEATABLE_READ)
"事物开启后,对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改(会出现幻读)"
(5). @Transactional(isolation = Isolation.SERIALIZABLE)
"最高的隔离级别,完全服从ACID的隔离级别,也是最慢的事务隔离级别,因为它通常是通过完全锁定事务相关的数据库表来实现的"
5.事务传播方式的注解使用
//添加注解方式
@Transactional(propagation =Propagation.REQUIRED )
(1).TransactionDefinition.PROPAGATION_REQUIRED:
"如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。这是默认值。"
(2).TransactionDefinition.PROPAGATION_REQUIRES_NEW:
"创建一个新的事务,如果当前存在事务,则把当前事务挂起。"
(3).TransactionDefinition.PROPAGATION_SUPPORTS:
"如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务的方式继续运行。"
(4).TransactionDefinition.PROPAGATION_NOT_SUPPORTED:
"以非事务方式运行,如果当前存在事务,则把当前事务挂起。"
(5).TransactionDefinition.PROPAGATION_NEVER:
"以非事务方式运行,如果当前存在事务,则抛出异常。"
(6).TransactionDefinition.PROPAGATION_MANDATORY:
"如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。"
(7).TransactionDefinition.PROPAGATION_NESTED:
"如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;"
"如果当前没有事务,则该取值等价于TransactionDefinition.PROPAGATION_REQUIRED。"
- 事务回滚
- @Transactional默认只能回滚RuntimeException和RuntimeException下面的子类抛出的异常,不能回滚Exception异常;
- 如果需要支持回滚Exception异常,需要显示的指明,如@Transactional(rollbackFor = Exception.class);
- 打了@Transactional但是事务不生效
(1)@Transactional注解未打在public方法上
- Java的访问权限主要有四种:private、default、protected、public;如果事务方法定义了错误的访问权限(非public方法),会导致事务失效;
(2)目标方法用final修饰
- 原因:Spring事务基于Spring AOP,通过JDK动态代理,在代理类中实现的事务功能;但如果某个方法用final修饰了,那么在它的代理类中,就无法重写该方法;同样,static修饰的方法,同样无法通过动态代理,变成事务方法;
(3)同一个类中的方法直接内部调用
- 原因:方法被事务管理是因为Apring AOP为其生成代理了对象,但是直接this调用同类方法,调用的是目标类对象的方法,而非代理类方法,因此,在同类中的方法直接内部调用,会导致事务失效;