Spring AOP核心概念

链接点 - Joinpoint切点 - Pointcut加强/通知 - Advice切面 - Aspect织入 - Weaving实例总结参考文献html

在上一章节中咱们初步了解 Spring AOP,包括 Spring AOP 的基本概念以及使用,本文将对 AOP 核心概念进行解读。java

链接点 - Joinpoint

链接点是指程序执行过程当中的一些点,好比方法调用,异常处理等。在 Spring AOP 中,仅支持方法级别的链接点。以上是官方说明,通俗地讲就是可以被拦截的地方,每一个成员方法均可以称之为链接点。咱们仍是借用租房的案例作分析。web

Rent 接口:spring

public interface Rent {
    //租房
    public void rent(String address);
    //买家具
    public void buyFurniture();
}
复制代码

该接口的实现类是 Host,即房东类,具体实现以下:express

@Component
public class Host implements Rent {

    @Override
    public void rent(String address) {
        System.out.println("出租位于"+address+"处的房屋");
    }

    @Override
    public void buyFurniture() {
        System.out.println("添置家具");
    }
}
复制代码

其中 rent()方法即为一个链接点。segmentfault

接下来咱们看看链接点的定义:api

public interface JoinPoint {
    String METHOD_EXECUTION = "method-execution";
    String METHOD_CALL = "method-call";
    String CONSTRUCTOR_EXECUTION = "constructor-execution";
    String CONSTRUCTOR_CALL = "constructor-call";
    String FIELD_GET = "field-get";
    String FIELD_SET = "field-set";
    String STATICINITIALIZATION = "staticinitialization";
    String PREINITIALIZATION = "preinitialization";
    String INITIALIZATION = "initialization";
    String EXCEPTION_HANDLER = "exception-handler";
    String SYNCHRONIZATION_LOCK = "lock";
    String SYNCHRONIZATION_UNLOCK = "unlock";
    String ADVICE_EXECUTION = "adviceexecution";

    String toString();

    String toShortString();

    String toLongString();

    //获取代理对象
    Object getThis();

    /**
    返回目标对象。该对象将始终与target切入点指示符匹配的对象相同。除非您特别须要此反射访问,不然应使用        target切入点指示符到达此对象,以得到更好的静态类型和性能。

    若是没有目标对象,则返回null。
    **/

    Object getTarget();

    //获取传入目标方法的参数对象
    Object[] getArgs();

    //获取封装了署名信息的对象,在该对象中能够获取到目标方法名,所属类的Class等信息
    Signature getSignature();

    /**
    返回与链接点对应的源位置。
    若是没有可用的源位置,则返回null。
    返回默认构造函数的定义类的SourceLocation。
    **/

    SourceLocation getSourceLocation();

    String getKind();

    JoinPoint.StaticPart getStaticPart();

    public interface EnclosingStaticPart extends JoinPoint.StaticPart {
    }

    //该帮助对象仅包含有关链接点的静态信息。它能够从JoinPoint.getStaticPart()方法中得到,也可使用特殊形式在建议中单独访问 thisJoinPointStaticPart。
    public interface StaticPart {
        Signature getSignature();

        SourceLocation getSourceLocation();

        String getKind();

        int getId();

        String toString();

        String toShortString();

        String toLongString();
    }
}
复制代码

JoinPoint 接口中经常使用 api 有:getSignature()、 getArgs()、 getTarget() 、 getThis() 。可是咱们平时使用并不直接使用 JoinPoint 的实现类,中间还有一个接口实现,叫作 ProceedingJoinPoint,其定义以下:app

public interface ProceedingJoinPoint extends JoinPoint {
    void set$AroundClosure(AroundClosure var1);

    //执行目标方法
    Object proceed() throws Throwable;
    //传入的新的参数去执行目标方法
    Object proceed(Object[] var1) throws Throwable;
}
复制代码

ProceedingJoinPoint 对象是 JoinPoint 的子接口,该对象只用在@Around 的切面方法中。在该接口中,proceed 方法是核心,该方法用于执行拦截器逻辑。关于拦截器这里说一下,之前置通知拦截器为例,在执行目标方法前,该拦截器首先会执行前置通知逻辑,若是拦截器链中还有其余的拦截器,则继续调用下一个拦截器逻辑。直到拦截器链中没有其余的拦截器后,再去调用目标方法。ide

proceed() 方法的具体实如今 MethodInvocationProceedingJoinPoint 类中,其定义以下:函数

public Object proceed() throws Throwable {
    return this.methodInvocation.invocableClone().proceed();
}

public Object proceed(Object[] arguments) throws Throwable {
    Assert.notNull(arguments, "Argument array passed to proceed cannot be null");
    if (arguments.length != this.methodInvocation.getArguments().length) {
        throw new IllegalArgumentException("Expecting " + this.methodInvocation.getArguments().length + " arguments to proceed, but was passed " + arguments.length + " arguments");
    } else {
        this.methodInvocation.setArguments(arguments);
        return this.methodInvocation.invocableClone(arguments).proceed();
    }
}
复制代码

查看代码可知,arguments 参数被传入到 ProxyMethodInvocation 对象中,并调用自身的 proceed()方法,接着咱们定位到此处进行查看相关代码:

public MethodInvocation invocableClone(Object... arguments) {
    if (this.userAttributes == null) {
        this.userAttributes = new HashMap();
    }

    try {
        ReflectiveMethodInvocation clone = (ReflectiveMethodInvocation)this.clone();
        clone.arguments = arguments;
        return clone;
    } catch (CloneNotSupportedException var3) {
        throw new IllegalStateException("Should be able to clone object of type [" + this.getClass() + "]: " + var3);
    }
}

public Object proceed() throws Throwable {
    if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
        return this.invokeJoinpoint();
    } else {
        Object interceptorOrInterceptionAdvice = this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
        if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
            InterceptorAndDynamicMethodMatcher dm = (InterceptorAndDynamicMethodMatcher)interceptorOrInterceptionAdvice;
            Class<?> targetClass = this.targetClass != null ? this.targetClass : this.method.getDeclaringClass();
            return dm.methodMatcher.matches(this.method, targetClass, this.arguments) ? dm.interceptor.invoke(this) : this.proceed();
        } else {
            return ((MethodInterceptor)interceptorOrInterceptionAdvice).invoke(this);
        }
    }
}
复制代码

如上所示,MethodInvocation 的实现类 ReflectiveMethodInvocation 获取到传入的参数以后,执行 proceed 方法,获取到前置通知拦截器逻辑,而后经过反射进行调用。关于 ReflectiveMethodInvocation 类,又继承自 JoinPoint 接口,因此咱们看一下这些定义之间的继承关系:

关于链接点的相关知识,咱们先了解到这里。有了这些链接点,咱们才能进行一些横切操做,可是在操做以前,咱们须要定位选择链接点,怎么选择的呢?这就是切点 Pointcut 要作的事情了,继续往下看。

切点 - Pointcut

在上述定义的接口中的全部方法均可以认为是 JoinPoint,可是有时咱们并不但愿在全部的方法上都添加 Advice(这个后续会讲到),而 Pointcut 的做用就是提供一组规则(使用 AspectJ pointcut expression language 来描述 )来匹配 JoinPoint,给知足规则的 JoinPoint 添加 Advice。

在上一节中咱们基于 XML 和注解实现了 AOP 功能,总结发现切点的定义也分为两种。当基于 XML 文件时,能够经过在配置文件中进行定义,具体以下:

<!--Spring基于Xml的切面-->
<aop:config>
    <!--定义切点函数-->
    <aop:pointcut id="rentPointCut" expression="execution(* com.msdn.bean.Host.rent())"/>

    <!-- 定义切面 order 定义优先级,值越小优先级越大-->
    <aop:aspect ref="proxy" order="0">
        <!--前置通知-->
        <aop:before method="seeHouse" pointcut-ref="rentPointCut" />
        <!--环绕通知-->
        <aop:around method="getMoney" pointcut-ref="rentPointCut" />
        <!--后置通知-->
        <aop:after method="fare" pointcut-ref="rentPointCut" />
    </aop:aspect>
