Skip to content

Eliminate reflection for metadata provider discovery to improve startup time and native image support #477

@stalep

Description

@stalep

Summary

The _AeshMetadata classes generated by the annotation processor are currently discovered via MetadataProviderRegistry using Class.forName() with a naming convention (e.g., MyCommandMyCommand_AeshMetadata). This has two costs:

  1. Reflection overheadClass.forName() on every command class during registry build
  2. 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type
    No fields configured for issues without a type.

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions