This library provides standalone zero-dependency annotations for generating GraalVM Native Image Reachability Metadata at compile time. They are compatible with standard Java code and are independent of any framework like Quarkus or Micronaut.
The annotations also make extensive use of reachability conditions. By default, the settings are only applied if the annotated type is reachable in the native image. This mitigates binary size explosion for features that aren't used. The condition can be changed by setting a custom condition, or disabled by setting condition = Object.class.
You can look at BEST_PRACTICES.md for examples of how to find reflective, JNI, and resource lookups, which annotation fits each case, and how to verify the result.
Note that the current default output is the older 1.0.0 format with separate *-config.json files, but this will likely change to 1.2.0 with a combined reachability-metadata.json in the future. The newer format has a narrower reachability condition (typeReached instead of typeReachable), so you should always specify the format to avoid surprises (see Generated Metadata). The examples below show the 1.2.0 output for readability.
@Reachable
public class Example {}The default configuration enables full reflection with a condition on the annotated type
{
"condition": { "typeReached": "reachability.sample.Example" },
"type": "reachability.sample.Example",
"allDeclaredMethods": true,
"allDeclaredFields": true,
"allDeclaredConstructors": true
}Specifying other targets (classes, classNames, resources, bundles, or proxies) replaces the default class. If the annotated class is needed as well, it needs to be added to the list
@Reachable // registers Example
public class Example {}
@Reachable(classes = { Other.class }) // registers only Other
public class Example {}
@Reachable(classes = { Example.class, Other.class }) // registers Example and Other
public class Example {}
@Reachable(resources = { "images/*.png" }) // registers only resources
public class Example {}
@Reachable(classes = Example.class, resources = { "images/*.png" }) // registers Example and resources
public class Example {}@Reachable(
condition = InetSocketAddress.class,
classes = { Example.class },
memberAccess = {
MemberAccess.ALL_DECLARED_CONSTRUCTORS,
MemberAccess.ALL_PUBLIC_METHODS,
},
resources = { "images/*.png" },
bundles = { "bundle" },
jniAccessible = true)
public class Example {
@ReachableMember
String field;
}The deeper annotation settings enable access to all of the fields available in the metadata format. Relative resource/bundle paths are relative to the annotated type.
{
"condition": { "typeReached": "java.net.InetSocketAddress" },
"type": "reachability.sample.Example",
"fields": [
{ "name": "field" }
],
"allDeclaredConstructors": true,
"allPublicMethods": true,
"jniAccessible": true
}
{
"condition": { "typeReached": "java.net.InetSocketAddress" },
"glob": "reachability/sample/images/*.png"
}
{
"condition": { "typeReached": "java.net.InetSocketAddress" },
"bundle": "reachability.sample.bundle"
}Below is a list of all supported annotations and some examples to get started. Check the JavaDoc for more detailed documentation.
Declares classes, resources, dynamic proxies, and bundles that should be available in the native image.
// Enables full reflection if the annotated class is reached from any point
@Reachable
public enum ReflectivelyAccessedEnum {}
// Full reflection of all specified classes and their parent hierarchy. The annotated
// class itself is not part of the list unless explicitly specified
@Reachable(
includeClassHierarchy = true,
classes = {InetAddress.class, InetSocketAddress.class},
classNames = {"some.internal.class$Nested", "other.internal.class"}
)
public class ReferencingOtherClasses {}
// Resource globs relative to the annotated class
@Reachable(condition = ImageLoader.class, resources = {
"images/*.png",
"images/*.jpg",
})
public class RelativeResourceGlobs {}
// Resource globs relative to the class path root (start with '/')
@Reachable(conditionName = "custom.condition.ImageLoader", resources = {
"/assets/*.png",
"/assets/*.jpg",
})
public class AbsoluteResourceGlobs {}
// Unconditional custom access of a hidden class w/ JNI
@Reachable(
condition = Object.class,
classNames = "sun.misc.Unsafe",
memberAccess = { MemberAccess.ALL_DECLARED_FIELDS},
jniAccessible = true
)
public static class FineGrainedAccess {}The @ReachableMember annotation provides more fine-grained access for configuring access to individual fields and methods. The JavaFX annotation @FXML gets picked up as well with the same behavior.
public static class IndividualFieldsAndMethods {
@ReachableMember
void doNothing(String input, Object output) {}
@ReachableMember
String field1;
}This is a special annotation for working with JavaFX FXML - a markup language for GUI layouts that makes extensive use of reflection.
Matching FXML and CSS files on the output classpath are automatically parsed, and add reachability configuration for the classes and resources they use.
The default naming convention follows established conventions (see FxmlKit or Afterburner.fx) to determine the resource names based on the lowercased view name. For example,
${Name}View.java->DialogView.java${name}.fxml->dialog.fxml${name}.css->dialog.css${name}*.properties->dialog_en.properties,dialog_de.properties, ...
@ReachableFxView("dialog") // checks for dialog.fxml, dialog.css, dialog.properties etc.
public class DialogView extends com.airhacks.afterburner.views.FXMLView {}Another JavaFX related annotation that provides more resource control and can parse multiple FXML/CSS files based on globs.
// Enables full reflection of all found classes and their parent hierarchy
@ReachableFxResources({
"/assets/images/*.png", // relative to classpath root
"views/**/*.fxml", // relative to annotated type
"views/**/*.css", // relative to annotated type
})
public class MyApp extends Application {}This processor also includes an opt-in feature that looks at common dependency injection annotations (both javafx.* and jakarta.*), and generates reflection configurations that covers most standard usage. It is opt-in to avoid interfering with static code generators of other frameworks.
You can enable it using the compiler argument -Areachability.processDependencyInjection=true. See below for more details.
For example,
public class InjectionSample {
@Inject
InjectionSample(String name) {}
@PostConstruct
void postConstruct() {}
@PreDestroy
void preDestroy() {}
@Inject
InjectedType injectedField;
}generates metadata for the class
{
"condition": { "typeReached": "demo.InjectionSample" },
"type": "demo.InjectionSample",
"methods": [
{ "name": "<init>", "parameterTypes": ["java.lang.String"] },
{ "name": "postConstruct", "parameterTypes": [] },
{ "name": "preDestroy", "parameterTypes": [] }
],
"fields": [
{ "name": "injectedField" }
]
}as well as default constructors for the types of injected fields
{
"condition": { "typeReached": "demo.InjectionSample" },
"type": "demo.InjectedType",
"methods": [
{ "name": "<init>", "parameterTypes": [] }
]
}The metadata gets generated into the META-INF/native-image/reachability-generated/${project}/ directory. The ${project} name should be unique and needs to be set via a compiler argument. This is compatible with picocli-codegen.
The processor currently defaults to the older 1.0.0 format (separate *-config.json files) rather than the modern 1.2.0 format (combined reachability-metadata.json) for three reasons: (1) the modern format is not yet fully implemented, (2) the older format is still supported by all GraalVM versions, and (3) narrowed condition behavior: the older typeReachable condition triggers as soon as a type is included in the image, while the newer typeReached condition adds a runtime trigger that requires the class to be initialized. In practice this is too limiting for some use cases and can cause issues at runtime (e.g., when the lookup can run before the condition class gets initialized).
However, for most applications the difference does not matter, and it is likely that the default will change to the newer format in the future. We recommend always setting the format explicitly with the compiler option -Areachability.outputFormat=1.0.0 to avoid future surprises.
For example, a Maven configuration could look like this:
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<compilerArgs>
<arg>-Aproject=${project.groupId}/${project.artifactId}</arg>
<arg>-Areachability.outputFormat=1.0.0</arg> <!-- or 1.2.0 -->
</compilerArgs>
</configuration>
</plugin>Compiler Argument (-A) |
Values | Comment |
|---|---|---|
project |
directory |
Should be ${project.groupId}/${project.artifactId} |
reachability.outputFormat |
1.0.0 (default), 1.2.0, all |
The output format of the metadata |
reachability.mergeSteps |
true (default), false |
Whether the different processing steps should be merged into a single file. Mainly for debugging. |
reachability.processDependencyInjection |
false (default), true |
True enables metadata generation for @Inject annotations. |
You need add a compile-time dependency on the annotations, and add the annotation-processor to the list of executed annotation processors.
Note that starting with JDK23, javac no longer runs annotation processors by default. You can set the annotationProcessorPaths as shown below, or use <proc>full</proc> in the compiler plugin configuration (-proc:full).
<properties>
<reachability.version>1.0.0-RC4</reachability.version>
</properties>
<dependencies>
<dependency> <!-- compile time annotations -->
<groupId>us.hebi.graalvm</groupId>
<artifactId>reachability-annotations</artifactId>
<version>${reachability.version}</version>
<scope>provided</scope> <!-- not needed at runtime -->
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.13.0</version>
<configuration>
<annotationProcessorPaths>
<path> <!-- annotation processor -->
<groupId>us.hebi.graalvm</groupId>
<artifactId>reachability-processor</artifactId>
<version>${reachability.version}</version>
</path>
</annotationProcessorPaths>
<compilerArgs> <!-- unique identifier -->
<arg>-Aproject=${project.groupId}/${project.artifactId}</arg>
<arg>-Areachability.processDependencyInjection=true</arg>
<arg>-Areachability.outputFormat=1.0.0</arg>
</compilerArgs>
</configuration>
</plugin>
</plugins>
</build>mvn clean verifyThis project supports jitpack, so you can directly depend on specific commits. You can manually truncate the desired commit hash to the first 10 digits, or check jitpack for available versions.
- Enable jitpack releases in the pom.xml
<repositories>
<repository>
<id>jitpack.io</id>
<url>https://jitpack.io</url>
</repository>
</repositories>- Specify the version (first 10 digits of the commit hash)
<!-- Annotations -->
<dependency>
<groupId>com.github.HebiRobotics</groupId>
<artifactId>reachability-annotations</artifactId>
<version>${reachability.hash}</version>
<scope>provided</scope>
</dependency>
<!-- Annotation Processor -->
<dependency>
<groupId>com.github.HebiRobotics</groupId>
<artifactId>reachability-processor</artifactId>
<version>${reachability.hash}</version>
<scope>provided</scope>
</dependency>