Spring Boot/Bean scopes — singleton, prototype, request, session
~26 phútSpring là gì & nền tảng IoCMiễn phí

Bean scopes — singleton, prototype, request, session

Spring có 6 scope built-in. Bài này giải thích cơ chế lưu trữ của từng scope, vì sao singleton là default, pitfall scope-mismatch (singleton hold prototype), 3 cách giải, scoped proxy mode (INTERFACES vs TARGET_CLASS), thread-binding cơ chế của request/session, custom scope với ThreadLocal, và refresh scope của Spring Cloud.

Mỗi bean trong container không sống độc lập — nó có scope, quyết định 2 thứ: container giữ bao nhiêu instance của bean, và mỗi caller getBean() nhận instance giống hay khác nhau. Bài 03-04 đều giả định bean là singleton — vì đó là default, và 95% bean là singleton. Bài này bóc 5% còn lại: khi nào bạn cần prototype, request, session, và scope-mismatch trap kinh điển.

1. 6 scope built-in của Spring

flowchart LR
    All["Spring Scopes"]
    Core["Spring Framework<br/>(luon co)"]
    Web["Spring Web<br/>(can servlet container)"]
    Sin["singleton<br/>(default)"]
    Pro["prototype"]
    Req["request"]
    Ses["session"]
    App["application"]
    Wsk["websocket"]

    All --> Core
    All --> Web
    Core --> Sin
    Core --> Pro
    Web --> Req
    Web --> Ses
    Web --> App
    Web --> Wsk

    style Sin fill:#d1fae5
ScopeSố instanceLifetimeKhi nào dùng
singleton1 / containerSuốt đời containerDefault. Stateless service, repository, configuration
prototypeN / mỗi getBean()Caller manageStateful object cần riêng cho từng caller (rất hiếm)
request1 / HTTP requestEnd of requestObject lưu state per-request (vd request-scoped audit log)
session1 / HTTP sessionEnd of sessionShopping cart, user session state
application1 / ServletContextWeb app lifetimeTương đương singleton trong web context (hiếm cần)
websocket1 / WebSocket sessionEnd of WS sessionState per WebSocket connection

Khai báo scope:

@Service
@Scope("prototype")              // hoac BeanDefinition.SCOPE_PROTOTYPE constant
public class OrderProcessor { ... }

@Component
@RequestScope                    // shortcut cho @Scope("request") + proxy
public class RequestAuditLog { ... }

1.1 Cơ chế lưu trữ — container giữ bean ở đâu

flowchart TB
    BeanFactory["DefaultListableBeanFactory"]
    SS["singletonObjects<br/>Map<String, Object>"]
    PS["No cache<br/>(new instance each call)"]
    RS["RequestScope<br/>= ThreadLocal<HttpServletRequest>.attributes"]
    SeS["SessionScope<br/>= HttpSession.attributes"]
    AS["ApplicationScope<br/>= ServletContext.attributes"]

    BeanFactory --> SS
    BeanFactory --> PS
    BeanFactory --> RS
    BeanFactory --> SeS
    BeanFactory --> AS

    style SS fill:#d1fae5
  • Singleton: cache trong Map<String, Object> singletonObjects của DefaultListableBeanFactory — đây là "bean cache" nổi tiếng.
  • Prototype: không cache — mỗi getBean() instantiate mới.
  • Request/Session/Application: lưu trong attributes của Servlet object tương ứng. Cách lookup: RequestContextHolder giữ HttpServletRequest qua ThreadLocal — bean nhìn vào ThreadLocal để lấy request hiện tại.

Hệ quả: thread-locality của request scope đến từ ThreadLocal chứ không phải Spring magic. Nếu bạn @Async (sang thread khác), ThreadLocal không propagate → request scope không truy cập được. Đây là pitfall classic.

2. Singleton — vì sao là default

Spring chọn singleton làm default vì 90% bean là stateless — không có field thay đổi sau init. Repository chỉ wrap DB; service chỉ chứa logic; configuration chỉ chứa setting. Stateless object có thể share giữa 1000 thread đồng thời mà không cần synchronize.

@Service
public class OrderService {
    private final OrderRepository repo;            // dep injected, set 1 lan, khong doi
    private final PaymentGateway payment;

