Spring框架之SpringAOP+声明式事务Tx

Spring框架系列之SpringAOP+声明式事务Tx



前言

框架的理解:在技术的基础上封装,实现简化操作,更加专注于写业务
AOP本质上:是简化动态代理的实现!AOP框架内部封装的就是动态代理技术,我们不需要写代理,只需写一些配置文件告诉框架哪个类需要动态代理即可!了解代理模式及两种动态代理区别是重点。
声明式事务:对比编程式事务的优势,是对AOP的再次封装,Spring-tx提供了事务增强。

一、Spring AOP面向切面编程

解决业务无关,却为业务模块所共同调用的逻辑或责任封装起来,减少重复代码
Spring AOP框架,基于AOP编程思维,封装动态代理技术,简化动态代理技术实现的框架
主要配置实现过程:声明切面类(通过一些注解就可以获取到目标类,并编写增强方法)→在xml / 配置类中开启aspectj注解支持
获取目标类bean:还是正常获取,只不过应用切面的bean,Spring AOP会为其创建代理对象,虽然看不见但是是实际存在的!!

1.1 场景设定和问题复现

1、场景需求
如声明接口(4个方法:+-×÷),有个类实现该接口,现在出现一个新需求:在每个方法中添加控制台输出,输出参数和输出计算后的返回值!以下是一个类实现接口的几个方法定义(计算器)——无代理方式实现

add(int i, int j)sub(int i, int j)mul(int i, int j)div(int i, int j)
核心操作前日志
result = i + j
核心操作后日志
核心操作前日志
result = i - j
核心操作后日志
核心操作前日志
result = i × j
核心操作后日志
核心操作前日志
result = i ÷ j
核心操作后日志

2、代码问题分析
缺陷:对核心业务有干扰;附加代码功能重复,分散在各个业务功能方法中,冗余且不方便统一维护!
解决思路:解耦,即把附加功能业务(即重复的代码业务)从业务功能代码中抽取出来,并且==[动态插入]==到每个业务方法!
技术难点:提取重复附加功能代码到一个类中可以实现,但是如何将代码动态插入到各个方法中?

1.2 解决技术代理

1、代理模式理解:代理——目标
23种设计模式中的一种,属于结构型模式。作用通过一个代理类间接调用目标方法,而不是直接对目标方法调用。从而实现解耦,即让不属于目标方法核心逻辑的代码从目标方法中剥离出来。调用目标方法时先调用代理对象的方法,减少对目标方法的调用和打扰同时让附加功能能够集中在一起也有利于同一维护。
总而言之,要调用目标方法和目标方法返回值给调用者都需要经过代理对象!非核心业务都交给代理!

  • 代理:将非核心逻辑剥离出来以后,封装这些非核心逻辑的类、对象、方法。(中介)。名词:扮演代理这个角色的类、对象、方法;动词:指做代理这个动作,或这项工作。代理实现方式动态(实际使用)、静态代理技术。
  • 目标:被代理套用了核心逻辑代码的类、对象、方法。(房东)
  • 生活中的代理:房产中介、太监、秘书、经纪人
    使用代理场景
    2、静态代理——找中介里面的人,适用于不同的功能实现不同的代理类
    为每一个目标类创建一个代理类
    关于代理类的设置:继承与目标类同样的接口,在代理类中将被代理的目标对象声明为成员变量,实现接口的所有方法,在方法中为目标方法的核心业务都添加非核心业务功能,核心功能使用目标类调用相应的方法!
    外部调用:只需调用代理的方法,通过代理再调用目标对象的核心业务逻辑!
    理解:目标类和代理类都实现接口,因此在这两个类中都需要实现同样的方法,在目标类中实现核心业务逻辑→在代理类中实现非核心业务逻辑并调用目标类的核心业务逻辑→调用代理类的方法,通过代理类再调用目标方法。
    缺点:代码串联、功能分散、重复、代理的非核心代码也很冗余、没有统一管理!如日志功能,将来需要在其他地方附加日志,还得再声明静态代理类。

代理类的创建如下

//Calculator: 目标对象和代理对象共同实现的接口
public class CalculatorStaticProxy implements Calculator {
    // 将被代理的目标对象声明为成员变量
    private Calculator target;
    public CalculatorStaticProxy(Calculator target) {
        this.target = target;
    }
    @Override
    public int add(int i, int j) {
        // 附加功能由代理类中的代理方法来实现
        System.out.println("参数是:" + i + "," + j);
        // 通过目标对象来实现核心业务逻辑
        int addResult = target.add(i, j);
        System.out.println("方法内部 result = " + result);
        return addResult;
    }

3、动态代理——找中介机构,即在代理工厂中根据目标类的接口动态生成代理对象(同样功能)
进一步需求:对于多个类会用到的同样的非核心业务功能,针对每个类创建同样的代理类太麻烦!比如日志功能,很多目标类中都需要同样的非核心业务功能。那么可以将日志功能集中到一个代理类工厂中,将来有任何日志需求,都可以通过这一个代理工厂得到相应目标类的代理类(都是一样的非核心功能)来实现。
一个代理工厂:根据不同的目标类来动态生成不同的代理类!——注意非核心的功能是一样的,比如都是日志功能——在不同的目标类中用同样的非核心功能
如果不使用代理工厂,那么对于一个非核心功能多个目标类都需要时,那么就需要多个代理类来实现,这样就比较重复!
技术分类:

  • cglib:第三方的已经继承到spring包下,不要求目标类实现接口,直接根据目标类生成一个子类对象(通过继承被代理的目标类实现代理)(认干爹)
    一般情况下,有接口用JDK动态代理、没有接口用cglib!
  • JDK动态代理:JDK原生实现方式,要求被代理的目标类必须实现接口,它是根据目标类的接口动态生成一个代理对象。代理对象和目标对象有相同的接口(代理类和目标类的关系:拜把子)
    使用方式:定义JDK代理工厂→根据目标类对象得到代理工厂对象→使用代理工厂.getProxy()得到代理对象,工厂定义返回值是泛型因此需要强制转换,并使用目标对象的代理接值
    JDK动态代理技术实现

JDK动态代理工厂的定义

//代理类工厂
public class ProxyFactory {
    private Object target;
    public ProxyFactory(Object target) {
        this.target = target;
    }
    public Object getProxy(){
        /**
         * newProxyInstance():创建一个代理实例
         * 其中有三个参数:
         * 1、classLoader:加载动态生成的代理类的类加载器
         * 2、interfaces:目标对象实现的所有接口的class对象所组成的数组
         * 3、invocationHandler:设置代理对象实现目标对象方法的过程,即代理类中如何重写接口中的抽象方法
         */
        ClassLoader classLoader = target.getClass().getClassLoader();
        Class<?>[] interfaces = target.getClass().getInterfaces();
        InvocationHandler invocationHandler = new InvocationHandler() {
            @Override
            public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
                /**
                 * proxy:代理对象
                 * method:代理对象需要实现的方法,即其中需要重写的方法
                 * args:method所对应方法的参数
                 */
                Object result = null;
                try {
                    System.out.println("[动态代理][日志] "+method.getName()+",参数:"+ Arrays.toString(args));
                    result = method.invoke(target, args);
                    System.out.println("[动态代理][日志] "+method.getName()+",结果:"+ result);
                } catch (Exception e) {
                    e.printStackTrace();
                    System.out.println("[动态代理][日志] "+method.getName()+",异常:"+e.getMessage());
                } finally {
                    System.out.println("[动态代理][日志] "+method.getName()+",方法执行完毕");
                }
                return result;
            }
        };
        return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler);
    }
}

使用代理工厂根据目标类的接口动态生成代理对象

//测试代码
@Test
public void testDynamicProxy(){
    ProxyFactory factory = new ProxyFactory(new CalculatorLogImpl());
    Calculator proxy = (Calculator) factory.getProxy();
    proxy.div(1,0);
    //proxy.div(1,1);
}

4、代理模式总结

  • 附加功能代码与目标核心代码分离,分别放在代理类中和目标类中
  • 静态代理适合有不同非核心功能的需求,JDK动态代理适合多个目标类有相同功能的需求,使用代理工厂根据目标类接口生成代理对象
  • 无论使用静态代理和动态代理(jdk,cglib),都需要自己编写代理工厂

因此出现Spring AOP框架简化动态代理的实现!

1.3 面向切面编程(AOP)

解决非核心代码冗余问题,简化动态代理的实现
1、面向切面编程思想AOP:提取非核心业务代码,使用代理技术,这种解决问题的思维称AOP

  • OOP(Object Oriented Programming,面向对象编程),OOP引入封装、继承、多态等概念来建立一种对象层次结构,用于模拟公共行为的一个集合。定义纵向关系,即完全使用父类方法/完全重写父类方法,不能做到对方法的局部修改!
  • AOP(Aspect Oriented Programming,面向切面编程),是OOP的补充和完善,它利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,命名为"Aspect"切面——与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,减少重复代码,降低模块之间的耦合度,可操作性和可维护性。

横向的关系:例如日志功能、安全性、异常处理和透明的持续性,散布在各处的无关的代码被称为横切(cross cutting),在OOP,导致了大量代码的重复,不利于各个模块的重用。
总结:与业务无关却与业务模块所共同调用的公共行为成为Aspect即切面。
2、AOP思想主要的应用场景
将通用的横切关注点(如日志、事务、权限控制等)与业务逻辑分离,使得代码更加清晰、简洁、易于维护。

  • 日志记录(前后异常处)、事务处理(保证数据的一致性)、安全控制(安全控制的操作:登录、改密码、授权等等)、性能监控(执行前后的)、异常处理(空指针、数据集连接异常等等)、缓存控制、动态代理(AOP的实现方式之一是通过动态代理,可以代理某个类的所有方法用于实现各种功能)
  • 主要有几个增强位置:目标方法执行前、后、异常,环绕通知则是三个的总和。
    3、AOP属于名词介绍
    AOP的概念理解
  • 横切关注点:从每个方法中抽取出来的同一类非核心业务。aop编程思维的应用场景,非核心代码处理场景。在同一个项目中,可使用多个横切关注点对相关方法进行多个不同方面的增强。
    横切关注点特点:发生在在核心关注点的多处,而各处基本相似,比如权限认证、日志、事务、异常等。AOP作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。
  • 通知(增强):每一个横切关注点上要做的事情都需要写一个方法来实现,该方法就叫通知方法,比如日志增强类(写日志对应的增强代码)
    ①前置增强:在被代理的目标方法前执行;②返回通知:在被代理的目标方法成功结束后执行;③异常通知:在被代理的目标方法异常结束后执行;④后置通知:在被代理的目标方法最终结束后执行;⑤环绕通知:使用try…catch…finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置!
  • 连接点:指那些被拦截到的点。可以被切入的目标类中的方法
  • 切入点:定位连接点的方式,即被选中切入的连接点
  • 切面:切入点+通知=一个类
  • 目标:被代理的目标对象
  • 代理:向目标对象应用通知之后创建的代理对象。包括目标的核心代码+增强代码!
  • 织入:把通知应用到目标上,生成代理对象的过程,即配置的动作。织入时机:编译期、运行期(Spring采用)。织入以后形成切面。

1.4 Spring AOP框架介绍和关系梳理

1、AOP一种区别于OOP的编程思维,用来完善和解决OOP的非核心代码冗余和不方便统一维护问题!
2、代理技术(动态代理|静态代理)是实现AOP思维编程的具体技术,但是自己使用动态代理实现代码比较繁琐!
3、Spring AOP框架,基于AOP编程思维,封装动态代理技术,简化动态代理技术实现的框架!SpringAOP内部帮助我们实现动态代理,我们只需写少量的配置,指定生效范围即可,即可完成面向切面思维编程的实现!

1.5 Spring AOP基于注解方式实现和细节

1.5.1 底层技术组成
  • 动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)。
  • cglib:通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口。
  • AspectJ:早期的AOP实现的框架,SpringAOP借用了AspectJ中的AOP注解。
    Spring AOP底层技术实现
    依赖导入:spring-context会传递依赖spring-aop因此无需导入aop,但是需要导入spring-aspectj(aspectj与spring aop的整合)、aspectj(aspectj与具体实现层的融合),其中spring-aspectj会传递依赖aspectj因此它无需导入。
1.5.2 Spring AOP初步实现

