之前在專案裡因為搞不清楚 Bean scope,導致一個坑埋了好久才發現。乾脆就把 Spring 的六種 scope 整理一下,這樣以後不會再踩到相同的坑。

六種 Scope 速覽

Spring Bean 的六種 scope 分別是:

  1. singleton - 預設,整個 container 只有一個實例
  2. prototype - 每次 getBean 都新建一個實例
  3. request - 每個 HTTP request 一個實例
  4. session - 每個 HTTP session 一個實例
  5. application - 整個 ServletContext 一個實例
  6. 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); // true
}
}

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 每次都是同一個!
// 即使它的 scope 是 prototype,也不會每次都新建
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; // 實現由 Spring 提供
}

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 {
// scope 會從 properties 檔案讀取
}

在 application.properties 裡設定:

1
app.scope=prototype

重點整理

  • singleton:預設,整個 container 一個實例,注意 thread-safe
  • prototype:每次都新建,常用於臨時物件
  • request/session/application/websocket:web application 專用
  • 重點提醒:singleton 注入 prototype 要用 ObjectProvider 或 @Lookup 才會真的每次新建
  • 後三種 scope(request、session、websocket)比較少用,通常只有特定場景才需要

下次寫程式時,先想清楚這個 Bean 到底需要什麼 scope,一開始想好就不會埋坑了。