TungDaDev's Blog

project Lombok

Java lombok.webp
Published on
/10 mins read/

Trong hệ sinh thái Java doanh nghiệp, Project Lombok là một trong những thư viện phổ biến nhất nhưng cũng gây tranh cãi gay gắt nhất giữa các kỹ sư phần mềm. Một mặt, Lombok giải phóng lập trình viên khỏi hàng nghìn dòng mã boilerplate (getter, setter, constructor, equals/hashCode). Mặt khác, sự tiện lợi đó đi kèm với một cái giá kỹ thuật đắt đỏ: phá vỡ ranh giới bao đóng (encapsulation), sinh ra các lỗi ngầm nghiêm trọng trong tầng ORM/JPA, và gây rò rỉ ngoại lệ ngoài tầm kiểm soát.

Là một Software Architect hay Principal Engineer, bạn không thể chỉ nhìn Lombok dưới góc độ "giúp gõ code nhanh hơn". Bài viết này sẽ phân tích cơ chế hoạt động thực sự của Lombok bên dưới máy ảo, chỉ ra 5 lỗi kinh điển đánh sập hệ thống production và định hình các quy tắc kiến trúc chặt chẽ để kiểm soát thư viện này.


# bản chất kỹ thuật: Lombok hoạt động như thế nào dưới JVM?

Nhiều lập trình viên lầm tưởng Lombok là một Annotation Processor tiêu chuẩn của Java. Thực tế không đơn giản như vậy.

Quy chuẩn JSR 269 (Pluggable Annotation Processing API) của Java chỉ cho phép các annotation processor kiểm tra mã nguồn và tạo ra các file source code mới trong quá trình biên dịch (như cách MapStruct hay Google AutoValue hoạt động). JSR 269 nghiêm cấm việc chỉnh sửa trực tiếp các class đang được biên dịch.

Để vượt qua giới hạn này, Lombok sử dụng cơ chế reflection nội bộ, ép kiểu đối tượng Trees của trình biên dịch javac sang com.sun.tools.javac.tree.JCTree. Sau đó, Lombok trực tiếp biến đổi cây cú pháp trừu tượng (Abstract Syntax Tree - AST) ngay trong bộ nhớ trước khi trình biên dịch xuất ra bytecode .class.

WARNING

Vì Lombok phụ thuộc vào các API nội bộ chưa chuẩn hóa của javac (internal non-public APIs), mỗi khi OpenJDK nâng cấp phiên bản mới hoặc thắt chặt cơ chế đóng gói module (JEP 396/403 Strong Encapsulation of JDK Internals), dự án sử dụng Lombok thường xuyên bị vỡ build cho đến khi nhóm tác giả Lombok tìm ra các cờ mở khóa JVM tương ứng.


# các hiểm họa sản xuất khi lạm dụng Lombok

# sử dụng @data trên JPA / Hibernate Entity

Đây là sai lầm phổ biến nhất của các kỹ sư cấp độ Junior/Mid, dẫn đến các sự cố rò rỉ dữ liệu hoặc treo cứng ứng dụng.

Khi gắn @Data lên một Hibernate Entity, Lombok tự động sinh ra:

  1. equals() và hashCode() dựa trên tất cả các trường của class.
  2. toString() duyệt qua tất cả các trường, bao gồm cả các quan hệ @OneToMany hoặc @ManyToOne.

Hậu quả trên Production:

  • Gãy vỡ Hashing Contract trong Java Collections: Khi lưu Entity vào Set hoặc làm khóa trong Map, nếu khóa chính id được sinh tự động (Database Identity), giá trị hashCode() của entity sẽ thay đổi trước và sau lệnh persist(). Entity sẽ "biến mất" khỏi Set dù nó vẫn tồn tại trong bộ nhớ!
  • Thảm họa N+1 và LazyInitializationException do toString(): Khi ghi log hoặc debug: log.info("Loaded order: {}", order);, hàm toString() của @Data sẽ âm thầm gọi getItems(). Điều này kích hoạt hàng trăm câu lệnh SQL phụ (N+1 Query) hoặc ném văng LazyInitializationException nếu Session đã đóng.
  • StackOverflowError trong quan hệ hai chiều: Nếu Order trỏ tới OrderItem và OrderItem trỏ ngược lại Order, lệnh toString() hoặc equals() sẽ lặp vô hạn giữa hai thực thể cho đến khi đầy Call Stack.

Quy tắc bất di bất dịch: TUYỆT ĐỐI KHÔNG DÙNG @Data CHO JPA ENTITIES. Hãy dùng @Getter, @Setter có kiểm soát, và tự cài đặt equals()/hashCode() dựa trên khóa nghiệp vụ duy nhất (Business Key) hoặc khóa chính.


# @equalsandhashcode(callsuper = false) trong cây phân cấp kế thừa

Mặc định, khi một class con kế thừa class cha và gắn @EqualsAndHashCode, Lombok đặt callSuper = false.

@Getter
@Setter
public class BaseEntity {
    private Long id;
    private Instant createdAt;
}
 
@Getter
@Setter
@EqualsAndHashCode // Nguy hiểm: callSuper mặc định là false!
public class AccountUser extends BaseEntity {
    private String email;
}

Nếu hệ thống có 2 đối tượng AccountUser:

  • User A: id = 1, email = "admin@tungdadev.com"
  • User B: id = 2, email = "admin@tungdadev.com"

Lệnh userA.equals(userB) sẽ trả về true vì Lombok hoàn toàn bỏ qua các trường của BaseEntity! Trong các hệ thống ngân hàng hoặc phân quyền, lỗi này có thể cho phép người dùng chiếm quyền thao tác tài nguyên của tài khoản khác khi cache phân tán kiểm tra định danh đối tượng.


# nuốt chửng giá trị khởi tạo với @Builder.default

Khi dùng @Builder, nếu lập trình viên khởi tạo giá trị mặc định cho trường:

@Builder
@Getter
public class RetryPolicy {
    @Builder.Default
    private int maxRetries = 3;
 
    @Builder.Default
    private Duration backoff = Duration.ofSeconds(2);
}

Nếu một request JSON được gửi tới Controller và Jackson deserialize payload rỗng {}, Jackson không sử dụng Builder mà dùng reflection gọi Default Constructor (hoặc Unsafe allocation). Kết quả:

  • maxRetries bị gán về 0 (giá trị mặc định của primitive int).
  • backoff bị gán về null.

Toàn bộ logic retry của hệ thống bị vô hiệu hóa ngầm mà không hề có bất kỳ cảnh báo biên dịch nào!


# @sneakythrows — tội đồ phá vỡ mô hình ngoại lệ phân tán

@SneakyThrows cho phép bạn ném ra các Checked Exception (như SQLException, IOException) mà không cần khai báo throws trong chữ ký phương thức:

@SneakyThrows
public void processTransaction(Transaction tx) {
    // Không cần try-catch SQLException
    dbConnection.commit();
}

Cách Lombok thực hiện: Trong bytecode, JVM thực chất không phân biệt Checked hay Unchecked Exception; quy tắc throws chỉ là sự ràng buộc do trình biên dịch javac thực thi. Lombok lợi dụng việc type erasure trong generic method Lombok.<RuntimeException>sneakyThrow(t) để đánh lừa javac.

Tại sao đây là một Anti-Pattern nguy hiểm?

  • Vỡ tầng trừu tượng: Lớp Service ném ra SQLException mà tầng Controller bên trên không hề hay biết để bắt (catch), vì Controller chỉ mong đợi các Unchecked Business Exception.
  • Gãy chuỗi Reactive Streams / Virtual Threads: Khi exception bị giấu, các engine xử lý luồng không thể kích hoạt fallback circuit breaker tương ứng, biến một lỗi cục bộ thành sự cố rò rỉ unhandled exceptions trên toàn luồng.

# ma trận quy tắc sử dụng Lombok chuẩn doanh nghiệp

Để đảm bảo an toàn tuyệt đối cho hệ thống, một kiến trúc sư cần thiết lập Lombok Governance Matrix trong quy chuẩn kỹ thuật của tổ chức:

Tầng Kiến Trúc / Đối TượngLombok Annotations ĐƯỢC PHÉPLombok Annotations BỊ CẤMGiải Pháp Thay Thế Chuẩn
Domain Entities (JPA/Hibernate)@Getter, @Setter (hạn chế), @ToString(onlyExplicitlyIncluded = true)@Data, @EqualsAndHashCode, @AllArgsConstructorTự implement equals/hashCode theo Business Key; dùng Explicit Factory methods
Data Transfer Objects (DTO)@Getter, @Builder, @Jacksonized@Data (nếu có kế thừa)Ưu tiên Java 16+ Records
Value Objects (DDD)@Value (Immutable)@Data, @SetterJava Records
Spring Services / Components@RequiredArgsConstructor (Constructor Injection)@Autowired field injectionConstructor Injection thuần túy
Utility Classes@UtilityClassKhởi tạo thủ côngfinal class với private constructor
Error / Exception HandlingKhông khuyến khích@SneakyThrowsKhai báo throws rõ ràng hoặc bọc vào BusinessException

# hiện đại hóa: Java Records & MapStruct thay thế Lombok

Từ Java 16+, Java đã chính thức cung cấp Records — mô hình dữ liệu bất biến (immutable data carrier) nguyên bản được tối ưu hóa sâu trong máy ảo JVM. Kết hợp cùng MapStruct, chúng ta có thể loại bỏ 80% sự phụ thuộc vào Lombok:

# so sánh DTO bằng Lombok vs Java Record

// CÁCH CŨ DÙNG LOMBOK: Cần 5 annotations, phụ thuộc vào javac AST hack
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Jacksonized
public class UserResponseDto {
    private Long id;
    private String username;
    private String email;
}
 
// CÁCH HIỆN ĐẠI DÙNG JAVA RECORD: Gọn gàng, bất biến, 100% Native JVM Support
public record UserResponseDto(
    Long id,
    String username,
    String email
) {
    // Compact constructor để validate dữ liệu nếu cần
    public UserResponseDto {
        Objects.requireNonNull(username, "Username must not be null");
        Objects.requireNonNull(email, "Email must not be null");
    }
}

Ưu thế vượt trội của Java Records:

  • Memory Footprint: JVM có thể áp dụng các tối ưu hóa phân tích thoát (Escape Analysis) và nén layout bộ nhớ tốt hơn nhiều so với POJO thông thường.
  • Serialization Safety: Records tuân thủ cơ chế tuần tự hóa bất biến an toàn (Canonical Constructor Serialization), ngăn chặn tấn công injection khi deserialize.
  • Pattern Matching: Hoàn toàn tương thích với cơ chế Pattern Matching for switch và Record Patterns trong Java 21+.

# kết luận

Lombok không phải là "thuốc độc", nhưng nó là một con dao hai lưỡi sắc bén. Trong các dự án quy mô lớn, việc để lập trình viên tự do gắn @Data hay @SneakyThrows mà không hiểu rõ cơ chế biên dịch là cách nhanh nhất tích lũy nợ kỹ thuật (technical debt).

Là người dẫn dắt kỹ thuật, hãy:

  1. Thiết lập file cấu hình lombok.config ở thư mục gốc của repository để chặn các annotation nguy hiểm:
    lombok.data.flagUsage = error
    lombok.sneakyThrows.flagUsage = error
    lombok.equalsAndHashCode.callSuper = call
  2. Thúc đẩy đội ngũ chuyển dịch các DTO và Value Object sang Java Records.
  3. Giữ cho Domain Entities luôn thuần khiết, kiểm soát chặt chẽ tính bao đóng để đảm bảo hiệu năng và tính toàn vẹn của dữ liệu trên Production.

Tài liệu tham khảo chuyên sâu:


Chỉ là những ghi chép cá nhân với hy vọng mang lại chút giá trị. Nếu thấy hữu ích, đừng ngại chia sẻ cho bạn bè & đồng nghiệp nhé!

Happy coding 😎 👍🏻 🚀 🔥.

Next post →hibernate ORM