SpringAOP以及事务管理和优化处理

发布时间:2024年01月20日

Spring的AOP

本篇章中所有的代码都将会放置到git仓库中去,并且会做一个简要的说明。

一、个人理解描述

Spring中所谓的AOP就是在不修改源码的情况下,来进行增强。所谓的增强其实就是在方法执行前后添加一些额外操作。

所谓的增强,就是我们如何来对方法(以类中的方法为基本单位)处理。处理方法有五种:前置增强、后置增强等等

但是最为常用的还是利用环绕通知来进行增强,习惯于手动控制,更加精细化操作。

简单利用画图说明一下上面的描述:

在这里插入图片描述

以这里的CourseController和UserController为例,希望在CourseService和UserService类中的每个都开始事务操作,而且还不在修改CourseService和UserService类中源码的情况下来进行操作,不破坏原来的代码的完整性。

那么首先来写个简单的Demo来体验一下SpringAOP的强大之处。

二、案例演示

  • 希望只针对UserServiceImpl类中的save无参方法来做日志打印处理。

因为工作中需要,需要利用到注解+xml的方法,所以下面没有提供纯注解的方式来进行操作。

在UserService接口中提供了三个方法来模拟实际开发过程中接口中的方法:

public interface UserService {
    void save();
    void save(String name);
    void save(String name, Integer age);
}

UserServiceImpl是对接口UserService的实现:

@Service("userService")
public class UserServiceImpl implements UserService {
    @Override
    public void save() {
        System.out.println("UserService.save()");
        try {
            int randomMs = (int) (Math.random()*1000);
            Thread.sleep(randomMs);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }

    @Override
    public void save(String name) {
        System.out.println("UserService.save(String)");
    }

    @Override
    public void save(String name, Integer age) {
        System.out.println("UserService.save(String, Integer)");
    }
}

上面两步操作中是最为简单的!下面就要来为我们的save无参方法来进行增强。

所谓的增强,就是调用save无参方法的时候,打印我们的日志。

那么来写一个增强类即可:

@Component
public class MyAdvice {

    /**
     * 环绕通知方法。调用者调用时,Spring会执行这个环绕通知方法
     * @param pjp 由Spring传递进来的切入点对象(目标方法、目标对象、方法实参等等封装成的对象)
     * @return
     */
    public Object aroundMethod(ProceedingJoinPoint pjp){
        Object result = null;
        try {
            System.out.println("调用业务方法之前,我希望看下是否已经走了动态代理");
            long start = System.currentTimeMillis();
            //自己调用目标对象,得到返回值
            // 固定写法:调用业务方法-----所以上面的逻辑可以称之为在调用方法之前操作
            result = pjp.proceed(pjp.getArgs());
            // 下面的逻辑可以表示成调用业务逻辑方法之后的操作
            System.out.println("调用业务方法之后,我希望看下业务方法执行之后的结果");
            long end = System.currentTimeMillis();
            System.out.println("当前业务方法调用过程中花费的是时间有:" + (end - start) + "毫秒");
        } catch (Throwable throwable) {
            System.out.println("当前方法抛出异常,对应的异常是:"+throwable);
        } finally {
            System.out.println("无论业务方法执行过程中出现了怎样的问题,那么这行代码最终都要来执行");
        }
        // 返回最终结果
        return result;
    }
}

因为利用到的是注解+xml的方式,所以再写一个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:aop="http://www.springframework.org/schema/aop"
       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/aop http://www.springframework.org/schema/aop/spring-aop.xsd http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
	<!--扫描组件-->
    <context:component-scan base-package="com.guang.service.impl"/>
    <context:component-scan base-package="com.guang.advice"/>
    
    <!--一切AOP的配置,都要放在 aop:config标签里-->
    <aop:config>
        <aop:aspect ref="myAdvice">
            <aop:around method="aroundMethod" pointcut="execution(public void com.guang.service..UserServiceImpl.save())"/>
        </aop:aspect>
    </aop:config>
</beans>

那么来写一段代码测试:

public class XmlAopTest {
    @Test
    public void test(){
        ApplicationContext app = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");

        UserService userService = app.getBean("userService", UserService.class);
        userService.save();
        System.out.println("---------------------");
        userService.save("tom");
        System.out.println("---------------------");
        userService.save("tom", 20);
    }
}

打印结果如下所示:

调用业务方法之前,我希望看下是否已经走了动态代理
UserService.save()
调用业务方法之后,我希望看下业务方法执行之后的结果
当前业务方法调用过程中花费的是时间有:838毫秒
无论业务方法执行过程中出现了怎样的问题,那么这行代码最终都要来执行
---------------------
UserService.save(String)
---------------------
UserService.save(String, Integer)

从这里可以看到,尽管利用userService调用了三个无参方法,但是只有save无参方法进行了增强。那么到底是如何做到的呢?

下面我们先介绍概念,然后从概念来进行入手,着手分析,然后再进行分析。

三、AOP相关的概念

3.1、AOP相关概念
  • 目标对象(Target):要代理的/要增强的目标对象。

  • 代理对象(Proxy):目标对象被AOP织入增强后,就得到一个代理对象

  • 连接点(JoinPoint):能够被拦截到的点,在Spring里指的是方法

    目标类里,所有能够进行增强的方法,都是连接点

