SpringAOP框架介绍

  1. AOP一种区别于OOP的编程思维,用来完善和解决OOP的非核心代码冗余和不方便统一维护问题!
  2. 代理技术(动态代理|静态代理)是实现AOP思维编程的具体技术,但是自己使用动态代理实现代码比较繁琐!
  3. Spring AOP框架,基于AOP编程思维,封装动态代理技术,简化动态代理技术实现的框架!SpringAOP内部帮助我们实现动态代理,我们只需写少量的配置,指定生效范围即可,即可完成面向切面思维编程的实现!
涉及的注解 含义 位置
@Before() 目标方法执行前 增强方法 切点或方法
@AfterReturning() 目标方法正常执行后 增强方法 切点或方法,返回值
@AfterThrowing() 目标方法出异常 增强方法 切点或方法,异常
@After() 目标方法执行后 增强方法 切点或方法
@Around() 自定义目标方法执行时机 增强方法 切点或方法
@Pointcut 定义切点 切点空方法 切点(“execution( cn...*(..))”)
@Aspect 定义切面
@EnableAspectJAutoProxy 开启aspect的注解 配置类
@Order() 定义切面优先级 切面类 值越小优先级越高

基于注解的方式实现SpringAOP

快速实现

  1. 引入依赖
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    <dependencies>
    <!--spring context依赖-->
    <!--当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了-->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>6.0.6</version>
    </dependency>

    <!-- spring-context会帮我们传递过来spring-aop
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>6.0.6</version>
    </dependency>
    -->

    <!-- spring-aspects会帮我们传递过来aspectjweaver -->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>6.0.6</version>
    </dependency>

    <!--junit5测试-->
    <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.3.1</version>
    </dependency>
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>6.0.6</version>
    <scope>test</scope>
    </dependency>
    </dependencies>
  2. 引入接口
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    cn.xnj.service

    public interface Calculator {

    int add(int i, int j);

    int sub(int i, int j);

    int mul(int i, int j);

    int div(int i, int j);
    }
  3. 引入实现类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    cn.xnj.service.impl

    //实现计算接口,单纯添加 + - * / 实现! 掺杂其他功能!
    // aop - 只针对ioc容器对象,所以要注册ioc容器
    @Component
    public class CalculatorPureImpl implements Calculator {

    @Override
    public int add(int i, int j) {
    int result = i + j;
    return result;
    }

    @Override
    public int sub(int i, int j) {
    int result = i - j;
    return result;
    }

    @Override
    public int mul(int i, int j) {
    int result = i * j;
    return result;
    }

    @Override
    public int div(int i, int j) {
    int result = i / j;
    return result;
    }
    }
  4. 配置类
    1
    2
    3
    4
    5
    6
    cn.xnj.config

    @Configuration
    @ComponentScan("cn.xnj")
    public class MyConfiguration {
    }
  5. 测试类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    test/java/cn.xnj.test

    @SpringJUnitConfig(classes = MyConfiguration.class)
    public class SpringAopTest {
    @Autowired
    private Calculator calculator;
    @Test
    public void test_01(){
    int result = calculator.add(1, 1);
    System.out.println(result);//控制台输出: 2
    //能运行成功说明环境搭建成功
    }
    }
  6. 切面类实现(cn.xnj.advice)
    步骤/** public class LogAdvice 
         * 此类为增强类,增强类的内部要存储增强代码
         *  1. 定义方法存储增强代码
         *     具体定义几个方法,根据插入的的位置决定
         *     
         *  2. 使用注解配置指定插入目标方法的位置
         *     前置  @Before
         *     后置  @AfterReturning
         *     异常  @AfterThrowing
         *     最后  @After
         *     环绕  @Around
         *     
         *     可以形象理解为如下
         *     try{
         *          前置
         *          目标方法执行
         *          后置
         *     }catch(){
         *         异常
         *     }finally(){
         *          最后
         *     }
         *
         *  3. 配置切点表达式 [ 选中插入的方法  切点 ]
         *     "execution(* cn.xnj.service.*.*(..))"   切点表达式  固定格式
         *
         *  4. 补全注解
         *     加入ioc容器  @Component
         *     配置切面  @Aspect = 切点 + 增强
         *
         *  5. 开启aop注解驱动
         *    在配置类上开启aop注解驱动的注解 @EnableAspectJAutoProxy
         *    等同于 xml配置文件 <aop:aspectj-autoproxy/>  开启aop注解驱动
         *
         */
    
    a.增强类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    @Component // 注册到ioc容器中
    @Aspect // 声明当前类为增强类-切面
    public class LogAdvice {

    @Before("execution(* cn.xnj.service.*.*(..))")
    public void start(){
    System.out.println("方法开始了");
    }

    @After("execution(* cn.xnj.service.*.*(..))")
    public void after(){
    System.out.println("方法结束了");
    }

    @AfterThrowing("execution(* cn.xnj.service.*.*(..))")
    public void error(){
    System.out.println("方法出现异常了");
    }
    }
    b.配置类开启aspect的注解(@EnableAspectJAutoProxy)
    1
    2
    3
    4
    5
    @Configuration
    @ComponentScan("cn.xnj") //请确保aop增强也在扫描的范围内
    @EnableAspectJAutoProxy // 开启aspect的注解
    public class MyConfiguration {
    }
  7. 输出结果
    回到前面的测试,再次运行即可看到输出:
         方法开始了  
         方法结束了  
         2
    

