Swoft AOP 記錄使用者操作日誌

oddsu發表於2019-11-01

在Swoft框架中,使用AOP配合自定義註解可以方便的實現使用者操作的監控。

首先建立資料庫表和實體

在資料庫中建立一張t_log表,用於儲存使用者的操作日誌,資料庫採用mysql 5.7:

DROP TABLE IF EXISTS `t_log`;
CREATE TABLE `t_log` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '日誌ID',
  `username` varchar(50) DEFAULT NULL COMMENT '操作使用者',
  `operation` text COMMENT '操作內容',
  `time` decimal(11,0) DEFAULT NULL COMMENT '耗時',
  `method` text COMMENT '操作方法',
  `params` text COMMENT '方法引數',
  `ip` varchar(64) DEFAULT NULL COMMENT '操作者IP',
  `location` varchar(50) DEFAULT NULL COMMENT '操作地點',
  `created_at` timestamp NULL DEFAULT NULL,
  `updated_at` timestamp NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=1839 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC;

庫表對應的實體:

php bin/swoft entity:create t_log

自定義註解

定義一個方法級別的@Log註解,用於標註需要監控的方法:

namespace App\Annotation\Mapping;

use Doctrine\Common\Annotations\Annotation\Target;

/**
 * @since 2.0
 *
 * @Annotation
 *
 * @Target("METHOD")
 */
class Log
{

    /**
     * @var string
     */
    private $value = '';

    /**
     * @return string
     */
    public function getValue(): string
    {
        return $this->value;
    }

    /**
     * @param string $value
     */
    public function setValue(string $value)
    {
        $this->value = $value;
    }

    public function __construct($value)
    {
        if (isset($value['value'])) {
            $this->value = $value['value'];
        }
    }
}

定義解析器

namespace App\Annotation\Parser;

use App\Annotation\Mapping\Log;
use App\Exception\ApiException;
use Swoft\Annotation\Annotation\Mapping\AnnotationParser;
use Swoft\Annotation\Annotation\Parser\Parser;
/**
 * @since 2.0
 *
 * @AnnotationParser(Log::class)
 */
class LogParser extends  Parser
{
    /**
     * @param int $type
     * @param Log $annotationObject
     *
     * @return array
     * @throws ApiException
     */
    public function parse(int $type, $annotationObject): array
    {
        if ($type != self::TYPE_METHOD){
            return [];
        }
        LogRegister::registerLogs($this->className, $this->methodName, $annotationObject);
        return [];
    }
}

定義暫存器

namespace App\Annotation\Parser;

use App\Annotation\Mapping\Log;
use App\Exception\ApiException;
/**
 * @since 2.0
 */
class LogRegister
{
    private static $logs = [];
    /**
     * @param string $className
     * @param string $method
     * @param Log $log
     * @throws ApiException
     */
    public static function registerLogs(
        string $className,
        string $method,
        Log $log
    ) {
        if (isset(self::$logs[$className][$method])) {
            throw new ApiException(
                sprintf('`@log` must be only one on method(%s->%s)!', $className, $method)
            );
        }
        self::$logs[$className][$method] = [
            'value'   => $log->getValue(),
        ];
    }
    /**
     * @param string $className
     * @param string $method
     *
     * @return array
     */
    public static function getLogs(string $className, string $method): array
    {
        return self::$logs[$className][$method];
    }
}

切面和切點

定義一個LogAspect類,使用@Aspect標註讓其成為一個切面,切點為使用@Log註解標註的方法,使用@Around環繞通知:

namespace App\Aspect;

use App\Annotation\Parser\LogRegister;
use App\Exception\ApiException;
use App\Model\Entity\TLog;
use Swoft\Aop\Annotation\Mapping\Around;
use Swoft\Aop\Annotation\Mapping\Aspect;
use Swoft\Aop\Annotation\Mapping\Before;
use Swoft\Aop\Annotation\Mapping\PointAnnotation;
use App\Annotation\Mapping\Log;
use Swoft\Aop\Point\ProceedingJoinPoint;
use Swoft\Aop\Proxy;
use Swoft\Context\Context;

/**
 * @Aspect(order=1)
 * @PointAnnotation(
 *     include={Log::class}
 * )
 */
class LogAspect
{

    protected $start;

    /**
     * @Before()
     */
    public function before()
    {
        $this->start = milliseconds();
    }

    /**
     * @Around()
     *
     * @param ProceedingJoinPoint $joinPoint
     *
     * @return mixed
     * @throws ApiException
     */
    public function around(ProceedingJoinPoint $joinPoint)
    {
        // 執行方法
        $result = $joinPoint->proceed();
//        $args      = $joinPoint->getArgs();
        $target        = $joinPoint->getTarget();
        $method        = $joinPoint->getMethod();
        $className     = get_class($target);
        $className     = Proxy::getOriginalClassName($className);
        $value         = LogRegister::getLogs($className, $method);
        $request       = Context::get()->getRequest();
        $remoteAddress = $request->getServerParams()["remote_addr"];
        // 執行時長(毫秒)
        $time = milliseconds() - $this->start;
        // 儲存日誌
        $log = new TLog();
        $log->setUsername('test');
        $log->setOperation($value['value']);
        $log->setIp($remoteAddress);
        $log->setLocation('');
        $log->setMethod($className . '::' . $method . '()');
        $log->setTime($time);
        $log->setParams(json_encode($request->input()));
        $log->save();
        return $result;
    }
}

測試


namespace App\Http\Controller;

use App\Annotation\Mapping\Log;
use App\Exception\ApiException;
use Swoft\Co;
use Swoft\Http\Server\Annotation\Mapping\Controller;
use Swoft\Http\Server\Annotation\Mapping\RequestMapping;

/**
 * @Controller(prefix="/test")
 */
class TestController
{
    /**
     * @Log("方法一")
     * @RequestMapping(route="one")
     * @return string
     */
    public function methodOne(){
        return 'one';
    }
    /**
     * @Log(value="方法二")
     * @RequestMapping(route="two")
     * @return string
     * @throws ApiException
     */
    public function methodTwo(){
        Co::sleep(0.2);
        return 'two';
    }
}

啟動專案,分別訪問:

http://localhost:18306/test/one?type=test1

http://localhost:18306/test/two
示例git地址:https://github.com/nilocsu/swoft-aop-log.g...

本作品採用《CC 協議》,轉載必須註明作者和本文連結

相關文章