为什么需要AOP
?
AOP
(面向切面编程)和OOP
(面向对象编程)一样,也是一种编程思想。具体来说,AOP
是OOP
的一种有效补充,以求解决OOP
中的一些弊端。在OOP
的思想下,我们可以很轻松的将一些业务需求抽象成一个个类,形成可重用的模块。但是遇到系统需求时,往往捉襟见肘,造成大量的重复代码,比如我们最常见的打印日志和权限验证的需求。
上图中上,
Class A
、Class B
、Class C
这三个不同的类,却都需要在某个方法执行前进行权限验证,在执行后进行日志记录。这样横跨了多个类的共同需求,我们称为横切关注点
。在这里显然varify()
和log()
在多个类中重复,当然重复代码还不是最主要的问题,当我们需要修改verify()
或log()
方法时,我们要在A、B、C
三个类中都进行修改,当类的数目越来越多,就会牵一发而动全身。那么有人会说,我们可以把verify()
和log()
抽象成一个类,如果需要进行修改时,就在这个类中进行。这个方案似乎可行,但是仍然存在问题
- 因为很多时候横切关注点的逻辑和业务逻辑纠缠在一起,并不是很好的进行抽取。
- 假设我们想将
log()
调整到方法执行之前,或者说在方法执行前也添加log()
打印日志,那我们还是需要去大量的类中手动添加代码,这个方法治标不治本。 - 如果能把所有的横切关注点的逻辑直接抽离出来,让程序员专注于业务代码就好了,这样子代码的可读性也会大大提高。
AOP
就是为了帮助我们解决上述问题而生的,具体来说就是
- 帮助我们把横切关注点从多个类中抽取出来,形成
Aspect
(切面)- 程序运行时/编译时,帮我们把这些横切逻辑重新插入到每个类中对应的位置(
pointcut
),这个过程叫做weaver
(织入)。
这种在运行时,动态地将代码切入到类的指定方法、指定位置上的编程思想就是面向切面的编程(AOP
)。AOP
是一种编程思想,而Spring AOP
则是AOP
思想的具体实现。
Spring AOP的使用
在具体应用之前,让我们先熟悉AOP
下的一些术语
术语 | 解释 |
---|---|
jointpoint |
系统运行前,AOP 的功能模块需要织入到OOP 的功能模块中去,jointpoint 就是指能够进行织入操作的执行点 |
pointcut |
切点,一次织入过程中, 具体的jointpoint信息,比如要在A() 方法处织入横切逻辑,那么A() 就是pointcut |
advice |
通知,代表具体的横切逻辑,可以类比OOP 中的method ,注意:advice 还指明了执行横切逻辑的时间的,比如在A() 执行方法之前执行,还是在其之后执行等 |
aspect |
切面,point + advice = aspect , 在哪些切点(切点是个集合)上执行何种横切逻辑(比如打印日志)就是一个切面 |
在不同的AOP
实现中,jointpoint
的粒度不同,在Spring AOP
中,这个jointpoint
是方法级别的,也就是只提供方法拦截,但即便这样,也足以满足80%的业务需求了。advice
除了定义了横切逻辑,还定义了横切逻辑执行的时机,在Spring AOP
中有前置、后置、返回、异常、环绕五种Advice
,例如前置型Advice
,表示在pointcut
前执行横切逻辑,下面会举例详细说明。
前置Advice
首先让我们定义一个People
类,它包含一个eatFruit
表示吃水果的这个行为,我们将尝试以这个访问为pointcut
,来进行织入工作。然后我们来定义Advice
,在Spring AOP
中,Advice
是实现了对应接口的类,如果我们要实现一个前置型的Advice
,就要实现MethodBeforeAdvice
中的方法。在这里我们定义了一个名为BeforeEat
的前置型Advice
,表示吃之前要执行的横切逻辑。
- people 类
public class People {
public void eatFruit(){
System.out.println("正在吃水果");
}
}
- BeforeEat 类
public class BeforeEat implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println("eat方法的前置通知: 我要开始吃了!");
}
}
接下来让我们把这两个类注入到Spring IOC
容器中,交由Spring
管理。
之后最重要的是告诉Spring
,pointcut
是哪些方法?,和pointcut
关联Advice
是哪一个,让我们完善aop config
。
表示pointcut
是People
类的eatFruit
方。之前我们有提到过point
+ advice
= aspect
,而
标签中的就可以理解为aspect
,它关联了与advice
对应的pointcut
。下面让我们调用下People
类的eatFruit()
方法看看是什么效果。
执行前需要先导入aspectJweaver.jar包
- 调用
earFruit()
方法
public static void main(String[] args) {
ApplicationContext applicationContext = new ClassPathXmlApplicationContext("applicationContext.xml");
People people = (People)applicationContext.getBean("people");
people.eatFruit();
}
- 执行结果
eat方法的前置通知: 我要开始吃了!
正在吃水果
可以发现横切逻辑在方法执行前被调用了。
之前我们说过, pointcut
在这里可以看作要被织入横切逻辑的具体位置(方法)的集合,因此pointcut
内部可以包含多种方法,让我们在People
类中添加一个drinkSomething
方法。
public class People {
public void eatFruit(){
System.out.println("正在吃水果");
}
public void drinkSomething(String sth){
System.out.println("正在喝"+sth);
}
}
把这个方法也加入到当前的pointcut
中去。
pointcut
中两个方法用or
连接。运行结果是在这2
个方法调用前都会执行横切逻辑BeforeEat
eat方法的前置通知: 我要开始吃了!
正在吃水果
eat方法的前置通知: 我要开始吃了!
正在喝牛奶
Process finished with exit code 0
可以看到pointcut
中的expression
是支持集合的交并补运算的,此外还支持通配符的方式,来指代一类方法。比如我们可以修改
为:
就表示任何以String
为参数(不限方法名)的方法,在这里也就只有drinkSomething(String sth)
满足条件,尝试运行发现也的确只在这个方法前执行了横切逻辑。通过通配符和集合运算的方式,可以容易的指定一类具体的的方法为pointcut
。
正在吃水果
eat方法的前置通知: 我要开始吃了!
正在喝牛奶
Process finished with exit code 0
现在让我们再回到Advice
类的定义上,看看接口方法中的参数都代表了什么。
public class BeforeEat implements MethodBeforeAdvice {
@Override
public void before(Method method, Object[] objects, Object o) throws Throwable {
System.out.println(method+" " + Arrays.toString(objects) + " " + o);
System.out.println("eat方法的前置通知: 我要开始吃了!");
}
}
执行结果
public void aop.People.drinkSomething(java.lang.String) [牛奶] aop.People@424e1977
eat方法的前置通知: 我要开始吃了!
正在喝牛奶
Process finished with exit code 0
可以发现method
即与横切逻辑advice
关联的具体方法,在这里就是public void aop.People.drinkSomething(java.lang.String)
, Object[] objects
则是传入该方法的参数,object
则是执行横切逻辑的方法所属的对象实例,这里就是IOC
中id=people
的这个bean
。
后置Advice
后置型Advice
与前置型Advice
正相反,表示在pointcut
之后执行横切逻辑。我们编写一个名为AfterEat
的后置型Advice
。
public class AfterEat implements AfterReturningAdvice {
@Override
public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
System.out.println("吃完了,洗洗手。");
}
}
为其编写xml
配置。
执行结果
public void aop.People.eatFruit() [] aop.People@1190200a
eat方法的前置通知: 我要开始吃了!//前置
正在吃水果
吃完了,洗洗手。//后置
public void aop.People.drinkSomething(java.lang.String) [牛奶] aop.People@1190200a
eat方法的前置通知: 我要开始吃了!//前置
正在喝牛奶
吃完了,洗洗手。//后置
注意到AfterReturningAdvice
接口中的afterReturning
方法中的参数与前置Advice
有差别,让我们尝试打印一下。
public class AfterEat implements AfterReturningAdvice {
@Override
public void afterReturning(Object o, Method method, Object[] objects, Object o1) throws Throwable {
System.out.println(method+" " + Arrays.toString(objects) + " " + o + " " + o1);
System.out.println("吃完了,洗洗手。");
}
}
输出结果
public void aop.People.eatFruit() [] null aop.People@1190200
可以看到o1
输出的是对象实例,而o
输出的值是null
, 那么o
代表什么呢?让我们修改drinkSomething(String)
的返回值为int
,再打印一次
public int drinkSomething(String sth){
System.out.println("正在喝"+sth);
return 0;
}
public int aop.People.drinkSomething(java.lang.String) [牛奶] 0 aop.People@1190200a
发现o
的值变为0
,也就是说其代表了横切逻辑执行前这个方法的返回值。
异常Advice
异常Advice
指的是当pointcut
中的方法抛出异常时,将会执行的横切逻辑。
- 编写异常
Advice
public class WhenException implements ThrowsAdvice {
/*
* public void afterThrowing(Exception ex)
* public void afterThrowing(RemoteException)
* public void afterThrowing(Method method, Object[] args, Object target, Exception ex)
* public void afterThrowing(Method method, Object[] args, Object target, ServletException ex)
*/
}
ThrowsAdvice
这个接口并没有要求我们实现任何接口方法,而是在文档里给出了一些示例,还告诉我们Method method, Object[] args, Object target
,这3个打包在一起的参数是可选的,如果你想获得更详细的信息,就加上它们。
- 实现异常
Advice
类
public class WhenException implements ThrowsAdvice {
public void afterThrowing(Exception ex) {
System.out.println("异常Advice : 发生了异常");
System.out.println(ex.getMessage());
}
}
编写app config
再在drinkSomething()
方法里故意引起一个异常。
public int drinkSomething(String sth){
System.out.println("正在喝"+sth);
int a = 1 / 0;
return 0;
}
执行结果
public int aop.People.drinkSomething(java.lang.String) [牛奶] aop.People@4c39bec8
eat方法的前置通知: 我要开始吃了!
正在喝牛奶
异常Advice : 发生了异常
/ by zero
环绕Advice
截至目前为止,我们已经实验了前置
、后置
、异常
三种Advice。它们执行的时机如下。
环绕型
Advice,可以实现以上三种Advice
的所有功能,即可以同时在上述的所有位置执行横切逻辑。
- 实现一个环绕型
Advice
public class AroundEat implements MethodInterceptor {
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
try {
System.out.println("环绕Advice: 方法执行前" );// 前置
Object result = invocation.proceed();// pointcut中方法的执行
System.out.println("环绕Advice: 方法执行后" );// 后置
} catch (Exception e) {
System.out.println("环绕Advice: 发生异常");
}
return null;
}
}
这里的关键是Object result = invocation.proceed();
,这里就相当于执行我们定义在pointcut
中的方法,因此在这行语句前面执行的逻辑,相当于前置advice
,在这行语句后面执行的逻辑,相当于后置advice
。捕捉到异常后实现的逻辑就相当于异常advice
。
为其配置aop
,进行验证。
- 运行结果
环绕Advice: 方法执行前
正在吃水果
环绕Advice: 方法执行后
环绕Advice: 方法执行前
正在喝牛奶
环绕Advice: 发生异常
利用注解的形式实现AOP
Spring AOP
,也提供了基于注解的形式实现AOP
, 较XML
配置的方法更加简单直观,我们来利用注解实现AOP
,以前置Advice
为例,将之前的BeforeEat
改进为基于注解的方式。
@Component("beforeEatAnnotation")
@Aspect
public class BeforeEatAnnotation {
@Before("execution(public void aop.People.eatFruit())") //定义切点
void before(){
System.out.println("采用注解形式实现的前置通知");
}
@AfterReturning("execution(public void aop.People.eatFruit())")
void after(){
System.out.println("采用注解形式实现的后置通知");
}
}
和我们之前基于XML
的配置一样,我们要定义具体的pointcut
并且把其和关联的Advice
绑定起来,在这个类里我们可以在任意方法前加上@Before注解,表示该方法是一个前置advice
,然后在其括号内注明pointcut
,这样pointcut
和advice
很自然的关联在一起了,所以也无需之前的
来指明两者关系了。@Aspect
代表这个类表示一个切面。@Component
把这个类交由Spring
管理,注意配置自动扫描。
最后,我还需要在xml
中配置aop
自动代理。
实验结果
采用注解形式实现的前置通知
正在吃水果
采用注解形式实现的后置通知
Process finished with exit code 0
之前利用接口的方式来实现AOP
可以很容易的获得目标对象,方法名、参数等信息,利用注解的方式也可以实现,这里需要借助一个特殊的JoinPoint
类。
@Component("adviceByAnnotation")
@Aspect
public class AdviceByAnnotation {
@Before("execution(public void aop.People.eatFruit())") //定义切点
void before(JoinPoint joinPoint){
System.out.println(joinPoint.getTarget() + " " + Arrays.toString(joinPoint.getArgs()) + " " + joinPoint.getSignature());
System.out.println("采用注解形式实现的前置通知");
}
@AfterReturning(pointcut="execution(public void aop.People.eatFruit())", returning = "returningValue")
void after(JoinPoint joinPoint, Object returningValue){
System.out.println("返回值为" + returningValue);
System.out.println("采用注解形式实现的后置通知");
}
}
可以发现pointcut
中的特定方法的有关信息都已经被包装到JoinPoint
类中去了。对于以@AfterReturning标注的后置Advice
,还可以指明获取返回值。
实验结果如下
aop.People@140c9f39 [] void aop.People.eatFruit()
采用注解形式实现的前置通知
正在吃水果
返回值为null
采用注解形式实现的后置通知
类似的我们还可以实现基于注解的异常Advice
和环绕Advice
以及最终Advice
。
@After("execution(public int aop.People.drinkSomething(String))")
void after(){
System.out.println("最终通知,无论有没有发生异常,都会执行");
}
//异常通知
@AfterThrowing("execution(public int aop.People.drinkSomething(String))")
void afterException(){
System.out.println("采用注解形式的异常通知");
}
//环绕通知
@Around("execution(public int aop.People.drinkSomething(String))")
void around(ProceedingJoinPoint proceedingJoinPoint) {
try {
System.out.println("采用注解形式的环绕通知[前置]");
proceedingJoinPoint.proceed();
System.out.println("采用注解形式的环绕通知[后置]");
}catch (Throwable e) {
System.out.println("采用注解形式的环绕通知[异常]");
} finally {
System.out.println("采用注解形式的环绕通知[最终]");
}
}
环绕Advice
里,proceedingJoinPoint.proceed();
就是真正执行了pointcut
集合中某个具体方法。注意这里区别最终和后置的区别,后置Advice
如果发生异常则不会被执行,而最终Advice
是一定会被执行的。
执行结果如下
采用注解形式的环绕通知[前置]
正在喝牛奶
采用注解形式的环绕通知[异常]
采用注解形式的环绕通知[最终]