【Spring】 AOP面向切面编程


AOP是什么?

AOP:Aspect Oriented Programming面向切面编程,解决非核心代码的冗余。

  • AOP利用一种称为"横切"的技术,剖解开封装的对象内部,并将那些影响了多个类的公共行为封装到一个可重用模块,并将其命名为"Aspect",即切面。
  • 所谓"切面",简单说就是那些与业务无关,却为业务模块所共同调用的逻辑或责任封装起来,便于减少系统的重复代码,降低模块之间的耦合度,并有利于未来的可操作性和可维护性。
  • 使用AOP,可以在不修改原来代码的基础上添加新功能。

AOP思想主要的应用场景:

  1. 日志记录:在系统中记录日志是非常重要的,可以使用AOP来实现日志记录的功能,可以在方法执行前、执行后异常抛出时记录日志
  2. 事务处理:在数据库操作中使用事务可以保证数据的一致性,可以使用AOP来实现事务处理的功能,可以在方法开始前开启事务,在方法执行完毕后提交或回滚事务
  3. 安全控制:在系统中包含某些需要安全控制的操作,如登录、修改密码、授权等,可以使用AOP来实现安全控制的功能。可以在方法执行前进行权限判断,如果用户没有权限,则抛出异常或转向到错误页面,以防止未经授权的访问。
  4. 性能监控:在系统运行过程中,有时需要对某些方法的性能进行监控,以找到系统的瓶颈并进行优化。可以使用AOP来实现性能监控的功能,可以在方法执行前记录时间戳,在方法执行完毕后计算方法执行时间并输出到日志中。
  5. 异常处理:系统中可能出现各种异常情况,如空指针异常、数据库连接异常等,可以使用AOP来实现异常处理的功能,在方法执行过程中,如果出现异常,则进行异常处理(如记录日志、发送邮件等)。
  6. 缓存控制:在系统中有些数据可以缓存起来以提高访问速度,可以使用AOP来实现缓存控制的功能,可以在方法执行前查询缓存中是否有数据,如果有则返回,否则执行方法并将方法返回值存入缓存中。
  7. 动态代理:AOP的实现方式之一是通过动态代理,可以代理某个类的所有方法,用于实现各种功能。
  • 综上所述,AOP可以应用于各种场景,它的作用是将通用的横切关注点与业务逻辑分离,使得代码更加清晰、简洁、易于维护。

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

AOP快速入门

需求:
统计各个业务层方法执行耗时。

实现步骤:

  1. 导入依赖:在pom.xml中导入AOP的依赖
  2. 编写AOP程序:针对于特定方法根据业务需要进行编程
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

AOP程序:TimeAspect

@Component
@Aspect //当前类为切面类
@Slf4j
public class TimeAspect {

    @Around("execution(* com.itheima.service.*.*(..))") 
    public Object recordTime(ProceedingJoinPoint pjp) throws Throwable {
        //记录方法执行开始时间
        long begin = System.currentTimeMillis();

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

        //记录方法执行结束时间
        long end = System.currentTimeMillis();

        //计算方法执行耗时
        log.info(pjp.getSignature()+"执行耗时: {}毫秒",end-begin);

        return result;
    }
}

1

常见AOP应用的典型场景:

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

AOP核心概念

1. 连接点:JoinPoint

  • 可以被AOP控制的方法(暗含方法执行时的相关信息)
  • 连接点指的是可以被aop控制的方法。
  • 例如:入门程序当中所有的业务方法都是可以被aop控制的方法。
    1

2. 通知:Advice

  • 指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
  • 将这部分重复的代码逻辑抽取出来单独定义。抽取出来的这一部分重复的逻辑,也就是共性的功能。
    1

3. 切入点:PointCut

  • 匹配连接点的条件,通知仅会在切入点方法执行时被应用
    1

4. 切面:Aspect

  • 描述通知与切入点的对应关系(通知+切入点)
  • 通过切面就能够描述当前aop程序需要针对于哪个原始方法,在什么时候执行什么样的操作。
  • 切面所在的类,我们一般称为切面类(被@Aspect注解标识的类)
    1

5. 目标对象:Target

  • 通知所应用的对象
    1

Spring的AOP底层是基于动态代理技术来实现的,也就是说在程序运行的时候,会自动的基于动态代理技术为目标对象生成一个对应的代理对象。在代理对象当中就会对目标对象当中的原始方法进行功能的增强。
1

AOP详解

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 ...");
    }
}

程序发生异常的情况下:

  • @AfterReturning标识的通知方法不会执行,@AfterThrowing标识的通知方法执行了
  • @Around环绕通知中原始方法调用时有异常,通知中的环绕后的代码逻辑也不会在执行了 (因为原始方法调用已经出异常了)

切入点表达式重复:

//前置通知
@Before("execution(* com.itheima.service.*.*(..))")

//环绕通知
@Around("execution(* com.itheima.service.*.*(..))")
  
//后置通知
@After("execution(* com.itheima.service.*.*(..))")

//返回后通知(程序在正常执行的情况下,会执行的后置通知)
@AfterReturning("execution(* com.itheima.service.*.*(..))")

//异常通知(程序在出现异常的情况下,执行的后置通知)
@AfterThrowing("execution(* com.itheima.service.*.*(..))")

抽取重复:

@Slf4j
@Component
@Aspect
public class MyAspect1 {

    //切入点方法(公共的切入点表达式)
    @Pointcut("execution(* com.itheima.service.*.*(..))")
    private void pt(){

    }

    //前置通知(引用切入点)
    @Before("pt()")
    public void before(JoinPoint joinPoint){
        log.info("before ...");

    }

    //环绕通知
    @Around("pt()")
    public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {
        log.info("around before ...");

        //调用目标对象的原始方法执行
        Object result = proceedingJoinPoint.proceed();
        //原始方法在执行时:发生异常
        //后续代码不在执行

        log.info("around after ...");
        return result;
    }

    //后置通知
    @After("pt()")
    public void after(JoinPoint joinPoint){
        log.info("after ...");
    }

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

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

需要注意的是:
当切入点方法使用private修饰时,仅能在当前切面类中引用该表达式, 当外部其他切面类中也要引用当前类中的切入点表达式,就需要把private改为public,而在引用的时候,具体的语法为:

全类名.方法名() :

@Slf4j
@Component
@Aspect
public class MyAspect2 {
    //引用MyAspect1切面类中的切入点表达式
    @Before("com.itheima.aspect.MyAspect1.pt()")
    public void before(){
        log.info("MyAspect2 -> before ...");
    }
}

2. 通知顺序

@Order注解,控制通知的执行顺序
(前置通知:数字越小先执行; 后置通知:数字越小越后执行)

3. 切入点表达式

  • 描述切入点方法的一种表达式

  • 作用:主要用来决定项目中的哪些方法需要加入通知

  • 常见形式:
    1

@annotation

实现步骤:

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

自定义注解:MyLog

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

业务类:DeptServiceImpl

@Slf4j
@Service
public class DeptServiceImpl implements DeptService {
    @Autowired
    private DeptMapper deptMapper;

    @Override
    @MyLog //自定义注解(表示:当前方法属于目标方法)
    public List<Dept> list() {
        List<Dept> deptList = deptMapper.list();
        //模拟异常
        //int num = 10/0;
        return deptList;
    }
    @Override
    @MyLog  //自定义注解(表示:当前方法属于目标方法)
    public void delete(Integer id) {
        //1. 删除部门
        deptMapper.delete(id);
    }

切面类

@Slf4j
@Component
@Aspect
public class MyAspect6 {
    //针对list方法、delete方法进行前置通知和后置通知

    //前置通知
    @Before("@annotation(com.itheima.anno.MyLog)")
    public void before(){
        log.info("MyAspect6 -> before ...");
    }

    //后置通知
    @After("@annotation(com.itheima.anno.MyLog)")
    public void after(){
        log.info("MyAspect6 -> after ...");
    }
}
  • execution切入点表达式
    • 根据我们所指定的方法的描述信息来匹配切入点方法,这种方式也是最为常用的一种方式
    • 如果我们要匹配的切入点方法的方法名不规则,或者有一些比较特殊的需求,通过execution切入点表达式描述比较繁琐
  • annotation 切入点表达式
    • 基于注解的方式来匹配切入点方法。这种方式虽然多一步操作,我们需要自定义一个注解,但是相对来比较灵活。我们需要匹配哪个方法,就在方法上加上对应的注解就可以了

4. 连接点

  • 对于@Around通知,获取连接点信息只能使用ProceedingJoinPoint类型
  • 对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型
    //后置通知
    @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;
    }

一、AOP术语名词介绍

  • 横切关注点
    • 从每个方法中抽取出来的同一类非核心业务
    • 在同一个项目中,我们可以使用多个横切关注点对相关方法进行多个不同方面的增强。
    • 这个概念不是语法层面天然存在的,而是根据附加功能的逻辑上的需要:有十个附加功能,就有十个横切关注点

·

  • AOP把软件系统分为两个部分:核心关注点横切关注点

