011-Spring aop 002-核心说明-切点PointCut通知Advice切面Advisor

Posted 木子旭

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了011-Spring aop 002-核心说明-切点PointCut通知Advice切面Advisor相关的知识,希望对你有一定的参考价值。

一、概述

  切点Pointcut,切点代表了一个关于目标函数的过滤规则,后续的通知是基于切点来跟目标函数关联起来的。

  然后要围绕该切点定义一系列的通知Advice,如@Before、@After、@AfterReturning、@AfterThrowing、@Around等等定义的方法都是通知。其含义是在切点定义的函数执行之前、完成之后、正常返回之后、抛出异常之后以及环绕前后执行对应的切面逻辑。

  一个切点和针对该切点的一个通知共同构成了一个切面Advisor。对于一个方法,我们可以定义多个切点都隐含它,并且对于每个切点都可定义多个通知来形成多个切面,SpringAOP底层框架会保证在该方法调用时候将所有符合条件的切面都切入到其执行之前或之后或环绕。通知Advice的子类Interceptor或MethodInterceptor的类名更具体一些,包含了拦截器的概念。

  SpringAOP使用运行时连接点Joinpoint的概念将切面切入到调用方法中,一个运行时连接点就是对于一个可访问对象的访问过程的具体化,可能其子类Invocation或MethodInvocation的类名会更加具体一些。在实际调用中运行时连接点包括了被调用方法、被调用对象、适用于该方法的拦截器链等等信息。

  执行的过程类似于FilterChain,先正向执行拦截器链的前置逻辑,然后调用method,接着反向执行拦截器链的后置逻辑,最后返回结果。

1.1、切点PointCut

上例中

    @Pointcut("execution(public * com.github.bjlhx15.springaop.service.MyTestService.doSomething1*(..))")
    public void doSomethingPointcut(){};

    @Pointcut("@annotation(com.github.bjlhx15.springaop.anno.TestTimer)")
    public void timerPointcut(){};

    @Pointcut("@within(com.github.bjlhx15.springaop.anno.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.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);
    }

其中:PointcutPrimitive 是来自于aspectj 的 参数。

常用的原生切点函数:

1、类型原生切点函数within

针对类型(全限定名)的过滤方法,语法格式如下:within(<typeName>);typeName表示类或接口的全限定名,支持使用通配符,例如:

/**
*匹配aopnew.service包中所有以MyTestService开头的类中的所有方法
*/
@Pointcut("within(aopnew.service.MyTestService*)")
 
/**
*匹配所有实现anpnew.interface.IUserService接口的类的所有方法
*/
@Pointcut("within(anpnew.interface.IUserService+)")

2、方法原生切点函数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*(..))")

3、类注释原生切点函数@within

用于匹配标注了指定注释的类型内的所有方法,与within是有区别的,within是用于匹配指定类型内的方法执行;语法如下:@within(<annotationName>)

annotationName表示注释类的全限定名,支持使用通配符,例如:

/**
*匹配标注了TestLogger的类中的所有方法
*/
@Pointcut("@within(aopnew.annotation.TestLogger)")

4、方法注释原生切点函数@annotation

用于匹配所有标注了指定注解的方法,语法如下:@annotation(<annotationName>)

annotationName表示注释类的全限定名,支持使用通配符,例如:

/** *匹配所有标注了TestTimer的方法 */ @Pointcut("@annotation(aopnew.annotation.TestTimer)")

1.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.1.3、逻辑表达式

切点指示符可以使用运算符语法进行表达式的混编,如and、or、not(或&&、||、!),例如:

/**
*匹配类上标注了TestLogger并且方法上标注了TestTimer的所有方法
*/
@Pointcut("@within(aopnew.annotation.TestLogger) && @annotation(aopnew.annotation.TestTimer)")

