Spring之Aop

代理模式

代理模式的两个设计原则:

  1. 代理类 与 委托类 具有相似的行为(共同)

  2. 代理类增强委托类的行为

代理的三要素

a、有共同的行为 - 接口

b、目标角色 - 实现行为

c、代理角色 - 实现行为 增强目标对象行为

静态代理

特点

1、目标角色固定

2、在应用程序执行前就得到目标角色

3、代理对象会增强目标对象的行为

4、有可能存在多个代理 引起"类爆炸"(缺点)

示例

接口

package com.yxh.service;

public interface CrudService {
    void insert();
    void delete();
    void update();
    void selectAll();
}

委托类

package com.yxh.service;

public class CrudServiceImpl implements CrudService{
    @Override
    public void insert() {
        System.out.println("增加");
    }

    @Override
    public void delete() {
        System.out.println("删除");
    }

    @Override
    public void update() {
        System.out.println("修改");

    }

    @Override
    public void selectAll() {
        System.out.println("查询所有");
    }
}

静态代理

package com.yxh.service;

public class CrudServiceImplProxy implements CrudService{
   private CrudServiceImpl csipl =  new CrudServiceImpl();
    @Override
    public void insert() {
        System.out.println("开启事务");
        csipl.insert();
        System.out.println("提交事务");
    }

    @Override
    public void delete() {
        System.out.println("开启事务");
        csipl.delete();
        System.out.println("提交事务");
    }

    @Override
    public void update() {
        System.out.println("开启事务");
        csipl.update();
        System.out.println("提交事务");
    }

    @Override
    public void selectAll() {
        csipl.selectAll();
    }
}

动态代理

相比于静态代理,动态代理在创建代理对象上更加的灵活,动态代理类的字节码在程序运行时,由Java反射机制动态产生。它会根据需要,通过反射机制在程序运行期,动态的为目标对象创建代理对象,无需程序员手动编写它的源代码。动态代理不仅简化了编程工作,而且提高了软件系统的可扩展性,因为反射机制可以生成任意类型的动态代理类。代理的行为可以代理多个方法,即满足生产需要的同时又达到代码通用的目的。

动态代理的两种实现方式:

  1. JDK 动态代理

  2. CGLIB动态代理

特点

  1. 目标对象不固定
  2. 在应用程序执行时动态创建目标对象
  3. 代理对象会增强目标对象的行为

示例:

package com.yxh.service;

import java.lang.reflect.InvocationHandler;
import java.lang.reflect.Method;
import java.lang.reflect.Proxy;

public class GetCrudServiceImplProxy {
    public static CrudService getProxyClass(){
        CrudService cs = (CrudService) Proxy.newProxyInstance(GetCrudServiceImplProxy.class.getClassLoader(), new Class[]{CrudService.class}, new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                CrudServiceImpl crudService = new CrudServiceImpl();
                String name = method.getName();
                if (name.contains("selectAll")) {
                    method.invoke(crudService);
                }else {
                    System.out.println("事务开始");
                    method.invoke(crudService);
                    System.out.println("事务提交");
                }
                return null;

            }
        });
        return cs;
    }
}

SpringAop

AOP简介和作用

  • AOP(Aspect Oriented Programming)面向切面编程,一种编程范式,指导开发者如何组织程序结构。
    • OOP(Object Oriented Programming)面向对象编程。
  • 作用:简单的说就是在不改变方法源代码的基础上对方法进行功能增强。
  • Spring理念:无入侵式/无侵入式

image-20230602165039248

  • 连接点(JoinPoint):正在执行的方法,例如:update()、delete()、select()等都是连接点。
  • 切入点(Pointcut):进行功能增强了的方法,例如:update()、delete()方法,select()方法没有被增强所以不是切入点,但是是连接点。
    • 在SpringAOP中,一个切入点可以只描述一个具体方法,也可以匹配多个方法
      • 一个具体方法:com.tyhxzy.dao包下的BookDao接口中的无形参无返回值的save方法
      • 匹配多个方法:所有的save方法,所有的get开头的方法,所有以Dao结尾的接口中的任意方法,所有带有一个参数的方法
  • 通知(Advice):在切入点前后执行的操作,也就是增强的共性功能
    • 在SpringAOP中,功能最终以方法的形式呈现
  • 通知类:通知方法所在的类叫做通知类
  • 切面(Aspect):描述通知与切入点的对应关系,也就是哪些通知方法对应哪些切入点方法。

AOP工作流程

1.启动spring的容器

2.扫描到切入点表达式

3.切入点表达式匹配到的类都会创建代理对象存储到spring的容器中

4.我们从spring容器中获取切入点表达式匹配的类的对象的时候其实拿到的都是代理对象。

(当实现类有代理类时,在调用spring自动注入时注入的是代理类对象,注入委托类会报空指针,所以需要写为接口类型指向代理对象)

AOP的真正目的

AOP真正目的是:你写代码的时候,只需考虑主流程,而不用考虑那些不重要的,但又必须要写的其它相同的代码,这些其它的相同代码所在的类就是切面类。

主要应用场景:

1.事务处理

2.日志记录

3.用户权限

AOP入门案例

配置文件实现

【第一步】导入aop相关坐标

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.tyhxzy</groupId>
    <artifactId>spring005_0602</artifactId>
    <version>1.0-SNAPSHOT</version>


    <dependencies>
        <!--junit的依赖-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <!--spring核心依赖,会将spring-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>

        <!-- spring整合junit包 -->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.2.10.RELEASE</version>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

接口

package com.yxh.service;

public interface CrudService {
    void add();
    void selectAll();
    void update();
    void delete();
}

实现类

package com.yxh.service;

public class CrudServiceImpl implements CrudService{
    @Override
    public void add() {
        System.out.println("add");
    }

    @Override
    public void selectAll() {
        System.out.println("selectAll");
    }

    @Override
    public void update() {
        System.out.println("update");
    }

    @Override
    public void delete() {
        System.out.println("delete");
    }
}

通知类

package com.yxh.util;

public class TrancationAdvice {
    public void start(){
        System.out.println("start advice---");
    }

    public void end(){
        System.out.println("end advice---");
    }
}


spring配置配置文件applicationContent.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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
                            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                            http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
">

    <bean id="cs" class="com.yxh.service.CrudServiceImpl">
    </bean>

    <bean id="ta" class="com.yxh.util.TrancationAdvice">
    </bean>

    <aop:config>
        <aop:pointcut id="ptc" expression="execution(void com.yxh.service.CrudServiceImpl.*())"/>
        <aop:aspect id="aspc" ref="ta">
            <aop:before method="start" pointcut-ref="ptc"/>
            <aop:after method="end" pointcut-ref="ptc"/>
        </aop:aspect>
    </aop:config>
</beans>

测试类

package com.yxh;

import com.yxh.service.CrudService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:applicationContent.xml")
public class AopTest {
    @Autowired
    private CrudService crudService;
    @Test
    public void testAop(){
        crudService.delete();
    }
}

AOP工作流程

  1. Spring容器启动
  2. 读取所有切面配置中的切入点
  3. 初始化bean,判定bean对应的类中的方法是否匹配到任意切入点
    • 匹配失败,创建原始对象
    • 匹配成功,创建原始对象(目标对象)的代理对象
  4. 获取bean执行方法
    • 获取的bean是原始对象时,调用方法并执行,完成操作
    • 获取的bean是代理对象时,根据代理对象的运行模式运行原始方法与增强的内容,完成操作

AOP核心概念

  • 目标对象(Target):被代理的对象,也叫原始对象,该对象中的方法没有任何功能增强。
  • 代理对象(Proxy):代理后生成的对象,由Spring帮我们创建代理对象。

AOP切入点 表达式

匹配被代理对象的某些方法

语法格式

  • 格式: 动作关键字(访问修饰符 返回值 包名.类/接口名.方法名(参数)异常名)

    例子:execution(public void com.tyhxzy.dao.impl.BookDaoImpl.update())"

    • 动作关键字:描述切入点的行为动作,例如execution表示执行到指定切入点
    • 访问修饰符:public,private等,可以省略
    • 返回值, 一般我都使用 *代表了匹配任意类型
    • 包名 , * 匹配一级目录, .. 匹配多级目录,甚至可以匹配到类
    • 类/接口名, 推荐写接口,因为写接口对所有的实现类都起作用,*任意字符,+子类或者实现类
    • 方法名, * 代表任意方法名
    • 参数 , .. 形参列表是任意的。
    • 异常名:方法定义中抛出指定异常,可以省略
  • 切入点:要进行增强的方法

  • 切入点表达式:要进行增强的方法的描述方式

    • 描述方式一:执行com.tyhxzy.dao包下的BookDao接口中的无参数update方法
    execution(void com.tyhxzy.dao.BookDao.update())
    
    • 描述方式二:执行com.tyhxzy.dao.impl包下的BookDaoImpl类中的无参数update方法
    execution(void com.tyhxzy.dao.impl.BookDaoImpl.update())
    
  • 切入点表达式标准格式:切点函数(访问修饰符 返回类型 类全名.方法名(参数类型) 异常类型)

    execution(public User com.tyhxzy.service.UserService.findById(int))
    
    • 切点函数:描述切入点的行为动作,例如execution表示执行到指定切入点
    • 访问修饰符:public,private等,可以省略
    • 返回值类型
    • 包名:多级包使用点连接
    • 类全名:包名+类名/接口名
    • 参数:直接写参数的类型,多个类型用逗号隔开
    • 异常名:方法定义中抛出指定异常,可以省略

