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)能匹配哪些連接點。
動態代理
代理模式
代理模式(Proxy /Surrogate)是GOF系23種設計模式中的其中一種。其定義為:
為對象提供一個代理,以控制對這個對象的訪問。
其常見實現的序列圖和類圖如下
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