AOP

介绍

首先,你不要看到 AOP 就感觉好难好复杂,看下去其实也就那样。而且在 IMI 中你也不一定需要用到AOP,这是非必须的。

AOP 的概念通过搜索引擎一定是看烦了,而且看了也没什么大卵用,不贴近实际。

我先举个 AOP 实际应用的简单例子,比如在写一个方法的时候,可能要针对某个方法写前置和后置操作,传统写法如下:

  1. abstract class ParentClass
  2. {
  3. public function test()
  4. {
  5. $this->__beforeTest();
  6. // 做一些事情...
  7. echo 'Parent->test()', PHP_EOL;
  8. $this->__afterTest();
  9. }
  10. public abstract function __beforeTest();
  11. public abstract function __afterTest();
  12. }
  13. class Child extends ParentClass
  14. {
  15. public function __beforeTest()
  16. {
  17. echo 'Child->__beforeTest()', PHP_EOL;
  18. }
  19. public function __afterTest()
  20. {
  21. echo 'Child->__afterTest()', PHP_EOL;
  22. }
  23. }
  24. $child = new Child;
  25. $child->test();

运行结果:

  1. Child->__beforeTest()
  2. Parent->test()
  3. Child->__afterTest()

这种写法你需要事先定义好前置和后置方法,如果需要前后置的方法一多,写起来会非常繁琐。

AOP 可以很好地解决这个问题,不仅可以在编写上不用事先定义这么多方法,还非常有助于解耦。

AOP 名词

切面 Aspect

普通的类,你要切入的类。

切入点 Pointcut

普通类中的方法,你要切入的方法。

连接点 Joinpoint

在这个方法相关的什么时机触发通知,比如:调用的前置后置、抛出异常等。

通知 Advice

在连接点触发的通知,比如在前置操作触发,通知里写前置的具体实现。

IMI 支持的通知点有:

@Before

前置操作

@After

后置操作

@Around

环绕操作。先触发环绕操作,在前置操作前和后置操作后,都可以做一些事情。甚至可以完全不让原方法运行,在环绕中实现该方法功能。

@AfterReturning

在原方法返回后触发,可以修改返回值

@AfterThrowing

在抛出异常后触发,允许设置allowdeny,设置允许和拒绝捕获的异常类

使用方法

使用注解注入方法

监听池子的资源获取和释放:

  1. <?php
  2. namespace Test;
  3. use Imi\Aop\JoinPoint;
  4. use Imi\Aop\Annotation\After;
  5. use Imi\Aop\Annotation\Aspect;
  6. use Imi\Aop\Annotation\PointCut;
  7. /**
  8. * @Aspect
  9. */
  10. class Pool
  11. {
  12. /**
  13. * @PointCut(
  14. * allow={
  15. * "Imi\*Pool*::getResource",
  16. * "Imi\*Pool*::release",
  17. * }
  18. * )
  19. * @After
  20. * @param JoinPoint $a
  21. * @return void
  22. */
  23. public function test(JoinPoint $joinPoint)
  24. {
  25. echo $joinPoint->getType() . ' ' . get_parent_class($joinPoint->getTarget()) . '::' . $joinPoint->getMethod() . '(): ' . $joinPoint->getTarget()->getFree() . '/' . $joinPoint->getTarget()->getCount() . PHP_EOL;
  26. }
  27. }

运行效果:

  1. after Imi\Redis\CoroutineRedisPool::getResource(): 0/1
  2. after Imi\Redis\CoroutineRedisPool::release(): 1/1

类名、方法名和命名空间没有要求,只要beanScan里能扫描到即可。

类注释中必须写@Aspect表名是一个切面类

方法中写@PointCut表示指定切入点,支持通配符

@After代表在该方法调用后触发

注入带有注解的方法

可参考imi\src\Db\Aop\TransactionAop.php文件:

  1. /**
  2. * @Aspect
  3. */
  4. class TransactionAop
  5. {
  6. /**
  7. * 自动事务支持
  8. * @PointCut(
  9. * type=PointCutType::ANNOTATION,
  10. * allow={
  11. * Transaction::class
  12. * }
  13. * )
  14. * @Around
  15. * @return mixed
  16. */
  17. public function parseTransaction(AroundJoinPoint $joinPoint)
  18. {
  19. }
  20. }

无论这个注解在方法上出现了几次,都只会触发一次注入处理

配置注入

实现代码

  1. namespace Test;
  2. use Imi\Aop\JoinPoint;
  3. class Test
  4. {
  5. /**
  6. * @param JoinPoint $a
  7. * @return void
  8. */
  9. public function test(JoinPoint $joinPoint)
  10. {
  11. echo $joinPoint->getType() . ' ' . get_parent_class($joinPoint->getTarget()) . '::' . $joinPoint->getMethod() . '(): ' . $joinPoint->getTarget()->getFree() . '/' . $joinPoint->getTarget()->getCount() . PHP_EOL;
  12. }
  13. }

对类没有任何要求,方法只需要参数对即可。

配置

  1. <?php
  2. return [
  3. // 类名
  4. \Test\Test::class => [
  5. // 固定写法methods
  6. 'methods' => [
  7. // 方法名
  8. 'test' => [
  9. // 指定切入点
  10. 'pointCut' => [
  11. 'allow' => [
  12. "Imi\*Pool*::getResource",
  13. "Imi\*Pool*::release",
  14. ]
  15. ],
  16. 'after' => [
  17. ]
  18. ]
  19. ]
  20. ],
  21. ];

所有注入演示

  1. <?php
  2. namespace Test;
  3. use Imi\Aop\JoinPoint;
  4. use Imi\Aop\AroundJoinPoint;
  5. use Imi\Aop\Annotation\After;
  6. use Imi\Aop\Annotation\Around;
  7. use Imi\Aop\Annotation\Aspect;
  8. use Imi\Aop\Annotation\Before;
  9. use Imi\Aop\Annotation\PointCut;
  10. use Imi\Aop\AfterThrowingJoinPoint;
  11. use Imi\Aop\AfterReturningJoinPoint;
  12. use Imi\Aop\Annotation\AfterThrowing;
  13. use Imi\Aop\Annotation\AfterReturning;
  14. /**
  15. * @Aspect
  16. */
  17. class Test
  18. {
  19. /**
  20. * 前置操作
  21. * @PointCut(
  22. * allow={
  23. * "ImiDemo\HttpDemo\MainServer\Model\Goods::getScore",
  24. * }
  25. * )
  26. * @Before
  27. * @param JoinPoint $a
  28. * @return void
  29. */
  30. public function before(JoinPoint $joinPoint)
  31. {
  32. // 修改参数
  33. // $joinPoint->setArgs(/*参数数组*/);
  34. echo 'getScore()-before', PHP_EOL;
  35. }
  36. /**
  37. * 后置操作
  38. * @PointCut(
  39. * allow={
  40. * "ImiDemo\HttpDemo\MainServer\Model\Goods::getScore",
  41. * }
  42. * )
  43. * @After
  44. * @param JoinPoint $a
  45. * @return void
  46. */
  47. public function after(JoinPoint $joinPoint)
  48. {
  49. echo 'getScore()-after', PHP_EOL;
  50. }
  51. /**
  52. * 环绕
  53. * @PointCut(
  54. * allow={
  55. * "ImiDemo\HttpDemo\MainServer\Model\Goods::getScore1",
  56. * }
  57. * )
  58. * @Around
  59. * @return mixed
  60. */
  61. public function around(AroundJoinPoint $joinPoint)
  62. {
  63. var_dump('调用前');
  64. // 调用原方法,获取返回值
  65. $result = $joinPoint->proceed();
  66. var_dump('调用后');
  67. return 'value'; // 无视原方法调用后的返回值,强制返回一个其它值
  68. return $result; // 返回原方法返回值
  69. }
  70. /**
  71. * 返回值
  72. * @PointCut(
  73. * allow={
  74. * "ImiDemo\HttpDemo\MainServer\Model\Goods::getScore",
  75. * }
  76. * )
  77. * @AfterReturning
  78. * @param AfterReturningJoinPoint $joinPoint
  79. * @return void
  80. */
  81. public function afterReturning(AfterReturningJoinPoint $joinPoint)
  82. {
  83. $joinPoint->setReturnValue('修改返回值');
  84. }
  85. /**
  86. * 异常捕获
  87. * @PointCut(
  88. * allow={
  89. * "ImiDemo\HttpDemo\MainServer\Model\Goods::getScore",
  90. * }
  91. * )
  92. * @AfterThrowing
  93. * @param AfterThrowingJoinPoint $joinPoint
  94. * @return void
  95. */
  96. public function afterThrowing(AfterThrowingJoinPoint $joinPoint)
  97. {
  98. // 异常不会被继续抛出
  99. $joinPoint->cancelThrow();
  100. var_dump('异常捕获:' . $joinPoint->getThrowable()->getMessage());
  101. }
  102. }

属性注入

如下代码例子,定义一个类,使用@Inject注解来注释属性,在通过getBean()实例化时,会自动给被注释的属性赋值相应的实例对象。

  1. namespace Test;
  2. class TestClass
  3. {
  4. /**
  5. * 某Model对象
  6. * @Inject("XXX\Model\User")
  7. */
  8. protected $model;
  9. public function test()
  10. {
  11. var_dump($model->toArray());
  12. }
  13. }
  14. $testClass = App::getBean('Test\TestClass');
  15. $testClass->test();

非 Bean 类使用属性注入

imi 提供了一个 Imi\Bean\Traits\TAutoInject 来让非 Bean 类也能够使用属性注入。也就是直接new对象,也可以自动注入属性。当然,这个类的命名空间必须在beanScan中配置,能被扫描到才可以正常注入。

无构造方法的类:

  1. namespace Test;
  2. use Imi\Aop\Annotation\Inject;
  3. class Test
  4. {
  5. use Imi\Bean\Traits\TAutoInject;
  6. /**
  7. * @Inject("XXX")
  8. */
  9. public $xxx;
  10. }
  11. $test = new Test;
  12. $test->xxx; // 会被自动注入,不用手动初始化

有构造方法的类:

  1. namespace Test;
  2. use Imi\Aop\Annotation\Inject;
  3. class Test
  4. {
  5. use Imi\Bean\Traits\TAutoInject;
  6. /**
  7. * @Inject("XXX")
  8. */
  9. public $xxx;
  10. private $value;
  11. public function __construct()
  12. {
  13. $this->__autoInject(); // 手动调用 __autoInject() 方法
  14. $this->value = 123;
  15. }
  16. }
  17. $test = new Test;
  18. $test->xxx; // 会被自动注入,不用手动初始化

方法参数注入

  1. /**
  2. * @InjectArg(name="a", value="123")
  3. * @InjectArg(name="b", value=@Inject("\ImiDemo\HttpDemo\MainServer\Model\User"))
  4. *
  5. * @return void
  6. */
  7. public function test($a, $b)
  8. {
  9. var_dump($a, $b);
  10. }

可以直接注入值,也可以使用值注入注解。