Spring Bean Scopes: Singleton, Prototype, và Hơn Thế Nữa | Java Spring Roadmap

Chào mừng trở lại với series “Java Spring Roadmap”! Trên hành trình khám phá Framework mạnh mẽ này, chúng ta đã cùng nhau đi qua những khái niệm cốt lõi như Bean, Dependency Injection (DI), IoC Container, và các phương pháp cấu hình. Chúng ta đã biết cách Spring tạo và quản lý các Bean – những đối tượng xương sống của ứng dụng.

Tuy nhiên, một câu hỏi quan trọng nảy sinh: Khi bạn định nghĩa một Bean, Spring sẽ tạo ra bao nhiêu instance (thể hiện) của Bean đó? Và các instance này sẽ được chia sẻ hay mỗi lần yêu cầu lại tạo mới? Câu trả lời nằm ở khái niệm Spring Bean Scope.

Hiểu rõ về Scope không chỉ giúp bạn quản lý tài nguyên hiệu quả hơn mà còn là chìa khóa để xây dựng các ứng dụng Spring phức tạp, đặc biệt là các ứng dụng web có trạng thái (stateful). Trong bài viết này, chúng ta sẽ đi sâu vào các Scope phổ biến nhất: Singleton (mặc định), Prototype, và lướt qua một số Scope dành cho môi trường web. Hãy cùng nhau khám phá!

Bean Scope là gì?

Một cách đơn giản nhất, Bean Scope định nghĩa vòng đời và khả năng hiển diện của một Bean trong Spring IoC Container. Nó xác định số lượng instance của một Bean được tạo ra bởi Container và cách các instance này được truy cập khi có yêu cầu.

Hãy tưởng tượng Spring IoC Container như một nhà máy sản xuất và quản lý các linh kiện (Bean). Scope của một Bean giống như “chính sách sản xuất” cho linh kiện đó:

  • Chính sách A: Chỉ sản xuất một linh kiện duy nhất cho toàn bộ nhà máy, bất kỳ ai cần đều dùng chung linh kiện đó. (Đây là Scope Singleton)
  • Chính sách B: Mỗi khi có người yêu cầu, sản xuất một linh kiện mới tinh. Ai yêu cầu người nấy dùng, không ai dùng chung. (Đây là Scope Prototype)
  • Và có những chính sách khác phức tạp hơn dành cho môi trường web, ví dụ: sản xuất 1 linh kiện cho mỗi “đơn đặt hàng” (request), hoặc 1 linh kiện cho mỗi “khách hàng thân thiết” (session).

Việc chọn đúng Scope cho Bean của bạn là rất quan trọng. Chọn sai có thể dẫn đến các vấn đề về chia sẻ trạng thái không mong muốn (state sharing issues), lãng phí tài nguyên hoặc hành vi không nhất quán của ứng dụng.

Scope Mặc Định: Singleton

Trong hầu hết các trường hợp và là hành vi mặc định của Spring Container, Bean được định nghĩa với Scope là Singleton.

Đặc điểm của Singleton Scope:

  • Spring Container sẽ tạo ra chỉ một instance duy nhất của Bean đó cho mỗi Spring IoC Container.
  • Instance này được lưu trữ trong cache của Container.
  • Mọi yêu cầu DI đến Bean này sẽ luôn nhận được cùng một instance.
  • Đây là Scope tiết kiệm tài nguyên nhất vì tránh được chi phí tạo và hủy nhiều instance.

Khi nào sử dụng Singleton?

Singleton là Scope phù hợp cho các Bean không chứa trạng thái (stateless) hoặc trạng thái của chúng được chia sẻ chung (ví dụ: cấu hình đọc từ file). Các ví dụ điển hình là:

  • Service Layer: Các service class thường chứa logic nghiệp vụ và không giữ trạng thái riêng cho từng người dùng hoặc request.
  • Data Access Objects (DAOs) / Repositories: Các đối tượng này tương tác với cơ sở dữ liệu và thường không giữ trạng thái riêng cho từng thao tác.
  • Utility Classes: Các lớp cung cấp các hàm tiện ích chung.

Cấu hình Singleton Scope:

Sử dụng Annotation (phổ biến trong Spring Boot):

@Service
// Mặc định là singleton, annotation @Scope("singleton") có thể bỏ qua
// @Scope("singleton")
public class MySingletonService {
    private int counter = 0; // Cẩn thận với trạng thái trong Singleton!

    public int incrementAndGetCounter() {
        return ++counter; // Vấn đề tiềm ẩn khi nhiều request dùng chung
    }

    public void doSomething() {
        System.out.println("Doing something in Singleton Service: " + this.hashCode());
    }
}

Sử dụng XML:

<bean id="mySingletonService" class="com.example.MySingletonService" scope="singleton"/>
<!-- Hoặc không cần khai báo 'scope="singleton"' vì đây là mặc định -->
<bean id="mySingletonService" class="com.example.MySingletonService"/>

Lưu ý quan trọng về Singleton và Trạng thái:

Bạn phải cực kỳ cẩn thận khi đưa các biến instance có thể thay đổi (mutable state) vào một Bean Singleton. Vì chỉ có một instance duy nhất được chia sẻ bởi nhiều luồng (thread) hoặc nhiều request, việc thay đổi trạng thái trong Singleton có thể dẫn đến các vấn đề đồng bộ hóa (synchronization issues) hoặc dữ liệu không nhất quán. Luôn cố gắng giữ cho Bean Singleton là stateless hoặc chỉ chứa trạng thái bất biến (immutable state).

Scope Khác Biệt: Prototype

Trái ngược hoàn toàn với Singleton, Scope Prototype yêu cầu Spring Container tạo ra một instance mới của Bean mỗi khi nó được yêu cầu.

Đặc điểm của Prototype Scope:

  • Spring Container không quản lý vòng đời đầy đủ của Bean Prototype. Nó chỉ tạo ra instance và inject (tiêm) vào nơi cần thiết.
  • Sau khi instance được tạo và giao cho client, Container không còn theo dõi nữa.
  • Client (đối tượng nhận được Bean Prototype) phải chịu trách nhiệm quản lý vòng đời tiếp theo, bao gồm cả việc giải phóng tài nguyên (nếu cần).
  • Điều này có nghĩa là các phương thức lifecycle như @PreDestroy (được gọi trước khi Bean Singleton bị hủy) sẽ không được Spring tự động gọi cho Bean Prototype.

Khi nào sử dụng Prototype?

Prototype là Scope phù hợp cho các Bean có trạng thái (stateful) và trạng thái đó là duy nhất cho từng ngữ cảnh sử dụng (ví dụ: cho từng người dùng, từng giao dịch, từng request cụ thể) và không nên được chia sẻ.

  • Objects Representation: Các đối tượng mô tả một thực thể có trạng thái tạm thời (ví dụ: một đối tượng cấu hình cho một tác vụ cụ thể, một đối tượng giỏ hàng cho một người dùng).
  • Objects that are not thread-safe: Nếu một lớp không an toàn cho đa luồng và bạn cần sử dụng nó trong môi trường đa luồng, việc tạo instance mới mỗi lần (Prototype) là một cách an toàn hơn Singleton.

Cấu hình Prototype Scope:

Sử dụng Annotation:

@Component // Hoặc @Service, @Repository, v.v.
@Scope("prototype")
public class MyPrototypeBean {
    private String message; // Biến trạng thái

    public String getMessage() {
        return message;
    }

    public void setMessage(String message) {
        this.message = message;
    }

    public void showMessage() {
        System.out.println("Message: " + this.message + " | Instance: " + this.hashCode());
    }

    // @PreDestroy sẽ KHÔNG được Spring gọi cho Prototype Scope
    // public void destroy() { ... }
}

Sử dụng XML:

<bean id="myPrototypeBean" class="com.example.MyPrototypeBean" scope="prototype"/>

Ví dụ sử dụng Prototype:

Giả sử bạn có một Bean Singleton (ví dụ: một Service) cần sử dụng một Bean Prototype.

@Service // Singleton Scope (default)
public class MyServiceUsingPrototype {

    // Injecting Prototype into Singleton - Cần cẩn thận!
    // Nếu inject trực tiếp thế này, bạn sẽ luôn nhận được CÙNG một instance prototype
    // được tạo ra KHI service này được khởi tạo.
    // Để nhận instance MỚI mỗi lần dùng, cần cách khác.
    // private MyPrototypeBean prototypeBean; // <-- KHÔNG khuyến khích nếu cần instance MỚI mỗi lần

    // Cách 1: Sử dụng ApplicationContext để lấy instance thủ công
    // @Autowired
    // private ApplicationContext context;

    // public void process(String userMessage) {
    //     MyPrototypeBean bean = context.getBean(MyPrototypeBean.class);
    //     bean.setMessage(userMessage);
    //     bean.showMessage(); // Mỗi lần gọi process sẽ tạo bean mới
    // }

    // Cách 2 (Tốt hơn): Sử dụng ObjectFactory/ObjectProvider (Spring 4.2+)
    @Autowired
    private ObjectProvider<MyPrototypeBean> prototypeBeanProvider;

    public void process(String userMessage) {
        MyPrototypeBean bean = prototypeBeanProvider.getObject(); // Nhận instance MỚI
        bean.setMessage(userMessage);
        bean.showMessage(); // Mỗi lần gọi process sẽ tạo bean mới
    }

    // Cách 3: Method Injection (Ít dùng hơn trong cấu hình hiện đại)
    // @Lookup
    // public abstract MyPrototypeBean createPrototypeBean();

    // public void process(String userMessage) {
    //     MyPrototypeBean bean = createPrototypeBean(); // Spring sẽ override method này để trả về instance mới
    //     bean.setMessage(userMessage);
    //     bean.showMessage();
    // }
}

Ví dụ trên minh họa vấn đề phổ biến khi injecting Prototype vào Singleton. Inject trực tiếp bằng @Autowired chỉ tạo một instance Prototype duy nhất khi Singleton được tạo. Để nhận instance mới mỗi lần sử dụng, bạn cần nhờ Spring “tạo giùm” instance đó vào thời điểm bạn cần, bằng cách sử dụng ApplicationContext, ObjectProvider, hoặc Method Injection (@Lookup).

Beyond the Basics: Các Scope Khác (Chủ yếu cho Web)

Ngoài Singleton và Prototype là hai Scope cốt lõi, Spring cung cấp thêm một số Scope khác, chủ yếu hữu ích trong môi trường ứng dụng web:

  • Request Scope:
    • Tạo ra một instance Bean mới cho mỗi HTTP request đến ứng dụng.
    • Instance này chỉ hiển diện trong suốt vòng đời của request đó.
    • Sau khi request kết thúc, instance Bean cũng bị loại bỏ.
    • Hữu ích cho các Bean cần giữ trạng thái tạm thời chỉ liên quan đến một request cụ thể (ví dụ: thông tin request-specific, log transaction id của request).
  • Session Scope:
    • Tạo ra một instance Bean mới cho mỗi HTTP Session.
    • Instance này hiển diện trong suốt vòng đời của Session đó.
    • Hữu ích cho các Bean cần giữ trạng thái liên quan đến một người dùng cụ thể trong suốt quá trình tương tác của họ với ứng dụng (ví dụ: thông tin giỏ hàng của người dùng đã đăng nhập).
  • Application Scope:
    • Tạo ra một instance Bean mới cho toàn bộ ServletContext (tức là toàn bộ ứng dụng web).
    • Instance này được chia sẻ bởi tất cả các request và Session.
    • Giống với Singleton, nhưng ở cấp độ ứng dụng web (không phải cấp độ IoC Container – có thể có nhiều Container trong một ứng dụng web lớn). Thường được coi là Singleton trong ngữ cảnh web.
    • Hữu ích cho các Bean cần giữ trạng thái chung cho toàn bộ ứng dụng (ví dụ: bộ đếm số lượng người dùng truy cập).
  • WebSocket Scope:
    • Tạo ra một instance Bean mới cho mỗi WebSocket session.
    • Hữu ích khi làm việc với các ứng dụng sử dụng WebSocket để giao tiếp hai chiều với client.

Lưu ý về các Scope web:

Để sử dụng các Scope như request, session, application, và websocket, bạn cần có một môi trường web và cấu hình Listener phù hợp (ví dụ: RequestContextListener hoặc ServletListenerRegistrationBean trong Spring Boot).

Cấu hình các Scope web:

Sử dụng Annotation:

@Component
@Scope(value = "request", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class RequestScopedBean {
    private String transactionId; // Trạng thái riêng cho từng request
    // getters, setters...
}

@Component
@Scope(value = "session", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class SessionScopedBean {
    private List<Item> cartItems; // Trạng thái riêng cho từng session
    // getters, setters...
}

@Component
@Scope(value = "application", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class ApplicationScopedBean {
    private AtomicInteger hitCounter = new AtomicInteger(0); // Trạng thái chung cho ứng dụng
    // methods...
}

Lưu ý thuộc tính proxyMode = ScopedProxyMode.TARGET_CLASS. Khi inject một Bean có Scope hẹp hơn (ví dụ: Request, Session) vào một Bean có Scope rộng hơn (ví dụ: Singleton), Spring không thể inject trực tiếp instance Bean đó (vì instance đó chỉ tồn tại trong ngữ cảnh hẹp hơn). Thay vào đó, Spring sẽ inject một proxy. Proxy này sẽ ủy thác các cuộc gọi phương thức đến instance Bean thực tế của ngữ cảnh hiện tại (request, session, v.v.). TARGET_CLASS chỉ định rằng Spring sẽ tạo một CGLIB proxy dựa trên lớp (class) của Bean. Nếu Bean implement interface, bạn có thể dùng ScopedProxyMode.INTERFACES.

Sử dụng XML:

<bean id="requestScopedBean" class="com.example.RequestScopedBean" scope="request">
    <aop:scoped-proxy proxy-target-class="true"/>
</bean>

<bean id="sessionScopedBean" class="com.example.SessionScopedBean" scope="session">
    <aop:scoped-proxy proxy-target-class="true"/>
</bean>

<bean id="applicationScopedBean" class="com.example.ApplicationScopedBean" scope="application">
    <aop:scoped-proxy proxy-target-class="true"/>
</bean>

Cần thêm namespace aop trong khai báo XML và dependency Spring AOP.

Tóm tắt các Scope chính

Dưới đây là bảng so sánh nhanh các Scope phổ biến nhất:

Scope Số lượng Instance Chia sẻ Instance Trạng thái (Nên có) Vòng đời (Hủy Bean) Ngữ cảnh chính
Singleton 1 instance duy nhất/Container Có (tất cả dùng chung) Stateless (Không trạng thái) Được Spring quản lý hoàn toàn (bao gồm @PreDestroy) Mặc định, ứng dụng chung
Prototype Instance mới mỗi lần yêu cầu Không (mỗi lần yêu cầu là mới) Stateful (Có trạng thái) Spring chỉ tạo, không quản lý hủy Objects tạm thời, non-thread-safe
Request 1 instance/HTTP Request Chỉ trong cùng 1 Request Request-specific Stateful Kết thúc khi Request kết thúc Ứng dụng Web (Request)
Session 1 instance/HTTP Session Chỉ trong cùng 1 Session User/Session-specific Stateful Kết thúc khi Session kết thúc Ứng dụng Web (Session)
Application 1 instance/ServletContext Có (tất cả dùng chung trong Web App) Application-wide Stateful (cẩn thận) Kết thúc khi Web App tắt Ứng dụng Web (Toàn cục)

Chọn Scope Phù Hợp: Đâu là nguyên tắc?

Quyết định sử dụng Scope nào phụ thuộc vào bản chất và mục đích sử dụng của Bean đó:

  1. Bắt đầu với Singleton: Luôn coi Singleton là lựa chọn mặc định. Phần lớn các Service, Repository, DAO trong ứng dụng là stateless và nên là Singleton để tiết kiệm tài nguyên.
  2. Chuyển sang Prototype khi cần trạng thái độc lập: Nếu Bean của bạn cần giữ trạng thái riêng cho từng lần sử dụng (ví dụ: cho từng tác vụ, từng transaction nhỏ) và trạng thái đó không được chia sẻ, hãy cân nhắc Prototype. Nhớ rằng bạn phải tự quản lý vòng đời sau khi nhận Bean.
  3. Sử dụng Scope Web cho trạng thái ứng dụng Web: Nếu bạn đang xây dựng ứng dụng web và cần quản lý trạng thái theo Request hoặc Session của người dùng, hãy sử dụng Scope tương ứng. Đừng cố gắng nhồi nhét trạng thái Request/Session vào Bean Singleton thông thường.

Một sai lầm phổ biến là sử dụng Prototype quá mức cần thiết. Việc tạo và thu gom rác (garbage collection) nhiều instance Prototype có thể ảnh hưởng đến hiệu suất. Chỉ sử dụng Prototype khi thực sự cần một instance mới cho mỗi lần sử dụng.

Kết nối với các Khái niệm đã học

Việc hiểu về Bean Scope làm sâu sắc thêm sự hiểu biết của bạn về cách Spring IoC Container hoạt động và quản lý các Bean. Khi bạn yêu cầu một Bean thông qua Dependency Injection (ví dụ: bằng @Autowired), Container sẽ kiểm tra Scope của Bean được yêu cầu:

  • Nếu là Singleton: Container trả về instance duy nhất đã được tạo hoặc tạo mới nếu chưa có.
  • Nếu là Prototype: Container tạo một instance mới và trả về.
  • Nếu là Request/Session/etc.: Container kiểm tra ngữ cảnh hiện tại (request/session) và trả về hoặc tạo mới instance phù hợp với ngữ cảnh đó.

Scope là một phần không thể thiếu trong việc định nghĩa Bean, cùng với việc cấu hình (Annotations/XML) và tiêm phụ thuộc (DI).

Kết luận

Spring Bean Scope là một khái niệm tưởng chừng đơn giản nhưng lại có tác động sâu sắc đến cách ứng dụng của bạn hoạt động và quản lý tài nguyên. Nắm vững sự khác biệt giữa Singleton, Prototype, và các Scope web giúp bạn đưa ra quyết định thiết kế đúng đắn, tránh các lỗi liên quan đến quản lý trạng thái và xây dựng các ứng dụng Spring mạnh mẽ, hiệu quả.

Hãy luôn bắt đầu với Singleton và chỉ xem xét các Scope khác khi có lý do chính đáng, đặc biệt là khi Bean cần giữ trạng thái độc lập theo ngữ cảnh sử dụng.

Chúng ta đã đi thêm một bước quan trọng trên hành trình “Java Spring Roadmap”. Việc làm chủ Bean Scope là nền tảng vững chắc để tiếp tục khám phá những khía cạnh phức tạp hơn của Spring Framework.

Bạn có bất kỳ câu hỏi nào về Bean Scope không? Hoặc bạn đã gặp phải những vấn đề thú vị nào liên quan đến Scope trong dự án thực tế? Hãy chia sẻ trong phần bình luận bên dưới!

Đừng quên theo dõi các bài viết tiếp theo trong series “Java Spring Roadmap” để tiếp tục nâng cao kiến thức về Spring nhé!

Xem lại các bài trước trong series:

Chỉ mục