Hyperf AOP本质是运行时动态代理,非编译期织入;通过DI容器生成继承原类的Proxy Class,在方法中插入process()调用链,仅容器获取实例生效,依赖runtime/container/proxy/下生成的合法PHP文件。

Hyperf AOP 本质是运行时动态代理,不是编译期织入
Hyperf 的 AOP 没有修改原始类文件,也不依赖 PHP 扩展(如 uopz),而是靠启动时生成 Proxy Class 实现方法拦截。这个代理类继承原类,并重写被切面匹配的方法,在其中插入 process() 调用链。关键点在于:它不改变源码,只在容器管理的实例化路径中“偷偷替换成代理对象”。
常见错误现象:new SomeClass() 创建的实例不会走 AOP;只有通过 DI 容器 $container->get(SomeClass::class) 获取的才受切面控制。因为只有容器才知道该返回代理类还是原类。
- 代理触发条件:目标类必须被容器管理(即至少被
@Inject、@Controller或手动绑定过) - 代理生成时机:仅在首次
$container->get()且缓存未命中时,才会调用Hyperf\Di\Proxy\ProxyFactory动态生成 - 代理类命名规则:
Hyperf\Di\Proxy\SomeClassProxy,实际文件落盘在runtime/container/proxy/下
代理类怎么生成?靠反射 + 字符串拼接,不是 eval 或 runkit
Hyperf 不使用 eval() 或任何运行时代码执行函数,所有代理类都是合法 PHP 文件,内容由 Hyperf\Di\Proxy\ProxyGenerator 类拼接生成。它读取原始类的 ReflectionClass,提取构造函数参数、方法签名、父类信息,再按模板注入 ProceedingJoinPoint 调用逻辑。
典型生成逻辑包含:
- 构造函数里调用
parent::__construct(...),并保存原始实例引用 - 每个被切的方法里,先 new
ProceedingJoinPoint,再遍历匹配的Aspect列表,依次调用process() - 方法返回值直接透传,不额外包装
性能影响:生成过程 CPU 密集,但只发生一次(或缓存失效时)。后续 include 代理文件和普通类加载开销几乎一致。
缓存文件不是可选优化,而是 AOP 正常工作的前提
runtime/container/proxy/ 目录下的 .php 文件不是日志或调试产物,而是 AOP 能生效的必要中间件。如果该目录被清空或权限不足导致写入失败,Hyperf 启动时不会报错,但所有切面将静默失效——process() 根本不会被调用。
验证方式很简单:
- 检查
runtime/container/proxy/是否存在且可写 - 确认目标类对应代理文件是否生成(如
SomeClassProxy.php) - 在切面
process()中加var_dump('hit'),然后请求触发方法,看是否输出
容易踩的坑:docker run 部署时挂载了空目录覆盖 runtime/,或 CI 构建镜像后没保留生成的 proxy 文件,都会导致 AOP 失效且无提示。
为什么不能直接修改原类?PHP 的限制与 Hyperf 的取舍
PHP 不支持运行时修改已加载类的方法体(不像 Java 可用 ASM 或 Byte Buddy),也没有官方的 AST 修改 API。Hyperf 放弃“热替换”或“字节码注入”,选择最兼容、最可控的路径:把代理逻辑提前固化为文件。这带来两个硬性约束:
- 新增切面或修改
$classes数组后,必须清空runtime/container/proxy/并重启服务,否则旧代理仍生效 - 代理类无法处理静态方法(
static)、final 方法、private 方法——这些在 PHP 反射中不可被子类重写,因此也不会出现在代理中 - 如果原始类用了
__call()或魔术方法,代理类需显式转发,Hyperf 默认不自动处理,需手动在切面中判断
真正复杂的地方不在语法怎么写,而在于理解:AOP 生效与否,取决于“谁创建了实例”“代理文件是否存在”“类是否可被继承”这三者的交集。漏掉任意一环,切面就变成摆设。

















