PHP 中的调用切入点和执行切入点之间的区别?

Difference between call and execution pointcuts in PHP?

在AOP in Java (AspectJ) 中,当我们谈到方法切入点时,我们可以将它们区分为两个不同的集合:method call pointcutsmethod execution pointcuts.

基于此处的这些资源:

还有一些AspectJ的背景,我们可以看出两者的区别基本上可以表述为:

鉴于这些 类:

class CallerObject {
      //...
      public void someMethod() {
         CompiletimeTypeObject target = new RuntimeTypeObject();
         target.someMethodOfTarget();
      }
      //...
}

class RuntimeTypeObject extends CompileTypeObject {
    @Override
    public void someMethodOfTarget() {
       super.someMethodOfTarget();
       //...some other stuff
    }
}

class CompiletimeTypeObject {
    public void someMethodOfTarget() {
       //...some stuff
    }
}

所以 method call pointcut 像这样:

pointcut methodCallPointcut(): 
   call(void com.example.CompiletimeTypeObject.someMethodOfTarget())

将匹配 CallerObject.someMethod() 方法内的 target.someMethodOfTarget(); 连接点,因为 RuntimeTypeObject 的 compile type 是 CompiletimeTypeObject,但此方法调用切入点:

pointcut methodCallPointcut(): 
   call(void com.example.RuntimeTypeObject.someMethodOfTarget())

不会匹配,因为对象 (CompiletimeTypeObject) 的编译时类型不是 RuntimeTypeObject 或其子类型(相反)。

因此,这两个方法执行切入点都将匹配 target.someMethodOfTarget(); 执行连接点:

pointcut methodCallPointcut(): 
       execution(void com.example.CompiletimeTypeObject.someMethodOfTarget())

pointcut methodCallPointcut(): 
       execution(void com.example.RuntimeTypeObject.someMethodOfTarget())

由于匹配基于对象的运行时类型,两者都是 RuntimeTypeObject,而 RuntimeTypeObject 既是 CompiletimeTypeObject(第一个切入点)又是 RuntimeTypeObject(第二个切入点)。

现在,由于 PHP 不提供对象的编译时类型(除非使用类型提示以某种方式模拟此行为),区分方法调用和方法执行切入点是否有意义PHP AOP 实现?那么切入点之间有何不同?

感谢关注!

编辑:@kriegaex 指出了 AspectJ 中调用和方法执行切入点之间的另一个有趣方面。

感谢您提供的简洁明了的示例。我也尝试自己举个例子,这是我的理解:

情况A(我使用第3方库)中,我实际上无法拦截库方法的执行,因为库本身已经被编译成字节码和任何关于那个库的方面也已经编织到那个字节码中(我需要编织源代码才能这样做)。

所以我只能拦截对库方法的方法调用,但是在我的代码[=96]中我只能拦截对库方法调用 =] 和 不是从库本身内部调用库方法 因为相同的原则(从库本身内部调用库方法也已经编译)。

这同样适用于系统类(相同的原理),如这里所说(即使引用指的是JBoss):

https://docs.jboss.org/jbossaop/docs/2.0.0.GA/docs/aspect-framework/reference/en/html/pointcuts.html

System classes cannot be used within execution expressions because it is impossible to instrument them.

在情况 B(我为其他用户提供了一个库)中,如果我确实需要在库本身或将来使用该方法的用户代码中拦截我的库的某个方法的使用,那么我需要使用执行切入点,因为切面编织器将编译 关注 我的库 而不是将使用我的库方法的用户代码的方法执行和调用切入点(仅仅是因为我在编写库时用户代码还不存在),因此使用执行切入点将确保编织将发生在内部方法执行(对于一个清晰直观的示例,请查看下面的 @kriegaex 伪代码)而不是在我的库中调用该方法的任何地方(即在调用方)。

因此,无论是在我的库中还是在用户的代码中使用我的库方法时,我都可以拦截该方法的使用(更准确地说,是执行)。 如果我在这种情况下使用了方法调用切入点,我将只会拦截从 我的库中进行的 调用,而不是在用户代码中进行的调用。

不管怎样,还是想想如果这些考虑是有意义的并且可以应用在PHP世界中,你觉得伙计们怎么样?

免责声明:我不会说话PHP,一点也不会。所以我的回答本质上是相当笼统的,而不是特定于 PHP.

AFAIK,PHP 是一种解释型而非编译型语言。因此,区别不在于编译时类型与运行时类型,而是语义上的声明类型与实际类型。我想象基于 PHP 的 AOP 框架不会 "compile" 而是预处理源代码,将额外的(方面)源代码注入原始文件。可能仍然有可能以某种方式将声明的类型与实际类型区分开来。

但是还有另一个重要因素也与 callexecution 连接点之间的差异相关:代码编织的位置。想象一下您使用库或自己提供库的情况。每种给定情况的问题是,在应用方面编织时,源代码的哪些部分在用户的控制之下。

  • 情况 A:您使用第 3 方库:让我们假设您不能(或不想)将方面编织到库中。那么你不能使用execution拦截库方法,但仍然使用call切入点,因为调用代码在你的控制之下。
  • 情况 B:您向其他用户提供了一个库:让我们假设您的库应该使用方面,但库的用户对此一无所知。然后 execution 切入点将始终有效,因为建议已经融入您的库的方法中,无论它们是从外部调用还是从库本身调用。但是 call 只适用于内部调用,因为没有方面代码被编织到用户的调用代码中。

只有当您控制调用和被调用(执行)代码时,使用 callexecution 并没有太大区别。但是等一下,它仍然有区别:execution 只是编织在一个地方,而 call 它可能编织到很多地方,所以 execution 生成的代码量较小。


更新:

根据要求,这是一些伪代码:

让我们假设我们有一个 class MyClass 要进行方面增强(通过源代码插入):

class MyClass {
    method foo() {
        print("foo");
        bar();
    }

    method bar() {
        print("bar");
        zot();
    }

    method zot() {
        print("zot");
    }

    static method main() {
        new McClass().foo();
    }
}

现在如果我们使用 call()

像这样应用 CallAspect
aspect CallAspect {
    before() : call(* *(..)) {
        print("before " + thisJoinPoint);
    }
}

根据我们的代码,源代码编织后看起来像这样:

class MyClass {
    method foo() {
        print("foo");
        print("before call(MyClass.bar())");
        bar();
    }

    method bar() {
        print("bar");
        print("before call(MyClass.zot())");
        zot();
    }

    method zot() {
        print("zot");
    }

    static method main() {
        print("before call(MyClass.foo())");
        new McClass().foo();
    }
}

或者,如果我们使用 execution()

应用这样的 ExecutionAspect
aspect ExecutionAspect {
    before() : execution(* *(..)) {
        print("before " + thisJoinPoint);
    }
}

根据我们的代码,源代码编织后看起来像这样:

class MyClass {
    method foo() {
        print("before execution(MyClass.foo())");
        print("foo");
        bar();
    }

    method bar() {
        print("before execution(MyClass.bar())");
        print("bar");
        zot();
    }

    method zot() {
        print("before execution(MyClass.zot())");
        print("zot");
    }

    static method main() {
        print("before execution(MyClass.main())");
        new McClass().foo();
    }
}

你现在能看出区别了吗?注意where代码被织入和what 打印语句说。

PHP 是动态语言,因此很难实现 call 连接点,因为有许多语言特性,例如 call_user_func_array()$func = 'var_dump'; $func($func);

@kriegaex 写了一个很好的答案,其中包含 callexecution 类型的连接点之间的主要区别。应用于 PHP,目前唯一可能的连接点是 execution 连接点,因为通过用装饰器包装 class 或提供 [=24] 来挂钩方法|函数的执行要容易得多=] 扩展名。

实际上,Go! AOP framework 仅提供 execution 连接点,以及 FLOW3 框架和其他框架。