Spring事务和AOP

Spring事务和AOP

一、SpringAOP

动态代理

1. 什么是代理:
	当不方便直接调用目标类的方法,或者不能直接调用目标类的方法,可以通过一个中间类去间接调用。这个中间类,就是代理
	调用者  --->  目标类.方法()
	调用者  ---> 代理类.方法()
                前置增强
                调用目标类的方法 ----> 目标类.方法()
                后置增强
2. 代理的作用:
	在不修改目标类源码的情况下,进行功能增强
3. 实现动态代理的技术:
	JDK动态代理:基于接口实现的。生成的代理类,和目标类  实现了共同的接口,是平级的兄弟关系
	cglib动态代理:基于子类继承实现的。生成的代理类,是目标类的 子类, 是父子关系

1. AOP概述

AOP是用于简化动态代理的使用

1.1 什么是AOP

  • AOP:Aspect Oriented Programming,面向切面编程。是通过预编译方式(aspectj)或者运行期动态代理(Spring)实现程序功能的统一维护的技术。
  • AOP是OOP(Object Oriented Programming)的技术延续,是软件开发中的一个热点,也是Spring中的一个重要内容。利用AOP可以实现对业务逻辑各个部分之间的隔离,从而使得业务逻辑各部分之间的耦合性降低,提高程序的可重用性,同时提高了开发效率。
面试问题:
Spring的两大核心是什么:IoC和AOP
分别有什么作用
	IoC:控制反转,目的用于解耦,底层使用的技术是反射+工厂模式
	AOP:面向切面编程,目的是在不修改目标对象源码的情况下,进行功能增强,底层使用的是动态代理技术

1.2 AOP的作用

  • 作用:不修改源码的情况下,进行功能增强,通过动态代理实现的
  • 优势:减少重复代码,提高开发效率,降低耦合度方便维护
  • 比如:给功能增加日志输出, 事务管理的功能

1.3 AOP的底层

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

  • 常用的动态代理技术有:
    • JDK的动态代理:基于接口实现的
    • cglib的动态代理:基于子类实现的
  • Spring的AOP采用了哪种代理方式?
    • 如果目标对象有接口,就采用JDK的动态代理技术
    • 如果目标对象没有接口,就采用cglib技术

1.4 AOP相关概念

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

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

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

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

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

  • 切面(Aspect):是切入点和通知的结合。 告诉Spring的AOP:要对哪个方法,做什么样的增强

  • 织入(Weaving):把增强/通知 应用到 目标对象来创建代理对象的过程

1.5 AOP开发前要明确的事情

有哪些事情需要我们完成的:

  • 目标类及里边的方法:完成功能的目标类。
  • 通知:要对目标类做什么样的增强,需要我们来编写代码
  • 切面:需要我们来配置切面,告诉Spring对哪个方法做什么样的增强

有哪些事情是由SpringAOP完成的:

  • 代理对象:由SpringAOP根据我们的配置,帮我们生成的
  • 织入:根据切面配置生成代理对象的过程

2. AOP入门

需求

统计Service层每个方法的执行耗时

分析

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

  • 目标类及方法:已经有了
  • 通知方法:要额外增加上的功能,没有需要我们编写
  • 切面:需要我们进行配置

配置切面:切面 = 切入点 + 通知

  • 切入点:Service层的每个方法
  • 通知:统计每个方法的执行时间

AOP的使用步骤:

  1. 添加AOP的起步依赖(已添加)

  2. 编写一个类:类上加@Component@Aspect

    在类里编写通知方法:要额外增加的功能

    给这个方法配置切入点:要对谁进行增强

实现

添加AOP依赖
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
Demo01AopAspect
package com.itheima.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

/**
 * @author liuyp
 * @since 2023/08/24
 */
@Aspect
@Component
public class Demo01AopAspect {

    @Around("execution(public * com.itheima.service.impl.*.*(..))")
    public Object executeTime(ProceedingJoinPoint pjp) throws Throwable {

        long start = System.currentTimeMillis();
        
        //调用目标方法
        Object res = pjp.proceed();
        
        long end = System.currentTimeMillis();

        System.out.println(pjp.getSignature() + "方法耗时:" + (end - start) + "毫秒");

        return res;
    }

}

测试

打开浏览器访问 http://localhost:8080/depts

可看到控制台输出了执行时间

3. AOP详解-通知类型

3.1 通知类型

Spring的AOP支持以下5种通知类型:

通知类型注解作用
前置通知@Before通知方法将在目标方法之前 先执行
后置通知@AfterReturning通知方法将在目标方法正常结束之后 再执行
异常通知@AfterThrowing通知方法将在目标方法抛出异常之后 再执行
最终通知@After通知方法将在目标方法之后 必定执行,无论有没有抛出异常
环绕通知@Around有最大自主性的通知方法,如何调用目标方法及如何增强,完全由我们自定义

使用示例

package com.itheima.aop;

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

/**
 * 当前类是一个切面配置类:
 *      类上加@Component:当前类对象必须交给Spring进行管理。不给Spring管理,Spring就不能增强
 *      类上加@Aspect:告诉Spring,这个类是切面配置类
 * 需求:
 *      对Service层的每个方法统计执行时间
 * @author liuyp
 * @since 2023/08/25
 */
@Aspect
@Component
public class Demo01Aspect {

    /**
     * 方法上加的注解:@Around("切入点表达式"),用于选择对哪些方法进行增强
     * @param pjp 目标方法的信息,被增强的那个方法
     * @return
     */
    @Around("execution(public * com.itheima.service.*.*(..))")
    public Object executeTime(ProceedingJoinPoint pjp) throws Throwable {

        long start = System.currentTimeMillis();
        //调用目标方法,得到返回值
        Object res = pjp.proceed();

        long end = System.currentTimeMillis();
        System.out.println("目标方法执行耗时:" + (end - start) + "毫秒");
        return res;
    }

    /**
     * 需求:要在Service的每个方法执行之前,先输出一句话:前置通知执行了
     * 分析:
     *      切入点:要对谁增强,Service的所有方法
     *      通知:要额外增加的功能,输出“前置通知执行了”
     *      通知类型:要什么时候进行增强,在目标方法执行之前
     */
    @Before("execution(public * com.itheima.service.*.*(..))")
    public void before(){
        System.out.println("前置通知执行了");
    }

    /**
     * 需求:要在Service的每个方法执行抛出异常时,输出一句话:异常通知执行了
     * 分析:
     *      切入点:要对谁增强,Service的所有方法
     *      通知:要额外增加的功能,输出“异常通知执行了”
     *      通知类型:要什么时候进行增强,在目标方法执行抛出异常之后
     */
    @AfterThrowing("execution(public * com.itheima.service.*.*(..))")
    public void afterThrowing(){
        System.out.println("异常通知执行了");
    }

    /**
     * 需求:要在Service的每个方法正常执行完成之后,输出一句话:后置通知执行了
     * 分析:
     *      切入点:要对谁增强,Service的所有方法
     *      通知:要额外增加的功能,输出“后置通知执行了”
     *      通知类型:要什么时候进行增强,在目标方法正常执行完成之后
     */
    @AfterReturning("execution(public * com.itheima.service.*.*(..))")
    public void afterReturning(){
        System.out.println("后置通知执行了");
    }

    /**
     * 需求:要在Service每个方法执行之后,必须执行,输出一句话:最终通知执行了
     * 分析:
     *      切入点:要对谁增强,Service的所有方法
     *      通知:要额外增加的功能,输出“最终通知执行了”
     *      通知类型:要什么时候进行增强,在目标方法执行之后,必须执行
     */
    @After("execution(public * com.itheima.service.*.*(..))")
    public void after(){
        System.out.println("最终通知执行了");
    }
}

3.2 通知方法里获取目标方法信息(备用)

不同通知方法里,获取被调用的目标方法:

  • 可以直接给通知方法加形参 JoinPoint
  • 如果是环绕通知不同,要加形参 ProceedingJoinPoint

JoinPoint常用方法:

  • getArgs():获取调用方法时的实参值
  • getTarget():获取被调用的目标对象(原始目标对象)
  • getThis():获取当前代理对象
  • getSignature():获取被调用的方法信息。Signature对象的方法:
    • getName():获取被调用的方法名。比如:queryAllDepts
    • getDeclaringType():获取被调用方法所属的类Class
    • getDeclaringTypeName():获取被调用方法所属的类的全限定类名
    • toLongString():获取方法的完整名称。比如:public java.util.List com.itheima.service.impl.DeptServiceImpl.queryAllDepts()
    • toShortString:获取方法的简短名称。比如:DeptServiceImpl.queryAllDepts()
    • toString:获取方法信息。比如:List com.itheima.service.impl.DeptServiceImpl.queryAllDepts()
package com.itheima.aop;

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

import java.util.Arrays;

/**
 * 当前类是一个切面配置类:
 *      类上加@Component:当前类对象必须交给Spring进行管理。不给Spring管理,Spring就不能增强
 *      类上加@Aspect:告诉Spring,这个类是切面配置类
 * 需求:
 *      对Service层的每个方法统计执行时间
 * @author liuyp
 * @since 2023/08/25
 */
@Aspect
@Component
public class Demo01Aspect {

    /**
     * 方法上加的注解:@Around("切入点表达式"),用于选择对哪些方法进行增强
     * @param pjp 目标方法的信息,被增强的那个方法
     * @return
     */
    @Around("execution(public * com.itheima.service.*.*(..))")
    public Object executeTime(ProceedingJoinPoint pjp) throws Throwable {

        long start = System.currentTimeMillis();
        //调用目标方法,得到返回值
        Object res = pjp.proceed();

        long end = System.currentTimeMillis();
        System.out.println("目标方法执行耗时:" + (end - start) + "毫秒");
        return res;
    }

    /**
     * 需求:要在Service的每个方法执行之前,先输出一句话:前置通知执行了
     * 分析:
     *      切入点:要对谁增强,Service的所有方法
     *      通知:要额外增加的功能,输出“前置通知执行了”
     *      通知类型:要什么时候进行增强,在目标方法执行之前
     */
    @Before("execution(public * com.itheima.service.*.*(..))")
    public void before(JoinPoint joinPoint){
        //调用目标方法时的实参值
        Object[] args = joinPoint.getArgs();
        //调用的目标类对象
        Object target = joinPoint.getTarget();
        //当前代理类对象
        Object aThis = joinPoint.getThis();
        //调用的目标方法
        Signature signature = joinPoint.getSignature();
        System.out.println("前置通知执行了, 目标类是:" + target.getClass().getName() + ", 目标方法:" + signature + ", 方法实参是:" + Arrays.toString(args));
    }

    /**
     * 需求:要在Service的每个方法执行抛出异常时,输出一句话:异常通知执行了
     * 分析:
     *      切入点:要对谁增强,Service的所有方法
     *      通知:要额外增加的功能,输出“异常通知执行了”
     *      通知类型:要什么时候进行增强,在目标方法执行抛出异常之后
     */
    @AfterThrowing("execution(public * com.itheima.service.*.*(..))")
    public void afterThrowing(JoinPoint joinPoint){
        System.out.println("异常通知执行了, joinPoint = " + joinPoint);
    }

    /**
     * 需求:要在Service的每个方法正常执行完成之后,输出一句话:后置通知执行了
     * 分析:
     *      切入点:要对谁增强,Service的所有方法
     *      通知:要额外增加的功能,输出“后置通知执行了”
     *      通知类型:要什么时候进行增强,在目标方法正常执行完成之后
     */
    @AfterReturning("execution(public * com.itheima.service.*.*(..))")
    public void afterReturning(JoinPoint joinPoint){
        System.out.println("后置通知执行了, joinPoint = " + joinPoint);
    }

    /**
     * 需求:要在Service每个方法执行之后,必须执行,输出一句话:最终通知执行了
     * 分析:
     *      切入点:要对谁增强,Service的所有方法
     *      通知:要额外增加的功能,输出“最终通知执行了”
     *      通知类型:要什么时候进行增强,在目标方法执行之后,必须执行
     */
    @After("execution(public * com.itheima.service.*.*(..))")
    public void after(JoinPoint joinPoint){
        System.out.println("最终通知执行了, joinPoint = " + joinPoint);
    }
}

