Web后端开发(AOP)

目录

AOP基础

        AOP概述

        AOP快速入门

        AOP核心概念

AOP进阶

        通知类型

        通知顺序

        切入点表达式

execution

annotation

        连接点

AOP案例


hello啊各位,今天我们来学习AOP,AOP,即spring框架的第二大核心,我们分为三部分来讲

AOP基础

        AOP概述

        首先,什么是AOP,AOP:Aspect Oriented Programing(面相切面编程,面向方面编程),其实就是面向特定方法编程。

        我们举一个例子来说明一下,现在我们的项目功能运行较慢,我想要定位执行耗时较长的业务方法,就需要统计每一个业务方法的执行耗时。我们如何统计呢?大部分想的肯定是记录一个开始时间和结束时间,然后运行完之后相减即可,这种思路没问题,但是这样是否过于冗余,繁琐呢?所有的业务方法都需要改动,但是工程量过于大。此时,如果我们利用AOP面向方法进行编程,我们就可以做到在不改动原始方法的基础上,来针对于原始的方法继续编程

        而我们AOP面向切面编程最主流的方式就是动态代理,即动态代理是面向切面编程最主流的实现。而SpringAOP是Spring框架的高级技术,旨在管理bean对象的过程中,主要通过底层的动态代理机制,对特定的方法进行编程

        AOP快速入门

        接下来我们通过一个简单的入门程序来对AOP进行一个快速入门,任务即统计前面案例中各个业务层方法执行耗时,从而定位出执行耗时较长的方法。具体步骤分为两步

  • 导入依赖:在pop.xml中导入AOP的依赖
  • 编写AOP程序:针对于特定方法根据业务逻辑进行编程

Web后端开发(AOP)_第1张图片

接下来我们进行代码实现,首先引入依赖


        
            org.springframework.boot
            spring-boot-starter-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
@Aspect //AOP类
public class TimeAspect {
    
    @Around("execution(* com.itheima.service.*.*(..))") //切入点表达式
    //当我们运行com.itheima.service下所有的接口或者是类当中所有的方法时,都会运行这个方法公共的逻辑代码
    public Object recordTime(ProceedingJoinPoint joinPoint) throws Throwable {
        //1.记录开始时间
        long begin = System.currentTimeMillis();

        //2.调用原始方法运行
        Object result = joinPoint.proceed();

        //3.记录结束时间,计算方法时间耗时
        long end = System.currentTimeMillis();
        log.info(joinPoint.getSignature() + "方法执行耗时:{}ms",end-begin);

        return result;
    }
}

接下来我们启动服务做一个测试,打开部门管理,接着看一下控制台,可以发现执行耗时已经出来

Web后端开发(AOP)_第2张图片

        以上便是该入门程序所有的演示了,其实aop的应用远不止于此,还可以进行记录操作日志、极限控制、事务管理等等很多,同时呢,我们也可以发现AOP代码完成的优势:代码无侵入、减少重复代码、提高开发效率、维护方便

        AOP核心概念

接下来,我们来讲一下AOP的核心概念

  • 连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
  • 通知:指那些抽取出来的重复的逻辑,也就是共性功能(最终体现为一个方法)
  • 切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
  • 切面:Aspect,描述通知和切入点的对应关系(通知+切入点)
  • 目标对象:Target,通知所应用的对象
  • 切入点表达式:execution(* com.itheima.service.*.*(..))

AOP执行流程

        一旦我们进行了AOP程序的开发,那最终执行的就不再是原始的目标对象,而是基于目标对象所生成的代理对象。

AOP进阶

接下来我们要对AOP进行详细的讲解,大致分为四部分

        通知类型

        在入门程序中,我们已经使用了最为强大的通知类型:@Around:环绕通知,即此注解的通知方法在目标方法前、后都被执行。除了环绕通知,在springAOP当中还支持以下四种类型

Web后端开发(AOP)_第3张图片

package com.itheima.aop;


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

@Slf4j
@Component
@Aspect
public class MyAspect1 {
    @Before("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    public void before(){
        log.info("before...");
    }

    @Around("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    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.impl.DeptServiceImpl.*(..))")
    public void after(){
        log.info("after...");
    }

    @AfterReturning("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    public void afterReturning(){
        log.info("afterReturning...");
    }

    @AfterThrowing("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    public void afterThrowing(){
        log.info("afterThrowing...");
    }

}

以上便是这五种通知类型,对于这几种通知类型,还有几点注意事项需要注意

  • @Around环绕通知需要自己调用ProceedingJoinPoint.proceed()来让原始方法执行,其他通知不需要考虑目标方法执行
  • @Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值

        大家想一下,如果此时切入点表达式需要变动,那我们需要一个一个去更改吗?此时就非常繁琐了,如何解决这个问题呢?我们需要将其抽取出来,具体更改如下

@Pointcut("execution(* com.itheima.service.impl.DeptServiceImpl.*(..))")
    private void pt(){}
    
    @Before("pt()")
    public void before(){
        log.info("before...");
    }

        此时如果需要改动只需改动上面一个地方即可,如果我们想要在其他的切面类中使用这个切入点表达式,那么pt这个方法必须得是public才能使用,如果是private,那么只能在本类中操作。

最后我们再来总结一下这个特殊的注解

@PointCut:该注解的作用是将公共的切点表达式抽取出来,需要用到时引用该切点表达式即可

权限修饰度:private:仅能在当前切面类中引用该表达式

public:在其他外部的切面类中也可以引用该表达式

        通知顺序

        上述已经讲解完了springAop的通知类型,接下来我们来讲解一下通知顺序,我们知道,当有多个切面的切入点都匹配到了目标方法,目标方法执行时,多个通知方法都会被执行。那此时,这多个通知方法哪个先运行,哪个后运行?此时就涉及到我们的通知顺序。

        接下来我们新建几个相同的切面类来进行测试,即运行同样的方法看这些不同切面类的相同通知哪个先执行,哪个后执行

Web后端开发(AOP)_第4张图片

        我们可以发现在,执行顺序其实是和切面类的类名有关的,原始方法运行之前,类名排名越靠前越先执行,原始方法执行之后,类名排名越靠前越后执行。此时,如果我们想改变这种顺序,就需要通过order注解来手动设置其顺序,同理order里面的数字就代表着你设置的类名排名。

        切入点表达式

        紧接着我们来讲解一下切入点表达式的详细写法,切入点表达式,即描述切入点方法的一种表达式,其作用主要用来决定项目中的哪些方法需要加入通知。

其常见形式大致有两种

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

2.@annotation(....):根据注解匹配

execution

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

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

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

同时,execution可以使用通配符描述切入点

  • *:单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
  • execution(* com.*.service.*.update*(*))
  • ..:多个连续的任意符号,可以通配任意层级的包,或任何类型、任意个数的参数
  • execution(* com.itheima..DeptService.*(..))

接下来我们通过一个切入点表达式匹配两个方法

public List list()

public void delete(Integer id)

可以发现这两个方法开始不同,结束不同,甚至一个有参,一个无参。

@Pointcut("execution(* com.itheima.service.DeptService.list()) ||" + "execution(*com.itheima.service.DeptService.delete(java.lang.Integer))")

        即我们可以如上满足匹配两个方法,我们在项目开发过程中,根据业务需要,可以使用且(&&)、或(||)、非(!)来组合比较复杂的切入点表达式

书写建议

  • 所有业务方法在命名时尽量规范,方便切入点表达式快速匹配。如:查询类方法都是find开头,更新类方法都是update开头。
  • 描述切入点方法通常基于接口描述,而不是直接描述实现类,增强拓展性
  • 在满足业务需要的前提下,尽量缩小切入点的匹配范围。如:包名匹配尽量不使用..,使用*匹配单个包
annotation

        刚刚我们去匹配多个无规则的方法,可以发现利用execution不是很方便了,此时我们可以借助另一种切入点表达式annotation来描述这一类切入点,从而来简化切入点表达式的书写。

@annotation切入点表达式,用于匹配标识有特定注解的方法

@Pointcut("@annotation(com.itheima.aop.MyLog)")
    private void pt(){}
//定义一个注解MyLog
    @MyLog
    @Override
    public List list() {
        List deptList = deptMapper.list();
        return deptList;
    }

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

所以此时我们只需要在想匹配的方法上加上定义的注解即可

以上便是我们的两种切入点表达式了

        连接点

        讲解完了切入点表达式,我们来讲解我们的最后一个知识点,连接点。连接点,可以简单理解为被aop控制的方法,在spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等。

  • 对于@Around通知,获取连接点信息只能使用 ProceedingJoinPoint
  • 对于其他四种通知,获取连接点信息只能使用JoinPoint,它是ProceedingJoinPoint的父类型
@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的相关知识就已经全部解释完成。

AOP案例

接下来,我们要紧接着对之前的tlias案例利用AOP进行一个案例应用

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

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

        大家想一下如何解决,增删改功能的方法有很多,我们需要改动每一个功能方法,然后在每一个功能接口当中都来记录这些操作日志吗?这样肯定过于繁琐,我们在每一个增删改的方法中记录操作日志的逻辑代码都是一样的,其实我们可以把这公用的逻辑代码抽取出来,然后定义在一个通知方法当中,接着通过AOP面向切面编程的方式在不改动原始功能的基础上来对原始的功能进行增强,即记录操作日志。

即我们需要对所有业务类中的增、删、改方法添加统一功能,使用AOP技术最为方便

        此时我们就可以考虑使用AOP中的五种通知类型的哪种呢?我们可以看下操作日志,我们需要记录方法的返回值和方法的执行时长,此时我们就要在原始方法开始之前记录开始时间,原始方法开始之后记录结束时间,即环绕式通知类型(Around)最为适宜。

        接下来我们需要考虑最后一个问题?切入点表达式如何书写?

Web后端开发(AOP)_第5张图片

        我们可以发现这些方法命名规则不一样,而且也没有共同的前缀和后缀,此时如果使用execution表达式会比较繁琐,那么我们就可以使用第二种切入点表达式,即@annotation。

接下来我们需要详细说明一下具体步骤,即准备工作和编码

准备

  • 在案例工程中引入AOP的起步依赖
  • 导入准备好的数据库表结构,并引入对应的实体类

编码

  • 自定义注解@Log
  • 定义切面类,完成记录操作日志的逻辑

接下来我们就根据上述步骤来完成具体的代码实现

首先,起步依赖前面的入门程序已经演示过了,故不再演示。

第二步,导入数据库表结构,引入实体类

-- 操作日志表
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.ittaotao.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.ittaotao.mapper;

import com.ittaotao.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});")
    public void insert(OperateLog log);

}

在这个mapper接口中准备好了一个insert方法,就是为了往日志表中来插入日志数据。

以上便是我们的准备工作,接下来我们来进行编码的操作

首先,建立一个注解

package com.ittaotao.anno;

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 Log { //仅仅起到标识方法的作用
}

然后,我们建立切面类,连包带类一次创建

package com.ittaotao.aop;

import com.alibaba.fastjson.JSONObject;
import com.ittaotao.mapper.OperateLogMapper;
import com.ittaotao.pojo.OperateLog;
import com.ittaotao.utils.JwtUtils;
import io.jsonwebtoken.Claims;
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 javax.servlet.http.HttpServletRequest;
import java.time.LocalDateTime;
import java.util.Arrays;


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

    @Autowired
    private HttpServletRequest request;

    @Autowired
    private OperateLogMapper operateLogMapper;

    @Around("@annotation(com.ittaotao.anno.Log)") //即匹配方法上加的有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;
    }
}

编写完之后,我们只需在部门管理和员工管理的增删改方法上加上@Log注解即可,启动服务,进行测试

此时我通过用户张无忌新增了一个人事部,同时删除了一个部门,可以查看数据表中的记录

以上便是我们通过AOP记录操作日志的这个案例

我们需要注意的点是获取当前登录用户

  • 获取request对象,从请求头中获取到jwt令牌,解析令牌获取出当前用户的id

        那么以上便是我们这篇博客的内容了,也就是AOP的所有内容了,如有疑问还请各位评论区多多指教,点赞收藏+关注,感谢!!!

你可能感兴趣的:(java,开发语言,spring,boot,spring,后端)