一、AOP简介
1.1 什么是AOP ?
AOP(Aspect-Oriented Programming)是一种软件开发技术,旨在通过将横切关注(cross-cutting concerns)从主要业务逻辑中分离出来,提供更好的模块化和可维护性。AOP通过在程序执行过程中动态地将这些关注点织入到代码中,从而实现了代码的解耦和重用。
1.2 什么是面向切面编程 ?
面向切面编程(Aspect-Oriented Programming)是AOP的一种具体实现方式。它通过将横切关注点(cross-cutting concerns)从主要业务逻辑中分离出来,以切面(Aspect)的形式进行模块化。切面定义了在何处和何时应该应用横切关注点。切面通常由切点(Pointcut)和通(Advice)组成。
1.3 AOP 的特点
- 横切关注点的模块化:AOP允许将与业务逻辑无关的横切关注点(如日志记录、事务管理、安全性等)从主要业务逻辑中分离出来,以切面的形式进行统一管理。这样可以提高代码的可维护性,使开发人员能够更好地关注核心业务逻辑。
- 解耦和重用:通过将横切关注点从主要业务逻辑中分离出来,AOP实现了代码的解耦。这意味着可以更容易地修改、扩展和重用横切关注点,而无需修改主要业务逻辑。这提高了代码的可重用性和可维护性。
- 声明式编程:AOP允许开发人员通过声明式的方式将切面应用到目标对象中,而无需在目标对象的代码中显式地编写切面逻辑。这使得代码更加清晰、简洁,并且易于理解和维护。
- 动态织入:AOP允许在程序运行时动态地将切面织入到目标对象中。这意味着可以根据需要选择性地应用切面,而无需在编译时或加载时进行硬编码。这提供了更大的灵活性和可配置性。
- 提高系统性能:AOP可以将一些通用的横切关注点(如性能监控、缓存管理等)应用到多个目标对象中,从而提高系统的性能和效率。这避免了在每个目标对象中重复编写相同的代码。
总的来说,AOP的特点包括横切关注点的模块化、解耦和重用、声明式编程、动态织入以及提高系统性能。这些特点使得AOP成为一种强大的技术,可以提高代码的可维护性、可重用性和可测试性,同时降低代码的复杂性和重复性。
二、 AOP的基本概念解读
2.1 AOP的基本概念
- 切面(Aspect):切面是一个模块化的单元,它封装了与横切关注点相关的行为。切面可以包含通知和切点。
- 连接点(Join Point):连接点是在应用程序执行过程中可以插入切面的点。它可以是方法调用、方法执行、异常抛出等。(也将Pointcut理解成一个条件 ,此条件决定了容器在什么情况下将通知和目标组合成代理返回给外部程序)
- 切点(Pointcut):切点定义了在哪些连接点上应用切面。它使用表达式来匹配连接点,例如指定特定的类、方法、注解等。
- 通知(Advice):通知是切面在特定连接点上执行的动作。常见的通知类型包括前置通知(Before)、后置通知(After)、返回通知(After Returning)和异常通知(After Throwing)。
- 引入(Introduction):引入允许在现有类中添加新的方法和属性。它允许将新功能引入到现有的类中,而无需修改类的源代码。
- 织入(Weaving):织入是将切面应用到目标对象中并创建新的代理对象的过程。织入可以在编译时、类加载时或运行时进行。
- AOP代理(Proxy):AOP框架创建的对象,代理就是对目标对象的加强。Spring中的AOP代理可以是JDK动态代理,也可以是CGLIB代理,前者基于接口,后者基于子类。(代理=目标+通知) 注:只有代理对象才有AOP功能,而AOP的代码是写在通知的方法里面的
- 目标对象(Target): 包含连接点的对象,被通知(被代理)的对象,完成具体的业务逻辑 。
2.2 AOP 概念趣事解读
AOP最多的就是概念,面对这么概念我们大多数人是记不住的。特别是像博主这样的越看感觉都要长脑子了,所以我准备一个小故事来解读AOP各功能(术语)的解读。
《 这是一个好故事 》
小猪侠是一个勇敢而机智的超级英雄,他的使命是保护城市的和平与安全。他有一个特殊的能力,可以通过棒棒糖来赋予他人额外的力量和技能。这个棒棒糖就是它的 " 切面(Aspect)"。
小猪侠的切面是一个模块化的单元,它封装了与超级英雄相关的行为。他的切面包含了一些 通知(Advice),这些通知定义了在特定的情境(连接点)下给其他超级英雄赋予的额外力量和技能。连接点(Join Point)可以是超级英雄的战斗、救援行动、危机处理等。
为了将它的切面应用到超级英雄身上,小猪侠需要创建一个 代理(Proxy)。代理是一个中间人,它将超级英雄包装起来,并在必要时调用切面中定义的通知。代理使得超级英雄在执行任务时能够自动获得额外的力量和技能。
超级英雄们在执行任务时,并不知道小猪侠的切面和代理的存在。他们只需要按照正常的方式进行战斗和救援,而不需要关注切面的实现细节。这就是 目标对象(Target)的作用。目标对象是超级英雄们正常的行动,而切面和代理则为他们提供了额外的力量和技能。
一天,小猪侠遇到了一个危机,城市中的一座大桥即将坍塌。他需要帮助其他超级英雄一起救援。小猪侠使用他的切面技能,创建了一个前置通知,它会在超级英雄们展开救援行动之前被触发。
小猪侠将他的切面应用到超级英雄们身上,通过代理来调用他们的救援行动。当超级英雄们准备展开救援行动时,切面中的前置通知被触发,小猪侠使用棒棒糖赋予他们额外的力量和技能,使得他们能够成功救援并修复大桥。
在这个故事中,小猪侠的切面代表了它的棒棒糖,连接点是超级英雄们展开救援行动的位置,代理是将切面应用到超级英雄们身上的中间人,通知是切面中定义的赋予额外力量和技能的动作,目标对象是超级英雄们正常的行动,织入是将切面应用到目标对象中的过程。
三、代码情景演示
3.1 编写目标对象(超级英雄们正常的行动)
1. 为了降低代码耦合性,首先编写一个动作行为的接口定义发动技能和飞行的方法。
package com.ycxw.aop.biz; /** * @author 云村小威 * @site blog.csdn.net/Justw320 * @create 2023-08-17 16:08 */ public interface Behavior { // 飞行 public boolean fly(String name); // 发动技能 public void skill(String name, String skills); }
2. 编写实现接口的类
package com.ycxw.aop.biz.impl; import com.ycxw.aop.biz.Behavior; /** * @author 云村小威 * @site blog.csdn.net/Justw320 * @create 2023-08-17 16:15 */ public class BehaviorImpl implements Behavior { public BehaviorImpl() { super(); } @Override public boolean fly(String name) { System.out.println("超级英雄:"+name); return true; } @Override public void skill(String name, String skills) { System.out.println("超级英雄:"+name+" 发动了"+skills); } }
3.2 编写通知类
通知:是切面中定义的赋予额外力量和技能的动作
3.2.1 前置通知
在目标对象使用前执行:
package com.ycxw.aop.advice; import org.springframework.aop.MethodBeforeAdvice; import java.lang.reflect.Method; import java.util.Arrays; /** * 前置通知 * @author 云村小威 * @site blog.csdn.net/Justw320 * @create 2023-08-17 16:20 */ public class MyMethodBeforeAdvice implements MethodBeforeAdvice { @Override public void before(Method arg0, Object[] arg1, Object arg2) throws Throwable { //在这里,可以获取到目标类的全路径及方法及方法参数,然后就可以将他们写到日志表里去 String target = arg2.getClass().getName(); String methodName = arg0.getName(); String args = Arrays.toString(arg1); System.out.println("【前置通知:系统日志】:"+target+"."+methodName+"("+args+") 被调用了"); } }
3.2.2 后置通知
在目标对象使用完后执行:
package com.ycxw.aop.advice; import org.springframework.aop.AfterReturningAdvice; import java.lang.reflect.Method; import java.util.Arrays; /** * 后置通知 * @author 云村小威 * @site blog.csdn.net/Justw320 * @create 2023-08-17 16:48 */ public class MyAfterReturningAdvice implements AfterReturningAdvice { @Override public void afterReturning(Object arg0, Method arg1, Object[] arg2, Object arg3) throws Throwable { String target = arg3.getClass().getName(); String methodName = arg1.getName(); String args = Arrays.toString(arg2); System.out.println("【后置通知:】:"+target+"."+methodName+"("+args+") 被调用了,"+"该方法被调用后的返回值为:"+arg0); } }
3.2.3 异常通知
当运行发生异常执行该通知:
package com.ycxw.aop.advice; import com.ycxw.aop.exception.PriceException; import org.springframework.aop.ThrowsAdvice; /** * 异常通知 * @author 云村小威 * @site blog.csdn.net/Justw320 * @create 2023-08-17 17:00 */ public class MyThrowsAdvice implements ThrowsAdvice { public void afterThrowing(PriceException ex) { System.out.println("【异常通知】:当技能施展发生异常,那么执行此处代码块!!!"); } }
3.2.4 环绕通知
这个在平常是用的最多的,比较便捷,相当于结合前置和后置的通知功能。
package com.ycxw.aop.advice; import org.aopalliance.intercept.MethodInterceptor; import org.aopalliance.intercept.MethodInvocation; import java.util.Arrays; /** * @author 云村小威 * @site blog.csdn.net/Justw320 * @create 2023-08-17 16:54 */ public class MyMethodInterceptor implements MethodInterceptor { @Override public Object invoke(MethodInvocation arg0) throws Throwable { String target = arg0.getThis().getClass().getName(); String methodName = arg0.getMethod().getName(); String args = Arrays.toString(arg0.getArguments()); System.out.println("【环绕通知调用前:】:"+target+"."+methodName+"("+args+")被调用了"); // arg0.proceed()就是目标对象的方法 Object proceed = arg0.proceed(); System.out.println("【环绕通知调用后:】:该方法被调用后的返回值为:"+proceed); return proceed; } }
3.3 spring核心xml文件配置
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!-- 目标对象 --> <bean class="com.ycxw.aop.biz.impl.BehaviorImpl" id="behavior"></bean> <!-- 通知 --> <bean class="com.ycxw.aop.advice.MyMethodBeforeAdvice" id="beforeAdvice"></bean> <bean class="com.ycxw.aop.advice.MyAfterReturningAdvice" id="afterAdvice"></bean> <bean class="com.ycxw.aop.advice.MyMethodInterceptor" id="interceptor"></bean> <bean class="com.ycxw.aop.advice.MyThrowsAdvice" id="throwsAdvice"></bean> <!-- 代理 ProxyFactoryBean类似于工厂模式(直接配置)--> <bean class="org.springframework.aop.framework.ProxyFactoryBean" id="behaviorProxy"> <!-- 配置目标对象 --> <property name="target" ref="behavior"></property> <!--配置代理目标对象的接口 cglid动态代理--> <property name="proxyInterfaces"> <list> <value>com.ycxw.aop.biz.Behavior</value> </list> </property> <!--配置通知--> <property name="interceptorNames"> <list> <value>beforeAdvice</value> <value>afterAdvice</value> <value>interceptor</value> <value>throwsAdvice</value> </list> </property> </bean> </beans>
3.4 测试运行
1. 编写测试类
package com.ycxw.aop.demo; import com.ycxw.aop.biz.Behavior; import org.springframework.context.support.ClassPathXmlApplicationContext; /** * @author 云村小威 * @site blog.csdn.net/Justw320 * @create 2023-08-17 16:08 */ public class demo1 { public static void main(String[] args) { ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext("spring-context.xml"); Behavior bean = (Behavior) context.getBean("behaviorProxy"); bean.fly("小威"); bean.skill("小威","变大技能"); } }
2. 运行结果
由此可见,应证了前面的通知。可看到环绕通知跟前置和、后置通知处于等效功能,而环绕通知更为简便。
3.5 配置过滤通知后测试
这里过滤了后置通知:
<?xml version="1.0" encoding="UTF-8"?> <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd"> <!--AOP--> <!-- 目标对象 --> <bean class="com.ycxw.aop.biz.impl.BehaviorImpl" id="behavior"></bean> <!-- 通知 --> <bean class="com.ycxw.aop.advice.MyMethodBeforeAdvice" id="beforeAdvice"></bean> <bean class="com.ycxw.aop.advice.MyAfterReturningAdvice" id="afterAdvice"></bean> <bean class="com.ycxw.aop.advice.MyMethodInterceptor" id="interceptor"></bean> <bean class="com.ycxw.aop.advice.MyThrowsAdvice" id="throwsAdvice"></bean> <!--过滤--> <bean class="org.springframework.aop.support.RegexpMethodPointcutAdvisor" id="regexpAdvisor"> <!--配置需要过滤的通知--> <property name="advice" ref="afterAdvice"></property> <!--通过正则过滤指定方法--> <property name="pattern" value=".*skill"></property> </bean> <!-- 代理 ProxyFactoryBean类似于工厂模式(直接配置)--> <bean class="org.springframework.aop.framework.ProxyFactoryBean" id="behaviorProxy"> <!-- 配置目标对象 --> <property name="target" ref="behavior"></property> <!--配置代理目标对象的接口 cglid动态代理--> <property name="proxyInterfaces"> <list> <value>com.ycxw.aop.biz.Behavior</value> </list> </property> <!--配置通知--> <property name="interceptorNames"> <list> <value>beforeAdvice</value> <value>regexpAdvisor</value> <value>throwsAdvice</value> </list> </property> </bean> </beans>
由运行结果可见,等代码执行完后才执行后置通知,在前面的fly方法后才过滤的后置通知。
总结
AOP是面向切面编程,它通过在程序执行过程中动态地将横切关注点织入到代码中来实现。当程序执行到目标对象的目标方法时,AOP可以在方法调用前、后或异常抛出时执行相应的通知。
具体来说,如果连接点上有前置通知,AOP会先执行前置通知,然后再执行目标方法。前置通知可以在目标方法执行之前执行一些预处理操作,如日志记录、参数验证等。
如果没有前置通知,AOP会直接执行目标方法。然后,AOP会检查目标方法上是否有后置通知。如果有后置通知,AOP会在目标方法执行后执行后置通知。后置通知可以在目标方法执行之后执行一些后处理操作,如结果处理、资源释放等。
除了前置和后置通知,AOP还支持异常通知和环绕通知。异常通知可以在目标方法抛出异常时执行一些处理逻辑。环绕通知是最强大的通知类型,它可以完全控制目标方法的执行过程,包括在方法调用前后执行自定义的逻辑。
需要注意的是,虽然通知代码通常是非业务核心代码,如日志记录和事务管理,但并不是所有的通知都是非业务核心代码。有些通知可能涉及到业务逻辑,例如在目标方法执行前进行权限检查。AOP的灵活性和可配置性使得开发人员可以根据具体需求来定义和应用切面,以实现代码的解耦和重用,提高代码的可维护性和可测试性。