事务&AOP

YangeIT大约 46 分钟SpringBoot事务AOP环绕通知统一异常·@TransactionalrollbackForpropagation·@Aspect·@Around

事务&AOP

今日目标标

  • 能够掌握 Spring 事务配置 ❤️ 🍐
  • 能够理解 AOP 的作用 🍐 ❤️
  • 能够完成 AOP 的入门案例 ✏️
  • 能够理解 AOP 的工作流程 🍐
  • 能够说出 AOP 的五种通知类型 🍐

1. 事务管理

1.1 事务回顾

事务回顾

事务是一组操作的集合,它是一个不可分割的工作单位。事务会把所有的操作作为一个整体,一起向数据库提交或者是撤销操作请求。所以这组操作要么同时成功,要么同时失败

image
image

事务的作用:

  1. 保证数据的完整性和一致性

事务可以将相关的数据库操作组合成一个执行单元,要么全部执行成功,要么全部失败回滚,保证数据不会处于中间失效的状态。

  1. 提供故障恢复机制

通过提交和回滚机制,在系统故障时仍能保证数据的一致性。

  1. 加快执行效率

将批量的操作组合为一个事务执行,可以显著减少磁盘I/O次数,加快执行速度。 在一个事务内只提交一条sql和在一个事务内提交多个sql,对数据库数据的批量处理影响是很大的。一个事务内提交多个sql可以提高批量数据的处理速度。

  1. 简化开发流程

事务将一系列操作作为单元执行,开发者只需指定操作逻辑,而不用担心其中间状态。

代码操作

事务的操作主要有三步

  1. 开启事务(一组操作开始前,开启事务):start transaction / begin ;
  2. 提交事务(这组操作全部成功后,提交事务):commit ;
  3. 回滚事务(中间任何一个操作出现异常,回滚事务):rollback ;

使用事务控制删除部门和删除该部门下的员工的操作:

-- 开启事务
start transaction ;

-- 删除学工部
delete from dept where id = 1;

-- 删除学工部的员工
delete from emp where dept_id = 1;
  • 上述的这组SQL语句,如果如果执行成功,则提交事务
-- 提交事务 (成功时执行)
commit ;
  • 上述的这组SQL语句,如果如果执行失败,则回滚事务,数据回到初始状态
-- 回滚事务 (出错时执行)
rollback ;

总结

课堂作业

  1. 什么是事务,事务有哪些作用?🎤
  2. 事务的四大特性是什么?🎤

1.2 Spring事务管理 🍐 ❤️

Spring事务管理

目标🎯 :Spring下进行事务管理

Spring中的事务管理 主要指的是通过Spring来管理事务的行为,使得我们可以通过配置的方式来实现程序中的事务管理。

通过Spring框架来管理事务,可以大大简化企业级Java应用中的事务处理,解耦业务代码和事务管理代码,使得开发人员能更加专注于核心业务逻辑的实现。

代码操作

需求

需求:当部门解散了不仅需要把部门信息删除了,还需要把该部门下的员工数据也删除了。

步骤:

  • 根据ID删除部门数据
  • 根据部门ID删除该部门下的员工
  1. DeptServiceImpl
@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
    @Autowired
    private DeptMapper deptMapper;
    @Autowired
    private EmpMapper empMapper;
    //根据部门id,删除部门信息及部门下的所有员工
    @Override
    public void delete(Integer id){
        //根据部门id删除部门信息
        deptMapper.deleteById(id);

        //删除部门下的所有员工信息
        empMapper.deleteByDeptId(id);   
    }
}











 


 


总结

  1. 在spring框架当中已经把事务控制的代码已经封装好了,并不需要开发者手动实现。
  2. 使用spring框架,只需要通过一个简单的注解 @Transactional 就搞定了。
  3. 可以在application.yml配置文件中开启事务管理日志,这样就可以在控制看到和事务相关的日志信息
#spring事务管理日志
logging:
  level:
    org.springframework.jdbc.support.JdbcTransactionManager: debug

课堂作业

  1. Spring中的事务是在哪层实现的?是通过什么注解实现的?🎤
  2. Spring中的@Transactional放在哪里?🎤
  3. 思考:Spring中的异常如果trycatch,还会自动回滚吗?🎤

1.3 事务进阶 🍐 🚀

@Transactional注解 当中的两个常见的属性:

  1. 异常回滚的属性:rollbackFor
  2. 事务传播行为:propagation

1.3.1 异常回滚的属性

