[原創(chuàng)]Swoft源碼剖析-Swoft中AOP的實現(xiàn)原理

AOP(面向切面編程)一方面是是開閉原則的良好實踐,你可以在不修改代碼的前提下為項目添加功能芹啥;更重要的是锻离,在面向?qū)ο笠酝猓峁┠懔硗庖环N思路去復(fù)用你的瑣碎代碼墓怀,并將其和你的業(yè)務(wù)代碼風(fēng)格開汽纠。

初探AOP

AOP是被Spring發(fā)揚光大的一個概念,在Java Web的圈子內(nèi)可謂無人不曉,但是在PHP圈內(nèi)其實現(xiàn)甚少傀履,因此很多PHPer對相關(guān)概念很陌生虱朵。且Swoft文檔直接說了一大堆術(shù)語如AOP切面,切面钓账、通知碴犬、連接點切入點梆暮,卻只給了一個關(guān)于Aspect(切面)的示例服协。沒有接觸過AOP的PHPer對于此肯定是一頭霧水的±泊猓考慮到這點我們先用一點小篇幅來談?wù)勏嚓P(guān)知識偿荷,熟悉的朋友可以直接往后跳。

基于實踐驅(qū)動學(xué)習(xí)的理念唠椭,這里我們先不談概念跳纳,先幫官網(wǎng)把示例補(bǔ)全。官方在文檔沒有提供完整的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()方法的返回值是否是符合預(yù)期
        $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";
    }

}

發(fā)現(xiàn)問題了沒?單元測試中$aopBean沒有顯式的使用編寫AOP相關(guān)代碼,而$aopBean->doAop()的返回值卻被改寫了斗塘。
這就是AOP的威力了,他可以以一種完全無感知無侵入的方式去拓展你的功能亮靴。但拓展代碼并不完全是AOP的目的逛拱,AOP的意義在于分離你的零碎關(guān)注點,以一種面向?qū)ο笸獾乃悸啡ソM織和復(fù)用你的各種零散邏輯。

AOP解決的問題是分散在引用各處的橫切關(guān)注點台猴。橫切關(guān)注點指的是分布于應(yīng)用中多處的功能,譬如日志,事務(wù)和安全饱狂。通常來說橫切關(guān)注點本身是和業(yè)務(wù)邏輯相分離的曹步,但按照傳統(tǒng)的編程方式,橫切關(guān)注點只能零散的嵌入到各個邏輯代碼中休讳。因此我們引入了AOP讲婚,他不僅提供一種集中式的方式去管理這些橫切關(guān)注點,而且分離了核心的業(yè)務(wù)代碼和橫切關(guān)注點俊柔,橫切關(guān)注點的修改不再需要修改核心代碼筹麸。

回到官方給的切面實例
<?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)類,一組被組織起來的橫切關(guān)注點雏婶。
@Before聲明了一個通知(Advice)方法物赶,即切面要干什么什么時候執(zhí)行
@PointBean聲明了一個切點(PointCut):即 切面(Aspect)在何處執(zhí)行通知(Advice)能匹配哪些連接點留晚。

關(guān)于AOP的更多知識可以閱讀<Spring實戰(zhàn)>

動態(tài)代理

代理模式

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

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

其常見實現(xiàn)的序列圖和類圖如下


序列圖.png
類圖.png

RealSubject是真正執(zhí)行操作的實體
Subject是從RealSubject中抽離出的抽象接口奖地,用于屏蔽具體的實現(xiàn)類
Proxy是代理,實現(xiàn)了Subject接口赋焕,一般會持有一個RealSubjecy實例参歹,將Client調(diào)用的方法委托給RealSubject真正執(zhí)行。

通過將真正執(zhí)行操作的對象委托給實現(xiàn)了Proxy能提供許多功能隆判。
遠(yuǎn)程代理(Remote Proxy/Ambassador):為一個不同地址空間的實例提供本地環(huán)境的代理犬庇,隱藏遠(yuǎn)程通信等復(fù)雜細(xì)節(jié)。
保護(hù)代理(Protection Proxy)對RealSubject的訪問提供權(quán)限控制等額外功能蜜氨。
虛代理(Virtual Proxy)根據(jù)實際需要創(chuàng)建開銷大的對象
智能引用(Smart Reference)可以在訪問對象時添加一些附件操作械筛。

更多可閱讀《設(shè)計模式 可復(fù)用面向?qū)ο筌浖幕A(chǔ)》的第四章

動態(tài)代理

一般而言我們使用的是靜態(tài)代理,即:在編譯期前通過手工或者自動化工具預(yù)先生成相關(guān)的代理類源碼飒炎。
這不僅大大的增加了開發(fā)成本和類的數(shù)量埋哟,而且缺少彈性。因此AOP一般使用的代理類都是在運行期動態(tài)生成的郎汪,也就是動態(tài)代理

Swoft中的AOP

回到Swoft,之所以示例中$aopBean的doAop()能被拓展的原因就是App::getBean(AopBean::class);返回的并不是AopBean的真正實例赤赊,而是一個持有AopBean對象的動態(tài)代理
Container->set()方法是App::getBean()底層實際創(chuàng)建bean的方法煞赢。

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

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

        //低相關(guān)code ....
        return $object;
    }

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

  • 調(diào)用對Bean的各個方法調(diào)用Aop->match();根據(jù)切面定義的切點獲取其合適的通知,并注冊到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;
            }

            //下面的代碼根據(jù)各個切面的@PointBean,@PointAnnotation,@PointExecution 進(jìn)行連接點匹配
            // 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))構(gòu)造一個動態(tài)代理
//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;
        //動態(tài)類直接繼承RealSubject
        $template
            = "class $proxyClassName extends $className {
            private \$hanadler;
            public function __construct(\$handler)
            {
                \$this->hanadler = \$handler;
            }
        ";
        // the template of methods
        //proxy類會重寫所有非static非構(gòu)造器函數(shù)照筑,將實現(xiàn)改為調(diào)用給$handler的invoke()函數(shù)
        $template .= self::getMethodsTemplate($reflectionMethods);
        $template .= "}";
        //通過動態(tài)生成的源碼構(gòu)造一個動態(tài)代理類吹截,并通過反射獲取動態(tài)代理的實例
        eval($template);
        $newRc = new \ReflectionClass($proxyClassName);

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

構(gòu)造動態(tài)代理需要一個Swoft\Proxy\Handler\HandlerInterface實例作為$handler參數(shù),AOP動態(tài)代理使用的是AopHandler瘦陈,其invoke()底層的關(guān)鍵操作為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 {
                    //調(diào)用后續(xù)切面
                    $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;
    }

通知的執(zhí)行(Aop->doPoint())也很簡單,構(gòu)造ProceedingJoinPoint,JoinPoint,Throwable對象波俄,并根據(jù)通知的參數(shù)聲明注入晨逝。

//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) {
            //用反射獲取參數(shù)類型,如果是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的整體實現(xiàn)原理了。

Swoft源碼剖析系列目錄:http://www.reibang.com/p/2f679e0b4d58

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末冬念,一起剝皮案震驚了整個濱河市趁窃,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌急前,老刑警劉巖醒陆,帶你破解...
    沈念sama閱讀 217,657評論 6 505
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異叔汁,居然都是意外死亡统求,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 92,889評論 3 394
  • 文/潘曉璐 我一進(jìn)店門据块,熙熙樓的掌柜王于貴愁眉苦臉地迎上來码邻,“玉大人,你說我怎么就攤上這事另假∠裎荩” “怎么了?”我有些...
    開封第一講書人閱讀 164,057評論 0 354
  • 文/不壞的土叔 我叫張陵边篮,是天一觀的道長己莺。 經(jīng)常有香客問我,道長戈轿,這世上最難降的妖魔是什么凌受? 我笑而不...
    開封第一講書人閱讀 58,509評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮思杯,結(jié)果婚禮上胜蛉,老公的妹妹穿的比我還像新娘。我一直安慰自己色乾,他們只是感情好誊册,可當(dāng)我...
    茶點故事閱讀 67,562評論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著暖璧,像睡著了一般案怯。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上澎办,一...
    開封第一講書人閱讀 51,443評論 1 302
  • 那天嘲碱,我揣著相機(jī)與錄音金砍,去河邊找鬼。 笑死麦锯,一個胖子當(dāng)著我的面吹牛捞魁,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播离咐,決...
    沈念sama閱讀 40,251評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼奉件!你這毒婦竟也來了宵蛀?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,129評論 0 276
  • 序言:老撾萬榮一對情侶失蹤县貌,失蹤者是張志新(化名)和其女友劉穎术陶,沒想到半個月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體煤痕,經(jīng)...
    沈念sama閱讀 45,561評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡梧宫,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,779評論 3 335
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了摆碉。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片塘匣。...
    茶點故事閱讀 39,902評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖巷帝,靈堂內(nèi)的尸體忽然破棺而出忌卤,到底是詐尸還是另有隱情,我是刑警寧澤楞泼,帶...
    沈念sama閱讀 35,621評論 5 345
  • 正文 年R本政府宣布驰徊,位于F島的核電站,受9級特大地震影響堕阔,放射性物質(zhì)發(fā)生泄漏棍厂。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,220評論 3 328
  • 文/蒙蒙 一超陆、第九天 我趴在偏房一處隱蔽的房頂上張望牺弹。 院中可真熱鬧,春花似錦侥猬、人聲如沸例驹。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,838評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽鹃锈。三九已至,卻和暖如春瞧预,著一層夾襖步出監(jiān)牢的瞬間屎债,已是汗流浹背仅政。 一陣腳步聲響...
    開封第一講書人閱讀 32,971評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機(jī)就差點兒被人妖公主榨干…… 1. 我叫王不留盆驹,地道東北人圆丹。 一個月前我還...
    沈念sama閱讀 48,025評論 2 370
  • 正文 我出身青樓,卻偏偏與公主長得像躯喇,于是被迫代替她去往敵國和親辫封。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點故事閱讀 44,843評論 2 354

推薦閱讀更多精彩內(nèi)容