通配符

目的:可以使用通配符描述切入点,快速描述。

目的:可以使用通配符描述切入点,快速描述。

* 单个独立的任意符号,可以独立出现,也可以作为前缀或者后缀的匹配符出现

匹配com.tyhxzy包下的任意包中的UserService类或接口中所有find开头的带有一个参数的方法

execution(public * com.tyhxzy.*.UserService.find*(*))

.. 多个连续的任意符号,可以独立出现,常用于简化包名与参数的书写

匹配com包下的任意包中的UserService类或接口中所有名称为findById的方法

execution(public User com..UserService.findById(..))

+ 专用于匹配子类类型

execution(* *..*Service+.*(..))

书写技巧

  • 所有代码按照标准规范开发,否则以下技巧全部失效
  • 描述切入点通常描述接口,而不描述实现类
  • 访问控制修饰符针对接口开发均采用public描述(可省略访问控制修饰符描述
  • 返回值类型我们一般都使用*
  • 包名书写尽量不使用..匹配,效率过低,常用*做单个包描述匹配,或精准匹配
  • 接口名/类名书写名称与模块相关的采用*匹配,例如UserService书写成*Service,绑定业务层接口名
  • 方法名书写以动词进行精准匹配,名词采用*匹配,例如getById书写成getBy*,selectAll书写成selectAll
  • 参数规则较为复杂,根据业务方法灵活调整
  • 通常不使用异常作为匹配规则

小结

  • 切入点表达式的格式?

  • executions (权限修饰符 返回值类型 包名.接口名|类名.方法名(形参列表) 异常)

  • 特殊符 * .. 号表示啥意思

  • *匹配一个包 , 可以包、类名、方法名

    • .. 匹配多级包, 形参任意类型形参

AOP通知类型

AOP通知共分为5种类型

  • 前置通知:在切入点方法执行之前执行
  • 最终通知:在切入点方法执行之后执行,无论切入点方法内部是否出现异常,最终通知都会执行。
  • 环绕通知(重点):手动调用切入点方法并对其进行增强的通知方式。
  • 后置通知(了解):在切入点方法执行之后执行,如果切入点方法内部出现异常将不会执行。
  • 异常通知(了解):在切入点方法执行之后执行,只有当切入点方法内部出现异常之后才执行。

前置通知

  • 名称:@Before
  • 类型:方法注解
  • 位置:通知方法定义上方
  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前运行
  • 范例:
@Before("pt()")
public void before() {
    System.out.println("before advice ...");
}

最终通知

  • 名称:@After
  • 类型:方法注解
  • 位置:通知方法定义上方
  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法后运行
  • 范例:
@After("pt()")
public void after() {
    System.out.println("after advice ...");
}

后置通知

  • 名称:@AfterReturning(了解)
  • 类型:方法注解
  • 位置:通知方法定义上方
  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法正常执行完毕后运行
  • 范例:
@AfterReturning("pt()")
public void afterReturning() {
    System.out.println("afterReturning advice ...");
}

异常通知

  • 名称:@AfterThrowing(了解)
  • 类型:方法注解
  • 位置:通知方法定义上方
  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法运行抛出异常后执行
  • 范例:
@AfterThrowing("pt()")
public void afterThrowing() {
    System.out.println("afterThrowing advice ...");
}

环绕通知

  • 名称:@Around(重点,常用)
  • 类型:方法注解
  • 位置:通知方法定义上方
  • 作用:设置当前通知方法与切入点之间的绑定关系,当前通知方法在原始切入点方法前后运行
  • 范例:

配置文件

接口:

package com.yxh.service;

public interface CrudService {
    void add();
    String selectAll(Integer aa);
    void update();
    void delete();
}

package com.yxh.service;

public class CrudServiceImpl implements CrudService{
    @Override
    public void add() {
        System.out.println("add");
    }

    @Override
    public String selectAll(Integer aa) {
        System.out.println("selectAll");
        return aa+"111";
    }

    @Override
    public void update() {
        System.out.println("update");
    }

    @Override
    public void delete() {
        System.out.println("delete");
    }
}

通知类:

package com.yxh.util;

import org.aspectj.lang.ProceedingJoinPoint;

public class TrancationAdvice {
    public Object aroundTest(ProceedingJoinPoint proceedingJoinPoint){
        try {
            System.out.println("前置通知");
            Object[] args = proceedingJoinPoint.getArgs();
            args[0]=666;
            Object proceed = proceedingJoinPoint.proceed(args);
            System.out.println("后置通知");
            return proceed;
        } catch (Throwable e) {
            System.out.println("异常通知");
            return null;
        }finally {
            System.out.println("最终通知");
        }
    }
}

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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
                            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                            http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
">

    <bean id="cs" class="com.yxh.service.CrudServiceImpl">
    </bean>

    <bean id="ta" class="com.yxh.util.TrancationAdvice">
    </bean>

    <aop:config>
        <aop:pointcut id="ptc" expression="execution(* com.yxh.service.CrudServiceImpl.*(..))"/>
        <aop:aspect id="aspc" ref="ta">
<!--            <aop:before method="start" pointcut-ref="ptc"/>-->
<!--            <aop:after method="end" pointcut-ref="ptc"/>-->
            <aop:around method="aroundTest" pointcut-ref="ptc"/>
        </aop:aspect>
    </aop:config>
</beans>
package com.yxh;

import com.yxh.service.CrudService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:applicationContent.xml")
public class AopTest {
    @Autowired
    private CrudService crudService;
    @Test
    public void testAop(){
        String s = crudService.selectAll(1);
        System.out.println(s);
    }
}

pom

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.yxh</groupId>
  <artifactId>SpringAopTest</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>SpringAopTest</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <!--junit的依赖-->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
    <!--spring核心依赖,会将spring-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>

    <!-- spring整合junit包 -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>5.2.10.RELEASE</version>
      <scope>test</scope>
    </dependency>
  </dependencies>
</project>

注解配置

通知类

package com.yxh.util;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Aspect//表示该类是一个切面类
@Component
public class TrancationAdvice {
    @Pointcut("execution(* com.yxh.service.CrudServiceImpl.*(..))")//切入点
    public void ad1(){
    }

    @Before("ad1()")//前置通知
    public void start(){
        System.out.println("前置通知1");
    }

    @AfterReturning("ad1()")//后置通知
    public void end(){
        System.out.println("后置通知1");
    }
    @AfterThrowing("ad1()")
    public void afterThrowing(){
        System.out.println("【异常通知】");
    }

    /* try{
        前置通知
        update()方法
                后置通知
    }catch(){
        异常通知
    }finally{
        最终通知
    }*/
    @Around("ad1()")//环绕通知
    public Object aroundTest(ProceedingJoinPoint proceedingJoinPoint){
        try {
            System.out.println("前置通知");
            Object[] args = proceedingJoinPoint.getArgs();
            args[0]=666; //修改形参的值
            Object proceed = proceedingJoinPoint.proceed(args);//目标方法执行完毕了之后,会得到返回值
            System.out.println("后置通知");
            return proceed;
        } catch (Throwable e) {
            System.out.println("异常通知");//异常通知   @AfterThrowing
            return null;
        }finally {
            System.out.println("最终通知");//最终通知   @After
        }
    }
    /*
        环绕通知方法的要求:
                1. 环绕通知的方法的返回值必须是Object。因为代表方法的返回值
                2. 环绕通知的方法必须有一个形参:ProceedingJoinPoint 代表了当前执行的方法
         环绕通知的特色:
                1. 对目标方法的返回值结果进行修改
                2. 对目标方法的参数进行修改。
                3. 拦截目标方法不给于放行。
     */
}

配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       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
                            http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
                            http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd
">
    
    <context:component-scan base-package="com.yxh.*"/>
    <aop:aspectj-autoproxy />

</beans>

委托类

package com.yxh.service;

import org.springframework.stereotype.Service;

@Service
public class CrudServiceImpl implements CrudService{
    @Override
    public void add() {
        System.out.println("add");
    }

    @Override
    public String selectAll(Integer aa) {
        System.out.println("selectAll");
        return aa+"111";
    }

    @Override
    public void update() {
        System.out.println("update");
    }

    @Override
    public void delete() {
        System.out.println("delete");
    }
}

接口

package com.yxh.service;

public interface CrudService {
    void add();
    String selectAll(Integer aa);
    void update();
    void delete();
}

纯注解

接口,实现类同上

配置类

package com.yxh.util;

import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;

@Configuration
@ComponentScan("com.yxh")
@EnableAspectJAutoProxy
public class SpringConfig {
}

测试类

package com.yxh;

import com.yxh.service.CrudService;
import com.yxh.util.SpringConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AopTest {
    @Autowired
    private CrudService crudService;
    @Test
    public void testAop(){
        String s = crudService.selectAll(1);
        System.out.println(s);
    }
}

环绕通知注意事项

  1. 环绕通知方法形参必须是ProceedingJoinPoint,表示正在执行的连接点,使用该对象的proceed()方法表示对原始对象方法进行调用,返回值为原始对象方法的返回值。
  2. 环绕通知方法的返回值建议写成Object类型,用于将原始对象方法的返回值进行返回,哪里使用代理对象就返回到哪里。

小结

  • AOP通知的类型有哪些?
  • 前置通知
    • 后置通知
    • 异常通知
    • 最终通知
    • 环绕通知
  • 环绕通知有什么要求?
    • 返回值类型是Object
    • 必须要有个形参:ProceedingJoinPoint 。 ProceedingJoinPoint代表当前执行的目标方法。

Spring事务管理

Spring事务作用

  • 事务作用:在数据层保障一系列的数据库操作同成功同失败
  • Spring事务作用:在数据层或业务层保障一系列的数据库操作同成功同失败

spring事务发生在业务逻辑层

image-20230605161852686

需求和分析

  • 需求:实现任意两个账户间转账操作
  • 需求微缩:A账户减钱,B账户加钱
  • 分析:
    ①:数据层提供基础操作,指定账户减钱(outMoney),指定账户加钱(inMoney)
    ②:业务层提供转账操作(transfer),调用减钱与加钱的操作
    ③:提供2个账号和操作金额执行转账操作
    ④:基于Spring整合MyBatis环境搭建上述操作
  • 结果分析:
    ①:程序正常执行时,账户金额A减B加,没有问题
    ②:程序出现异常后,转账失败,但是异常之前操作成功,异常之后操作失败,整体业务失败

代码实现

环境准备

-- 创建数据表,账户表
CREATE TABLE tbl_account (
	id INT PRIMARY KEY AUTO_INCREMENT,
	NAME VARCHAR(10),
	money DOUBLE  -- 金额
);

-- 添加数据
INSERT INTO tbl_account (NAME, money) VALUES ('Jack', 1000), ('Rose', 1000);

SELECT * FROM tbl_account;

-- 还原金额
update tbl_account set money=1000;

配置文件

编写AccountDao

package com.yxh.service;

import com.yxh.mapper.AccountMapper;
import lombok.Setter;
import org.springframework.stereotype.Service;

@Setter
@Service
public class AccountServiceImpl implements AccountService{
    private AccountMapper accountMapper;
    @Override
    public void Transfer(String fromName, String toName, double price) {
        accountMapper.delMoney(fromName,price);
//        System.out.println(2/0);
        accountMapper.addMoney(toName,price);
    }
}

编写AccountService

package com.yxh.service;

import org.springframework.transaction.annotation.Transactional;

public interface AccountService {
    @Transactional(rollbackFor = Exception.class)
    void Transfer(String fromName,String toName,double price);
}

编写AccountServiceImp

package com.yxh.service;

import com.yxh.mapper.AccountMapper;
import lombok.Setter;
import org.springframework.stereotype.Service;

@Setter
@Service
public class AccountServiceImpl implements AccountService{
    private AccountMapper accountMapper;
    @Override
    public void Transfer(String fromName, String toName, double price) {
        accountMapper.delMoney(fromName,price);
//        System.out.println(2/0);
        accountMapper.addMoney(toName,price);
    }
}

applicationContent.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:tx="http://www.springframework.org/schema/tx" xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">

    <context:property-placeholder location="classpath:jdbc.properties"/>
    <bean id="ds1" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="username" value="${jdbc.username}"/>
        <property name="password" value="${jdbc.password}"/>
        <property name="url" value="${jdbc.url}"/>
    </bean>

    <bean id="ssfb" class="org.mybatis.spring.SqlSessionFactoryBean">
        <property name="dataSource" ref="ds1"/>
    </bean>

    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="sqlSessionFactoryBeanName" value="ssfb"/>
        <property name="basePackage" value="com.yxh.mapper"/>
    </bean>

    <bean class="com.yxh.service.AccountServiceImpl">
        <property name="accountMapper" ref="accountMapper"/>
    </bean>

    <bean id="dstm" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="ds1"/>
    </bean>

    <tx:annotation-driven transaction-manager="dstm"/>
</beans>

jdbc.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/account
jdbc.username=root
jdbc.password=root

注意事项

  1. Spring注解式事务通常添加在业务层接口中而不会添加到业务层实现类中,降低耦合
  2. 注解式事务可以添加到业务方法上表示当前方法开启事务,也可以添加到接口上表示当前接口所有方法开启事务

设置事务管理器(将事务管理器添加到IOC容器中)

说明:可以在JdbcConfig中配置事务管理器

    <!--
    创建通知所在的类的对象 事务管理器  声明式事务   编程式事务
    -->
    <bean id="txmanager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="ds"/>
    </bean>

    <!--
    启用声明式事务的注解
    -->
    <tx:annotation-driven transaction-manager="txmanager"/>

注意事项

  1. 事务管理器要根据实现技术进行选择
  2. MyBatis框架使用的是JDBC事务

注解开发

domain-Account

package com.yxh.domain;

import lombok.Data;

@Data
public class Account {
    private Integer id;
    private String NAME;
    private Double money;
}

AccountMapper

package com.yxh.mapper;

import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;

public interface AccountMapper {
    @Update("update tbl_account set money=money-#{changeMoney} where NAME=#{fromName}")
    int delMoney(@Param("fromName") String fromName, @Param("changeMoney") double changeMoney);

    @Update("update tbl_account set money=money+#{changeMoney} where NAME=#{toName}")
    int addMoney(@Param("toName") String toName, @Param("changeMoney") double changeMoney);
}

AccountService

package com.yxh.service;

import org.springframework.transaction.annotation.Transactional;

public interface AccountService {
    @Transactional(rollbackFor = Exception.class)
    void Transfer(String fromName,String toName,double price);
}

AccountServiceImpl

package com.yxh.service;

import com.yxh.mapper.AccountMapper;
import lombok.Setter;
import org.springframework.stereotype.Service;

@Setter
@Service
public class AccountServiceImpl implements AccountService{
	@Resource
    private  AccountMapper accountMapper;

    @Override
    public void Transfer(String fromName, String toName, double price) {
        accountMapper.delMoney(fromName,price);
//        System.out.println(2/0);
        accountMapper.addMoney(toName,price);
    }
}

ConfigClazz

package com.yxh.util;

import com.alibaba.druid.pool.DruidDataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

@Configuration//当前类为配置类
@ComponentScan("com.yxh")//spring组件扫描,注解扫描
@PropertySource("classpath:jdbc.properties")//读取properties配置文件
@MapperScan("com.yxh.mapper")//扫描数据访问层mapper
@EnableTransactionManagement//开启事务
public class ConfigClazz {
    @Value("${jdbc.username}")
    private String name;
    @Value("${jdbc.password}")
    private String password;
    @Value("${jdbc.driver}")
    private String driverClassName;
    @Value("${jdbc.url}")
    private String url;

    @Bean("dataSource")//获取数据连接池对象
    public DataSource getDataSource() {
        DruidDataSource ds = new DruidDataSource();
        ds.setUsername(name);
        ds.setPassword(password);
        ds.setDriverClassName(driverClassName);
        ds.setUrl(url);
        return ds;
    }

    @Bean("ssfb")//获取SqlSessionFactoryBean对象
    public SqlSessionFactoryBean getSqlSessionFactoryBean(@Autowired @Qualifier("dataSource") DataSource dataSource){
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setTypeAliasesPackage("com.yxh.domain");
        return sqlSessionFactoryBean;
    }
    @Bean//DataSourceTransactionManager配置事务管理对象
    public DataSourceTransactionManager dataSourceTransactionManager(@Autowired @Qualifier("dataSource") DataSource dataSource){
        DataSourceTransactionManager dataSourceTransactionManager=new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource);
        return dataSourceTransactionManager;
    }
}

