Spring与SpringFramework

广义的 Spring:Spring 技术栈(全家桶)

广义上的 Spring 泛指以 Spring Framework 为基础的 Spring 技术栈。

经过十多年的发展,Spring 已经不再是一个单纯的应用框架,而是逐渐发展成为一个由多个不同子项目(模块)组成的成熟技术,例如 Spring Framework、Spring MVC、SpringBoot、Spring Cloud、Spring Data、Spring Security 等,其中 Spring Framework 是其他子项目的基础。

这些子项目涵盖了从企业级应用开发到云计算等各方面的内容,能够帮助开发人员解决软件发展过程中不断产生的各种实际问题,给开发人员带来了更好的开发体验。

狭义的 Spring:Spring Framework(基础框架)

狭义的 Spring 特指 Spring Framework,通常我们将它称为 Spring 框架。

Spring Framework(Spring框架)是一个开源的应用程序框架,由SpringSource公司开发,最初是为了解决企业级开发中各种常见问题而创建的。它提供了很多功能,例如:依赖注入(Dependency Injection)、面向切面编程(AOP)、声明式事务管理(TX)等。其主要目标是使企业级应用程序的开发变得更加简单和快速,并且Spring框架被广泛应用于Java企业开发领域。

Spring全家桶的其他框架都是以SpringFramework框架为基础

SpringFramework主要功能

功能模块 功能介绍
Core Container 核心容器,在 Spring 环境下使用任何功能都必须基于 IOC 容器。
AOP&Aspects 面向切面编程
TX 声明式事务管理。
Spring MVC 提供了面向Web应用程序的集成功能。

Spring IoC容器

IoC(Inversion of Control)控制反转,对象创建责任的反转,在spring中BeanFacotory是IoC容器的核心接口,负责实例化,定位,配置应用程序中的对象及建立这些对象间的依赖。XmlBeanFacotory实现BeanFactory接口,通过获取xml配置文件数据,组成应用对象及对象间的依赖关系
spring中有三种注入方式,一种是set注入,一种是接口注入,另一种是构造方法注入

IOC,字面理解是控制反转,即对象的控制权被反转了(是什么)。之前一个对象中依赖另一个对象,需要自己new出来,当对象间的依赖关系非常复杂时,这个过程就变得很繁琐,并且代码间的耦合会很高。现在可以通过Ioc容器来管理控制对象的生成,可以把对象的实例化过程简单化,代码间解耦(为什么)。具体可以从DI(Dependency Injection) DL(Dependency Lookup)两个角度理解Ioc。DI中注入的方式包括属性,构造器,setter注入,DL含义是通过容器的API来查找所依赖的资源和协作对象,从Ioc容器维护的bean map中取出来(怎么做)

组件和组件管理

整个项目就是由各种组件搭建而成的,原本使用组件需要先new出来并赋值才能使用,现在有了Spring之后组件可以完全交给Spring框架进行管理,Spring框架替代了程序员原有的new对象和对象属性赋值动作等

Spring具体的组件管理动作包含:

  • 组件对象实例化
  • 组件属性属性赋值
  • 组件对象之间引用
  • 组件对象存活周期管理

我们只需要编写元数据(配置文件)告知Spring管理哪些类组件和他们的关系即可!

注意:组件是映射到应用程序中所有可重用组件的Java对象,应该是可复用的功能对象!

  • 组件一定是对象
  • 对象不一定是组件

综上所述,Spring 充当一个组件容器,创建、管理、存储组件,减少了我们的编码压力,让我们更加专注进行业务编写!

Spring IoC容器和容器实现

普通和复杂容器

普通容器只能用来存储,没有更多功能。

  • 数组
  • 集合:List
  • 集合:Set

复杂容器可以持续管理组件,从产生到销毁

如:Servlet容器能够管理Servlet(init,service,destroy)、Filter、Listener这样的组件的一生,所以它是一个复杂容器。

名称 时机 次数
创建对象 默认情况:接收到第一次请求 修改启动顺序后:Web应用启动过程中 一次
初始化操作 创建对象之后 一次
处理请求 接收到请求 多次
销毁操作 Web应用卸载之前 一次

SpringIoC容器也是一个复杂容器。它们不仅要负责创建组件的对象、存储组件的对象,还要负责调用组件的方法让它们工作,最终在特定情况下销毁组件。
Spring管理组件的容器,就是一个复杂容器,不仅存储组件,也可以管理组件之间依赖关系,并且创建和销毁组件等

IoC容器具体接口和实现类

SpringIoc容器接口
BeanFactory 接口提供了一种高级配置机制,能够管理任何类型的对象,它是SpringIoC容器标准化超接口!
ApplicationContextBeanFactory 的子接口。它扩展了以下功能:

  • 更容易与 Spring 的 AOP 功能集成

  • 消息资源处理(用于国际化)

  • 特定于应用程序给予此接口实现,例如Web应用程序的 WebApplicationContext

    简而言之,BeanFactory 提供了配置框架和基本功能,而 ApplicationContext 添加了更多特定于企业的功能ApplicationContextBeanFactory的完整超集!

ApplicationContext容器实现类

类型名 简介
ClassPathXmlApplicationContext 通过读取类路径下的 XML 格式的配置文件创建 IOC 容器对象
FileSystemXmlApplicationContext 通过文件系统路径读取 XML 格式的配置文件创建 IOC 容器对象
AnnotationConfigApplicationContext 通过读取Java配置类创建 IOC 容器对象
WebApplicationContext 专门为 Web 应用准备,基于 Web 环境创建 IOC 容器对象,并将对象引入存入 ServletContext 域中。

SpringIoC容器管理配置方式

Spring IoC容器使用多种形式的配置元数据。此配置元数据表示作为应用程序开发人员如何告诉Spring容器实例化、配置和组装应用程序中的对象。
Spring框架提供了多种配置方式:XML配置方式、注解方式和Java配置类方式

  1. XML配置方式:是Spring框架最早的配置方式之一,通过在XML文件中定义Bean及其依赖关系、Bean的作用域等信息,让Spring IoC容器来管理Bean之间的依赖关系。该方式从Spring框架的第一版开始提供支持
  2. 注解方式:从Spring 2.5版本开始提供支持,可以通过在Bean类上使用注解来代替XML配置文件中的配置信息。通过在Bean类上加上相应的注解(如@Component, @Service, @Autowired等),将Bean注册到Spring IoC容器中,这样Spring IoC容器就可以管理这些Bean之间的依赖关系
  3. Java配置类方式:从Spring 3.0版本开始提供支持,通过Java类来定义Bean、Bean之间的依赖关系和配置信息,从而代替XML配置文件的方式。Java配置类是一种使用Java编写配置信息的方式,通过@Configuration、@Bean等注解来实现Bean和依赖关系的配置

SpringIoC/DI概念总结

  • IoC容器

    Spring IoC 容器,负责实例化、配置和组装 bean(组件)核心容器。容器通过读取配置元数据来获取有关要实例化、配置和组装组件的指令

  • IoC(Inversion of Control)控制反转

    IoC主要是针对对象的创建和调用控制而言的,也就是说,当应用程序需要使用一个对象时,不再是应用程序直接创建该对象,而是由 IoC容器来创建和管理,即控制权由应用程序转移到IoC容器中,也就是“反转”了控制权。这种方式基本上是通过依赖查找的方式来实现的,即IoC容器维护着构成应用程序的对象,并负责创建这些对象

  • DI (Dependency Injection) 依赖注入

    DI 是指在组件之间传递依赖关系的过程中,将依赖关系在容器内部进行处理,这样就不必在应用程序代码中硬编码对象之间的依赖关系,实现了对象之间的解耦合。在Spring中,DI是通过XML配置文件或注解的方式实现的。它提供了三种形式的依赖注入:构造函数注入、Setter方法注入和接口注入

Spring IoC实践

Spring IoC/DI实现步骤

  1. 配置元数据(配置)

基于 XML 的配置元数据的基本结构:

<bean id="..." [1] class="..." [2]>  
<!-- collaborators and configuration for this bean go here -->
</bean>
<?xml version="1.0" encoding="UTF-8"?>
<!-- 此处要添加一些约束,配置文件的标签并不是随意命名 -->
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd">

<bean id="..." [1] class="..." [2]>
<!-- collaborators and configuration for this bean go here -->
</bean>

<bean id="..." class="...">
<!-- collaborators and configuration for this bean go here -->
</bean>
<!-- more bean definitions go here -->
</beans>

Spring IoC容器管理一个或多个组件。这些组件是使用你提供给容器的配置元数据(例如,以 XML <bean/> 定义的形式)创建的。
<bean/>标签等于组件信息声明

  • id属性是标识单个 Bean 定义的字符串
  • class属性定义 Bean 的类型并使用完全限定的类名
  1. 实例化IoC容器

提供给 ApplicationContext 构造函数的位置路径是资源字符串地址,允许容器从各种外部资源(如本地文件系统、Java CLASSPATH 等)加载配置元数据。
选择一个合适的容器实现类,进行IoC容器的实例化工作:

//实例化ioc容器,读取外部配置文件,最终会在容器内进行ioc和di动作
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");
  1. 获取Bean(组件)

ApplicationContext是一个高级工厂的接口,能够维护不同 bean 及其依赖项的注册表。通过使用方法T getBean(String name, Class<T> requiredType),可以检索 bean 的实例。

//创建ioc容器对象,指定配置文件,ioc也开始实例组件对象
ApplicationContext context = new ClassPathXmlApplicationContext("services.xml", "daos.xml");
//获取ioc容器的组件对象
PetStoreService service = context.getBean("petStore", PetStoreService.class);
//使用组件对象
List<String> userList = service.getUsernameList();

基于配置类方式管理Bean

完全注解开发理解

​ Spring完全注解配置是指通过Java配置类代码来配置Spring应用程序,使用注解来替代原本在XML配置文件中的配置。相对于XML配置,完全注解配置具有更强的类型安全性和更好的可读性。
两种方式思维转化

配置类和扫描注解

xml+注解方式

配置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">


<!-- 配置自动扫描的包 -->
<!-- 1.包要精准,提高性能!
2.会扫描指定的包和子包内容
3.多个包可以使用,分割 例如: com.atguigu.controller,com.atguigu.service等
-->
<context:component-scan base-package="com.atguigu.components"/>

<!-- 引入外部配置文件-->
<context:property-placeholder location="application.properties" />
</beans>

测试创建IoC容器

// xml方式配置文件使用ClassPathXmlApplicationContext容器读取
ApplicationContext applicationContext =
new ClassPathXmlApplicationContext("application.xml");

配置类+注解方式(完全注解方式)
使用@Configuration注解将一个普通的类标记为Spring的配置类。

//标注当前类是配置类,替代application.xml    
@Configuration
//使用注解读取外部配置,替代 <context:property-placeholder标签
@PropertySource("classpath:application.properties")
//使用@ComponentScan注解,可以配置扫描包,替代<context:component-scan标签
@ComponentScan(basePackages = {"com.atguigu.components"})
public class MyConfiguration { }

测试创建IoC容器

// AnnotationConfigApplicationContext 根据配置类创建 IOC 容器对象
ApplicationContext iocContainerAnnotation =
new AnnotationConfigApplicationContext(MyConfiguration.class);

@Bean定义组件

场景需求:将Druid连接池对象存储到IoC容器
需求分析:第三方jar包的类,添加到ioc容器,无法使用@Component等相关注解!因为源码jar包内容为只读模式
配置类方式实现:
@Bean注释用于指示方法实例化、配置和初始化要由Spring IoC容器管理的新对象

//标注当前类是配置类,替代application.xml    
@Configuration
//引入jdbc.properties文件
@PropertySource({"classpath:application.properties","classpath:jdbc.properties"})
@ComponentScan(basePackages = {"com.atguigu.components"})
public class MyConfiguration {
//如果第三方类进行IoC管理,无法直接使用@Component相关注解
//解决方案: xml方式可以使用<bean标签
//解决方案: 配置类方式,可以使用方法返回值+@Bean注解
@Bean
public DataSource createDataSource(@Value("${jdbc.user}") String username,
@Value("${jdbc.password}")String password,
@Value("${jdbc.url}")String url,
@Value("${jdbc.driver}")String driverClassName){
//使用Java代码实例化
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setUrl(url);
dataSource.setDriverClassName(driverClassName);
return dataSource;
}
}

三种配置方式对比

XML方式配置

  1. 所有内容写到xml格式配置文件中
  2. 声明bean通过<bean标签
  3. <bean标签包含基本信息(id,class)和属性信息 <property name value / ref
  4. 引入外部的properties文件可以通过<context:property-placeholder
  5. IoC具体容器实现选择ClassPathXmlApplicationContext对象

XML+注解方式配置

  1. 注解负责标记IoC的类和进行属性装配
  2. xml文件依然需要,需要通过<context:component-scan标签指定注解范围
  3. 标记IoC注解:@Component,@Service,@Controller,@Repository
  4. 标记DI注解:@Autowired @Qualifier @Resource @Value
  5. IoC具体容器实现选择ClassPathXmlApplicationContext对象

完全注解方式配置

  1. 完全注解方式指的是去掉xml文件,使用配置类 + 注解实现
  2. xml文件替换成使用@Configuration注解标记的类
  3. 标记IoC注解:@Component,@Service,@Controller,@Repository
  4. 标记DI注解:@Autowired @Qualifier @Resource @Value
  5. <context:component-scan>标签指定注解范围使用@ComponentScan(basePackages = {“”})替代
  6. <context:property-placeholder>引入外部配置文件使用@PropertySource({“classpath:application.properties”,”classpath:jdbc.properties”})替代
  7. <bean>标签使用@Bean注解和方法实现
  8. IoC具体容器实现选择AnnotationConfigApplicationContext对象

整合Spring5-Test5搭建测试环境

  1. 整合测试环境作用

    好处1:不需要自己创建IOC容器对象了

    好处2:任何需要的bean都可以在测试类中直接享受自动装配

  2. 导入相关依赖

<!--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>
  1. 整合测试注解使用
//@SpringJUnitConfig(locations = {"classpath:spring-context.xml"})  //指定配置文件xml
@SpringJUnitConfig(value = {BeanConfig.class}) //指定配置类
public class Junit5IntegrationTest {

@Autowired
private User user;

@Test
public void testJunit5() {
System.out.println(user);
}
}

Spring AOP

为什么要有Spring AOP

业务代码已经被一些非核心的代码所混淆,并且占据了大量的空间,显然这种显示的调用过程成为了我们开发过程中的一个痛点,如何将类似这种的非核心的代码剥离出去成为一个迫切需要解决的问题。
诸如日志记录,登录权限控制,还有数据库事务的控制,数据库连接的创建和关闭等等,这些都充斥这大量重复性的模板代码

解决技术代理模式

代理模式

​ 二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。

相关术语:

  • 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。
  • 动词:指做代理这个动作,或这项工作
  • 名词:扮演代理这个角色的类、对象、方法
  • 目标:被代理“套用”了核心逻辑代码的类、对象、方法。
    代理在开发中实现的方式具体有两种:静态代理,[动态代理技术]

动态代理
动态代理技术分类

  • JDK动态代理:JDK原生的实现方式,需要被代理的目标类必须实现接口!他会根据目标类的接口动态生成一个代理对象!代理对象和目标对象有相同的接口
  • cglib:通过继承被代理的目标类实现代理,所以不需要目标类实现接口

面向切面编程思维(AOP)

  1. 面向切面编程思想AOP

AOP:Aspect Oriented Programming面向切面编程
AOP可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
AOP技术恰恰相反,它利用一种称为”横切”的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为”Aspect”,即切面。所谓”切面”,简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
使用AOP,可以在不修改原来代码的基础上添加新功能

  1. AOP思想主要的应用场景

AOP(面向切面编程)是一种编程范式,它通过将通用的横切关注点(如日志、事务、权限控制等)与业务逻辑分离,使得代码更加清晰、简洁、易于维护。AOP可以应用于各种场景,以下是一些常见的AOP应用场景:

  • 日志记录:在系统中记录日志是非常重要的,可以使用AOP来实现日志记录的功能,可以在方法执行前、执行后或异常抛出时记录日志。

  • 事务处理:在数据库操作中使用事务可以保证数据的一致性,可以使用AOP来实现事务处理的功能,可以在方法开始前开启事务,在方法执行完毕后提交或回滚事务。

  • 安全控制:在系统中包含某些需要安全控制的操作,如登录、修改密码、授权等,可以使用AOP来实现安全控制的功能。可以在方法执行前进行权限判断,如果用户没有权限,则抛出异常或转向到错误页面,以防止未经授权的访问。

  • 性能监控:在系统运行过程中,有时需要对某些方法的性能进行监控,以找到系统的瓶颈并进行优化。可以使用AOP来实现性能监控的功能,可以在方法执行前记录时间戳,在方法执行完毕后计算方法执行时间并输出到日志中。

  • 异常处理:系统中可能出现各种异常情况,如空指针异常、数据库连接异常等,可以使用AOP来实现异常处理的功能,在方法执行过程中,如果出现异常,则进行异常处理(如记录日志、发送邮件等)。

  • 缓存控制:在系统中有些数据可以缓存起来以提高访问速度,可以使用AOP来实现缓存控制的功能,可以在方法执行前查询缓存中是否有数据,如果有则返回,否则执行方法并将方法返回值存入缓存中。

  • 动态代理:AOP的实现方式之一是通过动态代理,可以代理某个类的所有方法,用于实现各种功能。

  1. AOP术语名词介绍
  • 横切关注点

​ 从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。
​ AOP把软件系统分为两个部分:核心关注点和横切关注点。业务处理的主要流程是核心关注点,与之关系不大的部分是横切关注点。横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事务、异常等。AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。

  • 通知(增强):每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。

    • 前置通知:在被代理的目标方法前执行
    • 返回通知:在被代理的目标方法成功结束后执行
    • 异常通知:在被代理的目标方法异常结束后执行
    • 后置通知:在被代理的目标方法最终结束后执行
    • 环绕通知:使用try…catch…finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
  • 连接点 joinpoint

​ 这也是一个纯逻辑概念,不是语法定义的。指那些被拦截到的点。在Spring中,可以被动态代理拦截目标类的方法

  • 切入点 pointcut

​ 定位连接点的方式,或者可以理解成被选中的连接点!

  • 切面 aspect

​ 切入点和通知的结合。是一个类。

  • 目标 target

​ 被代理的目标对象。

  • 代理 proxy

​ 向目标对象应用通知之后创建的代理对象。

  • 织入 weave

​ 指把通知应用到目标上,生成代理对象的过程。可以在编译期织入,也可以在运行期织入,Spring采用后者。

Spring AOP框架介绍和关系梳理

  1. AOP一种区别于OOP的编程思维,用来完善和解决OOP的非核心代码冗余和不方便统一维护问题!
  2. 代理技术(动态代理|静态代理)是实现AOP思维编程的具体技术,但是自己使用动态代理实现代码比较繁琐!
  3. Spring AOP框架,基于AOP编程思维,封装动态代理技术,简化动态代理技术实现的框架!SpringAOP内部帮助我们实现动态代理,我们只需写少量的配置,指定生效范围即可,即可完成面向切面思维编程的实现!

Spring AOP基于注解方式实现和细节

Spring AOP底层技术组成

  • 动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)。
  • cglib:通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口。
  • AspectJ:早期的AOP实现的框架,SpringAOP借用了AspectJ中的AOP注解。

初步实现

  1. 加入依赖
<!-- 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>
  1. 准备接口
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);

}
  1. 纯净实现类
@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;
}
}
  1. 声明切面类
// @Aspect表示这个类是一个切面类
@Aspect
// @Component注解保证这个切面类能够放入IOC容器
@Component
public class LogAspect {

// @Before注解:声明当前方法是前置通知方法
// value属性:指定切入点表达式,由切入点表达式控制当前通知方法要作用在哪一个目标方法上
@Before(value = "execution(public int proxy.CalculatorPureImpl.add(int,int))")
public void printLogBeforeCore() {
System.out.println("[AOP前置通知] 方法开始了");
}

@AfterReturning(value = "execution(public int proxy.CalculatorPureImpl.add(int,int))")
public void printLogAfterSuccess() {
System.out.println("[AOP返回通知] 方法成功返回了");
}

@AfterThrowing(value = "execution(public int proxy.CalculatorPureImpl.add(int,int))")
public void printLogAfterException() {
System.out.println("[AOP异常通知] 方法抛异常了");
}

@After(value = "execution(public int proxy.CalculatorPureImpl.add(int,int))")
public void printLogFinallyEnd() {
System.out.println("[AOP后置通知] 方法最终结束了");
}

}
  1. 开启aspectj注解支持
    1. 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="" />
<!-- 开启aspectj框架注解支持-->
<aop:aspectj-autoproxy />
</beans>

​ 2. 配置类方式

@Configuration
@ComponentScan(basePackages = "")
//作用等于 <aop:aspectj-autoproxy /> 配置类上开启 Aspectj注解支持!
@EnableAspectJAutoProxy
public class MyConfig {
}

  1. 测试效果
//@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);
}
}

​ 输出结果:

com.intellij.rt.junit.JUnitStarter -ideVersion5 -junit5 com.atguigu.test.AopTest,testCalculator
[AOP前置通知] 方法开始了
[AOP返回通知] 方法成功返回了
[AOP后置通知] 方法最终结束了

获取通知细节信息

  1. JointPoint接口
    需要获取方法签名、传入的实参等信息时,可以在通知方法声明JoinPoint类型的形参。
  • 要点1:JoinPoint 接口通过 getSignature() 方法获取目标方法的签名(方法声明时的完整信息)
  • 要点2:通过目标方法签名对象获取方法名
  • 要点3:通过JoinPoint对象获取外界调用目标方法时传入的实参列表组成的数组
// @Before注解标记前置通知方法
// value属性:切入点表达式,告诉Spring当前通知方法要套用到哪个目标方法上
// 在前置通知方法形参位置声明一个JoinPoint类型的参数,Spring就会将这个对象传入
// 根据JoinPoint对象就可以获取目标方法名称、实际参数列表
@Before(value = "execution(public int 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);
}
  1. 方法返回值

在返回通知中,通过**@AfterReturning**注解的returning属性获取目标方法的返回值!

// @AfterReturning注解标记返回通知方法
// 在返回通知中获取目标方法返回值分两步:
// 第一步:在@AfterReturning注解中通过returning属性设置一个名称
// 第二步:使用returning属性设置的名称在通知方法中声明一个对应的形参
@AfterReturning(
value = "execution(public int 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);
}
  1. 异常对象捕捉

在异常通知中,通过@AfterThrowing注解的throwing属性获取目标方法抛出的异常对象

// @AfterThrowing注解标记异常通知方法
// 在异常通知中获取目标方法抛出的异常分两步:
// 第一步:在@AfterThrowing注解中声明一个throwing属性设定形参名称
// 第二步:使用throwing属性指定的名称在通知方法声明形参,Spring会将目标方法抛出的异常对象从这里传给我们
@AfterThrowing(
value = "execution(public int 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());
}

切点表达式语法

  1. 切点表达式

AOP切点表达式(Pointcut Expression)是一种用于指定切点的语言,它可以通过定义匹配规则,来选择需要被切入的目标对象。

  1. 切点表达式语法

语法细节

  • 第一位:execution( )固定开头
  • 第二位:方法访问修饰符
public private 直接描述对应修饰符即可
  • 第三位:方法返回值
int String void 直接描述返回值类型
  • 第四位:指定包的地址
固定的包: com.atguigu.api | service | dao
单层的任意命名: com.atguigu.* = api dao * = 任意一层的任意命名
任意层任意命名: com.. = api.erdaye com.a.a.a.a.a.a.a ..任意层,任意命名 用在包上!
注意: ..不能用作包开头 public int .. 错误语法 com..
找到任何包下: *..
  • 第五位:指定类名称
固定名称: UserService
任意类名: *
部分任意: com..service.impl.*Impl
任意包任意类: *..*
  • 第六位:指定方法名称
    语法和类名一致
    任意访问修饰符,任意类的任意方法: * *..*.*
  • 第七位:方法参数
第七位: 方法的参数描述
具体值: (String,int) != (int,String) 没有参数 ()
模糊值: 任意参数 有 或者 没有 (..) ..任意参数的意识
部分具体和模糊:
第一个参数是字符串的方法 (String..)
最后一个参数是字符串 (..String)
字符串开头,int结尾 (String..int)
包含int类型(..int..)
  1. 切点表达式案例
1.查询某包某类下,访问修饰符是公有,返回值是int的全部方法
2.查询某包下类中第一个参数是String的方法
3.查询全部包下,无参数的方法!
4.查询com包下,以int参数类型结尾的方法
5.查询指定包下,Service开头类的私有返回值int的无参数方法

重用(提取)切点表达式

  1. 重用切点表达式
 // @Before注解:声明当前方法是前置通知方法
// value属性:指定切入点表达式,由切入点表达式控制当前通知方法要作用在哪一个目标方法上
@Before(value = "execution(public int proxy.CalculatorPureImpl.add(int,int))")
public void printLogBeforeCore() {
System.out.println("[AOP前置通知] 方法开始了");
}

@AfterReturning(value = "execution(public int proxy.CalculatorPureImpl.add(int,int))")
public void printLogAfterSuccess() {
System.out.println("[AOP返回通知] 方法成功返回了");
}

@AfterThrowing(value = "execution(public int proxy.CalculatorPureImpl.add(int,int))")
public void printLogAfterException() {
System.out.println("[AOP异常通知] 方法抛异常了");
}

@After(value = "execution(public int proxy.CalculatorPureImpl.add(int,int))")
public void printLogFinallyEnd() {
System.out.println("[AOP后置通知] 方法最终结束了");
}
  1. 同一类内部引用

提取

// 切入点表达式重用
@Pointcut("execution(public int aop.api.Calculator.add(int,int)))")
public void declarPointCut() {}

注意:提取切点注解使用@Pointcut(切点表达式) , 需要添加到一个无参数无返回值方法上即可!
引用

@Before(value = "declarPointCut()")
public void printLogBeforeCoreOperation(JoinPoint joinPoint) {
  1. 不同类中引用

不同类在引用切点,只需要添加类的全限定符+方法名即可!

@Before(value = "aop.aspect.LogAspect.declarPointCut()")
public Object roundAdvice(ProceedingJoinPoint joinPoint) {
  1. 切点统一管理

建议:将切点表达式统一存储到一个类中进行集中管理和维护!

@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(){}
}

环绕通知

环绕通知对应整个 try…catch…finally 结构,包括前面四种通知的所有功能。

// 使用@Around注解标明环绕通知方法
@Around(value = "aop.aspect.AtguiguPointCut.transactionPointCut()")
public Object manageTransaction(

// 通过在通知方法形参位置声明ProceedingJoinPoint类型的形参,
// Spring会将这个类型的对象传给我们
ProceedingJoinPoint joinPoint) {

// 通过ProceedingJoinPoint对象获取外界调用目标方法时传入的实参数组
Object[] args = joinPoint.getArgs();

// 通过ProceedingJoinPoint对象获取目标方法的签名对象
Signature signature = joinPoint.getSignature();

// 通过签名对象获取目标方法的方法名
String methodName = signature.getName();

// 声明变量用来存储目标方法的返回值
Object targetMethodReturnValue = null;
try {
// 在目标方法执行前:开启事务(模拟)
log.debug("[AOP 环绕通知] 开启事务,方法名:" + methodName + ",参数列表:" + Arrays.asList(args));

// 过ProceedingJoinPoint对象调用目标方法
// 目标方法的返回值一定要返回给外界调用者
targetMethodReturnValue = joinPoint.proceed(args);

// 在目标方法成功返回后:提交事务(模拟)
log.debug("[AOP 环绕通知] 提交事务,方法名:" + methodName + ",方法返回值:" + targetMethodReturnValue);

}catch (Throwable e){
// 在目标方法抛异常后:回滚事务(模拟)
log.debug("[AOP 环绕通知] 回滚事务,方法名:" + methodName + ",异常:" + e.getClass().getName());
}finally {

// 在目标方法最终结束后:释放数据库连接
log.debug("[AOP 环绕通知] 释放数据库连接,方法名:" + methodName);
}
return targetMethodReturnValue;
}

切面优先级设置

相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。

  • 优先级高的切面:外面
  • 优先级低的切面:里面

使用@Order注解可以控制切面的优先级:

  • @Order(较小的数):优先级高
  • @Order(较大的数):优先级低

此时应该将缓存切面的优先级提高,在事务操作之前先检查缓存中是否存在目标数据。

CGLib动态代理生效

在目标类没有实现任何接口的情况下,Spring会自动使用cglib技术实现代理。

使用总结:

  • 如果目标类有接口,选择使用jdk动态代理
  • 如果目标类没有接口,选择cglib动态代理
  • 如果有接口,接口接值
  • 如果没有接口,类进行接值

注解实现小结

Spring 声明式事务

声明式事务概念

声明式事务是指使用注解或XML配置的方式来控制事务的提交和回滚。
开发者只需要添加配置即可,具体事务的实现由第三方框架实现,避免我们直接进行事务操作!
使用声明式事务可以将事务的控制和业务逻辑分离开来,提高代码的可读性和可维护性。

Spring事务管理器

  1. Spring声明式事务对应依赖
  • spring-tx: 包含声明式事务实现的基本规范(事务管理器规范接口和事务增强等等)
  • spring-jdbc: 包含DataSource方式事务管理器实现类DataSourceTransactionManager
  • spring-orm: 包含其他持久层框架的事务管理器实现类例如:Hibernate/Jpa等
  1. Spring声明式事务对应事务管理器接口

​ 使用的事务管理器是org.springframework.jdbc.datasource.DataSourceTransactionManager,将来整合JDBC方式、JdbcTemplate方式、Mybatis方式的事务实现

DataSourceTransactionManager类中的主要方法:

  • doBegin():开启事务
  • doSuspend():挂起事务
  • doResume():恢复挂起的事务
  • doCommit():提交事务
  • doRollback():回滚事务

基于注解的声明式事务

  1. 准备项目
<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-tx</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>
  1. 外部配置文件

jdbc.properties

jdbc.url=jdbc:mysql://localhost:3306/studb
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.username=root
jdbc.password=root
  1. spring配置文件
@Configuration
@ComponentScan("/")
@PropertySource("classpath:jdbc.properties")
public class JavaConfig {

@Value("${jdbc.driver}")
private String driver;
@Value("${jdbc.url}")
private String url;
@Value("${jdbc.username}")
private String username;
@Value("${jdbc.password}")
private String password;

//druid连接池
@Bean
public DataSource dataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);
return dataSource;
}

@Bean
//jdbcTemplate
public JdbcTemplate jdbcTemplate(DataSource dataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
}
  1. 准备dao/service层

dao

@Repository
public class StudentDao {

@Autowired
private JdbcTemplate jdbcTemplate;

public void updateNameById(String name,Integer id){
String sql = "update students set name = ? where id = ? ;";
int rows = jdbcTemplate.update(sql, name, id);
}

public void updateAgeById(Integer age,Integer id){
String sql = "update students set age = ? where id = ? ;";
jdbcTemplate.update(sql,age,id);
}
}

service

@Service
public class StudentService {

@Autowired
private StudentDao studentDao;

public void changeInfo(){
studentDao.updateAgeById(100,1);
System.out.println("-----------");
studentDao.updateNameById("test1",1);
}
}

  1. 测试环境搭建
/**
* description:
*/
@SpringJUnitConfig(JavaConfig.class)
public class TxTest {

@Autowired
private StudentService studentService;

@Test
public void testTx(){
studentService.changeInfo();
}
}

基本事务控制

  1. 配置事务管理器

数据库相关的配置

/**
* description: 数据库和连接池配置类
*/

@Configuration
@ComponenScan("/")
@PropertySource(value = "classpath:jdbc.properties")
@EnableTransactionManagement
public class DataSourceConfig {
/**
* 实例化dataSource加入到ioc容器
* @param url
* @param driver
* @param username
* @param password
* @return
*/
@Bean
public DataSource dataSource(@Value("${jdbc.url}")String url,
@Value("${jdbc.driver}")String driver,
@Value("${jdbc.username}")String username,
@Value("${jdbc.password}")String password){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setDriverClassName(driver);
dataSource.setUrl(url);
dataSource.setUsername(username);
dataSource.setPassword(password);

return dataSource;
}
/**
* 实例化JdbcTemplate对象,需要使用ioc中的DataSource
* @param dataSource
* @return
*/
@Bean
public JdbcTemplate jdbcTemplate(DataSource dataSource){
JdbcTemplate jdbcTemplate = new JdbcTemplate();
jdbcTemplate.setDataSource(dataSource);
return jdbcTemplate;
}
/**
* 装配事务管理实现对象
* @param dataSource
* @return
*/
@Bean
public TransactionManager transactionManager(DataSource dataSource){
return new DataSourceTransactionManager(dataSource);
}
}
  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);
}
}
  1. 测试事务效果
@SpringJUnitConfig(classes = DataSourceConfig.class)
public class TxTest {

@Autowired
private StudentService studentService;

@Test
public void testTx(){
studentService.changeInfo();
}
}

事务属性:只读

  1. 只读介绍

对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化

  1. 设置方式
// readOnly = true把当前事务设置为只读 默认是false!
@Transactional(readOnly = true)
  1. 针对DML动作设置只读模式

会抛出下面异常:

Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed

  1. @Transactional注解放在类上
  2. 生效原则

​ 如果一个类中每一个方法上都使用了 @Transactional 注解,那么就可以将 @Transactional 注解提取到类上。反过来说:@Transactional 注解在类级别标记,会影响到类中的每一个方法。同时,类级别标记的 @Transactional 注解中设置的事务属性也会延续影响到方法执行时的事务属性。除非在方法上又设置了 @Transactional 注解。对一个方法来说,离它最近的 @Transactional 注解中的事务属性设置生效。

  1. 用法举例

​ 在类级别@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) {
}
}

事务属性:超时时间

  1. 需求

​ 事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。
​ 此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。

概括来说就是一句话:超时回滚,释放资源。

  1. 设置超时时间
@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);
}
}
  1. 测试超时效果

执行抛出事务超时异常

org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Wed May 24 09:10:43 IRKT 2023

at org.springframework.transaction.support.ResourceHolderSupport.checkTransactionTimeout(ResourceHolderSupport.java:155)
at org.springframework.transaction.support.ResourceHolderSupport.getTimeToLiveInMillis(ResourceHolderSupport.java:144)
at org.springframework.transaction.support.ResourceHolderSupport.getTimeToLiveInSeconds(ResourceHolderSupport.java:128)
at org.springframework.jdbc.datasource.DataSourceUtils.applyTimeout(DataSourceUtils.java:341)
at org.springframework.jdbc.core.JdbcTemplate.applyStatementSettings(JdbcTemplate.java:1467)

事务属性:事务异常

  1. 默认情况

默认只针对运行时异常回滚,编译时异常不回滚。情景模拟代码如下:

@Service
public class StudentService {

@Autowired
private StudentDao studentDao;

/**
* timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间!
* rollbackFor = 指定哪些异常才会回滚,默认是 RuntimeException and Error 异常方可回滚!
* noRollbackFor = 指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!
*/
@Transactional(readOnly = false,timeout = 3)
public void changeInfo() throws FileNotFoundException {
studentDao.updateAgeById(100,1);
//主动抛出一个检查异常,测试! 发现不会回滚,因为不在rollbackFor的默认范围内!
new FileInputStream("xxxx");
studentDao.updateNameById("test1",1);
}
}
  1. 设置回滚异常

rollbackFor属性:指定哪些异常类才会回滚,默认是 RuntimeException and Error 异常方可回滚!

/**
* timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间!
* rollbackFor = 指定哪些异常才会回滚,默认是 RuntimeException and Error 异常方可回滚!
* noRollbackFor = 指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!
*/
@Transactional(readOnly = false,timeout = 3,rollbackFor = Exception.class)
public void changeInfo() throws FileNotFoundException {
studentDao.updateAgeById(100,1);
//主动抛出一个检查异常,测试! 发现不会回滚,因为不在rollbackFor的默认范围内!
new FileInputStream("xxxx");
studentDao.updateNameById("test1",1);
}
  1. 设置不回滚的异常

在默认设置和已有设置的基础上,再指定一个异常类型,碰到它不回滚。

noRollbackFor属性:指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!

@Service
public class StudentService {
@Autowired
private StudentDao studentDao;
/**
* timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间!
* rollbackFor = 指定哪些异常才会回滚,默认是 RuntimeException and Error 异常方可回滚!
* noRollbackFor = 指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!
*/
@Transactional(readOnly = false,timeout = 3,rollbackFor = Exception.class,noRollbackFor = FileNotFoundException.class)
public void changeInfo() throws FileNotFoundException {
studentDao.updateAgeById(100,1);
//主动抛出一个检查异常,测试! 发现不会回滚,因为不在rollbackFor的默认范围内!
new FileInputStream("xxxx");
studentDao.updateNameById("test1",1);
}
}

事务属性:事务隔离级别

  1. 事务隔离级别

数据库事务的隔离级别是指在多个事务并发执行时,数据库系统为了保证数据一致性所遵循的规定。常见的隔离级别包括:

  • 读未提交(Read Uncommitted):事务可以读取未被提交的数据,容易产生脏读、不可重复读和幻读等问题。实现简单但不太安全,一般不用。

  • 读已提交(Read Committed):事务只能读取已经提交的数据,可以避免脏读问题,但可能引发不可重复读和幻读。

  • 可重复读(Repeatable Read):在一个事务中,相同的查询将返回相同的结果集,不管其他事务对数据做了什么修改。可以避免脏读和不可重复读,但仍有幻读的问题。

  • 串行化(Serializable):最高的隔离级别,完全禁止了并发,只允许一个事务执行完毕之后才能执行另一个事务。可以避免以上所有问题,但效率较低,不适用于高并发场景。

不同的隔离级别适用于不同的场景,需要根据实际业务需求进行选择和调整。

  1. 事务隔离级别设置
@Service
public class StudentService {
@Autowired
private StudentDao studentDao;

/**
* timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间!
* rollbackFor = 指定哪些异常才会回滚,默认是 RuntimeException and Error 异常方可回滚!
* noRollbackFor = 指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!
* isolation = 设置事务的隔离级别,mysql默认是repeatable read!
*/
@Transactional(readOnly = false,
timeout = 3,
rollbackFor = Exception.class,
noRollbackFor = FileNotFoundException.class,
isolation = Isolation.REPEATABLE_READ)
public void changeInfo() throws FileNotFoundException {
studentDao.updateAgeById(100,1);
//主动抛出一个检查异常,测试! 发现不会回滚,因为不在rollbackFor的默认范围内!
new FileInputStream("xxxx");
studentDao.updateNameById("test1",1);
}
}

事务属性:事务传播行为

  1. 事务传播行为要研究的问题

@Transactional
public void MethodA(){
// ...
MethodB();
// ...
}
//在被调用的子方法中设置传播行为,代表如何处理调用的事务! 是加入,还是新事务等!
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void MethodB(){
// ...
}
  1. propagation属性

@Transactional注解通过propagation属性设置事务的传播行为。它的默认值是:

Propagation propagation() default Propagation.REQUIRED;

propagation属性的可选值由org.springframework.transaction.annotation.Propagation枚举类提供:

名称 含义
REQUIRED 默认值 如果父方法有事务,就加入,如果没有就新建自己独立
REQUIRES_NEW 不管父方法是否有事务,都新建事务,都是独立的
  • 声明两个业务方法
@Service
public class StudentService {

@Autowired
private StudentDao studentDao;
/**
* timeout设置事务超时时间,单位秒! 默认: -1 永不超时,不限制事务时间!
* rollbackFor = 指定哪些异常才会回滚,默认是 RuntimeException and Error 异常方可回滚!
* noRollbackFor = 指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!
* isolation = 设置事务的隔离级别,mysql默认是repeatable read!
*/
@Transactional(readOnly = false,
timeout = 3,
rollbackFor = Exception.class,
noRollbackFor = FileNotFoundException.class,
isolation = Isolation.REPEATABLE_READ)
public void changeInfo() throws FileNotFoundException {
studentDao.updateAgeById(100,1);
//主动抛出一个检查异常,测试! 发现不会回滚,因为不在rollbackFor的默认范围内!
new FileInputStream("xxxx");
studentDao.updateNameById("test1",1);
}
/**
* 声明两个独立修改数据库的事务业务方法
*/
@Transactional(propagation = Propagation.REQUIRED)
public void changeAge(){
studentDao.updateAgeById(99,1);
}
@Transactional(propagation = Propagation.REQUIRED)
public void changeName(){
studentDao.updateNameById("test2",1);
int i = 1/0;
}
}
  • 声明一个整合业务方法
@Service
public class TopService {

@Autowired
private StudentService studentService;

@Transactional
public void topService(){
studentService.changeAge();
studentService.changeName();
}
}
  • 添加传播行为测试
@SpringJUnitConfig(classes = AppConfig.class)
public class TxTest {

@Autowired
private StudentService studentService;

@Autowired
private TopService topService;

@Test
public void testTx() throws FileNotFoundException {
topService.topService();
}
}

注意:
在同一个类中,对于@Transactional注解的方法调用,事务传播行为不会生效。这是因为Spring框架中使用代理模式实现了事务机制,在同一个类中的方法调用并不经过代理,而是通过对象的方法调用,因此@Transactional注解的设置不会被代理捕获,也就不会产生任何事务传播行为的效果。

其他传播行为值
  • Propagation.REQUIRED:如果当前存在事务,则加入当前事务,否则创建一个新事务。
  • Propagation.REQUIRES_NEW:创建一个新事务,并在新事务中执行。如果当前存在事务,则挂起当前事务,即使新事务抛出异常,也不会影响当前事务。
  • Propagation.NESTED:如果当前存在事务,则在该事务中嵌套一个新事务,如果没有事务,则与Propagation.REQUIRED一样。
  • Propagation.SUPPORTS:如果当前存在事务,则加入该事务,否则以非事务方式执行。
  • Propagation.NOT_SUPPORTED:以非事务方式执行,如果当前存在事务,挂起该事务。
  • Propagation.MANDATORY:必须在一个已有的事务中执行,否则抛出异常。
  • Propagation.NEVER:必须在没有事务的情况下执行,否则抛出异常。

Spring核心点

核心点 掌握目标
spring框架理解 spring家族和spring framework框架
spring核心功能 ioc/di , aop , tx
spring ioc / di 组件管理、ioc容器、ioc/di , 三种配置方式
spring aop aop和aop框架和代理技术、基于注解的aop配置
spring tx 声明式和编程式事务、动态事务管理器、事务注解、属性