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

ton*_*nix 1 php java aop aspectj

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

基于 SO 上的这些资源:

以及一些 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
    }
}
Run Code Online (Sandbox Code Playgroud)
  • 方法调用切入点呼叫的方法的从呼叫者它调用一个方法对象目标对象(这实际上实现该方法的一个被调用)。在上面的例子中,调用者是CallerObject,目标是RuntimeTypeObject。此外,方法调用切入点是指对象的编译时类型,即上例中的“CompiletimeTypeObject”;

所以method call pointcut像这样:

pointcut methodCallPointcut(): 
   call(void com.example.CompiletimeTypeObject.someMethodOfTarget())
Run Code Online (Sandbox Code Playgroud)

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

pointcut methodCallPointcut(): 
   call(void com.example.RuntimeTypeObject.someMethodOfTarget())
Run Code Online (Sandbox Code Playgroud)

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

  • 方法执行切入点执行的方法的(即的方法被调用或右之前该方法调用返回)。它不提供有关调用者的信息,更重要的是它指的是对象的运行时类型而不是编译时类型。

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

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

pointcut methodCallPointcut(): 
       execution(void com.example.RuntimeTypeObject.someMethodOfTarget())
Run Code Online (Sandbox Code Playgroud)

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

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

感谢关注!

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

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

情况A(我使用第三方库),我其实不能拦截库法的执行,因为库本身已经被编译成字节码,并就任何方面该库已经编织成字节码太多(我需要编织来源,以便这样做)。

所以我只能拦截方法调用的库方法,但同样我只能拦截的电话到库方法在我的代码,并没有从库本身库方法的调用,因为同样的原理(调用库方法从库本身内部也已经编译)。

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

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

系统类不能在执行表达式中使用,因为无法检测它们。

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

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

无论如何,仍然认为这些考虑是否有意义并且可以应用于 PHP 世界,你们怎么看?

kri*_*aex 5

免责声明:我不会说 PHP,甚至不会说一点。所以我的回答本质上是比较笼统的,而不是特定于 PHP 的。

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

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

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

仅当您控制调用以及被调用(执行)代码时,使用callexecution. 但是等一下,它仍然有区别:execution只是在一个地方编织,而call它可能编织到许多地方,因此生成的代码量较小execution


更新:

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

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

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

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

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

    static method main() {
        new McClass().foo();
    }
}
Run Code Online (Sandbox Code Playgroud)

现在,如果我们应用CallAspect这样的使用call()

aspect CallAspect {
    before() : call(* *(..)) {
        print("before " + thisJoinPoint);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

或者,如果我们应用ExecutionAspect这样的使用execution()

aspect ExecutionAspect {
    before() : execution(* *(..)) {
        print("before " + thisJoinPoint);
    }
}
Run Code Online (Sandbox Code Playgroud)

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

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();
    }
}
Run Code Online (Sandbox Code Playgroud)

你现在能看出区别了吗?要注意其中的代码被编织成和什么样的打印语句说。