Appearance
Spring
1.1 框架概念
1.1.1 生活中框架
框架(
Framework
)是一个集成了基本结构、规范、设计模式、编程语言和程序库等基础组件的软件系统,它可以用来构建更高级别的应用程序。框架的设计和实现旨在解决特定领域中的常见问题,帮助开发人员更高效、更稳定地实现软件开发目标。小结框架思维:为解决特定问题而提供的一整套解决方案
1.1.2 程序中框架
目前市面主流框架有很多,本阶段主要学习:SSM
- Spring:为所有bean(组件)提供管理解决方案
- SpringMVC:为解决表述层(控制层)常见问题,而提供的一整套解决方案,如:处理请求数据,响应数据,RESTFul等问题
- Mybatis:为解决数据访问层(Dao层),而提供一整套解决方案,如:简化传统JDBC代码,优化入参出参等问题
框架的优点包括以下几点:
- 提高开发效率:框架提供了许多预先设计好了的组件和工具,能够帮助开发人员快速进行开发。相较于传统手写代码,在框架提供的规范化环境中,开发者可以更快地实现项目的各种要求。
- 降低开发成本:框架的提供标准化的编程语言、数据操作等代码片段,避免了重复开发的问题,降低了开发成本,提供深度优化的系统,降低了维护成本,增强了系统的可靠性。
- 提高应用程序的稳定性:框架通常经过了很长时间的开发和测试,其中的许多组件、代码片段和设计模式都得到了验证。重复利用这些组件有助于减少bug的出现,从而提高了应用程序的稳定性。
- 提供标准化的解决方案:框架通常是针对某个特定领域的,通过提供标准化的解决方案,可以为开发人员提供一种共同的语言和思想基础,有助于更好地沟通和协作。
1.2 组件与容器概念
组件:具有一定功能的对象,常见组件如下:
- Controller层组件
- Service层组件
- Dao层组件
容器:管理组件的对象,包括组件的创建,获取及销毁等
- 生活中容器
- 简单容器:水杯,电脑包等
- 复杂容器:汽车,房屋等
- 程序中容器
- 简单容器:数组,集合等
- 复杂容器:Spring框架
第2章 Spring Framework简介
2.1 Spring广义与狭义
spring官网:https://spring.io/
广义的 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框架为基础!
2.2 Spring Framework概述
Spring是基于IOC和AOP的容器框架
IoC容器 | 核心容器
Spring IoC 容器,负责实例化、配置和组装 bean(组件)。容器通过读取配置元数据来获取有关要实例化、配置和组装组件的指令。
IoC(Inversion of Control)控制反转
IoC 主要是针对对象的创建和调用控制而言的,也就是说,当应用程序需要使用一个对象时,不再是应用程序直接创建该对象,而是由 IoC 容器来创建和管理,即控制权由应用程序转移到 IoC 容器中,也就是“反转”了控制权。这种方式基本上是通过依赖查找的方式来实现的,即 IoC 容器维护着构成应用程序的对象,并负责创建这些对象。
DI (Dependency Injection) 依赖注入
DI 是指在组件之间传递依赖关系的过程中,将依赖关系在容器内部进行处理,这样就不必在应用程序代码中硬编码对象之间的依赖关系,实现了对象之间的解耦合。在 Spring 中,DI 是通过 XML 配置文件或注解的方式实现的。它提供了三种形式的依赖注入:构造函数注入、Setter 方法注入和接口注入。
AOP:Aspect Oriented Programming面向切面编程思想,它可以在不修改源代码的情况下,给程序动态统一添加额外功能,AOP可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。(后面章节详细讲解)
主要功能模块如下
功能模块 功能介绍 Core Container 核心容器,控制反转和依赖注入 AOP&Aspects 面向切面编程 TX 声明式事务管理 Testing 快速整合测试环境 Data Access/Integration 提供了对数据访问/集成的功能。 Spring MVC 提供了面向Web应用程序的集成功能。
2.3 Spring Framework特点
- 丰富的生态系统:Spring 生态系统非常丰富,支持许多模块和库,如 Spring Boot、Spring Security、Spring Cloud 等等,可以帮助开发人员快速构建高可靠性的企业应用程序。
- 模块化的设计:框架组件之间的松散耦合和模块化设计使得 Spring Framework 具有良好的可重用性、可扩展性和可维护性。开发人员可以轻松地选择自己需要的模块,根据自己的需求进行开发。
- 简化 Java 开发:Spring Framework 简化了 Java 开发,提供了各种工具和 API,可以降低开发复杂度和学习成本。同时,Spring Framework 支持各种应用场景,包括 Web 应用程序、RESTful API、消息传递、批处理等等。
- 不断创新和发展:Spring Framework 开发团队一直在不断创新和发展,保持与最新技术的接轨,为开发人员提供更加先进和优秀的工具和框架。
因此,这些优点使得 Spring Framework 成为了一个稳定、可靠、且创新的框架,为企业级 Java 开发提供了一站式的解决方案。
Spring 使创建 Java 企业应用程序变得容易。它提供了在企业环境中采用 Java 语言所需的一切,支持 Groovy 和 Kotlin 作为 JVM 上的替代语言,并且可以根据应用程序的需求灵活地创建多种架构。从Spring Framework 6.0开始,Spring 需要 Java 17+。
2.4 Spring Framework底层实现
Spring框架底层使用BeanFactory接口实现的,具体相关接口及实现类如下:
BeanFactory
接口提供了一种高级配置机制,能够管理任何类型的对象,它是SpringIoC容器标准化超接口!
ApplicationContext
是BeanFactory
的子接口。它补充说:
- 更容易与 Spring 的 AOP 功能集成
- 消息资源处理(用于国际化)
- 特定于应用程序给予此接口实现,例如Web 应用程序的
WebApplicationContext
简而言之,BeanFactory
提供了配置框架和基本功能,而ApplicationContext
添加了更多特定于企业的功能。ApplicationContext
是BeanFactory
的完整超集!
第3章 SpringIOC/DI基本实现
实现SpringIOC核心思想
- 将组件装配到SpringIOC容器对象中
- 创建SpringIOC容器对象
- 通过SpringIOC容器对象获取组件
实现Spring框架搭建方式
- 方式一:纯XML方式
- 方式二:XML+注解方式
- 方式三:配置类+注解方式
实现Spring框架环境准备
3.1 基于XML方式装配组件
3.1.1 环境准备
xml
<dependencies>
<!--当你引入Spring Context依赖之后,表示将Spring的基础依赖引入了-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>6.0.6</version>
</dependency>
<!-- https://mvnrepository.com/artifact/org.junit.jupiter/junit-jupiter-api -->
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-api</artifactId>
<version>5.10.2</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.18.30</version>
</dependency>
</dependencies>
3.1.2 实现关键步骤
准备POJO类:Student
javapackage com.at.bean; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; /** * @Author at * @CreateTime: 2024/11/15 */ @Data @NoArgsConstructor @AllArgsConstructor public class Student { private Integer stuId; private String stuName; private Integer stuAge; }
创建配置文件:spring.xml
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" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- 将Student装配到IOC容器--> <bean id="zs" class="com.at.bean.Student"> <property name="stuId" value="1001"></property> <property name="stuName" value="zs"></property> <property name="stuAge" value="18"></property> </bean> </beans>
测试类
java@Test public void testStudent(){ //创建容器对象 ApplicationContext ioc = new ClassPathXmlApplicationContext("spring.xml"); //从容器获取Student Student bean = ioc.getBean(Student.class); System.out.println("bean = " + bean); }
3.2 基于配置类装配组件
3.2.1 环境准备(同上)
3.2.2 实现关键步骤
准备POJO类:Student
- 代码同上
创建配置类:SpringConfig
javapackage com.at; import com.at.bean.Student; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; /** * @Author at * @CreateTime: 2024/11/15 */ @Configuration //标识当前类是一个[配置类:代替xml配置文件] public class SpringConfig { /** 将student对象装配到IOC容器中,id名称=方法名称 */ @Bean public Student student(){ return new Student(1001, "zhangsan", 18); } }
测试类
java@Test public void testSpring(){ //创建容器对象(基于配置类) ApplicationContext ioc = new AnnotationConfigApplicationContext(SpringConfig.class); Student bean = ioc.getBean(Student.class); System.out.println("bean = " + bean); }
3.3 @Bean注解详解
在Spring框架中,
@Bean
注解是用于方法级别的注解,它告诉Spring容器这是一个bean的定义,并且该方法会返回一个对象,这个对象应该被注册为Spring应用上下文中的一个bean。@Bean
注解通常与@Configuration
类一起使用,以声明性的方式配置Spring IoC容器。
3.3.1 @Bean源码
java
package org.springframework.context.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.core.annotation.AliasFor;
@Target({ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Bean {
@AliasFor("name")
String[] value() default {};
@AliasFor("value")
String[] name() default {};
boolean autowireCandidate() default true;
String initMethod() default "";
String destroyMethod() default "(inferred)";
}
@Bean
注解中属性说明
name
:指定bean的名称。如果不提供,默认会采用方法名作为bean的名称。value
和name属性是别名关系,即它们代表相同的配置项。initMethod
:指定初始化方法,在bean注入完成之后执行。destroyMethod
:指定销毁方法,在包含bean的应用上下文关闭时调用。autowireCandidate:
定义了是否将此bean视为自动装配的候选者。默认值为true,意味着除非明确设置为false,否则该bean是可以作为其他bean依赖注入的候选对象。
3.3.2 使用@Bean装配DruidDataSource(第三方Bean)
导入DruidDataSource依赖
xml<!--DuirdDataSource坐标 --> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid</artifactId> <version>1.2.15</version> </dependency> <!--DruidDataSource启动器坐标(与上面坐标二选一即可)--> <dependency> <groupId>com.alibaba</groupId> <artifactId>druid-spring-boot-starter</artifactId> <version>1.2.8</version> </dependency> <dependency> <groupId>com.mysql</groupId> <artifactId>mysql-connector-j</artifactId> <scope>runtime</scope> </dependency>
编写配置文件:application.properties
properties#配置DruidDataSource jdbc.driverClassName=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/0923_demo jdbc.username=root jdbc.password=root
编写配置类:
- 未用属性类
javapackage com.at.config; import com.alibaba.druid.pool.DruidDataSource; import com.at.properties.JdbcProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; /** * @Author at * @CreateTime: 2024/11/26 */ @Configuration public class DruidConfig { @Value("${jdbc.driverClassName}") private String driverClassName; @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; @Bean public DataSource dataSource(){ DruidDataSource ds = new DruidDataSource(); ds.setDriverClassName(driverClassName); ds.setUrl(url); ds.setUsername(username); ds.setPassword(password); return ds; } }
- 使用属性类:
javapackage com.at.properties; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /** * @Author at * @CreateTime: 2024/11/26 */ @Component("jdbcProperties") @ConfigurationProperties(prefix = "jdbc") @Data public class JdbcProperties { private String driverClassName; private String url; private String username; private String password; }
javapackage com.at.config; import com.alibaba.druid.pool.DruidDataSource; import com.at.properties.JdbcProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; /** * @Author at * @CreateTime: 2024/11/26 */ @Configuration public class DruidConfig { @Autowired @Qualifier("jdbcProperties") private JdbcProperties jdbcProperties; @Bean public DataSource dataSource(/*JdbcProperties jdbcProperties*/){ DruidDataSource ds = new DruidDataSource(); ds.setDriverClassName(jdbcProperties.getDriverClassName()); ds.setUrl(jdbcProperties.getUrl()); ds.setUsername(jdbcProperties.getUsername()); ds.setPassword(jdbcProperties.getPassword()); return ds; } }
javapackage com.at.day13_springboot.property; import lombok.Data; import org.springframework.beans.factory.annotation.Value; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /** * @Author at * @CreateTime: 2025/01/14 */ //@Data @Component("jdbcProperties") //定义属性类,可以指定属性前缀(prefix) @ConfigurationProperties(prefix = "jdbc") public class JdbcProperties { // @Value("${jdbc.driverClassName}") private String driverClassName; private String url; private String username; private String password; @Override public String toString() { return "JdbcProperties{" + "driverClassName='" + driverClassName + '\'' + ", url='" + url + '\'' + ", username='" + username + '\'' + ", password='" + password + '\'' + '}'; } public String getDriverClassName() { return driverClassName; } public void setDriverClassName(String driverClassName) { this.driverClassName = driverClassName; } public String getUrl() { return url; } public void setUrl(String url) { this.url = url; } 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; } }
javapackage com.at.day13_springboot.config; import com.alibaba.druid.pool.DruidDataSource; import com.at.day13_springboot.property.JdbcProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; /** * @Author at * @CreateTime: 2025/01/14 */ @Configuration public class DruidConfig { // @Value("${jdbc.driverClassName}") // private String driverClassName; // @Value("${jdbc.url}") // private String url; // @Value("${jdbc.username}") // private String username; // @Value("${jdbc.password}") // private String password; @Autowired @Qualifier("jdbcProperties") private JdbcProperties jdbcProperties; @Bean("druidDataSource") public DruidDataSource getDruidDataSource() { DruidDataSource druidDataSource = new DruidDataSource(); druidDataSource.setDriverClassName(jdbcProperties.getDriverClassName()); druidDataSource.setUrl(jdbcProperties.getUrl()); druidDataSource.setUsername(jdbcProperties.getUsername()); druidDataSource.setPassword(jdbcProperties.getPassword()); return druidDataSource; } }
javapackage com.at.day13_springboot; import com.alibaba.druid.pool.DruidDataSource; import com.alibaba.druid.pool.DruidPooledConnection; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.context.ApplicationContext; import org.springframework.context.ConfigurableApplicationContext; import org.springframework.context.annotation.AnnotationConfigApplicationContext; import java.sql.SQLException; @SpringBootApplication public class Day13SpringbootApplication { public static void main(String[] args) throws SQLException { //springboot启动器,默认返回容器对象 ConfigurableApplicationContext ioc = SpringApplication.run(Day13SpringbootApplication.class, args); //获取容器对象 // ApplicationContext context = new AnnotationConfigApplicationContext(); DruidDataSource dataSource = ioc.getBean(DruidDataSource.class); DruidPooledConnection connection = dataSource.getConnection(); System.out.println("connection = " + connection); } }
3.3.3 Bean生命周期
生命周期的六个阶段
阶段一:加载Bean定义:
- Spring容器读取
XML文件
或其他配置文件
,解析配置信息。 - 将解析后的配置信息转换为Spring内部数据结构(
BeanDefinition
对象)。 - 存储
BeanDefinition
对象,待进行组件实例化。
- Spring容器读取
阶段二:实例化Bean组件:
- 根据
BeanDefinition
中的信息,实例化Bean对象。 - 如果有依赖其他Bean的情况,先实例化被依赖的Bean。
- 此步骤单纯实例化Bean和依赖的Bean组件,不会进行属性赋值。
- 根据
阶段三:设置Bean属性: - Spring容器将根据
BeanDefinition
中的配置,通过setter方法或字段直接注入属性值。 - Spring容器属性和实例化过程是分离的,所有在配置的时候,组件声明和引用不分先后顺序。阶段四:调用Bean的初始化方法:
- 如果Bean实现了
InitializingBean
接口,Spring将调用其afterPropertiesSet()
方法。 - 如果在XML配置中定义了
init-method
,则执行该方法。 - 如果Bean使用了
@PostConstruct
注解,则执行被注解的方法。 - 此阶段调用自定义初始化方法,可以进行相关的初始化工作,类似:
Servlet
的init
方法。
- 如果Bean实现了
阶段五:Bean可以使用:
- 此时Bean已经初始化完成,可以被其他Bean引用或者容器直接使用。
阶段六:调用Bean的销毁方法阶段(仅适用于单例Bean):
- 如果Bean实现了
DisposableBean
接口,Spring将调用其destroy()
方法。 - 如果在XML配置中定义了
destroy-method
,则执行该方法。 - 如果Bean使用了
@PreDestroy
注解,则在销毁之前执行被注解的方法。 - 此阶段调用自定义销毁方法,可以进行相关的初始化工作,类似:
Servlet
的destroy
方法。
- 如果Bean实现了
案例代码
POJO类:Student
javapackage com.at.bean; import lombok.AllArgsConstructor; import lombok.Data; import lombok.NoArgsConstructor; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Component; /** * @Author at * @CreateTime: 2024/11/15 */ public class Student { private Integer stuId; private String stuName; private Integer stuAge; public Student() { System.out.println("Student==>构造器!!!"); } public Student(Integer stuId, String stuName, Integer stuAge) { this.stuId = stuId; this.stuName = stuName; this.stuAge = stuAge; } public Integer getStuId() { return stuId; } public void setStuId(Integer stuId) { System.out.println("==>Student->setId()!!!"); this.stuId = stuId; } public String getStuName() { return stuName; } public void setStuName(String stuName) { this.stuName = stuName; } public Integer getStuAge() { return stuAge; } public void setStuAge(Integer stuAge) { this.stuAge = stuAge; } public void initStudent() { System.out.println("==>Student->init-method()!!!"); } public void destroyStudent() { System.out.println("==>Student->destroy-method()!!!"); } }
配置类:SpringConfig
javapackage com.at; import com.at.bean.Student; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; /** * @Author at * @CreateTime: 2024/11/15 */ @Configuration //标识当前类是一个[配置类:代替xml配置文件] public class SpringConfig { @Bean(value = "student", initMethod = "initStudent", destroyMethod = "destroyStudent") public Student student(){ Student student = new Student(); student.setStuId(101); return student; } }
测试类
java/** * @Author at * @CreateTime: 2024/11/15 */ public class TestSpring { @Test public void testSpring(){ //创建容器对象(基于配置类) ConfigurableApplicationContext ioc = new AnnotationConfigApplicationContext(SpringConfig.class); //从容器中获取Student对象 Student bean = ioc.getBean(Student.class); System.out.println("bean = " + bean); //关闭IOC容器对象 ioc.close(); }
3.3.4 Bean作用域
Bean作用域概念
在Spring框架中,bean的作用域(@Scope)决定了该bean的生命周期和可见性。Spring提供了多种作用域来满足不同的需求。
常用作用域
取值 含义 创建对象的时机 默认值 singleton 在 IOC 容器中,这个 bean 的对象始终为单实例 IOC 容器初始化时 是 prototype 这个 bean 在 IOC 容器中有多个实例 获取 bean 时 否 - 如果是在WebApplicationContext环境下还会有另外两个作用域(但不常用):
取值 含义 创建对象的时机 默认值 request 请求范围内有效的实例 每次请求 否 session 会话范围内有效的实例 每次会话 否 案例代码
POJO类:Student
javapackage com.at; import com.at.bean.Student; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Scope; /** * @Author at * @CreateTime: 2024/11/15 */ @Configuration //标识当前类是一个[配置类:代替xml配置文件] public class SpringConfig { @Bean(value = "student", initMethod = "initStudent", destroyMethod = "destroyStudent") // @Scope(value = "prototype") @Scope(value = "singleton") public Student student(){ Student student = new Student(); student.setStuId(101); return student; } }
测试类
java@Test public void testSpring(){ //创建容器对象(基于配置类) ApplicationContext ioc = new AnnotationConfigApplicationContext(SpringConfig.class); Student bean = ioc.getBean(Student.class); Student bean2 = ioc.getBean(Student.class); System.out.println("是否为同一对象:" + (bean==bean2)); }
第4章 SpringIOC/DI 深入学习
4.1 开启组件扫描实现分层研发
java
/**
* @Author at
* @CreateTime: 2024/11/15
*/
@Configuration //标识当前类是一个[配置类:代替xml配置文件]
@ComponentScan(basePackages = "com.at") //开启组件扫描:扫描com.atguigu包及其子包中的有效注解
public class SpringConfig {}
4.1.1 分层管理组件
分层管理组件,本质上是:将不同层组件装配到IOC容器,使用@Bean注解可以实现不同层组件的装配,但相对比较复杂,在实际研发中,一般使用如下注解,将分层中的组件装配到IOC容器.
注解 | 说明 |
---|---|
@Component | 该注解用于描述 Spring 中的 Bean,它是一个泛化的概念,仅仅表示容器中的一个组件(Bean),并且可以作用在应用的任何层次,例如 Service 层、Dao 层等。 使用时只需将该注解标注在相应类上即可。 |
@Repository | 该注解用于将数据访问层(Dao 层)的类标识为 Spring 中的 Bean,其功能与 @Component 相同。 |
@Service | 该注解通常作用在业务层(Service 层),用于将业务层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。 |
@Controller | 该注解通常作用在控制层(如SpringMVC 的 Controller),用于将控制层的类标识为 Spring 中的 Bean,其功能与 @Component 相同。 |
- 通过查看源码我们得知,@Controller、@Service、@Repository这三个注解只是在@Component注解的基础上起了三个新的名字。
- 对于Spring使用IOC容器管理这些组件来说没有区别,也就是语法层面没有区别。所以@Controller、@Service、@Repository这三个注解只是给开发人员看的,让我们能够便于分辨组件的作用。
注意:虽然它们本质上一样,但是为了代码的可读性、程序结构严谨!我们肯定不能随便胡乱标记。
案例代码
java@Component(value = "zs") //将Student组件装配到IOC容器 public class Student {} @Controller(value = "studentController") public class StudentController {} @Repository("studentDao") public class StudentDaoImpl implements StudentDao {} @Service("stuService") public class StudentServiceImpl implements StudentService {}
4.1.2 分层管理对象中属性
分层管理对象中属性,本质上是实现属性的自动注入,常用注解如下:
@Value
- 作用:为字面量实现自动装配
- 字面量:基本类型及其包装类+String
@Autowired
作用:为非字面量类型实现自动装配(自动将IOC容器中组件,装配到指定的位置)
自动装配规则
- 先按类型去IOC容器匹配对象
- 匹配1个对象,自动装配成功
- 匹配多个对象,再按id筛选对象
- 先按类型去IOC容器匹配对象
源码
- 位置:@Autowired注解可以书写在方法上(setxxx())或书写在成员变量上
- 属性:required
- true:表示当前被@Autowired注解标识的组件,必须自动装配,否则报错
- false:表示当前被@Autowired注解标识的组件,不必须自动装配,否则报错
@Qualifier
- 作用:配合@Autowired一起使用,指定需要被依赖注入的beanId
- 在Spring框架中,
@Qualifier
注解用于解决当有多个相同类型的bean时,自动装配(autowiring)哪一个bean的问题。当你的Spring应用上下文中存在多个相同类型的bean,并且你希望明确指定使用哪一个bean进行依赖注入时,@Qualifier
就显得非常有用 - 注意:@Qualifier一般不能单独使用,需要配合@Autowired一起使用
@Resource
@Resource 注解是Java EE(现在称为Jakarta EE)的一部分,主要导入相关依赖,它也可以在Spring框架中使用。
@Resource装配规则
- @Resource 默认情况下会按照名称进行匹配。它首先尝试根据字段名或setter方法参数名查找对应的bean
- 如果找到了一个匹配的bean,则注入该bean;如果找不到,则抛出异常。
- 如果没有找到与名称相匹配的bean,@Resource 会退而求其次,尝试根据类型进行匹配。
- 如果有且仅有一个匹配类型的bean,则注入该bean;如果有多个匹配类型的bean但名称不匹配,则抛出异常。
- @Resource 默认情况下会按照名称进行匹配。它首先尝试根据字段名或setter方法参数名查找对应的bean
注意:@Resource注解属于JDK,如JDK高于JDK11或低于JDK8,需要导入如下依赖
xml<dependency> <groupId>jakarta.annotation</groupId> <artifactId>jakarta.annotation-api</artifactId> <version>2.1.1</version> </dependency>
4.2 @Import注解
4.2.1 @Import注解简介
@Import是Spring框架提供的一个功能强大的工具,用于将一个或多个配置类导入到当前的配置中。它允许你在一个地方集中管理所有的配置类,从而简化了配置管理和维护
4.2.2 @Import注解作用
导入其他配置类:通过
@Import
注解,你可以将一个或多个配置类导入到当前的配置中,使得这些配置类中的bean定义和配置能够被当前应用上下文识别。导入单个配置类
java@Configuration @Import(DatabaseConfig.class) public class AppConfig { // ... }
导入多个配置类
java@Configuration @Import({ DatabaseConfig.class, WebConfig.class }) public class AppConfig { // ... }
引入非
@Configuration
类作为bean:可以使用@Import
来注册普通类(即不是用@Configuration
标注的类)为bean。如果你有一个普通的Java类,并希望它在Spring应用上下文中作为一个bean存在,可以直接使用
@Import
将其导入:java@Import(MyService.class) public class AppConfig { // MyService 将自动注册为一个bean }
引入实现
ImportSelector
或ImportBeanDefinitionRegistrar
接口的类:这允许你在导入时动态决定哪些bean应该被注册到Spring容器中。使用
ImportSelector
动态选择要导入的配置类ImportSelector
是一个接口,它允许你根据一定的逻辑动态决定要导入哪些配置类。你需要实现selectImports
方法,在该方法中返回一个字符串数组,表示要导入的配置类的全限定名。javapublic class MyImportSelector implements ImportSelector { @Override public String[] selectImports(AnnotationMetadata importingClassMetadata) { // 根据条件返回需要导入的配置类名称 return new String[] { "com.example.DatabaseConfig" }; } } @Configuration @Import(MyImportSelector.class) public class AppConfig { // 根据MyImportSelector的选择结果导入配置类 }
使用
ImportBeanDefinitionRegistrar
注册自定义bean定义ImportBeanDefinitionRegistrar
接口允许你在运行时向Spring容器注册额外的bean定义。这通常用于更复杂的场景,比如根据环境变量或其他条件动态创建bean。javapublic class MyBeanDefinitionRegistrar implements ImportBeanDefinitionRegistrar { @Override public void registerBeanDefinitions( AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) { // 动态注册bean定义 RootBeanDefinition beanDefinition = new RootBeanDefinition(MyCustomBean.class); registry.registerBeanDefinition("myCustomBean", beanDefinition); } } @Configuration @Import(MyBeanDefinitionRegistrar.class) public class AppConfig { // 动态注册bean }
4.2.3 @Import注解注意事项
@Import
不会触发组件扫描(@ComponentScan
),所以如果你想要让某些类成为Spring管理的bean,要么它们必须被显式地导入,要么它们所在的包需要被组件扫描。- 如果你正在使用Spring Boot并且你的主应用程序类位于根包下,那么通常不需要显式使用
@Import
,因为Spring Boot会自动处理很多常见的配置类。 @Import
与@Configuration
一起使用效果最佳,因为它确保了所有相关的bean都在同一个上下文中被正确地初始化和管理。
4.3 @Conditional注解详解
4.3.1 @Conditional简介
@Conditional
注解是Spring框架中用于条件化配置的核心注解之一。它允许根据特定的条件来决定是否应该创建一个bean或应用某个配置类。这是实现灵活和动态配置的关键机制,特别适用于微服务架构、多环境部署和支持多种运行时行为的应用程序。
4.3.2 @Conditional作用
条件化装配Bean:只有当某些条件满足时,才会创建该Bean。
@Conditional
需要与实现了Condition
接口的类一起使用。Condition
接口只有一个方法matches()
,该方法返回一个布尔值,指示条件是否匹配。如果返回true
,则创建Bean或应用配置;如果返回false
,则跳过。javapublic class MyCondition implements Condition { @Override public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) { // 实现条件逻辑 return true; //return false; } }
java@Configuration public class AppConfig { @Bean @Conditional(MyCondition.class) public MyService myService() { return new MyServiceImpl(); } }
支持复杂的条件逻辑:通过自定义条件类,可以实现非常复杂的决策逻辑。
为了简化常见的条件判断,Spring提供了一些@Conditional的衍生条件注解:
@ConditionalOnProperty
:基于配置文件中的属性值。@ConditionalOnClass
和@ConditionalOnMissingClass
:基于类路径中是否存在某个类。@ConditionalOnBean
和@ConditionalOnMissingBean
:基于容器中是否存在某个类型的bean。@ConditionalOnExpression
:基于SpEL表达式的条件。@ConditionalOnWebApplication
和@ConditionalOnNotWebApplication
:基于应用程序是否为Web应用。@ConditionalOnJava
:基于Java版本。@ConditionalOnResource
:基于资源是否存在。
例如,
@ConditionalOnProperty
的使用:java@Configuration public class DatabaseConfig { @Bean @ConditionalOnProperty(name = "app.use-druid", havingValue = "true") public DataSource dataSource() { return new DruidDataSource(); } }
这段代码表示只有当配置文件中有
app.use-druid=true
这个属性时,dataSource
Bean 才会被创建并加载到IOC容器。
4.4 其他注解(了解)
4.5 FactoryBean与BeanFactory区别
4.5.1 BeanFactory
BeanFactory
是Spring IoC容器的核心接口之一,提供了管理和检索bean的基本功能。它是Spring容器的基础接口,负责管理bean的生命周期、依赖注入等操作。与ApplicationContext
不同的是,BeanFactory
提供了更底层的操作,并且具有更高的性能(因为它不预加载所有的单例bean),但是缺少一些高级特性(如事件发布、国际化支持等)。
BeanFactory
主要用于那些对性能敏感的应用程序,或者是那些只需要基本IoC功能而不需要额外特性的应用程序。对于大多数Web应用和其他需要丰富特性的应用场景,推荐使用ApplicationContext
,它是BeanFactory
的子接口,提供了更多的企业级特性。
BeanFactory
接口主要方法
Object getBean(String name):
根据名称从容器中获取一个bean实例。<T> T getBean(Class<T> requiredType):
根据类型从容器中获取一个bean实例。Object getBean(String name, Object... args):
根据名称和构造参数获取一个bean实例。void preInstantiateSingletons():
提前实例化所有非懒加载的单例bean。
4.5.2 FactoryBean
FactoryBean
是一个接口,它允许你自定义bean的创建逻辑。通过实现这个接口,你可以控制如何创建某个特定类型的bean。FactoryBean
通常用于需要复杂初始化逻辑或需要返回代理对象(如AOP代理)的场景。FactoryBean接口主要方法如下:
Object getObject()
:返回由FactoryBean
创建的对象实例。Class<?> getObjectType()
:返回getObject()
方法返回对象的类型。如果类型是动态确定的,可以返回null
。boolean isSingleton()
:指示FactoryBean
创建的对象是否为单例(singleton)。默认情况下,Spring容器会假定所有bean都是单例,除非另有说明。使用场景
- 当你想要自定义bean的创建过程时,比如你需要在创建bean之前执行某些初始化代码、根据条件选择不同的实现类,或者需要返回一个代理对象而不是直接返回目标类的实例,这时就可以使用
FactoryBean
。
案例代码
javapublic class MyFactoryBean implements FactoryBean<MyInterface> { @Override public MyInterface getObject() throws Exception { // 自定义创建逻辑 return new MyInterfaceImpl(); } @Override public Class<?> getObjectType() { return MyInterface.class; } @Override public boolean isSingleton() { return true; // 或者 false,取决于你的需求 } }
4.5.3 FactoryBean与BeanFactory区别
特性 | FactoryBean | BeanFactory |
---|---|---|
角色 | 定义如何创建一个特定类型的bean | 管理bean的生命周期和依赖关系 |
主要用途 | 控制bean创建逻辑,例如返回代理对象 | 提供bean的获取、管理和生命周期管理 |
接口实现 | 实现者提供具体的bean创建逻辑 | Spring容器的基础接口,用于管理bean生命周期 |
使用频率 | 较少使用,主要用于特殊需求 | 底层API,广泛应用于Spring内部 |
性能 | 可能涉及额外的创建逻辑,影响不大 | 更高效,因为它是轻量级的 |
高级特性 | 不提供额外的企业级特性 | 不提供额外的企业级特性 |
第5章 SpringAOP详解
5.1 AOP前奏
5.1.1 设定场景复现问题
案例描述
1. 定义Calc接口及计算相应方法 - add():加法 - sub():减法 - mul():乘法 - div():除法 2. 为Calc定义实现类CalcImpl,分别实现加减乘除相应方法 - 添加日志功能 - 在计算之前,显示计算**方法名,及参数 - 在计算之后,显示计算**方法名,及结算结果 - 添加验证功能等等
案例实现
Calc接口
javapackage com.at.aopbefore; /** * @Author at * @CreateTime: 2024/08/16 */ public interface Calc1 { /** * @Author: at * @Date: 2024/8/16 9:09 * 加法 */ public int add(int i,int j); /** * @Author: at * @Date: 2024/8/16 9:09 * 减法 */ public int sub(int i,int j); /** * @Author: at * @Date: 2024/8/16 9:09 * 乘法 */ public int mul(int i,int j); /** * @Author: at * @Date: 2024/8/16 9:09 * 除法 */ public int div(int i,int j); }
CalcImpl实现类
javapackage com.at.aopbefore; /** * @Author at * @CreateTime: 2024/08/16 * **添加日志功能** * **添加验证功能等等** * */ public class Calc1Impl implements Calc1{ @Override public int add(int i, int j) { //验证.... 非核心业务代码 //日志... 非核心业务代码 // System.out.println("==>add()正在执行,参数:i:"+i+",j:"+j); // MyLogging.methodBefore("add",i,j); int rs = i + j; //核心代码 // MyLogging.methodAfter("add",rs); // System.out.println("==>add()执行完毕,结果为rs:"+rs); return rs; } @Override public int sub(int i, int j) { //验证.... 非核心业务代码 //日志... 非核心业务代码 // System.out.println("==>sub()正在执行,参数:i:"+i+",j:"+j); int rs = i - j; // System.out.println("==>sub()执行完毕,结果为rs:"+rs); return rs; } @Override public int mul(int i, int j) { int rs = i * j; return rs; } @Override public int div(int i, int j) { int rs = i / j; return rs; } }
5.1.2 发现问题及解决方案
发现问题
- 在核心业务代码(计算操作)中,直接书写非核心业务代码(日志,验证等)
- 导致代码分散及代码混乱问题
- 代码分散:每个计算方法都书写相同代码
- 代码混乱:核心代码与非核心代码耦合(书写在一处)
- 高内聚,低耦合
解决方案
解决代码分散:提取MyLogging工具类,提高代码可重用性
javapackage com.at.aopbefore; import java.util.Arrays; /** * @Author at * @CreateTime: 2024/11/16 */ public class MyLogging { //切面类 /** * @Author: at * @Date: 2024/11/16 10:45 * 日志(方法前)通知 */ public static void methodBefore(String methodName,Object... args){ System.out.println("==>"+methodName+"()正在执行,参数:"+ Arrays.toString(args)); } /** * @Author: at * @Date: 2024/11/16 10:45 * 日志(方法后)通知 */ public static void methodAfter(String methodName,Object rs){ System.out.println("==>"+methodName+"()执行完毕!,结果rs:"+rs); } }
解决代理混乱:将非核心业务代码(日志)先横向提取工具类中,再动态织入到核心代码中(CalcImpl)
- 需要使用代理模式解决该问题
5.2 代理模式
代理模式(Proxy Pattern)是软件设计模式中的一种结构型模式,它为其他对象提供一个代理以控制对这个对象的访问。代理模式的核心思想是在客户端和目标对象之间增加一层间接性,这层间接性由代理对象来实现。
- 生活中代理
- 海外代理商
- 经纪人
- 房屋中介等
- 程序中代理
- 静态代理:代理对象是固定
- 动态代理:代理对象不固定
5.2.1 静态代理
静态代理是指代理类和委托类(即真实主题)的关系是在编译时就确定下来的。也就是说,你需要为每个委托类手写一个对应的代理类。静态代理的代理类实现了与委托类相同的接口,并且持有一个委托类的实例,在调用方法时通过这个实例来间接调用委托类的方法。
java
package com.at.aopbefore;
/**
* @Author at
* @CreateTime: 2024/08/16
* Calc1Impl的静态代理
*/
public class Calc1ImplStaticProxy implements Calc1 {
//目标对象
private Calc1Impl calc1Impl;
//有参构造器:使用静态代理,必须指定目标对象
public Calc1ImplStaticProxy(Calc1Impl calc1Impl){
this.calc1Impl = calc1Impl;
}
@Override
public int add(int i, int j) {
MyLogging.methodBefore("add",i,j);
int rs = calc1Impl.add(1, 2);
MyLogging.methodAfter("add",rs);
return rs;
}
@Override
public int sub(int i, int j) {
MyLogging.methodBefore("sub",i,j);
int rs = calc1Impl.sub(1, 2);
MyLogging.methodAfter("sub",rs);
return rs;
}
@Override
public int mul(int i, int j) {
MyLogging.methodBefore("mul",i,j);
int rs = calc1Impl.mul(1, 2);
MyLogging.methodAfter("mul",rs);
return rs;
}
@Override
public int div(int i, int j) {
MyLogging.methodBefore("div",i,j);
int rs = calc1Impl.div(1, 2);
MyLogging.methodAfter("div",rs);
return rs;
}
}
java
/**
* @Author: at
* @Date: 2024/11/16 11:31
* 测试静态代理模式
*/
@Test
void testStaticProxy() {
Calc1ImplStaticProxy sp = new Calc1ImplStaticProxy(new Calc1Impl());
sp.add(1,2);
}
5.2.2 动态代理
动态代理是指代理类和委托类之间的关系是在运行时由Java反射机制动态生成的。与静态代理不同的是,你不需要为每个委托类都编写一个代理类。相反,你可以定义一个通用的代理处理器(InvocationHandler),它可以在运行时处理对任意接口或类的代理。
java
package com.at.aopbefore;
import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;
/**
* @Author at
* @CreateTime: 2024/08/16
* 动态代理类
* 1. 目标对象
* 2. 有参构造器
* 3. 获取动态代理对象
*/
public class Calc1ImplDynamicProxy {
private Object target; //目标对象(不固定)
//使用动态代理时,必须指定目标对象
public Calc1ImplDynamicProxy(Object target){
this.target = target;
}
/**
* @Author: at
* @Date: 2024/8/16 10:20
* 获取动态代理对象
* 1. 获取代理对象(Proxy.newProxyInstance())
* 2. 动态执行目标对象相应方法(知道目标对象实现接口)
*/
public Object getProxy(){
//定义代理对象
Object proxyObj = null;
//获取代理对象
//1. 目标对象类加载器
ClassLoader classLoader = target.getClass().getClassLoader();
//2. 目标对象实现接口(代理类实现接口集合)
Class<?>[] interfaces = target.getClass().getInterfaces();
//3. InvocationHandler:动态执行目标对象相应方法
proxyObj = Proxy.newProxyInstance(classLoader, interfaces, new InvocationHandler() {
/**
* @Author: at
* @Date: 2024/8/16 10:29
* Student{ 学生属性:静态特征,属性方法:动态特征}
* Cat{ 猫属性,猫方法}
* 将class共同特征,提取Class
* Method:方法
* Filed:字段
*/
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
//获取目标对象方法名称
String methodName = method.getName();
//日志(方法前):
MyLogging.methodBefore(methodName,args);
//动态执行目标对象方法
Object rs = method.invoke(target, args);
//日志(方法后):
MyLogging.methodAfter(methodName,rs);
return rs;
}
});
return proxyObj;
}
//内部类
// class MyInvocationHandler implements InvocationHandler {
// @Override
// public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
// return null;
// }
// }
}
java
/**
* @Author: at
* @Date: 2024/11/16 11:31
* 测试动态代理模式
*/
@Test
void testDynamicProxy() {
//创建动态代理工具类
Calc1ImplDynamicProxy dp = new Calc1ImplDynamicProxy(new Calc1Impl());
//代理对象
// Calc1Impl proxy = (Calc1Impl) dp.getProxy(); //错误(**代理对象不能转换为目标对象**)
Calc1 proxy = (Calc1) dp.getProxy();
System.out.println("proxy.getClass().getName() = " + proxy.getClass().getName());
int add = proxy.add(1, 3);
}
5.2.3 静态代理与动态代理小结
特性 | 静态代理 | 动态代理 |
---|---|---|
创建时机 | 编译时确定 | 运行时通过反射机制动态创建 |
灵活性 | 固定,每次新增委托类需新建代理类 | 高度灵活,支持任意接口或类的代理 |
代码量 | 较多,每个委托类对应一个代理类 | 较少,通用的代理处理器可复用 |
性能 | 较好,无反射开销 | 稍微逊色,但现代JVM优化后影响不大 |
适用场景 | 小规模项目或需求明确的情况下 | 大规模项目、框架开发、AOP等 |
目标对象与代理对象是平级关系(兄弟关系)
兄弟关系不能相互转换
代理对象不能转换为目标对象
识别代理对象,以$开头对象,都是代理对象
- jdk.proxy2.$Proxy98
5.2.4 Spring中支持的动态代理
在Spring框架中,动态代理是实现AOP(面向切面编程)和其他高级功能的关键技术之一。Spring支持两种主要的动态代理方式:JDK动态代理和CGLIB动态代理。这两种方式各有优缺点,适用于不同的场景。以下是它们的区别、实现机制以及适用情况的详细解析。
- JDK动态代理
- JDK 动态代理基于Java反射机制,通过
java.lang.reflect.Proxy
类和InvocationHandler
接口来创建代理对象。它只能为实现了接口的对象创建代理实例。当调用代理对象的方法时,实际调用的是InvocationHandler
的invoke()
方法,在这里可以添加额外的逻辑(如事务管理、日志记录等),然后再转发给目标对象的真实方法。 - 配置方式:proxy-target-class="false"
- JDK 动态代理基于Java反射机制,通过
- CGLIB动态代理
- CGLIB(Code Generation Library)是一个强大的高性能字节码生成库。与JDK动态代理不同,CGLIB可以在运行时动态地生成一个给定类的子类,并覆盖其中的方法以插入自定义逻辑。由于它是通过继承的方式工作,因此不需要目标对象实现任何接口。
- 配置方式:proxy-target-class="true"
特性 | JDK 动态代理 | CGLIB 动态代理 |
---|---|---|
创建方式 | 基于Java反射机制 | 基于字节码生成 |
适用对象 | 必须实现接口的对象 | 可以是任意类,但不能是final类 |
性能 | 较好 | 稍微逊色,但现代JVM优化后影响不大 |
复杂度 | 简单,基于标准Java API | 相对复杂,涉及字节码操作 |
与目标对象的关系 | 代理对象与目标对象是兄弟关系 | 代理对象与目标对象是父子关系 |
默认选择 | 当目标对象实现了接口时,Spring首选JDK代理 | 当目标对象没有实现接口时,Spring使用CGLIB代理 |
5.3 基于注解实现AOP
5.3.1 AOP概念
AOP
:Aspect Oriented Programming面向切面编程思想,它可以在不修改源代码的情况下,给程序动态统一添加额外功能
AOP
可以说是OOP(Object Oriented Programming,面向对象编程)的补充和完善。OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。不过OOP允许开发者定义纵向的关系,但并不适合定义横向的关系,例如日志功能。日志代码往往横向地散布在所有对象层次中,而与它对应的对象的核心功能毫无关系对于其他类型的代码,如安全性、异常处理和透明的持续性也都是如此,这种散布在各处的无关的代码被称为横切(cross cutting),在OOP设计中,它导致了大量代码的重复,而不利于各个模块的重用。
AOP
技术恰恰相反,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。使用AOP,可以在不修改原来代码的基础上添加新功能。
5.3.2 AOP相关术语
横切关注点:非核心业务代码(提取类之前)
- 如:日志代码
通知:将非核心业务代码提取到类中后,称之为通知(提取类之后)
连接点:非核心业务代码织入到核心业务代码的位置(通知之前)
- 日志功能在加减乘除方法中书写位置
**切入点:**非核心业务代码织入到核心业务代码的位置(通知之后)
切面类(Aspect):将非核心业务代码提取到类中,这个类称之为切面类
- 将日志功能提取MyLoggin,MyLogging就是切面类
目标对象:被代理的对象称之为目标对象
- 如:CalcImpl是目标对象
代理对象:通过代理类中getProxy()方法获取的对象,称之为代理对象
5.3.3 使用AspectJ基于注解实现AOP
导入启动器坐标
xml<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>
定义切面类及定义通知
javapackage com.at.aop; import org.aspectj.lang.JoinPoint; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.springframework.stereotype.Component; import java.util.Arrays; /** * @Author at * @CreateTime: 2024/11/16 */ @Component //将组件装配到IOC容器 @Aspect //标识当前类是一个切面类 public class MyLogging { /** * @Author: at * @Date: 2024/11/16 10:45 * 前置通知:日志(方法前)通知 */ @Before("execution(public int com.at.aop.CalcImpl.add(int,int))") public void methodBefore(JoinPoint joinPoint){ String methodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); System.out.println("==>"+methodName+"()正在执行,参数:"+ Arrays.toString(args)); } }
开启AspectJ注解支持(SpringBoot环境下可省略)
javapackage com.at.config; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.EnableAspectJAutoProxy; /** * @Author at * @CreateTime: 2024/11/16 */ @Configuration @ComponentScan(basePackages = "com.at.aop") //开启AspectJ动态代理 @EnableAspectJAutoProxy public class SpringConfigAop { }
测试
javapackage com.at; import com.at.aop.Calc; import com.at.aop.CalcImpl; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; /** * @Author at * @CreateTime: 2024/11/16 */ //@SpringBootTest注解,自动整合Junit @SpringBootTest public class TestAOP { @Autowired private CalcImpl calc; // private Calc calc; @Test void testAOP(){ System.out.println("calc.getClass().getName() = " + calc.getClass().getName()); calc.add(1,2); } }
小结
- @SpringBootTest注解作用:自动在Spring中整合Junit
- 在测试类中可以获取IOC容器中的组件
- SpringBoot中,自动开启AspectJ注解支持
- SpringBoot环境中默认代理方式:CGLIB动态代理
- com.at.aop.CalcImpl**$$SpringCGLIB$$0**
- 设置JDK动态代理方式如下:spring.aop.proxy-target-class=false
- @SpringBootTest注解作用:自动在Spring中整合Junit
5.3.4 切入点表达式详解
语法:
java@Before("execution(public int com.at.aop.CalcImpl.add(int,int))") @Before("execution(* com.at..*.*(..))") @Before("execution(* com.at.aop.*.*(..))")
重用切入点表达式
提取重用表达式
java@Pointcut("execution(* com.at.aop.*.*(..))") public void myJoinPoint(){}
引用表达式
java@Before("myJoinPoint()") public void methodBefore(JoinPoint joinPoint){}
[*]
- [*]可以代表任意权限修饰符及返回值类型
- [*]可以代表任意包名,任意类名及任意方法名
[..]
- 任意层包名
- 任意形参数量及类型
5.3.5 JoinPoint接口
java
// 获取方法签名=方法名+参数列表
Signature signature = joinPoint.getSignature();
// 获取方法名称
String methodName = signature.getName();
// 获取参数
Object[] args = joinPoint.getArgs();
5.3.6 AOP的五大通知(通知)
前置通知:@Before
执行时机:在目标方法执行之前执行
注意:如目标方法有异常,前置通知会执行
java@Before("myJoinPoint()") public void methodBefore(JoinPoint joinPoint){}
后置通知:@After
执行时机:在目标方法执行之后执行(最后)
注意:如目标方法有异常,后置通知执行
java@After("myJoinPoint()") public void methodAfter(JoinPoint joinPoint){}
返回通知:@AfterReturning
执行时机:在目标方法返回结果执行
注意:与目标方法有无异常没有关系(一般有异常时不返回结果,所以返回通知不执行)
java@AfterReturning(value = "myJoinPoint()",returning = "rs") public void methodAfterRetuning(JoinPoint joinPoint,Object rs){ String methodName = joinPoint.getSignature().getName(); System.out.println("返回通知==>"+methodName+"()方法返回结果rs:"+rs); }
异常通知:@AfterThrowing
执行时机:在目标方法抛出异常时执行
注意:有异常执行,无异常不执行,且throwing = "ex"与形参列表中的ex一致
java@AfterThrowing(value = "myJoinPoint()",throwing = "ex") public void methodAfterThrowing(JoinPoint joinPoint,Exception ex){ String methodName = joinPoint.getSignature().getName(); System.out.println("异常通知==>"+methodName+"()方法的异常ex:"+ex); }
环绕通知:
环绕通知:整合以上四个通知(前置&后置&返回&异常)
注意
- 环绕通知参数必须使用ProceedingJoinPoint,
- 使用ProceedingJoinPoint的joinPoint.proceed();可以手动触发目标对象的相应方法
- 环绕通知必须将目标方法的返回值返回,如不设置返回值,会报如下错:
org.springframework.aop.AopInvocationException: Null return value from advice does not match primitive return type for: public abstract int com.at.aop.Calc.add(int,int)
- 环绕通知参数必须使用ProceedingJoinPoint,
示例代码
java/** * @Author: at * @Date: 2024/11/16 15:55 * 环绕通知(四合一) */ @Around("myJoinPoint()") public Object methodAround(ProceedingJoinPoint pjp){ String methodName = pjp.getSignature().getName(); Object[] args = pjp.getArgs(); Object rs = null; try { //前置通知 System.out.println("前置通知==>"+methodName+"()正在执行,参数:"+ Arrays.toString(args)); // 执行目标方法,如:add() sub() mul() div() rs = pjp.proceed(); //返回通知 System.out.println("返回通知==>"+methodName+"()方法返回结果rs:"+rs); } catch (Throwable e) { //异常通知 System.out.println("异常通知==>"+methodName+"()方法的异常ex:"+e); throw new RuntimeException(e); } finally { //后置通知 System.out.println("后置通知==>"+methodName+"()执行完毕!"); } return rs; }
小结
- 通知执行顺序
- 有异常:前置通知 -> 异常通知 -> 后置通知
- 无异常:前置通知 -> 返回通知 -> 后置通知
- 通知执行顺序
5.3.7 定义切面优先级
语法:@Order(index)
- index数值越小优先级越高,但一般推荐正整数
源码
java@Retention(RetentionPolicy.RUNTIME) @Target({ElementType.TYPE, ElementType.METHOD, ElementType.FIELD}) @Documented public @interface Order { int value() default Integer.MAX_VALUE; }
5.3.8 AOP实现方式(动态代理实现方式)
JDK支持动态代理,底层目标对象实现接口方式**(代理对象目标对象关系:兄弟)**
- springboot设置jdk动态代理方式:spring.aop.proxy-target-class=false
- jdk.proxy2.$Proxy98
CGLIB(cglib)默认方式,底层目标对象不实现接口方式(认干爹:代理对象与目标对象关系:父子)
- com.at.aop.CalcImpl$$SpringCGLIB$$0
第6章 Spring之事务管理
6.1 JdbcTemplate基本用法
6.1.1 JdbcTemplate概念
Spring
为简化特定领域代码,封装了很多 『Template』形式的模板类。例如:JdbcTemplate、RedisTemplate、RestTemplate 等等。
JdbcTemplate
是 Spring JDBC 的核心类之一,它简化了与关系型数据库的交互。通过使用 JdbcTemplate,开发者不需要编写样板代码来管理资源(如打开或关闭连接),处理 SQL 语句,或者处理异常转换。以下是 JdbcTemplate 的一些关键特性和使用方式:
- 减少样板代码:自动处理数据库资源的获取和释放。
- SQL 执行:提供了多种方法来执行查询、更新等操作。
- 结果集映射:支持将结果集映射为 Java 对象,简化数据处理。
- 事务管理:可以轻松地在 Spring 的事务管理框架下工作。
- 异常处理:将 JDBC 异常转换为 Spring 的 DataAccessExceptions,便于错误处理。
6.1.2 JdbcTemplate基本使用
导入jar包(启动器)
编写配置文件:application.properties
propertiesspring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.datasource.url=jdbc:mysql://localhost:3306/spring_tx spring.datasource.username=root spring.datasource.password=root
将JdbcTemplate对象装配到IOC容器中
- SpringBoot自动配置原理,默认将JdbcTemplate装配到IOC容器
- SpringBoot自动配置相关概念后续重点讲解
6.1.3 JdbcTemplate常用API
jdbcTemplate.update(String sql ,Object... args):通用增删改功能
jdbcTemplate.queryForObject(String sql,RowMapper):查询单个对象
jdbcTemplate.queryForList(String sql):查询多个数据Map<String,Object>
jdbcTemplate.query(String sql,RowMapper):查询多个对象
6.1.4 相关案例代码
java
package com.at;
import com.at.pojo.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import java.util.List;
import java.util.Map;
@SpringBootTest
class Day15SpringTxApplicationTests {
@Autowired
private JdbcTemplate jdbcTemplate;
@Test
void contextLoads() {
System.out.println("jdbcTemplate = " + jdbcTemplate);
}
/**
添加user信息
*/
@Test
public void testAddStudent(){
String sql = "INSERT INTO t_user(ACCOUNT,PASSWORD,nickname)VALUES(?,?,?)";
jdbcTemplate.update(sql,"zhangsan","123456","普通员工");
}
/**
查询单个对象
*/
@Test
public void testQueryStudent(){
String sql = "SELECT id,`account`,`password`,nickname FROM t_user WHERE id = ?";
// 创建一个映射器
RowMapper<User> rowMapper = new BeanPropertyRowMapper<>(User.class);
User user = jdbcTemplate.queryForObject(sql, rowMapper,20010);
System.out.println("user = " + user);
}
/**
查询所有对象
*/
@Test
public void testQueryForList(){
String sql = "SELECT id,`account`,`password`,nickname FROM t_user";
// 创建一个映射器
RowMapper<User> rowMapper = new BeanPropertyRowMapper<>(User.class);
List<User> userList = jdbcTemplate.query(sql, rowMapper);
for (User user : userList) {
System.out.println("user = " + user);
}
System.out.println(" ====================== ");
List<Map<String, Object>> maps = jdbcTemplate.queryForList(sql);
for (Map<String, Object> map : maps) {
System.out.println("map = " + map);
}
}
}
6.2 事务概念回顾
6.2.1 事务概念
在 MySQL 中,事务(Transaction)是一组 SQL 操作的集合,这组操作要么全部执行成功,要么全部不执行,以此来确保数据的一致性和完整性。事务是数据库管理系统(DBMS)执行过程中的一个逻辑工作单元,它具有四个关键属性,通常被简称为 ACID 属性
- 原子性(Atomicity)
- 事务是一个不可分割的工作单位,事务中包含的所有操作要么全部提交成功,要么全部回滚失败。任何一部分操作的失败都会导致整个事务的撤销。
- 一致性(Consistency)
- 事务必须使数据库从一个一致状态转变到另一个一致状态。这意味着即使在系统崩溃或断电的情况下,事务也必须保证数据的完整性和约束条件不会被破坏。
- 隔离性(Isolation)
- 多个并发事务之间的操作应该是隔离的,即一个事务的结果不应该影响其他正在运行的事务,除非它们已经提交。MySQL 提供了不同的隔离级别来实现不同程度的隔离效果。
- 持久性(Durability)
- 一旦事务提交,它对数据库所做的更改就会永久保存下来,即使系统发生故障也不会丢失。
6.2.2 事务控制语句
在 MySQL 中,你可以使用以下 SQL 语句来控制事务:
START TRANSACTION
或BEGIN
- 开始一个新的事务。
COMMIT
- 提交当前事务,使得所有更改成为永久性的。提交后不能回滚。
ROLLBACK
- 回滚当前事务,撤销所有未提交的操作。这会将数据库恢复到事务开始之前的状态。
6.2.3 事务隔离级别
MySQL 支持四种标准的事务隔离级别,可以通过 SET TRANSACTION ISOLATION LEVEL
命令来设置:
- 读未提交(READ UNCOMMITTED)
- 最低的隔离级别,允许脏读、不可重复读和幻读。
- 读已提交(READ COMMITTED)
- 允许不可重复读和幻读,但不允许脏读。
- 可重复读(REPEATABLE READ)
- MySQL 的默认隔离级别,仅允许幻读,防止脏读和不可重复读。
- 串行化(SERIALIZABLE)
- 最高的隔离级别,完全串行化的读写操作,避免了脏读、不可重复读和幻读。
6.3 Spring声明式事务基本使用
6.3.1 事务管理概念
spring中支持两种事务管理,分别是
编程式事务管理
和声明式事务管理
两种.
编程式事务管理概念
编程式事务是指手动编写程序来管理事务,即通过编写代码的方式直接控制事务的提交和回滚。在 Java 中,通常使用事务管理器(如 Spring 中的
PlatformTransactionManager
)来实现编程式事务。编程式事务的主要优点是灵活性高,可以按照自己的需求来控制事务的粒度、模式等等。但是,编写大量的事务控制代码容易出现问题,对代码的可读性和可维护性有一定影响。
javaConnection conn = ...; try { // 开启事务:关闭事务的自动提交 conn.setAutoCommit(false); // 核心操作 // 业务代码 // 提交事务 conn.commit(); }catch(Exception e){ // 回滚事务 conn.rollBack(); }finally{ // 释放数据库连接 conn.close(); }
- 编程式的实现方式存在缺陷:
- 细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
- 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。
- 事务管理代码与核心业务代码相耦合
声明式事务管理概念
- 在 Spring 框架中,声明式事务管理是一种通过配置而非编程方式来管理事务的技术。它允许开发者以非侵入的方式定义方法的事务行为,而无需修改业务逻辑代码。Spring 的声明式事务管理主要依赖于 AOP(面向切面编程)来实现,并且可以通过 XML 配置或者基于注解的方式来进行配置。
- 声明式事务管理的优势
- 减少样板代码:减少了围绕事务控制所需的大量重复代码。
- 提高代码可读性:将事务逻辑与业务逻辑分离,使代码更易于维护和理解。
- 集中化管理:可以在一处集中管理事务规则,便于维护和调整。
小结
- 编程式事务需要手动编写代码来管理事务(不推荐使用)
- 声明式事务可以通过配置文件或注解来控制事务。(推荐使用)
6.3.2 声明式事务管理基本实现
SpringBoot环境中默认装配事务管理器,在service层使用@Transactional注解管理事务即可
当然也可以手动装配事务管理器,具体代码如下:
编写配置文件:jdbc.properties
properties#配置DruidDataSource jdbc.driverClassName=com.mysql.cj.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/0923_demo jdbc.username=root jdbc.password=root
编写属性类:JdbcProperties
javapackage com.at.properties; import lombok.Data; import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.stereotype.Component; /** * @Author at * @CreateTime: 2024/11/26 */ @Component("jdbcProperties") @ConfigurationProperties(prefix = "jdbc") @Data public class JdbcProperties { private String driverClassName; private String url; private String username; private String password; }
编写配置类:DruidConfig
javapackage com.at.config; import com.alibaba.druid.pool.DruidDataSource; import com.at.properties.JdbcProperties; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import javax.sql.DataSource; /** * @Author at * @CreateTime: 2024/11/26 */ @Configuration public class DruidConfig { @Autowired @Qualifier("jdbcProperties") private JdbcProperties jdbcProperties; @Bean public DataSource dataSource(/*JdbcProperties jdbcProperties*/){ DruidDataSource ds = new DruidDataSource(); ds.setDriverClassName(jdbcProperties.getDriverClassName()); ds.setUrl(jdbcProperties.getUrl()); ds.setUsername(jdbcProperties.getUsername()); ds.setPassword(jdbcProperties.getPassword()); return ds; } /** * 实例化JdbcTemplate对象,需要使用ioc中的DataSource * @param dataSource * @return */ @Bean public JdbcTemplate jdbcTemplate(DataSource dataSource){ JdbcTemplate jdbcTemplate = new JdbcTemplate(); jdbcTemplate.setDataSource(dataSource); return jdbcTemplate; } }
编写事务管理器配置类:TxConfig
java/** * @Author at * @CreateTime: 2024/11/26 */ @Configuration @EnableTransactionManagement //开启声明式事务管理 public class TxConfig { /** * 装配事务管理实现对象 * @param dataSource * @return */ @Bean public TransactionManager transactionManager(DataSource dataSource){ return new DataSourceTransactionManager(dataSource); } }
整合DruidConfig与TxConfig
java@Import(value = {DruidConfig.class, TxConfig.class}) @ComponentScan(basePackages = "com.at") @Configuration public class SpringConfig { }
使用@Transactional注解实现:声明式事务管理
service层代码
javapackage com.at.service; import com.at.pojo.User; /** * @Author at * @CreateTime: 2024/11/18 */ public interface UserService { /** * @Author: at * @Date: 2024/11/18 10:43 * 修改User信息 */ public void updateUser(User user); /** * @Author: at * @Date: 2024/11/18 14:07 * 通过用户名修改密码 */ public void updatePwdByUsername(String username, String password); /** * @Author: at * @Date: 2024/11/18 14:07 * 通过用户名修改昵称 */ public void updateNicknameByUsername(String nickname, String username); }
javapackage com.at.service.impl; import com.at.dao.UserDao; import com.at.pojo.User; import com.at.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; /** * @Author at * @CreateTime: 2024/11/18 */ @Service //@Transactional public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; /** * @Author: at * @Date: 2024/11/18 10:44 * 1. 通过姓名修改密码 * 2. 通过姓名修改昵称 */ @Transactional( readOnly = false, timeout = 3, noRollbackFor = ArithmeticException.class, isolation = Isolation.REPEATABLE_READ ) @Override public void updateUser(User user) { //1. 通过姓名修改密码 userDao.updatePwdByUsername(user.getAccount(),user.getPassword()); //事务超时 // try { // Thread.sleep(4000); // } catch (InterruptedException e) { // throw new RuntimeException(e); // } // bug int i = 1/0; //2. 通过姓名修改昵称 userDao.updateNicknameByUsername(user.getNickname(),user.getAccount()); } @Transactional(propagation=Propagation.REQUIRES_NEW) @Override public void updatePwdByUsername(String username, String password) { userDao.updatePwdByUsername(username, password); } @Transactional(propagation=Propagation.REQUIRES_NEW) @Override public void updateNicknameByUsername(String nickname, String username) { userDao.updateNicknameByUsername(nickname, username); } }
javapackage com.at.service.impl; import com.at.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; /** * @Author at * @CreateTime: 2024/11/18 */ @Service("userServiceAll") public class UserServiceAll { @Autowired private UserService userService; /** * @Author: at * @Date: 2024/11/18 14:19 * 测试事务传播行为(Propagation默认值:REQUIRED) */ @Transactional public void updateUserAll(){ //1. 通过用户名修改密码 userService.updatePwdByUsername("zhangsan", "666666"); // int i = 1/0; //2. 通过用户名修改昵称(普通员工) userService.updateNicknameByUsername("普通员工", "zhangsan"); } /** * @Author: at * @Date: 2024/11/18 14:19 * 测试事务传播行为(Propagation默认值:REQUIRES_NEW) */ @Transactional(propagation = Propagation.REQUIRED) public void updateUserAllNew(){ //1. 通过用户名修改密码 userService.updatePwdByUsername("zhangsan", "666666"); // int i = 1/0; //2. 通过用户名修改昵称(普通员工) userService.updateNicknameByUsername("普通员工", "zhangsan"); } }
测试类代码
javapackage com.at; import com.at.pojo.User; import com.at.service.UserService; import com.at.service.impl.UserServiceAll; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Qualifier; import org.springframework.boot.test.context.SpringBootTest; /** * @Author at * @CreateTime: 2024/11/18 */ @SpringBootTest public class TestUserService { @Autowired private UserService userService; @Autowired @Qualifier("userServiceAll") private UserServiceAll userServiceAll; /** * @Author: at * @Date: 2024/11/18 14:16 * 测试事务四个属性 */ @Test public void testUpdateUser() { User user = new User(); user.setAccount("zhangsan"); user.setPassword("666666"); user.setNickname("普通员工666"); userService.updateUser(user); } /** * @Author: at * @Date: 2024/11/18 14:16 * 测试事务propagation属性(传播行为) */ @Test public void testPropagation() { //测试 REQUIRED // userServiceAll.updateUserAll(); System.out.println("userService.getClass().getName() = " + userService.getClass().getName()); System.out.println("userServiceAll.getClass().getName() = " + userServiceAll.getClass().getName()); //测试 REQUIRES_NEW userServiceAll.updateUserAllNew(); } }
6.4 @Transactional注解(事务属性)
6.4.1 事务只读
只读介绍
对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。
设置方式
java// readOnly = true把当前事务设置为只读 默认是false! @Transactional(readOnly = true)
针对DML动作设置只读模式
会抛出下面异常:
Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed
@Transactional注解放在类上
生效原则
如果一个类中每一个方法上都使用了 @Transactional 注解,那么就可以将 @Transactional 注解提取到类上。反过来说:@Transactional 注解在类级别标记,会影响到类中的每一个方法。同时,类级别标记的 @Transactional 注解中设置的事务属性也会延续影响到方法执行时的事务属性。除非在方法上又设置了 @Transactional 注解。
对一个方法来说,离它最近的 @Transactional 注解中的事务属性设置生效。
用法举例
在类级别@Transactional注解中设置只读,这样类中所有的查询方法都不需要设置@Transactional注解了。因为对查询操作来说,其他属性通常不需要设置,所以使用公共设置即可。
然后在这个基础上,对增删改方法设置@Transactional注解 readOnly 属性为 false。
java@Service @Transactional(readOnly = true) public class EmpService { // 为了便于核对数据库操作结果,不要修改同一条记录 @Transactional(readOnly = false) public void updateTwice(……) { …… } // readOnly = true把当前事务设置为只读 // @Transactional(readOnly = true) public String getEmpName(Integer empId) { …… } }
6.4.2 事务超时
需求
事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。
此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。
概括来说就是一句话:超时回滚,释放资源。
设置超时时间
java@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); } }
测试超时效果
org.springframework.transaction.TransactionTimedOutException: Transaction timed out: deadline was Wed May 24 09:10:43 IRKT 2023
6.4.3 事务回滚|不回滚异常
默认情况
默认只针对运行时异常回滚,编译时异常不回滚。情景模拟代码如下:
设置回滚异常
java@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); } }
设置不回滚的异常
在默认设置和已有设置的基础上,再指定一个异常类型,碰到它不回滚。
noRollbackFor属性:指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!
java@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); } }
6.4.4 事务隔离级别
事务隔离级别回滚
数据库事务的隔离级别是指在多个事务并发执行时,数据库系统为了保证数据一致性所遵循的规定。常见的隔离级别包括:
- 读未提交(Read Uncommitted):事务可以读取未被提交的数据,容易产生脏读、不可重复读和幻读等问题。实现简单但不太安全,一般不用。
- 读已提交(Read Committed):事务只能读取已经提交的数据,可以避免脏读问题,但可能引发不可重复读和幻读。
- 可重复读(Repeatable Read):在一个事务中,相同的查询将返回相同的结果集,不管其他事务对数据做了什么修改。可以避免脏读和不可重复读,但仍有幻读的问题。
- 串行化(Serializable):最高的隔离级别,完全禁止了并发,只允许一个事务执行完毕之后才能执行另一个事务。可以避免以上所有问题,但效率较低,不适用于高并发场景。 不同的隔离级别适用于不同的场景,需要根据实际业务需求进行选择和调整。
事务隔离级别设置
javapackage com.at.service; import com.at.dao.StudentDao; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Isolation; import org.springframework.transaction.annotation.Transactional; import java.io.FileInputStream; import java.io.FileNotFoundException; /** * projectName: com.at.service */ @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); } }
6.4.5 事务传播行为
事务传播行为概念
事务传播行为(Propagation Behavior)定义了当一个方法被调用时,如何与已有的事务进行交互。在 Spring 框架中,
@Transactional
注解的propagation
属性用于指定方法的事务传播行为。不同的传播行为决定了当前方法是否应该运行在一个新事务中、加入现有事务、或者以非事务方式运行等。举例说明:
- 假设MethodA()方法的事务是tx1,MethodB()方法的事务tx2,MethodA()中调用MethodB()
- 事务传播行为决定了MethodB()按照tx1或tx2事务执行
- REQUIRED(默认值):tx1
- REQUIRES_NEW:tx2
案例代码
java@Transactional public void MethodA(){ // ... MethodB(); // ... } //在被调用的子方法中设置传播行为,代表如何处理调用的事务! 是加入,还是新事务等! @Transactional(propagation = Propagation.REQUIRES_NEW) public void MethodB(){ // ... }
propagation属性
propagation属性值
spring中支持七种事务传播行为,常用两种:REQUIRED和REQUIRES_NEW
REQUIRED
(默认值)- 如果当前存在事务,则加入该事务;如果不存在,则创建一个新的事务。
- SUPPORTS
- 如果当前存在事务,则加入该事务;如果没有事务,则以非事务方式执行。这意味着此方法对事务的存在与否不敏感。
- MANDATORY
- 必须在一个已有事务中运行,否则抛出异常(
IllegalTransactionStateException
)。这通常用于必须确保在事务上下文中执行的方法。
- 必须在一个已有事务中运行,否则抛出异常(
REQUIRES_NEW
- 创建一个新的事务,如果当前存在事务,则挂起当前事务。这意味着即使有现有的事务,也会为这个方法启动一个全新的事务。
- NOT_SUPPORTED
- 不支持事务,总是以非事务方式执行。如果有活动的事务,它将被暂停。
- NEVER
- 方法不应该在事务中运行。如果尝试在事务中调用该方法,将会抛出异常。
- NESTED
- 如果当前存在事务,则在嵌套事务内执行;如果不存在,则创建新的事务。嵌套事务可以独立于外部事务回滚,但它的提交依赖于外部事务的提交。
测试事务传播行为
javapackage com.at.service.impl; import com.at.service.UserService; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; /** * @Author at * @CreateTime: 2024/11/18 */ @Service("userServiceAll") public class UserServiceAll { @Autowired private UserService userService; /** * @Author: at * @Date: 2024/11/18 14:19 * 测试事务传播行为(Propagation默认值:REQUIRED) */ @Transactional public void updateUserAll(){ //1. 通过用户名修改密码 userService.updatePwdByUsername("zhangsan", "666666"); // int i = 1/0; //2. 通过用户名修改昵称(普通员工) userService.updateNicknameByUsername("普通员工", "zhangsan"); } /** * @Author: at * @Date: 2024/11/18 14:19 * 测试事务传播行为(Propagation默认值:REQUIRES_NEW) */ @Transactional(propagation = Propagation.REQUIRED) public void updateUserAllNew(){ //1. 通过用户名修改密码 userService.updatePwdByUsername("zhangsan", "666666"); // int i = 1/0; //2. 通过用户名修改昵称(普通员工) userService.updateNicknameByUsername("普通员工", "zhangsan"); } }