  • 切入点(PointCut):要对哪些连接点进行拦截的定义

    已经增强的连接点,叫切入点

  • 通知/增强(Advice):拦截到连接点之后要做的事情

    对目标对象的方法,进行功能增强的代码

  • 切面(Aspect):是切入点和通知的结合

  • 织入(Weaving):把增强/通知 应用到 目标对象来创建代理对象的过程。Spring采用动态代理技术织入,而AspectJ采用编译期织入和装载期织入

那么画图来描述一下,我觉得更为稳妥,如下所示:

在这里插入图片描述

以例子来进行说明:

  • 目标对象:就是courseservice所表示的单例对象;

  • 代理对象:就是需要对courseservice对应的单例对象来进行代理的对象;

  • 所谓的连接点:以上面的courseservice来举例,可以认为是courseservice类中的所有方法;

  • 所谓的切入点:就是我们需要筛选courseservice类中的个别方法来作为特殊的点(如何筛选,那么就需要我们手写point表达式来进行选择);

  • 增强:所谓的增强就是要对原来的方法来做何种操作;

  • 切面:切入点+增强;

3.2、AOP开发前要明确的事项
我们要做的事情:
  • 编写核心业务代码(Target目标类的目标方法)
  • 编写通知类,通知类中有通知方法(Advice增强功能方法)
  • 在配置文件中,配置织入关系,即将哪些通知与哪些切入点 结合,形成切面
Spring的AOP做的事情:
  • 生成动态代理的过程(把通知织入到切入点的过程),是由Spring来实现的
  • Spring会监控切入点方法的执行,一旦发现切入点方法执行,使用代理机制动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。
小结
  • AOP相关的概念/术语
    • 目标类Target:要对哪个类进行增强
    • 代理对象Proxy:对目标类增强后的那个代理对象
    • 连接点JoinPoint:目标类里可增强的方法
    • 切入点PointCut:要增强的方法
    • 通知Advice:功能增强的代码
    • 切面Aspect:切入点 + 通知
    • 织入Weaving:把切入点 和 通知 进行结合,生成代理对象的过程
  • 使用AOP,我们要做的事情:
    • 编写目标类,自己的业务代码
    • 编写通知类
    • 配置切面
  • 使用AOP,Spring做的事情
    • 根据我们配置的切面,进行织入生成代理对象

四、基于XML的AOP

快速入门
1) 需求描述
  • 有目标类UserServiceImpl,有通知类MyAdvice
  • 使用XML方式AOP,对目标类UserServiceImpl的方法进行增强
2) 步骤分析
  1. 创建maven项目,导入AOP相关的依赖坐标
  2. 创建目标类(要增强的类,内部有切入点),创建通知类(内部有增强的方法代码)
  3. 修改配置文件:
    1. 把目标类和通知类都配置成为bean对象
    2. 配置切入点和通知方法(增强方法)的织入关系:配置切面
  4. 测试代码
3) 入门实现
1. 创建maven项目,导入坐标
<dependencies>
    <!--Spring上下文核心包-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    <!--AOP的实现包-->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.8.9</version>
    </dependency>
    <!--Spring和单元测试集成-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    <!--单元测试-->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>
2. 创建目标类和通知类
  • 目标类:com.guang.aop.UserServiceImpl
public class UserService{
    void save();
}
public class UserServiceImpl {
    public void save(){
        System.out.println("UserServiceImpl.save......");
    }
}
  • 通知类:com.guang.aop.MyAdvice
public class MyAdvice {
    public void before(){
        System.out.println("前置通知...");
    }
}
3. 修改配置文件
  1. 把目标类和通知类都配置到Spring配置文件中
  2. 配置切入和通知方法(增强方法)的织入关系
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       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/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--配置要增强的目标对象-->
    <bean id="userService" class="com.guang.service.impl.UserServiceImpl"/>
    <!--配置切面对象-->
    <bean id="myAdvice" class="com.guang.aop.MyAdvice"/>
    <!--配置AOP-->
    <aop:config>
        <!--配置切面。切面:通知方法+切入点-->
        <aop:aspect ref="myAdvice">
            <!--配置通知-->
            <aop:before method="before" pointcut="execution(void com.guang.service.impl.UserServiceImpl.save())"/>
        </aop:aspect>
    </aop:config>
</beans>

注意:在xml中增加了aop的名称空间如下:

在这里插入图片描述

4. 测试代码
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class AopTest {
    @Autowired
    private UserService userService;

    @Test
    public void testQuickStart(){
        userService.save();
    }
}
4) 步骤小结
  1. 导入jar包:spring-context, aspectjweaver
  2. 编写目标类、编写通知类
  3. 配置切面
<aop:config>
	<aop:aspect ref="通知对象">
        <aop:before method="通知对象里的通知方法" pointcut="切入点表达式"/>
    </aop:aspect>
</aop:config>
AOP详解
1) 切点表达式的写法
语法:
execution([权限修饰符] 返回值类型 包名.类名.方法名(参数列表))
  • 修饰符:可以省略
  • 返回值类型:
    • 可以指定类型。比如String (如果类型有歧义,就写全限定类名,比如:java.util.Date
    • *,表示任意字符。比如Str*,或者*
  • 包名:
    • 可以写.:表示当前包下的类或者子包。比如com.guang.service
    • 可以写..:表示当前包里所有后代类、后代包。比如com..service
    • *:表示任意字符。比如:com.gua*, com.*
  • 类名:
    • 可以指定类名。比如:UserServiceImpl
    • * 表示任意字符。比如:*ServiceImpl*
  • 方法名:
    • 可以指定方法名
    • * 表示任意字符。比如:save**
  • 参数列表:
    • 可以指定类型。比如:String,Integer表示第一个参数是String,第二个参数是Integer类型
    • *表示任意字符。比如:
      • String, * 表示第一个参数是String,第二个参数是任意类型
      • Str*, Integer表示第一个参数类型Str开头,第二个参数是Integer类型
    • 可以使用..表示任意个数、任意类型的参数
示例
execution(public void com.guang.dao.impl.UserDao.save())
execution(void com.guang.dao.impl.UserDao.*(..))
execution(* com.guang.dao.impl.*.*(..))
execution(* com.guang.dao..*.*(..))
execution(* *..*.*(..)) --不建议使用
2) 通知的种类
通知的语法
<aop:通知类型 method="通知中的方法" pointcut="切点表达式"></aop:通知类型>
通知的类型
名称标签说明
前置通知<aop:before>通知方法在切入点方法之前执行
后置通知<aop:after-returning>在切入点方法正常执行之后,执行通知方法
异常通知<aop:after-throwing>在切入点方法抛出异常时,执行通知方法
最终通知<aop:after>无论切入点方法是否有异常,最终都执行通知方法
环绕通知<aop:around>通知方法在切入点方法之前、之后都执行
通知示例

注意:通知方法的名称随意,我们这里是为了方便理解,才起名称为:before, after等等

  • 前置通知

    • 通知方法定义MyAdvicebefore方法:
    public void before(){
        System.out.println("前置通知");
    }
    
    • xml配置
    <aop:before method="before" 
                pointcut="execution(void com.guang.service..*.save())"/>
    
  • 后置通知

    • 通知方法定义
    public void afterReturning(){
        System.out.println("后置通知");
    }
    
    • xml配置
    <aop:after-returning method="afterReturning" 
                         pointcut="execution(void com.guang.service..*.save())"/>
    
  • 环绕通知

    • 通知方法定义
    /**
     * @param pjp ProceedingJoinPoint:正在执行的切入点方法对象
     * @return 切入点方法的返回值
     */
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("环绕:前置通知...");
    
        Object[] args = pjp.getArgs();
        //切入点方法执行
        Object proceed = pjp.proceed(args);
    
        System.out.println("环绕:后置通知...");
    
        return proceed;
    }
    
    • xml配置
    <aop:around method="around" 
                pointcut="execution(void com.guang.service..*.save())"/>
    
  • 异常抛出通知

    • 通知方法定义
    public void afterThrowing(){
        System.out.println("抛出异常通知");
    }
    
    • xml配置
    <aop:after-throwing method="afterThrowing" 
                        pointcut="execution(void com.guang.service..*.save())"/>
    
  • 最终通知

    • 通知方法定义
    public void after(){
        System.out.println("最终通知");
    }
    
    • xml配置
    <aop:after method="after" 
               pointcut="execution(void com.guang.service..*.save())/>
    
3) 切点表达式的抽取
  • 当多个切面的切入点表达式相同时,可以将切入点表达式进行抽取;在增强中使用pointcut-ref代替pointcut,来引入切入点表达式。

  • 示例:

    <!--配置AOP-->
    <aop:config>
        <!--配置切入点表达式-->
        <aop:pointcut id="myPointCut" 
                      expression="execution(void com.guang.service..*.save())"/>
        <!--配置切面-->
        <aop:aspect ref="myAdvice">
            <!--前置通知-->
            <aop:before method="before" pointcut-ref="myPointCut"/>
            <!--后置通知-->
            <aop:after-returning method="afterReturning" pointcut-ref="myPointCut"/>
            <!--异常通知-->
            <aop:after-throwing method="afterThrowing" pointcut-ref="myPointCut"/>
            <!--最终通知-->
            <aop:after method="after" pointcut-ref="myPointCut"/>
        </aop:aspect>
    </aop:config>
    
4) 小结
  • 需要我们编写的内容:
    • 编写目标类,编写通知类
    • 配置切面
<aop:config>
	<aop:pointcut id="xxx" expression="切入点表达式"/>
    <aop:aspect ref="通知对象">
        <aop:before method="通知对象里的通知方法" pointcut-ref="xxx"/>
        <aop:after-returning method="通知对象里的通知方法" pointcut-ref="xxx"/>
        <aop:after-throwing method="通知对象里的通知方法" pointcut-ref="xxx"/>
        <aop:after method="通知对象里的通知方法" pointcut-ref="xxx"/>
        
        <aop:around method="通知对象里的通知方法" pointcut-ref="xxx"/>
    </aop:aspect>
</aop:config>
  • 注意环绕通知的方法
public Object aroundMethod(ProceedingJoinPoint pjp){
	Object reuslt = null;
    
    try{
        //写前置通知代码
        
        //调用目标对象的方法
    	result = pjp.proceed(pjp.getArgs());
        
        //写后置通知代码
    }catch(Throwable t){
        //写异常通知代码
    }finally{
        //写最终通知代码
    }
}

五、基于注解的AOP

快速入门
1) 需求描述
  • 有目标类UserServiceImpl,有通知类MyAdvice
  • 使用注解方式的AOP对目标类UserServiceImpl的方法进行增强
2) 步骤分析
  1. 创建maven项目,导入AOP需要的依赖坐标
  2. 创建目标类,创建通知类
    1. 使用注解@Component标注两个类,配置成为bean对象
    2. 在通知类中,使用注解配置织入关系
  3. 在配置文件中,开启组件扫描和AOP的自动代理(自动装配)
  4. 测试
3) 入门实现
1. 创建maven项目,导入坐标
  • 注意:需要增加AOP的实现包:aspectjweaver
<dependencies>
    <!--Spring上下文核心包-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    <!--AOP的实现包-->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.8.9</version>
    </dependency>
    <!--Spring和单元测试集成-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.0.2.RELEASE</version>
    </dependency>
    <!--单元测试-->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
    </dependency>
</dependencies>
2. 创建目标类,创建通知类
  1. 使用注解标注两个类,配置成为bean对象

    • 实际开发中,使用@Repository, @Service, @Controller注解,按照分层进行配置
  2. 在通知类中,使用注解配置织入关系

    • 目标类com.guang.aop.Target
    public class UserService{
        void save();
    }
    
    @Service("userService")
    public class UserServiceImpl {
        public void save(){
            System.out.println("UserServiceImpl.save......");
        }
    }
    
    • 通知类com.guang.aop.MyAdvice
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.*;
    import org.springframework.stereotype.Component;
    
    //声明当前类是切面类:把切入点和通知,在这个类里进行织入,当前类就成为了一个切面类
    @Aspect 
    @Component("myAdvice")
    public class MyAdvice {
    
        @Before("execution(void com.guang.impl..*.save())")
        public void before(){
            System.out.println("前置通知...");
        }
    
        @AfterReturning("execution(void com.guang.impl..*.save()))")
        public void afterReturning(){
            System.out.println("后置通知");
        }
    
        @After("execution(void com.guang.impl..*.save())")
        public void after(){
            System.out.println("最终通知");
        }
    
        @AfterThrowing("execution(void com.guang.impl..*.save())")
        public void afterThrowing(){
            System.out.println("抛出异常通知");
        }
    
        /**
         * @param pjp ProceedingJoinPoint:正在执行的切入点方法对象
         * @return 切入点方法的返回值
         */
        @Around("execution(void com.guang.impl..*.save())")
        public Object around(ProceedingJoinPoint pjp) throws Throwable {
            System.out.println("环绕:前置通知...");
    
            //切入点方法执行
            Object proceed = pjp.proceed();
    
            System.out.println("环绕:后置通知...");
    
            return proceed;
        }
    }
    
4. 开启组件扫描和AOP自动代理
  • applicationContext.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">

    <!--开启组件扫描-->
    <context:component-scan base-package="com.guang"/>

    <!--开启AOP自动代理-->
    <aop:aspectj-autoproxy/>
</beans>

如果要使用纯注解开发,可以使用配置类代替applicationContext.xml,配置类如下:

@Configuration //标记当前类是:配置类
@ComponentScan(basePackage="com.guang") //配置注解扫描
@EnableAspectJAutoProxy //开启AOP自动代理
public class AppConfig{   
}
5. 测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class AopTest {

    @Autowired
    private UserService userService;

    @Test
    public void testQuickStart(){
        userService.save()
    }
}
4) 步骤小结
AOP详解
1) 通知的种类
通知的语法
@通知注解("切入点表达式")
通知的类型
名称注解说明
前置通知@Before通知方法在切入点方法之前执行
后置通知@AfterRuturning通知方法在切入点方法之后执行
异常通知@AfterThrowing通知方法在抛出异常时执行
最终通知@After通知方法无论是否有异常,最终都执行
环绕通知@Around通知方法在切入点方法之前、之后都执行
  • 注意:
    • 注解方式配置的通知,执行顺序是:前置->最终->后置/异常
    • 如果想要指定执行的顺序,就使用环绕通知
2) 切点表达式的抽取
  • 同xml的AOP一样,当多个切面的切入点表达式相同时,可以将切入点表达式进行抽取;
  • 抽取方法是:
    • 在增强类(切面类,即被@Aspect标的类)上增加方法,在方法上使用@Pointcut注解定义切入点表达式,
    • 在增强注解中引用切入点表达式所在的方法
  • 示例:
@Aspect
@Component("myAdvice1")
public class MyAdvice1 {

    //定义切入点表达式
    @Pointcut("execution(void com.guang.service..*.save())")
    public void myPointcut(){}

    //引用切入点表达式
    //完整写法:com.guang.aop.MyAdvice.myPointcut()
    //简单写法:myPointcut(), 引入当前类里定义的表达式,可以省略包类和类名不写
    @Before("myPointcut()")
    public void before(){
        System.out.println("前置通知...");
    }
   
    @AfterReturning("myPointcut()")
    public void afterReturning(){
        System.out.println("后置通知");
    }

    @After("myPointcut()")
    public void after(){
        System.out.println("最终通知");
    }

    @AfterThrowing("myPointcut()")
    public void afterThrowing(){
        System.out.println("抛出异常通知");
    }

    /*@Around("myPointcut()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        System.out.println("前置通知...");

        //切入点方法执行
        Object proceed = pjp.proceed();

        System.out.println("后置通知...");

        return proceed;
    }*/
}
3) 小结
  1. 在通知类上加注解@Aspect,声明成一个切面
  2. 在通知类里方法上加注解@Before/@AfterReturning/@AfterThrowing/@After/@Around,配置切入点表达式
  3. 在xml里开启aop的自动代理:<aop:aspectj-autoproxy/>

六、纯注解的AOP

  • 主要是把XML的配置,放到核心配置类上
@Configuration
@ComponentScan(basePackages="com.guang")//开启组件扫描
@EnableAspectJAutoProxy //开启AOP的自动代理
public class AppConfig{
    
}
@Aspect
@Component("myAdvice2")
public class MyAdvice2 {

    //定义切入点表达式
    @Pointcut("execution(void com.guang.service..*.save())")
    public void myPointcut(){}

    @Before("myPointcut()")
    public void before(){
        System.out.println("前置通知...");
    }
   
    @AfterReturning("myPointcut()")
    public void afterReturning(){
        System.out.println("后置通知");
    }

    @After("myPointcut()")
    public void after(){
        System.out.println("最终通知");
    }

    @AfterThrowing("myPointcut()")
    public void afterThrowing(){
        System.out.println("异常通知");
    }
}

七、Spring事务管理

在Spring事务管理中,我推荐使用的是编程式事务,而不是声明式事务。

所谓的编程式事务就是我们手动来控制事务,而声明式事务则是有Spring来帮助我们来实现的。

而事务又是我们日常操作过程中最为常用且常用的,我们不知道Spring事务如何给我们操作的,所以建议不要将事务由Spring来进行处理,而是我们手动的来进行管理!!!

编程式事务管理
  • 所谓事务管理,即:按照给定的事务规则,来执行提交或回滚操作。其中:
    • 给定的事务规则:用TransactionDefinition表示
    • 按照…来执行提交或回滚操作:用PlatformTransactionManager来完成
    • TransactionStatus用于表示一个运行着的事务的状态

对于PlatformTransactionManager和TransactionStatus来说,是固定的套路,但是我们最需要关注的是这里的事务规则,也就是TransactionDefinition,在下面会重点关注和介绍。

关于编程式事务的说明以及API介绍
  • 编程式事务管理:通过编写代码的方式实现事务管理

    • 编程式事务管理,因事务管理与业务功能耦合性太强,不方便维护,目前已经基本不用,但是与SpringAOP结合起来使用更佳。

      spring 2.0 就已经提供了 xml配置的声明式事务管理的支持

  • 以下API仅做介绍了解,用于了解Spring事务相关的API,并回顾事务相关的概念

PlatformTransactionManager
  • 是Spring提供的事务管理器接口,它提供了我们常用的操作事务的方法:开启事务、提交事务等
  • 注意:PlatformTransactionManager是接口类型,不同的dao层技术有不同的实现,例如:
    • dao层是jdbcTemplate或Mybatis时,实现类是:DataSourceTransactionManager。而我们最常用的就是mybatis,所以肯定使用的是DataSourceTransactionManager。
    • dao层是Hibernate时,实现类是:HibernateTransactionManager
方法返回值说明
getTransaction(TransactionDefinition td)TransactionStatus开启事务,并得到事务状态
commit(TransactionStatus status)提交事务
rollback(TransactionStatus status)回滚事务
TransactionDefinition
  • 事务的定义信息对象,提供了以下常用方法:
方法参数返回值说明
getIsolationLevel()int获取事务的隔离级别
getPropogationBehavior()int获取事务的传播行为
getTimeout()int获取超时时间
isReadOnly()boolean是否只读的事务
事务的隔离级别:
  • ISOLATION_DEFAULT:默认事务隔离级别
    • MySql默认隔离级别:repeatable read
    • Oracle默认隔离级别:read committed
  • ISOLATION_READ_UNCOMMITTED:读未提交–存在脏读、不可重复读、幻读
  • ISOLATION_READ_COMMITTED:读已提交–存在不可重复读、幻读
  • ISOLATION_REPEATABLE_READ:重复读–存在幻读
  • ISOLATION_SERIALIZABLE:串行化–没有并发问题
事务的传播行为:

用于解决业务方法调用业务方法时,事务的统一性问题的

在这里插入图片描述

也就是说service中的methodA方法调用methodB方法的时候,对于methodB来说,叫做传播行为,决定使用怎么样的事务来调用methodB方法。

以下三个,是要当前事务的

  • PROPAGATION_REQUIRED需要有事务。默认
    • 如果有事务,就使用这个事务
    • 如果没有事务,就创建事务。
  • PROPAGATION_SUPPORTS:支持事务
    • 如果有事务,就使用当前事务,
    • 如果没有事务,就以非事务方式执行(没有事务)
  • PROPAGATION_MANDATORY:强制的
    • 如果有事务,就使用当前事务
    • 如果没有事务,就抛异常

以下三个,是不要当前事务的

  • PROPAGATION_REQUIRES_NEW:新建的
    • 如果有事务,就把事务挂起,再新建事务
    • 如果没有事务,新建事务
  • PROPAGATION_NOT_SUPPORTED:不支持的
    • 如果有事务,就把事务挂起,以非事务方式执行
    • 如果没有事务,就以非事务方式执行
  • PROPAGATION_NEVER:非事务的
    • 如果有事务,就抛异常
    • 如果没有事务,就以非事务方式执行

最后一个,是特殊的

  • PROPAGATION_NESTED:嵌套的
    • 如果有事务,就在事务里再嵌套一个事务执行
    • 如果没有事务,就是类似REQUIRED的操作
事务运行的超时时间:

超时后事务自动回滚

  • 默认值-1,表示没有超时限制
  • 如果有,可以以秒为单位进行设置
是否只读:
  • 如果设置为只读,那么方法只能查询,不能增删改
  • 通常是查询方法设置为只读
TransactionStatus
  • 提供了查询事务具体运行状态的方法,常用方法如下:
方法返回值说明
hasSavePoint()boolean事务是否有回滚点
isCompleted()boolean事务是否已经完成
isNewTransaction()boolean是否是新事务
isRollbackOnly()boolean事务是否是要回滚的状态
小结
  • PlatformTransactionManager接口:
    • 如果dao层用的是Mybatis、JdbcTemplate:用DataSourceTransactionManager
    • 如果dao层用的是Hibernate:用HibernateTransactionManager
  • 事务定义信息:
    • 事务的隔离级别:通常使用默认ISOLATION_DEFAULT
    • 事务的传播行为:通常使用默认PROPAGATION_REQUIRED
    • 事务的超时时间:如果事务执行超时,会回滚。单位是秒。值为-1表示永不超时
    • 事务是否是只读:如果只读,事务里只能执行查询操作,不能增删改
  • 事务状态接口
    • 事务是否有回滚点
    • 事务是否已经完成
    • 是否是新事务
    • 事务是否是要回滚的状态
示例代码一

下面也是来给出例子来进行说明展示:

给出实体类:

public class Account {
    private Integer id;
    private String name;
    private Float money;
    // 省略get/set方法
}

给出业务逻辑代码:

public interface AccountService {
    void transfer(String from, String to, Float money);
}

具体实现类中的代码:

@Service("accountService")
public class AccountServiceImpl implements AccountService {

    // 引入事务操作
    @Autowired
    private AccountDao accountDao;
    
    @Autowired
    private TransactionDefinition txDefinition;
    
    @Autowired
    private PlatformTransactionManager txManager;

    @Override
    public void transfer(String from, String to, Float money) {
        //开启事务,得到事务状态
        TransactionStatus txStatus = txManager.getTransaction(txDefinition);
        try {
            //操作dao
            Account fromAccount = accountDao.findByName(from);
            Account toAccount = accountDao.findByName(to);
            fromAccount.setMoney(fromAccount.getMoney() - money);
            toAccount.setMoney(toAccount.getMoney() + money);
            accountDao.edit(fromAccount);
            accountDao.edit(toAccount);
            //提交事务
            txManager.commit(txStatus);
        } catch (Exception e) {
            //回滚事务
            txManager.rollback(txStatus);
            // 出现异常,进行补偿机制
        }
    }
}

那么给出dao代码:

public interface AccountDao {
    void edit(Account account) throws SQLException;
    Account findByName(String name) throws SQLException;
}

实现类代码:

package com.guang.dao.impl;

import com.guang.dao.AccountDao;
import com.guang.domain.Account;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

import java.sql.SQLException;
import java.util.List;

@Repository("accountDao")
public class AccountDaoImpl implements AccountDao {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    @Override
    public void edit(Account account) throws SQLException {
        jdbcTemplate.update("update account set name=?, money=? where id=?", account.getName(), account.getMoney(), account.getId());
    }

    @Override
    public Account findByName(String name) throws SQLException {
        List<Account> accounts = jdbcTemplate.query("select * from account where name = ?", new BeanPropertyRowMapper<>(Account.class), name);
        if (accounts == null || accounts.size() == 0) {
            return null;
        }else{
            return accounts.get(0);
        }
    }
}

然后给出配置文件代码

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

    <context:component-scan base-package="com.guang"/>

    <!--事务的定义-->
    <bean id="txDefinition" class="org.springframework.transaction.support.DefaultTransactionDefinition">
        <!--事务传播特性-->
        <property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"/>
        <!--事务隔离级别-->
        <property name="isolationLevelName" value="ISOLATION_DEFAULT"/>
        <!--事务超时时间-->
        <property name="timeout" value="-1"/>
        <!--事务是否只读-->
        <property name="readOnly" value="false"/>
    </bean>
    <!--定义事务管理器-->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!--定义连接池-->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/spring"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
    </bean>
    <!--定义JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <constructor-arg name="dataSource" ref="dataSource"/>
    </bean>
</beans>

从上面来看业务逻辑操作的话,可以看到代码侵入性太高!很明显不适合来使用。

@Service("accountService")
public class AccountServiceImpl implements AccountService {
    
    @Autowired
    private TransactionDefinition txDefinition;
    @Autowired
    private PlatformTransactionManager txManager;