jdbc.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/account
jdbc.username=root
jdbc.password=root

AccountTransactionTest

package com.yxh;

import com.yxh.service.AccountService;
import com.yxh.util.ConfigClazz;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = ConfigClazz.class )
public class AccountTransactionTest {
    @Autowired
    private AccountService accountService;
    @Test
    public void testAccountTan(){
        accountService.Transfer("Jack","Rose",200);
    }
}

Spring事务角色

  • 事务管理员:发起事务方,在Spring中通常指代业务层开启事务的方法 transfer
  • 事务协调员:加入事务方,在Spring中通常指数据层方法,也可以是业务层方法

image-20230605184019464

Spring事务相关配置

事务配置

image-20230605184149322

说明:对于RuntimeException类型异常或者Error错误,Spring事务能够进行回滚操作。但是对于编译器异常,Spring事务是不进行回滚的,所以需要使用rollbackFor来设置要回滚的异常。

package com.tyhxzy.service.impl;

import com.tyhxzy.dao.AccountDao;
import com.tyhxzy.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.IOException;

@Service
public class AccountServiceImp implements AccountService {

    @Autowired
    private AccountDao accountDao;

    @Override
    @Transactional(rollbackFor = Exception.class) //@Transactional   该方法需要进行事务管理    rollbackFor = Exception.class 设置遇到哪些异常可以进行事务的回滚
    public void transfer(String out, String in, double money) throws IOException {
        accountDao.outMoney(out,money);
        //int i= 1/0;   在spring的事务中是遇到运行时异常才会回滚,遇到编译时异常默认是不会回滚的。
        if(true) {
            throw new IOException();
        }
        accountDao.inMoney(in,money);  //没有机会执行,所以编译报错
    }
}

