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


Posted in PHP onMay 12, 2020

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

项目背景

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

方案选择

  1. 重新分析需求,并改进代码
  2. 采用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。

<?PHP
namespace app\tests\controller;

use think\Controller;

class Test1 extends Controller
{
 public function test1()
 {
  echo $this->aspectAction();
 }
 
 public function aspectAction()
 {
  return 'hello';
 }
}

定义aspect

<?PHP
namespace app\tests\aspect;

use Go\Aop\Aspect;
use Go\Aop\Intercept\FieldAccess;
use Go\Aop\Intercept\MethodInvocation;
use Go\Lang\Annotation\After;
use Go\Lang\Annotation\Before;
use Go\Lang\Annotation\Around;
use Go\Lang\Annotation\Pointcut;

use app\tests\controller\Test1;

class MonitorAspect implements Aspect
{

 /**
  * Method that will be called before real method
  *
  * @param MethodInvocation $invocation Invocation
  * @Before("execution(public|protected app\tests\controller\Test1->aspectAction(*))")
  */
 public function beforeMethodExecution(MethodInvocation $invocation)
 {
  $obj = $invocation->getThis();
  echo 'Calling Before Interceptor for method: ',
    is_object($obj) ? get_class($obj) : $obj,
    $invocation->getMethod()->isStatic() ? '::' : '->',
    $invocation->getMethod()->getName(),
    '()',
    ' with arguments: ',
    json_encode($invocation->getArguments()),
    "<br>\n";
 }
}

启用aspect

<?PHP
// file: ./application/tests/service/ApplicationAspectKernel.php

namespace app\tests\service;

use Go\Core\AspectKernel;
use Go\Core\AspectContainer;

use app\tests\aspect\MonitorAspect;

/**
 * Application Aspect Kernel
 *
 * Class ApplicationAspectKernel
 * @package app\tests\service
 */
class ApplicationAspectKernel extends AspectKernel
{

 /**
  * Configure an AspectContainer with advisors, aspects and pointcuts
  *
  * @param AspectContainer $container
  *
  * @return void
  */
 protected function configureAop(AspectContainer $container)
 {
  $container->registerAspect(new MonitorAspect());
 }
}

go-aop 核心服务配置

<?PHP
// file: ./application/tests/behavior/Bootstrap.php
namespace app\tests\behavior;

use think\Exception;
use Composer\Autoload\ClassLoader;
use Go\Instrument\Transformer\FilterInjectorTransformer;
use Go\Instrument\ClassLoading\AopComposerLoader;
use Doctrine\Common\Annotations\AnnotationRegistry;

use app\tests\service\ApplicationAspectKernel;
use app\tests\ThinkPhpLoaderWrapper;

class Bootstrap
{
 public function moduleInit(&$params)
 {
  $applicationAspectKernel = ApplicationAspectKernel::getInstance();
  $applicationAspectKernel->init([
   'debug' => true,
   'appDir' => __DIR__ . './../../../',
    'cacheDir' => __DIR__ . './../../../runtime/aop_cache',
    'includePaths' => [
     __DIR__ . './../../tests/controller',
     __DIR__ . './../../../thinkphp/library/think/model'
    ],
    'excludePaths' => [
     __DIR__ . './../../aspect',
    ]
   ]);
  return $params;
 }
}

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

<?PHP
// file: ./application/tests/tags.php
// 由于是thinkphp5.10 没有容器,所有需要在module下的tags.php文件里配置调用他

return [
 // 应用初始化
 'app_init'  => [],
 // 应用开始
 'app_begin' => [],
 // 模块初始化
 'module_init' => [
  'app\\tests\\behavior\\Bootstrap'
 ],
 // 操作开始执行
 'action_begin' => [],
 // 视图内容过滤
 'view_filter' => [],
 // 日志写入
 'log_write' => [],
 // 应用结束
 'app_end'  => [],
];

兼容测试

好了,访问 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技术栈。

  1. 假设这里有台Ubuntu你已经配置好了LNMP环境
  2. 下载代码
  3. 配置nginx
# file: /usr/share/etc/nginx/conf.d/go-aop-test.conf
server {
 listen 8008;
# listen 443 ssl;
 server_name 0.0.0.0;
 root "/usr/share/nginx/html/app/vendor/lisachenko/go-aop-php/demos";
 index index.html index.htm index.php;
 charset utf-8;

 access_log /var/log/nginx/go-aop-access.log;
 error_log /var/log/nginx/go-aop-error.log notice;

 sendfile off;
 client_max_body_size 100m;

 location ~ \.php(.*)$ {
  include       fastcgi_params;
  fastcgi_pass      127.0.0.1:9000;
  fastcgi_index      index.php;

  fastcgi_param      PATH_INFO  $fastcgi_path_info;
#  fastcgi_param     SCRIPT_FILENAME /var/www/html/app/vendor/lisachenko/go-aop-php/demos$fastcgi_script_name; #docker的配置
  fastcgi_param      SCRIPT_FILENAME /usr/share/nginx/html/api/vendor/lisachenko/go-aop-php/demos$fastcgi_script_name;
  fastcgi_param      PATH_TRANSLATED $document_root$fastcgi_path_info;
  fastcgi_split_path_info   ((?U).+\.php)(/?.+)$;
 }
}

接下来要调整下代码

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

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

  1. 这个报错信息提示找不到这个类。来到报错的文件里。这文件使用了use找不到类,就是autoload出问题了,看到 vendor/lisachenko/go-aop-php/demos/autoload.php 这个文件。
<?PHP
···
if (file_exists(__DIR__ . '/../vendor/autoload.php')) {
 /** @var Composer\Autoload\ClassLoader $loader */
 $loader = include __DIR__ . '/../vendor/autoload.php';
 $loader->add('Demo', __DIR__);
}

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

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

再试试,demo运行起来了。

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

尝试了下,运行成功

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

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

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

<?PHP
// file: ./vendor/lisachenko/go-aop-php/src/Instrument/ClassLoading/AopComposerLoader.php

public function loadClass($class)
{
 if ($file = $this->original->findFile($class)) {
  $isInternal = false;
  foreach ($this->internalNamespaces as $ns) {
   if (strpos($class, $ns) === 0) {
    $isInternal = true;
    break;
   }
  }

  include ($isInternal ? $file : FilterInjectorTransformer::rewrite($file));
 }
}

这是一个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.
我们看看这个文件

<?PHP
// ./vendor/lisachenko/go-aop-php/src/Instrument/ClassLoading/AopComposerLoader.php:57

public static function init()
{
 $loaders = spl_autoload_functions();

 foreach ($loaders as &$loader) {
  $loaderToUnregister = $loader;
  if (is_array($loader) && ($loader[0] instanceof ClassLoader)) {
   $originalLoader = $loader[0];

   // Configure library loader for doctrine annotation loader
   AnnotationRegistry::registerLoader(function ($class) use ($originalLoader) {
    $originalLoader->loadClass($class);

    return class_exists($class, false);
   });
   $loader[0] = new AopComposerLoader($loader[0]);
  }
  spl_autoload_unregister($loaderToUnregister);
 }
 unset($loader);

 foreach ($loaders as $loader) {
  spl_autoload_register($loader);
 }
}

这个文件里有个类型检测,检测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上加了一个壳而已。

<?PHP
// file: ./application/tests 

namespace app\tests;

require_once __DIR__ . './../../vendor/composer/ClassLoader.php';

use think\Loader;
use \Composer\Autoload\ClassLoader;
use Go\Instrument\Transformer\FilterInjectorTransformer;
use Go\Instrument\ClassLoading\AopComposerLoader;
use Doctrine\Common\Annotations\AnnotationRegistry;


class ThinkPhpLoaderWrapper extends ClassLoader
{
 static protected $thinkLoader = Loader::class;

 /**
  * Autoload a class by it's name
  */
 public function loadClass($class)
 {
  return Loader::autoload($class);
 }

 /**
  * {@inheritDoc}
  */
 public function findFile($class)
 {
  $allowedNamespace = [
   'app\tests\controller'
  ];
  $isAllowed = false;
  foreach ($allowedNamespace as $ns) {
   if (strpos($class, $ns) === 0) {
    $isAllowed = true;
    break;
   }
  }
  // 不允许被AOP的类,则不进入AopComposer
  if(!$isAllowed)
   return false;
  
  $obj = new Loader;
  $observer = new \ReflectionClass(Loader::class);

  $method = $observer->getMethod('findFile');
  $method->setAccessible(true);
  $file = $method->invoke($obj, $class);
  return $file;
 }
}
<?PHP
// file: ./application/tests/behavior/Bootstrap.php 在刚刚我们新添加的文件当中
// 这个方法 \app\tests\behavior\Bootstrap::moduleInit 的后面追加如下内容

// 组成AOPComposerAutoLoader
$originalLoader = $thinkLoader = new ThinkPhpLoaderWrapper();
AnnotationRegistry::registerLoader(function ($class) use ($originalLoader) {
 $originalLoader->loadClass($class);

 return class_exists($class, false);
});
$aopLoader = new AopComposerLoader($thinkLoader);
spl_autoload_register([$aopLoader, 'loadClass'], false, true);

return $params;

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

最后

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

最后我们做个总结:

  1. PHP7 目前没有拓展实现的 AOP
  2. ThinkPHP5 有着自己的 Autoloader
  3. Go!AOP 的AOP实现依赖Class Autoloadcallback,通过替换原文件指向Proxy类实现。
  4. ThinkPHP5 整合 Go!AOP 需要调整 autoload

希望本文所述对大家基于ThinkPHP框架的PHP程序设计有所帮助。

PHP 相关文章推荐
PHP正确配置mysql(apache环境)
Aug 28 PHP
PHP使用数组实现队列
Feb 05 PHP
全新的PDO数据库操作类php版(仅适用Mysql)
Jul 22 PHP
windows下zendframework项目环境搭建(通过命令行配置)
Dec 06 PHP
PHP CURL获取cookies模拟登录的方法
Nov 04 PHP
smarty获得当前url的方法分享
Feb 14 PHP
PHP5.2下preg_replace函数的问题
May 08 PHP
PHP中addcslashes与stripcslashes函数用法分析
Jan 07 PHP
使用PHP连接多种数据库的实现代码(mysql,access,sqlserver,Oracle)
Dec 21 PHP
php实时倒计时功能实现方法详解
Feb 27 PHP
用php+ajax新建流程(请假、进货、出货等)
Jun 11 PHP
Laravel Validator 实现两个或多个字段联合索引唯一
May 08 PHP
php中用unset销毁变量并释放内存
May 10 #PHP
php屏蔽错误及提示的方法
May 10 #PHP
php判断数组是否为空的实例方法
May 10 #PHP
通过PHP实现获取访问用户IP
May 09 #PHP
如何通过PHP实现Des加密算法代码实例
May 09 #PHP
php变量与字符串的增删改查操作示例
May 07 #PHP
PHP数组与字符串互相转换实例
May 05 #PHP
You might like
第十一节 重载 [11]
2006/10/09 PHP
PHP mkdir()无写权限的问题解决方法
2014/06/19 PHP
PHP集成百度Ueditor 1.4.3
2014/11/23 PHP
php使用标签替换的方式生成静态页面
2015/05/21 PHP
PHP实现cookie跨域session共享的方法分析
2019/08/23 PHP
用javascript获取当页面上鼠标光标位置和触发事件的对象的代码
2009/12/09 Javascript
读jQuery之五(取DOM元素)
2011/06/20 Javascript
html超链接打开窗口大小的方法
2013/03/05 Javascript
JS获取当前日期和时间的简单实例
2013/11/19 Javascript
js通过八个点 拖动改变div大小的实现方法
2014/03/05 Javascript
js 实现浏览历史记录示例
2014/04/20 Javascript
JS动态增加删除UL节点LI及相关内容示例
2014/05/21 Javascript
JS原型对象的创建方法详解
2016/06/16 Javascript
Google Maps基础及实例解析
2016/08/06 Javascript
每个程序员都需要学习 JavaScript 的7个理由小结
2016/09/03 Javascript
Nodejs中解决cluster模块的多进程如何共享数据问题
2016/11/10 NodeJs
JS控件bootstrap datepicker使用方法详解
2017/03/25 Javascript
canvas实现弧形可拖动进度条效果
2017/05/11 Javascript
JavaScript实现学生在线做题计时器功能
2018/12/05 Javascript
vscode中eslint插件的配置(prettier配置无效)
2019/09/10 Javascript
JavaScript forEach中return失效问题解决方案
2020/06/01 Javascript
vue相同路由跳转强制刷新该路由组件操作
2020/08/05 Javascript
关于Vue中$refs的探索浅析
2020/11/05 Javascript
jQuery实现简单轮播图效果
2020/12/27 jQuery
[01:38]女王驾到——至宝魔廷新尊技能&特效展示
2020/06/16 DOTA
全面解读Python Web开发框架Django
2014/06/30 Python
利用pyecharts实现地图可视化的例子
2019/08/12 Python
Python log模块logging记录打印用法解析
2020/01/20 Python
Python 实现微信自动回复的方法
2020/09/11 Python
html5使用Canvas绘图的使用方法
2017/11/21 HTML / CSS
华丽的手绘陶瓷:MacKenzie-Childs
2017/02/04 全球购物
食品安全承诺书
2014/05/22 职场文书
校园绿化美化方案
2014/06/08 职场文书
大学毕业生个人自荐书
2014/07/02 职场文书
老公写给老婆的检讨书
2015/05/06 职场文书
《草船借箭》教学反思
2016/02/23 职场文书