Java学习:SpringAOP的学习

介绍

AOP(Aspcet Oriented Programming):面向切面编程,面向方面编程,其实就是面向特定方法的编程,在不改动源代码的基础上,实现代码的增强,或者是功能的改变。

SpringAop是Spring框架的高级技术,旨在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程。动态代理是面向切面编程最主流的实现。看到一篇关于动态代理不错的文章:java | 什么是动态代理? - 知乎

AOP执行流程

先介绍AOP简单的执行流程,在后面具体业务实现会再次加深、强调。引用关于上文提到的什么是动态代理的文章的例子。

假设有黄牛和我两个例子,我现在想去看演唱会,但是演唱会的门票特别难抢,我就想让黄牛替我去抢票,而我只管去看演唱会就可以。那此时,黄牛就是代理对象,我是目标对象,黄牛代理我去抢票。

Spring会生成一个代理对象实现对目标对象的功能增强。代理对象如何生成哪?AOP在实现时,我们要创建一个AOP类,AOP类中会要实现目标对象的方法之前,执行自己的方法(比如抢票),然后执行目标对象的方法(看演唱会),最后执行目标对象方法执行后的AOP的方法。这样就生成了一个代理对象。当程序调用目标对象去完成看演唱会这个过程时,就会生成一个代理对象,自动注入到程序的变成了代理对象而不是目标对象,从而实现功能的增强。

AOP核心概念

连接点:JoinPoint ,可以被AOP控制的方法(暗含方法执行时的相关信息)比如看演唱会

通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)

切入点:PointCut ,匹配连接点的条件,通知仅会在切入点方法执行时被应用。其实就是当执行到哪一个方法时,AOP该切入,执行通知。由切入点表达式指定具体切入点

切面:Aspect切入点表达式与通知结合形成切面,切面描述了,在什么方法下执行aop操作

切面类:被@Aspect注解所标识的类

目标对象:Target,通知所应用的对象

AOP简单案例

统计各个业务层方法执行耗时

导入依赖

在pom.xml文件中导入AOP的依赖

<!--AOP-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <version>3.2.0</version>
        </dependency>

编写AOP程序

针对特定方法根据业务需要进行编程

package com.itheima.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Slf4j
@Component//bean对象
@Aspect //AOP类
public class TimeAspect {

    @Around("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))") //切入点表达式,指定要在什么方法上执行通知
    public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
        //1. 记录开始时间
        long begin = System.currentTimeMillis();
        //2. 调用原始方法运行
        Object result = joinPoint.proceed();//返回一个原始方法执行的返回值
        //3. 记录结束时间, 计算方法执行耗时
        long end = System.currentTimeMillis();
        //getSignature 得到原始方法的签名,封装了原始方法的方法名,形参,返回值类型
        log.info(joinPoint.getSignature()+"方法执行耗时: {}ms", end-begin);
        
        return result;
    }

}

方法签名:由方法名、参数列表和返回类型组成。

具体而言,方法签名包括以下几个部分:

方法名:方法的名称,用于调用该方法;

参数列表:包含了方法的输入参数,每个参数都包括参数类型和参数名;

返回类型:方法的返回值类型,表示方法执行完毕后返回的结果类型。

运行启动类,然后进行服务的访问,控制台输出:(具体业务实现用自己的即可,使用的ngix服务器)

 实现了统计业务层的消耗时间。

AOP通知类型

AOP通知类型分为五种:

1、@Around :环绕通知,次注解标注的通知方法在目标前,后都被执行,也就是目标方法执行前执行通知方法,目标方法执行后也要执行通知方法。

2、@Before:前置通知,此注解标注的通知方法在目标方法前被执行

3、@After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行

4、@AfterReturning:返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行

5、@AfterThrowing:异常后通知,此注解标注的通知方法发生异常后执行

注意:

@Around环绕通知需要自己调用ProceedingJoinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行

@Around环绕通知方法的返回值,必须指定为Object,来接受原始方法的返回值

通知顺序

当多个切面的切入点都匹配到了目标方法,当目标方法运行时,多个通知方法都会运行,那么哪一个通知方法会先执行?

其实是和AOP类的类名有关系,默认是按照类名的字母排名来的。

目标方法前的通知方法:字母排名靠前的先执行

目标方法后的通知方法,字母排名靠前的后执行

当然我们可以通过在切面类上添加@Order注解,来控制执行顺序

目标方法前的通知方法:数字小的先执行

目标方法后的通知方法,数字小的后执行

切入点表达式

切入点表达式是描述切入点方法的一种表达式,主要是用来决定项目中的哪些方法需要加入通知。

@PointCut注解

该注解的作用就是把公共的切点表达式抽取出来,需要用到的时候引用该切点表达式即可,例如下面。

    @Pointcut("execution(* com.itheima.service.DeptService.*(..))")
    private void pt(){}

    @Before("pt()")
    public void before(JoinPoint joinPoint){
        
        log.info("MyAspect8 ... before ...");
    }

切入点表达式一般有两种形式:

