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)的序列圖和類圖如下
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