解析ThinkPHP5如何引入Go AOP和PHP AOP編程

項(xiàng)目背景

目前開發(fā)的web軟件里有這一個(gè)功能,php訪問api操作數(shù)據(jù)倉庫,剛開始數(shù)據(jù)倉庫小,沒發(fā)現(xiàn)問題,隨著數(shù)據(jù)越來越多,調(diào)用api時(shí)常超時(shí)(60s)。于是決定采用異步請(qǐng)求,改為60s能返回?cái)?shù)據(jù)則返回,不能則返回一個(gè)異步id,然后輪詢是否完成統(tǒng)計(jì)任務(wù)。由于項(xiàng)目緊,人手不足,必須以最小的代價(jià)解決當(dāng)前問題。

方案選擇

  1. 重新分析需求,并改進(jìn)代碼
  2. 采用AOP方式改動(dòng)程序

從新做需求分析,以及詳細(xì)設(shè)計(jì),并改動(dòng)代碼,需要產(chǎn)品,架構(gòu),前端,后端的支持。會(huì)驚動(dòng)的人過多,在資源緊張的情況下是不推薦的。
采用AOP方式,不改動(dòng)原有代碼邏輯,只需要后端就能完成大部分任務(wù)了。后端用AOP切入請(qǐng)求API的方法,通過監(jiān)聽API返回的結(jié)果來控制是否讓其繼續(xù)運(yùn)行原有的邏輯(API在60s返回了數(shù)據(jù)),或者是進(jìn)入離線任務(wù)功能(API報(bào)告統(tǒng)計(jì)任務(wù)不能在60s內(nèi)完成)。

之前用過AOP-PHP拓展,上手很簡單,不過后來在某一個(gè)大項(xiàng)目中引入該拓展后,直接爆了out of memory,然后就研究其源碼發(fā)現(xiàn),它改變了語法樹,并Hook了每個(gè)被調(diào)用的方法,也就是每個(gè)方法被調(diào)用是都會(huì)去詢問AOP-PHP,這個(gè)方法有沒有切面方法。所以效率損失是比較大的。而且這個(gè)項(xiàng)目距離現(xiàn)在已經(jīng)有8年沒更新了。所以不推薦該解決方案。

實(shí)際環(huán)境

debian,php-fpm-7.0,thinkphp-5.10。

引入AOP

作為一門zui好的語言,PHP是不自帶AOP的。那就得安裝AOP-PHP拓展,當(dāng)我打開pecl要下載時(shí),傻眼了,全是bate版,沒有顯示說明支持php7。但我還是抱著僥幸心理,找到了git,發(fā)現(xiàn)4-5年沒更新了,要不要等一波更新,哦,作者在issue里說了有時(shí)間就開始兼容php7。
好吧,狠話不多說,下一個(gè)方案:Go!AOP.看了下git,作者是個(gè)穿白體恤,喜歡山峰的大帥哥,基本每個(gè)issue都會(huì)很熱心回復(fù)。

立即學(xué)習(xí)PHP免費(fèi)學(xué)習(xí)筆記(深入)”;

composer require goaop/framework

ThinkPHP5 對(duì)composer兼容挺不錯(cuò)的哦,(到后面,我真想揍ThinkPHP5作者)這就裝好了,怎么用啊,git上的提示了簡單用法。我也就照著寫了個(gè)去切入controller。

<?PHP namespace app	estscontroller;  use thinkController;  class Test1 extends Controller {     public function test1()     {         echo $this->aspectAction();     }          public function aspectAction()     {         return 'hello';     } }

定義aspect

