资讯专栏INFORMATION COLUMN

ThinkPHP5 引入 Go AOP

shadowbook / 2002人阅读

摘要:由于项目紧,人手不足,必须以最小的代价解决当前问题。引入作为一门好的语言,是不自带的。通过断点进入,发现会对的文件进行语法解析,并根据注册的对相关的类生成类。

项目背景

目前开发的WEB软件里有这一个功能,PHP访问API操作数据仓库,刚开始数据仓库小,没发现问题,随着数据越来越多,调用API时常超时(60s)。于是决定采用异步请求,改为60s能返回数据则返回,不能则返回一个异步ID,然后轮询是否完成统计任务。由于项目紧,人手不足,必须以最小的代价解决当前问题。

方案选择

从新分析需求,并改进代码

采用AOP方式改动程序

从新做需求分析,以及详细设计,并改动代码,需要产品,架构,前端,后端的支持。会惊动的人过多,在资源紧张的情况下是不推荐的。
采用AOP方式,不改动原有代码逻辑,只需要后端就能完成大部分任务了。后端用AOP切入请求API的方法,通过监听API返回的结果来控制是否让其继续运行原有的逻辑(API在60s返回了数据),或者是进入离线任务功能(API报告统计任务不能在60s内完成)。

实际环境

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。

aspectAction();
    }
    
    public function aspectAction()
    {
        return "hello";
    }
}

定义aspect

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()),
             "
"; } }

启用aspect

registerAspect(new MonitorAspect());
    }
}

go-aop 核心服务配置

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

 [],
    // 应用开始
    "app_begin"    => [],
    // 模块初始化
    "module_init"  => [
        "app	estsehaviorBootstrap"
    ],
    // 操作开始执行
    "action_begin" => [],
    // 视图内容过滤
    "view_filter"  => [],
    // 日志写入
    "log_write"    => [],
    // 应用结束
    "app_end"      => [],
];
兼容测试

好了,访问 http://127.0.0.1/tests/test1/... 显示:

hello

这不是预期的效果,在aspect定义了,访问该方法前,会输出方法的更多信息信息。
像如下内容才是预期

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

上他官方Doc看看,是一些更高级的用法。没有讲go-aop的运行机制。
上git上也没看到类似issue,额,发现作者经常在issue里回复:试一试demo。也许我该试试demo。

Run Demos

我采用的是LNMP技术栈。

假设这里有台Ubuntu你已经配置好了LNMP环境

下载代码

配置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)(/?.+)$;
    }
}

接下来要调整下代码

访问 http://127.0.0.1:8008 试试,(估计大家都遇到了这个)

这个报错信息提示找不到这个类。来到报错的文件里。这文件使用了use找不到类,就是autoload出问题了,看到 vendor/lisachenko/go-aop-php/demos/autoload.php 这个文件。

add("Demo", __DIR__);
}

可以看到这个代码第一行没找到vendor下的autoload。我们做如下调整

add("Demo", __DIR__);
}

再试试,demo运行起来了。


尝试了下,运行成功


通过以上的输出,可以得出demo里是对方法运行前成功捕获。为什么在thinkphp的controller里运行就不成功呢。我决定采用断点进行调试。

通过断点我发现了这个文件

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.
我们看看这个文件

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上加了一个壳而已。

getMethod("findFile");
        $method->setAccessible(true);
        $file = $method->invoke($obj, $class);
        return $file;
    }
}
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 Autoloadcallback,通过替换原文件指向Proxy类实现。

ThinkPHP5 整合 Go!AOP 需要调整 autoload

文章版权归作者所有,未经允许请勿转载,若此文章存在违规行为,您可以联系管理员删除。

转载请注明本文地址:https://www.ucloud.cn/yun/26291.html

相关文章

  • wamp环境安装thinkphp5.0-wamp环境配置vhosts

    摘要:环境集成环境文件目录目录入口文件第一步修改文件,将配置文件包含进来点击,用记事本打开,定位到此处。在第一个例子中只改动了和,这个配置保证了我们在开启后仍然可以访问目录。 环境windows7 , wampserver集成环境 , thinkphp5.0文件wamp目录:D:/wamp/wwwthinkphp目录:F:/tp5thinkphp入口文件:F:/tp5/public/inde...

    xeblog 评论0 收藏0
  • ThinkPhp学习笔记(一)

    摘要:如上为三个模块行为行为是在预先定义好的一个应用位置执行的一些操作。发生作用的位置称之为钩子,当应用程序运行到这个钩子的时候,就会被拦截下来,统一执行相关的行为。给某一个钩子绑定相关行为就成了一种类编程的思想。 ThinkPhp5.0 Composer: php世界里的包管理器 目录结构 project 应用部署目录 ├─application 应用目录(可设置) ...

    imtianx 评论0 收藏0
  • 【好好面试】学完Aop,连动态代理的原理都不懂?

    摘要:总结动态代理的相关原理已经讲解完毕,接下来让我们回答以下几个思考题。 【干货点】 此处是【好好面试】系列文的第12篇文章。文章目标主要是通过原理剖析的方式解答Aop动态代理的面试热点问题,通过一步步提出问题和了解原理的方式,我们可以记得更深更牢,进而解决被面试官卡住喉咙的情况。问题如下 SpringBoot默认代理类型是什么 为什么不用静态代理 JDK动态代理原理 CGLIB动态代理...

    Keven 评论0 收藏0
  • 解读 thinkphp5 源码(一):自动加载

    摘要:索性读一下它的源码。行载入类载入类,这个类比较重要,实现了自动加载。注册错误和异常处理机制加载惯例配置文件接下来我们看一下自动加载的实现方法。所以借助此函数可以达到自动加载。博客链接解读源码一自动加载 听说 TP5 已经 RC4 了,曾经在 RC3 的时候用它写过一个小东西。官方说从 RC4 以后改动不是太大。索性读一下它的源码。然后顺便记录一下,如有错漏,请路过大神多多指正! 入口 ...

    stormzhang 评论0 收藏0
  • axios的post请求后台(ThinkPHP5)接收不到数据

    摘要:最近做项目,做分页的功能,使用给后台发送数据,使用接口还是工具都可获取数据,唯独获取不到经过排除,发现这与的传参格式有关系在使用时,要注意到配置选项中包含和两者,以为他们是相同的,实则不然。因为是添加到的请求字符串中的,用于请求。 最近做vue项目,做分页的功能,使用post给后台发送数据,使用接口还是工具(postman)都可获取数据,唯独axios获取不到;经过排除,发现这与axi...

    nemo 评论0 收藏0

发表评论

0条评论

最新活动
阅读需要支付1元查看
<