🎭 Spring Bean作用域:单身汉 vs 复制人大军!

34 阅读11分钟

副标题:一个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全局共享数据🏢
websocketWebSocket每个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作用域自动管理生命周期🌐
代理模式解决依赖问题🔧
自定义扩展满足特殊需求🎨

🚀 课后作业

  1. 初级: 创建一个原型Bean,测试每次获取都是新对象
  2. 中级: 实现一个购物车(session作用域)功能
  3. 高级: 实现一个自定义的线程级别作用域

📚 参考资料

  • Spring Framework官方文档 - Bean Scopes
  • 《Spring实战》- Bean作用域章节

最后的彩蛋: 🎁

Spring的Bean作用域就像:

  • Singleton = 全校一个校长 👨‍💼
  • Prototype = 每人一个笔记本 📓
  • Request = 每次点餐一个订单 🍕
  • Session = 每人一个购物车 🛒

记住这句话:

"选对作用域,Bean的生命才完美!" 💡


关注我,下期更精彩! 🌟

让Bean们各司其职,各得其所! 🎯✨


#Spring #Bean作用域 #Singleton #Prototype #最佳实践