<?PHP namespace app	estsspect;  use GoAopAspect; use GoAopInterceptFieldAccess; use GoAopInterceptMethodInvocation; use GoLangAnnotationAfter; use GoLangAnnotationBefore; use GoLangAnnotationAround; use GoLangAnnotationPointcut;  use app	estscontrollerTest1;  class MonitorAspect implements Aspect {      /**      * Method that will be called before real method      *      * @param MethodInvocation $invocation Invocation      * @Before("execution(public|protected app	estscontrollerTest1->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> ";     } }

啟用aspect

<?PHP // file: ./application/tests/service/ApplicationAspectKernel.php  namespace app	estsservice;  use GoCoreAspectKernel; use GoCoreAspectContainer;  use app	estsspectMonitorAspect;  /**  * Application Aspect Kernel  *  * Class ApplicationAspectKernel  * @package app	estsservice  */ 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 核心服務(wù)配置

<?PHP // file: ./application/tests/behavior/Bootstrap.php namespace app	estsehavior;  use thinkException; use ComposerAutoloadClassLoader; use GoInstrumentTransformerFilterInjectorTransformer; use GoInstrumentClassLoadingAopComposerLoader; use DoctrineCommonAnnotationsAnnotationRegistry;  use app	estsserviceApplicationAspectKernel; use app	estsThinkPhpLoaderWrapper;  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鉤子,讓其啟動(dòng) go-aop

<?PHP // file: ./application/tests/tags.php // 由于是thinkphp5.10 沒有容器,所有需要在module下的tags.php文件里配置調(diào)用他  return [     // 應(yīng)用初始化     'app_init'     => [],     // 應(yīng)用開始     'app_begin'    => [],     // 模塊初始化     'module_init'  => [         'apptestsbehaviorBootstrap'     ],     // 操作開始執(zhí)行     'action_begin' => [],     // 視圖內(nèi)容過濾     'view_filter'  => [],     // 日志寫入     'log_write'    => [],     // 應(yīng)用結(jié)束     'app_end'      => [], ];

兼容測試

好了,訪問 http://127.0.0.1/tests/test1/… 顯示:

hello

這不是預(yù)期的效果,在aspect定義了,訪問該方法前,會(huì)輸出方法的更多信息信息。
像如下內(nèi)容才是預(yù)期

Calling Before Interceptor for method: app	estscontrollerTest1->aspectAction() with arguments: []

上他官方Doc看看,是一些更高級(jí)的用法。沒有講go-aop的運(yùn)行機(jī)制。
上git上也沒看到類似issue,額,發(fā)現(xiàn)作者經(jīng)常在issue里回復(fù):試一試demo。也許我該試試demo。

Run Demos

我采用的是LNMP技術(shù)

  1. 假設(shè)這里有臺(tái)ubuntu你已經(jīng)配置好了LNMP環(huán)境
  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)(/?.+)$;     } }

接下來要調(diào)整下代碼

  1. 訪問 http://127.0.0.1:8008 試試,(估計(jì)大家都遇到了這個(gè))

解析ThinkPHP5如何引入Go AOP和PHP AOP編程

  1. 這個(gè)報(bào)錯(cuò)信息提示找不到這個(gè)類。來到報(bào)錯(cuò)的文件里。這文件使用了use找不到類,就是autoload出問題了,看到 vendor/lisachenko/go-aop-php/demos/autoload.php 這個(gè)文件。
<?PHP ··· if (file_exists(__DIR__ . '/../vendor/autoload.php')) {     /** @var ComposerAutoloadClassLoader $loader */     $loader = include __DIR__ . '/../vendor/autoload.php';     $loader->add('Demo', __DIR__); }

可以看到這個(gè)代碼第一行沒找到vendor下的autoload。我們做如下調(diào)整

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

再試試,demo運(yùn)行起來了。

解析ThinkPHP5如何引入Go AOP和PHP AOP編程

嘗試了下,運(yùn)行成功

解析ThinkPHP5如何引入Go AOP和PHP AOP編程

通過以上的輸出,可以得出demo里是對(duì)方法運(yùn)行前成功捕獲。為什么在thinkphp的controller里運(yùn)行就不成功呢。我決定采用斷點(diǎn)進(jìn)行調(diào)試。

通過斷點(diǎn)我發(fā)現(xiàn)了這個(gè)文件

<?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));     } }

這是一個(gè)autoload,每個(gè)類的載入都會(huì)經(jīng)過它,并且會(huì)對(duì)其判斷是否為內(nèi)部類,不是的都會(huì)進(jìn)入后續(xù)的操作。通過斷點(diǎn)進(jìn)入 FilterInjectorTransformer,發(fā)現(xiàn)會(huì)對(duì)load的文件進(jìn)行語法解析,并根據(jù)注冊(cè)的annotation對(duì)相關(guān)的類生成proxy類。說道這,大家就明白了go-aop是如何做到切入你的程序了吧,生成的proxy類,可以在你配置的cache-dir(我配置的是./runtime/aop_cache/)里看到。

