Skip to content

Commit c3d184f

Browse files
committed
Add annotation processor documentation
New page documenting the aesh-processor module: installation, how it works, generated code examples, compile-time validation, group commands, class hierarchies, and GraalVM benefits.
1 parent 8b123f2 commit c3d184f

4 files changed

Lines changed: 231 additions & 1 deletion

File tree

content/docs/aesh/_index.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,11 @@ That's it! Æsh automatically:
7979
- **Renderers** - Customize help output format
8080
- **Custom parsers** - Override default parsing behavior
8181

82+
### Performance & Native Images
83+
- **[Compile-time annotation processor](annotation-processor)** - Optional `aesh-processor` module generates command metadata at build time, eliminating runtime reflection
84+
- **GraalVM native-image friendly** - Generated metadata uses direct `new` calls instead of reflection
85+
- **Dual-mode** - Falls back to reflection automatically if the processor is not used
86+
8287
### Execution Modes
8388
- **Console mode** - Interactive shell with command history and editing (via `AeshConsoleRunner`)
8489
- **Runtime mode** - Single command execution for CLI tools (via `AeshRuntimeRunner`)

content/docs/aesh/advanced-topics.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -203,6 +203,8 @@ class ParameterizedCommandTest {
203203

204204
Æsh applications can be compiled to native executables using GraalVM, providing fast startup and low memory footprint.
205205

206+
> **Tip:** Adding the [Annotation Processor](../annotation-processor) (`aesh-processor`) significantly simplifies GraalVM native images. The generated metadata uses direct `new` calls instead of reflection, reducing the amount of reflection configuration you need to maintain.
207+
206208
### Adding GraalVM Support
207209

208210
Add the GraalVM native-image Maven plugin:
@@ -233,7 +235,9 @@ Add the GraalVM native-image Maven plugin:
233235

234236
### Reflection Configuration
235237

236-
Æsh uses reflection for command discovery. Create a reflection configuration file at `src/main/resources/META-INF/native-image/reflect-config.json`:
238+
Æsh uses reflection for command discovery by default. If you are using the [Annotation Processor](../annotation-processor), most of this reflection is eliminated and you may need significantly fewer entries here (primarily just for field injection, which still uses reflection).
239+
240+
Without the annotation processor, create a reflection configuration file at `src/main/resources/META-INF/native-image/reflect-config.json`:
237241

238242
```json
239243
[
Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
---
2+
date: '2026-03-12T12:00:00+01:00'
3+
draft: false
4+
title: 'Annotation Processor'
5+
weight: 18
6+
---
7+
8+
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.
9+
10+
## Why Use It?
11+
12+
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:
13+
14+
- **Faster startup** -- No annotation scanning or reflective instantiation at runtime
15+
- **Lower memory usage** -- No reflection metadata retained in memory
16+
- **GraalVM native-image friendly** -- Generated code uses direct `new` calls instead of reflection, reducing the need for reflection configuration
17+
- **Zero behavior change** -- Existing users are unaffected; the processor is fully optional
18+
19+
## Installation
20+
21+
Add `aesh-processor` as a `provided` dependency. The Java compiler will automatically discover and run the processor during compilation.
22+
23+
### Maven
24+
25+
```xml
26+
<dependency>
27+
<groupId>org.aesh</groupId>
28+
<artifactId>aesh-processor</artifactId>
29+
<version>${aesh.version}</version>
30+
<scope>provided</scope>
31+
</dependency>
32+
```
33+
34+
### Gradle
35+
36+
```groovy
37+
dependencies {
38+
annotationProcessor 'org.aesh:aesh-processor:${aeshVersion}'
39+
compileOnly 'org.aesh:aesh-processor:${aeshVersion}'
40+
}
41+
```
42+
43+
## How It Works
44+
45+
The processor follows a **dual-mode** approach:
46+
47+
1. At compile time, the processor scans classes annotated with `@CommandDefinition` or `@GroupCommandDefinition` and generates a `_AeshMetadata` class for each command
48+
2. The generated classes implement `CommandMetadataProvider` and are registered via `META-INF/services` (ServiceLoader)
49+
3. At runtime, when Aesh registers a command, it first checks if a generated provider exists for that command class
50+
4. If a provider is found, it uses the generated metadata (no reflection). If not, it falls back to the existing reflection-based path
51+
52+
```
53+
Compile Time Runtime
54+
┌─────────────────────┐
55+
│ @CommandDefinition │
56+
│ MyCommand.java │
57+
└─────────┬───────────┘
58+
│ annotation processor
59+
60+
┌─────────────────────────┐ ┌───────────────────────┐
61+
│ MyCommand_AeshMetadata │ ──▶ │ ServiceLoader lookup │
62+
│ (generated source) │ │ Provider found? ──Yes──▶ Use generated metadata
63+
└─────────────────────────┘ │ ──No───▶ Reflection fallback
64+
└───────────────────────┘
65+
```
66+
67+
### What Gets Eliminated
68+
69+
| Reflection operation | With processor |
70+
|---|---|
71+
| Annotation scanning (`getAnnotation`, `getDeclaredFields`) | Replaced by compile-time literals |
72+
| Instance creation for validators, converters, completers, activators, renderers, result handlers | Replaced by direct `new X()` calls |
73+
| Command instantiation via `ReflectionUtil.newInstance()` | Replaced by `new MyCommand()` |
74+
| Field injection (`field.set`) | Still uses reflection (fields are private) |
75+
76+
## Generated Code
77+
78+
For a command like:
79+
80+
```java
81+
@CommandDefinition(name = "build", description = "Run build")
82+
public class BuildCommand implements Command<CommandInvocation> {
83+
@Option(shortName = 'v', description = "Verbose")
84+
private boolean verbose;
85+
86+
@Option(name = "output", required = true)
87+
private String outputFile;
88+
89+
@Argument(description = "Source")
90+
private String source;
91+
92+
@Override
93+
public CommandResult execute(CommandInvocation invocation) {
94+
return CommandResult.SUCCESS;
95+
}
96+
}
97+
```
98+
99+
The processor generates `BuildCommand_AeshMetadata` in the same package:
100+
101+
```java
102+
public final class BuildCommand_AeshMetadata
103+
implements CommandMetadataProvider<BuildCommand> {
104+
105+
public Class<BuildCommand> commandType() {
106+
return BuildCommand.class;
107+
}
108+
109+
public BuildCommand newInstance() {
110+
return new BuildCommand(); // no reflection
111+
}
112+
113+
public boolean isGroupCommand() { return false; }
114+
115+
public Class<? extends Command>[] groupCommandClasses() {
116+
return new Class[0];
117+
}
118+
119+
public ProcessedCommand buildProcessedCommand(BuildCommand instance)
120+
throws CommandLineParserException {
121+
ProcessedCommand processedCommand = ProcessedCommandBuilder.builder()
122+
.name("build")
123+
.description("Run build")
124+
.command(instance)
125+
.create();
126+
127+
processedCommand.addOption(
128+
ProcessedOptionBuilder.builder()
129+
.shortName('v')
130+
.name("verbose")
131+
.description("Verbose")
132+
.type(boolean.class)
133+
.fieldName("verbose")
134+
.optionType(OptionType.BOOLEAN)
135+
.completer(new BooleanOptionCompleter())
136+
// ... other literal values
137+
.build());
138+
139+
// ... more options and argument
140+
141+
return processedCommand;
142+
}
143+
}
144+
```
145+
146+
The generated code uses the same `ProcessedCommandBuilder` and `ProcessedOptionBuilder` APIs that the reflection path uses, ensuring identical behavior.
147+
148+
## Compile-Time Validation
149+
150+
The processor validates commands at compile time and reports errors as compiler errors:
151+
152+
- Command class must not be abstract
153+
- Command class must implement `Command`
154+
- Command class must have an accessible no-arg constructor
155+
- `@OptionList` and `@Arguments` fields must be `Collection` subtypes
156+
- `@OptionGroup` fields must be `Map` subtypes
157+
158+
These checks catch errors that would otherwise only surface at runtime.
159+
160+
## Group Commands
161+
162+
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.
163+
164+
```java
165+
@GroupCommandDefinition(name = "remote", description = "Manage remotes",
166+
groupCommands = {RemoteAddCommand.class, RemoteRemoveCommand.class})
167+
public class RemoteCommand implements Command<CommandInvocation> {
168+
// ...
169+
}
170+
```
171+
172+
## Class Hierarchies
173+
174+
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.
175+
176+
```java
177+
public abstract class BaseCommand implements Command<CommandInvocation> {
178+
@Option(description = "Enable debug mode", hasValue = false)
179+
private boolean debug;
180+
}
181+
182+
@CommandDefinition(name = "deploy", description = "Deploy application")
183+
public class DeployCommand extends BaseCommand {
184+
@Option(description = "Target environment")
185+
private String environment;
186+
// Generated metadata includes both 'debug' and 'environment'
187+
}
188+
```
189+
190+
## Compatibility
191+
192+
- **Java 8+** -- The processor targets source version 8
193+
- **No code changes required** -- Drop in the dependency and the processor runs automatically
194+
- **Fully backward compatible** -- Removing the dependency reverts to the reflection path with no behavior change
195+
- **Works with all Aesh features** -- Custom validators, completers, converters, activators, renderers, result handlers, and option parsers are all supported

content/docs/aesh/installation.md

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,32 @@ dependencies {
2727
}
2828
```
2929

30+
## Annotation Processor (Optional)
31+
32+
To generate command metadata at compile time (faster startup, less reflection, GraalVM-friendly), add the `aesh-processor` dependency. See [Annotation Processor](../annotation-processor) for details.
33+
34+
### Maven
35+
36+
```xml
37+
<dependency>
38+
<groupId>org.aesh</groupId>
39+
<artifactId>aesh-processor</artifactId>
40+
<version>1.7</version>
41+
<scope>provided</scope>
42+
</dependency>
43+
```
44+
45+
### Gradle
46+
47+
```groovy
48+
dependencies {
49+
annotationProcessor 'org.aesh:aesh-processor:1.7'
50+
compileOnly 'org.aesh:aesh-processor:1.7'
51+
}
52+
```
53+
54+
No code changes are required -- Aesh automatically detects and uses the generated metadata when available.
55+
3056
## Build from Source
3157

3258
Clone the repository and build with Maven:

0 commit comments

Comments
 (0)