目录
介绍
AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,可以在不改变原有代码的情况下,通过在程序的各个关键点上增加切面(Aspect)的方式,实现对代码的增强和横切关注点的分离,从而提高代码的可重用性、可维护性和可扩展性。
AOP的核心思想是将程序中的不同关注点进行解耦,避免不同的关注点相互嵌入,导致代码冗长、难以维护的问题。通过将不同的关注点抽象成切面,实现了程序的层次性,使得不同层次中的各种关注点可以独立地进行开发、管理和维护。
在实现AOP时,需要定义切点(Pointcut)和切面(Aspect)两个概念。切点用于定义程序中需要增强的关键点,例如方法的调用、异常抛出、对象的初始化等等。切面则是对切点进行增强的具体实现,例如日志记录、性能监测、事务管理等等。AOP框架通过在程序中动态生成代理对象的方式,将切面织入到切点上,从而实现对程序的增强。
在Java中,常用的AOP框架包括Spring AOP和AspectJ。Spring AOP是基于代理的AOP框架,可以通过配置文件或注解的方式实现切面的定义;而AspectJ是基于注解的AOP框架,可以直接在Java代码中使用注解的方式定义切点和切面。
动态代理
动态代理是一种用于实现面向切面编程的技术。它允许您编写一些代码来控制某个类或接口的行为。它可以在不更改原始代码的情况下改变或增强类的行为。当类的对象被创建时,动态代理会在内存中创建一个新的类和一个代理类,以控制原始类的行为。
下面看一个代码示例:
测试
可以看到这里两个方法除了各自方法的核心业务逻辑外,方法执行前和执行后是重复的非核心业务代码,这里方法只有两个,如果方法成百上千时就需要改很多的地方,从而可以使用动态代理进行优化处理
jdk动态代理
JDK动态代理是通过Java自带的反射机制实现的,主要涉及两个类:java.lang.reflect.Proxy
和java.lang.reflect.InvocationHandler
。
Proxy
类是JDK提供的动态代理类,它提供了用于创建动态代理对象的静态方法newProxyInstance()
。这个方法有三个参数:
ClassLoader
对象,用于指定动态代理类的ClassLoader
,一般使用被代理对象的ClassLoader
;Class<?>[]
对象数组,用于指定被代理类实现的接口;InvocationHandler
对象,用于指定动态代理对象的方法调用处理器。
InvocationHandler
接口是一个函数式接口,它只有一个invoke()
方法,用于处理动态代理类的方法调用。invoke()
方法有三个参数:
Object
对象,表示被代理对象;Method
对象,表示方法对象;Object[]
数组,表示方法参数。
当动态代理类的方法被调用时,JVM会自动调用代理对象的invoke()
方法,将方法名、方法参数等作为参数传入该方法。在invoke()
方法中,我们可以通过反射机制执行被代理类中的相应方法,并对方法的返回值进行处理和返回。
总体来说,JDK动态代理是在运行时动态生成一个类,并在该类中实现代理接口中的方法。当该类的方法被调用时,JVM会调用invoke()
方法,从而实现代理方法的处理和返回。
执行测试:
这样的话后续再新增新的业务方法时只需要进行接口方法的核心业务的书写,而不需要再关注非核心业务代码的处理,非核心业务的代码统一在代理对象中处理即可
cglib动态代理
CGLib 动态代理的实现原理主要是利用 ASM 字节码操作库和 Java 反射机制,在程序运行时动态地生成一个新类。这个新类继承自被代理的类,并覆盖掉所有非 final 方法,然后在这些方法中插入额外的操作(如日志记录、权限验证、事务控制等),达到动态代理的目的。 CGLib 通过子类化的方式来实现代理,所以它只能代理出接口的实现类,而不能直接代理接口。CGLib 使用 ASM 框架直接读取 Class 文件,并对其进行转换,从而生成所需的子类,生成的代理对象可以直接访问到被代理类的所有属性和方法。而且由于它是子类化的,所以即使是没有接口的类也可以代理。因此,CGLib 动态代理的应用范围更加广泛。 由于 CGLib 实现的是通过字节码技术产生的子类来实现代理行为,所以代理速度更快,但也存在一定的局限性,例如被代理类不能是 final 类型的、代理的目标方法不能是 final 的等。
下面用cglib来实现前面的示例
执行测试:
注解实现Aop
添加必须依赖
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
添加Atm类 (主业务逻辑代码块)
定义打印log方法(提取公共代码逻辑块)
启用代理
测试:
切点表达式
Aop通知类型
前置通知(@Before)
在连接点方法执行之前的增强处理
前面演示的demo就是使用的前置通知@before,这里不再演示
后置通知(@After)
在连接点方法执行之后的增强处理,无论正常结束还是异常结束,都会执行的处理
调整atm方法
添加后置执行方法
执行测试:
手动调整异常再进行测试
正常结束通知(@AfterReturning)
在连接点方法正常结束后会进行的处理 如果方法有返回值,可以拿到方法的返回值
添加正常结束通知
刚才前面的例子中我们传入参数100时是手动定义的一个异常,那么这里添加一个正常结束通知不会执行才对,测试看下
调整参数,不抛出异常观察正常通知是否执行
获取正常通知参数
可以看到这里的方法是有返回值,可以通过正常通知来获取该返回值,从而做进一步的业务处理
再执行测试
异常结束通知(@AfterThrowing)
在连接点方法异常结束后会进行的处理 可以获取异常的信息
添加异常结束通知方法
此时先执行一个不会抛异常的方法观察异常结束通知会不会执行
可以看到正常结束通知执行了,异常结束通知没有执行
调整参数为100的异常执行,查看异常通知方法的执行情况
可以查看到异常结束通知执行,正常结束通知没有执行
获取异常信息
执行异常测试
注意此处的异常信息只是捕获到了,没有做任何的处理
环绕通知
通过代码调用方法,在方法的执行周围进行增强
先注释掉之前的通知方法
添加环绕通知
先进行异常测试
进行正常结束测试
注意:
当有前置后置以及环绕通知时,先进行环绕通知,在方法的具体执行前后进行增强
放开所有通知方法进行测试
注意观察通知执行的顺序
切面的优先级
当我们的程序中定义了多个切面时,可以通过@Order(数字)来控制各个切面的执行顺序,其中数字越小,执行优先级越高
话不多说,直接上代码
在之前原先的基础上再定义一个切面类
日志信息也稍作调整以做区分,Order设为1,
原先的log切面Order设置为2
启动测试查看执行优先级
可以观察到优先级顺序
Aop使用注意点
方法权限不能是private
连接点方法不能是private,会导致Aop不能进行增强
前面举例时的take是public的aop可以进行正常增强,那么如果调整为private,再进行测试看看:
测试
Aop增强失效
如果是protected呢
protected也可以进行aop增强
其他方法在内部被调用时不会被增强
直接上代码,这里直接调用两个方法来进行增强
此时两个方法都被增强
如果在取钱方法中调用了存钱方法,观察此时的增强通知会执行几次
测试
可以看到增强通知只执行了一次,且只执行了取钱方法的增强通知
注解实现Aop
基于注解实现服务层打印入参和返回参数日志
在业务开发中我们有时需要通过日志打印入参参数和方法返回,但是基本传统写法都是在方法中自定义log日志,这样写其实并不太优雅,可以通过aop进行优化日志打印
未使用Aop效果
新建两个实体模拟存储保存订单Do和更新订单Do
创建服务层模拟业务
调用接口查看打印日志
可以看到只是执行了服务层的主业务核心代码System.out.println模拟的业务代码,如果还想打印传参和返回值信息就需要使用日志打印log.info("日志信息")等
可以使用Aop进行业务优化
使用注解实现Aop入参返回值等日志打印
新建切面注解
新建统一转换参数
新建saveOrder和updateOrder转换Operate
定义主要切面类
import cn.hutool.json.JSONUtil;
import com.example.demo23.demos.web.annotation.MyLogOperate;
import com.example.demo23.demos.web.service.Convert;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.util.concurrent.LinkedBlockingDeque;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
@Component
@Aspect
public class LogAsept {
// 定义切入点
@Pointcut("@annotation(com.example.demo23.demos.web.annotation.MyLogOperate)")
public void pointcut(){}
private ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(1,1,1,
TimeUnit.SECONDS,new LinkedBlockingDeque<>());
//环绕通知
@Around("pointcut()")
public Object around(ProceedingJoinPoint proceedingJoinPoint) throws Throwable{
Object obj = proceedingJoinPoint.proceed();
threadPoolExecutor.execute(()->{
try{
MethodSignature methodSignature = (MethodSignature) proceedingJoinPoint.getSignature();
MyLogOperate myLogOperate = methodSignature.getMethod().getAnnotation(MyLogOperate.class);
Class<? extends Convert> convert = (Class<? extends Convert>) myLogOperate.convert();
Convert logConvert = convert.newInstance();
OperateLogDo operateLogDo = logConvert.convert(proceedingJoinPoint.getArgs()[0]);
operateLogDo.setDesc(myLogOperate.desc())
.setResult(obj.toString());
System.out.println("插入 operateLog" + JSONUtil.parseObj(operateLogDo));
}catch (InstantiationException ex) {
throw new RuntimeException(ex);
} catch (IllegalAccessException ex) {
throw new RuntimeException(ex);
}
});
return obj;
}
}
服务层添加注解使用aop进行日志切入
启动测试:
切入成功,打印入参和方法返回值成功
还可以再详细一些打印出接口的调用时间,方法,路径等
示例源码
需要源码的伙伴在下面自取即可
链接:aop示例源码
提取码:z2wp