Spring AOP中三种增强方式的示例详解

 更新时间:2022年07月04日 10:22:57   作者:wl_Honest  
AOP (Aspect Orient Programming),直译过来就是 面向切面编程。AOP 是一种编程思想,是面向对象编程(OOP)的一种补充。本文为大家介绍了Spring AOP中三种增强方式,感兴趣的可以了解一下

什么是AOP

AOP (Aspect Orient Programming),直译过来就是 面向切面编程。AOP 是一种编程思想,是面向对象编程(OOP)的一种补充。面向对象编程将程序抽象成各个层次的对象,而面向切面编程是将程序抽象成各个切面。从《Spring实战(第4版)》图书中扒了一张图:

从该图可以很形象地看出,所谓切面,相当于应用对象间的横切点,我们可以将其单独抽象为单独的模块。

为什么需要AOP

想象下面的场景,开发中在多个模块间有某段重复的代码,我们通常是怎么处理的?显然,没有人会靠“复制粘贴”吧。在传统的面向过程编程中,我们也会将这段代码,抽象成一个方法,然后在需要的地方分别调用这个方法,这样当这段代码需要修改时,我们只需要改变这个方法就可以了。然而需求总是变化的,有一天,新增了一个需求,需要再多出做修改,我们需要再抽象出一个方法,然后再在需要的地方分别调用这个方法,又或者我们不需要这个方法了,我们还是得删除掉每一处调用该方法的地方。实际上涉及到多个地方具有相同的修改的问题我们都可以通过 AOP 来解决。

AOP术语

AOP 领域中的特性术语:

  1. 通知(Advice): AOP 框架中的增强处理。通知描述了切面何时执行以及如何执行增强处理。
  2. 连接点(join point): 连接点表示应用执行过程中能够插入切面的一个点,这个点可以是方法的调用、异常的抛出。在 Spring AOP 中,连接点总是方法的调用。
  3. 切点(PointCut): 可以插入增强处理的连接点。
  4. 切面(Aspect): 切面是通知和切点的结合。
  5. 引入(Introduction):引入允许我们向现有的类添加新的方法或者属性。
  6. 织入(Weaving): 将增强处理添加到目标对象中,并创建一个被增强的对象,这个过程就是织入。

通过注解声明5种通知类型

Spring AOP 中有 5 中通知类型,分别如下:

本章中主要以@Before、@After和@Around为例展示AOP的增强方式。

首先引入依赖,这里只放aop的依赖,其它的依赖请根据自己的实际情况引入:

<!-- aop -->
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aop</artifactId>
    <version>5.1.18.RELEASE</version>
</dependency>
<dependency>
    <groupId>org.springframework</groupId>
    <artifactId>spring-aspects</artifactId>
    <version>5.1.18.RELEASE</version>
</dependency>

接着新建一个切面类TesstAspect,并且定义3个切点,就是后面要测试的3个切点:

package com.wl.standard.aop.aspect;
 
import com.wl.standard.util.JoinPointUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.ArrayUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
 
/**
 * @author wl
 * @date 2022/7/2 16:08
 */
@Slf4j
@Component
@Aspect
public class TestAspect {
 
    @Pointcut("execution(public * com.wl.standard.service.TravelRecordService.getAllRecord(..))")
    public void pointCut1(){};
 
    @Pointcut("execution(public * com.wl.standard.service.CityRailService.getTopRail(..))")
    public void pointCut2(){};
 
    @Pointcut("execution(public * com.wl.standard.service.CityGdpService.compareGDP(..))")
    public void pointCut3(){};
 
}

备注:execution():用于匹配方法执行的连接点,第一个*表示匹配任意的方法返回值

@Before

先测试第一个增强方法,在切点方法之前执行,因为是简单测试,就只打印一下日志就好了:

package com.wl.standard.aop.aspect;
 
import com.wl.standard.util.JoinPointUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.ArrayUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
 
/**
 * @author wl
 * @date 2022/7/2 16:08
 */
@Slf4j
@Component
@Aspect
public class TestAspect {
 
    @Pointcut("execution(public * com.wl.standard.service.TravelRecordService.getAllRecord(..))")
    public void pointCut1(){};
 
    @Pointcut("execution(public * com.wl.standard.service.CityRailService.getTopRail(..))")
    public void pointCut2(){};
 
    @Pointcut("execution(public * com.wl.standard.service.CityGdpService.compareGDP(..))")
    public void pointCut3(){};
 
    @Before("pointCut1()")
    public void doBefore(JoinPoint joinPoint) {
        log.info("当前线程: {} 开始执行查询前任务...", Thread.currentThread().getName());
    }
}

启动项目,进入swagger的页面调用接口测试:

调用接口后,在控制台可以看到日志打印的先后顺序,先执行的@Before里的增强方法再执行的service里的方法:

@After

接着测试@After,为了更好的展示增强方式,这次利用JoinPoint获取参数。

说明:Joinpoint是AOP的连接点。一个连接点代表一个被代理的方法。

为了获取参数的方法能够复用,这里新建一个工具类JoinPointUtils:

package com.wl.standard.util;
 
import org.apache.commons.lang.ArrayUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.reflect.MethodSignature;
 
/**
 * JoinPoint 工具类
 * @author wl
 * @date 2022/7/2 21:55
 */
public class JoinPointUtils {
 
    public static <T> T getParamByName(JoinPoint joinPoint, String paramName, Class<T> clazz) {
        // 获取所有参数的值
        Object[] args = joinPoint.getArgs();
        // 获取方法签名
        Signature signature = joinPoint.getSignature();
        MethodSignature methodSignature = (MethodSignature) signature;
        // 在方法签名中获取所有参数的名称
        String[] parameterNames = methodSignature.getParameterNames();
        // 根据参数名称拿到下标, 参数值的数组和参数名称的数组下标是一一对应的
        int index = ArrayUtils.indexOf(parameterNames, paramName);
        // 在参数数组中取出下标对应参数值
        Object obj = args[index];
        if (obj == null) {
            return null;
        }
        // 将object对象转为Class返回
        if (clazz.isInstance(obj)) {
            return clazz.cast(obj);
        }
        return (T) obj;
    }
}

接着编写@After的增强方法,在切点方法之后执行:

package com.wl.standard.aop.aspect;
 
import com.wl.standard.util.JoinPointUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.ArrayUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
 
/**
 * @author wl
 * @date 2022/7/2 16:08
 */
@Slf4j
@Component
@Aspect
public class TestAspect {
 
    @Pointcut("execution(public * com.wl.standard.service.TravelRecordService.getAllRecord(..))")
    public void pointCut1(){};
 
    @Pointcut("execution(public * com.wl.standard.service.CityRailService.getTopRail(..))")
    public void pointCut2(){};
 
    @Pointcut("execution(public * com.wl.standard.service.CityGdpService.compareGDP(..))")
    public void pointCut3(){};
 
    @Before("pointCut1()")
    public void doBefore(JoinPoint joinPoint) {
        log.info("当前线程: {} 开始执行查询前任务...", Thread.currentThread().getName());
    }
 
    @After("pointCut2()")
    public void doAfter(JoinPoint joinPoint) {
        Integer index = JoinPointUtils.getParamByName(joinPoint, "index", Integer.class);
        log.info("当前线程: {}执行完任务,请求参数值: {}", Thread.currentThread().getName(), index);
    }
}

为了方便理解这里获取的参数,下面放一下这里切入的方法:

然后一样的流程,启动项目,在swagger页面里调用接口:

@Around 

前面2个例子一个是在切点之前执行,一个是在切点之后执行,如果项目中我们想要记录一个sql执行的耗时时间,应该怎么做?

@Around环绕通知:它集成了@Before、@AfterReturing、@AfterThrowing、@After四大通知。需要注意的是,它和其他四大通知注解最大的不同是需要手动进行接口内方法的反射后才能执行接口中的方法,换言之,@Around其实就是一个动态代理。

利用@Around的话,就可以编写一个方法,切入多个切点记录耗时了:

package com.wl.standard.aop.aspect;
 
import com.wl.standard.util.JoinPointUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.ArrayUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
 
/**
 * @author wl
 * @date 2022/7/2 16:08
 */
@Slf4j
@Component
@Aspect
public class TestAspect {
 
    @Pointcut("execution(public * com.wl.standard.service.TravelRecordService.getAllRecord(..))")
    public void pointCut1(){};
 
    @Pointcut("execution(public * com.wl.standard.service.CityRailService.getTopRail(..))")
    public void pointCut2(){};
 
    @Pointcut("execution(public * com.wl.standard.service.CityGdpService.compareGDP(..))")
    public void pointCut3(){};
 
    @Before("pointCut1()")
    public void doBefore(JoinPoint joinPoint) {
        log.info("当前线程: {} 开始执行查询前任务...", Thread.currentThread().getName());
    }
 
    @After("pointCut2()")
    public void doAfter(JoinPoint joinPoint) {
        Integer index = JoinPointUtils.getParamByName(joinPoint, "index", Integer.class);
        log.info("当前线程: {}执行完任务,请求参数值: {}", Thread.currentThread().getName(), index);
    }
 
    @Around("pointCut3()")
    public Object  doAround(ProceedingJoinPoint pjp) throws Throwable {
        long start = System.currentTimeMillis();
        // 调用执行目标方法(result为目标方法执行结果),必须有此行代码才会执行目标调用的方法(等价于@befor+@after),否则只会执行一次之前的(等价于@before)
        Object result = pjp.proceed();
        long end = System.currentTimeMillis();
        log.info(pjp.getTarget().getClass().getSimpleName() + "->" + pjp.getSignature().getName() + " 耗费时间:" + (end - start) + "毫秒");
        return result;
    }
}

启动项目,调用接口,看控制台输出:

以上就是Spring AOP中三种增强方式的示例详解的详细内容,更多关于Spring AOP增强方式的资料请关注脚本之家其它相关文章!

相关文章

  • Java中可变长度参数代码详解

    Java中可变长度参数代码详解

    这篇文章主要介绍了Java中可变长度参数代码详解,涉及了实参个数可变的定义方法,数组包裹实参等几个问题,具有一定参考价值,需要的朋友可以了解下。
    2017-12-12
  • 利用mysql实现的雪花算法案例

    利用mysql实现的雪花算法案例

    这篇文章主要介绍了利用mysql实现的雪花算法案例,具有很好的参考价值,希望对大家有所帮助。一起跟随小编过来看看吧
    2020-08-08
  • java解析.yml文件方式

    java解析.yml文件方式

    这篇文章主要介绍了java解析.yml文件方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教
    2024-01-01
  • springboot配置http跳转https的过程

    springboot配置http跳转https的过程

    SSL是为网络通信提供安全以及保证数据完整性的的一种安全协议,SSL在网络传输层对网络连接进行加密,这篇文章主要介绍了springboot配置http跳转https的过程,需要的朋友可以参考下
    2023-04-04
  • JavaWeb乱码问题的终极解决方案(推荐)

    JavaWeb乱码问题的终极解决方案(推荐)

    这篇文章主要给大家介绍了关于JavaWeb乱码问题的终极解决方案,文中通过示例代码介绍的非常详细,对大家学习或者使用JavaWeb具有一定的参考学习价值,需要的朋友们下面来一起学习学习吧
    2019-04-04
  • Spring Security如何在Servlet中执行

    Spring Security如何在Servlet中执行

    这篇文章主要介绍了Spring Security如何在Servlet中执行,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友可以参考下
    2020-04-04
  • Java数组(Array)最全汇总(下篇)

    Java数组(Array)最全汇总(下篇)

    这篇文章主要介绍了Java数组(Array)最全汇总(下篇),本文章内容详细,通过案例可以更好的理解数组的相关知识,本模块分为了三部分,本次为下篇,需要的朋友可以参考下
    2023-01-01
  • springcloud项目快速开始起始模板的实现

    springcloud项目快速开始起始模板的实现

    本文主要介绍了springcloud项目快速开始起始模板思路的实现,文中通过示例代码介绍的非常详细,具有一定的参考价值,感兴趣的小伙伴们可以参考一下
    2021-12-12
  • 解决Java调用BAT批处理不弹出cmd窗口的方法分析

    解决Java调用BAT批处理不弹出cmd窗口的方法分析

    本篇文章是对Java调用BAT批处理不弹出cmd窗口的方法进行了详细的分析介绍,需要的朋友参考下
    2013-05-05
  • Java inputstream和outputstream使用详解

    Java inputstream和outputstream使用详解

    这篇文章主要介绍了Java inputstream和outputstream使用详解,本篇文章通过简要的案例,讲解了该项技术的了解与使用,以下就是详细内容,需要的朋友可以参考下
    2021-08-08

最新评论