spring框架

spring框架,第1张

一、Spring 1.1 概念
  • spring是一个控制反转(IOC)和面向切面的开原框架
  • Spring是众多优秀设计模式的组合(工厂、单例、代理、适配器、包装器、观察者、模板、策略)。
  • Spring并未替代现有框架产品,而是将众多框架进行有机整合,简化企业级开发,俗称"胶水框架"。
1.2 官网地址

点击跳转:spring官网

二、Spring框架的构成
  • 核心技术:依赖注入,AOP,事件,资源,i18n,验证,数据绑定,类型转换,SpEL。
  • 测试:模拟对象,TestContext框架,Spring MVC测试,WebTestClient。
  • 数据访问:事务,DAO支持,JDBC,ORM,封送XML。
  • Spring MVC和 Spring WebFlux Web框架。
  • 集成:远程处理,JMS,JCA,JMX,电子邮件,任务,调度,缓存。
  • 语言:Kotlin,Groovy,动态语言
三、IOC 3.1 DI 依赖注入 3.1.1 概念

Spring在创建对象的同时给对象的属性赋值称为依赖注入

3.1.2 属性注入的方式

三种属性注入方式:

  • 构造方法注入

  • set 方法注入(推荐)----创建对象时,Spring工厂会通过Set方法为对象的属性赋值。

  • p名称空间注入(本质上还是 set 方法注入)

各种注入属性:

  • 基本数据类型,直接使用标签的 value 属性注入
  • 对象
    • 外部定义好一个对象,然后通过 ref 引用对象
    • 直接在需要的地方通过 bean 标签定义一个对象(局限性,定义好的 bean 无法复用)
  • List 集合:list
  • 数组:array
  • Map:map
  • Properties :props
3.1.3 基于xml 实现 DI
  1. 自定义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;
    }
}
  1. 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>
  1. 容器类型
 <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

  1. 定义接口
package com.ahao.demo01;

public interface ShowCmd {
    String show();
}

  1. 接口实现类一
package com.ahao.demo01;

public class WindowsShowCmd implements ShowCmd{
    @Override
    public String show() {
        return "dir";
    }
}
  1. 接口实现类二
package com.ahao.demo01;

public class LinuxShowCmd implements ShowCmd {
    @Override
    public String show() {
        return "ls";
    }
}
  1. 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");
    }
}
  1. 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 使用案例

不同的生产环境注入不一样的配置

5.2.1 基于注解配置
  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 + '\'' +
                '}';
    }
}

  1. 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;
    }
}

  1. 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 生命周期方法
  • 自定义初始化方法:添加“init-method”属性,Spring则会在创建对象之后,调用此方法。

  • 自定义销毁方法:添加“destroy-method”属性,Spring则会在销毁对象之前,调用此方法。

  • 销毁:工厂的close()方法被调用之后,Spring会毁掉所有已创建的单例对象。

  • 分类:Singleton对象由Spring容器销毁、Prototype对象由JVM销毁。

6.3 生命周期注解

初始化注解、销毁注解

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(初始化) ==》 构建完成 ==》随工厂关闭销毁

**多例bean:**prototype

被使用时创建 ==》 构造方法 ==》 set方法(注入值) ==》 init(初始化) ==》 构建完成 ==》JVM垃圾回收销毁

七、代理模式 7.1 静态代理

通过代理类的对象,为原始类的对象(目标类的对象)添加辅助功能,更容易更换代理实现类、利于维护。

  • 代理类 = 实现原始类相同接口 + 添加辅助功能 + 调用原始类的业务方法。
  • 静态代理的问题
    • 代理类数量过多,不利于项目的管理。
    • 多个代理类的辅助功能代码冗余,修改时,维护性差。
7.2 动态代理

动态创建代理类的对象,为原始类的对象添加辅助功能。

有两种实现方式:

  • 基于 JDK(不需要额外引入jar):被代理的对象存在接口。
  • 基于 cglib(需要引入外部jar):被代理的对象可以没有接口。
八、AOP 8.1 概念

AOP(Aspect Oriented Programming),即面向切面编程,利用一种称为"横切"的技术,剖开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可 *** 作性和可维护性。

8.2 AOP术语
  • 连接点(Joinpoint):连接点是程序类中客观存在的方法,可被Spring拦截并切入内容。

  • 切点(Pointcut):被Spring切入连接点。

  • 通知、增强(Advice):可以为切入点添加额外功能,分为:前置通知、后置通知、异常通知、环绕通知、返回通知等。

  • 目标对象(Target):代理的目标对象

  • 引介(Introduction):一种特殊的增强,可在运行期为类动态添加Field和Method。

  • 织入(Weaving):把通知应用到具体的类,进而创建新的代理类的过程。

  • 代理(Proxy):被AOP织入通知后,产生的结果类。

  • 切面(Aspect):由切点和通知组成,将横切逻辑织入切面所指定的连接点中。

8.3 作用

Spring的AOP编程即是通过动态代理类为原始类的方法添加辅助功能。

8.4 AOP案例 8.4.1 基于xml配置
  1. 准备目标对象(被代理的对象)
    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));
    }
}
  1. 准备通知(增强的代码)
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; } }

  1. 配置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>
  1. 测试: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
  1. 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 {
}

  1. 通知类(增强)
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; } }

  1. 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);
    }
}

小结:

  1. 前置通知:目标方法执行直接触发
  2. 后置通知:目标方法执行之后触发
  3. 返回通知:当目标方法有返回值的时候触发
  4. 异常通知:当目标方法抛出异常的时候触发(全局异常处理可用他)
  5. 环绕通知:以上四个的集大成者,方法执行之前和之后都会触发
8.5 jdk动态代理和cglib动态代理 8.5.1 区别
  • JDK动态代理是面向接口,在创建代理实现类时比CGLib要快,创建代理速度快。

  • CGLib动态代理是通过字节码底层继承要代理类来实现(被代理类不能是final修饰的类),在创建代理这一块没有JDK动态代理快,但是运行速度比JDK动态代理要快。

8.5.2 使用

如果要被代理的对象是个实现类,那么Spring会使用JDK动态代理来完成 *** 作(Spirng默认采用JDK动态代理实现机制)

如果要被代理的对象不是个实现类那么,Spring会强制使用CGLib来实现动态代理。

springboot 2.0 之后都是使用cglib动态代理


九、 事务 9.1 什么是事务

事务是数据库 *** 作的最小工作单元,是作为单个逻辑工作单元执行的一系列 *** 作;这些 *** 作作为一个整体一起向系统提交,要么都执行、要么都不执行。

事务最经典也经常被拿出来说例子就是转账了。假如小明要给小红转账 1000 元,这个转账会涉及到两个关键 *** 作就是:

  1. 将小明的余额减少 1000 元
  2. 将小红的余额增加 1000 元。
    万一在这两个 *** 作之间突然出现错误比如银行系统崩溃或者网络故障,导致小明余额减少而小红的余额没有增加,这样就不对了。事务就是保证这两个关键 *** 作要么都成功,要么都要失败。
9.2 ACID
  1. 原子性:事务中所有 *** 作是不可分割的原子单位。事务中所有 *** 作要么全部执行成功,要么全部执行失败。
  2. 一致性:事务执行后,数据库状态与其它业务规则保持一致。如转正业务,无论事务执行成功与否,参与转账的两个账号余额之和应该是不变的。
  3. 隔离性:隔离性是指在并发 *** 作中,不同事务之间应该隔离开来,使每个并发中的事务不会相互干扰。
  4. 持久性:一旦事务提交成功,事务中所有的数据 *** 作都必须被持久化到数据库中,即使提交事务后,数据库马上崩溃,在数据库重启时,也必须你能保证通过某种机制恢复数据。
9.3 事务的隔离级别

四种隔离级别:

  • 读未提交:read uncommitted
  • 读提交:read committed
  • 可重复读:repeatable read:默认即此
  • 可串行化:serializable
隔离级别脏读(读到其他事务尚未提交的数据)不可重复读(同一个事务中,执行相同的 SQL,多次执行结果可能不一样)幻读(例如查询 id 为 10 的记录,在查询的过程中,还有其他事务做了添加 *** 作,会导致多次查询结果不一致)
读未提交:read uncommitted
读提交:read committed×
可重复读:repeatable read(默认)×××
可串行化:serializable×××
十、Spring对事务的支持

Msyql数据库的MyISAM引擎不支持事务,InnoDB引擎支持
MySQL 怎么保证原子性的?

我们知道如果想要保证事务的原子性,就需要在异常发生时,对已经执行的 *** 作进行回滚,在MySQL 中,恢复机制是通过 回滚日志(undo log) 实现的,所有事务进行的修改都会先先记录到这个回滚日志中,然后再执行相关的 *** 作。如果执行过程中遇到异常的话,我们直接利用 回滚日志 中的信息将数据回滚到修改之前的样子即可!并且,回滚日志会先于数据持久化到磁盘上。这样就保证了即使遇到数据库突然宕机等情况,当用户再次启动数据库的时候,数据库还能够通过查询回滚日志来回滚将之前未完成的事务。

10.1 编程式事务

通过 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 接口则提供了一些方法来获取事务相应的状态比如是否新事务、是否可以回滚等等。

10.3.1 PlatformTransactionManager:事务管理接口

Spring 并不直接管理事务,而是提供了多种事务管理器 。Spring 事务管理器的接口是:PlatformTransactionManager 。
通过这个接口,Spring 为各个平台如 JDBC( DataSourceTransactionManager )、Hibernate( HibernateTransactionManager )、JPA( JpaTransactionManager )等都提供了对应的事务管理器,但是具体的实现就是各个平台自己的事情了。

10.3.2 TransactionDefinition:事务属性

事务管理器接口 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 注解默认使用就是这个事务传播行为。如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。也就是说:

  1. 如果外部方法没有开启事务的话, Propagation.REQUIRED 修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
  2. 如果外部方法开启事务并且被 Propagation.REQUIRED 的话,所有Propagation.REQUIRED 修饰
    的内部方法和外部方法均属于同一事务 ,只要一个方法回滚,整个事务均回滚。
11.1.2 TransactionDefinition.PROPAGATION_REQUIRES_NEW

创建一个新的事务,如果当前存在事务,则把当前事务挂起。也就是说不管外部方法是否开启事务,Propagation.REQUIRES_NEW 修饰的内部方法会新开启自己的事务,且开启的事务相互独立,互不干扰。
举个例子:如果我们上面的 bMethod() 使用 PROPAGATION_REQUIRES_NEW 事务传播行为修饰,aMethod 还是用 PROPAGATION_REQUIRED 修饰的话。如果 aMethod() 发生异常回滚, bMethod() 不会跟着回滚,因为 bMethod() 开启了独立的事务。但是,如果 bMethod() 抛出了未被捕获的异常并且这个异常满足事务回滚规则的话, aMethod() 同样也会回滚,因为这个异常被 aMethod() 的事务管理机制检测到了。

11.1.3 TransactionDefinition.PROPAGATION_NESTED

如果当前存在事务,则创建一个事务作为当前事务的嵌套事务来运行;如果当前没有事务,则该取值等价于 TransactionDefinition.PROPAGATION_REQUIRED 。也就是说:

  1. 在外部方法未开启事务的情况下 Propagation.NESTED 和 Propagation.REQUIRED 作用相同,修饰的内部方法都会新开启自己的事务,且开启的事务相互独立,互不干扰。
  2. 如果外部方法开启事务的话, Propagation.NESTED 修饰的内部方法属于外部事务的子事务,外部主事务回滚的话,子事务也会回滚,而内部子事务可以单独回滚而不影响外部主事务和其他子事务。
11.1.4 TransactionDefinition.PROPAGATION_MANDATORY

如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。(mandatory:强制性)

11.2 事务的隔离级别
  1. 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;
    ......
  1. 为了方便使用,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;
    }
}
  1. 事务隔离级别介绍
    下面我依次对每一种事务隔离级别进行介绍:
    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 的隔离级别。所有的事务依次逐个执行,这样事务之间就完全不可能产生干扰,也就是说,该级别可以防止脏读、不可重复读以及幻读。但是这将严重影响程序的性能。通常情况下也不会用到该级别。
11.3 事务超时属性

所谓事务超时,就是指一个事务所允许执行的最长时间,如果超过该时间限制但事务还没有完成,则自动回滚事务。在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 会开启一个单独的事务,中间被其它事务改了数据,都会实时读取到最新值。
分享一下关于事务只读属性,其他人的解答:

  1. 如果你一次执行单条查询语句,则没有必要启用事务支持,数据库默认支持 SQL 执行期间的读一致性;
  2. 如果你一次执行多条查询语句,例如统计查询,报表查询,在这种场景下,多条查询 SQL 必须保证整体的读一致性,否则,在前条 SQL 查询之后,后条 SQL 查询之前,数据被其他用户改变,则该次整体的统计查询将会出现读数据不一致的状态,此时,应该启用事务支持.
11.5 事务回滚规则

这些规则定义了哪些异常会导致事务回滚而哪些不会。默认情况下,事务只有遇到运行期异常
(RuntimeException 的子类)时才会回滚,Error 也会导致事务回滚,但是,在遇到检查型
(Checked)异常时不会回滚。
如果你想要回滚你定义的特定的异常类型的话,可以这样:

@Transactional(rollbackFor= MyException.class)
十二、@Transactional 注解使用详解 12.1 @Transactional 的作用范围
  1. 方法 :推荐将注解使用于方法上,不过需要注意的是:该注解只能应用到 public 方法上,否则不生效。
  2. 类 :如果这个注解使用在类上的话,表明该注解对该类中所有的 public 方法都生效。
  3. 接口 :不推荐在接口上使用。
12.2 @Transactional 的常用配置参数

五个常用的配置参数

属性名说明
propagation事务的传播行为,默认值为 REQUIRED
isolation事务的隔离级别,默认值采用 DEFAULT
timeout事务的超时时间,默认值为-1(不会超时)。如果超过该时间限制但事务还没有完成,则自动回滚事务。
readOnly指定事务是否为只读事务,默认值为 false。
rollbackFor用于指定能够触发事务回滚的异常类型,并且可以指定多个异常类型。
12.3 @Transactional 事务注解原理

@Transactional 的工作机制是基于 AOP 实现的,AOP 又是使用动态代理实现的。如果目标对象实现了接口,默认情况下会采用 JDK 的动态代理,如果目标对象没有实现了接口,会使用CGLIB 动态代理。
如果一个类或者一个类中的 public 方法上被标注 @Transactional 注解的话,Spring 容器就会在启动的时候为其创建一个代理类,在调用被 @Transactional 注解的 public 方法的时候,实际调用的是,TransactionInterceptor 类中的 invoke() 方法。这个方法的作用就是在目标方法之前开启事务,方法执行过程中如果遇到异常的时候回滚事务,方法调用完成之后提交事务

12.4 Spring AOP 自调用问题

若同一类中的其他没有 @Transactional 注解的方法内部调用有 @Transactional 注解的方法,有@Transactional 注解的方法的事务会失效。
这是由于 Spring AOP 代理的原因造成的,因为只有当 @Transactional 注解的方法在类以外被调用的时候,Spring 事务管理才生效。
解决办法就是避免同一类中自调用或者使用 AspectJ 取代 Spring AOP 代理。

12.5 @Transactional 的使用注意事项总结
  1. @Transactional 注解只有作用到 public 方法上事务才生效,不推荐在接口上使用;
  2. 避免同一个类中调用 @Transactional 注解的方法,这样会导致事务失效;
  3. 正确的设置 @Transactional 的 rollbackFor 和 propagation 属性,否则事务可能会回滚失败

欢迎分享,转载请注明来源:内存溢出

原文地址: http://outofmemory.cn/langs/726008.html

(0)
打赏 微信扫一扫 微信扫一扫 支付宝扫一扫 支付宝扫一扫
上一篇 2022-04-26
下一篇 2022-04-26

发表评论

登录后才能评论

评论列表(0条)

保存