@[toc]

spring框架之AOP

概念

面向切面编程(AOP:Aspect Oriented Programming),通过预编译方式和运行期动态代理实现程序功能的统一维护的一种技术。是对OOP的一种延续,OOP语言提供了类与类之间纵向的关系(继承、接口),而AOP补充了横向的关系(比如在不改变目标类中源代码的情况下给com.spring.demo.dao包下所有类中以insert和update开头的方法添加事务管理),下图中的业务模块是OOP的体现,而横向的功能则可以又AOP来实现。

img

AOP与AspectJ

Aop只是一个概念,一个规范,本身并没有设置具体的语言的实现,那么要怎么实现它的功能呢?AspectJ的出现则解决了这个问题.

首先给出一个案例

public class Fruit {

    public static void eatFruit(){
        System.out.println("吃了个apple!");
    }
    public static void main(String args[]){
        eatFruit();
    }
}

Aspect: Aspect 声明类似于 Java 中的类声明,在 Aspect 中会包含着一些 Pointcut 以及相应的 Advice。

public aspect TestAspectJ {
    /**
     * 定义切点,日志记录(自定义函数recordLog())
     */
    pointcut recordLog():cell(* Fruit.eatFruit(..));	

    /**
     * 定义切点,权限验证(一切Fruit里的任意返回值的eatFruit()方法都会被拦截,无论参数有几个是什么类型)
     * 自定义函数authCheck()
     */
    pointcut authCheck():cell(* Fruit.eatFruit(..));

    /**
     * 定义前置通知(目标方法执行前通知,但无法阻止目标方法执行)
     */
    before():authCheck(){
        System.out.println("eatFruit方法执行前验证权限");
    }

    /**
     * 定义后置通知(目标方法执行通知,但无法阻止目标方法执行)
     */
    after():recordLog(){
        System.out.println("eatFruit方法执行后记录日志");
    }
}

但是它的编译需要专门的编译器acj,类似与javac可以把aspect文件编译成class字节码文件,因此如果读者要动手练习一下,那么就需要下载一个acj的编译器。在maven项目中可以添加两个依赖,分别是aspectJ的核心库和工具包(包含了acj编译器)

安装acj编译器

<!-- aspectJ工具包依赖 -->
<dependency>
	<groupId>org.aspectj</groupId>
	<artifactId>aspectjtools</artifactId>
	<version>1.8.9</version>
</dependency>
<!-- aspectJ核心库依赖 -->
<dependency>
	<groupId>org.aspectj</groupId>
	<artifactId>aspectjrt</artifactId>
	<version>1.5.4</version>
</dependency>

我以我用的idea为例,在已安装的插件中看一下有没有aspectJ插件

检查aspectJ插件

然后在Java compiler中根据下图选择acj编译器

切换编译器

这里我是idea和maven项目为例,至于其他类型的就需要读者自己选择自己下载jar包或者添加其他类的依赖。到这里AspectJ就介绍的差不多了,

AOP中的术语

切面(Aspect)

我们将自己需要插入到目标业务逻辑中的代码模块化, 通过AOP使之可以横切多个类的模块,称之为切面。
在Spring AOP配置中切面通常包含三部分:

  • 切面模块本身
  • 通知
  • 切入点
切点(pointcut)
  • 要横切的点(在Spring中就是Pointcut,通常是一个表达式,见下表)
  • 切面与目标对象间的连接点有很多,切点就是从众多的连接点中选择我们感兴趣的点将切面织入到目标代码中
AspectJ 指示器描述
execution () cell()用于匹配连接点的执行方法最常用
args ()限制连接点的指定参数为指定类型的执行方法
@args ()限制连接点匹配参数类型由指定注解标注的执行方法
this ()限制连接点匹配 AOP 代理的 Bean 引用为指定类型的类
target ()限制连接点匹配特定的执行对象,目标对象是指定的类型
@target ()限制连接点匹配特定的执行对象,这些对象对应的类要具备指定类型注解
within()限制连接点匹配指定类型,比如哪个包下,或哪个类里面
@within()限制连接点匹配指定注释所标注的类型(当使用 Spring AOP 时,方法定义在由指定的注解所标注的类里)
@annotation限制匹配带有指定注释的连接点

切入点表达式语法:

语法一:单个表达式
切入点表达式语法

  • execution表达式以 * 号开头,标识了我们不关心的方法返回值的类型。
  • *后我们指定了权限定类名和方法名。(类名和方法名都可以用* 号通配符代替)
  • 对于方法的参数列表,使用(..)标识切点选择任意的 play( ) 方法,无论入参是什么。

  1. 语法二:多个表达式组合
    切入点表达式语法

这里我们使用 && 将 execution( ) 和 within( ) 连接起来,形成的 and 关系。同理也可以使用 || 或关系、!非关系

连接点(JoinPoint)

连接点有很多种,比如方法执行期间(开始执行、执行结束、抛出异常)、字段修饰符、字段值被更改....

连接点与切入点的关系可以简单理解为: 切入点一定是连接点, 连接点不一定是切入点。

通知(advice)

通知其实就是我们要织入的代码,它包括了五种通知类型:前置通知(before)、后置通知(after returning)、异常通知(after throwing)、环绕通知(around)、最终通知(after)

前置通知(before)

在目标方法调用前通知切面, 什么参数也无法获取。也不能终止目标方法执行

后置(返回值)通知(after returning)

只有在目标方法 正常 执行结束后才会通知, 在通知方法中可以获取到方法的返回值

异常通知(after throwing)

只有在目标方法 出现异常 才会通知, 在通知方法中可以获取到抛出的异常信息

环绕通知(around)

可以通过proceed()控制目标方法是否执行,也可以在此通知种控制是否返回真实的目标方法的返回值

后置(最终)通知 (after)

在目标方法执行结束后通知切面, 什么参数也无法获取。无论目标方法是正常执行结束还是抛出异常终止,都会被通知(类似于finally块)

通知使用语法
/*[返回值类型] 通知函数名称(参数) [returning/throwing 表达式]:连接点函数(切点函数){ 
	函数体 
}*/
// 比如:环绕通知
Object around():testAroundAdvice(){
    System.out.println("环绕通知")
    return proceed();
}
// 再比如:异常通知
after() throwing(Exceprion e):testAfterThrowing(){
    System.out.println("异常信息为:" + e.toString());
}
织入(Weaver)

织入的过程其实就是AspectJ帮我们把切面中的代码织入到目标代码中的过程。

AspectJ的优劣

AspectJ优势非常明显,2001年的框架,技术非常成熟。但是它的有很大的劣势,那就是过于复杂,破坏了封装,以及需要专门的Java编译器ajc,很是麻烦。

Spring AOP

Spring AOP引用了AspectJ的切面语法,但采用的还是以动态代理的方式实现AOP的,通过IOC的控制将需要添加的业务逻辑添加到连接点方法中。那么如此方便的Spring AOP该怎么用呢?我在下面给出了示例,由于Spring AOP沿用了AspectJ的切面语法,所以就不再介绍了。

XML方式

添加依赖

首先我们需要先将aspectJ的依赖导入maven项目中

<dependency>           
 <groupId>org.aspectj</groupId>
 <artifactId>aspectjweaver</artifactId>
 <version>1.9.4</version>
</dependency>
使用示例

这里给出一个需要被织入的Java类的示例

下面使用到的log日志需要的可以看我另一篇博客的介绍
附:Logback的简介与使用

package com.spring.demo;
/**
* @program: spring
*
* @description: Calc接口实现类(接口就不放出来了,自己建吧)
*
* @author: Bennett
*
* @create: 2019-06-17 11:46
*
* @version: 1.0
**/

public class CalcImpl implements Calc {
    /**
    * 加法
    */
    @Override
    public int add(int num1, int num2) {
        return num1 + num2;
    }
	/**
    * 减法
    */
    @Override
    public int subtract(int minuend, int subtrahend) {
        return minuend - subtrahend;
    }
    /**
    * 乘法
    */
    @Override
    public int multiply(int num1, int num2) {
        return num1 * num2;
    }
    /**
    * 除法
    */
    @Override
    public int divide(int dividend, int divisor) {
        return dividend / divisor;
    }
}

以及切面类

package com.spring.demo;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;

import java.util.Arrays;

/**
* @program: spring
*
* @description: Calc的切面类
*
* @author: Bennett
*
* @create: 2019-06-17 14:05
*
* @version: 1.0
**/

@Slf4j//该注解用于下面使用log输出(不想使用log的换成控制台输出语句即可)
public class CalcAspect {

