Spring - AOP

Spring - AOP

一、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相关的概念

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【重点】

快速入门

1) 需求描述
  • 有目标类UserServiceImpl,有通知类或增强类MyAdvice
  • 使用XML方式AOP,对目标类UserServiceImpl的方法进行增强
2) 步骤分析
  1. 创建maven项目,导入AOP相关的依赖坐标
  2. 创建目标类(被增强的类,内部有切入点(要有方法)),创建通知类(内部有增强的方法代码)
  3. 修改配置文件:
    1. 把目标类和通知类都配置成为bean对象
    2. 配置切入点和通知方法(增强方法)的织入关系:配置切面
  4. 测试代码
3) 入门实现
1. 创建maven项目,导入坐标
<dependencies>
        
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-contextartifactId>
            <version>5.0.2.RELEASEversion>
        dependency>
        
        <dependency>
            <groupId>org.aspectjgroupId>
            <artifactId>aspectjweaverartifactId>
            <version>1.9.4version>
        dependency>
        
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-testartifactId>
            <version>5.0.2.RELEASEversion>
        dependency>
        
        <dependency>
            <groupId>junitgroupId>
            <artifactId>junitartifactId>
            <version>4.12version>
        dependency>
    dependencies>
2. 创建目标类和通知类
  • 目标类:com.mingye.aop.UserServiceImpl
package com.mingye.service;

public interface UserService {
    void add();

    void update();
}


package com.mingye.service.impl;

import com.mingye.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.mingye.aop.MyAdvice
package com.mingye.advice;

import org.aspectj.lang.ProceedingJoinPoint;

public class MyAdvice {

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

}
3. 修改配置文件
  1. 把目标类和通知类都配置到Spring配置文件中
  2. 配置切入和通知方法(增强方法)的织入关系

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


    
    <bean id="us" class="com.mingye.service.impl.UserServiceImpl"/>
    <bean id="myAdvice" class="com.mingye.advice.MyAdvice"/>

    
    <aop:config>
        <aop:aspect ref="myAdvice">
            <aop:before method="print" pointcut="execution(* com.mingye.service.impl.UserServiceImpl.add())"/>
        aop:aspect>
    aop:config>


beans>

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

4. 测试代码
/*
    AOP 入门:
        1. 导入依赖

        2. 定义接口和实现类

        3. 定义扩展的功能类 MyAdvice,提供一个方法即可

        4. 配置AOP

 */

@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();
    }
}

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.mingye.service
    • 可以写..:表示当前包里所有后代类、后代包。比如com..service
    • *:表示任意字符。比如:com.it*, com.*
    • … : 表示多个任意字符
  • 类名:
    • 可以指定类名。比如:UserServiceImpl
    • * 表示任意字符。比如:*ServiceImpl*
  • 方法名:
    • 可以指定方法名
    • * 表示任意字符。比如:save**
  • 参数列表:
    • 可以指定类型。比如:String,Integer表示第一个参数是String,第二个参数是Integer类型
    • *表示任意字符。比如:
      • String, * 表示第一个参数是String,第二个参数是任意类型
      • Str*, Integer表示第一个参数类型Str开头,第二个参数是Integer类型
    • 可以使用..表示任意个数、任意类型的参数
示例
execution(public void com.mingye.dao.impl.UserDao.save())
execution(void com.mingye.dao.impl.UserDao.*(..))
execution(* com.mingye.dao.impl.*.*(..))
execution(* com.mingye.dao..*.*(..))
execution(* *..*.*(..)) --不建议使用
       
    <aop:config>
        <aop:aspect ref="myAdvice">
            

            
            
            
            
            
            
            
            
            

            
            

            
            <aop:before method="print" pointcut="execution(* com.mingye..*.*(..))"/>
        aop:aspect>
    aop:config>
