Spring(二)

3.AOP

3.1代理模式

3.1.1概念

①介绍

二十三种设计模式中的一种,属于结构型模式。它的作用就是通过提供一个代理类,让我们在调用目标方法的时候,不再是直接对目标方法进行调用,而是通过代理类间接调用。

让不属于目标方法核心逻辑的代码从目标方法中剥离出来——解耦。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰,同时让附加功能能够集中在一起也有利于统一维护。

image-20230106190542434

使用代理后:

image-20230106190604657

②生活中的代理
  1. 广告商找大明星拍广告需要经过经纪人
  2. 合作伙伴找大老板谈合作要约见面时间需要经过秘书
  3. 房产中介是买卖双方的代理
③相关术语
  • 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。
  • 目标:被代理“套用”了非核心逻辑代码的类、对象、方法。

3.2.2、静态代理

创建静态代理类:

public class CalculatorStaticProxy implements Calculator {
    // 将被代理的目标对象声明为成员变量
    private Calculator target;
    public CalculatorStaticProxy(Calculator target) {
        this.target = target;
    }
    @Override
    public int add(int i, int j) {
        // 附加功能由代理类中的代理方法来实现
        System.out.println("[日志] add 方法开始了,参数是:" + i + "," + j);
        // 通过目标对象来实现核心业务逻辑
        int addResult = target.add(i, j);
        System.out.println("[日志] add 方法结束了,结果是:" + addResult);
        return addResult;
    }
}

静态代理确实实现了解耦,但是由于代码都写死了,完全不具备任何的灵活性。就拿日志功能来说,将来其他地方也需要附加日志,那还得再声明更多个静态代理类,那就产生了大量重复的代码,日志功能还是分散的,没有统一管理。

提出进一步的需求:将日志功能集中到一个代理类中,将来有任何日志需求,都通过这一个代理类来实现。这就需要使用动态代理技术了。

3.2.3、动态代理

image-20230106190915622

生产代理对象的工厂类:

public class ProxyFactory {
    private Object target;
    public ProxyFactory(Object target) {
        this.target = target;
    }
    public Object getProxy(){
        /**
        * newProxyInstance():创建一个代理实例
        * 其中有三个参数:
        * 1、classLoader:加载动态生成的代理类的类加载器
        * 2、interfaces:目标对象实现的所有接口的class对象所组成的数组
        * 3、invocationHandler:设置代理对象实现目标对象方法的过程,即代理类中如何重写接口中的抽象方法 
        */        
        ClassLoader classLoader = target.getClass().getClassLoader();
        Class<?>[] interfaces = target.getClass().getInterfaces();
        InvocationHandler invocationHandler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                /**
                * proxy:代理对象
                * method:代理对象需要实现的方法,即其中需要重写的方法
                * args:method所对应方法的参数
                */                
                Object result = null;
                try {
                    System.out.println("[动态代理][日志] "+method.getName()+",参数:"+ Arrays.toString(args));
                    result = method.invoke(target, args);
                    System.out.println("[动态代理][日志] "+method.getName()+",结果:"+ result);
                } catch (Exception e) {
                    e.printStackTrace();
                    System.out.println("[动态代理][日志] "+method.getName()+",异常:"+e.getMessage());
                } finally {
                    System.out.println("[动态代理][日志] "+method.getName()+",方法执行完毕");
                }                
                return result;
            } 
        };
        return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
    }
}

3.2.4、测试

@Test
public void testDynamicProxy(){
    ProxyFactory factory = new ProxyFactory(new CalculatorLogImpl());
    Calculator proxy = (Calculator) factory.getProxy();
    proxy.div(1,0);
    //proxy.div(1,1);
}

3.2AOP概念及相关术语

3.2.1、概述

AOP(Aspect Oriented Programming)是一种设计思想,是软件设计领域中的面向切面编程,它是面向对象编程的一种补充和完善,它以通过预编译方式和运行期动态代理方式实现在不修改源代码的情况下给程序动态统一添加额外功能的一种技术。

3.2.2相关术语

①横切关注点

从每个方法中抽取出来的同一类非核心业务。在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。

这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点。

image-20230106205537309

②通知
  • 每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。
    1. 前置通知:在被代理的目标方法前执行
    2. 返回通知:在被代理的目标方法成功结束后执行(寿终正寝)
    3. 异常通知:在被代理的目标方法异常结束后执行(死于非命)
    4. 后置通知:在被代理的目标方法最终结束后执行(盖棺定论)
    5. 环绕通知:使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置

image-20230106205618362

③切面

封装通知方法的类。

image-20230106205640064

④目标

被代理的目标对象。

⑤代理

向目标对象应用通知之后创建的代理对象。

⑥连接点

这也是一个纯逻辑概念,不是语法定义的。

把方法排成一排,每一个横切位置看成x轴方向,把方法从上到下执行的顺序看成y轴,x轴和y轴的交叉点就是连接点。

image-20230106205724743

⑦切入点

定位连接点的方式。

  • 每个类的方法中都包含多个连接点,所以连接点是类中客观存在的事物(从逻辑上来说)。
  • 如果把连接点看作数据库中的记录,那么切入点就是查询记录的 SQL 语句。
  • Spring 的 AOP 技术可以通过切入点定位到特定的连接点。
  • 切点通过 org.springframework.aop.Pointcut 接口进行描述,它使用类和方法作为连接点的查询条件。

3.2.3作用

  1. 简化代码:把方法中固定位置的重复的代码抽取出来,让被抽取的方法更专注于自己的核心功能,提高内聚性。
  2. 代码增强:把特定的功能封装到切面类中,看哪里有需要,就往上套,被套用了切面逻辑的方法就被切面给增强了。

3.3基于注解的AOP

3.3.1技术说明

image-20230106205915513

  1. 动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)。
  2. cglib:通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口。
  3. AspectJ:本质上是静态代理,将代理逻辑“织入”被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver就是织入器。Spring只是借用了AspectJ中的注解。

3.3.2入门案例

①添加依赖
<!-- spring-aspects会帮我们传递过来aspectjweaver -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.3.1</version>
</dependency>
<!-- 下面是IOC的依赖,上面是AOP的依赖 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.1</version>
</dependency>
<!-- junit测试 -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
②准备被代理的目标资源
  • 接口

    • public interface Calculator {
          int add(int i, int j);
          int sub(int i, int j);
          int mul(int i, int j);
          int div(int i, int j);
      }
      
  • 实现类

    • public class CalculatorStaticFactory implements Calculator{
          @Override
          public int add(int i, int j) {
              int result = i + j;
              System.out.println("方法内部 result = " + result);
              return result;
          }
      
          @Override
          public int sub(int i, int j) {
              int result = i - j;
              System.out.println("方法内部 result = " + result);
              return result;
          }
      
          @Override
          public int mul(int i, int j) {
              int result = i * j;
              System.out.println("方法内部 result = " + result);
              return result;
          }
      
          @Override
          public int div(int i, int j) {
              int result = i / j;
              System.out.println("方法内部 result = " + result);
              return result;
          }
      }
      
③创建切面类
/*添加组件注解和添加切面注解*/
@Component
@Aspect
public class LoggerAspect {
}
④扫描并且开启AOP
  • 创建spring配置文件

    <context:component-scan base-package="com.chenchen.Spring"></context:component-scan>
    <!--开启基于注解的AOP-->
    <aop:aspectj-autoproxy></aop:aspectj-autoproxy>
    
⑤配置切面类
  • @Before("execution(public int com.chenchen.aop.annotation.CalculatorImpl.*(..))")
    public void beforeMethod(JoinPoint joinPoint){
        //获取连接点所对应方法的签名信息
        Signature signature = joinPoint.getSignature();
        //获取连接点所对应的方法名
        String mathonName = signature.getName();
        //获取连接点所对应的方法的参数
        String args = Arrays.toString(joinPoint.getArgs());
        System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
    }    
    @After("execution(* com.chenchen.aop.annotation.CalculatorImpl.*(..))")
    public void afterMethod(JoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->后置通知,方法名:"+methodName);
    }
    /*在返回通知中若要获取目标对象方法的返回值
    *只需要通过@AfterReturning注解的returning属性
    *就可以将通知方法的某个参数指定为接收目标对象方法的返回值的参数
    */
    @AfterReturning(value = "execution(* com.chenchen.aop.annotation.CalculatorImpl.*(..))", returning = "result")
    public void afterReturningMethod(JoinPoint joinPoint, Object result){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->返回通知,方法名:"+methodName+",结果:"+result);
    }   
    /*在返回通知中若要获取目标对象方法的返回值
    *只需要通过AfterThrowing注解的throwing属性
    *就可以将通知方法的某个参数指定为接收目标对象方法的异常的参数
    */
    @AfterThrowing(value = "execution(* com.chenchen.aop.annotation.CalculatorImpl.*(..))", throwing = "ex")
    public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){
        String methodName = joinPoint.getSignature().getName();
        System.out.println("Logger-->异常通知,方法名:"+methodName+",异常:"+ex);
    }
    @Around("execution(* com.chenchen.aop.annotation.CalculatorImpl.*(..))")
    public Object aroundMethod(ProceedingJoinPoint joinPoint){
        String methodName = joinPoint.getSignature().getName();
        String args = Arrays.toString(joinPoint.getArgs());
        Object result = null;
        try {
            System.out.println("环绕通知-->目标对象方法执行之前");
            //目标对象(连接点)方法的执行
            result = joinPoint.proceed();
            System.out.println("环绕通知-->目标对象方法返回值之后");
        } catch (Throwable throwable) {
            throwable.printStackTrace();
            System.out.println("环绕通知-->目标对象方法出现异常时");
        } finally {
            System.out.println("环绕通知-->目标对象方法执行完毕");
        }        
        return result;
    }
    
⑥测试
//获取IOC容器对象
ApplicationContext ioc = new ClassPathXmlApplicationContext("annotation.xml");
//获取代理类(接口)
Calculator bean = ioc.getBean(Calculator.class);
//调用方法
bean.add(2,3);

3.3.3各种通知

  1. 前置通知:使用@Before注解标识,在被代理的目标方法前执行
  2. 返回通知:使用@AfterReturning注解标识,在被代理的目标方法成功结束后执行(寿终正寝)
  3. 异常通知:使用@AfterThrowing注解标识,在被代理的目标方法异常结束后执行(死于非命)
  4. 后置通知:使用@After注解标识,在被代理的目标方法最终结束后执行(盖棺定论)
  5. 环绕通知:使用@Around注解标识,使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置
  • 各种通知的执行顺序
  • Spring版本5.3.x以前:
    1. 前置通知
    2. 目标操作
    3. 后置通知
    4. 返回通知或异常通知
  • Spring版本5.3.x以后:
    1. 前置通知
    2. 目标操作
    3. 返回通知或异常通知
    4. 后置通知

3.3.4切入点表达式语法

①作用

image-20230106221232853

②语法细节
  1. 号代替“权限修饰符”和“返回值”部分表示“权限修饰符”和“返回值”不限
  2. 在包名的部分,一个“”号只能代表包的层次结构中的一层,表示这一层是任意的。
    • 例如:.Hello匹配com.Hello,不匹配com.atguigu.Hello
  3. 在包名的部分,使用“..”表示包名任意、包的层次深度任意
  4. 在类名的部分,类名部分整体用号代替,表示类名任意
  5. 在类名的部分,可以使用号代替类名的一部分
    • 例如:*Service匹配所有名称以Service结尾的类或接口
  6. 在方法名部分,可以使用号表示方法名任意
  7. 在方法名部分,可以使用号代替方法名的一部分
    • 例如:Operation匹配所有方法名以Operation结尾的方法
  8. 在方法参数列表部分,使用(..)表示参数列表任意
  9. 在方法参数列表部分,使用(int,..)表示参数列表以一个int类型的参数开头
  10. 在方法参数列表部分,基本数据类型和对应的包装类型是不一样的
    • *切入点表达式中使用 int 和实际方法中 Integer 是不匹配的
  11. 在方法返回值部分,如果想要明确指定一个返回值类型,那么必须同时写明权限修饰符
    1. 例如:execution(public int ..Service.(.., int)) 正确
    2. 例如:execution( int ..Service.*(.., int)) 错误

image-20230106221446145

3.3.5重用切入点表达式

①声明
@Pointcut("execution(* com.chenchen.aop.annotation.*.*(..))")
public void pointCut(){}
②使用
  1. 在同一个切面中使用

    • @Before("pointCut()")
      
  2. 在不同切面中使用

    • @Before("com.chenchen.aop.LoggerAspect.pointCut()")
      

3.3.6获取通知的相关信息

①获取连接点信息
  • 获取连接点信息可以在通知方法的参数位置设置JoinPoint类型的形参

    • @Before("execution(public int com.chenchen.aop.annotation.CalculatorImpl.*(..))")
      public void beforeMethod(JoinPoint joinPoint){
          //获取连接点的签名信息
          String methodName = joinPoint.getSignature().getName();
          //获取目标方法到的实参信息
          String args = Arrays.toString(joinPoint.getArgs());
          System.out.println("Logger-->前置通知,方法名:"+methodName+",参数:"+args);
      }
      
②获取目标方法的返回值
  • @AfterReturning中的属性returning,用来将通知方法的某个形参,接收目标方法的返回值

    • @AfterReturning(value = "execution(* com.chenchen.aop.annotation.CalculatorImpl.*(..))", returning = "result")
      public void afterReturningMethod(JoinPoint joinPoint, Object result){
          String methodName = joinPoint.getSignature().getName();
          System.out.println("Logger-->返回通知,方法名:"+methodName+",结果:"+result);
      }
      
③获取目标方法的异常
  • @AfterThrowing中的属性throwing,用来将通知方法的某个形参,接收目标方法的异常

    • @AfterThrowing(value = "execution(* com.chenchen.aop.annotation.CalculatorImpl.*(..))", throwing = "ex")
      public void afterThrowingMethod(JoinPoint joinPoint, Throwable ex){
          String methodName = joinPoint.getSignature().getName(); 
          System.out.println("Logger-->异常通知,方法名:"+methodName+",异常:"+ex);
      }
      

3.3.7环绕通知

@Around("execution(* com.chenchen.aop.annotation.CalculatorImpl.*(..))")
public Object aroundMethod(ProceedingJoinPoint joinPoint){
    String methodName = joinPoint.getSignature().getName();
    String args = Arrays.toString(joinPoint.getArgs());
    Object result = null;
    try {
        System.out.println("环绕通知-->目标对象方法执行之前");
        //目标方法的执行,目标方法的返回值一定要返回给外界调用者
        result = joinPoint.proceed();
        System.out.println("环绕通知-->目标对象方法返回值之后");
    } catch (Throwable throwable) {
        throwable.printStackTrace();
        System.out.println("环绕通知-->目标对象方法出现异常时");
    } finally {
        System.out.println("环绕通知-->目标对象方法执行完毕");
    }    
    return result;
}

3.3.8切面的优先级

  1. 相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。
    1. 优先级高的切面:外面
    2. 优先级低的切面:里面
  2. 使用@Order注解可以控制切面的优先级
    1. @Order(较小的数):优先级高
    2. @Order(较大的数):优先级低

image-20230107103535084

3.4基于XML的AOP

<aop:config>
    <!--配置切面类-->
    <aop:aspect ref="loggerAspect">
        <!--  重用 -->
        <aop:pointcut id="pointCut" expression="execution(* com.chenchen.aop.xml.CalculatorImpl.*(..))"/>
        <aop:before method="beforeMethod" pointcut-ref="pointCut"></aop:before>
        <aop:after method="afterMethod" pointcut-ref="pointCut"></aop:after>
        <aop:after-returning method="afterReturningMethod" returning="result" pointcut-ref="pointCut"></aop:after-returning>
        <aop:after-throwing method="afterThrowingMethod" throwing="ex" pointcut-ref="pointCut"></aop:after-throwing>
        <aop:around method="aroundMethod" pointcut-ref="pointCut"></aop:around>
    </aop:aspect>
    <aop:aspect ref="validateAspect" order="1">
        <aop:before method="validateBeforeMethod" pointcut-ref="pointCut"></aop:before>
    </aop:aspect>
</aop:config>

4.声明式事务

4.1JdbcTemplate

4.1.1介绍

Spring 框架对 JDBC 进行封装,使用 JdbcTemplate 方便实现对数据库操作

4.1.2入门案例

①导入依赖
<!-- Spring 持久化层支持jar包 -->
<!-- Spring 在执行持久化层操作、与持久化层技术进行整合过程中,需要使用orm、jdbc、tx三个jar包 -->
<!-- 导入 orm 包就可以通过 Maven 的依赖传递性把其他两个也导入 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-orm</artifactId>
    <version>5.3.1</version>
</dependency>
<!-- Spring 测试相关 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.3.1</version>
</dependency>
<!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.1</version>
</dependency>
<!-- junit测试 -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
<!-- MySQL驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.16</version>
</dependency>
<!-- 数据源 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.0.31</version>
</dependency>
②创建jdbc.properties
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC
jdbc.username=root
jdbc.password=123456
③配置Spring的配置文件
<!--引入数据源-->
<context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
<!--配置数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="${jdbc.driver"></property>
    <property name="url" value="${jdbc.url}"></property>
    <property name="username" value="${jdbc.username}"></property>
    <property name="password" value="${jdbc.password}"></property>
</bean>
<!--配置jdbcTemplate-->
<bean class="org.springframework.jdbc.core.JdbcTemplate">
    <!--装配数据源-->
    <property name="dataSource" ref="dataSource"></property>
</bean>
④测试
//指定当前测试类在spring的测试环境中执行,此时就可以通过注入的方式直接获取IOC容器中的bean
@RunWith(SpringJUnit4ClassRunner.class)
//设置spring测试环境的配置文件
@ContextConfiguration("classpath:jdbc.xml")
public class jdbcTest {
    //自动装配
    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Test
    public void testJdbc(){
        //增删改功能
        /String sql = "insert into `ssm`.t_emp values (?,?,?)";
        jdbcTemplate.update(sql,4,"sunqi",1500);
        //查询一条数据
        String sql = "select * from `ssm`.t_emp where id = ?";
        User user = jdbcTemplate.queryForObject(sql, new BeanPropertyRowMapper<>(User.class), 1);
        System.out.println(user);
        //查询多条数据
        String sql = "select * from `ssm`.t_emp";
        List<User> list = jdbcTemplate.query(sql, new BeanPropertyRowMapper<>(User.class));
        list.forEach(System.out::println);
        //查询总条数
        String sql = "select count(*) from `ssm`.t_emp";
        System.out.println(jdbcTemplate.queryForObject(sql, Integer.class));
    }
}

4.2声明式事务概念

4.2.1编程式事务

  • 事务功能的相关操作全部通过自己编写代码来实现:

    • Connection conn = ...;
      try {
          // 开启事务:关闭事务的自动提交
          conn.setAutoCommit(false);
          // 核心操作
          // 提交事务
          conn.commit();
      }catch(Exception e)
      {        
          // 回滚事务
          conn.rollBack();
      }finally{
          // 释放数据库连接
          conn.close();
      }
      
  • 编程式的实现方式存在缺陷:

    • 细节没有被屏蔽:具体操作过程中,所有细节都需要程序员自己来完成,比较繁琐。
    • 代码复用性不高:如果没有有效抽取出来,每次实现功能都需要自己编写代码,代码就没有得到复用。

4.2.2声明式事务

  • 既然事务控制的代码有规律可循,代码的结构基本是确定的,所以框架就可以将固定模式的代码抽取出来,进行相关的封装。
  • 封装起来后,我们只需要在配置文件中进行简单的配置即可完成操作。
    • 好处1:提高开发效率
    • 好处2:消除了冗余的代码
    • 好处3:框架会综合考虑相关领域中在实际开发环境下有可能遇到的各种问题,进行了健壮性、性能等各个方面的优化
  • 总结
    • 编程式自己写代码实现功能
    • 声明式:通过配置框架实现功能

4.3基于注解的声明式事务

4.3.1测试工作准备

①加入依赖
<!-- Spring 持久化层支持jar包 -->
<!-- Spring 在执行持久化层操作、与持久化层技术进行整合过程中,需要使用orm、jdbc、tx三个jar包 -->
<!-- 导入 orm 包就可以通过 Maven 的依赖传递性把其他两个也导入 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-orm</artifactId>
    <version>5.3.1</version>
</dependency>
<!-- Spring 测试相关 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-test</artifactId>
    <version>5.3.1</version>
</dependency>
<!-- 基于Maven依赖传递性,导入spring-context依赖即可导入当前所需所有jar包 -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-context</artifactId>
    <version>5.3.1</version>
</dependency>
<!-- junit测试 -->
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
<!-- MySQL驱动 -->
<dependency>
    <groupId>mysql</groupId>
    <artifactId>mysql-connector-java</artifactId>
    <version>8.0.16</version>
</dependency>
<!-- 数据源 -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>druid</artifactId>
    <version>1.0.31</version>
</dependency>
②创建jdbc.properties
jdbc.driver=com.mysql.cj.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/ssm?serverTimezone=UTC
jdbc.username=root
jdbc.password=123456
③配置Spring的配置文件
<!--扫描组件-->
<context:component-scan base-package="com.chenchen.Spring"></context:component-scan>
<!--引入数据源-->
<context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
<!--配置数据源-->
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
    <property name="driverClassName" value="${jdbc.driver}"></property>
    <property name="url" value="${jdbc.url}"></property>
    <property name="username" value="${jdbc.username}"></property>
    <property name="password" value="${jdbc.password}"></property>
</bean>
<!--配置jdbcTemplate-->
<bean class="org.springframework.jdbc.core.JdbcTemplate">
    <!--装配数据源-->
    <property name="dataSource" ref="dataSource"></property>
</bean>
④创建表
CREATE TABLE `t_book` (
    `book_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
    `book_name` varchar(20) DEFAULT NULL COMMENT '图书名称',
    `price` int(11) DEFAULT NULL COMMENT '价格',
    `stock` int(10) unsigned DEFAULT NULL COMMENT '库存(无符号)',
    PRIMARY KEY (`book_id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
insert into `t_book`(`book_id`,`book_name`,`price`,`stock`) values (1,'斗破苍穹',80,100),(2,'斗罗大陆',50,100);
CREATE TABLE `t_user` (
    `user_id` int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
    `username` varchar(20) DEFAULT NULL COMMENT '用户名',
    `balance` int(10) unsigned DEFAULT NULL COMMENT '余额(无符号)',
    PRIMARY KEY (`user_id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
insert into `t_user`(`user_id`,`username`,`balance`) values (1,'admin',50);
⑤创建组件
  1. 创建类BookController

    • @Controller
      public class BookController {
          @Autowired
          private BookService bookService;
          public void buyBook(Integer bookId, Integer userId){
              //买书的方法
              bookService.buyBook(bookId, userId);
          }
      }
      
  2. 创建接口BookService

    • public interface BookService {
          void buyBook(Integer bookId,Integer userId);
      }
      
  3. 创建实现类BookServiceImpl

    • @Service
      public class BookServiceImpl implements BookService {
          @Autowired
          private BookDao bookDao;
          @Override
          public void buyBook(Integer bookId, Integer userId){
              //查询图书的价格
              Integer price = bookDao.getPriceByBookId(bookId);
              // 更新图书的库存
              bookDao.updateStock(bookId);
              // 更新用户的余额
              bookDao.updateBalance(userId, price);
          }
      }
      
  4. 创建接口BookDao

    • public interface BookDao {
          Integer getPriceByBookId(Integer bookId);
          void updateStock(Integer bookId);
          void updateBalance(Integer userId, Integer price);
      }
      
  5. 创建实现类BookDaoImpl

    • @Registered
      public class BookDaoImpl implements BookDao {
          @Autowired 
          private JdbcTemplate jdbcTemplate; 
          @Override 
          public Integer getPriceByBookId(Integer bookId) {
              String sql = "select price from `ssm`.t_book where book_id = ?";
              return jdbcTemplate.queryForObject(sql, Integer.class, bookId);
          } 
          @Override 
          public void updateStock(Integer bookId) {
              String sql = "update `ssm`.t_book set stock = stock - 1 where book_id = ?";
              jdbcTemplate.update(sql, bookId);
          } 
          @Override 
          public void updateBalance(Integer userId, Integer price) {
              String sql = "update `ssm`.t_user set balance = balance - ? where user_id = ?";
              jdbcTemplate.update(sql, price, userId);
          }
      }
      

4.3.2测试无事务情况

  • 场景

    1. 用户购买图书,先查询图书的价格,再更新图书的库存和用户的余额
    2. 假设用户id为1的用户,购买id为1的图书
    3. 用户余额为50,而图书价格为80
    4. 购买图书之后,用户的余额为-30,数据库中余额字段设置了无符号,因此无法将-30插入到余额字段
    5. 此时执行sql语句会抛出SQLException
    //指定当前测试类在spring的测试环境中执行,此时就可以通过注入的方式直接获取IOC容器中的bean
    @RunWith(SpringJUnit4ClassRunner.class)
    //设置spring测试环境的配置文件
    @ContextConfiguration("classpath:jdbc.xml")
    public class AnnotationTest {
        //自动装配
        @Autowired
        private BookController bookController;
        @Test
        public void testBuyBook(){
            bookController.buyBook(1,1);
        }
    }
    
  • 结果

    1. 因为没有添加事务,图书的库存更新了,但是用户的余额没有更新
    2. 显然这样的结果是错误的,购买图书是一个完整的功能,更新库存和更新余额要么都成功要么都失败

4.3.3加入事务

①添加事务配置
  • 在spring的配置文件中添加配置

    • <!--配置事务管理器-->
      <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
          <property name="dataSource" ref="dataSource"></property>
      </bean>
      <!--	开启事务的注解驱动
      		注意:导入的tx:annotation-driven需要`tx`结尾的那个。
              将使用@Transactional注解所标识的方法或类中所有的方法使用事务进行管理
              transaction-manager属性设置事务管理器的id
              若事务管理器的bean的id默认为transactionManager,则该属性可以不写-->
      <tx:annotation-driven transaction-manager="transactionManager"></tx:annotation-driven>
      
②添加事务注解
  • 因为service层表示业务逻辑层,一个方法表示一个完成的功能,因此处理事务一般在service层处理
  • BookServiceImplbuybook()添加注解@Transactionalimage-20230107160541204

4.3.4@Transactional

①注解标识的位置
  • @Transactional标识在方法上,则只会影响该方法
  • @Transactional标识的上,则会影响类中所有的方法
②事务属性:只读

对一个查询操作来说,如果我们把它设置成只读,就能够明确告诉数据库,这个操作不涉及写操作。这样数据库就能够针对查询操作来进行优化。

  • 使用方式

    • @Transactional(readOnly = true)
      
  • 对增删改操作设置只读会抛出下面异常:

    • Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modificationare not allowed
      
③事务属性:超时

事务在执行过程中,有可能因为遇到某些问题,导致程序卡住,从而长时间占用数据库资源。而长时间占用资源,大概率是因为程序运行出现了问题(可能是Java程序或MySQL数据库或网络连接等等)。

此时这个很可能出问题的程序应该被回滚,撤销它已做的操作,事务结束,把资源让出来,让其他正常程序可以执行。

概括来说就是一句话:超时回滚,释放资源。

  • 使用方式

    • @Transactional(timeout = 3)
      
    • 单位是秒,默认是-1:一直等待

  • 执行过程中抛出异常

    • org.springframework.transaction.TransactionTimedOutException: Transaction timed out:deadline was Fri Jun 04 16:25:39 CST 2022
      
④事务属性:回滚策略
  • 声明式事务默认只针对运行时异常回滚,编译时异常不回滚。
  • 可以通过@Transactional中相关属性设置回滚策略
    1. rollbackFor属性:需要设置一个Class类型的对象
    2. rollbackForClassName属性:需要设置一个字符串类型的全类名
    3. noRollbackFor属性:需要设置一个Class类型的对象
    4. rollbackFor属性:需要设置一个字符串类型的全类名
  • 使用方式

    • @Transactional(noRollbackFor = ArithmeticException.class)
      
  • 购买图书功能中出现了数学运算异常(ArithmeticException),但是我们设置的回滚策略是,当出现ArithmeticException不发生回滚,因此购买图书的操作正常执行

⑤事务属性:事务隔离级别

数据库系统必须具有隔离并发运行各个事务的能力,使它们不会相互影响,避免各种并发问题。一个事务与其他事务隔离的程度称为隔离级别。SQL标准中规定了多种事务隔离级别,不同隔离级别对应不同的干扰程度,隔离级别越高,数据一致性就越好,但并发性越弱。

  • 隔离级别一共有四种

    1. 读未提交:READ UNCOMMITTED
      • 允许Transaction01读取Transaction02未提交的修改。
    2. 读已提交:READ COMMITTED
      • 要求Transaction01只能读取Transaction02已提交的修改。
    3. 可重复读:REPEATABLE READ
      • 确保Transaction01可以多次从一个字段中读取到相同的值,即Transaction01执行期间禁止其它事务对这个字段进行更新。
    4. 串行化:SERIALIZABLE
      • 确保Transaction01可以多次从一个表中读取到相同的行,在Transaction01执行期间,禁止其它事务对这个表进行添加、更新、删除操作。
      • 可以避免任何并发问题,但性能十分低下。
  • 各个隔离级别解决并发问题的能力见下表

    • 隔离级别 脏读 不可重复读 幻读
      READ UNCOMMITTED
      READ COMMITTED
      REPEATABLE READ
      SERIALIZABLE
  • 各种数据库产品对事务隔离级别的支持程度

    • 隔离级别 Oracle MySQL
      READ UNCOMMITTED ×
      READ COMMITTED √(默认)
      REPEATABLE READ × √(默认)
      SERIALIZABLE
  • 使用方式

    • @Transactional(isolation = Isolation.DEFAULT)//使用数据库默认的隔离级别
      @Transactional(isolation = Isolation.READ_UNCOMMITTED)//读未提交
      @Transactional(isolation = Isolation.READ_COMMITTED)//读已提交
      @Transactional(isolation = Isolation.REPEATABLE_READ)//可重复读
      @Transactional(isolation = Isolation.SERIALIZABLE)//串行化
      
⑥事务属性:事务传播行为

当事务方法被另一个事务方法调用时,必须指定事务应该如何传播。

例如:方法可能继续在现有事务中运行,也可能开启一个新事务,并在自己的事务中运行。

  • 测试

    1. 创建接口CheckoutService

      • public interface CheckoutService {
            void checkout(Integer[] bookIds, Integer userId);
        }
        
    2. 创建实现类CheckoutServiceImpl

      • @Service
        public class CheckoutServiceImpl implements CheckoutService {
            @Autowired
            private BookService bookService;
            @Override
            @Transactional
            //一次购买多本图书
            public void checkout(Integer[] bookIds, Integer userId) {
                for (Integer bookId : bookIds) {
                    bookService.buyBook(bookId, userId);
                }
            }
        }
        
    3. BookController中添加方法

      • @Autowired
        private CheckoutService checkoutService;
        public void checkout(Integer[] bookIds, Integer userId){
            checkoutService.checkout(bookIds, userId);
        }
        
    4. 在数据库中将用户的余额修改为100元

  • 结果

    1. 可以通过@Transactional中的propagation属性设置事务传播行为
    2. 修改BookServiceImplbuyBook()上,注解@Transactionalpropagation属性
      1. @Transactional(propagation = Propagation.REQUIRED),默认情况
        1. 表示如果当前线程上有已经开启的事务可用,那么就在这个事务中运行。
        2. 购买图书的方法buyBook()checkout()中被调用,checkout()上有事务注解,因此在此事务中执行。
        3. 所购买的两本图书的价格为80和50,而用户的余额为100,因此在购买第二本图书时余额不足失败,导致整个checkout()回滚,即只要有一本书买不了,就都买不了
      2. @Transactional(propagation = Propagation.REQUIRES_NEW)
        1. 表示不管当前线程上是否有已经开启的事务,都要开启新事务。
        2. 同样的场景,每次购买图书都是在buyBook()的事务中执行,因此第一本图书购买成功,事务结束,第二本图书购买失败,只在第二次的buyBook()中回滚,购买第一本图书不受影响,即能买几本就买几本

4.4基于XML的声明式事务

4.4.1测试工作准备

参考上面4.3.1

4.4.2添加依赖

  • <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-aspects</artifactId>
        <version>5.3.1</version>
    </dependency>
    

4.4.3添加Spring配置文件

  • <!--扫描组件-->
    <context:component-scan base-package="com.chenchen.Spring"></context:component-scan>
    <!--引入数据源-->
    <context:property-placeholder location="classpath:jdbc.properties"></context:property-placeholder>
    <!--配置数据源-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driver}"></property>
        <property name="url" value="${jdbc.url}"></property>
        <property name="username" value="${jdbc.username}"></property>
        <property name="password" value="${jdbc.password}"></property>
    </bean>
    <!--配置jdbcTemplate-->
    <bean class="org.springframework.jdbc.core.JdbcTemplate">
        <!--装配数据源-->
        <property name="dataSource" ref="dataSource"></property>
    </bean>
    <aop:config>
        <!-- 配置事务通知和切入点表达式 -->
        <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.chenchen.spring.tx.xml.service.impl.*.*(..))"></aop:advisor>
    </aop:config>
    <!-- tx:advice标签:配置事务通知 -->
    <!-- id属性:给事务通知标签设置唯一标识,便于引用 -->
    <!-- transaction-manager属性:关联事务管理器 -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <!-- tx:method标签:配置具体的事务方法 -->
            <!-- name属性:指定方法名,可以使用星号代表多个字符 -->
            <tx:method name="get*" read-only="true"/>
            <tx:method name="query*" read-only="true"/>
            <tx:method name="find*" read-only="true"/>
            <!-- read-only属性:设置只读属性 -->
            <!-- rollback-for属性:设置回滚的异常 -->
            <!-- no-rollback-for属性:设置不回滚的异常 -->
            <!-- isolation属性:设置事务的隔离级别 --> 
            <!-- timeout属性:设置事务的超时属性 -->
            <!-- propagation属性:设置事务的传播行为 -->
            <tx:method name="save*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/> 
            <tx:method name="update*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
            <tx:method name="delete*" read-only="false" rollback-for="java.lang.Exception" propagation="REQUIRES_NEW"/>
        </tx:attributes>
    </tx:advice>
    
posted @   22-10-21  阅读(16)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
点击右上角即可分享
微信分享提示