Jilt

repository·master·Indexed 43 days ago

https://github.com/skinny85/jilt

A Java annotation processor that automatically generates Builder pattern implementations at compile time. Jilt supports standard, staged (type-safe), and functional builder styles with zero runtime overhead. It can be applied to classes, constructors, or static methods and is compatible with Java 14+ Records and Lombok.

Tokens
3.2K
Snippets
9
Records
13
Agent score
5%

What's inside Jilt

  1. How Staged Builders work with @Builder(style = BuilderStyle.STAGED)

    master

    A Staged Builder enforces a specific order of property initialization using generated interfaces. This ensures type-safety by requiring properties to be set in the exact order they are declared. If a property is marked with @Opt, it becomes optional and is grouped into a single interface that allows the user to call .build() immediately after the required properties are set.

    import org.jilt.Builder;
    import org.jilt.BuilderStyle;
    import org.jilt.Opt;
    
    @Builder(style = BuilderStyle.STAGED)
    public final class Person {
        public final String name;
        public final boolean isAdult;
    
        public Person(String name, boolean isAdult) {
            this.name = name;
            this.isAdult = isAdult;
        }
    }
    
    // Usage:
    // Person p = PersonBuilder.person().name("John").isAdult(true).build();
  2. How 'Staged, but preserving order' Builders work

    master

    The STAGED_PRESERVING_ORDER style is similar to STAGED but handles optional properties differently to maintain API backwards-compatibility. In this style, optional properties are not bunched at the end; instead, they can be 'skipped' at their specific position in the sequence. This allows you to change a required property to an optional one (using @Opt) without breaking existing client code that previously called that property.

    @Builder(style = BuilderStyle.STAGED_PRESERVING_ORDER)
    public final class User {
        public final String email, username, firstName, lastName;
    
        public User(String email, @Opt String username, String firstName, String lastName) {
            // ...
        }
    }
    
    // If username is @Opt, the client can still call:
    // User u = UserBuilder.user().email("a@b.com").username("user").firstName("A").lastName("B").build();
    // OR skip username:
    // User u = UserBuilder.user().email("a@b.com").firstName("A").lastName("B").build();
  3. How Functional Builders work

    master

    Functional Builders (set via style = BuilderStyle.FUNCTIONAL) implement a pattern similar to named parameters. Instead of a fluent interface, Jilt generates a static factory method on the Builder class that accepts arguments of specific interface types. These interfaces are obtained via static methods on the Builder class. Optional properties are nested within an Optional class to improve discoverability.

    @Builder(style = BuilderStyle.FUNCTIONAL, toBuilder = "copy")
    public final class User {
        public final String email, firstName, lastName;
        public final String username; // @Opt
    
        public User(String email, String firstName, String lastName, String username) {
            // ...
        }
    }
    
    // Usage with static imports:
    // import static example.UserBuilder.*;
    // import static example.UserBuilder.Optional.*;
    // User u = user(email("a@b.com"), firstName("A"), lastName("B"), username("u"));
  4. Handle private constructors with Jilt

    master

    If you want to force usage of a Builder and prevent direct instantiation, you can place @Builder on a private constructor. Because Jilt cannot modify existing classes to make the Builder a nested class, it handles this by making the generated Builder class abstract, its fields protected, and its build() method abstract.

    To complete the implementation, you must create a private static final nested class inside your target class that extends the generated Builder and overrides the build() method to call your private constructor.

    public final class User {
        private User(String email, String name) { /* ... */ }
    
        @Builder(style = BuilderStyle.STAGED)
        private User(String email, String name) { /* ... */ }
    
        private static final class InnerBuilder extends UserBuilder {
            @Override
            public User build() {
                return new User(email, name);
            }
        }
    
        public static UserBuilders.Email builder() {
            return new InnerBuilder();
        }
    }
  5. Install Jilt via Maven or Gradle

    master

    Jilt is a compile-time code generator that uses the Java annotation processing API. It does not require runtime dependencies. Use provided scope in Maven or compileOnly and annotationProcessor in Gradle to ensure it is only used during compilation.

    <!-- Maven -->
    <dependencies>
        <dependency>
            <groupId>cc.jilt</groupId>
            <artifactId>jilt</artifactId>
            <version>1.9.2</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>
    // Gradle
    repositories {
        mavenCentral()
    }
    
    dependencies {
        compileOnly("cc.jilt:jilt:1.9.2")
        annotationProcessor("cc.jilt:jilt:1.9.2")
    }
  6. Use Meta-annotations to reuse Builder configurations

    master

    You can define a custom annotation annotated with both @Builder and @BuilderInterfaces to create a 'meta-annotation'. This allows you to apply a consistent Builder configuration (like style, prefixes, and interface naming) across multiple classes without repeating the configuration.

    Note: Because Jilt is an annotation processor, meta-annotations must be defined and used within the same source set (e.g., both in main or both in test).

    @Builder(style = BuilderStyle.STAGED, setterPrefix = "set", buildMethod = "create")
    @BuilderInterfaces(innerNames = "B_*")
    public @interface MyBuilder {}
    
    @MyBuilder
    public final class MyValueClass {
        // ...
    }
  7. Troubleshoot IDE issues with Jilt

    master

    Annotation processors may require manual configuration in IDEs to recognize generated code.

    IntelliJ IDEA:

    1. Enable annotation processing: File -> Settings -> Build, Execution, Deployment -> Compiler -> Annotation Processors -> check Enable annotation processing.
    2. If generated classes are not found, change Store generated sources relative to to Module content root and mark the generated or generated_tests directories as Sources Root or Test Sources Root.
  8. Troubleshoot IDE issues with Jilt (Eclipse)

    master

    To use Jilt in Eclipse:

    1. Right-click project -> Properties -> Java Compiler -> Annotation Processing -> check Enable project-specific settings and Enable annotation processing.
    2. Under Factory Path, check Enable project-specific settings and add the Jilt JAR.
    3. Ensure the Generated source directory is marked as a Source Folder.
  9. Generate a Builder using @Builder on a constructor

    master

    Placing @Builder on a constructor makes the Builder properties match the constructor parameters. The Builder will use that specific constructor to instantiate the class. Fields in the class that are not part of the annotated constructor will be ignored by the Builder.

    import org.jilt.Builder;
    
    public final class Person {
        public final String name;
        public final boolean isAdult;
        private int ignoredField;
    
        @Builder
        public Person(String name, boolean isAdult) {
            this.name = name;
            this.isAdult = isAdult;
        }
    }
  10. Generate a Builder using @Builder on a static method

    master

    Placing @Builder on a static method is the most flexible approach. The Builder properties will be the method's parameters, and the instance will be created by calling the annotated method. This allows you to generate Builders for classes you don't control, or for classes with non-standard construction logic (like validation or default values).

    import org.jilt.Builder;
    import java.util.Date;
    
    public abstract class DateFactory {
        @Builder(packageName = "com.example")
        public static Date make(int month, int day, int year) {
            if (month < 1 || month > 12) throw new IllegalArgumentException();
            return new Date(year + 1900, month - 1, day);
        }
    }
    
    // Usage:
    // Date d = DateBuilder.date().month(12).year(23).build();
  11. Generate a basic Builder using @Builder on a class

    master

    Placing @Builder on a class generates a Builder class (named <BuiltClass>Builder) that includes all instance fields. The Builder assumes the target class has a constructor that accepts all these properties in the order they are declared. This works seamlessly with Java 14+ Records and Lombok.

    import org.jilt.Builder;
    
    @Builder
    public final class Person {
        public final String name;
        public final boolean isAdult;
    
        public Person(String name, boolean isAdult) {
            this.name = name;
            this.isAdult = isAdult;
        }
    }
    
    // Usage:
    // Person p = PersonBuilder.person().name("Alice").isAdult(true).build();
  12. Configure @BuilderInterfaces attributes

    master

    When using STAGED or FUNCTIONAL styles, use @BuilderInterfaces to customize the generated type-safe interfaces:

    • outerName: The name of the outer interface containing per-property interfaces (default: <BuilderClass>s).
    • packageName: The package for the generated interfaces.
    • innerNames: A pattern for naming per-property interfaces. Use * for the property name (default: "*").
    • lastInnerName: The name of the final interface containing the build method (default: Optionals for STAGED, Build for STAGED_PRESERVING_ORDER).