0%

spring bean的作用域

Spring Bean 作用域(Scope)全解析:从单例到 Web 专用作用域

Spring Bean 的作用域(Scope) 定义了 Bean 在 Spring 容器中的生命周期范围与实例创建规则,直接影响 Bean 的复用性、线程安全性和资源占用。Spring 提供 5 种核心作用域,涵盖普通 Java 应用(单例 / 原型)和 Web 应用(请求 / 会话 / 应用级),本文将详细拆解每种作用域的特性、适用场景、底层实现及注意事项。

Spring Bean 作用域总览

Spring 容器中 Bean 的 5 种作用域,按适用场景可分为普通应用作用域(适用于所有环境)和Web 专用作用域(仅适用于 Spring Web 环境,如 Spring MVC):

作用域名称 英文标识 核心特性 适用场景 生效环境
单例 singleton 容器中仅存在1 个实例,随容器启动创建(默认懒加载可配置),全局共享 无状态组件(如 Service、Dao、工具类) 所有环境(默认)
原型 prototype 每次调用 getBean() 或注入时创建新实例,容器不管理销毁 有状态组件(如 Command 对象、请求参数封装类) 所有环境
请求 request 每个 HTTP 请求对应1 个实例,请求结束后销毁 存储请求级数据(如当前请求的用户信息) Web 环境(Spring Web)
会话 session 每个 HTTP Session 对应1 个实例,会话过期后销毁 存储会话级数据(如用户登录状态、购物车) Web 环境(Spring Web)
应用 / 全局会话 application 整个 ServletContext 对应1 个实例,与应用生命周期一致 存储应用级全局数据(如系统配置、字典缓存) Web 环境(Spring Web)

注意:globalSessionapplication 的早期别名,仅在 Portlet 环境中有特殊含义,现代 Web 开发(Servlet 环境)中统一使用 application

各作用域详解与实战

1. 单例(singleton):默认作用域

核心特性
  • 实例数量:容器中唯一实例,所有请求共享同一个 Bean;
  • 创建时机:默认在容器启动时创建(预初始化),可通过 @Lazy 注解改为 “首次使用时创建”(懒加载);
  • 生命周期:与 Spring 容器一致(容器启动时创建,容器关闭时销毁);
  • 线程安全无状态时线程安全(如 Service 仅调用 Dao,无成员变量修改);有状态时线程不安全(如成员变量存储请求数据)。
配置方式
  • XML 配置<bean id="userService" class="com.example.UserService" scope="singleton"/>(默认可省略 scope);
  • 注解配置@Scope("singleton")(或 @Scope(ConfigurableBeanFactory.SCOPE_SINGLETON))。
示例(默认单例)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Service
// 省略 @Scope,默认 singleton
public class UserService {
private int count = 0; // 有状态成员变量(线程不安全)

public void increment() {
count++; // 多线程并发修改时会出现线程安全问题
System.out.println("当前计数:" + count);
}
}

// 测试:多次获取 Bean 是同一个实例
public class SingletonTest {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
UserService service1 = context.getBean(UserService.class);
UserService service2 = context.getBean(UserService.class);

System.out.println(service1 == service2); // true(同一实例)
service1.increment(); // 计数 1
service2.increment(); // 计数 2(共享状态)
}
}
关键注意点
  • 推荐无状态设计:单例 Bean 若需线程安全,应避免定义可修改的成员变量(如 count),或通过 ThreadLocal 隔离线程状态;

  • 懒加载配置:通过@Lazy延迟创建(适合初始化耗时的 Bean,如缓存服务):

    1
    2
    3
    4
    5
    @Service
    @Scope("singleton")
    @Lazy // 容器启动不创建,首次 getBean() 时创建
    public class CacheService {
    }

2. 原型(prototype):每次请求新实例

核心特性
  • 实例数量:每次调用 getBean()、自动注入(@Autowired)或通过 BeanFactory 获取时,创建新实例
  • 创建时机懒加载(仅在获取时创建,容器启动不初始化);
  • 生命周期:容器仅负责 “创建实例”,不管理 “销毁”(实例成为垃圾后由 JVM 回收,@PreDestroy 注解无效);
  • 线程安全:每个线程 / 请求获取独立实例,天然线程安全(状态不共享)。
配置方式
  • XML 配置<bean id="user" class="com.example.User" scope="prototype"/>
  • 注解配置@Scope("prototype")(或 @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE))。
示例(原型 Bean)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
@Component
@Scope("prototype") // 原型作用域
public class User {
private String name;
private int age;

// 构造器:每次创建实例时执行
public User() {
System.out.println("User 实例创建:" + this);
}

// @PreDestroy 无效:容器不管理原型 Bean 销毁
@PreDestroy
public void destroy() {
System.out.println("User 实例销毁"); // 不会执行
}
}

// 测试:多次获取 Bean 是不同实例
public class PrototypeTest {
public static void main(String[] args) {
ApplicationContext context = new AnnotationConfigApplicationContext(AppConfig.class);
User user1 = context.getBean(User.class);
User user2 = context.getBean(User.class);

System.out.println(user1 == user2); // false(不同实例)
}
}
关键注意点
  • 销毁逻辑需手动处理:若原型 Bean 持有资源(如数据库连接、文件流),需在业务代码中手动关闭(@PreDestroy 不生效);

  • 注入方式影响实例数量:若在单例 Bean 中注入原型 Bean,仅在单例 Bean 初始化时创建一次原型实例(后续复用),需通过

    ObjectFactory动态获取新实例:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    @Service
    public class OrderService {
    // 错误:单例 Bean 注入原型 Bean,仅创建一次
    @Autowired
    private User user;

    // 正确:通过 ObjectFactory 动态获取新实例
    @Autowired
    private ObjectFactory<User> userFactory;

    public void createOrder() {
    User newUser = userFactory.getObject(); // 每次调用获取新实例
    }
    }

3. Web 专用作用域(request/session/application)

Web 专用作用域仅在 Spring Web 环境(如 Spring MVC、Spring Boot Web)中生效,依赖 WebApplicationContext 容器,核心是将 Bean 实例与 Web 容器的生命周期(请求、会话、应用)绑定。

(1)请求作用域(request):每个请求一个实例
  • 核心特性:每个 HTTP 请求对应 1 个 Bean 实例,请求处理完成后,Bean 被销毁(随请求生命周期结束);
  • 适用场景:存储请求级临时数据(如请求参数封装、当前请求的用户 IP);
  • 配置方式@Scope("request")(或 @Scope(WebApplicationContext.SCOPE_REQUEST))。
示例(request 作用域)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
@Component
@Scope("request") // 请求作用域
public class RequestContext {
private String requestId; // 存储当前请求的唯一 ID

@PostConstruct
public void init() {
// 初始化:生成请求 ID
requestId = UUID.randomUUID().toString();
System.out.println("RequestContext 初始化:" + requestId);
}

@PreDestroy
public void destroy() {
// 请求结束时销毁
System.out.println("RequestContext 销毁:" + requestId);
}

// 获取请求 ID
public String getRequestId() {
return requestId;
}
}

// Controller 中使用(每个请求获取新实例)
@Controller
public class TestController {
@Autowired
private RequestContext requestContext;

@GetMapping("/test")
@ResponseBody
public String test() {
// 每个请求返回不同的 requestId
return "当前请求 ID:" + requestContext.getRequestId();
}
}
(2)会话作用域(session):每个会话一个实例
  • 核心特性:每个 HTTP Session 对应 1 个 Bean 实例,会话过期(如用户退出、超时)后,Bean 被销毁;
  • 适用场景:存储会话级数据(如用户登录信息、购物车内容);
  • 配置方式@Scope("session")(或 @Scope(WebApplicationContext.SCOPE_SESSION))。
示例(session 作用域)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
@Component
@Scope("session") // 会话作用域
public class UserSession {
private User loginUser; // 存储当前会话的登录用户

// 设置登录用户
public void setLoginUser(User user) {
this.loginUser = user;
}

// 获取登录用户
public User getLoginUser() {
return loginUser;
}

@PreDestroy
public void destroy() {
System.out.println("用户会话销毁:" + loginUser.getUsername());
}
}

// 登录 Controller
@Controller
public class LoginController {
@Autowired
private UserSession userSession;

@PostMapping("/login")
public String login(String username) {
// 模拟登录:将用户信息存入会话 Bean
User user = new User(username);
userSession.setLoginUser(user);
return "index";
}

@GetMapping("/userInfo")
@ResponseBody
public String getUserInfo() {
// 同一会话中获取相同的登录用户
return "当前登录用户:" + userSession.getLoginUser().getUsername();
}
}
(3)应用作用域(application):全局应用一个实例
  • 核心特性:整个 Web 应用(ServletContext)对应 1 个 Bean 实例,与应用生命周期一致(应用启动时创建,关闭时销毁);
  • 与 singleton 区别singleton 是 Spring 容器级单例,若多个 WebApplicationContext 共存(如多模块部署),会有多个 singleton 实例;applicationServletContext 级单例,整个应用唯一;
  • 适用场景:存储应用级全局数据(如系统配置、字典缓存、计数器);
  • 配置方式@Scope("application")(或 @Scope(WebApplicationContext.SCOPE_APPLICATION))。
示例(application 作用域)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Component
@Scope("application") // 应用作用域
public class AppConfigHolder {
private Map<String, String> configMap; // 存储全局配置

@PostConstruct
public void init() {
// 应用启动时加载配置(如从数据库或配置文件)
configMap = new HashMap<>();
configMap.put("app.name", "Spring Web App");
configMap.put("app.version", "1.0.0");
System.out.println("全局配置加载完成");
}

// 获取全局配置
public String getConfig(String key) {
return configMap.get(key);
}
}

// 测试 Controller
@Controller
public class AppController {
@Autowired
private AppConfigHolder appConfigHolder;

@GetMapping("/appInfo")
@ResponseBody
public String getAppInfo() {
// 所有请求/会话获取相同的全局配置
return "应用名称:" + appConfigHolder.getConfig("app.name")
+ ",版本:" + appConfigHolder.getConfig("app.version");
}
}

作用域底层实现原理

Spring 对不同作用域的 Bean 采用不同的实例管理策略,核心依赖 Scope 接口(定义作用域的实例获取与销毁逻辑):

1
2
3
4
5
6
7
8
9
10
11
12
public interface Scope {
// 获取作用域内的 Bean 实例
Object get(String name, ObjectFactory<?> objectFactory);

// 移除作用域内的 Bean 实例
Object remove(String name);

// 注册 Bean 销毁回调
void registerDestructionCallback(String name, Runnable callback);

// ... 其他方法
}

各作用域对应 Scope 接口的实现类:

  • singletonSingletonScope → 实例存储在 DefaultSingletonBeanRegistrysingletonObjects 缓存中;
  • prototypePrototypeScope → 每次调用 get() 时通过 ObjectFactory 创建新实例,不缓存;
  • requestRequestScope → 实例存储在 HttpServletRequest 的属性中(request.setAttribute());
  • sessionSessionScope → 实例存储在 HttpSession 的属性中(session.setAttribute());
  • applicationServletContextScope → 实例存储在 ServletContext 的属性中(servletContext.setAttribute())。

作用域选择最佳实践

  1. 优先使用 singleton:无状态组件(Service、Dao、工具类)默认用 singleton,节省资源且复用性高;
  2. 原型用于有状态组件:如 Command 对象(封装单次请求的业务参数)、线程不安全的工具类(如 SimpleDateFormat);
  3. Web 作用域按需选择:
    • 临时请求数据 → request;
    • 用户会话数据 → session;
    • 全局应用数据 → application;
  4. 避免滥用 session/application:session 存储过多数据会导致内存占用过高,application 需注意线程安全(多线程并发修改时需加锁)。

常见问题与避坑指南

  1. 原型 Bean 注入单例 Bean 后复用
    • 问题:单例 Bean 初始化时注入原型 Bean,后续调用复用同一实例;
    • 解决方案:通过 ObjectFactory<User> userFactoryProvider<User>(JSR-330)动态获取新实例。
  2. Web 作用域在非 Web 环境生效
    • 问题:在普通 Java 应用(无 WebApplicationContext)中使用 request/session 作用域,启动报错;
    • 解决方案:确保仅在 Web 环境中使用 Web 专用作用域,或通过 @Profile("web") 限制环境。
  3. session 作用域线程安全
    • 问题:同一 session 可能被多个线程并发访问(如用户同时打开多个标签页);
    • 解决方案:session Bean 避免定义可修改的成员变量,或通过 synchronized 加锁。

总结

Spring Bean 作用域是控制 Bean 生命周期和实例管理的核心机制,不同作用域对应不同的使用场景:

  • singleton 是默认选择,适合无状态组件;
  • prototype 适合有状态、一次性使用的组件;
  • Web 专用作用域(request/session/application)则绑定 Web 容器生命周期,解决 Web 开发中的数据隔离问题

欢迎关注我的其它发布渠道