Spring AOP
- AspectJ:Java 社区里最完整最流行的 AOP 框架
- 在 Spring2.0 以上版本中, 可以使用基于 AspectJ 注解或基于 XML 配置的 AOP
基于 AspectJ 注解的 AOP
启用 AspectJ 注解支持
- 引入 Jar 包(aopalliance.jar、aspectj.weaver.jar 和 spring-aspects.jar、spring-aop.jar)
- 将 aop Schema 添加到 <beans> 根元素中
- 要在 Spring IOC 容器中启用 AspectJ 注解支持, 只要在 Bean 配置文件中定义一个空的 XML 元素 <aop:aspectj-autoproxy>
- 当 Spring IOC 容器侦测到 Bean 配置文件中的 <aop:aspectj-autoproxy> 元素时, 会自动为与 AspectJ 切面匹配的 Bean 创建代理.
用 AspectJ 注解声明切面
- 要在 Spring 中声明 AspectJ 切面, 只需要在 IOC 容器中将切面声明为 Bean 实例. 当在 Spring IOC 容器中初始化 AspectJ 切面之后, Spring IOC 容器就会为那些与 AspectJ 切面相匹配的 Bean 创建代理.
- 在 AspectJ 注解中, 切面只是一个带有 @Aspect 注解的 Java 类.
- 通知是标注有某种注解的简单的 Java 方法.
- AspectJ 支持 5 种类型的通知注解:
- @Before: 前置通知, 在方法执行之前执行
- @After: 后置通知, 在方法执行之后执行
- @AfterRunning: 返回通知, 在方法返回结果之后执行
- @AfterThrowing: 异常通知, 在方法抛出异常之后
- @Around: 环绕通知, 围绕着方法执行
1. 前置通知
- 前置通知:在方法执行之前执行的通知
- 前置通知使用 @Before 注解, 并将切入点表达式的值作为注解值.
@Aspect @Component public class LoggingAspect { @Before("execution(public int com.axon.spring5.aop.impl.ArithmeticCalculator.add(int, int))") public void beforeMethod(JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); List<Object> args = (List<Object>) Arrays.asList(joinPoint.getArgs()); System.out.println("The method " + methodName + "begins with "+args); } }
- 编写 AspectJ 切入点表达式
- execution * com.axon.spring.ArithmeticCalculator.*(..): 匹配 ArithmeticCalculator 中声明的所有方法,第一个 * 代表任意修饰符及任意返回值. 第二个 * 代表任意方法. .. 匹配任意数量的参数. 若目标类与接口与该切面在同一个包中, 可以省略包名.
- execution public * ArithmeticCalculator.*(..): 匹配 ArithmeticCalculator 接口的 所有公有方法.
- execution public double ArithmeticCalculator.*(..): 匹配 ArithmeticCalculator 中 返回 double 类型数值的方法
- execution public double ArithmeticCalculator.*(double, ..): 匹配第一个参数为 double 类型的方法, .. 匹配任意数量任意类型的参数
- execution public double ArithmeticCalculator.*(double, double): 匹配参数类型为 double, double 类型的方法.
- 在 AspectJ 中, 切入点表达式可以通过操作符 &&, ||, ! 结合起来
@Aspect @Component public class LoggingAspect { @Pointcut("execution(* *.add(int,..)) || execution(* *.sub(int,..))") private void loggingOperation(){} @Before("loggingOperation()") public void beforeMethod(JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); List<Object> args = (List<Object>) Arrays.asList(joinPoint.getArgs()); System.out.println("The method " + methodName + "begins with "+args); } }
2. 后置通知
- 后置通知是在连接点完成之后执行的, 即连接点返回结果或者抛出异常的时候, 下面的后置通知记录了方法的终止.
- 一个切面可以包括一个或者多个通知.
- 后置通知还不能访问目标方法执行的结果
@After("execution(* com.axon.spring5.aop.impl.ArithmeticCalculator.*(int, int))") public void afterMethod(JoinPoint joinPoint) { String methodName = joinPoint.getSignature().getName(); System.out.println("The method " + methodName + " ends"); }
3. 返回通知
- 无论连接点是正常返回还是抛出异常, 后置通知都会执行. 如果只想在连接点返回的时候记录日志, 应使用返回通知代替后置通知.
- 可以访问方法的返回值
@AfterReturning(pointcut="execution(* com.axon.spring5.aop.impl.ArithmeticCalculator.*(int, int))",returning="result") public void afterReturning(JoinPoint joinPoint,Object result) { String methodName = joinPoint.getSignature().getName(); System.out.println("The method " + methodName + " ends with " + result); }
4. 异常通知
- 只在连接点抛出异常时才执行异常通知
- 将 throwing 属性添加到 @AfterThrowing 注解中, 也可以访问连接点抛出的异常. Throwable 是所有错误和异常类的超类. 所以在异常通知方法可以捕获到任何错误和异常.
- 如果只对某种特殊的异常类型感兴趣, 可以将参数声明为其他异常的参数类型. 然后通知就只在抛出这个类型及其子类的异常时才被执行.
@AfterThrowing(value="execution(* com.axon.spring5.aop.impl.ArithmeticCalculator.*(int, int))",throwing="ex") public void afterThrowing(JoinPoint joinPoint,Exception ex) { String methodName = joinPoint.getSignature().getName(); System.out.println("The method " + methodName + " occurs exception: " + ex); }
5. 环绕通知
- 环绕通知是所有通知类型中功能最为强大的,环绕通知类似于动态代理的全过程
- ProceedingJoinPoint . 它是 JoinPoint 的子接口, 允许控制何时执行, 是否执行连接点.
- 注意: 环绕通知的方法需要返回目标方法执行之后的结果, 即调用 joinPoint.proceed(); 的返回值, 否则会出现空指针异常
@Around("execution(* com.axon.spring5.aop.impl.ArithmeticCalculator.*(int, int))") public Object aroundMethod(ProceedingJoinPoint pjd) throws Throwable { Object result = null; String methodName = pjd.getSignature().getName(); //执行目标方法 try{ //前置通知 System.out.println("The method " + methodName + " begins with "+ Arrays.asList(pjd.getArgs())); result = pjd.proceed(); //返回通知 System.out.println("The method " + methodName + " ends with " + result); } catch (Throwable e){ //异常通知 System.out.println("The method " + methodName + " occurs exception: " + e); throw new RuntimeException(e); } //后置通知 System.out.println("The method " + methodName + " ends "); return result; }
指定切面的优先级
- 同一个连接点上应用不止一个切面时, 除非明确指定, 否则它们的优先级是不确定的
- 切面的优先级可以通过实现 Ordered 接口或利用 @Order 注解指定.
- 实现 Ordered 接口, getOrder() 方法的返回值越小, 优先级越高.
- 若使用 @Order 注解, 序号出现在注解中
@Aspect @Order(1) @Component public class LoggingAspect { } @Aspect @Order(2) @Component public class ValidationAspect { }
基于 XML 的配置的 AOP
- 除了使用 AspectJ 注解声明切面, Spring 也支持在 Bean 配置文件中声明切面. 这种声明是通过 aop schema 中的 XML 元素完成的.
- 正常情况下, 基于注解的声明要优先于基于 XML 的声明. 通过 AspectJ 注解, 切面可以与 AspectJ 兼容, 而基于 XML 的配置则是 Spring 专有的. 由于 AspectJ 得到越来越多的 AOP 框架支持, 所以以注解风格编写的切面将会有更多重用的机会.
声明切面步骤
- 首先 需要在 <beans> 根元素中导入 aop Schema
- 在 Bean 配置文件中, 所有的 Spring AOP 配置都必须定义在 <aop:config> 元素内部. 对于每个切面而言, 都要创建一个 <aop:aspect> 元素来为具体的切面实现引用后端 Bean 实例.
- 切面 Bean 必须有一个标示符, 供 <aop:aspect> 元素引用
- 切入点使用 <aop:pointcut> 元素声明
- 切入点必须定义在 <aop:aspect> 元素下, 或者直接定义在 <aop:config> 元素下.
- 定义在 <aop:aspect> 元素下: 只对当前切面有效
<!-- 配置 aop --> <aop:config> <!-- 配置切面以及通知 --> <aop:aspect ref="xxxx" order="1"> <aop:around method="aroundMethod" pointcut="execution(* com.axon.spring5.aop.impl.ArithmeticCalculator.*(int, int))" /> </aop:aspect> </aop:config>
- 定义在 <aop:config> 元素下: 对所有切面都有效
<aop:config> <!-- 配置切入点表达式 --> <aop:pointcut expression="execution(* com.axon.spring5.aop.impl.ArithmeticCalculator.*(int, int))" id="pointcut" /> <aop:aspect ref="xxxx" order="1"> <aop:before method="beforeMethod" pointcut-ref="pointcut" /> <aop:after method="afterMethod" pointcut-ref="pointcut" /> <aop:after-returning method="afterReturning" pointcut-ref="pointcut" returning="result"/> <aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="ex"/> </aop:aspect> <aop:aspect ref="xxxx" order="0"> <aop:before method="validateArgs" pointcut-ref="pointcut"/> </aop:aspect> </aop:config>
- 基于 XML 的 AOP 配置不允许在切入点表达式中用名称引用其他切入点.
- 详细示例:
<!-- 配置切面的 bean --> <bean id="loggingAspect" class="com.axon.spring5.aop.impl.LoggingAspect"></bean> <bean id="validationAspect" class="com.axon.spring5.aop.impl.ValidationAspect"></bean> <!-- 配置 aop --> <aop:config> <!-- 配置切入点表达式 --> <aop:pointcut expression="execution(* com.axon.spring5.aop.impl.ArithmeticCalculator.*(int, int))" id="pointcut" /> <!-- 配置切面以及通知 --> <aop:aspect ref="loggingAspect" order="1"> <aop:before method="beforeMethod" pointcut-ref="pointcut" /> <aop:after method="afterMethod" pointcut-ref="pointcut" /> <aop:after-returning method="afterReturning" pointcut-ref="pointcut" returning="result"/> <aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="ex"/> <!-- <aop:around method="aroundMethod" pointcut-ref="pointcut" /> --> <!-- <aop:around method="aroundMethod" pointcut="execution(* com.axon.spring5.aop.impl.ArithmeticCalculator.*(int, int))" /> --> </aop:aspect> <aop:aspect ref="validationAspect" order="0"> <aop:before method="validateArgs" pointcut-ref="pointcut"/> </aop:aspect> </aop:config>