0
0
Lập trình
TT

Tách rời logic với PublishEvent và EventHandler trong Spring Boot

Đăng vào 7 tháng trước

• 8 phút đọc

Chủ đề:

KungFuTech

Giới thiệu

Khi phát triển ứng dụng trong Spring Boot, bạn thường gặp phải những tình huống mà một hành động đơn lẻ cần kích hoạt nhiều hậu quả khác nhau. Hãy tưởng tượng về việc đăng ký người dùng: ngoài việc lưu dữ liệu vào cơ sở dữ liệu, có thể bạn cần gửi email chào mừng, ghi lại nhật ký hoặc thông báo cho hệ thống khác.

Giải pháp đơn giản nhất có thể là thực hiện tất cả trong UserService. Nhưng ngay lập tức, bạn sẽ gặp vấn đề: phương thức chỉ nên đăng ký người dùng lại phải đảm nhiệm nhiều trách nhiệm khác nhau, khiến cho mã nguồn ngày càng khó bảo trì và phát triển.

👉 Vậy, làm thế nào để tách rời những logic này mà không biến mã của bạn thành một khối monolith với nhiều phụ thuộc nội bộ, hoặc không phải ngay lập tức chuyển sang những giải pháp phức tạp như Kafka hay microservices?

Đây chính là lúc mà các sự kiện trong Spring Boot xuất hiện. Với việc sử dụng publishEvent@EventListener, chúng ta có thể tạo ra một giao tiếp nội bộ tách rời, nơi mà dịch vụ chính chỉ cần công bố những gì đã xảy ra, và các thành phần khác sẽ chịu trách nhiệm phản ứng một cách độc lập.

Trong bài viết này, tôi sẽ chỉ cho bạn cách làm:

  • Cách sử dụng sự kiện trong Spring Boot.
  • Lợi ích của cách tiếp cận này.
  • Các thực hành tốt và cạm bẫy thường gặp.
  • Cách mà mẫu này kết nối với các kiến trúc lớn hơn, như CQRSevent-driven.

📌 Cách sử dụng sự kiện trong Spring Boot

Spring Boot đã cung cấp hỗ trợ sẵn cho sự kiện, và quy trình cơ bản khá đơn giản:

  1. Định nghĩa một sự kiện (thường đại diện cho một điều gì đó đã xảy ra trong miền).
  2. Công bố sự kiện đó ở một điểm nào đó trong hệ thống.
  3. Tạo các handler/listener sẽ phản ứng với nó.

Hãy xem điều này từng bước trong một ví dụ thực tiễn về việc đăng ký người dùng.

1. Tạo sự kiện

Chúng ta có thể đại diện cho một sự kiện dưới dạng một lớp đơn giản. Trong trường hợp này, chúng ta sẽ tạo một UserCreatedEvent để chỉ ra rằng một người dùng mới đã được đăng ký.

java Copy
public record UserCreatedEvent(Long userId, String email) {}

💡 Ở đây, tôi đã sử dụng một record để làm cho mã ngắn gọn hơn, nhưng bạn có thể sử dụng một lớp bình thường nếu bạn muốn.

2. Công bố sự kiện

Trong UserService, ngay sau khi người dùng được đăng ký, chúng ta sẽ công bố sự kiện.

java Copy
@Service
@RequiredArgsConstructor
public class UserService {

    private final ApplicationEventPublisher publisher;

    public void registerUser(String email) {
        // Simulasi đăng ký
        User user = saveUser(email);

        // Công bố sự kiện
        publisher.publishEvent(new UserCreatedEvent(user.getId(), user.getEmail()));
    }

    private User saveUser(String email) {
        // Lưu trữ giả để ví dụ
        return new User(1L, email);
    }
}

3. Phản ứng với sự kiện

Bây giờ chúng ta có thể tạo bao nhiêu handler tùy thích để phản ứng với sự kiện. Spring Boot sẽ tự động gọi từng handler.

java Copy
@Component
public class SendWelcomeEmailHandler {

    @EventListener
    public void handle(UserCreatedEvent event) {
        System.out.println("Đang gửi email đến: " + event.email());
    }
}
java Copy
@Component
public class CreateLogHandler {

    @EventListener
    public void handle(UserCreatedEvent event) {
        System.out.println("Đang tạo nhật ký cho người dùng: " + event.userId());
    }
}

🔎 Lưu ý rằng UserService không biết cách gửi email hoặc nơi ghi nhật ký. Nó chỉ công bố sự kiện, và mỗi handler thực hiện công việc của mình một cách độc lập.

Với chỉ vài dòng mã, chúng ta đã có một luồng công việc tách rời và có thể mở rộng: nếu ngày mai bạn muốn thêm một hành động mới (như thông báo cho hệ thống khác qua API), chỉ cần tạo một handler mới mà không cần chạm vào mã đăng ký.


🚀 Lợi ích của việc sử dụng sự kiện trong Spring Boot

Việc áp dụng việc công bố và xử lý sự kiện trong Spring Boot mang lại nhiều lợi ích cho kiến trúc ứng dụng, ngay cả trong các dự án vẫn là monolith.

🔹 1. Tách rời

Dịch vụ chính (ví dụ: UserService) không cần biết chi tiết cách thực hiện mỗi hành động bổ sung. Nó chỉ phát sự kiện — và các handler sẽ lo phần còn lại. Điều này giảm sự phụ thuộc trực tiếp giữa các lớp và giúp dễ bảo trì hơn.

🔹 2. Khả năng mở rộng

Nếu ngày mai có nhu cầu gửi thông báo đẩy hoặc tích hợp với một hệ thống khác, không cần sửa mã của việc đăng ký người dùng. Chỉ cần tạo một handler mới để phản ứng với cùng một sự kiện.

Hay nói cách khác, hệ thống phát triển một cách mở cho việc mở rộng và đóng cho việc sửa đổi (nguyên tắc OCP của SOLID).

🔹 3. Tổ chức mã

Mỗi trách nhiệm được giữ trong thành phần riêng của nó, tránh các phương thức lớn làm “mọi thứ cùng một lúc”. Điều này giúp mã trở nên dễ đọc hơn và các lớp trở nên đồng nhất hơn.

🔹 4. Tái sử dụng và khả năng kiểm thử

Các handler có thể được kiểm thử độc lập mà không cần phải đi qua toàn bộ luồng của dịch vụ chính. Hơn nữa, một sự kiện có thể được sử dụng để kích hoạt nhiều hành động trong các ngữ cảnh khác nhau.

🔹 5. Chuẩn bị cho các kiến trúc lớn hơn

Ngay cả trong một monolith, làm việc với sự kiện đã tạo ra một tư duy hướng theo sự kiện, giúp việc chuyển đổi sang các giải pháp có nhắn tin (Kafka, RabbitMQ) hoặc các mẫu như CQRS trở nên tự nhiên hơn.


⚠️ Cạm bẫy và thực hành tốt

Mặc dù mang lại nhiều lợi ích, các sự kiện trong Spring Boot cũng yêu cầu sự chú ý để tránh các vấn đề về bảo trì và hành vi không mong muốn.

⚠️ 1. Sự kiện đồng bộ theo mặc định

Khi bạn sử dụng publishEvent, luồng chờ tất cả các handler hoàn thành trước khi tiếp tục.

Nếu một handler chậm (ví dụ: gửi email bên ngoài) hoặc ném ngoại lệ, điều này có thể ảnh hưởng trực tiếp đến dịch vụ chính.

✅ Thực hành tốt:

Sử dụng @Async cho các handler có thể chạy song song, tránh làm chậm luồng chính.

Để làm điều này, chỉ cần kích hoạt @EnableAsync trong ứng dụng của bạn.

⚠️ 2. Cẩn thận với giao dịch

Nếu bạn công bố sự kiện trong một giao dịch, nó sẽ được phát ngay lập tức — ngay cả khi giao dịch chưa được xác nhận. Điều này có thể gây ra sự không nhất quán (ví dụ: handler cố gắng truy cập dữ liệu chưa được lưu trữ).

✅ Thực hành tốt:

Sử dụng @TransactionalEventListener để đảm bảo rằng handler chỉ được thực thi sau khi giao dịch được cam kết.

Ví dụ:

java Copy
@Component
public class CreateLogHandler {

    @TransactionalEventListener
    public void handle(UserCreatedEvent event) {
        System.out.println("Đang tạo nhật ký sau khi cam kết cho người dùng: " + event.userId());
    }
}

⚠️ 3. Không biến mọi thứ thành sự kiện

Thật hấp dẫn khi sử dụng sự kiện cho “mọi thứ”, nhưng điều này có thể khiến hệ thống khó gỡ lỗi và theo dõi luồng.

✅ Thực hành tốt:

Sử dụng sự kiện khi có nhiều bên quan tâm đến một hành động. Đối với các logic đơn giản và cục bộ, hãy giữ mã trực tiếp trong dịch vụ.

⚠️ 4. Xử lý lỗi

Theo mặc định, nếu một handler ném ngoại lệ, nó có thể lan truyền và ảnh hưởng đến luồng. Điều này có thể rất nghiêm trọng nếu sự kiện là một phần của hành động kinh doanh thiết yếu.

✅ Thực hành tốt:

Cách ly logic của các handler bằng cách sử dụng try/catch.

Đối với các sự kiện không đồng bộ, cấu hình retry hoặc dead-letter (khi chuyển sang nhắn tin).

👉 Tóm lại: sự kiện là mạnh mẽ, nhưng không phải là giải pháp cho mọi vấn đề. Nếu được sử dụng một cách có ý thức, chúng giúp duy trì ứng dụng được tổ chức và sẵn sàng để phát triển.


🔗 Kết nối với các kiến trúc lớn hơn

Làm việc với publishEvent@EventListener trong Spring Boot có thể có vẻ đơn giản, nhưng thực tế là một bước đầu tiên cho những kiến trúc phức tạp hơn.

Mẫu này đã giới thiệu một tư duy hướng theo sự kiện, giúp việc di chuyển trong tương lai đến:

  • CQRS – phân tách các lệnh và truy vấn.
  • Nhắn tin (Kafka, RabbitMQ) – phân phối sự kiện giữa các microservices.
  • Khả năng mở rộng – cho phép thêm chức năng mới mà không cần kết nối với lõi.

Kết luận

Việc sử dụng sự kiện trong Spring Boot là một cách thực tiễn để tách rời các trách nhiệm, giữ mã nguồn sạch sẽ và có thể mở rộng, cũng như chuẩn bị cho các kiến trúc nâng cao hơn.

Ngay cả trong các ứng dụng monolith, cách tiếp cận này cũng đã mang lại những lợi ích thực sự về tổ chức và bảo trì.

👉 Bạn đã bao giờ sử dụng sự kiện trong dự án của mình với Spring Boot chưa? Bạn có thích giữ mọi thứ đồng bộ hay đã chuyển sang nhắn tin với Kafka/RabbitMQ? Hãy cho tôi biết trong phần bình luận!

Gợi ý câu hỏi phỏng vấn
Không có dữ liệu

Không có dữ liệu

Bài viết được đề xuất
Bài viết cùng tác giả

Bình luận

Chưa có bình luận nào

Chưa có bình luận nào