目前开发的WEB软件里有这一个功能,PHP访问API操做数据仓库,刚开始数据仓库小,没发现问题,随着数据愈来愈多,调用API时常超时(60s)。因而决定采用异步请求,改成60s能返回数据则返回,不能则返回一个异步ID,而后轮询是否完成统计任务。因为项目紧,人手不足,必须以最小的代价解决当前问题。php
重新作需求分析,以及详细设计,并改动代码,须要产品,架构,前端,后端的支持。会惊动的人过多,在资源紧张的状况下是不推荐的。
采用AOP方式,不改动原有代码逻辑,只须要后端就能完成大部分任务了。后端用AOP切入请求API的方法,经过监听API返回的结果来控制是否让其继续运行原有的逻辑(API在60s返回了数据),或者是进入离线任务功能(API报告统计任务不能在60s内完成)。html
Debian,php-fpm-7.0,ThinkPHP-5.10。前端
做为一门zui好的语言,PHP是不自带AOP的。那就得安装AOP-PHP拓展,当我打开pecl要下载时,傻眼了,全是bate版,没有显示说明支持php7。但我仍是抱着侥幸心理,找到了git,发现4-5年没更新了,要不要等一波更新,哦,做者在issue里说了有时间就开始兼容php7。
好吧,狠话很少说,下一个方案:Go!AOP.看了下git,做者是个穿白体恤,喜欢山峰的大帅哥,基本每一个issue都会很热心回复。nginx
composer require goaop/framework
ThinkPHP5 对composer兼容挺不错的哦,(到后面,我真想揍ThinkPHP5做者)这就装好了,怎么用啊,git上的提示了简单用法。我也就照着写了个去切入controller。git
<?PHP namespace app\tests\controller; use think\Controller; class Test1 extends Controller { public function test1() { echo $this->aspectAction(); } public function aspectAction() { return 'hello'; } }
定义aspectgithub
<?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"; } }
启用aspectdocker
<?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 核心服务配置thinkphp
<?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-aopshell
<?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/... 显示:json
hello
这不是预期的效果,在aspect定义了,访问该方法前,会输出方法的更多信息信息。
像以下内容才是预期
Calling Before Interceptor for method: app\tests\controller\Test1->aspectAction() with arguments: []
上他官方Doc看看,是一些更高级的用法。没有讲go-aop的运行机制。
上git上也没看到相似issue,额,发现做者常常在issue里回复:试一试demo。也许我该试试demo。
我采用的是LNMP技术栈。
# 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)(/?.+)$; } }
接下来要调整下代码
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运行起来了。
尝试了下,运行成功
经过以上的输出,能够得出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里,默认有且只会注册一个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/... 你就能看到来自 aspect
输出的信息了。
最后咱们作个总结:
PHP7
目前没有拓展实现的 AOP
。ThinkPHP5
有着本身的 Autoloader
。Go!AOP
的AOP实现依赖Class Autoload
的 callback
,经过替换原文件
指向Proxy类
实现。ThinkPHP5
整合 Go!AOP
须要调整 autoload
。