转账业务追加日志

需求和分析

  • 需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行记录(不管转账成功或者失败日志都需要记录)
  • 需求微缩:A账户减钱,B账户加钱,数据库记录日志
  • 分析:
    ①:基于转账操作案例添加日志模块,实现数据库中记录日志
    ②:业务层转账操作(transfer),调用减钱、加钱与记录日志功能
  • 实现效果预期:
    无论转账操作是否成功,均进行转账操作的日志留痕
  • 存在的问题:
    日志的记录与转账操作隶属同一个事务,同成功同失败
  • 实现效果预期改进:
    无论转账操作是否成功,日志必须保留
  • 事务传播行为:事务协调员对事务管理员所携带事务的处理态度

image-20230605194551715

环境整备

CREATE TABLE tbl_log(
	id INT PRIMARY KEY AUTO_INCREMENT,
	info VARCHAR(255),
	create_date DATETIME
);

Account

package com.yxh.domain;

import lombok.Data;

@Data
public class Account {
    private Integer id;
    private String NAME;
    private Double money;
}

AccountMapper,LogDao

package com.yxh.mapper;

import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;

public interface AccountMapper {
    @Update("update tbl_account set money=money-#{changeMoney} where NAME=#{fromName}")
    int delMoney(@Param("fromName") String fromName, @Param("changeMoney") double changeMoney);

