面向切面编程(AOP)是针对面向对象编程(OOP)的补充,可以非侵入式的为多个不具有继承关系的对象引入相同的公共行为例如日志、安全、事务、性能监控等等。SpringAOP允许将公共行为从业务逻辑中抽离出来,并将这些行为以一种非侵入的方式织入到所有需要的业务逻辑中,相较于OOP纵向模式的业务逻辑实现,其关注的方向是横向的切面。
从Spring2.0开始,引入AspectJ注释来对POJO进行标注,支持通过切点函数、逻辑运算符、通配符等高级功能来对切点进行灵活的定义,结合各种类型的通知来形成强大的连接点描述能力。
下面先给出一个实现例子,然后介绍SpringAOP的基本概念并通过源码分析其实现原理。
第二部分 基本概念介绍
如上一章例子所示,当我们需要为业务逻辑定义公共的切面逻辑时,我们首先需要定义一个切点Pointcut,切点代表了一个关于目标函数的过滤规则,后续的通知是基于切点来跟目标函数关联起来的。
然后我们要围绕该切点定义一系列的通知Advice,示例中使用@Before、@After、@AfterReturning、@AfterThrowing、@Around等等定义的方法都是通知。其含义是在切点定义的函数执行之前、完成之后、正常返回之后、抛出异常之后以及环绕前后执行对应的切面逻辑。
一个切点和针对该切点的一个通知共同构成了一个切面Advisor。对于一个方法,我们可以定义多个切点都隐含它,并且对于每个切点都可定义多个通知来形成多个切面,SpringAOP底层框架会保证在该方法调用时候将所有符合条件的切面都切入到其执行之前或之后或环绕。通知Advice的子类Interceptor或MethodInterceptor的类名更具体一些,包含了拦截器的概念。
SpringAOP使用运行时连接点Joinpoint的概念将切面切入到调用方法中,一个运行时连接点就是对于一个可访问对象的访问过程的具体化,可能其子类Invocation或MethodInvocation的类名会更加具体一些。在实际调用中运行时连接点包括了被调用方法、被调用对象、适用于该方法的拦截器链等等信息。
执行的过程类似于FilterChain,先正向执行拦截器链的前置逻辑,然后调用method,接着反向执行拦截器链的后置逻辑,最后返回结果。SpringAOP逻辑上的流程和概念就是这些,下面将详细介绍其中涉及到的每一个概念。
1 切点Pointcut
在第一部分的示例代码中:
@Pointcut("execution(public * aopnew.service.MyTestService2.doSomething*(..))")
public void doSomethingPointcut(){};
@Pointcut("@annotation(aopnew.annotation.TestTimer)")
public void timerPointcut(){};
@Pointcut("@within(aopnew.annotation.TestLogger)")
public void recordLogPointcut(){};
都是用于定义一个切点,注释Pointcut中的value值就是切入点指示符,SpringAOP提供的这种匹配表达式是用于计算哪些方法符合该切点的定义。Pointcut接口如下所示:
public interface Pointcut {
ClassFilter getClassFilter();
MethodMatcher getMethodMatcher();
Pointcut TRUE = TruePointcut.INSTANCE;
}
其中定义了两个抽象方法:获得类过滤器和获得方法匹配器。意思很明确,就是可以通过类过滤及方法过滤,来定义对目标函数的过滤规则。各子类可以指定具体的过滤器来实现不同的过滤过则。
Spring2.0中增加了AspectJExpressionPointcut来支持AspectJ关于切点定义的表达式语法。其中定义了支持的各种类型的切点函数,并支持通配符和逻辑表达式。
1.1 原生切点函数
原生切点函数就是我们在示例中定义切点时使用的execution、@annotation、@within等函数,在AspectJExpressionPointcut中定义了支持的各种类型的原生切点函数:
private static final Set<PointcutPrimitive> SUPPORTED_PRIMITIVES = new HashSet<PointcutPrimitive>();
static {
SUPPORTED_PRIMITIVES.add(PointcutPrimitive.EXECUTION);
SUPPORTED_PRIMITIVES.add(PointcutPrimitive.ARGS);
SUPPORTED_PRIMITIVES.add(PointcutPrimitive.REFERENCE);
SUPPORTED_PRIMITIVES.add(PointcutPrimitive.THIS);
SUPPORTED_PRIMITIVES.add(PointcutPrimitive.TARGET);
SUPPORTED_PRIMITIVES.add(PointcutPrimitive.WITHIN);
SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_ANNOTATION);
SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_WITHIN);
SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_ARGS);
SUPPORTED_PRIMITIVES.add(PointcutPrimitive.AT_TARGET);
}
下面介绍其中几个比较常用的原生切点函数:
类型原生切点函数within
针对类型(全限定名)的过滤方法,语法格式如下:
within(<typeName>)
typeName表示类或接口的全限定名,支持使用通配符,例如:
/**
*匹配aopnew.service包中所有以MyTestService开头的类中的所有方法
*/
@Pointcut("within(aopnew.service.MyTestService*)")
/**
*匹配所有实现anpnew.interface.IUserService接口的类的所有方法
*/
@Pointcut("within(anpnew.interface.IUserService+)")
方法原生切点函数execution
针对方法签名进行过滤,语法表达式如下:
/**
*scope:表示方法作用域,例如:public, private, protect
*return-type:表示返回类型
*fully-qualified-class-name:表示类的完全限定名
*method-name:表示方法名
*parameters:表示参数
*/
execution(<scope> <return-type> <fully-qualified-class-name><method-name>(<parameters>))
对于给定的作用域、返回值类型、完全限定类名、方法名以及参数匹配的方法将会应用切点函数指定的通知,例如:
/**
*匹配作用域为public,所在类全限定名为aopnew.service.MyTestService,方法名以doSomething开头的所有方法
*
*/
@Pointcut("execution(public * aopnew.service.MyTestService.doSomething*(..))")
类注释原生切点函数@within
用于匹配标注了指定注释的类型内的所有方法,与within是有区别的,within是用于匹配指定类型内的方法执行;语法如下:
@within(<annotationName>)
annotationName表示注释类的全限定名,支持使用通配符,例如:
/**
*匹配标注了TestLogger的类中的所有方法
*/
@Pointcut("@within(aopnew.annotation.TestLogger)")
方法注释原生切点函数@annotation
用于匹配所有标注了指定注释的方法,语法如下:
@annotation(<annotationName>)
annotationName表示注释类的全限定名,支持使用通配符,例如:
/**
*匹配所有标注了TestTimer的方法
*/
@Pointcut("@annotation(aopnew.annotation.TestTimer)")
更多原生切点函数的用法请参考相关技术文档。
1.2 通配符
上述的原生切点函数中都支持通配符,在示例中我们看到了很多如 * , .. , +等,它们的含义如下:
.. :匹配方法定义中的任意数量的参数,此外还匹配类定义中的任意数量包,例如:
/**
*匹配aopnew包及子包中的类名为MyTestService2中的以doSomething开头并且作用域为public的所有方法
*/
@Pointcut("execution(public * aopnew..MyTestService2.doSomething*(..))")
+ :匹配给定类的任意子类,例如:
/**
*匹配所有实现anpnew.interface.IUserService接口的类的所有方法
*/
@Pointcut("within(anpnew.interface.IUserService+)")
* :匹配任意数量的字符,例如:
/**
*匹配aopnew.service包中任意类中的所有方法
*/
@Pointcut("within(aopnew.service.*)")
1.3 逻辑表达式
切点指示符可以使用运算符语法进行表达式的混编,如and、or、not(或&&、||、!),例如:
/**
*匹配类上标注了TestLogger并且方法上标注了TestTimer的所有方法
*/
@Pointcut("@within(aopnew.annotation.TestLogger) && @annotation(aopnew.annotation.TestTimer)")
综上所述,结合使用原生切点函数、通配符及逻辑表达式,可以形成非常强大的表述能力,我们可以定义出非常复杂的切点表达式。
2 通知Advice
通知Advice描述了当符合某切点的方法调用时,在调用过程的哪个时机执行哪样的切面逻辑。Spring2.0引入了AspectJ的通知类型,主要分5种,分别是前置通知@Before、后置通知@AfterReturn、异常通知@AfterThrowing、最终通知@After以及环绕通知@Around。
单单解释通知Advice可能不是很直观,其子类拦截器Interceptor可能更直观更容易理解一些。AspectJ各个不同的通知注释最终会解析并构建成为不同类型的拦截器,它们的作用就是拦截方法并在方法调用的不同时机执行拦截器定义的切入逻辑。
下面分别介绍这五种通知:
前置通知@Before
前置通知通过@Before注解进行标注,可直接传入切点表达式的值也可以传入@Pointcut定义的切点函数名。该通知在目标函数执行前执行,其中传递的参数JoinPoint是运行时对方法调用过程的一个具体化,是一个运行时动态的概念,内部包含了被调用方法、方法所在的对象及拦截器链等信息,我们将在下面详细讲解。示例代码如下:
@Before("doSomethingPointcut()")
public void cuth(JoinPoint pjp) throws Throwable{
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod(); //获取被拦截的方法
String methodName = method.getName(); //获取被拦截的方法名
logger.info("权限认证:调用方法为:{}", methodName);
};
后置通知@AfterReturning
通过@AfterReturning注解进行标注,该函数在目标函数执行完成后执行,并可以获取到目标函数最终的返回值returnVal,当目标函数没有返回值时,returnVal将返回null,必须通过returning = “returnVal”注明参数的名称而且必须与通知函数的参数名称相同。请注意,在任何通知中这些参数都是可选的,需要使用时直接填写即可,不需要使用时,可以完全不用声明出来。示例代码如下:
@AfterReturning(value = "doSomethingPointcut()", returning = "returnVal")
public void logNormal(JoinPoint pjp, Object returnVal) throws Throwable{
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod(); //获取被拦截的方法
String methodName = method.getName(); //获取被拦截的方法名
logger.info("正常返回记日志:调用方法为:{};返回结果为:{}", methodName, returnVal);
};
异常通知 @AfterThrowing
该通知只有在异常时才会被触发,并由throwing来声明一个接收异常信息的变量,同样异常通知也拥有Joinpoint参数,需要时加上即可,示例代码如下:
@AfterThrowing(value = "doSomethingPointcut()", throwing = "e")
public void logThrowing(JoinPoint pjp, Throwable e) throws Throwable{
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod(); //获取被拦截的方法
String methodName = method.getName(); //获取被拦截的方法名
logger.info("抛出异常记日志:调用方法为:{};异常信息为:{}", methodName, e.getMessage());
};
最终通知 @After
该通知有点类似于finally代码块,只要应用了无论什么情况下都会执行。示例代码如下:
@After(value = "doSomethingPointcut()")
public void afterall(JoinPoint pjp) throws Throwable{
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod(); //获取被拦截的方法
String methodName = method.getName(); //获取被拦截的方法名
logger.info("方法调用完成:调用方法为:{}", methodName);
}
环绕通知@Around
环绕通知既可以在目标方法前执行也可在目标方法之后执行,更重要的是环绕通知可以控制目标方法是否执行。第一个参数必须是ProceedingJoinPoint,通过该对象的proceed()方法来传递拦截器(通知)链或执行函数,proceed()的返回值就是环绕通知的返回值。同样的,ProceedingJoinPoint是运行时对方法调用过程的一个具体化,是一个运行时动态的概念,内部包含了被调用方法、方法所在的对象及拦截器链等信息,并且其相较于JoinPoint增加了proceed函数用于传递拦截器链或执行函数。
@Around("doSomethingPointcut()")
public Object timer(ProceedingJoinPoint pjp) throws Throwable{
long beginTime = System.currentTimeMillis();
MethodSignature signature = (MethodSignature) pjp.getSignature();
Method method = signature.getMethod(); //获取被拦截的方法
String methodName = method.getName(); //获取被拦截的方法名
logger.info("计时切面:请求开始,方法:{}", methodName);
Object result = null;
try {
// 一切正常的情况下,继续执行被拦截的方法
result = pjp.proceed();
} catch (Throwable e) {
logger.info("exception: ", e);
}
long endTime = System.currentTimeMillis();
logger.info("计时切面:请求结束,方法:{},执行时间:{}", methodName, (endTime-beginTime));
return result;
}
通知的继承路径为:
Advice<-Interceptor<-MethodInterceptor
其中MethodInterceptor的接口定义如下:
public interface MethodInterceptor extends Interceptor {
Object invoke(MethodInvocation invocation) throws Throwable;
}
也就是说方法拦截器的子类都需要实现一个方法,接受MethodInvocation类型的参数invocation。MethodInvocation顾名思义是动态概念方法调用的具体化,其本质上是一个运行时连接点JoinPoint,我们将在下面详细介绍。
MethodInterceptor子类在invoke方法中执行自己的业务逻辑并调用invocation.proceed()来传递拦截器调用链。例如:
Object invoke(MethodInvocation invocation) throws Throwable{
...do something before method invocation...
Object obj = null;
try{
obj = invocation.proceed();
}catch(Throwable e){
...do something after throwing...
}finally{
...do something after method invoke...
}
...do something after method return...
return obj;
}
上面的示例显示出了拦截器可以在方法调用的各个时机执行切入业务的大体实现,而前面的五种通知本质上都是上述代码的一个变种。
3 切面Advisor
切面代表符合某切点的方法在调用的某一个时机执行某切入逻辑的一个综合概念。其结合了通知Advice和判断是否应用该通知(例如通过Pointcut)的概念。先看Advisor的接口定义:
public interface Advisor {
Advice getAdvice();
boolean isPerInstance();
}
其中isPerInstance函数代表该通知是使用在某个特定实例上还是适用于被切入的类的所有实例上。
然后,我们重点使用的是PointcutAdvisor,先看其接口定义:
public interface PointcutAdvisor extends Advisor {
Pointcut getPointcut();
}
可以看到PointcutAdvisor是结合了切点Pointcut和通知Advice的概念。PointcutAdvisor是SpringAOP根据我们定义的@Aspect类中的切点定义和通知定义来自动生成的。项目启动时,Spring先收集beanFactory中所有的@Aspect定义的切面类,将其内部定义的Pointcut和Advice封装成Advisor,然后在每一个bean创建的时,判断其是否有适用的Advisor,如果有就为其生成代理,并将适用的advisors设置进去。
切面Advisor不是一个面向用户使用的接口,而是由SpringAOP底层使用的一个接口。其代表了一个逻辑上的“切面”。
4 运行时连接点JoinPoint
当符合某切点条件的函数在被执行时,就产生了一个运行时连接点Joinpoint的概念。运行时连接点代表了一个在静态连接点(程序中的某个位置)上发生的事件。例如:一次调用就是一个对于方法(静态连接点)的运行时连接点。
在基于拦截器框架的上下文中,一个运行时连接点就是对于一个可访问对象的访问过程的具体化。Joinpoint接口定义如下:
public interface Joinpoint {
Object proceed() throws Throwable;
Object getThis();
AccessibleObject getStaticPart();
}
如上所述Joinpoint代表了运行时连接点,也就是代表了方法调用过程的具体化。因此是一个动态的概念,getThis()就是返回这个运行时连接点的动态部分(如方法所在的对象实例),而getStaticPart()就用于返回对应的静态连接点的信息(如方法定义本身)。
另外,proceed()用于执行本运行时连接点的拦截器链上的下一个拦截器。由此可知,运行时连接点中除了维护被调用方法,方法所在的对象实例外还应该维护定义于该方法的所有拦截器(通知)。Joinpoint接口的继承链为:
Joinpoint<-Invocation<-MethodInvocation<-ProxyMethodInvocation
从子类的名称上会更容易理解,运行时连接点更侧重的是描述一个调用的过程。其实现类为ReflectiveMethodInvocation,该类中维护的属性如下:
protected final Object proxy;
protected final Object target;
protected final Method method;
protected Object[] arguments;
private final Class<?> targetClass;
/**
* Lazily initialized map of user-specific attributes for this invocation.
*/
private Map<String, Object> userAttributes;
/**
* List of MethodInterceptor and InterceptorAndDynamicMethodMatcher
* that need dynamic checks.
*/
protected final List<?> interceptorsAndDynamicMethodMatchers;
/**
* Index from 0 of the current interceptor we're invoking.
* -1 until we invoke: then the current interceptor.
*/
private int currentInterceptorIndex = -1;
其中interceptorsAndDynamicMethodMatchers就是我们上面所说的拦截器链,ReflectiveMethodInvocation的proceed方法如下所示:
@Override
public Object proceed() throws Throwable {
// We start with an index of -1 and increment early.
if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
return invokeJoinpoint();
}
Object interceptorOrInterceptionAdvice =
this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
// Evaluate dynamic method matcher here: static part will already have
// been evaluated and found to match.
InterceptorAndDynamicMethodMatcher dm =
(InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) {
return dm.interceptor.invoke(this);
}
else {
// Dynamic matching failed.
// Skip this interceptor and invoke the next in the chain.
return proceed();
}
}
else {
// It's an interceptor, so we just invoke it: The pointcut will have
// been evaluated statically before this object was constructed.
return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
}
}
结合前面对于MethodIntercepter子类中invoke函数的实现,运行时连接点中拦截器链的调用方式如下:
- 如果拦截器链尚未执行完,就执行拦截器链上的下一个拦截器并将this(本动态连接点)传递过去
- 每一个拦截器的拦截函数中都执行自己的前置逻辑并调用invocation.proceed()重复步骤1
- 档拦截器链执行完毕,则执行方法调用,并返回结果
- 在返回的过程中,按照前面调用顺序的反向顺序执行方法调用的后置逻辑,也就是在invocation.proceed()之后编写的逻辑
- 拦截器链反向执行完成后,最终返回结果。
由此可得,一个定义了切面的方法调用过程如下所示:
interceptor1.before()
interceptor2.before()
......
interceptorn.before()
method.invoke()
interceptorn.aft()
......
interceptor2.aft()
interceptor1.aft()
@Before定义的通知(拦截器)只有before()逻辑;@After、@AfterReturning、@AfterThrowing定义的通知(拦截器)只有after()逻辑;@Around定义的通知(拦截器)可以自己来定义before()和after()逻辑。
小结:本篇介绍了SpringAOP的基本概念及整体设计思路,搞清除其中的几个核心概念如切点、通知、切面、连接点等等对我们了解SpringAOP的整体框架非常重要。下一篇我们将在此基础上通过源码分析其具体的实现原理。