前提:aop只针对ioc容器的对象 -为其创建代理对象 -将代理对象存储到ioc容器!
目标:横向插入。比如需求:给计算的业务类添加日志——类中各个方法的执行顺序:日志输出-核心方法-日志输出-报错日志输出

  • IoC与DI实现
    • 加入依赖:spring-aop、spring-aspectj
    • 正常编写核心业务即可,加入ioc容器。有准备接口及对应的纯净实现类(实现类实现接口方式,因此aop会根据是否实现接口选择使用jdk / cglib动态代理)
    • 编写ioc的配置类/xml文件
    • 测试环境
  • 配置AOP
    • 声明切面类:@Aspect声明,并且需要把这个切面类放入IoC容器中@Component。
    • 切面类的配置:在切面类中定义增强方法(存储横切关注点的代码),几个位置就定义几个方法。不同的切入点使用不同的注解标记(四种@Before、@AfterReturning、@AfterThrowing、@After、@Round标识增强方法),在注解中有属性可以指定作用哪个目标类方法上(在这里经过aop的配置后根据目标类拿到其代理对象,aop会根据目标类是否实现接口选择使用jdk / cglib动态代理)
    • 开启aop的配置:因为使用的注解是aspectj的注解,因此这里需要告诉spring开启aspectj注解支持
    • 测试运行:如果有接口,可以通过目标类的接口(只能根据这个接口类型,因为得到的代理对象也是继承该接口)获取对象,调用对象相关方法,那么Spring内部会创建代理对象,通过代理对象再调用目标方法
1.5.3 在通知方法中目标方法信息

理解:比如输出方法开始了,但是也不知道是哪个方法开始了?增强方法中获取目标方法信息,如:①全部增强方法中获取目标方法的信息(方法名、访问修饰符、所属的类的信息、参数);②返回的结果@AfterReturning;③异常的信息@AfterThrowing。
1、JoinPoint接口作为通知方法的形参传入可以得到:JoinPoint joinPoint—Spring会将这个对象传入!
(1)方法签名:
通过 getSignature() 方法获取目标方法签名Signature(一个方法的全部声明信息—方法名、修饰符、声明类型名)这是由signature前面对象分别通过getName()、getModifiers()、getDeclaringTypeName()方法获取的,返回值都是String类型!
(2)传入的实参:
通过joinPoint.getArgs()方法获取外界调用目标方法时传入的实参列表组成的数组。

  • @Before前置通知方法:得到目标方法签名(方法名、修饰符、声明类型名)、传入目标方法的实参
  • @AfterReturning返回通知方法:得到目标方法的返回值
    returning属性——@AfterReturning(value =,returning = “targetMethodReturnValue”)并在通知方法中加上形参Object targetMethodReturnValue表示指定用targetMethodReturnValue形参变量来接返回结果,因为Spring会自动将目标方法的方法结果传给它。
  • @AfterThrowing异常通知方法:获取目标方法抛出的异常对象
    throwing属性——@AfterThrowing(value = ,throwing = “targetMethodException”) 并在通知方法中加上形参Throwable targetMethodException
    使用throwing属性指定的名称在通知方法声明形参,Spring会将目标方法抛出的异常对象从这里传给这个形参。
1.5.4 切点表达式语法及其重用

1、作用:AOP切点表达式(Pointcut Expression)是一种用于指定切点的语言,它可以通过定义匹配规则,来选择需要被切入的目标对象。
切点表达式语法
2、注意点:
(1)访问修饰符和方法的返回参数类型,要考虑都考虑要不考虑都不考虑!
(2)包的位置:最后一层模糊:.* ;中间层模糊:com…impl;开头模糊:…impl ✔ …impl❌其中…任意开头的任意包。
(3)类名、方法名:全模糊:* ;部分模糊:*Impl
(4)形参列表:没有();有具体参数(String, int);模糊参数:(…);部分模糊:(String…)、(…int)、(String…int)
3、实战

  • 查询某包某类下,访问修饰符是公有,返回值是int的全部方法:pulic int com.zy.service.ServiceImpl.(…)
  • 查询某包下类中第一个参数是String的方法: * com.zy.service.ServiceImpl. (String…)
  • 查询全部包下,无参数的方法: * … * . *(),几个 * 的意思:修饰符&返回值类型任意、 * …任意包、任意类、任意方法名
  • 查询com包下,以int参数类型结尾的方法: * com…* . * (…int)
  • 查询指定包下,Service开头类的私有返回值int的无参数方法:private int com.zy.service.Service* . * ()

4、重用(提取)切点表达式
问题:在每个增强方法中的切点表达式会出现冗余问题,不方便统一维护。
解决方法:提取切点,再增强上引用即可!——同一类内部、不同类中、切点统一管理

  • 同一类内部引用:定义切点表达式方法
    • 定义一个空方法,用注解@Pointcut(“execution(…)”),注解在一个无参数无返回值方法上!增强注解中引用切点表达式的方法eg:@Before(“方法名()”)
  • 不同类中引用:切点表达式方法不同在一个类中
    • 增强方法中直接调用对应的方法名,调用"类的权限定符号.方法名"
    • 可以单独维护切点表达式:创建一个存储切点的类(也需要@Component加入ioc容器中)

5、切面优先级
相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序,从外到里优先级依次降低。使用 @Order(数字越大优先级越低) 注解可以控制切面的优先级。优先级越高前置先执行后置后执行!
举例:缓存切面 (查数据)> 事务切面(开、提交、回滚事务)

切面类的定义:声明切面类、放入IoC容器、四种通知方法、获取目标方法的相关细节信息、切点表达式细节、优先级设置

package com.atguigu.advice;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;
// @Aspect表示这个类是一个切面类
@Aspect
// @Component注解保证这个切面类能够放入IOC容器
@Component
@Order//用于定义切面的优先级
public class LogAspect {
	/**
	* 切点表达式同一类内部引用:切入点表达式重用注解——注解一个无参数无返回值的方法上,通知方法注解引用该方法名即可!
	*/
	//@Pointcut("execution(public int com.atguigu.aop.api.Calculator.add(int,int)))")
	public void declarPointCut() {}
    // @Before注解:声明当前方法是前置通知方法
    // value属性:指定切入点表达式,由切入点表达式控制当前通知方法要作用在哪一个目标方法上
    //@Before(value = "declarPointCut()") //引用可重用的切点表达式
    @Before(value = "execution(public int com.atguigu.proxy.CalculatorPureImpl.add(int,int))")//得到的是目标类的代理对象
    /**
    * JoinPoint接口作为通知方法的形参传入,获取目标方法的详细信息
    * 前置方法:得到目标方法签名(方法名、修饰符、声明类型名)、传入目标方法的实参
    */
    public void printLogBeforeCore(JoinPoint joinPoint) {
        //1.获取方法属于的类的信息
		String simpleName = joinPoint.getTarget().getClass().getSimpleName();
		//2.通过JoinPoint对象获取目标方法的签名对象signature——即一个方法的全部声明信息
		Signature signature = joinPoint.getSignature();
		//2.1通过signature获取目标方法的详细信息:即方法名、修饰符、声明类型名
		String methodName = signature.getName();
		System.out.println("methodName = " + methodName);
		int modifiers = signature.getModifiers();
		System.out.println("modifiers = " + modifiers);
		String declaringTypeName = signature.getDeclaringTypeName();
		System.out.println("declaringTypeName = " + declaringTypeName);
		// 3.通过JoinPoint对象获取外界调用目标方法时传入的实参列表
		Object[] args = joinPoint.getArgs();
		// 4.由于数组直接打印看不到具体数据,所以转换为List集合
		List<Object> argList = Arrays.asList(args);
		System.out.println("[AOP前置通知] " + methodName + "方法开始了,参数列表:" + argList);
    }
    /**
    * 后置方法:得到目标方法的返回值
    */
    @AfterReturning(value = "execution(public int com.atguigu.proxy.CalculatorPureImpl.add(int,int))")
    public void printLogAfterSuccess(JoinPoint joinPoint) {
        String methodName = joinPoint.getSignature().getName();
		System.out.println("[AOP返回通知] "+methodName+"方法成功结束了,返回值是:" + targetMethodReturnValue);
    }
    /**
    * 异常通知方法:获取目标方法抛出的异常对象
    */
    @AfterThrowing(value = "execution(public int com.atguigu.proxy.CalculatorPureImpl.add(int,int))")
    public void printLogAfterException() {
        String methodName = joinPoint.getSignature().getName();
		System.out.println("[AOP异常通知] "+methodName+"方法抛异常了,异常类型是:" + 		targetMethodException.getClass().getName());
    }
    @After(value = "execution(public int com.atguigu.proxy.CalculatorPureImpl.add(int,int))")
    public void printLogFinallyEnd() {
        System.out.println("[AOP后置通知] 方法最终结束了");
    }
}

开启aspectj注解支持:配置类方式

@Configuration
@ComponentScan(basePackages = "com.atguigu")
//作用等于 <aop:aspectj-autoproxy /> 配置类上开启 Aspectj注解支持!
@EnableAspectJAutoProxy
public class MyConfig {
}

开启aspectj注解支持:xml配置文件方式

    <!-- 开启aspectj框架注解支持-->
    <aop:aspectj-autoproxy />
1.5.5 环绕通知详讲

// 使用@Around注解标明环绕通知方法,因为有接口ProceedingJoinPoint可以获取目标方法的相信信息才得以实现。

@Around(value = "com.atguigu.aop.aspect.AtguiguPointCut.transactionPointCut()")
public Object manageTransaction(ProceedingJoinPoint joinPoint) {
	// 通过ProceedingJoinPoint对象获取外界调用目标方法时传入的实参数组
    Object[] args = joinPoint.getArgs();
    // 声明变量用来存储目标方法的返回值
    Object targetMethodReturnValue = null;
    try {
        // 在目标方法执行前:开启事务(模拟)
        log.debug("[AOP 环绕通知] 开启事务,方法名:" + methodName + ",参数列表:" + Arrays.asList(args));
        /**目标方法执行逻辑*/
        targetMethodReturnValue = joinPoint.proceed(args);
        // 在目标方法成功返回后:提交事务(模拟)
        log.debug("[AOP 环绕通知] 提交事务");
    }catch (Throwable e){
        // 在目标方法抛异常后:回滚事务(模拟)
        log.debug("[AOP 环绕通知] 回滚事务");
    }finally {
        // 在目标方法最终结束后:释放数据库连接
        log.debug("[AOP 环绕通知]");
    }
    return targetMethodReturnValue;
}
1.5.5 CGLIB动态代理生效

这个依赖已经整合到spring-core中了。
在目标类没有实现任何接口的情况下,Spring会自动使用cglib技术实现代理,并使用类进行接值。
如果有接口选择使用jdk动态代理,接口接值。因为这里的代理类和目标类是拜把子关系没办法根据类的类型接值。

cglib动态代理生效

//这个类已经配置了切面类定义相关的通知方法了
@Service
public class EmployeeService {
    public void getEmpList() {
       System.out.print("方法内部 com.atguigu.aop.imp.EmployeeService.getEmpList");
    }
}
  @Autowired
  private EmployeeService employeeService;
  @Test
  public void testNoInterfaceProxy() {
      employeeService.getEmpList();
  }

1.6 Spring AOP对获取bean的影响理解

获取bean:可以根据其实现的接口类型/该目标类本身
由于对目标类bean配置类切面,虽然还是根据这些方式获取bean,但是由于对目标bean横切一些非核心业务代码,对于获取bean还是有影响的。
获取bean方式总结:

  • 对实现了接口的类应用切面
    生成目标类bean应用切面后的代理类,该代理类与目标类实现同样的接口,将该代理类放入IoC容器
  • 对实现了接口的类应用切面
    生成目标类bean应用切面后的代理类,该代理类继承目标类,将该代理类放入IoC容器
目标类bean是否实现接口&是否应用切面类测试
情景1都没有根据bean本身类型获取bean
test1:IOC容器中同类型的 bean 只有一个——可以正常获取bean对象
test2:IOC 容器中同类型的 bean 有多个——抛出 NoUniqueBeanDefinitionException 异常
情景2实现接口没有应用切面类,该接口只有一个实现类根据接口类型或类获取 bean——正常获取到 bean,且是同一个对象
情景3实现接口没有应用切面类,该接口有多个实现类且都在ioc容器中test1:根据接口类型获取 bean——抛出 NoUniqueBeanDefinitionException 异常
test2:根据bean类获取bean——正常获取
情景4实现接口并应用切面类,该接口有一个实现类test1:根据接口类型获取bean——正常
test2:根据bean类获取bean——无法获取,因为应用切面后,在IOC容器中的是代理类的对象(也是继承该接口),与目标类是拜把子关系
情景5没有接口应用切面类根据类获取 bean——正常,因为代理对象是继承该类的

1.7 Spring AOP实现的两种方式(XML/注解)

1、注解实现AOP:创建切面类
位置注解(4种位置)、切点表达式、切面注解、优先级注解、切点注解
注解实现AOP
2、XML配置方式:
ioc配置:需要将切面类加入IoC容器
配置AOP:配置切入点表达式、配置切面(其中配置几种通知方法)

1.8 Spring AOP总结

  • 本质上还是要获取组件bean,只不过现在想要在bean上添加一些与核心业务无关的非核心功能,因此采用Spring AOP来实现,而其为了实现这样的功能封装了动态代理技术(JDK / cglib),具体调用哪种取决于目标bean是否实现了接口,如果实现接口使用JDK动态代理。
  • 为了实现Spring AOP,对获取bean有一些影响,具体流程:声明了切面类对目标bean添加一些非核心业务功能,JDK动态代理只能根据目标类实现的接口获取对象(这里获取的对象是代理对象,与目标类对象关系:如果是JDK动态代理则是拜把子关系即实现了共同的接口;如果是cglib动态代理则是父子关系即继承了目标类)
  • 获取bean还是和之前一样,Spring AOP只是对目标bean横切一些非核心业务功能。即对bean应用切面后获取bean得到的是其代理类,表面上看不到,但存在。

二、Spring声明式事务

其实AOP也可以实现声明式事务,但是如果使用AOP实现需要写增强类等等,而Spring声明式事务指的是对AOP的再次封装,Spring-tx提供了事务增强。
需要开启事务注解支持@EnableTransactionManagement

2.1 声明式事务&编程式事务

编程式事务声明式事务
通过编写代码方式直接控制事务的提交和回滚。在 Java 中,通常使用事务管理器(如 Spring 中的 PlatformTransactionManager)来实现编程式事务。

优点:灵活性、按需编写;缺点:冗余、细节没有被屏蔽、代码复用性不高
使用注解或 XML 配置的方式来控制事务的提交和回滚。开发者只需要添加配置(指定哪些方法需要添加事务、事务的属性), 具体事务的实现由第三方框架实现。
优点:将事务的控制和业务逻辑分离开来,提高代码的可读性和可维护性。

2.2 Spring事务管理器

由于不同的持久层框架事务实现方式不一样,因此Spring-tx的事务增强中提供事务管理的接口,没有直接实现代码,而是使用插拔的方式—多态,即使用事务管理器接口提供事务方法,spring中对事务管理器定义了几种实现方式(针对不同持久层框架的)

  • 选择合适的事务管理器实现加入IoC容器
  • 指定哪些方法需要添加事务即可
    Spring-tx的实现方式原理
    1、Spring声明式事务对应依赖
  • spring-tx: 包含声明式事务实现的基本规范(事务管理器规范接口和事务增强等等)
  • spring-jdbc: 包含DataSource方式事务管理器实现类DataSourceTransactionManager,适合的持久层框架:jdbc、jdbcTemplate、mybatis
  • spring-orm: 包含其他持久层框架的事务管理器实现类例如:Hibernate/Jpa等,适合的持久层框架:Hibernate、Jpa
    2、Spring声明式事务对应事务管理器接口
    事务管理器:存放是事务实现代码,交给spring-tx(对应增强),那么就可以根据实现进行事务操作!
    spring声明式事务对应事务管理器的接口

2.3 基于注解的声明式事务

1、Spring-Tx实现流程

  • 基本配置
    • 导入依赖:spring-tx
    • Spring配置类设置:导入外部配置文件、druid连接池&jdbcTemplate
    • 准备dao、service层
  • 对于事务的控制配置
    • 开启支持事务注解:配置类添加注解@EnableTransactionManagement
    • 在配置类中装配事务管理实现对象:不同的持久层架构使用不同的事务管理器对象
    • 使用声明事务注解:在目标方法上添加注解@Transactional

2、事务属性
事务属性的设置都在目标方法上添加的注解@Transactional的属性中设置

  • 只读:查询操作可以设置,如果是DML会出现异常
    @Transactional(readOnly = true)

DML只读的异常信息:Caused by: java.sql.SQLException: Connection is read-only. Queries leading to data modification are not allowed

  • 超时时间:默认情况下没有时间限制,资源被长时间占有:程序卡住、运行出现问题(java程序、数据库/网络连接等等)
    @Transactional(readOnly = false,timeout = 3),timeout的默认值-1

超时执行效果:org.springframework.transaction.TransactionTimedOutException

  • 事务异常回滚
    @Transactional(readOnly = false,timeout = 3,rollbackFor = Exception.class)
    @Transactional(readOnly = false,timeout = 3,rollbackFor = Exception.class,noRollbackFor = FileNotFoundException.class)

异常机制:Throwable→Exception→(RuntimeException、IOException)
事务默认情况下只针对运行时异常回滚,编译时异常不回滚。需要设置回滚异常范围值。
rollbackFor属性:指定哪些异常类才会回滚,默认是 RuntimeException and Error 异常方可回滚!
noRollbackFor属性:指定哪些异常不会回滚, 默认没有指定,如果指定,应该在rollbackFor的范围内!
比如new FileInputStream(“xxxx”);该异常不在rollbackFor的默认范围内。

  • 事务隔离级别
    @Transactional(isolation = Isolation.REPEATABLE_READ)
    isolation属性:设置事务的隔离级别,mysql默认是repeatable read!推荐设置第二级别!

多个事务并发执行时,数据库为了保证数据一致性所遵循的规定,常见的隔离级别:

  • 读未提交(Read Uncommitted):事务可以读取未被提交的数据,容易产生脏读、不可重复读和幻读等问题。实现简单但不太安全,一般不用。
  • 读已提交(Read Committed):事务只能读取已经提交的数据,可以避免脏读问题,但可能引发不可重复读和幻读。
  • 可重复读(Repeatable Read):在一个事务中,相同的查询将返回相同的结果集,不管其他事务对数据做了什么修改。可以避免脏读和不可重复读,但仍有幻读的问题。
  • 串行化(Serializable):最高的隔离级别,完全禁止了并发,只允许一个事务执行完毕之后才能执行另一个事务。可以避免以上所有问题,但效率较低,不适用于高并发场景。
    不同的隔离级别适用于不同的场景,需要根据实际业务需求进行选择和调整。
  • 事务传播行为:父方法调用子方法时,子方法是否需要加入到父方法的事务中?还是新建自己独立?
    propagation属性:指定事务传播行为
属性propagation可选值含义
REQUIRED默认值【推荐!!】如果父方法中有事务就加入,没有就新建自己独立
REQUIRES_NEW不管父方法是否有事务,都新建自己独立
NESTED如果当前存在事务,则在该事务中嵌套一个新事务,如果没有事务,则与Propagation.REQUIRED一样
SUPPORTS如果当前存在事务,则加入该事务,否则以非事务方式执行
NOT_SUPPORTED以非事务方式执行,如果当前存在事务,挂起该事务
MANDATORY必须在一个已有的事务中执行,否则抛出异常
NEVER必须在没有事务的情况下执行,否则抛出异常

事务传播行为指定:事务之间调用,如何影响子事务?事务传播行为的属性设置到子事务上——加入/独立?
注意点:
在同一个类中,对于@Transactional注解的方法调用,事务传播行为不会生效。因为Spring框架中使用代理模式实现了事务机制,在同一个类中的方法调用并不经过代理,而是通过对象的方法调用,因此@Transactional注解的设置不会被代理捕获,也就不会产生任何事务传播行为的效果。

2.4 Spring-Tx事务总结

  • 在配置类中开启事务注解支持,并配置相关事务管理器,在对应的目标方法加上注解@Transactional
  • 对AOP的再次封装,获取的不是bean,而是他的代理对象!!
  • 25
    点赞
  • 24
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
Spring声明事务是一种在Spring框架中实现事务管理的方,它基于AOP(面向切面编程)的思想,通过将事务管理的逻辑从业务代码中分离出来,使得业务代码不需要关注事务的处理,降低了业务代码的复杂度,并且提高了事务的可控性和可复用性。 Spring声明事务的核心是TransactionInterceptor拦截器,它是一个AOP拦截器,用于拦截带有@Transactional注解的方法,并在方法执行前后自动开启和提交事务。在Spring中,可以通过XML配置或注解的方声明事务,例如: XML配置: <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="save*" propagation="REQUIRED"/> <tx:method name="*" propagation="SUPPORTS"/> </tx:attributes> </tx:advice> <aop:config> <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.example.service.*.*(..))"/> </aop:config> 注解方: @Configuration @EnableTransactionManagement public class AppConfig { @Bean public DataSource dataSource() { // create and return a DataSource } @Bean public PlatformTransactionManager transactionManager() { return new DataSourceTransactionManager(dataSource()); } } @Service public class UserServiceImpl implements UserService { @Autowired private UserDao userDao; @Override @Transactional(propagation = Propagation.REQUIRED) public void saveUser(User user) { userDao.save(user); } } 在上述示例中,我们可以看到声明事务的两种方:XML配置和注解方。其中,<tx:advice>元素用于配置事务通知,<aop:advisor>元素用于配置切入点和通知之间的关联;@EnableTransactionManagement注解用于启用声明事务,@Transactional注解用于标记需要进行事务管理的方法。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值