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 相关文章推荐
Oracle 常见问题解答
Oct 09 PHP
PHP类的使用 实例代码讲解
Dec 28 PHP
php中使用临时表查询数据的一个例子
Feb 03 PHP
php实现下载限制速度示例分享
Feb 13 PHP
easyui的tabs update正确用法分享
Mar 21 PHP
php检测url是否存在的方法
Apr 14 PHP
PHP中的类型约束介绍
May 11 PHP
PHP记录页面停留时间的方法
Mar 30 PHP
php遍历、读取文件夹中图片并分页显示图片的方法
Nov 15 PHP
/etc/php-fpm.d/www.conf 配置注意事项
Feb 04 PHP
解决在Laravel 中处理OPTIONS请求的问题
Oct 11 PHP
laravel实现一个上传图片的接口,并建立软链接,访问图片的方法
Oct 12 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
Could not load type System.ServiceModel.Activation.HttpModule解决办法
2012/12/29 PHP
探讨PHP函数ip2long转换IP时数值太大产生负数的解决方法
2013/06/06 PHP
PHP中ini_set和ini_get函数的用法小结
2014/02/18 PHP
RSA实现JS前端加密与PHP后端解密功能示例
2019/08/05 PHP
PHP安全之register_globals的on和off的区别
2020/07/23 PHP
一个简单的javascript类定义例子
2009/09/12 Javascript
用js格式化金额可设置保留的小数位数
2014/05/09 Javascript
JavaScript中伪协议 javascript:使用探讨
2014/07/18 Javascript
javascript实现微信分享
2014/12/23 Javascript
jQuery中not()方法用法实例
2015/01/06 Javascript
Node.JS使用Sequelize操作MySQL的示例代码
2017/10/09 Javascript
vue router动态路由设置参数可选问题
2019/08/21 Javascript
Vue.js仿Select下拉框效果
2020/02/18 Javascript
Python中使用socket发送HTTP请求数据接收不完整问题解决方法
2015/02/04 Python
基于Python实现文件大小输出
2016/01/11 Python
python+pygame简单画板实现代码实例
2017/12/13 Python
使用pandas对矢量化数据进行替换处理的方法
2018/04/11 Python
用python爬取租房网站信息的代码
2018/12/14 Python
python字典的setdefault的巧妙用法
2019/08/07 Python
Python 中判断列表是否为空的方法
2019/11/24 Python
使用keras时input_shape的维度表示问题说明
2020/06/29 Python
在Tensorflow中实现leakyRelu操作详解(高效)
2020/06/30 Python
Python 读取位于包中的数据文件
2020/08/07 Python
如何让PyQt5中QWebEngineView与JavaScript交互
2020/10/21 Python
python 实现简单的计算器(gui界面)
2020/11/11 Python
pycharm进入时每次都是insert模式的解决方式
2021/02/05 Python
CSS3为背景图设置遮罩并解决遮罩样式继承问题
2020/06/22 HTML / CSS
项目管理计划书
2014/01/09 职场文书
大学生冰淇淋店商业计划书
2014/01/14 职场文书
优秀老员工获奖感言
2014/02/15 职场文书
学习2014年全国两会心得体会
2014/03/12 职场文书
水污染治理工程专业自荐信
2014/06/21 职场文书
学生抄袭作业的检讨书
2014/10/02 职场文书
2015年小学开学寄语
2015/02/27 职场文书
财务稽核岗位职责
2015/04/13 职场文书
react中useState使用:如何实现在当前表格直接更改数据
2022/08/05 Javascript