AOP(Aspect-Oriented Programming):面向切面编程,是对传统的面向对象编程的补充。
什么意思呢?
比如上图中,在不同的方法中,有许多相同的功能代码,那我们就可以把这些相同的功能代码抽取出来,放到类中,那么这个类就被叫做切面。
实际上,AOP 的原理就是利用了动态代理,当我们需要调用目标对象的时候,Spring 就会帮我们生成一个代理对象,将切面和核心的业务逻辑代码组装起来,形成完整的模块。即使我们将代码抽离出来,也并不会影响我们的正常使用。
这样做的好处是:
对于面向切面编程,我们可以使用 ASpectJ 框架,AspectJ 是 Java 社区里最完整最流行的 AOP 框架。
因此我们想要使用 AscpectJ 框架,就必须要先导入依赖的 jar 包:
其次我们还需要将 aop Schema 命名空间添加到配置文件中。
接下来我们就可以使用注解实现 AOP 了。
面向切面编程,那么首先我们得有切面。上文说到切面就是一个类,那难道说我们创建了一个类,这个类就是一个切面吗?Spring 如何识别这是一个切面呢?
我们可以使用 @Aspect
注解,只要在对应的类上标注这个注解,那么此类就是一个切面。
切面中是一个一个的通知,一个切面中可以有多个通知,通知就是切面要完成的工作,在我们想要调用业务方法时,会将这些通知加入到业务方法中的某个位置,比如方法前、后等,从而形成一个完整的业务功能。通知在代码中的体现就是加了某种注解的 Java 方法。
AspectJ 一共支持 5 种类型的通知,它们对应的注解分别是:
(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 元素:
<aop:aspectj-autoproxy>aop:aspectj-autoproxy>
当 Spring IOC 容器侦测到 bean 配置文件中的
元素时,会自动为与 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 的声明。
(1)
和注解配置类似,首先我们也要配置一个切面。切面所在的类要先实例化。
<bean id="loggingAspect" class="com.atguigu.spring.aop.xml.LoggingAspect">bean>
<aop:config>
<aop:aspect ref="loggingAspect" order="2">
aop:aspect>
aop:config>
(2)
第二步我们要配置切入点表达式。使用
标签,如果配置在
标签下,则所有的切面都可使用,如果配置在
标签下,则只能在此切面中使用。
<aop:config>
<aop:pointcut id="pointcut" expression="execution(*com.spring.aop.xml.ArithmeticCalculator.*(int, int))" />
aop:config>
(3)
第三步就是配置各个通知了,每种通知都对应这不同的 aop
标签,在通知中可以使用 pointcut
属性来单独配置切入点表达式,也可以使用 pointcut-ref
属性来引用已经配置好的切入点表达式。
<bean id="loggingAspect" class="com.atguigu.spring.aop.xml.LoggingAspect">bean>
<bean id="vlidationAspect" class="com.atguigu.spring.aop.xml.VlidationAspect">bean>
<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:aspect>
<aop:aspect ref="vlidationAspect" order="1">
<aop:before method="validateArgs" pointcut-ref="pointcut"/>
aop:aspect>
aop:config>