</aop:config>
复制代码

当基于注解进行配置时,定义切点须要两个步骤:

  1. 定义一个空方法,无需参数,不能有返回值
  2. 使用 @Pointcut 标注,填入切点表达式

代码定义以下:

    //定义一个切入点表达式,用来肯定哪些类须要代理
    @Pointcut("execution(* com.msdn.bean.Host.*(..))")
    public void rentPointCut(){

    }
复制代码

这里你们也都看到了,关于切点的定义要么是经过来定义,又或者使用@Pointcut 来定义,并无其余的地方出现过,可是经过以前在 Spring IoC 自定义标签解析一文能够知道,若是声明了注解,那么就必定会在程序中的某个地方注册了对应的解析器。这里就不从头找起了,咱们先查看一下 Pointcut 定义:

package org.springframework.aop;

public interface Pointcut {
    Pointcut TRUE = TruePointcut.INSTANCE;

    /** 返回一个类型过滤器 */
    ClassFilter getClassFilter();

    /** 返回一个方法匹配器 */
    MethodMatcher getMethodMatcher();
}
复制代码

Pointcut 接口中定义了两个接口,分别用于返回类型过滤器和方法匹配器。用于对定义的切点函数进行解析,关于切点函数的讲解,你们能够阅读Spring AOP : AspectJ Pointcut 切点。下面咱们再来看一下类型过滤器和方法匹配器接口的定义:

@FunctionalInterface
public interface ClassFilter {
    ClassFilter TRUE = TrueClassFilter.INSTANCE;

    boolean matches(Class<?> var1);
}

public interface MethodMatcher {
    MethodMatcher TRUE = TrueMethodMatcher.INSTANCE;

    boolean matches(Method var1, Class<?> var2);

    boolean isRuntime();

    boolean matches(Method var1, Class<?> var2, Object... var3);
}
复制代码

上面的两个接口均定义了 matches 方法,咱们定义的切点函数就是经过 matches 方法进行解析的,而后选择知足规则的链接点。在 Spring 中提供了一个 AspectJ 表达式切点类 AspectJExpressionPointcut,下面咱们来看一下这个类的继承关系:

如上所示,这个类最终实现了 Pointcut、ClassFilter 和 MethodMatcher 接口,其中就包括 matches 方法的实现,具体代码以下:

   public boolean matches(Class<?> targetClass) {
        PointcutExpression pointcutExpression = this.obtainPointcutExpression();

        try {
            try {
                return pointcutExpression.couldMatchJoinPointsInType(targetClass);
            } catch (ReflectionWorldException var5) {
                logger.debug("PointcutExpression matching rejected target class - trying fallback expression", var5);
                PointcutExpression fallbackExpression = this.getFallbackPointcutExpression(targetClass);
                if (fallbackExpression != null) {
                    return fallbackExpression.couldMatchJoinPointsInType(targetClass);
                }
            }
        } catch (Throwable var6) {
            logger.debug("PointcutExpression matching rejected target class", var6);
        }

        return false;
    }
复制代码

经过该方法,对切点函数进行解析,该类也就具有了经过 AspectJ 表达式对链接点进行选择的能力。

经过切点选择出链接点以后,就要进行接下来的处理——通知(Advice)。

加强/通知 - Advice

通知 Advice 即咱们定义的横切逻辑,好比咱们能够定义一个用于监控方法性能的通知,也能够定义一个事务处理的通知等。若是说切点解决了通知在哪里调用的问题,那么如今还须要考虑了一个问题,即通知在什么时候被调用?是在目标方法执行前被调用,仍是在目标方法执行结束后被调用,还在二者兼备呢?Spring 帮咱们解答了这个问题,Spring 中定义了如下几种通知类型:

上面是五种通知的介绍,下面咱们来看一下通知的源码,以下:

package org.aopalliance.aop;

public interface Advice {
}
复制代码

如上,通知接口里好像什么都没定义。不过别慌,咱们再去到它的子类接口中一探究竟。

//前置通知
public interface BeforeAdvice extends Advice {
}

