如何快速熟悉项目

如何快速上手一个项目,是每个程序员都会遇到的问题,我来分享一下,我是如何做的

前言

现在好多文章,在教大家如何面试,跳槽。跳了之后呢,如何快速熟悉项目,通过试用期呢。

大家在入职都会面对一个问题,就是熟悉项目。那么熟悉

相信大家都在看到一座代码大(shi)山放到自己面前,都不知从何下手。如果有老同事讲解,健全的文档供查阅,那么入手起来就很方便。但就我入职以来多年经验,大部分公司的项目文档不全。你一入职,老同事就甩手(对,你是去接锅的);除了HR姐姐的入职培训,没有更多的信息;这种情况下,该如何快速入手项目呢。

这里代码部分拿PHP项目举例,其他语言的读者也可借鉴思想。

何谓熟悉了项目

一个项目,它的存在就是为了解决问题,那么熟悉项目就是,了解他能解决什么问题。

那么就需要向这个 项目提问–发起请求。再看我们能得到什么样的回答–响应。

那么我们能提什么问题呢?比如,Android APP问:现在手机手机壳是什么颜色?我要根据这个颜色变换壁纸。显然向项目提出这样的我问题,项目是不能回答的。

那么我们得到了什么回答(响应)呢,我得到了它返回给我的参数。哦不仅仅是这些,这背后它查阅了什么数据,或者更改了什么数据,还给其他项目发送了请求,或者消息,这都是这项目的响应。

所以熟悉项目我们要知道:

  • 可以解决什么问题
  • 解决问题过程中做了什么?

把项目当黑盒

这里我们抽象一下,参考 冯·诺伊曼结构

冯·诺伊曼结构-百度百科图

不论我们的http服务,Api服务,Rpc服务,都可以抽象成如上模型。

http服务的输入就是http请求,运算器是php-fpm程序,控制其表现的是php代码,储存是磁盘,内存,或是redis,输出是http响应

Rpc服务的输入就是TCP报文, 运算器是php常驻程序。

MySQL服务也能这样抽象,输入是MySQL格式编码的TCP报文,运算器,控制器是MySQL程序,存储是磁盘,内存。输出也是TCP报文, 然后在php程序解码出数据。

那么熟悉项目就是可抽象为:

  • 了解系统的I/O

什么是输入输出

一个项目运算器,控制器,就是这个项目的程序代码。存储器就是数据。这就好比一个线团,不知从哪开始理,如果你立马就能看懂咋回事,那么这篇文字你看到这里就可以关了。

在抽丝剥茧时,我们会先找到一个线头作为开始,而代码,就需要找到输入。

网页,APP是项目里所见即所得的输入,也是最容易理解的,从这个方向输入可以看到用户用例。

php我么这里把它作为运算器,php代码作为控制器。

php内部自己实现了一个Zend虚拟机,能编译php代码成opcode并运行。可以把它比做运算器

产生的日志,图片等文件写入,作为项目里面的存储器

数据,不论本地还是远程的,我们都作为输出。(输出数据保存,输出数据查询请求)。特别是关系型数据库,从关系结构可推测出系统建模。

一个具体的项目对用户提供服务,在多服务的架构里,同样它也会是一个请求发出者,此时它发出的请求作为输出。例如:Rpc请求,Redis操作。

自己熟悉项目时,第一时间就开始啃代码是很难的,这是把它当作一个黑盒,从什么样输入,会得到什么样的输出,开始理解系统,只有这中间代码做了什么不用第一时间关心。

就像TDD一样,接到的是需求,不那么关心中间实现过程。这样的参数输入能得到那样的数据返还,这个代码的happy-path就算过关了。

所以接下来,我开始从项目,输入,输出,存储开始给大家讲解快速熟悉项目。

输入

有哪些输入呢?

http请求,tcpwebsocket, 自定义rpc请求。还有消息,cron都是输入。

这里我联想到ServerLess,只关心输入输出

输出

SQL,Redis, Curl, RPC,文件写入,应用对外发出的请求都是输出。

配置日志记录

Http 日志

  • 在浏览器端开启请求记录

例:

这里只是抓了http请求,还有个websocket容易被忽略,可以通过WS标签过滤查看。

在浏览器端开启产看http请求是相较于抓包,避免了添加https证书问题。

  • 在服务端开启日志记录
## file: /etc/nginx/nginx.conf

http {
...
    ## 这是默认的日志
    log_format main '$remote_addr - $remote_user [$time_local] '
                       '"$request" $status $bytes_sent '
                       '"$http_referer" "$http_user_agent" "$gzip_ratio"';
    ##  注意这里新增了一个 $request_body
    log_format log_requets '$remote_addr - $remote_user [$time_local] '
                       '"$request" "$request_body" $status $bytes_sent '
                       '"$http_referer" "$http_user_agent" "$gzip_ratio"';
...
}

nginxaccess_log会记录GET请求以及参数,POST请求就需要额外配置,这里我们新增了一种日志格式——log_requets

## file: /etc/nginx/conf.d/www.conf
server
{
...
    root  /usr/share/nginx/html/xiunobbs/;

    ## 这里使用新的格式日志
    access_log  /var/log/nginx/access_xiuno.log log_requests;
    error_log /var/log/nginx/error_xiuno.log;
...

例:

图中最后一行POST,日志请求记录了提交的内容。

消息队列和Cron输入

消息队列一般要通过查日志记录来查阅。cron这块除了一般的Linux crontab,还有一些专门的cron管理工具。这点需要询问IT运维部门得知。

SQL日志

MySQL举例,数据库软件会有日志记录,为了debug,我们把一般的操作日志打开。这样就能,记录所有提交到该数据库的sql记录。

## file /etc/mysql/my.cnf
general_log_file        = /var/log/mysql/mysql.log
general_log             = 1
log_output              = FILE

配置好,重启数据库,再运行项目就能记录代码的SQL操作了。

例:

至于bin-log,这个只记录编辑操作,不记录查询操作。

Redis日志

monitorredis的调试命令,输入后,当前窗口会返回redis处理的每一个命令,它能帮助我们了解在redis上发生了什么操作。redis是没有任何配置能够将操作命令记录到日志。

Redis 能够记录一些服务状态改变日志,以及慢日志。

登陆到redis-cli,然后键入monitor,\n

这里我们可以用这个命令将日志操作日志记录到文件里。

nohup redis-cli monitor > /var/log/redis/redis_op.log 2>&1 &

Memecached日志

在启动memcaced是带上参数-vv

memcached -vv

通过如下配置把Memcached的日志记录到文件里。

memcached -m 64 -l 0.0.0.0 -p 11211 -u memcache -vv > /var/log/memcached.log 2>&1

RPC 日志

如果该Web项目依赖了RPC,需要也需要进行记录。

日志汇总

捕获到以上日志后,就能知道这个Web项目,接收到到了什么请求,查了什么DB,操作了Redis什么,请求了什么RPC

但是在3个窗口来回切换,有些不便,接下来我们想办法让他们在展示在一个窗口内。

tail -f -q /var/log/nginx/access_xiuno.log   /var/log/mysql/mysql.log /var/log/redis/redis_op.log /var/log/memcached.log

效果如下:

好的这里就是通过日志,配置请求来熟悉代码。

AOP日志

好了到这里,这篇文章就快结束了,什么,以上日志方式你都没发实现,Http网关,数据库,消息队列都是运维在负责,他们做的日志级别不够,或者没有收集。以上都搞不定。

那就要祭出大招,PHP-AOP,这个利器了。在不影响原代码逻辑,注解方式侵入代码,无需DBA,运维配合就能通过PHP代码,完成Requst,MySQL请求,Redis请求的日志记录。

详情看我的文章:PHP7中使用AOP

这里就不做展开了。哦,如果你要熟悉的代码是 函数式编程风格,那么这个Go!AOP就无能为力。动手该代码加日志吧。

文档和代码的关系

Jira/RedMind/Tower/禅道/Teambition 等也存有很多信息,不过这太依赖于公司制度和员工态度,有的人全是口头交流,软件只是用来记录状态的;有的员工事无巨细都记录在案,经手过的任务,都自带Wiki。

代码,于任务管理工具之间的联系,一般是在提交记录或分支名命名带有任务ID号。这样可以通过代码找到任务ID,再查阅当时的备注信息。

最后

通过如上操作,你已能够通过日志,了解到这段代码做了什么,梳理出主要业务。快速上手。至于参数校验,逻辑判断,最总都会落实到I/O操作的不同上。

祝愿你早日熟悉公司业务,扛起大梁。

ThinkPHP5 引入 Go AOP PHP AOP编程

ThinkPHP5,PHP7,要用AOP切面编程,没有现成的,一步到位的命令,需要怎么配置呢?什么你不知道什么是AOP,那还不来看看

项目背景

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

方案选择

  1. 重新分析需求,并改进代码
  2. 采用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都会很热心回复。

composer require goaop/framework

ThinkPHP5 对composer兼容挺不错的哦,(到后面,我真想揍ThinkPHP5作者)这就装好了,怎么用啊,git上的提示了简单用法。我也就照着写了个去切入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 核心服务配置

<?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

<?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 显示:

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技术栈。

  1. 假设这里有台Ubuntu你已经配置好了LNMP环境
  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)(/?.+)$;
    }
}

接下来要调整下代码

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

  1. 这个报错信息提示找不到这个类。来到报错的文件里。这文件使用了use找不到类,就是autoload出问题了,看到 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

在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/test1 你就能看到来自 aspect 输出的信息了。
最后我们做个总结:

  1. PHP7目前没有拓展实现的 AOP
  2. ThinkPHP5 有着自己的 Autoloader
  3. Go!AOP 的AOP实现依赖Class Autoloadcallback,通过替换原文件指向Proxy类实现。
  4. ThinkPHP5 整合 Go!AOP 需要调整 autoload