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 相关文章推荐
深入了解php4(2)--重访过去
Oct 09 PHP
php mssql 日期出现中文字符的解决方法
Mar 10 PHP
PHPnow安装服务[apache_pn]失败的问题的解决方法
Sep 10 PHP
PHP删除目录及目录下所有文件的方法详解
Jun 06 PHP
win7计划任务定时执行PHP脚本设置图解
May 09 PHP
php定时计划任务与fsockopen持续进程实例
May 23 PHP
PHP实现即时输出、实时输出内容方法
May 27 PHP
php三元运算符知识汇总
Jul 02 PHP
Linux下编译redis和phpredis的方法
Apr 07 PHP
PHP邮箱验证示例教程
Jun 01 PHP
PHP页面跳转操作实例分析(header方法)
Sep 28 PHP
Yii2.0 RESTful API 基础配置教程详解
Dec 26 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
第五节 克隆 [5]
2006/10/09 PHP
php编程实现获取excel文档内容的代码实例
2011/06/28 PHP
PHP基于MySQLI函数封装的数据库连接工具类【定义与用法】
2017/08/11 PHP
PHP中strtr与str_replace函数运行性能简单测试示例
2019/06/22 PHP
laravel5.1 ajax post 传值_token示例
2019/10/24 PHP
php7连接MySQL实现简易查询程序的方法
2020/10/13 PHP
拉动滚动条加载数据的jquery代码
2012/05/03 Javascript
在JS数组特定索引处指定位置插入元素
2014/07/27 Javascript
javascript中实现兼容JAVA的hashCode算法代码分享
2020/08/11 Javascript
jQuery基于ajax实现星星评论代码
2015/08/07 Javascript
javascript RegExp 使用说明
2016/05/21 Javascript
Google 地图API资料整理及详细介绍
2016/08/06 Javascript
浅析Javascript ES6新增值比较函数Object.is
2016/08/24 Javascript
node.js实现快速截图
2016/08/27 Javascript
js实现hashtable的赋值、取值、遍历操作实例详解
2016/12/25 Javascript
详解在Vue中通过自定义指令获取dom元素
2017/03/04 Javascript
webpack多入口文件页面打包配置详解
2018/01/09 Javascript
小程序实现左滑删除效果
2019/07/25 Javascript
JS中的算法与数据结构之二叉查找树(Binary Sort Tree)实例详解
2019/08/16 Javascript
Vue通过配置WebSocket并实现群聊功能
2019/12/31 Javascript
解决vue.js中settimeout遇到的问题(时间参数短效果不稳定)
2020/07/21 Javascript
原生js实现自定义滚动条组件
2021/01/20 Javascript
[46:27]DOTA2上海特级锦标赛主赛事日 - 1 胜者组第一轮#2LGD VS MVP.Phx第一局
2016/03/02 DOTA
[50:17]Newbee vs Serenity 2018国际邀请赛小组赛BO2 第二场 8.17
2018/08/18 DOTA
Python中规范定义命名空间的一些建议
2016/06/04 Python
Python异常继承关系和自定义异常实现代码实例
2020/02/20 Python
简单了解python列表和元组的区别
2020/05/14 Python
html5 canvas实现给图片添加平铺水印
2019/08/20 HTML / CSS
管理科学大学生求职信
2013/11/13 职场文书
教师的实习鉴定
2013/12/15 职场文书
团队激励口号
2014/06/06 职场文书
十佳党员事迹材料
2014/08/28 职场文书
公司股东出资证明书
2014/11/01 职场文书
毕业生个人自荐书
2015/03/05 职场文书
在 Golang 中实现 Cache::remember 方法详解
2021/03/30 Python
使用Springboot实现健身房管理系统
2021/07/01 Java/Android