副标题:一个Bean的生命可以有多长? ⏳
🎬 开场白:Bean有几条命?
嘿,小伙伴们!👋 今天我们要聊一个超级有趣的话题——Spring Bean的作用域!
想象这个场景:
- 🎯 有的Bean只需要一个(单例)
- 🎪 有的Bean每次用都要新建一个(原型)
- 🌐 有的Bean每个HTTP请求一个(请求)
- 🍪 有的Bean每个Session一个(会话)
这就是Bean的作用域(Scope)!
就像:
- 单例Bean = 全校就一个校长 👨💼
- 原型Bean = 每个学生都有自己的笔记本 📓
- 请求Bean = 每次点餐都有一个新订单 🍕
- 会话Bean = 每个用户有自己的购物车 🛒
📚 第一幕:Bean作用域全家福
Spring支持的作用域
| 作用域 | 说明 | 生命周期 | 适用场景 | 图标 |
|---|---|---|---|---|
| singleton | 单例(默认) | IOC容器创建到销毁 | 无状态Bean | 👑 |
| prototype | 原型 | 每次获取都创建新的 | 有状态Bean | 🎭 |
| request | 请求 | 每个HTTP请求一个 | Web层Bean | 🌐 |
| session | 会话 | 每个Session一个 | 用户会话数据 | 🍪 |
| application | 应用 | 整个ServletContext | 全局共享数据 | 🏢 |
| websocket | WebSocket | 每个WebSocket连接 | WebSocket场景 | 🔌 |
🎯 第二幕:Singleton - 单例模式
什么是单例?
单例Bean在整个Spring容器中只有一个实例!
@Component
// @Scope("singleton") // 默认就是singleton,可以省略
public class UserService {
public UserService() {
System.out.println("👑 UserService被创建了!");
}
public void doSomething() {
System.out.println("🎯 执行业务逻辑...");
}
}
测试代码:
@SpringBootTest
public class SingletonTest {
@Autowired
private ApplicationContext context;
@Test
public void testSingleton() {
// 获取两次Bean
UserService service1 = context.getBean(UserService.class);
UserService service2 = context.getBean(UserService.class);
// 判断是否同一个对象
System.out.println("是同一个对象?" + (service1 == service2));
System.out.println("service1的hashCode:" + service1.hashCode());
System.out.println("service2的hashCode:" + service2.hashCode());
// 输出结果:
// 👑 UserService被创建了!(只打印一次!)
// 是同一个对象?true
// service1的hashCode:1234567890
// service2的hashCode:1234567890
}
}
生活比喻:学校的校长
学校(Spring容器)只有一个校长(UserService)
学生A找校长 → 校长
学生B找校长 → 还是这个校长
学生C找校长 → 还是这个校长
全校共享一个校长!👨💼
⚠️ 单例Bean的注意事项
1. 线程安全问题
// ❌ 危险!单例Bean有状态字段
@Component
public class UnsafeService {
private int counter = 0; // 多线程会有问题!
public void increment() {
counter++; // 非线程安全!
}
}
// ✅ 安全方式1:无状态
@Component
public class SafeService {
public int calculate(int a, int b) {
return a + b; // 不依赖实例变量
}
}
// ✅ 安全方式2:使用ThreadLocal
@Component
public class ThreadSafeService {
private ThreadLocal<Integer> counter = ThreadLocal.withInitial(() -> 0);
public void increment() {
counter.set(counter.get() + 1);
}
}
// ✅ 安全方式3:使用同步
@Component
public class SynchronizedService {
private int counter = 0;
public synchronized void increment() {
counter++;
}
}
🎪 第三幕:Prototype - 原型模式
什么是原型?
每次获取都创建一个新的Bean实例!
@Component
@Scope("prototype")
// 或者:@Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public class OrderForm {
private String orderId;
private Date createTime;
public OrderForm() {
this.orderId = UUID.randomUUID().toString();
this.createTime = new Date();
System.out.println("🎭 创建新的OrderForm:" + orderId);
}
// getters and setters...
}
测试代码:
@SpringBootTest
public class PrototypeTest {
@Autowired
private ApplicationContext context;
@Test
public void testPrototype() {
// 获取三次Bean
OrderForm form1 = context.getBean(OrderForm.class);
OrderForm form2 = context.getBean(OrderForm.class);
OrderForm form3 = context.getBean(OrderForm.class);
// 判断是否不同对象
System.out.println("是不同对象?" + (form1 != form2));
System.out.println("form1:" + form1.getOrderId());
System.out.println("form2:" + form2.getOrderId());
System.out.println("form3:" + form3.getOrderId());
// 输出结果:
// 🎭 创建新的OrderForm:550e8400-e29b-41d4-a716-446655440000
// 🎭 创建新的OrderForm:6ba7b810-9dad-11d1-80b4-00c04fd430c8
// 🎭 创建新的OrderForm:7c9e6679-7425-40de-944b-e07fc1f90ae7
// 是不同对象?true
// form1:550e8400-e29b-41d4-a716-446655440000
// form2:6ba7b810-9dad-11d1-80b4-00c04fd430c8
// form3:7c9e6679-7425-40de-944b-e07fc1f90ae7
}
}
生活比喻:学生的笔记本
每个学生都有自己的笔记本 📓
学生A要笔记本 → 给他一个新的
学生B要笔记本 → 再给一个新的
学生C要笔记本 → 又给一个新的
每人都有自己独立的笔记本!
⚠️ 原型Bean的注意事项
1. 依赖注入的问题
// ❌ 问题:单例Bean注入原型Bean
@Component // 默认单例
public class SingletonService {
@Autowired
private PrototypeBean prototypeBean; // 只注入一次!
public void doSomething() {
// 每次调用都是同一个prototypeBean实例!
prototypeBean.execute();
}
}
// ✅ 解决方案1:使用ApplicationContext
@Component
public class SingletonService {
@Autowired
private ApplicationContext context;
public void doSomething() {
// 每次手动获取新的实例
PrototypeBean bean = context.getBean(PrototypeBean.class);
bean.execute();
}
}
// ✅ 解决方案2:使用@Lookup方法注入
@Component
public abstract class SingletonService {
@Lookup
public abstract PrototypeBean getPrototypeBean();
public void doSomething() {
// Spring会动态实现这个方法,每次返回新实例
PrototypeBean bean = getPrototypeBean();
bean.execute();
}
}
// ✅ 解决方案3:使用Provider
@Component
public class SingletonService {
@Autowired
private Provider<PrototypeBean> provider;
public void doSomething() {
// 每次通过Provider获取新实例
PrototypeBean bean = provider.get();
bean.execute();
}
}
🌐 第四幕:Request - 请求作用域
什么是Request作用域?
每个HTTP请求都会创建一个新的Bean实例,请求结束后销毁!
@Component
@Scope(value = WebApplicationContext.SCOPE_REQUEST)
// 或者:@RequestScope
public class RequestInfo {
private String requestId;
private String userAgent;
private Date requestTime;
public RequestInfo(HttpServletRequest request) {
this.requestId = UUID.randomUUID().toString();
this.userAgent = request.getHeader("User-Agent");
this.requestTime = new Date();
System.out.println("🌐 创建RequestInfo:" + requestId);
}
// getters and setters...
}
使用Request Bean
@RestController
public class UserController {
@Autowired
private RequestInfo requestInfo; // 自动注入请求作用域Bean
@GetMapping("/user/info")
public Map<String, Object> getUserInfo() {
Map<String, Object> result = new HashMap<>();
result.put("requestId", requestInfo.getRequestId());
result.put("userAgent", requestInfo.getUserAgent());
result.put("requestTime", requestInfo.getRequestTime());
return result;
}
@GetMapping("/user/profile")
public String getProfile() {
// 同一个请求中,requestInfo是同一个实例
return "Request ID: " + requestInfo.getRequestId();
}
}
生活比喻:餐厅的订单
每次点餐都有一个新订单 🍕
顾客A点餐 → 订单A(点餐时创建,结账后销毁)
顾客B点餐 → 订单B(点餐时创建,结账后销毁)
每个订单独立存在,互不干扰!
测试Request Bean
@SpringBootTest
@AutoConfigureMockMvc
public class RequestScopeTest {
@Autowired
private MockMvc mockMvc;
@Test
public void testRequestScope() throws Exception {
// 第一次请求
MvcResult result1 = mockMvc.perform(get("/user/info"))
.andExpect(status().isOk())
.andReturn();
String response1 = result1.getResponse().getContentAsString();
System.out.println("第一次请求:" + response1);
// 第二次请求
MvcResult result2 = mockMvc.perform(get("/user/info"))
.andExpect(status().isOk())
.andReturn();
String response2 = result2.getResponse().getContentAsString();
System.out.println("第二次请求:" + response2);
// 两次请求的requestId不同!
assertNotEquals(response1, response2);
}
}
🍪 第五幕:Session - 会话作用域
什么是Session作用域?
每个用户会话都会创建一个Bean实例,会话结束后销毁!
@Component
@Scope(value = WebApplicationContext.SCOPE_SESSION)
// 或者:@SessionScope
public class ShoppingCart {
private String sessionId;
private List<CartItem> items = new ArrayList<>();
private Date createTime;
public ShoppingCart(HttpSession session) {
this.sessionId = session.getId();
this.createTime = new Date();
System.out.println("🛒 创建购物车:" + sessionId);
}
public void addItem(CartItem item) {
items.add(item);
System.out.println("➕ 添加商品:" + item.getName());
}
public List<CartItem> getItems() {
return items;
}
// getters and setters...
}
@Data
@AllArgsConstructor
public class CartItem {
private String name;
private Integer quantity;
private Double price;
}
使用Session Bean
@RestController
@RequestMapping("/cart")
public class CartController {
@Autowired
private ShoppingCart shoppingCart; // 会话作用域Bean
@PostMapping("/add")
public String addToCart(@RequestBody CartItem item) {
shoppingCart.addItem(item);
return "✅ 已添加到购物车";
}
@GetMapping("/list")
public List<CartItem> listCart() {
return shoppingCart.getItems();
}
@GetMapping("/count")
public int getCount() {
return shoppingCart.getItems().size();
}
}
生活比喻:超市的购物车
每个顾客有自己的购物车 🛒
顾客A进店 → 给A一个购物车(A购物期间一直用这个)
顾客B进店 → 给B一个购物车(B购物期间一直用这个)
购物车跟着顾客,结账离开后回收!
🔧 第六幕:Scope代理
为什么需要Scope代理?
问题场景: 单例Bean依赖请求作用域Bean
// ❌ 这样会有问题!
@Component // 单例
public class OrderService {
@Autowired
private RequestInfo requestInfo; // 请求作用域
public void createOrder() {
// 问题:单例Bean在创建时注入requestInfo
// 但requestInfo应该每个请求都不同!
System.out.println("Request ID: " + requestInfo.getRequestId());
}
}
解决方案:使用代理
@Component
@Scope(
value = WebApplicationContext.SCOPE_REQUEST,
proxyMode = ScopedProxyMode.TARGET_CLASS // 🔑 关键!
)
public class RequestInfo {
// ...
}
工作原理:
单例Bean → 代理对象(一直存在)
↓
每次调用方法时 → 找到当前请求的真实RequestInfo对象
proxyMode的选项
public enum ScopedProxyMode {
DEFAULT, // 默认:不使用代理
NO, // 不使用代理
INTERFACES, // JDK动态代理(基于接口)
TARGET_CLASS // CGLIB代理(基于类)
}
使用建议:
- 如果Bean有接口 → 使用
INTERFACES - 如果Bean没有接口 → 使用
TARGET_CLASS
完整示例
// 请求作用域Bean(使用代理)
@Component
@Scope(
value = WebApplicationContext.SCOPE_REQUEST,
proxyMode = ScopedProxyMode.TARGET_CLASS
)
public class RequestContext {
private final String requestId;
public RequestContext() {
this.requestId = UUID.randomUUID().toString();
System.out.println("🌐 创建RequestContext:" + requestId);
}
public String getRequestId() {
return requestId;
}
}
// 单例Bean(注入代理对象)
@Component
public class OrderService {
@Autowired
private RequestContext requestContext; // 注入的是代理对象
public void createOrder() {
// 调用方法时,代理会找到当前请求的真实对象
System.out.println("📦 创建订单,Request ID: " +
requestContext.getRequestId());
}
}
// Controller
@RestController
public class OrderController {
@Autowired
private OrderService orderService;
@GetMapping("/order/create")
public String createOrder() {
orderService.createOrder();
return "✅ 订单已创建";
}
}
测试:
第一次请求:
🌐 创建RequestContext:123e4567-e89b-12d3-a456-426614174000
📦 创建订单,Request ID: 123e4567-e89b-12d3-a456-426614174000
第二次请求:
🌐 创建RequestContext:987fcdeb-89ab-45ef-bcde-123456789abc
📦 创建订单,Request ID: 987fcdeb-89ab-45ef-bcde-123456789abc
每次请求的requestId都不同!✅
🎨 第七幕:自定义作用域
实现Scope接口
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);
System.out.println("🧵 创建线程级别的Bean:" + name);
}
return bean;
}
@Override
public Object remove(String name) {
Map<String, Object> scope = threadScope.get();
return scope.remove(name);
}
@Override
public void registerDestructionCallback(String name, Runnable callback) {
// 可选:注册销毁回调
}
@Override
public Object resolveContextualObject(String key) {
return null;
}
@Override
public String getConversationId() {
return Thread.currentThread().getName();
}
}
注册自定义作用域
@Configuration
public class CustomScopeConfig {
@Bean
public static BeanFactoryPostProcessor beanFactoryPostProcessor() {
return beanFactory -> {
if (beanFactory instanceof ConfigurableListableBeanFactory) {
ConfigurableListableBeanFactory factory =
(ConfigurableListableBeanFactory) beanFactory;
// 注册自定义作用域
factory.registerScope("thread", new ThreadScope());
System.out.println("✅ 注册自定义作用域:thread");
}
};
}
}
使用自定义作用域
@Component
@Scope("thread")
public class ThreadLocalBean {
private final long threadId;
public ThreadLocalBean() {
this.threadId = Thread.currentThread().getId();
System.out.println("🧵 创建ThreadLocalBean,线程ID:" + threadId);
}
public long getThreadId() {
return threadId;
}
}
🎪 第八幕:实战场景
场景1:缓存服务(单例)
@Component
@Scope("singleton") // 单例
public class CacheService {
private final Map<String, Object> cache = new ConcurrentHashMap<>();
public void put(String key, Object value) {
cache.put(key, value);
System.out.println("💾 缓存数据:" + key);
}
public Object get(String key) {
return cache.get(key);
}
@PreDestroy
public void destroy() {
System.out.println("🗑️ 清空缓存");
cache.clear();
}
}
场景2:订单处理(原型)
@Component
@Scope("prototype") // 原型
public class OrderProcessor {
private final String processorId;
private Order order;
public OrderProcessor() {
this.processorId = UUID.randomUUID().toString();
System.out.println("🎭 创建订单处理器:" + processorId);
}
public void process(Order order) {
this.order = order;
// 处理订单逻辑...
System.out.println("📦 处理订单:" + order.getId());
}
@PreDestroy
public void destroy() {
System.out.println("🗑️ 销毁订单处理器:" + processorId);
}
}
场景3:用户上下文(请求)
@Component
@RequestScope
@Data
public class UserContext {
private String userId;
private String username;
private String ip;
private Map<String, Object> attributes = new HashMap<>();
public UserContext(HttpServletRequest request) {
this.ip = request.getRemoteAddr();
// 从请求中提取用户信息...
System.out.println("🌐 创建用户上下文,IP:" + ip);
}
}
⚠️ 第九幕:常见坑点
坑点1:原型Bean的销毁方法不会被调用
@Component
@Scope("prototype")
public class PrototypeBean {
@PreDestroy
public void destroy() {
// ❌ 这个方法不会被自动调用!
// 因为Spring不管理原型Bean的完整生命周期
System.out.println("销毁Bean");
}
}
// ✅ 解决方案:手动销毁
@Component
public class BeanManager implements DisposableBean {
private List<DisposableBean> prototypeBeans = new ArrayList<>();
public void registerBean(DisposableBean bean) {
prototypeBeans.add(bean);
}
@Override
public void destroy() throws Exception {
for (DisposableBean bean : prototypeBeans) {
bean.destroy();
}
}
}
坑点2:忘记设置代理模式
// ❌ 错误:单例注入请求Bean
@Component
@RequestScope // 没有设置proxyMode
public class RequestBean {
// ...
}
@Component // 单例
public class SingletonBean {
@Autowired
private RequestBean requestBean; // 有问题!
}
// ✅ 正确:设置代理
@Component
@RequestScope(proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestBean {
// ...
}
坑点3:Web作用域在非Web环境使用
// ❌ 在非Web环境(如单元测试)使用request作用域
@SpringBootTest // 不是WebEnvironment
public class RequestScopeTest {
@Autowired
private RequestBean requestBean; // 会报错!
}
// ✅ 正确:指定Web环境
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class RequestScopeTest {
@Autowired
private RequestBean requestBean; // OK!
}
🎯 第十幕:选择合适的作用域
决策树
你的Bean是否有状态?
├─ 无状态 → singleton(默认)✅
└─ 有状态
└─ 状态的生命周期?
├─ 整个应用 → singleton
├─ 每次使用都不同 → prototype
├─ HTTP请求 → request
├─ 用户会话 → session
└─ 其他 → 自定义作用域
常见Bean的作用域选择
| Bean类型 | 推荐作用域 | 原因 |
|---|---|---|
| Service层 | singleton | 无状态,可共享 |
| Repository层 | singleton | 无状态,可共享 |
| Controller层 | singleton | 无状态,可共享 |
| 配置类 | singleton | 全局共享 |
| 工具类 | singleton | 无状态,可共享 |
| 命令对象 | prototype | 每次执行都不同 |
| 表单对象 | prototype | 每次提交都不同 |
| 请求日志 | request | 每个请求独立 |
| 购物车 | session | 跟随用户会话 |
🎉 总结
Bean作用域对比
| 作用域 | 创建时机 | 销毁时机 | 适用场景 |
|---|---|---|---|
| singleton | 容器启动 | 容器销毁 | 无状态Bean |
| prototype | 每次获取 | 不管理 | 有状态Bean |
| request | 请求开始 | 请求结束 | Web请求数据 |
| session | 会话开始 | 会话结束 | 用户会话数据 |
核心要点
| 特性 | 说明 | 图标 |
|---|---|---|
| 默认单例 | 性能最优 | 👑 |
| 原型按需 | 灵活但开销大 | 🎭 |
| Web作用域 | 自动管理生命周期 | 🌐 |
| 代理模式 | 解决依赖问题 | 🔧 |
| 自定义扩展 | 满足特殊需求 | 🎨 |
🚀 课后作业
- 初级: 创建一个原型Bean,测试每次获取都是新对象
- 中级: 实现一个购物车(session作用域)功能
- 高级: 实现一个自定义的线程级别作用域
📚 参考资料
- Spring Framework官方文档 - Bean Scopes
- 《Spring实战》- Bean作用域章节
最后的彩蛋: 🎁
Spring的Bean作用域就像:
- Singleton = 全校一个校长 👨💼
- Prototype = 每人一个笔记本 📓
- Request = 每次点餐一个订单 🍕
- Session = 每人一个购物车 🛒
记住这句话:
"选对作用域,Bean的生命才完美!" 💡
关注我,下期更精彩! 🌟
让Bean们各司其职,各得其所! 🎯✨
#Spring #Bean作用域 #Singleton #Prototype #最佳实践