【大白话说Java面试题 第154题】【06_Spring篇】第14题:Spring 支持的 Bean 作用域

📌 PDF :大白话说Java面试题 --- 06_Spring篇

第14题:Spring 支持的 Bean 作用域

📚 回答:

  • 核心考点 : Spring Bean 作用域是 Spring IoC 容器的核心设计之一,大厂面试不会只问"有哪几种",而是深入考察 各作用域的底层实现机制DefaultListableBeanFactory 如何管理不同作用域的 Bean)、作用域代理 ScopedProxyMode 的工作原理 (CGLIB/JDK 代理在跨作用域注入时的角色)、Web 作用域与 RequestContextListener/ServletRequestListener 的生命周期绑定以及 prototype 作用域 Bean 的销毁机制与内存泄漏风险。面试官真正想判断的是:你是否能从源码层面理解作用域的设计意图,以及能否在 Web 应用、微服务、分布式会话等生产场景中正确选型。

1. Spring 支持的六种 Bean 作用域

Spring Framework 定义了 6 种标准作用域,其中 2 种适用于所有应用,4 种仅适用于 Web 环境:

作用域 常量 说明 生命周期 线程安全 适用场景
singleton ConfigurableBeanFactory.SCOPE_SINGLETON 每个 Spring 容器只有一个实例 容器启动时创建,容器关闭时销毁 无状态安全,有状态不安全 Service、DAO、配置类
prototype ConfigurableBeanFactory.SCOPE_PROTOTYPE 每次获取都创建新实例 获取时创建,Spring 不管理销毁 安全(实例隔离) 有状态对象、多例策略
request WebApplicationContext.SCOPE_REQUEST 每个 HTTP 请求一个实例 请求开始时创建,请求结束时销毁 安全(请求隔离) 请求级上下文、TraceId
session WebApplicationContext.SCOPE_SESSION 每个 HTTP Session 一个实例 Session 创建时创建,Session 失效时销毁 安全(会话隔离) 用户购物车、登录状态
application WebApplicationContext.SCOPE_APPLICATION 每个 ServletContext 一个实例 应用启动时创建,应用关闭时销毁 无状态安全,有状态不安全 全局配置、应用级缓存
websocket WebApplicationContext.SCOPE_WEBSOCKET 每个 WebSocket 连接一个实例 连接建立时创建,连接关闭时销毁 安全(连接隔离) WebSocket 会话状态

注意global-session(全局会话)在 Spring 5 中已随 Portlet 支持一起移除,不再推荐使用。


2. singleton 作用域------默认且最常用
  • 2.1 定义与实现

    singleton 是 Spring 的默认作用域,每个 Spring 容器只创建一个 Bean 实例,存储在 DefaultSingletonBeanRegistry.singletonObjects(一级缓存)中。

    java 复制代码
    // 默认就是 singleton,可省略 @Scope
    @Component
    public class UserService { }
    
    // 显式声明
    @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON)
    @Component
    public class UserService { }
  • 2.2 单例 Bean 的创建时机

    配置 创建时机 说明
    默认(非懒加载) 容器启动时 ApplicationContext.refresh() 阶段
    @Lazy 首次获取时 getBean() 或依赖注入时
    lazy-init="true"(XML) 首次获取时 @Lazy
    java 复制代码
    @Lazy  // 延迟初始化
    @Component
    public class HeavyService { }
  • 2.3 单例 Bean 的线程安全

    单例 Bean 被多线程共享,必须设计为无状态

    java 复制代码
    @Service  // singleton,无状态,线程安全
    public class UserService {
        @Autowired
        private UserDao userDao;  // 依赖注入,本身无状态
    
        public User getUser(Long id) {
            return userDao.findById(id);  // 纯查询,不修改实例变量
        }
    }

3. prototype 作用域------每次获取新实例
  • 3.1 定义与实现

    每次调用 getBean() 或注入依赖时,Spring 都会创建一个新的 Bean 实例。

    java 复制代码
    @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
    @Component
    public class PrototypeBean {
        private int count = 0;
        public void increment() { count++; }
        public int getCount() { return count; }
    }
    java 复制代码
    @Autowired
    private PrototypeBean bean1;
    
    @Autowired
    private PrototypeBean bean2;
    
    // bean1 != bean2,是两个不同的实例
  • 3.2 prototype 的销毁机制------重大陷阱!

    Spring 不管理 prototype Bean 的完整生命周期 。虽然会调用初始化回调(@PostConstructInitializingBean),但不会调用销毁回调@PreDestroyDisposableBean)。

    java 复制代码
    @Scope("prototype")
    @Component
    public class PrototypeResource implements DisposableBean {
        private Connection connection;
    
        @PostConstruct
        public void init() {
            connection = dataSource.getConnection();  // 获取资源
        }
    
        @Override
        public void destroy() {
            connection.close();  // ❌ Spring 不会调用!内存泄漏!
        }
    }

    解决方案

    方案 实现方式 说明
    自定义销毁逻辑 客户端代码手动调用销毁 侵入性强,不推荐
    Bean 后处理器 实现 DestructionAwareBeanPostProcessor 在 Bean 销毁前执行清理
    使用 ObjectFactory 延迟获取,客户端管理生命周期 推荐
    java 复制代码
    // 推荐:使用 ObjectFactory,客户端控制生命周期
    @Service
    public class ClientService {
        @Autowired
        private ObjectFactory<PrototypeResource> resourceFactory;
    
        public void doWork() {
            PrototypeResource resource = resourceFactory.getObject();
            try {
                // 使用资源...
            } finally {
                resource.close();  // 客户端负责清理
            }
        }
    }
  • 3.3 prototype 的性能考量

    频繁创建 prototype Bean 可能带来性能问题:

    场景 影响 优化方案
    每次请求创建 对象创建开销大 使用对象池(Apache Commons Pool)
    依赖注入复杂 依赖树递归创建 使用 ObjectFactory 延迟创建
    内存占用高 大量实例未回收 确保客户端及时释放

4. Web 作用域------request、session、application、websocket

Web 作用域仅在 Web 应用上下文中有效(WebApplicationContext),需要配置 RequestContextListenerDispatcherServlet

  • 4.1 request 作用域

    每个 HTTP 请求创建一个实例,请求结束后销毁。

    java 复制代码
    @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
    @Component
    public class RequestContext {
        private String traceId;
        private Long userId;
        // ... 请求级状态
    }

    底层绑定机制 :Spring 通过 RequestContextListener 监听请求生命周期,在 requestInitialized() 时创建 Bean,在 requestDestroyed() 时销毁。

  • 4.2 session 作用域

    每个 HTTP Session 创建一个实例,Session 失效时销毁。

    java 复制代码
    @Scope(value = WebApplicationContext.SCOPE_SESSION, proxyMode = ScopedProxyMode.TARGET_CLASS)
    @Component
    public class ShoppingCart {
        private List<Item> items = new ArrayList<>();
        // ... 购物车状态
    }

    分布式 Session 问题:在微服务/集群环境下,Session 默认不共享。解决方案:

    1. Spring Session + Redis:将 Session 存储到 Redis,实现分布式共享;
    2. JWT Token:无状态认证,不依赖 Session;
    3. Sticky Session:负载均衡器将同一用户固定到同一节点(不推荐)。
  • 4.3 application 作用域

    每个 ServletContext 创建一个实例,等同于 singleton,但生命周期绑定到 Web 应用。

    java 复制代码
    @Scope(value = WebApplicationContext.SCOPE_APPLICATION, proxyMode = ScopedProxyMode.TARGET_CLASS)
    @Component
    public class AppConfig {
        private Map<String, Object> globalCache = new ConcurrentHashMap<>();
    }
  • 4.4 websocket 作用域

    每个 WebSocket 连接创建一个实例,连接关闭时销毁。

    java 复制代码
    @Scope(scopeName = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
    @Component
    public class WebSocketSession {
        private String sessionId;
        private List<Message> messages = new ArrayList<>();
    }

5. 作用域代理 ScopedProxyMode------跨作用域注入的核心
  • 5.1 为什么需要作用域代理?

    singleton Bean 注入 request/session/prototype 作用域 Bean 时,由于 singleton Bean 只创建一次,注入的依赖在首次注入后固定不变,导致后续请求获取的是旧实例。

    java 复制代码
    @Service  // singleton
    public class UserService {
        @Autowired
        private RequestContext requestContext;  // request 作用域
    
        public void process() {
            // 问题:requestContext 是第一次注入时的实例,不是当前请求的!
            String traceId = requestContext.getTraceId();
        }
    }
  • 5.2 ScopedProxyMode 的工作原理

    ScopedProxyMode 为作用域 Bean 创建代理对象singleton Bean 注入的是代理而非真实实例。每次调用代理方法时,代理从当前作用域获取真实实例。

    代理模式 实现方式 适用条件 说明
    NO 不创建代理 同作用域注入 默认,无代理开销
    INTERFACES JDK 动态代理 目标类实现接口 要求目标类有接口
    TARGET_CLASS CGLIB 代理 任意类 最常用,无需接口
    java 复制代码
    @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
    @Component
    public class RequestContext { }

    代理执行流程

    复制代码
    UserService(singleton)调用 requestContext.getTraceId()
        ↓
    调用 CGLIB 代理对象的 getTraceId()
        ↓
    代理从 RequestAttributes(ThreadLocal)获取当前 Request
        ↓
    从 Request 作用域缓存中获取真实的 RequestContext 实例
        ↓
    调用真实实例的 getTraceId()
  • 5.3 prototype 作用域的代理问题

    即使配置了 proxyMode = TARGET_CLASSsingleton Bean 注入 prototype Bean 时,由于代理对象本身也是单例的,每次调用代理方法时虽然会创建新的目标实例,但如果代理方法内部缓存了引用,仍然可能共享状态。

    java 复制代码
    @Scope(value = ConfigurableBeanFactory.SCOPE_PROTOTYPE, proxyMode = ScopedProxyMode.TARGET_CLASS)
    @Component
    public class PrototypeBean { }
    
    @Service
    public class UserService {
        @Autowired
        private PrototypeBean prototypeBean;
    
        public void process() {
            // 每次调用都会创建新的 PrototypeBean 实例
            prototypeBean.doSomething();
        }
    }

    更推荐的方式 :使用 ObjectFactory@Lookup 方法:

    java 复制代码
    @Service
    public class UserService {
        @Autowired
        private ObjectFactory<PrototypeBean> prototypeBeanFactory;
    
        public void process() {
            PrototypeBean bean = prototypeBeanFactory.getObject();  // 每次获取新实例
            bean.doSomething();
        }
    }
    
    // 或使用 @Lookup
    @Service
    public abstract class UserService {
        @Lookup
        protected abstract PrototypeBean getPrototypeBean();
    
        public void process() {
            PrototypeBean bean = getPrototypeBean();  // 每次获取新实例
            bean.doSomething();
        }
    }

6. 自定义作用域

Spring 允许自定义作用域,实现 Scope 接口:

java 复制代码
// 自定义线程作用域
public class ThreadScope implements Scope {
    private final ThreadLocal<Map<String, Object>> threadScope = 
        ThreadLocal.withInitial(HashMap::new);

    @Override
    public Object get(String name, ObjectFactory<?> objectFactory) {
        Map<String, Object> scope = threadScope.get();
        Object bean = scope.get(name);
        if (bean == null) {
            bean = objectFactory.getObject();
            scope.put(name, bean);
        }
        return bean;
    }

    @Override
    public Object remove(String name) {
        return threadScope.get().remove(name);
    }

    // ... 其他方法
}

// 注册自定义作用域
@Bean
public CustomScopeConfigurer customScopeConfigurer() {
    CustomScopeConfigurer configurer = new CustomScopeConfigurer();
    configurer.addScope("thread", new ThreadScope());
    return configurer;
}

// 使用
@Scope("thread")
@Component
public class ThreadScopedBean { }

7. 生产环境避坑指南
  • 7.1 prototype Bean 的内存泄漏

    Spring 不管理 prototype Bean 的销毁,如果 Bean 持有资源(数据库连接、线程池),必须客户端手动释放。

  • 7.2 session 作用域在分布式环境失效

    集群环境下 Session 默认不共享,使用 Spring Session + Redis 或 JWT 替代。

  • 7.3 忘记配置 proxyMode 导致数据串乱

    java 复制代码
    @Scope("request")  // ❌ 忘记 proxyMode
    @Component
    public class RequestContext { }
    
    // singleton Bean 注入后,所有请求共享同一个 RequestContext 实例!
  • 7.4 Web 作用域在非 Web 环境启动失败

    request/session 等作用域需要 WebApplicationContext,在纯 Java 应用中使用会抛出 IllegalStateException

  • 7.5 @Async 与作用域 Bean

    @Async 使用线程池执行异步任务,脱离了原请求/会话的上下文,作用域 Bean 可能获取不到正确的实例。


8. 面试官追问与高分回答模板
  • 追问 1:"Spring 支持哪些 Bean 作用域?"

    低分回答:"有 singleton、prototype、request、session、global-session 五种。"(过时,缺少 application 和 websocket)

    高分回答

    "Spring 支持 6 种标准作用域:

    作用域 适用范围 说明
    singleton 所有应用 每个容器一个实例,默认作用域
    prototype 所有应用 每次获取创建新实例
    request Web 应用 每个 HTTP 请求一个实例
    session Web 应用 每个 HTTP Session 一个实例
    application Web 应用 每个 ServletContext 一个实例
    websocket Web 应用 每个 WebSocket 连接一个实例

    注意 global-session 在 Spring 5 中已随 Portlet 支持移除。其中 singletonprototype 适用于所有应用类型,其余 4 种需要 Web 环境。"

  • 追问 2:"singleton 和 prototype 有什么区别?prototype 有什么陷阱?"

    高分回答

    "| 维度 | singleton | prototype |

    |------|-----------|-----------|

    | 实例数量 | 每个容器一个 | 每次获取创建新实例 |

    | 创建时机 | 容器启动(默认)或首次获取(@Lazy) | 每次 getBean() 或注入时 |

    | 销毁管理 | Spring 管理完整生命周期 | Spring 不调用销毁回调! |

    | 线程安全 | 需设计为无状态 | 天然安全(实例隔离) |

    | 性能 | 创建一次,复用 | 频繁创建,开销大 |

    prototype 的最大陷阱是销毁机制 :Spring 会调用 @PostConstruct/InitializingBean 初始化,但不会调用 @PreDestroy/DisposableBean 销毁。如果 prototype Bean 持有数据库连接、线程池等资源,会导致内存泄漏。

    解决方案:使用 ObjectFactory 延迟获取,由客户端管理生命周期;或实现自定义的 DestructionAwareBeanPostProcessor。"

  • 追问 3:"request 作用域 Bean 怎么在 singleton Bean 中使用?"

    高分回答

    "直接在 singleton Bean 中注入 request 作用域 Bean 会有问题:singleton 只创建一次,注入的 request Bean 在首次注入后固定不变,后续请求获取的是旧实例。

    解决方案是配置 proxyMode = ScopedProxyMode.TARGET_CLASS

    java 复制代码
    @Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)
    @Component
    public class RequestContext { }

    原理:Spring 为 request Bean 创建 CGLIB 代理对象,singleton Bean 注入的是代理。每次调用代理方法时,代理从当前 Request 作用域(通过 RequestContextHolder,底层 ThreadLocal)获取真实的 Bean 实例,确保每次请求获取的都是当前请求的实例。

    类似地,session、application、websocket 作用域跨域注入时也需要配置 proxyMode。"

  • 追问 4:"Spring 的 session 作用域在分布式环境下有什么问题?"

    高分回答

    "Spring 的 session 作用域基于 Servlet 容器的 HttpSession,在分布式/集群环境下存在 Session 不共享的问题:

    1. Session 粘滞(Sticky Session):负载均衡器将同一用户固定到同一节点,但节点故障时会话丢失;
    2. Session 复制:Tomcat 等容器支持 Session 复制,但性能开销大,不适合大规模集群;
    3. Session 共享:使用 Redis/Memcached 等外部存储共享 Session。

    推荐方案:

    • Spring Session + Redis :将 Session 存储到 Redis,实现分布式共享,同时支持 session 作用域 Bean 的跨节点一致性;
    • JWT Token:无状态认证,不依赖服务器 Session,天然支持分布式;
    • OAuth2/OIDC:使用令牌机制,服务端无会话状态。

    现代微服务架构中,推荐使用 JWT 等无状态方案,彻底避免 Session 共享问题。"

  • 追问 5:"prototype 作用域 Bean 的销毁机制是怎样的?"

    高分回答

    "Spring 不管理 prototype Bean 的销毁。具体表现:

    1. 初始化回调会执行@PostConstructInitializingBean.afterPropertiesSet()、自定义 init-method 都会正常调用;
    2. 销毁回调不会执行@PreDestroyDisposableBean.destroy()、自定义 destroy-method 不会被 Spring 调用

    原因:Spring 的 DefaultSingletonBeanRegistry 只管理 singleton Bean 的销毁,prototype Bean 创建后直接返回给客户端,Spring 不持有引用,因此无法在容器关闭时遍历销毁。

    解决方案

    1. 使用 ObjectFactory :客户端通过 ObjectFactory.getObject() 获取实例,使用完毕后手动调用清理方法;
    2. 自定义 Scope 实现:在自定义 Scope 中管理 Bean 的生命周期;
    3. DestructionAwareBeanPostProcessor :实现该接口,在 postProcessBeforeDestruction() 中处理 prototype Bean 的清理。

    最佳实践:避免在 prototype Bean 中持有需要释放的资源,或确保客户端代码负责资源清理。"

  • 追问 6:"Spring 允许自定义作用域吗?怎么实现?"

    高分回答

    "Spring 允许自定义作用域,需要实现 org.springframework.beans.factory.config.Scope 接口:

    java 复制代码
    public class ThreadScope implements Scope {
        private final ThreadLocal<Map<String, Object>> threadScope = 
            ThreadLocal.withInitial(HashMap::new);
    
        @Override
        public Object get(String name, ObjectFactory<?> objectFactory) {
            Map<String, Object> scope = threadScope.get();
            return scope.computeIfAbsent(name, k -> objectFactory.getObject());
        }
    
        @Override
        public Object remove(String name) {
            return threadScope.get().remove(name);
        }
        // ... 实现 registerDestructionCallback、resolveContextualObject、getConversationId
    }

    然后通过 CustomScopeConfigurer 注册:

    java 复制代码
    @Bean
    public CustomScopeConfigurer customScopeConfigurer() {
        CustomScopeConfigurer configurer = new CustomScopeConfigurer();
        configurer.addScope("thread", new ThreadScope());
        return configurer;
    }

    典型应用场景:实现线程级作用域(每个线程一个实例),用于线程池环境下的状态隔离。"


9. 方案选型速查表
业务场景 推荐作用域 代理模式 核心理由
Service/DAO 层 singleton NO 无状态设计,性能最优
有状态策略对象 prototype NO / ObjectFactory 每次获取新实例,客户端管理生命周期
请求级上下文(TraceId) request TARGET_CLASS 请求隔离,跨 singleton 注入需代理
用户购物车 session TARGET_CLASS 会话隔离,注意分布式 Session
全局配置/缓存 application TARGET_CLASS 应用级共享
WebSocket 会话 websocket TARGET_CLASS 连接隔离
线程级状态隔离 自定义 thread NO 线程池环境下隔离状态
延迟初始化 singleton + @Lazy NO 优化启动速度

💡 面试官想要的满分总结

Spring 的 6 种 Bean 作用域是 IoC 容器管理对象生命周期和可见范围的核心机制。理解作用域必须抓住三个关键点:

  1. singleton 是默认且最常用 :必须设计为无状态,利用容器启动时创建、全局复用的特性提升性能。@Lazy 可优化启动速度。
  2. prototype 的销毁陷阱 :Spring 不管理 prototype Bean 的销毁,如果持有资源(连接、线程池)会导致内存泄漏。推荐使用 ObjectFactory 由客户端管理生命周期。
  3. Web 作用域必须配代理request/session 等作用域 Bean 被 singleton Bean 注入时,必须配置 proxyMode = TARGET_CLASS,通过 CGLIB 代理确保每次调用获取当前作用域的真实实例。

工程实践中,99% 的 Bean 使用 singleton + 无状态设计 。Web 作用域(request/session)适用于请求级/会话级上下文,但需注意分布式环境下的 Session 共享问题。自定义作用域(如线程级作用域)在特定场景下(线程池状态隔离)有独特价值。理解 ScopedProxyMode 的代理机制------从 ThreadLocal 获取当前作用域实例------是掌握 Web 作用域的核心。


觉得对您有帮助,麻烦 点点关注啦 ,您的关注是我创作的最大动力~ 🎯

相关推荐
CodeStats1 小时前
《源纹天书》第121-125章:源匠归来——全栈重构与归元圣域的2.0时代
java·开发语言·源纹天书
binbin_521 小时前
UIAbility 与 WindowStage:窗口创建、加载、销毁的完整链路
开发语言·javascript·深度学习·华为·harmonyos
小林ixn1 小时前
用 100 行代码手搓一个 MCP Server,让 LLM 直接读你本地文件
面试·llm
wuminyu2 小时前
markword在高并发场景下变化剖析
java·linux·c语言·jvm·c++
wear工程师2 小时前
可重复读能不能防幻读?MVCC 和 Next-Key Lock 到底谁在起作用
mysql·面试
weedsfly2 小时前
Cookie 安全三属性:HttpOnly、Secure、SameSite 分别防什么?
前端·javascript·面试
旖-旎2 小时前
QT界面优化(6)
开发语言·c++·qt
AI科技星2 小时前
基于超复数广义分形流形的电磁耦合与缪子反常磁矩几何理论
开发语言·平面·重构·概率论·量子计算·乖乖数学·全域数学
组合缺一2 小时前
用 ChatModel 构建 LLM 驱动的 Java 应用
java·开发语言·ai·llm·solon·rag