SSM速通2
在进行新章节------AOP学习之前,补充 spring 自动提供的测试注解------@SpringbootTest
@SpringBootTest 是 Spring Boot 提供的一个用于集成测试的注解,它能够启动一个完整的 Spring 应用上下文(ApplicationContext),并加载所有相关的配置,使得我们可以对应用程序的不同层(如 Controller、Service、Repository)进行测试。
以下给出基本用法:
java
@SpringBootTest
class MyApplicationTests {
@Autowired
private MyService myService;
@Test
void testServiceMethod() {
String result = myService.doSomething();
assertThat(result).isEqualTo("expectedResult");
}
}
默认情况下,
@SpringBootTest会加载整个 Spring Boot 应用,包括所有@Component、@Service、@Repository、@Controller等 Bean。即当我们只需要测试一个小方法,只需将 @SpringbootTest 注解注释掉即可,如此便不需要启动完整的 Spring Boot 应用就可测试其中的部分方法
三、AOP
AOP(面向切面编程)是 Spring 框架的核心功能之一,它允许你将横切关注点(如日志、事务、安全等)与业务逻辑分离,提高代码的模块化程度。
AOP 的目的是提高代码的模块化程度,即在不改变原有业务逻辑的代码的情况下(即进行非侵入式编程),动态地添加或修改程序的执行逻辑的一种思想。
1、核心概念
- Aspect(切面):横跨多个类的模块化关注点(如日志模块)
- Join point(连接点) :程序执行过程中能够插入切面的"点"(方法调用、异常抛出、字段访问...)
- Pointcut(切点):匹配连接点的表达式,定义"在哪些连接点"插入通知
- Weaving(织入):将切面应用到目标对象的过程
- Advice(通知):在特定连接点执行的动作,切面里真正要执行的代码
- Target(目标对象) :真正执行业务逻辑、被代理的原始 Bean
精简总结:
- 切面是所有跟业务无关、却到处都要用的重复代码 (日志、事务、权限、监控......)抽成一个可复用的模块
- 连接点是程序执行过程中"理论上"可以插入切面的任何点
- 切点是连接点中真正选择插入切面的点 ,即 目标对象里的方法
- 织入是将切面插入的过程
- 通知是插入的切面所要执行的代码 ,装填通知的类就是切面类(带 @Aspect 的类)
- 目标对象是真正执行业务逻辑、被代理的原始 Bean
2、使用
先总结 AOP 动态织入的实现过程(以Maven工程为例)
- 导入 AOP 依赖
- 编写切面 Aspect
- 编写通知方法
- 指定切入点表达式
- 测试 AOP 动态织入
首先在 pom.xml 中加入 spring 框架-aop 依赖
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
接着编写切面类,加 @Aspect
java
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect // 声明这是一个切面
@Component // 交给 Spring 管理,注入到 aoc 容器
public class TestAspect {
}
然后在我们的 TestAspect 切面类中添加通知方法
java
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect // 声明这是一个切面
@Component // 交给 Spring 管理,注入到 aoc 容器
public class TestAspect {
public void method1(){
// 填写方法1
}
public void method2(){
// 填写方法2
}
public void method3(){
// 填写方法3
}
}
指定切入点表达式
2.1、切入点表达式细节
①、通知类型和增强型注解
"通知类型"是干活的(5 种),"增强型注解"是打辅助的(2 种);前者决定"什么时候切入",后者决定"切哪儿、谁先切"。
如果只有增强型注解(如@Pointcut),而不含任意一种的通知类型,应用启动正常,但没有任何代码会被织入
五大核心通知类型
| 注解 | 执行时机 | 能否阻止目标方法 | 常用参数 | 一句话记忆 | 典型场景 |
|---|---|---|---|---|---|
| @Before | 目标方法开始前 | ❌ | JoinPoint | "前置埋点" | 参数校验、日志、权限 |
| @After | 目标方法结束后(无论成败) | ❌ | JoinPoint | "finally 块" | 清理资源、释放锁 |
| @AfterReturning | 正常返回后 | ❌ | JoinPoint + returning | "成功回调" | 结果日志、缓存、发事件 |
| @AfterThrowing | 异常抛出后 | ❌ | JoinPoint + throwing | "失败回调" | 异常告警、降级 |
| @Around | 包裹整个调用 | ✅(可不调 proceed) | ProceedingJoinPoint | "全能代理" | 事务、重试、计时、限流 |
两个"增强型"注解
| 注解 | 作用 | 使用位置 | 举例 |
|---|---|---|---|
| @Pointcut | 复用切点表达式 | 空方法 | @Pointcut("execution(* com.service..*(..))") |
| @Order(int) | 控制多切面执行顺序 | 切面类 | @Order(1) 数字越小越外层,即越先执行 |
切入点表达式语法骨架:
execution(修饰符 如public、private、provided等)? 返回的数据类型 声明类型? 方法所在包全签名.方法(参数) 异常?)
? 表示可省略,其余部分用通配符、注解、类型匹配等组合定位。
| 元素 | 写法示例 | 说明 |
|---|---|---|
| 修饰符 | public / * / 省略 |
* 代表任意修饰符 |
| 返回类型 | String / void / * |
* 任意返回 |
| 声明类型(包+类) | com.demo.service.UserService / com.demo.service.* / com.demo..* |
. 单层包,.. 多层包/任意参数 |
| 方法名 | save* / *save* / * |
前缀/后缀/任意 |
| 参数 | (..) 任意参数 (String,*) 前两参固定 (String,..) 首参固定,后面任意 |
.. 在参数里表示"0~N 个" |
| 异常 | throws * / throws RuntimeException |
可省略 |
java
execution(* com.demo.service..*(..)) // service 及子包下所有方法
execution(public * com.demo..*Controller.*(..)) // 任意 Controller 的 public 方法
execution(* com.demo.service.*Service.save*(String,int))
// 以 save 开头且参数一个是 String 类型,另一个是 int 类型
举例:
java
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
@Aspect // 声明这是一个切面
@Component // 交给 Spring 管理,注入到 aoc 容器
public class TestAspect {
// 使用增强型注解
// 使用了增强型注解,为了可以让他人使用,需要封装一个空方法
@Pointcut("execution(* com.service..*(..))")
public void kongff(){} //之后有要使用方法全签名可直接调用这个空方法即可
@Before("kongff()")
public void method1(){
// 填写方法1
}
@Around("kongff()")
public void method2(){
// 填写方法2
}
@After("kongff()")
public void method3(){
// 填写方法3
}
}
②、常用切点
Ⅰ、execution 最常用------方法级别
| 元素 | 写法示例 | 说明 |
|---|---|---|
| 修饰符 | public / * / 省略 |
* 代表任意修饰符 |
| 返回类型 | String / void / * |
* 任意返回 |
| 声明类型(包+类) | com.demo.service.UserService / com.demo.service.* / com.demo..* |
. 单层包,.. 多层包/任意参数 |
| 方法名 | save* / *save* / * |
前缀/后缀/任意 |
| 参数 | (..) 任意参数(String,*) 前两参固定(String,..) 首参固定,后面任意 |
.. 在参数里表示"0~N 个" |
| 异常 | throws * / throws RuntimeException |
可省略 |
java
execution(* com.demo.service..*(..)) // service 及子包下所有方法
execution(public * com.demo..*Controller.*(..)) // 任意 Controller 的 public 方法
execution(* com.demo.service.*Service.save*(String,..)) // 以 save 开头且首参 String
Ⅱ、within------类级别(粗粒度)
within(包+类):匹配整个类的所有方法。
java
within(com.demo.service.UserService) // 只切这个类
within(com.demo.service..*) // service 及子包所有类
within(@org.springframework.stereotype.Service *) // 类上有 @Service 注解
Ⅲ、@annotation------注解级别
@annotation(注解全限定名):方法被标了指定注解就切。
java
@annotation(com.demo.anno.LogSlow) // 方法级
// 即方法上有 @LogSlow 这个注解就切(实现通知方法)
@within(org.springframework.stereotype.Service) // 类级
Ⅳ、this / target / args------类型/参数级
| 关键字 | 含义 | 示例 |
|---|---|---|
| this | 代理对象类型 | this(com.demo.service.MyInterface) |
| target | 目标对象类型 | target(com.demo.service.MyInterface) |
| args | 运行时参数类型 | args(String,Long) 两个参数且类型匹配 |
之后正常编写业务类即可
2.2、通知方法执行顺序
①、正常链路
前置通知------>目标方法------>返回通知------>后置通知
②、异常链路
前置通知------>目标方法------>返回异常通知------>后置通知
2.3、连接点
连接点即程序执行过程中能够插入切面的"点"(方法调用、异常抛出、字段访问...),实际插入了切面的连接点就是切点
我们可以通过在通知方法中传连接点获取其中的连接点信息
java
@Aspect // 声明这是一个切面
@Component // 交给 Spring 管理,注入到 aoc 容器
public class TestAspect {
// 使用增强型注解
// 使用了增强型注解,为了可以让他人使用,需要封装一个空方法
@Pointcut("execution(* com.service..*(..))")
public void kongff(){} //之后有要使用方法全签名可直接调用这个空方法即可
@Before("kongff()")
public void method1(JointPoint joinpoint){
// 获取方法的全签名
Methodsignature signature = (Methodsignature) joinPoint.getsignature();
// 获取调用当前切面的方法名,注意不是 method1
String name = signature.getNameO;
// 填写方法1
}
@AfterThrowing(value = "kongff()",throwing e)
// 通知类型中新加了参数,其值和通知方法中的参数名称要相同,即throwing后的值和Exception后的名称相同
public void method2(JointPoint joinpoint,Exception e){
// 注意其中的连接点和其他参数的先后顺序,连接点建议放前面,否则可能报错
// 获取方法的全签名
Methodsignature signature = (Methodsignature) joinPoint.getsignature();
// 获取调用当前切面的方法名
String name = signature.getNameO;
// 获取异常报错信息
String error = e.getMessage();
// 填写方法2
}
@After("kongff()")
public void method3(){
// 填写方法3
}
}
通知类型后的参数可以是value(即execution那部分切点),还可以有多个,比如@AfterThrowing中还有throwing(用来接收你的异常信息),@AfterReturning中还有returning(用来接收返回信息)等等
3、底层原理
简单来说就是:
- Spring为每个被切面切入的组件使用spring CGLIB创建代理对象,可在没有接口的时候实现方法
- 代理对象中保存了切面类中定义的所有通知方法,将这些通知方法构成增强器链
- 每次目标方法执行前都会去增强器链中获取要执行的通知方法
四、事务
Spring 框架提供了强大而灵活的事务管理支持,这是企业应用开发中的核心功能之一。
1、引入代码
以连接数据库(包含book和user表)后实现对数据库的操作为例,需完成的实验如下:
-
按照id查询图书
-
新增一个图书
-
按照username修改用户的年龄
-
按照id删除图书
-
编写完整的书籍易主,即书换主人的完整业务
1.1、连接数据库
要进行事务的操作,前提是我们已经整合数据源,连接数据库
以下提供两种方法:
- application.properties中进行配置数据库的连接信息
- 直接使用idea右侧的数据库连接

1.2、创建包并写业务
在创建包之前,我先给出我自我理解的架构,不同于ssm1中给出的,这个包含了我的自我理解,所以不一定要读者理解,可能部分会理解错误

①、entity包
entity包根据数据库创建的表的列进行类的创建,其中的数据类型要使用包装类


补充:由于Lombok的@Data注解并不会为类生成带参构造器,所以后面我在book和user类什么添加了@AllArgsConstructor@NoArgsConstructor两个注解,帮助我们完成类对应的带参构造
②、dao包
dao包主要是对数据库进行操作的,其内部也是支持接口加其对应的实现类的结构
dao接口要进行的是将每个要进行的操作进行封装为方法,调用即可,其方法的内部实现是在对应的接口的实现类中进行的

③、service包
service包符合接口+实现类的结构,实现类实现接口的方法,service层主要是完整执行你要进行的业务

1.3、创建测试类
在编写好了相应的业务后,为了测试业务是否正确,应该编写测试类进行测试,以下给出一个简单的测试类,只测试其中的通过id查询书和插入书籍
Java
@SpringBootTest
class Spring03TxApplicationTests {
@Autowired
DataSource dataSource;
@Autowired
BookDao bookDao;
@Autowired
ChangeMaster changeMaster;
// @Test
// void contextLoads() throws SQLException {
// System.out.println(dataSource.getConnection());
// }
@Test
void testBookDao() {
Book book1 = bookDao.QueryBookById(1);
selectBookById(book1.getBookId());
Book book = new Book(1001, "这是测试书", new BigDecimal("99.99"), 1001);
bookDao.InsertBook(book);
Book book2 = bookDao.QueryBookById(1001);
selectBookById(book2.getBookId());
changeMaster.changeMaster(1001, 1);
}
void selectBookById(Integer id) {
Book book = bookDao.QueryBookById(id);
System.out.println("查询到的书的id是:" + book.getBookId());
System.out.println("查询到的书的名称是:" + book.getName());
System.out.println("查询到的书的价格是:" + book.getPrice());
System.out.println("查询到的书的所有者id是:" + book.getOwnerId());
}
}
以下是并未实现书籍易主的book表的变化以及测试类打印情况

