Bean scope & lifecycle: một bean sống bao lâu, dùng chung với ai

4 câu hỏi~25 phútTừ khóa: singleton, prototype, request scope, thread-safe, ObjectProvider, @PostConstruct, @PreDestroy, BeanPostProcessor

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!

3. Giải pháp: hiểu scope và giữ singleton stateless

ScopeSố instanceDùng khi
singleton (mặc định)1 / containerService, repository, client stateless
prototypeMới mỗi lần xinObject có state, ngắn hạn (builder, parser)
request1 / HTTP requestContext của request (user, trace id)
session1 / HTTP sessionGiỏ hàng lưu trong session
application1 / ServletContextHiế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ƯuNhược
@PostConstructGọn, chuẩn JakartaCần jakarta.annotation (Boot đã có)
InitializingBean.afterPropertiesSet()Không cần annotationCode dính vào API Spring
@Bean(initMethod="start")Dùng cho class thư việnTên method là string
ApplicationRunner / ApplicationReadyEventChạy khi cả app sẵn sàngKhông phải callback của 1 bean

7. Khi nào nên dùng / không nên dùng

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":

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, @PreDestroy và 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 @PostConstruct run, 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, @PreDestroy and 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ùng ObjectProvider.getObject(), @Lookup hoặ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, use ObjectProvider.getObject(), a @Lookup method or a scoped proxy.

Cách nhớ ngắn gọn


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.