Manual Gradle and Maven Setup
- Full manual
build.gradle.ktsconfiguration for Higher-Kinded-J - Full manual
pom.xmlconfiguration for Higher-Kinded-J - Snapshot repository configuration for both build tools
Most projects should use the HKJ build plugin: a single line replaces all the boilerplate below. This page documents the full manual configuration for the projects that cannot use the plugin (constrained environments, in-house build frameworks, or plugins that conflict with the HKJ plugin). If the plugin is an option for you, start there instead.
Both configurations below pin the release to 25 and enable preview features, which go together: javac accepts --enable-preview only for the release it is running on, so the toolchain has to be Java 25 as well. Prerequisites explains what the flag is for, and where it is genuinely needed.
The Spring Boot starter, and the processor that generates @HkjHttpClient clients, are not covered here: Declarative HTTP Clients shows both, for Gradle and Maven.
Gradle Manual Setup
// build.gradle.kts
plugins { java }
java {
toolchain {
languageVersion.set(JavaLanguageVersion.of(25))
}
}
dependencies {
implementation("io.github.higher-kinded-j:hkj-core:LATEST_VERSION")
// The annotation processor: generates the optics, Focus paths and Effect paths your annotations ask for
annotationProcessor("io.github.higher-kinded-j:hkj-processor-plugins:LATEST_VERSION")
}
// Enable Java preview features, for code that reaches the preview classes (see Prerequisites)
tasks.withType<JavaCompile>().configureEach {
options.compilerArgs.add("--enable-preview")
}
tasks.withType<Test>().configureEach {
jvmArgs("--enable-preview")
}
tasks.withType<JavaExec>().configureEach {
jvmArgs("--enable-preview")
}
Gradle SNAPSHOT Configuration
For SNAPSHOT versions of HKJ, add the Sonatype snapshots repository:
repositories {
mavenCentral()
maven {
url = uri("https://central.sonatype.com/repository/maven-snapshots/")
}
}
Maven Manual Setup
<properties>
<maven.compiler.release>25</maven.compiler.release>
<maven.compiler.enablePreview>true</maven.compiler.enablePreview>
</properties>
<dependencies>
<dependency>
<groupId>io.github.higher-kinded-j</groupId>
<artifactId>hkj-core</artifactId>
<version>LATEST_VERSION</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- The annotation processor: generates the optics, Focus paths and Effect paths your annotations ask for -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.14.1</version>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>io.github.higher-kinded-j</groupId>
<artifactId>hkj-processor-plugins</artifactId>
<version>LATEST_VERSION</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>
<!-- Enable preview features for tests, for code that reaches the preview classes -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<configuration>
<argLine>--enable-preview</argLine>
</configuration>
</plugin>
<!-- Enable preview features for application execution, for code that reaches the preview classes -->
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<configuration>
<executable>java</executable>
<arguments>
<argument>--enable-preview</argument>
</arguments>
</configuration>
</plugin>
</plugins>
</build>
Maven SNAPSHOT Configuration
For SNAPSHOT versions, add the Sonatype snapshots repository to your pom.xml:
<repositories>
<repository>
<id>central-snapshots</id>
<url>https://central.sonatype.com/repository/maven-snapshots/</url>
<snapshots>
<enabled>true</enabled>
</snapshots>
</repository>
</repositories>
The HKJ build plugin handles all of the above (dependencies, preview flags, annotation processors, and compile-time Path type checking) with a single plugins { } block. Unless you have a specific reason to avoid it, the plugin is the recommended path.
Incremental compilation
The HKJ annotation processors register with Gradle's incremental annotation processing: the processors that generate code as aggregating, since cross-spec features such as nested mapping resolution may read any annotated element in the compilation and the mapping processors also read the classpath index described below; and CompanionAnnotationProcessor, which generates nothing and claims hkj's annotations so that -Xlint:processing does not report them, as isolating. A source set using them keeps incremental compilation; no configuration is needed. Unregistered third-party processors on the same processor path disable incrementality for the whole source set, so if compile times regress, audit the other entries on the path first.
Multi-module builds
A @GenerateMapping spec compiled in one module is found by the specs of another through an index the processor writes beside each generated Impl (Across modules explains the resolution). The index travels in the jar as an empty class in the package org.higherkindedj.mapping.index, and four things follow for the build:
- Every module that declares specs needs
hkj-processoron its processor path, not only the one that nests them. A module compiled without it has noImpland no index entry, and a downstream spec reports its pair as having no usable source. - A spec newly added to a dependency may need a clean downstream build. Gradle recompiles a source set when its classpath changes, but chooses which sources to recompile from what they reference, and no downstream source references an index entry. Until the downstream module is rebuilt from clean, or one of its sources changes, the new spec can go unseen.
- The index is classpath-only. A module with a
module-infoneither writes nor reads it, not supported yet, since the index is one package that two modules cannot share; for the same reason two spec-carrying jars cannot serve as automatic modules side by side. Across such a boundary, delegate with a leaf calling the otherImpl'sasValidatedPrism(). - The index can be turned off. The processor option
-Ahkj.mapping.index=false(in Gradle,options.compilerArgs.add("-Ahkj.mapping.index=false")on the compile task) makes a compilation write no entries and read none, which is what a library bound for a module path beside other spec-carrying jars wants. A downstream spec that then cannot find a pair says the index is off.
A shared vocabulary is the exception to all four, because it is a plain interface rather than a spec and is found by ordinary inheritance rather than through the index. The module publishing one needs no processor at all, only the library its members name (hkj-core for a ValidatedPrism leaf, hkj-api for a Getter), and none of the caveats above apply to it: no index entry to go unseen, and a module-info or a disabled index does not stop a downstream spec extending it. So the module that owns an API's house vocabulary need not be a mapping module at all. What it does need is to be on the compile classpath wherever a spec extending it is used. A dependency's spec is read from its class file against the consumer's compile classpath, so a vocabulary the dependency declares with implementation leaves that spec unreadable downstream: a use site nesting it fails, naming the missing interface. Either the consumer declares the vocabulary's module itself, or the dependency exposes it with api rather than implementation, which needs the java-library plugin applied alongside HKJ's.
Navigators across modules use no index either. A navigator into a record declared in a dependency composes the Focus class that dependency generated, so a module whose @GenerateFocus records are navigated from elsewhere needs the processor too; without it, a consumer generates no navigator for those fields and says so in a note. A consumer composes only what the dependency published, so the two need not share a processor version or generator plugins to compile. What a consumer does need is to see the types a field names: a field whose type comes from one of the dependency's own dependencies is left out of the consumer's navigator, with a note, unless that type is on the consumer's compile classpath. Either the consumer declares that module itself, or the dependency exposes it with api rather than implementation, which needs the java-library plugin applied alongside HKJ's, since HKJ's applies plain java. As with specs, a dependency newly given the processor may need a clean downstream build before its records become navigable: no downstream source references the new Focus classes, so Gradle has nothing to recompile.
Lombok
Lombok and the HKJ processors run in the same javac invocation, and the pairing is covered by a test in the HKJ build: a @Data class works as a bean-shaped @GenerateMapping wire, with the generated getters and setters visible to the bean analyser. Order matters: list Lombok before hkj-processor (within a javac round, processors run in listed order, and the bean analyser needs the accessors already materialised; the reverse order fails with a clear diagnostic). No binding artefact is needed beyond that:
dependencies {
annotationProcessor("org.projectlombok:lombok:LOMBOK_VERSION")
annotationProcessor("io.github.higher-kinded-j:hkj-processor:LATEST_VERSION")
compileOnly("org.projectlombok:lombok:LOMBOK_VERSION")
}
Mapping over types other processors generate
A mapping may name a type another annotation processor writes in the same compilation: a wire class generated from a schema, say, or a value type another library generates. Such a type does not exist until the processing round after the one that writes it, so the mapping processors wait for it. A @GenerateMapping or @GenerateMerge spec whose domain or wire type, or anything read from them (a record component, a bean property, a builder, a mix-in method), names a type not written yet is generated once that type exists, and so is any spec that nests it. It maps exactly as it would were the type written by hand, and nothing needs configuring.
If the type never appears, for example because the processor that writes it is missing from the processor path, the spec generates nothing. javac's own error names the missing type, and code that uses the spec's Impl reports that Impl missing too; supply the type and both errors go.
Previous: Build Plugins Next: Compile-Time Checks