什么是
AOP,如何理解面向切面编程
AOP,
Aspect Oriented Programming
,即面向切面编程 。
通过预编译方法和运行期间动态代理的方式实现,在不修改源代码的方式下,给程序动态统一添加额外功能的一种技术。
AOP是对面向对象编程OOP的一个补充。
它的目的是将复杂的需求分解为不同的切面,将散布在系统中的公共功能集中解决。
它的实际含义是在运行时将代码切入到类的指定方法、指定位置上,将不同方法的同一个位置抽象为一个切面对象,并对该对象进行编程。
AOP 的目的是为了解耦 其次是简化开发,AOP 是 Spring 的核心
面向切面编程
他是一套规范,通过预编译方式和运行期间动态代理实现程序的统一维护
核心概念 就是
将分散在各个业务逻辑代码中的相同的代码通过横向切割的方式抽取到一个独立的模块中
img
AOP的优点
降低模块之间的耦合度
使系统更容易扩展
更好的代码复用
非业务代码更加集中,不分散,便于统一管理
业务代码更加简洁纯粹,不掺杂其他的代码的影响
AOP中出现的一些概念
切面:横切关注点,从每个方法中抽取的非核心业务,被模块化的抽象对象,横切关注点的模块化,比如下标提到的日志组件。可以认为是通知、引入和切入点的组合;在Spring中可以使用Schema和@AspectJ方式进行组织实现;在AOP中表示为在哪干和干什么集合 ;
通知:切面对象完成的工作(非业务代码),在连接点上执行的行为,通知提供了在AOP中需要在切入点所选择的连接点处进行扩展现有行为的手段;包括前置通知(before
advice)、后置通知(after advice)、环绕通知(around
advice),在Spring中通过代理模式实现AOP,并通过拦截器模式以环绕连接点的拦截器链织入通知;在AOP中表示为干什么;
目标:被通知的对象(即被横切的对象),需要被织入横切关注点的对象,即该对象是切入点选择的对象,需要被通知的对象,从而也可称为被通知对象;由于Spring
AOP
通过代理模式实现,从而这个对象永远是被代理对象,在AOP中表示为对谁干 ;
代理:切面、通知、目标混合之后的对象。封装通知方法的类
AOP代理:AOP框架使用代理模式创建的对象,从而实现在连接点处插入通知(即应用切面),就是通过代理来对目标对象应用切面。在Spring中,AOP代理可以用JDK动态代理或CGLIB代理实现,而通过拦截器模型应用切面。在AOP中表示为怎么实现的一种典型方式 ;
连接点:通知要插入业务代码的具体位置,表示需要在程序中插入横切关注点的扩展点,连接点可能是类初始化、方法执行、方法调用、字段调用或处理异常等等 ,Spring只支持方法执行连接点,在AOP中表示为spring允许你使用通知的地方 ;(如Spring实现中的JoinPoint)
切入点:选择一组相关连接点的模式,即可以认为连接点的集合,Spring支持perl5正则表达式和AspectJ切入点模式,Spring默认使用AspectJ语法,在AOP中表示为在哪里干的集合 ;
织入:把切面连接到其它的应用程序类型或者对象上,并创建一个被通知的对象。这些可以在编译时(例如使用AspectJ编译器),类加载时和运行时完成。Spring和其他纯Java
AOP框架一样,在运行时完成织入。在AOP中表示为怎么实现的 ;
引入:也称为内部类型声明,为已有的类添加额外新的字段或方法,Spring允许引入新的接口(必须对应一个实现)到所有被代理对象(目标对象),
在AOP中表示为干什么(引入什么) ;
img
AOP 的核心是连接点
连接点是我们需要关注的程序拓展点
可能是类初始化 方法执行 方法调用 字段调用 异常处理等
Spring 支持的连接点是方法执行点
切入点是一系列连接点的集合,Spring默认使用AspectJ语法,在AOP中抽象表示为可以进行操作的集合
之后就是通知
通知就是我们在连接点上执行的行为
连接点 切入点 通知组合在一起 就是一个切面
把切面映入到其他应用程序或者对象上,创建一个被通知的对象,这些就是织入,Spring
在运行时完成织入 ,在 AOP 中表示为怎么实现的,实现方式
通知类型:
前置通知(Before advice):
在被代理的目标方法之前执行的通知,但这个通知不能阻止连接点之前的执行流程(除非它抛出一个异常)。
后置通知(After returning advice):
在被代理的目标方法后执行的通知:例如,一个方法没有抛出任何异常,正常返回。
异常通知(After throwing advice):
在被代理的目标方法抛出异常退出时执行的通知。
最终通知(After (finally) advice):
当被代理的目标方法最终说明的时候执行的通知(不论是正常返回还是异常退出)。
环绕通知(Around Advice):
包围一个连接点的通知,如方法调用。这是最强大的一种通知类型。环绕通知可以在方法调用前后完成自定义的行为。它也会选择是否继续执行连接点或直接返回它自己的返回值或抛出异常来结束执行。
环绕通知是最常用的通知类型。和AspectJ一样,Spring提供所有类型的通知,推荐你使用尽可能简单的通知类型来实现需要的功能。例如,如果你只是需要一个方法的返回值来更新缓存,最好使用后置通知而不是环绕通知,尽管环绕通知也能完成同样的事情。用最合适的通知类型可以使得编程模型变得简单,并且能够避免很多潜在的错误。比如,你不需要在JoinPoint上调用用于环绕通知的proceed()方法,就不会有调用的问题。
Spring AOP和 AspectJ
是什么关系
AspectJ 是一个更加强大的 AOP 框架 是一个 AOP 标准
如果只是简单的业务 可以使用 AOP
AOP 一个重要的原则就是无侵入性
AspectJ 重要的是 一般在编译期进行 即静态织入
在这个期间使用AspectJ的acj编译器(类似javac)把aspect类编译成class字节码后,在java目标类编译时织入,即先编译aspect类再编译目标类。
动态织入的方式是在运行时动态将要增强的代码织入到目标类中,这样往往是通过动态代理技术完成的,如Java
JDK的动态代理(Proxy,底层通过反射实现)或者CGLIB的动态代理(底层通过继承实现),Spring
AOP采用的就是基于运行时增强的代理技术。
Spring AOP更易用,AspectJ更强大
Java中的代理模式实现AOP
创建一个计算器接口,定义四个方法
1 2 3 4 5 6 7 8 package edu.software.ergoutree.spring6aop.example;public interface Calculator { public int add (int a, int b) ; public int subtract (int a, int b) ; public int multiply (int a, int b) ; public int divide (int a, int b) ; }
创建一个带日志输出的实现类
高耦合的写法,每次打印日志都要手动完成
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 package edu.software.ergoutree.spring6aop.example;public class CalculatirLogImpl implements Calculator { @Override public int add (int a, int b) { System.out.println("[日志] add 方法开始" ); int result = a + b; System.out.println("方法内部Result: " + result); System.out.println("[日志] add 方法结束" ); return result; } @Override public int subtract (int a, int b) { System.out.println("[日志] sub 方法开始" ); int result = a - b; System.out.println("方法内部Result: " + result); System.out.println("[日志] sub 方法结束" ); return result; } @Override public int multiply (int a, int b) { System.out.println("[日志] mul 方法开始" ); int result = a * b; System.out.println("方法内部Result: " + result); System.out.println("[日志] mul 方法结束" ); return result; } @Override public int divide (int a, int b) { System.out.println("[日志] div 方法开始" ); int result = a / b; System.out.println("方法内部Result: " + result); System.out.println("[日志] div 方法结束" ); return result; } }
使用静态代理进行优化
上方代码中,日志信息和业务逻辑的耦合性很高,不利于代码的维护。
关于静态代理和动态代理的部分,可以看我的另一篇文章,这里只做简单讲解
介绍:代理是一种设计模式,提供一个代理类,调用方法的时候使用代理类间接调用,让不属于目标方法核心方法中的代码解耦出来,减少对目标方法的调用和打扰,同时让功能更加集中统一更利于维护
静态代理是通过创建一个代理类来实现对目标对象的访问控制。下面是计算器例子改为静态代理的实现方式
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 package edu.software.ergoutree.spring6aop.example;public class CalculatorProxy implements Calculator { private final Calculator target; public CalculatorProxy (Calculator target) { this .target = target; } @Override public int add (int a, int b) { System.out.println("代理: 开始执行add方法" ); int result = target.add(a, b); System.out.println("代理: add方法执行完成,结果=" + result); return result; } @Override public int subtract (int a, int b) { System.out.println("代理: 开始执行subtract方法" ); int result = target.subtract(a, b); System.out.println("代理: subtract方法执行完成,结果=" + result); return result; } @Override public int multiply (int a, int b) { System.out.println("代理: 开始执行multiply方法" ); int result = target.multiply(a, b); System.out.println("代理: multiply方法执行完成,结果=" + result); return result; } @Override public int divide (int a, int b) { System.out.println("代理: 开始执行divide方法" ); try { int result = target.divide(a, b); System.out.println("代理: divide方法执行完成,结果=" + result); return result; } catch (ArithmeticException e) { System.out.println("代理: divide方法执行出错 - " + e.getMessage()); throw e; } } }
测试类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 package edu.software.ergoutree.spring6aop.example;public class Main { public static void main (String[] args) { Calculator target = new CalculatorImpl (); Calculator proxy = new CalculatorProxy (target); System.out.println("10 + 5 = " + proxy.add(10 , 5 )); System.out.println("10 - 5 = " + proxy.subtract(10 , 5 )); System.out.println("10 * 5 = " + proxy.multiply(10 , 5 )); System.out.println("10 / 5 = " + proxy.divide(10 , 5 )); try { System.out.println("10 / 0 = " + proxy.divide(10 , 0 )); } catch (Exception e) { System.out.println("捕获到异常: " + e.getMessage()); } } }
使用动态代理优化
静态代理没有实现对日志的统一管理,实际上并没有进行多少的优化
image-20250422204359251
定义计算器接口和实现类都不变,我们添加一个日志处理器,实现
InvocationHandler 接口
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 class LogHandler implements InvocationHandler { private final Object target; public LogHandler (Object target) { this .target = target; } @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { String methodName = method.getName(); System.out.println("[日志] " + methodName + " 方法开始" ); Object result = method.invoke(target, args); System.out.println("方法内部Result: " + result); System.out.println("[日志] " + methodName + " 方法结束" ); return result; } }
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 public class DynamicProxyExample { public static void main (String[] args) { Calculator calculator = new CalculatorImpl (); LogHandler logHandler = new LogHandler (calculator); Calculator proxyCalculator = (Calculator) Proxy.newProxyInstance( calculator.getClass().getClassLoader(), calculator.getClass().getInterfaces(), logHandler ); proxyCalculator.add(2 , 3 ); proxyCalculator.subtract(5 , 2 ); proxyCalculator.multiply(3 , 4 ); proxyCalculator.divide(10 , 2 ); } }
在上述动态代理的例子中,我们通过 InvocationHandler
实现了日志功能的增强。然而,当项目变得复杂时,手动编写代理逻辑会变得繁琐,并且难以维护。这时候,Spring
AOP(面向切面编程)就可以发挥作用了。
Spring AOP 是 Spring
框架的一个重要特性,它允许我们在不修改原有代码的情况下,对程序进行增强。Spring
AOP
基于动态代理实现,提供了一种更加简洁、灵活的方式来实现横切关注点(如日志、事务管理等)。
动态代理的工厂类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 package edu.software.ergoutree.spring6aop.example;import java.lang.reflect.InvocationHandler;import java.lang.reflect.Method;import java.lang.reflect.Proxy;public class ProxyFactory { private Object target; public ProxyFactory (Object target) { this .target = target; } public Object getProxy () { ClassLoader classLoader = target.getClass().getClassLoader(); Class<?>[] interfaces = target.getClass().getInterfaces(); InvocationHandler invocationHandler = new InvocationHandler () { @Override public Object invoke (Object proxy, Method method, Object[] args) throws Throwable { System.out.println("[动态代理日志] 开始执行 " + method.getName() + " 方法,参数: " + java.util.Arrays.toString(args)); Object result = method.invoke(target, args); System.out.println("[动态代理日志] " + method.getName() + " 方法执行完毕,结果: " + result); System.out.println("----------------------------------" ); return result; } }; return Proxy.newProxyInstance(classLoader, interfaces, invocationHandler); } }
测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 package edu.software.ergoutree.spring6aop.example;public class CalcutorTestProxy { public static void main (String[] args) { Calculator target = new CalculatorImpl (); ProxyFactory proxyFactory = new ProxyFactory (target); Calculator proxy = (Calculator) proxyFactory.getProxy(); System.out.println("===== 测试加法 =====" ); int addResult = proxy.add(10 , 5 ); System.out.println("主程序获取的结果: " + addResult); System.out.println("\n===== 测试减法 =====" ); int subtractResult = proxy.subtract(10 , 5 ); System.out.println("主程序获取的结果: " + subtractResult); System.out.println("\n===== 测试乘法 =====" ); int multiplyResult = proxy.multiply(10 , 5 ); System.out.println("主程序获取的结果: " + multiplyResult); System.out.println("\n===== 测试除法 =====" ); int divideResult = proxy.divide(10 , 5 ); System.out.println("主程序获取的结果: " + divideResult); try { System.out.println("\n===== 测试除法异常 =====" ); proxy.divide(10 , 0 ); } catch (Exception e) { System.out.println("捕获到异常: " + e.getClass().getName()); } } }
基于注解的 AOP
技术说明
动态代理分类:JDK动态代理 和 cglib动态代理
有接口,使用 JDK
动态代理,生成接口实现类的代理对象,代理对象和目标对象都实现同样的接口
无接口,使用 cglib 动态代理,生成子类的代理对象,继承目标类
image-20250423163635376
动态代理分为JDK动态代理和glib动态代理
当目标类有接口的情况使用JDK动态代理和cglib动态代理,
没有接口时只能使用cgib动态代理
JDK动态代理动态生成的代理类会在com.sun.proxy包下,类名为$proxy1,和目标类实现相同的接口●cglib动态代理动态生成的代理类会和目标在在相同的包下,会继承目标类
动态代理(InvocationHandler) :
JDK原生的实现方式,需要被代理的目标类必须实现接口。因为这个技术要求*“代理对象和目标对象实现同样的接口**
(兄弟两个拜把子模式)。
cglib:
通过继承被代理的目标类(认干爹模式)实现代理,所以不需要目标类实现接口。
AspectJ:是AOP思想的一种实现。本质上是静态代理,将代理逻辑”织入”被代理的目标类编译得到的字节码文件,所以最终效果是动态的。weaver就是织入器。
Spring只是借用了 AspectJ中的注解。
使用注解配置AOP
Spring AOP允许使用基于注解的方式实现AOP,这样做可以简化Spring配置文件 的臃肿代码。
image-20250423163517236
步骤:
引入 AOP 相关依赖
创建目标资源
创建切面类
实例
所以先创建 LogAspect 切面类
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 package edu.software.ergoutree.spring6aop.anoaop;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.ProceedingJoinPoint;import org.aspectj.lang.annotation.*;import org.springframework.stereotype.Component;import java.util.Arrays;@Component @Aspect public class LogAspect { @Pointcut("execution(* edu.software.ergoutree.spring6aop.anoaop.Calculator.*(..))") public void calculatorPointcut () {} @Before("calculatorPointcut()") public void beforeMethod (JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); System.out.println("Logger->前置通知,方法名" + methodName + " 参数 " + args); } @After("calculatorPointcut()") public void afterMethod (JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); System.out.println("Logger->后置通知,方法名" + methodName + " 参数 " + args); } @AfterReturning(value = "calculatorPointcut()", returning = "result") public void afterReturningMethod (JoinPoint joinPoint, Object result) { String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); System.out.println("Logger->返回通知,方法名" + methodName + " 参数 " + args + "目标方法的返回值" + result); } @AfterThrowing(value = "calculatorPointcut()", throwing = "ex") public void afterThrowingMethod (JoinPoint joinPoint, Throwable ex) { String methodName = joinPoint.getSignature().getName(); String args = Arrays.toString(joinPoint.getArgs()); System.out.println("Logger->异常通知 方法 " + methodName + " 参数 " + args + " 执行抛出异常: " + ex.getMessage()); } @Around("calculatorPointcut()") public Object aroundMethod (ProceedingJoinPoint joinPoint) throws Throwable { String methodName = joinPoint.getSignature().getName(); Object[] args = joinPoint.getArgs(); System.out.println("Logger->环绕通知-前 方法 " + methodName + " 开始执行,参数: " + Arrays.toString(args)); try { System.out.println("Logger->环绕通知-目标方法之前执行" ); Object result = joinPoint.proceed(); System.out.println("Logger->环绕通知-目标方法之后执行 方法 " + methodName + " 执行成功,结果: " + result); return result; } catch (Exception e) { System.out.println("Logger->环绕通知-异常 方法 " + methodName + " 执行失败,异常: " + e.getMessage()); throw e; } finally { System.out.println("Logger->环绕通知-最终 方法 " + methodName + " 执行结束" ); } } @Around("calculatorPointcut()") public Object timingAround (ProceedingJoinPoint joinPoint) throws Throwable { long start = System.currentTimeMillis(); try { return joinPoint.proceed(); } finally { long end = System.currentTimeMillis(); System.out.println("方法 " + joinPoint.getSignature().getName() + " 执行耗时: " + (end - start) + "ms" ); } } }
关于切入点表达式
语法细节:
用号代替”权限修饰符”和“返回值”部分表示”权限修饰符”和“返回值”不限
在包名的部分,一个*号只能代表包的层次结构中的一层,表示这一层是任意的。
例如: *.Hello匹配com.Hello, 不匹配com.atguigu.Hello
在包名的部分,使用
*..
表示包名任意,包的层次深度任意
在类名的部分,类名部分整体用 * 号代替,表示类名任意
在类名的部分,可以使用 * 号代替类名的一部分
例如:
*Service
匹配所有名称以Service
结尾的类或接口
在方法名部分,可以使用 * 号表示方法名任意
在方法名部分,可以使用 * 号代替方法名的一部分
例如: *Operation匹配所有方法名以Operation结尾的方法
在方法参数列表部分,使用(..)表示参数列表任意
在方法参数列表部分,使用(int,..)表示参数列表以一个int类型的参数开头
在方法参数列表部分,基本数据类型和对应的包装类型是不一-样的
切入点表达式中使用int和实际方法中Integer 是不匹配的
在方法返回值部分,如果想要明确指定一个返回值类型,
那么必须同时写明楫限修饰符。
例如: execution(public int ..Service,(..,int))正确
例如: execution( int ..Service.*(..,int))错误
完整例子:
execution(访问修饰符 增强方法的返回类型 增强方法所在类全路径.方法名)
image-20250423165621177
关于 环绕通知
环绕通知的特点
功能最强大的通知类型 :可以控制目标方法是否执行、何时执行、如何执行
唯一能控制方法执行的切入点 :可以决定是否调用proceed()
方法来执行目标方法
可以修改返回值 :在方法执行前后对返回值进行处理
可以处理异常 :捕获并处理目标方法抛出的异常
aroundMethod 的方法参数说明
需要注意的是,环绕通知必须接收一个类型为ProceedingJoinPoint
的参数,返回值也必须是Object
类型,且必须抛出异常
ProceedingJoinPoint
:继承自JoinPoint
,新增了proceed()
方法
proceed()
:执行目标方法,返回目标方法的返回值
proceed(Object[] args)
:使用新参数执行目标方法
与其他通知的执行顺序
当环绕通知与其他通知共存时,执行顺序为:
环绕通知的前半部分
前置通知
目标方法
返回通知/异常通知
后置通知
环绕通知的后半部分
重用切入表达式
关于 @Pointcut
@Pointcut
是 Spring AOP
里的一个注解,其作用是定义一个切入点(Pointcut)
public void calculatorPointcut() {}
这是一个空方法,其作用是为切入点命名。通过这种方式,可以在其他通知注解(如
@Before
、@After
等)中引用这个切入点
创建 bean.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 <?xml version="1.0" encoding="UTF-8" ?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:context ="http://www.springframework.org/schema/context" xmlns:aop ="http://www.springframework.org/schema/aop" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd" > <context:component-scan base-package ="edu.software.ergoutree.spring6aop.anoaop" > </context:component-scan > <aop:aspectj-autoproxy > </aop:aspectj-autoproxy > </beans >
当然也可以用注解类
1 2 3 4 5 6 7 8 9 import org.springframework.context.annotation.ComponentScan;import org.springframework.context.annotation.Configuration;import org.springframework.context.annotation.EnableAspectJAutoProxy;@Configuration @ComponentScan("edu.software.ergoutree.spring6aop.anoaop") @EnableAspectJAutoProxy public class AppConfig {}
基于 XML 的AOP实现
因为Spring AOP的代理对象由IoC容器 自动生成,所以开发者无须过多关注代理对象生成的过程,只需选择连接点、创建切面、定义切点并在XML文件中添加配置信息即可。
image-20250423174728711
配置切面
在Spring的配置文件中,配置切面使用的是<aop:aspect>
元素,该元素会将一个已定义好的Spring Bean
转换成切面Bean,因此,在使用<aop:aspect>
元素之前,要在配置文件中先定义一个普通的Spring Bean
。Spring Bean
定义完成后,通过<aop:aspect>
元素的ref
属性即可引用该
Bean。
配置<aop:aspect>
元素时,通常会指定 id 和 ref
这两个属性
配置切入点
在Spring的配置文件中,切入点是通过<aop:pointcut>
元素来定义的,表示该切入点是全局切入点,它可被多个切面共享;当<aop:pointcut>
元素作为<aop:aspect>
元素的子元素时,表示该切入点只对当前切面有效。
在定义<aop:aspect>
元素时,通常会指定 id 和
expression 这两个属性
属性名称
描述
id
用于指定切入点的唯一标识
expression
用于指定切入点关联的切入点表达式
配置通知
在Spring的配置文件中,使用<aop:aspect>
元素配置了5种常用通知,如表所示,5种通知分别为前置通知、后置通知、环绕通知、返回通知和异常通知,<aop:aspect>
元素的常用属性如表4所示。
image-20250423175046604
实例
文件结构
1 2 3 4 5 6 7 8 9 10 11 12 src/ ├── main/ │ ├── java/ │ │ └── edu.software.ergoutree.spring6aop.xmlaop/ │ │ ├── Calculator.java # 接口 │ │ ├── CalculatorImpl.java # 实现类 │ │ └── LogAspect.java # 切面类 │ └── resources/ │ └── bean-aopxml.xml # XML配置 └── test/ └── java/ └── CalculatorAOPTest.java # 测试类
导入AspectJ框架相关JAR包的依赖,在pom.xml中添加的代码如下:
1 2 3 4 5 6 7 8 9 10 <dependency > <groupId > org.aspectj</groupId > <artifactId > aspectjrt</artifactId > <version > 1.9.1</version > </dependency > <dependency > <groupId > org.aspectj</groupId > <artifactId > aspectjweaver</artifactId > <version > 1.9.6</version > </dependency >
计算器的接口和实现类依旧用上面的那个
切面类,定义了5种通知
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 package edu.software.ergoutree.spring6aop.xmlaop;import org.aspectj.lang.JoinPoint;import org.aspectj.lang.ProceedingJoinPoint;public class LogAspect { public void beforeAdvice (JoinPoint joinPoint) { System.out.println("[前置] 方法名: " + joinPoint.getSignature().getName()); } public void afterAdvice (JoinPoint joinPoint) { System.out.println("[后置] 方法执行完毕" ); } public void afterReturningAdvice (JoinPoint joinPoint, Object result) { System.out.println("[返回] 结果: " + result); } public void afterThrowingAdvice (JoinPoint joinPoint, Exception ex) { System.out.println("[异常] 错误信息: " + ex.getMessage()); } public Object aroundAdvice (ProceedingJoinPoint pjp) throws Throwable { System.out.println("[环绕前] 参数: " + pjp.getArgs()[0 ] + ", " + pjp.getArgs()[1 ]); Object result = pjp.proceed(); System.out.println("[环绕后] 结果: " + result); return result; } }
xml 配置 bean-aopxml.xml
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 <?xml version="1.0" encoding="UTF-8" ?> <beans xmlns ="http://www.springframework.org/schema/beans" xmlns:xsi ="http://www.w3.org/2001/XMLSchema-instance" xmlns:context ="http://www.springframework.org/schema/context" xmlns:aop ="http://www.springframework.org/schema/aop" xsi:schemaLocation ="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/aop https://www.springframework.org/schema/aop/spring-aop.xsd" > <context:component-scan base-package ="edu.software.ergoutree.spring6aop.xmlaop" /> <bean id ="logAspect" class ="edu.software.ergoutree.spring6aop.xmlaop.LogAspect" /> <aop:config > <aop:aspect ref ="logAspect" > <aop:pointcut id ="calculatorMethods" expression ="execution(* edu.software.ergoutree.spring6aop.xmlaop.Calculator.*(..))" /> <aop:before method ="beforeAdvice" pointcut-ref ="calculatorMethods" /> <aop:after method ="afterAdvice" pointcut-ref ="calculatorMethods" /> <aop:after-returning method ="afterReturningAdvice" pointcut-ref ="calculatorMethods" returning ="result" /> <aop:after-throwing method ="afterThrowingAdvice" pointcut-ref ="calculatorMethods" throwing ="ex" /> <aop:around method ="aroundAdvice" pointcut-ref ="calculatorMethods" /> </aop:aspect > </aop:config > <bean id ="calculator" class ="edu.software.ergoutree.spring6aop.xmlaop.CalculatorImpl" /> </beans >
测试
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 package edu.software.ergoutree.spring6aop;import edu.software.ergoutree.spring6aop.xmlaop.Calculator;import edu.software.ergoutree.spring6aop.xmlaop.CalculatorImpl;import org.junit.jupiter.api.Test;import org.springframework.context.support.ClassPathXmlApplicationContext;public class CalculatroAOPTest { @Test public void Test1 () { try (ClassPathXmlApplicationContext context = new ClassPathXmlApplicationContext ("bean-aopxml.xml" )) { CalculatorImpl calculator = context.getBean("calculator" , CalculatorImpl.class); System.out.println("=== 正常测试 ===" ); calculator.add(5 , 3 ); calculator.divide(6 , 2 ); System.out.println("\n=== 异常测试 ===" ); try { calculator.divide(6 , 0 ); } catch (ArithmeticException ignored) {} } } }