什么是 AOP
AOP(Aspect-Oriented Programming):面向切面编程,是对传统的面向对象编程的补充。
什么意思呢?
比如上图中,在不同的方法中,有许多相同的功能代码,那我们就可以把这些相同的功能代码抽取出来,放到类中,那么这个类就被叫做切面。
实际上,AOP 的原理就是利用了动态代理,当我们需要调用目标对象的时候,Spring 就会帮我们生成一个代理对象,将切面和核心的业务逻辑代码组装起来,形成完整的模块。即使我们将代码抽离出来,也并不会影响我们的正常使用。
这样做的好处是:
- 业务模块更简洁,只包含核心业务代码,在我们编码的时候就可以更加专注于核心代码的编写上。其次在维护、调试的时候也更加容易定位问题的所在。
- 当我们需要修改公共功能的代码时,只需修改公共功能所在的切面即可,不必再去一个一个的方法中修改。
注解配置
对于面向切面编程,我们可以使用 ASpectJ 框架,AspectJ 是 Java 社区里最完整最流行的 AOP 框架。
准备工作
因此我们想要使用 AscpectJ 框架,就必须要先导入依赖的 jar 包:
- aopalliance.jar
- aspectj.weaver.jar
- spring-aspects.jar
其次我们还需要将 aop Schema 命名空间添加到配置文件中。
基于注解的 AOP
接下来我们就可以使用注解实现 AOP 了。
面向切面编程,那么首先我们得有切面。上文说到切面就是一个类,那难道说我们创建了一个类,这个类就是一个切面吗?Spring 如何识别这是一个切面呢?
我们可以使用 @Aspect
注解,只要在对应的类上标注这个注解,那么此类就是一个切面。
切面中是一个一个的通知,一个切面中可以有多个通知,通知就是切面要完成的工作,在我们想要调用业务方法时,会将这些通知加入到业务方法中的某个位置,比如方法前、后等,从而形成一个完整的业务功能。通知在代码中的体现就是加了某种注解的 Java 方法。
AspectJ 一共支持 5 种类型的通知,它们对应的注解分别是:
- @Before:前置通知,在方法执行之前执行。
- @After:后置通知,在方法执行之后执行,无论方法中是否有异常都会执行。
- @AfterRunning:返回通知,在方法返回结果之后执行。
- @AfterThrowing:异常通知,在方法抛出异常之后执行。
- @Around:环绕通知,需要我们自己在方法中控制。
前置通知
(1)
@Before
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
Object [] args = joinPoint.getArgs();
System.out.println("The method " + methodName + " begins with " + Arrays.asList(args));
}
(2)
配置了通知之后,还缺一样东西,把这些通知应用到哪些方法上呢,我们要告诉 Spring,所以我们还要配置切入点表达式。
例如:
@Before("execution(public int com.spring.aop.ArithmeticCalculator.*(..))")
以下是一些示例:
表达式 | 含义 |
---|---|
execution(* com.atguigu.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 类型的方法。 |
execution (* *.add(int,…)) || execution(* *.sub(int,…)) | 切入点表达式可以通过 “&&”、“||”、“!”等操作符结合起来。 |
(3)
除此之外,要想让这些注解起作用,还需要在配置文件中配置一样东西。在 Spring IOC 容器中启用 AspectJ 注解支持,需要在配置文件中定义一个空的 XML 元素:
<!-- 配置自动为匹配 aspectJ 注解的 Java 类生成代理对象 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
当 Spring IOC 容器侦测到 bean 配置文件中的 <aop:aspectj-autoproxy>
元素时,会自动为与 AspectJ 切面匹配的 bean 创建代理。
JoinPoint 类
通过 JoinPoint 类我们就可以访问一些链接细节,比如当前方法的名称、参数等。
后置通知
@After()
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " ends");
}
返回通知
在返回通知中,可以访问到方法的返回值,只需要将 returning
属性加入到 @AfterReturning
注解中,返回值就会传给 returning
属性对应的值。此外还要在方法中添加一个同名参数。
@AfterReturning(value="declareJointPointExpression()",returning="result")
public void afterReturning(JoinPoint joinPoint, Object result){
String methodName = joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " ends with " + result);
}
异常通知
异常通知中,可以定义发生何种异常时,才执行异常通知,并且可以访问到异常对象。和返回通知类似,我们需要在注解中添加 throwing
属性,以及在方法中添加一个和 throwing
属性值同名的参数,此参数即指定了发生何种异常执行此通知。
/**
* 在目标方法出现异常时会执行的代码.
* 可以访问到异常对象;,且可以指定在出现特定异常时再执行通知代码
*/
@AfterThrowing(value="declareJointPointExpression()",
throwing="e")
public void afterThrowing(JoinPoint joinPoint, Exception e){
String methodName = joinPoint.getSignature().getName();
System.out.println("The method " + methodName + " occurs excetion:" + e);
}
环绕通知
环绕通知类似于动态代理的全过程,需要我们手动控制在何时(方法前?后?)执行什么代码。
与上述通知不同的是:环绕通知的连接点参数类型必须是 ProceedingJoinPoint
,它是 JoinPoint
的子接口,如果想要执行被代理的方法,必须调用 ProceedingJoinPoint
的 proceed()
方法。此外,环绕通知还必须有返回值,返回值即为目标方法的返回值,即 ProceedingJoinPoint.proceed()
方法的返回值。
/**
* 环绕通知需要携带 ProceedingJoinPoint 类型的参数.
* 环绕通知类似于动态代理的全过程: ProceedingJoinPoint 类型的参数可以决定是否执行目标方法.
* 且环绕通知必须有返回值, 返回值即为目标方法的返回值
*/
@Around("execution(public int com.atguigu.spring.aop.ArithmeticCalculator.*(..))")
public Object aroundMethod(ProceedingJoinPoint pjd){
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;
}
切入点可重用
如果我们在每一个注解的后面都指定切入点表达式,则非常麻烦,如果修改还需要一个一个修改。因此我们可不可以将切入点表达式抽离出来呢?
答案是:可以的。我们可以使用 @Pointcut
注解来配置统一的切入点表达式,我们只需要在一个方法上方用 @Pointcut
注解标注,其他的注解直接引用该方法名即可。
切入点方法的访问控制符同时也控制着这个切入点的可见性。如果切入点要在多个切面中共用,最好将它们集中在一个公共的类中。在这种情况下,它们必须被声明为 public。在引入这个切入点时,必须将类名也包括在内。如果类没有与这个切面放在同一个包中,还必须包含包名。
/**
* 定义一个方法, 用于声明切入点表达式。 一般地,该方法中再不需要添入其他的代码。
* 使用 @Pointcut 来声明切入点表达式。
* 后面的其他通知直接使用方法名来引用当前的切入点表达式。
*/
@Pointcut("execution(public int com.spring.aop.ArithmeticCalculator.*(..))")
public void declareJointPointExpression(){
}
@Before("declareJointPointExpression()")
配置切面的优先级
如果我们有好几个切面,Spring 就不知道谁改先执行,谁该后执行。不过,我们可以明确指定它们之间的执行顺序,切面的优先级可以通过实现 Ordered
接口或利用 @Order
注解指定。
实现 Ordered 接口,getOrder() 方法的返回值越小,优先级越高。@Order 注解也类似,数值越小,优先级越高。
@Aspect
@Order(1)
public class LoggingAspect {
}
XML 配置
切面除了支持注解配置,还支持使用配置文件的方式来配置。不过正常情况下,基于注解的声明要优先于基于 XML 的声明。
(1)
和注解配置类似,首先我们也要配置一个切面。切面所在的类要先实例化。
<!-- 配置切面的 bean. -->
<bean id="loggingAspect" class="com.atguigu.spring.aop.xml.LoggingAspect"></bean>
<!-- 配置 AOP -->
<aop:config>
<!-- 配置切面 -->
<aop:aspect ref="loggingAspect" order="2">
</aop:aspect>
</aop:config>
(2)
第二步我们要配置切入点表达式。使用 <aop:pointcut>
标签,如果配置在 <aop:config>
标签下,则所有的切面都可使用,如果配置在 <aop:aspect>
标签下,则只能在此切面中使用。
<!-- 配置 AOP -->
<aop:config>
<!-- 配置切点表达式 -->
<aop:pointcut id="pointcut" expression="execution(*com.spring.aop.xml.ArithmeticCalculator.*(int, int))" />
</aop:config>
(3)
第三步就是配置各个通知了,每种通知都对应这不同的 aop
标签,在通知中可以使用 pointcut
属性来单独配置切入点表达式,也可以使用 pointcut-ref
属性来引用已经配置好的切入点表达式。
<!-- 配置切面的 bean. -->
<bean id="loggingAspect" class="com.atguigu.spring.aop.xml.LoggingAspect"></bean>
<bean id="vlidationAspect" class="com.atguigu.spring.aop.xml.VlidationAspect"></bean>
<!-- 配置 AOP -->
<aop:config>
<!-- 配置切点表达式 -->
<aop:pointcut id="pointcut" expression="execution(* com.spring.aop.xml.ArithmeticCalculator.*(int, int))" />
<!-- 配置切面及通知 -->
<aop:aspect ref="loggingAspect" order="2">
<aop:before method="beforeMethod" pointcut-ref="pointcut"/>
<aop:after method="afterMethod" pointcut-ref="pointcut"/>
<aop:after-throwing method="afterThrowing" pointcut-ref="pointcut" throwing="e"/>
<aop:after-returning method="afterReturning" pointcut-ref="pointcut" returning="result"/>
<!--
<aop:around method="aroundMethod" pointcut-ref="pointcut"/>
-->
</aop:aspect>
<aop:aspect ref="vlidationAspect" order="1">
<aop:before method="validateArgs" pointcut-ref="pointcut"/>
</aop:aspect>
</aop:config>