ReflectAOT is a small but powerful Gradle plugin for handling reflection at build time. Its number one goal is to avoid
java.lang.reflect entirely and optimize blazing fast reflection, while keeping ahead-of-time compilers like GraalVM,
TeaVM, and MobiVM happy.
Use the same version for the plugin and for reflectaot-runtime. Use JitPack for downloading the plugin as a dependency.
In your properties.gradle:
reflectaotVersion = '1.0.0' // So you can apply the version automatically.Then, in your settings.gradle:
pluginManagement {
repositories {
maven { url 'https://jitpack.io' }
}
resolutionStrategy {
eachPlugin {
if (requested.id.id == 'me.stringdotjar.reflectaot') {
useModule("com.github.flixelgdx.ReflectAOT:reflectaot-gradle-plugin:${requested.version}")
}
}
}
}Finally, in your build.gradle:
plugins {
id 'java'
id 'me.stringdotjar.reflectaot' version '1.0.0'
}
repositories {
maven { url 'https://jitpack.io' }
}
dependencies {
// Or runtime, depending on your circumstances.
api "com.github.flixelgdx.ReflectAOT:reflectaot-runtime:$reflectaotVersion"
}Published artifacts include -sources and -javadoc jars for IDE support when you depend on reflectaot-runtime, reflectaot-codegen, or the plugin from Maven Local or Central.
When the repository is public, JitPack can build from tags or commits using jitpack.yml at the repository root. The install step runs ./gradlew install, which publishes all modules to the local Maven repository for JitPack to pick up.
The reflectaot extension controls what gets generated:
reflectaot {
// Default: bytecode-only (ASM `.class` under build/generated/reflectaot/classes)
output.set(me.stringdotjar.reflectaot.gradle.ReflectAOTOutput.CLASS)
// Optional: Java sources only (GWT-style pipelines)
// output.set(me.stringdotjar.reflectaot.gradle.ReflectAOTOutput.JAVA)
// Optional: both bytecode and Java sources from the same model
// output.set(me.stringdotjar.reflectaot.gradle.ReflectAOTOutput.BOTH)
excludePackages.add("com.thirdparty.noisy")
extraClasses.add("com.example.MustGenerateEvenIfNotSeen")
}Defaults:
outputisCLASS.excludePackagesis empty (built-in excludes always apply forjava.*,javax.*, Kotlin/Gradle internals, and generated package roots).
After compileJava (and compileKotlin when the Kotlin JVM plugin is present), generateReflectAOT scans:
- compiled project classes
compileClasspathentries (directories and JARs)
It looks for invokestatic calls into me.stringdotjar.reflectaot.Reflect for a focused set of members (field, setField, getProperty, setProperty, hasField, fields, callMethod).
For member names passed to field, setField, property, and setProperty, the scanner resolves string literals and String locals that are assigned from a supported constant chain in the same method (for example String n = "x"; Reflect.field(o, n)). The same tracing applies to hasField so the build can see which names you query. Method parameters, concatenation, and other non-constant shapes are not traced.
For hasField, unknown names are not build errors: after specialization, the generated accessor returns false when the name does not match a known field or bean property on that receiver type.
When the receiver type is visible in bytecode as a concrete class, ReflectAOT generates per-type accessors plus a ReflectAOTRegistry implementation.
When the receiver is only java.lang.Object at the call site, the build fails with an actionable error (narrow types, or add reflectaot { extraClasses.add("fully.qualified.Name") }).
The plugin:
- runs
generateReflectAOTafter compilation tasks it depends on - packages generated bytecode into the
jartask output - adds generated bytecode to the
runtimeOnlyconfiguration (sorun, tests, and the IDE pick upReflectAOTBootstrapwithout extra wiring) - registers generated Java sources (JAVA/BOTH) on the main
javasource set
Reflect contains a small static initializer that attempts to load me.stringdotjar.reflectaot.generated.ReflectAOTBootstrap via Class.forName (this is not java.lang.reflect method/field/constructor dispatch).
Gradle is the supported integration path for dependency injection and scanning.
For Maven/Ant/Bazel/etc., you would depend on reflectaot-runtime and run an equivalent codegen step (this repository does not ship a standalone CLI in v1).
- TeaVM / MobiVM (bytecode-first): default
CLASSoutput is intended to look like normal.classinputs to those toolchains. You still must validate against your exact toolchain version and JDK subset rules. - GWT / source-first: use
JAVAorBOTHand treat emitted sources as generated roots in your GWT module configuration. GWT language subset rules still apply.
ReflectASM popularized fast, specialized accessors.
ReflectAOT is similar in spirit (specialized accessors), but differs in major ways:
- ReflectAOT is primarily build-time emission (ASM
.class), not runtime bytecode weaving in the application. - ReflectAOT targets a compact
ReflectAPI and a nojava.lang.reflectrule across runtime + generated code. - ReflectAOT trades away some "magic" (fully dynamic receivers invisible to bytecode) for deterministic builds.
- Runtime Javadoc:
reflectaot-runtime(Reflect,ReflectAOTRuntime,ReflectAOTServices,ReflectAOTDefaultDispatch). - Gradle DSL KDoc:
reflectaot-gradle-plugin(ReflectAOTPlugin,ReflectAOTExtension,GenerateReflectAOTTask,ReflectAOTOutput). - Codegen entrypoint KDoc:
reflectaot-codegen(ReflectAOTCodegen).
- Bytecode scanning cannot see through fully dynamic patterns (
Class.forName, deserialization-only types, runtime-generated classes,Reflect.field(o, name)whenois only typed asObject, ...). - Scanning
compileClasspathcan be slow on large dependency graphs; useexcludePackagesto trim. - Kotlin/Groovy call shapes may require additional tuning beyond the Java-focused smoke tests.