Annotation Processor
The aesh-processor module provides a compile-time annotation processor that generates command metadata at build time, eliminating runtime reflection for annotation scanning and object instantiation.
Why Use It?
By default, Aesh uses runtime reflection to scan @CommandDefinition, @Option, @Argument and other annotations every time a command is registered. The annotation processor shifts this work to compile time:
- Faster startup – No annotation scanning or reflective instantiation at runtime
- Lower memory usage – No reflection metadata retained in memory
- GraalVM native-image friendly – Generated code uses direct
newcalls instead of reflection, reducing the need for reflection configuration - Zero behavior change – Existing users are unaffected; the processor is fully optional
Installation
Add aesh-processor as a provided dependency. The Java compiler will automatically discover and run the processor during compilation.
Maven
<dependency>
<groupId>org.aesh</groupId>
<artifactId>aesh-processor</artifactId>
<version>${aesh.version}</version>
<scope>provided</scope>
</dependency>Gradle
dependencies {
annotationProcessor 'org.aesh:aesh-processor:${aeshVersion}'
compileOnly 'org.aesh:aesh-processor:${aeshVersion}'
}How It Works
The processor follows a dual-mode approach:
- At compile time, the processor scans classes annotated with
@CommandDefinitionor@GroupCommandDefinitionand generates a_AeshMetadataclass for each command - The generated classes implement
CommandMetadataProviderand are registered viaMETA-INF/services(ServiceLoader) - At runtime, when Aesh registers a command, it first checks if a generated provider exists for that command class
- If a provider is found, it uses the generated metadata (no reflection). If not, it falls back to the existing reflection-based path
Compile Time Runtime
┌─────────────────────┐
│ @CommandDefinition │
│ MyCommand.java │
└─────────┬───────────┘
│ annotation processor
▼
┌─────────────────────────┐ ┌───────────────────────┐
│ MyCommand_AeshMetadata │ ──▶ │ ServiceLoader lookup │
│ (generated source) │ │ Provider found? ──Yes──▶ Use generated metadata
└─────────────────────────┘ │ ──No───▶ Reflection fallback
└───────────────────────┘What Gets Eliminated
| Reflection operation | With processor |
|---|---|
Annotation scanning (getAnnotation, getDeclaredFields) | Replaced by compile-time literals |
| Instance creation for validators, converters, completers, activators, renderers, result handlers | Replaced by direct new X() calls |
Command instantiation via ReflectionUtil.newInstance() | Replaced by new MyCommand() |
Field injection (field.set) | Still uses reflection (fields are private) |
Generated Code
For a command like:
@CommandDefinition(name = "build", description = "Run build")
public class BuildCommand implements Command<CommandInvocation> {
@Option(shortName = 'v', description = "Verbose")
private boolean verbose;
@Option(name = "output", required = true)
private String outputFile;
@Argument(description = "Source")
private String source;
@Override
public CommandResult execute(CommandInvocation invocation) {
return CommandResult.SUCCESS;
}
}The processor generates BuildCommand_AeshMetadata in the same package:
public final class BuildCommand_AeshMetadata
implements CommandMetadataProvider<BuildCommand> {
public Class<BuildCommand> commandType() {
return BuildCommand.class;
}
public BuildCommand newInstance() {
return new BuildCommand(); // no reflection
}
public boolean isGroupCommand() { return false; }
public Class<? extends Command>[] groupCommandClasses() {
return new Class[0];
}
public ProcessedCommand buildProcessedCommand(BuildCommand instance)
throws CommandLineParserException {
ProcessedCommand processedCommand = ProcessedCommandBuilder.builder()
.name("build")
.description("Run build")
.command(instance)
.create();
processedCommand.addOption(
ProcessedOptionBuilder.builder()
.shortName('v')
.name("verbose")
.description("Verbose")
.type(boolean.class)
.fieldName("verbose")
.optionType(OptionType.BOOLEAN)
.completer(new BooleanOptionCompleter())
// ... other literal values
.build());
// ... more options and argument
return processedCommand;
}
}The generated code uses the same ProcessedCommandBuilder and ProcessedOptionBuilder APIs that the reflection path uses, ensuring identical behavior.
Compile-Time Validation
The processor validates commands at compile time and reports errors as compiler errors:
- Command class must not be abstract
- Command class must implement
Command - Command class must have an accessible no-arg constructor
@OptionListand@Argumentsfields must beCollectionsubtypes@OptionGroupfields must beMapsubtypes
These checks catch errors that would otherwise only surface at runtime.
Group Commands
Group commands with @GroupCommandDefinition are fully supported. The processor generates metadata for the parent command and records its subcommand classes. At runtime, each subcommand is resolved through its own provider (if available) or via the reflection fallback.
@GroupCommandDefinition(name = "remote", description = "Manage remotes",
groupCommands = {RemoteAddCommand.class, RemoteRemoveCommand.class})
public class RemoteCommand implements Command<CommandInvocation> {
// ...
}Class Hierarchies
The processor walks the full class hierarchy, collecting annotated fields from superclasses. If your commands extend a base class with shared options, those options are included in the generated metadata.
public abstract class BaseCommand implements Command<CommandInvocation> {
@Option(description = "Enable debug mode", hasValue = false)
private boolean debug;
}
@CommandDefinition(name = "deploy", description = "Deploy application")
public class DeployCommand extends BaseCommand {
@Option(description = "Target environment")
private String environment;
// Generated metadata includes both 'debug' and 'environment'
}Compatibility
- Java 8+ – The processor targets source version 8
- No code changes required – Drop in the dependency and the processor runs automatically
- Fully backward compatible – Removing the dependency reverts to the reflection path with no behavior change
- Works with all Aesh features – Custom validators, completers, converters, activators, renderers, result handlers, and option parsers are all supported