SpringBoot的AOP理解

1:AOP的快速入门和核心概念:

AOP:Aspect Oriented Programming(面向切面编程、面向方面编程),其实就是面向特定方法编程。

比如你想统计业务中每个方法的执行耗时,那我们最初的想法就是对每个方法一开始写一个获取方法运行开始时间,然后再来一个获取方法运行结束时间
然后相减,那这个做法思路简单,不过问题也很明显,就是如果都每个方法都这样做,我们需要写很多重复的代码,所以AOP就可以解决这样的问题


AOP快速入门:


第一步:引入AOP依赖:

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

第二步:编写AOP程序:针对特定方法根据业务需要进行编程

@Component//将这个类交给IOC容器管理
@Aspect//声明这个是个AOP类
public class TimeAspect {
    @Around("execution(* com.springboottlias.service.*.*(..))")//声明那些方法被AOP类编程(切入点表达式)
    public Object recordTime(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
        long begin = System.currentTimeMillis();//获取方法开始运行时间,这也是AOP类中方法的第一个步
        Object object = proceedingJoinPoint.proceed();//调用原始方法运行
        long end = System.currentTimeMillis();//获取方法运行结束时间
        log.info(proceedingJoinPoint.getSignature()+"执行耗时:{}ms",end-begin);
        return object;
    }
}

AOP核心概念:

连接点:JoinPoint,可以被AOP控制的方法(暗含方法执行时的相关信息)
通知:Advice,指哪些重复的逻辑,也就是共性功能(最终体现为一个方法)
切入点:PointCut,匹配连接点的条件,通知仅会在切入点方法执行时被应用
切面:Aspect,描述通知与切入点的对应关系(通知+切入点)
目标对象:Target,通知所应用的对象

这些都是一些基本概念:根据代码更好理解这些概念可以看这个文件夹下的图

AOP执行流程:


    一旦我们进行了AOP的方法开发,那我们运行得就不是原始对象得方法了,运行得就是一个代理对象
    这个代理对象如何理解呢:可以理解为是原始对象方法得加强版(这个加强得意思就是在这个代理对象中多了其它的方法)
    当我们想执行原来的原始对象方法,这个时候我们就不是执行原始对象,我们执行的就是这个代理对象。

2:AOP通知类型:

1:@Around:环绕通知,此注解标注的通知方法在目标方法前、后都被执行(如果目标方法有异常,那环绕通知的第三格部分结束方法就不会执行)
2:@Before:前置通知,此注解标注的通知方法在目标方法前被执行
3:@After :后置通知,此注解标注的通知方法在目标方法后被执行,无论是否有异常都会执行
4:@AfterReturning : 返回后通知,此注解标注的通知方法在目标方法后被执行,有异常不会执行(这个通知程序必须正常执行才会运行)
5:@AfterThrowing : 异常后通知,此注解标注的通知方法发生异常后执行(这个只有程序有异常了才会运行)

从上面几种通知就能看出来,第四个和第五个是两个对立的通知。

注意:
@Around环绕通知需要自己调用 ProceedingJoinPoint.proceed() 来让原始方法执行,其他通知不需要考虑目标方法执行
@Around环绕通知方法的返回值,必须指定为Object,来接收原始方法的返回值。(拿不到返回值的话,界面上就不会显示数据)

切入点表达式小技巧:

@Pointcut("execution(* com.springboottlias.service.*.*(..))")
 private void pt(){}
 @Around("pt()")//声明那些方法被AOP类编程(切入点表达式)

这个切入点表达式就类似于将相同的切入点表达式抽取到类上,当然这个不是抽取到类上
写一个方法,然后将这个切入点表达式放在里面。


这个切入点表达式的方法还可以在其它类中引用,不过肯定你得把private变成public

3:AOP通知顺序:

    

当有多个切面的切入点都匹配到了目标方法,目标方法运行时,多个通知方法都会被执行。

这很好理解,比如你有很多个类,同时匹配了一个方法,那那个类会先执行呢?

答案是和切面类得类名有关系:(和Filter执行顺序一样)
不过这里也有点不同,比如你用到是Around通知类型,你会有两个方法,一个在目标方法执行前执行,一个后执行
这里你可以理解这是一个栈
比如你这个时候有MyAspect2 ,MyAspect3,MyAspect4
四个类,你同时匹配相同得方法并且执行,那按照上面得结论来说,你是MyAspect2先执行,
但是MyAspect2中是有两个方法得,这里就得提到我上面说的栈了,MyAspect2先进去,最后出来
所以MyAspect2得after(目标方法结束之后执行的方法)反而是最后执行的,
所以MyAspect4的after却是最先执行的,这就是这个执行顺序和Filter不同的地方。

我们也可以手动更改这个执行顺序:
    用 @Order(数字) 加在切面类上来控制顺序
    目标方法前的通知方法:数字小的先执行
    目标方法后的通知方法:数字小的后执行


在这个类上@Order(数字)就行。

4:AOP切入点表达式execution:

切入点表达式:描述切入点方法的一种表达式
作用:主要用来决定项目中的哪些方法需要加入通知 (Advice)


常见形式:


1:execution(……):根据方法的签名来匹配
2:@annotation(……) :根据注解匹配

切入点表达式的语法:


execution(访问修饰符?  返回值  包名.类名.?方法名(方法参数) throws 异常?)
例子:execution(public void com.springboottlias.service.impl.DeptServiceImpl.deletedept(java.lang.Integer))

访问修饰符:可省略(比如: public、protected)

切入点表达式中的通配符:* 和 ..

* :单个独立的任意符号,可以通配任意返回值、包名、类名、方法名、任意类型的一个参数,也可以通配包、类、方法名的一部分
例子:execution(* com.*.service.*.update*(*))
第一个*表示:任意返回值
第二个*表示:com包下的任意包
第三个*也表示service的包名或类名
第四个*表示方法名中开头是update的方法名,如果是*deptservice这样的,说明以deptservice结尾的方法名
第五个*表示update*方法中任意的一个参数

.. :多个连续的任意符号,可以通配任意层级的包,或任意类型、任意个数的参数
例子:execution(* com.itheima..DeptService.*(..))
表示这个函数里面可以有任意个参数。

且(&&)、或(||)、非(!):

根据业务需要,可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式。
@Pointcut("execution(* com.springboottlias.service.impl.DeptServiceImpl.list())||" +
            "execution(* com.springboottlias.service.impl.DeptServiceImpl.deletedept(Integer))")

比如如果你想指定DeptServiceImpl包下的两个方法:list()和deletedept(id),然后这两个方法没什么相同点(没有共同的前后缀,返回值不一样,一个有参一个无参)这个时候可以考虑用(&&)、或(||)、非(!)来进行匹配。

切入点表达式的建议:


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

4:AOP切入点表达式annotation:

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

根据上面的切入点表达式,发现一旦方法的位置比较分散,或者说用这个execution方式很难表示,那这个时候就可以考虑使用这个annotation表达式。

操作步骤:

1:编写一个自定义的注解类:

这里需要说一下这个注解类上面的两个注解:

@Retention:描述这个注解什么时候生效

@Target:当前这个注解可以作用在那些地方

2:在需要的方法上加上注解:

这样,这两个方法就可以被AOP控制,执行我们想要执行的操作了。

5:AOP连接点:

连接点:
在Spring中用JoinPoint抽象了连接点,用它可以获得方法执行时的相关信息,如目标类名、方法名、方法参数等
对于 @Around 通知,获取连接点信息只能使用  ProceedingJoinPoint
对于其他四种通知,获取连接点信息只能使用 JoinPoint ,它是 ProceedingJoinPoint 的父类型

JoinPoint的基本方法:
String className = joinPoint.getTarget().getClass().getName(); //获取目标类名
Signature signature = joinPoint.getSignature(); //获取目标方法签名
String methodName = joinPoint.getSignature().getName(); //获取目标方法名
Object[] args = joinPoint.getArgs(); //获取目标方法运行参数 
Object res = joinPoint.proceed(); //执行原始方法,获取返回值(环绕通知)(最后一种只有环绕通知能使用)

6:AOP案例:记录操作日志

将案例中 增、删、改 相关接口的操作日志记录到数据库表中
日志信息包含:操作人、操作时间、执行方法的全类名、执行方法名、方法运行时参数、返回值、方法执行时长

整体的案例总共分为两步:

第一步 准备:

1:在案例工程中引入AOP的起步依赖


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

2:导入资料中准备好的数据库表结构,并引入对应的实体类


下面是实体类:
@Data
@AllArgsConstructor
@NoArgsConstructor
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; //操作耗时
}

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

第二步:编码:

1:自定义注解 @Log


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


2:定义切面类,完成记录操作日志的逻辑


@Slf4j
@Component//将这个类交给IOC容器管理
@Aspect//声明这个类是一个AOP类
public class OperateLogAspect{

    @Autowired
    private HttpServletRequest request;


    @Autowired
    private OperateLogMapper operateLogMapper;

    @Pointcut("@annotation(com.springboottlias.anno.Log)")
    private void pt(){}
    @Around("pt()")
    public Object RecordtoDatebase(ProceedingJoinPoint proceedingJoinPoint) throws Throwable {

        //操作人ID - 当前登录员工ID
        //获取请求头中的jwt令牌,解析令牌
        String jwt = request.getHeader("token");
        Claims claims = JwtUtils.parseJWT(jwt);
        Integer operateUserId = (Integer) claims.get("id");
        //操作时间
        LocalDateTime operateTime = LocalDateTime.now();

        //操作类名
        String classname = proceedingJoinPoint.getTarget().getClass().getName();

        //操作方法名
        String methodname = proceedingJoinPoint.getSignature().getName();

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


        long begin = System.currentTimeMillis();//原始方法执行之前的时间
        //调用原始目标方法运行
        Object res = proceedingJoinPoint.proceed();

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

        //操作耗时
        long end = System.currentTimeMillis();//原始方法执行之后的时间
        long costtime = end-begin;

        //记录操作日志
        OperateLog operateLog = new OperateLog(null,operateUserId,operateTime,classname,
                methodname,methodParams,returnValue,costtime);
        operateLogMapper.insert(operateLog);
        log.info("AOP操作日志:{}",operateLog);
        return res;
    }
}

3:在想要的方法上加上注解@Log:

    @Log
    @DeleteMapping("/{id}")
    public Result detele(@PathVariable Integer id){
        log.info("删除部门:{}",id);
        deptservice.deletedept(id);
        return Result.success();
    }


    /**
     * 增加部门操作
     */
    @Log
    @PostMapping
    public Result insert(@RequestBody Dept dept){
        log.info("增加部门");
        deptservice.insertdept(dept);
        return Result.success();
    }

注意点:

1:你想要获得操作人的ID,就得用到JWT令牌功能:你得先从请求头中获得jwt令牌,并且解析token
现在的问题就转化为了,你怎么得到这个令牌,我们知道令牌是在请求头中的,请求头的对象是Request
所以,我们可以从IOC容器中获得Request对象,然后获取令牌。
    @Autowired
        private HttpServletRequest request;

//操作人ID - 当前登录员工ID
        //获取请求头中的jwt令牌,解析令牌
        String jwt = request.getHeader("token");
        Claims claims = JwtUtils.parseJWT(jwt);
        Integer operateUserId = (Integer) claims.get("id");


2:第二个处理的点就是这个方法返回值这边的处理:
用到了之前的一个工具类,将对象转化成json格式的数据。

3:如果你想知道那个方法加了@Log注解:将鼠标放在这个Log上按住ctrl就可以看到了。

  • 15
    点赞
  • 19
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值