作者:
链接:來源:简书著作权归作者所有,本文已获得作者授权转载,并对原文进行了重新的排版。Swoft Github:前言
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.phpclass 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";
发现问题了没?单元测试中$aopBean
没有显式的使用编写AOP相关代码,而$aopBean->doAop()
的返回值却被改写了。
AOP
解决的问题是分散在引用各处的横切关注点
。横切关注点
指的是分布于应用中多处的功能,譬如日志,事务和安全。通常来说横切关注点本身是和业务逻辑相分离的,但按照传统的编程方式,横切关注点只能零散的嵌入到各个逻辑代码中。因此我们引入了AOP,他不仅提供一种集中式的方式去管理这些横切关注点,而且分离了核心的业务代码和横切关注点,横切关注点的修改不再需要修改核心代码。
回到官方给的切面实例
test .= ' before1 '; } //other code....}
上面的AllPointAspect
主要使用了3个注解去描述一个切面(Aspect)
关于AOP的更多知识可以阅读
动态代理
代理模式
代理模式(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源码剖析系列目录: