AOP技术

news/2024/11/7 7:43:20/

说明:AOP(Aspect Oriented Programming,面相切面编程),是Spring的特性之一,可以在不改变代码的情况下,对方法进行增强,其底层实现是动态代理。

一、代码实现

第一步:使用AOP,先添加依赖

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

第二步:创建AOP类

创建一个AOP类,类名上加@Component(将该类加入到IOC容器中)、@Aspect注解(表明该类是一个AOP类)

再写一个方法,对代理对象进行增强逻辑,该方法上加@Around()注解,括号内填切入点表达式,表达式里面定义的是目标对象(即被增强对象)

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;/*** AOP类(通知类)*/
@Component
@Aspect
public class MyAop {/*** 通知* @param pjp* @throws Throwable*/@Around("execution(* com.essay.service.impl.AopServiceImpl.*(..))")public Object aopFunction(ProceedingJoinPoint pjp) throws Throwable {// 记录程序开始时的时间戳System.out.println("<<<<<<<<<<<程序开始,获取开始时间戳>>>>>>>>");long begin = System.currentTimeMillis();// 获取代理方法的返回值对象Object obj = pjp.proceed();// 记录程序结束后的时间戳long end = System.currentTimeMillis();System.out.println("<<<<<<<<<<<程序结束,执行时间=" + (end - begin) + "ms>>>>>>>>");return obj;}
}

AopService、AopServiceImpl类

public interface AopService {void test1();void test2();String test3();
}
import com.essay.service.AopService;
import org.springframework.stereotype.Service;@Service
public class AopServiceImpl implements AopService {@Overridepublic void test1() {int sum = 0;for (int i = 0; i < 1000; i++) {sum += i;}System.out.println(sum);}@Overridepublic void test2() {int sum = 0;for (int i = 0; i < 10000; i++) {sum += i;}System.out.println(sum);}@Overridepublic String test3(){System.out.println("test3()...");return "这是一个带返回值的方法...";}
}

第三步:创建测试类

创建测试类,调用AopServiceImpl内的三个方法

import com.essay.service.AopService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;@SpringBootTest
public class MyTest {@Autowiredprivate AopService aopService;@Testpublic void aopTest1(){aopService.test1();}@Testpublic void aopTest2(){aopService.test2();}@Testpublic void aopTest3(){System.out.println(aopService.test3());}
}

第四步:启动

可以看出,AOP对目标对象的所有方法都做了增强。

aopTest1()方法

在这里插入图片描述


aopTest2()方法

在这里插入图片描述


aopTest3()方法

在这里插入图片描述


二、执行流程

在动态代理中(参考:http://t.csdn.cn/no8zu),使用Proxy.newProxyInstance()根据需要代理的类,会生成一个Proxy对象,该对象的方法包含了被代理类的所有方法,使用Proxy对象的方法,就是使用被代理类的方法+逻辑增强的方法。

AOP技术底层实现原理是动态代理,所以AOP的执行流程中也做了类似这样的事情。在项目启动时,程序实际上已经根据AOP类中对目标对象的增强方法,生成了代理对象,后续项目执行具体方法时,实际上就是代理对象在执行对应的方法,此时的执行结果就是目标对象的方法+AOP中的增强方法。

可以使用Debug,查看使用AOP时,aopService对象的内容

(未使用AOP)

在这里插入图片描述


(使用AOP,实际执行方法的是代理对象,执行的结果就是在AOP类中写的通知方法)
在这里插入图片描述

三、术语

AOP中的术语及指代如下,熟悉后方便沟通交流:

被增强对象(被代理对象/目标对象): 要被增强的对象【aopServiceImpl类】;

增强对象(代理对象): 增强后的对象 【Debug中看到的aopService的内容】;

连接点: 目标对象所有可以资格被增强的方法都被称为连接点 【aopServiceImpl类中的所有方法】;

切入点表达式: 把连接点转为切入点的表达式,【aopFunction()方法上面,@Around()注解里括号的内容】;

切入点: 确定了要增强的方法,由切入点表达式决定,即切入点表达式指定的方法;

通知: 最终增强的业务逻辑(方法)【aopFunction()方法】;

织入: 通知+切入点合并到一起的过程称为织入(动态代理) 【aopFunction()方法执行的过程】;

切面: 织入的结果(面向切面编程),通知+切入点=切面 【aopFunction()方法+切入点表达式指定的方法】

四、通知类型

AOP的通知类型,即增强方法的类型,除了Around(前置通知)外,还有另外四种,分别是:Before(前置通知)、AfterReturning(返回后通知)、AfterThrowing(异常后通知)和After(后置通知),使用时在方法上添加对应的注解。

为了方便记忆,可以将类型分为两类,一类是Around,另一类是非Around。因为这四种类型做的增强方法,Around也都可以做,可以理解为这四种类型是对Around的具体表现。

(1)非Around

非Around对目标对象的增强,执行结构如下:

        try {// 前置通知aopBefor();// 目标对象的方法System.out.println("这是目标对象方法");// 返回后通知aopAfterReturning();}catch (Throwable throwable){// 异常后通知aopAfterThrowing();}finally {// 后置通知aopAfter();}

我这里将非Around的四个方法都写出来,看下这四个方法对目标对象方法的增强效果

(MyAop类)

    @Before("execution(* com.essay.service.impl.AopServiceImpl.*(..))")public void aopBefor(){System.out.println("这是前置通知...");}@After("execution(* com.essay.service.impl.AopServiceImpl.*(..))")public void aopAfter(){System.out.println("这是后置通知...");}@AfterReturning("execution(* com.essay.service.impl.AopServiceImpl.*(..))")public void aopAfterReturning(){System.out.println("这是返回后通知...");}@AfterThrowing("execution(* com.essay.service.impl.AopServiceImpl.*(..))")public void aopAfterThrowing(){System.out.println("这是异常后通知...");}

添加测试方法

(AopService类)

	void test4();

(AopServiceImpl类)

    @Overridepublic void test4(){System.out.println("这是目标对象方法");}

(MyTest类)

    @Testpublic void aopTest4(){aopService.test4();}

执行结果

在这里插入图片描述
在这里插入图片描述


在目标对象方法内,设置一个异常,查看执行结果

(AopServiceImpl类)

    @Overridepublic void test4(){// 手动设置一个算数异常int i = 1/0;System.out.println("这是目标对象方法");}

在这里插入图片描述
在这里插入图片描述

根据上面的结果可分析出,这四种通知类型的执行情况:

@Before(前置通知):此注解标注的通知方法在目标方法前被执行;

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

@AfterThrowing(异常后通知):此注解标注的通知方法发生异常后执行;

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

(2)Around

@Around环绕通知,完全可以做到以上四种类型一模一样的执行步骤,代码如下

    /*** 环绕通知* @param pjp* @throws Throwable*/@Around("execution(* com.essay.service.impl.AopServiceImpl.*(..))")public void aopAround(ProceedingJoinPoint pjp) {try {System.out.println("前置通知");pjp.proceed();System.out.println("返回后通知");}catch (Throwable throwable){System.out.println("异常后通知");}finally {System.out.println("后置通知");}}

执行test4()方法

在这里插入图片描述

手动添加一个异常,执行

在这里插入图片描述

可以看到执行结果一模一样,因为环绕通知捕捉了异常,所以程序不会因为报错终止。

小结

@Around环绕通知,可以达到与其他四种通知相同的效果,但使用环绕通知需要使用ProceedingJoinPoint对象,决定切入点对象放置的位置(位置放错了,执行结果会不一样)、接收该proceed()方法的结果并返回,其他四种通知不需要。具体使用时,需根据实际需求选择。

另外,需要注意的是,如果切入点方法有返回值,但在环绕通知中未返回Object,方法执行并不会报错,而是返回null,如使用以下的通知,执行test3()方法

    /*** 通知* @param pjp* @throws Throwable*/@Around("execution(* com.essay.service.impl.AopServiceImpl.test3(..))")public void aopFunction(ProceedingJoinPoint pjp) throws Throwable {// 记录程序开始时的时间戳System.out.println("<<<<<<<<<<<程序开始,获取开始时间戳>>>>>>>>");long begin = System.currentTimeMillis();// 获取代理方法的返回值对象pjp.proceed();// 记录程序结束后的时间戳long end = System.currentTimeMillis();System.out.println("<<<<<<<<<<<程序结束,执行时间=" + (end - begin) + "ms>>>>>>>>");}

在这里插入图片描述

五、切面表达式

切面表达式为,通知注解括号里面的内容,指定了目标对象的范围。

切面表达式由以下六个部分组成,除了修饰符外均不可省略,书写规格如下:

(1)修饰符:public/private,可以省略不写;

(2)返回值:可指定返回值,或者(/*)表示所有返回值;

(3)包:全包名,(/*)代替一个包名,(/…)代替任意子包;

(4)类名:可以指定类名,或者(/*)代替,表示包下的所有类;

(5)方法名:可指定方法名,或者(/*)代替,表示类里面的所有方法;

(6)方法参数:(…/)表示任意类型的参数;

可以看到,此处切面表达式对目标对象的选择,基于包、类和方法的命名,如果包、类、方法的命名越规范,(如查询类方法以find开头,更新方法以update开头),则表达式越容易选择出目标对象。

另外,建议在满足业务需要的前提下,尽量缩小切入点的匹配范围。如,包名匹配尽量不使用…(任意子包),使用*匹配单个包,可以提高效率。

(1)@Pointcut

可使用@Pointcut注解,将切入点表达式的内容抽取出来成一个无参、无返回值、无内容、方法名随意的方法,方便后面通知直接复用切入点表达式,如前面写的四种非Around类型的通知,可以下面这样:

    @Pointcut("execution(* com.essay.service.impl.AopServiceImpl.*(..))")public void pt(){}@Before("pt()")public void aopBefor(){System.out.println("这是前置通知...");}@After("pt()")public void aopAfter(){System.out.println("这是后置通知...");}@AfterReturning("pt()")public void aopAfterReturning(){System.out.println("这是返回后通知...");}@AfterThrowing("pt()")public void aopAfterThrowing(){System.out.println("这是异常后通知...");}

值得一提的是,@Pointcut方法的权限修饰符可以设置为private/public,可决定仅被本类复用还是被其他类使用。但建议一个AOP类定义一个@Pointcut方法,仅供本类服务,避免AOP类之间相互调用@Pointcut方法,造成结构混乱。

(2)匹配类中的多个方法

如果需要匹配某个类中的多个方法,不想全选类,也不能单独选一个方法,可以使用两种方法实现

第一种:使用或(||)连接两个目标对象;

限定test1()方法、test2()方法为切入点

    @Around("execution(* com.essay.service.impl.AopServiceImpl.test1(..))"+"execution(* com.essay.service.impl.AopServiceImpl.test2(..))")

注:根据业务需要,也可以使用 且(&&)、或(||)、非(!) 来组合比较复杂的切入点表达式


第二种:使用自定义注解;

自定义一个注解,设置该注解仅用于方法上【@Target(ElementType.METHOD)】,且保留至程序运行时【@Retention(RetentionPolicy.RUNTIME)】

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 MyAnnotation {
}

在需要使用AOP的方法上,添加该注解,选择test3()、test4()方法

    @Override@MyAnnotationpublic String test3(){System.out.println("test3()...");return "这是一个带返回值的方法...";}@Override@MyAnnotationpublic void test4(){// 手动设置一个算数异常int i = 1/0;System.out.println("这是目标对象方法");}

在AOP类中,切入表达式写成以下这样,表示目标对象为加了@MyAnnotation注解的方法

    @Pointcut("@annotation(com.itheima.aop.MyAnnotation)")

注:这有点像接口的作用,将具有某一类功能/特性的类,实现同一个接口。只不过这里作用于不是类上,而是方法上。

六、AOP执行顺序

当多个AOP对同一目标对象的方法增强时,AOP的执行顺序是按照AOP类的排序顺序,当有前置通知、后置通知时,执行结果是嵌套的

我这里使用@Around,设置前置通知、后置通知对test1()方法的增强。

    @Pointcut("execution(* com.essay.service.impl.AopServiceImpl.*(..))")public void pt(){}/*** 环绕通知01* @param pjp* @throws Throwable*/@Around("pt()")public void aopAround01(ProceedingJoinPoint pjp) throws Throwable {System.out.println("01——前置通知");pjp.proceed();System.out.println("01——后置通知");}/*** 环绕通知02* @param pjp* @throws Throwable*/@Around("pt()")public void aopAround02(ProceedingJoinPoint pjp) throws Throwable {System.out.println("02——前置通知");pjp.proceed();System.out.println("02——后置通知");}

在这里插入图片描述

七、连接点参数对象(ProceedingJoinPoint)

前面介绍过,ProceedingJoinPoint对象的proceed(),表示执行目标对象的方法,当使用@Around环绕通知时,必须接收该方法执行的结果并返回。

该对象还有以下方法可供使用:

	// 1.获取切入点的类名String className = pjp.getTarget().getClass().getName();// 2.获取切入点的方法签名Signature signature = joinPoint.getSignature(); // 3. 获取切入点的方法名String methodName = pjp.getSignature().getName();// 4.获取切入点的参数String methodArgs = Arrays.toString(pjp.getArgs());

以上方法,配合Token令牌,配合AOP对一些操作方法(增加、修改、删除)进行增强,可以实现对用户操作(增加、修改、删除)的日志记录,将用户的信息、操作封装成一个日志对象,写入到数据库中。

日志类(OperateLog )

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;import java.time.LocalDateTime;@Data
@NoArgsConstructor
@AllArgsConstructor
public class OperateLog {/*** 日志ID*/private Integer id;/*** 操作人用户名*/private String operateUser;/*** 操作时间*/private LocalDateTime operateTime;/*** 操作的类名*/private String className;/*** 操作的方法名*/private String methodName;/*** 操作的方法参数*/private String methodParams;/*** 操作的方法返回值*/private String returnValue;/*** 操作的时长*/private Long costTime;
}

AOP类中通知方法

	......@Pointcut("@annotation(com.itheima.aop.MyAnnotation)")public void pt(){};@Around("pt()")public Object around(ProceedingJoinPoint pjp) throws Throwable {// 记录程序执行开始时间戳long begin = System.currentTimeMillis();// 执行切入点的方法Object obj = pjp.proceed();// 记录程序执行结束时间戳long end = System.currentTimeMillis();// 1.获取切入点的类名String className = pjp.getTarget().getClass().getName();// 2. 获取切入点的方法名String methodName = pjp.getSignature().getName();// 3.获取切入点的参数String methodArgs = Arrays.toString(pjp.getArgs());// 4.获取方法的执行时长long time = end - begin;// 5.获取当时操作人的操作时间LocalDateTime dateTime = LocalDateTime.now();// 获取Token令牌String token = request.getHeader("token");// 解析Token令牌Claims claims = JwtUtils.parseJWT(token);// 6.获取Token令牌中的用户名信息String username = ((String) claims.get("username"));// 7.将收集到的信息封装成一个日志对象OperateLog log = new OperateLog(null, username, dateTime, className, methodName, methodArgs, obj.toString(), time);// 8.写入到数据库中logMapper.insert(log);return obj;}

然后在Controller层中需要记录日志的业务方法上加上@MyAnnotation注解,表示这些方法用户进行操作时需要记录下日志信息

数据库记录的日志如下
在这里插入图片描述


http://www.ppmy.cn/news/358937.html

相关文章

13. ReentrantLock、ReentrantReadWriteLock、StampedLock讲解

13.1 关于锁的面试题 ● 你知道Java里面有那些锁 ● 你说说你用过的锁&#xff0c;锁饥饿问题是什么&#xff1f; ● 有没有比读写锁更快的锁 ● StampedLock知道吗&#xff1f;&#xff08;邮戳锁/票据锁&#xff09; ● ReentrantReadWriteLock有锁降级机制&#xff0c;你知道…

页面置换算法的模拟与比较

前言 在计算机操作系统中&#xff0c;页面置换算法是虚拟存储管理中的重要环节。通过对页面置换算法的模拟实验&#xff0c;我们可以更深入地理解虚拟存储技术&#xff0c;并比较不同算法在请求页式虚拟存储管理中的优劣。 随着计算机系统和应用程序的日益复杂&#xff0c;内存…

更有效的协同程序【插件:More Effective Coroutines】

插件地址&#xff1a;传送门 1、命名空间 using System.Collections.Generic; using MEC; 2、与传统的协程相比 传统&#xff1a;StartCoroutine(_CheckForWin()); 被RunCoroutine取代。必须选择执行循环进程&#xff0c;默认为“Segment.Update”。 using System.Coll…

从由器入手改善网络的安全性需要启用javascri

摘要&#xff1a;由器往往有不同的角色。 由器往往有不同的角色。例如&#xff0c;一般情况下&#xff0c;一个以太网端口连接到外部网络&#xff0c;四个端口提供到达局域网设备的互联网连接&#xff0c;无线发射装置向无线客户端提供访问。无线接口甚至可能提供多种SSID。 由…

互联网摸鱼日报(2023-06-13)

互联网摸鱼日报(2023-06-13) InfoQ 热门话题 数字化转型背景下&#xff1a;关于企业数据分析的趋势与预判 字节跳动全域数据治理平台负责人王慧祥确认出席 ArchSummit 深圳 2023开放原子全球开源峰会在北京成功举办 解决制造业效率、质量和成本的取舍问题&#xff0c;技术可…

windows7安装打印机提示“本地打印后台处理程序服务没有运行”

在win7系统中安装打印机经常会碰到以下问题。 解决方法&#xff1a; 1).在“运行”输入“services.msc”&#xff0c;弹出以下画面。 2).找到“Print Spooler ”双击&#xff0c;弹出下面画面&#xff0c;“启动类型”选择“自动”&#xff0c;点击“服务状态”的“启动”&…

无法启动计算机打印机服务程序,Windows10下使用打印机时提示打印后台处理程序服务没有运行怎么办...

在windows 10系统中&#xff0c;准备打印文件&#xff0c;在使用打印机的时候弹出了 windows 无法连接到打印机。 本地打印后台处理程序服务没有运行。请重新启动打印机后台处理程序或重新启动计算机。的提示&#xff0c;该怎么办呢&#xff1f; 出现这样的提示是由于windows 1…

计算机打印后台处理程序在哪里,Win7系统连接打印机出现本地打印后台处理程序服务没有运行怎么办...

最近有 解决方法&#xff1a; 1、打开 c:\windows\system32\spool\PRINTERS文件夹&#xff0c;点击右键-属性&#xff0c;取消只读属性、并删除PRINTERS文件夹中的所有文件(一般没有); 2、修改注册表 运行-regedit打开注册表 删除HKEY_LOCAL_MACHINE\SYSTEM\ControlSet001\Cont…