2022尚硅谷SSM框架跟学 五Spring基础二
- 3.AOP
- 3.1场景模拟
- 3.1.1声明接口
- 3.1.2创建实现类
- 3.1.3创建带日志功能的实现类
- 3.1.4提出问题
- (1)现有代码缺陷
- (2)解决思路
- (3)困难
- 3.2代理模式
- 3.2.1概念
- (1)介绍
- (2)生活中的代理
- (3)相关术语
- 3.2.2静态代理
- 3.2.3动态代理
- 3.2.4测试
- 3.3AOP概念及相关术语
- 3.3.1概述
- 3.3.2相关术语
- (1)横切关注点
- (2)通知
- (3)切面
- (4)目标
- (5)代理
- (6)连接点
- (7)切入点
- 3.3.3作用
- 3.4基于注解的AOP
- 3.4.1技术说明
- 3.4.2准备工作
- (1)添加依赖
- (2)准备被代理的目标资源
- 3.4.3创建切面类并配置
- 3.4.4各种通知
- 3.4.5切入点表达式语法
- (1)作用
- (2)语法细节
- 前置通知
- 3.4.6重用切入点表达式
- (1)声明
- (2)在同一个切面中使用
- 后置通知
- 返回通知
- 异常(例外)通知
- (3)在不同切面中使用
- 3.4.7获取通知的相关信息
- (1)获取连接点信息
- (2)获取目标方法的返回值
- (3)获取目标方法的异常
- 3.4.8环绕通知
- 3.4.9切面的优先级
- 3.5基于XML的AOP(了解)
- 3.5.1准备工作
- 3.5.2实现
- 4声明式事务
- 4.1JdbcTemplate
- 4.1.1简介
- 4.1.2准备工作
- (1)加入依赖
- (2)创建jdbc.properties
- (3)配置Spring的配置文件
- 4.1.3测试
- (1)在测试类装配 JdbcTemplate
- (2)测试增删改功能
- (3)查询一条数据为实体类对象
- (4)查询多条数据为一个list集合
- (5)查询单行单列的值
- 4.2声明式事务概念
- 4.2.1编程式事务
- 4.2.2声明式事务
- 4.3基于注解的声明式事务
- 4.3.1准备工作
- (1)加入依赖
- (2)创建jdbc.properties
- (3)配置Spring的配置文件
- (4)创建表
- (5)创建组件
- 4.3.2测试无事务情况
- (1)创建测试类
- (2)模拟场景
- (3)观察结果
- 4.3.3加入事务
- (1)添加事务配置
- (2)添加事务注解
- (3)观察结果
- (4)声明式事务的配置步骤
- 4.3.4@Transactional注解标识的位置
- 4.3.5事务属性:只读
- (1)介绍
- (2)使用方式
- (3)注意
- 4.3.6事务属性:超时
- (1)介绍
- (2)使用方式
- (3)观察结果
- 4.3.7事务属性:回滚策略
- (1)介绍
- (2)使用方式
- (3)观察结果
- 4.3.8事务属性:事务隔离级别
- (1)介绍
- (2)使用方式
- 4.3.9事务属性:事务传播行为
- (1)介绍
- (2)测试
- (3)观察结果
- 4.4基于XML的声明式事务
- 4.4.1场景模拟
- 4.3.2修改Spring配置文件
3.AOP
3.1场景模拟
新建Module
Name:spring-proxy
GroupId:com.atguigu.spring
设置打包方式
<packaging>jar</packaging>
配置pom.xml,加入junit依赖
<dependencies>
<!-- junit测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
</dependencies>
3.1.1声明接口
声明计算器接口Calculator,包含加减乘除的抽象方法
创建接口com.atguigu.spring.proxy.Calculator
Calculator.java
package com.atguigu.spring.proxy;
/**
* @InterfaceName: Calculator
* @Description:
* @Author: wty
* @Date: 2023/1/11
*/
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创建实现类
创建接口的实现类com.atguigu.spring.proxy.CalculatorImpl
package com.atguigu.spring.proxy;
/**
* @ClassName: CalculatorImpl
* @Description:
* @Author: wty
* @Date: 2023/1/11
*/
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.1.3创建带日志功能的实现类
修改类CalculatorImpl.java,添加日志记录
package com.atguigu.spring.proxy;
/**
* @ClassName: CalculatorImpl
* @Description:
* @Author: wty
* @Date: 2023/1/11
*/
public class CalculatorImpl 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.1.4提出问题
(1)现有代码缺陷
针对带日志功能的实现类,我们发现有如下缺陷:
- 对核心业务功能有干扰,导致程序员在开发核心业务功能时分散了精力
- 附加功能分散在各个业务功能方法中,不利于统一维护
(2)解决思路
解决这两个问题,核心就是:解耦。我们需要把附加功能从业务功能代码中抽取出来。
(3)困难
解决问题的困难:要抽取的代码在方法内部,靠以前把子类中的重复代码抽取到父类的方式没法解决。所以需要引入新的技术。
3.2代理模式
3.2.1概念
(1)介绍
二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。
使用代理后:
(2)生活中的代理
- 广告商找大明星拍广告需要经过经纪人
- 合作伙伴找大老板谈合作要约见面时间需要经过秘书
- 房产中介是买卖双方的代理
(3)相关术语
- 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。
- 目标:被代理“套用”了非核心逻辑代码的类、对象、方法。
3.2.2静态代理
创建静态代理类:CalculatorStaticProxy.java
package com.atguigu.spring.proxy;
/**
* @ClassName: CalculatorStaticProxy
* @Description:
* @Author: wty
* @Date: 2023/1/11
*/
public class CalculatorStaticProxy implements Calculator {
private Calculator target;
public CalculatorStaticProxy() {
}
public CalculatorStaticProxy(Calculator calculator) {
this.target = calculator;
}
public Calculator getCalculator() {
return target;
}
public void setCalculator(Calculator calculator) {
this.target = calculator;
}
@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;
}
}
修改CalculatorImpl.java
package com.atguigu.spring.proxy;
/**
* @ClassName: CalculatorImpl
* @Description:
* @Author: wty
* @Date: 2023/1/11
*/
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;
}
}
创建测试类com.atguigu.spring.proxy.ProxyTest
public class ProxyTest {
@Test
public void test() {
Calculator calculator = new CalculatorImpl();
CalculatorStaticProxy proxy = new CalculatorStaticProxy(calculator);
proxy.add(1, 2);
}
}
执行结果
这里要明白静态代理不仅仅只是在目标方法前和后进行非核心方法的调用,应该分为以下4种
静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理。
提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现。这就需要使用动态代理技术了。
3.2.3动态代理
生产代理对象的工厂类:com.atguigu.spring.proxy.ProxyFactory
package com.atguigu.spring.proxy;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
import java.util.Arrays;
/**
* @ClassName: ProxyFactory
* @Description:
* @Author: wty
* @Date: 2023/1/11
*/
public class ProxyFactory {
// 目标对象
private Object target;
public ProxyFactory(Object target) {
this.target = target;
}
public Object getProxy() {
// JDK动态代理
/**
* newProxyInstance 的源码
* newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
*
* ClassLoader loader 指定加载动态生成的代理类的类加载器
*
*Class<?>[] interfaces 获取目标对象实现的所有的接口的class对象的数组
*
* InvocationHandler 执行处理,设置代理类中的抽象方法该如何重写
*/
ClassLoader classLoader = this.getClass().getClassLoader();
Class<?>[] interfaces = target.getClass().getInterfaces();
InvocationHandler invocationHandler = new InvocationHandler() {
@Override
/**
* @description //TODO
*
* @param
* @param: proxy 表示代理对象
* @param: method 表示要执行的方法
* @param: args 表示要执行的方法的参数列表
* @return java.lang.Object
* @date 2023/1/11 12:50
* @author wty
**/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
System.out.println("[日志] " + method.getName() + " 方法开始了,参数是:" + Arrays.toString(args));
Object result = method.invoke(target, args);
System.out.println("[日志] " + method.getName() + " 方法结束了,结果是:" + result);
return result;
}
};
return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
}
}
修改测试类ProxyTest.java,新增一个方法
// 动态代理
@Test
public void test2() {
Calculator calculator = new CalculatorImpl();
ProxyFactory proxyFactory = new ProxyFactory(calculator);
Object o = proxyFactory.getProxy();
// 通过向下转型
Calculator proxy = (Calculator) o;
proxy.add(1, 2);
}
执行测试类ProxyTest.java
3.2.4测试
修改ProxyFactory.java,添加try和catry、finally代码块
public Object getProxy() {
// JDK动态代理
/**
* newProxyInstance 的源码
* newProxyInstance(ClassLoader loader, Class<?>[] interfaces, InvocationHandler h)
*
* ClassLoader loader 指定加载动态生成的代理类的类加载器
*
*Class<?>[] interfaces 获取目标对象实现的所有的接口的class对象的数组
*
* InvocationHandler 执行处理,设置代理类中的抽象方法该如何重写
*/
ClassLoader classLoader = this.getClass().getClassLoader();
Class<?>[] interfaces = target.getClass().getInterfaces();
InvocationHandler invocationHandler = new InvocationHandler() {
@Override
/**
* @description //TODO
*
* @param
* @param: proxy 表示代理对象
* @param: method 表示要执行的方法
* @param: args 表示要执行的方法的参数列表
* @return java.lang.Object
* @date 2023/1/11 12:50
* @author wty
**/
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object result = null;
try {
System.out.println("[日志] " + method.getName() + " 方法开始了,参数是:" + Arrays.toString(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, invocationHandler);
}
执行结果
那如果抛出异常会如何呢,我们来试一下
修改测试类ProxyTest.java,调用除法,把除数写成0
// 动态代理
@Test
public void test2() {
Calculator calculator = new CalculatorImpl();
ProxyFactory proxyFactory = new ProxyFactory(calculator);
Object o = proxyFactory.getProxy();
// 通过向下转型
Calculator proxy = (Calculator) o;
//proxy.add(1, 2);
proxy.div(1, 0);
}
直接运行测试类
发现执行了catch里面的打印语句
总结: 动态代理有2种
- 1.jdk动态代理,要求必须有接口,最终生成的代理类在com.sun.proxy包下,类名为${proxy2}。
- 2.cglib动态代理,最终生成的代理类会继承目标类,并且和目标类在相同的包下。
3.3AOP概念及相关术语
3.3.1概述
AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程的一种补充和完善,它以通过预编译方式和运行期动态代理方式实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术。
3.3.2相关术语
(1)横切关注点
从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。
这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。
(2)通知
每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。
- 前置通知:在被代理的目标方法前执行
- 返回通知:在被代理的目标方法成功结束后执行(寿终正寝)
- 异常通知:在被代理的目标方法异常结束后执行(死于非命)
- 后置通知:在被代理的目标方法最终结束后执行(盖棺定论)
- 环绕通知:使用try…catch…finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置。
(3)切面
封装通知方法(横切关注点)的类。
(4)目标
被代理的目标对象。
(5)代理
向目标对象应用通知之后创建的代理对象。
(6)连接点
这也是一个纯逻辑概念,不是语法定义的。
把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是连接点。
(7)切入点
定位连接点的方式。
每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。
如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句。
Spring 的 AOP 技术可以通过切入点定位到特定的连接点。
切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。
3.3.3作用
- 简化代码:把方法中固定位置的重复的代码抽取出来,让被抽取的方法更专注于自己的核心功能,提高内聚性。
- 代码增强:把特定的功能封装到切面类中,看哪里有需要,就往上套,被套用了切面逻辑的方法就被切面给增强了。
3.4基于注解的AOP
3.4.1技术说明
AOP是面向切面编程的思想,而AspectJ是面向切面编程思想的实现。
动态代理(InvocationHandler)
- JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)。
- cglib:通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口。
- AspectJ:本质上是静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver就是织入器。Spring只是借用了AspectJ中的注解。
3.4.2准备工作
创建新的Module
Name:spring-aop
GroupID:com.atguigu.spring
在pom.xml中添加打包方式
<packaging>jar</packaging>
(1)添加依赖
在pom.xml中添加依赖
<dependencies>
<!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 -->
<dependency>
<groupId>org.springframework</groupId>
<!--spring上下文 -->
<artifactId>spring-context</artifactId>
<version>5.3.1</version>
</dependency>
<!-- junit测试 -->
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
<scope>test</scope>
</dependency>
<!-- spring-aspects会帮我们传递过来aspectjweaver -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.1</version>
</dependency>
</dependencies>
(2)准备被代理的目标资源
接口:
package com.atguigu.spring.aop.annotation;
/**
* @InterfaceName: Calculator
* @Description:
* @Author: wty
* @Date: 2023/1/11
*/
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);
}
实现类
package com.atguigu.spring.aop.annotation;
/**
* @ClassName: CalculatorImpl
* @Description:
* @Author: wty
* @Date: 2023/1/11
*/
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.4.3创建切面类并配置
创建切面类com.atguigu.spring.aop.annotation.LogerAspect
添加注解@Component(因为其它都用这个表示)和切面注解@Aspect
package com.atguigu.spring.aop.annotation;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;
/**
* @ClassName: LogerAspect
* @Description:
* @Author: wty
* @Date: 2023/1/12
*/
@Component
@Aspect
public class LogerAspect {
}
新建配置文件aop-annotation.xml
在Spring的配置文件中配置:
<!--
切面类和目标类都需要交给IOC容器,这里是通过注解的方式
切面类必须通过@Aspect注解标识为一个切面,这里是LogerAspect.java
在spring配置文件中设置aop:aspectj-autoproxy/,这里是当前文件aop-annotation.xml
-->
<!-- 这里配置扫描自动装配 -->
<context:component-scan base-package="com.atguigu.spring.aop.annotation"></context:component-scan>
<!-- aop配置:开启基于注解的AOP -->
<aop:aspectj-autoproxy/>
<!-- aop配置:开启基于注解的AOP -->
<aop:aspectj-autoproxy/>
修改CalculatorImpl.java,添加注解@Component
以前置通知为例,来标记一下注解
修改LogerAspect.java
package com.atguigu.spring.aop.annotation;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
/**
* @ClassName: LogerAspect
* @Description: 在切面中。需要通过指定的注解将方法标识为通知方法
* Before:前置通知,在目标对象方法执行之前执行
* @Author: wty
* @Date: 2023/1/12
*/
@Component
// 将当前组件标识为切面
@Aspect
public class LogerAspect {
@Before("execution(public int com.atguigu.spring.aop.annotation.CalculatorImpl.add(int,int))")
public void beforeAdviceMethod() {
System.out.println("前置通知");
}
}
创建测试类com.atguigu.spring.aop.AOPTest
添加测试类,我们先尝试一下getBean()里面放目标类的class
@Test
public void testAOPByAnnotation() {
ApplicationContext ioc = new ClassPathXmlApplicationContext("aop-annotation.xml");
CalculatorImpl calculator = ioc.getBean(CalculatorImpl.class);
calculator.add(1, 3);
}
运行测试类后,发现抛出异常NoSuchBeanDefinitionException
很自然的想到,AOP的AspectJ是采用了静态代理的模式,通过代理类,间接调用目标类,这里继续修改AOPTest.java
@Test
public void testAOPByAnnotation() {
ApplicationContext ioc = new ClassPathXmlApplicationContext("aop-annotation.xml");
Calculator calculator = ioc.getBean(Calculator.class);
calculator.add(1, 3);
}
我们发现,其实代理类我们也不知道是啥,是由系统自动装配的,但是我们知道其接口是Calculator,并且我们知道代理类也一定实现了Calculator,那么我们就在getBean()中写入接口类。
执行测试类,查看结果:
3.4.4各种通知
- 前置通知:使用@Before注解标识,在被代理的目标方法前执行
- 返回通知:使用@AfterReturning注解标识,在被代理的目标方法成功结束后执行(寿终正寝)
- 异常通知:使用@AfterThrowing注解标识,在被代理的目标方法异常结束后执行(死于非命)
- 后置通知:使用@After注解标识,在被代理的目标方法最终结束后执行(盖棺定论)
- 环绕通知:使用@Around注解标识,使用try…catch…finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置。
当前项目的spring版本是5.3.1遵循Spring版本5.3.x以后的通知顺序
各种通知的执行顺序:
- Spring版本5.3.x以前:(我们使用的spring版本是5.3.1)
⨀ \bigodot ⨀前置通知
⨀ \bigodot ⨀目标操作
⨀ \bigodot ⨀后置通知
⨀ \bigodot ⨀返回通知或异常通知 - Spring版本5.3.x以后:
⨀ \bigodot ⨀前置通知
⨀ \bigodot ⨀目标操作
⨀ \bigodot ⨀返回通知或异常通知
⨀ \bigodot ⨀后置通知
3.4.5切入点表达式语法
(1)作用
(2)语法细节
- 用*号代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限
- 在包名的部分,一个“”号只能代表包的层次结构中的一层,表示这一层是任意的。
⨀ \bigodot ⨀例如:.Hello匹配com.Hello,不匹配com.atguigu.Hello - 在包名的部分,使用“*…”表示包名任意、包的层次深度任意
- 在类名的部分,类名部分整体用*号代替,表示类名任意
- 在类名的部分,可以使用*号代替类名的一部分
⨀ \bigodot ⨀例如:*Service匹配所有名称以Service结尾的类或接口 - 在方法名部分,可以使用*号表示方法名任意
- 在方法名部分,可以使用*号代替方法名的一部分
⨀ \bigodot ⨀例如:*Operation匹配所有方法名以Operation结尾的方法 - 在方法参数列表部分,使用(…)表示参数列表任意
- 在方法参数列表部分,使用(int,…)表示参数列表以一个int类型的参数开头
- 在方法参数列表部分,基本数据类型和对应的包装类型是不一样的
切入点表达式中使用 int 和实际方法中 Integer 是不匹配的 - 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符
⨀ \bigodot ⨀例如:execution(public int …Service.(…, int)) 正确
⨀ \bigodot ⨀例如:execution( int …Service.*(…, int)) 错误
前置通知
切入点表达式总结
设置位置:需要设置在标识通知的注解的value属性中。
例如:
@Before("execution(public int com.atguigu.spring.aop.annotation.CalculatorImpl.add(int,int))")
可以简写为
@Before("execution(*com.atguigu.spring.aop.annotation.CalculatorImpl.*(..))")
其中:
第一个 * 表示任意的访问修饰符和返回值类型
第二个 * 表示当前类中的任意方法
… 表示任意的参数列表
类的地方也可以使用*,表示包下所有的类,例如:
@Before("execution(*com.atguigu.spring.aop.annotation.*.*(..))")
包的地方也可以使用*,表示当前包下的所有的子包,例如
@Before("execution(*com.atguigu.spring.aop.*.*.*(..))")
修改完后,我们测试一下,是否目标类其它方法可以调用
我们给测试类AOPTest.java中添加一下目标类的其它方法,比如sub()方法,看一下
发现运行结果
那如何获取到切入点的通知方法的方法名和参数名呢,下面我们来修改一下LogerAspect.java
获取连接点的信息
在通知方法的参数位置,设置JoinPoint类型的参数,就可以获取连接点对应的通知方法的信息。
比如:
joinPoint.getSignature()获取连接点通知方法的签名信息
joinPoint.getArgs()获取连接点通知方法的参数信息
@Component
// 将当前组件标识为切面
@Aspect
public class LogerAspect {
//@Before("execution(public int com.atguigu.spring.aop.annotation.CalculatorImpl.add(int,int))")
@Before("execution (* com.atguigu.spring.aop.annotation.CalculatorImpl.*(..))")
public void beforeAdviceMethod(JoinPoint joinPoint) {
// 获取连接点对应方法的方法名
Signature signature = joinPoint.getSignature();
System.out.println("连接点对应方法的方法名是" + signature);
// 获取连接点对应方法的参数
Object[] args = joinPoint.getArgs();
System.out.println("连接点对应方法的参数是" + Arrays.toString(args));
System.out.println("前置通知");
}
}
之后运行测试类AOPTest.java查看结果
紧接着,我们来看一下后置通知,那就需要用到切入点表达式的重用了。
3.4.6重用切入点表达式
@Pointcut声明一个公共的切入点表达式
声明完之后使用
@通知类型(“方法名称”)
(1)声明
修改LogerAspect.java,修改注解中的内容
@Pointcut("execution(* com.atguigu.spring.aop.annotation.CalculatorImpl.*(..))")
public void pointCut() {}
(2)在同一个切面中使用
后置通知
在LogerAspect.java中添加方法
@After("pointCut()")
public void afterAdviceMethod() {
System.out.println("后置通知");
}
运行测试类AOPTest.java
查看结果
这里针对后置通知,我们不禁会疑问后置通知的位置在哪儿,是finally子句还是方法体返回后执行,下面我们来验证一下。
修改测试类AOPTest.java,调用除法的方式
@Test
public void testAOPByAnnotation() {
ApplicationContext ioc = new ClassPathXmlApplicationContext("aop-annotation.xml");
Calculator calculator = ioc.getBean(Calculator.class);
// calculator.add(1, 3);
// calculator.sub(1, 3);
calculator.div(1, 0);
}
输出结果
得出结论:@After是在目标类方法的finally中执行。
为了看出通知方法的详细信息,我们继续修改LogerAspect.java
@After("pointCut()")
public void afterAdviceMethod(JoinPoint joinPoint) {
// 获取连接点对应方法的方法名
Signature signature = joinPoint.getSignature();
System.out.println("连接点对应方法的方法名是" + signature);
// 获取连接点对应方法的参数
Object[] args = joinPoint.getArgs();
System.out.println("连接点对应方法的参数是" + Arrays.toString(args));
System.out.println("后置通知");
}
运行测试类,查看结果
接着我们看返回通知
返回通知
修改LogerAspect.java的方法
@AfterReturning("pointCut()")
public void afterReturningAdviceMethod() {
System.out.println("返回通知");
}
直接运行测试类AOPTest.java,查看结果
可以看出来,抛出异常后,不会执行返回通知的内容。
那我们执行一次正确的。
修改AOPTest.java
@Test
public void testAOPByAnnotation() {
ApplicationContext ioc = new ClassPathXmlApplicationContext("aop-annotation.xml");
Calculator calculator = ioc.getBean(Calculator.class);
// calculator.add(1, 3);
// calculator.sub(1, 3);
calculator.div(1, 1);
}
作为返回通知,目标类的方法已经产生了结果,那我们如何获取方法的返回值呢,下面我们修改一下LogerAspect.java
代码如下:
@AfterReturning(value = "pointCut()", returning = "result")
public void afterReturningAdviceMethod(JoinPoint joinPoint, Object result) {
Signature signature = joinPoint.getSignature();
System.out.println("连接点对应方法的方法名是" + signature);
System.out.println("返回通知");
System.out.println("目标对象的返回值:" + result);
}
运行测试类AOPTest.java
获取到返回值。
最后看一下异常(例外)通知。
异常(例外)通知
修改类LogerAspect.java
@AfterThrowing("pointCut()")
public void afterThrowingAdviceMethod(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
System.out.println("连接点对应方法的方法名是" + signature.getName());
System.out.println("异常通知");
}
异常通知在编译器中有闪电图标
修改测试类AOPTest.java
@Test
public void testAOPByAnnotation() {
ApplicationContext ioc = new ClassPathXmlApplicationContext("aop-annotation.xml");
Calculator calculator = ioc.getBean(Calculator.class);
calculator.div(10, 0);
}
返回结果
既然是异常通知,这里我们想返回异常的信息
修改LogerAspect.java
@AfterThrowing(value = "pointCut()", throwing = "e")
public void afterThrowingAdviceMethod(JoinPoint joinPoint, Throwable e) {
Signature signature = joinPoint.getSignature();
System.out.println("连接点对应方法的方法名是" + signature.getName());
System.out.println("异常通知");
System.out.println("异常信息是:" + e);
}
这里用Throwable e或者Exception e 都可以。
执行测试类AOPTest.java查看结果。
总结: 在异常通知中若要获取目标对象方法的异常
只需要通过@AfterThrowing注解的throwing属性,
就可以将通知方法的某个参数指定为接收目标对象方法出现的异常的参数。
(3)在不同切面中使用
创建类存放另一个验证切面,com.atguigu.spring.aop.annotation.ValidateAspect
package com.atguigu.spring.aop.annotation;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
/**
* @ClassName: ValidateAspect
* @Description:计算器加减乘除的验证
* @Author: wty
* @Date: 2023/1/13
*/
@Component
@Aspect
public class ValidateAspect {
//@Before("execution(* com.atguigu.spring.aop.annotation.Calculator.*(..))")
@Before("com.atguigu.spring.aop.annotation.LogerAspect.pointCut()")
public void BeforeAdviceMethod(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
System.out.println("ValidateAspect通知方法:" + signature.getName() + "前置通知");
}
}
修改之前的切面LogerAspect.java,将输出语句加上切面类的类名
//@Before("execution(public int com.atguigu.spring.aop.annotation.CalculatorImpl.add(int,int))")
@Before("execution (* com.atguigu.spring.aop.annotation.CalculatorImpl.*(..))")
public void beforeAdviceMethod(JoinPoint joinPoint) {
// 获取连接点对应方法的方法名
Signature signature = joinPoint.getSignature();
System.out.println("LogerAspect连接点对应方法的方法名是" + signature.getName());
// 获取连接点对应方法的参数
Object[] args = joinPoint.getArgs();
System.out.println("LogerAspect连接点对应方法的参数是" + Arrays.toString(args));
System.out.println("LogerAspect前置通知");
}
执行测试类AOPTest.java
@Test
public void testAOPByAnnotation() {
ApplicationContext ioc = new ClassPathXmlApplicationContext("aop-annotation.xml");
Calculator calculator = ioc.getBean(Calculator.class);
// calculator.add(1, 3);
// calculator.sub(1, 3);
calculator.div(10, 1);
}
执行结果
那切面的执行顺序是怎样 的呢,见下面。
3.4.7获取通知的相关信息
(1)获取连接点信息
获取连接点信息可以在通知方法的参数位置设置JoinPoint类型的形参
例如:
@Before("execution(public int com.atguigu.spring.aop.annotation.CalculatorImpl.add(int,int))")
public void beforeAdviceMethod(JoinPoint joinPoint) {
// 获取连接点对应方法的方法名
Signature signature = joinPoint.getSignature();
System.out.println("连接点对应方法的方法名是" + signature.getName());
// 获取连接点对应方法的参数
Object[] args = joinPoint.getArgs();
System.out.println("连接点对应方法的参数是" + Arrays.toString(args));
System.out.println("前置通知");
}
(2)获取目标方法的返回值
@AfterReturning中的属性returning,用来将通知方法的某个形参,接收目标方法的返回值,例如:
@AfterReturning(value = "pointCut()", returning = "result")
public void afterReturningAdviceMethod(JoinPoint joinPoint, Object result) {
Signature signature = joinPoint.getSignature();
System.out.println("连接点对应方法的方法名是" + signature.getName());
System.out.println("返回通知");
System.out.println("目标对象的返回值:" + result);
}
(3)获取目标方法的异常
@AfterThrowing中的属性throwing,用来将通知方法的某个形参,接收目标方法的异常,例如:
@AfterThrowing(value = "pointCut()", throwing = "e")
public void afterThrowingAdviceMethod(JoinPoint joinPoint, Throwable e) {
Signature signature = joinPoint.getSignature();
System.out.println("连接点对应方法的方法名是" + signature.getName());
System.out.println("异常通知");
System.out.println("异常信息是:" + e);
}
3.4.8环绕通知
在LogerAspect.java中添加方法
@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;
}
在测试类AOPTest.java中添加方法
@Test
public void testAOPByAnnotation() {
ApplicationContext ioc = new ClassPathXmlApplicationContext("aop-annotation.xml");
Calculator calculator = ioc.getBean(Calculator.class);
// calculator.add(1, 3);
// calculator.sub(1, 3);
calculator.div(10, 1);
}
查看结果
修改AOPTest.java看一下异常的结果
@Test
public void testAOPByAnnotation() {
ApplicationContext ioc = new ClassPathXmlApplicationContext("aop-annotation.xml");
Calculator calculator = ioc.getBean(Calculator.class);
calculator.div(10, 0);
}
3.4.9切面的优先级
相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。
- 优先级高的切面:外面
- 优先级低的切面:里面
使用@Order注解可以控制切面的优先级: - @Order(较小的数):优先级高
- @Order(较大的数):优先级低
之前多个切面的时候,先输出的是LogerAspect,紧接着才是ValidateAspect,那我们想调换一下切面的执行顺序该怎么做呢?
我们可以用到@Order注解,看一下源码
Order默认是Integer的最大值,而我们知道,Order里面的值越小,优先级越高,那我们设置成1
修改ValidateAspect.java,想让验证切面先跑
package com.atguigu.spring.aop.annotation;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;
/**
* @ClassName: ValidateAspect
* @Description:计算器加减乘除的验证
* @Author: wty
* @Date: 2023/1/13
*/
@Component
@Aspect
@Order(1)
public class ValidateAspect {
//@Before("execution(* com.atguigu.spring.aop.annotation.Calculator.*(..))")
@Before("com.atguigu.spring.aop.annotation.LogerAspect.pointCut()")
public void BeforeAdviceMethod(JoinPoint joinPoint) {
Signature signature = joinPoint.getSignature();
System.out.println("ValidateAspect通知方法:" + signature.getName() + "前置通知");
}
}
运行测试类AOPTest.java
发现成功改变了切面的优先级。
3.5基于XML的AOP(了解)
3.5.1准备工作
参考基于注解的AOP环境
新建包com.atguigu.spring.aop.xml,然后将四个类拷贝到新包之中。
删除LogerAspect.java、ValidateAspect.java中与AOP相关的注解
@Aspect、@Pointcut、 @Before、@After、@AfterReturning、@AfterThrowing
创建配置文件aop-xml.xml
新建测试类com.atguigu.spring.aop.XMLTest
@Test
public void testAOPByXml() {
ApplicationContext ioc = new ClassPathXmlApplicationContext("aop-xml.xml");
Calculator calculator = ioc.getBean(Calculator.class);
calculator.add(1, 2);
}
3.5.2实现
aop-xml.xml中添加相关bean
<!-- 扫描组件 -->
<context:component-scan base-package="com.atguigu.spring.aop.xml"></context:component-scan>
<aop:config>
<!-- 设置一个公共的切入点表达式 -->
<aop:pointcut id="pointCut" expression="execution(* com.atguigu.spring.aop.xml.CalculatorImpl.*(..))"/>
<!--
aop:aspect 将IOC容器中的某个组件设置成切面,将组件设置成切面
aop:pointcut 设置切入点表达式
aop:advisor 设置通知,很少用,声明式事务中使用
-->
<aop:aspect ref="logerAspect">
<!--
aop:before 前置通知
aop:after 后置通知
aop:after-returning 返回通知
aop:after-throwing 异常通知
aop:around 环绕通知
-->
<aop:before method="beforeAdviceMethod" pointcut-ref="pointCut"></aop:before>
<aop:after method="afterAdviceMethod" pointcut-ref="pointCut"></aop:after>
<aop:after-returning method="afterReturningAdviceMethod" pointcut-ref="pointCut"
returning="result"></aop:after-returning>
<aop:after-throwing method="afterThrowingAdviceMethod" pointcut-ref="pointCut"
throwing="e"></aop:after-throwing>
<aop:around method="aroundAdviceMethod" pointcut-ref="pointCut"></aop:around>
</aop:aspect>
</aop:config>
执行测试类XMLTest
设置另一个切面,修改aop-xml.xml
<!-- 设置另一个切面-->
<aop:aspect ref="validateAspect">
<aop:before method="BeforeAdviceMethod" pointcut-ref="pointCut"></aop:before>
</aop:aspect>
执行测试类XMLTest.java
设置优先级aop-xml.xml,里面有个order属性
<!-- 设置另一个切面 order设置优先级-->
<aop:aspect ref="validateAspect" order="1">
<aop:before method="BeforeAdviceMethod" pointcut-ref="pointCut"></aop:before>
</aop:aspect>
再次执行测试类XMLTest.java
4声明式事务
4.1JdbcTemplate
创建新的Module
Name:spring-transaction
GroupId:com.atguigu.spring
4.1.1简介
Spring 框架对 JDBC 进行封装,使用 JdbcTemplate 方便实现对数据库操作
4.1.2准备工作
(1)加入依赖
pom.xml添加打包方式
<packaging>jar</packaging>
pom.xml加入依赖
<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 测试相关 可以不用手动生成IOC容器 -->
<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.12</version>
<scope>test</scope>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.20</version>
</dependency>
<!-- 数据源Druid连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.31</version>
</dependency>
</dependencies>
依赖如下:
(2)创建jdbc.properties
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/ssm
jdbc.username=root
jdbc.password=hsp
(3)配置Spring的配置文件
创建spring-jdbc.xml,因为都是引入的第三方jar包,不是自己写的类,所以不能用扫描组件的方式,要手动配置。
在这里插入代码片
4.1.3测试
(1)在测试类装配 JdbcTemplate
<!--引入jdbc.properties 其中location最好加上classpath: -->
<context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<!-- 这里id的设置可以省略,因为ioc获取当前类,可以通过byType -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
打开sqlyog,操作几张表
截断表t_user
插入一条数据
创建测试类com.atguigu.spring.test.JdbcTemplateTest
(2)测试增删改功能
修改JdbcTemplateTest.java
package com.atguigu.spring.test;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @ClassName: JdbcTemplateTest
* @Description:
* @Author: wty
* @Date: 2023/1/13
*/
// 设置当前类的测试环境:在spring的测试环境中执行,此时就可以通过注入的方式直接获取IOC容器中的bean
@RunWith(SpringJUnit4ClassRunner.class)
// classpath:类路径
@ContextConfiguration("classpath:spring-jdbc.xml")
public class JdbcTemplateTest {
// 自动装配的方式进行属性的注入
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
public void testInsert() {
// jdbcTemplate.update()能实现增删改
String sqlStr = "insert into t_user values(null,?,?,?,?,?)";
int i = jdbcTemplate.update(sqlStr, "hsp", "1234", 22, "男", "hsp@126.com");
System.out.println("增加了:" + i + "条数据");
}
}
执行测试类
看一下数据库
(3)查询一条数据为实体类对象
创建实体类com.atguigu.spring.pojo.User
package com.atguigu.spring.pojo;
/**
* @ClassName: User
* @Description:
* @Author: wty
* @Date: 2023/1/13
*/
public class User {
private Integer id;
private String username;
private String password;
private Integer age;
private String gender;
private String email;
public User() {
}
public User(Integer id, String username, String password, Integer age, String gender, String email) {
this.id = id;
this.username = username;
this.password = password;
this.age = age;
this.gender = gender;
this.email = email;
}
public Integer getId() {
return id;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
public String getPassword() {
return password;
}
public void setPassword(String password) {
this.password = password;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
public String getGender() {
return gender;
}
public void setGender(String gender) {
this.gender = gender;
}
public String getEmail() {
return email;
}
public void setEmail(String email) {
this.email = email;
}
@Override
public String toString() {
return "User{" +
"id=" + id +
", username='" + username + '\'' +
", password='" + password + '\'' +
", age=" + age +
", gender='" + gender + '\'' +
", email='" + email + '\'' +
'}';
}
}
修改测试类JdbcTemplateTest.java
/**
* @param
* @return void
* @description //获取单个对象
* @date 2023/1/13 17:01
* @author wty
**/
@Test
public void getUserByUserId() {
String sqlStr = "select * from t_user where id = ?";
User user = jdbcTemplate.queryForObject(sqlStr, new BeanPropertyRowMapper<>(User.class), 2);
System.out.println(user);
}
测试结果
(4)查询多条数据为一个list集合
在JdbcTemplateTest.java中新增方法
@Test
public void getAllUser() {
String sqlStr = "select * from t_user";
List<User> list = jdbcTemplate.query(sqlStr, new BeanPropertyRowMapper<>(User.class));
list.forEach(System.out::println);
}
并执行测试方法
(5)查询单行单列的值
在JdbcTemplateTest.java中新增方法
@Test
public void getCount() {
String sqlStr = "select count(*) from t_user";
Integer count = jdbcTemplate.queryForObject(sqlStr, Integer.class);
System.out.println(count);
}
查询结果
4.2声明式事务概念
4.2.1编程式事务
事务功能的相关操作全部通过自己编写代码来实现:
Connection conn = null;
try {
// 开启事务:关闭事务的自动提交
conn.setAutoCommit(false);
// 核心操作
// 提交事务
conn.commit();
} catch (Exception e) {
// 回滚事务
conn.rollBack();
} finally {
}
// 释放数据库连接
conn.close();
编程式的实现方式存在缺陷:
- 细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
- 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。
4.2.2声明式事务
既然事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装。
封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作。
- 好处1:提高开发效率
- 好处2:消除了冗余的代码
- 好处3:框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性、性能等各个方面的优化。
所以,我们可以总结下面两个概念:
- 编程式:自己写代码实现功能
- 声明式:通过配置让框架实现功能
4.3基于注解的声明式事务
4.3.1准备工作
(1)加入依赖
沿用上一个项目即可
<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.12</version>
<scope>test</scope>
</dependency>
<!-- MySQL驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.20</version>
</dependency>
<!-- 数据源Druid连接池 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.0.31</version>
</dependency>
</dependencies>
(2)创建jdbc.properties
沿用上一个项目
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/ssm
jdbc.username=root
jdbc.password=hsp
(3)配置Spring的配置文件
创建新的配置文件:tx-annotation
<context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"></property>
<property name="url" value="${jdbc.url}"></property>
<property name="username" value="${jdbc.username}"></property>
<property name="password" value="${jdbc.password}"></property>
</bean>
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 扫描组件 -->
<context:component-scan base-package="com.atguigu.spring"></context:component-scan>
(4)创建表
无符号UNSIGNED可以理解为无符号,从mysql层面解决负数问题
首先删除t_user表,然后执行下面的sql
CREATE TABLE `t_book` (
`book_id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`book_name` VARCHAR(20) DEFAULT NULL COMMENT '图书名称',
`price` INT(11) DEFAULT NULL COMMENT '价格',
`stock` INT(10) UNSIGNED DEFAULT NULL COMMENT '库存(无符号)',
PRIMARY KEY (`book_id`)
) ENGINE=INNODB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
INSERT INTO `t_book`(`book_id`,`book_name`,`price`,`stock`) VALUES (1,'斗破苍
穹',80,100),(2,'斗罗大陆',50,100);
CREATE TABLE `t_user` (
`user_id` INT(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
`username` VARCHAR(20) DEFAULT NULL COMMENT '用户名',
`balance` INT(10) UNSIGNED DEFAULT NULL COMMENT '余额(无符号)',
PRIMARY KEY (`user_id`)
) ENGINE=INNODB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
INSERT INTO `t_user`(`user_id`,`username`,`balance`) VALUES (1,'admin',50);
(5)创建组件
创建BookController:
package com.atguigu.spring.controller;
import com.atguigu.spring.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
/**
* @ClassName: BookController
* @Description:
* @Author: wty
* @Date: 2023/1/14
*/
@Controller
public class BookController {
@Autowired
private BookService bookService;
/**
* @param
* @return void
* @description //模拟用户买书的功能
* @param: userId
* @param: bookId
* @date 2023/1/14 13:19
* @author wty
**/
public void buyBook(Integer userId, Integer bookId) {
bookService.buyBook(userId, bookId);
}
}
创建接口BookService:
package com.atguigu.spring.service;
import org.springframework.stereotype.Service;
/**
* @InterfaceName: BookService
* @Description:
* @Author: wty
* @Date: 2023/1/14
*/
public interface BookService {
void buyBook(Integer userId, Integer bookId);
}
创建实现类BookServiceImpl:
package com.atguigu.spring.service.impl;
import com.atguigu.spring.dao.BookDao;
import com.atguigu.spring.service.BookService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
/**
* @ClassName: BookServiceImpl
* @Description:
* @Author: wty
* @Date: 2023/1/14
*/
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
@Override
public void buyBook(Integer userId, Integer bookId) {
// 查询图书价格
Integer price = bookDao.getPriceByBookId(bookId);
// 更新图书库存
Integer stock = bookDao.updateStock(bookId);
// 更新用户的余额
Integer balance = bookDao.updateBalance(userId, price);
}
}
创建接口BookDao:
package com.atguigu.spring.dao;
/**
* @InterfaceName: BookDao
* @Description:
* @Author: wty
* @Date: 2023/1/14
*/
public interface BookDao {
/**
* @param
* @return java.lang.Integer
* @description //根据图书Id查询价格
* @param: bookId
* @date 2023/1/14 14:24
* @author wty
**/
Integer getPriceByBookId(Integer bookId);
/**
* @param
* @return java.lang.Integer
* @description //更新图书库存
* @param: bookId
* @date 2023/1/14 14:25
* @author wty
**/
Integer updateStock(Integer bookId);
/**
* @param
* @return java.lang.Integer
* @description //更新用户余额
* @param: userId
* @param: price
* @date 2023/1/14 14:25
* @author wty
**/
Integer updateBalance(Integer userId, Integer price);
}
创建实现类BookDaoImpl:
package com.atguigu.spring.dao.impl;
import com.atguigu.spring.dao.BookDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Repository;
/**
* @ClassName: BookDaoImpl
* @Description:
* @Author: wty
* @Date: 2023/1/14
*/
@Repository
public class BookDaoImpl implements BookDao {
@Autowired
private JdbcTemplate jdbcTemplate;
@Override
public Integer getPriceByBookId(Integer bookId) {
String sqlStr = "select price from t_book where book_id = ?";
Integer price = jdbcTemplate.queryForObject(sqlStr, Integer.class, bookId);
return price;
}
@Override
public Integer updateStock(Integer bookId) {
String sqlStr = "update t_book set stock = stock - 1 where book_id = ?";
int stock = jdbcTemplate.update(sqlStr, bookId);
return stock;
}
@Override
public Integer updateBalance(Integer userId, Integer price) {
String sqlStr = "update t_user set balance = balance - ? where user_id = ?";
int balance = jdbcTemplate.update(sqlStr, price, userId);
return balance;
}
}
类图如下:
4.3.2测试无事务情况
(1)创建测试类
创建测试类com.atguigu.spring.test.TxByAnnotationTest
package com.atguigu.spring.test;
import com.atguigu.spring.controller.BookController;
import com.atguigu.spring.pojo.Book;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @ClassName: TxByAnnotationTest
* @Description:
* @Author: wty
* @Date: 2023/1/14
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:tx-annotation.xml")
public class TxByAnnotationTest {
@Autowired
private BookController bookController;
public void test() {
bookController.buyBook(1, 1);
}
}
(2)模拟场景
用户购买图书,先查询图书的价格,再更新图书的库存和用户的余额
假设用户id为1的用户,购买id为1的图书
用户余额为50,而图书价格为80
购买图书之后,用户的余额为-30,数据库中余额字段设置了无符号,因此无法将-30插入到余额字段。此时执行sql语句会抛出SQLException
(3)观察结果
直接运行测试类TxByAnnotationTest.java
t_book的库存减少
t_user的余额没有变更
因为没有添加事务,图书的库存更新了,但是用户的余额没有更新
显然这样的结果是错误的,购买图书是一个完整的功能,更新库存和更新余额要么都成功要么都失败。
4.3.3加入事务
(1)添加事务配置
在Spring的配置文件中添加配置:
tx-annotation.xml中修改
<!-- 扫描组件 -->
<context:component-scan base-package="com.atguigu.spring"></context:component-scan>
<!-- 配置事务管理器 -->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"></property>
</bean>
<!-- 开启事务的注解驱动 -->
<tx:annotation-driven transaction-manager="transactionManager"/>
注意:导入的名称空间需要 tx 结尾的那个。
最后配置文件如下:
(2)添加事务注解
因为service层表示业务逻辑层,一个方法表示一个完成的功能,因此处理事务一般在service层处理
在BookServiceImpl的buybook()添加注解@Transactional
修改数据库中的库存
(3)观察结果
再次执行测试类TxByAnnotationTest.java
库存没有减少
余额也没有变更
由于使用了Spring的声明式事务,更新库存和更新余额都没有执行
(4)声明式事务的配置步骤
- 在spring的配置文件中配置事务管理器
- 开启事务的注解驱动
在需要被事务管理的方法上,添加@Transactional注解,该方法就会被事务管理。
那不禁会问,类上能添加@Transactional注解吗?
4.3.4@Transactional注解标识的位置
@Transactional标识在方法上,只会影响该方法
@Transactional标识的类上,会影响类中所有的方法
@Transactional事务中的属性
4.3.5事务属性:只读
(1)介绍
对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。
(2)使用方式
在@Transactional后添加(readOnly = true)
@Transactional(readOnly = true)
public void buyBook(Integer userId, Integer bookId) {
// 查询图书价格
Integer price = bookDao.getPriceByBookId(bookId);
// 更新图书库存
Integer stock = bookDao.updateStock(bookId);
// 更新用户的余额
Integer balance = bookDao.updateBalance(userId, price);
}
(3)注意
直接在TxByAnnotationTest.java中运行
对增删改操作设置只读会抛出下面异常:
Caused by: java.sql.SQLException: Connection is read-only. Queries
leading to data modification are not allowed
4.3.6事务属性:超时
(1)介绍
事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。
此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。
概括来说就是一句话:超时回滚,释放资源。
(2)使用方式
在@Transactional后增加(timeout = 时间)
这里设置的3,就是3秒的意思
@Transactional(timeout = 3)
public void buyBook(Integer userId, Integer bookId) {
try {
TimeUnit.SECONDS.sleep(5);
} catch (InterruptedException e) {
e.printStackTrace();
}
// 查询图书价格
Integer price = bookDao.getPriceByBookId(bookId);
// 更新图书库存
Integer stock = bookDao.updateStock(bookId);
// 更新用户的余额
Integer balance = bookDao.updateBalance(userId, price);
}
(3)观察结果
执行TxByAnnotationTest.java
执行过程中抛出异常:
org.springframework.transaction.TransactionTimedOutException:
Transaction timed out: deadline was Fri Jun 04 16:25:39 CST 2022
4.3.7事务属性:回滚策略
(1)介绍
声明式事务默认只针对运行时异常回滚,编译时异常不回滚。
可以通过@Transactional中相关属性设置回滚策略
- rollbackFor属性:需要设置一个Class类型的对象
- rollbackForClassName属性:需要设置一个字符串类型的全类名。
- noRollbackFor属性:需要设置一个Class类型的对象
- rollbackFor属性:需要设置一个字符串类型的全类名
(2)使用方式
更改t_user表的余额,更改为100
public void buyBook(Integer userId, Integer bookId) {
// 查询图书价格
Integer price = bookDao.getPriceByBookId(bookId);
// 更新图书库存
Integer stock = bookDao.updateStock(bookId);
// 更新用户的余额
Integer balance = bookDao.updateBalance(userId, price);
System.out.println(1 / 0);
}
因为加了1/0,这种默认无事务的情况下,是不回滚,都执行的,查看结果。
执行TxByAnnotationTest.java
t_user中余额减少
t_book中库存减少
这个时候我们更改BookServiceImpl.java,采用注解@Transactional的默认策略,对任意的运行时异常回滚
@Transactional
public void buyBook(Integer userId, Integer bookId) {
// 查询图书价格
Integer price = bookDao.getPriceByBookId(bookId);
// 更新图书库存
Integer stock = bookDao.updateStock(bookId);
// 更新用户的余额
Integer balance = bookDao.updateBalance(userId, price);
System.out.println(1 / 0);
}
恢复数据库中的数据
再执行TxByAnnotationTest.java
发现库存不变
发现余额也不变
现在想要出现算数异常的时候不回滚,我们怎么办呢,看下面的操作。
(3)观察结果
更改BookServiceImpl.java,注解中用noRollbackFor,意思是算术类型异常不回滚。
@Transactional(noRollbackFor = ArithmeticException.class)
public void buyBook(Integer userId, Integer bookId) {
// 查询图书价格
Integer price = bookDao.getPriceByBookId(bookId);
// 更新图书库存
Integer stock = bookDao.updateStock(bookId);
// 更新用户的余额
Integer balance = bookDao.updateBalance(userId, price);
System.out.println(1 / 0);
}
再执行TxByAnnotationTest.java
查看数据库结果
这个时候我们继续更改BookServiceImpl.java,采用noRollbackForClassName
@Transactional(noRollbackForClassName = "java.lang.ArithmeticException")
public void buyBook(Integer userId, Integer bookId) {
// 查询图书价格
Integer price = bookDao.getPriceByBookId(bookId);
// 更新图书库存
Integer stock = bookDao.updateStock(bookId);
// 更新用户的余额
Integer balance = bookDao.updateBalance(userId, price);
System.out.println(1 / 0);
}
我们先恢复数据库的数据。把库存和余额恢复
再运行测试类TxByAnnotationTest.java查看结果,我们发现还是不会回滚。
虽然购买图书功能中出现了数学运算异常(ArithmeticException),但是我们设置的回滚策略是,当出现ArithmeticException不发生回滚,因此购买图书的操作正常执行。
4.3.8事务属性:事务隔离级别
(1)介绍
数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同
的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。
隔离级别一共有四种:
- 读未提交:READ UNCOMMITTED
允许Transaction01读取Transaction02未提交的修改。 - 读已提交:READ COMMITTED、
要求Transaction01只能读取Transaction02已提交的修改。 - 可重复读:REPEATABLE READ
确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它事务对这个字段进行更新。 - 串行化:SERIALIZABLE
确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。可以避免任何并发问题,但性能十分低下。
各个隔离级别解决并发问题的能力见下表:
隔离级别 | 脏读 | 不可重复读 | 幻读 |
---|---|---|---|
READ UNCOMMITTED | √ | √ | √ |
READ COMMITTED | × | √ | √ |
REPEATABLE READ | × | × | √ |
SERIALIZABLE | × | × | × |
各种数据库产品对事务隔离级别的支持程度:
隔离级别 | Oracle | MySQL |
---|---|---|
READ UNCOMMITTED | × | √ |
READ COMMITTED | √ | √ |
REPEATABLE READ | × | √(默认) |
SERIALIZABLE | √ | √ |
(2)使用方式
@Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别
@Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交
@Transactional(isolation = Isolation.READ_COMMITTED)//读已提交
@Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读
@Transactional(isolation = Isolation.SERIALIZABLE)//串行化
枚举类型
4.3.9事务属性:事务传播行为
(1)介绍
当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
(2)测试
创建接口CheckoutService:
package com.atguigu.spring.service;
/**
* @InterfaceName: CheckOutService
* @Description:
* @Author: wty
* @Date: 2023/1/15
*/
public interface CheckOutService {
void checkOut(Integer userId, Integer[] bookIds);
}
创建实现类CheckoutServiceImpl:注意checkOut方法是@Transactional修饰的。
/**
* @ClassName: CheckOutServiceImpl
* @Description:
* @Author: wty
* @Date: 2023/1/15
*/
@Service
public class CheckOutServiceImpl implements CheckOutService {
@Autowired
private BookService bookService;
@Override
@Transactional
public void checkOut(Integer userId, Integer[] bookIds) {
for (Integer bookId : bookIds) {
bookService.buyBook(userId, bookId);
}
}
}
在BookController中添加方法:
/**
* @param
* @return void
* @description //结账
* @date 2023/1/15 17:57
* @author wty
**/
public void checkOut(Integer userId, Integer[] bookIds) {
checkOutService.checkOut(userId, bookIds);
}
恢复BookServiceImpl.java中的信息,不要抛出异常。注意buyBook方法是@Transactional修饰的。
/**
* @ClassName: BookServiceImpl
* @Description:
* @Author: wty
* @Date: 2023/1/14
*/
@Service
public class BookServiceImpl implements BookService {
@Autowired
private BookDao bookDao;
@Override
@Transactional(isolation = Isolation.DEFAULT)
public void buyBook(Integer userId, Integer bookId) {
// 查询图书价格
Integer price = bookDao.getPriceByBookId(bookId);
// 更新图书库存
Integer stock = bookDao.updateStock(bookId);
// 更新用户的余额
Integer balance = bookDao.updateBalance(userId, price);
}
}
在数据库中将用户(t_user)的余额修改为100元
在数据库中将图书(t_book)的库存修改为100本
在测试类TxByAnnotationTest.java中添加方法
@Test
public void test2() {
Integer[] nums = {1, 2};
bookController.checkOut(1, nums);
}
(3)观察结果
运行测试类为TxByAnnotationTest.java
看一下数据库的情况
t_user无变化
t_book无变化
无变化,说明即在checkOut中设置了事务,又在buyBook方法上设置了事务,最后有效的是checkOut中的,这保证了,所有书要买就都得成功,要有一本书买不了,所有书都有问题,那么我们如果想更改一下呢,只让事务对buyBook有效,每本书各买各的。
修改BookServiceImpl.java,增加注解的属性
@Transactional(isolation = Isolation.DEFAULT, propagation = Propagation.REQUIRES_NEW)
再次执行测试类TxByAnnotationTest.java
可以通过@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的声明式事务
4.4.1场景模拟
参考基于注解的声明式事务
删除(注释)CheckOutServiceImpl.java中checkOut方法的注解@Transactional
删除(注释)BookServiceImpl.java中buyBook方法的注解@Transactional
4.3.2修改Spring配置文件
新建tx-xml.xml配置文件,大体拷贝以前的配置文件tx-xml.xml即可。
将Spring配置文件中去掉tx:annotation-driven 标签,并添加配置:
<!--
配置事务的通知
id 设置事务通知的唯一标识
transaction-manager:设置事务管理器的id,如果事务管理器的id是transactionManager,那么下面不用写
-->
<tx:advice id="tx" transaction-manager="transactionManager">
<tx:attributes>
<tx:method name="buyBook"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:advisor advice-ref="tx" pointcut="execution(* com.atguigu.spring.service.impl.*.*(..))"></aop:advisor>
</aop:config>
在这里插入代码片
在数据库中将用户(t_user)的余额修改为100元
在数据库中将图书(t_book)的库存修改为100本
新建测试类TxByXmlTest.java,大体拷贝TxByAnnotationTest.java即可
执行测试类的test方法,报错
package com.atguigu.spring.test;
import com.atguigu.spring.controller.BookController;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* @ClassName: TxByAnnotationTest
* @Description:
* @Author: wty
* @Date: 2023/1/14
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:tx-xml.xml")
public class TxByXmlTest {
@Autowired
private BookController bookController;
@Test
public void test() {
bookController.buyBook(1, 1);
}
@Test
public void test2() {
Integer[] nums = {1, 2};
bookController.checkOut(1, nums);
}
}
注意:基于xml实现的声明式事务,必须引入aspectJ的依赖
在pom.xml中添加依赖
<!-- 导入aspectJ的依赖 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>5.3.1</version>
</dependency>
此时再执行测试类的test方法,发现正常执行,没有问题。
库存减少
余额减少
那每次配置文件tx-xml.xml都要配置方法,根据不同方法配置事务很麻烦,我们想到了根据方法名的规则来统一管理
<!--
配置事务的通知
id 设置事务通知的唯一标识
transaction-manager:设置事务管理器的id,如果事务管理器的id是transactionManager,那么下面不用写
-->
<tx:advice id="tx" transaction-manager="transactionManager">
<tx:attributes>
<!-- 如果所有方法都要事务管理,就用* -->
<!--<tx:method name="*"/>-->
<tx:method name="buyBook"/>
<!-- 如果以get、query、find开头的方法,都是查询只读 -->
<tx:method name="get*" read-only="true"></tx:method>
<tx:method name="query*" read-only="true"></tx:method>
<tx:method name="find*" read-only="true"></tx:method>
<!-- read-only属性:设置只读属性 -->
<!-- rollback-for属性:设置回滚的异常 -->
<!-- no-rollback-for属性:设置不回滚的异常 -->
<!-- isolation属性:设置事务的隔离级别 -->
<!-- timeout属性:设置事务的超时属性 -->
<!-- propagation属性:设置事务的传播行为 -->
<!-- 如果以update开头的方法,都以修改 -->
<tx:method name="update*" read-only="false" rollback-for="java.lang.Exception"
propagation="REQUIRES_NEW"></tx:method>
<!-- 如果以delete开头的方法,都是删除 -->
<tx:method name="delete*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
<!-- 如果以save开头的方法,都是插入 -->
<tx:method name="save*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:advisor advice-ref="tx" pointcut="execution(* com.atguigu.spring.service.impl.*.*(..))"></aop:advisor>
</aop:config>