DEV Community

Solon Framework
Solon Framework

Posted on

How Solon Bypasses Heavy Classpath Scanning in GraalVM Native Image using IndexFiles

Classpath scanning is a ubiquitous practice in modern Java frameworks. When your application starts, the framework scans JARs, reads directories, and looks for classes annotated with @Component, @Controller, or XML mapper files.

While this dynamic approach works wonderfully on standard JVMs, it poses a severe bottleneck for GraalVM Native Images. The closed-world assumption of GraalVM mandates that all classes, reflections, and resources must be known at compile time. Furthermore, traditional classpath scanning (ClassLoader.getResources(...)) is either highly restricted or incredibly slow in a compiled native binary.

In this article, we'll dive deep into Solon's AOT (Ahead-of-Time) compilation engine and see how it solves classpath scanning under GraalVM Native Image using a lightweight, elegant mechanism called IndexFiles.


The GraalVM Native Image Dilemma

To compile a Java application into a standalone executable, GraalVM needs to build a static dependency graph starting from the main entry point. Any class accessed via reflection, any dynamic proxy, and any resource file must be registered beforehand in JSON configuration files:

  • reflect-config.json
  • resource-config.json
  • serialization-config.json
  • proxy-config.json

If your framework relies on scanning directory trees inside JAR files at runtime to discover components, it will fail under Native Image—there are no JAR files at runtime!

While some frameworks solve this by generating massive amounts of source code or bytecode during compilation, Solon takes a cleaner, more runtime-friendly approach: IndexFiles.


Introducing Solon's IndexFiles

Solon introduces org.noear.solon.core.runtime.IndexFiles, a internal helper class designed to record scans during build time and substitute them with static indexes at runtime.

The core idea is simple:

  1. At AOT Compilation Time: Run the Solon container in a special AOT-processing mode to intercept and record all classpath scans.
  2. Write Flat Index Files: Save these scan results into META-INF/solon-index/ as simple .index text files.
  3. At Runtime: Bypass expensive scanning. Read the .index files directly and load the recorded classes or resources instantly.

This mechanism applies to both class scanning (discovering beans) and resource scanning (finding templates, config files, or RPC descriptors).


How It Works: Step-by-Step

Let's look at the underlying implementation details.

1. The AOT Flag Interceptor

During Solon's AOT phase, the maven/gradle plugin triggers the main execution via org.noear.solon.aot.SolonAotProcessor. The processor sets a crucial system property:

System.setProperty(NativeDetector.AOT_PROCESSING, "true");
Enter fullscreen mode Exit fullscreen mode

This tells the container that it is running in build-time AOT pre-processing mode.

2. Intercepting Class Scanning

When Solon scans for beans, it uses ClassUtil.scanClasses(clzExpr). Let's look at how it behaves under the hood:

public static void scanClasses(ClassLoader classLoader, String clzExpr, Consumer<Class<?>> clzConsumer) {
    if (NativeDetector.isAotRuntime()) {
        // Build-time AOT Processing
        List<String> clzNames = new ArrayList<>();
        doScanClasses0(classLoader, clzExpr, filter, (name, clz) -> {
            clzNames.add(name);
            clzConsumer.accept(clz);
        });
        // Write the recorded scan results to an index file
        IndexFiles.writeIndexFile(clzExpr, "scan_clz", clzNames);
    } else {
        // Standard JVM or Native Image Runtime
        Collection<String> clzNames = IndexFiles.loadIndexFile(clzExpr, "scan_clz");
        if (clzNames != null) {
            // Index exists! Directly load classes from the pre-recorded index
            for (String clzName : clzNames) {
                Class<?> clz = ClassUtil.loadClass(classLoader, clzName);
                if (clz != null) {
                    clzConsumer.accept(clz);
                }
            }
            return;
        }
        // Fallback to slow scan if no index exists
        doScanClasses0(classLoader, clzExpr, filter, (name, clz) -> clzConsumer.accept(clz));
    }
}
Enter fullscreen mode Exit fullscreen mode

By substituting raw scanning with a pre-built index, Solon bypasses JAR scanning entirely!

3. Intercepting Resource Scanning

