bigywater.tech
Spring · 7

作用域:单例里注入 prototype 的坑与 scoped proxy

单例里注入 prototype 为什么拿到的总是同一个?ObjectProvider、@Lookup、scoped proxy 各自怎么解决?scoped proxy 的代理是什么样?单例里放可变状态到底会怎样?这篇在 Spring Framework 7.0.9 上逐项量出来。

1,1,1
单例里直接 @Autowired 一个 prototype,连续三次拿到的 id
4 个
thread 作用域加 scoped proxy 时,主线程加 3 个线程一共创建的目标对象数
约 110 万
单例里 int 字段 n++,8 线程各 200000 次(期望 1600000)平均丢失的次数

直接注入 prototype

实测 · 本机执行输出
  (a) 单例 Naive 里 @Autowired 了 prototype 的 Proto:连续 3 次 use() 得到的 id → 1,1,1(同一个对象:创建 Naive 时注入的那一个)
  (b) ObjectProvider<Proto>.getObject():连续 3 次得到的 id → 2,3,4
      直接 getBean(Proto) 两次的 id → 5,6

单例只在创建时注入一次,之后一直是同一个对象。

另外三种办法

实测 · 本机执行输出
  (c) @Lookup:连续 3 次 use() 的 id → 1,2,3;LookupBean 的类 Main$LookupBean$$…(Spring 生成的 CGLIB 子类)
  (d) prototype + scoped proxy(TARGET_CLASS):注入的是代理?true;连续 3 次 p.id() → 1,2,3(每次方法调用都创建了新的目标对象);made=3

ObjectProvider 和 @Lookup 每次新对象;prototype 加 scoped proxy 是每次方法调用换对象。

thread 作用域

实测 · 本机执行输出
  单例 Holder 里的 ctx 是代理?true,实现了 ScopedObject?true
  主线程连续 3 次 id → 1,1,1
  另外 3 个线程各调用 2 次,拿到的 id(排序后)→ [2, 2, 3, 3, 4, 4];目标对象一共创建了 4 个(主线程 1 个 + 3 个线程各 1 个)

scoped proxy 把按线程变化的依赖变成稳定的引用:同线程同目标,不同线程不同目标。

代理类型

实测 · 本机执行输出
  proxyMode=INTERFACES,却按具体类 ThreadCtx 注入 → NoSuchBeanDefinitionException: No qualifying bean of type 'ThreadCtx' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}

INTERFACES 的代理不能按具体类注入。

单例的线程安全

实测 · 本机执行输出
  单例里的 int 字段 n++:期望 1600000,实际 407345,丢了 1192655 次;同一个单例里的 AtomicInteger:1600000
  单例里的 int 字段 n++:期望 1600000,实际 504079,丢了 1095921 次;同一个单例里的 AtomicInteger:1600000
  单例里的 int 字段 n++:期望 1600000,实际 581252,丢了 1018748 次;同一个单例里的 AtomicInteger:1600000

单例被所有线程共享,可变字段会丢更新。

分步演示

作用域与单例的状态
本步的对象

    本文用到的工具与边界

    • 本机 macOS 27 arm64 / OpenJDK 21.0.12(Homebrew) / Spring Boot 4.0.8(Spring Framework 7.0.9、Tomcat 11.0.24) / Spring Cloud 2025.1.3;用 javac 编译、java 直接运行,依赖由 Maven 从 Maven Central 解析;数字每次运行会有小幅抖动。用不带 Boot 的 Spring Framework 7.0.9;thread 作用域用 Spring 自带的 SimpleThreadScope(它不会在线程结束时销毁目标对象);request / session 作用域需要 Web 环境,这篇没有测;int 字段丢失次数每次运行不同,三次分别是 1192655、1095921、1018748
    • 没有验证:request / session / application 作用域;scoped proxy 的 TARGET_CLASS 在 thread 作用域下的行为;@Lookup 在 @Bean 方法返回的对象上是否生效;SimpleThreadScope 的内存泄漏风险(线程池线程不退出时目标对象一直不释放);Boot 里 @RefreshScope 的作用域(见后面的 Cloud 篇)
    京ICP备2026067502号-1京公网安备11010502063487号