Spring AOP(五)切入点和通知

本文主要描述 Spring AOP 中的 PointcutAdvice 接口。

我们从 ProxyFactory 类开始说起,先来看一个简单的 Demo。

public class ProxyFactoryDemo {

    public static void main(String[] args) {
        // 1. 构造目标对象
        Cat catTarget = new Cat();

        // 2. 通过目标对象,构造 ProxyFactory 对象
        ProxyFactory factory = new ProxyFactory(catTarget);

        // 添加一个方法拦截器
        factory.addAdvice(new MyMethodInterceptor());

        // 3. 根据目标对象生成代理对象
        Object proxy = factory.getProxy();

        Animal cat = (Animal) proxy;
        cat.eat();
    }

    public static class MyMethodInterceptor implements MethodInterceptor {

        @Override
        public Object invoke(MethodInvocation invocation) throws Throwable {
            System.out.println("MyMethodInterceptor invoke 调用 before invocation.proceed");

            Object ret = invocation.proceed();

            System.out.println("MyMethodInterceptor invoke 调用 after invocation.proceed");
            return ret;
        }
    }
}

运行上面的 main 方法,结果如下所示。

输出结果.jpg

源码分析

首先,我们先来看一下 ProxyFactory 类的 Object target 参数构造函数。

public ProxyFactory(Object target) {
  // 1. 设置目标对象,在创建代理对象以及调用代理方法时会用到
  setTarget(target);
  // 2. 设置代理类需要实现的接口,也就是目标类实现的所有接口和其父接口
  setInterfaces(ClassUtils.getAllInterfaces(target));
}

在讨论 addAdvice 方法之前,我们先来看一下 org.aopalliance.intercept.MethodInterceptor 的 UML 类图。

MethodInterceptor UML 关系.jpg

注意org.aopalliance.intercept.MethodInterceptor 不是 Spring AOP (三) CGLIB 动态代理 中的 net.sf.cglib.proxy.MethodInterceptor 接口,但是和 Spring AOP (四) 多重代理和责任链模式 中定义的 MyMethodInterceptor 接口类似。

然后我们来看一下 addAdvice 的方法内部实现。

public void addAdvice(Advice advice) throws AopConfigException {
  int pos = this.advisors.size();
  addAdvice(pos, advice);
}

public void addAdvice(int pos, Advice advice) throws AopConfigException {
  Assert.notNull(advice, "Advice must not be null");
  // 添加一个 DefaultPointcutAdvisor 到 Advisor 集合
  addAdvisor(pos, new DefaultPointcutAdvisor(advice));
}

DefaultPointcutAdvisor UML 类图如下所示。

PointcutAdvisor UML 关系.jpg

简单介绍下上图中出现接口和类:

  • Advisor:持有 org.aopalliance.aop.Advice 对象的基础接口,可以简单的看作是 org.aopalliance.aop.Advice 在 Spring 中的等价接口。

  • PointcutAdvisor:包含切入点的 Advisor,从而可以针对符合 Pointcut 规则的连接点进行增强处理。

  • Ordered:用来确定当前 Advisor 在拦截器责任链列表中的位置,主要用在 Aspect 中。

切入点 API

Spring 的 Pointcut 模型使切入点独立于 Advisor(或者说是 Advice) 类型。您可以使用相同的切入点来对方法做不能的增强处理。

org.springframework.aop.Pointcut 接口是核心接口,用来将 Advice 应用到特定的类和方法。完整的接口如下:

package org.springframework.aop;

/**
 * 一个切入点由 ClassFilter 和 MethodMatcher 组成。
 */
public interface Pointcut {

  ClassFilter getClassFilter();

  MethodMatcher getMethodMatcher();
}

Pointcut 接口拆分为两部分允许重用 ClassFilterMethodMatcher 部分以及细粒度合成操作(可以查看 UnionClassFilterIntersectionClassFilterUnionMethodMatcherIntersectionMethodMatcher 等类的实现细节)。

ClassFilter 接口用于将切入点限制为给定的一组目标类。如果 matches() 方法始终返回 true,则匹配所有目标类。以下清单显示了 ClassFilter 接口定义:

package org.springframework.aop;

public interface ClassFilter {

  /**
   * 切入点是否应用于给定的接口或目标类?
   * @param clazz 目标类
   * @return 该 Pointcut 关联的 Advice 是否需要适用于给定的目标类
   */
  boolean matches(Class<?> clazz);

}

MethodMatcher 接口通常更重要。完整的接口清单如下所示:

package org.springframework.aop;

/**
 * 检查 Pointcut 关联的 Advice 是否需要适用于给定的目标方法
 */
public interface MethodMatcher {

  /**
   * 对方法进行静态匹配,即不对方法调用的传入的实参进行校验
   */
  boolean matches(Method method, Class<?> targetClass);

  /**
   * 返回当前 MethodMatcher 是否需要进行动态匹配。
   *   如果 isRuntime() 方法返回 true,则表示需要调用 matches(Method, Class, Object[])方法对目标方法进行匹配
   */
  boolean isRuntime();

  /**
   * 对方法进行动态匹配,即对方法调用的传入的实参进行校验
   */
  boolean matches(Method method, Class<?> targetClass, Object... args);

}

matches(Method, Class) 方法用于测试此切入点是否与目标类上的给定方法匹配。

如果双参数 matches 方法返回 true,并且 MethodMatcherisRuntime() 方法返回 true,则在每次方法调用时都会调用三参数 matches(Method, Class, Object[]) 方法。这使得切入点可以在执行目标 Advice 做增强处理之前通过传递给方法调用的参数对方法进行匹配。

大多数 MethodMatcher 实现是静态的,这意味着它们的 isRuntime() 方法返回 false。在这种情况下,matches 永远不会调用三参数方法。