    @Override
    public void transfer(String from, String to, Float money) {
        //开启事务,得到事务状态
        TransactionStatus txStatus = txManager.getTransaction(txDefinition);
        try {
            // ....
            //提交事务
            txManager.commit(txStatus);
        } catch (Exception e) {
            //回滚事务
            txManager.rollback(txStatus);
            e.printStackTrace();
        }
    }
}

但是从这里来看的话,无非就是多用了PlatformTransactionManager和TransactionDefinition而已。

  • 那么我的想法是将上面的代码抽取起来写成一个通知类型,然后配置PointCut和Advice组成Advisor。然而在我即将要动手去写的时候,在查看DefaultTransactionDefinition类的时候,我发现有继承类TransactionTemplate,我一看这玩意儿不是我们在项目中经常使用的一个类吗?以前也写过,也遇到过,但是在这里遇到的时候,我才恍然大悟。原来我们一直就是这么用的,但是从来没有去想过背后的原理,所以这也是写这篇博客的收获之一!非常幸运。

那么首先来分析一下我原来的思路(即按照切面类的想法)

  • 1、针对读写类型来定义事务规则,这里涉及到四个属性,在DefaultTransactionDefinition中存在:

    • propagationBehavior,默认值是PROPAGATION_REQUIRED,即表示有事务就使用这个事务,没有事务就不用;
    • isolationLevel,默认值是:ISOLATION_DEFAULT,即表示默认使用数据库的事务。如果使用的是MySQL,那么就用MySQL的RR,可重复读级别;
    • timeout:默认值是:TIMEOUT_DEFAULT(-1),表示永不超时,超时时间以秒为单位;
    • readOnly:默认值是:false,表示的是增删改查询使用事务,查询不使用事务;

    之前我想的是定义多个事务规则对象,然后配置多个环绕通知方法,在开启事务的时候,根据不同的事务规则对象来获取得到事务。

  • 2、注入事务管理器,然后传入事务规则定义,获取得到当前的事务状态;

  • 3、利用事务管理器通过状态来进行提交或者是回滚事务;

上面可以通过AOP切面类来进行配置,但是看到了TransactionTemplate之后,我发现根本就不需要使用到切面类就可以来进行操作。

那么首先来看下TransactionTemplate类的继承体系:

在这里插入图片描述

分析一下继承体系:

  • 1、继承了DefaultTransactionDefinition类,所以可以用来定义事务规则信息对象;

  • 2、实现了InitializingBean接口,那么肯定是要在初始化方法来做操作;

  • 3、实现了TransactionOperations接口,从接口名称中可以知道这里代表的是事务;

而在TransactionTemplate类中,只有一个属性PlatformTransactionManager,而这是又是我们所需要的事务管理器。

而在InitializingBean接口的初始化方法中做了校验,如下所示:

	public void afterPropertiesSet() {
		if (this.transactionManager == null) {
			throw new IllegalArgumentException("Property 'transactionManager' is required");
		}
	}

因为TransactionTemplate中存在无参方法,没有注入transactionManager对象,说明我们需要手动注入当前容器中的transactionManager对象。那么配置TransactionTemplate的时候可以配置如下所示:

<bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
    <property name="transactionManager" ref="transactionManager" />
    <!--针对于不同的类型来配置传播行为、隔离级别、超时时间和是否只读,因为可以继承父类中的属性-->
    <!--还可以配置传播行为、隔离级别、超时时间和是否只读,因为可以继承父类中的属性-->
</bean>

如下所示,配置多个事务定义规则:

<bean id="transactionTemplate1" class="org.springframework.transaction.support.TransactionTemplate">
    <property name="transactionManager" ref="transactionManager" />
    <!--事务传播特性-->
    <property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"/>
    <!--事务隔离级别-->
    <property name="isolationLevelName" value="ISOLATION_DEFAULT"/>
    <!--事务超时时间-->
    <property name="timeout" value="-1"/>
    <!--事务是否只读-->
    <property name="readOnly" value="false"/>
</bean>
    
    
<bean id="transactionTemplat2" class="org.springframework.transaction.support.TransactionTemplate">
    <property name="transactionManager" ref="transactionManager" />
    <!--事务传播特性-->
    <property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"/>
    <!--事务隔离级别-->
    <property name="isolationLevelName" value="ISOLATION_DEFAULT"/>
    <!--事务超时时间-->
    <property name="timeout" value="3"/>
    <!--事务是否只读-->
    <property name="readOnly" value="false"/>
</bean>    
    
<bean id="transactionTemplat3" class="org.springframework.transaction.support.TransactionTemplate">
    <property name="transactionManager" ref="transactionManager" />
    <!--事务传播特性-->
    <property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"/>
    <!--事务隔离级别-->
    <property name="isolationLevelName" value="ISOLATION_DEFAULT"/>
    <!--事务超时时间-->
    <property name="timeout" value="3"/>
    <!--事务是否只读-->
    <property name="readOnly" value="true"/>
</bean>        

那么只需要在使用的时候,通过以下方式注入即可:

@Autowired
@Qulifier("transactionTemplat1")
private TransactionTemplate transactionTemplate
    
@Autowired
@Qulifier("transactionTemplat2")
private TransactionTemplate transactionTemplate
    
@Autowired
@Qulifier("transactionTemplat3")
private TransactionTemplate transactionTemplate    

通过这种方式来进行注入即可,也是比较方便的。

