《剖析 Spring 原理:深入源码的旅程(一)》

embedded/2024/11/27 21:32:35/

一、Spring 工作原理概述

Spring 是一个全面的企业应用开发解决方案,它通过控制反转(IOC)和面向切面编程(AOP)等技术,实现了轻量级、灵活的开发模式。Spring 框架由多个模块组成,包括核心容器、上下文、AOP 等,每个模块都有其特定的功能。

Spring 工作原理概述

Spring 的工作原理主要围绕控制反转(IOC)和面向切面编程(AOP)展开。

一、控制反转(IOC)

  1. IOC 的基础知识以及原理
    • 在采用面向对象方法设计的软件系统中,底层实现由众多对象组成,对象之间存在复杂的耦合关系。为了解决这个问题,Michael Mattson 在 1996 年提出了 IoC 的概念。把复杂系统分解成相互合作的对象,这些对象类通过封装以后,内部实现对外部是透明的,从而降低了解决问题的复杂度,而且可以灵活地被重用和扩展。
    • 借助于 “第三方” 实现具有依赖关系的对象之间的解耦,把各个对象类封装之后,通过 IoC 容器来关联这些对象类。这样对象与对象之间就通过 IoC 容器进行联系,但对象与对象之间并没有直接联系。
  1. 什么是控制反转(IoC)
    • IoC 是 Inversion of Control 的缩写,有多种翻译方式,如 “控制反转”“控制反向” 或 “控制倒置”。
    • 简单来说就是把复杂系统分解成相互合作的对象,这些对象类通过封装以后,内部实现对外部是透明的,从而降低了解决问题的复杂度,而且可以灵活地被重用和扩展。
    • 软件系统在没有引入 IoC 容器之前,对象 A 依赖对象 B,A 对象在实例化或者运行到某一点的时候,自己必须主动创建对象 B 或者使用已经创建好的对象 B,控制权在自己手上。引入 IoC 容器之后,对象 A 和对象 B 之间失去了直接联系,当对象 A 实例化和运行时,如果需要对象 B,IoC 容器会主动创建一个对象 B 注入到对象 A 所需要的地方,对象 A 获得依赖对象 B 的过程由主动行为变成了被动行为,即把创建对象交给了 IoC 容器处理,控制权颠倒过来了,这就是控制反转的由来。
  1. IoC 的别名:依赖注入(DI)
    • 2004 年,Martin Fowler 探讨了同一个问题,得出了 “获得依赖对象的过程被反转了” 的答案,并给 “控制反转” 取了一个更合适的名字叫做 “依赖注入(Dependency Injection,DI)”。
    • 所谓依赖注入,就是由 IoC 容器在运行期间,动态地将某种依赖关系注入到对象之中。
    • 依赖注入(DI)和控制反转(IoC)是从不同的角度描述的同一件事情,就是指通过引入 IoC 容器,利用依赖关系注入的方式,实现对象之间的解耦。
  1. 使用 IoC 的好处
    • 可维护性比较好,非常便于进行单元测试,便于调试程序和诊断故障。代码中的每一个 Class 都可以单独测试,彼此之间互不影响,提高了模块的可复用性。
    • 每个开发团队的成员都只需要关注自己要实现的业务逻辑,完全不用去关心其他人的工作进展,提高了开发效率和产品质量。
    • 可复用性好,我们可以把具有普遍性的常用组件独立出来,反复应用到项目中的其它部分,或者是其它项目。
    • IoC 生成对象的方式转为外置方式,也就是把对象生成放在配置文件里进行定义,当我们更换一个实现子类将会变得很简单,只要修改配置文件就可以了,完全具有热插拨的特性。
  1. IOC 的原理
    • 控制反转是 Spring 框架的核心。其原理是基于面向对象(OO)设计原则的 The Hollywood Principle:Don't call us, we'll call you(别找我,我会来找你的)。也就是说,所有的组件都是被动的,所有的组件初始化和调用都由容器负责。组件处在一个容器当中,由容器负责管理。简单的来讲,就是由容器控制程序之间的关系,而非传统实现中,由程序代码直接操控,即在一个类中调用另外一个类。这也就是所谓 “控制反转” 的概念所在:控制权由应用代码中转到了外部容器,控制权的转移,即所谓反转。
  1. 工厂模式
    • 在 Spring IoC 中经常用到一个设计模式,即工厂模式。工厂模式提供创建对象的接口。
    • 工厂模式是指当应用程序中甲组件需要乙组件协助时,并不是在甲组件中直接实例化乙组件对象,而是通过乙组件的工厂获取,即该工厂可以生成某一类型组件的实例对象。在这种模式下,甲组件无需与乙组件以硬编码的方式耦合在一起,而只需与乙组件的工厂耦合。

二、面向切面编程(AOP)

  1. 面向切面编程在软件开发中的作用
    • 散布于应用中多处的功能被称为横切关注点。这些横切关注点从概念上是与应用的业务逻辑相分离的,但往往会直接嵌入到应用的业务逻辑之中。把这些横切关注点与业务逻辑相分离正是面向切面编程(AOP)所要解决的问题。
    • 切面提供了取代继承和委托的另一种可选方案,而且在很多场景下更清晰简洁。在使用面向切面编程时,我们仍然在一个地方定义通用功能,但是可以通过声明的方式定义这个功能要以何种方式在何处应用,而无需修改受影响的类。
  1. 定义 AOP 术语
    • 通知(Advice):在 AOP 术语中,切面的工作被称为通知。通知定义了切面是什么以及何时使用。除了描述切面要完成的工作,通知还解决了何时执行这个工作的问题。Spring 切面可以应用 5 种类型的通知:前置通知、后置通知、返回通知、异常通知、环绕通知。
    • 连接点(Join point):应用执行过程中能够插入切面的一个点。这个点可以是调用方法时、抛出异常时、甚至修改一个字段时。切面代码可以利用这些点插入到应用的正常流程之中,并添加新的行为。
    • 切点((Poincut):如果说通知定义了切面的 “什么” 和 “何时” 的话,那么切点就定义了 “何处”。切点的定义会匹配通知所要织入的一个或多个连接点。我们通常使用明确的类和方法名称,或是利用正则表达式定义所匹配的类和方法名称来指定这些切点。
    • 切面(Aspect):切面是通知和切点的结合。通知和切点共同定义了切面的全部内容 —— 它是什么,在何时和何处完成其功能。
    • 引入(Introduction):引入允许我们向现有的类添加新方法或属性。
    • 织入(Weaving):织入是把切面应用到目标对象并创建新的代理对象的过程。切面在指定的连接点被织入到目标对象中。

Spring 通过 IOC 和 AOP 等技术,为企业应用开发提供了强大的支持,使得开发更加高效、灵活和可维护。

二、Spring 的核心概念

1. IOC(控制反转)

在上文中我们已经详细介绍了 Spring 的工作原理概述,其中重点阐述了控制反转(IOC)和面向切面编程(AOP)。IOC 是 Spring 的核心概念之一,它通过描述创建对象的方式,将对象的创建和管理交给 Spring 容器,实现了控制权的反转。

在代码中,不直接与对象和服务连接,而是在配置文件中描述依赖关系,由容器负责将这些联系在一起。例如,在传统的编程模式下,一个对象会直接创建它所依赖的对象,或者通过 setter 方法手动设置这些依赖。而在使用 IOC 的情况下,这些依赖关系不再由对象自身管理,而是由外部容器来负责创建和管理这些依赖。

2. DI(依赖注入)

依赖注入是实现 IOC 的一种方式,Spring 使用 Java Bean 对象的 Set 方法或者带参数的构造方法为对象在创建时自动设置所需的值。依赖注入分为 Setter 方法注入和构造器注入两种方式。

Setter 方法注入通过类的 setter 方法将依赖注入到类中。这种方式具有可选性,依赖可以在运行时决定是否注入;延迟初始化,依赖可以在任何时间点注入,甚至可以在第一次调用 setter 方法时才注入;测试性好,由于依赖是通过 setter 方法注入的,因此可以通过构造无依赖的对象来进行单元测试。

构造器注入通过构造函数来注入依赖。这是 Spring 推荐的依赖注入方式之一,因为它能够确保依赖关系在创建对象时已经被满足,并且使得对象在创建后处于一种完整可用的状态。其特点包括强制性,依赖必须在构造对象时提供;不可变性,依赖一旦注入就无法更改;更好的测试性,因为依赖是在构造器中提供的,所以可以通过不同的构造器参数来进行测试;更安全,确保了依赖不会是 null。

3. Bean 的作用域

Spring 支持多种 Bean 的作用域,包括 singleton(单例)、prototype(原型)、request(请求作用域)、session(会话作用域)、application(应用作用域)和 websocket(HTTP WebSocket 作用域)。不同的作用域适用于不同的场景。

singleton 作用域表示在整个 Spring 容器中一个 bean 定义只生成了唯一的一个 bean 实例,被 Spring 容器管理。所有对这个 bean 的请求和引用都会返回这个 bean 实例。

prototype 作用域表示的是一个 bean 定义可以创建多个 bean 实例,当注入到其他的 bean 中或者对这个 bean 定义调用 getBean () 时,都会生成一个新的 bean 实例。

request 作用域每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP request 内有效。

session 作用域每一次 HTTP 请求都会产生一个新的 bean,该 bean 仅在当前 HTTP session 内有效。

application 作用域为整个 web 上下文创建一个 bean,也就是说,这个 bean 作用域是在 ServletContext 级别,它是作为 ServletContext 的一个属性存在的。

websocket 作用域在 HTTP WebSocket 通信中有效。

4. Bean 的生命周期

Bean 的生命周期包括实例化、属性赋值、初始化、使用和销毁五个阶段。在每个阶段,Spring 框架都会调用相应的方法进行处理,开发者可以通过实现特定的接口或使用注解来定制 Bean 的生命周期。

实例化阶段:当 Spring 容器启动时,根据配置文件或注解等信息,创建一个与 Bean 相对应的对象实例。

属性赋值阶段:如果 Bean 有属性需要被赋值,Spring 会根据配置文件或注解等信息进行属性赋值。

初始化阶段:Bean 实例化并配置完成后,会调用其无参数的构造函数,进入初始化状态。在这个阶段,通常会进行一些初始化工作,例如启动监听器、加载配置、调用初始化方法等。

正常使用阶段:Bean 进入正常使用状态,可以被 Spring 容器使用和操纵。

销毁阶段:当 Bean 不再需要时,Spring 容器会销毁该 Bean,并释放其占用的资源。在整个生命周期中,Spring 容器会对 Bean 进行一系列的自动管理,包括但不限于依赖注入、属性设置、初始化方法调用、Bean 级别的操作限制等。

三、Spring 的核心模块

1. 核心容器

Spring 的核心容器是 Spring 框架的基础,提供了基本功能。其主要组件是 BeanFactory,它是工厂模式的实现,使用控制反转模式将应用程序的配置和依赖性规范与实际的应用程序代码分开。

一、BeanFactory 的功能和作用

BeanFactory 作为 Spring 核心容器的关键组件,承担着重要的职责。它通过工厂模式,为应用程序创建和管理 Bean。在传统的编程模式下,对象之间的依赖关系通常由对象自身在代码中直接创建或通过 setter 方法手动设置。而在 Spring 中,BeanFactory 利用控制反转模式,将对象的创建和管理交给容器,实现了控制权的反转。这样,对象与对象之间的联系不再是直接的,而是通过 BeanFactory 进行管理。

二、BeanDefinition 的重要性

BeanDefinition 是 Spring 中用于描述 Bean 的配置信息的数据结构。它包含了 Bean 的各种属性,如类名、构造参数、属性值等。通过 BeanDefinition,Spring 能够在运行时准确地创建和配置 Bean。例如,当应用程序启动时,Spring 会根据配置文件中的 BeanDefinition 信息创建相应的 Bean 实例,并根据指定的依赖关系进行注入。

三、BeanFactory 继承关系

BeanFactory 有多种实现类,它们之间存在着继承关系。这种继承关系使得不同的实现类可以根据具体的需求提供不同的功能扩展。例如,ApplicationContext 是 BeanFactory 的子接口,它提供了更多面向实际应用的功能,如支持国际化、资源加载、事件传播等。通过继承关系,Spring 能够在保持核心功能稳定的同时,不断扩展和增强其功能。

四、依赖日志框架

Spring 的核心容器在运行过程中会产生大量的日志信息,这些日志信息对于调试和监控应用程序的运行状态非常重要。为了实现日志功能,Spring 通常会依赖于外部的日志框架,如 Log4j、Logback 等。通过与日志框架的集成,Spring 能够在不同的环境中灵活地配置日志输出级别、格式等,为开发人员提供详细的运行时信息。

2. 上下文

Spring 上下文是一个配置文件,向 Spring 框架提供上下文信息,包括企业服务如 JNDI、EJB、电子邮件、国际化、校验和调度功能等。上下文组件包括 ApplicationContext、ConfigurableApplicationContext、WebApplicationContext 等,它们扩展了 BeanFactory 的功能,提供了更多面向实际应用的功能。

一、ApplicationContext 的作用

ApplicationContext 是 Spring 上下文的核心接口之一,它提供了一种高级的方式来管理和访问 Spring 容器中的 Bean。与 BeanFactory 相比,ApplicationContext 不仅具备 BeanFactory 的所有功能,还提供了更多企业级的服务。例如,它支持国际化,可以根据不同的语言环境加载相应的资源文件;支持事件传播,允许 Bean 之间通过发布和监听事件进行通信;支持资源加载,可以方便地读取外部配置文件、图片、音频等资源。

二、ConfigurableApplicationContext 的功能

ConfigurableApplicationContext 是 ApplicationContext 的子接口,它提供了一些额外的配置功能。通过 ConfigurableApplicationContext,开发人员可以在运行时动态地修改 Spring 容器的配置,如添加或删除 Bean、修改 Bean 的属性值等。这使得应用程序在运行过程中能够更加灵活地适应不同的环境和需求。

三、WebApplicationContext 的特点

WebApplicationContext 是专门为 Web 应用程序设计的上下文实现。它建立在应用程序上下文模块之上,为基于 Web 的应用程序提供了特定的上下文环境。WebApplicationContext 能够与 Web 容器(如 Tomcat、Jetty 等)进行集成,实现对 Web 应用程序中 Bean 的管理和生命周期的控制。例如,在 Web 应用程序中,当一个请求到达服务器时,WebApplicationContext 可以根据请求的上下文信息创建相应的 Bean 实例,并将其注入到处理请求的控制器中。

3. AOP

AOP 是面向切面编程,通过配置管理特性,将面向方面的编程功能集成到 Spring 框架中,为基于 Spring 的应用程序中的对象提供了事务管理服务。AOP 的实现原理是通过目标类的代理类实现,包括静态 AOP 和动态 AOP 机制,Spring 对 AOP 的支持包括自动生成 AOP 代理、管理代理的依赖关系等。

一、AOP 的基本概念和作用

AOP(Aspect-Oriented Programming)是一种编程思想,它将横切关注点从业务逻辑中分离出来,以提高代码的可维护性和可重用性。在传统的编程模式中,业务逻辑和横切关注点(如日志记录、事务管理、安全检查等)通常混合在一起,导致代码的可读性和可维护性降低。而 AOP 通过将这些横切关注点封装成切面,在不修改业务逻辑代码的情况下,将其动态地织入到业务逻辑中,实现了业务逻辑和横切关注点的分离。

二、AOP 的实现原理

AOP 的实现原理是通过目标类的代理类实现。Spring AOP 主要采用动态代理技术,在运行时生成目标类的代理对象,将切面逻辑织入到代理对象中。代理对象封装了目标对象,并拦截被通知方法的调用,在将调用转发给真正的目标对象之前或之后执行切面逻辑。这种方式使得开发人员可以在不修改目标对象代码的情况下,为其添加额外的功能。

Spring AOP 支持两种代理方式:JDK 动态代理和 CGLIB 动态代理。如果目标对象实现了接口,Spring 会优先使用 JDK 动态代理;如果目标对象没有实现接口,则会使用 CGLIB 动态代理。JDK 动态代理是通过反射机制实现的,要求目标对象必须实现接口;而 CGLIB 动态代理是通过继承目标对象生成子类的方式实现的,不需要目标对象实现接口,但不能对 final 类、private 方法和 static 方法进行代理。

三、Spring 对 AOP 的支持

Spring 对 AOP 的支持非常丰富,包括自动生成 AOP 代理、管理代理的依赖关系等。Spring 提供了多种方式来定义切面和通知,如基于 XML 配置、基于注解和基于 Java 代码的方式。开发人员可以根据自己的需求选择合适的方式来实现 AOP。

Spring AOP 支持五种类型的通知:前置通知(Before)、后置通知(AfterReturning)、返回通知(After-returning)、异常通知(AfterThrowing)和环绕通知(Around)。这些通知可以在目标方法执行的不同阶段执行切面逻辑,满足不同的业务需求。例如,前置通知可以在目标方法执行之前进行权限检查、参数校验等操作;后置通知可以在目标方法执行成功后进行日志记录、缓存更新等操作;异常通知可以在目标方法抛出异常时进行异常处理、事务回滚等操作;环绕通知可以在目标方法执行前后进行更加复杂的逻辑处理,如性能监控、事务管理等。

此外,Spring AOP 还支持切入点(Pointcut)的定义,用于指定哪些连接点(Joinpoint)需要被织入切面。连接点是在应用执行过程中能够插入切面的一个点,如方法调用、抛出异常等。切入点通过正则表达式或明确的类和方法名称来指定需要被织入切面的连接点。这样,开发人员可以更加精确地控制切面的织入位置,提高代码的灵活性和可维护性。

四、Spring 的自动配置和注解

1. Spring Boot 自动配置

Spring Boot 的自动配置是一种强大的特性,它极大地简化了开发过程。当 Spring 容器启动后,一些自动配置类会自动存入到 IoC 容器中,无需手动声明。其原理是让第三方作者通过在第三方 jar 包中实现特定的接口或使用特定的注解,使得 Spring 框架能够自动识别并加载这些 Bean。

一个 Spring Boot 工程想要成功运行,通常需要一个主程序类,该类被@SpringBootApplication注解标识。而自动配置的相关工作就在这个注解上。@SpringBootApplication是一个复合注解,由@SpringBootConfiguration、@EnableAutoConfiguration和@ComponentScan这三个注解组成。

  • @SpringBootConfiguration:在其源码中可以发现有@Configuration,代表这是一个配置类,说明主程序类也是一个配置类。
  • @ComponentScan:指定扫描哪些组件,默认是扫描主程序所在的包以及其子包。
  • @EnableAutoConfiguration:告诉 Spring Boot 开启自动配置功能。其中,@AutoConfigurationPackage将指定的一个包下的所有组件导入到容器当中。自动配置包通过@Import({AutoConfigurationPackages.Registrar.class})注解实现,Registrar 类的方法将主程序类所在的包以及子包的名称放入到一个 String 数组当中,再将该数组中的包的所有组件导入到容器当中。此外,@Import({AutoConfigurationImportSelector.class})注解的核心函数selectImports()获取spring.factories中EnableAutoConfiguration所对应的Configuration类列表,从META-INF/spring.factories位置加载文件,将需要导入到容器中的组件名加载进来。每一个自动配置类都是容器中的一个组件,用它们来做自动配置,按需开启自动配置项,虽然启动时默认全部加载,但会按照条件装配规则,最终按需配置,需要使用到@Conditional注解以及其子注解。

2. Spring 重要注解

Spring 提供了大量的注解,用于不同的场景。

  • @Component、@Controller、@Service、@Repository:这些注解的作用都是实现 bean 的注入,在 Java 的 web 开发中提供不同的功能。@Component泛指组件,当组件不好归类的时候可以使用这个注解进行标注;@Controller用于标识此类为控制层,相当于 struts 中的 action 层;@Service用于标识此类为服务层,主要用来进行业务的逻辑处理;@Repository用于标识此类为数据访问层,也可以说用于标注数据访问组件,即 DAO 组件。
  • @Autowired:自动装配对象,在 Spring 容器中查找相应的对象并将其注入到需要使用的地方。可以用于构造函数、属性、Setter 方法和方法参数上。默认优先按照类型取 IOC 容器中寻找对应的组件,如果有多个相同类型的组件,再将属性的名称作为组件的 id 去容器中查找。可以配合@Primary使用,当使用@Autowired自动装配时,默认优先选择被注解@Primary标注的组件;也可以配合@Qualifier使用,使用注解@Qualifier可以指定需要装配组件的 id。
  • @Configuration:用于声明一个类作为 Spring 应用程序上下文的配置类。声明类为配置类,声明 Bean,声明环境属性,导入其他配置类。
  • @ComponentScan:指定要扫描的包,相当于在 XML 配置中<context:component-scan base-package="com.org"/>。
  • @Bean:被标注方法的返回值将以指定的名称存储到 Spring 容器中。
  • @Aspect、@Before、@After、@Around、@Pointcut:用于切面编程,分别表示切面、前置通知、后置通知、环绕通知和切点。

五、Spring 的事务管理

Spring 支持编程式事务管理和声明式事务管理两种方式。编程式事务控制需要使用 TransactionTemplate 来实现,对业务代码有侵入性,很少被使用。声明式事务管理建立在 AOP 之上,通过在配置文件中做相关的事务规则声明或使用 @Transactional 注解,将事务规则应用到业务逻辑中,最大的优点是不需要在业务逻辑代码中掺杂事务管理的代码。

声明式事务管理中的 @Transactional 注解

在 Spring 中,声明式事务管理常常使用@Transactional注解。这个注解可以应用于接口、接口方法、类以及类方法上,当作用于类上时,该类的所有 public 方法将都具有该类型的事务属性,同时,也可以在方法级别使用该标注来覆盖类级别的定义。

@Transactional 注解的属性信息
  • 事务传播行为介绍
    • @Transactional(propagation=Propagation.REQUIRED):如果有事务,那么加入事务,没有的话新建一个 (默认情况下)。
    • @Transactional(propagation=Propagation.NOT_SUPPORTED):容器不为这个方法开启事务。
    • @Transactional(propagation=Propagation.REQUIRES_NEW):不管是否存在事务,都创建一个新的事务,原来的挂起,新的执行完毕后继续执行老的事务。
    • @Transactional(propagation=Propagation.MANDATORY):必须在一个已有的事务中执行,否则抛出异常。
    • @Transactional(propagation=Propagation.NEVER):必须在一个没有的事务中执行,否则抛出异常 (与Propagation.MANDATORY相反)。
    • @Transactional(propagation=Propagation.SUPPORTS):如果其他 bean 调用这个方法,在其他 bean 中声明事务那就用事务,如果其他 bean 没有声明事务,那就不用事务。
  • 事物超时设置:@Transactional(timeout=30) //默认是 30 秒。
  • 事务隔离级别
    • @Transactional(isolation = Isolation.READ_UNCOMMITTED):读取未提交数据 (会出现脏读,不可重复读),基本不使用。
    • @Transactional(isolation = Isolation.READ_COMMITTED):读取已提交数据 (会出现不可重复读和幻读)。
    • @Transactional(isolation = Isolation.REPEATABLE_READ):可重复读 (会出现幻读)。
    • @Transactional(isolation = Isolation.SERIALIZABLE):串行化。MYSQL:默认为REPEATABLE_READ级别;SQLSERVER:默认为READ_COMMITTED。

脏读:一个事务读取到另一事务未提交的更新数据。不可重复读:在同一事务中,多次读取同一数据返回的结果有所不同,即后续读取可能读到另一事务已提交的更新数据,相反,“可重复读” 在同一事务中多次读取数据时能够保证所读数据一样,即后续读取不能读到另一事务已提交的更新数据。幻读:一个事务读到另一个事务已提交的 insert 数据。

@Transactional 注解的使用场景举例
  1. 场景一,最常见的用法,在方法上使用@Transactional注解,事务正常起作用。无异常时正常提交,有异常时数据回滚,代码如下:
@Servicepublic class ComeServiceImpl implements ComeService {@AutowiredUserMapper userMapper;@Override@Transactionalpublic int saveUser() {User user1 = new User(11,"a",111,"a");userMapper.saveUser(user1);User user2 = new User(11,"b",111,"b");userMapper.saveUser(user2);return 0;}}
  1. 场景二,常见的用法,在类上使用@Transactional注解,对整个类的方法,事务起作用。无异常时正常提交,有异常时数据回滚,代码如下:
 
@Service@Transactionalpublic class ComeServiceImpl implements ComeService {@AutowiredUserMapper userMapper;@Overridepublic int saveUser() {User user1 = new User(11,"a",111,"a");userMapper.saveUser(user1);User user2 = new User(11,"b",111,"b");userMapper.saveUser(user2);return 0;}}
  1. 场景三,将异常信息使用 try-catch 包裹,异常被处理,@Transactional注解不起作用,数据提交,没有回滚,代码如下:
 
@Service@Slf4jpublic class ComeServiceImpl implements ComeService {@AutowiredUserMapper userMapper;@Override@Transactionalpublic int saveUser() {User user1 = new User(11,"a",111,"a");userMapper.saveUser(user1);User user2 = new User(11,"b",111,"b");userMapper.saveUser(user2);try {int i = 1/0;} catch (Exception e){System.out.println("异常。。。");}return 0;}}
  1. 场景四,同一个 Service 内方法调用,当@Transactional注解作用在 A 方法上时,事务起作用。方法 A 中的数据回滚,方法 B 中的数据回滚,代码如下:
 
@Service@Slf4jpublic class ComeServiceImpl implements ComeService {@AutowiredUserMapper userMapper;@Override@Transactionalpublic int A() {User user1 = new User(11,"a",111,"a");userMapper.saveUser(user1);this.B();return 0;}@Overridepublic int B() {User user2 = new User(11,"b",111,"b");userMapper.saveUser(user2);int i = 1 / 0;return 0;}}
  1. 场景五,同一个 Service 内方法调用,当@Transactional注解作用在 B 方法上时,事务不起作用。方法 A 中的数据提交,方法 B 中数据提交,遇到异常没有回滚,代码如下:
 
@Service@Slf4jpublic class ComeServiceImpl implements ComeService {@AutowiredUserMapper userMapper;@Overridepublic int A() {User user1 = new User(11,"a",111,"a");userMapper.saveUser(user1);this.B();return 0;}@Override@Transactionalpublic int B() {User user2 = new User(11,"b",111,"b");userMapper.saveUser(user2);int i = 1 / 0;return 0;}}
  1. 场景六,同一个 Service 内方法调用,当@Transactional注解作用在类上时,事务起作用,数据回滚,代码如下:
 
@Service@Slf4j@Transactionalpublic class ComeServiceImpl implements ComeService {@AutowiredUserMapper userMapper;@Overridepublic int A() {User user1 = new User(11,"a",111,"a");userMapper.saveUser(user1);this.B();return 0;}@Overridepublic int B() {User user2 = new User(11,"b",111,"b");userMapper.saveUser(user2);int i = 1 / 0;return 0;}}
  1. 场景七,同一个 Service 内方法调用私有的方法 C,当@Transactional注解作用在方法 A 上时,事务起作用,数据回滚,代码如下:
 
@Service@Slf4jpublic class ComeServiceImpl implements ComeService {@AutowiredUserMapper userMapper;@Override@Transactionalpublic int A() {User user1 = new User(11,"a",111,"a");userMapper.saveUser(user1);this.C();return 0;}private int C() {User user2 = new User(11,"b",111,"b");userMapper.saveUser(user2);int i = 1 / 0;return 0;}@Overridepublic int B() {return 0;}}
  1. 场景八,同一个 Service 内方法调用私有的方法 C,当@Transactional注解作用在方法 C 上时,事务不起作用,方法 A 中的数据提交,方法 C 中的数据提交,代码如下:
 
@Service@Slf4jpublic class ComeServiceImpl implements ComeService {@AutowiredUserMapper userMapper;@Overridepublic int A() {User user1 = new User(11,"a",111,"a");userMapper.saveUser(user1);this.C();return 0;}@Transactionalprivate int C() {User user2 = new User(11,"b",111,"b");userMapper.saveUser(user2);int i = 1 / 0;return 0;}@Overridepublic int B() {return 0;}}
  1. 场景九,不同 Service 方法间调用,当@Transactional注解作用在方法 A 上时,事务起作用,方法 A 中的数据回滚,方法 saveClassInfo 中的数据回滚,代码如下:
 
@Service@Slf4jpublic class ComeServiceImpl implements ComeService {@AutowiredUserMapper userMapper;@AutowiredClassInfoService classInfoService;@Override@Transactionalpublic int A() {User user1 = new User(11,"a",111,"a");userMapper.saveUser(user1);classInfoService.saveClassInfo();return 0;}@Overridepublic int B() {return 0;}}@Servicepublic class ClassInfoServiceImpl implements ClassInfoService {@AutowiredClassInfoMapper classInfoMapper;@Overridepublic int saveClassInfo() {ClassInfo classInfo = new ClassInfo("c","c",69D);classInfoMapper.saveClassInfo(classInfo);int i = 1/0;return 0;}}
  1. 场景十,不同 Service 方法间调用,当@Transactional注解作用在方法 saveClassInfo 上时,事务对 A 不起作用,方法 A 中的数据提交,方法 saveClassInfo 数据回滚,代码如下:
 
@Service@Slf4jpublic class ComeServiceImpl implements ComeService {@AutowiredUserMapper userMapper;@AutowiredClassInfoService classInfoService;@Overridepublic int A() {User user1 = new User(11,"a",111,"a");userMapper.saveUser(user1);classInfoService.saveClassInfo();return 0;}@Overridepublic int B() {return 0;}}@Servicepublic class ClassInfoServiceImpl implements ClassInfoService {@AutowiredClassInfoMapper classInfoMapper;@Override@Transactionalpublic int saveClassInfo() {ClassInfo classInfo = new ClassInfo("c","c",69D);classInfoMapper.saveClassInfo(classInfo);int i = 1/0;return 0;}}
需要注意的几点
  1. @Transactional注解只能被应用到 public 方法上,对于其它非 public 的方法,如果标记了@Transactional也不会报错,但方法没有事务功能。
  1. spring 事务管理器,由 spring 来负责数据库的打开、提交、回滚,默认遇到运行期异常 (throw new RuntimeException("注释");) 会回滚,即遇到不受检查(unchecked)的异常时执行回滚;而遇到需要捕获的异常 (throw new Exception("注释");) 不会回滚,即遇到受检查的异常(就是非运行时抛出的异常,编译器会检查到的异常叫受检异常)时,需要我们指定方式来让事务回滚。如果要想所有异常都回滚,则要加上@Transactional( rollbackFor=Exception.class),如果让 unchecked 异常不回滚:@Transactional(notRollbackFor=RunTimeException.class)。如下代码所示:
 
@Transactional(rollbackFor=Exception.class) //指定回滚,遇到异常 Exception 时回滚public void methodName() {throw new Exception("注释");}@Transactional(noRollbackFor=Exception.class)//指定不回滚,遇到运行期例外(throw new RuntimeException("注释");)会回滚public ItimDaoImpl getItemDaoImpl() {throw new RuntimeException("注释");}
  1. @Transactional注解应该只被应用到 public 可见度的方法上。如果你在 protected、private 或者 package-visible 的方法上使用@Transactional注解,它也不会报错,但是这个被注解的方法将不具有事务功能。
  1. @Transactional注解可以被应用于接口定义和接口方法、类定义和类的 public 方法上。然而,请注意仅仅@Transactional注解的出现不足于开启事务行为,它仅仅是一种元数据,能够被可以识别。

声明式事务管理的实现原理

Spring 的声明式事务管理主要是通过 AOP 实现的,具体步骤如下:

  1. 启动时扫描@Transactional注解:在启动时,Spring Boot 会扫描所有使用了@Transactional注解的方法,并将其封装成TransactionAnnotationParser对象。
  1. 将TransactionInterceptor织入到目标方法中:在 AOP 编程中,使用 AspectJ 编写切面类,通过@Around注解将TransactionInterceptor织入到目标方法中。
  1. 在目标方法执行前创建事务:在目标方法执行前,TransactionInterceptor会调用PlatformTransactionManager创建一个新的事务,并将其纳入到当前线程的事务上下文中。
  1. 执行目标方法:在目标方法执行时,如果发生异常,则将事务状态标记为ROLLBACK_ONLY;否则,将事务状态标记为COMMIT。
  1. 提交或回滚事务:在目标方法执行完成后,TransactionInterceptor会根据事务状态(COMMIT或ROLLBACK_ONLY)来决定是否提交或回滚事务。

源码如下:

 
@Override@Nullablepublic Object invoke(MethodInvocation invocation) throws Throwable {// Work out the target class: may be {@code null}.// The TransactionAttributeSource should be passed the target class// as well as the method, which may be from an interface.Class<?> targetClass = (invocation.getThis()!= null? AopUtils.getTargetClass(invocation.getThis()) : null);// Adapt to TransactionAspectSupport's invokeWithinTransaction...return invokeWithinTransaction(invocation.getMethod(), targetClass, new CoroutinesInvocationCallback() {@Override@Nullablepublic Object proceedWithInvocation() throws Throwable {return invocation.proceed();}@Overridepublic Object getTarget() {return invocation.getThis();}@Overridepublic Object[] getArguments() {return invocation.getArguments();}});}

下面是核心处理方法,把不太重要的代码忽略了,留下每一步的节点。

 
@Nullableprotected Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass, final InvocationCallback invocation) throws Throwable {// 获取事务属性final TransactionManager tm = determineTransactionManager(txAttr);// 准备事务TransactionInfo txInfo = prepareTransactionInfo(ptm, txAttr, joinpointIdentification, status);// 执行目标方法Object retVal = invocation.proceedWithInvocation();// 回滚事务completeTransactionAfterThrowing(txInfo, ex);// 提交事务commitTransactionAfterReturning(txInfo);}

声明式事务管理的优缺点

  • 优点
    • 与业务逻辑分离:声明式事务管理将事务管理逻辑从业务代码中分离出来,使得业务逻辑更清晰,降低了代码的耦合性。
    • 配置简单:通过注解或 XML 配置,可以简单地定义事务的传播行为、隔离级别等属性,而无需在每个业务方法中编写重复的事务管理代码。
    • 易于维护:由于事务管理逻辑集中在配置中,易于维护和修改,提高了代码的可读性和可维护性。
    • 提高一致性:声明式事务管理可以确保在所有业务方法中都应用相同的事务管理策略,提高了事务管理的一致性。
  • 缺点
    • 灵活性有限:声明式事务管理的灵活性相对较低,无法在运行时动态地改变事务管理策略,有一定的局限性。
    • RPC 远程调用成功,但是本地事务回滚了,RPC 调用无法回滚。并且事务中有远程调用,会拉长整个事务,导致本地


http://www.ppmy.cn/embedded/141010.html

相关文章

利用DeepFlow解决APISIX故障诊断中的方向偏差问题

概要&#xff1a;随着APISIX作为IT应用系统入口的普及&#xff0c;其故障定位能力的不足导致了在业务故障诊断中&#xff0c;APISIX常常成为首要的“嫌疑对象”。这不仅导致了“兴师动众”式的资源投入&#xff0c;还可能使诊断方向“背道而驰”&#xff0c;从而导致业务故障“…

0 基础 入门简单 linux操作 上篇 利用apt命令装13 linux搭建自己的服务器

前言 目前web网站大多数都是以linux服务器为主 &#xff0c; 还有就是kali工具都是 linux 所以说这个Linux很重要呀 前期准备 &#xff1a; 为了方便我建议直接去阿里白嫖 1年 新人云服务器 然后就是一个远程连接软件&#xff08;这里建议使用 finnalshell 或者Xs…

Windows Pycharm 远程 Spark 开发 PySpark

一、环境版本 环境版本PyCharm2024.1.2 (Professional Edition)Ubuntu Kylin16.04Hadoop3.3.5Hive3.1.3Spark2.4.0 二、Pycharm远程开发 文件-远程-开发 选择 SSH连接&#xff0c;连接虚拟机&#xff0c;选择项目目录即可远程开发

wget/curl命令笔记

wget/curl命令使用笔记 操作wgetcurl备注输出到终端wget -q -O - http://example.com/file.zipcurl http://example.com/file.txtcurl默认输出到终端直接下载wget http://example.com/file.zipcurl -O http://example.com/file.zip文件名与远程文件名相同发送 JSON 数据wget -…

Cmakelist.txt之win-c-udp-client

1.cmakelist.txt cmake_minimum_required(VERSION 3.16) ​ project(c_udp_client LANGUAGES C) ​ add_executable(c_udp_client main.c) ​ target_link_libraries(c_udp_client wsock32) ​ ​ include(GNUInstallDirs) install(TARGETS c_udp_clientLIBRARY DESTINATION $…

深入探索API爬虫工作的技术难点与高效解决思路

在大数据与信息化高速发展的今天&#xff0c;API&#xff08;应用程序编程接口&#xff09;爬虫成为了数据收集与分析的重要工具。然而&#xff0c;API爬虫工作并非一帆风顺&#xff0c;它面临着诸多技术挑战。本文将深入探讨几个API爬虫工作的技术难点&#xff0c;并提出相应的…

类文件结构详解.下

当前类、父类、接口索引集合 u2 this_class;//当前类u2 super_class;//父类u2 interfaces_count;//接口数量u2 interfaces[interfaces_count];//一个类可以实现多个接口 Java 类的继承关系由类索引、父类索引和接口索引集合三…

C++ 结构体(struct)

C 结构体&#xff08;struct&#xff09; 在C编程语言中&#xff0c;结构体&#xff08;struct&#xff09;是一种强大的数据结构&#xff0c;它允许我们将不同类型的数据项组合成一个单一的类型。本文将深入探讨C中结构体的相关知识点&#xff0c;并提供实际应用示例。 结构…