    public Object aroundM(ProceedingJoinPoint joinPoint) throws Throwable {
        // 获取连接点代表的方法的签名
        Signature signature = joinPoint.getSignature();
        // 获取目标方法名
        String methodName = signature.getName();
        // 获取目标方法的参数
        Object[] args = joinPoint.getArgs();
        log.debug("[aroundM] ---- 目标方法[" + methodName + "(" + Arrays.toString(args) + ")]开始执行");
        long start = System.currentTimeMillis();
        // 调用目标方法
        Object retVal = joinPoint.proceed();
        // 插入公共需要的逻辑代码
        long timer = System.currentTimeMillis() - start;
        log.debug("[aroundM] ---- 目标方法[" + methodName + "(" + Arrays.toString(args) + ")]" +
                "执行结束,返回值: " + retVal + ",耗时: " + timer + "ms.");
        // 正常返回目标方法的返回值
        return retVal;
    }
    /**
    * 前置通知
    */
    public void beforeM(){
        log.debug("[beforeM] ---- 目标方法开始执行");
    }
    /**
    * 后置(返回值)通知
    */
    public void afterReturningM(Object retVal){
        log.debug("[afterReturningM] ---- 目标方法执行结束,返回值: " + retVal);
    }
    /**
    * 后置(最终)通知
    */
    public void afterFinallyM(){
        log.error("[afterFinallyM] ---- 方法执行结束");
    }
    /**
    * 异常通知
    */
    public void afterThrowing(Throwable throwable){
        log.error("[afterThrowing] ---- 方法执行出错", throwable);
    }

}

准备工作做完了,就需要在spring的配置文件中进行配置了

<?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对象 -->
    <!-- 目标业务逻辑代码 -->
    <bean id="calc" class="com.spring.demo.CalcImpl"/>
    <!-- 切面模块化对象(代表我们要附加到原始业务逻辑中的代码) -->
    <bean id="calcAspect" class="com.spring.demo.CalcAspect"/>
    
    <!-- 示例说明: 将切面calcAspect中的代码插入到calc原始业务代码中 -->
	<!-- 使用aop进行配置,需要在文件的头部加入aop schema-->
    <aop:config>
        <!-- 定义公用的切入点表达式,如果aspect中有多个通知,都可以通过pointcut-ref复用 -->
        <aop:pointcut id="all_calc_method" expression="execution(* com.lanou3g.spring.CalcImpl.*(..))"/>
        <!-- 引用外部定义的aspect bean -->
        <aop:aspect ref="calcAspect">
            <!-- 切面包含的通知(什么时间)、切入点(什么地点) -->
            <!-- 前置通知 -->
            <aop:before method="beforeM" pointcut-ref="all_calc_method"/>
            <!-- 环绕通知 -->
            <aop:around pointcut-ref="all_calc_method" method="aroundM"/>
            <!-- 最终通知 -->
            <aop:after method="afterFinallyM" pointcut-ref="all_calc_method"/>
            <!-- 后置(返回值)通知 -->
            <aop:after-returning method="afterReturningM" pointcut-ref="all_calc_method" returning="retVal"/>
            <!-- 异常通知 -->
            <aop:after-throwing method="afterThrowing" pointcut-ref="all_calc_method" throwing="throwable"/>
        </aop:aspect>
    </aop:config>
</beans>
<aop>标签的属性
AOP 配置元素描述
aop : advisor定义 AOP 通知器
aop : after定义 AOP 后置通知(不管被通知方法是否执行成功)
aop : after-returing定义 AOP after-returing 通知
aop : after-throwing定义 AOP after-throwing 通知
aop : around定义 AOP 环绕通知
aop : aspect定义切面
aop : aspectj-autoproxy启动 @AspectJ 注解驱动的切面
aop : before定义 AOP 前置通知
aop : config顶层的 AOP 配置元素,大多数 aop : * 元素必须包含在 元素内
aop : declare-parents为被通知的对象引入额外接口,并透明的实现
aop : pointcut定义切点

最后通过获取Bean对象调用CalcImpl中的方法

package com.spring.demo;

import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * 测试spring aop
 */
public class App {
    
    public static void main(String[] args) {
        ApplicationContext config = new ClassPathXmlApplicationContext("applicationConfig.xml");//配置文件换成你自己的
        Calc calc =  config.getBean("calc",Calc.class);
        calc.add(800000000,900000000);
        calc.subtract(894739429,482942849);
        calc.multiply(843438,834993);
        calc.divide(843822,223);
    }
}

Annotation方式

开启自动织入支持
在xml中开启支持
<!-- 开启aop注解支持 -->
<aop:aspectj-autoproxy />

