Swoft 源碼剖析 - Swoft 中 AOP 的實現原理

做者:bromine
連接:https://www.jianshu.com/p/e13...
來源:簡書
著做權歸做者全部,本文已得到做者受權轉載,並對原文進行了從新的排版。
Swoft Github: https://github.com/swoft-clou...php

前言

AOP(面向切面編程)一方面是是開閉原則的良好實踐,你能夠在不修改代碼的前提下爲項目添加功能;更重要的是,在面向對象之外,他提供你另一種思路去複用你的瑣碎代碼,並將其和你的業務代碼風格開。git

初探AOP

AOP是被Spring發揚光大的一個概念,在Java Web的圈子內可謂無人不曉,可是在PHP圈內其實現甚少,所以不少PHPer對相關概念很陌生。且Swoft文檔直接說了一大堆術語如AOP切面切面通知鏈接點切入點,卻只給了一個關於Aspect(切面)的示例。沒有接觸過AOP的PHPer對於此確定是一頭霧水的。考慮到這點咱們先用一點小篇幅來談談相關知識,熟悉的朋友能夠直接日後跳。github

基於實踐驅動學習的理念,這裏咱們先不談概念,先幫官網把示例補全。官方在文檔沒有提供完整的AOP Demo,但咱們仍是能夠在單元測試中找獲得的用法。編程

這裏是Aop的其中一個單元測試,這個測試的目的是檢查AopTest->doAop()的返回值是不是:
'do aop around-before2 before2 around-after2 afterReturn2 around-before1 before1 around-after1 afterReturn1 'segmentfault

//Swoft\Test\Cases\AopTest.php
class AopTest extends TestCase
{
    public function testAllAdvice()
    {
        /* @var \Swoft\Testing\Aop\AopBean $aopBean*/
        $aopBean = App::getBean(AopBean::class);
        $result = $aopBean->doAop();
        //此處是PHPUnit的斷言語法,他判斷AopBean Bean的doAop()方法的返回值是不是符合預期
        $this->assertEquals('do aop around-before2  before2  around-after2  afterReturn2  around-before1  before1  around-after1  afterReturn1 ', $result);
    }
}

上面的測試使用到了AopBean::class這個Bean。這個bean有一個很簡單的方法doAop(),直接返回一串固定的字符串"do aop";設計模式

<?php
//Swoft\Test\Testing\Aop\AopBean.php
/**
 * @Bean()
 */
class AopBean
{
    public function doAop()
    {
        return "do aop";
    }
}

發現問題了沒?單元測試中$aopBean沒有顯式的使用編寫AOP相關代碼,而$aopBean->doAop()的返回值卻被改寫了。
這就是AOP的威力了,他能夠以一種徹底無感知無侵入的方式去拓展你的功能。但拓展代碼並不徹底是AOP的目的,AOP的意義在於分離你的零碎關注點,以一種面向對象外的思路去組織和複用你的各類零散邏輯。安全

AOP解決的問題是分散在引用各處的橫切關注點橫切關注點指的是分佈於應用中多處的功能,譬如日誌,事務和安全。一般來講橫切關注點自己是和業務邏輯相分離的,但按照傳統的編程方式,橫切關注點只能零散的嵌入到各個邏輯代碼中。所以咱們引入了AOP,他不只提供一種集中式的方式去管理這些橫切關注點,並且分離了核心的業務代碼和橫切關注點,橫切關注點的修改再也不須要修改核心代碼。函數

回到官方給的切面實例工具

<?php
//Swoft\Test\Testing\Aop\AllPointAspect.php
/**
 * @Aspect()
 * @PointBean(
 *     include={AopBean::class},
 * )(Joinpoint)
 */
class AllPointAspect
{
    //other code....

    /**
     * @Before()
     */
    public function before()
    {
        $this->test .= ' before1 ';
    }

    //other code....
}

上面的AllPointAspect主要使用了3個註解去描述一個切面(Aspect)
@Aspect聲明這是一個切面(Aspect)類,一組被組織起來的橫切關注點。
@Before聲明瞭一個通知(Advice)方法,即切面要幹什麼何時執行
@PointBean聲明瞭一個切點(PointCut):即 切面(Aspect)在何處執行通知(Advice)能匹配哪些鏈接點單元測試

關於AOP的更多知識能夠閱讀<Spring實戰>

動態代理

代理模式

代理模式(Proxy /Surrogate)是GOF系23種設計模式中的其中一種。其定義爲:

爲對象提供一個代理,以控制對這個對象的訪問。

其常見實現的序列圖類圖以下
序列圖
10598194-4e5cf41ecfcd3a1e.png
類圖
10598194-3f82c88bb8edb30d.png

RealSubject是真正執行操做的實體
Subject是從RealSubject中抽離出的抽象接口,用於屏蔽具體的實現類
Proxy是代理,實現了Subject接口,通常會持有一個RealSubjecy實例,將Client調用的方法委託給RealSubject真正執行。

經過將真正執行操做的對象委託給實現了Proxy能提供許多功能。
遠程代理(Remote Proxy/Ambassador):爲一個不一樣地址空間的實例提供本地環境的代理,隱藏遠程通訊等複雜細節。
保護代理(Protection Proxy)對RealSubject的訪問提供權限控制等額外功能。
虛擬代理(Virtual Proxy)根據實際須要建立開銷大的對象
智能引用(Smart Reference)能夠在訪問對象時添加一些附件操做。

更多可閱讀《設計模式 可複用面向對象軟件的基礎》的第四章

動態代理

通常而言咱們使用的是靜態代理,即:在編譯期前經過手工或者自動化工具預先生成相關的代理類源碼。
這不只大大的增長了開發成本和類的數量,並且缺乏彈性。所以AOP通常使用的代理類都是在運行期動態生成的,也就是動態代理

Swoft中的AOP

回到Swoft,之因此示例中$aopBeandoAop()能被拓展的緣由就是App::getBean(AopBean::class)返回的並非AopBean的真正實例,而是一個持有AopBean對象的動態代理
Container->set()方法是App::getBean()底層實際建立bean的方法。

//Swoft\Bean\Container.php
/**
 * 建立Bean
 *
 * @param string           $name             名稱
 * @param ObjectDefinition $objectDefinition bean定義
 * @return object
 * @throws \ReflectionException
 * @throws \InvalidArgumentException
 */
private function set(string $name, ObjectDefinition $objectDefinition)
{
    //低相關code...

    //注意此處,在返回前使用了一個Aop動態代理對象包裝並替換實際對象,因此咱們拿到的Bean都是Proxy
    if (!$object instanceof AopInterface) {
        $object = $this->proxyBean($name, $className, $object);//
    }

    //低相關code ....
    return $object;
}

Container->proxyBean()的主要操做有兩個

  • 調用對Bean的各個方法調用Aop->match();根據切面定義的切點獲取其合適的通知,並註冊到Aop->map中
//Swoft\Aop\Aop.php
/**
 * Match aop
 *
 * @param string $beanName    Bean name
 * @param string $class       Class name
 * @param string $method      Method name
 * @param array  $annotations The annotations of method
 */
public function match(string $beanName, string $class, string $method, array $annotations)
{
    foreach ($this->aspects as $aspectClass => $aspect) {
        if (! isset($aspect['point']) || ! isset($aspect['advice'])) {
            continue;
        }

        //下面的代碼根據各個切面的@PointBean,@PointAnnotation,@PointExecution 進行鏈接點匹配
        // Include
        $pointBeanInclude = $aspect['point']['bean']['include'] ?? [];
        $pointAnnotationInclude = $aspect['point']['annotation']['include'] ?? [];
        $pointExecutionInclude = $aspect['point']['execution']['include'] ?? [];

        // Exclude
        $pointBeanExclude = $aspect['point']['bean']['exclude'] ?? [];
        $pointAnnotationExclude = $aspect['point']['annotation']['exclude'] ?? [];
        $pointExecutionExclude = $aspect['point']['execution']['exclude'] ?? [];

        $includeMath = $this->matchBeanAndAnnotation([$beanName], $pointBeanInclude) || $this->matchBeanAndAnnotation($annotations, $pointAnnotationInclude) || $this->matchExecution($class, $method, $pointExecutionInclude);

        $excludeMath = $this->matchBeanAndAnnotation([$beanName], $pointBeanExclude) || $this->matchBeanAndAnnotation($annotations, $pointAnnotationExclude) || $this->matchExecution($class, $method, $pointExecutionExclude);

        if ($includeMath && ! $excludeMath) {
            //註冊該方法級別的鏈接點適配的各個通知
            $this->map[$class][$method][] = $aspect['advice'];
        }
    }
}
  • 經過Proxy::newProxyInstance(get_class($object),new AopHandler($object))構造一個動態代理
//Swoft\Proxy\Proxy.php
/**
 * return a proxy instance
 *
 * @param string           $className
 * @param HandlerInterface $handler
 *
 * @return object
 */
public static function newProxyInstance(string $className, HandlerInterface $handler)
{
    $reflectionClass   = new \ReflectionClass($className);
    $reflectionMethods = $reflectionClass->getMethods(\ReflectionMethod::IS_PUBLIC | \ReflectionMethod::IS_PROTECTED);

    // the template of methods
    $id             = uniqid();
    $proxyClassName = basename(str_replace("\\", '/', $className));
    $proxyClassName = $proxyClassName . "_" . $id;
    //動態類直接繼承RealSubject
    $template
        = "class $proxyClassName extends $className {
        private \$hanadler;
        public function __construct(\$handler)
        {
            \$this->hanadler = \$handler;
        }
    ";
    // the template of methods
    //proxy類會重寫全部非static非構造器函數,將實現改成調用給$handler的invoke()函數
    $template .= self::getMethodsTemplate($reflectionMethods);
    $template .= "}";
    //經過動態生成的源碼構造一個動態代理類,並經過反射獲取動態代理的實例
    eval($template);
    $newRc = new \ReflectionClass($proxyClassName);

    return $newRc->newInstance($handler);
}

構造動態代理須要一個Swoft\Proxy\Handler\HandlerInterface實例做爲$handler參數,AOP動態代理使用的是AopHandler,其invoke()底層的關鍵操做爲Aop->doAdvice()

//Swoft\Aop\Aop.php
/**
 * @param object $target  Origin object
 * @param string $method  The execution method
 * @param array  $params  The parameters of execution method
 * @param array  $advices The advices of this object method
 * @return mixed
 * @throws \ReflectionException|Throwable
 */
public function doAdvice($target, string $method, array $params, array $advices)
{
    $result = null;
    $advice = array_shift($advices);

    try {

        // Around通知條用
        if (isset($advice['around']) && ! empty($advice['around'])) {
            $result = $this->doPoint($advice['around'], $target, $method, $params, $advice, $advices);
        } else {
            // Before
            if ($advice['before'] && ! empty($advice['before'])) {
                // The result of before point will not effect origin object method
                $this->doPoint($advice['before'], $target, $method, $params, $advice, $advices);
            }
            if (0 === \count($advices)) {
                 //委託請求給Realsuject
                $result = $target->$method(...$params);
            } else {
                //調用後續切面
                $this->doAdvice($target, $method, $params, $advices);
            }
        }

        // After
        if (isset($advice['after']) && ! empty($advice['after'])) {
            $this->doPoint($advice['after'], $target, $method, $params, $advice, $advices, $result);
        }
    } catch (Throwable $t) {
        if (isset($advice['afterThrowing']) && ! empty($advice['afterThrowing'])) {
            return $this->doPoint($advice['afterThrowing'], $target, $method, $params, $advice, $advices, null, $t);
        } else {
            throw $t;
        }
    }

    // afterReturning
    if (isset($advice['afterReturning']) && ! empty($advice['afterReturning'])) {
        return $this->doPoint($advice['afterReturning'], $target, $method, $params, $advice, $advices, $result);
    }

    return $result;
}

通知的執行(Aop->doPoint())也很簡單,構造ProceedingJoinPoint,JoinPoint,Throwable對象,並根據通知的參數聲明注入。

//Swoft\Aop\Aop.php
/**
 * Do pointcut
 *
 * @param array  $pointAdvice the pointcut advice
 * @param object $target      Origin object
 * @param string $method      The execution method
 * @param array  $args        The parameters of execution method
 * @param array  $advice      the advice of pointcut
 * @param array  $advices     The advices of this object method
 * @param mixed  $return
 * @param Throwable $catch    The  Throwable object caught
 * @return mixed
 * @throws \ReflectionException
 */
private function doPoint(
    array $pointAdvice,
    $target,
    string $method,
    array $args,
    array $advice,
    array $advices,
    $return = null,
    Throwable $catch = null
) {
    list($aspectClass, $aspectMethod) = $pointAdvice;

    $reflectionClass = new \ReflectionClass($aspectClass);
    $reflectionMethod = $reflectionClass->getMethod($aspectMethod);
    $reflectionParameters = $reflectionMethod->getParameters();

    // Bind the param of method
    $aspectArgs = [];
    foreach ($reflectionParameters as $reflectionParameter) {
        //用反射獲取參數類型,若是是JoinPoint,ProceedingJoinPoint,或特定Throwable,則注入,不然直接傳null
        $parameterType = $reflectionParameter->getType();
        if ($parameterType === null) {
            $aspectArgs[] = null;
            continue;
        }

        // JoinPoint object
        $type = $parameterType->__toString();
        if ($type === JoinPoint::class) {
            $aspectArgs[] = new JoinPoint($target, $method, $args, $return, $catch);
            continue;
        }

        // ProceedingJoinPoint object
        if ($type === ProceedingJoinPoint::class) {
            $aspectArgs[] = new ProceedingJoinPoint($target, $method, $args, $advice, $advices);
            continue;
        }
        
        //Throwable object
        if (isset($catch) && $catch instanceof $type) {
            $aspectArgs[] = $catch;
            continue;
        }
        $aspectArgs[] = null;
    }

    $aspect = \bean($aspectClass);

    return $aspect->$aspectMethod(...$aspectArgs);
}

以上就是AOP的總體實現原理了。

Swoft源碼剖析系列目錄: https://segmentfault.com/a/11...
相關文章
相關標籤/搜索