Sourcegen 2.2.0-SNAPSHOT
On this page

Sourcegen

1 Introduction

Micronaut SourceGen exposes a language-neutral API for performing source code generation. Since JavaPoet is no longer maintained this module includes a fork of the code with extensions to support Java Records and other modern Java constructs.

An additional API is provided in the io.micronaut.sourcegen.model package that abstracts both JavaPoet and KotlinPoet such that the developer can build source generators in a language neutral manner.

Any processors built with the Micronaut SourceGen API work with:

  • Java

  • Kotlin via KAPT

  • Kotlin via KSP

Note that Groovy is not supported at this time of writing since it lacks APIs to add generated sources to the current compilation unit and perform multiple rounds of processing like Java and Kotlin do.

2 Release History

For this project, you can find a list of releases (with release notes) here:

3 Quick Start

To get started add Micronaut SourceGen to the annotation processor scope of your build configuration:

For Java projects add:

annotationProcessor("io.micronaut.sourcegen:micronaut-sourcegen-generator-java")

For Kotlin projects using KSP add:

ksp("io.micronaut.sourcegen:micronaut-sourcegen-generator-kotlin")

or, for those using KAPT add:

kapt("io.micronaut.sourcegen:micronaut-sourcegen-generator-kotlin")

4 Writing a Source Generator

To write a source generator you have a few options. If you only wish to support Java you can use the JavaPoet API that is forked into the io.micronaut.sourcegen.javapoet package. Since JavaPoet is no longer being developed and maintained we’re recommending using this fork since it is maintained as part of the project and includes support for Java 17+ constructors like Records.

Alternatively, you can also use the language neutral code generation API defined in the io.micronaut.sourcegen.model package that works with both Kotlin and Java adding the ability to write code generators that work cross language.

To get started add dependency on the micronaut-core-processor module:

implementation("io.micronaut.sourcegen:micronaut-core-processor")

And the micronaut-sourcegen-model module:

implementation("io.micronaut.sourcegen:micronaut-sourcegen-model")
Warning
The module you build will have to be placed on the annotation processor classpath of the target project. You CANNOT mix source generation code and application code is the same source tree. Typically, you will have a separate project for your source generator a separate project for your application that uses the source generator. In addition, the micronaut-core-processor module should NEVER be in the application classpath.

The following is an example of using the API:

  1. Once you have built the model write it to a source file!

Tip
To see more examples take a look at some of the existing implementations like BuilderAnnotationVisitor that powers the @Builder annotation.

JDK bytecode backend

The sourcegen-generator-bytecode-jdk annotation-processor backend uses the JDK 25 toolchain and emits Java 17-compatible classes. It is an alternative to sourcegen-generator-bytecode, which uses ASM. Select exactly one Java Sourcegen backend on an annotation-processor path. The direct sourcegen-bytecode-writer-jdk API is available when a byte[] is required; it also requires JDK 25.

The backend writes class files directly for classes, records, interfaces, enums and their member types. A definition using a construct the direct writer does not lower yet is rendered to Java source and compiled with the in-process JDK compiler. Should that compilation fail as well, the generator emits the Java source file and reports a compiler warning naming the class and the reason.

A generated class often references types that are still being compiled and so have no class file to read. The annotation-processor backend gives the writer the compilation’s own type lookup so their hierarchy can be resolved. Code calling sourcegen-bytecode-writer-jdk directly should pass the same lookup, as an io.micronaut.sourcegen.bytecode.jdk.CompilationTypes, when it runs inside a compiler.

When a generated class references a type that is still being compiled, the source compilation resolves such types from src/main/java and src/test/java of the project directory. Sources kept elsewhere, such as shared source sets, can be added through the micronaut.sourcegen.bytecode.jdk.sourcepath annotation-processor option. An annotation processor only sees its own processor path, so generated classes that reference compile-scope dependencies need the compilation’s class path passed through the micronaut.sourcegen.bytecode.jdk.classpath option. Both accept a path-separator-delimited list:

tasks.withType<JavaCompile>().configureEach {
    options.compilerArgs.add("-Amicronaut.sourcegen.bytecode.jdk.sourcepath=/path/to/shared/src/main/java")
    options.compilerArgs.add("-Amicronaut.sourcegen.bytecode.jdk.classpath=${classpath.asPath}")
}

5 Annotations

The module Micronaut SourceGen annotations ships with annotations which you can use in your projects:

To use them, you need the following dependency:

implementation("io.micronaut.sourcegen:micronaut-sourcegen-annotations")
Annotation Description

Builder

Create a builder of the annotated type.

StagedBuilder

Create a builder of the annotated type that assigns the required properties in stages.

Wither

Create an interface with copy style method and possible builder style methods for a record

ToString

Generated a new class with a static implementation of the java.utils.Object’s toString() method for annotated Java Bean

EqualsAndHashCode

Generated a new class with a static implementation of the java.utils.Object’s equals() and hashCode() methods for annotated Java Bean

5.1 Builder

If you annotate a Java Record with @Builder, a PersonBuilder class is generated at compilation-time.

import io.micronaut.sourcegen.annotations.Builder;

@Builder
public record Person(Long id, String name, byte[] bytes) {
}

You can use a builder pattern to create a Person instance:

@Test
public void buildsPerson() {
    var person = PersonBuilder.builder()
        .id(123L)
        .name("Cédric")
        .bytes(new byte[]{1,2,3})
        .build();
    assertEquals("Cédric", person.name());
    assertArrayEquals(new byte[]{1, 2, 3}, person.bytes());
    assertEquals(123L, person.id());
}
Note
The BuilderAnnotationVisitor is an example of how to use the Micronaut SourceGen API.

5.2 Staged Builder

The @StagedBuilder annotation generates a staged builder: a builder that assigns the required properties in stages, so that forgetting one of them is a compilation error instead of a missing value at runtime.

If you annotate a type with @StagedBuilder, an EmployeeStagedBuilder class is generated at compilation-time.

import io.micronaut.core.annotation.Nullable;
import io.micronaut.sourcegen.annotations.StagedBuilder;

@StagedBuilder
public record Employee(String name, int age, boolean employed, @Nullable String nickname) {
}

Every required property gets its own stage interface, which declares the single method that assigns that property and returns the next stage. The last of them returns BuildFinal, the only stage that declares the build method:

public final class EmployeeStagedBuilder {
    public static NameBuildStage builder() { ... }

    public interface NameBuildStage {
        AgeBuildStage name(String name);
    }
    public interface AgeBuildStage {
        EmployedBuildStage age(int age);
    }
    public interface EmployedBuildStage {
        BuildFinal employed(boolean employed);
    }
    public interface BuildFinal {
        BuildFinal nickname(String nickname);

        Employee build();
    }

    public static final class Builder implements NameBuildStage, AgeBuildStage, EmployedBuildStage, BuildFinal { ... }
}

You can use the builder to create an Employee instance, assigning the required properties in the order they are declared in:

@Test
public void buildsEmployee() {
    var employee = EmployeeStagedBuilder.builder()
        .name("Billy Bounce")
        .age(33)
        .employed(false)
        .nickname("Billy")
        .build();
    assertEquals("Billy Bounce", employee.name());
    assertEquals(33, employee.age());
    assertEquals(false, employee.employed());
    assertEquals("Billy", employee.nickname());
}

A property is required unless the builder can leave it unassigned, which is the case for:

  • a nullable property, which keeps its null

  • a property with a @Bindable(defaultValue = "…​") default value, which keeps that value

  • a property annotated with Singular, which accumulates into an empty collection

Those properties are declared on the BuildFinal stage instead and can be assigned in any order, or not at all.

Note
The stages add generated interfaces to the compilation output, which affects the class-loading time of an application using them extensively. Use @Builder when the compile-time safety is not worth that.

5.3 Wither

If you annotate a Java Record with @Wither, a YourRecordWither interface is generated at compilation-time with all the methods having a default implementation (nothing needs to be implemented). The record class can add that interface using implements YourRecordWither which will expand it with the default withProperty copy style methods like YourRecord withPropertyName(PropertyValue), calling those methods will create a copy of the record with a modified property.

