五、Spring AOP面向切面编程
1、场景设定和问题复现
①准备AOP项目
项目名:Spring-aop-annotation
②声明接口
/**
* + - * / 运算的标准接口!
*/
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);
}
③实现接口
/**
* 实现计算接口,单纯添加 + - * / 实现! 掺杂其他功能!
*/
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;
}
}
④声明带日志接口实现
新需求:需要在每一个方法中,添加控制台输出,输出参数和输出计算后的返回值!
/**
* 在每个方法中,输出传入的参数和计算后的返回结果!
*/
public class CalculatorLogImpl implements Calculator {
@Override
public int add(int i, int j) {
System.out.println("参数是:" + i + "," + j);
int result = i + j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int sub(int i, int j) {
System.out.println("参数是:" + i + "," + j);
int result = i - j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int mul(int i, int j) {
System.out.println("参数是:" + i + "," + j);
int result = i * j;
System.out.println("方法内部 result = " + result);
return result;
}
@Override
public int div(int i, int j) {
System.out.println("参数是:" + i + "," + j);
int result = i / j;
System.out.println("方法内部 result = " + result);
return result;
}
}
⑤代码问题分析
1)代码缺陷
- 对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力
- 附加功能代码重复,分散在各个业务功能方法中,冗余,且不方便统一维护!
2)解决问题
- 核心就是:解耦。我们需要把附加功能从业务功能代码中抽取出来。将重复的代码统一提取,并且【动态插入】到每个业务方法!
3)技术困难
- 解决问题的困难:提取重复附加功能代码到一个类中,可以实现但是如何将代码插入到各个方法中?我们不会,我们需要引用新技术!!!
2、解决技术代理模式
① 代理模式
23种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直视对目标方法进行调用,而是通过代理类间调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。
代理场景:
- 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。(中介)
- 动词:指做代理这个动作,或这项工作
- 名词:扮演代理这个角色的类、对象、方法
- 目标:被代理“套用”了非核心逻辑代码的类、对象、方法。代理在开发中实现的方式具体有两种:静态代理,动态代理。
② 静态代理
主动创建代理类:
public class CalculatorStaticProxy implements Calculator {
// 将被代理的目标对象声明为成员变量
private Calculator target;
public CalculatorStaticProxy(Calculator target) {
this.target = target;
}
@Override
public int add(int i, int j) {
// 附加功能由代理类中的代理方法来实现
System.out.println("参数是:" + i + "," + j);
// 通过目标对象来实现核心业务逻辑
int addResult = target.add(i, j);
System.out.println("方法内部 result = " + result);
return addResult;
}
……
静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来说,将其他地方也需要附加日志,那还得再声明更多个静态代理类,那就生产了大量重复代码,日志功能还是分散的,没有统一管理。
③ 动态代理
- JDK动态代理:JDK原生的实现方式,需要被代理的目标类必须实现接口!它会根据目标类的接口动态生成一个代理对象!代理对象和目标对象有相同的接口!(拜把子)
- cglib:通过继承被代理的目标类实现代理,所以不需要目标类实现接口!(认干爹)
代理工程:基于jdk代理技术,生成代理对象
public class ProxyFactory<T> {
//目标对象。具体什么类型由调用者指定
private T target;
public ProxyFactory(T target) {
this.target = target;
}
public T getProxy(){
//1、获取目标对象的类加载器
ClassLoader classLoader = target.getClass().getClassLoader();
//2、获取目标对象所实现的所有对象
Class<?>[] interfaces = target.getClass().getInterfaces();
//1、JDK动态代理方式:
T o = (T)Proxy.newProxyInstance(classLoader, interfaces, new MyInvocationHandler());
return o;
}
public class MyInvocationHandler implements InvocationHandler{
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//反射角度:method是目标方法,args是目标方法执行时所需要的参数。
System.out.println("日志功能,"+method.getName()+"方法执行了,参数为:"+ Arrays.toString(args));
//核心:目标方法
Object result = method.invoke(target, args);
System.out.println("日志功能,"+method.getName()+"方法执行结束,结果为:"+ result);
return result;
}
}
}
④ 代理总结
代理方式可以解决附加功能代码干扰核心代码和不方便统一维护的问题!
它主要是将附加功能代码提取到代理中执行,不干扰目标核心代码。但是我们也发现,无论使用静态代理和动态代理,程序员的工作都比较繁琐,需要自己编写代理工厂等。但是,提前剧透,我们在实际开发中,不需要编写代理代码,我们可以使用SpringAOP框架,它会简化代理的实现!
3、面向切面编程思维(AOP)
1)面向切面编程思想AOP
AOP:Aspect Oriented Programming 面向切面编程
AOP可以说是OOP的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都如此,这种散布在各处的无关的代码被称为横切,在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
AOP技术恰恰相反,它利用一种称为”横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为“Aspect”,即切面。所谓“切面”,简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
使用AOP,可以在不修改原来代码的基础上添加新功能。
2)AOP思想主要的应用场景
AOP(面向切面编程)是一种编程范式,它通过将通用的横切关注点(如日志、事务、权限控制等)与业务逻辑分离,使得代码更加清晰、简洁、易于维护。
应用场景:
日志记录、事务处理、安全控制、性能监控、异常处理、缓存控制、动态代理
3)AOP术语
①横切关注点
②通知(增强)
③切入点-pointcut
④切面-aspect
⑤目标-target
⑥代理-proxy
⑦织入-weave
4、Spring AOP框架介绍和关系梳理
- AOP一种区别于OOP的编程思维,用来完善和解决OOP的非核心代码冗余和不方便统一维护问题。
- 代理技术(动态代理 | 静态代理)是实现AOP思维编程的具体技术,但是自己使用动态代理实现代码比较繁琐。
- Spring AOP框架,基于AOP编程思维,封装动态代理技术,简化动态代理技术实现的框架,SpringAOP内部帮助我们实现动态代理,我们只需写少量的配置,指定生效范围即可完成面向切面思维编程的实现。
5、Spring AOP基于注解方式实现和细节
1)Spring AOP底层技术组成
- 动态代理:JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)
- cglib:通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标实现接口。
- Aspectj:早期的AOP实现的框架,SpringAOP借用了Aspectj中的AOP注解。
2)初步实现
①加入依赖
<!-- spring-aspects会帮我们传递过来aspectjweaver -->
<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>
②准备接口
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);
}
③纯净实现类
@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;
}
}
```
④声明切面类
// @Aspect表示这个类是一个切面类
@Aspect
// @Component注解保证这个切面类能够放入IOC容器
@Component
public class LogAspect {
// @Before注解:声明当前方法是前置通知方法
// value属性:指定切入点表达式,由切入点表达式控制当前通知方法要作用在哪一个目标方法上
@Before(value = "execution(public int com.atguigu.proxy.CalculatorPureImpl.add(int,int))")
public void printLogBeforeCore() {
System.out.println("[AOP前置通知] 方法开始了");
}
@AfterReturning(value = "execution(public int com.atguigu.proxy.CalculatorPureImpl.add(int,int))")
public void printLogAfterSuccess() {
System.out.println("[AOP返回通知] 方法成功返回了");
}
@AfterThrowing(value = "execution(public int com.atguigu.proxy.CalculatorPureImpl.add(int,int))")
public void printLogAfterException() {
System.out.println("[AOP异常通知] 方法抛异常了");
}
@After(value = "execution(public int com.atguigu.proxy.CalculatorPureImpl.add(int,int))")
public void printLogFinallyEnd() {
System.out.println("[AOP后置通知] 方法最终结束了");
}
}
⑤开启aspectj注解支持
a. xml方式
<?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="com.atguigu" />
<!-- 开启aspectj框架注解支持-->
<aop:aspectj-autoproxy />
</beans>
b. 配置类方式
@Configuration
@ComponentScan(basePackages = "com.atguigu")
//作用等于 <aop:aspectj-autoproxy /> 配置类上开启 Aspectj注解支持!
@EnableAspectJAutoProxy
public class MyConfig {
}
⑥测试效果
//@SpringJUnitConfig(locations = "classpath:spring-aop.xml")
@SpringJUnitConfig(value = {MyConfig.class})
public class AopTest {
@Autowired
private Calculator calculator;
@Test
public void testCalculator(){
calculator.add(1,1);
}
}
注意:
// @Autowired Spring 知道目标已被代理,所以被保护起来了
// private CalculatorPureImpl calculatorPure;
输出结果:
[AOP前置通知] 方法开始了
[AOP返回通知] 方法成功返回了
[AOP后置通知] 方法最终结束了
3)获取通知细节信息
①JoinPoint 接口
需要获取方法签名、传入的实参等信息时,可以在通知方法声明 JoinPoint 类型的形参。
- 要点1:JoinPoint 接口通过 getSignature() 方法获取目标方法的签名(方法声明时的完整信息)
- 要点2:通过目标方法签名对象获取方法名
- 要点3:通过 JoinPoint 对象获取外界调用目标放法时传入的实参列表组成的数组
// @Before注解标记前置通知方法
// value属性:切入点表达式,告诉Spring当前通知方法要套用到哪个目标方法上
// 在前置通知方法形参位置声明一个JoinPoint类型的参数,Spring就会将这个对象传入
// 根据JoinPoint对象就可以获取目标方法名称、实际参数列表
@Before(value = "execution(public int com.atguigu.aop.api.Calculator.add(int,int))")
public void printLogBeforeCore(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);
}
②方法返回值
在返回通知中,通过 @AfterReturning 注解的 returning 属性获取目标方法的返回值
// @AfterReturning注解标记返回通知方法
// 在返回通知中获取目标方法返回值分两步:
// 第一步:在@AfterReturning注解中通过returning属性设置一个名称
// 第二步:使用returning属性设置的名称在通知方法中声明一个对应的形参
@AfterReturning(
value = "execution(public int com.atguigu.aop.api.Calculator.add(int,int))",
returning = "targetMethodReturnValue"
)
public void printLogAfterCoreSuccess(JoinPoint joinPoint, Object targetMethodReturnValue) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[AOP返回通知] "+methodName+"方法成功结束了,返回值是:" + targetMethodReturnValue);
}
③异常对象捕捉
在异常通知中,通过 @AfterThrowing 注解的 throwing 属性获取目标方法抛出的异常对象
// @AfterThrowing注解标记异常通知方法
// 在异常通知中获取目标方法抛出的异常分两步:
// 第一步:在@AfterThrowing注解中声明一个throwing属性设定形参名称
// 第二步:使用throwing属性指定的名称在通知方法声明形参,Spring会将目标方法抛出的异常对象从这里传给我们
@AfterThrowing(
value = "execution(public int com.atguigu.aop.api.Calculator.add(int,int))",
throwing = "targetMethodException"
)
public void printLogAfterCoreException(JoinPoint joinPoint, Throwable targetMethodException) {
String methodName = joinPoint.getSignature().getName();
System.out.println("[AOP异常通知] "+methodName+"方法抛异常了,异常类型是:" + targetMethodException.getClass().getName());
}
4)切点表达式语法
①切点表达式作用
AOP切点表达式(Pointcut Expression)是一种用于指定切点的语言,它可以通过定义匹配规则,来选择需要被切入的目标对象。
②切点表达式语法
a. execution() 固定开头
b. 方法访问修饰符
public private 直接描述对应修饰符即可
c. 方法返回值
int String void 直接描述返回值类型
注意:特殊情况不考虑访问修饰符和返回值
execution(* * )这是错误语法
execution( *)== 你只要考虑返回值或者不考虑访问修饰符相当于全部不考虑了
d. 指定包的地址
固定的包: com.atguigu.api | service | dao
单层的任意命名: com.atguigu.* = com.atguigu.api com.atguigu.dao * = 任意一层的任意命名
任意层任意命名: com.. = com.atguigu.api.erdaye com.a.a.a.a.a.a.a ..任意层,任意命名 用在包上!
注意: ..不能用作包开头 public int .. 错误语法 com..
找到任何包下: *..
e. 指定类名称
固定名称: UserService
任意类名: *
部分任意: com..service.impl.*Impl
任意包任意类: *..*
f. 指定方法名称
语法和类名一致
任意访问修饰符,任意类的任意方法: * *..*.*
g. 方法参数
具体值: (String,int) != (int,String) 没有参数 ()
模糊值: 任意参数 有 或者 没有 (..) ..任意参数的意识
部分具体和模糊:
第一个参数是字符串的方法 (String..)
最后一个参数是字符串 (..String)
字符串开头,int结尾 (String..int)
包含int类型(..int..)
5)重用(提取)切点表达式
①重用切点表达式优点
减少代码冗余,便于统一维护。
②同一类内部引用
a. 提取
// 切入点表达式重用
@Pointcut("execution(public int com.atguigu.aop.api.Calculator.add(int,int)))")
public void declarPointCut() {}
注意:提取切点注解使用 @Pointcut(切点表达式),需要添加到一个无参无返回值方法上即可。
b. 引用
@Before(value = "declarPointCut()")
public void printLogBeforeCoreOperation(JoinPoint joinPoint) {
③不同类中引用
不同类在引用切点,只需要添加类的全限定符 + 方法名即可。
@Before(value = "com.atguigu.spring.aop.aspect.LogAspect.declarPointCut()")
public Object roundAdvice(ProceedingJoinPoint joinPoint) {
④切点统一管理
建议:将切点表达式统一存储到一个类中进行集中管理和维护。
@Component
public class AtguiguPointCut {
@Pointcut(value = "execution(public int *..Calculator.sub(int,int))")
public void atguiguGlobalPointCut(){}
@Pointcut(value = "execution(public int *..Calculator.add(int,int))")
public void atguiguSecondPointCut(){}
@Pointcut(value = "execution(* *..*Service.*(..))")
public void transactionPointCut(){}
}
6)环绕通知
环绕通知对应整个 try...catch...finally 结构,包括前面四种通知的所有功能。
@Around("pointCut()")
public Object aroundLog(ProceedingJoinPoint point){
Object[] args = point.getArgs();
System.out.println("日志功能,"+point.getSignature().getName()+"方法执行了,入参为:"+Arrays.toString(args));
Object result = null;
try {
//执行目标方法的,并返回目标方法执行后的结果
result = point.proceed();
System.out.println("日志功能,"+point.getSignature().getName()+"方法执行结束了,返回结果为:"+result);
} catch (Throwable e) {
System.out.println("日志功能,"+point.getSignature().getName()+"方法执行出现异常了,原因是:"+e.getMessage());
} finally {
System.out.println("AOP的异常通知方法执行了!");
}
return result;
}
7)切面优先级设置
相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。
- 优先级高的切面:外面
- 优先级低的切面:里面
使用@Order 注解可以控制切面的优先级:
- @Order(较小的数):优先级高
- @Order(较大的数):优先级低
实际意义:实际开发中,如果有多个切面嵌套的情况,要慎重考虑。例如:如果事务切面优先级高,那么在缓存中命中数据的情况下,事务切面的操作都浪费了。
8)CGLib动态代理生效
在目标没有实现任何接口的情况下,Spring会自动使用cglib技术实现代理。
使用总结:
a. 如果目标类有接口,选择使用 jdk 动态代理
b. 如果目标类没有接口,选择 cglib 动态代理
c. 如果有接口,接口接值
d. 如果没有接口,类进行接值
6、SpringAOP基于XML方式实现(了解)
①准备工作
加入依赖
②配置Spring配置文件
<!-- 配置目标类的bean -->
<bean id="calculatorPure" class="com.atguigu.aop.imp.CalculatorPureImpl"/>
<!-- 配置切面类的bean -->
<bean id="logAspect" class="com.atguigu.aop.aspect.LogAspect"/>
<!-- 配置AOP -->
<aop:config>
<!-- 配置切入点表达式 -->
<aop:pointcut id="logPointCut" expression="execution(* *..*.*(..))"/>
<!-- aop:aspect标签:配置切面 -->
<!-- ref属性:关联切面类的bean -->
<aop:aspect ref="logAspect">
<!-- aop:before标签:配置前置通知 -->
<!-- method属性:指定前置通知的方法名 -->
<!-- pointcut-ref属性:引用切入点表达式 -->
<aop:before method="printLogBeforeCore" pointcut-ref="logPointCut"/>
<!-- aop:after-returning标签:配置返回通知 -->
<!-- returning属性:指定通知方法中用来接收目标方法返回值的参数名 -->
<aop:after-returning
method="printLogAfterCoreSuccess"
pointcut-ref="logPointCut"
returning="targetMethodReturnValue"/>
<!-- aop:after-throwing标签:配置异常通知 -->
<!-- throwing属性:指定通知方法中用来接收目标方法抛出异常的异常对象的参数名 -->
<aop:after-throwing
method="printLogAfterCoreException"
pointcut-ref="logPointCut"
throwing="targetMethodException"/>
<!-- aop:after标签:配置后置通知 -->
<aop:after method="printLogCoreFinallyEnd" pointcut-ref="logPointCut"/>
<!-- aop:around标签:配置环绕通知 -->
<!--<aop:around method="……" pointcut-ref="logPointCut"/>-->
</aop:aspect>
</aop:config>
③测试
@SpringJUnitConfig(locations = "classpath:spring-aop.xml")
public class AopTest {
@Autowired
private Calculator calculator;
@Test
public void testCalculator(){
System.out.println(calculator);
calculator.add(1,1);
}
}
7、SpringAOP对获取bean的影响理解
六、Spring 声明式事务
1、JdbcTemplate使用
①准备
pom.xml
<dependencies>
<!--spring context依赖-->
<!--当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了-->
<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-aop</artifactId>
<version>6.0.6</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>6.0.6</version>
</dependency>
</dependencies>
数据库脚本
数据库脚本
```sql
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, '高中二班');
jdbc.properties
atguigu.url=jdbc:mysql://localhost:3306/studb
atguigu.driver=com.mysql.cj.jdbc.Driver
atguigu.username=root
atguigu.password=123456
application.xml
<?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"
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">
<!-- 导入外部属性文件 -->
<context:property-placeholder location="classpath:jdbc.properties" />
<!-- 配置数据源 -->
<bean id="druidDataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="url" value="${atguigu.url}"/>
<property name="driverClassName" value="${atguigu.driver}"/>
<property name="username" value="${atguigu.username}"/>
<property name="password" value="${atguigu.password}"/>
</bean>
<!-- 配置 JdbcTemplate -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<!-- 装配数据源 -->
<property name="dataSource" ref="druidDataSource"/>
</bean>
</beans>
②使用JdbcTemplate
@SpringJUnitConfig(locations = "classpath:spring-jdbc-xml.xml")
public class JdbcTemplateTest {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
public void testUpdate(){
String sql = "insert into students(NAME,gender,age,class) values(?,?,?,?)";
Object[] args = {"longSpring","女","68","老年五班"};
int update = jdbcTemplate.update(sql, args);
System.out.println("update = " + update);
String sql2 = "update students set name = ? where id = ?";
Object[] args2 = {"长春",9};
int update2 = jdbcTemplate.update(sql, args);
System.out.println("update = " + update);
String sql3 = "delete from students where id = ?";
int update3 = jdbcTemplate.update(sql, 9);
System.out.println("sql = " + sql);
}
@Test
public void testSelect(){
//查询 单行单列数据 ,一个简单数据,结果能直接转换对应的数据类型
String sql = "select max(age) from students";
Integer maxAge = jdbcTemplate.queryForObject(sql, Integer.class);
System.out.println("maxAge = " + maxAge);
//查询 单行多列, 一个对象
String sql2 = "select id,name,gender,age,class as classes from students where id = ?";
//RowMapper 就是查询的行的每个列对应的是一个对象的各个属性
Students students = jdbcTemplate.queryForObject(sql2, new BeanPropertyRowMapper<>(Students.class),8);
System.out.println("students = " + students);
//查询 多行多列
String sql3 = "select id,name,gender,age,class as classes from students";
List<Students> studentsList = jdbcTemplate.query(sql3, new BeanPropertyRowMapper<>(Students.class));
studentsList.forEach(System.out::println);
}
}
2、声明式事务概念
1) 事务概念
① 什么是事务:
概念:是一个原子操作,可以由一个或多个sql语句组成。在同一个事务中,当所有的sql执行时,整个事务要么都成功,要么都失败。
转帐:a->减钱 b->加钱
② 事务的四大特性:
a. 原子性,表示事务是一个整体,要么都成功,要么都失败
c. 一致性,数据要么是事务成功之后的状态,要么回滚到事务之前的状态
I. 隔离性,多个事务之间,互不干扰。a事务只能读取a事务开始之后的数据,b事务的操作不会影响a事务
d. 持久性,一旦事务成功了,提交之后,对数据的影响是永久性的
③ MySQL的四大隔离级别:
-
READ_UNCOMMITED:读未提交
-
A事务可以读取到B事务未提交的数据
-
-
READ_COMMITED:读已提交
-
A事务可以读取B事务提交后的数据
-
不可重复读
-
-
REPETABLE_READ:可重复读【MySQL的默认隔离级别】
-
A事务只能读取A事务开始之后的数据。不会被其他事务所影响
-
表、行、列加锁
-
在Java中,事务的操作:
-
connection.setAutoCommit(false):关闭当前链接的自动提交
-
connection.commit():提交当前事务
-
connection.rollback():回滚当前事务
④ Spring事务
-
Spring认为事务代码属于增强,站在核心业务的角度,事务属于非核心代码
-
Spring支持两种事务
-
编程式事务:事务的代码和业务的代码写在一起。自己写
-
问题:耦合度过高,代码分散,代码冗余。
-
可以利用AOP的思想,前置通知(开启事务)、返回通知(提交事务)、异常通知(回滚事务)
-
-
声明式事务,你需要,Spring帮你写
-
2)编程式事务
编程式事务是指手动编写程序来管理事务,即通过编写代码的方式直接控制事务的提交和回滚。在Java中,通常使用事务管理器(如Spring 中的 PlatformTransactionManager)来实现编程式事务。
编程式事务的主要优点是灵活性高,可以按照自己的需求来控制事务的粒度、模式等等。但是,编写大量的事务控制代码容易出现问题,对代码的可读性和可维护性有一定影响。
Connection conn = ...;
try {
// 开启事务:关闭事务的自动提交
conn.setAutoCommit(false);
// 核心操作
// 业务代码
// 提交事务
conn.commit();
}catch(Exception e){
// 回滚事务
conn.rollBack();
}finally{
// 释放数据库连接
conn.close();
}
编程式的实现方式存在缺陷:
- 细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
- 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。
3)声明式事务
声明式事务是指使用注解或xml 配置的方式来控制事务的提交和回滚。
开发者只需要添加配置即可,具体事务的实现由第三方框架实现,避免我们直接进行事务操作!使用声明式事务可以将事务的控制和业务逻辑分离出来,提高代码的可读性和可维护性。
区别:
- 编程式事务需要手动编写代码来管理事务
- 而声明式事务可以通过配置文件或注解来控制事务。
4)Spring 事务管理器
1)Spring 声明式事务对应依赖
- spring-tx:包含声明式事务实现的基本规范(事务管理器规范接口和事务增强等等)
- spring-jdbc:包含DataSource方式事务管理器实现类 DataSourceTransactionManager
- spring-orm:包含其他持久层框架的事务管理器来实现类例如:Hibernate/Jpa等
2)Spring 声明式事务对应事务管理器接口
我们现在要使用的事务管理器是 org.springframework.jdbc.datasource.DataSourceTransactionManager,将来整合 JDBC 方式、JdbcTemplate方式、Mybatis方式的事务实现!
DataSourceTransactionManager类中的主要方法:
- doBegin():开启事务
- doSuspend():挂起事务
- doResume():恢复挂起事务
- doCommit():提交事务
- doRollback():回滚事务
3、基于注解的声明式事务
1)基本事务控制
使用声明事务注解 @Transactional
@Service
public class StudentService {
@Autowired
private StudentDao studentDao;
@Transactional
public void changeInfo(){
studentDao.updateAgeById(100,1);
System.out.println("-----------");
int i = 1/0;
studentDao.updateNameById("test1",1);
}
}
2)事务属性:只读
只读:对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。
①设置方式
// readOnly = true把当前事务设置为只读 默认是false!
@Transactional(readOnly = true)
②针对DML动作设置只读模式
会抛出下面异常
Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
③ @Transactional 注解放在类上
a. 生效原则
如果一个类中每一个方法上都使用了 @Transactional 注解,那么就可以将 @Transactional 注解提取到类上,返过来说:@Transactional 注解在类级别标记,会影响到类中的每一个方法。同时,类级别标记的 @Transactional 注解中设置的事务属性也会延续影响到方法执行时的事务属性,除非在方法上又设置了 @Transactional 注解。
b. 用法举例
在类级别 @Transactional 注解中设置只读,这样类中所有的查询方法都不需要设置 @Transactional 注解了。因为对查询操作来说,其他属性通常不需要设置,所以使用公共设置即可。然后在这个基础上,对增删查改方法设置 @Transactional 注解 readOnly 属性为 false。
@Service
@Transactional(readOnly = true)
public class EmpService {
// 为了便于核对数据库操作结果,不要修改同一条记录
@Transactional(readOnly = false)
public void updateTwice(……) {
……
}
// readOnly = true把当前事务设置为只读
// @Transactional(readOnly = true)
public String getEmpName(Integer empId) {
……
}
}
3)事务属性:超时时间
① 需求
事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据资源。而长时间占用资源,大概率是因为程序运行出现了问题。
此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。
概括:超时回滚,释放资源。
② 设置超时时间
@Service
public class StudentService {
@Autowired
private StudentDao studentDao;
/**
* timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间!
*/
@Transactional(readOnly = false,timeout = 3)
public void changeInfo(){
studentDao.updateAgeById(100,1);
//休眠4秒,等待方法超时!
try {
Thread.sleep(4000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
studentDao.updateNameById("test1",1);
}
}