获取通知细节信息

下面演示用的组件如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
cn.xnj.servcie

public interface Calculator {
int add(int i, int j);

int div(int i, int j);
}


cn.xnj.service.impl

@Component
public class CalculatorPureImpl implements Calculator {
@Override
public int add(int i, int j) {//两数相加
int result = i + j;
return result;
}

@Override
public int div(int i, int j) {//两数相除
int result = i / j;
return result;
}

}

测试用的组件如下
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@SpringJUnitConfig(classes = MyConfiguration.class)
public class SpringAopTest {
@Autowired
private Calculator calculator;

@Test
public void test_01(){
int result = calculator.add(1, 1);
System.out.println(result);
}

@Test
public void test_02(){
calculator.div(1, 0);
}
}

JointPoint接口

需要获取方法签名传入的实参等信息时,可以在通知方法声明JoinPoint类型的形参。

  • 要点1:JoinPoint 接口通过 getSignature() 方法获取目标方法的签名(方法声明时的完整信息)
  • 要点2:通过目标方法签名对象获取方法名
  • 要点3:通过 JoinPoint 对象获取外界调用目标方法时传入的实参列表组成的数组
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
@Component
@Aspect
public class MyAdvice {

@Before("execution(public int cn.xnj.service.Calculator.add(int,int))")
public void before(JoinPoint joinPoint){
// 1.通过JoinPoint对象获取目标方法签名对象
// 方法的签名:一个方法的全部声明信息
Signature signature = joinPoint.getSignature();

// 2.通过方法的签名对象获取目标方法的详细信息
String methodName = signature.getName();
System.out.println("methodName = " + methodName);//方法名

int modifiers = signature.getModifiers();// 方法的修饰符
System.out.println("modifiers = " + modifiers);

String declaringTypeName = signature.getDeclaringTypeName();
System.out.println("declaringTypeName = " + declaringTypeName);// 目标方法所在类的全限定类名

// 3.通过JoinPoint对象获取外界调用目标方法时传入的实参列表
Object[] args = joinPoint.getArgs();

// 4.由于数组直接打印看不到具体数据,所以转换为List集合
List<Object> argList = Arrays.asList(args);

System.out.println("[AOP前置通知] " + methodName + "方法开始了,参数列表:" + argList);
}
}

控制台输出结果

1
2
3
4
5
methodName = add
modifiers = 1025
declaringTypeName = cn.xnj.service.Calculator
[AOP前置通知] add方法开始了,参数列表:[1, 1]
2

方法返回值

在返回通知中,通过@AfterReturning注解的returning属性获取目标方法的返回值!

  • 在方法形参上加:(Object result) result表示接收返回结果
  • 在注解里添加:(returning = "接收返回结果的形参名")
1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
@Aspect
public class MyAdvice {

@AfterReturning(
value = "execution(public int cn.xnj.service.Calculator.add(int,int)))",
returning = "result")
public void afterThrowing(JoinPoint joinPoint, Object result){
String name = joinPoint.getSignature().getName();
System.out.println("方法的名字是: " + name);
System.out.println("方法的返回值是: " + result);
}

}

控制台输出结果:

1
2
3
方法的名字是: add
方法的返回值是: 2
2

异常对象捕捉

在异常通知中,通过@AfterThrowing注解的throwing属性获取目标方法抛出的异常对象

  • 在方法的形参上添加(Throwable e) e表示接收方法返回的异常
  • 在注解上添加throwing = "接收异常的形参名"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package cn.xnj.advice;

import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

import java.util.Arrays;
import java.util.List;

@Component
@Aspect
public class MyAdvice {

@AfterThrowing(
value = "execution(public int cn.xnj.service.Calculator.div(int,int))",
throwing = "e"
)
public void error(JoinPoint joinPoint, Throwable e){
String name = joinPoint.getSignature().getName();
System.out.println("方法的名字是: " + name);
System.out.println("方法的异常是: " + e);
}

}

控制台输出结果:

1
2
方法的名字是: div
方法的异常是: java.lang.ArithmeticException: / by zero

切点表达式语法

语法是固定格式的:execution(1 2 3.4.5.(6))

  • 括号里面一共6个部分
  • 1:访问修饰符 2:方法的返回参数类型 3:包的位置 4:类名 5:方法名 6:方法参数
  • 如:execution(public int cn.xnj.service.Calculator.div(int,int))
  1. 访问修饰符

    • public / private
  2. 方法的返回参数类型

    • String int void
    • 如果不考虑访问修饰符和返回值! 这两位整合在一起写作*
    • 不考虑的时候必须两个都不考虑!不能出现 * String
  3. 包的位置

    • 具体包:cn.xnj.service.impl
    • 单层模糊:cn.xnj.service.* * 单层模糊
    • 多层模糊:cn..impl ..任意层的模糊
    • 注意: ..不能放开头
  4. 类的名称

    • 具体:CalculatorPureImpl
    • 模糊:*
    • 部分模糊:*Impl 表示以Impl结尾的类
  5. 方法名 语法和类名一致

  6. (6)形参参数列表

    • 没有参数:()
    • 有具体参数:(String)(String,int)
    • 模糊参数:(..)
    • 部分模糊:
      • (String..) String后面还有没有参数无所谓
      • (..int) 最后一个参数为int
      • (String..int) 第一个参数为String、最后一个参数为int

场景示例

  1. 查询某包某类下,访问修饰符是公有,返回值是int的全部方法
    execution(public int xx.xx.jj.*(..))
  2. 查询某包下类中第一个参数是String的方法
    execution(* xx.xx.jj.*(String..))
  3. 查询全部包下,无参数的方法!
    execution(* *..*.*())
  4. 查询com包下,以int参数类型结尾的方法
    execution(* com..*.*(..int))
  5. 查询指定包下,Service开头类的私有返回值int的无参数方法
    execution(private int xx.xx.Service*.*())

切点表达式的提取(重用)

  1. 当前类中提取切点

    • 定义一个空方法并加上注解@pointcut()
    • 在该注解里写切点表达式
    • 在增强注解中引用切点表达式的方法即可 直接调用方法
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Component
    @Aspect
    public class MyAdvice {

    @Pointcut("execution(* cn..impl.*.*(..))")
    public void mp(){};

    @Before("mp()")
    public void before(){
    System.out.println("前置通知");
    }

    @AfterReturning("mp()")
    public void after(){
    System.out.println("后置通知");
    }
    }
  2. 切点统一管理

    • 创建一个存储切点的类
    • 创建空方法单独维护切点表达式
    • 引用方式:类的全限定符合.方法名()

    统一管理切点的类(cn.xnj.pointcut)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    @Component // 注册到ioc容器中
    public class MyPointCut {

    @Pointcut("execution(* cn.xnj.service.*.add(..))")
    public void log(){}

    @Pointcut("execution(* cn.xnj.service.*.div(..))")
    public void log2(){}
    }

    引用(cn.xnj.advice)

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Component
    @Aspect
    public class MyAdvice {

    @Before("cn.xnj.pointcut.MyPointCut.log()")
    public void before(){
    System.out.println("方法开始了");
    }

    @AfterReturning("cn.xnj.pointcut.MyPointCut.log()")
    public void after(){
    System.out.println("方法结束了");
    }
    }

