3.AOP
3.1 代理模式
代理模式(Proxy Pattern)是一种结构型设计模式,它提供了一种中介机制,允许在不改变目标对象的情况下,允许一个对象以间接的方式与另一个对象进行交互。这种模式常见于在不改变原系统的情况下,提供额外的操作或服务。代理模式在很多情况下都有应用,例如访问受限制的资源、控制访问权限、管理对象的生命周期等。
它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。
代理模式通常包含三种角色:
- 抽象主题(Subject):定义了一个接口,用于与被代理对象进行交互。
- 真实主题(Real Subject):实现了抽象主题接口,并负责与被代理对象进行交互。
- 代理对象(Proxy):也实现了抽象主题接口,并负责处理代理逻辑。它可以是实际主题的子类或委托类,也可以是第三方提供的代理。
代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。
目标:被代理“套用”了非核心逻辑代码的类、对象、方法。
在代理模式中,代理对象充当中介的角色,负责在需要时获取真实主题的对象并控制访问权限。它还可能提供额外的功能,如日志记录、安全控制或访问限制等。通过使用代理模式,可以实现灵活的权限控制、缓存机制、异常处理等功能,提高应用程序的性能和可靠性。
<img src="https://gitee.com/Lowell_37/picgoImg/raw/master/202312112025743.png" alt="image-20231108081456959" style="zoom: 33%;" />
<img src="https://gitee.com/Lowell_37/picgoImg/raw/master/202311080817883.png" alt="image-20231108081754733" style="zoom:33%;" />
代理模式通常有以下几种形式:
- 静态代理:代理类是预先定义好的,可以通过接口与被代理类进行交互。
- 动态代理:在运行时动态创建代理类,可以通过反射机制实现。动态代理通常用于实现 AOP,可以拦截目标对象的调用行为。
- 远程代理:用于代表远程对象,客户端通过代理对象与远程对象进行交互。
- 虚拟代理:只存储对目标对象的引用,但不存储实际对象,这样可以节省内存空间。
- 保护性代理:主要用于保护目标对象,限制其访问权限。
代理模式在Java中通常使用接口和继承来实现,但也可以使用代理库或框架来实现。Spring框架就是一个典型的例子,它提供了许多代理模式的应用,例如AOP(面向切面编程)中的前置通知、后置通知、异常处理、拦截器等,都是通过代理模式实现的。
3.2 示例
3.2.1 创建Calculator
接口并实现它
public class CalculatorImpl implements Calculator{
@Override
public int add(int i, int j) {
int result = i + j;
System.out.println("方法内部, result:"+ result);
return result;
}
@Override
public int sub(int i, int j) {
int result = i - j;
System.out.println("方法内部, result:"+ result);
return result;
}
@Override
public int mul(int i, int j) {
int result = i * j;
System.out.println("方法内部, result:"+ result);
return result;
}
@Override
public int div(int i, int j) {
int result = i / j;
System.out.println("方法内部, result:"+ result);
return result;
}
}
3.2.2 创建带日志功能的实现类
public class CalculatorLogImpl implements Calculator {
@Override
public int add(int i, int j) {
System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
int result = i + j;
System.out.println("方法内部 result = " + result);
System.out.println("[日志] add 方法结束了,结果是:" + result);
return result;
}
@Override
public int sub(int i, int j) {
System.out.println("[日志] sub 方法开始了,参数是:" + i + "," + j);
int result = i - j;
System.out.println("方法内部 result = " + result);
System.out.println("[日志] sub 方法结束了,结果是:" + result);
return result;
}
@Override
public int mul(int i, int j) {
System.out.println("[日志] mul 方法开始了,参数是:" + i + "," + j);
int result = i * j;
System.out.println("方法内部 result = " + result);
System.out.println("[日志] mul 方法结束了,结果是:" + result);
return result;
}
@Override
public int div(int i, int j) {
System.out.println("[日志] div 方法开始了,参数是:" + i + "," + j);
int result = i / j;
System.out.println("方法内部 result = " + result);
System.out.println("[日志] div 方法结束了,结果是:" + result);
return result;
}
}
3.2.3 现存问题
①现有代码缺陷
- 针对带日志功能的实现类,我们发现有如下缺陷: 对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力
- 附加功能分散在各个业务功能方法中,不利于统一维护
②解决思路
解决这两个问题,核心就是:解耦。我们需要把附加功能从业务功能代码中抽取出来。
③困难
解决问题的困难:要抽取的代码在方法内部,靠以前把子类中的重复代码抽取到父类的方式没法解决。 所以需要引入新的技术。
3.2.4 静态代理
public class CalculatorProxy implements Calculator{
private CalculatorImpl target;
public CalculatorProxy(CalculatorImpl target) {
this.target = target;
}
@Override
public int add(int i, int j) {
System.out.println("日志,方法:add,参数:"+ i + "," + j);
int result = target.add(i, j);
System.out.println("日志,方法:add,结果:"+result);
return result;
}
@Override
public int sub(int i, int j) {
System.out.println("日志,方法:sub,参数:"+ i + "," + j);
int result = target.sub(i, j);
System.out.println("日志,方法:sub,结果:"+result);
return result;
}
@Override
public int mul(int i, int j) {
System.out.println("日志,方法:mul,参数:"+ i + "," + j);
int result = target.mul(i, j);
System.out.println("日志,方法:mul,结果:"+result);
return result;
}
@Override
public int div(int i, int j) {
System.out.println("日志,方法:div,参数:"+ i + "," + j);
int result = target.div(i, j);
System.out.println("日志,方法:div,结果:"+result);
return result;
}
}
静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理。
提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理 类来实现。这就需要使用动态代理技术了。
3..2.5 动态代理
<img src="https://gitee.com/Lowell_37/picgoImg/raw/master/202311080836501.png" alt="image-20231108083654326" style="zoom: 50%;" />
生产代理对象的工厂类:
public class ProxyFactory {
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
public Object getProxy() {
/**
* newProxyInstance():创建一个代理实例
* 其中有三个参数:
* 1、classLoader:加载动态生成的代理类的类加载器
* 2、interfaces:目标对象实现的所有接口的class对象所组成的数组
* 3、invocationHandler:设置代理对象实现目标对象方法的过程,即代理类中如何重写接
口中的抽象方法
*/
ClassLoader classLoader = this.getClass().getClassLoader();
Class<?>[] interfaces = target.getClass().getInterfaces();
InvocationHandler h = new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
try {
System.out.println("日志,方法" + method.getName()+ ",参数:"+ Arrays.toString(args));
// proxy表示代理对象,method表示要执行的方法,args表示要执行的方法到的参数列表
result = method.invoke(target, args);
System.out.println("日志,方法" + method.getName()+ ",结果:"+ result);
} catch (Exception e) {
e.printStackTrace();
System.out.println("日志,方法" + method.getName()+ ",异常:"+ e);
} finally {
System.out.println("日志,方法" + method.getName()+ ",方法执行完毕");
}
return result;
}
};
return Proxy.newProxyInstance(classLoader, interfaces, h);
}
}
3.2.6 测试
@Test
public void testProxy() {
/*CalculatorProxy proxy = new CalculatorProxy(new CalculatorImpl());
proxy.add(1, 2);*/
ProxyFactory proxyFactory = new ProxyFactory(new CalculatorImpl());
Calculator proxy = (Calculator) proxyFactory.getProxy();
proxy.add(1, 2);
}
日志,方法add,参数:[1, 2]
方法内部, result:3
日志,方法add,结果:3
日志,方法add,方法执行完毕
3.3 AOP概念
在 AOP(面向切面编程)中,代理模式是一种重要的技术手段,它用于拦截、修改、扩展目标对象的行为。在 AOP 中,代理对象通常称为 Advice(通知),它可以是**前置通知(前置通知在方法执行前执行)、后置通知(在方法执行后执行)、返回通知(在方法返回后执行)、异常通知(在方法抛出异常时执行)**等。
AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程的一种补充和完善,它以通过预编译方式和运行期动态代理方式实现在不修改源代码的情况 下给程序动态统一添加额外功能的一种技术
通过使用AOP,开发人员可以将关注点分离,使代码更加模块化,提高可维护性和可扩展性。
AOP的实现通常使用代理模式,即将被代理对象与代理对象关联起来,代理对象负责拦截目标方法的调用,并在方法执行前后添加额外的逻辑。代理对象可以是实际对象的子类或委托类,也可以是第三方提供的代理。
①横切关注点
从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方 法进行多个不同方面的增强。 这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。
②通知
每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。
- 前置通知:在被代理的目标方法前执行
- 返回通知:在被代理的目标方法成功结束后执行(寿终正寝)
- 异常通知:在被代理的目标方法异常结束后执行(死于非命)
- 后置通知:在被代理的目标方法最终结束后执行(盖棺定论)
- 环绕通知:使用
try...catch...finally
结构围绕整个被代理的目标方法,包括上面四种通知对应的所 有位置
<img src="https://gitee.com/Lowell_37/picgoImg/raw/master/202311080913517.png" alt="image-20231108091311314" style="zoom:50%;" />
③切面
封装通知方法的类。
<img src="https://gitee.com/Lowell_37/picgoImg/raw/master/202311080914334.png" alt="image-20231108091436151" style="zoom:50%;" />
④目标
被代理的目标对象。
⑤代理
向目标对象应用通知之后创建的代理对象。
⑥连接点
这也是一个纯逻辑概念,不是语法定义的。 把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉 点就是连接点。
<img src="https://gitee.com/Lowell_37/picgoImg/raw/master/202311080916862.png" alt="image-20231108091608300" style="zoom:50%;" />
⑦切入点
定位连接点的方式。
每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。
如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句。 Spring 的 AOP 技术可以通过切入点定位到特定的连接点。
切点通过 org.springframework.aop.Pointcut
接口进行描述,它使用类和方法作为连接点的查询条 件。
作用
简化代码:把方法中固定位置的重复的代码抽取出来,让被抽取的方法更专注于自己的核心功能, 提高内聚性。
代码增强:把特定的功能封装到切面类中,看哪里有需要,就往上套,被套用了切面逻辑的方法就 被切面给增强了。
3.4 基于注解的AOP
3.4.1 技术介绍
<img src="https://gitee.com/Lowell_37/picgoImg/raw/master/202311080923713.png" alt="image-20231108092312532" style="zoom:50%;" />
- 动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)。
- cglib:通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口。
- AspectJ:本质上是静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver就是织入器。Spring只是借用了AspectJ中的注解。
3.4.2 示例
添加依赖
<!-- spring-aspects会帮我们传递过来aspectjweaver -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.1</version>
</dependency>
准备被代理的目标资源
calculator
接口与实现类,同上
创建切面类并配置
package com.lowell.spring.aop.annotation;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
import java.util.Arrays;
/**
* @Date:2023/10/22 10:09
* @Author:Lowell
* @Description:
* 在切面中需要通过指定的注解方法标识为通知方法
* @Before: 前置通知,在目标对象方法执行之前执行
* @After: 后置通知,在目标对象的finally子句中执行
* @AfterReturning: 返回通知,在目标对象的返回值之后执行
* @AfterThrowing: 异常通知,在目标对象方法的catch子句中执行
*
* 2、切入点表达式:设置在标识通知的注解的value属性中
* execution(public int com.lowell.spring.aop.annotation.CalculatorImpl.add(int, int))
* execution(* com.lowell.spring.aop.annotation.CalculatorImpl.*(..))
* 第一个 * 表示任意的访问修饰符返回值类型
* 第二个 * 表示类中任意的方法
* .. 表示任意的参数列表
* 类的地方也可以使用 * ,表示包下所有的类
*
* 3、重用切入点表达式
* // @PointCut声明一个公共的切入点表达式
* @Pointcut("execution(* com.lowell.spring.aop.annotation.CalculatorImpl.*(..))")
* public void pointCut(){}
* 使用方式:@Before("pointCut()")
*
*
*
* 4、获取连接点的信息
* 在通知方法的参数位置,设置JoinPoint类型的参数,就可以获取父结点所对应方法的信息
* // 获取连接点所对应方法的签名信息
* Signature signature = joinPoint.getSignature();
* // 获取连接点所对应方法的参数
* Object[] args = joinPoint.getArgs();
*
* 5、切面的优先级
* 可以通过@Order注解的value属性设置优先级,默认值Integer的最大值
* @Order注解的value属性值越小,优先级越高
**/
@Component
@Aspect // 将当前组件标识为切面
public class LoggerAspect {
// @PointCut声明一个公共的切入点表达式
@Pointcut("execution(* com.lowell.spring.aop.annotation.CalculatorImpl.*(..))")
public void pointCut(){}
// 将当前方法标识为前置通知
// @Before("execution(public int com.lowell.spring.aop.annotation.CalculatorImpl.add(int, int))")
// *表示当前包
@Before("pointCut()")
public void beforeAdviceMethod(JoinPoint joinPoint) {
// 获取连接点所对应方法的签名信息
Signature signature = joinPoint.getSignature();
// 获取连接点所对应方法的参数
// Object[]输出的为内存地址,需要使用Arrays.toString(args)转换
Object[] args = joinPoint.getArgs();
System.out.println("LoggerAspect, 方法:" + signature.getName()+ ", 参数" + Arrays.toString(args));
}
@After("pointCut()")
public void afterAdviceMethod(JoinPoint joinPoint){
// 获取连接点所对应方法的签名信息
Signature signature = joinPoint.getSignature();
System.out.println("LoggerAspect, 方法:" + signature.getName()+ ", 执行完毕");
}
/**
* 在返回通知中要想获取目标对象方法的返回值,
* 只需要通过@AfterReturning注解的returning属性,
* 就可以将通知方法的某个参数指定为接收目标对象方法的返回值的参数
* @param joinPoint
* @param result
*/
@AfterReturning(value = "pointCut()", returning = "result")
public void afterReturningAdviceMethod(JoinPoint joinPoint, Object result) {
// 获取连接点所对应方法的签名信息
Signature signature = joinPoint.getSignature();
System.out.println("LoggerAspect, 方法:" + signature.getName()+ ", 结果: " + result);
}
/**
* 在异常通知中要想获取目标对象方法的异常
* 需要通过@AfterThrowing注解的throwing属性,
* 就可以将通知方法的某个参数指定为接收目标对象方法出现的异常的参数
* @param joinPoint
* @param exception
*/
@AfterThrowing(value = "pointCut()", throwing = "exception")
public void afterThrowingAdviceMethod(JoinPoint joinPoint, Throwable exception) {
// 获取连接点所对应方法的签名信息
Signature signature = joinPoint.getSignature();
System.out.println("LoggerAspect, 方法: " + signature.getName() + ", 异常: " + exception);
}
@Around("pointCut()")
public Object aroundAdviceMethod(ProceedingJoinPoint joinPoint) {
Object result = null;
try {
System.out.println("环绕通知-->前置通知");
// 表示目标对象方法的执行
result = joinPoint.proceed();
System.out.println("环绕通知-->返回通知");
} catch (Throwable e) {
e.printStackTrace();
System.out.println("环绕通知-->异常通知");
} finally {
System.out.println("环绕通知-->后置通知");
}
return result;
}
}
在Spring配置文件中配置
<!--
AOP的注意事项:
切面类和目标类都需要交给IOC容器管理
切面类必须通过@Aspect注解标识为一个切面
在Spring的配置文件中设置<aop:aspectj-autoproxy/>开启基于注解的AOP
-->
<context:component-scan base-package="com.lowell.spring.aop.annotation"/>
<!--开启基于注解的AOP-->
<aop:aspectj-autoproxy/>
测试
public class AOPByAnnotationTest {
@Test
public void testAOPByAnnotation() {
ApplicationContext ioc = new ClassPathXmlApplicationContext("aop-annotation.xml");
// 获取的是代理对象
Calculator calculator = ioc.getBean(Calculator.class);
calculator.add(10, 1);
}
}
ValidateAspect-->前置通知
环绕通知-->前置通知
LoggerAspect, 方法:add, 参数[10, 1]
方法内部, result:11
LoggerAspect, 方法:add, 结果: 11
LoggerAspect, 方法:add, 执行完毕
环绕通知-->返回通知
环绕通知-->后置通知
3.5 基于XML的AOP
<!--扫描组件-->
<context:component-scan base-package="com.lowell.spring.aop.xml"/>
<aop:config>
<!--设置一个公共点的切入点表达式-->
<aop:pointcut id="pointCut" expression="execution(* com.lowell.spring.aop.xml.CalculatorImpl.*(..))"/>
<!--将IOC容器中的某个bean设置为切面-->
<aop:aspect ref="loggerAspect">
<aop:before method="beforeAdviceMethod" pointcut-ref="pointCut"/>
<aop:after method="afterAdviceMethod" pointcut-ref="pointCut"/>
<aop:after-returning method="afterReturningAdviceMethod" returning="result" pointcut-ref="pointCut"/>
<aop:after-throwing method="afterThrowingAdviceMethod" throwing="exception" pointcut-ref="pointCut"/>
<aop:around method="aroundAdviceMethod" pointcut-ref="pointCut"/>
</aop:aspect>
<aop:aspect ref="validateAspect" order="1">
<aop:before method="beforeMethod" pointcut-ref="pointCut"/>
</aop:aspect>
</aop:config>
4.声明式事务
4.1JdbcTemplate
Spring 框架对 JDBC
进行封装,使用 JdbcTemplate
方便实现对数据库操作
4.1.2 添加依赖
<dependencies>
<!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.1</version>
</dependency>
<!-- Spring 持久化层支持jar包 -->
<!-- Spring 在执行持久化层操作、与持久化层技术进行整合过程中,需要使用orm、jdbc、tx三个
jar包 -->
<!-- 导入 orm 包就可以通过 Maven 的依赖传递性把其他两个也导入 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-orm</artifactId>
<version>5.3.1</version>
</dependency>
<!-- Spring 测试相关 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.1</version>
</dependency>
<!-- junit测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.1</version>
<scope>test</scope>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version>
</dependency>
<!-- 数据源 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.31</version>
</dependency>
</dependencies>
4.1.3 创建jdbc.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/ssm?characterencoding=utf-8
jdbc.username=root
jdbc.password=******
4.1.4 配置Spring的配置文件
<!--引入jdbc.properties-->
<context:property-placeholder location="jdbc.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<bean class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"/>
</bean>
4.1.5 测试
在测试类装配JdbcTemplate
// 指定当前测试类在Spring的测试环境中执行,此时就可以通过注入的方式直接获取IOC容器的bean
@RunWith(SpringJUnit4ClassRunner.class)
// 设置Spring测试环境的配置文件
@ContextConfiguration("classpath:spring-jdbc.xml")
public class JdbcTemplateTest {
@Autowired
private JdbcTemplate jdbcTemplate;
}
测试增删改功能
@Test
public void testInsert() {
String sql = "insert into t_user values(null, ?, ?, ?, ?, ?)";
jdbcTemplate.update(sql, "root", "123", 23, "女", "123456@qq.com");
}
@Test
public void testGetUserById() {
String sql = "select * from t_user where id = ?";
User user = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(User.class),1);
System.out.println("user = " + user);
}
@Test
public void testGetAllUser() {
String sql = "select * from t_user";
List<User> userList = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
System.out.println("userList = " + userList);
}
@Test
public void testGetCount() {
String sql = "select count(*) from t_user";
Integer count = jdbcTemplate.queryForObject(sql, Integer.class);
System.out.println("count = " + count);
}
4.2 声明式事务概念
4.2.1 编程式事务
事务功能的相关操作全部通过自己编写代码来实现
编程式的实现方式存在缺陷:
- 细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
- 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。
4.2.2 声明式事务
既然事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装。
封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作。
- 好处1:提高开发效率
- 好处2:消除了冗余的代码
- 好处3:框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性、性 能等各个方面的优化
编程式:自己写代码实现功能
声明式:通过配置让框架实现功能
4.3 基于注解的声明式事务
4.3.1 创建表
<img src="https://gitee.com/Lowell_37/picgoImg/raw/master/202311091510490.png" alt="image-20231109150258961" style="zoom:50%;" />
<img src="https://gitee.com/Lowell_37/picgoImg/raw/master/202311091510593.png" alt="image-20231109150323073" style="zoom:50%;" />
4.3.2 创建组件
@Controller
public class BookController {
@Autowired
private BookService bookService;
@Autowired
private CheckOutService checkOutService;
public void buyBook(Integer userId, Integer bookId) {
bookService.buyBook(userId, bookId);
}
public void checkout(Integer userId, Integer[] bookIds) {
checkOutService.checkout(userId, bookIds);
}
}
public interface BookDao {
/**
* 根据图书的Id查询图书的价格
* @param bookId
* @return
*/
Integer getPriceByBookId(Integer bookId);
/**
* 更新图书的库存
* @param bookId
*/
void updateStock(Integer bookId);
/**
* 更新用户的余额
* @param userId
* @param price
*/
void updateBalance(Integer userId, Integer price);
}
@Repository
public class BookDaoImpl implements BookDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public Integer getPriceByBookId(Integer bookId) {
String sql = "select price from t_book where book_id = ?";
return jdbcTemplate.queryForObject(sql, Integer.class, bookId);
}
@Override
public void updateStock(Integer bookId) {
String sql = "update t_book set stock = stock - 1 where book_id = ?";
jdbcTemplate.update(sql, bookId);
}
@Override
public void updateBalance(Integer userId, Integer price) {
String sql = "update t_user1 set balance = balance - ? where user_id = ?";
jdbcTemplate.update(sql, price, userId);
}
}
public interface BookService {
void buyBook(Integer userId, Integer bookId);
}
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
@Override
public void buyBook(Integer userId, Integer bookId) {
// 查询图书的价格
Integer price = bookDao.getPriceByBookId(bookId);
// 更新图书的库存
bookDao.updateStock(bookId);
// 更新用户的余额
bookDao.updateBalance(userId, price);
}
}
4.3.3 测试无事务情况
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:tx-annotation.xml")
public class TxByAnnotationTest {
@Autowired
private BookController bookController;
@Test
public void testBuyBook() {
bookController.buyBook(1,1);
}
}
用户购买图书,先查询图书的价格,再更新图书的库存和用户的余额
假设用户id为1的用户,购买id为1的图书 用户余额为50,而图书价格为80 购买图书之后,用户的余额为-30,数据库中余额字段设置了无符号,因此无法将-30插入到余额字段 此时执行sql语句会抛出SQLException
因为没有添加事务,图书的库存更新了,但是用户的余额没有更新 显然这样的结果是错误的,购买图书是一个完整的功能,更新库存和更新余额要么都成功要么都失败
org.springframework.dao.DataIntegrityViolationException: PreparedStatementCallback; SQL [update t_user1 set balance = balance - ? where user_id = ?]; Data truncation: BIGINT UNSIGNED value is out of range in '(`ssm`.`t_user1`.`balance` - 80)'; nested exception is com.mysql.jdbc.MysqlDataTruncation: Data truncation: BIGINT UNSIGNED value is out of range in '(`ssm`.`t_user1`.`balance` - 80)'
4.3.4 加入事务
在Spring的配置文件中添加配置
<!--配置事务管理器-->
<!--这里便是一个切面-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!--
开启事务的注解驱动
使用@Transactional注解所标识的方法或则类中所有的方法使用事务进行管理
transaction-manager属性设置事务管理器的id
若事务管理器的bean的id默认为transactionManager,则该属性可以不写
-->
<tx:annotation-driven transaction-manager="transactionManager"/>
<img src="https://gitee.com/Lowell_37/picgoImg/raw/master/202312112026490.png" alt="image-20231109153543681" style="zoom:80%;" />
添加事务注解
因为service层表示业务逻辑层,一个方法表示一个完成的功能,因此处理事务一般在service
层处理 在BookServiceImpl的buybook()
添加注解@Transactional
观察结果
由于使用了Spring的声明式事务,更新库存和更新余额都没有执行
4.3.5@Transactional
注解标识的位置
@Transactional
标识在方法上,只会影响该方法
@Transactional
标识的类上,影响类中所有的方法
4.3.6 事务属性
只读
对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这 样数据库就能够针对查询操作来进行优化。
使用方式
@Transactional(readOnly = true)
public void buyBook(Integer bookId, Integer userId) {
//查询图书的价格
Integer price = bookDao.getPriceByBookId(bookId);
//更新图书的库存
bookDao.updateStock(bookId);
//更新用户的余额
bookDao.updateBalance(userId, price);
}
对增删改操作设置只读会抛出下面异常: Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
超时
事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间 占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。 此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常 程序可以执行。
概括来说就是一句话:超时回滚,释放资源。
使用方式
@Transactional(timeout = 3)
public void buyBook(Integer bookId, Integer userId) {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
//查询图书的价格
Integer price = bookDao.getPriceByBookId(bookId);
//更新图书的库存
bookDao.updateStock(bookId);
//更新用户的余额
bookDao.updateBalance(userId, price);
//System.out.println(1/0);
}
回滚策略
声明式事务默认只针对运行时异常回滚,编译时异常不回滚。
可以通过@Transactional
中相关属性设置回滚策略
rollbackFor
属性:需要设置一个Class类型的对象rollbackForClassName
属性:需要设置一个字符串类型的全类名noRollbackFor
属性:需要设置一个Class类型的对象rollbackFor
属性:需要设置一个字符串类型的全类名
@Transactional(noRollbackFor = ArithmeticException.class)
//@Transactional(noRollbackForClassName = "java.lang.ArithmeticException")
public void buyBook(Integer bookId, Integer userId) {
//查询图书的价格
Integer price = bookDao.getPriceByBookId(bookId);
//更新图书的库存
bookDao.updateStock(bookId);
//更新用户的余额
bookDao.updateBalance(userId, price);
System.out.println(1/0);
}
事务隔离级别
数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事 务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同 的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。
隔离级别一共有四种:
-
读未提交:
READ UNCOMMITTED
允许Transaction01读取Transaction02未提交的修改。
-
读已提交:
READ COMMITTED
要求Transaction01只能读取Transaction02已提交的修改。
-
可重复读:
REPEATABLE READ
确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它 事务对这个字段进行更新。
-
串行化:
SERIALIZABLE
确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它 事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。
@Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别
@Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交
@Transactional(isolation = Isolation.READ_COMMITTED)//读已提交
@Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读
@Transactional(isolation = Isolation.SERIALIZABLE)//串行化
事务传播行为
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中 运行,也可能开启一个新事务,并在自己的事务中运行。
测试
创建接口CheckoutService:
public interface CheckoutService {
void checkout(Integer[] bookIds, Integer userId);
}
创建实现类CheckoutServiceImpl:
@Service
public class CheckoutServiceImpl implements CheckoutService {
@Autowired
private BookService bookService;
@Override
@Transactional
//一次购买多本图书
public void checkout(Integer[] bookIds, Integer userId) {
for (Integer bookId : bookIds) {
bookService.buyBook(bookId, userId);
}
}
}
在BookController中添加方法:
@Autowired
private CheckoutService checkoutService;
public void checkout(Integer[] bookIds, Integer userId){
checkoutService.checkout(bookIds, userId);
}
观察结果
可以通过@Transactional
中的propagation
属性设置事务传播行为
修改BookServiceImpl中buyBook()
上,注解@Transactional的propagation属性
@Transactional(propagation = Propagation.REQUIRED)
,默认情况,表示如果当前线程上有已经开 启的事务可用,那么就在这个事务中运行。
经过观察,购买图书的方法buyBook()
在checkout()
中被调 用,checkout()
上有事务注解,因此在此事务中执行。所购买的两本图书的价格为80和50,而用户的余 额为100,因此在购买第二本图书时余额不足失败,导致整个checkout()回滚,即只要有一本书买不了,就都买不了
@Transactional(propagation = Propagation.REQUIRES_NEW)
,表示不管当前线程上是否有已经开启 的事务,都要开启新事务。同样的场景,每次购买图书都是在buyBook()的事务中执行,因此第一本图 书购买成功,事务结束,第二本图书购买失败,只在第二次的buyBook()中回滚,购买第一本图书不受 影响,即能买几本就买几本
4.4 基于XML的声明式事务
<aop:config>
<!-- 配置事务通知和切入点表达式 -->
<aop:advisor advice-ref="txAdvice" pointcut="execution(*
com.atguigu.spring.tx.xml.service.impl.*.*(..))"></aop:advisor>
</aop:config>
<!-- tx:advice标签:配置事务通知 -->
<!-- id属性:给事务通知标签设置唯一标识,便于引用 -->
<!-- transaction-manager属性:关联事务管理器 -->
<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- tx:method标签:配置具体的事务方法 -->
<!-- name属性:指定方法名,可以使用星号代表多个字符 -->
<tx:method name="get*" read-only="true"/>
<tx:method name="query*" read-only="true"/>
<tx:method name="find*" read-only="true"/>
<!-- read-only属性:设置只读属性 -->
<!-- rollback-for属性:设置回滚的异常 -->
<!-- no-rollback-for属性:设置不回滚的异常 -->
<!-- isolation属性:设置事务的隔离级别 -->
<!-- timeout属性:设置事务的超时属性 -->
<!-- propagation属性:设置事务的传播行为 -->
<tx:method name="save*" read-only="false" rollbackfor="java.lang.Exception" propagation="REQUIRES_NEW"/>
<tx:method name="update*" read-only="false" rollbackfor="java.lang.Exception" propagation="REQUIRES_NEW"/>
<tx:method name="delete*" read-only="false" rollbackfor="java.lang.Exception" propagation="REQUIRES_NEW"/>
</tx:attributes>
</tx:advice>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.1</version>
</dependency>