【问题标题】:Difference between call and execution pointcuts in PHP?PHP中调用和执行切入点的区别?
【发布时间】:2015-02-02 15:54:10
【问题描述】:

在 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
    }
}
  • 方法调用切入点是指调用者对象调用目标方法的方法 /em> 对象(实际实现被调用方法的对象)。在上面的例子中,调用者是CallerObject,目标是RuntimeTypeObject。另外,方法调用切入点是指对象的编译时类型,即上例中的“CompiletimeTypeObject”;

所以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方库)中,我实际上无法拦截库方法的执行,因为库本身已经编译成字节码以及与相关的任何方面该库已经编织到该字节码中(我需要编织源代码才能这样做)。

所以我只能拦截对库方法的方法调用,但我只能在我的代码中拦截对库方法的调用而不是由于相同的原理,从库本身调用库方法(从库本身对库方法的调用也已经编译)。

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

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

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

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

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

不管怎样,如果这些考虑是有意义的并且可以应用于 PHP 世界,你们觉得呢?

【问题讨论】:

    标签: java php aop aspectj


    【解决方案1】:

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

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

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

    • 案例 A:您使用 3rd 方库:假设您不能(或不想)将方面编织到库中。那么你不能使用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();
        }
    }
    

    现在,如果我们使用 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();
        }
    }
    

    你现在能看出区别了吗?请注意代码被编入的位置以及打印语句所说的内容。

    【讨论】:

    • 我只是稍微改写了案例 A 和 B 以使解释更准确。也许这会有所帮助。
    • @user3019105:是的,如果目标方法在第 3 方库中,或者只是在不受切面编织约束的目录中,则只能在使用 call 时拦截其中的方法。为了能够使用execution,必须编织目标方法本身。其实很简单,在AspectJ中试一试即可。
    • 我添加了一些(类似 Java 的)伪代码,显示我们假设的源代码编织器将插入代码中的内容。请检查更新。
    • 是的,他们基本上是对的。但我建议我们现在停止讨论并结束讨论,因为 StackOverflow 不是一个论坛,而是一个解决具体的、与编程相关的问题的平台。如果您喜欢我的回答,请随时接受并投票。否则,祝你实现基于 PHP 的 AOP 框架好运,或者执行web search
    • @kriegaex 关于 CallAspect 模块,之前的建议还选择了 print("") 方法的所有调用,而不仅仅是 foo、bar 和 zot。不过,您可能想将它们称为 cmets。
    【解决方案2】:

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

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

    其实Go! AOP framework只提供executionjoinpoint,以及FLOW3框架等。

    【讨论】:

    • 感谢您对 PHP 的考虑!您在 Go! 中的实现! AOP 真的很有趣。让我问你一个问题,据我所知,Go!自动加载时在类上编织方面,对吗?基本上,您在任何其他spl_autoload_register 之前拦截autoloading 并通过PHP 令牌反射使用静态分析,创建编织类的“代理/装饰”镜像(如果它与方面匹配)规则,当然),然后用一个与原始代理同名的类来扩展这个代理?但是你怎么服从 Liskov 的 SP 呢?
    • @user3019105 是的,关于框架中的编织逻辑和方法拦截技术,您是对的。关于 LSP 的问题非常好。当self 用于类型提示而不是static 时,LSP 可以在极少数情况下被违反,但另一方面,我只是用子类替换原始类,感谢 LSP,这在 99 中有效% 百分比。
    • 感谢static,您允许覆盖父静态方法,但作为 Go!甚至可以拦截最终的类和方法(静态与否)我只能认为您根据其结构在目标上编织,如果它不是最终的,则扩展它,否则您创建它的代理类。但如果是这样,在最后一种情况下,如果我有一个 DI 容器并且我键入提示一个方法,例如someMethod(SomeType $obj),编织之后,SomeTypeProxy会被作为$obj传递吗?这违反了LSP,当然会导致Catchable Fatal Error,那么在这种情况下如何保证与容器的互操作性呢?
    • 去吧! AOP 总是扩展原始类,甚至是最终类 :) 如果类是最终类,那么框架只会从原始类中删除“final”关键字并将其移动到装饰器生成的代码中。所以从用户的角度来看,类仍然是最终的,但它已经具有扩展功能。无SomeTypeProxy 类,无LSP 违规,SomeType 将一直使用。
    • 巧妙!谢谢你的解释!
    猜你喜欢
    • 1970-01-01
    • 2020-05-15
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-11-06
    • 2020-02-19
    • 2017-05-20
    • 1970-01-01
    相关资源
    最近更新 更多