When you hear "Java native compilation with GraalVM," you probably think of configuration hell — endless JSON metadata files, trial-and-error reflection registrations, and build scripts that take more effort than the application itself.

Solon takes a different path. Its AOT pipeline compiles in three phases, where each phase feeds into the next. The framework's "restraint" philosophy — minimal dynamic proxies, explicit configuration over magic — makes native compilation more straightforward than you'd expect.

Let me walk through how it works, what you get, and where you might need to reach for the manual override.


The Three-Phase Pipeline

Solon Native compilation is three distinct stages, each independently runnable:

Phase What it does Maven command JDK
1 Standard Java compilation mvn clean -DskipTests=true package jdk8+
2 Solon AOT processing mvn clean -DskipTests=true -P aot package jdk8+ (any JDK)
3 GraalVM native-image mvn clean -DskipTests=true -P native native:compile graalvm jdk17+

Phase 2 is where Solon does the heavy lifting.


Phase 2 Deep Dive: Solon AOT

The Solon AOT processor (SolonAotProcessor from the solon-aot module) runs automatically after mvn package when you use the -P aot profile. What does it do?

It bootstraps your application at compile time, runs through the natural startup flow, and captures everything the framework needs to work without runtime reflection:

  • Proxy class pre-compilation — Dynamic AOP proxies that would normally be generated via ASM bytecode at runtime are compiled to real .class files at build time
  • Class index generation — If your project has many classes, this index accelerates startup by avoiding classpath scanning
  • GraalVM metadata generation — Reflection, resource, and serialization metadata are written in GraalVM's native configuration format automatically

The key detail: since v3.7.2, you can run -P aot with any JDK (not just GraalVM). The -P aot profile strips out the GraalVM build tools dependency, making AOT processing a lightweight optimization pass available to everyone — even if you're still on JDK 8.

When you're ready for the full native binary, -P native includes the GraalVM native-maven-plugin, which requires a GraalVM JDK.


What You Gain

Building a native executable with Solon gives you:

  • Startup in milliseconds — No JVM warmup, no class loading tax
  • Memory between JVM and Go — GraalVM native images strip out everything unused
  • Standalone binary — No JRE required, no java -jar. Just ./myapp

These benefits are especially meaningful for serverless functions, containerized microservices, and CLI tools.


The Realities: What Doesn't Work in Native

GraalVM native images impose constraints that every framework must handle. Here's what Solon's AOT pipeline automatically manages:

Constraint How Solon handles it
All reflection must be pre-registered Solon AOT bootstraps the app, captures reflection usage, and generates the config
All resource files must be pre-registered Same — auto-collected during the AOT bootstrap phase
No runtime classpath scanning Use ResourceUtil.scanResources() instead — it reads from the pre-registered resource index
No dynamic compilation Use expression engines (SnEL) or script tools instead of compiling code on the fly
No ASM bytecode generation Solon AOT pre-compiles proxy classes during Phase 2

Manual Override: RuntimeNativeRegistrar

No automatic system is perfect. Third-party libraries that use reflection or load resources outside of Solon's managed scope need manual registration.

Solon provides the RuntimeNativeRegistrar interface — a single @Component bean that gives you full control over what gets registered:

@Component
public class NativeRegistrar implements RuntimeNativeRegistrar {
    @Override
    public void register(AppContext ctx, RuntimeNativeMetadata metadata) {
        // Register resource files that aren't auto-detected
        metadata.registerResourceInclude("com/mysql/jdbc/LocalizedErrorMessages.properties");

        // Register classes for serialization
        metadata.registerSerialization(JsonResult.class);

        // Register reflection access with specific member categories
        metadata.registerReflection(BufferedImage.class, 
            MemberCategory.INVOKE_DECLARED_CONSTRUCTORS,
            MemberCategory.INVOKE_DECLARED_METHODS);
    }
}

Enter fullscreen mode Exit fullscreen mode

Third-party libraries typically fall into four categories:

  • A: Not supported — frameworks with dynamic compilation or bytecode manipulation (e.g., CGLIB proxies). These won't work.
  • B: Supported with significant config — like mysql-connector-java 5.x. Plan for extra registration work.
  • C: Supported with minor config — like mysql-connector-java 8.x. One or two resource registrations.
  • D: Fully supported — libraries that ship their own native-image metadata.

The Solon team maintains a working example with nginxWebUI — a real-world project that took about half a day to fully adapt.


Utility Tools for Native-Aware Code

When writing code that might run both on JVM and native, three utility classes help:

// Check the runtime environment
if (NativeDetector.inNativeImage()) {
    // Use ResourceUtil instead of ClassLoader.getResources()
}

// Native-compatible resource scanning
ResourceUtil scanUtil = ...;

// Native-compatible reflection
ReflectUtil.getMethods(myClass);

Enter fullscreen mode Exit fullscreen mode

These tools are transparent — they delegate to standard Java reflection on JVM, and use registered metadata in native images.


Quick Start: Getting a Native Binary

If you want to try this yourself, the steps are minimal:

  1. Add the dependencysolon-aot (org.noear, managed version)
  2. Install GraalVM — JDK 17, 21, or 25; run gu install native-image
  3. Single-module project: mvn clean -P native native:compile -DskipTests
  4. Multi-module project: mvn install all modules first, then run the main module with -P native native:compile

That's it. The generated binary lives in target/ — no JRE, just run it.


The Honest Trade-offs

Solon's AOT pipeline is pragmatic, not magical. Three things to keep in mind:

  1. First compilation is slow — GraalVM native-image has to analyze the entire closed world. Expect minutes, not seconds.
  2. Not every library works — If a dependency uses dynamic class loading or bytecode generation, you'll need to either replace it or skip native compilation.
  3. -P aot does nothing for small apps — On a minimal project with ~10 classes, the startup improvement is negligible (the docs note this with the native-example project on a 2020 MacBook Pro).

Solon's three-phase approach to native compilation is honest about constraints while automating as much as possible. The -P aot profile — usable on any JDK — lets you get the metadata generation benefits without committing to a full native binary.

If you've been avoiding GraalVM because of configuration overhead, Solon's pipeline removes most of the friction. The escape hatch is there when you need it, but you won't need it often.


Solon AOT & Native docs: https://solon.noear.org/article/learn-solon-native
solon-aot Maven: org.noear:solon-aot
Example project: https://gitee.com/noear/solon-native-example