@Wither
public record Walrus (
    @NonNull
    String name,
    int age,
    byte[] chipInfo
) implements WalrusWither  {
}

Example of different ways to use the copy methods:

@Test
public void test() throws Exception {
    Walrus walrus = new Walrus("Abc", 123, new byte[]{56});

    assertEquals("Abc", walrus.name());
    assertEquals(123, walrus.age());
    assertArrayEquals(new byte[]{56}, walrus.chipInfo());

    Walrus finalWalrus = walrus;
    // The name property is annotated with @NonNull the `withName(null)` method should fail
    assertThrowsExactly(NullPointerException.class, () -> finalWalrus.withName(null));

    walrus = walrus.withName("Xyz");

    assertEquals("Xyz", walrus.name());
    assertEquals(123, walrus.age());
    assertArrayEquals(new byte[]{56}, walrus.chipInfo());

    walrus = walrus.withAge(99);

    assertEquals("Xyz", walrus.name());
    assertEquals(99, walrus.age());
    assertArrayEquals(new byte[]{56}, walrus.chipInfo());

    walrus = walrus.withChipInfo(new byte[]{1, 2, 3});

    assertEquals("Xyz", walrus.name());
    assertEquals(99, walrus.age());
    assertArrayEquals(new byte[]{1, 2, 3}, walrus.chipInfo());
}

If your record is annotated with Builder the wither interface will also include:

  • default YourRecordBuilder with() {…​} method that will return the record builder populated with the current values of the record

  • default YourRecord with(Consumer<YourRecordBuilder> consumer) {…​} a method receiving a lambda that can modify the populated record builder with the current values of the record and producing a new instance of a record as a result

@Wither
@Builder
public record Walrus2(
    String name,
    int age,
    byte[] chipInfo
) implements Walrus2Wither  {
}

Example of different ways to use the copy and builder methods:

@Test
public void testWitherAndBuilder() throws Exception {
    Walrus2 walrus = new Walrus2("Abc", 123, new byte[]{56});

    assertEquals("Abc", walrus.name());
    assertEquals(123, walrus.age());
    assertArrayEquals(new byte[]{56}, walrus.chipInfo());

    // The name property is NOT annotated with @NotNull so `withName(null)` method should NOT fail
    walrus = walrus.withName(null);

    assertNull(walrus.name());
    assertEquals(123, walrus.age());
    assertArrayEquals(new byte[]{56}, walrus.chipInfo());

    walrus = walrus.withName("Xyz");

    assertEquals("Xyz", walrus.name());
    assertEquals(123, walrus.age());
    assertArrayEquals(new byte[]{56}, walrus.chipInfo());

    walrus = walrus.withAge(99);

    assertEquals("Xyz", walrus.name());
    assertEquals(99, walrus.age());
    assertArrayEquals(new byte[]{56}, walrus.chipInfo());

    walrus = walrus.withChipInfo(new byte[]{1, 2, 3});

    assertEquals("Xyz", walrus.name());
    assertEquals(99, walrus.age());
    assertArrayEquals(new byte[]{1, 2, 3}, walrus.chipInfo());

    walrus = walrus.with().build();

    assertEquals("Xyz", walrus.name());
    assertEquals(99, walrus.age());
    assertArrayEquals(new byte[]{1, 2, 3}, walrus.chipInfo());

    walrus = walrus.with().name("Foobar").build();

    assertEquals("Foobar", walrus.name());
    assertEquals(99, walrus.age());
    assertArrayEquals(new byte[]{1, 2, 3}, walrus.chipInfo());

    walrus = walrus.with().name("Abc").age(123).chipInfo(new byte[]{9, 8, 7}).build();

    assertEquals("Abc", walrus.name());
    assertEquals(123, walrus.age());
    assertArrayEquals(new byte[]{9, 8, 7}, walrus.chipInfo());

    walrus = walrus.with(builder -> builder.name("Denis"));

    assertEquals("Denis", walrus.name());
    assertEquals(123, walrus.age());
    assertArrayEquals(new byte[]{9, 8, 7}, walrus.chipInfo());

    walrus = walrus.with(builder -> builder.name("Kevin").age(1).chipInfo(new byte[]{123}));

    assertEquals("Kevin", walrus.name());
    assertEquals(1, walrus.age());
    assertArrayEquals(new byte[]{123}, walrus.chipInfo());
}