那么上面说完事务规则定义这块,这块应该有个疑问:在TransactionTemplate从哪里根据事务管理器获取得到事务状态的代码呢?

类似如下的代码:

@Service("accountService")
public class AccountServiceImpl implements AccountService {
    
    @Autowired
    private TransactionDefinition txDefinition;
    @Autowired
    private PlatformTransactionManager txManager;

    @Override
    public void transfer(String from, String to, Float money) {
        //开启事务,得到事务状态
        TransactionStatus txStatus = txManager.getTransaction(txDefinition);
        try {
            // ....
            //提交事务
            txManager.commit(txStatus);
        } catch (Exception e) {
            //回滚事务
            txManager.rollback(txStatus);
            e.printStackTrace();
        }
    }
}

来从TransactionTemplate中找,发现就提供了一个方法:execute方法,那么重点就关注一下execute方法即可。

这个方法也是非常的简单,重点是这里的TransactionCallback接口,提前看下:

public interface TransactionCallback<T> {
	T doInTransaction(TransactionStatus status);
}

接口中只有一个抽象方法,又被称之为函数式接口。不懂的话接着向下看

在这里插入图片描述

这里的方法也是非常简单呐,相当于是已经帮助我们做好了。

给出一个使用代码示例:

这里给的是没有返回值的,所以这里给的是Void类型。

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionDefinition;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallback;
import org.springframework.transaction.support.TransactionTemplate;
 
@Service
public class TransactionalService {
 
    @Autowired
    private TransactionTemplate transactionTemplate;
 
    public void performTransactionalOperation() {
        // 不建议使用下面两行代码!因为如果忘记了设置会默认值,那么后面再次使用的时候默认使用的九种方式
        /**nsactionTemplate.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
        transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_COMMITTED);**/
 
        transactionTemplate.execute(new TransactionCallback<Void>() {
            public Void doInTransaction(TransactionStatus status) {
                // 在这里执行事务操作
                // 可以进行数据库操作、调用其他需要事务支持的方法等
 
                return null;
            }
        });
    }
}

再给出一个有值的代码且可以自己来处理事务状态。不利用事务管理中的回滚方法而已!

public Object getObject(String str) {
        /*
         *  执行带有返回值<Object>的事务管理
         */
        transactionTemplate.execute(new TransactionCallback<Object>() {
            @Override
            public Object doInTransaction(TransactionStatus transactionStatus) {
                try {
                      ...
                    //.......   业务代码
                    return new Object();
                    // 注意:这里没有提交代码的逻辑
                } catch (Exception e) {
                    // 回滚,这里并不是利用事务管理器进行提交的,这里只是设置了一个标记而已
                    // 在事务管理进行事务提交的时候会来检查这里的状态而已
                    transactionStatus.setRollbackOnly();
                    return null;
                }
            }
        });
}
示例代码二

在上面的分析代码中,给出示例代码二:

首先给出配置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: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">

    <context:component-scan base-package="com.guang.dao"/>
    <context:component-scan base-package="com.guang.service.impl1."/>

    <!--事务的定义-->
    <bean id="transactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
        <!--配置事务管理器-->
        <property name="transactionManager" ref="txManager"/>
        <!--事务传播特性!如果调用其他方法的时候,直接使用当前方法中的事务-->
        <property name="propagationBehaviorName" value="PROPAGATION_REQUIRED"/>
        <!--事务隔离级别,使用数据库的操作-->
        <property name="isolationLevelName" value="ISOLATION_DEFAULT"/>
        <!--事务超时时间-->
        <property name="timeout" value="-1"/>
        <!--事务是否只读!因为是有更新操作,所以这里进行修改操作-->
        <property name="readOnly" value="false"/>
    </bean>
    <!--定义事务管理器-->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>
    <!--定义连接池-->
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="com.mysql.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/spring"/>
        <property name="username" value="root"/>
        <property name="password" value="root"/>
    </bean>
    <!--定义JdbcTemplate-->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <constructor-arg name="dataSource" ref="dataSource"/>
    </bean>
</beans>

然后给出代码:

package com.guang.service.impl1;

import com.guang.dao.AccountDao;
import com.guang.domain.Account;
import com.guang.service.AccountService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.support.TransactionTemplate;


@Service("accountService")
public class AccountServiceImpl1 implements AccountService {

    @Autowired
    private AccountDao accountDao;

    @Autowired
    private TransactionTemplate transactionTemplate;

    @Override
    public void transfer(String from, String to, Float money) {

        transactionTemplate.execute((status)-> {
                try {
                    // 来做事务操作
                    Account fromAccount = accountDao.findByName(from);
                    Account toAccount = accountDao.findByName(to);
                    fromAccount.setMoney(fromAccount.getMoney() - money);
                    toAccount.setMoney(toAccount.getMoney() + money);
                    accountDao.edit(fromAccount);
                    int i = 1 / 0;
                    accountDao.edit(toAccount);
                } catch ( Exception exception) {
                    System.out.println("执行SQL阶段出现异常!不能够进行提交");
                    status.setRollbackOnly();
                }
                return null;
            });
        System.out.println("使用事务管理器来执行代码完成");
    }
}

然后将代码放置到仓库中去

文章来源:https://blog.csdn.net/weixin_44023129/article/details/135715703
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。