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| Scope | Số instance | Lifetime | Khi nào dùng |
|---|---|---|---|
singleton | 1 / container | Suốt đời container | Default. Stateless service, repository, configuration |
prototype | N / mỗi getBean() | Caller manage | Stateful object cần riêng cho từng caller (rất hiếm) |
request | 1 / HTTP request | End of request | Object lưu state per-request (vd request-scoped audit log) |
session | 1 / HTTP session | End of session | Shopping cart, user session state |
application | 1 / ServletContext | Web app lifetime | Tương đương singleton trong web context (hiếm cần) |
websocket | 1 / WebSocket session | End of WS session | State 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> singletonObjectscủaDefaultListableBeanFactory— đâ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:
RequestContextHoldergiữHttpServletRequestquaThreadLocal— 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
| Scope | Concurrency model | Mutable state |
|---|---|---|
singleton | Multi-thread đọc cùng instance | Phải thread-safe (immutable, atomic, synchronized) |
prototype | Mỗi caller instance riêng | An toàn — không share |
request | 1 instance / request, request đa phần 1 thread | An toàn (nếu không spawn thread con) |
session | 1 instance / session, session có thể nhiều request đồng thời | Phả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ị:
| Mode | Cơ chế | Khi nào dùng |
|---|---|---|
NO (default) | Không proxy — inject thẳng | Cùng scope với caller (vd 2 bean cùng singleton) |
INTERFACES | JDK dynamic proxy implement interface | Bean implement interface |
TARGET_CLASS | CGLIB subclass | Bean không implement interface, hoặc cần ép subclass |
DEFAULT | INTERFACES nếu có interface, không thì TARGET_CLASS | Auto-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
INTERFACESphù hợp. - Cần
instanceofclass concrete → CGLIBTARGET_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
requestIdqua method param thay vì lookup ctx trong async thread. - Hoặc dùng
RequestContextHolder.setRequestAttributes(attrs, true)trước async —inheritable=truecho phép propagate. - Hoặc dùng
TaskDecoratorpropagate 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
Reference docs:
- Spring Framework Reference — Bean Scopes — full doc, đọc đặc biệt section "Scoped Beans as Dependencies" và "Custom Scopes".
- Spring Framework Reference — Request, Session, Application, and WebSocket Scopes — chi tiết web scope.
- Spring Framework Reference — Custom Scopes — implement Scope interface.
- Spring Framework — ObjectProvider Javadoc — API chuẩn để get instance prototype/optional bean.
- Spring Session Reference — externalize session sang Redis/JDBC.
- Spring Cloud — Refresh Scope — dynamic config reload.
Source:
org.springframework.context.annotation.ScopeMetadataResolver— cách Spring đọc@Scopetừ 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 →
Mapcache; prototype → no cache; request → ThreadLocal-bound HttpServletRequest attribute. - Prototype: mỗi
getBean()instance mới. Pitfall: inject prototype vào singleton fix instance — dùngObjectProvider<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) vsTARGET_CLASS(CGLIB). Default Boot dùngTARGET_CLASS.- Request scope ↔ ThreadLocal —
@Asynckhô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.
@PreDestroykhô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
@RefreshScopecho dynamic config update không restart.
10. Tự kiểm tra
Q1Bạ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?▸
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
SecurityContextHoldercủa Spring Security (ThreadLocal-based) hoặcRequestContextHolder. 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());
}
}
▸
@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-3Vì 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.
Q3Singleton 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
finalhoặ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ằngsynchronized/ReentrantLock. Không dùngHashMap,ArrayListtrầ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".
Q4Vì sao @RequestScope mặc định bao proxyMode = TARGET_CLASS? Nếu bỏ proxy mode đi, code sẽ vỡ thế nào?▸
@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.
Q5Bạ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.▸
TenantConfig nên scope nào? Nêu tradeoff của 3 lựa chọn.- Singleton + Map keyed by tenantId:Ưu: đơn giản, ít object. Nhược: service phải explicitly pass
@Service public class TenantConfigService { Map<String, TenantConfig> configs; public TenantConfig get(String tenantId) { return configs.get(tenantId); } }tenantIdmọi nơi — leak vào API. - Custom
tenantscope:Spring container giữ map@Component @Scope("tenant") public class TenantConfig { ... }tenantId → instance. Inject như normal bean, scope tự resolve theoThreadLocal<tenantId>set ở filter/interceptor.Ưu: service code clean, không leaktenantId. Nhược: phải implementScopeinterface, setThreadLocalđúng chỗ, cleanup khi tenant context kết thúc. - Request scope (resolve
tenantIdmỗ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
// ...
});
}
}
▸
@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:Đơn giản nhất, recommended cho hầu hết case.
public CompletableFuture<Order> placeAsync(OrderRequest req, String reqId) { return CompletableFuture.supplyAsync(() -> { /* use reqId */ }); } - Cách 2 — TaskDecorator propagate context:Setup 1 lần, mọi
@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; } }@Asynctự 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.
Q7Bạ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?▸
@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).
ArrayIndexOutOfBoundsExceptionkhi 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<>();CopyOnWriteArrayListtố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...