Swoft source code analysis-implementation principle of AOP in sw oft

  php, swoole

Author:bromine
Links:https://www.jianshu.com/p/e13 …
Source: Simple Book
The copyright belongs to the author. This article has been reprinted with the authorization of the author and the original text has been rearranged.
Swoft Github:https://github.com/swoft-clou …

Preface

AOP(Aspect Oriented Programming) On the one hand, it is a good practice of open closed principle. You can add functions to the project without modifying the code. More importantly, besides object-oriented, it provides you with another way to reuse your trivial code and separate it from your business code style.

AOP

AOP is a concept carried forward by Spring. It is well known in Java Web circles, but it is seldom implemented in PHP circles, so many PHPer are unfamiliar with related concepts. And the Swoft document directly said a lot of terms such asAOPsectionsectionnoticeConnection pointEntry pointOnly one example of Aspect is given. PHPer, who has not been exposed to AOP, is definitely confused about this. Considering this point, let’s talk about the relevant knowledge with a little space, and familiar friends can jump back directly.

Based onPractice-driven learningWe won’t talk about the concept here, but we will help the official website to complete the example first. Official documents do not provide a complete AOP Demo, but we can still find usage in unit tests.

This is one of the unit tests of Aop. The purpose of this test is to checkAopTest->doAop()Is the return value of:
'do aop around-before2 before2 around-after2 afterReturn2 around-before1 before1 around-after1 afterReturn1 '

//Swoft\Test\Cases\AopTest.php
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);
    }
} 

The above test was usedAopBean::classThis Bean. This bean has a very simple method, doAop (), which directly returns a fixed string “do aop”;

<?php
//Swoft\Test\Testing\Aop\AopBean.php
/**
 * @Bean()
 */
class AopBean
{
    public function doAop()
    {
        return "do aop";
    }
}

Did you find the problem? Unit testing$aopBeanThere is no explicit use to write AOP-related code, while$aopBean->doAop()The return value of has been rewritten.
This is the power of AOP, which can expand your functions in a completely non-perceptual and non-invasive way. However, expanding code is not entirely the purpose of AOP. The significance of AOP lies in separating your fragmentary concerns and organizing and reusing your various fragmentary logics in an object-oriented way.

AOPThe problem to be solved is scattered throughout the citationCross cutting concerns.Cross cutting concernsIt refers to functions distributed in many places in the application, such as logs, transactions and security. Generally speaking, crosscutting concerns are separated from business logic, but according to the traditional programming method, crosscutting concerns can only be embedded into various logic codes in a scattered way. Therefore, we have introduced AOP, which not only provides a centralized way to manage these crosscutting concerns, but also separates the core business code and crosscutting concerns. The modification of crosscutting concerns no longer needs to modify the core code.

Returning to the official example of cutting plane

<?php
//Swoft\Test\Testing\Aop\AllPointAspect.php
/**
 * @Aspect()
 * @PointBean(
 *     include={AopBean::class},
 * )(Joinpoint)
 */
class AllPointAspect
{
    //other code....

    /**
     * @Before()
     */
    public function before()
    {
        $this->test .= ' before1 ';
    }

    //other code....
}

AboveAllPointAspectThree annotations are mainly used to describe an Aspect
@AspectDeclare that this is aAspectClass, a group of organized crosscutting concerns.
@BeforeA statement was madeAdviceMethod, namely the section toWhat to doAndWhen will it be implemented
@PointBeanA statement was madePointCut: AspectWhere to execute,AdviceWhich matchesConnection point.

More knowledge about AOP can be read< Spring actual combat >

Dynamic proxy

Agent mode

Proxy /SurrogateIt is one of 23 design modes of GOF system. It is defined as:

Provides a proxy for an object to control access to this object.

Common implementations ofSequence diagramAndClass diagramAs follows
Sequence diagram
10598194-4e5cf41ecfcd3a1e.png
Class diagram
10598194-3f82c88bb8edb30d.png

RealSubjectIs the entity that actually performs the operation
SubjectIt is an abstract interface separated from the RealSubject and used to mask specific implementation classes.
ProxyIs a proxy, implements the Subject interface, generally holds a RealSubject instance, and delegates the method called by the Client to realsubject for real execution.

Proxy can provide many functions by delegating the object that actually performs the operation to the implementation proxy.
Remote Proxy/Ambassador: Provide a proxy for the local environment for an instance of a different address space and hide complex details such as remote communication.
Protection ProxyAccess to RealSubject provides additional functions such as permission control.
Virtual ProxyCreate costly objects according to actual needs
Smart ReferenceYou can add some attachment actions when accessing objects.

More to readChapter 4 of the Basis of Reusable Object-Oriented Software for Design Patterns

Dynamic proxy

Generally speaking, we use static agents, i.e. the relevant agent class source code is generated in advance by manual or automated tools before the compilation period.
This not only greatly increases the development cost and the number of classes, but also lacks flexibility. Therefore, proxy classes commonly used in AOP are dynamically generated at runtime, that is, dynamic proxies

AOP in Swoft

Returning to Swoft, the reason why in the example$aopBeanThedoAop()The reason why it can be expanded is thatApp::getBean(AopBean::class)The return is not a real instance of AopBean, but one that holds an AopBean object.Dynamic proxy.
Container->set()The method isApp::getBean()The underlying method of actually creating the 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()There are two main operations of

  • Invoking Aop->match () for each method of Bean; According to the tangent point defined by the tangent plane, the appropriate notification is obtained and registered in 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'];
        }
    }
}
  • A dynamic proxy is constructed by proxy:: newproxystance (get _ class ($ object), newaopphandler ($ 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);
}

Constructing a dynamic proxy requires aSwoft\Proxy\Handler\HandlerInterfaceAs an example$handlerParameter, AOP dynamic proxy usesAopHandler, whichinvoke()The key operations at the bottom areAop->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;
}

noticeExecution of (Aop->doPoint()) is also very simple, construct processing joinpoint, joinpoint, throwable object, and according tonoticeParameter declaration injection for.

//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);
}

This is the overall implementation principle of AOP.

Swoft source code analysis series catalog:https://segmentfault.com/a/11 …