Bean scope & lifecycle: một bean sống bao lâu, dùng chung với ai
Học về vòng đời của bean trong Spring.
Ở bài trước, Spring tạo OrderService, PaymentService, EmailService cho mình. Nhưng object không chỉ được new rồi dùng ngay. Spring còn inject, chạy init, có thể bọc proxy, và cuối cùng destroy. Trước khi đi vào lifecycle, phải hiểu scope: một bean có bao nhiêu instance.
1. Bài toán: đếm số đơn trong OrderService
Một bạn trong team muốn biết đơn hiện tại là đơn thứ mấy, và lưu user hiện tại để ghi log:
@Service
public class OrderService {
private int orderCount = 0; // state dùng chung
private String currentUser; // state theo request (SAI)
public Order createOrder(String user, CreateOrderRequest req) {
this.currentUser = user;
orderCount++;
// ... lưu order, gọi payment mất 200ms
log.info("User {} created order #{}", currentUser, orderCount);
return order;
}
}
2. Vấn đề
Mặc định bean là singleton: cả app chỉ có một OrderService. Tomcat có 200 thread, cùng gọi vào một object:
Thread A (user=an) Thread B (user=binh)
currentUser = "an"
currentUser = "binh"
orderCount++ (đọc 5)
orderCount++ (đọc 5)
ghi 6 ghi 6 ← mất 1 lần đếm
log "binh created #6" ← A log sai user!
orderCount++không atomic → race condition.currentUserbị request khác ghi đè → log sai, nặng hơn là lộ dữ liệu người khác.
3. Giải pháp: hiểu scope và giữ singleton stateless
| Scope | Số instance | Dùng khi |
|---|---|---|
singleton (mặc định) | 1 / container | Service, repository, client stateless |
prototype | Mới mỗi lần xin | Object có state, ngắn hạn (builder, parser) |
request | 1 / HTTP request | Context của request (user, trace id) |
session | 1 / HTTP session | Giỏ hàng lưu trong session |
application | 1 / ServletContext | Hiếm dùng |
Quy tắc: singleton không giữ state thay đổi theo request. Dữ liệu theo request đi qua tham số method. Đếm dùng chung thì dùng AtomicLong hoặc DB.
4. Code từng bước
Bước 1: Sửa OrderService thành stateless
@Service
public class OrderService {
private final AtomicLong orderCount = new AtomicLong(); // thread-safe
public Order createOrder(String user, CreateOrderRequest req) {
long n = orderCount.incrementAndGet();
log.info("User {} created order #{}", user, n); // user là biến local
...
}
}
Bước 2: Request scope cho context của request
@Component
@RequestScope // = @Scope(value="request", proxyMode=TARGET_CLASS)
public class RequestContext {
private String userId;
private String traceId = UUID.randomUUID().toString();
// getter/setter
}
@Service
public class AuditService {
private final RequestContext ctx; // thực ra là proxy
public AuditService(RequestContext ctx) { this.ctx = ctx; }
public void audit(String action) {
log.info("[{}] {} did {}", ctx.getTraceId(), ctx.getUserId(), action);
}
}
AuditService là singleton, inject một lần. Vì vậy Spring inject một scoped proxy; mỗi lần gọi method, proxy tìm đúng RequestContext của request hiện tại.
Bước 3: Lifecycle callback
@Service
public class EmailService {
private final MailProperties props;
private SmtpConnectionPool pool;
public EmailService(MailProperties props) {
this.props = props; // 1. constructor
}
@PostConstruct // 2. sau khi inject xong
void init() {
pool = SmtpConnectionPool.open(props.host(), props.port());
log.info("SMTP pool ready");
}
@PreDestroy // 3. khi app shutdown
void close() {
pool.close();
}
}
Bước 4: Chạy và quan sát
./mvnw spring-boot:run
# ... SMTP pool ready
curl -X POST localhost:8080/orders -H "Content-Type: application/json" -d '{"productId":1,"quantity":1}'
# Ctrl+C
# ... closing SMTP pool
5. Cách hoạt động: lifecycle đầy đủ
1. Instantiate new EmailService(props)
↓
2. Populate inject field/setter (@Autowired)
↓
3. Aware callbacks setBeanName(), setApplicationContext()
↓
4. BeanPostProcessor postProcessBeforeInitialization()
↓
5. Initialization @PostConstruct → afterPropertiesSet() → initMethod
↓
6. BeanPostProcessor postProcessAfterInitialization()
← ĐÂY là chỗ tạo proxy cho @Transactional, @Async, AOP
↓
7. READY, app dùng
↓
8. Shutdown @PreDestroy → destroy() → destroyMethod
Chú ý: trong constructor, field @Autowired vẫn còn null. Cần dùng dependency để khởi tạo thì làm trong @PostConstruct hoặc dùng constructor injection.
6. So sánh init callback
| Cách | Ưu | Nhược |
|---|---|---|
@PostConstruct | Gọn, chuẩn Jakarta | Cần jakarta.annotation (Boot đã có) |
InitializingBean.afterPropertiesSet() | Không cần annotation | Code dính vào API Spring |
@Bean(initMethod="start") | Dùng cho class thư viện | Tên method là string |
ApplicationRunner / ApplicationReadyEvent | Chạy khi cả app sẵn sàng | Không phải callback của 1 bean |
7. Khi nào nên dùng / không nên dùng
- 99% bean để singleton, stateless.
@PostConstructcho khởi tạo nhanh (mở pool, validate config). Không gọi HTTP chậm hoặc warm cache nặng ở đây vì làm chậm startup; dùngApplicationReadyEvent.@PreDestroykhông chạy với bean prototype và không chạy khi process bịkill -9.
Vậy tôi đánh @Scope("prototype") rồi inject vào OrderService là mỗi lần gọi có object mới đúng không?
Chưa đúng, đây là bẫy kinh điển. Injection chỉ xảy ra một lần, lúc tạo OrderService. Singleton giữ đúng một instance prototype đó mãi mãi.
@Component
@Scope("prototype")
public class InvoiceBuilder { // có state: lines, total
private final List<String> lines = new ArrayList<>();
...
}
@Service
public class OrderService {
private final InvoiceBuilder builder; // SAI: chỉ 1 instance cho mọi request
}
Cách đúng: xin instance mới mỗi lần cần, qua ObjectProvider:
@Service
public class OrderService {
private final ObjectProvider<InvoiceBuilder> builderProvider;
public OrderService(ObjectProvider<InvoiceBuilder> builderProvider) {
this.builderProvider = builderProvider;
}
public Invoice buildInvoice(Order order) {
InvoiceBuilder builder = builderProvider.getObject(); // mới mỗi lần
return builder.add(order).build();
}
}
Các cách khác: @Lookup method, hoặc @Scope(value="prototype", proxyMode=ScopedProxyMode.TARGET_CLASS). Nhưng thường đơn giản nhất là: builder không cần là bean, cứ new InvoiceBuilder().
BeanPostProcessor là gì mà quan trọng vậy?
Đúng là nó quan trọng: đây là hook để can thiệp mọi bean sau khi tạo. Spring dùng nó để làm phần lớn "magic":
AutowiredAnnotationBeanPostProcessor: xử lý@Autowired,@Value.CommonAnnotationBeanPostProcessor: gọi@PostConstruct,@PreDestroy.AnnotationAwareAspectJAutoProxyCreator: bọc proxy cho bean có@Transactional,@Aspect.AsyncAnnotationBeanPostProcessor: bọc proxy cho@Async.
Tự viết một cái để thấy:
@Component
public class TimingBeanPostProcessor implements BeanPostProcessor {
@Override
public Object postProcessAfterInitialization(Object bean, String beanName) {
if (bean instanceof PaymentService) {
log.info("PaymentService ready: {}", bean.getClass().getName());
}
return bean; // có thể trả về object khác, ví dụ proxy bọc bean
}
}
PaymentService ready: com.example.shop.payment.PaymentService$$SpringCGLIB$$0
(Giả sử PaymentService có method @Transactional.) Thấy $$SpringCGLIB$$: bean bạn nhận được không phải object gốc mà là proxy. Bài sau sẽ mổ xẻ proxy.
Phỏng vấn hỏi bean scope và lifecycle thì trả lời sao?
Q1. Giải thích Spring Bean lifecycle.
🇻🇳 Tiếng Việt:
Container tạo instance, inject dependency, gọi các Aware callback. Sau đó BeanPostProcessor chạy trước init, rồi tới init callback như@PostConstruct, rồi BeanPostProcessor chạy sau init, đây là chỗ Spring tạo proxy cho AOP, transaction, async. Bean sẵn sàng sử dụng. Khi shutdown,@PreDestroyvà destroy method được gọi.
🇬🇧 English:
The container instantiates the bean, injects dependencies and invokes Aware callbacks. BeanPostProcessors run before initialization, then init callbacks such as@PostConstructrun, then BeanPostProcessors run after initialization, which is where Spring wraps the bean in a proxy for AOP, transactions or async. The bean is then ready. On shutdown,@PreDestroyand destroy methods are called.
Q2. Singleton bean có thread-safe không?
🇻🇳 Tiếng Việt:
Không tự động. Singleton nghĩa là một instance dùng chung cho mọi thread. Nếu bean có field thay đổi thì có race condition. Cách đúng là giữ bean stateless, dữ liệu theo request đi qua tham số hoặc request scope, state dùng chung thì dùng atomic hoặc lưu ngoài.
🇬🇧 English:
Not automatically. Singleton means one shared instance for all threads, so any mutable field can cause race conditions. Keep beans stateless: pass request data as method parameters or use request scope, and use atomics or external storage for shared state.
Q3. Inject prototype vào singleton thì sao?
🇻🇳 Tiếng Việt:
Prototype chỉ được tạo một lần lúc inject, nên singleton luôn dùng cùng một instance. Muốn instance mới mỗi lần thì dùngObjectProvider.getObject(),@Lookuphoặc scoped proxy.
🇬🇧 English:
The prototype is created only once at injection time, so the singleton keeps reusing the same instance. To get a new one each time, useObjectProvider.getObject(), a@Lookupmethod or a scoped proxy.
Cách nhớ ngắn gọn
- Singleton = dùng chung = phải stateless.
- Prototype trong singleton = chỉ tạo 1 lần, cần
ObjectProvider. - Lifecycle: new → inject → aware → BPP before → init → BPP after (proxy) → dùng → destroy.
- Proxy được tạo ở BPP after, nên
@Transactional/@Asyncphụ thuộc bước này.
Tự kiểm tra
Vì sao field currentUser trong OrderService là bug?
OrderService là singleton dùng chung cho mọi thread; request khác có thể ghi đè giá trị trước khi request hiện tại đọc lại.
Trong constructor, field @Autowired đã có giá trị chưa?
Chưa. Field injection xảy ra sau constructor. Dùng @PostConstruct hoặc constructor injection.
Vì sao singleton inject được bean request scope?
Spring inject một scoped proxy; mỗi lần gọi, proxy lấy đúng instance của request hiện tại.
Proxy cho @Transactional được tạo ở bước nào?
postProcessAfterInitialization của BeanPostProcessor.
@PreDestroy có chạy cho bean prototype không?
Không. Container không quản lý việc hủy prototype.