@inject默认注入单例,但若类被显式设为prototype作用域、手动注册为原型或被切面(如injectaspect)触发代理类生成,则每次注入均为新实例,导致“非单例”现象。

为什么@Inject注入的对象不是单例
因为 @Inject 默认行为是单例(Singleton),但你看到“不是单例”,大概率是类本身被显式配置成了原型(Prototype)作用域,或者被切面标记后触发了代理类的特殊实例化逻辑。
Hyperf 的 @Inject 注解本身不决定生命周期;真正起效的是容器注册时绑定的作用域。默认扫描注册的类(如带 @Service 的类)会被注册为 Singleton,但如果你加了 scope=ScopeEnum::PROTOTYPE,那每次注入都会 new 一个新实例。
- 检查类上是否写了
#[Inject(scope=ScopeEnum::PROTOTYPE)]—— 这会覆盖默认单例行为 - 检查是否在
config/autoload/dependencies.php中用$container->prototype()或等价方式手动注册过该类 - 确认该类是否被任何 Aspect(比如自定义日志、事务切面)命中:只要类被切面介入,Hyperf 就会为其生成代理类,而代理类的构造过程可能绕过常规单例缓存逻辑
如何确认某个类当前注册的作用域
最直接的方式是运行命令查看容器定义:
php bin/hyperf.php di:dump | grep "YourServiceClass"
输出中若出现 prototype 字样,说明它确实是原型作用域;若无特别标注,默认是 singleton。注意:这个命令输出的是当前 DI 容器已加载的定义,不是源码注释。
-
di:dump不会显示未被扫描到的类(比如没加@Service且没在dependencies.php显式绑定的类) - 如果类只靠属性上的
@Inject被间接引用,它仍会被自动注册,但作用域始终是 Singleton —— 除非你主动改过 - 运行
di:clear && di:dump确保缓存不是旧的
@Inject 在 new 实例时为什么还能生效
这不是“注入发生在 new 之后”,而是你根本没调用原始类的 new —— Hyperf 拦截了它。
当你写 new UserService(),实际创建的是 UserServiceProxy(位于 runtime/container/proxy/),这个代理类的构造函数里已经完成了依赖解析和属性赋值。所以即使你绕过容器手动 new,只要类被 @Inject 标记过,就逃不开代理机制。
- 代理类只对被切面标记的类生成,
@Inject自身就是一个切面触发器(InjectAspect) - 如果类没被任何切面命中(比如纯 DTO、没注解的工具类),
new就是原生行为,@Inject完全不生效 - 这种代理机制导致“看似手动 new,实则自动注入”,容易误判生命周期
业务 Service 类为什么不该默认用单例
单例在协程环境下极易引发状态污染:一次请求设了 $this->uid = 1001,下个协程再进来,读到的还是 1001 —— 因为它们共享同一个实例。
数据库连接、Redis 客户端这类无状态资源适合 Singleton;但含用户上下文、临时计算状态的 Service 必须隔离。
- 用
#[Inject(scope=ScopeEnum::PROTOTYPE)]显式声明,比依赖默认更安全 - 构造函数注入 + 原型作用域是推荐组合,避免属性被意外复用
- 别指望“我只读不写”就用单例 —— 协程切换不可控,中间件、异常处理都可能跨协程残留状态
@Inject 看似简单,背后连着代理生成、切面注册、容器绑定三层逻辑。最容易忽略的是:你以为自己在 new 普通对象,其实早就进了代理流水线。











