Lombok for Reducing Boilerplate in Java

Introduction

Lombok is a Java library that automatically plugs into your editor and build tools, spicing up your Java code. It helps eliminate boilerplate code like getters, setters, constructors, equals, hashCode, and toString methods through annotations, making your code cleaner and more maintainable.

Maven Dependencies

<!-- Lombok dependency --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.30</version> <scope>provided</scope> </dependency> <!-- For Spring Boot --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter</artifactId> </dependency> <!-- Annotation processor for IDEs --> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> <version>1.18.30</version> <scope>provided</scope> <optional>true</optional> </dependency>

Core Annotations

@Data - The Complete Package

import lombok.Data; // Without Lombok - 50+ lines of boilerplate public class UserWithoutLombok { private Long id; private String username; private String email; private int age; private boolean active; // Constructors, getters, setters, equals, hashCode, toString... } // With Lombok - Just 7 lines! @Data public class User { private Long id; private String username; private String email; private int age; private boolean active; } // Usage example public class UserService { public void processUser() { User user = new User(); user.setId(1L); user.setUsername("john_doe"); user.setEmail("[email protected]"); user.setAge(30); user.setActive(true); System.out.println(user); // Auto-generated toString() User anotherUser = new User(); anotherUser.setId(1L); System.out.println(user.equals(anotherUser)); // Auto-generated equals() } }

@Getter and @Setter

import lombok.Getter; import lombok.Setter; public class Product { // Field-level annotations @Getter @Setter private Long id; @Getter @Setter private String name; // Class-level annotations with access levels @Getter @Setter public static class Category { private Long id; private String name; private String description; // Lombok generates: // public Long getId() { return this.id; } // public void setId(Long id) { this.id = id; } // ... and so on for other fields } // Configuring access levels @Getter(AccessLevel.PROTECTED) @Setter(AccessLevel.PRIVATE) private String internalCode; @Getter(AccessLevel.PUBLIC) @Setter(AccessLevel.NONE) // No setter generated private final String sku; public Product(String sku) { this.sku = sku; } } // Usage class ProductService { public void demonstrateGettersSetters() { Product product = new Product("SKU123"); Product.Category category = new Product.Category(); category.setId(1L); category.setName("Electronics"); // product.setSku("NEW123"); // Compile error - no setter for final field String sku = product.getSku(); // This works // String code = product.getInternalCode(); // Compile error - protected getter // product.setInternalCode("CODE"); // Compile error - private setter } }

Constructors Made Easy

import lombok.AllArgsConstructor; import lombok.NoArgsConstructor; import lombok.RequiredArgsConstructor; @NoArgsConstructor @AllArgsConstructor public class Employee { private Long id; private String name; private String department; private double salary; // Lombok generates: // public Employee() {} // public Employee(Long id, String name, String department, double salary) { // this.id = id; // this.name = name; // this.department = department; // this.salary = salary; // } } @RequiredArgsConstructor public class Customer { private final Long id; private final String customerCode; @NonNull private String name; private String email; private String phone; // Lombok generates: // public Customer(Long id, String customerCode, String name) { // if (name == null) throw new NullPointerException("name is marked non-null but is null"); // this.id = id; // this.customerCode = customerCode; // this.name = name; // } } // Static factory methods @AllArgsConstructor(staticName = "of") public class Point { private final int x; private final int y; // Usage: Point point = Point.of(10, 20); } // Builder pattern with constructor @AllArgsConstructor @NoArgsConstructor @Builder public class Address { private String street; private String city; private String zipCode; private String country; } class ConstructorExamples { public void demonstrateConstructors() { // No-args constructor Employee emp1 = new Employee(); // All-args constructor Employee emp2 = new Employee(1L, "John Doe", "IT", 75000.0); // Required args constructor Customer customer = new Customer(1L, "CUST001", "Jane Smith"); // Customer invalid = new Customer(1L, "CUST001", null); // Throws NPE // Static factory Point point = Point.of(10, 20); // Builder Address address = Address.builder() .street("123 Main St") .city("New York") .zipCode("10001") .country("USA") .build(); } }

Advanced Lombok Features

@Builder - Fluent Object Creation

import lombok.Builder; import lombok.Singular; import lombok.ToString; import java.util.List; import java.util.Map; @Builder @ToString public class Order { private Long id; private String orderNumber; private String customerEmail; private double totalAmount; @Singular private List<OrderItem> items; @Singular private Map<String, String> metadata; @Builder.Default private OrderStatus status = OrderStatus.PENDING; @Builder.Default private int priority = 1; public enum OrderStatus { PENDING, CONFIRMED, SHIPPED, DELIVERED, CANCELLED } } @Builder @ToString class OrderItem { private String productId; private String productName; private int quantity; private double unitPrice; } class BuilderExamples { public void demonstrateBuilder() { // Basic builder Order order = Order.builder() .id(1L) .orderNumber("ORD001") .customerEmail("[email protected]") .totalAmount(199.99) .build(); // With singular collections Order complexOrder = Order.builder() .id(2L) .orderNumber("ORD002") .item(OrderItem.builder() .productId("P001") .productName("Laptop") .quantity(1) .unitPrice(999.99) .build()) .item(OrderItem.builder() .productId("P002") .productName("Mouse") .quantity(2) .unitPrice(25.99) .build()) .metadata("source", "web") .metadata("campaign", "spring_sale") .status(Order.OrderStatus.CONFIRMED) .build(); System.out.println(complexOrder); // Output includes all items and metadata // ToBuilder for creating modified copies Order modifiedOrder = complexOrder.toBuilder() .totalAmount(1050.97) .status(Order.OrderStatus.SHIPPED) .build(); } }

@Value - Immutable Classes

import lombok.Value; import lombok.With; import lombok.experimental.NonFinal; @Value public class ImmutableUser { Long id; String username; String email; @NonFinal @With String temporaryPassword; // Lombok generates: // - All fields are made private and final // - Getter methods for all fields (no setters) // - All-args constructor // - equals(), hashCode(), toString() // Custom constructor public ImmutableUser(String username, String email) { this.id = null; this.username = username; this.email = email; this.temporaryPassword = null; } } @Value @Builder public class ImmutableProduct { String id; String name; String description; double price; @Builder.Default boolean available = true; } class ValueExamples { public void demonstrateImmutableObjects() { // Using @Value ImmutableUser user = new ImmutableUser(1L, "john", "[email protected]"); System.out.println(user.getUsername()); // john // user.setUsername("new"); // Compile error - no setter // Using @With for modification ImmutableUser updatedUser = user.withTemporaryPassword("temp123"); System.out.println(updatedUser.getTemporaryPassword()); // temp123 // Builder with @Value ImmutableProduct product = ImmutableProduct.builder() .id("P001") .name("Smartphone") .description("Latest model") .price(699.99) .available(true) .build(); System.out.println(product); } }

Logging Annotations

import lombok.extern.slf4j.Slf4j; import lombok.extern.java.Log; import lombok.extern.log4j.Log4j2; import lombok.Cleanup; import java.io.FileInputStream; import java.util.logging.Logger; @Slf4j public class LoggingService { public void processData(String data) { log.trace("Entering processData with: {}", data); if (data == null) { log.warn("Null data received"); return; } try { // Automatic resource management @Cleanup FileInputStream input = new FileInputStream("config.properties"); // FileInputStream will be automatically closed at the end of the scope log.info("Processing data: {}", data); // Complex processing logic here log.debug("Data processing completed successfully"); } catch (Exception e) { log.error("Error processing data: {}", data, e); } } } @Log4j2 public class AdvancedLoggingService { public void performOperation() { log.info("Operation started"); try { // Business logic if (log.isDebugEnabled()) { log.debug("Detailed debug information"); } } catch (Exception e) { log.error("Operation failed", e); throw new RuntimeException("Operation failed", e); } log.info("Operation completed successfully"); } } // For java.util.logging @Log public class JavaUtilLoggingExample { private static final Logger logger = Logger.getLogger(JavaUtilLoggingExample.class.getName()); public void demo() { log.info("This uses java.util.logging"); } }

Lombok in Spring Applications

Spring Boot Entities with Lombok

import lombok.*; import org.springframework.data.annotation.Id; import org.springframework.data.mongodb.core.mapping.Document; import javax.persistence.*; import javax.validation.constraints.Email; import javax.validation.constraints.NotBlank; import javax.validation.constraints.Size; import java.time.LocalDateTime; // JPA Entity example @Entity @Table(name = "users") @Data @NoArgsConstructor @AllArgsConstructor @Builder @EqualsAndHashCode(onlyExplicitlyIncluded = true) public class UserEntity { @Id @EqualsAndHashCode.Include @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; @NotBlank @Size(min = 3, max = 50) @Column(unique = true, nullable = false) private String username; @Email @NotBlank @Column(unique = true, nullable = false) private String email; @NotBlank @Size(min = 6) @Column(nullable = false) private String password; @Builder.Default private boolean enabled = true; @Builder.Default private LocalDateTime createdAt = LocalDateTime.now(); // Custom getter public String getDisplayName() { return username + " (" + email + ")"; } } // MongoDB Document example @Document(collection = "products") @Data @NoArgsConstructor @AllArgsConstructor @Builder public class ProductDocument { @Id private String id; private String name; private String description; private Double price; @Builder.Default private Boolean active = true; @Singular private List<String> tags; } // Spring Service with Lombok @Service @RequiredArgsConstructor @Slf4j public class UserService { private final UserRepository userRepository; private final PasswordEncoder passwordEncoder; private final EmailService emailService; public UserEntity createUser(CreateUserRequest request) { log.info("Creating user with username: {}", request.getUsername()); UserEntity user = UserEntity.builder() .username(request.getUsername()) .email(request.getEmail()) .password(passwordEncoder.encode(request.getPassword())) .build(); UserEntity savedUser = userRepository.save(user); log.info("User created successfully with ID: {}", savedUser.getId()); return savedUser; } @Transactional public UserEntity updateUser(Long userId, UpdateUserRequest request) { return userRepository.findById(userId) .map(user -> { user.setEmail(request.getEmail()); if (request.getPassword() != null) { user.setPassword(passwordEncoder.encode(request.getPassword())); } return userRepository.save(user); }) .orElseThrow(() -> new UserNotFoundException(userId)); } } // DTOs with Lombok @Data @Builder @NoArgsConstructor @AllArgsConstructor public class CreateUserRequest { @NotBlank private String username; @Email @NotBlank private String email; @NotBlank @Size(min = 6) private String password; } @Data @Builder @NoArgsConstructor @AllArgsConstructor public class UpdateUserRequest { @Email private String email; @Size(min = 6) private String password; }

Configuration and Customization

Lombok Configuration File

# lombok.config # Enable experimental features lombok.anyConstructor.suppressConstructorProperties = true # Add null checks in generated setters lombok.setter.flagUsage = WARNING # Make generated builders public lombok.accessors.chain = true # Configure logging lombok.log.fieldName = LOG # Exclude fields from toString lombok.toString.exclude = [password, ssn] # Customize field names lombok.accessors.prefix += m_

Customizing Lombok Behavior

import lombok.AccessLevel; import lombok.ToString; import lombok.experimental.FieldDefaults; // Field defaults for entire class @FieldDefaults(level = AccessLevel.PRIVATE, makeFinal = true) @ToString public class ConfigurationExample { String name; // Becomes: private final String name; int value; // Becomes: private final int value; // No need to explicitly declare them as private final } // Customizing toString @ToString(callSuper = true, includeFieldNames = false, exclude = {"password", "secretKey"}) public class SecureUser extends BaseEntity { private String username; private String email; private String password; private String secretKey; // toString will include superclass and exclude password/secretKey } // Customizing equals and hashCode @EqualsAndHashCode(callSuper = true, onlyExplicitlyIncluded = true) public class Employee extends Person { @EqualsAndHashCode.Include private String employeeId; private String department; private double salary; // Only employeeId is included in equals/hashCode }

Best Practices and Patterns

Effective Lombok Usage

import lombok.*; import lombok.experimental.Accessors; // Builder with accessors chain @Getter @Setter @Accessors(chain = true) @NoArgsConstructor @AllArgsConstructor public class ChainableUser { private Long id; private String username; private String email; // Enables method chaining: user.setId(1L).setUsername("john").setEmail("[email protected]"); } // Immutable configuration class @Value @Builder public class AppConfig { String databaseUrl; String username; String password; int connectionTimeout; @Builder.Default int maxConnections = 10; @Builder.Default boolean cacheEnabled = true; @Singular List<String> allowedHosts; } // Service with constructor injection @Service @RequiredArgsConstructor @Slf4j public class OrderProcessingService { private final OrderRepository orderRepository; private final PaymentService paymentService; private final NotificationService notificationService; private final AppConfig appConfig; @Transactional public Order processOrder(OrderRequest request) { log.info("Processing order for customer: {}", request.getCustomerEmail()); Order order = Order.builder() .orderNumber(generateOrderNumber()) .customerEmail(request.getCustomerEmail()) .totalAmount(request.getTotalAmount()) .items(request.getItems()) .build(); Order savedOrder = orderRepository.save(order); // Process payment paymentService.processPayment(savedOrder); // Send notification notificationService.sendOrderConfirmation(savedOrder); log.info("Order processed successfully: {}", savedOrder.getOrderNumber()); return savedOrder; } private String generateOrderNumber() { return "ORD_" + System.currentTimeMillis(); } } // Exception classes with Lombok @Getter public class BusinessException extends RuntimeException { private final String errorCode; private final LocalDateTime timestamp; public BusinessException(String errorCode, String message) { super(message); this.errorCode = errorCode; this.timestamp = LocalDateTime.now(); } } // Record-like classes with Lombok (pre-Java 16) @Value @Builder public class Point { int x; int y; public double distanceTo(Point other) { return Math.sqrt(Math.pow(x - other.x, 2) + Math.pow(y - other.y, 2)); } }

Common Pitfalls and Solutions

Avoiding Lombok Issues

// Problem: Circular dependencies in equals/hashCode @Data public class Department { private Long id; private String name; private List<Employee> employees; // Circular reference! } @Data public class Employee { private Long id; private String name; private Department department; // Circular reference! } // Solution: Use @EqualsAndHashCode.Exclude @Data public class Department { private Long id; private String name; @EqualsAndHashCode.Exclude private List<Employee> employees; } @Data public class Employee { private Long id; private String name; @EqualsAndHashCode.Exclude private Department department; } // Problem: JPA relationships with Lombok @Entity @Data public class Order { @Id @GeneratedValue private Long id; @OneToMany(mappedBy = "order", cascade = CascadeType.ALL) @ToString.Exclude @EqualsAndHashCode.Exclude private List<OrderItem> items = new ArrayList<>(); // Helper method for bidirectional relationship public void addItem(OrderItem item) { items.add(item); item.setOrder(this); } } @Entity @Data public class OrderItem { @Id @GeneratedValue private Long id; @ManyToOne @JoinColumn(name = "order_id") @ToString.Exclude private Order order; private String productName; private int quantity; }

Testing with Lombok

Test Classes with Lombok

import lombok.val; import lombok.var; @Slf4j @ExtendWith(MockitoExtension.class) class UserServiceTest { @Mock private UserRepository userRepository; @Mock private PasswordEncoder passwordEncoder; @InjectMocks private UserService userService; @Test void shouldCreateUserSuccessfully() { // given val request = CreateUserRequest.builder() .username("testuser") .email("[email protected]") .password("password123") .build(); val encodedPassword = "encodedPassword123"; val savedUser = UserEntity.builder() .id(1L) .username("testuser") .email("[email protected]") .password(encodedPassword) .build(); when(passwordEncoder.encode("password123")).thenReturn(encodedPassword); when(userRepository.save(any(UserEntity.class))).thenReturn(savedUser); // when val result = userService.createUser(request); // then assertThat(result).isNotNull(); assertThat(result.getId()).isEqualTo(1L); assertThat(result.getUsername()).isEqualTo("testuser"); verify(userRepository).save(any(UserEntity.class)); verify(passwordEncoder).encode("password123"); } @Test void shouldHandleUserNotFound() { // given val userId = 999L; when(userRepository.findById(userId)).thenReturn(Optional.empty()); // when & then assertThrows(UserNotFoundException.class, () -> userService.updateUser(userId, UpdateUserRequest.builder().build())); } } // Using val and var for local variables class TypeInferenceExample { public void demonstrateValVar() { val immutableList = List.of("a", "b", "c"); // final List<String> var mutableList = new ArrayList<String>(); // ArrayList<String> // val is final, so this won't compile: // immutableList = new ArrayList<>(); // var can be reassigned mutableList = new ArrayList<>(List.of("x", "y", "z")); for (val item : immutableList) { System.out.println(item); // item is final String } } }

Conclusion

Lombok significantly reduces Java boilerplate code and improves developer productivity. Key benefits include:

  • Reduced Code Volume: Eliminates repetitive getters, setters, constructors
  • Improved Readability: Focus on business logic rather than boilerplate
  • Fewer Bugs: Auto-generated methods are consistent and well-tested
  • Better Maintenance: Changes in fields automatically reflect in generated methods

When to Use Lombok:

  • Entities and DTOs: Perfect for data classes
  • Configuration Classes: Great for builder patterns
  • Service Classes: Constructor injection with @RequiredArgsConstructor
  • Testing: Use val for cleaner test code

When to Be Cautious:

  • JPA Entities: Be careful with @ToString and @EqualsAndHashCode
  • Inheritance: Configure callSuper properly
  • Complex Business Logic: Sometimes explicit code is clearer

Lombok works best when used consistently across a project and when team members are familiar with its features and limitations.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper