之前在專案裡因為搞不清楚 Bean scope,導致一個坑埋了好久才發現。乾脆就把 Spring 的六種 scope 整理一下,這樣以後不會再踩到相同的坑。
六種 Scope 速覽
Spring Bean 的六種 scope 分別是:
- singleton - 預設,整個 container 只有一個實例
- prototype - 每次 getBean 都新建一個實例
- request - 每個 HTTP request 一個實例
- session - 每個 HTTP session 一個實例
- application - 整個 ServletContext 一個實例
- websocket - 每個 WebSocket session 一個實例
後面四種(request、session、application、websocket)只在 web application 中可用。
Singleton 作用域
這是預設的 scope。如果你沒有特別指定,Spring 就會把 Bean 當成 singleton:
1 2 3 4 5 6
| @Component public class UserService { public void getUser() { System.out.println("Getting user"); } }
|
或者明確指定:
1 2 3 4 5 6 7
| @Component @Scope("singleton") public class UserService { public void getUser() { System.out.println("Getting user"); } }
|
Singleton Bean 在 Spring container 啟動時就會被建立(預設),並且整個應用生命週期內都只有一個實例。所以多個地方都注入同一個 UserService,拿到的都是同一個物件。
1 2 3 4 5 6 7 8 9 10 11 12
| @Service public class OrderService { @Autowired private UserService userService1; @Autowired private UserService userService2; public void testSingleton() { System.out.println(userService1 == userService2); } }
|
Prototype 作用域
Prototype 就不一樣了,每次你從 Spring 拿 Bean,都會給你一個全新的實例:
1 2 3 4 5 6 7 8 9
| @Component @Scope("prototype") public class RequestContext { private String requestId; public void setRequestId(String id) { this.requestId = id; } }
|
這個 Bean 拿出來就是用完即丟,所以很適合存放一些請求相關的臨時資料。
Singleton 注入 Prototype 的坑
這裡要特別注意。假設你有一個 singleton 的 service,想要注入一個 prototype 的 Bean:
1 2 3 4 5 6 7 8 9 10 11
| @Service public class OrderService { @Autowired private RequestContext requestContext; public void placeOrder() { requestContext.setRequestId("12345"); } }
|
為什麼會這樣?因為 Spring 在建立 singleton Bean 時就把 prototype 的 dependency 注入進去了,只注入一次。之後即使 prototype 的 scope 說要新建,但已經被注入了,就沒有新建的機會。
解法一:ObjectProvider
1 2 3 4 5 6 7 8 9 10 11
| @Service public class OrderService { @Autowired private ObjectProvider<RequestContext> requestContextProvider; public void placeOrder() { RequestContext requestContext = requestContextProvider.getObject(); requestContext.setRequestId("12345"); } }
|
ObjectProvider 提供了 getObject() 方法,每次呼叫都會向 Spring 要一個新的 Bean。
解法二:@Lookup
1 2 3 4 5 6 7 8 9 10 11 12 13
| @Service public class OrderService { @Lookup protected RequestContext getRequestContext() { return null; } public void placeOrder() { RequestContext requestContext = getRequestContext(); requestContext.setRequestId("12345"); } }
|
@Lookup 這個註解告訴 Spring,這個方法的實現由 Spring 透過 CGLIB 代理來提供。每次呼叫都會建新實例。
Request 作用域
Request scope 的 Bean 在每個 HTTP request 的生命週期內只有一個實例,request 結束後就會被銷毀。常用來存放單一 request 的相關資訊:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| @Component @Scope("request") public class RequestLogger { private String requestId; @PostConstruct public void init() { this.requestId = UUID.randomUUID().toString(); System.out.println("Request started: " + requestId); } @PreDestroy public void cleanup() { System.out.println("Request ended: " + requestId); } }
|
或者用註解方式:
1 2 3 4 5
| @Component @Scope(value = WebApplicationContext.SCOPE_REQUEST) public class RequestLogger { }
|
Session 作用域
Session scope 的 Bean 在單個 HTTP session 的生命週期內有一個實例,不同的 session 會有不同的實例。很適合存放使用者相關的資訊(雖然一般還是用 session attribute 比較多):
1 2 3 4 5 6 7 8 9 10
| @Component @Scope("session") public class UserSession { private String username; private List<String> permissions; public void setUsername(String name) { this.username = name; } }
|
Application 作用域
Application scope 就是整個 ServletContext 的生命週期,通常用於存放全應用共享的狀態:
1 2 3 4 5 6 7 8 9
| @Component @Scope("application") public class AppConfig { private int totalRequests; public synchronized void incrementRequestCount() { totalRequests++; } }
|
因為是全應用共享,如果要改變狀態的話要注意 thread-safe。
WebSocket 作用域
WebSocket scope 在使用 WebSocket 時,為每個 WebSocket session 建立一個實例。這個比較少用,通常在實時應用裡才會碰到:
1 2 3 4 5 6 7 8 9 10
| @Component @Scope("websocket") public class WebSocketHandler { private String sessionId; @PostConstruct public void init() { this.sessionId = UUID.randomUUID().toString(); } }
|
如何用 SpEL 語法
有時候你想要用 SpEL(Spring Expression Language)來設定 scope,可以這樣做:
1 2 3 4 5
| @Component @Scope("${app.scope}") public class ConfigurableBean { }
|
在 application.properties 裡設定:
重點整理
- singleton:預設,整個 container 一個實例,注意 thread-safe
- prototype:每次都新建,常用於臨時物件
- request/session/application/websocket:web application 專用
- 重點提醒:singleton 注入 prototype 要用 ObjectProvider 或 @Lookup 才會真的每次新建
- 後三種 scope(request、session、websocket)比較少用,通常只有特定場景才需要
下次寫程式時,先想清楚這個 Bean 到底需要什麼 scope,一開始想好就不會埋坑了。
你的鼓勵將被轉換為我明天繼續加班的動力(真的)。 ❤️