- spring是一个控制反转(IOC)和面向切面的开原框架
- Spring是众多优秀设计模式的组合(工厂、单例、代理、适配器、包装器、观察者、模板、策略)。
- Spring并未替代现有框架产品,而是将众多框架进行有机整合,简化企业级开发,俗称"胶水框架"。
点击跳转:spring官网
二、Spring框架的构成- 核心技术:依赖注入,AOP,事件,资源,i18n,验证,数据绑定,类型转换,SpEL。
- 测试:模拟对象,TestContext框架,Spring MVC测试,WebTestClient。
- 数据访问:事务,DAO支持,JDBC,ORM,封送XML。
- Spring MVC和 Spring WebFlux Web框架。
- 集成:远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。
- 语言:Kotlin,Groovy,动态语言
Spring在创建对象的同时给对象的属性赋值称为依赖注入
3.1.2 属性注入的方式三种属性注入方式:
-
构造方法注入
-
set 方法注入(推荐)----创建对象时,Spring工厂会通过Set方法为对象的属性赋值。
-
p名称空间注入(本质上还是 set 方法注入)
各种注入属性:
- 基本数据类型,直接使用标签的 value 属性注入
- 对象
- 外部定义好一个对象,然后通过 ref 引用对象
- 直接在需要的地方通过 bean 标签定义一个对象(局限性,定义好的 bean 无法复用)
- List 集合:list
- 数组:array
- Map:map
- Properties :props
- 自定义Bean类型
package com.ahao.demo01;
public class User {
private Integer id;
private String username;
public Integer getId() {
return id;
}
public User(Integer id, String username) {
this.id = id;
this.username = username;
}
public void setId(Integer id) {
this.id = id;
}
public String getUsername() {
return username;
}
public void setUsername(String username) {
this.username = username;
}
}
- xml 配置文件
<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">
<bean class="com.ahao.demo01.User" id="user"/>
<bean class="com.ahao.demo01.User" id="user2">
<constructor-arg name="id" value="1"/>
<constructor-arg name="username" value="ahao"/>
bean>
<bean class="com.ahao.demo01.User" id="user3">
<property name="id" value="2"/>
<property name="username" value="zhangsan"/>
bean>
beans>
- 容器类型
<bean class="com.ahao.demo03.Cat" id="cat1">
<property name="id" value="1"/>
<property name="color" value="黄色"/>
bean>
<bean class="com.ahao.demo03.User" id="user">
<property name="id" value="1"/>
<property name="name" value="Ahao"/>
<property name="cats1">
<list>
<ref bean="cat1"/>
<bean class="com.ahao.demo03.Cat" id="cat2">
<property name="id" value="2"/>
<property name="color" value="红色"/>
bean>
list>
property>
<property name="cats2">
<array>
<ref bean="cat1"/>
<bean class="com.ahao.demo03.Cat" id="cat3">
<property name="id" value="3"/>
<property name="color" value="黑色"/>
bean>
array>
property>
<property name="cats3">
<map>
<entry key="cat1" value-ref="cat1"/>
<entry key="cat4">
<bean class="com.ahao.demo03.Cat" id="cat4">
<property name="id" value="4"/>
<property name="color" value="花色"/>
bean>
entry>
map>
property>
bean>
3.1.4 基于注解实现 DI
@Configuration
public class UserConfig {
@Bean
User user(){
User user = new User();
user.setId(1);
user.setName("ahao");
return user;
}
}
四、条件注解@Condition
案例:
假设我现在使用命令提示符窗口,如果是windows系统就输出dir;如果是linux系统就输出ls;
先在使用条件注解注入符合条件的bean
- 定义接口
package com.ahao.demo01;
public interface ShowCmd {
String show();
}
- 接口实现类一
package com.ahao.demo01;
public class WindowsShowCmd implements ShowCmd{
@Override
public String show() {
return "dir";
}
}
- 接口实现类二
package com.ahao.demo01;
public class LinuxShowCmd implements ShowCmd {
@Override
public String show() {
return "ls";
}
}
- WindowsCondition.java
package com.ahao.demo01;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class WindowsCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
//如果当前系统的名字中包含 windows,则认为是一个 windows *** 作系统
return context.getEnvironment().getProperty("os.name").toLowerCase().contains("windows");
}
}
- LinuxCodition.java
package com.ahao.demo01;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;
public class LinuxCondition implements Condition {
@Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
//如果当前系统的名字中包含 linux,则认为是一个 Linux *** 作系统
return context.getEnvironment().getProperty("os.name").toLowerCase().contains("linux");
}
}
6.Test.java
package com.ahao.demo01;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Test{
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(JavaConfig.class);
//因为本系统是windows系统,所以此时容器中只会注册WinndowsShowCmd的bean
ShowCmd bean = context.getBean(ShowCmd.class);
System.out.println("bean.show() = " + bean.show());//运行结果:bean.show() = dir
}
}
五、@Profile注解的使用
5.1 作用
- 指定组件在哪个环境的情况下才能被注册到容器中,不指定,任何环境下都能注册这个组件。
- 我们在开发过程中有很多的生产环境,如开发、测试,不同的环境需要用到不同的数据库;使用@Profile注解就可以解决这个问题,当在实际运行的时候,只需给定一个参数来激活对应的Profile即可。
5.2.1 基于注解配置不同的生产环境注入不一样的配置
- DataSource.java
package com.ahao.demo02;
public class DataSource {
private String url;
private String username;
private String password;
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;
}
@Override
public String toString() {
return "DataSource{" +
"url='" + url + '\'' +
", username='" + username + '\'' +
", password='" + password + '\'' +
'}';
}
}
- DataSourceConfig.java
package com.ahao.demo02;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Conditional;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
@Configuration
public class DataSourceConfig {
@Bean()
//如果当前系统环境是 dev,则这个 bean 注册到 Spring 容器中
@Profile("dev")
DataSource devDs(){
DataSource ds = new DataSource();
ds.setUrl("jdbc:mysql://localhost:3306/dev");
ds.setUsername("root");
ds.setPassword("123456");
return ds;
}
@Bean()
//如果当前系统环境是 prod,则这个 bean 注册到 Spring 容器中
@Profile("prod")
DataSource prodDs(){
DataSource ds = new DataSource();
ds.setUrl("jdbc:mysql://localhost:3306/prod");
ds.setUsername("root");
ds.setPassword("xxx123");
return ds;
}
}
- TestDemo.java
package com.ahao.demo02;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class TestDemo {
public static void main(String[] args) {
//注意,这里先不要设置 DataSourceConfig
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
//先设置系统环境是 dev 还是 prod,设置完成后,再去加载配置类
//设置当前系统环境为 dev
context.getEnvironment().setActiveProfiles("dev");
//设置完系统环境之后,再去加载 配置类
context.register(DataSourceConfig.class);
//刷新容器
context.refresh();
DataSource dataSource = context.getBean(DataSource.class);
System.out.println("dataSource = " + dataSource);
}
}
5.2.2 基于xml配置
<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">
<beans profile="dev">
<bean id="dataSource" class="com.ahao.demo03.DataSource">
<property name="url" value="jdbc:mysql://localhost:3306/dep"/>
<property name="username" value="root"/>
<property name="password" value="123456"/>
bean>
beans>
<beans profile="prod">
<bean id="dataSource" class="com.ahao.demo03.DataSource">
<property name="url" value="jdbc:mysql://localhost:3306/prod"/>
<property name="username" value="root"/>
<property name="password" value="dsfsdgsd"/>
bean>
beans>
beans>
TestDemo.java
package com.ahao.demo03;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestDemo{
public static void main(String[] args) {
//先不要加载配置文件,先去设置环境
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext();
context.getEnvironment().setActiveProfiles("prod");
//设置配置文件
context.setConfigLocation("applicationContext.xml");
context.refresh();
DataSource bean = context.getBean(DataSource.class);
System.out.println("bean = " + bean);
}
}
六、Spring工厂特性
6.1 饿汉式创建优势
工厂创建之后,会将Spring配置文件中的所有对象都创建完成(饿汉式)。 提高程序运行效率。避免多次IO,减少对象创建时间。(概念接近连接池,一次性创建好,使用时直接获取)
6.2 生命周期方法6.3 生命周期注解
自定义初始化方法:添加“init-method”属性,Spring则会在创建对象之后,调用此方法。
自定义销毁方法:添加“destroy-method”属性,Spring则会在销毁对象之前,调用此方法。
销毁:工厂的close()方法被调用之后,Spring会毁掉所有已创建的单例对象。
分类:Singleton对象由Spring容器销毁、Prototype对象由JVM销毁。
初始化注解、销毁注解
import javax.annotation.PostConstruct;
import javax.annotation.PreDestroy;
@PostConstruct //初始化
public void init(){
System.out.println("init method executed");
}
@PreDestroy //销毁
public void destroy(){
System.out.println("destroy method executed");
}
6.4 生命周期阶段
**单例bean:**singleton
随工厂启动创建 ==》 构造方法 ==》 set方法(注入值) ==》 init(初始化) ==》 构建完成 ==》随工厂关闭销毁
七、代理模式 7.1 静态代理**多例bean:**prototype
被使用时创建 ==》 构造方法 ==》 set方法(注入值) ==》 init(初始化) ==》 构建完成 ==》JVM垃圾回收销毁
通过代理类的对象,为原始类的对象(目标类的对象)添加辅助功能,更容易更换代理实现类、利于维护。
- 代理类 = 实现原始类相同接口 + 添加辅助功能 + 调用原始类的业务方法。
- 静态代理的问题
- 代理类数量过多,不利于项目的管理。
- 多个代理类的辅助功能代码冗余,修改时,维护性差。
动态创建代理类的对象,为原始类的对象添加辅助功能。
有两种实现方式:
- 基于 JDK(不需要额外引入jar):被代理的对象存在接口。
- 基于 cglib(需要引入外部jar):被代理的对象可以没有接口。
AOP(Aspect Oriented Programming),即面向切面编程,利用一种称为"横切"的技术,剖开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可 *** 作性和可维护性。
8.2 AOP术语-
连接点(Joinpoint):连接点是程序类中客观存在的方法,可被Spring拦截并切入内容。
-
切点(Pointcut):被Spring切入连接点。
-
通知、增强(Advice):可以为切入点添加额外功能,分为:前置通知、后置通知、异常通知、环绕通知、返回通知等。
-
目标对象(Target):代理的目标对象
-
引介(Introduction):一种特殊的增强,可在运行期为类动态添加Field和Method。
-
织入(Weaving):把通知应用到具体的类,进而创建新的代理类的过程。
-
代理(Proxy):被AOP织入通知后,产生的结果类。
-
切面(Aspect):由切点和通知组成,将横切逻辑织入切面所指定的连接点中。
Spring的AOP编程即是通过动态代理类为原始类的方法添加辅助功能。
8.4 AOP案例 8.4.1 基于xml配置- 准备目标对象(被代理的对象)
Calculator.java
package com.ahao.demo.service;
public interface Calculator {
int add(int a, int b);
void minus(int a, int b);
}
CalculatorImpl.java
package com.ahao.demo.service;
public class CalculatorImpl implements Calculator{
@Override
public int add(int a, int b) {
return a+b;
}
@Override
public void minus(int a, int b) {
System.out.println(a + "-" + b + "=" + (a - b));
}
}
- 准备通知(增强的代码)
package com.ahao.demo;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Before;
/**
* 这是我的日志切面类
*
* Spring AOP 包含五种通知:
*
* 1. 前置通知:目标方法执行直接触发
* 2. 后置通知:目标方法执行之后触发
* 3. 返回通知:当目标方法有返回值的时候触发
* 4. 异常通知:当目标方法抛出异常的时候触发(全局异常处理可用他)
* 5. 环绕通知:以上四个的集大成者
*/
public class LogAdvice {
/**
* 这是前置通知,目标方法执行之前,这个方法会被触发
* 目标方法也就是我们要拦截的方法
*/
public void brfore(JoinPoint joinPoint){
//获取目标方法的名称(被拦截的方法名称)
String name = joinPoint.getSignature().getName();
System.out.println(name + " 方法开始执行了。。。");
}
/**
* 这是后置通知,目标方法执行之后,这个方法会被触发
* @param jp
*/
public void after(JoinPoint jp) {
Signature signature = jp.getSignature();
String name = signature.getName();
System.out.println(name + " 方法执行结束了...");
}
/**
* 返回通知
*
* 我想知道目标方法的返回值到底是多少?
*
* 注意这个接收目标方法返回值的参数的类型,必须要匹配,用了 int,那么当返回值类型为 int 的时候,才会进入到当前方法中
*
* @param jp
*/
public void returning(JoinPoint jp, Object result) {
System.out.println("返回通知。。。" + result);
}
/**
* 异常通知
* 只有当目标方法抛出异常的时候,这个方法会被触发
*
* 注意,只有拦截的异常类型能够匹配上实际抛出的异常,这个方法才会被触发。
* 例如如果这里的参数是 ArithmeticException,实际抛出的异常是空指针异常,那么这个方法就不会被触发
*
* 如果向拦截所有异常,那么这里的参数类型可以使用 Exception
*
* @param jp
*/
public void exception(JoinPoint jp, ArithmeticException e) {
System.out.println(jp.getSignature().getName() + " 方法抛出异常了 " + e.getMessage());
}
/**
* 环绕通知,这个是集大成者
*/
public Object around(ProceedingJoinPoint pjp) {
//这个方法类似于之前的 method.invoke() 方法
try {
//这一句其实是在调用目标方法
System.out.println("====这里就相当于前置通知====");
long startTime = System.nanoTime();
Object proceed = pjp.proceed();
long endTime = System.nanoTime();
System.out.println(pjp.getSignature().getName() + " 方法执行耗时:" + (endTime - startTime));
System.out.println("====这里就相当于后置通知====");
return proceed;
} catch (Throwable throwable) {
throwable.printStackTrace();
System.out.println("====这里相当于异常通知");
}
return null;
}
}
- 配置applicationContext.xml
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean class="com.ahao.demo.service.CalculatorImpl" id="calculator"/>
<bean class="com.ahao.demo.LogAdvice" id="LogAdvice"/>
<aop:config>
<aop:pointcut id="pointcut1" expression="execution(* com.ahao.demo.service.*.*(..))"/>
<aop:aspect ref="LogAdvice">
<aop:before method="brfore" pointcut-ref="pointcut1"/>
<aop:after method="after" pointcut-ref="pointcut1"/>
<aop:after-returning method="returning" pointcut-ref="pointcut1" returning="result"/>
<aop:after-throwing method="exception" pointcut-ref="pointcut1" throwing="e"/>
<aop:around method="around" pointcut-ref="pointcut1"/>
aop:aspect>
aop:config>
beans>
- 测试:TestDemo.java
package com.ahao.demo;
import com.ahao.demo.service.Calculator;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class TestDemo {
public static void main(String[] args) {
ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("applicationContext.xml");
//注意,这个地方拿到的对象实际上是 Spring AOP 利用 JDK 动态代理给 Calculator 接口自动生成的一个对象
Calculator calculator = context.getBean("calculator",Calculator.class);
System.out.println("calculator.getClass() = " + calculator.getClass());
calculator.add(3, 4);
calculator.minus(4, 5);
}
}
8.4.2 基于注解实现AOP
- JavaConfig 配置类
package com.ahao.demo01.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
@Configuration
@ComponentScan("com.ahao.demo01")
public class JavaConfig {
}
- 通知类(增强)
package com.ahao.demo01.aop;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
import org.springframework.stereotype.Component;
/**
* LogAdvice
*
* LogAspect=PointCut+LogAdvice
*
* @Component 表示将 LogAdvice 注入到 Spring 容器中
* @Aspect 表示当前类是一个切面
* @EnableAspectJAutoProxy 开启切面的自动代理
*/
@Component
@Aspect
@EnableAspectJAutoProxy
public class LogAdvice {
//配置被增强的方法(切点)
@Pointcut("execution(* com.ahao.demo01.service.*.*(..))")
public void pointCut(){
}
//前置通知
@Before("pointCut()")
public void before(JoinPoint joinPoint){
System.out.println(joinPoint.getSignature().getName()+"before...执行了");
}
//后置通知
@After("pointCut()")
public void after(JoinPoint joinPoint){
System.out.println(joinPoint.getSignature().getName()+"after...执行了");
}
//返回通知
@AfterReturning(value = "pointCut()",returning = "r")
public void returning(JoinPoint joinPoint,Object r){
System.out.println(joinPoint.getSignature().getName()+"方法的返回值是"+r);
}
//异常
@AfterThrowing(value = "pointCut()",throwing = "e")
public void throwing(JoinPoint joinPoint,Exception e){
System.out.println(joinPoint.getSignature().getName()+"方法抛出了"+e+"异常");
}
//环绕
@Around("pointCut()")
public Object around(ProceedingJoinPoint pjp){
try {
return pjp.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
return null;
}
}
- TestDemo.java
package com.ahao.demo01;
import com.ahao.demo01.config.JavaConfig;
import com.ahao.demo01.service.Calculator;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
public class Demo {
public static void main(String[] args) {
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(JavaConfig.class);
Calculator bean = context.getBean(Calculator.class);
bean.add(5,9);
}
}
小结:
- 前置通知:目标方法执行直接触发
- 后置通知:目标方法执行之后触发
- 返回通知:当目标方法有返回值的时候触发
- 异常通知:当目标方法抛出异常的时候触发(全局异常处理可用他)
- 环绕通知:以上四个的集大成者,方法执行之前和之后都会触发
-
JDK动态代理是面向接口,在创建代理实现类时比CGLib要快,创建代理速度快。
-
CGLib动态代理是通过字节码底层继承要代理类来实现(被代理类不能是final修饰的类),在创建代理这一块没有JDK动态代理快,但是运行速度比JDK动态代理要快。
如果要被代理的对象是个实现类,那么Spring会使用JDK动态代理来完成 *** 作(Spirng默认采用JDK动态代理实现机制)
如果要被代理的对象不是个实现类那么,Spring会强制使用CGLib来实现动态代理。
springboot 2.0 之后都是使用cglib动态代理
九、 事务 9.1 什么是事务
事务是数据库 *** 作的最小工作单元,是作为单个逻辑工作单元执行的一系列 *** 作;这些 *** 作作为一个整体一起向系统提交,要么都执行、要么都不执行。
9.2 ACID事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账 1000 元,这个转账会涉及到两个关键 *** 作就是:
- 将小明的余额减少 1000 元
- 将小红的余额增加 1000 元。
万一在这两个 *** 作之间突然出现错误比如银行系统崩溃或者网络故障,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键 *** 作要么都成功,要么都要失败。
- 原子性:事务中所有 *** 作是不可分割的原子单位。事务中所有 *** 作要么全部执行成功,要么全部执行失败。
- 一致性:事务执行后,数据库状态与其它业务规则保持一致。如转正业务,无论事务执行成功与否,参与转账的两个账号余额之和应该是不变的。
- 隔离性:隔离性是指在并发 *** 作中,不同事务之间应该隔离开来,使每个并发中的事务不会相互干扰。
- 持久性:一旦事务提交成功,事务中所有的数据 *** 作都必须被持久化到数据库中,即使提交事务后,数据库马上崩溃,在数据库重启时,也必须你能保证通过某种机制恢复数据。
四种隔离级别:
- 读未提交:read uncommitted
- 读提交:read committed
- 可重复读:repeatable read:默认即此
- 可串行化:serializable
隔离级别 | 脏读(读到其他事务尚未提交的数据) | 不可重复读(同一个事务中,执行相同的 SQL,多次执行结果可能不一样) | 幻读(例如查询 id 为 10 的记录,在查询的过程中,还有其他事务做了添加 *** 作,会导致多次查询结果不一致) |
---|---|---|---|
读未提交:read uncommitted | √ | √ | √ |
读提交:read committed | × | √ | √ |
可重复读:repeatable read(默认) | × | × | × |
可串行化:serializable | × | × | × |
Msyql数据库的MyISAM引擎不支持事务,InnoDB引擎支持
MySQL 怎么保证原子性的?
10.1 编程式事务我们知道如果想要保证事务的原子性,就需要在异常发生时,对已经执行的 *** 作进行回滚,在MySQL 中,恢复机制是通过 回滚日志(undo log) 实现的,所有事务进行的修改都会先先记录到这个回滚日志中,然后再执行相关的 *** 作。如果执行过程中遇到异常的话,我们直接利用 回滚日志 中的信息将数据回滚到修改之前的样子即可!并且,回滚日志会先于数据持久化到磁盘上。这样就保证了即使遇到数据库突然宕机等情况,当用户再次启动数据库的时候,数据库还能够通过查询回滚日志来回滚将之前未完成的事务。
通过 TransactionTemplate 或者 TransactionManager 手动管理事务,实际应用中很少使用
使用TransactionTemplate 和TransactionManager 进行声明式事务的案例代码:
package com.ahao.demo.service;
import com.ahao.demo.dao.AccountDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.DefaultTransactionDefinition;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;
/**
* 编程式事务
*/
@Service
public class AccountService {
@Autowired
AccountDao accountDao;
@Autowired
TransactionTemplate transactionTemplate;
//因为 DataSourceTransactionManager 是 PlatformTransactionManager 的子类,所以我们在注入的时候,可以用接口去接收这个对象。
@Autowired
PlatformTransactionManager platformTransactionManager;
public void transfer01(String from, String to, double money) {
//TransactionCallbackWithoutResult 表示一个不带返回结果的事务
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
@Override
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
accountDao.minusMoney(from, money);
// int i = 1 / 0;
accountDao.addMoney(to, money);
//正常执行的话,不需要手动提交
} catch (Exception e) {
e.printStackTrace();
//出错时回滚
status.setRollbackOnly();
}
}
});
}
public void transfer02(String from, String to, Double money) {
//定义事务的各种属性,各种属性都定义在 definition 中
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
//开启一个事务
TransactionStatus status = platformTransactionManager.getTransaction(definition);
try {
accountDao.minusMoney(from, money);
// int i = 1 / 0;
accountDao.addMoney(to, money);
//手动提交事务
platformTransactionManager.commit(status);
} catch (Exception e) {
e.printStackTrace();
//手动回滚事务
platformTransactionManager.rollback(status);
}
}
}
10.2 声明式事务
推荐使用(代码侵入性最小),实际是通过 AOP 实现(基于@Transactional 的全注解方式使用最多)
关键代码:
配置
package com.ahao.demo.config;
import com.alibaba.druid.pool.DruidDataSource;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;
import javax.sql.DataSource;
@Configuration
@PropertySource("classpath:db.properties")
@ComponentScan(basePackages = "com.ahao.demo")
@EnableTransactionManagement
public class JavaConfig {
@Value("${db.username}")
String username;
@Value("${db.password}")
String password;
@Value("${db.url}")
String url;
@Bean
DataSource dataSource(){
DruidDataSource dataSource = new DruidDataSource();
dataSource.setUsername(username);
dataSource.setPassword(password);
dataSource.setUrl(url);
return dataSource;
}
@Bean
DataSourceTransactionManager dataSourceTransactionManager(){
DataSourceTransactionManager manager = new DataSourceTransactionManager();
manager.setDataSource(dataSource());
return manager;
}
}
package com.ahao.demo.service;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.transaction.annotation.Transactional;
@Component
public class A {
@Autowired
B b;
@Transactional
public void methodA(){
System.out.println("A方法执行了..........");
b.methodB();
}
}
10.3 事务管理接口
Spring 框架中,事务管理相关最重要的 3 个接口如下:
PlatformTransactionManager : (平台)事务管理器,Spring 事务策略的核心。
TransactionDefinition : 事务定义信息(事务隔离级别、传播行为、超时、只读、回滚规则)。TransactionStatus : 事务运行状态。我们可以把 PlatformTransactionManager 接口可以被看作是事务上层的管理者,而TransactionDefinition 和 TransactionStatus 这两个接口可以看作是事务的描述。PlatformTransactionManager 会根据 TransactionDefinition 的定义比如事务超时时间、隔离级别、传播行为等来进行事务管理 ,TransactionStatus 接口则提供了一些方法来获取事务相应的状态比如是否新事务、是否可以回滚等等。
Spring 并不直接管理事务,而是提供了多种事务管理器 。Spring 事务管理器的接口是:PlatformTransactionManager 。
通过这个接口,Spring 为各个平台如 JDBC( DataSourceTransactionManager )、Hibernate( HibernateTransactionManager )、JPA( JpaTransactionManager )等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。
事务管理器接口 PlatformTransactionManager 通过 getTransaction(TransactionDefinition definition) 方法来得到一个事务,这个方法里面的参数是 TransactionDefinition 类 ,这个类就定义了一些基本的事务属性
事务的属性包含了5个方面:
TransactionDefinition
接口中定义了 5 个方法以及一些表示事务属性的常量比如隔离级别、传播行为等等。
import org.springframework.lang.Nullable;
public interface TransactionDefinition {
int PROPAGATION_REQUIRED = 0;
int PROPAGATION_SUPPORTS = 1;
int PROPAGATION_MANDATORY = 2;
int PROPAGATION_REQUIRES_NEW = 3;
int PROPAGATION_NOT_SUPPORTED = 4;
int PROPAGATION_NEVER = 5;
int PROPAGATION_NESTED = 6;
int ISOLATION_DEFAULT = -1;
int ISOLATION_READ_UNCOMMITTED = 1;
int ISOLATION_READ_COMMITTED = 2;
int ISOLATION_REPEATABLE_READ = 4;
int ISOLATION_SERIALIZABLE = 8;
int TIMEOUT_DEFAULT = -1; // 返回事务的传播行为,默认值为 REQUIRED。
int getPropagationBehavior();
//返回事务的隔离级别,默认值是 DEFAULT
int getIsolationLevel();
// 返回事务的超时时间,默认值为-1。如果超过该时间限制但事务还没有完成,则自动回滚事务。
int getTimeout();
// 返回是否为只读事务,默认值为 false
boolean isReadOnly();
@Nullable
String getName();
}
十一、事务属性详解
11.1 事务传播行为
- 事务传播行为是为了解决业务层方法之间互相调用的事务问题。
- 当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。
TransactionDefinition接口中定义了传播行为的常量:
public interface TransactionDefinition {
int PROPAGATION_REQUIRED = 0;
int PROPAGATION_SUPPORTS = 1;
int PROPAGATION_MANDATORY = 2;
int PROPAGATION_REQUIRES_NEW = 3;
int PROPAGATION_NOT_SUPPORTED = 4;
int PROPAGATION_NEVER = 5;
int PROPAGATION_NESTED = 6;
int ISOLATION_DEFAULT = -1;
int ISOLATION_READ_UNCOMMITTED = 1;
int ISOLATION_READ_COMMITTED = 2;
int ISOLATION_REPEATABLE_READ = 4;
int ISOLATION_SERIALIZABLE = 8;
int TIMEOUT_DEFAULT = -1;
...
}
为了方便使用,Spring 相应地定义了一个枚举类: Propagation
public enum Propagation {
REQUIRED(0),
SUPPORTS(1),
MANDATORY(2),
REQUIRES_NEW(3),
NOT_SUPPORTED(4),
NEVER(5),
NESTED(6);
private final int value;
private Propagation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
11.1.1 TransactionDefinition.PROPAGATION_REQUIRED
使用的最多的一个事务传播行为,我们平时经常使用的 @Transactional 注解默认使用就是这个事务传播行为。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。也就是说:
- 如果外部方法没有开启事务的话, Propagation.REQUIRED 修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
- 如果外部方法开启事务并且被 Propagation.REQUIRED 的话,所有Propagation.REQUIRED 修饰
的内部方法和外部方法均属于同一事务 ,只要一个方法回滚,整个事务均回滚。
创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
举个例子:如果我们上面的 bMethod() 使用 PROPAGATION_REQUIRES_NEW 事务传播行为修饰,aMethod 还是用 PROPAGATION_REQUIRED 修饰的话。如果 aMethod() 发生异常回滚, bMethod() 不会跟着回滚,因为 bMethod() 开启了独立的事务。但是,如果 bMethod() 抛出了未被捕获的异常并且这个异常满足事务回滚规则的话, aMethod() 同样也会回滚,因为这个异常被 aMethod() 的事务管理机制检测到了。
如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED 。也就是说:
- 在外部方法未开启事务的情况下 Propagation.NESTED 和 Propagation.REQUIRED 作用相同,修饰的内部方法都会新开启自己的事务,且开启的事务相互独立,互不干扰。
- 如果外部方法开启事务的话, Propagation.NESTED 修饰的内部方法属于外部事务的子事务,外部主事务回滚的话,子事务也会回滚,而内部子事务可以单独回滚而不影响外部主事务和其他子事务。
如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)
11.2 事务的隔离级别- TransactionDefinition 接口中定义了五个表示隔离级别的常量:
public interface TransactionDefinition {
......
int ISOLATION_DEFAULT = -1;
int ISOLATION_READ_UNCOMMITTED = 1;
int ISOLATION_READ_COMMITTED = 2;
int ISOLATION_REPEATABLE_READ = 4;
int ISOLATION_SERIALIZABLE = 8;
......
- 为了方便使用,Spring 相应地定义了一个枚举类: Isolation
public enum Isolation {
DEFAULT(-1),
READ_UNCOMMITTED(1),
READ_COMMITTED(2),
REPEATABLE_READ(4),
SERIALIZABLE(8);
private final int value;
private Isolation(int value) {
this.value = value;
}
public int value() {
return this.value;
}
}
- 事务隔离级别介绍
下面我依次对每一种事务隔离级别进行介绍:
TransactionDefinition.ISOLATION_DEFAULT :使用后端数据库默认的隔离级别,MySQL 默认采用的 REPEATABLE_READ 隔离级别 Oracle 默认采用的 READ_COMMITTED 隔离级别. TransactionDefinition.ISOLATION_READ_UNCOMMITTED :最低的隔离级别,使用这个隔离级别很少,因为它允许读取尚未提交的数据变更,可能会导致脏读、幻读或不可重复读
TransactionDefinition.ISOLATION_READ_COMMITTED : 允许读取并发事务已经提交的数据,可以阻止脏读,但是幻读或不可重复读仍有可能发生TransactionDefinition.ISOLATION_REPEATABLE_READ : 对同一字段的多次读取结果都是一致的,除非数据是被本身事务自己所修改,可以阻止脏读和不可重复读,但幻读仍有可能发生。TransactionDefinition.ISOLATION_SERIALIZABLE : 最高的隔离级别,完全服从 ACID 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在TransactionDefinition 中以 int 的值来表示超时时间,其单位是秒,默认值为-1。
11.4 事务只读属性对于只有读取数据查询的事务,可以指定事务类型为 readonly,即只读事务。只读事务不涉及数据的修改,数据库会提供一些优化手段,适合用在有多条数据库查询 *** 作的方法中。
很多人就会疑问了,为什么我一个数据查询 *** 作还要启用事务支持呢?
拿 MySQL 的 innodb 举例子,根据官网 https://dev.mysql.com/doc/refman/5.7/en/innodbautocommit-commit-rollback.html 描述:
MySQL 默认对每一个新建立的连接都启用了 autocommit 模式。在该模式下,每一个发送到MySQL 服务器的 sql 语句都会在一个单独的事务中进行处理,执行结束后会自动提交事务,并开启一个新的事务。
但是,如果你给方法加上了 Transactional 注解的话,这个方法执行的所有 sql 会被放在一个事务中。如果声明了只读事务的话,数据库就会去优化它的执行,并不会带来其他的什么收益。
如果不加 Transactional ,每条 sql 会开启一个单独的事务,中间被其它事务改了数据,都会实时读取到最新值。
分享一下关于事务只读属性,其他人的解答:
- 如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持 SQL 执行期间的读一致性;
- 如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询 SQL 必须保证整体的读一致性,否则,在前条 SQL 查询之后,后条 SQL 查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持.
这些规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下,事务只有遇到运行期异常
(RuntimeException 的子类)时才会回滚,Error 也会导致事务回滚,但是,在遇到检查型
(Checked)异常时不会回滚。
如果你想要回滚你定义的特定的异常类型的话,可以这样:
@Transactional(rollbackFor= MyException.class)
十二、@Transactional 注解使用详解
12.1 @Transactional 的作用范围
- 方法 :推荐将注解使用于方法上,不过需要注意的是:该注解只能应用到 public 方法上,否则不生效。
- 类 :如果这个注解使用在类上的话,表明该注解对该类中所有的 public 方法都生效。
- 接口 :不推荐在接口上使用。
五个常用的配置参数
属性名 | 说明 |
---|---|
propagation | 事务的传播行为,默认值为 REQUIRED |
isolation | 事务的隔离级别,默认值采用 DEFAULT |
timeout | 事务的超时时间,默认值为-1(不会超时)。如果超过该时间限制但事务还没有完成,则自动回滚事务。 |
readOnly | 指定事务是否为只读事务,默认值为 false。 |
rollbackFor | 用于指定能够触发事务回滚的异常类型,并且可以指定多个异常类型。 |
@Transactional 的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用CGLIB 动态代理。
如果一个类或者一个类中的 public 方法上被标注 @Transactional 注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被 @Transactional 注解的 public 方法的时候,实际调用的是,TransactionInterceptor 类中的 invoke() 方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务
若同一类中的其他没有 @Transactional 注解的方法内部调用有 @Transactional 注解的方法,有@Transactional 注解的方法的事务会失效。
这是由于 Spring AOP 代理的原因造成的,因为只有当 @Transactional 注解的方法在类以外被调用的时候,Spring 事务管理才生效。
解决办法就是避免同一类中自调用或者使用 AspectJ 取代 Spring AOP 代理。
- @Transactional 注解只有作用到 public 方法上事务才生效,不推荐在接口上使用;
- 避免同一个类中调用 @Transactional 注解的方法,这样会导致事务失效;
- 正确的设置 @Transactional 的 rollbackFor 和 propagation 属性,否则事务可能会回滚失败
欢迎分享,转载请注明来源:内存溢出
评论列表(0条)