同時(shí)./runtime/aop_cache/ 文件夾下也生成了很多東西,通過查看aop_cache文件內(nèi)產(chǎn)生了與Test1文件名相同的文件,打開文件,發(fā)現(xiàn)它代理了原有的Test1控制器。這一系列信息,可以得出,Go!AOP 通過”劫持” composer autoload 讓每個(gè)類都進(jìn)過它,根據(jù)aspect的定義來決定是否為其創(chuàng)建一個(gè)代理類,并植入advice。
額,ThinkPHP5是把composer autoload里的東西copy出來,放到自己autoload里,然后就沒composer啥事了。然后go-aop一直等不到composer autoload下發(fā)的命令,自然就不能起作用了,so,下一步

改進(jìn)ThinkPHP5

在ThinkPHP5里,默認(rèn)有且只會(huì)注冊(cè)一個(gè)TP5內(nèi)部的 Loader,并不會(huì)把include請(qǐng)求下發(fā)給composer的autoload。所以,為其讓go-aop起作用,那么必須讓讓include class的請(qǐng)求經(jīng)過 AopComposerLoad.
我們看看這個(gè)文件

<?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);     } }

這個(gè)文件里有個(gè)類型檢測,檢測autoload callback是否為Classloader類型,然而ThinkPHP5不是,通過斷點(diǎn)你會(huì)發(fā)現(xiàn)ThinkPHP5是一個(gè)字符串數(shù)組,so,這里也就無法把go-aop注冊(cè)到class loader的callback當(dāng)中了。

這里就要提一下PHP autoload機(jī)制了,這是現(xiàn)代PHP非常重要的一個(gè)功能,它讓我們?cè)谟玫揭粋€(gè)類時(shí),通過名字能自動(dòng)加載文件。我們通過定義一定的類名規(guī)則與文件結(jié)構(gòu)目錄,再加上能實(shí)現(xiàn)以上規(guī)則的函數(shù)就能實(shí)現(xiàn)自動(dòng)加載了。在通過 spl_autoload_register 函數(shù)的第三個(gè)參數(shù) prepend 設(shè)置為true,就能讓其排在在TP5的loader前面,先一步被調(diào)用。

依照如上原理,就可以做如下改進(jìn)
這個(gè)是為go-aop包裝的新autoload,本質(zhì)上是在原來的ThinkPHP5的loader上加了一個(gè)殼而已。

<?PHP // file: ./application/tests   namespace app	ests;  require_once __DIR__ . './../../vendor/composer/ClassLoader.php';  use thinkLoader; use ComposerAutoloadClassLoader; use GoInstrumentTransformerFilterInjectorTransformer; use GoInstrumentClassLoadingAopComposerLoader; use DoctrineCommonAnnotationsAnnotationRegistry;   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	estscontroller'         ];         $isAllowed = false;         foreach ($allowedNamespace as $ns) {             if (strpos($class, $ns) === 0) {                 $isAllowed = true;                 break;             }         }         // 不允許被AOP的類,則不進(jìn)入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 在剛剛我們新添加的文件當(dāng)中 // 這個(gè)方法 pp	estsehaviorBootstrap::moduleInit 的后面追加如下內(nèi)容  // 組成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;

在這里我們做了一個(gè)autload 并直接把它插入到了最前面(如果項(xiàng)目內(nèi)還有其他autloader,請(qǐng)注意他們的先后順序)。

最后

現(xiàn)在我們?cè)僭L問一下http://127.0.0.1/tests/test1/test1你就能看到來自 aspect 輸出的信息了。
最后我們做個(gè)總結(jié):

  1. PHP7 目前沒有拓展實(shí)現(xiàn)的 AOP。
  2. ThinkPHP5 有著自己的 Autoloader。
  3. Go!AOP 的AOP實(shí)現(xiàn)依賴Class Autoload的 callback,通過替換原文件指向Proxy類實(shí)現(xiàn)。
  4. ThinkPHP5 整合 Go!AOP 需要調(diào)整 autoload。

? 版權(quán)聲明
THE END
喜歡就支持一下吧
點(diǎn)贊15 分享