异常回滚的属性

异常回滚的属性:rollbackFor 👇

所有异常都会触发回滚吗?rollbackFor属性的作用是什么?

  • 在Spring的事务管理中,默认只有运行时异常 RuntimeException才会回滚。
  • 如果还需要回滚指定类型的异常,可以通过rollbackFor属性来指定。
    @Transactional(rollbackFor=Exception.class) //指定Exception异常,说明可以接收所有的异常
    public void delete(Integer id){
        // 省略代码
    }

代码操作

下面我们在做一个测试,我们修改业务功能代码,在模拟异常的位置上直接抛出Exception异常(编译时异常)

@Transactional
public void delete(Integer id) throws Exception {
        //根据部门id删除部门信息
        deptMapper.deleteById(id);
        
        //模拟:异常发生
        if(true){
            throw new Exception("出现异常了```");
        }

        //删除部门下的所有员工信息
        empMapper.deleteByDeptId(id);   
}







 





总结

课堂作业

  1. 所有异常都会触发回滚吗? 🎤

1.3.2 事物传播行为 🍐

事物传播行为

事务的传播行为就是A事务方法被B事务方法调用时,A事务方法应该如何进行事务控制。

属性propagation: 用来配置事务的传播行为的(常见的传播行为)

  • REQUIRED常用 【默认值】需要事务,有则加入,无则创建新事务
  • REQUIRES_NEW 常用 需要新事务,无论有无,总是创建新事务

例如:两个事务方法,一个A方法,一个B方法。在这两个方法上都添加了@Transactional注解,就代表这两个方法都具有事务,而在A方法当中又去调用了B方法。

image-20230112152543953
image-20230112152543953

所谓事务的传播行为,指的就是在A方法运行的时候,首先会开启一个事务,在A方法当中又调用了B方法, B方法自身也具有事务,那么B方法在运行的时候,到底是加入到A方法的事务当中来,还是B方法在运行的时候新建一个事务?这个就涉及到了事务的传播行为。👇

常见的事务传播行为:

属性值含义
REQUIRED常用【默认值】需要事务,有则加入,无则创建新事务
REQUIRES_NEW 常用需要新事务,无论有无,总是创建新事务
SUPPORTS支持事务,有则加入,无则在无事务状态中运行
NOT_SUPPORTED不支持事务,在无事务状态下运行,如果当前存在已有事务,则挂起当前事务
MANDATORY必须有事务,否则抛异常
NEVER必须没事务,否则抛异常

需求和步骤

需求:解散部门时需要记录操作日志

由于解散部门是一个非常重要而且非常危险的操作,所以在业务当中要求每一次执行解散部门的操作都需要留下痕迹,就是要记录操作日志。而且还要求无论是执行成功了还是执行失败了,都需要留下痕迹。

步骤:

  1. 执行解散部门的业务:先删除部门,再删除部门下的员工(前面已实现)
  2. 记录解散部门的日志,到日志表(未实现)

准备工作:

  1. 创建数据库表 dept_log 日志表:
create table dept_log(
   	id int auto_increment comment '主键ID' primary key,
    create_time datetime null comment '操作时间',
    description varchar(300) null comment '操作描述'
)comment '部门操作日志表';
  1. 引入资料中提供的实体类:DeptLog
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeptLog {
    private Integer id;
    private LocalDateTime createTime;
    private String description;
}
  1. 引入资料中提供的Mapper接口:DeptLogMapper
@Mapper
public interface DeptLogMapper {

    @Insert("insert into dept_log(create_time,description) values(#{createTime},#{description})")
    void insert(DeptLog log);

}
  1. 引入资料中提供的业务接口:DeptLogService
public interface DeptLogService {
    void insert(DeptLog deptLog);
}
  1. 引入资料中提供的业务实现类:DeptLogServiceImpl
@Service
public class DeptLogServiceImpl implements DeptLogService {

    @Autowired
    private DeptLogMapper deptLogMapper;

    @Transactional //事务传播行为:有事务就加入、没有事务就新建事务
    @Override
    public void insert(DeptLog deptLog) {
        deptLogMapper.insert(deptLog);
    }
}

总结

事务的传播行为

  • REQUIRED :大部分情况下都是用该传播行为即可。

  • REQUIRES_NEW 实用 :当我们不希望事务之间相互影响时,可以使用该传播行为。比如:下订单前需要记录日志,不论订单保存成功与否,都需要保证日志记录能够记录成功。

课堂作业

  1. 能够流畅的说出事务回滚的注意事项以及常用的事务传播行为的特点, 🎤
  2. 能够写出日志案例的思路流程并完成解散部门时需要记录操作日志案例。 ✏️

2. AOP基础

目标🎯 :学习AOP的基础,理解Aop的作用和优势以及AOP快速入门

2.1 AOP概述和快速入门

AOP概述

  • AOP英文全称:Aspect Oriented Programming(面向切面编程、面向方面编程),其实说白了,面向切面编程就是面向特定方法编程

那什么又是面向方法编程呢, 为什么又需要面向方法编程呢? 来我们举个例子做一个说明.

比如,我们这里有一个项目,项目中开发了很多的业务功能。

image-20230112154547523
image-20230112154547523

然而有一些业务功能执行效率比较低,执行耗时较长,我们需要针对于这些业务方法进行优化。 那首先第一步就需要定位出执行耗时比较长的业务方法,再针对于业务方法再来进行优化。

此时我们就需要统计当前这个项目当中每一个业务方法的执行耗时。那么统计每一个业务方法的执行耗时该怎么实现?

可能多数人首先想到的就是在每一个业务方法运行之前,记录这个方法运行的开始时间。在这个方法运行完毕之后,再来记录这个方法运行的结束时间。拿结束时间减去开始时间,不就是这个方法的执行耗时吗?

image-20230112154605206

以上分析的实现方式是可以解决需求问题的。但是对于一个项目来讲,里面会包含很多的业务模块,每个业务模块又包含很多增删改查的方法,如果我们要在每一个模块下的业务方法中,添加记录开始时间、结束时间、计算执行耗时的代码,就会让程序员的工作变得非常繁琐。

image-20230112154627546

而AOP面向方法编程,就可以做到在不改动这些原始方法的基础上,针对特定的方法进行功能的增强。

AOP的作用:在程序运行期间在不修改源代码的基础上对已有方法进行增强(无侵入性: 解耦)

我们要想完成统计各个业务方法执行耗时的需求,我们只需要定义一个模板方法,将记录方法执行耗时这一部分公共的逻辑代码,定义在模板方法当中,在这个方法开始运行之前,来记录这个方法运行的开始时间,在方法结束运行的时候,再来记录方法运行的结束时间,中间就来运行原始的业务方法。

image-20230112154530101

而中间运行的原始业务方法,可能是其中的一个业务方法,比如:我们只想通过 部门管理的 list 方法的执行耗时,那就只有这一个方法是原始业务方法。 而如果,我们是先想统计所有部门管理的业务方法执行耗时,那此时,所有的部门管理的业务方法都是 原始业务方法。 那面向这样的指定的一个或多个方法进行编程,我们就称之为 面向切面编程。

那此时,当我们再调用部门管理的 list 业务方法时啊,并不会直接执行 list 方法的逻辑,而是会执行我们所定义的 模板方法 , 然后再模板方法中:

  • 记录方法运行开始时间
  • 运行原始的业务方法(那此时原始的业务方法,就是 list 方法)
  • 记录方法运行结束时间,计算方法执行耗时
image-20230112155813944

不论,我们运行的是那个业务方法,最后其实运行的就是我们定义的模板方法,而在模板方法中,就完成了原始方法执行耗时的统计操作 。(那这样呢,我们就通过一个模板方法就完成了指定的一个或多个业务方法执行耗时的统计)

而大家会发现,这个流程,我们是不是似曾相识啊?

对了,就是和我们之前所学习的动态代理技术是非常类似的。 我们所说的模板方法,其实就是代理对象中所定义的方法,那代理对象中的方法以及根据对应的业务需要, 完成了对应的业务功能,当运行原始业务方法时,就会运行代理对象中的方法,从而实现统计业务方法执行耗时的操作。

其实,AOP面向切面编程和OOP面向对象编程一样,它们都仅仅是一种编程思想,而动态代理技术是这种思想最主流的实现方式。而Spring的AOP是Spring框架的高级技术,旨在管理bean对象的过程中底层使用动态代理机制,对特定的方法进行编程(功能增强)。

AOP快速入门

需求和步骤

需求: 统计各个业务层方法执行耗时(结束时间-开始时间)。

实现步骤:

  1. 导入依赖:在pom.xml中导入AOP的依赖
  2. 编写AOP程序:创建切面类,针对于特定方法根据业务需要进行编程

为演示方便,可以自建新项目或导入提供的springboot-aop-quickstart项目工程

pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

总结

结合上述案例,AOP常见的应用场景如下:

  • 业务方法执行耗时的统计(已完成)
  • 记录系统的操作日志
  • 权限控制
  • 事务管理:我们前面所讲解的Spring事务管理,底层其实也是通过AOP来实现的,只要添加@Transactional注解之后,AOP程序自动会在原始方法运行前先来开启事务,在原始方法运行完毕之后提交或回滚事务

课堂作业

  1. AOP的作用是什么? 有什么优势!🎤

2.2 AOP核心概念 🍐

AOP 核心概念

1. 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)

​ 连接点指的是可以被aop控制的方法。例如:入门程序当中所有的业务方法都是可以被aop控制的方法。

image-20230112160708474

​ 在SpringAOP提供的JoinPoint当中,封装了连接点方法在执行时的相关信息。(后面会有具体的讲解)

总结

查看下列图片,回答问题 看图理解核心概念
  • 下列哪个是目标对象,哪个是代理对象?

    image-20220513203556476
    image-20220513203556476

课堂作业

  1. 结上图观察对应AOP的核心概念 🎤
  2. 说说核心概念中的通知以及切入点的含义 🎤
  3. 完成Aop入门案例(统计DeptService中方法中的执行效率)✏️

3. AOP进阶

目标🎯 :学习AOP中各个细节,如:通知类型、通知顺序、切入点表达式、连接点

3.1 通知类型🍐

通知类型

Spring中AOP的通知类型:

  • @Around常用:环绕通知,此注解标注的通知方法在目标方法前、后都被执行
  • @Before:前置通知,此注解标注的通知方法在目标方法前被执行
  • @After:后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
  • @AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行
  • @AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行

代码操作

下面我们通过代码演示,来加深对于不同通知类型的理解:

@Slf4j
@Component
@Aspect
public class MyAspect1 {
    //前置通知
    @Before("execution(* com.itheima.service.*.*(..))")
    public void before(JoinPoint joinPoint){
        log.info("before ...");

    }

    //环绕通知
    @Around("execution(* com.itheima.service.*.*(..))")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.info("around before ...");

        //调用目标对象的原始方法执行
        Object result = proceedingJoinPoint.proceed();
        
        //原始方法如果执行时有异常,环绕通知中的后置代码不会在执行了
        
        log.info("around after ...");
        return result;
    }

    //后置通知
    @After("execution(* com.itheima.service.*.*(..))")
    public void after(JoinPoint joinPoint){
        log.info("after ...");
    }

    //返回后通知(程序在正常执行的情况下,会执行的后置通知)
    @AfterReturning("execution(* com.itheima.service.*.*(..))")
    public void afterReturning(JoinPoint joinPoint){
        log.info("afterReturning ...");
    }

    //异常通知(程序在出现异常的情况下,执行的后置通知)
    @AfterThrowing("execution(* com.itheima.service.*.*(..))")
    public void afterThrowing(JoinPoint joinPoint){
        log.info("afterThrowing ...");
    }
}






 






 
 



 








 





 





 





总结

课堂作业

  1. AOP有哪几种通知类型,其中什么通知类型最为典型,能够替代其他类型🎤

3.2 通知顺序 🚀 🍐

目标🎯 :了解通知的执行顺序

执行顺序

通知的执行顺序大家主要知道两点即可:

  1. 不同的切面类当中,默认情况下通知的执行顺序是与切面类的类名字母排序是有关系的

    • 目标方法前的通知方法:字母排名靠前的先执行
    • 目标方法后的通知方法:字母排名靠前的后执行
  2. 可以在切面类上面加上@Order注解,来控制不同的切面类通知的执行顺序

    • @Order(x) 切面类的执行顺序(前置通知:数字x越小先执行; 后置通知:数字x越小越后执行

代码操作

通知执行顺序的验证代码

定义多个切面类: 👇 👇

@Slf4j
@Component
@Aspect
public class MyAspect2 {
    //前置通知
    @Before("execution(* com.itheima.service.*.*(..))")
    public void before(){
        log.info("MyAspect2 -> before ...");
    }

    //后置通知
    @After("execution(* com.itheima.service.*.*(..))")
    public void after(){
        log.info("MyAspect2 -> after ...");
    }
}

@Slf4j
@Component
@Aspect
public class MyAspect3 {
    //前置通知
    @Before("execution(* com.itheima.service.*.*(..))")
    public void before(){
        log.info("MyAspect3 -> before ...");
    }

    //后置通知
    @After("execution(* com.itheima.service.*.*(..))")
    public void after(){
        log.info("MyAspect3 ->  after ...");
    }
}
@Slf4j
@Component
@Aspect
public class MyAspect4 {
    //前置通知
    @Before("execution(* com.itheima.service.*.*(..))")
    public void before(){
        log.info("MyAspect4 -> before ...");
    }

    //后置通知
    @After("execution(* com.itheima.service.*.*(..))")
    public void after(){
        log.info("MyAspect4 -> after ...");
    }
}

3.3 连接点信息获取

@Around通知,获取连接点信息是什么类?

  • 对于@Around环绕通知,获取连接点信息只能使用ProceedingJoinPoint类型

  • 对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型

ProceedingJoinPointJoinPoint 可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。

点击查看示例代码
@Slf4j
@Component
@Aspect
public class MyAspect7 {

    @Pointcut("execution(* com.itheima.service.*.*(..))")
    private void pt(){}
   
    //前置通知
    @Before("pt()")
    public void before(JoinPoint joinPoint){
        log.info(joinPoint.getSignature().getName() + " MyAspect7 -> before ...");
    }
    
    //后置通知
    @Before("pt()")
    public void after(JoinPoint joinPoint){
        log.info(joinPoint.getSignature().getName() + " MyAspect7 -> after ...");
    }

    //环绕通知
    @Around("pt()")
    public Object around(ProceedingJoinPoint pjp) throws Throwable {
        //获取目标类名
        String name = pjp.getTarget().getClass().getName();
        log.info("目标类名:{}",name);

        //目标方法名
        String methodName = pjp.getSignature().getName();
        log.info("目标方法名:{}",methodName);

        //获取方法执行时需要的参数
        Object[] args = pjp.getArgs();
        log.info("目标方法参数:{}", Arrays.toString(args));

        //执行原始方法
        Object returnValue = pjp.proceed();

        return returnValue;
    }
}












 





 






 



 



 



 





重新启动SpringBoot服务,执行查询部门数据的功能:

image-20230110231629140
image-20230110231629140

总结

课堂作业

  1. 如果有多个切面类,执行顺序如何控制?🎤

3.4 切入点表达式 🍐 ✏️

切入点表达式

切入点表达式:

  • 描述切入点方法的一种表达式
  • 作用:主要用来决定项目中的哪些方法需要加入通知确定谁增强
  • 常见形式:
    1. execution(……):根据方法的签名来匹配覆盖面广image-20230110214150215
    2. @annotation(……) :根据注解匹配 精准增强,灵活image-20230110214242083

代码操作

@annotation注解匹配实现步骤:

  1. 编写自定义注解

  2. 在业务类要做为连接点的方法上添加自定义注解

自定义注解:MyLog

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface MyLog {
}

总结

execution通配符匹配

execution主要根据方法的返回值、包名、类名、方法名、方法参数等信息来匹配,语法为:

execution(访问修饰符?  返回值  包名.类名.?方法名(方法参数) throws 异常?)

其中带?的表示可以省略的部分

  • 访问修饰符:可省略(比如: public、protected)
  • 包名.类名: 可省略
  • throws 异常:可省略(注意是方法上声明抛出的异常,不是实际抛出的异常)

示例:

@Before("execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))")

使用通配符描述切入点

  • * :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分

  • .. :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数

切入点表达式的语法规则不要死背,理解即可

  1. 方法的访问修饰符可以省略
  2. 返回值可以使用*号代替(任意返回值类型)
  3. 包名可以使用*号代替,代表任意包(一层包使用一个*
  4. 使用..配置包名,标识此包以及此包下的所有子包
  5. 类名可以使用*号代替,标识任意类
  6. 方法名可以使用*号代替,表示任意方法
  7. 可以使用 * 配置参数,一个任意类型的参数
  8. 可以使用.. 配置参数,任意个任意类型的参数
点击查看切入点表达式示例
  • 省略方法的修饰符号

    execution(void com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))
    
  • 使用*代替返回值类型

    execution(* com.itheima.service.impl.DeptServiceImpl.delete(java.lang.Integer))
    
  • 使用*代替包名(一层包使用一个*

    execution(* com.itheima.*.*.DeptServiceImpl.delete(java.lang.Integer))
    
  • 使用..省略包名

    execution(* com..DeptServiceImpl.delete(java.lang.Integer))    
    
  • 使用*代替类名

    execution(* com..*.delete(java.lang.Integer))   
    
  • 使用*代替方法名

    execution(* com..*.*(java.lang.Integer))   
    
  • 使用 * 代替参数

    execution(* com.itheima.service.impl.DeptServiceImpl.delete(*))
    
  • 使用..省略参数

    execution(* com..*.*(..))
    

注意事项:

  • 根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。

    execution(* com.itheima.service.DeptService.list(..)) || execution(* com.itheima.service.DeptService.delete(..))
    

切入点表达式的书写建议🍐❤️:企业实用

  • 所有业务方法名在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是 find 开头,更新类方法都是update开头 👍

    //业务类
    @Service
    public class DeptServiceImpl implements DeptService {
        
        public List<Dept> findAllDept() {
           //省略代码...
        }
        
        public Dept findDeptById(Integer id) {
           //省略代码...
        }
        
        public void updateDeptById(Integer id) {
           //省略代码...
        }
        
        public void updateDeptByMoreCondition(Dept dept) {
           //省略代码...
        }
        //其他代码...
    }
    
    //匹配DeptServiceImpl类中以find开头的方法
    execution(* com.itheima.service.impl.DeptServiceImpl.find*(..))
    
  • 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性 👍

    execution(* com.itheima.service.DeptService.*(..))
    
  • 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用 ..,使用 * 匹配单个包 👍

    execution(* com.itheima.*.*.DeptServiceImpl.find*(..))
    

技巧

  • execution切入点表达式
    • 根据我们所指定的方法的描述信息来匹配切入点方法,这种方式也是最为常用的一种方式
    • 如果我们要匹配的切入点方法的方法名不规则,或者有一些比较特殊的需求,通过execution切入点表达式描述比较繁琐
  • annotation 切入点表达式企业实用
    • 基于注解的方式来匹配切入点方法。这种方式虽然多一步操作,我们需要自定义一个注解,但是相对来比较灵活。我们需要匹配哪个方法,就在方法上加上对应的注解就可以了

注意

因为AOP面向切面编程的特性,因此书写AOP代码需要慎重,以防增强不需要增强的代码

4. AOP案例

目标🎯 :通过案例理解AOP通知类型的使用

AOP案例

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

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

操作日志信息包含:

  • 操作人(所记录的日志信息包括当前接口的操作人是谁操作的)
  • 操作时间(什么时间点操作)
  • 执行方法的全类名(访问的是哪个类)
  • 执行方法名(类当中的哪个方法)
  • 方法运行时参数(访问这个方法的时候传入进来的参数是什么)
  • 返回值(访问这个方法最终拿到的返回值是什么)
  • 方法执行时长(整个接口方法的运行时长是多长时间)

案例的实现步骤其实就两步:

  • 准备工作
    1. 引入AOP的起步依赖
    2. 导入资料中准备好的数据库表结构,并引入对应的实体类
  • 编码实现
    1. 自定义注解@Log
    2. 定义切面类,完成记录操作日志的逻辑
  1. AOP起步依赖
<!--AOP起步依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>
  1. 导入资料中准备好的数据库表结构,并引入对应的实体类

数据表

-- 操作日志表
create table operate_log(
    id int unsigned primary key auto_increment comment 'ID',
    operate_user int unsigned comment '操作人',
    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 '操作日志表';

实体类

//操作日志实体类
@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接口

@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});")
    public void insert(OperateLog log);

}

课后作业

🚩 1. 重点完成上述的课堂作业

  1. 晚自习第一节课的前30分钟,总结完毕之后,每个同学先必须梳理今日知识点 (记得写不知道的,以及感恩三件事);整理好的笔记可以发给组长,组长交给班长,意在培养大家总结的能力)

  2. 晚自习第一节课的后30分钟开始练习(记住:程序员是代码堆起来的):

    • 先要把今天的所有案例或者课堂练习,如果没练完的,练完他
    • 独立书写今日作业 今日作业单独发微信群里👈
  3. 剩余的时间:预习第二天的知识,预习的时候一定要注意:

  • 预习不是学习,不要死看第二天的视频(很容易出现看了白看,为了看视频而看视频)
  • 预习看第二天的笔记,把笔记中标注重要的知识,可以找到预习视频,先看一遍,如果不懂的 ,记住做好标注。

5.拓展

5.1 AOP底层实现原理

想知道AOP底层实现原理吗,点击这里学习 👈 👈

5.2 SpringBoot原理

SpringBoot这么好用,想知道他的原理吗?点击这里学习