跪拜 Guibai
← All articles
Backend · Java · Coding Standards

Lombok Beyond @Data: Six Annotations That Actually Prevent Crashes

By 程序员代码随笔 ·
Read original on juejin.cn ↗ Google Translate ↗ Alt translation

Teams that treat @Data as the default Lombok annotation risk production crashes from bidirectional JPA relationships. The six alternatives covered here each target a specific boilerplate pain point — Builder construction, checked exceptions in streams, resource cleanup, utility-class discipline, immutable updates, and collection building — and collectively can cut a codebase's ceremonial Java by roughly 30% without introducing runtime surprises.

Summary

A common JPA pitfall: annotating bidirectional entities with @Data triggers infinite recursion in toString(), crashing the JVM with a StackOverflowError. The fix is straightforward — use @Getter and @Setter individually with @ToString.Exclude — but the real productivity lies in six Lombok annotations most teams overlook.

@Builder and @SuperBuilder collapse 60-line hand-written Builder inner classes into a single annotation, with @SuperBuilder solving the inheritance gap that plain @Builder ignores. @SneakyThrows lets Stream lambdas call checked-exception methods without wrapping them in noisy try-catch blocks, while @Cleanup manages I/O resources with less ceremony than try-with-resources. @UtilityClass enforces the final-class, private-constructor, all-static-method contract for utility classes automatically. @With generates immutable field-update methods for @Value objects, and @Singular adds element-by-element collection building to @Builder.

Each annotation replaces a specific, repetitive Java boilerplate pattern. The article includes verified console output from JDK 21 and Lombok 1.18.34, confirming behavior across inheritance, exception propagation, resource closing order, and reflection-based instantiation prevention.

Takeaways
@Data on JPA entities with bidirectional associations causes StackOverflowError because toString() calls cycle between the two sides.
Replace @Data on entities with @Getter, @Setter, and @ToString(exclude = "...") to break the recursion chain.
@Builder collapses 60+ lines of hand-written Builder code into one annotation but does not propagate parent fields to child Builders.
@SuperBuilder fixes the inheritance gap: both parent and child classes must be annotated for parent fields to appear in the child's Builder.
@Builder ignores field default values unless the field is annotated with @Builder.Default.
@SneakyThrows hides checked-exception throws declarations at the bytecode level so Stream lambdas can call methods like Files.lines() without try-catch wrappers.
@SneakyThrows does not swallow exceptions; the original exception type is preserved and thrown normally at runtime.
@Cleanup auto-closes resources at scope end in reverse declaration order, matching try-with-resources semantics with less nesting.
@Cleanup resources stay open until the enclosing scope exits; narrow the scope with {} blocks to release them earlier.
@UtilityClass makes a class final, all methods static, and the constructor private — preventing both direct instantiation and reflection-based instantiation.
@With generates withXxx() methods that return a new shallow copy of an immutable @Value object with one field changed.
@Singular adds singular-form builder methods (tag(), contributor(), score()) for adding collection elements one at a time, plus clearXxx() methods to reset collections.
Conclusions

Lombok's design rewards selective use: the annotations that prevent the most boilerplate — @Builder, @SuperBuilder — are also the ones with the sharpest edge cases around inheritance and default values.

The @Cleanup scope trap is a structural reminder that resource-management annotations tied to lexical scope invert the usual Java instinct to declare resources at the top of a method.

@SneakyThrows is philosophically controversial because it subverts Java's checked-exception design, but its practical value in Stream pipelines is undeniable — the alternative is exception-wrapping boilerplate that obscures the original failure.

The @Data StackOverflow example is a concrete case of a broader pattern: convenience annotations that bundle multiple concerns (@ToString, @EqualsAndHashCode, @Setter) become dangerous when the bundled behavior is wrong for the domain model.

Concepts & terms
Bidirectional JPA association
A relationship where two JPA entities reference each other — e.g., User has a List<Order> and each Order has a User field. Calling toString() on either side can trigger infinite mutual recursion if both sides include the other in their toString() output.
@SuperBuilder
A Lombok annotation that generates a Builder pattern supporting class inheritance. Unlike @Builder, which only exposes the annotated class's own fields, @SuperBuilder propagates parent-class fields into the child class's Builder — both parent and child must be annotated.
Shallow copy
A copy of an object where the top-level fields are duplicated but nested objects (like List or Map fields) are shared by reference between the original and the copy. @With performs shallow copies, so modifying a shared collection affects both objects.
Checked exception
In Java, an exception that the compiler forces calling code to either catch or declare in its method signature with throws. IOException is a common example. @SneakyThrows bypasses this compile-time check by not emitting the throws declaration in bytecode.
Source: juejin.cn ↗ Google Translate ↗ Backup ↗