以下是书籍易主的变化

1.4、开启事务
以上引入代码都是默认没有开启事务的,即不具备原子性,无法做到同时成功同时失败,当book表变化时,如果在更新user表时可能遇到错误无法随着book表的变化而变化,如此就会导致你的book表更新了而user表更新失败,即你的数据库可能出现错误。
要想解决这个问题就可以通过开启事务来保证二者的原子性
- 首先在你的Application中添加@EnableTransactionManagement // 开启基于注解的自动化事务
- 其次在你的实现类中将要开启事务的方法进行添加注解@Transactional
以上就是开启事务的方法
2、事务基本概念
事务是指一系列操作要么全部成功执行,要么全部不执行,保证数据的一致性和完整性。事务具有 ACID 特性:
- 原子性(Atomicity):事务是不可分割的工作单位
- 一致性(Consistency):事务执行前后数据保持一致状态
- 隔离性(Isolation):多个事务并发执行时互不干扰
- 持久性(Durability):事务提交后对数据的改变是永久的
和数据库事务的比对异同:
①、相同点
- 核心概念一致:都遵循 ACID 特性(原子性、一致性、隔离性、持久性)
- 最终执行者:Spring 事务最终会最终委托给数据库事务来实现
- 隔离级别:隔离级别的定义与数据库隔离级别概念相同
②、不同点
| 对比维度 | 数据库事务 | Spring 事务 |
|---|---|---|
| 作用范围 | 单个数据库连接内的操作 | 可以跨多个数据库、消息队列等资源(通过 JTA 实现分布式事务) |
| 管理方式 | 通过 SQL 语句(BEGIN, COMMIT, ROLLBACK) | 通过编程或声明式方式管理 |
| 抽象层次 | 底层实现 | 高层抽象,可支持不同的事务 API(JDBC、Hibernate、JPA等) |
| 传播行为 | 不支持复杂传播行为 | 支持 7 种传播行为(REQUIRED, REQUIRES_NEW 等) |
| 异常处理 | 依赖数据库错误码 | 可以通过异常类型配置回滚规则 |
Spring 事务的本质:Spring 事务实际上是对数据库事务的增强和扩展,即直接使用数据库的事务操作可能更直接高效,但 Spring 事务为复杂应用提供了更强大的管理能力。
3、@Transactional细节
3.1、传播行为 (propagation)
即控制大事务和小事务之间的绑定关系
REQUIRED (默认): 如果当前没有事务,就新建一个事务;如果已存在事务,就加入该事务
REQUIRES_NEW: 新建事务,如果当前存在事务,把当前事务挂起
SUPPORTS: 如果当前存在事务,就加入该事务;如果没有事务,就以非事务方式执行
NOT_SUPPORTED: 以非事务方式执行,如果当前存在事务,就把当前事务挂起
MANDATORY: 必须在一个已有的事务中执行,否则抛出异常
NEVER: 必须不在事务中执行,否则抛出异常
NESTED: 如果当前存在事务,则在嵌套事务内执行
java
// 先在一个类1中定义以下method1和method2
// 将method1和大事务绑定
@Transactional(propagation = Propagation.REQUIRED)
void method1(){
}
// 将method2和大事务不绑定
@Transactional(propagation = Propagation.REQUIRES_NEW)
void method2(){
}
// 在类2中调用类1的两个方法,此时我的method就是大事务
// 当大事务炸了之后,method1由于和大事务绑定也会炸,method2不会而是继续执行
@Transactional(propagation = Propagation.REQUIRES_NEW)
void method(){
method1();
method2();
}
以下再给出尚硅谷老师的例子:
java
A {
B(){ //REQUIRED
F(); // REQUIRES_NEW
G(); // REQUIRED
H(); // REQUIRES_NEW
}
C(){ // REQUIRES_NEW
I(); // REQUIRES_NEW
J(); // REQUIRED
}
D(){ // REQUIRES_NEW
K(); // REQUIRES_NEW
L(); // REQUIRES_NEW // 点位2:此处加int i = 10/0; K,F,H,C(i,j) = ok,E整个代码走不到,剩下炸
}
E(){ // REQUIRED
M(); // REQUIRED
// 点位3:此处加int i = 10/0; F,H,C(i,j),D(K,L)= ok
N(); // REQUIRES_NEW
}
// 点位1: 此处加int i = 10/0; C(I, J) ,D(K, L) , F, H, N= ok
}
可见当小事务炸会导致异常的传递,传递给大事务,进而影响同一个大事务中的其他小事务,除了使用了REQUIRES_NEW的小事务
同时如果小事务(REQUIRED)和大事务公用,则该小事务的自身属性失效,而使用大事务的属性
3.2、隔离级别 (isolation)
该隔离级别和数据库的隔离级别相同
①、并发事务问题
并发事务问题即两个事务同时对同一数据的操作时会出现的问题
| 问题 | 描述 |
|---|---|
| 脏读 | 一个事务读到另外一个事务还没有提交的数据。 |
| 不可重复读 | 一个事务先后读取同一条记录,但两次读取的数据不同,称之为不可重复读。 |
| 幻读 | 一个事务按照条件查询数据时,没有对应的数据行,但是在插入数据时,又发现这行数据已经存在,好像出现了"幻影"。 |

②、隔离级别的分类
DEFAULT: 使用底层数据库的默认隔离级别
READ_UNCOMMITTED: 读未提交
READ_COMMITTED: 读已提交
REPEATABLE_READ: 可重复读
SERIALIZABLE: 串行化
| 隔离级别 | 脏读 | 不可重复读 | 幻读 |
|---|---|---|---|
| Read uncommitted | √ | √ | √ |
| Read committed | × | √ | √ |
| Repeatable Read(默认) | × | × | √ |
| Serializable | × | × | × |
mysql
# 以下是MySQL中的隔离级别的操作
# 查看事务隔离级别
SELECT @@TRANSACTION_ISOLATION;
# 设置事务隔离级别
SET [SESSION|GLOBAL] TRANSACTION ISOLATION LEVEL {READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE}
java
// 以下是Java中的隔离级别操作
@Transactional(isolation = Isolation.{READ UNCOMMITTED | READ COMMITTED | REPEATABLE READ | SERIALIZABLE})
// 从以上隔离级别选一种即可
性能排序(从高到低):
- Read uncommitted
- Read committed
- Repeatable Read(默认)
- Serializable
隔离级别排序(从高到低):
- Serializable
- Repeatable Read(默认)
- Read committed
- Read uncommitted
3.3、超时 (timeout)
事务超时时间(秒),默认-1表示使用底层事务系统的默认超时,即超过我设置的超时时间就认为是失败,触发事务的回滚
注意:超时时间范围是从该方法开始到最后一次操作数据库即最后的dao操作,而不是到该方法结束
3.4、只读 (readOnly)
默认为false,设置为true表示该事务只读取数据但不修改数据
3.5、回滚规则
该回滚规则指定哪些异常触发回滚
异常分两类:
- 运行时异常(必须要运行了才知道有异常)比如数学运算的10/0
- 编译时异常(没有运行就已经知道有异常)
默认回滚策略:运行时异常回滚,编译时异常不回滚
①、rollbackFor, rollbackForClassName
使用手动的设置回滚规则就可以指定哪些异常进行回滚,即加了显示的回滚策略,运行时异常+额外指定的异常均进行回滚
rollbackFor后面加异常的类
rollbackForClassName后面加的是字符串类型的异常的全类名
以下给出一个简单的例子:
java
@Transactional(timeout = 3, rollbackFor = {IOException.class}, rollbackForClassName = {java.lang.Exception})
// 即被该注解注释的方法,其设置的超时时间为3s,超过3s默认失败,进行事务的回滚,以及当出现运行时异常和IOException的异常、异常全类名为java.lang.Exception的异常也进行事务的回滚
②、noRollbackFor, noRollbackForClassName
使用以上的no回滚规则就可以指定哪些异常不进行回滚,即加了显示的回滚策略,编译时异常+额外指定的异常均不进行回滚
3.6、事务管理器(PlatformTransactionManager)
事务管理器是 Spring 事务抽象的核心组件,负责协调和管理事务的创建、提交和回滚。
Spring 事务管理的核心接口是 PlatformTransactionManager,主要定义了三个方法:
java
public interface PlatformTransactionManager {
// 获取事务状态
TransactionStatus getTransaction(@Nullable TransactionDefinition definition);
// 提交事务
void commit(TransactionStatus status);
// 回滚事务
void rollback(TransactionStatus status);
}
其底层包含:
事务管理器TransactionManager(接口):控制提交和回滚
事务拦截器TransactionInterceptor(切面):控制何时提交和回滚
-
completeTransactionAfterThrowing(txInfo, ex):抛异常时在这时候调用事务管理器的回滚
-
commitTransactionAfterReturning(txInfo):在这个时候调用事务管理器的提交