Skip to content

About

Java annotation processor for automatically delegating interface APIs to a composed instance of that interface

Resources

Stars

18 stars

Watchers

2 watching

Forks

Repository files navigation

AutoDelegate

Validate Maven Central Version

Java annotation processor for automatically delegating interface APIs to a composed instance of that interface. This project was inspired by Google's auto project and leverages utilities exposed in auto-common.

Intro blog post: https://www.ryandens.com/post/auto_delegate/

Usage

Requirements:

  • JDK 11 or above

Gradle

dependencies {
    annotationProcessor("com.ryandens:auto-delegate-processor:0.4.0")
    compileOnly("com.ryandens:auto-delegate-annotations:0.4.0")
}

Maven

<project>
    <dependencies>
        <dependency>
            <groupId>com.ryandens</groupId>
            <artifactId>auto-delegate-annotations</artifactId>
            <version>${auto-delegate.version}</version>
            <scope>provided</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <artifactId>maven-compiler-plugin</artifactId>
                <configuration>
                    <annotationProcessorPaths>
                        <path>
                            <groupId>com.ryandens</groupId>
                            <artifactId>auto-delegate-processor</artifactId>
                            <version>${auto-delegate.version}</version>
                        </path>
                    </annotationProcessorPaths>
                </configuration>
            </plugin>
        </plugins>
    </build>
</project>

Simple example

This simple example usage of @AutoDelegate is based off of the example given in kotlin's delegation documentation. There are more examples in auto-delegate-examples

public interface Base {
    void print();
}

public final class BaseImpl implements Base {
    private final int x;

    public BaseImpl(final int x) {
        this.x = x;
    }

    @Override
    void print() {
        System.out.println(x);
    }
}

@AutoDelegate(Base.class)
final class Derived extends AutoDelegate_Derived implements Base {

    Derived(final Base base) {
        super(base);
    }
}

Why?

Decorates a class with metadata to describe an abstract parent class that automatically delegates to an inner composed instance of an interface. This annotation processor is inspired by the Kotlin language feature delegation.

The goal of this is to encourage the use of composition over inheritance as described by Effective Java Item 18 "Favor composition over inheritance". In the section of the book, Bloch describes an InstrumentedSetthat counts the number of items added to it. In order to accomplish this, Bloch creates an abstract implementation of java.util.Set called ForwardingSet that simply composes a java.util.Set instance and forwards all calls to it. This allows Bloch to write the InstrumentedSet in a less verbose manner, by extending ForwardingSet and overriding the "add" related methods for instrumentation purposes. This is a great solution in the context of Java, but Kotlin lowers the cognitive barrier of using composition by making it less verbose to do so. In Kotlin, the need for a ForwardingSetis obviated by the "delegation" language feature linked above. The InstrumentedSet can be written concisely without relying on writing a ForwardingSet like:

 class InstrumentedSet<E>(val inner: MutableSet<E>) : MutableSet<E> by inner {
     var count: Int = 0

     override fun add(element: E): Boolean {
         count++
         return inner.add(element)
     }

     override fun addAll(elements: Collection<E>) : Boolean {
         count += elements.size
         return inner.addAll(elements)
     }
 }

This annotation strives to enable developers in the same fashion by generating abstract Forwarding classes that delegate to the inner composed instance. An equivalent InstrumentedSet implementation written with AutoDelegate is

@AutoDelegate(Set.class)
public final class InstrumentedSet<E> extends AutoDelegate_InstrumentedSet<E> implements Set<E> {
    private int addCount;

    public InstrumentedSet(final Set<E> inner) {
        super(inner);
        this.addCount = 0;
    }

    @Override
    public boolean add(final E t) {
        addCount++;
        return super.add(t);
    }

    @Override
    public boolean addAll(final Collection<? extends E> c) {
        addCount += c.size();
        return super.addAll(c);
    }

    /**
     * @return the number of times a caller has attempted to add an item to this set
     */
    public int addCount() {
        return addCount;
    }
}

While this is not as concise as the Kotlin implementation, it generates a class called AutoDelegate_InstrumentedSet in the same package as the declaring class. The declared class can then extend the generated class and call super APIs where appropriate, only overriding methods that are relevant to the implementation

Internals

👩‍💻 Development Requirements

  • JDK 27

🚀 Releasing

The project version lives in root gradle.properties. Push a matching X.Y.Z tag from a commit on main to validate, sign, publish both library modules to Maven Central, and create a GitHub release.

See the release guide for the four GitHub environment secrets, Keybase/1Password setup, and recovery instructions.

Building

Run ./gradlew build :build-logic:check --warning-mode fail to build and test all modules and validate the convention plugins. The build-logic included build provides shared formatting, metadata, publishing, and Java/JUnit conventions. Dependency versions live in gradle/libs.versions.toml, shared with build-logic; dependency repositories are declared in each build's settings. No buildSrc is required. The build-logic checks include TestKit coverage for publishing artifacts, plugin application order, configuration-cache reuse, and build-cache reuse. Install JDK 27 before building. The Gradle daemon and compilation use JDK 27 through toolchains, with automatic JDK downloads disabled so missing local or CI setup fails explicitly. CI installs Temurin 27 through actions/setup-java. Production code (including examples) compiles with --release 11; tests compile and run on JDK 27 using the toolchain defaults. JUnit dependencies are aligned through the JUnit BOM. The processor test also verifies generated code with --release 11. The included build also compiles with JDK 27, targeting Java 17 for Gradle plugin compatibility because the bundled Kotlin compiler cannot emit Java 27 bytecode. Regenerate daemon criteria with ./gradlew updateDaemonJvm; no download URLs are generated.

Develocity publishes build scans automatically when CI=true. For a local scan, run ./gradlew build --scan; use --no-scan to skip publication. Scans are published to Gradle's public service under its configured terms of use, so avoid including sensitive build information.

About

Java annotation processor for automatically delegating interface APIs to a composed instance of that interface

Resources

Stars

18 stars

Watchers

2 watching

Forks

Releases

Packages

Used by

Contributors

Languages