2) 通知的种类
通知的语法
<aop:通知类型 method="通知中的方法" pointcut="切点表达式">aop:通知类型>
通知的类型
名称 标签 说明
前置通知 通知方法在切入点方法之前执行
后置通知 在切入点方法正常执行之后,执行通知方法
异常通知 在切入点方法抛出异常时,执行通知方法
最终通知 无论切入点方法是否有异常,最终都执行通知方法
环绕通知 通知方法可以在切入点方法之前、之后都执行
通知示例

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

  • 前置通知

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

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

    • 通知方法定义
           /*
            环绕增强比较特殊一些,它需要我们在增强的方法里面手动调用目标方法;其实起到了特殊作用:
            1.掌控方法的执行,起到拦截的效果
            2.给方法传参数
            3.也可以拿到方法的返回值
         */
        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.mingye.service..*.*())"/>
    
  • 异常抛出通知

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

    • 通知方法定义
    public void after(){
        System.out.println("最终通知");
    }
    
    • xml配置
        
        <aop:config>
            <aop:aspect ref="myAdvice">
    
                
                <aop:pointcut id="pointCut01" expression="execution(* com.mingye..*.*(..))"/>
    
    
    
    
                
                
                <aop:before method="before" pointcut-ref="pointCut01"/>
    
                
                 
                 <aop:after-returning method="afterReturning" pointcut-ref="pointCut01"/>
    
                
               
    
                
                
    
                
               
            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. 在增强类身上,需要打上注解 @Aspect ,让其成为增强类|对象
    3. 在增强类的方法上,打注解@Before | @AfterReturing | … 表示想要增强谁。
  3. 在配置文件中,开启组件扫描和AOP的自动代理(自动装配)
  4. 测试
3) 入门实现
1. 创建maven项目,导入坐标
  • 注意:需要增加AOP的实现包:aspectjweaver
<dependencies>
        
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-contextartifactId>
            <version>5.0.2.RELEASEversion>
        dependency>
        
        <dependency>
            <groupId>org.aspectjgroupId>
            <artifactId>aspectjweaverartifactId>
            <version>1.9.4version>
        dependency>
        
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-testartifactId>
            <version>5.0.2.RELEASEversion>
        dependency>
        
        <dependency>
            <groupId>junitgroupId>
            <artifactId>junitartifactId>
            <version>4.12version>
        dependency>
    dependencies>
2. 创建目标类,创建通知类
  1. 使用注解标注两个类,配置成为bean对象

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

    • 目标类com.mingye.aop.Target
    package com.mingye.service;
    
    public interface UserService {
    
        void add();
        void update();
    }
    
    
    package com.mingye.service.impl;
    
    import com.mingye.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.mingye.aop.MyAdvice
    /*
        MyAdvice是增强类,它需要做:
            1. 把自己交给spring管理 , 打上注解@Component
            2. 表示这个类是切面增强类,专门用来做增强的,打注解 @Aspect
            3. 把什么方法用来做什么增强(增强谁),就在这个方法上面打注解
                前置增强 === @Before
                后置增强 === @AfterReturning
     */
    
    @Component
    @Aspect
    public class MyAdvice {
    
        @Before("execution(* com.mingye..*.*(..))")
        public void print(){
            System.out.println("打印日志~");
        }
    
    
        ...
    }
    
    
4. 开启组件扫描和AOP自动代理
  • applicationContext.xml

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

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

    
    <aop:aspectj-autoproxy/>
beans>
5. 测试
package com.mingye.test;

import com.mingye.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. 给他们都打上注解

    1. UserServiceImpl : @Service
      2. MyAdvice : @Component @Aspect
      1. 方法上面打上前置或者后置的注解

    2. 在applicationContext.xml中打开开关

        
        <context:component-scan base-package="com.mingye"/>
    
        
        <aop:aspectj-autoproxy/>
    

AOP详解

1) 通知的种类
通知的语法
@通知注解("切入点表达式")
通知的类型
名称 注解 说明
前置通知 @Before 通知方法在切入点方法之前执行
后置通知 @AfterRuturning 通知方法在切入点方法之后执行
异常通知 @AfterThrowing 通知方法在抛出异常时执行
最终通知 @After 通知方法无论是否有异常,最终都执行
环绕通知 @Around 通知方法在切入点方法之前、之后都执行
  • 注意:
    • 注解方式配置的通知,执行顺序是:前置->最终->后置/异常
    • 如果想要指定执行的顺序,就使用环绕通知 , 因为环绕增强是由我们手动控制的。
2) 切点表达式的抽取
  • 同xml的AOP一样,当多个切面的切入点表达式相同时,可以将切入点表达式进行抽取;
  • 抽取方法是:
    • 在增强类(切面类,即被@Aspect标的类)上增加一个额外的方法,在方法上使用@Pointcut注解定义切入点表达式,
    • 在增强注解中引用切入点表达式所在的方法
  • 示例:
package com.mingye.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.mingye..*.*(..))")
    public void print(){
        System.out.println("打印日志~!");
    }

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

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


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

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

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

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

    /*
        环绕增强比较特殊一些,它需要我们在增强的方法里面手动调用目标方法。
     */
    //@Around("execution(* com.mingye..*.*(..))")
    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

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

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

  • 核心配置类
package com.mingye.config;

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

@Configuration
@ComponentScan("com.mingye")
@EnableAspectJAutoProxy
public class AppConfig {
}
  • 增强类 : MyAdvice
package com.mingye.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.mingye..*.*(..))")
    public void print(){
        System.out.println("打印日志~");
    }
}

  • UserService接口
package com.mingye.service;

public interface UserService {

    void add();

    void update();
}

  • UserServiceImpl实现类
package com.mingye.service.impl;

import com.mingye.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方法~!~");
    }
}


  • 单元测试
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = AppConfig.class)
public class TestUserServiceImpl02 {

    @Autowired
    private UserService us;

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

    }
}

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

问题导入

能不能描述一下环绕通知里面的实现步骤?

1.1 需求和分析

需求:任意业务层接口执行均可显示其执行效率(执行时长)

分析:

​ ①:业务功能:业务层接口执行前后分别记录时间,求差值得到执行效率
​ ②:通知类型选择前后均可以增强的类型——环绕通知

1.2 代码实现

【前置工作】环境准备
  1. Spring整合mybatis对spring_db数据库中的Account进行CRUD操作

  2. Spring整合Junit测试CRUD是否OK。

  3. 在pom.xml中添加aspectjweaver切入点表达式依赖

  4. … …

【第一步】编写通知类
@Component
@Aspect
public class MyAdvice02 {

    //=======================使用环绕增强测试目标方法调用一万次耗费时间===============================

    @Around("execution( * com.mingye..*.*(..))")
    public void runSpeed(ProceedingJoinPoint joinPoint) throws Throwable {


        //0. 通过方法签名得到方法的信息
        Signature signature = joinPoint.getSignature();

        //得到接口名称
        String typeName = signature.getDeclaringTypeName();

        //得到方法名字
        String methodName = signature.getName();

        //1. 记录时间
        long start = System.currentTimeMillis();

        //2. 调用目标方法
        for (int i = 0; i < 10000; i++) {
            joinPoint.proceed(joinPoint.getArgs());
        }

        //3. 记录时间
        long end = System.currentTimeMillis();

        //4. 计算差值

        System.out.println("调用 "+typeName+" 的 " + methodName+ " 一万次,耗费时间:" + (end - start));

    }
}

【第二步】在SpringConfig配置类上开启AOP注解功能
@Configuration
@ComponentScan("com.mingye")
@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层技术要使用JdbcTemplate,不能使用dbutils **
<dependencies>
        
        <dependency>
            <groupId>mysqlgroupId>
            <artifactId>mysql-connector-javaartifactId>
            <version>5.1.47version>
        dependency>
        
        <dependency>
            <groupId>c3p0groupId>
            <artifactId>c3p0artifactId>
            <version>0.9.1.2version>
        dependency>
        
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-contextartifactId>
            <version>5.0.2.RELEASEversion>
        dependency>
        
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-jdbcartifactId>
            <version>5.0.2.RELEASEversion>
        dependency>
        
        <dependency>
            <groupId>org.aspectjgroupId>
            <artifactId>aspectjweaverartifactId>
            <version>1.9.4version>
        dependency>
        
        <dependency>
            <groupId>org.springframeworkgroupId>
            <artifactId>spring-testartifactId>
            <version>5.0.2.RELEASEversion>
        dependency>
        
        <dependency>
            <groupId>junitgroupId>
            <artifactId>junitartifactId>
            <version>4.12version>
        dependency>

        <dependency>
            <groupId>org.projectlombokgroupId>
            <artifactId>lombokartifactId>
            <version>1.18.18version>
        dependency>

    dependencies>
2) 创建dao,Service:
  • AccountDaoAccountDaoImpl
package com.mingye.dao;

public interface AccountDao {

    void kouqian(String from , int moeny);
    void jiaqian(String to , int moeny);
}


package com.mingye.dao.impl;

import com.mingye.dao.AccountDao;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.stereotype.Repository;

/*
    1. 把自己交给spring管理
    2. 让spring注入进来jdbctemplate
 */

@Repository
public class AccountDaoImpl implements AccountDao {

    @Autowired
    private JdbcTemplate template;

    public void kouqian(String from, int money) {
        String sql="update t_account set money = money - ? where name = ? ";
        template.update(sql , money , from);
    }

    public void jiaqian(String to, int money) {
        String sql="update t_account set money = money + ? where name = ? ";
        template.update(sql , money , to);
    }
}


  • AccountServiceAccountServiceImpl
package com.mingye.service;

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


package com.mingye.service.impl;

import com.mingye.dao.AccountDao;
import com.mingye.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
    <context:component-scan base-package="com.mingye"/>

    <context:property-placeholder location="db.properties"/>

    
    <bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
        <property name="driverClassName" value="${db.driver}"/>
        <property name="url" value="${db.url}"/>
        <property name="username" value="${db.username}"/>
        <property name="password" value="${db.password}"/>
    bean>

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

    <bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
        <property name="basePackage" value="com.mingye.dao"/>
    bean>

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

    
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            <tx:method name="transfer" isolation="DEFAULT" timeout="-1" read-only="false" propagation="REQUIRED"/>
            <tx:method name="add*" isolation="DEFAULT" timeout="-1" read-only="false" propagation="REQUIRED"/>
            <tx:method name="update*" isolation="DEFAULT" timeout="-1" read-only="false" propagation="REQUIRED"/>
            <tx:method name="delete*" isolation="DEFAULT" timeout="-1" read-only="false" propagation="REQUIRED"/>
            <tx:method name="find*" isolation="DEFAULT" timeout="-1" read-only="true" propagation="REQUIRED"/>
        tx:attributes>
    tx:advice>

   
    <aop:config>
        <aop:advisor advice-ref="txAdvice" pointcut="execution( * com.mingye..*.*(..))"/>
    aop:config>
beans>
4) 功能测试
package com.mingye.test;

import com.mingye.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. 配置切面,把事务通知织入到转账方法中
功能实现

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


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

    
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <constructor-arg name="dataSource" ref="dataSource"/>
    bean>

    
    <context:property-placeholder location="db.properties"/>
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${driverClass}"/>
        <property name="jdbcUrl" value="${jdbcUrl}"/>
        <property name="user" value="${user}"/>
        <property name="password" value="${password}"/>
    bean>

    


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

    
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
        <tx:attributes>
            
            
            <tx:method name="*" isolation="DEFAULT" propagation="REQUIRED" read-only="false" timeout="-1"/>


            
            <tx:method name="save*" isolation="DEFAULT" propagation="REQUIRED" read-only="false"/>

            
            <tx:method name="edit*" isolation="DEFAULT" propagation="REQUIRED" read-only="false"/>

            
            <tx:method name="delete*" isolation="DEFAULT" propagation="REQUIRED" read-only="false"/>

            
            <tx:method name="query*" isolation="DEFAULT" propagation="REQUIRED" read-only="true"/>

            
            <tx:method name="find*" isolation="DEFAULT" propagation="REQUIRED" read-only="true"/>
        tx:attributes>
    tx:advice>

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

beans>
3) 配置详解
aop:config:切面配置

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

<aop:config>
    <aop:advisor advice-ref="txAdvice"  
                 pointcut="execution(* com.mingye.service.impl..*.*(..))"/>
aop:config>
  • aop:config:aop提供的用于配置切面的标签
  • aop:advisor:Spring提供的专门用于配置事务的,作用类似于aop:aspect
    • advice-ref:要引入的通知配置,必须要引用所配置的事务通知
    • pointcut:切入点表达式
tx:advice:事务通知配置
  • 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>
        
        <tx:method name="transfer" isolation="REPEATABLE_READ" propagation="REQUIRED" read-only="false"/>    
        
        
        <tx:method name="save*" isolation="DEFAULT" propagation="REQUIRED" read-only="false"/>
        
        
        <tx:method name="edit*" isolation="DEFAULT" propagation="REQUIRED" read-only="false"/>
        
        
        <tx:method name="delete*" isolation="DEFAULT" propagation="REQUIRED" read-only="false"/>
        
        
        <tx:method name="query*" isolation="DEFAULT" propagation="REQUIRED" read-only="true"/>
        
        
        <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的注解配置,对银行转账功能,进行事务控制
实现步骤
  1. 在需要事务控制的方法/类上增加注解@Transactional
@Transactional //类里面的所有方法都有事务
@Service
public class AccountServiceImpl implements AccountService {
}
  1. 在配置文件applicationContext.xml中修改配置
  • 配置事务管理器
  • 开启事务的注解驱动
 
    <tx:annotation-driven transaction-manager="tm"/>
功能实现
  • 修改银行转账的Service类:AccountServiceImpl
package com.mingye.service.impl;

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

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

        2. 在xml里面打开注解的开关
            
 */

//@Transactional  //1. 类上打注解
@Service
public class AccountServiceImpl implements AccountService {

    @Autowired
    private AccountDao dao;


    /**
     * 转账
     * @param from
     * @param to
     * @param money
     */
    @Transactional(isolation = Isolation.DEFAULT , propagation = Propagation.REQUIRED , readOnly = false , timeout = -1)
//    @Transactional
    public void transfer(String from, String to, int money) {

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

        //int a = 1 / 0 ;

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

  • 修改配置文件applicationContext.xml
    
    <context:component-scan base-package="com.mingye"/>

    
    <bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
        <constructor-arg name="dataSource" ref="dataSource"/>
    bean>

    
    <context:property-placeholder location="db.properties"/>
    <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
        <property name="driverClass" value="${driverClass}"/>
        <property name="jdbcUrl" value="${jdbcUrl}"/>
        <property name="user" value="${user}"/>
        <property name="password" value="${password}"/>
    bean>

    

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


    
    

    
    <tx:annotation-driven />
beans>
2) 配置详解
注解@Transactional
  • 加在 需要进行事务控制的方法/类上,用于代替xml配置中的tx:advice和事务切面的aop:config
  • isolation属性:设置事务的隔离级别,从枚举Isolation中取值
  • propagation属性:设置事务的传播特性,从枚举Propagation中取值
  • readOnly属性:设置是否是只读的
  • timeout属性:设置超时时间,单位秒。-1表示不限制
开启事务的注解驱动

XML方式

  • 使用注解进行事务管理,必须要在applicationContext.xml中开启 事务的注解驱动,否则无效

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

<tx:annotation-driver/>

纯注解方式

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

  • 配置示例


@Configuration
@ComponentScan("com.mingye")
@PropertySource("db.properties")
@EnableTransactionManagement
public class AppConfig {

    @Value("${driverClass}")
    private String driverClass;

    @Value("${jdbcUrl}")
    private String jdbcUrl;

    @Value("${user}")
    private String user;

    @Value("${password}")
    private String password;


    //创建jdbctemplate
    @Bean
    public JdbcTemplate template(DataSource dataSource){
        return new JdbcTemplate(dataSource);
    }


    @Bean
    public DataSource dataSource() throws PropertyVetoException {
        ComboPooledDataSource ds = new ComboPooledDataSource();
        ds.setDriverClass(driverClass);
        ds.setJdbcUrl(jdbcUrl);
        ds.setUser(user);
        ds.setPassword(password);
        return ds;
    }

    @Bean
    public PlatformTransactionManager transactionManager(DataSource dataSource){
        DataSourceTransactionManager tm = new DataSourceTransactionManager();
        tm.setDataSource(dataSource);
        return tm;
    }

}

  • 单元测试
package com.mingye.test;

import com.mingye.config.AppConfig;
import com.mingye.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.mingye"/>
  • 哪个方法需要事务管理,就在哪个方法上加注解:@Transactional

你可能感兴趣的:(spring,java,spring,boot)