<!-- 开启注解支持,同时强制指定代理机制为cglib -->
<aop:aspectj-autoproxy proxy-target-class="true" />
通过注解开启注解支持
@Configuration
@ComponentScan(basePackages = "com.spring.simple.say")
// 开启注解支持,同时强制指定代理机制为cglib 
@EnableAspectJAutoProxy(proxyTargetClass = true)
public class MyAOPConf {}
定义切面类

添加@Aspect注解、@Component注解(由于@Aspect注解没有让Spring作为组件bean扫描的能力,所以我们需要额外添加@Component注解)

package com.spring.simple.say;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

import java.util.Arrays;

/**
 * 定义一个切面,负责收集方法调用的入参、出参(返回值)
 */
@Slf4j// 日志输出注解
@Aspect     // 表示该类是一个切面
@Component  // Aspect切面首先必须也是一个普通的bean
public class MethodInOutAspect {

    // 指定该方法是一个环绕通知,通知注解的参数代表引用一个切入点表达式
    @Around("com.spring.GlobalPointcut.say_all_method()")
    public Object aroundM(ProceedingJoinPoint joinPoint) throws Throwable {

        // 获取连接点方法的名称
        String methodName = joinPoint.getSignature().getName();

        // 获取连接点方法的参数
        Object[] args = joinPoint.getArgs();

        log.debug("[aroundM] "+methodName+"("+ Arrays.toString(args) +") 开始执行");
        Object retuVal = joinPoint.proceed();
        log.debug("[aroundM] "+methodName+"("+ Arrays.toString(args) +") 返回值: " + retuVal);
        return retuVal;
    }

    @AfterReturning(pointcut = "com.spring.GlobalPointcut.say_all_method()", returning = "ret")
    public Object afterRM(Object ret) {
        log.debug("[afterRM] 返回值: " + ret);
        return ret;
    }

}
定义切点

通过方法+注解的方式定义一个全局的切点类

package com.spring;

import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

/**
 * 定义系统中所有用到的切入点表达式
 */
@Component
public class GlobalPointcut {
    /**
     * 通过@Pointcut注解定义切入点表达式
     * 此处表达式含义:拦截com.spring.simple.say包下所有类(包括子包中所有类)中的所有方法
     */
    @Pointcut("execution(* com.lanou3g.spring.simple.say..*.*(..))")
    public void say_all_method() {}

}

定义通知

通过方法+注解

@AfterReturning(pointcut = "com.spring.GlobalPointcut.say_all_method()", returning = "ret")
public Object afterRM(Object ret) {
	System.out.println("[afterRM] 返回值: " + ret);
	return ret;
}

Spring AOP的应用

首先本章节用到了spring的jdbc工具类,需要将spring-jdbc依赖添加到maven

<dependency>
	<groupId>org.springframework</groupId>
	<artifactId>spring-jdbc</artifactId>
    <!-- 换成自己的spring框架的版本 -->
	<version>5.1.5.RELEASE</version>
</dependency>
声明式事务

代码示例

package com.transaction.bean;

import lombok.Getter;
import lombok.Setter;

/**
* @program: spring
*
* @description: 图书类型实体类
*
* @author: Bennett
*
* @create: 2019-06-19 21:56
*
* @version: 1.0
**/
//通过注解生成一些setter、getter方法
@Setter
@Getter
public class BookType {
    private Integer id;// 图书类型id(主键、自增)
    private String tname;// 图书类型

    public BookType() {    }

    public BookType(String tname) {
        this.tname = tname;
    }

    public BookType(Integer id, String tname) {
        this.id = id;
        this.tname = tname;
    }
}
package com.transaction.dao;

import com.transaction.bean.BookType;
import lombok.Getter;
import lombok.Setter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.BeanPropertyRowMapper;
import org.springframework.jdbc.core.JdbcTemplate;
import java.util.List;

/**
* @program: spring
*
* @description: dao层
*
* @author: Bennett
*
* @create: 2019-06-19 21:59
*
* @version: 1.0
**/

@Setter
@Getter
public class BookTypeDaoImpl {

    private JdbcTemplate jdbcTemplate;

    /**
     * 查询所有的图书类型
     * @return
     */
    public List<BookType> queryAll(){
        return jdbcTemplate.query("select * from booktype",new BeanPropertyRowMapper<BookType>(BookType.class));
    }
    
