本文實例講述了ThinkPHP5 框架引入 Go AOP,PHP AOP編程。分享給大家供大家參考,具體如下:
項目背景
目前開發的WEB軟件里有這一個功能,PHP訪問API操作數據倉庫,剛開始數據倉庫小,沒發現問題,隨著數據越來越多,調用API時常超時(60s)。于是決定采用異步請求,改為60s能返回數據則返回,不能則返回一個異步ID,然后輪詢是否完成統計任務。由于項目緊,人手不足,必須以最小的代價解決當前問題。
方案選擇
- 重新分析需求,并改進代碼
- 采用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都會很熱心回復。
1
|
composer require goaop/framework |
ThinkPHP5 對composer兼容挺不錯的哦,(到后面,我真想揍ThinkPHP5作者)這就裝好了,怎么用啊,git上的提示了簡單用法。我也就照著寫了個去切入controller。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<?PHP namespace app\tests\controller; use think\Controller; class Test1 extends Controller { public function test1() { echo $this ->aspectAction(); } public function aspectAction() { return 'hello' ; } } |
定義aspect
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
|
<?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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
|
<?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 核心服務配置
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
|
<?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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
|
<?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 顯示:
1
|
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技術棧。
- 假設這里有臺Ubuntu你已經配置好了LNMP環境
- 下載代碼
- 配置nginx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
|
# 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)(/?.+)$; } } |
接下來要調整下代碼
- 訪問 http://127.0.0.1:8008 試試,(估計大家都遇到了這個)
-
這個報錯信息提示找不到這個類。來到報錯的文件里。這文件使用了use找不到類,就是autoload出問題了,看到
vendor/lisachenko/go-aop-php/demos/autoload.php
這個文件。
1
2
3
4
5
6
7
|
<?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。我們做如下調整
1
2
3
4
5
6
7
|
<?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里運行就不成功呢。我決定采用斷點進行調試。
通過斷點我發現了這個文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
<?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.
我們看看這個文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
|
<?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上加了一個殼而已。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
|
<?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 ; } } |
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
<?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
輸出的信息了。
最后我們做個總結:
-
PHP7
目前沒有拓展實現的AOP
。 -
ThinkPHP5
有著自己的Autoloader
。 -
Go!AOP
的AOP實現依賴Class Autoload
的callback
,通過替換原文件
指向Proxy類
實現。 -
ThinkPHP5
整合Go!AOP
需要調整autoload
。
希望本文所述對大家基于ThinkPHP框架的PHP程序設計有所幫助。
原文鏈接:https://segmentfault.com/a/1190000012656671