前言
为什么学Spring技术:
- 简化开发。降低企业及开发的复杂性
- 整合框架。高效整合其他技术,提高开发效率
学哪些东西:
- Spring Framework Spring全家桶中的技术都依赖Spring框架
- SpringBoot 在简化开发的基础上加速开发,写更少的东西
- SpringCloud 分布式开发的相关技术
Spring发展史
- Spring1.0 纯配置方式
- Spring2.0 为加速开发引入注解
- Spring3.0 可以不写配置的开发模式
- Spring4.0 对部分API做了调整
- Spring5.0 Spring框架全面支持JDK8
Spring框架
系统架构
红色数字为建议学习顺序
SPring框架中有两个核心理念:
IoC(Inversion of Control控制反转)和 AOP(Aspect Oriented Programming面向切面编程)
AOP下又衍生出了Spring独到的事务处理
一、Core Container核心容器
现状
代码书写时耦合度过高,当数据层实现想添加新功能时,业务层的数据层对象也得跟着变,整个项目又要重新遍历、测试、发布
解决方案:
使用对象时,不主动new,转换为由外部提供
这就是IoC(Inversion of Control控制反转)的思想,对象的创建控制权由程序转移到外部
IoC 控制反转
Spring技术实现了IoC思想。Sping提供了一个容器,称为IoC容器,用来充当IoC思想中的外部
IoC容器负责对象的创建、初始化等工作,被创建的对象在IoC容器中统称为Bean
使用流程
XML版
-
在maven项目配置文件pom.xml中导入spring的坐标,解决依赖
-
解决依赖后,在resources目录下新建一个Spring config文件
-
在spring config文件中配置bean
id:bean名,唯一 class:bean的类型,全路径类名 <bean id="bookDao" class="com.itheima.dao.impl.BookDaoImpl"/>
-
在程序中获取容器,然后通过容器获取bean
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
DI 依赖注入
代码运行时不同Bean之间可能存在依赖关系,如Service对象会依赖dao对象
为了解决这个问题,容器自动建立bean与bean间的依赖关系,这个建立依赖的过程称为依赖注入
如A对象中依赖B对象,当我们需要用A对象时,容器会自动为我们创建所需的B对象,我们只需要请求A对象即可
使用流程
XML版
-
删除service中使用new形式创建对象的代码,只留对象声明
-
在service中提供对应对象的set方法,参数为传入对象类型,供容器传入bean
-
在Spring的xml中配置service与dao的关系,将被依赖的bean写到service的bean配置中
<bean id="bookService" class="com.itheima.service.impl.BookServiceImpl"> <property name="bookDao" ref="bookDao"/> </bean>
IoC 和 DI的目标就是充分解耦,最终使用对象时可以直接从容器中获取,且获取到的bean已经绑定了依赖关系
Bean
基础配置
- 给bean起别名
在pom.xml文件中的< bean> 标签中使用name属性,多个名称可以用“,” “;“ “ ”隔开
- bean的作用范围
spring 默认创建的bean是单例的(scope属性默认为singleton),即无论获取多少对象,只要bean名相同,都指向同一个bean。若想要非单例的,只需要设置bean标签中的scope属性值为prototype
单例模式可以减小容器压力
适合交给容器管理的bean:可复用的,如表现层、业务层、数据层对象
不适合容器管理的bean:封装实体的域对象,要记录状态的对象
实例化方式(3种)
- 使用构造方法
spring通过调用类的无参构造方法创建bean
- 静态工厂
定义一个A工厂类,其中定义一个静态方法以new的方式返回一个A的bean,通过直接调用工厂类的方法去获取A的bean
如何通过spring托管呢?
在spring的xml文件中配置工厂类的bean标签,设置factory-method方法值为工厂类中返回bean的那个方法名
这样通过容器获取工厂类的bean时就会自动执行factory-method对应的方法返回A的bean
- 实例工厂
定义一个A工厂类,其中定义一个非静态方法以new的方式返回一个A的Bean
由于是非静态方法,我们需要先得到工厂类的bean,然后通过工厂bean执行非静态方法去获取A的bean
若要通过spring托管
首先在spring的xml文件中配置工厂类的bean标签,然后配置A的bean标签,其中需要设置factory-bean属性为工厂类bean的id(指向工厂bean),factory-method属性值为工厂类返回A的bean的方法名
- FactoryBean方式(实例工厂的格式优化)
该方法和实例工厂相似,但是简化了spring的配置过程,统一了返回A对象的方法名,不需要再配置两个bean标签
首先还是定义一个A工厂类,但是这个工厂类要实现FactoryBean接口,指定泛型类型(即A的类)并重写getObject和getObjectType两个方法,分别返回A的bean实例和A的class字节码,可选重写isSingleton方法,返回true表示单例模式,false表示非单例
然后配置spring的xml,bean标签的class设置为A工厂类即可,然后通过id获取bean时就会得到A的bean
生命周期
生命周期:从创建到销毁的整个过程
生命周期控制:在bean创建后到销毁前做一些事情
Bean的生命周期:
- 初始化容器
-
创建对象(内存分配) 执行构造方法 执行属性注入(set操作) 执行bean初始化方法
- 使用bean
-
执行业务操作
- 关闭容器
-
执行bean销毁方法
控制流程
- 在bean的对应类中声明两个方法(一般为init和destroy),分别用于bean创建前执行和销毁后执行
- 在spring的配置文件中设置对应bean标签的init-method和destroy-method属性值为两个方法名,这样在容器创建和销毁bean前就会自动执行对应方法
Spring标准化版控制流程(接口控制)
- bean的对应类实现两个接口:InitializingBean, DisposableBean,并重写afterPropertiesSet()和destroy()两个方法,分别在加载bean之前和销毁bean之前执行
- 在spring配置文件中只需要配置对应bean标签中的id、class等属性即可,不需要再额外配置init-method和destroy-method属性
有时候程序执行完成后jvm直接退出,不给bean销毁的机会,如何执行destroy方法呢?有两种方式
- 手动关闭容器 调用ClassPathXMLApplicationContext类对象的close方法暴力关闭容器
- 设置关闭钩子 获取容器对象ctx后调用其registerShutdownHook()方法注册关闭钩子,就是说告诉虚拟机在退出之前先把容器关了
- 注意!!!!!ApplicationContext类的对象没有注册关闭钩子和容器关闭的方法。要用该类的子类对象
依赖注入
–setter注入–
引用类型:在bean类中声明引用类型变量,提供该变量的set方法,在Spring的配置文件中的"< bean>< /bean>"间利用property标签的name属性指定变量名,ref属性指定变量对应的bean, 依赖将通过参数将引用类型实例传入。当有多个引用类型变量需要注入时,在bean类中提供每个变量的set方法,通过写多个property标签实现多次注入
普通类型:步骤与引用类型注入相同,区别在于引用类型注入通过ref属性指定注入的bean,而普通类型注入通过value属性指定要注入的值
–构造器注入–
引用类型:在bean类中创建带参的构造方法,参数为要注入的bean。然后在Spring的配置文件中的"< bean>< /bean>"间利用constructor-arg标签的name和ref属性配置构造器的参数。若有多个参数则写多个constructor-arg
普通类型:步骤与引用类型注入相同,区别在于引用类型注入通过ref属性指定注入的bean,而普通类型注入通过value属性指定要注入的值
上述方式其实耦合度很高,当构造方法的参数名改变了之后,spring配置文件中constructor-arg标签中name属性的值也要变,这违背了spring解耦的初衷。
因此我们可以不通过name属性确定传给哪个参数,参数名可能变,但是参数类型不会变,因此可以利用指定type属性的值,按形参类型注入。
又有问题了:如果参数类型重复呢?参数的位置次序不会变吧,可以利用指定index属性的值,按形参位置注入。
注入方式的选择
什么时候用setter注入,什么时候用构造器注入?
强制依赖用构造器注入,setter注入有概率不进行注入导致null对象出现
可选依赖用setter注入,灵活性强,不配置就不注
必要时可以 构造器注入+setter注入
如果要管理的bean没有提供setter方法,必须用构造器注入
自己开发的模块推荐使用setter注入
–自动装配–
什么是自动装配:IoC容器根据bean所依赖的资源在容器中自动查找并注入到bean中的过程,不用再写< property>和< constructor-arg>进行配置了
配置流程:
bean类中依赖对象的set方法得有,然后通过设置spring配置文件中bean标签的autowire属性值设置自动装配,该属性的值有两种:
- byName按名称:根据bean类中set方法名(去掉set剩余的部分)和spring配置文件中被依赖bean的id匹配来进行依赖注入。需要被依赖对象的bean标签必须有id属性
- byType按类型:根据类型匹配来实现注入,set方法得有,容器会根据依赖的类型自动在容器中找有没有对应的bean,然后进行setter注入。注意,此时spring配置文件中有且只能有一个依赖的bean,否则会找到多个可用bean而报错
自动装配的特征
- 只能装配引用类型,不能操作简单类型
- 按类型装配,要求容器中相同类型的bean唯一 推荐使用
- 按名称装配,要求容器中指定名称的bean必须存在,但变量名和配置耦合,不推荐
- 依赖注入的优先级,setter注入和构造器注入 > 自动装配,同时出现自动装配失效
集合注入
引用类型和普通类型的注入实现了,集合类型怎么注入呢?
总的来说需要以下几步:
在spring的xml配置文件中的双目bean标签内通过property标签的name属性指明要注入的集合变量名
在property标签内用对应集合类型的标签指明集合变量类型
在集合类型标签内用多个标签设置集合内的元素
- 数组
<property name="array"> <!-- 设置要注入的bean中的变量名 --> <array> <!-- 指明是数组类型 --> <value>100</value> <!-- 设置数组内的元素 --> <value>200</value> <value>300</value> </array> </property>
- List
<property name="array"> <list> <!-- 指明是List类型 --> <value>100</value> <!-- 设置数组内的元素 --> <value>200</value> <value>300</value> </list> </property>
- Set
<property name="array"> <set> <!-- 指明是set类型 --> <value>itcast</value> <!-- 设置数组内的元素 --> <value>itheima</value> <value>zjh</value> <value>zjh</value> <!-- set标签会自动过滤重复元素 --> </set> </property>
- Map
<property name="array"> <map> <entry key="country" value="china"/> <entry key="province" value="gansu"/> <entry key="city" value="lanzhou"/> </map> </property>
- Properties
<property name="array"> <props> <prop key="country">china</prop> <prop key="province">gansu</prop> <prop key="city">lanzhou</prop> </props> </property>
管理第三方bean(拿到别人的东西应该怎么思考)
现在我们用spring管理一个c3p0的连接池对象(完全没见过的如何分析管理)
- 获取c3p0的坐标。去maven repository里去查,选择对应版本并复制其坐标,到pom.xml进行配置
- 获取c3p0的类型。去搜索引擎搜索,或者到spring配置文件中写一个bean标签,在class属性中输一部分内容,根据提示找到目标类型
- 配置对象的依赖。如c3p0是一个数据库连接池的对象,一般需要驱动、url、用户名和密码,试着去看看c3p0中对应的依赖变量叫啥名,进行配置
- 可能会报错提示没找到啥啥啥的,可以去maven repository中找对应模块的坐标导入
加载properties文件
像用户名、密码等信息,我们一般不直接写死在spring的配置文件中,而是单独写一个property文件记录,spring是如何使用properties文件的呢
-
加载properties文件。
-
在spring配置文件中开启新命名空间(假设名为context)
复制下图中白框中的内容得到红框中的内容,注意标注出的红框与白框中不相同的部分
-
使用context空间加载properties文件
若有多个properties文件,可以用逗号分隔多个文件名。也可以用通配符的形式全部加载classpath*: *.properties,其中classpath表示类路径,意味着加载当前工程及其依赖jar包中的properties文件
若properties文件中变量名称命名不规范可能会与系统变量冲突,系统变量优先级会高。为了避免咱们的配置失效,可以在加载properties文件时设置system-properties-mode为Never,表示不加载系统属性
-
-
将properties文件中的配置信息替换原来的属性注入
容器相关
- 创建容器
-
加载类路径下的配置文件, 自动去resources目录下去找
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
-
加载文件路径下的配置文件必须写配置文件的绝对路径
ApplicationContext ctx = new FileSystemXmlApplicationContext("S:\\tech\\SSM\\Spring_10_container\\src\\main\\resources\\applicationContext.xml");
- 获取bean
-
使用bean名称,并强转类型
BookDao bookDao = (BookDao) ctx.getBean("bookDao");
-
使用bean名称获取时指定bean的类型
BookDao bookDao = ctx.getBean("bookDao", BookDao.class);
-
使用bean类型获取(此时容器中的同类型bean要唯一)
BookDao bookDao = ctx.getBean(BookDao.class);
- 容器接口层次结构
Spring中有很多方法能够构建IoC容器,我们用的ApplicationContext接口是其一。其中最顶层的接口是BeanFactory接口。
二者有一个区别是:BeanFactory默认是延迟加载bean,而ApplicationContext默认是立即加载bean。
延迟加载bean是指容器构建好后并不创建bean的实例,而是获取时时候容器才给你创建;立即加载bean是容器构建好后立即创建bean的实例,需要时直接拿
如何设置特定bean的延迟加载呢?在对应的bean标签里设置lazy-init标签值为true即可
注解开发
注解开发是Spring简化开发的重要手段
Spring2.0开始提供各种各样的注解,到2.5的时候注解比较完善了
Spring3.0提供了纯注解开发
定义bean
-
在bean类的上面用@Component定义bean
@Component("bookDao") public class BookDaoImpl implements BookDao {} @Component public class BookDaoImpl implements BookDao {} Component后括号内为bean的名称,相当于spring的xml中bean标签中的id,可以省略,省略的话通过容器获取bean只能通过类型
-
在spring配置文件中通过组件扫描加载bean
<context:component-scan base-package="com.itheima"/>
-
衍生注解
项目中所有bean都用@Component定义缺乏区分度,因此spring提供三个衍生注解,其功能和用法与@Component一样@Controller 用于表现层bean定义 @Service 用于业务层bean定义 @Repository 用于数据层bean定义
纯注解开发模式
一点配置都不写了,Spring3.0使用Java类代替配置文件,使用配置类上方的各种注解的形式去描述原来的配置文件中的信息
-
用配置类替代原来的Spring配置xml文件
@Configuration注解 设定当前类为配置类
@ComponentScan注解 设定bean的扫描路径,此注解只能添加一次,多个数据用数组格式{name1, name2, …}@Configuration @ComponentScan({"com.itheima.dao", "com.itheima.service"}) public class SpringConfig(){}
-
读取spring xml文件初始化容器对象 换为 通过java配置类初始化容器对象
// 加载配置文件初始化容器 ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml"); // 加载配置类初始化容器 ApplicationContext ctx = new AnnotationConfigApplicationContext(SpringConfig.class);
bean管理
作用范围
用类外@Scope注解控制bean是否为单例,singleton为单例,prototype为非单例
生命周期
在类内、方法外用注解标记
用@PostConstruct注解标记init方法,在构造函数后自动执行
用@Predestroy注解标记destroy方法,在对象销毁前执行
依赖注入
为加速开发,这里只用自动装配实现依赖注入
- 引用类型对象的注入
对于类内的被依赖对象,用@Autowired注解标记该对象进行自动装配,容器会byType自动装配。与配置自动装配中的利用setter方法完成注入不同,注解的自动装配基于反射创建对象,并暴力反射初始化私有属性,因此不需要setter方法,但需要有无参构造方法
但是这样有个问题,当用注解标记了多个同类型的bean时byType会冲突报错,此时应该在@Autowired下面加上@Qualifier(“bean的id”)注解同时限定,按名称进行注入。注意@Qualifier注解依赖于@Autowired注解,不能单独使用
@Component
public class BookServiceImpl implements BookService{
@Autowired
private BookDao bookDao;
@Autowired
@Qualifier("bookDao")
private BookDao bookDao1;
}
- 简单类型的注入
使用@value(“值”)注解实现简单类型的注入
@Repository("bd")
public class BookDaoImpl implements BookDao {
@Value("zjh")
private String name;
@Value("18")
private int age;
public void save() {
System.out.println("book dao save..." + name + age);
}
}
这样写是不是感觉和直接给变量赋值没啥区别,这样好就好在可以从外部properties文件中读取数据,而不是把值写死在程序里
首先在Spring配置类上使用@PropertySource(“jdbc.properties”)注解加载外部文件,多个外部文件用数组格式{name1, name2, …},与配置xml加载properties不同,@PropertySource注解中的文件名不支持通配符*
在@Value标签里原来填值的地方用 ${} 从外部文件中取值
jdbc.properties文件中的内容:
name=zjh
age=18
@Configuration
@ComponentScan({"com.itheima.dao", "com.itheima.service"})
@PropertySource("jdbc.properties")
public class SpringConfig {
}
@Repository("bd")
public class BookDaoImpl implements BookDao {
@Value("${name}")
private String name;
@Value("${age}")
private int age;
}
第三方bean管理
使用@Bean注解将某个方法的返回对象定义为一个bean
简单类型注入:
利用成员变量的形式注入值
引用类型装配:
只需要为bean定义方法设置形参,容器会根据类型自动装配,前提是容器中有该类型的bean,注意用@ComponentScan扫描相应路径,
XML配置与注解配置的对比
二、AOP面向切面编程
AOP是一种编程范式(类似于OOP面向对象编程)
作用:在不修改原始设计的基础上为其进行功能增强,这是Spring倡导的一种理念:无入侵式编程
相关概念
连接点:程序执行过程中的任意位置,粒度为执行方法、抛出异常、设置变量等。
- Spring AOP中理解为可以追加功能的那些方法
切入点:匹配连接点的式子。
- Spring AOP中切入点可以描述一个具体方法,也可以匹配多个方法
- 如BookDao接口中的save方法;所有get开头的方法、所有Dao结尾的接口中的任意方法
通知:在切入点处执行的操作,即共性功能
- Spring AOP中以方法的形式呈现,定义该方法的类叫通知类
切面:描述通知和切入点的对应关系,相当于绑定通知和切入点
简单实现
目标:在每个方法执行前输出当前时间
-
导入依赖坐标:
<dependencies> <!-- 1. 导入aop开发依赖的包 --> <dependency> <groupId>org.springframework</groupId> <artifactId>spring-context</artifactId> <version>5.2.10.RELEASE</version> </dependency> <dependency> <groupId>org.aspectj</groupId> <artifactId>aspectjweaver</artifactId> <version>1.9.4</version> </dependency> </dependencies>
-
写业务逻辑代码
package com.itheima.dao.impl; import com.itheima.dao.BookDao; import org.springframework.stereotype.Repository; @Repository public class BookDaoImpl implements BookDao { // 2. 写业务逻辑代码(连接点) public void update() { System.out.println("book dao update..."); } public void delete() { System.out.println("book dao delete..."); } }
-
编写通知,定义切入点,编写切面(绑定切入点和通知)
package com.itheima.aop; import org.aspectj.lang.annotation.Aspect; import org.aspectj.lang.annotation.Before; import org.aspectj.lang.annotation.Pointcut; import org.springframework.stereotype.Component; @Component // 告诉Spring容器将通知类管理上 @Aspect // 告诉Spring这是AOP,不是普通的bean public class MyAdvice { // 3. 编写共性功能(通知) // 5. 定义切面 @Before("cut()") public void method(){ System.out.println(System.currentTimeMillis()); } // 4. 定义切入点,通过Pointcut注解描述. 切入点方法本身无参无返无方法体内容 @Pointcut("execution(void com.itheima.dao.BookDao.update())") private void cut(){} }
-
告知Spring有用注解开发的AOP
@Configuration @ComponentScan("com.itheima") @EnableAspectJAutoProxy // 告诉Spring有用注解开发的AOP,开启Spring对AOP注解的驱动支持 public class SpringConfig { }
-
结果
AOP工作流程(本质是代理模式)
- Spring容器启动
- 读取所有切面配置中的切入点,即绑定了通知的切入点,不是所有的切入点
- 初始化bean,判定bean对应的类中的方法是否匹配到读取的切入点
- 匹配失败:创建对象
- 匹配成功:创建原始对象(目标对象)的代理对象
- 获取bean执行方法
- 获取bean,调用方法并执行,完成操作
- 获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强内容,完成操作
AOP切入点表达式
切入点:要增强的方法
切入点表达式:要增强的方法的描述方式,可以面向接口方法,也可以面向实现类的方法
语法格式
动作关键字:描述切入点的行为动作,如execution表示执行到切入点时
访问修饰符:public、private等,可省略
返回值:
包名.类名[接口名].方法名(参数)
通配符
可以使用通配符描述切入点,加速描述。常用的通配符有:
- * 匹配单个独立的任意符号
如匹配com.itheima包下的任意包中的UserService类或接口中find开头的带一个参数的方法
execution(public * com.itheima..UserService.find(*)) - . . 匹配多个连续的任意符号,常用于简化包名与参数
如匹配com包下的任意包中的UserService类或接口中所有名称为findByld的方法
execution(public User com. .UserService.findbyId(. .)) - + 专用于匹配子类类型
书写技巧
- 描述切入点,通常描述接口,不描述实现类(解耦)
- 访问控制修饰符对接口开发均采用public描述,一般省略
- 增删改方法的返回值用精准类型加速匹配(一般都是boolean),查询方法的返回值使用*快速描述(因为不同查询方法返回值的对象不一样)
- 包名 尽量不使用. .匹配,效率很低,常用*做单个包描述匹配
- 接口/类名与模块相关的用*匹配,如UserService写成*Service
- 方法名 以动词进行精准匹配,名词采用*匹配,如findById写成findBy*
AOP通知类型
AOP通知共分为5种类型
- 前置通知 @Before(“pc()”)
- 后置通知 @After(“pc()”)
- 环绕通知 @Around(“pc()”)
- 返回后通知 @AfterReturning(“pc()”)
- 抛出异常后通知 @AfterThrowing(“pc()”)
ps:pc()是切入点对应的名称
后置通知和返回后通知的区别:
后置通知方法停止(无论正常结束还是抛异常)后就执行,返回后通知只有在方法正常结束后才执行
环绕通知要点:
环绕通知可以模拟其他所有类型的通知,但需要通过ProceedingJoinPoint参数在通知里对原始方法进行调用,以区分前后置通知,写法是固定的如代码所示;
如果不显示调用则只会执行通知不执行原操作(隔离原始操作,可以通过这个做权限校验)。
有的切入点还有返回值,因此环绕通知方法还要有一个Object类型的返回值。没有返回值的切入点会返回一个null
环绕通知方法必须抛出Throwable对象,因为无法预知原始方法会不会抛异常。
@Around("pc()")
public Object around(ProceedingJoinPoint pjp) throws Throwable {
Signature signature = pjp.getSignature(); // 获取切入点方法一次执行的签名信息,其中封装了执行过程
String classname = signature.getDeclaringTypeName();// 切入点所属类
String methodname = signature.getName();// 切入点方法名
System.out.println("around before advice");
// 表示执行原始操作,用于区分前置通知和后置通知
Object re = pjp.proceed();
System.out.println("around after advice");
return re;
}
AOP通知获取数据
前面已经实现了对现有方法进行增强,但通常增强方案会根据情况的不同而变化,这就需要得到原始操作中的相关数据。
AOP通知可以获得原始操作中的三类数据:
- 参数
- 返回值
- 异常
不是所有的通知都能拿到上面三种数据。
-
参数。所有通知都能拿到。环绕通知最屌,获取参数后还能修改后再传入
@Before("pc()") public void before(JoinPoint jp){ Object[] args = jp.getArgs(); // 获取参数列表,封装在一个数组中 System.out.println(Arrays.toString(args)); } @Around("pc()") public Object around(ProceedingJoinPoint pjp) throws Throwable { Object[] args = pjp.getArgs(); System.out.println(Arrays.toString(args)); args[0] = 666; //修改原始参数 System.out.println("around before advice"); // 表示执行原始操作,可以用于区分前置通知和后置通知 // pjp的proceed方法可以传入一个数组,而这个数组可以作为切入点方法的参数值 // 因此通过这个能力,可以对进行参数校验 Object num = pjp.proceed(args); System.out.println("around after advice"); return num; }
-
返回值。只有后置通知和环绕通知能拿到,环绕通知中获取pjp.proceed()的返回值
-
异常。只有抛出异常后通知和环绕通知能拿到,环绕通知中用try catch包围pjp.proceed()
三、Spring事务
事务作用:保障数据层操作的一致性。保障一系列的数据库操作同成功同失败
Spring事务作用:在数据层或业务层保障一系列的数据库操作同成功同失败
开启Spring事务
-
在业务层接口上用@Transactional注解开启事务
为降低耦合,一般不在业务层实现类上开启事务,虽然也可以;
也可以写在接口上,对接口中的所有方法都开启事务public interface AccountService { @Transactional //1. 开启事务 public void transfer(String in, String out, Double money); }
-
在config中配置一个事务管理bean
@Bean //2. 配置一个事务管理器 public PlatformTransactionManager transactionManager(DataSource dataSource){ // Mybatis用的是jdbc的事务,因此这里注入一个JDBC的数据源 DataSourceTransactionManager transactionManager = new DataSourceTransactionManager(); transactionManager.setDataSource(dataSource); return transactionManager; }
-
开启Spring的事务管理功能
@Configuration @ComponentScan("com.itheima") @PropertySource("jdbc.properties") @Import({jdbcConfig.class, MybatisConfig.class}) @EnableTransactionManagement //3. 开启注解式事务驱动 public class SpringConfig { }
Spring事务角色
Spring事务是通过什么机制实现的呢?事务合并!这是基于Spring事务管理器Mybatis共用同一个DataSource,否则没法搞
事务管理员:事务发起方,Spring中通常指业务层开启事务的方法
事务协调员:事务加入方,通常指代数据层方法,也可以是业务层方法
Spring事务将事务发起方中的子事务合并成为一个事务,从而实现事务管理
Spring事务配置
为了更精细化的管理事务,spring提供了若干配置项,可以在注解上开启这些配置项
Spring事务不是遇到异常就回滚,只有遇到运行时异常和errors类异常才回滚,这会导致不一致性问题(如遇到IO异常不回滚),因此需要手动添加回滚的异常类型
public interface AccountService {
@Transactional(rollbackFor = {IOException.class}) //1. 开启事务
public void transfer(String in, String out, Double money);
}
事务传播行为
有时候一个事务中的子任务不想受到其他子任务的限制,无论其他任务回滚不回滚,它都不想回滚。例如:在银行转账时,无论转账是否成功,都要记录转账信息,如果转账失败导致事务回滚,转账信息也就不能记录了。
因此,可以设置一个事务为事务协调员时(该方法在其他方法内被调用),不受事务管理员控制,单独开一个新事务
public interface LogService {
// 设置当该事务为事务协调员时,不受事务管理员的控制,单独开一个新事务
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void log(String out, String in, Double money, boolean flag);
}
@Transactional注解的propagation还有很多可选值