环绕通知

环绕通知对应整个 try…catch…finally 结构,包括前面四种通知的所有功能。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Component
@Aspect
public class TxAdvice {
@Pointcut("execution(public int cn.xnj.service.Calculator.add(int,int))")
public void p(){};

@Around("p()")
public Object transatcion(ProceedingJoinPoint joinPoint){
//保证目标方法被执行
Object[] args = joinPoint.getArgs();// 外界传入的实参
Object result = null;
try {
System.out.println("开启事务");// 相当于前置通知:@Before
Object result1 = joinPoint.proceed(args);// 执行目标方法,并获取返回结果。
System.out.println("提交事务");// 相当于后置通知: @AfterReturning

result = result1;
} catch (Throwable e) {
System.out.println("出现异常,回滚事务");// 相当于异常通知: @AfterThrowing
throw new RuntimeException(e);
}finally {
System.out.println("释放资源");// 相当于最终通知: @After
}

return result;
}
}

测试类控制台输出结果
1
2
3
4
开启事务
结束提交事务
释放资源
2


切面优先级

相同的目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序
想象一个树的年轮图

  • 目标方法就是最里面的圈,每一个圈代表一个切面
  • 优先级越高的切面,越是靠外边(优先级越低靠内侧,离目标方法近)
  • 执行的顺序相当于砍树,先是外侧,再是内侧,再是外侧
    • 优先级高的前置先执行,后置后执行

使用@Order注解可以控制切面的优先级

  • @Order(较小的数): 优先级高
  • @Order(较大的数): 优先级低

1
2
3
4
@Component
@Aspect
@Order(10)// 优先级 优先级高的前置先执行,后置后执行
public class TxAdvice {}

使用XML方式实现SpringAOP

  1. 准备组件
    1
    2
    3
    4
    5
    6
    7
    8
    9
    cn.xnj.service

    @Service//注入ioc容器
    public class UserServcie {

    public int add(int i,int j){
    return i+j;
    }
    }
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    cn.xnj.advice
    //切面类
    @Component//注入ioc容器
    public class TxAdvice {

    public void begin(JoinPoint joinPoint) {
    System.out.println("开启事务");
    }

    public void commit(Object result) {
    System.out.println("提交事务");
    }

    public void rollback(JoinPoint joinPoint,Throwable e) {
    System.out.println("回滚事务");
    }
    }
  2. Xml配置文件
    spring-01.xml
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans"
    xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xmlns:context="http://www.springframework.org/schema/context"
    xmlns:aop="http://www.springframework.org/schema/aop"
    xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">

    <!-- 自动扫描 -->
    <context:component-scan base-package="cn.xnj"/>

    <!--使用标签进行aop的配置 :切面配置,声明切点,位置指定-->
    <aop:config>

    <!-- 声明切点标签 @Point-->
    <aop:pointcut id="pc" expression="execution(* cn..service.*.*(..))"/>
    <aop:pointcut id="mypc" expression="execution(* cn..service.*.*(..))"/>

    <!-- 声明切面 @Aspect
    ref= 增强对象 order= 切面的优先级 值越小 优先级越高 在外圈
    -->
    <aop:aspect ref="txAdvice" order="5">
    <!-- 声明通知 -->
    <!-- begin -> @Before("pc()") -->
    <aop:before method="begin" pointcut-ref="pc"/>

    <!-- commit-> @After(value="pc()",returing="result")-->
    <aop:after-returning method="commit" pointcut-ref="pc" returning="result"/>

    <!-- rollback-> @AfterThrowing(value="pc()",throwing="e")-->
    <aop:after-throwing method="rollback" pointcut-ref="pc" throwing="e"/>
    </aop:aspect>
    </aop:config>
    </beans>
  3. 测试
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    test/java/cn.xnj.test
    @SpringJUnitConfig(locations = "classpath:spring-01.xml")
    public class SpringAopXmlTest {
    @Autowired
    private UserServcie userServcie;

    @Test
    public void test(){
    int add = userServcie.add(1, 2);
    System.out.println(add);
    }
    }
  4. 输出
    1
    2
    3
    开启事务
    提交事务
    3