Selecting the properties

The @Wither annotation can also be placed directly on the record components, in that case a with method is only generated for the annotated components. This is useful when only a subset of the record should be copy-modifiable. Placing @Wither on a component also works when the record itself is annotated with @Wither, the component level annotations then restrict the generation to the annotated components.

public record Walrus4(
    @Wither
    String name,
    int age,
    @Wither
    byte[] chipInfo
) implements Walrus4Wither {
}
@Test
public void test() {
    Walrus4 walrus = new Walrus4("Abc", 123, new byte[]{56});

    walrus = walrus.withName("Xyz").withChipInfo(new byte[]{1, 2, 3});

    assertEquals("Xyz", walrus.name());
    assertEquals(123, walrus.age());
    assertArrayEquals(new byte[]{1, 2, 3}, walrus.chipInfo());
}

@Test
public void testOnlyAnnotatedComponentsHaveWithMethods() {
    // `age` is not annotated with @Wither, so no `withAge` method is generated
    assertTrue(Arrays.stream(Walrus4Wither.class.getMethods()).noneMatch(m -> m.getName().equals("withAge")));
}
Note
The generated interface still declares the accessors of every record component, because all of them are required to create the modified copy.

5.4 Super Builder

To use a builder pattern with types that are using inheritance you annotate a Java bean with @SuperBuilder:

@SuperBuilder
public abstract class Animal {

    private String name;
    private int age;
    private String color;

    // Getters / Setters
}
@SuperBuilder
public class Cat extends Animal {

    private int meowLevel;
    private String bread;

    // Getters / Setters

    @Vetoed // vetoed is currently required.
    public static CatSuperBuilder builder() {
        return new CatSuperBuilder();
    }
}
@SuperBuilder
public class Dog extends Animal {

    private int barkLevel;
    private String bread;
    private boolean big;

    // Getters / Setters
}

For every class annotated with @SuperBuilder there going to be two builders generated at compilation-time, an abstract one intended to be inherited and second one to constructor the bean.

In the previous example five classes are generated:

  • AbstractAnimalSuperBuilder

  • AbstractCatSuperBuilder

  • CatSuperBuilder

  • AbstractDogSuperBuilder

  • DogSuperBuilder

Note
@SuperBuilder requires every super-class to be annotated with @SuperBuilder

You can use a builder to create an instance:

@Test
public void testCat() {
    Cat cat = new CatSuperBuilder()
        .name("MrPurr")
        .age(2)
        .bread("British")
        .meowLevel(100)
        .color("Red")
        .build();

    assertEquals("MrPurr", cat.getName());
    assertEquals(2, cat.getAge());
    assertEquals("British", cat.getBread());
    assertEquals(100, cat.getMeowLevel());
    assertEquals("Red", cat.getColor());
}

@Test
public void testDog() {
    Dog dog = new DogSuperBuilder()
        .name("MrDog")
        .age(3)
        .bread("JackR")
        .barkLevel(20)
        .color("Blue")
        .big(true)
        .build();

    assertEquals("MrDog", dog.getName());
    assertEquals(3, dog.getAge());
    assertEquals("JackR", dog.getBread());
    assertEquals(20, dog.getBarkLevel());
    assertEquals("Blue", dog.getColor());
    assertTrue(dog.isBig());
}

5.5 Singular

The @Singular annotation is used together with @Builder or @SuperBuilder on a collection property.

The property annotated as singular will have the following:

  • A method with a singular name to add a single element to a collection

  • A method with a plural name and a collection parameter to include all the elements from that collection

  • A clear+PropertyName to clean all the items of the collection.

The annotation only following collection types:

  • java.lang.Iterable

  • java.util.Collection

  • java.util.List

  • java.util.Set

  • java.util.SortedSet

  • java.util.Map

  • java.util.SortedMap

Note
The final collection is always immutable
An example of a bean with two properties annotated as singular
import io.micronaut.sourcegen.annotations.Singular;
import io.micronaut.sourcegen.annotations.SuperBuilder;

import java.util.List;
import java.util.Map;

@SuperBuilder
public record User(Long id,
                   String name,
                   @Singular List<String> roles,
                   @Singular("property") Map<String, Object> properties) {
}
A test case with different examples how new methods can be used
@Test
public void testSimple() {
    User user = new UserSuperBuilder()
        .id(123L)
        .name("Denis")
        .role("READ")
        .role("WRITE")
        .property("key", "value")
        .build();
    assertEquals(123L, user.id());
    assertEquals("Denis", user.name());
    assertEquals(List.of("READ", "WRITE"), user.roles());
    assertEquals(Map.of("key", "value"), user.properties());
}

@Test
public void testAddAll() {
    User user = new UserSuperBuilder()
        .id(123L)
        .name("Denis")
        .role("READ")
        .role("WRITE")
        .roles(List.of("ROLES_ADMIN", "ROLES_USER"))
        .property("key", "value")
        .properties(Map.of("key1", "value1", "key2", "value2"))
        .build();
    assertEquals(123L, user.id());
    assertEquals("Denis", user.name());
    assertEquals(List.of("READ", "WRITE", "ROLES_ADMIN", "ROLES_USER"), user.roles());
    assertEquals(Map.of("key", "value", "key1", "value1", "key2", "value2"), user.properties());
}

@Test
public void testClear() {
    User user = new UserSuperBuilder()
        .id(123L)
        .name("Denis")
        .role("READ")
        .role("WRITE")
        .clearRoles()
        .roles(List.of("ROLES_ADMIN", "ROLES_USER"))
        .property("key", "value")
        .clearProperties()
        .properties(Map.of("key1", "value1", "key2", "value2"))
        .build();
    assertEquals(123L, user.id());
    assertEquals("Denis", user.name());
    assertEquals(List.of("ROLES_ADMIN", "ROLES_USER"), user.roles());
    assertEquals(Map.of("key1", "value1", "key2", "value2"), user.properties());
}

5.6 ToString

If you annotate a Java Bean with @ToString, a [Bean]Object class is generated at compilation-time with a static implementation of the java.utils.Object’s toString() method.

  • Properties that do not wish to be printed out should be annotated with @ToString.Exclude

  • All bean properties (with getters) will be written in the form of a Java Record in the generated toString() method with signature:

public static String [BeanName]Object.toString(BeanName object)
Note
The user is expected to use the generated static method by overriding the toString() method themselves.
An example annotated bean with different type of properties and an overridden toString() method:
import io.micronaut.sourcegen.annotations.EqualsAndHashCode;
import io.micronaut.sourcegen.annotations.ToString;

@ToString
@EqualsAndHashCode
public class Elephant {
    @EqualsAndHashCode.Exclude
    public String name;
    public int age;
    private boolean hasSibling;

    @ToString.Exclude
    private int[][] values;

    public Elephant(String name, int age, boolean hasSibling, int value) {
        this.name = name;
        this.age = age;
        this.hasSibling = hasSibling;
        values = new int[3][3];
        for(int i=0; i<3; i++) {
            for(int j=0; j<3; j++) {
                values[i][j] = value;
            }
        }
    }

    public Elephant(String name) {
        this.name = name;
    }

    public String getName() {
        return name;
    }

    public boolean isHasSibling() {
        return hasSibling;
    }

    public int[][] getValues() {
        return values;
    }

    @Override
    public String toString() {
        return ElephantObject.toString(this);
    }

    @Override
    public boolean equals(Object o) {
        return ElephantObject.equals(this, o);
    }

    @Override
    public int hashCode() {
        return ElephantObject.hashCode(this);
    }
}
A test case with different examples how new methods can be used
@Test
public void testToString() {
    var person = new Person4(123L, Person4.Title.MR,"Cédric", new byte[]{1,2,3});

    assertNotNull(Person4Object.toString(person));
    Assertions.assertTrue(person.toString().contains("Person4["));
    assertEquals("Person4[id=123, title=MR, name=Cédric, bytes=[1, 2, 3]]", person.toString());
}

5.7 Equals and HashCode

If you annotate a Java Bean with @EqualsAndHashCode, a [Bean]Object class is generated at compilation-time with a static implementation of the java.utils.Object’s equals() and hashCode() methods.

  • Properties that do not wish to be processed should be annotated with @EqualsAndHashCode.Exclude

  • All bean properties (with getters) will be used in the calculations of both methods with signature:

public static int [BeanName]Object.hashCode(BeanName object)
public static boolean [BeanName]Object.equals(BeanName object, Object o)
Note
The user is expected to use the generated static method by overriding the equals() and hashCode() methods themselves.
An example annotated bean with different type of properties
import io.micronaut.sourcegen.annotations.EqualsAndHashCode;
import io.micronaut.sourcegen.annotations.ToString;

import java.util.Arrays;

@ToString
@EqualsAndHashCode
public class Person4 {
    public enum Title {
        MRS,
        MR,
        MS
    }

    private long id;
    private Title title;
    private String name;
    private byte[] bytes;

    public Person4(long id, Title title, String name, byte[] bytes) {
        this.id = id;
        this.title = title;
        this.name = name;
        this.bytes = (bytes != null) ? Arrays.copyOf(bytes, bytes.length) : null;
    }

    public long getId() {
        return id;
    }

    public Title getTitle() {
        return title;
    }

    public String getName() {
        return name;
    }

    public byte[] getBytes() {
        return bytes;
    }

    @Override
    public String toString() {
        return Person4Object.toString(this);
    }

    @Override
    public boolean equals(Object o) {
        return Person4Object.equals(this, o);
    }

    @Override
    public int hashCode() {
        return Person4Object.hashCode(this);
    }
}
A test case with different examples how new methods can be used
@Test
public void testMultipleDimensionArrays() {
    var elephant = new Elephant("Daisy", 5, false, 1);
    var elephantDiff = new Elephant("Daisy", 5, false, 2);
    var elephantSame = new Elephant("Dumbo", 5, false, 1);

    assertNotEquals(elephant.hashCode(), elephantDiff.hashCode());
    assertEquals(elephant.hashCode(), elephantSame.hashCode());
}

@Test
public void testEqualsWithExclude() {
    var elephant = new Elephant("Daisy", 5, false, 1);
    var elephantDiff = new Elephant("Daisy", 5, false, 2);
    var elephantSame = new Elephant("Dumbo", 5, false, 1);

    assertNotEquals(elephant, elephantDiff);
    assertEquals(elephant, elephantSame);
}

@Test
public void testEqualsWithCorrectObjects() {
    var person = new Person4(123L, Person4.Title.MR,"Cédric", new byte[]{1,2,3});
    var personSame = new Person4(123L, Person4.Title.MR,"Cédric", new byte[]{1,2,3});
    var personDiffPrimitive = new Person4(124L, Person4.Title.MR,"Cédric", new byte[]{1,2,3});
    var personDiffEnum = new Person4(123L, Person4.Title.MRS,"Cédric", new byte[]{1,2,3});
    var personDiffObject = new Person4(123L, Person4.Title.MR,"Cédric Jr.", new byte[]{1,2,3});
    var personDiffArray = new Person4(123L, Person4.Title.MR,"Cédric", new byte[]{1,2,4});

    assertNotNull(Person4Object.equals(person, personSame));

    assertEquals(person, person);
    assertEquals(person, personSame);

    assertNotEquals(person, personDiffPrimitive);
    assertNotEquals(person, personDiffEnum);
    assertNotEquals(person, personDiffObject);
    assertNotEquals(person, personDiffArray);
}

@Test
public void testEqualsWithNulls() {
    var person = new Person4(123L, Person4.Title.MR,"Cédric", new byte[]{1,2,3});
    var personDoubleNull1 = new Person4(123L, Person4.Title.MR,null, new byte[]{1,2,3});
    var personDoubleNull2 = new Person4(123L, Person4.Title.MR,null, new byte[]{1,2,3});
    var personSingleNull = new Person4(124L, Person4.Title.MR,"Cédric", null);

    assertNotEquals(null, person);
    assertNotEquals(new Object(), person);

    assertEquals(personDoubleNull1, personDoubleNull2);
    assertNotEquals(personSingleNull, person);
    assertNotEquals(person, personSingleNull);
}

@Test
public void testHashCodeWithNulls() {
    var person = new Person4(123L, Person4.Title.MR,"Cédric", new byte[]{1,2,3});
    var personDoubleNull1 = new Person4(123L, Person4.Title.MR,null, new byte[]{1,2,3});
    var personDoubleNull2 = new Person4(123L, Person4.Title.MR,null, new byte[]{1,2,3});
    var personSingleNull = new Person4(124L, Person4.Title.MR,"Cédric", null);

    assertEquals(personDoubleNull1.hashCode(), personDoubleNull2.hashCode());
    assertNotEquals(personSingleNull.hashCode(), person.hashCode());
    assertNotEquals(person.hashCode(), personSingleNull.hashCode());
}

@Test
public void testHashCode() {
    var person = new Person4(123L, Person4.Title.MR,"Cédric", new byte[]{1,2,3});
    var personSame = new Person4(123L, Person4.Title.MR,"Cédric", new byte[]{1,2,3});
    var personDiffPrimitive = new Person4(124L, Person4.Title.MR,"Cédric", new byte[]{1,2,3});
    var personDiffEnum = new Person4(123L, Person4.Title.MRS,"Cédric", new byte[]{1,2,3});
    var personDiffObject = new Person4(123L, Person4.Title.MR,"Cédric Jr.", new byte[]{1,2,3});
    var personDiffArray = new Person4(123L, Person4.Title.MR,"Cédric", new byte[]{1,2,4});

    assertNotNull(Person4Object.hashCode(person));
    assertEquals(person.hashCode(), person.hashCode());
    assertEquals(person.hashCode(), personSame.hashCode());
    assertNotEquals(person.hashCode(), personDiffPrimitive.hashCode());
    assertNotEquals(person.hashCode(), personDiffEnum.hashCode());
    assertNotEquals(person.hashCode(), personDiffObject.hashCode());
    assertNotEquals(person.hashCode(), personDiffArray.hashCode());
}

5.8 Delegate

If you annotate a Java interface with @Delegate, a <type>Delegate abstract class is generated at compilation-time.

import io.micronaut.sourcegen.annotations.Delegate;

import java.util.List;
import java.util.Set;

/**
 * A worker interface that delegate will be generated for.
 */
@Delegate
public interface Worker<T> extends SimpleWorker {

    String name();

    boolean canComplete(List<T> tasks);

    T currentTask();

    List<String> competencies();

    Set<?> complaints();

}
/**
 * A simple worker interface.
 */
public interface SimpleWorker {

    String name();

    double tasksPerDay();

}

You can use a delegate pattern to change the behavior of a Worker delegatee:

/**
 * A delegate changing tasks per day.
 */
public class OvertimeWorker<T> extends WorkerDelegate<T> {

    OvertimeWorker(Worker<T> delegatee) {
        super(delegatee);
    }

    @Override
    public double tasksPerDay() {
        return super.tasksPerDay() * 1.2;
    }
}

The delegate will delegate the behavior to inner object except for changed methods:

OvertimeWorker<String> worker = new OvertimeWorker<>(new RobotWorker<String>(
    "robot",
    10,
    List.of("does everything"),
    "wash flowers",
    Set.of()
));

assertEquals("robot", worker.name());
assertEquals(12, worker.tasksPerDay()); // increased because of delegate
assertEquals(List.of("does everything"), worker.competencies());
assertEquals("wash flowers", worker.currentTask());

6 Repository

You can find the source code of this project in this repository: