Spring中AOP及事务管理详解



Spring


一、AOP简介


什么是AOP


AOP:Aspect Oriented Programming,面向切面编程。是通过预编译方式(aspectj)或者运行期动态代理(Spring)实现程序功能的统一维护的技术。

AOP是OOP(面向对象编程)的技术延续,是软件开发中的一个热点,也是Spring中的一个重要内容。利用AOP可以实现对业务逻辑各个部分之间的隔离,从而使得业务逻辑各部分之间的耦合性降低,提高程序的可重用性,同时提高了开发效率。


AOP的作用

作用:不修改源码的情况下,进行功能增强,通过动态代理实现的

优势:减少重复代码,提高开发效率,方便维护

比如:给功能增加日志输出, 事务管理的功能

10个方法: 想给10个方法都增加一种打印日志的功能,但是又不想(不能)改源码,此时可以给它使用AOP增强。


AOP的底层实现

实际上,Spring的AOP,底层是通过动态代理实现的。在运行期间,通过代理技术动态生成代理对象,代理对象方法执行时进行功能的增强介入,再去调用目标方法,从而完成功能增强。

常用的动态代理技术有:

  • JDK的动态代理:基于接口实现的

  • cglib的动态代理:基于子类实现的

Spring的AOP采用了哪种代理方式?

  • 如果目标对象有接口,就采用JDK的动态代理技术

  • 如果目标对象没有接口,就采用cglib技术


小结

  • AOP是:在不修改源码的情况下,进行功能增强

  • AOP的本质是:动态代理


二、Spring的AOP


AOP相关的概念

  • 目标对象(Target):要代理的/要增强的目标对象。

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

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

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

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

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

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

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

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

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


在这里插入图片描述


AOP开发前要明确的事项

我们要做的事情:

  • 编写核心业务代码(Target目标类的目标方法)

  • 编写通知类,通知类中有通知方法(Advice增强功能方法)

  • 在配置文件中,配置织入关系,即将哪些通知与哪些切入点 结合,形成切面


Spring的AOP做的事情:

  • 生成动态代理的过程(把通知织入到切入点的过程),是由Spring来实现的

  • Spring会监控切入点方法的执行,一旦发现切入点方法执行,使用代理机制动态创建目标对象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑运行。


小结

AOP相关的概念/术语

  • 目标类Target:要对哪个类进行增强

  • 代理对象Proxy:对目标类增强后的那个代理对象

  • 连接点JoinPoint:目标类里可增强的方法

  • 切入点PointCut:要增强的方法

  • 通知Advice:要增强的功能方法

  • 切面Aspect:切入点 + 通知

  • 织入Weaving:把切入点 和 通知 进行结合,生成代理对象的过程

  • 使用AOP,我们要做的事情:

    • 编写目标类,自己的业务代码

    • 编写通知类

    • 配置切面

  • 使用AOP,Spring做的事情

    • 根据我们配置的切面,进行织入生成代理对象

基于XML的AOP


创建maven项目,导入坐标

<dependencies>
        <!--Spring上下文核心包-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.1.2.RELEASE</version>
        </dependency>
        <!--AOP的实现包-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.4</version>
        </dependency>
        <!--Spring和单元测试集成-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.1.2.RELEASE</version>
        </dependency>
        <!--单元测试-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
    </dependencies>

创建目标类和通知类


目标类:com.execise.aop.UserServiceImpl


package com.execise.service;

public interface UserService {
    void add();

    void update();
}


package com.execise.service.impl;

import com.execise.service.UserService;

public class UserServiceImpl implements UserService {
    public void add() {
        System.out.println("调用了UserServiceImpl的add方法~!");
        //int a = 1 / 0 ;
    }

    public void update() {
        System.out.println("调用了UserServiceImpl的update方法~!");
    }
}


通知类|增强:com.execise.aop.MyAdvice

package com.execise.advice;

import org.aspectj.lang.ProceedingJoinPoint;

public class MyAdvice {

    public void print(){
        System.out.println("打印日志~!");
    }

}

修改配置文件

  1. 把目标类和通知类都配置到Spring配置文件中

  2. 配置切入和通知方法(增强方法)的织入关系

<?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"
       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">


    <!--1. 把userServiceImpl和MyAdvice 这两个类都交给spring管理-->
    <bean id="us" class="com.execise.service.impl.UserServiceImpl"/>
    <bean id="myAdvice" class="com.execise.advice.MyAdvice"/>

    <!--
        2. 下面要做AOP的配置:就是告诉spring,把MyAdvice的print方法,增强到UserServiceImpl里面的add方法
       aop:config : 用来做AOP配置的
          aop:aspect :用来配置切面
               ref : 表示用哪个类来做增强  这里表示使用myAdvice类来做增强
             aop:before :表示要做前置增强(在执行目标代码之前,先执行增强的代码)
                 method: 表示用增强类中的哪个方法来增强。
                 pointcut : 切入点, 打算对什么方法做增强。
    -->
    <aop:config>
        <aop:aspect ref="myAdvice">
            <aop:before method="print" pointcut="execution(* com.execise.service.impl.UserServiceImpl.add())"/>
        </aop:aspect>
    </aop:config>


</beans>

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


在这里插入图片描述


测试代码

package com.execise.test;

import com.execise.service.UserService;
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.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class TestUserServiceImpl {

    @Autowired
    private UserService us;

    @Test
    public void testAdd(){
        us.add();
    }

    @Test
    public void testUpdate(){
        us.update();
    }
}


步骤小结

  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.execise.service

  • 可以写..:表示当前包里所有后代类、后代包。比如com..service

  • *:表示任意字符。比如:com.it*, com.*

类名:

  • 可以指定类名。比如:UserServiceImpl

  • * 表示任意字符。比如:*ServiceImpl*

方法名:

  • 可以指定方法名

  • * 表示任意字符。比如:save**

参数列表:

  • 可以指定类型。比如:String,Integer表示第一个参数是String,第二个参数是Integer类型

  • *表示任意字符。比如:

    • String, * 表示第一个参数是String,第二个参数是任意类型

    • Str*, Integer表示第一个参数类型Str开头,第二个参数是Integer类型

  • 可以使用..表示任意个数、任意类型的参数


示例

execution(public void com.execise.dao.impl.UserDao.save())
execution(void com.execise.dao.impl.UserDao.*(..))
execution(* com.execise.dao.impl.*.*(..))
execution(* com.execise.dao..*.*(..))
execution(* *..*.*(..)) --不建议使用
       <!--
        2. 下面要做AOP的配置:就是告诉spring,把MyAdvice的print方法,增强到UserServiceImpl里面的add方法
       aop:config : 用来做AOP配置的
          aop:aspect :用来配置切面
               ref : 表示用哪个类来做增强  这里表示使用myAdvice类来做增强
             aop:before :表示要做前置增强(在执行目标代码之前,先执行增强的代码)
                 method: 表示用增强类中的哪个方法来增强。
                 pointcut : 切入点, 打算对什么方法做增强。
    -->
    <aop:config>
        <aop:aspect ref="myAdvice">
            <!--<aop:before method="print" pointcut="execution(* com.execise.service.impl.UserServiceImpl.add())"/>-->

            <!--最完整-->
            <!--<aop:before method="print" pointcut="execution(public void com.execise.service.impl.UserServiceImpl.add())"/>-->
            <!--<aop:before method="print" pointcut="execution(void com.execise.service.impl.UserServiceImpl.add())"/>-->
            <!--<aop:before method="print" pointcut="execution(* com.execise.service.impl.UserServiceImpl.add())"/>-->
            <!--<aop:before method="print" pointcut="execution(* com.execise.service.impl.UserServiceImpl.add())"/>-->
            <!--<aop:before method="print" pointcut="execution(* com..UserServiceImpl.add())"/>-->
            <!--<aop:before method="print" pointcut="execution(* com..*.add())"/>-->
            <!--<aop:before method="print" pointcut="execution(* com..*.*())"/>-->
            <!--<aop:before method="print" pointcut="execution(* com..*.*(..))"/>-->

            <!--最简单的写法: 一般很少写成这样,因为涉及的范围太广!-->
            <!--<aop:before method="print" pointcut="execution(* *..*.*(..))"/>-->

            <!--一般写成这样,表示给我们自己包下的所有类都增强-->
            <aop:before method="print" pointcut="execution(* com.execise..*.*(..))"/>
        </aop:aspect>
    </aop:config>

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(* com.execise.service..*.*())"/>

后置通知


通知方法定义

public void afterReturning(){
    System.out.println("后置通知");
}

xml配置

<aop:after-returning method="afterReturning" 
                     pointcut="execution(* com.execise.service..*.*())"/>

环绕通知

通知方法定义

/*
  环绕增强比较特殊一些,它需要我们在增强的方法里面手动调用目标方法。
*/
public void around(ProceedingJoinPoint joinPoint) throws Throwable {
  //System.out.println("环绕:打印日志~!");

  before();

  //调用目标方法
  //joinPoint.proceed(); //目标方法没有参数的调用
  joinPoint.proceed(joinPoint.getArgs()); //目标方法有参的方式调用

  afterReturning();


}


xml配置


<aop:around method="around" 
            pointcut="execution(* com.execise.service..*.*())"/>

异常抛出通知

通知方法定义

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

xml配置

<aop:after-throwing method="afterThrowing" 
                    pointcut="execution(* com.execise.service..*.*())"/>

最终通知

通知方法定义

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

xml配置

<aop:after method="after" 
           pointcut="execution(* com.execise.service..*.*())/>

3) 切点表达式的抽取


当多个切面的切入点表达式相同时,可以将切入点表达式进行抽取;在增强中使用pointcut-ref代替pointcut,来引入切入点表达式。


示例:

    <!--
        2. 下面要做AOP的配置:就是告诉spring,把MyAdvice的print方法,增强到UserServiceImpl里面的add方法
       aop:config : 用来做AOP配置的
          aop:aspect :用来配置切面
               ref : 表示用哪个类来做增强  这里表示使用myAdvice类来做增强
             aop:before :表示要做前置增强(在执行目标代码之前,先执行增强的代码)
                 method: 表示用增强类中的哪个方法来增强。
                 pointcut : 切入点, 打算对什么方法做增强。
    -->
    <aop:config>
        <aop:aspect ref="myAdvice">

            <!--抽取切点表达式-->
            <aop:pointcut id="pointCut01" expression="execution(* com.execise..*.*(..))"/>




            <!--前置增强:在目标方法之前执行增强的代码-->
            <!--<aop:before method="before" pointcut="execution(* com.execise..*.*(..))"/>-->
            <aop:before method="before" pointcut-ref="pointCut01"/>

            <!--后置增强:在目标方法之后执行增强的代码-->
             <!--<aop:after-returning method="afterReturning" pointcut="execution(* com.execise..*.*(..))"/>-->
             <aop:after-returning method="afterReturning" pointcut-ref="pointCut01"/>

            <!--异常增强:目标方法出现异常,才增强-->
           <!-- <aop:after-throwing method="afterThrowing" pointcut="execution(* com.execise..*.*(..))"/>-->

            <!--最终增强:不管目标方法有没有异常,都增强-->
            <!--<aop:after method="after" pointcut="execution(* com.execise..*.*(..))"/>-->

            <!--环绕增强:包含前置和后置-->
           <!-- <aop:around method="around" pointcut="execution(* com.execise..*.*(..))"/>-->
        </aop:aspect>
    </aop:config>

  1. 小结

需要我们编写的内容:

编写目标类,编写通知(增强)类

配置切面

<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


创建maven项目,导入坐标

注意:需要增加AOP的实现包:aspectjweaver

<dependencies>
        <!--Spring上下文核心包-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.1.2.RELEASE</version>
        </dependency>
        <!--AOP的实现包-->
        <dependency>
            <groupId>org.aspectj</groupId>
            <artifactId>aspectjweaver</artifactId>
            <version>1.9.4</version>
        </dependency>
        <!--Spring和单元测试集成-->
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.1.2.RELEASE</version>
        </dependency>
        <!--单元测试-->
        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>
    </dependencies>

创建目标类,创建通知类

  1. 使用注解标注两个类,配置成为bean对象

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

    • 目标类com.execise.aop.Target

package com.execise.service;

public interface UserService {

    void add();
    void update();
}
   
package com.execise.service.impl;

import com.execise.service.UserService;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {
    public void add() {
        System.out.println("调用了UserServiceImpl的add方法~!~");
        //int a =  1 / 0 ;
    }

    public void update() {
        System.out.println("调用了UserServiceImpl的update方法~!~");

    }
}


通知类com.execise.aop.MyAdvice

package com.execise.advice;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
/*
    MyAdvice是增强类,它需要做:
        1. 把自己交给spring管理 , 打上注解@Component
        2. 表示这个类是切面增强类,专门用来做增强的,打注解 @Aspect
        3. 把什么方法用来做什么增强(增强谁),就在这个方法上面打注解
            前置增强 === @Before
            后置增强 === @AfterReturning
 */

@Component
@Aspect
public class MyAdvice {

    @Before("execution(* com.execise..*.*(..))")
    public void print(){
        System.out.println("打印日志~");
    }


    ...
}

开启组件扫描和AOP自动代理

applicationContext.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:context="http://www.springframework.org/schema/context"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/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">

    <!--1. IOC的开关-->
    <context:component-scan base-package="com.execise"/>

    <!--2. AOP的开关-->
    <aop:aspectj-autoproxy/>
</beans>

测试

package com.execise.test;

import com.execise.service.UserService;
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.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class TestUserServiceImpl {

    @Autowired
    private UserService us;

    @Test
    public void testAdd(){
        us.add();

    }
}


4) 步骤小结

  1. 创建功能类UserServiceImpl

  2. 创建增强类MyAdvice

  3. 给他们都打上注解

    • UserServiceImpl : @Service

      • MyAdvice : @Component @Aspect

        • 方法上面打上前置或者后置的注解
  4. 在applicationContext.xml中打开开关

        <!--1. 打开IOC扫描包开关-->
        <context:component-scan base-package="com.execise"/>
    
        <!--2. 打开AOP的开关-->
        <aop:aspectj-autoproxy/>
    

AOP详解


1) 通知的种类

通知的语法

@通知注解("切入点表达式")

通知的类型


名称注解说明
前置通知@Before通知方法在切入点方法之前执行
后置通知@AfterRuturning通知方法在切入点方法之后执行
异常通知@AfterThrowing通知方法在抛出异常时执行
最终通知@After通知方法无论是否有异常,最终都执行
环绕通知@Around通知方法在切入点方法之前、之后都执行

注意:

  • 注解方式配置的通知,执行顺序是:前置->最终->后置/异常

  • 如果想要指定执行的顺序,就使用环绕通知 , 因为环绕增强是由我们手动控制的。


2) 切点表达式的抽取

同xml的AOP一样,当多个切面的切入点表达式相同时,可以将切入点表达式进行抽取;

抽取方法是:

在增强类(切面类,即被`@Aspect`标的类)上增加一个额外的方法,在方法上使用`@Pointcut`注解定义切入点表达式,

在增强注解中引用切入点表达式所在的方法


示例:

package com.execise.advice;

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

/*
    MyAdvice是增强类,它需要做:
        1. 把自己交给spring管理 , 打上注解@Component
        2. 表示这个类是切面增强类,专门用来做增强的,打注解 @Aspect
        3. 把什么方法用来做什么增强(增强谁),就在这个方法上面打注解
            前置增强 === @Before
            后置增强 === @AfterReturning
 */

@Aspect
@Component
public class MyAdvice {

    //@Before("execution(* com.execise..*.*(..))")
    public void print(){
        System.out.println("打印日志~!");
    }

    //===============================================================

    //这个abc方法的作用就是为了抽取切点表达式! ,并且这个abc方法不会被调用!
    @Pointcut("execution(* com.execise..*.*(..))")
    public void abc(){
        System.out.println("调用abc方法了~!");
    }


    //@Before("execution(* com.execise..*.*(..))")
    @Before("abc()")
    public void before(){
        System.out.println("前置:打印日志~!");
    }

    //@AfterReturning("execution(* com.execise..*.*(..))")
    @AfterReturning("abc()")
    public void afterReturning(){
        System.out.println("后置:打印日志~!");
    }

    //@AfterThrowing("execution(* com.execise..*.*(..))")
    public void afterThrowing(){
        System.out.println("异常:打印日志~!");
    }

    //@After("execution(* com.execise..*.*(..))")
    public void after(){
        System.out.println("最终:打印日志~!");
    }

    /*
        环绕增强比较特殊一些,它需要我们在增强的方法里面手动调用目标方法。
     */
    //@Around("execution(* com.execise..*.*(..))")
    public void around(ProceedingJoinPoint joinPoint) throws Throwable {
        //System.out.println("环绕:打印日志~!");

        before();

        //调用目标方法
        //joinPoint.proceed(); //目标方法没有参数的调用
        joinPoint.proceed(joinPoint.getArgs()); //目标方法有参的方式调用

        afterReturning();
    }
}

3) 小结

  1. 在通知类上加注解@Aspect,声明成一个切面

  2. 在通知类里方法上加注解@Before/@AfterReturning/@AfterThrowing/@After/@Around,配置切入点表达式

  3. 在xml里开启aop的自动代理:<aop:aspectj-autoproxy/>


纯注解的AOP


主要是把XML的配置,放到核心配置类上


使用 @EnableAspectJAutoProxy 来允许AOP的自动配置


核心配置类

package com.execise.config;

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

@Configuration
@ComponentScan("com.execise")
@EnableAspectJAutoProxy
public class AppConfig {
}

增强类 : MyAdvice
package com.execise.aop;

import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class MyAdvice {

    @Before("execution(* com.execise..*.*(..))")
    public void print(){
        System.out.println("打印日志~");
    }
}


UserService接口

package com.execise.service;

public interface UserService {

    void add();

    void update();
}


UserServiceImpl实现类

package com.execise.service.impl;

import com.execise.service.UserService;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl implements UserService {
    public void add() {
        System.out.println("调用了UserServiceImpl的add方法~!~");
    }

    public void update() {
        System.out.println("调用了UserServiceImpl的update方法~!~");
    }
}


单元测试

package com.execise.test;

import com.execise.config.AppConfig;
import com.execise.service.UserService;
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.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = AppConfig.class)
public class TestUserServiceImpl02 {

    @Autowired
    private UserService us;

    @Test
    public void testAdd(){
        us.add();

    }
}


案例-测量业务层接口万次执行效率


【第一步】编写通知类

@Component
@Aspect
public class ProjectAdvice {
    //匹配业务层的所有方法
    @Pointcut("execution(* com.execise.service.*Service.*(..))")
    private void servicePt(){}

    //设置环绕通知,在原始操作的运行前后记录执行时间
    @Around("ProjectAdvice.servicePt()") //本类类名可以省略不写
    public void runSpeed(ProceedingJoinPoint pjp) throws Throwable {
        //获取执行的签名对象
        Signature signature = pjp.getSignature();
        //获取接口/类全限定名
        String className = signature.getDeclaringTypeName();
        //获取方法名
        String methodName = signature.getName();
        //记录开始时间
        long start = System.currentTimeMillis();
        //执行万次操作
        for (int i = 0; i < 10000; i++) {
           pjp.proceed();
        }
        //记录结束时间
        long end = System.currentTimeMillis();
        //打印执行结果
        System.out.println("万次执行:"+ className+"."+methodName+"---->" +(end-start) + "ms");
    }
}

【第二步】在SpringConfig配置类上开启AOP注解功能

@Configuration
@ComponentScan("com.execise")
@PropertySource("classpath:jdbc.properties")
@Import({JdbcConfig.class,MybatisConfig.class})
@EnableAspectJAutoProxy //开启AOP注解功能
public class SpringConfig {
}

【第三步】运行测试类,查看结果

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = SpringConfig.class)
public class AccountServiceTestCase {
    @Autowired
    private AccountService accountService;
    @Test
    public void testFindById(){
        Account account = accountService.findById(2);
    }
    @Test
    public void testFindAll(){
        List<Account> list = accountService.findAll();
    }
}

在这里插入图片描述


三、Spring的事务管理


1. 编程式事务管理

所谓事务管理,即:按照给定的事务规则,来执行提交或回滚操作。其中:

  • “给定的事务规则”:用TransactionDefinition表示

  • “按照…来执行提交或回滚操作”:用PlatformTransactionManager来完成

  • TransactionStatus用于表示一个运行着的事务的状态


关于编程式事务的说明

编程式事务管理:通过编写代码的方式实现事务管理

编程式事务管理,因事务管理与业务功能耦合性太强,不方便维护,目前已经基本不用


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


如果想要了解Spring的编程式事务,可参考《资料/spring02_transaction_program》

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


PlatformTransactionManager

  • 是Spring提供的事务管理器接口,它提供了我们常用的操作事务的方法:开启事务、提交事务等

  • 注意:PlatformTransactionManager是接口类型,不同的dao层技术有不同的实现,例如:

    • dao层是jdbcTemplate或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:串行化–没有并发问题


事务的传播行为:

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

比如: A方法开启事务了之后,就调用了B方法,那么B方法是否也会被纳入事务管理的范畴呢?


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


  • 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表示永不超时

    • 事务是否是只读:如果只读,事务里只能执行查询操作,不能增删改


2. 声明式事务管理


转账功能的环境准备

zs给ls转账,不带事务的功能实现,为后边的事务控制做准备


1) 创建Maven项目,导入依赖坐标


dao层技术要使用MyBatis

<!--依赖管理-->
<dependencies>
    <!--junit单元测试-->
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    <!--Spring核心依赖-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-context</artifactId>
        <version>5.1.2.RELEASE</version>
    </dependency>

    <!--整合Mybatis-->
    <!--1. 数据库依赖-->
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <version>5.1.47</version>
    </dependency>
    <!--2. 接池依赖-->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>druid</artifactId>
        <version>1.1.23</version>
    </dependency>
    <!--3. mybatis本身的依赖-->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis</artifactId>
        <version>3.5.6</version>
    </dependency>
    <!--4. 整合mybatis和spring的依赖-->
    <!--MyBatis提供的和Spring进行整合的jar包-->
    <dependency>
        <groupId>org.mybatis</groupId>
        <artifactId>mybatis-spring</artifactId>
        <version>2.0.6</version>
    </dependency>
    <!--spring对jdbc封装的jar包也要导入进来,否则mybatis无法整合-->
    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-jdbc</artifactId>
        <version>5.1.2.RELEASE</version>
    </dependency>

    <!--5. 日志依赖-->
    <!-- 添加slf4j日志api -->
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-api</artifactId>
        <version>1.7.20</version>
    </dependency>
    <!-- 添加logback-classic依赖 -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-classic</artifactId>
        <version>1.2.3</version>
    </dependency>
    <!-- 添加logback-core依赖 -->
    <dependency>
        <groupId>ch.qos.logback</groupId>
        <artifactId>logback-core</artifactId>
        <version>1.2.3</version>
    </dependency>

    <dependency>
        <groupId>org.springframework</groupId>
        <artifactId>spring-test</artifactId>
        <version>5.1.2.RELEASE</version>
    </dependency>
    <!--6.AOP相关jar包-->
    <!--Aspect-->
    <dependency>
        <groupId>org.aspectj</groupId>
        <artifactId>aspectjweaver</artifactId>
        <version>1.9.4</version>
    </dependency>
</dependencies>

2) 创建dao,Service:


AccountDao接口:


package com.execise.dao;

import org.apache.ibatis.annotations.Param;

public interface AccountDao {

    void kouqian(@Param("from") String from ,@Param("money") int money);

    void jiaqian(@Param("to") String to ,@Param("money") int money);

}

AccountDao.xml映射文件:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.execise.dao.AccountDao">

    <update id="kouqian">
        update account set money = money - #{money} where name = #{from}
    </update>

    <update id="jiaqian">
        update account set money = money + #{money} where name = #{to}
    </update>

</mapper>

AccountServiceAccountServiceImpl

package com.execise.service;

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


package com.execise.service.impl;

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

/*
    1. 把这个类交给spring管理
    2. 注入进来dao的对象!
 */

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao dao;

    /**
     * 转账
     * @param from
     * @param to
     * @param money
     */
    public void transfer(String from, String to, int money) {

        //扣钱
        dao.kouqian(from ,money);

        //加钱
        dao.jiaqian(to , money);
    }
}


3) 配置bean和依赖注入


applicationContext.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: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/context http://www.springframework.org/schema/context/spring-context.xsd">

    <!--1.打开注解扫描开关-->
    <context:component-scan base-package="com.execise"/>

    <!--2.加载外部properties配置文件-->
    <context:property-placeholder location="classpath:db.properties"/>

    <!--3.配置第三方数据源【连接池】 交由Spring管理-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${db.driverClass}"/>
        <property name="url" value="${db.url}"/>
        <property name="username" value="${db.username}"/>
        <property name="password" value="${db.password}"/>
    </bean>

    <!--4.配置SqlSessionFactory  通过MyBatis-Spring整合包提供的SqlSessionFactoryBean创建对象 交由Spring管理 从而可以取代MyBatis配置文件-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!--1.注入数据源-->
        <property name="dataSource" ref="dataSource"/>
        <!--2.设置实体类别名-->
        <property name="typeAliasesPackage" value="com.execise.bean"/>
    </bean>

    <!--5.配置MapperScannerConfigurer  扫描dao接口创建代理对象-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.execise.dao"/>
    </bean>
</beans>

4) 功能测试

package com.execise.test;

import com.execise.service.AccountService;
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.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:applicationContext.xml")
public class TestAccountServiceImpl {

    @Autowired
    private AccountService as;

    @Test
    public void testTransfer(){
        as.transfer("zs", "ls" , 100);
    }
}



什么是声明式事务控制


介绍:

  • 声明式事务控制,是采用声明的方式进行事务管理。所谓的声明,指的就是在配置文件中进行配置。

  • 通过声明式(配置)的方式来处理事务,代替编码式事务控制

  • 作用:

    • 事务管理不入侵开发的组件,松耦合

      • 业务逻辑代码中,没有事务的代码,甚至不会意识到正在事务当中。

      • 事实上也应该如此,业务逻辑代码只处理业务功能,事务控制是属于系统层面的服务;如果想要更改事务,只需要在配置文件中重新配置即可

    • 能以模板的方式使用

      • Spring的声明式事务以AOP为基础,但是几乎是固定的配置模板,即使不懂AOP,也可以配置实现事务管理
    • 易维护。

      • 在不需要事务管理的时候,只需要在配置文件中进行修改,即可把事务管理移除掉,而不需要修改源码,方便维护

注意:Spring的声明式事务,底层就是AOP


基于XML的声明式事务控制


1) 需要明确的事项

  • 谁是目标类?(哪个类想用事务) AccountserviceImpl

  • 谁是切入点?(哪个方法想用事务 ) transfer

  • 谁是通知(增强)?(给上面的方法增强什么功能) 事务管理

  • dao层技术是JdbcTemplate,事务的管理员使用DataSourceTransactionManager


2) 快速入门

通过Spring的xml配置,对银行转账功能,进行事务控制


实现步骤

只需要修改applicationContext.xml即可:

  1. 在配置文件中增加aop和tx的名称空间

  2. 配置事务的通知(增强)

  3. 配置切面,把事务通知织入到转账方法中


功能实现

<?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:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
       http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--1.打开注解扫描开关-->
    <context:component-scan base-package="com.execise"/>

    <!--2.加载外部properties配置文件-->
    <context:property-placeholder location="classpath:db.properties"/>

    <!--3.配置第三方数据源【连接池】 交由Spring管理-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${db.driverClass}"/>
        <property name="url" value="${db.url}"/>
        <property name="username" value="${db.username}"/>
        <property name="password" value="${db.password}"/>
    </bean>

    <!--4.配置SqlSessionFactory  通过MyBatis-Spring整合包提供的SqlSessionFactoryBean创建对象 交由Spring管理 从而可以取代MyBatis配置文件-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!--1.注入数据源-->
        <property name="dataSource" ref="dataSource"/>
        <!--2.设置实体类别名-->
        <property name="typeAliasesPackage" value="com.execise.bean"/>
        <!--3.加载Mybatis核心配置文件-->
        <!--<property name="configLocation" value="classpath:mybatis-config.xml"/>-->
        <!--4.配置映射文件位置【当映射文件和dao接口不在同一目录时配置】-->
        <!--<property name="mapperLocations" value="classpath:com/aa/*Dao.xml" />-->
    </bean>

    <!--5.配置MapperScannerConfigurer  扫描dao接口创建代理对象-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.execise.dao"/>
    </bean>

    
    <!--========================以下代码属于配置事务的范畴==========================-->

    <!--
    1. 定义事务的管理员
        1.1 spring 管理事务,一定是由管理员来完成事务的操作: 开启事务、提交事务、回滚事务
        1.2 根据dao层用到的技术不同,使用的管理员也不同
            jdbctempalte | mybatis  ============= DataSourceTransactionManager
            hibernate =========== HibernateTransactionManager
        1.3 事务管理员去操作事务的时候,需要用到连接对象,所以要给它注入DataSource
   -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--
        2. 定义事务的规则
            tx:advice : 用来定义事务的规则
                id : 唯一标识 ,方便一会能找到这个规则
                transaction-manager : 要用哪个管理员来管理事务
             tx:attributes : 用来配置事务的规则,里面可以配置很多的事务规则,可以针对不同的方法配置不同的事务规则
                tx:method : 给具体的某一个方法 或者是 所有的方法配置事务的规则,这个标签可以写很多个!
    -->
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <!--下面的切点表达式找到的所有方法,都给他们应用上事务,并且事务的规则都是默认的那一套-->
            <!--<tx:method name="*"/>-->
            <tx:method name="*" isolation="DEFAULT" propagation="REQUIRED" read-only="false" timeout="-1"/>


            <!-- save开头的方法,隔离级别是数据库默认的,事务传播特性是required,非只读 -->
            <tx:method name="save*" isolation="DEFAULT" propagation="REQUIRED" read-only="false"/>

            <!-- edit开头的方法,隔离级别是数据库默认的,事务传播特性是required,非只读 -->
            <tx:method name="edit*" isolation="DEFAULT" propagation="REQUIRED" read-only="false"/>

            <!-- delete开头的方法,隔离级别是数据库默认的,事务传播特性是required,非只读 -->
            <tx:method name="delete*" isolation="DEFAULT" propagation="REQUIRED" read-only="false"/>

            <!-- query开头的方法,隔离级别是数据库默认的,事务传播特性是required,非只读 -->
            <tx:method name="query*" isolation="DEFAULT" propagation="REQUIRED" read-only="true"/>

            <!-- find开头的方法,隔离级别是数据库默认的,事务传播特性是required,非只读 -->
            <tx:method name="find*" isolation="DEFAULT" propagation="REQUIRED" read-only="true"/>
        </tx:attributes>
    </tx:advice>

    <!--
        3. 定义切面:也就是表示哪个方法想用事务!
        aop:config :用于配置切面
            aop:advisor :专门是用来匹配|衔接上面的事务的规则
                advice-ref : 事务的规则 id写下来
                pointcut : 切入点,表示要去找方法!
     -->
    <aop:config>
        <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.execise..*.*(..))"/>
    </aop:config>
</beans>

3) 配置详解


aop:config:切面配置


这个标签的配置,就是为了找到方法,然后给这些方法应用上事务。


<aop:config>
    <aop:advisor advice-ref="txAdvice"  
                 pointcut="execution(* com.execise.service.impl..*.*(..))"/>
</aop:config>

aop:config:aop提供的用于配置切面的标签

aop:advisor:Spring提供的专门用于配置事务的,作用类似于aop:aspect

  • advice-ref:要引入的通知配置,必须要引用<tx:advice>所配置的事务通知

  • pointcut:切入点表达式


tx:advice:事务通知配置


id属性:唯一标识

transaction-manager属性:配置一个事务管理器,即PlatformTransactionManager的实现类对象
类似于我们的自己编写的事务管理器,里边提供了事务管理的方法,例如:提交、回滚事务的方法等等

tx:attributes:在标签内部设置事务的属性信息(事务定义信息,TransactionDefinition)

tx:method:要进行事务控制的方法配置,表示 要对哪些方法,进行什么样的事务控制

name属性:要进行事务控制方法名称,可以使用通配符*

isolation属性:事务的隔离级别设置

propagation属性:事务传播特性

read-only属性:是否只读

timeout属性:超时时间。默认-1表示不限制,如果设置的话,单位是秒


<tx:advice id="txAdvice" transaction-manager="txManager">
    <tx:attributes>
        <!-- transfer方法:隔离级别是repeatable-read,事务传播特性是required,非只读 -->
        <tx:method name="transfer" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>    
        
        <!-- save开头的方法,隔离级别是数据库默认的,事务传播特性是required,非只读 -->
        <tx:method name="save*" isolation="DEFAULT" propagation="REQUIRED" read-only="false"/>
        
        <!-- edit开头的方法,隔离级别是数据库默认的,事务传播特性是required,非只读 -->
        <tx:method name="edit*" isolation="DEFAULT" propagation="REQUIRED" read-only="false"/>
        
        <!-- delete开头的方法,隔离级别是数据库默认的,事务传播特性是required,非只读 -->
        <tx:method name="delete*" isolation="DEFAULT" propagation="REQUIRED" read-only="false"/>
        
        <!-- query开头的方法,隔离级别是数据库默认的,事务传播特性是required,非只读 -->
        <tx:method name="query*" isolation="DEFAULT" propagation="REQUIRED" read-only="true"/>
        
        <!-- find开头的方法,隔离级别是数据库默认的,事务传播特性是required,非只读 -->
        <tx:method name="find*" isolation="DEFAULT" propagation="REQUIRED" read-only="true"/>
    </tx:attributes>
</tx:advice>

4) 小结


service里的方法,不需要有任何事务管理相关的代码

只需要在xml里配置即可


<!-- 配置事务管理器 -->
<bean id="txManager" class="DataSourceTransactionManager全限定类名">
	<property name="dataSource" ref="连接池"/>
</bean>

<!-- 配置事务通知 -->
<tx:advice id="txAdvice" transaction-manager="txManager">
	<tx:attributes>
    	<tx:method name="*"/>
    </tx:attributes>
</tx:advice>

<!-- 配置事务切面 -->
<aop:config>
	<aop:advisor advice-ref="txAdvice" pointcut="切入点表达式"/>
</aop:config>

基于注解的声明式事务控制


1) 快速入门


通过Spring的注解配置,对银行转账功能,进行事务控制


实现步骤


在需要事务控制的方法/类上增加注解@Transactional


@Transactional //类里面的所有方法都有事务
@Service
public class AccountServiceImpl implements AccountService {
}

在配置文件applicationContext.xml中修改配置

  • 配置事务管理器

  • 开启事务的注解驱动

 <!--打开事务的开关-->
    <tx:annotation-driven transaction-manager="tm"/>

功能实现


修改银行转账的Service接口:AccountService接口


package com.execise.service;

import com.execise.bean.Account;
import org.springframework.transaction.annotation.Isolation;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

import java.util.List;

/*
    注解事务的配置:
        0.在applicationContext.xml中配置事务管理员bean
        1. 在类上或者方法上打注解 @Transactional
            1.1 在类身上打,即表示该类中的所有方法都会应用上事务
            1.2 在方法身上打,即表示只有这个方法会应用上事务。

        2. 在xml里面打开注解的开关
            <tx:annotation-driven transaction-manager="transactionManager"/>
        注意:事务注解可以打在业务层接口或实现类上,一般建议打在接口上,这样程序的耦合性更低一些
 */

//1. 类上打注解
//@Transactional
public interface AccountService {

    //2.方法上打注解
    //@Transactional
    @Transactional(isolation = Isolation.DEFAULT , propagation = Propagation.REQUIRED , readOnly = false , timeout = -1)
    void transfer(String from ,String to , int money);
}

修改配置文件applicationContext.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:context="http://www.springframework.org/schema/context"
       xmlns:tx="http://www.springframework.org/schema/tx"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
       http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop.xsd">

    <!--1.打开注解扫描开关-->
    <context:component-scan base-package="com.execise"/>

    <!--2.加载外部properties配置文件-->
    <context:property-placeholder location="classpath:db.properties"/>

    <!--3.配置第三方数据源【连接池】 交由Spring管理-->
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${db.driverClass}"/>
        <property name="url" value="${db.url}"/>
        <property name="username" value="${db.username}"/>
        <property name="password" value="${db.password}"/>
    </bean>

    <!--4.配置SqlSessionFactory  通过MyBatis-Spring整合包提供的SqlSessionFactoryBean创建对象 交由Spring管理 从而可以取代MyBatis配置文件-->
    <bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
        <!--1.注入数据源-->
        <property name="dataSource" ref="dataSource"/>
        <!--2.设置实体类别名-->
        <property name="typeAliasesPackage" value="com.execise.bean"/>
        <!--3.加载Mybatis核心配置文件-->
        <!--<property name="configLocation" value="classpath:mybatis-config.xml"/>-->
        <!--4.配置映射文件位置【当映射文件和dao接口不在同一目录时配置】-->
        <!--<property name="mapperLocations" value="classpath:com/aa/*Dao.xml" />-->
    </bean>

    <!--5.配置MapperScannerConfigurer  扫描dao接口创建代理对象-->
    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.execise.dao"/>
    </bean>

    <!--========================以下代码属于配置事务的范畴==========================-->


    <!--
    1. 定义事务的管理员
        1.1 spring 管理事务,一定是由管理员来完成事务的操作: 开启事务、提交事务、回滚事务
        1.2 根据dao层用到的技术不同,使用的管理员也不同
            jdbctempalte | mybatis  ============= DataSourceTransactionManager
            hibernate =========== HibernateTransactionManager
        1.3 事务管理员去操作事务的时候,需要用到连接对象,所以要给它注入DataSource
   -->
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!--2. 打开注解事务的开关-->
    <!--<tx:annotation-driven transaction-manager="transactionManager"/>-->
    <!--如果事务的管理员的id名字正好是: transactionManager 那么可以省略掉 transaction-manager 属性不赋值!-->
    <tx:annotation-driven />
</beans>

2) 配置详解


注解@Transactional

  • 加在 需要进行事务控制的方法/类上,用于代替xml配置中的tx:advice和事务切面的aop:config

  • isolation属性:设置事务的隔离级别,从枚举Isolation中取值

  • propagation属性:设置事务的传播特性,从枚举Propagation中取值

  • readOnly属性:设置是否是只读的

  • timeout属性:设置超时时间,单位秒。-1表示不限制


开启事务的注解驱动


XML方式

  • 使用注解进行事务管理,必须要在applicationContext.xml中开启 事务的注解驱动,否则无效
<!-- 开启事务的注解驱动。`transaction-manager`属性:指定事务管理器 -->
<tx:annotation-driven transaction-manager="txManager"/>
<!-- 开启事务的注解驱动。默认注入名称为transactionManager的事务管理器 -->
<tx:annotation-driver/>

纯注解方式

  • 如果是纯注解,开启事务的注解驱动,需要在核心配置类上增加注解:@EnableTransactionManagement

配置示例

package com.execise.config;

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

import javax.sql.DataSource;

@Configuration
@ComponentScan("com.execise")
@PropertySource("classpath:db.properties")
@MapperScan("com.execise.dao")
//4.核心配置类上使用@EnableTransactionManagement注解开启事务管理
@EnableTransactionManagement
public class AppConfig {

    @Value("${db.driverClass}")
    private String driver;
    @Value("${db.url}")
    private String url;
    @Value("${db.username}")
    private String username;
    @Value("${db.password}")
    private String password;


    //1.创建数据源bean 交由Spring管理
    @Bean
    public DataSource dataSource(){
        DruidDataSource dataSource = new DruidDataSource();
        dataSource.setDriverClassName(driver);
        dataSource.setUrl(url);
        dataSource.setUsername(username);
        dataSource.setPassword(password);
        return  dataSource;
    }

    //2.创建SqlSessionFactory bean 交由Spring管理
    @Bean
    public SqlSessionFactoryBean sqlSessionFactory(DataSource dataSource){
        SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
        sqlSessionFactoryBean.setDataSource(dataSource);
        sqlSessionFactoryBean.setTypeAliasesPackage("com.execise.bean");
        return sqlSessionFactoryBean;
    }

    //3.声明事务管理员bean 交由Spring管理
    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource){
        DataSourceTransactionManager tm = new DataSourceTransactionManager();
        tm.setDataSource(dataSource);
        return tm;
    }
    
}

单元测试

package com.execise.test;

import com.execise.config.AppConfig;
import com.execise.service.AccountService;
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.SpringJUnit4ClassRunner;

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = AppConfig.class)
public class TestAccountServiceImpl02 {

    @Autowired
    private AccountService as;

    @Test
    public void testTransfer(){
        as.transfer("zs", "ls" , 100);
    }
}


3) 小结


在xml文件里

<!-- 配置事务管理器 -->
<bean id="txManager" class="DataSourceTransactionManager全限定类名">
	<property name="dataSource" ref="连接池"/>
</bean>

<!-- 开启事务的注解驱动 -->
<tx:annotation-driven transaction-manager="txManager"/>
<context:component-scan base-package="com.execise"/>

哪个方法需要事务管理,就在哪个方法上加注解:@Transactional


3 Spring事务相关配置


什么样的异常,Spring事务默认是不进行回滚的?


事务配置


在这里插入图片描述


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


案例:转账业务追加日志

需求和分析

  • 需求:实现任意两个账户间转账操作,并对每次转账操作在数据库进行留痕

  • 需求微缩:A账户减钱,B账户加钱,数据库记录日志

  • 分析:

    ①:基于转账操作案例添加日志模块,实现数据库中记录日志

    ②:业务层转账操作(transfer),调用减钱、加钱与记录日志功能

  • 实现效果预期:

    无论转账操作是否成功,均进行转账操作的日志留痕
    
  • 存在的问题:

    日志的记录与转账操作隶属同一个事务,同成功同失败
    
  • 实现效果预期改进:

    无论转账操作是否成功,日志必须保留
    
  • 事务传播行为:事务协调员对事务管理员所携带事务的处理态度


在这里插入图片描述


【准备工作】环境整备

USE day32;
CREATE TABLE tbl_log(
	id INT PRIMARY KEY AUTO_INCREMENT,
	info VARCHAR(255),
	createDate TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);
public interface LogService {
    //propagation设置事务属性:传播行为设置为当前操作需要新事务
    @Transactional
    void log(String from, String to, Integer money);
}

@Service
public class LogServiceImpl implements LogService {

    @Autowired
    private LogDao logDao;
    
    @Override
    public void log(String from,String to,Integer money ) {
        logDao.log("转账操作由"+from+"到"+to+",金额:"+money);
    }
}

public interface LogDao {
    @Insert("insert into tbl_log (info) values(#{info})")
    void log(String info);
}

【第一步】在AccountServiceImpl中调用logService中添加日志的方法

package com.execise.service.impl;

import com.execise.bean.Account;
import com.execise.dao.AccountDao;
import com.execise.service.AccountService;
import com.execise.service.LogService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao accountDao;

    @Autowired
    private LogService logService;

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

        try {
            //扣钱
            accountDao.kouqian(from ,money);
            //测试异常
            int i=1/0;
            //加钱
            accountDao.jiaqian(to , money);
        } finally {
            logService.log(from,to,money);
        }
    }
}


【第二步】在LogService的log()方法上设置事务的传播行为

public interface LogService {
    //propagation设置事务属性:传播行为设置为当前操作需要新事务
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    void log(String from, String to, Integer money);
}

【第三步】运行测试类,查看结果

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = AppConfig.class)
public class AccountServiceTest {
    @Autowired
    private AccountService as;

    @Test
    public void testTransfer() throws IOException {
        as.transfer("zs","ls",200);
    }
}

事务传播行为


在这里插入图片描述

  • 1
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

请叫我阿杰

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值