一、AOP概述
1. AOP介绍
AOP(Aspect Oriented Programming):面向切面编程(或者面向特定方法编程),一种编程范式,指导开发者如何组织程序结构,是oop的延续。(OOP面向对象编程)
AOP作用:在不惊动原始设计的基础上为其进行功能增强(简单的说就是在不改变方法源代码的基础上对方法进行功能增强)。
Spring理念:无入侵式/无侵入式。(没有修改源代码,可插拔)
AOP应用场景:
- 日志记录:可以使用AOP在方法执行前后记录日志信息,包括方法名、参数、返回值等,用于问题排查和性能分析。
- 安全验证:可以使用AOP在方法执行前进行身份验证和权限校验,确保只有合法用户才能访问敏感操作或受限资源。
- 事务管理:可以使用AOP在方法执行前启动事务,在方法执行后根据执行结果决定是否提交事务或进行回滚,确保数据的一致性和完整性。
- 性能监控和统计:可以使用AOP在方法执行前后记录方法的执行时间、调用次数等信息,用于性能分析、优化和容量规划。
- 异常处理:可以使用AOP在方法执行过程中捕获异常,并进行统一处理,如记录日志、发送告警等,避免异常导致系统崩溃或数据丢失。
- 缓存管理:可以使用AOP在方法执行前先从缓存中查询数据,如果存在则直接返回,避免重复计算或数据库访问等开销。
- 审计跟踪:可以使用AOP在方法执行前后记录操作日志,包括方法名、参数、执行时间等,用于追踪操作和查找问题。
- 懒加载:可以使用AOP延迟加载一些资源,只在需要时才进行初始化和加载,提高系统的启动速度和资源利用率。
需要注意的是,AOP并不适用于所有场景,适当使用AOP可以提高代码的可维护性、复用性和灵活性,但不恰当的使用可能导致代码复杂度增加,影响系统性能,因此需要根据具体需求和情况进行评估和选择。
AOP中的核心概念
如图下伪代码为例,记录业务方法的执行耗时:
连接点(JoinPoint):在spring中可以认为是目标类中的方法,例如:update()、delete()等。
切入点(Pointcut):进行功能增强的方法,例如:update()、delete()方法,select()方法没有被增强所以不是切入点,但是是连接点。
-
在SpringAOP中,一个切入点可以只描述一个具体方法,也可以匹配多个方法:
-
一个具体方法:com.baidou.dao包下的BookDao接口中的无形参无返回值的save方法。
-
匹配多个方法:所有的save方法,所有的get开头的方法,所有以Dao结尾的接口中的任意方法,所有带有一个参数的方法。
-
通知(Advice):在切入点前后执行的操作,也就是增强的共性功能。
- 在SpringAOP中,功能最终以方法的形式呈现。
通知类(切面类):通知方法所在的类叫做通知类。
切面(Aspect):描述通知与切入点的对应关系,也就是哪些通知方法对应哪些切入点方法。
底层实现:动态代理是面向切面编程最主流的实现。而SpringAOP是Spring框架的高级技术,旨在管理bean对象的过程中,通过底层的动态代理机制,对特定的方法进行编程。
2. AOP快速入门
案例:在执行某个方法之前输出当前系统时间。(基于注解实现,xml配置没有注解直观)
1、导入依赖坐标(pom.xml)
<dependencies>
<!--spring核心依赖,会将spring-aop传递进来-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<!--aop框架,简化aop开发-->
<!--切入点表达式依赖,目的是找到切入点方法,也就是找到要增强的方法-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
</dependencies>
2、定义dao接口和实现类
package com.baidou.dao;
public interface BookDao {
public void save();
public void update();
}
package com.baidou.dao.impl;
import com.baidou.dao.BookDao;
@Repository
public class BookDaoImpl implements BookDao {
public void save() {
System.out.println(System.currentTimeMillis());//手动添加一个时间输出
System.out.println("book dao save ...");
}
public void update() { //一会对此方法进行增强
System.out.println("book dao update ...");
}
}
3、定义通知类(切面类),制作通知方法
package com.baidou.aop;
import org.springframework.stereotype.Component;
//通知类必须配置成Spring管理的bean
@Component
public class MyAdvice {
public void method(){
System.out.println(System.currentTimeMillis());
}
}
4、定义切入点表达式、配置切面(绑定切入点与通知关系)
@Component //通知类必须配置成Spring管理的bean
@Aspect //设置当前类为切面类
public class MyAdvice {
//设置切入点,@Pointcut注解要求配置在方法上方
@Pointcut("execution(void com.baidou.dao.BookDao.update())")//切入点表达式
private void pt(){}
//设置在控制方法pt之前执行(前置通知)
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}
5、在spring配置类中进行Spring注解包扫描、以及开启AOP功能。
@Configuration
@ComponentScan("com.baidou")
@EnableAspectJAutoProxy //开启注解开发AOP功能
public class SpringConfig {
}
6、测试
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
BookDao bookDao = ctx.getBean(BookDao.class);
bookDao.update();
}
}
3. AOP工作流程
底层使用动态代理实现。
1、Spring容器启动。
2、读取所有切面配置中的切入点。
3、初始化bean,判定bean对应的类中的方法是否匹配到任意切入点:
- 匹配失败,创建原始对象。
- 匹配成功,创建原始对象(目标对象)的代理对象。
4、获取bean执行方法:
- 获取的bean是原始对象时,调用方法并执行,完成操作。
- 获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作。(切入点)
目标对象(Target):被代理的对象,也叫原始对象,该对象中的方法没有任何功能增强。
代理对象(Proxy):代理后生成的对象,由Spring帮我们创建代理对象。
4. AOP切入点表达式
4.1 语法格式
1、切入点:要进行增强的方法。
2、切入点表达式:要进行增强的方法的描述方式:(能定位到增强方法的表达式)
- 描述方式一:执行com.baidou.dao包下的BookDao接口中的无参数update方法;(推荐在接口方法中增强)
execution(void com.baidou.dao.BookDao.update())
// execution:执行
- 描述方式二:执行com.baidou.dao.impl包下的BookDaoImpl类中的无参数update方法;
execution(void com.baidou.dao.impl.BookDaoImpl.update())
3、切入点表达式标准格式:动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数) 异常名)
execution(public User com.baidou.service.UserService.findById(int))
- 动作关键字:描述切入点的行为动作,例如
execution
表示执行到指定切入点。 - 访问修饰符:public,private等,可以省略。
- 返回值:写返回值类型。
- 包名:多级包使用点连接。
- 类/接口名。
- 方法名。
- 参数:直接写参数的类型,多个类型用逗号隔开。
- 异常名:方法定义中抛出指定异常,可以省略。
4.2 通配符
可以在切点表达式中使用通配符快速描述切入点。
1、*
:单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现。
示例:匹配com.baidou包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法
execution(public * com.baidou.*.UserService.find*(*))
2、..
:多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写。
示例:匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法
execution(public User com..UserService.findById(..))
3、+
:专用于匹配子类类型。
execution(* *..*Service+.*(..))
//以Service结尾的子类或子接口
@Component
@Aspect
public class MyAdvice {
//切入点表达式:
//@Pointcut("execution(void com.baidou.dao.BookDao.update())") //标准格式
//@Pointcut("execution(* com.baidou.dao.BookDao.update())") //返回值任意
//@Pointcut("execution(* com.baidou.dao.BookDao.update(*))") //有一个参数的update
//@Pointcut("execution(* com.baidou.dao.BookDao.update(..))") //任意参数个数的update
//@Pointcut("execution(* com.baidou.dao.*.update(..))") //dao包下任意类的任意参数个数的update方法
//@Pointcut("execution(* com.baidou.dao.*.u*(..))") //dao包下任意类的任意参数个数的以"u"开头方法
//@Pointcut("execution(* com.baidou.dao.*.*e(..))") //dao包下任意类的任意参数个数的以"e"结尾方法
//@Pointcut("execution(* com.baidou.dao..*.*e(..))") //dao包下的所有包的任意类的任意参数个数的以"e"结尾方法
//@Pointcut("execution(* com.baidou.dao..*.*(..))") //dao包下的所有包的任意类的任意参数个数的任意方法
//@Pointcut("execution(* com..*.*(..))") //com包下的所有包下的任意类的任意参数个数的任意方法
//@Pointcut("execution(* *..*.*(..))") //任意包下的任意类的任意参数个数的任意方法,一般不用
@Pointcut("execution(* *(..))") //上面方式的简写方式,一般不用
//@Pointcut("execution(* com.baidou.service.*.*(..))") //常用写法
private void pt(){}
@Before("pt()")
public void method(){
System.out.println(System.currentTimeMillis());
}
}
4.3 书写技巧
- 所有代码按照标准规范开发,否则以下技巧全部失效。
- 切入点表达式,描述切入点**通常描述接口**,而不描述实现类。
- 访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述)。
- 返回值类型对于增删改类使用精准类型加速匹配,对于查询类使用*通配快速描述。
- 包名书写尽量不使用…匹配,效率过低,常用*做单个包描述匹配,或精准匹配。
- 接口名/类名书写名称与模块相关的采用*匹配,例如UserService书写成*Service,绑定业务层接口名。
- 方法名书写以动词进行精准匹配,名词采用
*
匹配,例如getById书写成getBy*,selectAll书写成selectAll。 - 参数规则较为复杂,根据业务方法灵活调整。
- 通常**不使用异常作为匹配**规则。
示例:
//切入点表达式:
//@Pointcut("execution(public void com.baidou.dao.BookDao.u*())")
//指定包下指定接口中以"u"开头的无参方法且返回值为void
//@Pointcut("execution(public void com.baidou.dao.BookDao.*e())")
//指定包下指定接口中以"e"结尾的无参方法且返回值为void
//@Pointcut("execution(public void com.baidou.dao.BookDao.*())")
//指定包下指定接口中所有无参方法且返回值为void
//@Pointcut("execution(public void com.baidou.dao.BookDao.*(*))")
//指定包下指定接口中所有有一个参数的方法且返回值为void
//@Pointcut("execution(public void com.baidou.dao.BookDao.*(..))")
//指定包下指定接口中所有方法且返回值为void
//@Pointcut("execution(public void com.baidou.dao.*.*(..))")//指定包下所有类中所有方法且返回值为void
//@Pointcut("execution(* com.baidou.dao.*.*(..))")//指定包下所有类中所有方法
//@Pointcut("execution(* com.baidou.*.*.*(..))")//com.baidou包下所有子包中所有类中所有方法
//@Pointcut("execution(* com.baidou..*.*(..))")//com.baidou包下所有包中所有类中所有方法
//@Pointcut("execution(* *..*.*(..))")//任意包下所有包中所有类中所有方法
//@Pointcut("execution(* *(..))")//任意包下所有包中所有类中所有方法
private void pt(){}
5. AOP通知类型
5.1 AOP通知分类
AOP通知描述了抽取的共性功能,根据共性功能抽取的位置不同,最终运行代码时要将其加入到合理的位置。
AOP有5种通知类型:
- 前置通知(before):在切入点方法执行之前执行。
- 后置通知(after):在切入点方法执行之后执行,无论切入点方法内部是否出现异常,后置通知都会执行。
- **环绕通知(around):**手动调用切入点方法并对其进行增强的通知方式。
- 返回后通知(afterReturning):在切入点方法执行之后执行,如果切入点方法内部出现异常将不会执行。
- 抛出异常后通知(afterThrowing):在切入点方法执行之后执行,只有当切入点方法内部出现异常之后才执行。
5.2 AOP通知详解
5.2.1 前置通知
- 注解名称:@Before
- 类型:方法注解
- 位置:定义在通知方法上
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法执行之前执行。
示例:
@Component
@Aspect
public class MyAdvice {
//切入点
@Pointcut("execution(void com.baidou.dao.BookDao.update())")
private void pt(){}
@Before("pt()") //前置通知
public void before() {
System.out.println("before advice ...");
}
}
5.2.2 后置通知
- 注解名称:@After
- 类型:方法注解
- 位置:定义在通知方法上
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法执行之后运行。
示例:
@Component
@Aspect
public class MyAdvice {
//切入点
@Pointcut("execution(void com.baidou.dao.BookDao.update())")
private void pt(){}
@After("pt()") //后置通知
public void after() {
System.out.println("after advice ...");
}
}
5.2.3 返回后通知
- 名称:@AfterReturning(了解)
- 类型:方法注解
- 位置:定义在通知方法上
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法正常执行完毕后运行,如果切入点方法内部出现异常将不会执行。
示例:
@Component
@Aspect
public class MyAdvice {
//切入点
@Pointcut("execution(void com.baidou.dao.BookDao.update())")
private void pt(){}
@AfterReturning("pt()") //返回后通知
public void afterReturning() {
System.out.println("afterReturning advice ...");
}
}
5.2.4 抛出异常后通知
- 名称:@AfterThrowing(了解)
- 类型:方法注解
- 位置:定义在通知方法上
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行。(只有当切入点方法内部出现异常之后才执行)
示例:
@Component
@Aspect
public class MyAdvice {
//切入点
@Pointcut("execution(void com.baidou.dao.BookDao.update())")
private void pt(){}
@AfterThrowing("pt()") //抛出异常后通知
public void afterThrowing() {
System.out.println("afterThrowing advice ...");
}
}
示例:
5.2.5 环绕通知
- 名称:@Around(重点,常用)
- 类型:方法注解
- 位置:定义在通知方法上
- 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前后运行。
示例:
@Component
@Aspect
public class MyAdvice {
//切入点
@Pointcut("execution(void com.baidou.dao.BookDao.update())")
private void pt(){}
@Around("pt()") // 环绕通知
public Object around(ProceedingJoinPoint pjp) throws Throwable {
System.out.println("around before advice ...");
Object ret = pjp.proceed();
System.out.println("around after advice ...");
return ret;
}
}
环绕通知注意事项:
-
环绕通知方法形参必须是ProceedingJoinPoint,表示正在执行的连接点,使用该对象的proceed()方法表示对原始对象方法进行调用,返回值为原始对象方法的返回值。
-
环绕通知方法的返回值建议写成Object类型,用于将原始对象方法的返回值进行返回,哪里使用代理对象就返回到哪里。
二、AOP案例
1. 测量业务接口万次执行效率
需求:任意业务层接口执行均可显示其执行效率(执行时长)
案例分析:
- 业务功能:业务层接口执行前后分别记录时间,求差值得到执行效率。
- 通知类型选择前后均可以增强的类型——环绕通知。
代码实现:
1、前期准备:
-
Spring整合mybatis对spring_db数据库中的Account进行CRUD操作。(在上一个spring笔记中写过)
-
Spring整合Junit测试CRUD是否OK。
-
在pom.xml中添加aspectjweaver切入点表达式依赖。
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-jdbc</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.10.RELEASE</version>
</dependency>
<!--切入点表达式依赖-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.16</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis</artifactId>
<version>3.5.6</version>
</dependency>
<dependency>
<groupId>org.mybatis</groupId>
<artifactId>mybatis-spring</artifactId>
<version>1.3.0</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
2、编写通知类
@Component //让spring容器管理
@Aspect //定义当前类是切面
public class ProjectAdvice {
//匹配业务层的所有方法
@Pointcut("execution(* com.baidou.service.*Service.*(..))")
private void servicePt(){}
//设置环绕通知,在原始操作的运行前后记录执行时间
@Around("ProjectAdvice.servicePt()") //本类类名可以省略不写
public void runSpeed(ProceedingJoinPoint pjp) throws Throwable {
//获取执行的签名对象
Signature signature = pjp.getSignature();
//获取接口/类全限定名
String className = signature.getDeclaringTypeName();
//获取方法名
String methodName = signature.getName();
//记录开始时间
long start = System.currentTimeMillis();
//执行万次操作
for (int i = 1; i <= 10000; i++) {
pjp.proceed();//调用原始方法的
}
//记录结束时间
long end = System.currentTimeMillis();
//打印执行结果
System.out.println("万次执行:"+ className+"."+methodName+"---->" +(end-start) + "ms");
}
/*
//简写
@Around("execution(* com.baidou.service.*Service.*(..))")
public void runSpeed(ProceedingJoinPoint pjp) throws Throwable {
//获取执行的签名对象
Signature signature = pjp.getSignature();
//获取接口/类全限定名
String className = signature.getDeclaringTypeName();
//获取方法名
String methodName = signature.getName();
//记录开始时间
long start = System.currentTimeMillis();
//执行万次操作
for (int i = 1; i <= 10000; i++) {
pjp.proceed();//调用原始方法的
}
//记录结束时间
long end = System.currentTimeMillis();
//打印执行结果
System.out.println("万次执行:"+ className+"."+methodName+"---->" +(end-start) + "ms");
}
*/
}
3、在SpringConfig配置类上开启AOP注解功能
@Configuration
@ComponentScan("com.baidou") //spring注解包扫描
@PropertySource("classpath:jdbc.properties") //加载配置文件
@Import({JdbcConfig.class,MybatisConfig.class}) //导入其他配置类(第三方bean)
@EnableAspectJAutoProxy //开启AOP注解功能
public class SpringConfig {
}
4、编写测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTestCase {
@Autowired
private AccountService accountService;
@Test
public void testFindById(){
Account account = accountService.findById(2);
}
@Test
public void testFindAll(){
List<Account> list = accountService.findAll();
}
}
2. AOP切入点数据获取
在环绕通知中可以获取到哪些数据?
- 获取参数
- 获取返回值
- 获取异常
2.1 获取参数
说明:在所有通知中都可以获取到连接点方法的参数。jp.getArgs()
- JoinPoint对象描述了连接点方法的运行状态,可以获取到原始方法的调用参数。
@Before("pt()")//前置通知
public void before(JoinPoint jp) {
Object[] args = jp.getArgs(); //获取连接点方法的参数
System.out.println(Arrays.toString(args));
}
- ProccedJointPoint是JoinPoint的子类,适用于环绕通知,可以实现对原始方法的调用。
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Object[] args = pjp.getArgs(); //获取连接点方法的参数
System.out.println(Arrays.toString(args));
Object ret = pjp.proceed();//调用原始方法
return ret;
}
2.2 获取返回值
说明:在返回后通知和环绕通知中都可以获取到连接点方法的返回值
- 抛出异常后通知可以获取切入点方法中出现的异常信息,使用形参可以接收对应的异常对象。
@AfterReturning(value = "pt()",returning = "ret")
public void afterReturning(String ret) { //变量名要和returning="ret"的属性值一致
System.out.println("afterReturning advice ..."+ret);
}
- 环绕通知中可以手工书写对原始方法的调用,得到的结果即为原始方法的返回值。
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
// 手动调用连接点方法,返回值就是连接点方法的返回值
Object ret = pjp.proceed();
return ret;
}
2.3 获取异常
说明:在抛出异常后通知和环绕通知中都可以获取到连接点方法中出现的异常
- 抛出异常后通知可以获取切入点方法中出现的异常信息,使用形参可以接收对应的异常对象
@AfterThrowing(value = "pt()",throwing = "t")
public void afterThrowing(Throwable t) {//变量名要和throwing = "t"的属性值一致
System.out.println("afterThrowing advice ..."+ t);
}
- 使用环绕通知获取切入点方法的异常信息:
@Around("pt()")
public Object around(ProceedingJoinPoint pjp) {
Object ret = null;
//此处需要try...catch处理,catch中捕获到的异常就是连接点方法中抛出的异常
try {
ret = pjp.proceed();
} catch (Throwable t) {
t.printStackTrace();
}
return ret;
}
3. 网盘密码数据兼容处理
需求:对网盘分享链接输入密码时尾部多输入的空格做兼容处理。
- 在业务方法执行之前对所有的输入参数进行格式处理——trim()去首尾空格。
- 使用处理后的参数调用原始方法——环绕通知中存调用原始方法,传入处理后的密码参数。
//突发小技巧:百度网盘通过接口传参实现密码校验
https://pan.baidu.com/s/1HgvjN9yFgNpQ5q4TP2bB5w?pwd=1111
1、编写接口
//service层代码
public interface ResourcesService {
public boolean openURL(String url ,String password);
}
@Service
public class ResourcesServiceImpl implements ResourcesService {
@Autowired
private ResourcesDao resourcesDao;
public boolean openURL(String url, String password) {
return resourcesDao.readResources(url,password);
}
}
//dao层代码
public interface ResourcesDao {
boolean readResources(String url, String password);
}
@Repository
public class ResourcesDaoImpl implements ResourcesDao {
public boolean readResources(String url, String password) {
System.out.println(password.length());
//模拟校验
return password.equals("root");
}
}
2、编写通知类
@Component
@Aspect//通知类
public class HandlePwdAdvice {
//1.定义切入点
@Pointcut("execution(* com.baidou.service.*.openURL(..))")
private void pt(){}
//2.编写环绕通知,配置切面
@Around("pt()")
public Object handlePwd(ProceedingJoinPoint pjp) throws Throwable {
//获取原始方法第二个参数(密码)
Object[] args = pjp.getArgs();
Object pwd = args[1];
//b.对密码进行trim操作
args[1] = pwd.toString().trim();
//调用原始方法,但是使用修改后的参数,接收返回值
Object result = pjp.proceed(args);//不传参数的话,就还是使用原来的参数.
//d.返回密码校验结果
return result;
}
}
3、在SpringConfig配置类上开启AOP注解功能
@Configuration
@ComponentScan("com.baidou")
@EnableAspectJAutoProxy
public class SpringConfig {
}
4、测试
public class App {
public static void main(String[] args) {
ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
ResourcesService resourcesService = ctx.getBean(ResourcesService.class);
boolean flag = resourcesService.openURL("http://pan.baidu.com/helloworld", "root ");
System.out.println(flag);
}
}
三、Spring事务管理
1. Spring事务介绍
事务作用:在数据层保障一系列的数据库操作,要么一起成功,要么一起失败。
Spring事务作用:在数据层或业务层保障一系列的数据库操作,要么一起成功,要么一起失败。
我们只需要在业务逻辑执行之前开启事务,执行完毕后提交事务。如果执行过程中报错,则回滚事务,把数据恢复到事务开始之前的状态。
//spring中进行事务管理的核心接口
public interface PlatformTransactionManager extends TransactionManager {
// 提交事务
void commit(TransactionStatus status) throws TransactionException;
// 回滚事务
void rollback(TransactionStatus status) throws TransactionException;
}
//我们的数据层使用的是MyBatis,所以使用DataSourceTransactionManager实现类
public class DataSourceTransactionManager extends AbstractPlatformTransactionManager
implements ResourceTransactionManager, InitializingBean {
...
}
事务的操作:
- 开启事务(一组操作开始前,开启事务):start transaction/begin;
- 提交事务(这组操作全部成功后,提交事务):commit;
- 回滚事务(中间任何一个操作出现异常,回滚事务):rollback;
2. Spring事务快速入门
2.1 基础环境准备
1、执行sql脚本
create database if not exists spring_db;
use spring_db;
create table tbl_account(
id int primary key auto_increment,
name varchar(20) null,
money double null
);
insert into `tbl_account`(name,money) values('zhangsan',1000);
insert into `tbl_account`(name,money) values('lisi',1000);
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pU3Jgmcv-1687709893290)(assets/image-20230625174707752.png)]
2、创将springboot工程
3、在pom.xml中,导入相关依赖
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.baidou</groupId>
<artifactId>springboot_transactional</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>jar</packaging>
<name>springboot_transactional</name>
<description>Spring事务</description>
<!--父工程-->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.4.5</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<maven.compiler.source>8</maven.compiler.source>
<maven.compiler.target>8</maven.compiler.target>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties>
<dependencies>
<!-- web -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- lombok -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- mp -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.4.1</version>
</dependency>
<!--aop切面框架-->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.4</version>
</dependency>
<!-- mysql驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<!-- junit -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
</dependency>
</dependencies>
<!--打包-->
<build>
<!--<finalName>springboot_transactional</finalName>-->
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
4、在application.yml中,添加一些配置
spring:
application:
name: springboot-aop # 服务名称
# 配置数据源
datasource:
driver-class-name: com.mysql.jdbc.Driver
url: jdbc:mysql://localhost:3306/spring_db?useSSL=false
username: root
password: 123456
# mp配置
mybatis-plus:
mapper-locations: classpath:mapper/*.xml # mapper文件所在的目录
type-aliases-package: cn.aopmin.demo.entity # 实体类所在的包
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印sql语句
server:
port: 8866 # 服务端口
5、创键实体类
package cn.aopmin.demo.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
/**
* 账户实体
*
* @author 白豆五
* @version 2023/06/25
* @since JDK8
*/
@TableName("tbl_account")
@Data
public class Account {
private Long id;
private String name;
private Float money;
}
6、定义mapper接口
package cn.aopmin.demo.mapper;
import cn.aopmin.demo.entity.Account;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
/**
* 账户持久层接口
*/
@Mapper
public interface IAccountMapper extends BaseMapper<Account> {
// 入账
@Update("update tbl_account set money = money - #{money} where name = #{name}")
void outMoney(@Param("name") String outUser,@Param("money") Double money);
// 出账
@Update("update tbl_account set money = money + #{money} where name = #{name}")
void inMoney(@Param("name") String outUser,@Param("money") Double money);
}
7、定义service接口
package cn.aopmin.demo.service;
import cn.aopmin.demo.entity.Account;
import com.baomidou.mybatisplus.extension.service.IService;
/**
* 账户业务接口
*/
public interface IAccountService extends IService<Account> {
/**
* 转账操作
*
* @param outUser 汇款方
* @param inUser 收款方
* @param money 转账金额
*/
void transfer(String outUser, String inUser, Float money);
}
package cn.aopmin.demo.service.impl;
import cn.aopmin.demo.entity.Account;
import cn.aopmin.demo.mapper.IAccountMapper;
import cn.aopmin.demo.service.IAccountService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 账户业务实现类
*
* @author 白豆五
* @version 2023/06/25
* @since JDK8
*/
@Slf4j
@Service
public class IAccountServiceImpl extends ServiceImpl<IAccountMapper, Account> implements IAccountService {
@Autowired
private IAccountMapper accountMapper;
@Override
public void transfer(String outUser, String inUser, Float money) {
accountMapper.outMoney(outUser, money); // 转出
// int i = 1 / 0;
accountMapper.inMoney(inUser, money); // 转入
}
}
8、编写控制器类
package cn.aopmin.demo.controller;
import cn.aopmin.demo.service.IAccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
/**
* 账户控制器
*
* @author 白豆五
* @version 2023/06/25
* @since JDK8
*/
@RestController
@RequestMapping
public class AccountController {
@Autowired
private IAccountService accountService;
/**
* 转账接口
*
* @param outUser 汇款人
* @param inUser 收款人
* @param money 转账金额
* @return
*/
@PostMapping("/transfer")
private String transfer(String outUser, String inUser, Double money) {
try {
accountService.transfer(outUser, inUser, money);
return "ok";
} catch (Exception e) {
e.printStackTrace();
return "fail";
}
}
}
9、在主类开启mapper扫描、aop功能
package cn.aopmin.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.transaction.annotation.Transactional;
@SpringBootApplication
@EnableAspectJAutoProxy // 开启AOP
@MapperScan("cn.aopmin.demo.mapper") // 扫描mapper
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}
10、启动服务&测试:
2.2 使用Spring事务
springboot提供了事务的自动装配,即spring-boot-starter-data-jdbc,如果单独使用spring需要添加一些配置。
springboot相关事务自动装配的类有:
- TransactionAutoConfiguration
- DataSourceTransactionManagerAutoConfiguration
1、在业务层接口上添加Spring事务管理注解(在接口方法或实现类方法添加都可以)
package cn.aopmin.demo.service;
import cn.aopmin.demo.entity.Account;
import com.baomidou.mybatisplus.extension.service.IService;
import org.springframework.transaction.annotation.Transactional;
/**
* 账户业务接口
*/
public interface IAccountService extends IService<Account> {
/**
* 转账操作
*
* @param outUser 汇款方
* @param inUser 收款方
* @param money 转账金额
*/
@Transactional //spring事务管理注解(声明式事务有两种:方式1通过xml配置、方式2通过spring事务注解声明)
void transfer(String outUser, String inUser, Double money);
}
注意事项:
-
Spring注解式事务通常添加在业务层接口中而不会添加到业务层实现类中,降低耦合。
-
注解式事务可以添加到业务方法上表示当前方法开启事务,也可以添加到接口上表示当前接口所有方法开启事务。
2、在springboot启动类上添加开启事务管理注解:@EnableTransactionManagement
注意:如果在spring中使用,需要配置平台事务管理器PlatformTransactionManager实例。
package cn.aopmin.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@SpringBootApplication
@EnableAspectJAutoProxy // 开启AOP
@MapperScan("cn.aopmin.demo.mapper") // 扫描mapper
@EnableTransactionManagement // 开启事务管理
public class MainApplication {
public static void main(String[] args) {
SpringApplication.run(MainApplication.class, args);
}
}
3、修改业务实现类的转账方法
@Override
public void transfer(String outUser, String inUser, Double money) {
// 转出
accountMapper.outMoney(outUser, money);
int i = 1 / 0; // 模拟异常
// 转入
accountMapper.inMoney(inUser, money);
}
4、重启项目测试接口:
小结:
@Transactional作用:将当前方法交给spring进行事务管理,方法执行前,开启事务;成功执行完毕,提交事务;出现异常,回滚事务;
3. Spring事务角色
在Spring事务管理中,有两种主要的角色,即事务管理员和事务协调员。这两个角色共同工作以确保事务的一致性和可靠性。
- 事务管理员(全局事务):发起事务方,在Spring中通常指代业务层开启事务的方法;
- 事务协调员(子事务/分支事务):加入事务方,在Spring中通常指代数据层方法,也可以是业务层方法;
4. Spring事务相关配置
也就是@Transactional注解的配置信息。
@Transactional注解源码:
package org.springframework.transaction.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
import org.springframework.transaction.TransactionDefinition;
@Target({ElementType.TYPE, ElementType.METHOD}) //作用在类/接口、方法上
@Retention(RetentionPolicy.RUNTIME) //生命周期
@Inherited //可以被继承
@Documented //文档
public @interface Transactional {
@AliasFor("transactionManager")
String value() default "";
@AliasFor("value")
String transactionManager() default "";
String[] label() default {};
// 设置事务传播行为
Propagation propagation() default Propagation.REQUIRED;
// 设置事务隔离级别
Isolation isolation() default Isolation.DEFAULT;
// 事务超时时间,默认值为-1,表示永不超时
int timeout() default TransactionDefinition.TIMEOUT_DEFAULT;
String timeoutString() default "";
// 设置是否为只读事务,默认为false ,不只读
boolean readOnly() default false;
// 设置事务回滚异常 (字节码类型)
// 默认遇到运行时异常和错误时才回滚
Class<? extends Throwable>[] rollbackFor() default {};
// 设置事务回滚异常 (字符串,类的全限定名)
String[] rollbackForClassName() default {};
// 设置事务不回滚异常 (字节码类型)
Class<? extends Throwable>[] noRollbackFor() default {};
// 设置事务不回滚异常 (字符串,类的全限定名)
String[] noRollbackForClassName() default {};
}
注意:对于RuntimeException类型异常或者Error错误,Spring事务才能够进行回滚操作。但是对于编译异常(Exception),Spring事务是不进行回滚的,所以需要通过rollbackFor属性来指定要回滚的异常。
@Transactional(rollbackFor = IOException.class)//到遇到IO异常进行回滚
@Override
public void transfer(String outUser, String inUser, Double money) throws IOException {
// 转出
accountMapper.outMoney(outUser, money);
if (true) throw new IOException();
// 转入
accountMapper.inMoney(inUser, money);
}
示例:转账时记录日志到数据库中
1、执行sql脚本
create table tbl_log(
id INT PRIMARY KEY auto_increment,
info varchar(255),
createTime DATE
);
2、实体类
package cn.aopmin.demo.entity;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.util.Date;
/**
* 日志实体
*
* @author 白豆五
* @version 2023/06/25
* @since JDK8
*/
@TableName("tbl_log")
@Data
public class Log {
private Integer id;
private String info;
private Date createTime;
}
3、定义mapper接口
package cn.aopmin.demo.mapper;
import cn.aopmin.demo.entity.Log;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
/**
* 日志持久层接口
*/
@Mapper
public interface ILogMapper extends BaseMapper<Log> {
/**
* 记录日志
* @param info
*/
@Insert("insert into tbl_log values(#{info},now())") // now()是mysql的函数,表示当前时间
void log(String info);
}
4、定义service接口
package cn.aopmin.demo.service;
import cn.aopmin.demo.entity.Log;
import com.baomidou.mybatisplus.extension.service.IService;
import org.springframework.transaction.annotation.Transactional;
/**
* 日志业务接口
*/
public interface ILogService extends IService<Log> {
// propagation设置事务属性:传播行为设置为当前操作需要新事务
@Transactional
void log(String outUser, String inUser, Double money);
}
package cn.aopmin.demo.service.impl;
import cn.aopmin.demo.entity.Log;
import cn.aopmin.demo.mapper.ILogMapper;
import cn.aopmin.demo.service.ILogService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* 日志业务实现类
*
* @author 白豆五
* @version 2023/06/25
* @since JDK8
*/
@Slf4j
@Service
public class ILogServiceImpl extends ServiceImpl<ILogMapper, Log> implements ILogService {
@Autowired
private ILogMapper logMapper;
/**
* 记录日志
*
* @param outUser 汇款方
* @param inUser 收款方
* @param money 转账金额
*/
@Override
public void log(String outUser, String inUser, Double money) {
logMapper.log("转账操作由" + outUser + "到" + inUser + ",金额:" + money);
}
}
4、在转账业务中添加日志记录
package cn.aopmin.demo.service.impl;
import cn.aopmin.demo.entity.Account;
import cn.aopmin.demo.mapper.IAccountMapper;
import cn.aopmin.demo.service.IAccountService;
import cn.aopmin.demo.service.ILogService;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.io.IOException;
/**
* 账户业务实现类
*
* @author 白豆五
* @version 2023/06/25
* @since JDK8
*/
@Slf4j
@Service
public class IAccountServiceImpl extends ServiceImpl<IAccountMapper, Account> implements IAccountService {
@Autowired
private IAccountMapper accountMapper;
@Autowired
private ILogService logService;//记录日志
@Transactional //spring事务管理注解
@Override
public void transfer(String outUser, String inUser, Double money) {
try {
accountMapper.outMoney(outUser, money);// 转出
// int i =1/0;
accountMapper.inMoney(inUser, money); // 转入
} finally {
// 记录日志
logService.log(outUser, inUser, money);
}
}
}
5、重启项目测试:
5.1 将金额都恢复到1000
5.2 测试接口:http://localhost:8866/transfer
5.3、查看控制台日志和表数据
注:当我们转账业务方法出现异常时,并没有记录日志
- 原因:日志的记录与转账操作隶属同一个事务,要么同时成功,要么同时失败。
5. 事务的传播行为
事务传播行为:事务协调员对事务管理员所携带事务的处理态度。
Spring 事务传播行为(Transaction Propagation Behavior)是指在多个事务方法之间进行事务的传播和管理,即一个事务方法调用另一个事务方法时,如何处理事务。
在 Spring 中,我们可以通过使用 @Transactional 注解来管理事务,而其中的 propagation 属性用于指定事务的传播行为。下面是一些常用的事务传播行为选项:(7种)
-
REQUIRED(默认): 如果当前没有事务,就创建一个新事务;如果当前已经有事务,就加入到当前事务中。
-
REQUIRES_NEW: 总是创建一个新的事务,如果当前已经有事务正在进行,就将当前事务挂起。
-
SUPPORTS: 如果当前有事务在进行,就加入该事务;如果没有事务在进行,就以非事务方式执行。
-
MANDATORY: 必须在一个已存在的事务中运行,否则会抛出异常。
-
NOT_SUPPORTED: 以非事务方式运行,如果当前有事务在进行,就将其挂起。
-
NEVER: 以非事务方式运行,如果当前有事务在进行,就会抛出异常。
-
NESTED: 如果当前有事务在进行,就在嵌套事务内执行;如果当前没有事务,在似于REQUIRED的方式下进行。
接下来我们使用REQUIRES NEW传播行为,优化记录日志案例:
public interface ILogService extends IService<Log> {
// propagation设置事务属性:传播行为设置为当前操作需要新事务
@Transactional(propagation = Propagation.REQUIRES_NEW ) //表示无论你有没有事务,我都要新开一个事务
void log(String outUser, String inUser, Double money);
}
这样我们不管转账是否成功,都会记录日志到数据库中。