13

Swoft 2 小白教程系列:AOP 详解

 4 years ago
source link: https://mp.weixin.qq.com/s/VYCjAkY_xRI1xSRhFak1vg
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

Swoft 微信技术交流

AOP 中有很多概念 例如(通知)(切面)(连接点)等等,但是作为一名刚入手 AOP 的同学想要理解这些概念其实是很困难的,本文以实战的方式介绍 AOP 不讨论概念,致力于让读者可以 顺利的在 swoft2 中使用AOP

AOP 说明

AOP 的操作对象是方法,所以不管哪种形式的声明,都是对相应的方法增加功能. 例如现在有个 A 方法.我们如果想在执行 A 方法之前做某些事,例如计算 A 方法开始的执行时间,然后再 A 方法执行后再进行某些操作,例如统计执行时间.这个时候我们就可以考虑 AOP ,AOP 可以让我们在不修改 A 方法本身,在 A 方法之前或者之后添加一系列操作逻辑.这样的好处显而易见,我们 A 方法只需要关注他本身自己的逻辑,而一些辅助操作,可以在 AOP 中完成,这样代码更易维护.主逻辑与辅助逻辑分离,可以更好的复用.

swoft2 中的 AOP

简单示例

文件 App/Aspect/MethodAspect.php

我们这里只给出了 方法AOP 的实例,其他 类AOP 和 注解AOP 的实例其实功能一样.大家只需要修改 @Point* 注解就可以了

<?php declare(strict_types=1);
 
namespace App\Aspect;
 
use Swoft\Aop\Annotation\Mapping\After;
use Swoft\Aop\Annotation\Mapping\AfterReturning;
use Swoft\Aop\Annotation\Mapping\AfterThrowing;
use Swoft\Aop\Annotation\Mapping\Around;
use Swoft\Aop\Annotation\Mapping\Aspect;
use Swoft\Aop\Annotation\Mapping\Before;
use Swoft\Aop\Annotation\Mapping\PointExecution;
use Swoft\Aop\Point\JoinPoint;
use Swoft\Aop\Point\ProceedingJoinPoint;
 
/**
* Class MethodAspect
*  @Aspect()
*  @PointExecution(
*     include={"HomeController::index"}
* )
* @since 2.0
*/
class MethodAspect
{
/**
* @Before()
*/
public function before()
{
 
echo "before方法调用";
}
 
/**
* @After()
* @param JoinPoint $joinPoint
*/
public function after(JoinPoint $joinPoint,$id)
{
var_dump($joinPoint->getTarget());
var_dump($joinPoint->getArgs());
var_dump($joinPoint->getMethod());
var_dump($joinPoint->getClassName());
echo "after方法调用";
}
 
/**
* @Around()
* @param ProceedingJoinPoint $joinPoint
* @return mixed
* @throws \Throwable
*/
public function around(ProceedingJoinPoint $joinPoint){
echo "around 前置方法调用\n";
$result = $joinPoint->proceed();//这里result 是HomeController::index的返回值
echo "around 后置方法调用\n";
return $result;
}
/**
* @AfterReturning()
* @param JoinPoint $joinPoint
* @return mixed
* @throws \Throwable
*/
public  function  afterReturning(JoinPoint $joinPoint,$id){
echo "afterReturning 方法调用";
return $joinPoint->getReturn()->withContent("afterReturning 方法调用");
}
/**
* @AfterThrowing()
* @return mixed
* @throws \Throwable
*/
public  function  afterThrowing($id){
echo "捕获到异常调用";
}
}

控制台输出

around 前置方法调用
 
before方法调用
 
around 后置方法调用
 
after方法调用
 
afterReturning 方法调用

代码说明

Aspect 类与任何其他正常的 bean 类似,并且可能像任何其他类一样拥有方法和字段,但必须使用 @Aspect 注解 (这个必须的)

@Aspect

定义一个类为切面类

