Spring Core & Boot
19/41
Bài 19 / 41~12 phútBean Lifecycle & ScopesLộ trình · chặng 13/21Miễn phí… lượt xem

Request & Session Scope: Cơ Chế Scoped Proxy Cho Web

Tìm hiểu cơ chế ThreadLocal của Request và Session scope trong Spring Web, vai trò của Scoped Proxy và cách tránh mất context với @Async.

TL;DR: @RequestScope tạo một bean instance cho mỗi HTTP request; @SessionScope tạo một instance cho mỗi HTTP session. Request scope vận hành dựa trên RequestContextHolder lưu HttpServletRequest qua ThreadLocal. Khi một Singleton bean tiêm một bean request-scoped, Spring bắt buộc phải dùng Scoped Proxy (CGLIB subclass) để trì hoãn việc lấy instance thật đến khi có request thực tế. Cạm bẫy lớn nhất là @Async: khi chuyển sang thread pool khác, ThreadLocal không tự lan truyền khiến request context bị mất hoàn toàn.

Nắm vững hai web scope này giúp bạn quản lý trạng thái phiên làm việc và ngữ cảnh phân tán chuẩn xác mà không gây rò rỉ bộ nhớ.

1. Request Scope giải quyết bài toán chia sẻ dữ liệu nào trong Web?

Trong ứng dụng web, nhu cầu gắn một mã định danh duy nhất (requestId hay traceId) vào mọi log xuyên suốt từ Controller xuống Service và Repository là rất phổ biến. Nếu truyền thủ công qua tham số của từng hàm, danh sách tham số sẽ phình to và làm bẩn mã nghiệp vụ.

Request Scope giải quyết triệt để vấn đề này bằng cách cho phép bạn định nghĩa một bean sống theo vòng đời của một HTTP request:

Java
@Component
@RequestScope
public class RequestContext {
    private final String requestId = UUID.randomUUID().toString();
    public String getRequestId() { return requestId; }
}

@Service
public class OrderService {
    private final RequestContext context; // Tiem Scoped Proxy

    public OrderService(RequestContext context) {
        this.context = context;
    }

    public void processOrder() {
        log.info("[{}] Processing order logic", context.getRequestId());
    }
}

Mỗi khi có một HTTP request gọi tới OrderService, phương thức context.getRequestId() luôn trả về chính xác ID của request đó mà không bị nhầm lẫn với các request khác đang chạy song song.

2. RequestContextHolder lưu trữ thông tin qua ThreadLocal như thế nào?

Cơ chế nền tảng giúp Request Scope vận hành là ThreadLocal. Trong mô hình servlet truyền thống của Tomcat, mỗi request được giao cho một worker thread xử lý từ đầu đến cuối:

Java
// Logic co ban ben trong RequestContextHolder
public abstract class RequestContextHolder {
    private static final ThreadLocal<RequestAttributes> attributesHolder =
        new NamedThreadLocal<>("Request attributes");

    public static void setRequestAttributes(RequestAttributes attributes) {
        attributesHolder.set(attributes);
    }

    public static RequestAttributes currentRequestAttributes() {
        RequestAttributes attrs = attributesHolder.get();
        if (attrs == null) {
            throw new IllegalStateException("No thread-bound request found");
        }
        return attrs;
    }
}

Khi nhận request, DispatcherServlet lưu request vào RequestContextHolder. Khi Spring cần lấy một request-scoped bean, nó truy xuất từ request.getAttribute(beanName):

Một RequestContextProxy duy nhất nằm trong OrderService singleton, phục vụ hai thread Tomcat: mỗi lần gọi RequestScope.get nó tra ThreadLocal của thread đang chạy, nên thread A nhận RequestContext id R1 còn thread B nhận id R2

Vì mỗi worker thread có bản sao ThreadLocal riêng biệt, các request chạy song song tự nhiên được cách ly mà không cần đến bảng lưu trữ toàn cục nào.

3. Vì sao Singleton bắt buộc phải dùng Scoped Proxy khi tiêm Web Bean?

Singleton Bean được khởi tạo ngay lúc ứng dụng khởi động (ApplicationContext startup) khi chưa hề có bất kỳ HTTP request nào. Nếu Spring cố gắng tiêm một bean request-scoped trực tiếp vào Singleton tại thời điểm này, RequestContextHolder.currentRequestAttributes() sẽ ném ngay IllegalStateException vì ThreadLocal đang rỗng.

Để giải quyết mâu thuẫn về thời điểm khởi tạo, Spring sử dụng Scoped Proxy:

Hai pha đặt cạnh nhau: lúc khởi động chưa có request nào nên container tiêm một proxy CGLIB tạo được vì vỏ rỗng; mỗi request thì proxy hỏi RequestScope.get để tra ThreadLocal và trả về RequestContext của đúng request đó

Annotation @RequestScope là viết tắt của:

Java
@Scope(value = WebApplicationContext.SCOPE_REQUEST, proxyMode = ScopedProxyMode.TARGET_CLASS)

Thay vì tiêm đối tượng thật, Spring tiêm một CGLIB Proxy rỗng vào Singleton Bean lúc khởi động. Khi một phương thức trên proxy được gọi trong lúc xử lý request, Proxy mới tra cứu ThreadLocal để tìm bean thật tương ứng của request hiện tại và ủy quyền lời gọi xuống đó.

4. Tại sao annotation @Async làm mất Request Context và cách xử lý?

Khi một phương thức được đánh dấu @Async, Spring sẽ đẩy tác vụ đó sang một thread pool khác (TaskExecutor) thay vì chạy trên Tomcat worker thread hiện tại:

Java
@Service
public class OrderService {
    private final RequestContext context;

    @Async
    public CompletableFuture<Void> sendEmailAsync() {
        // Chay tren thread pool: ThreadLocal khong tu lan truyen!
        String reqId = context.getRequestId(); // Nem IllegalStateException
        return CompletableFuture.completedFuture(null);
    }
}

Vì ThreadLocal chỉ gắn liền với thread ban đầu, worker thread mới không hề có thông tin về request hiện tại, dẫn tới lỗi No thread-bound request found.

Hai giải pháp khắc phục:

  1. Truyền dữ liệu tường minh qua tham số hàm (Khuyến nghị): Lấy dữ liệu cần thiết từ request bean trước khi gọi hàm bất đồng bộ và truyền vào qua tham số. Cách này trong sáng và dễ kiểm thử nhất.
  2. Sử dụng TaskDecorator: Cấu hình executor với bộ trang trí tự động sao chép RequestAttributes sang thread con:
Java
@Configuration
@EnableAsync
public class AsyncConfig implements AsyncConfigurer {
    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setTaskDecorator(runnable -> {
            RequestAttributes context = RequestContextHolder.getRequestAttributes();
            return () -> {
                try {
                    RequestContextHolder.setRequestAttributes(context);
                    runnable.run();
                } finally {
                    RequestContextHolder.resetRequestAttributes();
                }
            };
        });
        executor.initialize();
        return executor;
    }
}

5. Session Scope yêu cầu Serializable vì lý do gì trong môi trường Cluster?

@SessionScope duy trì một bean instance duy nhất cho toàn bộ phiên làm việc của một người dùng (HTTP Session):

Java
@Component
@SessionScope
public class UserCart implements Serializable {
    private static final long serialVersionUID = 1L;
    private final List<String> itemIds = new ArrayList<>();

    private transient HttpClient client; // Danh dau transient neu khong the serialize

    public void addItem(String id) { itemIds.add(id); }
}

Bean session bắt buộc phải implements Serializable vì hai lý do thực tế trong môi trường phân tán:

  1. Sao lưu Session trong Cluster (Replication): Khi chạy nhiều instance ứng dụng phía sau Load Balancer, nếu một server gặp sự cố, session sẽ được chuyển giao sang server khác thông qua serialization.
  2. Lưu trữ ngoài với Spring Session: Khi lưu trữ session tập trung trong Redis hoặc Database, đối tượng bắt buộc phải được chuyển đổi thành chuỗi byte.

Nếu một trường trong bean không thể tuần tự hóa (như database connection hay socket), bạn bắt buộc phải đánh dấu trường đó bằng từ khóa transient.

6. Khi nào nên và không nên sử dụng Web Scope trong ứng dụng hiện đại?

Tình huống sử dụngPhạm vi phù hợpGhi chú kiến trúc
Lưu Correlation ID / Trace ID@RequestScopeRất hữu ích khi muốn gắn ID vào log mọi tầng
Lưu cache tạm trong nội bộ request@RequestScopeTránh truy vấn lặp lại cùng một dữ liệu trong 1 request
Giỏ hàng ứng dụng Monolith cũ@SessionScopeBắt buộc Serializable và xử lý tranh chấp đa luồng
REST API Stateless dùng JWTKhông dùng Web ScopeĐẩy trạng thái về Client hoặc lưu trong Redis/Database
Lưu thông tin người dùng hiện tạiSecurityContextHolderNên dùng chuẩn của Spring Security thay vì Session

7. Thiết kế luồng truyền Trace ID xuyên suốt các tầng và qua luồng Async?

Khi xây dựng hệ thống phân tán, việc giữ trọn vẹn traceId từ tầng web xuống các tác vụ ngầm được thiết kế chuẩn mực như sau:

Java
@Service
public class PaymentProcessingService {
    private final RequestContext requestContext;
    private final NotificationService notificationService;

    public PaymentProcessingService(RequestContext requestContext, 
                                    NotificationService notificationService) {
        this.requestContext = requestContext;
        this.notificationService = notificationService;
    }

    public void processPayment() {
        String currentTraceId = requestContext.getRequestId();
        log.info("[{}] Processing payment", currentTraceId);
        
        // Trich xuat traceId truoc khi goi sang thread pool bat dong bo
        notificationService.sendReceiptAsync(currentTraceId);
    }
}

Bằng cách truyền currentTraceId trực tiếp như một giá trị nguyên thủy vào phương thức bất đồng bộ, hệ thống vừa giữ được khả năng truy vết liền mạch, vừa tránh được ngoại lệ rỗng ThreadLocal.

Tóm tắt

  • @RequestScope và @SessionScope sử dụng ThreadLocal qua RequestContextHolder để cô lập dữ liệu theo từng luồng xử lý web.
  • Singleton bean tiêm web-scoped bean bắt buộc phải thông qua Scoped Proxy để tách biệt thời điểm khởi tạo lúc startup khỏi lúc gọi method.
  • Phương thức @Async chạy trên thread pool khác nên không có ThreadLocal từ Tomcat; cần truyền dữ liệu qua tham số hoặc dùng TaskDecorator.
  • Session bean bắt buộc phải implement Serializable để hỗ trợ cơ chế nhân bản trong môi trường multi-node và Spring Session.

Tự kiểm tra

Tự kiểm tra
0/5 câu đã trả lời
  1. Q1
    Cơ chế cốt lõi nào giúp Request Scope cô lập dữ liệu giữa hàng trăm HTTP request chạy đồng thời mà không cần bảng quản lý tập trung?
  2. Q2
    Tại sao Spring Container sẽ ném ngoại lệ IllegalStateException lúc startup nếu tiêm trực tiếp một Request-scoped bean vào Singleton bean mà KHÔNG dùng Scoped Proxy?
  3. Q3
    Vai trò chính của Scoped Proxy (CGLIB subclass) khi tiêm vào một Singleton bean là gì?
  4. Q4
    Điều gì xảy ra khi bạn gọi một phương thức trên Request-scoped bean bên trong một method được đánh dấu @Async?
  5. Q5
    Tại sao các Session-scoped bean bắt buộc phải implements giao diện java.io.Serializable trong ứng dụng production?

Bài tiếp theo: Tổng Kết Module 03: Vòng Đời Và Phạm Vi Spring Bean

Bài này có giúp bạn hiểu bản chất không?

Bài này đáng gửi cho bạn học cùng?

Copy link đã gắn nguồn — dán group, chat, hoặc LinkedIn.

Lộ trình Junior Java Backend Developer

Chặng 13/21 · Spring IoC, DI & Bean — bài 11/17 của chặng

Thảo luận & góp ý

Hỏi đáp về bài này

Bài tiếp theo

Tổng Kết Module 03: Vòng Đời Và Phạm Vi Spring Bean