    public OrderService(OrderRepository repo, PaymentGateway payment) {
        this.repo = repo;
        this.payment = payment;
    }

    public Order place(OrderRequest req) {         // method stateless
        var order = Order.from(req);
        repo.save(order);
        payment.charge(order.total());
        return order;
    }
}

Bean này singleton: 1 instance shared cho mọi request. Vì không có mutable state, không có race condition.

Cảnh báo: singleton không tự động thread-safe. Nếu bạn thêm field mutable:

@Service
public class BadCounter {
    private int count = 0;          // mutable state shared

    public void increment() {
        count++;                    // race condition !
    }
}

count++ không atomic (3 step: read, +1, write). 2 thread tăng cùng lúc có thể chỉ +1 thay vì +2. Fix: AtomicInteger, hoặc synchronized, hoặc tốt nhất — không giữ state trong singleton.

Quy tắc thực dụng: nếu bean có field mutable, hãy nghĩ kỹ. 90% case nên đẩy state ra khỏi bean (vào DB, cache, request param), giữ bean stateless.

2.1 Concurrency implications per scope

ScopeConcurrency modelMutable state
singletonMulti-thread đọc cùng instancePhải thread-safe (immutable, atomic, synchronized)
prototypeMỗi caller instance riêngAn toàn — không share
request1 instance / request, request đa phần 1 threadAn toàn (nếu không spawn thread con)
session1 instance / session, session có thể nhiều request đồng thờiPhải thread-safe

Lưu ý quan trọng về session: 1 user mở 2 tab cùng đăng nhập → 2 request song song, cùng session. Session-scoped bean share giữa 2 thread → cần thread-safe. Đây là điểm developer hay quên — "session = user" không đúng nghĩa "1 thread / instance".

3. Prototype — và pitfall lớn nhất

Prototype: mỗi lần getBean() (hoặc inject) trả về instance mới.

@Component
@Scope("prototype")
public class Workflow {
    private List<Step> steps = new ArrayList<>();

    public Workflow add(Step s) { steps.add(s); return this; }
    public void run() { steps.forEach(Step::execute); }
}

Có vẻ hợp lý — mỗi workflow là instance riêng có state riêng. Nhưng pitfall nằm ở scope mismatch:

@Service                                // SINGLETON
public class ReportService {
    @Autowired private Workflow workflow;   // PROTOTYPE — nhung inject 1 lan

    public void runReport() {
        workflow.add(...).run();        // moi lan goi, dung CUNG instance
    }
}

Workflow đáng lẽ prototype (instance mới mỗi lần) nhưng vì được inject vào singleton một lần lúc startup, mọi call runReport() dùng chính instance đó. State steps tích lại qua nhiều call → bug subtle.

Đây là bug số 1 của prototype scope. Hệ quả: scope inner (prototype) "kế thừa" scope outer (singleton) khi inject thẳng.

3.1 3 cách giải

Cách 1 — ObjectProvider<T> (Spring 4.3+, KHUYẾN NGHỊ):

@Service
public class ReportService {
    private final ObjectProvider<Workflow> workflowProvider;

    public ReportService(ObjectProvider<Workflow> workflowProvider) {
        this.workflowProvider = workflowProvider;
    }

    public void runReport() {
        Workflow w = workflowProvider.getObject();   // moi lan goi tao instance moi
        w.add(...).run();
    }
}

ObjectProvider là API hiện đại, type-safe, null-safe (ifAvailable, ifUnique). Đây là cách chuẩn 2026.

Cách 2 — @Lookup method:

@Service
public abstract class ReportService {
    public void runReport() {
        Workflow w = createWorkflow();
        w.add(...).run();
    }

    @Lookup
    protected abstract Workflow createWorkflow();
}

Spring tạo subclass override createWorkflow() để trả getBean(Workflow.class). Class phải abstract — Spring sẽ instantiate subclass concrete tại runtime. Cú pháp legacy, ít dùng.

Cách 3 — Scoped proxy:

@Component
@Scope(value = "prototype", proxyMode = ScopedProxyMode.TARGET_CLASS)
public class Workflow { ... }

@Service
public class ReportService {
    @Autowired private Workflow workflow;   // INJECT PROXY