1、execution():根据方法的签名来匹配

 

 这个表达式就是指 * 任意返回值类型,在com.itheima.service.impl的包下,DeptServiceImpl实现类中,* 为任意的方法,(..)中任意的参数类型和个数

如果多个切点表达式就用||来连接

2、根据@annotation来匹配

如果要标识多个不同包下的方法,按照切点表达式就非常繁琐,根据@annotation就可以用于标识有特定注解的方法。

我们需要先自定义注解,用来标识目标方法。

package com.itheima.aop;

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

@Retention(RetentionPolicy.RUNTIME)//指定注解生效时间:在运行时生效
@Target(ElementType.METHOD)//指定只能作用在方法上
public @interface MyLog {
}

然后在需要标识的方法上添加该注解

    @MyLog
    @Override
    public List<Dept> list() {
        List<Dept> deptList = deptMapper.list();
        return deptList;
    }

    @MyLog
    @Override
    public void delete(Integer id) {
        //1. 删除部门
        deptMapper.delete(id);
    }
package com.itheima.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

//切面类
@Slf4j
@Aspect
@Component
public class MyAspect7 {

    //匹配DeptServiceImpl中的 list() 和 delete(Integer id)方法
    //@Pointcut("execution(* com.itheima.service.DeptService.list()) || execution(* com.itheima.service.DeptService.delete(java.lang.Integer))")
    @Pointcut("@annotation(com.itheima.aop.MyLog)")
    private void pt(){}

    @Before("pt()")
    public void before(){
        log.info("MyAspect7 ... before ...");
    }

}

连接点

JoinPoint ,可以被AOP控制的方法。在Spring中用JoinPoint抽象了连接点,利用JoinPoint可以获得方法执行时的有关信息,比如方法类名、方法名、方法参数(通过getSignature方法签名)。

对于 @Around 通知,获取连接点信息只能使用ProceedingJoinPoint。

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

package com.itheima.aop;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;

import java.util.Arrays;

//切面类
@Slf4j
@Aspect
@Component
public class MyAspect8 {

    @Pointcut("execution(* com.itheima.service.DeptService.*(..))")
    private void pt(){}

   
    @Around("pt()")
    public Object around(ProceedingJoinPoint joinPoint) throws Throwable {
        log.info("MyAspect8 around before ...");

        //1. 获取 目标对象的类名 .      对象         对象类     对象类名
        String className = joinPoint.getTarget().getClass().getName();
        log.info("目标对象的类名:{}", className);

        //2. 获取 目标方法的方法名 .
        String methodName = joinPoint.getSignature().getName();
        log.info("目标方法的方法名: {}",methodName);

        //3. 获取 目标方法运行时传入的参数 .
        Object[] args = joinPoint.getArgs();
        log.info("目标方法运行时传入的参数: {}", Arrays.toString(args));

        //4. 放行 目标方法执行 .
        Object result = joinPoint.proceed();

        //5. 获取 目标方法运行的返回值 .
        log.info("目标方法运行的返回值: {}",result);

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

案例:通过AOP实现操作日志的记录

将案例中有关数据增删改的操作日志记录到数据库表中。

用自己的案例进行操作就行

1、创建数据库表结构,记录操作日志详细信息

创建了一个operate_log表用来记录详细的操作信息

2、引入AOP依赖

<!--        aop依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
            <version>3.2.0</version>
        </dependency>
 、

3、编写AOP类实现具体的业务逻辑

此处直接借用黑马的代码了,时间来不及了

package com.kk.aop;

import com.alibaba.fastjson2.JSONObject;
import com.kk.Utils.JwtUtils;
import com.kk.mapper.OperateLogMapper;
import com.kk.pojo.OperateLog;
import io.jsonwebtoken.Claims;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
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 java.time.LocalDateTime;
import java.util.Arrays;

@Slf4j
@Component
@Aspect
public class LogAspect {
    //需要调用OperateLogMapper的Insert记录日志,实现Mapper 的注入
    @Autowired
    private OperateLogMapper operateLogMapper;
    //拿到请求对象,request对象
    @Autowired
    private HttpServletRequest request;
    //环绕
    @Around("@annotation(com.kk.anno.Log)")
    public Object recordLog(ProceedingJoinPoint joinPoint) throws Throwable {
        //操作人的Id 登录人的id,通过JWT令牌中的id信息
        //获取请求头中的jwt令牌,解析出来id
        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;
    }
}

因jwt跳跃跨度较大,详细回顾一下JWT令牌的相关操作

因为我们要进行记录操作人的id,但是我们所执行的方法中,并没有对象记录操作人或者登录人的id,但是jwt令牌里面是封装了id。

如何获取jwt令牌哪?

我们将jwt令牌封装到了请求体中token下,当前端请求对数据进行操作时,我们可以通过请求体获取token,然后解析jwt令牌,获取的是一个Claims对象,Claims继承了Map

所以我们可以通过Map集合的get方法获取到claims里面的id数据。


到此,AOP实现日志记录回顾结束。如有错误,欢迎批评指正

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值