    /**
     * 插入一条图书类型
     * @param bookType
     * @return 影响的行数
     */
    public int insertBookType(BookType bookType){
        // 通过伪代码演示如果我们手写事务控制代码的套路
        // setAutoCommit(false);
        // beginTransaction
        // try{
        int result = jdbcTemplate.update("insert into booktype (tname) values (?)",bookType.getTname());
        // int retVal = 9 / 0;
        // commit;
        // } catch (Exception e) {
        // rollback;
        // }
        //
        return result;
    }
}
XML配置方式

首先需要要给数据库连接配置信息

jdbc.url=jdbc:mysql://localhost:3306/books
jdbc.driver=com.mysql.jdbc.Driver
jdbc.user=root
jdbc.password=root
jdbc.characterEncoding=utf8

之后就可以在spring的xml文件中配置相应的bean

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

    <!-- 通过xml方式配置spring声明事务 配置start -->
    <!-- 开启注解支持并给出扫描包路径 -->
    <context:component-scan base-package="com.spring.transaction" />
	<!-- 创建JdbcTemplate对象,并注入dataSource -->
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <property name="dataSource" ref="dataSource" />
    </bean>

    <bean id="bookTypeDao" class="com.spring.transaction.dao.BookTypeDaoImpl">
        <property name="jdbcTemplate" ref="jdbcTemplate" />
    </bean>

    <!-- 配置数据源 -->
    <context:property-placeholder location="classpath:jdbc.properties"/>
    <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
        <property name="driverClassName" value="${jdbc.driver}"/>
        <property name="url" value="${jdbc.url}"/>
        <property name="username" value="${jdbc.user}"/>
        <property name="password" value="${jdbc.password}"/>
        <property name="connectionProperties">
            <props>
                <prop key="charsetEncoding">${jdbc.characterEncoding}</prop>
            </props>
        </property>
    </bean>

    <!-- 初始化事务管理器 -->
    <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
        <property name="dataSource" ref="dataSource"/>
    </bean>

    <!-- 配置事务AOP通知 -->
    <tx:advice id="txAdvier" transaction-manager="txManager">
        <tx:attributes>
            <tx:method name="insert*" rollback-for="java.lang.Exception"/>
            <tx:method name="query*" read-only="true"/>
        </tx:attributes>
    </tx:advice>

    <!-- 定义AOP配置(将上面的通知和表达式组装起来) -->
    <aop:config>
        <aop:pointcut id="all_dao_method" expression="execution(* com.lanou3g.transaction.dao.*.*(..))"/>
        <aop:advisor advice-ref="txAdvier" pointcut-ref="all_dao_method"/>
    </aop:config>

    <!-- 通过xml方式配置spring声明事务 配置end -->
</beans>

< tx:advice / >默认配置如下:

  • 传播行为(propagation)是REQUIRED
  • 隔离级别(isolation level)是DEFAULT
  • 事务默认可读写(read-write)
  • 事务超时时间是数据库默认事务超时时间
  • unchecked异常RuntimeException异常触发事务回滚,checked异常Exception不会触发回滚

< tx:method / > 设置项:

属性是否必须默认值描述
nameYes 与事务属性关联的方法名称。支持*通配符(如, get*, handle*, on*Event
propagationNoREQUIRED事务传播行为
isolationNoDEFAULT事务隔离级别 仅当传播行为设置为 REQUIRED 或者 REQUIRES_NEW时有效.
timeoutNo-1事务超时时间(单位:秒). 仅当传播行为设置为 REQUIRED 或者 REQUIRES_NEW时有效.
read-onlyNofalse设置读写事务或者只读事务. 仅当传播行为设置为 REQUIRED或者 REQUIRES_NEW时有效.
rollback-forNo 设置多个可以触发事务回滚的异常(多个用英文逗号隔开). 如,com.foo.MyBusinessException,ServletException.
no-rollback-forNo 设置多个禁止触发事务回滚的异常(多个用英文逗号隔开).如,com.foo.MyBusinessException,ServletException.

经过配置后,我们就可以测试一下,是否可用

package com.lanou3g.transaction;

import com.lanou3g.transaction.bean.BookType;
import com.lanou3g.transaction.dao.BookTypeDaoImpl;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

import javax.sql.DataSource;
import java.util.List;

/**
* @program: spring
*
* @description: 测试数据库事务操作
*
* @author: Bennett
*
* @create: 2019-06-20 00:43
*
* @version: 1.0
**/

@Slf4j
public class TransactionLauncher {

    public static void main(String[] args) {
        ApplicationContext context = new ClassPathXmlApplicationContext("tx_conf.xml");
        testQuery(context);
        testTransaction(context);
        testQuery(context);
    }

    /**
    * 测试插入数据
    */
    private static void testTransaction(ApplicationContext context) {
        BookTypeDaoImpl bookTypeDao = context.getBean(BookTypeDaoImpl.class);
        int row = bookTypeDao.insertBookType(new BookType("武侠"));
        log.info("影响了" + row + "行.");
    }
	/**
    * 测试查询数据
    */
    private static void testQuery(ApplicationContext context) {
        BookTypeDaoImpl bookTypeDao = context.getBean(BookTypeDaoImpl.class);
        List<BookType> bookTypeList = bookTypeDao.queryAll();
        for (BookType bookType : bookTypeList){
            log.info("id: " + bookType.getId()+", tname: " + bookType.getTname());
        }
    }
}
Annotation配置方式

在XML中开启:

<!-- 开启事务注解扫描 -->
<!-- 如果定义的事务管理器名称就叫transactionManager(就是一个方法名),则此属性(transaction-manager)可以省略 -->
<tx:annotation-driven transaction-manager="txManager" />

注解方式开启:

/**
* @program: spring
*
* @description: 测试事务的入口类
*
* @author: Bennett
*
* @create: 2019-06-20 00:43
*
* @version: 1.0
**/

@Slf4j
@Configuration
@ComponentScan(basePackages = "com.transaction.annotation")
// 开启事务注解支持
@EnableTransactionManagement
public class TransactionLauncherByAnnotation {

    /**
     * 定义了一个JdbcTemplate类型的bean,是Spring提供给我们做jdbc CRUD操作的类
     * @param dataSource
     * @return
     */
    @Bean
    public JdbcTemplate jdbcTemplate(DataSource dataSource){
        return new JdbcTemplate(dataSource);
    }

    /**
     * 定义事务管理器bean
     * @param dataSource
     * @return
     */
    @Bean
    public PlatformTransactionManager txManager(DataSource dataSource){
        return new DataSourceTransactionManager(dataSource);
    }

    public static void main(String[] args) {
        ApplicationContext context = new AnnotationConfigApplicationContext(TransactionLauncherByAnnotation.class);
        testQuery(context);
        testTransaction(context);
        testQuery(context);
    }

    private static void testTransaction(ApplicationContext context) {
        BookTypeDaoImpl bookTypeDao = context.getBean(BookTypeDaoImpl.class);
        int row = bookTypeDao.insertBookType(new BookType("武侠"));
        log.info("影响了" + row + "行.");
    }

    private static void testQuery(ApplicationContext context) {
        BookTypeDaoImpl bookTypeDao = context.getBean(BookTypeDaoImpl.class);
        List<BookType> bookTypeList = bookTypeDao.queryAll();
        for (BookType bookType : bookTypeList){
            log.info("id: " + bookType.getId()+", tname: " + bookType.getTname());
        }
    }
}

在需要事务的方法上添加事务注解

/**
* @program: spring
*
* @description: dao层----注解配置版
*
* @author: Bennett
*
* @create: 2019-06-19 21:59
*
* @version: 1.0
**/

@Setter
@Getter
@Repository// 此注解和@Component作用一样,只是含有特殊语义(一般用来标注dao层的类)
public class BookTypeDaoImpl {

    @Autowired
    private JdbcTemplate jdbcTemplate;

    /**
     * 查询所有的图书类型
     * @return
     */
    // 通过@Transactional 开启事务、设置事务属性
    // 设置只读事务
    @Transactional(readOnly = true)
    public List<BookType> queryAll(){
        return jdbcTemplate.query("select * from booktype",new BeanPropertyRowMapper<BookType>(BookType.class));
    }

    /**
     * 插入一条图书类型
     * @param bookType
     * @return 影响的行数
     */
    // 设置异常回滚事务
    @Transactional(rollbackFor = {java.lang.Exception.class})
    public int insertBookType(BookType bookType){
        int row = jdbcTemplate.update("insert into booktype (tname) values (?)",bookType.getTname());
        // 模拟异常事务
        int x = 3/0;
        return row;
    }
}

@Transactional属性说明

  • value: 指定特定的事务管理器,默认是transactionManager
  • 其他属性和xml中的的属性类似
Spring事务的隔离级别和传播行为
七种传播行为
属性说明备注
PROPAGATION_REQUIRED支持当前事务,如果当前没有事务,就新建一个事务。这是最常见的选择常用
PROPAGATION_SUPPORTS支持当前事务,如果当前没有事务,就以非事务方式执行
PROPAGATION_MANDATORY支持当前事务,如果当前没有事务,就抛出异常
PROPAGATION_REQUIRES_NEW新建事务,如果当前存在事务,把当前事务挂起常用
PROPAGATION_NOT_SUPPORTED以非事务方式执行操作,如果当前存在事务,就把当前事务挂起
PROPAGATION_NEVER以非事务方式执行,如果当前存在事务,则抛出异常
PROPAGATION_NESTED如果当前存在事务,则在嵌套事务内执行。如果当前没有事务,则进行与PROPAGATION_REQUIRED类似的操作
五种隔离级别
隔离级别说明
ISOLATION_DEFAULT这是一个PlatfromTransactionManager默认的隔离级别,使用数据库默认的事务隔离级别.
ISOLATIONREADUNCOMMITTED这是事务最低的隔离级别,它允许另外一个事务可以看到这个事务未提交的数据。 这种隔离级别会产生脏读,不可重复读和幻像读。
ISOLATIONREADCOMMITTED保证一个事务修改的数据提交后才能被另外一个事务读取。另外一个事务不能读取该事务未提交的数据。 这种事务隔离级别可以避免脏读出现,但是可能会出现不可重复读和幻像读。
ISOLATIONREPEATABLEREAD这种事务隔离级别可以防止脏读,不可重复读。但是可能出现幻像读。 它除了保证一个事务不能读取另一个事务未提交的数据外,还保证了避免下面的情况产生(不可重复读)。
ISOLATION_SERIALIZABLE这是花费最高代价但是最可靠的事务隔离级别。事务被处理为顺序执行。 除了防止脏读,不可重复读外,还避免了幻像读。

关键词:

幻读(虚读)

事务1读取记录时事务2增加了记录并提交,事务1再次读取时可以看到事务2新增的记录;
通俗的说,幻读就是指在一个事务内读取了别的事务插入的数据,导致前后读取不一致(insert)

不可重复读取

事务1读取记录时,事务2更新了记录并提交,事务1再次读取时可以看到事务2修改后的记录;
在一个事务内读取表中的某一行数据,多次读取结果不同.一个事务读取到了另一个事务提交后的数据.

脏读

事务1更新了记录,但没有提交,事务2读取了更新后的行,然后事务T1回滚,现在T2读取无效。
通俗的说,脏读就是指一个事务读取了一个未提交事务的数据

其他业务场景
  • 统一异常处理

  • 日志记录

Spring AOP的代理机制

被代理的类

/**
* @program: spring
*
* @description: 模拟业务实现代码
*
* @author: Bennett
*
* @create: 2019-06-19 09:26
*
* @version: 1.0
**/

@Slf4j
public class MyRandom {

    public int getRandomNum(){
        int result = new Random().nextInt(10000);
        log.info("生成随机数: " + result);
        return result;
    }
}

public interface ICar {
    String getFeature();
}

public class Benz implements ICar {
    @Override
    public String getFeature(){
        return "空挡滑行";
    }
}
静态代理

首先先看一下静态的代理,静态代理其实就是通过继承、实现接口、两个类的组合等纵向的方式来完成事务

/**
* @program: spring
*
* @description: 静态代理
*
* @author: Bennett
*
* @create: 2019-06-19 09:44
*
* @version: 1.0
**/

@Slf4j
public class StaticProxy {

    public static void main(String[] args) {
//        MyRandom myRandom = new StaticProxyByExtends();

//        StaticProxyByComb myRandom = new StaticProxyByComb();
//        myRandom.setMyRandom(new MyRandom());
//        log.info("返回值: " + myRandom.getRandomNum());

        ICar iCar = new StaticProxyByInterface();
        ((StaticProxyByInterface) iCar).setICar(new Benz());
        log.info(iCar.getFeature());
    }

    /**
     * 通过继承实现静态代理
     */
    @Slf4j
    static class StaticProxyByExtends extends MyRandom {
        @Override
        public int getRandomNum() {
            log.info("开始执行");
            int ret =  super.getRandomNum();
            log.info("执行结束,返回值: " + ret);
            return ret;
        }
    }

    /**
     * 通过接口方式实现静态代理
     */
    @Setter
    @Slf4j
    static class StaticProxyByInterface implements ICar {
        private ICar iCar;

        @Override
        public String getFeature() {
            log.debug("开始执行");
            String ret = iCar.getFeature();
            log.debug("执行结束,返回值: " + ret);
            return ret;
        }
    }

    /**
     * 通过组合的方式实现静态代理
     */
    @Setter
    @Slf4j
    static class StaticProxyByComb{
        private MyRandom myRandom;
        
        public int getRandomNum() {
            log.info("开始执行");
            int ret =  myRandom.getRandomNum();
            log.info("执行结束,返回值: " + ret);
            return ret;
        }

    }
}
动态代理
JDK动态代理
/**
 * @program: spring
 * @description: jdk动态代理,只适合代理实现了接口的类
 * @author: Bennett
 * @create: 2019-06-19 09:41
 * @version: 1.0
 **/
@Slf4j
public class JDKDynamicProxy {

    public static<T> T getProxy(final T t) {
        return (T) Proxy.newProxyInstance(t.getClass().getClassLoader(), t.getClass().getInterfaces(), new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                log.info("开始执行");
                // 调用被代理类的方法
                Object ret = method.invoke(t, args);
                log.info("执行结束,返回值: " + ret);
                return ret;
            }
        });
    }
}
Cglib动态代理
/**
* @program: spring
*
* @description: cglib动态代理,特点:无论被代理类是否实现接口都可以代理
*
* @author: Bennett
*
* @create: 2019-06-19 09:43
*
* @version: 1.0
**/

@Slf4j
public class CglibDynamicProxy {

    // Cglib代理的核心实现类,设置将来动态生成的代理对象的所有行为
    @Setter
    @Getter
    private Enhancer enhancer;

    /**
    * @Description: 
    * @Param: [superClass 指定代理类要继承的父类
     *       , interfaces 指定代理类要实现的接口
     *       , callback 指定代理类里要附加的逻辑代码]
    * @Return: 
    * @Author: Bennett
    * @Date: 2019/6/19
    **/
    private CglibDynamicProxy(Class superClass, Class[] interfaces, Callback callback){
        enhancer = new Enhancer();
        // 设置父类类型
        enhancer.setSuperclass(superClass);
        // 由于框架中没有对类的接口数为0时判断,因此需要我们自己加上判断(算是spring的小bug吧?)
        if (interfaces != null && interfaces.length < 1){
            interfaces = null;
        }
        // 设置类实现的接口
        enhancer.setInterfaces(interfaces);
        // 设置回调函数
        enhancer.setCallback(callback);
        // 设置不使用缓存
        enhancer.setUseCache(false);

    }

    public static <T> T getProxy(final T t) {
        System.out.println("t.getClass(): " + t.getClass());
        CglibDynamicProxy proxy = new CglibDynamicProxy(t.getClass(), t.getClass().getInterfaces(), new MethodInterceptor() {
            @Override
            public Object intercept(Object o, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
                log.debug("开始执行");
                // 调用被代理类中的方法
                Object ret = method.invoke(t,args);
                log.debug("执行结束");
                return ret;
            }
        });
        return (T) proxy.getEnhancer().create();
    }
}

测试动态代理

/**
* @program: spring
*
* @description: 测试动态代理
*
* @author: Bennett
*
* @create: 2019-06-19 10:30
*
* @version: 1.0
**/

@Slf4j
public class DynamicProxyLauncher {

    public static void main(String[] args) {
        // jdk动态代理
        ICar iCar = JDKDynamicProxy.getProxy(new Benz());
        log.info("JDKDynamicProxy: " + iCar.getFeature());

        // Cglib动态代理
        // Cglib代理实现了接口的类
        ICar iCar1 = CglibDynamicProxy.getProxy(new Benz());
        log.info("CglibDynamicProxy: " + iCar1.getFeature());

        // Cglib代理未实现接口的类
        MyRandom random = CglibDynamicProxy.getProxy(new MyRandom());
        log.info("CglibDynamicProxy: " + random.getRandomNum());
        log.info("MyRandom代理类的类型: " + random.getClass());
        log.info("MyRandom代理类继承了哪个类: " + random.getClass().getSuperclass());
        log.info("MyRandom代理类继承了哪些接口: " + random.getClass().getInterfaces());
    }
}

参考文章:

作者:zejian_
来源:https://blog.csdn.net/javazejian/article/details/56267036