Spring Boot Bean 生命周期与作用域:从单例到原型,完整剖析

6 阅读10分钟

一、引言

Spring 的核心是 IoC 容器,而 IoC 容器最核心的“成员”就是 Bean。很多人刚学 Spring 时,会被“生命周期”和“作用域”两个概念绕晕:

  • 生命周期讲的是 Bean 从创建到销毁的全过程
  • 作用域讲的是 Bean 存在的范围与数量

实际上,二者必须结合起来理解——不同作用域的 Bean,生命周期行为差异巨大。

本文将带你彻底搞懂:

  1. Singleton 作用域下的完整生命周期(8 个阶段)。
  2. Prototype 作用域为什么“只管生不管死”。
  3. Request / Session 作用域的特殊之处。
  4. 不同作用域混合注入时的注意事项。

二、Spring 所有作用域总览

Spring 共支持 6 种内置作用域(其中前两种在任何环境中可用,后四种仅在 Web 环境中可用):

作用域说明创建时机销毁时机是否需代理
singleton容器级别单例容器启动(或懒加载时)容器关闭
prototype每次获取都创建新实例每次调用 getBean()容器不管理,需手动
request每个 HTTP 请求一个实例每个请求到达时请求结束
session每个 HTTP Session 一个实例第一次请求时创建 SessionSession 过期/销毁
applicationServletContext 级别单例ServletContext 初始化时应用关闭
websocket每个 WebSocket 会话一个实例WebSocket 连接建立时连接关闭

此外,Spring Cloud 等生态还提供了自定义作用域(如 refresh scope),本文先聚焦内置作用域。


三、标准 Bean 生命周期(以 Singleton 为例)

Singleton 是 Spring 默认作用域,也是生命周期最完整的代表。  整个过程由 Spring 容器全权管理。

完整阶段一览(建议对照下文理解)

实例化 → 属性注入 → Aware 回调 → 初始化前 → 自定义初始化 → 初始化后 → 使用中 → 销毁

1. 实例化(Instantiation)

  • 通过构造器(或工厂方法)创建 Bean 实例。
  • 此时对象已存在,但属性还未注入。

2. 属性注入(Populate Properties)

  • Spring 根据 @Autowired@Resource@Value 等注解,或者 XML 配置,完成依赖注入。

3. Aware 接口回调

如果 Bean 实现了以下 Aware 接口,Spring 会回调对应方法:

Aware 接口回调时机用途
BeanNameAware注入 Bean 在容器中的名称获取自己的 Bean 名字
BeanClassLoaderAware注入类加载器动态加载类
BeanFactoryAware注入当前 BeanFactory手动获取其他 Bean
EnvironmentAware注入环境变量读取配置
ApplicationContextAware注入 ApplicationContext获取更丰富的容器能力

4. 初始化前(BeanPostProcessor.before)

  • 调用所有注册的 BeanPostProcessor 的 postProcessBeforeInitialization 方法。
  • 常见应用:对 Bean 做前置增强、校验。

5. 自定义初始化(三选一或多选)

Spring 按以下顺序执行(可同时存在,但建议只用一种):

  1. @PostConstruct 注解的方法
  2. InitializingBean.afterPropertiesSet()
  3. @Bean(initMethod = "myInit") 指定的方法

6. 初始化后(BeanPostProcessor.after)

  • 调用 postProcessAfterInitialization
  • 这是 AOP 生成代理对象的常见时机(如 @Transactional 在这里生效)。

7. 使用中

  • Bean 已经就绪,可以被业务代码调用。

8. 销毁(容器关闭时)

Spring 按以下顺序执行:

  1. @PreDestroy 注解的方法
  2. DisposableBean.destroy()
  3. @Bean(destroyMethod = "myDestroy") 指定的方法

四、6 大作用域详解(逐个拆解)

1. Singleton(单例作用域)

这是 Spring 的默认作用域。

特征

  • 整个 Spring 容器中,该 Bean 只有一个实例
  • 所有使用该 Bean 的地方,共享同一个对象。

生命周期

  • 创建:容器启动时立即创建(除非加 @Lazy 懒加载)。
  • 初始化:完整走完 1~6 步。
  • 销毁:容器正常关闭时,走第 8 步。
  • 结论:所有生命周期扩展点都生效,Spring 全权管理。

代码示例

@Component
@Scope("singleton")  // 可省略,因为默认就是 singleton
public class SingletonBean {
    public SingletonBean() {
        System.out.println("SingletonBean 实例化");
    }
}

适用场景

  • 无状态的服务类(Service、DAO)
  • 工具类、配置类
  • 线程安全的组件

2. Prototype(原型作用域)

最容易踩坑的作用域。

特征

  • 每次通过 getBean() 获取时,都会创建一个全新的实例
  • Spring 容器只负责创建和初始化,不负责销毁。

生命周期

  • 创建:每次获取时创建(包括初始化流程)。
  • 初始化完整走完 1~6 步(和 singleton 一样,@PostConstruct 每次都会执行)。
  • 销毁容器不会调用任何销毁方法@PreDestroyDisposableBean 统统不执行)。
  • 结论:Spring 只负责创建和初始化,资源释放需要开发者手动管理。

为什么这样设计?
因为原型 Bean 可能被任意多个地方持有,Spring 无法知道什么时候该销毁它。

代码示例

@Component
@Scope("prototype")
public class PrototypeBean implements DisposableBean {
    
    private int counter = 0;
    
    public PrototypeBean() {
        System.out.println("PrototypeBean 实例化,counter=" + (++counter));
    }
    
    @PostConstruct
    public void init() {
        System.out.println("@PostConstruct 执行");
    }
    
    @PreDestroy
    public void preDestroy() {
        System.out.println("@PreDestroy 执行");  // 永远不会打印!
    }
    @Override
    public void destroy() {
        System.out.println("DisposableBean.destroy 执行");  // 永远不会打印!
    }
}

如何手动销毁原型 Bean?

@Component
public class PrototypeManager {
    @Autowired
    private ApplicationContext context;
    
    private final List<DisposableBean> prototypes = new ArrayList<>();
    
    public <T> T getPrototype(Class<T> clazz) {
        T bean = context.getBean(clazz);
        if (bean instanceof DisposableBean) {
            prototypes.add((DisposableBean) bean);
        }
        return bean;
    }
    
    @PreDestroy
    public void destroyPrototypes() throws Exception {
        for (DisposableBean bean : prototypes) {
            bean.destroy();
        }
    }
}

适用场景

  • 有状态的 Bean(每个调用者需要独立的状态)
  • 非线程安全的对象
  • 需要频繁创建临时对象的场景

3. Request(请求作用域)【Web 环境】

特征

  • 每个 HTTP 请求会创建一个全新的 Bean 实例。
  • 同一个请求内,多次获取返回同一个对象。
  • 不同请求之间互不干扰。

生命周期

  • 创建:第一次在当前请求中访问该 Bean 时创建。
  • 初始化:完整走完 1~6 步。
  • 销毁:HTTP 请求结束时,由 Web 容器销毁。
  • 特殊要求:必须使用 代理模式

为什么需要代理?

因为单例 Bean(如 Controller)在启动时就创建了,此时还没有 HTTP 请求。Spring 会注入一个代理对象,代理对象在每次调用时,再去当前请求中查找真正的实例。

代码示例

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestBean {
    private String requestId = UUID.randomUUID().toString();
    private long timestamp = System.currentTimeMillis();
    
    public String getRequestId() {
        return requestId;
    }
    
    public long getTimestamp() {
        return timestamp;
    }
}

@RestController
public class TestController {
    @Autowired
    private RequestBean requestBean;  // 注入的是代理对象
    
    @GetMapping("/test")
    public String test() {
        return "Request ID: " + requestBean.getRequestId() + 
               ", Timestamp: " + requestBean.getTimestamp();
    }
}

多次请求会看到不同的 requestId 和时间戳。

适用场景

  • 请求级别的日志追踪(每个请求生成一个 traceId)
  • 请求级别的临时数据缓存
  • 分页参数、当前用户信息等

4. Session(会话作用域)【Web 环境】

特征

  • 每个 HTTP Session 会创建一个 Bean 实例。
  • 同一个用户的多个请求(同一 Session)共享同一个实例。
  • 不同用户之间互不干扰。

生命周期

  • 创建:第一次在当前 Session 中访问该 Bean 时创建。
  • 初始化:完整走完 1~6 步。
  • 销毁:Session 过期或主动销毁时,由 Web 容器销毁。
  • 特殊要求:必须使用 代理模式(同 request 作用域)。

看到这里感觉这个玩意儿好像能当ThreadLocal去使用?但是其实是不太能行的

维度Request 作用域ThreadLocal
生命周期HTTP 请求开始→结束线程开始→结束(需手动清理)
适用环境仅限 Web 环境任何 Java 环境
跨层访问需通过注入或 AOP同一线程内任意位置直接访问
实现原理Spring 容器管理的代理对象JDK 原生线程局部变量
资源清理自动(请求结束)手动(finally 中 remove)
性能开销代理 + 容器查找极低(ThreadLocalMap)
异步支持差(子线程拿不到)差(需 InheritableThreadLocal)

Request作用域不能完全替代ThreadLocal:Request作用域仅适用于Web环境且生命周期绑定HTTP请求,而ThreadLocal可用于任何Java环境(定时任务、MQ、RPC等),并且性能更高、生命周期控制更灵活。两者是互补关系——Web层传递请求级数据优先用Request作用域(自动清理更优雅),框架底层、非Web环境或性能敏感场景则必须用ThreadLocal。

代码示例

@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SessionBean {
    private String sessionId;
    private String username;
    private List<String> cart = new ArrayList<>();
    
    public void setSessionId(String sessionId) {
        this.sessionId = sessionId;
    }
    
    public void setUsername(String username) {
        this.username = username;
    }
    
    public void addToCart(String item) {
        cart.add(item);
    }
    
    public List<String> getCart() {
        return cart;
    }
}

@RestController
public class CartController {
    @Autowired
    private SessionBean sessionBean;
    
    @PostMapping("/cart/add")
    public String addToCart(@RequestParam String item) {
        sessionBean.addToCart(item);
        return "Added: " + item;
    }
    
    @GetMapping("/cart")
    public List<String> getCart() {
        return sessionBean.getCart();
    }
}

同一个用户的购物车数据会持久化在整个 Session 生命周期中。

适用场景

  • 用户购物车
  • 用户登录信息(替代方案:直接存 Session,但用 Bean 更优雅)
  • 用户偏好设置

5. Application(应用作用域)【Web 环境】

特征

  • 整个 ServletContext 级别只有一个实例。
  • 所有用户、所有请求共享同一个对象。
  • 类似于 singleton,但生命周期与 ServletContext 绑定。

与 Singleton 的区别

特性singletonapplication
生命周期绑定Spring 容器ServletContext
一个应用多容器每个容器有自己的实例所有容器共享同一个实例
使用场景普通 Spring Boot 应用传统 Web 应用(多个 DispatcherServlet)

在绝大多数 Spring Boot 应用中(只有一个容器),singleton 和 application 行为几乎一样。

生命周期

  • 创建:ServletContext 初始化时创建。
  • 初始化:完整走完 1~6 步。
  • 销毁:应用关闭时,由 Web 容器销毁。
  • 特殊要求:必须使用 代理模式(因为可能被非 Web 组件引用)。

代码示例

@Component
@Scope(value = "application", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ApplicationBean {
    private long startTime = System.currentTimeMillis();
    private long requestCount = 0;
    
    public synchronized void incrementCount() {
        requestCount++;
    }
    
    public long getRequestCount() {
        return requestCount;
    }
    
    public long getUptime() {
        return System.currentTimeMillis() - startTime;
    }
}

@RestController
public class StatsController {
    @Autowired
    private ApplicationBean appBean;
    
    @GetMapping("/stats")
    public String getStats() {
        appBean.incrementCount();
        return "Uptime: " + appBean.getUptime() + "ms, Requests: " + appBean.getRequestCount();
    }
}

所有用户访问 /stats 都会看到累计的请求数。

适用场景

  • 全局计数器
  • 应用级别的配置缓存
  • 共享的全局状态(注意线程安全)

6. WebSocket(WebSocket 作用域)【Web 环境】

特征

  • 每个 WebSocket 会话(连接)会创建一个 Bean 实例。
  • 同一个 WebSocket 连接内的多次消息交互共享同一个实例。
  • 不同连接之间互不干扰。

生命周期

  • 创建:WebSocket 连接建立时创建。
  • 初始化:完整走完 1~6 步。
  • 销毁:WebSocket 连接关闭时,由 WebSocket 容器销毁。
  • 特殊要求:必须使用 代理模式

前置条件:启用 WebSocket

@Configuration
@EnableWebSocketMessageBroker
public class WebSocketConfig implements WebSocketMessageBrokerConfigurer {
    @Override
    public void configureMessageBroker(MessageBrokerRegistry config) {
        config.enableSimpleBroker("/topic");
        config.setApplicationDestinationPrefixes("/app");
    }
    
    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/ws").withSockJS();
    }
}

代码示例

@Component
@Scope(value = "websocket", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class WebSocketSessionBean {
    private String sessionId;
    private String username;
    private List<String> messages = new ArrayList<>();
    
    public void setSessionId(String sessionId) {
        this.sessionId = sessionId;
    }
    
    public void setUsername(String username) {
        this.username = username;
    }
    
    public void addMessage(String msg) {
        messages.add(msg);
    }
    
    public List<String> getMessages() {
        return messages;
    }
}

@Controller
public class WebSocketController {
    @Autowired
    private WebSocketSessionBean sessionBean;
    
    @MessageMapping("/chat")
    @SendTo("/topic/messages")
    public String handleMessage(String message) {
        sessionBean.addMessage(message);
        return "[" + sessionBean.getSessionId() + "] " + message;
    }
}

每个 WebSocket 连接都有自己独立的会话状态。

适用场景

  • 聊天室中的用户临时状态
  • WebSocket 连接的会话管理
  • 实时协作应用中的临时数据

混合作用域注入的注意事项

问题 1:Singleton 注入 Prototype

@Component
@Scope("singleton")
public class SingletonBean {
    @Autowired
    private PrototypeBean prototypeBean;  // 注入后不会更新!
    
    public void show() {
        System.out.println(prototypeBean.hashCode());  // 永远相同
    }
}

解决方案:使用 ObjectFactory@Lookup 或 ApplicationContext

@Component
public class SingletonBean {
    @Autowired
    private ObjectFactory<PrototypeBean> prototypeFactory;
    
    public void show() {
        PrototypeBean bean = prototypeFactory.getObject();
        System.out.println(bean.hashCode());  // 每次不同
    }
}

问题 2:Prototype 注入 Singleton

@Component
@Scope("prototype")
public class PrototypeBean {
    @Autowired
    private SingletonBean singletonBean;  // 正常工作,注入同一个单例
}

这是没问题的,每次创建 prototype Bean 时都会注入同一个 singleton 实例。

问题 3:Singleton 注入 Request/Session

@Component  // 默认 singleton
public class MyService {
    @Autowired
    private RequestBean requestBean;  // 需要代理!
}

必须加代理,否则启动时会报错。正确的 Request Bean 定义:

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestBean { ... }

问题 4:原型 Bean 中的资源释放

@Component
@Scope("prototype")
public class PrototypeWithResource {
    private Connection connection;
    
    public PrototypeWithResource() {
        connection = DriverManager.getConnection(...);  // 模拟资源
    }
    
    @PreDestroy
    public void cleanup() {
        connection.close();  // 永远不会被调用!
    }
}

解决方案:手动管理

@Component
public class PrototypeManager {
    private final List<AutoCloseable> resources = new ArrayList<>();
    
    public <T> T getPrototype(Class<T> clazz) {
        T bean = context.getBean(clazz);
        if (bean instanceof AutoCloseable) {
            resources.add((AutoCloseable) bean);
        }
        return bean;
    }
    
    @PreDestroy
    public void cleanup() {
        resources.forEach(resource -> {
            try { resource.close(); } catch (Exception e) { /* log */ }
        });
    }
}