Spring 容器中的 Bean 作用域决定了实例的创建时机、缓存方式以及在不同请求或线程之间的可见性。Spring 原生支持 singleton、prototype、request、session、application、websocket 等作用域,但有时这些默认范围无法覆盖特殊业务,比如每个线程需要一份独立的 Bean 实例,或者每个租户上下文使用不同的数据源。Spring 并没有把作用域写死,而是提供了 Scope 接口和 ConfigurableBeanFactory#registerScope 扩展点。EnableScope 并不是 Spring 官方标准注解,而是一种常见的自定义扩展方式,它借助 @Import 和 ImportBeanDefinitionRegistrar 把作用域注册动作封装成注解开关,使业务代码只需要添加一个注解就能启用自定义作用域。

在 Spring Boot 应用中整合自定义作用域,核心要解决两件事:一是实现 Scope 接口,明确 Bean 在目标范围内的获取与销毁逻辑;二是通过 EnableScope 注解把该接口注册到 Spring 容器中,使后续定义的 Bean 可以使用 @Scope("名称") 引用它们。本文将以线程级作用域为例,逐步拆解实现和整合过程。
一、Spring 的 Bean 作用域体系与 EnableScope 的定位
Spring 框架在创建 Bean 时会根据作用域信息决定是复用已有实例还是重新创建。singleton 作用域在整个容器生命周期内只维护一个实例,适合无状态服务;prototype 作用域在每次注入或 getBean 调用时创建新实例,适合有状态的短期对象。对于 Web 场景,request 和 session 作用域分别把 Bean 实例绑定到 HTTP 请求和会话,由 Spring MVC 在底层维护线程上下文。
当默认作用域不够用时,可以通过实现 org.springframework.beans.factory.config.Scope 接口来扩展。Scope 接口包含 get、remove、registerDestructionCallback、resolveContextualObject 和 getConversationId 五个方法。其中 get 方法负责从当前上下文获取对象,如果不存在则使用 ObjectFactory 创建并缓存;remove 负责从当前上下文移除对象。实现完 Scope 接口后,需要调用 ConfigurableBeanFactory#registerScope 将其注册到容器,并指定一个作用域名称。
直接手动注册虽然可行,但 Configuration 类会变得杂乱,而且每个项目都要重复这段初始化代码。EnableScope 注解相当于 Spring Boot 中的 @EnableXxx 模式,它把注册逻辑隐藏到注解导入的配置类或注册器中。业务模块只需要在启动类或配置类上标注 @EnableScope,就能声明当前应用启用某个自定义作用域,后续 Bean 定义中就可以通过 @Scope("thread") 使用它。这种方式统一了开关入口,也便于团队在不同服务间复用。
二、实现一个线程级自定义 Scope
线程级作用域的核心特征是:同一个线程内多次获取同一个 Bean 时返回同一个实例,不同线程之间实例互相隔离。实现这个效果最直接的方式是使用 ThreadLocal 存储已创建的实例。下面的 ThreadScope 类使用两个 ThreadLocal 分别保存 Bean 实例和销毁回调,避免销毁逻辑与实例存储混在一起。
public class ThreadScope implements Scope {
private final ThreadLocal<Map<String, Object>> scopedObjects = ThreadLocal.withInitial(HashMap::new);
private final ThreadLocal<Map<String, Runnable>> destructionCallbacks = ThreadLocal.withInitial(HashMap::new);
@Override
public Object get(String name, ObjectFactory<?> objectFactory) {
Map<String, Object> objects = scopedObjects.get();
Object scopedObject = objects.get(name);
if (scopedObject == null) {
scopedObject = objectFactory.getObject();
objects.put(name, scopedObject);
}
return scopedObject;
}
@Override
public Object remove(String name) {
Map<String, Object> objects = scopedObjects.get();
Object scopedObject = objects.remove(name);
Map<String, Runnable> callbacks = destructionCallbacks.get();
Runnable callback = callbacks.remove(name);
if (callback != null) {
callback.run();
}
return scopedObject;
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
destructionCallbacks.get().put(name, callback);
}
@Override
public Object resolveContextualObject(String key) {
return null;
}
@Override
public String getConversationId() {
return String.valueOf(Thread.currentThread().getId());
}
}
get 方法先从当前线程的 ThreadLocal 缓存中查找 Bean 名称,如果存在就直接返回,否则调用 objectFactory.getObject() 触发 Spring 创建 Bean,并将其放入缓存。这里的关键在于不要使用 synchronized 或全局锁,因为每个线程的 ThreadLocal 天然隔离,不需要额外同步。
remove 方法除了删除实例,还需要执行之前注册的销毁回调。Spring 容器在注销或替换 Bean 时会调用 registerDestructionCallback,但线程级 Bean 通常不会由容器主动销毁,因此更可靠的做法是在线程结束时显式调用 remove 进行清理。getConversationId 返回当前线程 ID,它在调试和日志中能帮助定位作用域会话。
如果希望线程结束时自动清理 Bean,可以配合 ThreadPoolExecutor 的 afterExecute 钩子,或者在自定义 Runnable 包装器里调用 Scope.remove。简单的线程级作用域也可以通过线程池任务包装器清理,但这已经超出 Scope 接口本身的职责,本文不展开。
三、声明 EnableScope 注解并通过注册器整合
有了 ThreadScope 之后,下一步是定义 EnableScope 注解,并让它能够自动完成作用域注册。注解本身需要保留到运行时,并导入一个 ImportBeanDefinitionRegistrar 实现类。下面的注解定义中,value 属性默认设置为 thread,这样后续 Bean 可以通过 @Scope("thread") 引用它。
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Import(ThreadScopeRegistrar.class)
public @interface EnableScope {
String value() default "thread";
}
ThreadScopeRegistrar 负责读取注解元数据,判断目标 BeanFactory 是否为 ConfigurableBeanFactory,然后调用 registerScope。这里把作用域名称与 ThreadScope 实例绑定,并支持一个 ALREADY_REGISTERED 标记,防止重复注册导致异常。
public class ThreadScopeRegistrar implements ImportBeanDefinitionRegistrar {
@Override
public void registerBeanDefinitions(AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
if (registry instanceof ConfigurableBeanFactory beanFactory) {
String scopeName = "thread";
Map<String, Object> attributes = importingClassMetadata
.getAnnotationAttributes(EnableScope.class.getName());
if (attributes != null && attributes.get("value") != null) {
scopeName = (String) attributes.get("value");
}
if (!beanFactory.getRegisteredScope(scopeName).equals(ThreadScope.class)) {
beanFactory.registerScope(scopeName, new ThreadScope());
}
}
}
}
注意这里 getRegisteredScope 返回的是注册的作用域实例类型,如果尚未注册会返回 null。实际项目里建议先判空再调用 equals,或者用 ObjectUtils.nullSafeEquals 避免空指针。为了示例简洁,这里假设重复注册判断不会遇到空值场景,但生产代码需要更严谨。
在 Spring Boot 配置类上添加 @EnableScope 后,容器启动时就会执行注册器,完成 ThreadScope 的注册。之后可以定义一个线程作用域的 Bean,例如下面这个 TraceService 使用 @Scope("thread") 标注。这样每次从同一个线程获取 TraceService 时都会复用第一次创建的实例。
@Configuration
@EnableScope
public class ScopeConfig {
@Bean
@Scope("thread")
public TraceService traceService() {
return new TraceService();
}
}
如果同一应用需要多个作用域,可以给 @EnableScope 增加别名属性,或者定义多个注解分别导入不同的注册器。但过多的自定义作用域会让 Bean 生命周期复杂化,建议只在确实需要强隔离的场景下使用。
四、Spring Boot 自动装配整合与多线程测试
在实际的 Spring Boot 项目中,更希望引入一个 starter 后就能自动启用自定义作用域,而不需要每个模块手动加 @EnableScope。这可以通过 Spring Boot 的自动装配机制实现。定义一个 EnableScopeAutoConfiguration,并使用 @AutoConfiguration 标注,然后在构造方法中直接注册 ThreadScope,或者复用前面的注册器。
@AutoConfiguration
public class EnableScopeAutoConfiguration {
@Bean
public static BeanFactoryPostProcessor threadScopeRegistrar() {
return beanFactory -> {
if (beanFactory instanceof ConfigurableBeanFactory configurableBeanFactory) {
configurableBeanFactory.registerScope("thread", new ThreadScope());
}
};
}
}
将该类配置到 META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports 文件中后,Spring Boot 启动时就会自动注册 thread 作用域。这样业务模块不需要复制任何注册代码,只要在 Bean 定义中使用 @Scope("thread") 即可。相比手动 @EnableScope,自动装配更适合通用基础组件;如果希望业务方显式开启,可以保留 @EnableScope 作为可选开关,并在自动装配中使用 @ConditionalOnMissingBean 防止重复覆盖。
为了验证线程级作用域是否生效,可以写一个简单的 CommandLineRunner,在不同线程中注入同一个 Bean,对比实例哈希码。下面的代码中,主线程两次获取 TraceService 输出相同哈希码,新建线程中获取的实例应该不同。
@Component
public class ScopeTestRunner implements CommandLineRunner {
@Autowired
private ApplicationContext context;
@Override
public void run(String... args) throws Exception {
TraceService first = context.getBean(TraceService.class);
TraceService second = context.getBean(TraceService.class);
System.out.println("same thread same instance: " + (first == second));
Thread thread = new Thread(() -> {
TraceService other = context.getBean(TraceService.class);
System.out.println("different thread same instance: " + (first == other));
});
thread.start();
thread.join();
}
}
运行结果会显示主线程中两个引用相同,而另一个线程中获取的实例不同。这说明 ThreadScope 成功把 Bean 生命周期绑定到线程上下文。需要注意,线程级 Bean 不会自动销毁,若线程频繁创建或长时间存活,可能引起内存堆积。建议在任务结束前手动调用 scope.remove,或者在线程池包装器中统一清理。
另外,如果线程级 Bean 依赖了 prototype 或其他长生命周期 Bean,Spring 的默认代理机制可能会让依赖注入提前固定引用。此时需要在 @Scope 中设置 proxyMode = ScopedProxyMode.TARGET_CLASS,让注入的是代理对象而不是提前创建的实例。不过启用代理会增加复杂度,需要根据实际依赖关系评估。
从扩展点的角度来看,Spring Boot 整合 EnableScope 的实质是组合使用 Scope 接口、ImportBeanDefinitionRegistrar 以及自动装配配置。理解了这三个组件之间的关系,就可以把同一套模式迁移到租户作用域、请求子作用域、批处理上下文作用域等场景中。作用域为 Bean 生命周期提供了强大的灵活性,但也要注意生命周期边界和清理机制,否则容易在并发环境中引发内存或数据串扰问题。
Spring BootEnableScope自定义作用域修改时间:2026-08-25 16:05:41