    public void runReport() {
        workflow.add(...).run();   // moi method call qua proxy → proxy resolve instance moi
    }
}

Proxy delegate mỗi method call về getBean() mới. Cách này hoạt động nhưng performance hit (proxy overhead) và state semantic khó hơn — không khuyến nghị cho prototype.

3.2 ScopedProxyMode — INTERFACES vs TARGET_CLASS

@Scope(proxyMode = ...) có 4 giá trị:

ModeCơ chếKhi nào dùng
NO (default)Không proxy — inject thẳngCùng scope với caller (vd 2 bean cùng singleton)
INTERFACESJDK dynamic proxy implement interfaceBean implement interface
TARGET_CLASSCGLIB subclassBean không implement interface, hoặc cần ép subclass
DEFAULTINTERFACES nếu có interface, không thì TARGET_CLASSAuto-pick

@RequestScope/@SessionScope/@ApplicationScope shortcut tự bao proxyMode = TARGET_CLASS — phù hợp default cho 95% case.

Khi nào quan tâm? Khi bạn:

  • Inject bean qua interface trong code business → JDK proxy INTERFACES phù hợp.
  • Cần instanceof class concrete → CGLIB TARGET_CLASS.

99% trường hợp dùng TARGET_CLASS (default Boot), không cần nghĩ.

4. Request scope — bean per-request

request scope: 1 instance mỗi HTTP request, destroy khi request done. Yêu cầu Spring Web (servlet container).

@Component
@RequestScope                            // = @Scope(value = "request", proxyMode = TARGET_CLASS)
public class RequestContext {
    private final String requestId = UUID.randomUUID().toString();
    private final long startTime = System.currentTimeMillis();

    public String getRequestId() { return requestId; }
    public long getElapsed() { return System.currentTimeMillis() - startTime; }
}

@Service
public class OrderService {
    @Autowired private RequestContext ctx;   // inject proxy

    public Order place(...) {
        log.info("processing order requestId={}", ctx.getRequestId());
        // ...
    }
}

@RequestScope annotation tự bao proxyMode = TARGET_CLASS. Lý do: bean inject vào singleton (như OrderService), nhưng mỗi request cần instance khác — proxy resolve runtime.

4.1 Cơ chế thread-binding

Request scope hoạt động qua RequestContextHolder — class giữ HttpServletRequest qua ThreadLocal:

// Internal Spring code (don gian hoa):
public class RequestContextHolder {
    private static final ThreadLocal<RequestAttributes> requestAttributesHolder = new ThreadLocal<>();
    public static RequestAttributes currentRequestAttributes() { return requestAttributesHolder.get(); }
}

// RequestScope lookup bean:
public Object get(String name, ObjectFactory<?> factory) {
    var attrs = RequestContextHolder.currentRequestAttributes();
    Object scopedObject = attrs.getAttribute(name, RequestAttributes.SCOPE_REQUEST);
    if (scopedObject == null) {
        scopedObject = factory.getObject();
        attrs.setAttribute(name, scopedObject, RequestAttributes.SCOPE_REQUEST);
    }
    return scopedObject;
}

Tomcat thread → 1 request → set ThreadLocal lúc nhận request → clear khi response. Bean lookup attribute từ ThreadLocal.

Pitfall classic: @Async chuyển sang thread khác → ThreadLocal không propagate (mặc định) → request-scoped bean không lookup được.

@Service
public class OrderService {
    @Autowired RequestContext ctx;        // request-scoped

    @Async
    public CompletableFuture<Result> processAsync() {
        return CompletableFuture.supplyAsync(() -> {
            String reqId = ctx.getRequestId();   // EXCEPTION — khong co request context o thread Async
        });
    }
}

Fix:

  • Pass requestId qua method param thay vì lookup ctx trong async thread.
  • Hoặc dùng RequestContextHolder.setRequestAttributes(attrs, true) trước async — inheritable=true cho phép propagate.
  • Hoặc dùng TaskDecorator propagate context tự động.

4.2 Use case thực tế

  • Request ID/correlation ID cho distributed tracing.
  • Request-scoped cache (vd cache user permission cho request).
  • MDC log context.

Đa phần dùng cases trên không cần custom bean — Spring có sẵn RequestContextHolder, MDC từ SLF4J, micrometer trace context. Custom request-scope bean chỉ cần khi cấu trúc data phức tạp.

5. Session scope và pitfall serialization

Session scope: 1 instance mỗi HTTP session, destroy khi session timeout hoặc invalidate.

@Component
@SessionScope
public class ShoppingCart implements Serializable {     // PHAI Serializable
    private static final long serialVersionUID = 1L;

    private final List<CartItem> items = new ArrayList<>();

    public void add(CartItem item) { items.add(item); }
    public List<CartItem> getItems() { return List.copyOf(items); }
}

Serializable bắt buộc vì session có thể replicate giữa node trong cluster (sticky session vẫn cần serialize cho failover). Field non-serializable → NotSerializableException runtime.

Use case truyền thống: shopping cart, multi-step form (wizard). Tuy nhiên trong app modern (SPA + REST API stateless + JWT), session scope ít dùng — state thường client-side hoặc trong DB/Redis.

5.1 Spring Session — externalize session

Production-grade pattern: lưu session ngoài JVM (Redis/JDBC) qua Spring Session:

# application.yml
spring:
  session:
    store-type: redis
  data:
    redis:
      host: redis.internal
      port: 6379

Ưu điểm:

  • Session survive restart pod (stateless deployment).
  • Multi-instance share session (no sticky session).
  • Session size unlimited (không phụ thuộc heap).

Spring Session là project riêng (không thuộc Framework core), dùng Spring Data Redis under the hood.

6. Custom scope

Spring cho phép tự định nghĩa scope:

public class TenantScope implements Scope {
    private final ThreadLocal<Map<String, Object>> tenant = ThreadLocal.withInitial(HashMap::new);

    public Object get(String name, ObjectFactory<?> factory) {
        return tenant.get().computeIfAbsent(name, k -> factory.getObject());
    }

    public Object remove(String name) {
        return tenant.get().remove(name);
    }

    public void registerDestructionCallback(String name, Runnable callback) { /* ... */ }
    public Object resolveContextualObject(String key) { return null; }
    public String getConversationId() { return null; }
}

// Register:
@Configuration
public class ScopeConfig {
    @Bean
    public CustomScopeConfigurer scopeConfigurer() {
        var c = new CustomScopeConfigurer();
        c.addScope("tenant", new TenantScope());
        return c;
    }
}

@Component
@Scope("tenant")
public class TenantContext { ... }

Use case thực tế: multi-tenant SaaS (1 instance per tenant), feature flag context, batch job context. Hiếm khi cần — built-in scope cover phần lớn nhu cầu.

6.1 Refresh scope của Spring Cloud (preview)

Spring Cloud thêm scope refresh — bean tái tạo khi config thay đổi:

@Component
@RefreshScope
public class FeatureFlags {
    @Value("${feature.audit.enabled}") private boolean auditEnabled;
}

// Khi POST /actuator/refresh hoac receive event tu Spring Cloud Bus:
// FeatureFlags bi destroy + tao lai voi gia tri property moi

Đây là cơ chế chính của dynamic config update không cần restart pod. Module 11 (Microservices) sẽ đào sâu.

7. Pitfall tổng hợp

Nhầm 1: Inject prototype vào singleton mà không dùng provider/proxy.

@Service class S { @Autowired Workflow w; }   // w la instance fixed, khong "prototype"

✅ Dùng ObjectProvider<Workflow>.

Nhầm 2: Dùng singleton cho mutable state shared cho user-specific data.

@Service
public class CurrentUserHolder {
    private User user;   // singleton + mutable + per-request data = data leak giua user
}

✅ Dùng @RequestScope hoặc lưu trong RequestContextHolder/MDC. Nguy cơ bảo mật cực cao — user A nhận data user B.

Nhầm 3: Quên Serializable cho session bean. ✅ Implement Serializable, mark non-serializable field transient.

Nhầm 4: @PreDestroy trên prototype.

@Scope("prototype")
public class Worker {
    @PreDestroy void cleanup() { ... }   // KHONG chay
}

✅ Spring không track destroy của prototype. Caller cleanup, hoặc dùng BeanFactory.destroyBean(bean).

Nhầm 5: Scope name typo.

@Scope("singletone")   // typo - khong throw, fall back ve... nothing predictable

✅ Dùng constant ConfigurableBeanFactory.SCOPE_SINGLETON/SCOPE_PROTOTYPE. Hoặc shortcut @RequestScope, @SessionScope.

Nhầm 6: Dùng request scope cho non-web context (vd background job).

@Async public void runJob() { useRequestScopedBean(); }   // khong co request → BeanCreationException

✅ Refactor — pass data qua method param, không qua scope.

Nhầm 7: Session-scoped bean nhưng giả định 1 thread / instance.

@SessionScope
public class UserCart {
    private List<Item> items = new ArrayList<>();   // KHONG thread-safe
}

✅ Session có thể nhiều request đồng thời (user mở 2 tab). Dùng Collections.synchronizedList() hoặc CopyOnWriteArrayList hoặc lock.

Nhầm 8: Inject request-scoped bean rồi gọi từ @Async. ✅ Pass data qua method param, hoặc setup TaskDecorator propagate context, hoặc đơn giản: không dùng request scope với async.

8. 📚 Deep Dive Spring Reference

📚 Tài liệu chính chủ

Reference docs:

Source:

  • org.springframework.context.annotation.ScopeMetadataResolver — cách Spring đọc @Scope từ annotation.
  • org.springframework.aop.scope.ScopedProxyFactoryBean — implementation của scoped proxy.
  • org.springframework.web.context.request.RequestContextHolder — ThreadLocal cho request scope.

Ghi chú: đọc section "Scoped Beans as Dependencies" của Spring docs ít nhất 1 lần — đây là chỗ giải thích vì sao prototype bị scope-mismatch. Đó là kiến thức core mà nhiều dev miss.

9. Tóm tắt

  • 6 scope built-in: singleton (default), prototype, request, session, application, websocket.
  • Singleton là default vì 90% bean stateless — share giữa thread không cần sync.
  • Singleton không tự thread-safe — field mutable trong singleton là race condition.
  • Cơ chế lưu trữ: singleton → Map cache; prototype → no cache; request → ThreadLocal-bound HttpServletRequest attribute.
  • Prototype: mỗi getBean() instance mới. Pitfall: inject prototype vào singleton fix instance — dùng ObjectProvider<T>.
  • Request/Session scope cần Spring Web. Session bean phải Serializable.
  • Modern app ít dùng session scope — state client-side hoặc DB/Redis (Spring Session).
  • ScopedProxyMode: INTERFACES (JDK proxy) vs TARGET_CLASS (CGLIB). Default Boot dùng TARGET_CLASS.
  • Request scope ↔ ThreadLocal — @Async không propagate → bean lookup fail. Fix: pass data param hoặc TaskDecorator.
  • Session scope không đảm bảo 1 thread/instance — user mở 2 tab → race condition. Cần thread-safe.
  • @PreDestroy không chạy cho prototype — caller chịu trách nhiệm cleanup.
  • Sai scope cho per-user data trong singleton là lỗi bảo mật — user A nhận data user B.
  • Custom scope cho domain (tenant, batch job) — dùng khi built-in không cover.
  • Spring Cloud thêm @RefreshScope cho dynamic config update không restart.

10. Tự kiểm tra

Tự kiểm tra
Q1
Bạn thiết kế class UserContext chứa user đang đăng nhập, dùng trong nhiều service. Đặt scope nào, vì sao?

Sai cách phổ biến: singleton với field currentUser — gây data leak nghiêm trọng giữa user. 1 thread set currentUser = userA, thread khác đọc thấy A → user B truy cập với quyền A.

Cách đúng tuỳ context:

  • Web app stateful (session-based): @SessionScope — instance per session, isolated tự nhiên.
  • REST API stateless (JWT): @RequestScope — instance per HTTP request, parse từ JWT vào.
  • Mọi case: tốt hơn nữa — không tạo bean, dùng SecurityContextHolder của Spring Security (ThreadLocal-based) hoặc RequestContextHolder. 2 holder này được Spring quản lý đúng cho web context.

Quy tắc: per-user data tuyệt đối không để trong singleton mutable.

Q2
Đoạn sau in gì? Vì sao?
@Component
@Scope("prototype")
public class Greeter {
  private int count = 0;
  public String greet() { count++; return "hello-" + count; }
}

@Service
public class App {
  @Autowired Greeter g;

  public void run() {
      System.out.println(g.greet());
      System.out.println(g.greet());
      System.out.println(g.greet());
  }
}

In:

hello-1
hello-2
hello-3

Vì sao: mặc dù Greeter là prototype, nó được inject một lần vào singleton App lúc startup. Sau đó g trỏ đến 1 instance cố định. Mỗi greet() tăng count trên cùng instance.

Đây chính là pitfall scope-mismatch. Để mỗi greet() dùng instance mới (đúng nghĩa prototype):

@Service
public class App {
  @Autowired ObjectProvider<Greeter> provider;

  public void run() {
      for (int i = 0; i < 3; i++) {
          System.out.println(provider.getObject().greet());   // moi lan instance moi
      }
  }
}

Lúc này in hello-1, hello-1, hello-1 — đúng nghĩa prototype.

Q3
Singleton bean có thể là thread-safe không? Liệt kê 3 điều kiện.

Có thể, với 3 điều kiện:

  • 1. Không có mutable field sau khi init: tất cả field final hoặc immutable. Đây là pattern phổ biến nhất — service Spring stateless.
  • 2. Field mutable bắt buộc thì phải thread-safe: dùng AtomicInteger, ConcurrentHashMap, CopyOnWriteArrayList, hoặc bao bằng synchronized/ReentrantLock. Không dùng HashMap, ArrayList trần.
  • 3. Method không thay đổi state external chia sẻ mà không sync — ví dụ ghi vào file shared, gọi external service không idempotent. Cần handle qua transaction/lock external.

Quy tắc: nếu cảm thấy bean cần state, hãy nghĩ lại. 90% case state nên đẩy ra DB/cache, giữ bean stateless. Đó là "Spring way".

Q4
Vì sao @RequestScope mặc định bao proxyMode = TARGET_CLASS? Nếu bỏ proxy mode đi, code sẽ vỡ thế nào?

Lý do: request-scoped bean thường được inject vào singleton bean (vd RequestAuditLog inject vào OrderService). Singleton tạo 1 lần lúc startup — lúc đó chưa có HTTP request nào, không thể tạo request-scoped bean → throw ScopeNotActiveException.

Proxy giải quyết bằng cách: inject 1 proxy object (tạo được lúc startup, không phụ thuộc request). Mỗi method call qua proxy → proxy resolve runtime "bean nào của request hiện tại?" → delegate.

Nếu bỏ proxy:

@Component
@Scope("request")     // KHONG co proxy
public class RequestAuditLog { ... }

@Service
public class OrderService {
  @Autowired RequestAuditLog log;   // STARTUP FAIL
}

Spring throw lúc startup vì không tạo được instance RequestAuditLog ngoài request context. Khắc phục: thêm proxyMode = TARGET_CLASS hoặc dùng shortcut @RequestScope.

Khi nào không cần proxy: bean request-scoped chỉ inject vào bean cùng scope (request-scoped → request-scoped). Hiếm.

Q5
Bạn build app multi-tenant SaaS — mỗi tenant có config riêng (DB connection string, theme, feature flag). Bean TenantConfig nên scope nào? Nêu tradeoff của 3 lựa chọn.
  • Singleton + Map keyed by tenantId:
    @Service
    public class TenantConfigService {
      Map<String, TenantConfig> configs;
      public TenantConfig get(String tenantId) { return configs.get(tenantId); }
    }
    Ưu: đơn giản, ít object. Nhược: service phải explicitly pass tenantId mọi nơi — leak vào API.
  • Custom tenant scope:
    @Component
    @Scope("tenant")
    public class TenantConfig { ... }
    Spring container giữ map tenantId → instance. Inject như normal bean, scope tự resolve theo ThreadLocal<tenantId> set ở filter/interceptor.Ưu: service code clean, không leak tenantId. Nhược: phải implement Scope interface, set ThreadLocal đúng chỗ, cleanup khi tenant context kết thúc.
  • Request scope (resolve tenantId mỗi request):Ưu: built-in, không tự code scope. Nhược: tốn — mỗi request load lại config từ DB/cache, không có lợi thế caching cross-request cho cùng tenant.

Lựa chọn thực tế: custom tenant scope cho config (ít thay đổi, cache tốt) + request scope cho user context (đổi mỗi request). Mix là OK.

Q6
Đoạn code này có bug gì? Xảy ra khi nào? Fix ra sao?
@RestController
public class OrderController {
  @Autowired RequestContext reqCtx;        // @RequestScope
  @Autowired OrderService orderSvc;

  @PostMapping("/orders")
  public Order placeOrder(@RequestBody OrderRequest req) {
      return orderSvc.placeAsync(req).join();
  }
}

@Service
public class OrderService {
  @Autowired RequestContext reqCtx;

  @Async
  public CompletableFuture<Order> placeAsync(OrderRequest req) {
      return CompletableFuture.supplyAsync(() -> {
          String reqId = reqCtx.getRequestId();   // <-- bug here
          // ...
      });
  }
}

Bug: reqCtx.getRequestId() trong @Async method throw exception IllegalStateException: No thread-bound request found.

Vì sao: @Async chạy trên thread pool khác Tomcat thread. RequestContextHolder giữ HttpServletRequest qua ThreadLocal — không propagate sang thread mới. Lookup request-scoped bean từ thread không có request context → fail.

3 cách fix:

  • Cách 1 — Pass data qua param:
    public CompletableFuture<Order> placeAsync(OrderRequest req, String reqId) {
      return CompletableFuture.supplyAsync(() -> { /* use reqId */ });
    }
    Đơn giản nhất, recommended cho hầu hết case.
  • Cách 2 — TaskDecorator propagate context:
    @Configuration
    public class AsyncConfig implements AsyncConfigurer {
      public Executor getAsyncExecutor() {
          var ex = new ThreadPoolTaskExecutor();
          ex.setTaskDecorator(runnable -> {
              var attrs = RequestContextHolder.getRequestAttributes();
              return () -> {
                  try {
                      RequestContextHolder.setRequestAttributes(attrs);
                      runnable.run();
                  } finally {
                      RequestContextHolder.resetRequestAttributes();
                  }
              };
          });
          ex.initialize();
          return ex;
      }
    }
    Setup 1 lần, mọi @Async tự propagate. Phù hợp khi bạn thực sự cần request scope trong async.
  • Cách 3 — Dùng Micrometer ContextSnapshot: Spring Boot 3 có infrastructure observability tự propagate trace/log context — dùng được cho cả request scope nếu setup đúng.

Khuyến nghị: Cách 1 cho code mới — clearer, không hidden state. Cách 2 cho legacy migration.

Q7
Bạn có 1 web app với @SessionScope cho ShoppingCart. User mở 2 tab cùng lúc thêm sản phẩm vào cart. Có bug nào xảy ra không? Vì sao? Fix ra sao?

Có bug — race condition. 2 tab = 2 HTTP request song song trên cùng 1 session. Cùng 1 instance ShoppingCart, 2 thread Tomcat đồng thời call cart.add(item).

Nếu ShoppingCart dùng List items = new ArrayList<>()ArrayList.add không thread-safe. 2 thread add cùng lúc có thể:

  • Mất item (1 ghi đè 1).
  • ArrayIndexOutOfBoundsException khi internal array resize race.
  • Internal state corrupt → exception unexpected sau đó.

Fix:

  • Thread-safe collection:
    private final List<CartItem> items = Collections.synchronizedList(new ArrayList<>());
    // hoac
    private final List<CartItem> items = new CopyOnWriteArrayList<>();
    CopyOnWriteArrayList tốt cho read-heavy, ghi ít. synchronizedList đủ cho cart thường.
  • Synchronized methods:
    public synchronized void add(CartItem item) { items.add(item); }
    public synchronized List<CartItem> getItems() { return List.copyOf(items); }
  • Externalize state vào DB/Redis: persist cart, mỗi request load + save. Atomic operation đảm bảo bởi transaction. Đây là pattern modern hơn — session-scoped bean không scale tốt giữa nhiều pod.

Bài học: @SessionScope không đảm bảo 1 thread/instance. Phải code thread-safe nếu giữ mutable state.

Bài tiếp theo: @Configuration, @Bean, @Component — 3 cách khai báo bean

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

Bình luận (0)

Đang tải...