Advisor API

在 Spring 中,Advisor 是一个切面,它包含与切入点表达式关联的单个 Advice 对象。

除了介绍的特殊情况,任何 Advisor 都可以使用任何 Adviceorg.springframework.aop.support.DefaultPointcutAdvisor 是最常用的 Advisor 类。它可以与使用MethodInterceptorBeforeAdviceThrowsAdvice

可以在同一个 AOP 代理中混合 Spring 中的 AdvisorAdvice 类型。例如,您可以在一个代理配置中使用拦截建议,抛出建议和建议之前。Spring 自动创建必要的拦截链。

我们继续来看 DefaultPointcutAdvisor 类的构造方法代码清单。

// TruePointcut 对象,表示匹配所有类的所有方法,即对所有方法进行增强处理
private Pointcut pointcut = Pointcut.TRUE;

public DefaultPointcutAdvisor(Advice advice) {
  this(Pointcut.TRUE, advice);
}

public DefaultPointcutAdvisor(Pointcut pointcut, Advice advice) {
  this.pointcut = pointcut;
  setAdvice(advice);
}

由此我们可以得知,通过 factory.addAdvice 添加的 Advice 会对目标对象的所有方法进行增强处理。

我们修改 Animal 接口,为其增加 go 方法定义,并在 Cat 类中实现 go 方法,代码清单如下所示。

public interface Animal {
    void eat();
    void go();
}

public class Cat implements Animal {
    @Override
    public void eat() {
        System.out.println("猫吃鱼");
    }

    @Override
    public void go() {
        System.out.println("猫在跑");
    }
}

然后我们在 ProxyFactoryDemo 类的 main 方法中,新增对其 go 方法的调用逻辑。

public static void main(String[] args) {
  // 1. 构造目标对象
  Animal catTarget = new Cat();

  // 2. 通过目标对象,构造 ProxyFactory 对象
  ProxyFactory factory = new ProxyFactory(catTarget);

  // 添加一个方法拦截器
  factory.addAdvice(new MyMethodInterceptor());

  // 3. 根据目标对象生成代理对象
  Object proxy = factory.getProxy();

  Animal cat = (Animal) proxy;
  System.out.println(cat.getClass());
  cat.eat();

  System.out.println("---------------------------------------");

  cat.go();
}

运行 ProxyFactoryDemo 类的 main 方法,我们可以得到如下图所示的打印结果。

输出结果.jpg

接下来,我们定义一个 Pointcut 接口的实现类 MyPointcut,代码如下所示。

public class MyPointcut implements Pointcut {
    @Override
    public ClassFilter getClassFilter() {
        return new ClassFilter() {
            @Override
            public boolean matches(Class<?> clazz) {
                // 匹配所有的类
                return true;
            }
        };
    }

    @Override
    public MethodMatcher getMethodMatcher() {
        // 继承 StaticMethodMatcher,忽略方法实参,只对方法进行动态匹配。
        return new StaticMethodMatcher() {
            @Override
            public boolean matches(Method method, Class<?> targetClass) {
                // 如果方法名称是 go,则匹配,否则不匹配
                if (method.getName().equals("go")) {
                    return true;
                }
                return false;
            }
        };
    }
}

然后定义一个 PointcutAdvisor 接口的实现类 MyPointcutAdvisor,代码清单如下所示。

public class MyPointcutAdvisor implements PointcutAdvisor {

    private Pointcut pointcut = new MyPointcut();

    private Advice advice;

    public MyPointcutAdvisor(Advice advice) {
        this.advice = advice;
    }

    @Override
    public Pointcut getPointcut() {
        return this.pointcut;
    }

    @Override
    public Advice getAdvice() {
        return this.advice;
    }

    /**
     * 此方法暂时忽略,不需要理会
     */
    @Override
    public boolean isPerInstance() {
        return false;
    }
}

然后我们修改 ProxyFactoryDemo 类的 main 方法中的逻辑,修改后的方法如下所示。

public static void main(String[] args) {
  // 1. 构造目标对象
  Animal catTarget = new Cat();

  // 2. 通过目标对象,构造 ProxyFactory 对象
  ProxyFactory factory = new ProxyFactory(catTarget);

  // 添加一个 Advice (DefaultPointcutAdvisor)
  factory.addAdvice(new MyMethodInterceptor());

  // 新增代码:添加一个 PointcutAdvisor
  MyPointcutAdvisor myPointcutAdvisor = new MyPointcutAdvisor(new MyMethodInterceptor());
  factory.addAdvisor(myPointcutAdvisor);

  // 3. 根据目标对象生成代理对象
  Object proxy = factory.getProxy();

  Animal cat = (Animal) proxy;
  System.out.println(cat.getClass());
  cat.eat();

  System.out.println("---------------------------------------");

  cat.go();
}

运行 ProxyFactoryDemo 类的 main 方法,我们可以得到如下图所示的打印结果。

输出结果.jpg

由上图可知,eat 方法被增强了一次,而 go 方法被增强了两次,说明我们自定义的切入点 MyPointcut 已经生效。

至此,PointcutAdviceAdvisor) 以及 PointcutAdvisor 的整体架构脉络我们就都清楚了。

参考文献

(正文完)

本文所用代码地址

扩展阅读

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 212,383评论 6 493
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 90,522评论 3 385
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 157,852评论 0 348
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 56,621评论 1 284
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 65,741评论 6 386
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 49,929评论 1 290
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 39,076评论 3 410
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 37,803评论 0 268
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 44,265评论 1 303
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 36,582评论 2 327
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 38,716评论 1 341
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 34,395评论 4 333
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 40,039评论 3 316
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,798评论 0 21
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 32,027评论 1 266
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 46,488评论 2 361
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 43,612评论 2 350