Swoft2 框架精华教程:面向切面编程(Aspect)
- 作者: 刘杰
- 来源: 技术那些事
- 阅读:97
- 发布: 2025-06-17 11:04
- 最后更新: 2025-08-23 06:57
面向切面编程(AOP)框架是 Swoft 框架的关键组件之一。
面向方面编程需要将程序逻辑分解为称为所谓关注的不同部分。跨越应用程序多个点的功能称为横切关注点,这些横切关注点在概念上与应用程序的业务逻辑分离。有很多常见的很好的例子,例如日志记录,审计,声明式事务,安全性,缓存等等。
OOP中模块化的关键单元是类,而在AOP中,模块化的单元是方面。依赖注入可帮助您将应用程序对象与其他对象解耦,而AOP可帮助您将交叉关注与其影响的对象分离。AOP就像Perl,.NET,Java等编程语言中的触发器。Swoft AOP 组件提供拦截器来拦截应用程序。例如,执行方法时,可以在方法执行之前或之后添加额外的功能。
AOP术语
在我们开始使用AOP之前,让我们熟悉AOP的概念和术语。这些术语并非特定于 Swoft,而是与 AOP 相关。
Aspect
其实就是共有功能的实现。如日志切面、权限切面、事务切面等。在实际应用中通常是一个存放共有功能实现的普通PHP类(切面类),之所以能被AOP容器识别成切面,是在配置中指定的。
Join point
就是程序在运行过程中能够插入切面的地点。例如,方法调用、异常抛出或字段修改等,但Swoft只支持方法级的连接点。
Advice
是切面的具体实现。以 目标方法 (要被代理的方法)为参照点,根据放置的地方不同,可分为前置通知(Before)、后置通知(AfterReturning)、异常通知(AfterThrowing)、最终通知(After)与环绕通知(Around)5种。在实际应用中通常是指向切面类中的一个方法,具体属于哪类通知,同样是在配置中指定的。
Pointcut
用于定义通知应该切入到哪些连接点上。不同的通知通常需要切入到不同的连接点上,这种精准的匹配是由切入点的正则表达式来定义的。
Target object
目标被一个或多个方面提供建议。该对象将始终是代理对象,也称为建议对象。
切面声明
切入点声明
@PointBean(针对 Bean 中所有的方法)
- include 定义包含的目标 bean 的集合(bean 名称,或者类全名)
- exclude 定义排除的目标 bean 的集合(bean 名称,或者类全名)
示例:
通过 bean 的类名指定 bean 集合
php
/**
* Class PrintParamsAspect
*
* @Aspect()
* @PointBean(
* include={CategoryData::class, TextData::class}
* )
*/
通过 bean 的别名获取指定 bean 的集合
php
/**
* Class PrintParamsAspect
*
* @Aspect()
* @PointBean(
* include={"beanName"}
* )
*/
@PointAnnotation(针对使用了某个注解的类或方法)
- include 定义需要切入的匹配集合,匹配的注解类名(bean 类名,或者别名)
- exclude 定义需要排序的匹配集合,匹配的注解类名(bean 类名,或者别名)
php
use Swoft\Http\Server\Annotation\Mapping\Controller;
/**
* Class ViewAspect
* @Aspect()
* @PointAnnotation(include={Controller::class, 'aliasName'})
*/
@PointExecution(针对目标类的方法)
- include 定义需要切入的匹配集合,匹配的类方法,支持正则表达式
- exclude 定义需要排序的匹配集合,匹配的类方法,支持正则表达式
通过类方法指定不同的方法集合。
注意集合每个配置为字符串,不能用类似 Class::class
这种形式,因为注解进行解析的时候,无法识别 Class::method
这个为常量,会报错。
php
/**
* Class ViewAspect
* @Aspect()
* @PointExecution(include={"Renderer::renderContent"})
*/
使用正则表达式匹配方法,注意,类和方法都可以使用正则
根据双冒号,分为类和方法部分:
例如:Test::action
表示类路径中有 Test 的类,类其中有 public 方法 action 为目标切点
例如:Test::action.*
表示前缀类路径中有 Test,且方法前缀为 action 的方法需为目标切点
php
/**
* Class ViewAspect
* @Aspect()
* @PointExecution(include={"Renderer::renderContent"})
*/
示例
在指定切面方法中修改参数
around 方法因为包裹着真正的类执行代码,所以可以实际去修改其参数的具体值。其他通知方法只能读取相关数据,而无法对数据进行修改。
php
class Renderer {
/**
* 在 $data 参数内自动注入一个参数
* @param string $content
* @param array $data
* @param string|null $layout override default layout file
*
* @return string
* @throws Throwable
*/
public function renderContent(string $content, array $data = [], $layout = null): string
{
// Render layout
if ($layout = $layout ?: $this->layout) {
$main = $this->fetch($layout, $data);
$content = str_replace($this->placeholder, $content, $main);
}
return $content;
}
}
切面具体代码示例:
通过指定某个类的具体的方法(@PointExecution(include={"Renderer::renderContent"})
),来将自定义的代码进行织入。
php
/**
* Class ViewAspect
* @Aspect()
* @PointExecution(include={"Renderer::renderContent"})
*/
class ViewAspect
{
/**
* @Around()
* @param ProceedingJoinPoint $joinPoint
* @return mixed
* @throws \Throwable
*/
function around(ProceedingJoinPoint $joinPoint)
{
$args = $joinPoint->getArgs();
// @todo 将 seo 相关数据注入到页面.
// @todo 将 js/css 相关数据注入到模板中.
if (empty($args[1]['insert'])) {
$args[1]['insert'] = '123';
}
return $joinPoint->proceed($args);
}
}
Model 层方法执行后自动打印日志
通过指定具体的类(@PointBean(include={CategoryData::class, TextData::class})
),将其中所有的方法执行前,打印出参数的日志。
php
/**
* Class PrintParamsAspect
*
* @Aspect()
* @PointBean(
* include={CategoryData::class, TextData::class}
* )
*/
class PrintParamsAspect
{
/**
* @Before()
* @param JoinPoint $joinPoint
* @return void
*/
public function before(JoinPoint $joinPoint)
{
if (APP_DEBUG) {
CLog::debug(
'method: %s, params: %s',
$joinPoint->getMethod(),
var_export($joinPoint->getArgs(), true)
);
}
}
}