1、什么是AOP
切面编程(Aspect-Oriented Programming,AOP)是一种软件开发方法,旨在通过分离关注点(Concerns)来增强代码的模块性、可维护性和可重用性。
AOP 是 OOP 的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内容,是函数式编程的一种衍生范型。利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高程序的可重用性,同时提高了开发的效率。
切面编程通过将交叉关注点(Cross-Cutting Concerns)从主要业务逻辑中抽离出来,从而使代码更易于管理和理解。
作为一个Java工程师,你可能会经常遇到需要处理跨越多个模块和类的问题,例如日志记录、事务管理、安全认证等。这些问题通常与主要的业务逻辑分离,但它们又贯穿在整个应用中。这就是切面编程的用武之地。
以下是切面编程的关键概念和组成部分:
- 切面(Aspect): 切面是关注点的模块化,它包含一组跨越多个类的行为。例如,一个日志记录切面可以定义在每个方法执行前后记录日志。切面本身并不包含完整的业务逻辑,而是定义了如何在关注点处执行逻辑。
- 连接点(Join Point): 连接点是在应用执行过程中可以插入切面的点,通常是方法调用、对象初始化等。切面可以在连接点前、后、环绕等不同的时机执行。
- 通知(Advice): 通知是切面在连接点处执行的具体行为,可以是方法调用、异常处理、环绕逻辑等。主要的通知类型有前置通知(Before)、后置通知(After)、环绕通知(Around)、返回通知(AfterReturning)和异常通知(AfterThrowing)。
- 切点(Pointcut): 切点定义了哪些连接点会触发切面的通知。它通过表达式或规则来匹配一组连接点。
- 织入(Weaving): 织入是将切面应用到目标代码的过程,即将切面的通知插入到连接点处。织入可以在编译时、类加载时、运行时等不同阶段进行。
- 引入(Introduction): 引入是一种动态为类添加新方法或属性的机制,允许切面向现有类添加新功能。这可以用来实现类似于多继承的效果。
切面编程的优势在于它提供了一种清晰的方式来处理横切关注点,将其与主要业务逻辑分离,从而增强代码的可维护性和可扩展性。在 Java 中,切面编程的实现主要通过 Spring AOP 框架来实现,它允许你通过注解或 XML 配置来创建和管理切面。使用切面编程,你可以更加专注于核心业务逻辑,而无需过多关心交叉关注点的处理。
2、作用及其优势
AOP(Aspect-Oriented Programming,面向切面编程)的作用是通过分离交叉关注点(Cross-Cutting Concerns)来提高代码的模块性、可维护性和可重用性。它允许开发人员将横切关注点(如日志、事务、安全性等)从主要业务逻辑中分离出来,使代码更加清晰、可维护和可扩展。
AOP 的优势体现在以下几个方面:
- 分离关注点: AOP 通过将横切关注点与主要业务逻辑分离,使代码更加模块化。这使得开发人员可以专注于核心业务逻辑,而不必在各处重复编写相同的交叉关注点代码。
- 提高可维护性: 通过将交叉关注点集中处理,AOP 使代码更加易于维护。如果需要修改或扩展横切关注点的逻辑,只需在一个地方进行修改,而不需要在多个地方进行修改。
- 降低代码冗余: 由于交叉关注点被分离到切面中,不同的模块可以共享相同的切面,从而避免了代码冗余。例如,多个模块都需要记录日志,可以共享一个日志切面。
- 提高可重用性: 切面可以在不同的模块中重用,从而提高了代码的可重用性。当需要在多个项目中使用相同的交叉关注点逻辑时,只需将切面应用到不同的项目中。
- 增强代码灵活性: AOP 允许在不修改原始代码的情况下,动态地将切面织入到目标代码中。这使得你可以在运行时根据需求添加或移除切面,而不必修改主要业务逻辑。
- 提高代码可读性: 将交叉关注点从主要业务逻辑中抽离,可以使主要业务逻辑更加清晰、简洁,从而提高了代码的可读性和理解性。
- 促进团队协作: 通过统一处理交叉关注点,团队成员可以共享相同的设计和实现模式,从而促进团队协作和开发效率。
总而言之,作用及其优势为:
3、AOP底层实现
AOP 的底层实现通常基于动态代理和字节码操作,以在运行时将切面逻辑织入到目标代码中。
在 Java 中,主要有两种常见的 AOP 底层实现方式:基于 JDK 动态代理和基于 CGLIB 字节码操作。
下面分别介绍这两种实现方式的基本原理:
- 基于 JDK 动态代理: JDK 动态代理利用 Java 标准库中的 java.lang.reflect.Proxy 类和 java.lang.reflect.InvocationHandler 接口来实现。当一个接口被代理时,JDK 动态代理会在运行时创建一个实现了该接口的代理类,并且每个代理方法都会被重定向到实现 InvocationHandler 接口的对象的调用处理方法。代理方法的调用将由调用处理方法处理,从而允许在调用目标方法之前、之后或环绕时执行切面逻辑。
- 基于 CGLIB 字节码操作: CGLIB(Code Generation Library)是一个功能强大的字节码操作库,它能够生成子类并覆盖父类的方法。在基于 CGLIB 的 AOP 实现中,目标类的子类将在运行时动态生成,重写目标方法以织入切面逻辑。这种方式适用于没有实现接口的类。CGLIB 的底层机制涉及了对目标类字节码的修改,从而使得切面逻辑能够在目标类的方法中生效。
AOP 的底层实现大致流程如下:
- 定义切面: 定义切面的通知方法,如前置通知、后置通知、环绕通知等。
- 创建代理: 在运行时,AOP 框架会根据切面定义和目标对象,生成代理对象。
- 织入切面逻辑: 将切面逻辑织入到目标对象的方法中,使得切面逻辑能够在目标方法调用的前后或环绕时执行。
- 执行代理方法: 当调用代理对象的方法时,实际上是调用了代理对象中的通知方法。在通知方法内部,会执行切面逻辑,并最终调用目标方法。
需要注意的是,虽然 AOP 通过代理技术实现了切面逻辑的织入,但这种织入是在运行时动态完成的,因此可能会带来一些性能开销。选择使用 JDK 动态代理还是 CGLIB 字节码操作取决于目标类是否实现接口等因素。
总之,AOP 的底层实现主要基于动态代理和字节码操作,通过在运行时将切面逻辑织入到目标代码中,实现了交叉关注点的分离和处理。
4、JDK 的动态代理
目录如下:
目标接口:
目标对象:
增强方法:
代理工具:
package com.xzl.proxy.jdk; import java.lang.reflect.InvocationHandler; import java.lang.reflect.Method; import java.lang.reflect.Proxy; /** * @author 逐梦苍穹 * @date 2023/8/13 10:28 */ public class ProxyUtil { public static Advice advice = new Advice(); public static <T> T getProxy(T object) { return (T) Proxy.newProxyInstance(object.getClass().getClassLoader(), object.getClass().getInterfaces(), new InvocationHandler() { @Override public Object invoke(Object o, Method method, Object[] objects) throws Throwable { advice.before(); Object result = method.invoke(object, objects); advice.afterReturning(); return result; } }); } }
测试方法:
测试结果:
5、cglib 的动态代理
CGLIB(Code Generation Library)是一个功能强大的字节码操作库,用于在运行时生成和修改 Java 类的字节码。CGLIB 动态代理是一种基于字节码操作的动态代理机制,与 JDK 动态代理不同,它可以代理类而不仅仅是接口,适用于那些没有实现接口的类。
CGLIB 被广泛应用于许多 Java 框架和库,其中包括 Spring AOP。
以下是 CGLIB 动态代理的基本原理和使用方式:
- 生成子类: CGLIB 动态代理通过生成目标类的子类来实现代理。这个子类会重写目标类的方法,并在方法调用前后添加切面逻辑。这种方式不需要目标类实现接口。
- 织入切面逻辑: CGLIB 动态代理在生成的子类中织入切面逻辑,使得切面逻辑能够在目标类的方法中生效。织入可以在方法调用前、后或环绕时进行。
- 创建代理对象: 使用 CGLIB 提供的 Enhancer 类创建代理对象。Enhancer 是一个工具类,用于生成代理类的实例。
目录如下:
增强方法:
目标对象:
代理工具:
package com.xzl.proxy.cglib; import org.springframework.cglib.proxy.Enhancer; import org.springframework.cglib.proxy.MethodInterceptor; import org.springframework.cglib.proxy.MethodProxy; import java.lang.reflect.Method; /** * @author 逐梦苍穹 * @date 2023/8/13 10:28 */ public class ProxyUtil<T> { //创建增强方法 public static Advice advice = new Advice(); //创建目标对象 public final T target; //初始化目标对象 public ProxyUtil(T target){ this.target = target; } public T getProxy(T t){ Enhancer enhancer = new Enhancer();//创建增强器 enhancer.setSuperclass(t.getClass());//设置父类 //设置回调 enhancer.setCallback(new MethodInterceptor() { @Override public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable { advice.before(); Object invoke = method.invoke(target,objects); advice.afterReturning(); return invoke; } }); //创建代理对象并返回 return (T) enhancer.create(); } }
测试:
结果:
6、总结
两种代码实现方式的区别有:
区别解释:
- 代理库不同: 第一段代码使用了 JDK 动态代理,通过 Proxy.newProxyInstance 创建代理对象。第二段代码使用了 CGLIB 动态代理,通过 Enhancer 创建代理对象。
- 适用范围不同: JDK 动态代理只能代理实现了接口的类,而 CGLIB 动态代理可以代理任何类,包括没有实现接口的类。
- 代理对象创建方式不同: 在 JDK 动态代理中,代理对象是通过接口数组来创建的。而在 CGLIB 动态代理中,代理对象是通过设置父类来创建的。
- 方法拦截方式不同: JDK 动态代理使用 InvocationHandler 来实现方法拦截。CGLIB 动态代理使用 MethodInterceptor 来实现方法拦截。
- 性能差异: 通常情况下,JDK 动态代理比 CGLIB 动态代理要快一些。CGLIB 动态代理涉及到生成目标类的子类,并使用了更多的字节码操作,可能引入一些性能开销。
根据你的需求和场景,可以选择适合的动态代理方式。如果目标类实现了接口,可以考虑使用 JDK 动态代理;如果目标类没有实现接口,可以考虑使用 CGLIB 动态代理。