如果使用AOP技术,目标类有接口,必须使用接口类型接收IoC容器中的代理组件


Spring声明式事务

准备演示项目

  1. 导入依赖
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    <dependencies>
    <!--spring context依赖-->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>6.0.6</version>
    </dependency>

    <!--junit5测试-->
    <dependency>
    <groupId>org.junit.jupiter</groupId>
    <artifactId>junit-jupiter-api</artifactId>
    <version>5.3.1</version>
    </dependency>


    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>6.0.6</version>
    <scope>test</scope>
    </dependency>

    <dependency>
    <groupId>jakarta.annotation</groupId>
    <artifactId>jakarta.annotation-api</artifactId>
    <version>2.1.1</version>
    </dependency>

    <!-- 数据库驱动 和 连接池-->
    <dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.25</version>
    </dependency>

    <dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.2.8</version>
    </dependency>

    <!-- spring-jdbc -->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-jdbc</artifactId>
    <version>6.0.6</version>
    </dependency>

    <!-- 声明式事务依赖-->
    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-tx</artifactId>
    <version>6.0.6</version>
    </dependency>


    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>6.0.6</version>
    </dependency>

    <dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>6.0.6</version>
    </dependency>
    </dependencies>
  2. 数据库准备
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    create database studb;

    use studb;

    CREATE TABLE students (
    id INT PRIMARY KEY,
    name VARCHAR(50) NOT NULL,
    gender VARCHAR(10) NOT NULL,
    age INT,
    class VARCHAR(50)
    );

    INSERT INTO students (id, name, gender, age, class)
    VALUES
    (1, '张三', '男', 20, '高中一班'),
    (2, '李四', '男', 19, '高中二班'),
    (3, '王五', '女', 18, '高中一班'),
    (4, '赵六', '女', 20, '高中三班'),
    (5, '刘七', '男', 19, '高中二班'),
    (6, '陈八', '女', 18, '高中一班'),
    (7, '杨九', '男', 20, '高中三班'),
    (8, '吴十', '男', 19, '高中二班');
  3. 外部配置文件
    1
    2
    3
    4
    jdbc.url=jdbc:mysql://localhost:3306/studb
    jdbc.driver=com.mysql.cj.jdbc.Driver
    jdbc.username=root
    jdbc.password=123456
  4. spring配置类
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    @Configuration
    @ComponentScan("cn.xnj")
    @PropertySource("classpath:jdbc.properties")
    public class JavaConfig {

    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String username;
    @Value("${jdbc.password}")
    private String password;



    //druid连接池
    @Bean
    public DataSource dataSource(){
    DruidDataSource dataSource = new DruidDataSource();
    dataSource.setDriverClassName(driver);
    dataSource.setUrl(url);
    dataSource.setUsername(username);
    dataSource.setPassword(password);
    return dataSource;
    }


    @Bean
    //jdbcTemplate
    public JdbcTemplate jdbcTemplate(DataSource dataSource){
    JdbcTemplate jdbcTemplate = new JdbcTemplate();
    jdbcTemplate.setDataSource(dataSource);
    return jdbcTemplate;
    }

    }
  5. 准备dao/service层
    dao

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    @Repository
    public class StudentDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    public void updateNameById(String name,Integer id){
    String sql = "update students set name = ? where id = ? ;";
    int rows = jdbcTemplate.update(sql, name, id);
    }

    public void updateAgeById(Integer age,Integer id){
    String sql = "update students set age = ? where id = ? ;";
    jdbcTemplate.update(sql,age,id);
    }
    }

    service

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    @Service
    public class StudentService {

    @Autowired
    private StudentDao studentDao;

    public void changeInfo(){
    studentDao.updateAgeById(100,1);
    System.out.println("-----------");
    studentDao.updateNameById("test1",1);
    }
    }
  6. 搭建测试环境
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    /**
    * projectName: cn.xnj.test
    */
    @SpringJUnitConfig(JavaConfig.class)
    public class TxTest {

    @Autowired
    private StudentService studentService;

    @Test
    public void testTx(){
    studentService.changeInfo();
    }
    }

