0

Làm Chủ Spring `@Scope("prototype")`

1. Giới Thiệu: @Scope("prototype") Là Gì?

Annotation @Scope("prototype") trong Spring Framework là một cấu hình quan trọng để quản lý vòng đời (lifecycle) của một Spring bean.

Khác với scope mặc định là Singleton (chỉ có một instance duy nhất được tạo ra cho toàn bộ ứng dụng và được chia sẻ bởi mọi component), scope Prototype đảm bảo rằng một instance mới của bean sẽ được tạo ra mỗi lần nó được yêu cầu từ container.

Đặc Điểm Chính:

Scope Thời Điểm Khởi Tạo Quản Lý Vòng Đời An Toàn Luồng (Thread Safety)
Singleton (Mặc Định) Khi ứng dụng khởi động (hoặc lần yêu cầu đầu tiên). Được Spring quản lý toàn bộ (từ khởi tạo đến lúc ứng dụng tắt, bao gồm @PostConstruct@PreDestroy). Phải được đảm bảo thủ công (tức là bean phải là stateless).
Prototype Theo yêu cầu (khi được gọi). Spring chỉ quản lý khởi tạo (@PostConstruct) và chấm dứt quản lý ngay lập tức. Tự động an toàn luồng nhờ tính biệt lập (không chia sẻ trạng thái).

2. Trường Hợp Sử Dụng Cốt Lõi: Biệt Lập Trạng Thái (Stateful Logic)

Lý do chính để sử dụng scope Prototype là để cách ly an toàn các object stateful (có trạng thái), đặc biệt khi một bean Singleton (được sử dụng bởi nhiều luồng - thread) cần thực hiện một tác vụ liên quan đến dữ liệu tạm thời, có thể thay đổi (mutable data).

Vấn Đề: Điều Kiện Tranh Chấp (Race Conditions) trong Singleton

Nếu một bean Singleton có một field instance lưu trữ dữ liệu riêng cho một tác vụ duy nhất (ví dụ: bộ đếm, tổng đang chạy, hoặc ID giao dịch), hai luồng đồng thời truy cập vào cùng một field đó sẽ dẫn đến race condition và dữ liệu bị hỏng, không thể dự đoán.

@Component // NGUY HIỂM: Mặc định là Singleton
public class Calculator {
    private int runningTotal = 0; // Trạng thái bị chia sẻ
    // ...
}
// Hai luồng sửa đổi 'runningTotal' đồng thời sẽ làm hỏng dữ liệu.

Giải Pháp: Biệt Lập Prototype

Bằng cách định scope cho object stateful là prototype, mọi yêu cầu từ client sẽ nhận được một instance mới, sạch, đảm bảo cách ly hoàn toàn giữa các luồng.

3. Vấn Đề Lệch Scope (Scope Mismatch - Sai Lầm Thường Gặp)

Sai lầm phổ biến nhất là cố gắng inject một bean Prototype vào một bean Singleton bằng cách sử dụng @Autowired tiêu chuẩn.

@Service // Singleton
public class CheckoutService {
    
    // VẤN ĐỀ: Việc inject này chỉ xảy ra MỘT LẦN khi ứng dụng khởi động.
    // Singleton sẽ luôn giữ cùng MỘT instance Prototype.
    @Autowired 
    private TransactionProcessor processor; 

    // Mọi người dùng sẽ chia sẻ cùng MỘT instance này. Mục đích prototype bị phá vỡ.
}

Để giải quyết sự lệch scope này, bạn phải sử dụng một phương pháp ủy quyền yêu cầu tạo bean trở lại cho Spring container tại thời điểm chạy (runtime), chứ không chỉ lúc khởi động.

4. Các Giải Pháp cho Việc Inject Prototype Theo Yêu Cầu

Có hai giải pháp hiện đại được khuyến nghị để khắc phục vấn đề lệch scope:

Giải Pháp A: Annotation @Lookup

Annotation @Lookup là giải pháp ngắn gọn nhất của Spring. Nó được áp dụng cho một method abstract bên trong client Singleton. Spring sẽ tạo một subclass tại thời điểm chạy để override method này, thực hiện lệnh getBean() mới mỗi khi method được gọi.

Khía Cạnh Chi Tiết
Yêu Cầu Client Phải là một class abstract.
Mã Lệnh ```java

@Service public abstract class ReportService { @Lookup public abstract MyPrototypeBean getNewPrototypeBean(); // Spring sẽ tự động implement

public void runReport() {
    MyPrototypeBean instance = getNewPrototypeBean(); // INSTANCE MỚI mỗi lần gọi
    // ...
}

}

Giải Pháp B: Sử Dụng Provider (Tiêu Chuẩn JSR-330)


Interface `Provider` (hoặc `ObjectFactory` của Spring) cho phép bạn inject một factory vào client của mình. Việc gọi method `get()` của factory sẽ yêu cầu container tạo một instance mới. Đây là cách tiếp cận theo **mẫu hình Factory** rõ ràng và được chuẩn hóa hơn.

| Khía Cạnh | Chi Tiết |
| :--- | :--- |
| **Yêu Cầu Client** | Class concrete (không cần `abstract`). |
| **Mã Lệnh** | ```java
@Service
public class DataService {
    @Autowired
    private Provider<MyPrototypeBean> provider; // Tiêu chuẩn JSR-330
    
    public void processData() {
        MyPrototypeBean instance = provider.get(); // INSTANCE MỚI mỗi lần gọi
        // ...
    }
}
``` |
| **Lưu Ý** | **`Provider`** (`javax.inject.Provider` hoặc `jakarta.inject.Provider`) được ưu tiên hơn `ObjectFactory` vì nó tuân thủ tiêu chuẩn DI JSR-330, giúp mã dễ chuyển đổi hơn. |

## 5. Vòng Đời Prototype: Mảnh Ghép Bị Thiếu

Phần kiến thức cuối cùng về Prototype là vòng đời của nó:

* **Khởi tạo:** Spring gọi đúng method `@PostConstruct` trên instance mới.
* **Hủy bỏ (Destruction):** Spring **KHÔNG** quản lý việc hủy bỏ các bean prototype. Ngay khi instance được tạo và giao cho client, container sẽ quên nó.
* **Hệ quả:** Method **`@PreDestroy` sẽ không được gọi** đối với bean prototype. Nếu bean prototype của bạn giữ các tài nguyên tốn kém (kết nối cơ sở dữ liệu, file handle), mã client (hoặc một bean post-processor tùy chỉnh) có trách nhiệm phải **tự dọn dẹp** các tài nguyên đó.

### Tóm Tắt Các Nguyên Tắc Tốt Nhất (Best Practices)

| Nguyên Tắc | Hành Động |
| :--- | :--- |
| **Biệt Lập** | Chỉ sử dụng `@Scope("prototype")` cho các bean stateful. |
| **Injection** | **TUYỆT ĐỐI KHÔNG** sử dụng `@Autowired` trực tiếp để inject Prototype vào Singleton. |
| **Giải Quyết** | Sử dụng **`@Lookup`** hoặc **`Provider`** để lấy instance mới theo yêu cầu. |
| **Dọn Dẹp** | Nhớ rằng **`@PreDestroy` không được hỗ trợ** cho bean prototype. |

All rights reserved

Viblo
Hãy đăng ký một tài khoản Viblo để nhận được nhiều bài viết thú vị hơn.
Đăng kí