The exact same logic applies to resource scanning via ResourceUtil.scanResources:

public static Collection<String> scanResources(ClassLoader classLoader, String resExpr) {
    if (NativeDetector.isAotRuntime()) {
        List<String> resList = new ArrayList<>();
        scanResources(classLoader, resExpr, resList::add);
        // Write to an index file, e.g., mapping to a "scan_res" tag
        IndexFiles.writeIndexFile(resExpr, "scan_res", resList);
        return resList;
    } else {
        List<String> resList = IndexFiles.loadIndexFile(resExpr, "scan_res");
        if (resList == null) {
            resList = new ArrayList<>();
            scanResources(classLoader, resExpr, resList::add);
        }
        return resList;
    }
}
Enter fullscreen mode Exit fullscreen mode

Anatomy of an Index File

How does Solon serialize these expressions into filenames? Filesystem paths have strict character limitations, whereas package patterns or scan expressions contain wildcard asterisks (*) and colons (:).

IndexFiles.getIndexFileName safely sanitizes expressions:

  • Dots (.), slashes (/), and backslashes (\) are replaced with hyphens (-).
  • Asterisks (*) are replaced with at-signs (@).
  • Colons (:) are replaced with exclamation marks (!).
  • The filename is post-fixed with _{tag}.index.

For example, a scan expression like classpath:demo/**/*.json (mapped to tag scan_res) will generate an index file located at:
META-INF/solon-index/classpath!demo--@@-@.json_scan_res.index

Inside the file is a simple list of matching resources:

demo/config/db.json
demo/static/data.json
Enter fullscreen mode Exit fullscreen mode

When running in GraalVM Native Image, ResourceUtil simply opens this text file, reads the lines, and returns the resources instantly.


Seamless GraalVM Integration

Of course, recording these files isn't enough; GraalVM needs to build these resources and reflected classes into the binary. solon-aot coordinates this.

During AOT compilation:

  1. SolonAotProcessor invokes addResourceConfig(metadata). It automatically registers standard directories (static/.*, templates/.*, META-INF/.*) and saves the pre-scanned resource paths to solon-resource.json.
  2. It generates resource-config.json listing the generated .index files.
  3. It generates reflect-config.json registering the default constructors of all components recorded in scan_clz.index so GraalVM doesn't strip them away.

Solon Native Customization: RuntimeNativeRegistrar

Sometimes, third-party libraries perform dynamic reflections or resources scans that Solon's automated AOT processor cannot intercept.

For these cases, Solon provides the RuntimeNativeRegistrar interface. You can declare a custom component to manually register resources or reflections:

@Component
public class MyNativeRegistrar implements RuntimeNativeRegistrar {
    @Override
    public void register(AppContext context, RuntimeNativeMetadata metadata) {
        // Manually register some resources
        metadata.registerResourceInclude("my-custom-config.xml");

        // Manually register classes for serialization
        metadata.registerSerialization(MyDataTransferObject.class);

        // Register class for reflection (constructors + methods)
        metadata.registerReflection(MyLegacyService.class, 
            MemberCategory.INVOKE_DECLARED_CONSTRUCTORS, 
            MemberCategory.INVOKE_DECLARED_METHODS
        );
    }
}
Enter fullscreen mode Exit fullscreen mode

This bean is automatically collected during AOT compilation and injected into the GraalVM metadata compilation suite.


Summary: Performance Benefits

By substituting runtime Classpath/JAR scanning with compile-time index generation, Solon achieves:

  1. Near-Zero Scanning Overhead: No CPU cycles are wasted traversing JAR directories during startup.
  2. GraalVM native compatibility: Seamlessly translates classpath wildcard scans (classpath:com/demo/**/*.class) into predictable, static lookups that satisfy GraalVM's closed-world restrictions.
  3. Incredibly Fast Startup: Solon applications typically boot in 0.1 to 0.2 seconds on standard JVMs, and under 2 to 5 milliseconds as compiled GraalVM native binaries.

Solon's AOT compiler demonstrates that achieving GraalVM native support doesn't require rewriting your code or producing messy generated classes—sometimes, a simple index is all you need to keep things clean, simple, and blazing fast.

Top comments (0)