本文實(shí)例講述了ThinkPHP5 框架引入 Go AOP,PHP AOP編程。分享給大家供大家參考,具體如下:
項(xiàng)目背景
目前開(kāi)發(fā)的WEB軟件里有這一個(gè)功能,PHP訪問(wèn)API操作數(shù)據(jù)倉(cāng)庫(kù),剛開(kāi)始數(shù)據(jù)倉(cāng)庫(kù)小,沒(méi)發(fā)現(xiàn)問(wèn)題,隨著數(shù)據(jù)越來(lái)越多,調(diào)用API時(shí)常超時(shí)(60s)。于是決定采用異步請(qǐng)求,改為60s能返回?cái)?shù)據(jù)則返回,不能則返回一個(gè)異步ID,然后輪詢是否完成統(tǒng)計(jì)任務(wù)。由于項(xiàng)目緊,人手不足,必須以最小的代價(jià)解決當(dāng)前問(wèn)題。
方案選擇
- 重新分析需求,并改進(jìn)代碼
- 采用AOP方式改動(dòng)程序
從新做需求分析,以及詳細(xì)設(shè)計(jì),并改動(dòng)代碼,需要產(chǎn)品,架構(gòu),前端,后端的支持。會(huì)驚動(dòng)的人過(guò)多,在資源緊張的情況下是不推薦的。
采用AOP方式,不改動(dòng)原有代碼邏輯,只需要后端就能完成大部分任務(wù)了。后端用AOP切入請(qǐng)求API的方法,通過(guò)監(jiān)聽(tīng)API返回的結(jié)果來(lái)控制是否讓其繼續(xù)運(yùn)行原有的邏輯(API在60s返回了數(shù)據(jù)),或者是進(jìn)入離線任務(wù)功能(API報(bào)告統(tǒng)計(jì)任務(wù)不能在60s內(nèi)完成)。
之前用過(guò)AOP-PHP
拓展,上手很簡(jiǎn)單,不過(guò)后來(lái)在某一個(gè)大項(xiàng)目中引入該拓展后,直接爆了out of memory
,然后就研究其源碼發(fā)現(xiàn),它改變了語(yǔ)法樹(shù),并Hook了每個(gè)被調(diào)用的方法,也就是每個(gè)方法被調(diào)用是都會(huì)去詢問(wèn)AOP-PHP
,這個(gè)方法有沒(méi)有切面方法。所以效率損失是比較大的。而且這個(gè)項(xiàng)目距離現(xiàn)在已經(jīng)有8年沒(méi)更新了。所以不推薦該解決方案。
實(shí)際環(huán)境
Debian,php-fpm-7.0,ThinkPHP-5.10。
引入AOP
作為一門zui好的語(yǔ)言,PHP是不自帶AOP的。那就得安裝AOP-PHP拓展,當(dāng)我打開(kāi)pecl要下載時(shí),傻眼了,全是bate版,沒(méi)有顯示說(shuō)明支持php7。但我還是抱著僥幸心理,找到了git,發(fā)現(xiàn)4-5年沒(méi)更新了,要不要等一波更新,哦,作者在issue里說(shuō)了有時(shí)間就開(kāi)始兼容php7。
好吧,狠話不多說(shuō),下一個(gè)方案:Go!AOP.看了下git,作者是個(gè)穿白體恤,喜歡山峰的大帥哥,基本每個(gè)issue都會(huì)很熱心回復(fù)。
composer require goaop/framework
ThinkPHP5 對(duì)composer兼容挺不錯(cuò)的哦,(到后面,我真想揍ThinkPHP5作者)這就裝好了,怎么用啊,git上的提示了簡(jiǎn)單用法。我也就照著寫了個(gè)去切入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 核心服務(wù)配置
?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鉤子,讓其啟動(dòng) go-aop
?PHP
// file: ./application/tests/tags.php
// 由于是thinkphp5.10 沒(méi)有容器,所有需要在module下的tags.php文件里配置調(diào)用他
return [
// 應(yīng)用初始化
'app_init' => [],
// 應(yīng)用開(kāi)始
'app_begin' => [],
// 模塊初始化
'module_init' => [
'app\\tests\\behavior\\Bootstrap'
],
// 操作開(kāi)始執(zhí)行
'action_begin' => [],
// 視圖內(nèi)容過(guò)濾
'view_filter' => [],
// 日志寫入
'log_write' => [],
// 應(yīng)用結(jié)束
'app_end' => [],
];
兼容測(cè)試
好了,訪問(wèn) http://127.0.0.1/tests/test1/test1 顯示:
這不是預(yù)期的效果,在aspect定義了,訪問(wèn)該方法前,會(huì)輸出方法的更多信息信息。
像如下內(nèi)容才是預(yù)期
Calling Before Interceptor for method: app\tests\controller\Test1->aspectAction() with arguments: []
上他官方Doc看看,是一些更高級(jí)的用法。沒(méi)有講go-aop的運(yùn)行機(jī)制。
上git上也沒(méi)看到類似issue,額,發(fā)現(xiàn)作者經(jīng)常在issue里回復(fù):試一試demo。也許我該試試demo。
Run Demos
我采用的是LNMP技術(shù)棧。
- 假設(shè)這里有臺(tái)Ubuntu你已經(jīng)配置好了LNMP環(huán)境
- 下載代碼
- 配置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)(/?.+)$;
}
}
接下來(lái)要調(diào)整下代碼
- 訪問(wèn) http://127.0.0.1:8008 試試,(估計(jì)大家都遇到了這個(gè))
- 這個(gè)報(bào)錯(cuò)信息提示找不到這個(gè)類。來(lái)到報(bào)錯(cuò)的文件里。這文件使用了use找不到類,就是autoload出問(wèn)題了,看到
vendor/lisachenko/go-aop-php/demos/autoload.php
這個(gè)文件。
?PHP
···
if (file_exists(__DIR__ . '/../vendor/autoload.php')) {
/** @var Composer\Autoload\ClassLoader $loader */
$loader = include __DIR__ . '/../vendor/autoload.php';
$loader->add('Demo', __DIR__);
}
可以看到這個(gè)代碼第一行沒(méi)找到vendor下的autoload。我們做如下調(diào)整
?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運(yùn)行起來(lái)了。
嘗試了下,運(yùn)行成功
通過(guò)以上的輸出,可以得出demo里是對(duì)方法運(yùn)行前成功捕獲。為什么在thinkphp的controller里運(yùn)行就不成功呢。我決定采用斷點(diǎn)進(jìn)行調(diào)試。
通過(guò)斷點(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)過(guò)它,并且會(huì)對(duì)其判斷是否為內(nèi)部類,不是的都會(huì)進(jìn)入后續(xù)的操作。通過(guò)斷點(diǎn)進(jìn)入 FilterInjectorTransformer,發(fā)現(xiàn)會(huì)對(duì)load的文件進(jìn)行語(yǔ)法解析,并根據(jù)注冊(cè)的annotation對(duì)相關(guān)的類生成proxy類。說(shuō)道這,大家就明白了go-aop是如何做到切入你的程序了吧,生成的proxy類,可以在你配置的cache-dir(我配置的是./runtime/aop_cache/
)里看到。
同時(shí)./runtime/aop_cache/ 文件夾下也生成了很多東西,通過(guò)查看aop_cache文件內(nèi)產(chǎn)生了與Test1文件名相同的文件,打開(kāi)文件,發(fā)現(xiàn)它代理了原有的Test1控制器。這一系列信息,可以得出,Go!AOP 通過(guò)"劫持" composer autoload 讓每個(gè)類都進(jìn)過(guò)它,根據(jù)aspect的定義來(lái)決定是否為其創(chuàng)建一個(gè)代理類,并植入advice。
額,ThinkPHP5是把composer autoload里的東西copy出來(lái),放到自己autoload里,然后就沒(méi)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)過(guò) 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è)類型檢測(cè),檢測(cè)autoload callback是否為Classloader類型,然而ThinkPHP5不是,通過(guò)斷點(diǎn)你會(huì)發(fā)現(xiàn)ThinkPHP5是一個(gè)字符串?dāng)?shù)組,so,這里也就無(wú)法把go-aop注冊(cè)到class loader的callback當(dāng)中了。
這里就要提一下PHP autoload機(jī)制了,這是現(xiàn)代PHP非常重要的一個(gè)功能,它讓我們?cè)谟玫揭粋€(gè)類時(shí),通過(guò)名字能自動(dòng)加載文件。我們通過(guò)定義一定的類名規(guī)則與文件結(jié)構(gòu)目錄,再加上能實(shí)現(xiàn)以上規(guī)則的函數(shù)就能實(shí)現(xiàn)自動(dòng)加載了。在通過(guò) spl_autoload_register 函數(shù)的第三個(gè)參數(shù) prepend
設(shè)置為true,就能讓其排在在TP5的loader前面,先一步被調(diào)用。
依照如上原理,就可以做如下改進(jìn)
這個(gè)是為go-aop包裝的新autoload,本質(zhì)上是在原來(lái)的ThinkPHP5的loader上加了一個(gè)殼而已。
?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的類,則不進(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è)方法 \app\tests\behavior\Bootstrap::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)目?jī)?nèi)還有其他autloader,請(qǐng)注意他們的先后順序)。
最后
現(xiàn)在我們?cè)僭L問(wèn)一下 http://127.0.0.1/tests/test1/test1 你就能看到來(lái)自 aspect
輸出的信息了。
最后我們做個(gè)總結(jié):
PHP7
目前沒(méi)有拓展實(shí)現(xiàn)的 AOP
。
ThinkPHP5
有著自己的 Autoloader
。
Go!AOP
的AOP實(shí)現(xiàn)依賴Class Autoload
的 callback
,通過(guò)替換原文件
指向Proxy類
實(shí)現(xiàn)。
ThinkPHP5
整合 Go!AOP
需要調(diào)整 autoload
。
更多關(guān)于thinkPHP相關(guān)內(nèi)容感興趣的讀者可查看本站專題:《ThinkPHP入門教程》、《thinkPHP模板操作技巧總結(jié)》、《ThinkPHP常用方法總結(jié)》、《codeigniter入門教程》、《CI(CodeIgniter)框架進(jìn)階教程》、《Zend FrameWork框架入門教程》及《PHP模板技術(shù)總結(jié)》。
希望本文所述對(duì)大家基于ThinkPHP框架的PHP程序設(shè)計(jì)有所幫助。
您可能感興趣的文章:- ThinkPHP類似AOP思想的參數(shù)驗(yàn)證的實(shí)現(xiàn)方法
- php筆記之:AOP的應(yīng)用
- PHP三層結(jié)構(gòu)(下) PHP實(shí)現(xiàn)AOP
- PHP AOP教程案例