    @Update("update tbl_account set money=money+#{changeMoney} where NAME=#{toName}")
    int addMoney(@Param("toName") String toName, @Param("changeMoney") double changeMoney);
}

package com.yxh.mapper;

import org.apache.ibatis.annotations.Insert;

public interface LogDao {
    @Insert("insert into tbl_log values(null,#{info},now())")
    void addLog(String info);
}

AccountService

package com.yxh.service;

import org.springframework.transaction.annotation.Transactional;

public interface AccountService {
    //@Transactional   该方法需要进行事务管理    rollbackFor = Exception.class 设置遇到哪些异常可以进行事务的回滚
    @Transactional(rollbackFor = Exception.class)
    void Transfer(String fromName,String toName,double price);
}

AccountServiceImpl

package com.yxh.service;

import com.yxh.mapper.AccountMapper;
import lombok.Setter;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Setter
@Service
public class AccountServiceImpl implements AccountService{
    @Resource
    private  AccountMapper accountMapper;
    @Resource
    private LogService logService;
    @Override
    public void Transfer(String fromName, String toName, double price) {
        String isSuccess = "失败";
        try {
            accountMapper.delMoney(fromName,price);
//            System.out.println(2/0);
            accountMapper.addMoney(toName,price);
            isSuccess="成功";
        } finally {
            //添加日志
            logService.addLog(fromName,toName,price,isSuccess);
        }
    }
}

LogService

package com.yxh.service;

import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

public interface LogService {
    @Transactional(propagation = Propagation.REQUIRES_NEW)//propagation 设置事务的传播行为的,
    void addLog(String out,String in,double money ,String isSuccess);
}

LogServiceImp

package com.yxh.service;

import com.yxh.domain.Log;
import com.yxh.mapper.LogDao;
import lombok.Setter;
import org.springframework.stereotype.Service;

import javax.annotation.Resource;
import java.text.SimpleDateFormat;
import java.util.Date;


@Setter
@Service
public class LogServiceImp implements LogService{
    @Resource
    private  LogDao logDao;
    @Override
    public void addLog(String out, String in, double money, String isSuccess) {
        String info = out+"给"+in+"转账"+money+"元"+isSuccess;
        logDao.addLog(new Log(null,info,new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date().getTime())));
    }
}

ConfigClazz

package com.yxh.util;

import com.alibaba.druid.pool.DruidDataSource;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.jdbc.datasource.DataSourceTransactionManager;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;

@Configuration//当前类为配置类
@ComponentScan("com.yxh")//spring组件扫描,注解扫描
@PropertySource("classpath:jdbc.properties")//读取properties配置文件
@MapperScan("com.yxh.mapper")//扫描数据访问层mapper
@EnableTransactionManagement//开启事务
public class ConfigClazz {
    @Value("${jdbc.username}")
    private String name;
    @Value("${jdbc.password}")
    private String password;
    @Value("${jdbc.driver}")
    private String driverClassName;
    @Value("${jdbc.url}")
    private String url;

    @Bean("dataSource")//获取数据连接池对象
    public DataSource getDataSource() {
        DruidDataSource ds = new DruidDataSource();
        ds.setUsername(name);
        ds.setPassword(password);
        ds.setDriverClassName(driverClassName);
        ds.setUrl(url);
        return ds;
    }

    @Bean("ssfb")//获取SqlSessionFactoryBean对象
    public SqlSessionFactoryBean getSqlSessionFactoryBean(@Autowired @Qualifier("dataSource") DataSource dataSource){
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setTypeAliasesPackage("com.yxh.domain");
        return sqlSessionFactoryBean;
    }
    @Bean//DataSourceTransactionManager配置事务管理对象
    public DataSourceTransactionManager dataSourceTransactionManager(@Autowired @Qualifier("dataSource") DataSource dataSource){
        DataSourceTransactionManager dataSourceTransactionManager=new DataSourceTransactionManager();
        dataSourceTransactionManager.setDataSource(dataSource);
        return dataSourceTransactionManager;
    }
}

jdbc.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/account
jdbc.username=root
jdbc.password=root

AccountTransactionTest

package com.yxh;

import com.yxh.service.AccountService;
import com.yxh.util.ConfigClazz;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

@RunWith(SpringRunner.class)
@ContextConfiguration(classes = ConfigClazz.class)
public class AccountTransactionTest {
    @Autowired
    private AccountService accountService;
    @Test
    public void testAccountTan(){
        accountService.Transfer("Jack","Rose",200);
    }
}

pom.xml

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>

  <groupId>com.yxh</groupId>
  <artifactId>AccountTransaction</artifactId>
  <version>1.0-SNAPSHOT</version>
  <packaging>jar</packaging>

  <name>AccountTransaction</name>
  <url>http://maven.apache.org</url>

  <properties>
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  </properties>

  <dependencies>
    <!--spring核心依赖-->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-context</artifactId>
      <version>5.3.12</version>
    </dependency>

    <!--junit的依赖-->
    <dependency>
      <groupId>junit</groupId>
      <artifactId>junit</artifactId>
      <version>4.12</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.projectlombok</groupId>
      <artifactId>lombok</artifactId>
      <version>1.18.12</version>
      <scope>compile</scope>
    </dependency>

    <!--
       德鲁伊的依赖 他可以用来创建连接池
       -->
    <dependency>
      <groupId>com.alibaba</groupId>
      <artifactId>druid</artifactId>
      <version>1.1.16</version>
    </dependency>

    <!--mybatis 依赖-->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis</artifactId>
      <version>3.4.6</version>
    </dependency>

    <!--mysql 驱动-->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.22</version>
    </dependency>

    <!-- spring整合mybatis -->
    <dependency>
      <groupId>org.mybatis</groupId>
      <artifactId>mybatis-spring</artifactId>
      <version>1.3.0</version>
    </dependency>

    <!-- spring-jdbc -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-jdbc</artifactId>
      <version>5.2.10.RELEASE</version>
    </dependency>

    <!-- spring整合junit -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>5.2.10.RELEASE</version>
      <scope>test</scope>
    </dependency>

  </dependencies>
</project>

事务传播行为

image-20230606093524235

@Teansactional(propagation=Propagation.REQUIRED)
posted @ 2023-06-05 09:07  YxinHaaa  阅读(2)  评论(0编辑  收藏  举报