基本事务控制

  1. 配置事务管理器

    • 在配置类里配置装配事务管理实现对象TransactionManager
    • 在配置类上添加@EnableTransactionManagement注解,来支持事务
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    @Configuration
    @ComponentScan("cn.xnj")
    @PropertySource("classpath:jdbc.properties")
    @EnableTransactionManagement //开启事务注解的支持
    public class JavaConfig {

    @Value("${jdbc.driver}")
    private String driver;
    @Value("${jdbc.url}")
    private String url;
    @Value("${jdbc.username}")
    private String username;
    @Value("${jdbc.password}")
    private String password;


    //druid连接池
    @Bean
    public DataSource dataSource() {
    DruidDataSource dataSource = new DruidDataSource();
    dataSource.setDriverClassName(driver);
    dataSource.setUrl(url);
    dataSource.setUsername(username);
    dataSource.setPassword(password);
    return dataSource;
    }


    @Bean
    //jdbcTemplate
    public JdbcTemplate jdbcTemplate(DataSource dataSource) {
    JdbcTemplate jdbcTemplate = new JdbcTemplate();
    jdbcTemplate.setDataSource(dataSource);
    return jdbcTemplate;
    }

    //装配事务管理实现对象
    @Bean
    public TransactionManager transactionManager(DataSource dataSource){
    //内部要进行事务的操作,基于durid连接池
    DataSourceTransactionManager transactionManager = new DataSourceTransactionManager ();
    //设置连接池
    transactionManager.setDataSource(dataSource);
    return transactionManager;
    }

    }
  2. 使用声明事务注解@Transactional

    • 加在类上:该类下的所有方法都有事务
    • 加在方法上:该方法有事务

    在原本的方法上添加注解,并加入一条错误语句模拟异常

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Service
    public class StudentService {

    @Autowired
    private StudentDao studentDao;

    @Transactional
    public void changeInfo(){
    studentDao.updateAgeById(100,1);
    int i=1/0;//模拟出现异常,验证事务回滚
    System.out.println("-----------");
    studentDao.updateNameById("test1",1);
    }
    }
  3. 测试
    没有加@Transactional

    • 观察数据库发现:错误代码之前的sql执行成功了,修改了对应数据,而错误代码后的没有执行。
    • 控制台输出报错信息
    • 这会导致数据不一致的问题(比如说转账,转账者扣了钱,收款者未收到款)

    加了@Transactional

    • 观察数据库发现:数据库中的数据没有被修改,事务回滚了
    • 控制台输出报错信息

事务属性—只读

  1. 只读介绍:当这个操作不涉及写操作。将其设置为只读,这样数据库就能够针对查询操作来进行优化。
  2. 语法:

    • 在注解中添加属性:readOnly
    • @Transactional(readOnly = true)//只读事务
  3. 说明:如果我们给某个类上添加了@Transactional注解时,其下所有方法都会有事务,当其中的一些方法不涉及写操作时,我们就可以给这些方法上添加@Transactional(readOnly = true)注解

事务属性—超时

  1. 超时介绍:事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。概括即:超时回滚,释放资源

  2. 语法:

    • 在注解中添加属性:timeout
    • timeout的值:单位为秒,默认值为-1,表示永不超时
    • @Transactional(timeout = 3)
  3. 说明:当@Transactional(timeout = 3)加在了类上,而类下的方法也声明了@Transactional注解,但没有声明超时时,这时方法上的注解会覆盖类上的注解,该方法不会生效超时。

事务属性—事务异常

  1. 事务异常介绍:当我们加了@Transactional注解后,默认只针对运行时异常回滚,编译时异常不回滚。我们可以设置事务回滚的异常,也可以指定在发送某异常时不回滚

  2. 指定异常回滚
    语法:

    • 在注解中添加属性:
      • rollbackFor属性:指定哪些异常才会回滚,默认是 RuntimeException and Error 异常方可回滚!
      • @Transactional(rollbackFor = Exception.class)
      • 指定为Exception.class,表示所有异常都回滚
  3. 指定异常不回滚
    在默认设置和已有设置的基础上,再指定一个异常类型,碰到它不回滚。
    语法:

    • 在注解中添加属性:
    • noRollbackFor属性:指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!
    • @Transactional(rollbackFor = Exception.class,noRollbackFor = FileLockInterruptionException.class)
    • 指定为FileLockInterruptionException.class,在io操作找不到文件时回滚

事务属性—隔离级别

  1. 事务隔离级别
    数据库事务的隔离级别是指在多个事务并发执行时,数据库系统为了保证数据一致性所遵循的规定。常见的隔离级别包括:

    1. 读未提交(Read Uncommitted):事务可以读取未被提交的数据,容易产生脏读、不可重复读和幻读等问题。实现简单但不太安全,一般不用。
    2. 读已提交(Read Committed):事务只能读取已经提交的数据,可以避免脏读问题,但可能引发不可重复读和幻读。
    3. 可重复读(Repeatable Read):在一个事务中,相同的查询将返回相同的结果集,不管其他事务对数据做了什么修改。可以避免脏读和不可重复读,但仍有幻读的问题。
    4. 串行化(Serializable):最高的隔离级别,完全禁止了并发,只允许一个事务执行完毕之后才能执行另一个事务。可以避免以上所有问题,但效率较低,不适用于高并发场景。

    不同的隔离级别适用于不同的场景,需要根据实际业务需求进行选择和调整。一般推荐第二个级别

  2. 事务隔离级别设置
    语法:

    • 在注解中添加属性:isolation
    • @Transactional(isolation = Isolation.READ_COMMITTED),级别:读已提交

事务属性—传播行为

  1. propagation属性
    propagation 属性的可选值由 org.springframework.transaction.annotation.Propagation 枚举类提供:
名称 含义
REQUIRED 默认值 如果父方法有事务,就加入,如果没有就新建自己独立!
REQUIRES_NEW 不管父方法是否有事务,我都新建事务,都是独立的!
  1. 语法

    • 在注解中添加属性:propagation
    • @Transactional(propagation = Propagation.REQUIRES_NEW)
  2. 举例代码
    声明两个业务方法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    @Service
    public class StudentService {

    @Autowired
    private StudentDao studentDao;

    @Transactional(propagation = Propagation.REQUIRES_NEW)//自己独立为新事务
    public void changeInfo1(){
    studentDao.updateAgeById(100,1);
    System.out.println("-----------");
    studentDao.updateNameById("test1",1);
    }

    @Transactional(propagation = Propagation.REQUIRED)//默认,加入到外部事务
    public void changeInfo2(){
    studentDao.updateAgeById(21,3);
    System.out.println("-----------");
    studentDao.updateNameById("张老三",3);
    }
    }

    整合到外部事务

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    @Service
    public class TopService {
    @Autowired
    private StudentService studentService;

    @Transactional //外部事务整合两个事物
    public void changeInfo(){
    studentService.changeInfo1();
    studentService.changeInfo2();
    }
    }

如何理解?

  • 如果两个事务设置的默认值,则都加入外部的事务,当其中一个事务发生回滚,则另一个事务也会执行回滚
  • 如果其中一个事务设置的REQUIRES_NEW,则该事务视为一个独立的事务,不加入外部事物中,即使另一个事务发生了回滚,该事务不受干扰,继续正常执行
  1. 注意
    在同一个类中,对于@Transactional注解的方法调用,事务传播行为不会生效。这是因为Spring框架中使用代理模式实现了事务机制,在同一个类中的方法调用并不经过代理,而是通过对象的方法调用,因此@Transactional注解的设置不会被代理捕获,也就不会产生任何事务传播行为的效果