//返回通知
public interface AfterReturningAdvice extends AfterAdvice {
    void afterReturning(@Nullable Object var1, Method var2, Object[] var3, @Nullable Object var4) throws Throwable;
}

//后置通知
public interface AfterAdvice extends Advice {
}

//环绕通知
@FunctionalInterface
public interface MethodInterceptor extends Interceptor {
    Object invoke(MethodInvocation var1) throws Throwable;
}

//异常通知
public interface ThrowsAdvice extends AfterAdvice {
}
复制代码

以上通知的定义很简单,咱们找一下它们的具体实现类,这里先看一下前置通知的实现类 AspectJMethodBeforeAdvice。

public class AspectJMethodBeforeAdvice extends AbstractAspectJAdvice implements MethodBeforeAdviceSerializable {
    public AspectJMethodBeforeAdvice(Method aspectJBeforeAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) {
        super(aspectJBeforeAdviceMethod, pointcut, aif);
    }

    public void before(Method method, Object[] args, @Nullable Object target) throws Throwable {
        this.invokeAdviceMethod(this.getJoinPointMatch(), (Object)null, (Throwable)null);
    }

    public boolean isBeforeAdvice() {
        return true;
    }

    public boolean isAfterAdvice() {
        return false;
    }
}
复制代码

上面的核心代码是 before()方法,用于执行咱们定义的前置通知函数。

因为环绕通知比较重要,因此再来看一下它的具体实现类代码。

public class AspectJAroundAdvice extends AbstractAspectJAdvice implements MethodInterceptorSerializable {
    public AspectJAroundAdvice(Method aspectJAroundAdviceMethod, AspectJExpressionPointcut pointcut, AspectInstanceFactory aif) {
        super(aspectJAroundAdviceMethod, pointcut, aif);
    }

    public boolean isBeforeAdvice() {
        return false;
    }

    public boolean isAfterAdvice() {
        return false;
    }

    protected boolean supportsProceedingJoinPoint() {
        return true;
    }

    public Object invoke(MethodInvocation mi) throws Throwable {
        if (!(mi instanceof ProxyMethodInvocation)) {
            throw new IllegalStateException("MethodInvocation is not a Spring ProxyMethodInvocation: " + mi);
        } else {
            ProxyMethodInvocation pmi = (ProxyMethodInvocation)mi;
            ProceedingJoinPoint pjp = this.lazyGetProceedingJoinPoint(pmi);
            JoinPointMatch jpm = this.getJoinPointMatch(pmi);
            return this.invokeAdviceMethod(pjp, jpm, (Object)null, (Throwable)null);
        }
    }

    protected ProceedingJoinPoint lazyGetProceedingJoinPoint(ProxyMethodInvocation rmi) {
        return new MethodInvocationProceedingJoinPoint(rmi);
    }
}
复制代码

核心代码为 invoke 方法,当执行目标方法时,会首先来到这里,而后再进入到自定义的环绕方法中。

如今咱们有了切点 Pointcut 和通知 Advice,因为这两个模块目前仍是分离的,咱们须要把它们整合在一块儿。这样切点就能够为通知进行导航,而后由通知逻辑实施精确打击。那怎么整合两个模块呢?答案是,切面。好的,是时候来介绍切面 Aspect 这个概念了。

切面 - Aspect

切面 Aspect 整合了切点和通知两个模块,切点解决了 where 问题,通知解决了 when 和 how 问题。切面把二者整合起来,就能够解决 对什么方法(where)在什么时候(when - 前置仍是后置,或者环绕)执行什么样的横切逻辑(how)的三连发问题。在 AOP 中,切面只是一个概念,并无一个具体的接口或类与此对应。

切面类型主要分红了三种

  • 通常切面
  • 切点切面
  • 引介/引入切面

通常切面,切点切面,引介/引入切面介绍:

public interface Advisor {
    Advice EMPTY_ADVICE = new Advice() {
    };

    Advice getAdvice();

    boolean isPerInstance();
}
复制代码

咱们重点看一下 PointcutAdvisor ,关于该接口的定义以下:

public interface PointcutAdvisor extends Advisor {
    Pointcut getPointcut();
}
复制代码