1.2、通知Advice

  通知Advice描述了当符合某切点的方法调用时,在调用过程的哪个时机执行哪样的切面逻辑。Spring2.0引入了AspectJ的通知类型,主要分5种,分别是前置通知@Before、后置通知@AfterReturn、异常通知@AfterThrowing、最终通知@After以及环绕通知@Around。

  单单解释通知Advice可能不是很直观,其子类拦截器Interceptor可能更直观更容易理解一些。AspectJ各个不同的通知注释最终会解析并构建成为不同类型的拦截器,它们的作用就是拦截方法并在方法调用的不同时机执行拦截器定义的切入逻辑。

1、前置通知@Before

  前置通知通过@Before注解进行标注,可直接传入切点表达式的值也可以传入@Pointcut定义的切点函数名。该通知在目标函数执行前执行,其中传递的参数JoinPoint是运行时对方法调用过程的一个具体化,是一个运行时动态的概念,内部包含了被调用方法、方法所在的对象及拦截器链等信息。

2、后置通知@AfterReturning 

  通过@AfterReturning注解进行标注,该函数在目标函数执行完成后执行,并可以获取到目标函数最终的返回值returnVal,当目标函数没有返回值时,returnVal将返回null,必须通过returning = “returnVal”注明参数的名称而且必须与通知函数的参数名称相同。

  请注意,在任何通知中这些参数都是可选的,需要使用时直接填写即可,不需要使用时,可以完全不用声明出来。

3、异常通知 @AfterThrowing

  该通知只有在异常时才会被触发,并由throwing来声明一个接收异常信息的变量,同样异常通知也拥有Joinpoint参数,需要时加上即可

4、最终通知 @After

  该通知有点类似于finally代码块,只要应用了无论什么情况下都会执行。

5、环绕通知@Around 

  环绕通知既可以在目标方法前执行也可在目标方法之后执行,更重要的是环绕通知可以控制目标方法是否指向执行。第一个参数必须是ProceedingJoinPoint,通过该对象的proceed()方法来传递拦截器(通知)链或执行函数,proceed()的返回值就是环绕通知的返回值。

  同样的,ProceedingJoinPoint是运行时对方法调用过程的一个具体化,是一个运行时动态的概念,内部包含了被调用方法、方法所在的对象及拦截器链等信息,并且其相较于JoinPoint增加了proceed函数用于传递拦截器链或执行函数。

1.2.1、说明

  通知的继承路径为: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;
}

  上面的示例显示出了拦截器可以在方法调用的各个时机执行切入业务的大体实现,而前面的五种通知本质上都是上述代码的一个变种。

1.3、切面Advisor

  当符合某切点条件的函数在被执行时,就产生了一个运行时连接点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;
View Code

其中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);
        }
    }
View Code

结合前面对于MethodIntercepter子类中invoke函数的实现,运行时连接点中拦截器链的调用方式如下:

  1、如果拦截器链尚未执行完,就执行拦截器链上的下一个拦截器并将this(本动态连接点)传递过去

  2、每一个拦截器的拦截函数中都执行自己的前置逻辑并调用invocation.proceed()重复步骤1

  3、档拦截器链执行完毕,则执行方法调用,并返回结果

  4、在返回的过程中,按照前面调用顺序的反向顺序执行方法调用的后置逻辑,也就是在invocation.proceed()之后编写的逻辑

  5、拦截器链反向执行完成后,最终返回结果。

由此可得,一个定义了切面的方法调用过程如下所示:

interceptor1.before()
interceptor2.before()
......
interceptorn.before()
method.invoke()
interceptorn.aft()
......
interceptor2.aft()
interceptor1.aft()

  @Before定义的通知(拦截器)只有before()逻辑;@After、@AfterReturning、@AfterThrowing定义的通知(拦截器)只有after()逻辑;@Around定义的通知(拦截器)可以自己来定义before()和after()逻辑。

 

 

以上是关于011-Spring aop 002-核心说明-切点PointCut通知Advice切面Advisor的主要内容,如果未能解决你的问题,请参考以下文章

Spring Boot -- Spring AOP原理及简单实现

Spring AOP之切面的配置

Spring AOP

细聊AOP理论

SpringAOP的应用实例与总结

Spring里面的切面(aop)重要吗?还有Spring里面主要用到了哪几种注入方式