1 AOP简介
AOP是面向切面编程(Aspect-Oriented Programming)的缩写,是对传统的面向对象编程的一种补充,下面来介绍下在AOP中经常用到的一些术语。
- 切面(Aspect):横切关注点(跨越应用程序多个模块的功能)被模块化的特殊对象
- 通知(Advice):切面必须要完成的工作
- 目标(Target):被通知的对象
- 代理(Proxy): 向目标对象应用通知之后创建的对象
- 连接点(Joinpoint):程序执行的某个特定位置:如类某个方法调用前、调用后、方法抛出异常后等。
- 切点(pointcut): 一个类中可以有多个连接点 ,就和每个类中可以有多个方法一样,AOP通过切点定位到特定的连接点。 可以这么理解 :连接点相当于数据库中的记录,切点则相当于查询条件,切点使用类和方法作为连接点的查询条件。
AOP切面编程是围绕着通知注解这个东西展开的,在SpringAOP中共有五种类型的通知注解:
- @Before: 前置通知,在方法执行之前执行
- @After: 后置通知,在方法执行之后执行
- @AfterRunning:返回通知,在方法返回结果之后执行
- @AfterThrowing:异常通知,在方法抛出异常之后
- @Around:环绕通知,围绕着方法执行 (并不常用)
2 一个小需求
我通过一个小小的需求来向大家展示SpringAOP编程的各个细节,比如说,我们写了两个方法:一个方法将两个整形的数字做加法运算、一个方法将两个整形数字做除法运算,这样一个需求看起来非常简单。但是,我们要求, 在方法开始时、结束时、返回时、发生异常时,分别在控制台上输出一段文字来说明当前的状态 ,这时该怎么办呢??
我们通过AOP可以很轻松的解决这个问题。
3 通过注解的方式配置AOP
首先建立一个JAVA项目,在项目下导入Spring的开发包,然后创建一个接口ArithmeticCalculator,在接口中定义两个方法,add()和div()。代码如下:
package com.byh.aop.hello;
public interface ArithmeticCalculator {
int add(int i,int j);
int div(int i,int j);
}
在src下创建Spring的配置文件applicationContext.xml,并在Namespaces选项中勾选aop和context这两个选项,之后在applicationContext.xml中启用 AspectJ 注解支持,并指定IOC容器的扫描范围。代码如下:
<context:component-scan base-package="com.byh.aop.hello"></context:component-scan> <!-- 指定IOC容器的扫描范围 -->
<aop:aspectj-autoproxy></aop:aspectj-autoproxy> <!-- 启用 AspectJ 注解支持 -->
添加该接口的实现类ArithmeticCalculatorImpl,实现ArithmeticCalculator接口的两个方法。代码如下:
package com.byh.aop.hello;
import org.springframework.stereotype.Component;
@Component //将该类注入到IOC容器中
public class ArithmeticCalculatorImpl implements ArithmeticCalculator {
@Override
public int add(int i, int j) {
int result = i+j;
return result;
}
@Override
public int div(int i, int j) {
int result = i/j;
return result;
}
}
写一个测试类来测试,代码如下:
package com.byh.aop.hello;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Main {
public static void main(String[] args) {
ApplicationContext ctx = new ClassPathXmlApplicationContext("applicationContext.xml");
ArithmeticCalculator arithmeticCalculator = ctx.getBean(ArithmeticCalculator.class);
int result = arithmeticCalculator.add(3, 6);
System.out.println("result:"+result);
result = arithmeticCalculator.div(12, 6);
System.out.println("result:"+result);
}
}
运行之后我们发现运行是成功的,但是这还没有达到我们的要求,在需求中我们要求在方法开始时、结束时、返回时、发生异常时,分别在控制台上输出一段文字来说明当前的状态,现在我们就用AOP来实现。
3-1 前置通知
首先我们来想如何让方法开始时在控制台输出一段话呢?这是就要用到AOP的前置通知了。前置通知就是在方法执行之前执行的通知,前置通知使用@Before注解,并将切入点表达式的值也就是该方法的路径,作为其注解值。如果要把一个类声明为一个切面的话,只要在该类的前面加上@Aspect注解就可以了。可以在通知方法中声明一个类型为 JoinPoint 的参数.。然后就能访问链接细节. 如方法名称和参数值。
我们来新建一个类名为LoggingAspect,并在该类中实现前置通知,代码如下:
@Component //将该类注入到IOC容器
@Aspect //将该类声明为切面
public class LoggingAspect {
@Before(value = "execution(public int com.byh.aop.hello.ArithmeticCalculator.*(..))")//将该方法声明为前置通知,切入点表达式的值为连接点的路径
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("前置通知: The method "+methodName+" begins with "+args);
}
}
3-2 后置通知
后置通知是在连接点完成之后执行的, 即连接点返回结果或者抛出异常的时候,。<font color=red >一个切面可以包括一个或者多个通知</font>,所以根据我们的需求,我们在刚才前置通知的基础上在LoggingAspect类中添加实现后置通知的方法即可,代码如下:
@After(value = "execution(public int com.byh.aop.hello.ArithmeticCalculator.*(..))")
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
System.out.println("后置通知: end method "+methodName);
}
这时不知道大家有没有发现一个问题,我们在每一个通知的前面都要写上切点表达式,而且切点表达式的内容是一样的,这样就不符合我们编码的风格了,该如何进行简化呢?我们可以声明一个类去指定切入点表达式的值,这个方法中什么都不用写,只需要在方法前使用@Pointcut注解来指定切点表达式即可,代码如下。
@Pointcut("execution(public int com.byh.aop.hello.ArithmeticCalculator.*(..))")
public void declareJointPointExpression(){}
将该方法加入到LoggingAspect类中,并且写在所有通知之前,这样一来,当需要生命切入点表达式时,直接将该方法的名字写上就好了,我们修改之前写的前置通知和后置通知的切入点表达式声明方法,代码如下:
@Before("declareJointPointExpression()")//修改切入点表达式的声明方法
public void beforeMethod(JoinPoint joinPoint){
...
}
3-3 返回通知
无论连接点是正常返回还是抛出异常, 后置通知都会执行。如果想在连接点返回的时候记录日志, 应使用返回通知。代码如下:
@AfterReturning(value="declareJointPointExpression()",
returning="result")//returning指定返回的参数
public void afterReturning(JoinPoint joinPoint,Object result){
String methodName = joinPoint.getSignature().getName();
System.out.println("返回通知: The method "+methodName+" ends with "+result);
}
3-4 异常通知
异常通知只在连接点抛出异常时才会执行,将 throwing 属性添加到 @AfterThrowing 注解中, 也可以访问连接点抛出的异常..Throwable 是所有错误和异常类的超类.。所以在异常通知方法可以捕获到任何错误和异常。如果只对某种特殊的异常类型感兴趣, 可以将参数声明为其他异常的参数类型, 然后通知就只在抛出这个类型及其子类的异常时才被执行。代码如下:
@AfterThrowing(value="declareJointPointExpression()",
throwing="ex")
public void afterThrowing(JoinPoint joinPoint,Exception ex){
String methodName = joinPoint.getSignature().getName();
System.out.println("异常通知: The method "+methodName+" occurs exception: "+ex);
}
这时我们再去运行下测试方法,发现运行的结果已经变成了这样:
前置通知: The method add begins with [3, 6]
后置通知: end method add
返回通知: The method add ends with 9
result:9
前置通知: The method div begins with [12, 6]
后置通知: end method div
返回通知: The method div ends with 2
result:2
这就表明我们写的几个通知已经起到它该有的作用了,测试异常通知的话,只需将除法的被除数改为0即可,这里就不做演示了,至此我们已经了解AOP到底是如何使用的了。
4 通过基于 XML 的配置来配置AOP
除了使用注解的方式去配置之外,我们还可以使用配置文件的方式去配置。
在 Bean 配置文件中, 所有的 Spring AOP 配置都必须定义在 aop:config 元素内部。
对于每个切面而言, 都要创建一个 aop:aspect 元素来为具体的切面实现引用后端 Bean 实例。
切入点使用 aop:pointcut 元素声明,切入点必须定义在 aop:aspect 元素下, 或者直接定义在 aop:config 元素下。区别是定义在 aop:aspect 元素下: 只对当前切面有效,定义在 aop:config 元素下: 对所有切面都有效。
修改applicationContext.xml,代码如下:
<context:component-scan base-package="com.byh.aop.hello"></context:component-scan>
<!-- 配置AOP -->
<aop:config>
<!-- 配置切点表达式 -->
<aop:pointcut expression="execution(* com.byh.aop.hello.ArithmeticCalculator.*(..))" id="pointcut"/>
<!--声明切面-->
<aop:aspect ref="loggingAspect">
<!--前置通知-->
<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:config>
然后我们将LoggingAspect类中所有关于AOP的注解都删除掉,删除之后代码如下:
@Component
public class LoggingAspect {
public void beforeMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
List<Object> args = Arrays.asList(joinPoint.getArgs());
System.out.println("前置通知: The method "+methodName+" begins with "+args);
}
public void afterMethod(JoinPoint joinPoint){
String methodName = joinPoint.getSignature().getName();
System.out.println("后置通知: end method "+methodName);
}
public void afterReturning(JoinPoint joinPoint,Object result){
String methodName = joinPoint.getSignature().getName();
System.out.println("返回通知: The method "+methodName+" ends with "+result);
}
public void afterThrowing(JoinPoint joinPoint,Exception ex){
String methodName = joinPoint.getSignature().getName();
System.out.println("异常通知: The method "+methodName+" occurs exception: "+ex);
}
}
之后运行测试方法,发现运行结果与使用注解进行配置的方式是一样的。
笔者水平有限,若有错漏,欢迎指正,如果转载以及CV操作,请务必注明出处,谢谢!
版权声明:本文为博主原创文章,未经博主允许不得转载。