Summary
The _AeshMetadata classes generated by the annotation processor are currently discovered via MetadataProviderRegistry using Class.forName() with a naming convention (e.g., MyCommand → MyCommand_AeshMetadata). This has two costs:
- Reflection overhead —
Class.forName() on every command class during registry build
- Native image configuration — Every generated metadata class needs an explicit entry in
reachability-metadata.json (jbang has 65 such entries)
Since startup time is critical for aesh (5.5x faster than picocli on native image), eliminating this reflection could further improve cold-start performance.
Current behavior
AeshCommandContainerBuilder.create(MyCommand.class)
→ MetadataProviderRegistry.lookup("MyCommand_AeshMetadata")
→ Class.forName("com.example.MyCommand_AeshMetadata") // reflection
→ provider.newInstance() // reflection
Each Class.forName() call involves classloader delegation, security checks, and on native image requires pre-registered reflection metadata.
Alternative approaches
Option 1: Java ServiceLoader (recommended)
The annotation processor generates a META-INF/services/org.aesh.command.CommandMetadataProvider file listing all metadata classes:
com.example.MyCommand_AeshMetadata
com.example.OtherCommand_AeshMetadata
Discovery becomes:
ServiceLoader.load(CommandMetadataProvider.class)
Advantages:
- Standard Java mechanism, well-understood
- GraalVM has built-in ServiceLoader support — auto-discovers services at build time without manual reflection entries
- Eliminates all 65+ reflection entries from native-image config
- Works with incremental compilation (processor appends to services file)
Considerations:
- Need to handle the services file across incremental and multi-module builds
- ServiceLoader loads all providers eagerly; may need a keyed lookup (provider reports which command class it handles) to avoid loading unused providers
- The processor would need to use
Filer.createResource() to generate the services file
Option 2: Generated static registry
The annotation processor generates a single registry class that directly instantiates all providers:
public class _AeshMetadataRegistry {
private static final Map<Class<?>, CommandMetadataProvider> PROVIDERS = new HashMap<>();
static {
PROVIDERS.put(MyCommand.class, new MyCommand_AeshMetadata());
PROVIDERS.put(OtherCommand.class, new OtherCommand_AeshMetadata());
}
public static CommandMetadataProvider get(Class<?> cmdClass) {
return PROVIDERS.get(cmdClass);
}
}
Advantages:
- Zero reflection — direct
new calls
- Single class to register in native-image config (if any)
- Fastest possible lookup (HashMap get)
- No ServiceLoader overhead
Disadvantages:
- Incremental compilation is harder — the registry must be regenerated when any command changes
- Multi-module builds need coordination (each module has its own registry)
- Tight coupling between the registry and all command classes
Option 3: Direct instantiation in generated code
Instead of looking up the metadata provider externally, each command class could have a static method or field:
// Generated by processor alongside MyCommand_AeshMetadata
public class MyCommand implements Command<CommandInvocation> {
// ...existing code...
// Added by processor via source generation or bytecode weaving
public static CommandMetadataProvider _aeshMetadata() {
return new MyCommand_AeshMetadata();
}
}
Then AeshCommandContainerBuilder calls MyCommand._aeshMetadata() directly.
Advantages:
- Zero reflection, zero ServiceLoader
- Natural per-class locality
- Works with incremental compilation
Disadvantages:
- Cannot add methods to user classes via annotation processing (would need a different approach like a companion class or interface)
- Could use a convention:
MetadataProviderRegistry first checks if the class implements a HasMetadataProvider interface before falling back to Class.forName()
Option 4: Hybrid — direct instantiation with reflection fallback
Keep the current Class.forName() approach as a fallback, but add a fast path:
public static CommandMetadataProvider lookup(Class<?> cmdClass) {
// Fast path: check if the metadata class is already on the classpath
String metadataClassName = cmdClass.getName() + "_AeshMetadata";
try {
// Use the classloader that loaded the command class
Class<?> metadataClass = cmdClass.getClassLoader().loadClass(metadataClassName);
return (CommandMetadataProvider) metadataClass.getConstructor().newInstance();
} catch (ClassNotFoundException e) {
// Fallback to reflection-based builder
return null;
}
}
This is essentially the current approach but makes the reflection more explicit. Not a real improvement.
Recommendation
Option 1 (ServiceLoader) is the best balance of simplicity, standards compliance, and native image support. It eliminates all reflection entries with minimal code change.
If startup time is the absolute priority and ServiceLoader's eager loading is a concern, Option 2 (static registry) is the fastest but harder to maintain across incremental builds.
Impact estimate
For jbang with 65 metadata classes:
- Eliminates 65
Class.forName() calls during startup
- Removes 65 lines from
reachability-metadata.json
- On native image, removes reflection initialization overhead for 65 classes
Context
Found during jbang picocli-to-aesh migration (jbangdev/jbang#2453). The _AeshMetadata reflection entries are the largest single category in jbang's native-image configuration.
Summary
The
_AeshMetadataclasses generated by the annotation processor are currently discovered viaMetadataProviderRegistryusingClass.forName()with a naming convention (e.g.,MyCommand→MyCommand_AeshMetadata). This has two costs:Class.forName()on every command class during registry buildreachability-metadata.json(jbang has 65 such entries)Since startup time is critical for aesh (5.5x faster than picocli on native image), eliminating this reflection could further improve cold-start performance.
Current behavior
Each
Class.forName()call involves classloader delegation, security checks, and on native image requires pre-registered reflection metadata.Alternative approaches
Option 1: Java ServiceLoader (recommended)
The annotation processor generates a
META-INF/services/org.aesh.command.CommandMetadataProviderfile listing all metadata classes:Discovery becomes:
Advantages:
Considerations:
Filer.createResource()to generate the services fileOption 2: Generated static registry
The annotation processor generates a single registry class that directly instantiates all providers:
Advantages:
newcallsDisadvantages:
Option 3: Direct instantiation in generated code
Instead of looking up the metadata provider externally, each command class could have a static method or field:
Then
AeshCommandContainerBuildercallsMyCommand._aeshMetadata()directly.Advantages:
Disadvantages:
MetadataProviderRegistryfirst checks if the class implements aHasMetadataProviderinterface before falling back toClass.forName()Option 4: Hybrid — direct instantiation with reflection fallback
Keep the current
Class.forName()approach as a fallback, but add a fast path:This is essentially the current approach but makes the reflection more explicit. Not a real improvement.
Recommendation
Option 1 (ServiceLoader) is the best balance of simplicity, standards compliance, and native image support. It eliminates all reflection entries with minimal code change.
If startup time is the absolute priority and ServiceLoader's eager loading is a concern, Option 2 (static registry) is the fastest but harder to maintain across incremental builds.
Impact estimate
For jbang with 65 metadata classes:
Class.forName()calls during startupreachability-metadata.jsonContext
Found during jbang picocli-to-aesh migration (jbangdev/jbang#2453). The
_AeshMetadatareflection entries are the largest single category in jbang's native-image configuration.