4. AOP详解-切入点表达式

4.1 execution

用于根据方法的名称进行模糊匹配的

语法:execution(权限修饰符 返回值类型 全限定类名.方法名(形参列表))

详解:

  • 权限修饰符:通常是public,可省略不写
  • 返回值类型:可以精确写,也可以用通配符*
  • 全限定类名:可以精确写,也可以用通配符*,还可以用..表示后代级别
  • 方法名:可以精确写,也可以用通配符*
  • 形参列表:可以精确写,也可以用通配符*。使用..表示任意个任意类型的形参

示例:

  • public void com.itheima.service.XxService.delete(Integer)
  • void com.itheima.service.XxService.delete(Integer)
  • * com.itheima.service.XxService.delete(Integer)
  • * com.itheima.service.*.delete(Integer)
  • * com.itheima..*.delete(Integer)
  • * com.itheima..*.*(Integer)
  • * com.itheima..*.*(Integer, String)
  • * com.itheima..*.*(Integer, *)
  • * com.itheima..*.*(..)
package com.itheima.aop;

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

/**
 * 切入点表达式:
 *      语法:`execution(权限修饰符 返回值类型 全限定类名.方法名(形参列表))`
 *      详解:
 *          权限修饰符:通常是`public`,可省略不写
 *          返回值类型:可以精确写,也可以用通配符*
 *             execution(String com.itheima.service.*.*(..))
 					表示要选中com.itheima.service下任意类的、返回值是String类型的方法
 *             execution(void com.itheima.service.*.*(..))
 					表示要选中com.itheima.service下任意类的、返回值是void类型的方法
 *             execution(* com.itheima.service.*.*(..)),
 					表示要选中com.itheima.service下任意类的、返回值是任意类型的方法
 *          全限定类名:
 *              可以精确写,execution(* com.itheima.service.DeptService.queryAllDepts(..))
 *                        表示选中com.itheima.service.DeptService类下名称为queryAllDepts方法
 *              可以用通配符,execution(* com.itheima.service.*.queryAllDepts(..))
 *                        表示选中com.itheima.service.包下任意类下名称为queryAllDepts方法
 *              可以使用..表示后代,execution(* com.itheima..*.*(..))
 *                        表示选中com.itheima包下所有的类(包括这个包里的类,以下所有下级包里的类)的所有方法
 *         方法名:可以精确写,也可以用通配符*
 *              如果写:execution(* com.itheima..*.queryAllDepts(..))
 *                     表示选中com.itheima包下所有类里queryAllDepts方法
 *              如果写:execution(* com.itheima..*.*(..))
 *                     表示选中com.itheima包下所有类里任意方法
 *         形参列表:
 *              可以精确写,
 *                  execution(* com.itheima..*.*(String)) 
 						表示选中com.itheima包下所有类的任意方法,但是要求方法有1个形参必须是String
 *                  execution(* com.itheima..*.*(String, Integer)) 
 						表示选中com.itheima包下所有类的任意方法,但是要求方法有2个形参按顺序必须是String、Integer
 *              也可以用通配符*
 *                  execution(* com.itheima..*.*(String, *)) 
 						表示选中com.itheima包下所有类的任意方法,但是要求方法有2个形参按顺序必须是String、任意类型
 *              任意个任意类型:..
 *                  execution(* com.itheima..*.*(..)) 
 					表示选中com.itheima包下任意类里任意方法,方法的形参个数和类型都不限制
 *     示例:execution(* *..*.*(..)) 表示选择任意包下、任意类的任意方法,形参不限,返回值不限。不要用
 * @author liuyp
 * @since 2023/08/25
 */
@Aspect
@Component
public class Demo02Aspect {

    @Before("execution(* com.itheima.service.*.*(..))")
    public void before(){
        System.out.println("=====Demo02Aspect.before执行了======");
    }


    @After("execution(* com.itheima.service.*.*(..))")
    public void after(){
        System.out.println("=====Demo02Aspect.after执行了======");
    }
}

4.2 @annotation

根据注解选择要增强的方法

语法:@annotation(注解的全限定类名),会选择所有加了指定注解的方法,进行增强

示例:

  1. 自定义注解
package com.itheima.aop;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
}
  1. 创建切面配置类
package com.itheima.aop;

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

/**
 * @author liuyp
 * @since 2023/08/25
 */
@Aspect
@Component
public class Demo03Aspect {

    @Before("@annotation(com.itheima.aop.MyLog)")
    public void saveLogs(){
        System.out.println("========保存日志信息======");
    }
}
  1. 给DeptServiceImpl的queryAllDepts方法上加注解@MyLog

在这里插入图片描述

  1. 测试:使用浏览器访问http://localhost:8080/depts,通知类会执行,输出日志

4.3 抽取公用切入点表达式

语法:

  • 定义公用的切入点表达式:在切面类里创建一个方法,方法上加注解@Pointcut("公用的切入点表达式")
  • 引用公用的切入点表达式:
    • 完整用法:Before("com.itheima.aop.Demo02Aspect.pc()")
    • 简写形式:Before("pc()"),仅适合于 切入点方法 和 通知方法在同一个类里

示例:

package com.itheima.aop;

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

/**
 * 如果要抽取公用的切入点表达式:
 *      使用注解@Pointcut("公用的切入点表达式")
 *      把注解加在方法上
 * 当需要使用切入点表达式时:
 *      完整的用法:@Before("com.itheima.aop.Demo02Aspect.pc()")
 *      如果调用的同一个类里的切入点表达式方法,可以简写@Before("方法名()")。
 * @author liuyp
 * @since 2023/08/25
 */
@Aspect
@Component
public class Demo02Aspect {

    /**这个方法仅仅作为切入点表达式的载体*/
    @Pointcut("execution(* com.itheima.service.*.*(..))")
    public void pc(){}

    // @Before("execution(* com.itheima.service.*.*(..))")
    // @Before("com.itheima.aop.Demo02Aspect.pc()")
    @Before("pc()")
    public void before(){
        System.out.println("=====Demo02Aspect.before执行了======");
    }


    // @After("execution(* com.itheima.service.*.*(..))")
    @After("pc()")
    public void after(){
        System.out.println("=====Demo02Aspect.after执行了======");
    }
}

5. AOP详解-通知执行顺序

说明

在这里插入图片描述

示例

package com.itheima.aop;

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

/**
 * @author liuyp
 * @since 2023/08/25
 */
@Aspect
//@Order(150)
@Component
public class Demo04Aspect {
    @Pointcut("execution(* com.itheima.service.*.*(..))")
    public void pc(){}

    @Before("pc()")
    public void before(){
        System.out.println("===Demo04Aspect.before===");
    }

    @AfterReturning("pc()")
    public void afterReturning(){
        System.out.println("===Demo04Aspect.afterReturning===");
    }

    @AfterThrowing("pc()")
    public void afterThrowing(){
        System.out.println("===Demo04Aspect.afterThrowing===");
    }

    @After("pc()")
    public void after(){
        System.out.println("===Demo04Aspect.after===");
    }

    @Around("pc()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {

        System.out.println("===Demo04Aspect.around前===");

        //调用目标方法
        Object res = pjp.proceed();

        System.out.println("===Demo04Aspect.around后===");

        return res;
    }
}
package com.itheima.aop;

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

/**
 * @author liuyp
 * @since 2023/08/25
 */
@Aspect
//@Order(100)
@Component
public class Demo05Aspect {
    @Pointcut("execution(* com.itheima.service.*.*(..))")
    public void pc(){}

    @Before("pc()")
    public void before(){
        System.out.println("===Demo05Aspect.before===");
    }

    @AfterReturning("pc()")
    public void afterReturning(){
        System.out.println("===Demo05Aspect.afterReturning===");
    }

    @AfterThrowing("pc()")
    public void afterThrowing(){
        System.out.println("===Demo05Aspect.afterThrowing===");
    }

    @After("pc()")
    public void after(){
        System.out.println("===Demo05Aspect.after===");
    }

    @Around("pc()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {

        System.out.println("===Demo05Aspect.around前===");

        //调用目标方法
        Object res = pjp.proceed();

        System.out.println("===Demo05Aspect.around后===");

        return res;
    }
}

6. AOP练习

需求

将tlias案例中增、删、改相关接口的操作日志记录到数据库表中。

  • 就是当访问部门管理和员工管理当中的增、删、改相关功能接口时,需要详细的操作日志,并保存在数据表中,便于后期数据追踪。

操作日志信息包含:操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长

准备

SQL脚本
-- 操作日志表
create table operate_log(
    id int unsigned primary key auto_increment comment 'ID',
    operate_user int unsigned comment '操作人ID',
    operate_time datetime comment '操作时间',
    class_name varchar(100) comment '操作的类名',
    method_name varchar(100) comment '操作的方法名',
    method_params varchar(1000) comment '方法参数',
    return_value varchar(2000) comment '返回值',
    cost_time bigint comment '方法执行耗时, 单位:ms'
) comment '操作日志表';
实体类
package com.itheima.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;

@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog {
    private Integer id; //ID
    private Integer operateUser; //操作人ID
    private LocalDateTime operateTime; //操作时间
    private String className; //操作类名
    private String methodName; //操作方法名
    private String methodParams; //操作方法参数
    private String returnValue; //操作方法返回值
    private Long costTime; //操作耗时
}
Mapper接口
package com.itheima.mapper;

import com.itheima.pojo.OperateLog;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface OperateLogMapper {

    //插入日志数据
    @Insert("insert into operate_log (operate_user, operate_time, class_name, method_name, method_params, return_value, cost_time) " +
            "values (#{operateUser}, #{operateTime}, #{className}, #{methodName}, #{methodParams}, #{returnValue}, #{costTime});")
    void insert(OperateLog log);

}

分析

选择AOP技术实现:

  • 如果不使用AOP,就需要修改源码,给大量的方法增加保存日志的代码。代码重复,日志和业务功能耦合到了一起
  • 希望:不修改源码的情况下,给Service层的方法进行增强,就使用AOP

要使用AOP技术:

  • 切入点:对哪些方法增强

    哪个方法需要保存日志,就在哪个方法上加一个自定义注解

    使用注解切入点表达式@annotation(自定义注解)

  • 通知:

    通知类型:使用@Around环绕通知

    通知方法:获取当前用户、当前时间、当前方法的类名和方法名、方法的实参、返回值,运行耗时,把这些信息保存到日志表

实现

1. 自定义注解MyLog
package com.itheima.aop;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * @author liuyp
 * @since 2023/08/24
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
}
2. 给增删改方法添加注解

给DeptServiceImpl中,增、删、改方法上,添加自定义的MyLog注解

3. 创建切面类
package com.itheima.aop;

import com.itheima.mapper.OperateLogMapper;
import com.itheima.pojo.OperateLog;
import com.itheima.util.JwtUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.Arrays;

/**
 * @author liuyp
 * @since 2023/08/24
 */
@Aspect
@Component
public class OperateLogAspect {

    @Autowired
    private OperateLogMapper logMapper;
    @Autowired
    private HttpServletRequest request;

    @Around("@annotation(MyLog)")
    public Object operateLog(ProceedingJoinPoint pjp) throws Throwable {
        //通过request对象获取token,解析token得到当前用户id
        String token = request.getHeader("Token");
        Object userId = JwtUtils.parseJWT(token).get("id");

        //获取当前时间-开始,用于计算 方法耗时
        long start = System.currentTimeMillis();
        //调用目标方法,得到执行结果
        Object res = pjp.proceed();
        //获取当前时间-结束,用于计算 方法耗时
        long end = System.currentTimeMillis();

        //获取被调用的目标方法
        Signature signature = pjp.getSignature();

        //准备保存日志,创建日志对象
        OperateLog log = new OperateLog();
        //  操作人:当前用户
        log.setOperateUser(Integer.parseInt(userId.toString()));
        //  操作时间:现在
        log.setOperateTime(LocalDateTime.now());
        //  操作的类:从目标方法签名中获取当前类
        log.setClassName(signature.getDeclaringTypeName());
        //  操作的方法:从目标方法签名中获取方法名
        log.setMethodName(signature.getName());
        //  方法的参数:从pjp里获取参数
        log.setMethodParams(Arrays.toString(pjp.getArgs()));
        //  方法的执行结果:如果结果是null,就存储空串
        log.setReturnValue(res==null?"":res.toString());
        //  方法耗时
        log.setCostTime(end - start);
        //  保存到数据库
        logMapper.insert(log);
        return res;
    }
}

测试

启动nginx后,在页面上测试

二、Spring事务

1. 复习数据库事务

事务:用于保证组成事务的多个操作,要么一起成功,要么一起失败

使用的步骤:

  1. 开启事务:start transaction; 或者 begin
  2. 执行多条SQL语句。SQL语句不会立即生效,而是被暂时缓存起来
  3. 关闭事务:
    • 提交事务:commit 事务里所有的操作将会全部、立即生效
    • 回滚事务:rollback 事务里所有的操作将会全部、立即撤消

2. Spring事务入门

2.1 事务问题演示

  1. 准备事务的基础代码

  2. 要求:删除部门的同时,也要删除部门里的员工

2.2 Spring事务入门

哪个方法需要放到同一个事务里,就在哪个方法上加注解@Transactional

2.3 Spring事务原理

Spring的事务管理,底层使用的是AOP技术

在这里插入图片描述

3. @Transactional详解

注解@Transactional(rollbackFor=异常类名.class, propagation=传播行为)两个常用的属性

3.1 rollbackFor事务回滚

默认情况,Spring的事务,只会在抓捕到RuntimeException时,才会回滚

  • 如果代码里抛出的不属于RuntimeException,事务将会失效

如果想要修改Spring的事务:指定的异常进行回滚:

  • @Transactional(rollbackFor=异常类名.class)

使用示例:

@Override
@Transactional(rollbackFor = Exception.class)
public void deleteDeptById(Integer id) throws Exception {
    //删除部门
    deptMapper.deleteById(id);

    // int i = 1/0;

    //如果在页面上删除id为2的部门,代码会报异常。但是Spring的事务默认是不会抓捕这种异常的
    if (id == 2) {
        throw new Exception("人造异常");
    }

    //删除部门里的员工:delete from emp where dept_id = ?
    empMapper.deleteByDeptId(id);
}

3.2 propagation事务传播行为

事务传播行为:业务方法调用业务方法时,事务的一致性问题

Spring配置传播行为:@Transactional(propagation=Propagation.传播行为)

可用的传播行为有:

  • REQUIRED:如果有事务就用;如果没有事务就开启新事务
  • SUPPORT:如果有事务就用;如果没有事务就以非事务方式执行
  • MANDATORY:如果有事务就用;如果没有事务就抛出异常
  • REQUIRES_NEW:如果有事务就挂起不用,而是开启新事务;如果没有事务就开启新事务
  • NOT_SUPPORTED:如果有事务就挂起不用,以非事务方式执行;如果没有事务,以非事务方式执行
  • NEVER:如果有事务就抛出异常;如果没有事务,就以非事务方式执行
  • NESTED:事务嵌套。如果有事务,就开启一个子事务;如果没有事务,就开启新事务

在这里插入图片描述

4. 事务失效的情况【面试】

正常情况下,哪个方法加了@Transactional注解,就有事务了

但是一些特殊情况,可能会事务失效:

  • 方法是private。不允许加事务

  • 类上没有加@Component或者@Service等等注解。类没有被Spring管理

  • 同一个类里:一个方法调用了事务方法,事务会失效

    因为事务管理的代码,实质上是在AOP生成的代理对象里

    同一类里通过this直接调用方法,不经过代理对象,所以没有事务效果

  • 事务里的异常没有抛,而是try…catch了:★

    比如事务方法里自己加了try…catch,异常没有抛出去

    AOP的代理对象就抓不到异常,也就不会进行回滚

  • 事务里抛的异常,不是RuntimeException

    默认情况下,Spring的事务只抓取RuntimeException

    如果抛的不是这种异常,事务AOP就抓不到异常,也就不会进行回滚


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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值