    • 业务处理的主要流程是核心关注点,非核心代码是横切关注点。
    • 横切关注点的一个特点是,他们经常发生在核心关注点的多处,而各处基本相似,比如权限认证、日志、事务、异常等。
    • AOP的作用在于分离系统中的各种关注点,将核心关注点和横切关注点分离开来。(分离核心业务代码与非核心代码
  • 通知(增强)

    • 每一个横切关注点上要做的事情都需要写一个方法来实现,这样的方法就叫通知方法。
      • 前置通知:在被代理的目标方法执行
      • 返回通知:在被代理的目标方法成功结束后执行(寿终正寝
      • 异常通知:在被代理的目标方法异常结束后执行(死于非命
      • 后置通知:在被代理的目标方法最终结束后执行(盖棺定论
      • 环绕通知:使用try...catch...finally结构围绕整个被代理的目标方法,包括上面四种通知对应的所有位置

1

  • 切入点 pointcut
    定位连接点的方式,或者可以理解成被选中的连接点!
    是一个表达式,比如execution(* com.spring.service.impl..(…))。
    符合条件的每个方法都是一个具体的连接点。

  • 切面 aspect
    切入点和通知的结合。是一个类。
    1

  • 目标 target
    被代理的目标对象。

  • 代理 proxy
    向目标对象应用通知之后创建的代理对象。

  • 织入 weave
    指把通知应用到目标上,生成代理对象的过程。
    可以在编译期织入,也可以在运行期织入,Spring采用后者。

二、Spring AOP框架介绍和关系梳理

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

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

3.1 Spring AOP底层技术组成

1

  • 动态代理(InvocationHandler):JDK原生的实现方式,需要被代理的目标类必须实现接口
    • 因为这个技术要求代理对象和目标对象实现同样的接口(兄弟两个拜把子模式)。
  • cglib:通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口。
  • AspectJ:早期的AOP实现的框架,SpringAOP借用了AspectJ中的AOP注解。

3.2 初步实现

1
1

  1. 导入依赖
<!-- spring-aspects会帮我们传递过来aspectjweaver -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>6.0.6</version>
</dependency>

<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>6.0.6</version>
</dependency>
  1. 准备接口
public interface Calculator {
    
    int add(int i, int j);
    
    int sub(int i, int j);
    
    int mul(int i, int j);
    
    int div(int i, int j);
    
}
  1. 纯净实现类
/**
 * @Description: 实现计算接口,单纯添加加减乘除 实现,掺杂其它功能
 *  AOP -》 只针对IOC容器对象 - 创建代理对象 -> 将代理对象存储到IOC容器
 */
@Component
public class CalculatorImpl implements Calculator {
    @Override
    public int add(int i, int j) {

        int result = i + j;

        return result;
    }

    @Override
    public int sub(int i, int j) {

        int result = i - j;

        return result;
    }

    @Override
    public int mul(int i, int j) {

        int result = i * j;

        return result;
    }

    @Override
    public int div(int i, int j) {

        int result = i / j;

        return result;
    }
}
  1. 声明切面类
package com.doug.advice;

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

/**
 * @Description: 增强类 实现增强方法
 */
@Component
@Aspect // @Aspect表示这个类是一个切面类
public class LogAspect {
    /*
    * 1. 定义方法存储增强代码
    *       具体定义几个方法,根据插入的位置决定!
    * 2. 使用配置注解 指定插入目标方法的位置
    *   前置 @Before
    *   后置 @AfterReturning
    *   异常 @AfterThrowing
    *   最后 @After
    *   环绕 @Around
    *
    *   try{
    *       前置
    *       目标方法执行
    *       后置
    *   }catch(){
    *       异常
    *   }finally{
    *       最后
    *   }
    *
    * 3.配置切点表达式 [选择要插入的方法  切点
    * 4.注解补全
    *   加入IOC容器 @Component
    *   配置切面 @Aspect
    * 5.开启Aspect注解的支持
    * */

    @Before("execution(* com.doug.aop.impl.*.*(..))")
    public void start(){
        System.out.println("方法开始了");
    }

    @After("execution(* com.doug.aop.impl.*.*(..))")
    public void after(){
        System.out.println("方法结束了");
    }

    @AfterThrowing("execution(* com.doug.aop.impl.*.*(..))")
    public void error(){
        System.out.println("方法报错了");
    }
}
  1. 配置类
@Configuration
@ComponentScan("com.doug")
@EnableAspectJAutoProxy // 开启Aspectj的注解  xml: <aop:aspectj-autoproxy />
public class MyConfiguration {
}
  1. 测试
@SpringJUnitConfig(value = MyConfiguration.class)
public class TestAop {
    @Autowired
    private Calculator calculator;

    @Test
    public void aopTest(){
        int add = calculator.add(1, 0);
        System.out.println(add);
    }
}

1

3.3 获取通知细节信息

  • JointPoint接口
    • JoinPoint 接口通过 getSignature() 方法获取目标方法的签名(方法声明时的完整信息)
  • 方法返回值
    • 在返回通知中,通过 @AfterReturning注解的returning属性获取目标方法的返回值!
  • 异常对象捕捉
    • 在异常通知中,通过**@AfterThrowing**注解的throwing属性获取目标方法抛出的异常对象
package com.doug.advice;

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

import java.lang.reflect.Modifier;

/**
 * @Description: 定义四个增强方法,获取目标方法的信息 返回值 异常
 *  1. 定义方法 - 增强代码
 *  2. 使用注解指定对应的位置
 *  3. 配置切点表达式选中方法
 *  4. 切面和IOC的配置
 *  5. 开启aspectj注解的支持
 */
@Component
@Aspect
public class MyAdvice {
    /*
    *   增强方法中获取目标方法信息:
    *
    *  1. 全部增强方法中,获取目标方法的信息(方法名,参数,访问修饰符,所属类的信息...)
    *   (JoinPoint joinPoint) import org.aspectj.lang.JoinPoint;
    *
    *  2. 返回结果 - @AfterReturning
    *     (Object result) result 接受返回的结果
    *       @AfterReturning(value = "execution(* com..impl.*.*(..))",returning = "result")
    *
    * 3.  异常信息 - @AfterThrowing
    *   (Throwing t) t接收异常信息
    *   @AfterThrowing(value = "execution(* com..impl.*.*(..))",throwing = "result")
    * */

    @Before("execution(* com..impl.*.*(..))")
    public void start(JoinPoint joinPoint){
        // 1. 获取方法属于类的信息
        String simpleName = joinPoint.getTarget().getClass().getSimpleName();
        // 2. 获取方法名称
        int modifiers = joinPoint.getSignature().getModifiers();
        String s = Modifier.toString(modifiers);
        String name = joinPoint.getSignature().getName();//获取方法名

        // 3. 获取参数列表
        Object[] args = joinPoint.getArgs();
    }

    @AfterReturning(value = "execution(* com..impl.*.*(..))",returning = "result")
    public void afterReturn(JoinPoint joinPoint,Object result){

    }

    @After("execution(* com..impl.*.*(..))")
    public void after(){

    }

    @AfterThrowing(value = "execution(* com..impl.*.*(..))",throwing = "result")
    public void afterThrowing(Throwable result){

    }
}

3.4 切点表达式语法

切点表达式作用
AOP切点表达式(Pointcut Expression)是一种用于指定切点的语言,它可以通过定义匹配规则,来选择需要被切入的目标对象。
1
切点表达式语法
1
语法细节:
1

  • 实战:
1.查询某包某类下,访问修饰符是公有,返回值是int的全部方法
     execution(public int com.doug.xClass.*(..))
2.查询某包下类中第一个参数是String的方法
      execution(* com.doug.xClass.*(String..))
3.查询全部包下,无参数的方法!
      execution(* *..*.*())
4.查询com包下,以int参数类型结尾的方法
     execution(* com..*.*(..int))
5.查询指定包下,Service开头类的私有返回值int的无参数方法
     execution(private int com.doug.Service*.*())

3.5 重用(提取)切点表达式

1
提取重复的切点表达式
1
切点统一管理:
将切点表达式统一存储到一个类中进行集中管理和维护!

3.6 环绕通知

三合一(四合一)
1

3.7 切面优先级设置

相同目标方法上同时存在多个切面时,切面的优先级控制切面的内外嵌套顺序。

  • 优先级高的切面:外面
  • 优先级低的切面:里面

使用 @Order 注解可以控制切面的优先级:

  • @Order(较小的数):优先级高
  • @Order(较大的数):优先级低
    1
    实际意义:
    实际开发时,如果有多个切面嵌套的情况,要慎重考虑。例如:如果事务切面优先级高,那么在缓存中命中数据的情况下,事务切面的操作都浪费了。
    1
    此时应该将缓存切面的优先级提高,在事务操作之前先检查缓存中是否存在目标数据。
    1

3.8 CGLib动态代理生效

在目标类没有实现任何接口的情况下,Spring会自动使用cglib技术实现代理。

总结:
a. 如果目标类有接口,选择使用jdk动态代理

b. 如果目标类没有接口,选择cglib动态代理

c. 如果有接口,接口接值

d. 如果没有接口,类进行接值

3.9 注解实现小结

1

四、Spring AOP对获取Bean的影响理解

对实现了接口的类应用切面
在这里插入图片描述
对没实现接口的类应用切面new
1
如果使用AOP技术,目标类有接口,必须使用接口类型接收IoC容器中代理组件!

五、AOP 案例

5.1 需求

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

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

操作日志信息包含:

  • 操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长

5.2 分析

  1. 使用AOP解决(每一个增删改功能接口中要实现的记录操作日志的逻辑代码是相同)。

可以把这部分记录操作日志的通用的、重复性的逻辑代码抽取出来定义在一个通知方法当中,我们通过AOP面向切面编程的方式,在不改动原始功能的基础上来对原始的功能进行增强。目前我们所增强的功能就是来记录操作日志,所以也可以使用AOP的技术来实现。使用AOP的技术来实现也是最为简单,最为方便的。

  1. 使用环绕通知
  • 所记录的操作日志当中包括:操作人、操作时间,访问的是哪个类、哪个方法、方法运行时参数、方法的返回值、方法的运行时长。
  • 方法返回值,是在原始方法执行后才能获取到的。
  • 方法的运行时长,需要原始方法运行之前记录开始时间,原始方法运行之后记录结束时间。通过计算获得方法的执行耗时。
  • 基于以上的分析我们确定要使用Around环绕通知。
  1. 使用annotation来描述表达式

要匹配业务接口当中所有的增删改的方法,而增删改方法在命名上没有共同的前缀或后缀。此时如果使用execution切入点表达式也可以,但是会比较繁琐。 当遇到增删改的方法名没有规律时,就可以使用 annotation切入点表达式

5.3 步骤

  • 准备工作
    1. 引入AOP的起步依赖
    2. 导入数据库表结构,并引入对应的实体类
  • 编码实现
    1. 自定义注解@Log
    2. 定义切面类,完成记录操作日志的逻辑

5.4 实现

5.4.1 准备

依赖:

<!--AOP起步依赖-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

数据库:

-- 操作日志表
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);

}

5.4.2 编码实现

  • 自定义注解@Log
/**
 * 自定义Log注解
 */
@Target({ElementType.METHOD})
@Documented
@Retention(RetentionPolicy.RUNTIME)
public @interface Log {
}
  • 修改业务实现类,在增删改业务方法上添加@Log注解
@Slf4j
@Service
public class EmpServiceImpl implements EmpService {
    @Autowired
    private EmpMapper empMapper;

    @Override
    @Log
    public void update(Emp emp) {
        emp.setUpdateTime(LocalDateTime.now()); //更新修改时间为当前时间

        empMapper.update(emp);
    }

    @Override
    @Log
    public void save(Emp emp) {
        //补全数据
        emp.setCreateTime(LocalDateTime.now());
        emp.setUpdateTime(LocalDateTime.now());
        //调用添加方法
        empMapper.insert(emp);
    }

    @Override
    @Log
    public void delete(List<Integer> ids) {
        empMapper.delete(ids);
    }

    //省略其他代码...
}
  • 定义切面类,完成记录操作日志的逻辑
@Slf4j
@Component
@Aspect //切面类
public class LogAspect {

    @Autowired
    private HttpServletRequest request;

    @Autowired
    private OperateLogMapper operateLogMapper;

    @Around("@annotation(com.itheima.anno.Log)")
    public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
        //操作人ID - 当前登录员工ID
        //获取请求头中的jwt令牌, 解析令牌
        String jwt = request.getHeader("token");
        Claims claims = JwtUtils.parseJWT(jwt);
        Integer operateUser = (Integer) claims.get("id");

        //操作时间
        LocalDateTime operateTime = LocalDateTime.now();

        //操作类名
        String className = joinPoint.getTarget().getClass().getName();

        //操作方法名
        String methodName = joinPoint.getSignature().getName();

        //操作方法参数
        Object[] args = joinPoint.getArgs();
        String methodParams = Arrays.toString(args);

        long begin = System.currentTimeMillis();
        //调用原始目标方法运行
        Object result = joinPoint.proceed();
        long end = System.currentTimeMillis();

        //方法返回值
        String returnValue = JSONObject.toJSONString(result);

        //操作耗时
        Long costTime = end - begin;


        //记录操作日志
        OperateLog operateLog = new OperateLog(null,operateUser,operateTime,className,methodName,methodParams,returnValue,costTime);
        operateLogMapper.insert(operateLog);

        log.info("AOP记录操作日志: {}" , operateLog);

        return result;
    }

}

代码实现细节: 获取request对象,从请求头中获取到jwt令牌,解析令牌获取出当前用户的id。


总结

代理介绍:

1

AOP名词理解:

1

AOP思维以及与代理的关系:

1

  • 13
    点赞
  • 17
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 2
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

道格维克

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值