Advisor 中有一个 getAdvice 方法,用于返回通知。PointcutAdvisor 在 Advisor 基础上,新增了 getPointcut 方法,用于返回切点对象。所以 PointcutAdvisor 的实现类便可以返回切点,也能够返回通知,因此说 PointcutAdvisor 和切面的功能类似。因此说 PointcutAdvisor 和切面的功能类似。不过他们之间仍是有一些差别的,好比看下面的配置:

<!--Spring基于Xml的切面-->
    <aop:config>
        <!--定义切点函数-->
        <aop:pointcut id="rentPointCut" expression="execution(* com.msdn.bean.Host.rent())"/>

            <!-- 定义切面 order 定义优先级,值越小优先级越大-->
            <aop:aspect ref="proxy" order="0">
                <!--前置通知-->
                <aop:before method="seeHouse" pointcut-ref="rentPointCut" />
                    <!--环绕通知-->
                    <aop:around method="aroundMethod" pointcut-ref="rentPointCut" />
                        <!--后置通知-->
                        <aop:after method="fare" pointcut-ref="rentPointCut" />
                            </aop:aspect>
                                </aop:config>
复制代码

如上,一个切面中配置了一个切点和三个通知,三个通知均引用了同一个切点,即 pointcut-ref="helloPointcut"。这里在一个切面中,一个切点对应多个通知,是一对多的关系(能够配置多个 pointcut,造成多对多的关系)。而在 PointcutAdvisor 的实现类 AspectJPointcutAdvisor 中,切点和通知是一一对应的关系。

    public AspectJPointcutAdvisor(AbstractAspectJAdvice advice) {
        Assert.notNull(advice, "Advice must not be null");
        this.advice = advice;
        this.pointcut = advice.buildSafePointcut();
    }
复制代码

上面的通知最终会被转换成三个 PointcutAdvisor,这里我把在 AbstractAdvisorAutoProxyCreator 源码调试的结果贴在下面:

织入 - Weaving

织入是把切面应用到目标对象并建立新的代理对象的过程。切面在指定的链接点被织入到目标对象中。在目标对象的生命周期里有不少个点能够织入:

  • 编译期:切面在目标类编译时被织入。这种方式须要特殊的编译器。AspectJ 的织入编译器就是以这种方式织入切面的 。
  • 类加载期:切面在目标类加载到 JVM 时被织入。这种方式须要特殊的类加载器(ClassLoader),它能够在目标类被引入应用以前 加强该目标类的字节码。
  • 运行期:切面在应用运行的某个时刻被织入。通常状况下,在织入切面时,AOP 容器会为目标对象动态地建立一个代理对象。Spring AOP 就是以这种方式织入切面的。

Spring AOP 既然是在目标对象运行期织入切面的,那它是经过什么方式织入的呢?先来讲说以何种方式进行织入,首先仍是从 LoadTimeWeaverAwareProcessor 开始,该类是后置处理器 BeanPostProcessor 的一个实现类,咱们都知道 BeanPostProcessor 有两个核心方法,用于在 bean 初始化以前和以后被调用。具体是在 bean 对象初始化完成后,Spring经过切点对 bean 类中的方法进行匹配。若匹配成功,则会为该 bean 生成代理对象,并将代理对象返回给容器。容器向后置处理器输入 bean 对象,获得 bean 对象的代理,这样就完成了织入过程。 关于后置处理器的细节,这里就很少说了,你们如有兴趣,能够参考以前写的Spring之BeanFactoryPostProcessor和BeanPostProcessor

实例

结合上述分析的内容,咱们对上一章节中的案例进行扩展,修改切面定义。

@Order(0)
@Aspect
@Component
public class JoinPointDemo {

    //定义一个切入点表达式,用来肯定哪些类须要代理
    @Pointcut("execution(* com.msdn.bean.Host.*(..))")
    public void rentPointCut(){
    }

    /**
     * 前置方法,在目标方法执行前执行
     * @param joinPoint 封装了代理方法信息的对象,若用不到则能够忽略不写
     */

    @Before("rentPointCut()")
    public void seeHouse(JoinPoint joinPoint){

        Signature oo = joinPoint.getSignature();
        System.out.println("前置方法准备执行......");
        System.out.println("目标方法名为:" + joinPoint.getSignature().getName());
        System.out.println("目标方法所属类的简单类名:" +        joinPoint.getSignature().getDeclaringType().getSimpleName());
        System.out.println("目标方法所属类的类名:" + joinPoint.getSignature().getDeclaringTypeName());
        System.out.println("目标方法声明类型:" + Modifier.toString(joinPoint.getSignature().getModifiers()));

        //获取传入目标方法的参数
        Object[] args = joinPoint.getArgs();
        for (int i = 0; i < args.length; i++) {
            System.out.println("第" + (i+1) + "个参数为:" + args[i]);
        }

        System.out.println("被代理的对象:" + joinPoint.getTarget());
        System.out.println("代理对象本身:" + joinPoint.getThis());
        System.out.println("前置方法执行结束......");
    }

    /**
     * 环绕方法,可自定义目标方法执行的时机
     * @param point
     * @return  该方法须要返回值,返回值视为目标方法的返回值
     */

    @Around("rentPointCut()")
    public Object aroundMethod(ProceedingJoinPoint point){
        Object result = null;

        try {
            System.out.println("目标方法执行前...");
            //获取目标方法的参数,判断执行哪一个proceed方法
            Object[] args = point.getArgs();
            if (args.length > 0){
                //用新的参数值执行目标方法
                result = point.proceed(new Object[]{"上海市黄浦区中华路某某公寓19楼2号"});
            }else{
                //执行目标方法
                result = point.proceed();
            }
        } catch (Throwable e) {
            //异常通知
            System.out.println("执行目标方法异常后...");
            throw new RuntimeException(e);
        }
        System.out.println("目标方法执行后...");
        return result;
    }

    @After("rentPointCut()")
    public void fare(JoinPoint joinPoint){
        System.out.println("执行后置方法");
    }

}
复制代码

配置文件以下:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:aop="http://www.springframework.org/schema/aop"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd
       http://www.springframework.org/schema/aop
       http://www.springframework.org/schema/aop/spring-aop.xsd">


    <context:component-scan base-package="com.msdn.bean,com.msdn.aop" />
    <aop:aspectj-autoproxy />

</beans>
复制代码

测试代码为:

@Test
public void aopTest(){
    ApplicationContext context = new ClassPathXmlApplicationContext("spring-aop.xml");

    Object host = context.getBean("host");
    Rent o = (Rent) host;
    o.rent("新东方");
    o.buyFurniture();
}
复制代码

执行结果为:

目标方法执行前...
前置方法准备执行......
目标方法名为:rent
目标方法所属类的简单类名:Rent
目标方法所属类的类名:com.msdn.bean.Rent
目标方法声明类型:public abstract
1个参数为:上海市黄浦区中华路某某公寓192
被代理的对象:com.msdn.bean.Host@524d6d96
代理对象本身:com.msdn.bean.Host@524d6d96
前置方法执行结束......
出租位于上海市黄浦区中华路某某公寓192号处的房屋
目标方法执行后...
执行后置方法
********************
目标方法执行前...
前置方法准备执行......
目标方法名为:buyFurniture
目标方法所属类的简单类名:Rent
目标方法所属类的类名:com.msdn.bean.Rent
目标方法声明类型:public abstract
被代理的对象:com.msdn.bean.Host@524d6d96
代理对象本身:com.msdn.bean.Host@524d6d96
前置方法执行结束......
添置家具
目标方法执行后...
执行后置方法
复制代码

总结

前面三篇文章只是在介绍 Spring AOP 的原理和基本使用,从本文开始准备深刻学习 AOP,其中就先了解 AOP 的核心概念,对于后续源码的学习很是有必要。若是文中有什么不对的地方,欢迎指正。

参考文献

SpringAop中JoinPoint对象的使用方法

《Spring实战(第4版)》高清PDF

Spring AOP 源码分析系列文章导读

相关文章
相关标签/搜索