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

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

初探AOP

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

基于實踐驅動學習的理念,這里我們先不談概念,先幫官網把示例補全。官方在文檔沒有提供完整的AOP Demo,但我們還是可以在單元測試中找得到的用法。

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

//Swoft\Test\Cases\AopTest.php
/**
 *
 *
 * @uses      AopTest
 * @version   2017年12月24日
 * @author    stelin <phpcrazy@126.com>
 * @copyright Copyright 2010-2016 swoft software
 * @license   PHP Version 7.x {@link http://www.php.net/license/3_0.txt}
 */
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()
 * @uses      AopBean
 * @version   2017年12月26日
 * @author    stelin <phpcrazy@126.com>
 * @copyright Copyright 2010-2016 swoft software
 * @license   PHP Version 7.x {@link http://www.php.net/license/3_0.txt}
 */
class AopBean
{
    public function doAop()
    {
        return "do aop";
    }

}

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

AOP解決的問題是分散在引用各處的橫切關注點橫切關注點指的是分布于應用中多處的功能,譬如日志,事務和安全。通常來說橫切關注點本身是和業務邏輯相分離的,但按照傳統的編程方式,橫切關注點只能零散的嵌入到各個邏輯代碼中。因此我們引入了AOP,他不僅提供一種集中式的方式去管理這些橫切關注點,而且分離了核心的業務代碼和橫切關注點,橫切關注點的修改不再需要修改核心代碼。

回到官方給的切面實例
<?php
//Swoft\Test\Testing\Aop\AllPointAspect.php
/**
 * the test of aspcet
 *
 * @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種設計模式中的其中一種。其定義為:

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

其常見實現的序列圖和類圖如下


序列圖.png
類圖.png

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

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

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

動態代理

一般而言我們使用的是靜態代理,即:在編譯期前通過手工或者自動化工具預先生成相關的代理類源碼。
這不僅大大的增加了開發成本和類的數量,而且缺少彈性。因此AOP一般使用的代理類都是在運行期動態生成的,也就是動態代理

Swoft中的AOP

回到Swoft,之所以示例中$aopBean的doAop()能被拓展的原因就是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源碼剖析系列目錄:http://www.lxweimin.com/p/2f679e0b4d58

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。

推薦閱讀更多精彩內容