SSM速通2

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工程为例)

  1. 导入 AOP 依赖
  2. 编写切面 Aspect
  3. 编写通知方法
  4. 指定切入点表达式
  5. 测试 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、底层原理

简单来说就是:

  1. Spring为每个被切面切入的组件使用spring CGLIB创建代理对象,可在没有接口的时候实现方法
  2. 代理对象中保存了切面类中定义的所有通知方法,将这些通知方法构成增强器链
  3. 每次目标方法执行前都会去增强器链中获取要执行的通知方法

四、事务

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表更新失败,即你的数据库可能出现错误。

要想解决这个问题就可以通过开启事务来保证二者的原子性

  1. 首先在你的Application中添加@EnableTransactionManagement // 开启基于注解的自动化事务
  2. 其次在你的实现类中将要开启事务的方法进行添加注解@Transactional

以上就是开启事务的方法

2、事务基本概念

事务是指一系列操作要么全部成功执行,要么全部不执行,保证数据的一致性和完整性。事务具有 ACID 特性:

  • 原子性(Atomicity):事务是不可分割的工作单位
  • 一致性(Consistency):事务执行前后数据保持一致状态
  • 隔离性(Isolation):多个事务并发执行时互不干扰
  • 持久性(Durability):事务提交后对数据的改变是永久的
和数据库事务的比对异同:
①、相同点
  1. 核心概念一致:都遵循 ACID 特性(原子性、一致性、隔离性、持久性)
  2. 最终执行者:Spring 事务最终会最终委托给数据库事务来实现
  3. 隔离级别:隔离级别的定义与数据库隔离级别概念相同
②、不同点
对比维度 数据库事务 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)

    在这个时候调用事务管理器的提交

相关推荐
qq_12498707532 小时前
基于协同过滤算法的运动场馆服务平台设计与实现(源码+论文+部署+安装)
java·大数据·数据库·人工智能·spring boot·毕业设计·计算机毕业设计
大飞哥~BigFei2 小时前
自定义注解记录接口切面log日志入库优化
java
人道领域2 小时前
javaWeb从入门到进阶(maven高级进阶)
java·spring·maven
一路向北⁢2 小时前
Spring Boot 3 整合 SSE (Server-Sent Events) 企业级最佳实践(一)
java·spring boot·后端·sse·通信
风象南2 小时前
JFR:Spring Boot 应用的性能诊断利器
java·spring boot·后端
爱吃山竹的大肚肚2 小时前
微服务间通过Feign传输文件,处理MultipartFile类型
java·spring boot·后端·spring cloud·微服务
_周游2 小时前
Java8 API文档搜索引擎_使用内存缓冲区优化
java·搜索引擎·intellij-idea
twj_one2 小时前
java中23种设计模式
java·开发语言·设计模式
tsyjjOvO3 小时前
JDBC(Java Database Connectivity)
java·数据库