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 相关文章推荐
利用static实现表格的颜色隔行显示的代码
Sep 02 PHP
解决MySQL中文输出变成问号的问题
Jun 05 PHP
php 无限级 SelectTree 类
May 19 PHP
ThinkPHP 防止表单重复提交的方法
Aug 08 PHP
php实现文件编码批量转换
Mar 10 PHP
Codeigniter整合Tank Auth权限类库详解
Jun 12 PHP
PHP的消息通信机制测试实例
Nov 10 PHP
总结的一些PHP开发中的tips(必看篇)
Mar 24 PHP
PHP简单实现二维数组的矩阵转置操作示例
Nov 24 PHP
PHP基于面向对象封装的分页类示例
Mar 15 PHP
PHP如何防止XSS攻击与XSS攻击原理的讲解
Mar 22 PHP
PHP中关于php.ini参数优化详解
Feb 28 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
PHP中mb_convert_encoding与iconv函数的深入解析
2013/06/21 PHP
基于simple_html_dom的使用小结
2013/07/01 PHP
2014年10个最佳的PHP图像操作库
2014/07/14 PHP
PHP 读取和编写 XML
2014/11/19 PHP
ThinkPHP里用U方法调用js文件实例
2015/06/18 PHP
分享3个php获取日历的函数
2015/09/25 PHP
PHP简单获取多个checkbox值的方法
2016/06/13 PHP
PHP页面间传递值和保持值的方法
2016/08/24 PHP
js替代copy(示例代码)
2013/11/27 Javascript
JQuery radio(单选按钮)操作方法汇总
2015/04/15 Javascript
使用JavaScript判断手机浏览器是横屏还是竖屏问题
2016/08/02 Javascript
输入法的回车与消息发送快捷键回车的冲突解决方法
2016/08/09 Javascript
浅析jQuery操作select控件的取值和设值
2016/12/07 Javascript
JS+DIV实现的卷帘效果示例
2017/03/22 Javascript
JS实现动态给标签控件添加事件的方法示例
2017/05/13 Javascript
jQuery实现的淡入淡出与滑入滑出效果示例
2018/04/18 jQuery
JS实现十分钟倒计时代码实例
2018/10/18 Javascript
配置eslint规范项目代码风格
2019/03/11 Javascript
Vue Render函数原理及代码实例解析
2020/07/30 Javascript
vue中如何自定义右键菜单详解
2020/12/08 Vue.js
python简单实现刷新智联简历
2016/03/30 Python
Python深入06——python的内存管理详解
2016/12/07 Python
Python学习小技巧之列表项的推导式与过滤操作
2017/05/20 Python
Python实现的特征提取操作示例
2018/12/03 Python
Python实现插入排序和选择排序的方法
2019/05/12 Python
python集合是否可变总结
2019/06/20 Python
python中的时区问题
2021/01/14 Python
Optimalprint加拿大:在线打印服务
2020/04/03 全球购物
通用C#笔试题附答案
2016/11/26 面试题
数控专业推荐信范文
2013/12/02 职场文书
银行爱岗敬业演讲稿
2014/05/05 职场文书
八项规定个人对照检查材料思想汇报
2014/09/25 职场文书
好段摘抄大全(48句)
2019/08/08 职场文书
使用这 6个Vue加载动画库来减少我们网站的跳出率
2021/05/18 Vue.js
angular异步验证器防抖实例详解
2022/03/31 Javascript
MySQL安装失败的原因及解决步骤
2022/06/14 MySQL