参数

  1. order 优先级,多个切面,越小预先执行

声明切入点

swoft2中的 AOP 根据作用范围分为三种

给具体某个方法添加AOP#(PointExecution)

/*
* @PointExecution(
*     include={OrderService::createOrder,UserService::getUserBalance},
*     exclude={OrderService::generateOrder}
* )
*/

定义匹配切入点, 指明要代理目标类的哪些方法

include 定义需要切入的匹配集合,匹配的类方法,支持正则表达式

exclude 定义需要排序的匹配集合,匹配的类方法,支持正则表达式

注意 实体名称(类名)必须指定 namespace 完整路径 例如 ‘App\Controller\HomeController::index’ 或者先用 use 将目标类 use 进来

这里需要注意下,如果需要使用正则,则传入的必须使用双引号 “ “ 引起来,命名空间分隔符必须使用 \ 转义,同时双引号内必须是类的完整路径。

给类中的所有方法添加AOP#(PointBean)

/*
* @PointBean(
*     include={OrderService::class,UserService::class},
*     exclude={}
* )
*/

定义bean切入点, 这个bean类里的所有public方法执行都会经过此切面类的代理

include 定义需要切入的注解类名集合

exclude 定义需要排除的注解类名集合

注意 实体名称(类名)必须指定 namespace 完整路径 例如 ‘App\Http\Controller\HomeController’ 或者先用 use 将目标类 use 进来

给定义对应注解的方法添加AOP#(PointAnnotation)

/*
* @PointAnnotation(
*     include={RequestMapping::class},
*     exclude={}
* )
*/

定义注解类切入点, 所有包含使用了对应注解的方法都会经过此切面类的代理

include 定义需要切入的注解类名集合

exclude 定义需要排除的注解类名集合

注意 实体名称(类名)必须指定 namespace 完整路径 例如 ‘Swoft\Http\Server\Annotation\Mapping\RequestMapping::class’ 或者先用 use 将目标类 use 进来

@Pointbean、@Pointannotation、@Pointexecution 三种定义的关系是并集,三种里面定义的排除也是并集后在排除。建议为了便于理解和使用,一个切面类尽量只使用上面三个中的一个

AOP 注意事项

AOP只拦截 public 方法,不拦截private方法。

另外在 Swoft AOP 中 如果切入了多个方法,此时在某一个方法内调用了另一个被切入的方法,此时AOP 也会织入通知。例如 我们定义了一个类 A 它有两个 public 方法 fun1(),fun2(),然后我们定义一个切面,使用 @PointBean(include={A::class}) 注解将 A 类中的两个方法都进行切入,这是我们看下这两个方法的定义:

<?php
class A
{
function fun1()
{
echo 'fun1'."\n";
}
function fun2()
{
$this->fun1();
echo 'fun2'."\n";
}
}

此时如果我们访问 fun2() 这个方法,那么我们的切面就会执行两次,切面的执行顺序和方法的执行顺序相同。先执行 fun2() 方法的织入通知,再执行 fun1() 方法的织入通知。

代码解析-方法简单介绍

我们假设 AOP 的目标方法是 A 方法 以方便下面分析.并且 A 方法有一个参数叫 $id

注解 before 定义的方法没有参数,在 A方法执行前执行

如果定义了注解around方法,则执行顺序是 先执行输出 around 前置方法调用 , 在执行输出  before方法调

注解 around 定义方法在整个A方法前后都执行.

参数

  • ProceedingJoinPoint $joinPoint 注意这个参数和其他方法的区别

这个方法需要注意的是必须要调用 $joinPoint->proceed() 不然会出现A方法不执行的问题.这个操作的返回值,就是 A方法 的返回值并且我们可以修改A方法的实际参数,这个 proceed 函数可以传一个数组,数组对应了 A 方法的形参,我们可以动态修改参数, $joinPoint->getArgs()可以获得原始参数,在原始参数上加工,传给 proceed 方法

注解 after 定义方法 在执行A方法以后执行

参数

  • JoinPoint $joinPoint

注解 afterReturning 定义的方法,在执行A方法完成以后并且有返回值才执行的方法

参数

  • JoinPoint $joinPoint

注解 afterThrowing 定义方法 在A 方法抛出异常后执行的方法

方法注解说明

@Before:前置通知。在目标方法之前执行

@After:后置通知。在目标方法之后执行

@AfterReturing:返回通知

@AfterThrowing:异常通知。目标方法异常时执行

@Around:环绕通知。等同于前置通知加上后置通知,在目标方法之前及之后执行

关于 $joinPoint 常用方法说明

$joinPoint->getTarget() //返回操作的类 上面的例子是 HomeController
 
$joinPoint->getArgs() //返回参数 例如你在A函数注入 AOP 这里就返回A函数的调用参数
 
$joinPoint->getReturn() //返回A函数的返回值
 
$joinPoint->getMethod() //返回调用的方法名 上面的例子是 index
 
$joinPoint->setReturn("返回值") //修改返回值在下次 getreturn 时返回的就是修改过的返回值
 
$joinPoint->getClassName() //返回调用的类名 上面的例子是 "App\Http\Controller\HomeController"

执行流程

主方法正常执行流程

QraUf2J.png!web

主方法抛出异常的执行流程

uuU3IbM.png!web

多个AOP调用执行流程

yimIRjv.png!web

简单示例-计算方法执行时间

文件 app/Aspect/TimerAspect.php

<?php declare(strict_types=1);
 
namespace App\Aspect;
 
use App\Http\Controller\HomeController;
use Swoft\Aop\Annotation\Mapping\After;
use Swoft\Aop\Annotation\Mapping\AfterReturning;
use Swoft\Aop\Annotation\Mapping\AfterThrowing;
use Swoft\Aop\Annotation\Mapping\Around;
use Swoft\Aop\Annotation\Mapping\Aspect;
use Swoft\Aop\Annotation\Mapping\Before;
use Swoft\Aop\Annotation\Mapping\PointBean;
use Swoft\Aop\Point\JoinPoint;
use Swoft\Aop\Point\ProceedingJoinPoint;
 
/**
* Class AnnotationAspect
*  @Aspect()
*  @PointBean(
*     include={HomeController::class}
* )
* @since 2.0
*/
class TimerAspect
{
protected  start_time;
/**
* @Before()
*/
public function before($id)
{
$this->start_time=microtime(true);
}
 
/**
* @After()
* @param JoinPoint $joinPoint
*/
public function after(JoinPoint $joinPoint,$id)
{
$method = $joinPoint->getMethod();
$after = microtime(true);
$runtime = ($after - $this->start_time) * 1000;
echo "{$method} 方法,本次执行时间为: {$runtime}ms\n";
}
 
 
}

文件 app/Http/Controller/HomeController.php

<?php declare(strict_types=1);
 
namespace App\Http\Controller;
 
use App\Annotation\Mapping\MyAnnotation;
use App\Annotation\Mapping\MyAnnotation2;
use Swoft;
use Swoft\Http\Message\ContentType;
 
use Swoft\Http\Message\Response;
use Swoft\Http\Server\Annotation\Mapping\Controller;
use Swoft\Http\Server\Annotation\Mapping\RequestMapping;
use Swoft\View\Renderer;
use Swoole\Coroutine;
use Throwable;
 
/**
* Class HomeController
* @Controller()
*/
class HomeController{
/**
* @RequestMapping("/testapsect")
* @param string $name
* @return Response
*/
public function testapsect(string $name): Response
{
Coroutine::sleep(3);
return context()->getResponse()->withData(['ok']);
}
}

控制台输出

testapsect 方法,本次执行时间为: 3000.9479522705ms

Swoft 微信技术交流


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK