当前位置:首页 > CMS教程 > Thinkphp > 列表

ThinkPHP5 框架引入 Go AOP,PHP AOP编程项目详解

发布:smiling 来源: PHP粉丝网  添加日期:2022-03-06 11:32:23 浏览: 评论:0 

这篇文章主要介绍了ThinkPHP5 框架引入 Go AOP,PHP AOP编程,结合具体项目项目分析了ThinkPHP5 引入 Go AOP,PHP AOP编程相关概念、原理、操作技巧与注意事项,需要的朋友可以参考下。

本文实例讲述了ThinkPHP5 框架引入 Go AOP,PHP AOP编程,分享给大家供大家参考,具体如下:

项目背景

目前开发的WEB软件里有这一个功能,PHP访问API操作数据仓库,刚开始数据仓库小,没发现问题,随着数据越来越多,调用API时常超时(60s),于是决定采用异步请求,改为60s能返回数据则返回,不能则返回一个异步ID,然后轮询是否完成统计任务,由于项目紧,人手不足,必须以最小的代价解决当前问题。

方案选择

重新分析需求,并改进代码

采用AOP方式改动程序

从新做需求分析,以及详细设计,并改动代码,需要产品,架构,前端,后端的支持。会惊动的人过多,在资源紧张的情况下是不推荐的。

采用AOP方式,不改动原有代码逻辑,只需要后端就能完成大部分任务了。后端用AOP切入请求API的方法,通过监听API返回的结果来控制是否让其继续运行原有的逻辑(API在60s返回了数据),或者是进入离线任务功能(API报告统计任务不能在60s内完成)。

之前用过AOP-PHP拓展,上手很简单,不过后来在某一个大项目中引入该拓展后,直接爆了out of memory,然后就研究其源码发现,它改变了语法树,并Hook了每个被调用的方法,也就是每个方法被调用是都会去询问AOP-PHP,这个方法有没有切面方法。所以效率损失是比较大的。而且这个项目距离现在已经有8年没更新了。所以不推荐该解决方案。

实际环境

Debian,php-fpm-7.0,ThinkPHP-5.10。

引入AOP

作为一门zui好的语言,PHP是不自带AOP的。那就得安装AOP-PHP拓展,当我打开pecl要下载时,傻眼了,全是bate版,没有显示说明支持php7。但我还是抱着侥幸心理,找到了git,发现4-5年没更新了,要不要等一波更新,哦,作者在issue里说了有时间就开始兼容php7。

好吧,狠话不多说,下一个方案:Go!AOP.看了下git,作者是个穿白体恤,喜欢山峰的大帅哥,基本每个issue都会很热心回复。

composer require goaop/framework

ThinkPHP5 对composer兼容挺不错的哦,(到后面,我真想揍ThinkPHP5作者)这就装好了,怎么用啊,git上的提示了简单用法。我也就照着写了个去切入controller。

  1. <?PHP 
  2. namespace app\tests\controller; 
  3.  
  4. use think\Controller; 
  5.  
  6. class Test1 extends Controller 
  7.  public function test1() 
  8.  { 
  9.   echo $this->aspectAction(); 
  10.  } 
  11.    
  12.  public function aspectAction() 
  13.  { 
  14.   return 'hello'
  15.  } 

定义aspect

  1. <?PHP 
  2. namespace app\tests\aspect; 
  3.  
  4. use Go\Aop\Aspect; 
  5. use Go\Aop\Intercept\FieldAccess; 
  6. use Go\Aop\Intercept\MethodInvocation; 
  7. use Go\Lang\Annotation\After; 
  8. use Go\Lang\Annotation\Before; 
  9. use Go\Lang\Annotation\Around; 
  10. use Go\Lang\Annotation\Pointcut; 
  11.  
  12. use app\tests\controller\Test1; 
  13.  
  14. class MonitorAspect implements Aspect 
  15.  
  16.  /** 
  17.   * Method that will be called before real method 
  18.   * 
  19.   * @param MethodInvocation $invocation Invocation 
  20.   * @Before("execution(public|protected app\tests\controller\Test1->aspectAction(*))") 
  21.   */ 
  22.  public function beforeMethodExecution(MethodInvocation $invocation
  23.  { 
  24.   $obj = $invocation->getThis(); 
  25.   echo 'Calling Before Interceptor for method: '
  26.     is_object($obj) ? get_class($obj) : $obj
  27.     $invocation->getMethod()->isStatic() ? '::' : '->'
  28.     $invocation->getMethod()->getName(), 
  29.     '()'
  30.     ' with arguments: '
  31.     json_encode($invocation->getArguments()), 
  32.     "<br>\n"
  33.  } 

启用aspect

  1. <?PHP 
  2. // file: ./application/tests/service/ApplicationAspectKernel.php 
  3.  
  4. namespace app\tests\service; 
  5.  
  6. use Go\Core\AspectKernel; 
  7. use Go\Core\AspectContainer; 
  8.  
  9. use app\tests\aspect\MonitorAspect; 
  10.  
  11. /** 
  12.  * Application Aspect Kernel 
  13.  * 
  14.  * Class ApplicationAspectKernel 
  15.  * @package app\tests\service 
  16.  */ 
  17. class ApplicationAspectKernel extends AspectKernel 
  18.  
  19.  /** 
  20.   * Configure an AspectContainer with advisors, aspects and pointcuts 
  21.   * 
  22.   * @param AspectContainer $container 
  23.   * 
  24.   * @return void 
  25.   */ 
  26.  protected function configureAop(AspectContainer $container
  27.  { 
  28.   $container->registerAspect(new MonitorAspect()); 
  29.  } 

go-aop 核心服务配置

  1. <?PHP 
  2. // file: ./application/tests/behavior/Bootstrap.php 
  3. namespace app\tests\behavior; 
  4.  
  5. use think\Exception; 
  6. use Composer\Autoload\ClassLoader; 
  7. use Go\Instrument\Transformer\FilterInjectorTransformer; 
  8. use Go\Instrument\ClassLoading\AopComposerLoader; 
  9. use Doctrine\Common\Annotations\AnnotationRegistry; 
  10.  
  11. use app\tests\service\ApplicationAspectKernel; 
  12. use app\tests\ThinkPhpLoaderWrapper; 
  13.  
  14. class Bootstrap 
  15.  public function moduleInit(&$params
  16.  { 
  17.   $applicationAspectKernel = ApplicationAspectKernel::getInstance(); 
  18.   $applicationAspectKernel->init([ 
  19.    'debug' => true, 
  20.    'appDir' => __DIR__ . './../../../'
  21.     'cacheDir' => __DIR__ . './../../../runtime/aop_cache'
  22.     'includePaths' => [ 
  23.      __DIR__ . './../../tests/controller'
  24.      __DIR__ . './../../../thinkphp/library/think/model' 
  25.     ], 
  26.     'excludePaths' => [ 
  27.      __DIR__ . './../../aspect'
  28.     ] 
  29.    ]); 
  30.   return $params
  31.  } 

配置模块init钩子,让其启动 go-aop

  1. <?PHP 
  2. // file: ./application/tests/tags.php 
  3. // 由于是thinkphp5.10 没有容器,所有需要在module下的tags.php文件里配置调用他 
  4.  
  5. return [ 
  6.  // 应用初始化 
  7.  'app_init'  => [], 
  8.  // 应用开始 
  9.  'app_begin' => [], 
  10.  // 模块初始化 
  11.  'module_init' => [ 
  12.   'app\\tests\\behavior\\Bootstrap' 
  13.  ], 
  14.  // 操作开始执行 
  15.  'action_begin' => [], 
  16.  // 视图内容过滤 
  17.  'view_filter' => [], 
  18.  // 日志写入 
  19.  'log_write' => [], 
  20.  // 应用结束 
  21.  'app_end'  => [], 
  22. ]; 

兼容测试

好了,访问 http://127.0.0.1/tests/test1/test1 显示:

hello

这不是预期的效果,在aspect定义了,访问该方法前,会输出方法的更多信息信息。

像如下内容才是预期

Calling Before Interceptor for method: app\tests\controller\Test1->aspectAction() with arguments: []

上他官方Doc看看,是一些更高级的用法。没有讲go-aop的运行机制。

上git上也没看到类似issue,额,发现作者经常在issue里回复:试一试demo。也许我该试试demo。

Run Demos

我采用的是LNMP技术栈。

假设这里有台Ubuntu你已经配置好了LNMP环境

下载代码

配置nginx

  1. # file: /usr/share/etc/nginx/conf.d/go-aop-test.conf 
  2. server { 
  3.  listen 8008; 
  4. # listen 443 ssl; 
  5.  server_name 0.0.0.0; 
  6.  root "/usr/share/nginx/html/app/vendor/lisachenko/go-aop-php/demos"
  7.  index index.html index.htm index.php; 
  8.  charset utf-8; 
  9.  
  10.  access_log /var/log/nginx/go-aop-access.log; 
  11.  error_log /var/log/nginx/go-aop-error.log notice; 
  12.  
  13.  sendfile off; 
  14.  client_max_body_size 100m; 
  15.  
  16.  location ~ \.php(.*)$ { 
  17.   include       fastcgi_params; 
  18.   fastcgi_pass      127.0.0.1:9000; 
  19.   fastcgi_index      index.php; 
  20.  
  21.   fastcgi_param      PATH_INFO  $fastcgi_path_info
  22. #  fastcgi_param     SCRIPT_FILENAME /var/www/html/app/vendor/lisachenko/go-aop-php/demos$fastcgi_script_name; #docker的配置 
  23.   fastcgi_param      SCRIPT_FILENAME /usr/share/nginx/html/api/vendor/lisachenko/go-aop-php/demos$fastcgi_script_name
  24.   fastcgi_param      PATH_TRANSLATED $document_root$fastcgi_path_info
  25.   fastcgi_split_path_info   ((?U).+\.php)(/?.+)$; 
  26.  } 

接下来要调整下代码

访问 http://127.0.0.1:8008 试试,(估计大家都遇到了这个)

ThinkPHP5 框架引入 Go AOP,PHP AOP编程项目详解

这个报错信息提示找不到这个类。来到报错的文件里。这文件使用了use找不到类,就是autoload出问题了,看到 vendor/lisachenko/go-aop-php/demos/autoload.php 这个文件。

  1. <?PHP 
  2. ··· 
  3. if (file_exists(__DIR__ . '/../vendor/autoload.php')) { 
  4.  /** @var Composer\Autoload\ClassLoader $loader */ 
  5.  $loader = include __DIR__ . '/../vendor/autoload.php'
  6.  $loader->add('Demo', __DIR__); 

可以看到这个代码第一行没找到vendor下的autoload,我们做如下调整

  1. <?PHP 
  2. $re = __DIR__ . '/../../../vendor/autoload.php'
  3. if (file_exists(__DIR__ . '/../../../autoload.php')) { 
  4.  /** @var Composer\Autoload\ClassLoader $loader */ 
  5.  $loader = include __DIR__ . '/../../../autoload.php'
  6.  $loader->add('Demo', __DIR__); 

再试试,demo运行起来了。

ThinkPHP5 框架引入 Go AOP,PHP AOP编程项目详解

尝试了下,运行成功

ThinkPHP5 框架引入 Go AOP,PHP AOP编程项目详解

通过以上的输出,可以得出demo里是对方法运行前成功捕获,为什么在thinkphp的controller里运行就不成功呢。我决定采用断点进行调试。

通过断点我发现了这个文件

  1. <?PHP 
  2. // file: ./vendor/lisachenko/go-aop-php/src/Instrument/ClassLoading/AopComposerLoader.php 
  3.  
  4. public function loadClass($class
  5.  if ($file = $this->original->findFile($class)) { 
  6.   $isInternal = false; 
  7.   foreach ($this->internalNamespaces as $ns) { 
  8.    if (strpos($class$ns) === 0) { 
  9.     $isInternal = true; 
  10.     break
  11.    } 
  12.   } 
  13.  
  14.   include ($isInternal ? $file : FilterInjectorTransformer::rewrite($file)); 
  15.  } 

这是一个autoload,每个类的载入都会经过它,并且会对其判断是否为内部类,不是的都会进入后续的操作。通过断点进入 FilterInjectorTransformer,发现会对load的文件进行语法解析,并根据注册的annotation对相关的类生成proxy类。说道这,大家就明白了go-aop是如何做到切入你的程序了吧,生成的proxy类,可以在你配置的cache-dir(我配置的是./runtime/aop_cache/)里看到。

同时./runtime/aop_cache/ 文件夹下也生成了很多东西,通过查看aop_cache文件内产生了与Test1文件名相同的文件,打开文件,发现它代理了原有的Test1控制器。这一系列信息,可以得出,Go!AOP 通过"劫持" composer autoload 让每个类都进过它,根据aspect的定义来决定是否为其创建一个代理类,并植入advice。

额,ThinkPHP5是把composer autoload里的东西copy出来,放到自己autoload里,然后就没composer啥事了。然后go-aop一直等不到composer autoload下发的命令,自然就不能起作用了,so,下一步

改进ThinkPHP5

在ThinkPHP5里,默认有且只会注册一个TP5内部的 Loader,并不会把include请求下发给composer的autoload。所以,为其让go-aop起作用,那么必须让让include class的请求经过 AopComposerLoad.

我们看看这个文件

  1. <?PHP 
  2. // ./vendor/lisachenko/go-aop-php/src/Instrument/ClassLoading/AopComposerLoader.php:57 
  3.  
  4. public static function init() 
  5.  $loaders = spl_autoload_functions(); 
  6.  
  7.  foreach ($loaders as &$loader) { 
  8.   $loaderToUnregister = $loader
  9.   if (is_array($loader) && ($loader[0] instanceof ClassLoader)) { 
  10.    $originalLoader = $loader[0]; 
  11.  
  12.    // Configure library loader for doctrine annotation loader 
  13.    AnnotationRegistry::registerLoader(function ($classuse ($originalLoader) { 
  14.     $originalLoader->loadClass($class); 
  15.  
  16.     return class_exists($class, false); 
  17.    }); 
  18.    $loader[0] = new AopComposerLoader($loader[0]); 
  19.   } 
  20.   spl_autoload_unregister($loaderToUnregister); 
  21.  } 
  22.  unset($loader); 
  23.  
  24.  foreach ($loaders as $loader) { 
  25.   spl_autoload_register($loader); 
  26.  } 

这个文件里有个类型检测,检测autoload callback是否为Classloader类型,然而ThinkPHP5不是,通过断点你会发现ThinkPHP5是一个字符串数组,so,这里也就无法把go-aop注册到class loader的callback当中了。

这里就要提一下PHP autoload机制了,这是现代PHP非常重要的一个功能,它让我们在用到一个类时,通过名字能自动加载文件。我们通过定义一定的类名规则与文件结构目录,再加上能实现以上规则的函数就能实现自动加载了。在通过 spl_autoload_register 函数的第三个参数 prepend 设置为true,就能让其排在在TP5的loader前面,先一步被调用。

依照如上原理,就可以做如下改进

这个是为go-aop包装的新autoload,本质上是在原来的ThinkPHP5的loader上加了一个壳而已。

  1. <?PHP 
  2. // file: ./application/tests  
  3.  
  4. namespace app\tests; 
  5.  
  6. require_once __DIR__ . './../../vendor/composer/ClassLoader.php'
  7.  
  8. use think\Loader; 
  9. use \Composer\Autoload\ClassLoader; 
  10. use Go\Instrument\Transformer\FilterInjectorTransformer; 
  11. use Go\Instrument\ClassLoading\AopComposerLoader; 
  12. use Doctrine\Common\Annotations\AnnotationRegistry; 
  13.  
  14.  
  15. class ThinkPhpLoaderWrapper extends ClassLoader 
  16.  static protected $thinkLoader = Loader::class
  17.  
  18.  /** 
  19.   * Autoload a class by it's name 
  20.   */ 
  21.  public function loadClass($class
  22.  { 
  23.   return Loader::autoload($class); 
  24.  } 
  25.  
  26.  /** 
  27.   * {@inheritDoc} 
  28.   */ 
  29.  public function findFile($class
  30.  { 
  31.   $allowedNamespace = [ 
  32.    'app\tests\controller' 
  33.   ]; 
  34.   $isAllowed = false; 
  35.   foreach ($allowedNamespace as $ns) { 
  36.    if (strpos($class$ns) === 0) { 
  37.     $isAllowed = true; 
  38.     break
  39.    } 
  40.   } 
  41.   // 不允许被AOP的类,则不进入AopComposer 
  42.   if(!$isAllowed
  43.    return false; 
  44.     
  45.   $obj = new Loader; 
  46.   $observer = new \ReflectionClass(Loader::class); 
  47.  
  48.   $method = $observer->getMethod('findFile'); 
  49.   $method->setAccessible(true); 
  50.   $file = $method->invoke($obj$class); 
  51.   return $file
  52.  } 
  53.  
  54. <?PHP 
  55. // file: ./application/tests/behavior/Bootstrap.php 在刚刚我们新添加的文件当中 
  56. // 这个方法 \app\tests\behavior\Bootstrap::moduleInit 的后面追加如下内容 
  57.  
  58. // 组成AOPComposerAutoLoader 
  59. $originalLoader = $thinkLoader = new ThinkPhpLoaderWrapper(); 
  60. AnnotationRegistry::registerLoader(function ($classuse ($originalLoader) { 
  61.  $originalLoader->loadClass($class); 
  62.  
  63.  return class_exists($class, false); 
  64. }); 
  65. $aopLoader = new AopComposerLoader($thinkLoader); 
  66. spl_autoload_register([$aopLoader'loadClass'], false, true); 
  67.  
  68. return $params

在这里我们做了一个autload 并直接把它插入到了最前面(如果项目内还有其他autloader,请注意他们的先后顺序)。

最后

现在我们再访问一下 http://127.0.0.1/tests/test1/test1 你就能看到来自 aspect 输出的信息了。

最后我们做个总结:

PHP7 目前没有拓展实现的 AOP。

ThinkPHP5 有着自己的 Autoloader。

Go!AOP 的AOP实现依赖Class Autoload的 callback,通过替换原文件指向Proxy类实现。

ThinkPHP5 整合 Go!AOP 需要调整 autoload。

Tags: ThinkPHP5AOP PHPAOP

分享到: