Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions .github/workflows/native-image-dynamic-class-loading.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
name: native-image/dynamic-class-loading
on:
push:
paths:
- 'native-image/dynamic-class-loading/**'
- '.github/workflows/native-image-dynamic-class-loading.yml'
pull_request:
paths:
- 'native-image/dynamic-class-loading/**'
- '.github/workflows/native-image-dynamic-class-loading.yml'
schedule:
- cron: "0 0 1 * *" # run every month
workflow_dispatch:
permissions:
contents: read
jobs:
run:
name: Run 'native-image/dynamic-class-loading'
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@v6
- uses: graalvm/setup-graalvm@v1
with:
java-version: 'latest-ea'
distribution: 'graalvm'
github-token: ${{ secrets.GITHUB_TOKEN }}
cache: 'maven'
native-image-job-reports: 'true'
- name: Run 'native-image/dynamic-class-loading'
run: |
set -euo pipefail
cd native-image/dynamic-class-loading

mvn --no-transfer-progress clean -Pdynamic package

output=$(./target/micronaut-bytebuddy-dynamic conference)
echo "$output"
grep -Fq 'Generated class: demo.crema.generated.ConferencePlugin1' <<< "$output"
grep -Fq "hello from Alina at Devoxx London! Let's talk about what's latest and greatest in GraalVM :)" <<< "$output"
3 changes: 2 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,7 @@ Demos for building native images, including configurations and setup steps for v
* [list-files](native-image/list-files/) - Shows how to create a native executable from the command line, and then apply Profile-Guided Optimization (PGO)
* [native-build-tools](native-image/native-build-tools/) - Contains two Java projects, and shows how to create native executables from those applications using [Maven](https://graalvm.github.io/native-build-tools/latest/maven-plugin.html) and [Gradle](https://graalvm.github.io/native-build-tools/latest/gradle-plugin.html) plugins for GraalVM Native Image
* [preserve-package](native-image/preserve-package/) - Demonstrates how to use the `-H:Preserve` option to include all classes from a package in a native image, eliminating the need for JSON metadata configuration.
* [dynamic-class-loading](native-image/dynamic-class-loading/) - Demonstrates how a Micronaut native executable uses Byte Buddy to generate and load a new class at run time

### Configure
Demos illustrating how to compile applications with Native Image that use some dynamic Java features including reflection, resource access, and so on.
Expand Down Expand Up @@ -112,4 +113,4 @@ Legacy or blog-related demos, as well as examples involving polyglot capabilitie

## License

Unless specified otherwise, all code in this repository is licensed under the [Universal Permissive License (UPL)](http://opensource.org/licenses/UPL).
Unless specified otherwise, all code in this repository is licensed under the [Universal Permissive License (UPL)](http://opensource.org/licenses/UPL).
72 changes: 72 additions & 0 deletions native-image/dynamic-class-loading/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
# Runtime Class Loading with Micronaut and Byte Buddy

This example shows how to compile a [Micronaut](https://micronaut.io/) application that uses [Byte Buddy](https://bytebuddy.net/) with [GraalVM Native Image](https://www.graalvm.org/latest/reference-manual/native-image/). It showcases the recently released [Runtime Class Loading](https://github.com/oracle/graal/issues/11327) feature of Native Image.

The classes generated by Byte Buddy do not exist during the Native Image build. After the native executable starts, Byte Buddy defines a new `demo.crema.generated.ConferencePlugin...` class, the runtime loads it dynamically, and the application invokes its `message()` method through a method handle.

## Preparation

1. Install GraalVM 25.1+ and Maven, and make GraalVM your active JDK.

2. Clone this repository and navigate to the example directory:
```shell
git clone https://github.com/graalvm/graalvm-demos.git
cd graalvm-demos/native-image/dynamic-class-loading
```

3. Confirm that Runtime Class Loading is available:
```shell
java --version
native-image --expert-options-all | grep RuntimeClassLoading
```

## Run the Application on the JVM

1. Run the tests:
```shell
mvn clean test
```

2. Load the default conference plugin:
```shell
mvn -q compile exec:java -Dexec.args="plugins/conference.properties"
```

The output includes the generated class name and the greeting from `plugins/conference.properties`.

## Build and Run a Native Executable

1. Build with the `dynamic` profile. It enables `-H:+RuntimeClassLoading`:
```shell
mvn clean -Pdynamic package
```

2. Run the native executable:
```shell
./target/micronaut-bytebuddy-dynamic conference
```

You should see `demo.crema.generated.ConferencePlugin1`, proving that Byte Buddy generated and loaded the class at run time.

## Edit the Plugin and Run Again

1. Edit `plugins/conference.properties` and change `name`, `location`, or `speaker`.

2. Run the demo again:
```shell
./target/micronaut-bytebuddy-dynamic conference
```

Each run reads the updated plugin file, generates and loads the class at run time, and prints the new greeting. Edit the file and rerun the native executable as often as you like—there is no need to rebuild it.

To load another plugin file, run:

```shell
./target/micronaut-bytebuddy-dynamic path/to/plugin.properties
```

## Related Documentation

- [GraalVM Native Image](https://www.graalvm.org/latest/reference-manual/native-image/)
- [Micronaut](https://docs.micronaut.io/latest/guide/)
- [Project Crema](https://github.com/oracle/graal/issues/11327)
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
name=Devoxx
location=London
speaker=Alina
127 changes: 127 additions & 0 deletions native-image/dynamic-class-loading/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,127 @@
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<parent>
<groupId>io.micronaut.platform</groupId>
<artifactId>micronaut-parent</artifactId>
<version>4.10.16</version>
</parent>

<groupId>demo.crema</groupId>
<artifactId>micronaut-bytebuddy-dynamic</artifactId>
<version>0.1.0</version>

<properties>
<jdk.version>25</jdk.version>
<release.version>25</release.version>
<exec.mainClass>demo.crema.Application</exec.mainClass>
<exec.args />
<bytebuddy.version>1.18.10</bytebuddy.version>
<native.maven.plugin.version>1.1.3</native.maven.plugin.version>
<exec.maven.plugin.version>3.5.1</exec.maven.plugin.version>
</properties>

<dependencies>
<dependency>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-runtime</artifactId>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>slf4j-nop</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>net.bytebuddy</groupId>
<artifactId>byte-buddy</artifactId>
<version>${bytebuddy.version}</version>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>io.micronaut.maven</groupId>
<artifactId>micronaut-maven-plugin</artifactId>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths combine.children="append">
<path>
<groupId>io.micronaut</groupId>
<artifactId>micronaut-inject-java</artifactId>
<version>${micronaut.core.version}</version>
</path>
</annotationProcessorPaths>
<compilerArgs>
<arg>-Amicronaut.processing.group=demo.crema</arg>
<arg>-Amicronaut.processing.module=micronaut-bytebuddy-dynamic</arg>
</compilerArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>exec-maven-plugin</artifactId>
<version>${exec.maven.plugin.version}</version>
<configuration>
<mainClass>${exec.mainClass}</mainClass>
<arguments combine.self="override" />
<commandlineArgs>${exec.args}</commandlineArgs>
</configuration>
</plugin>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>${native.maven.plugin.version}</version>
<configuration>
<mainClass>${exec.mainClass}</mainClass>
<buildArgs>
<buildArg>-Ob</buildArg>
<buildArg>--initialize-at-build-time=net.bytebuddy</buildArg>
</buildArgs>
</configuration>
</plugin>
</plugins>
</build>

<profiles>
<profile>
<id>dynamic</id>
<build>
<plugins>
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<configuration>
<imageName>micronaut-bytebuddy-dynamic</imageName>
<buildArgs combine.children="append">
<buildArg>-H:+UnlockExperimentalVMOptions</buildArg>
<buildArg>-H:+RuntimeClassLoading</buildArg>
<buildArg>-H:-UnlockExperimentalVMOptions</buildArg>
</buildArgs>
</configuration>
<executions>
<execution>
<id>native-build</id>
<phase>package</phase>
<goals>
<goal>compile-no-fork</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
package demo.crema;

import io.micronaut.context.ApplicationContext;
import java.io.IOException;
import java.nio.file.Path;

public final class Application {

private static final Path DEFAULT_PLUGIN_PATH = Path.of("plugins/conference.properties");

private Application() {
}

public static void main(String[] args) {
Path pluginPath;
try {
pluginPath = parsePluginPath(args);
} catch (IllegalArgumentException exception) {
System.err.println("Invalid command: " + exception.getMessage());
System.exit(2);
return;
}

try (ApplicationContext context = ApplicationContext.run()) {
ByteBuddyGreetingFactory factory = context.getBean(ByteBuddyGreetingFactory.class);

System.out.println("Micronaut context: " + context.isRunning());
printPluginGreeting(factory, pluginPath);
} catch (IOException exception) {
System.err.println("Could not read the plugin file.");
exception.printStackTrace(System.err);
System.exit(2);
} catch (RuntimeException exception) {
System.err.println("Byte Buddy runtime class loading failed.");
exception.printStackTrace(System.err);
System.exit(1);
}
}

private static void printPluginGreeting(ByteBuddyGreetingFactory factory, Path pluginPath) throws IOException {
PluginSpec plugin = PluginSpec.load(pluginPath);
GreetingResult result = factory.createGreeting(plugin);

System.out.println("Plugin file: " + pluginPath.toAbsolutePath().normalize());
System.out.println("Generated class: " + result.generatedClassName());
System.out.println(result.message());
}

private static Path parsePluginPath(String[] args) {
if (args.length == 0) {
return DEFAULT_PLUGIN_PATH;
}
if (args.length == 1) {
if ("conference".equals(args[0]) || "--plugin".equals(args[0])) {
return DEFAULT_PLUGIN_PATH;
}
if (args[0].endsWith(".properties")) {
return Path.of(args[0]);
}
}
if (args.length == 2 && "--plugin".equals(args[0])) {
return Path.of(args[1]);
}

throw new IllegalArgumentException("expected conference, --plugin [path], or a .properties file path");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
package demo.crema;

import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.lang.invoke.MethodType;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicLong;
import jakarta.inject.Singleton;
import net.bytebuddy.ByteBuddy;
import net.bytebuddy.ClassFileVersion;
import net.bytebuddy.description.modifier.Visibility;
import net.bytebuddy.dynamic.loading.ClassLoadingStrategy;
import net.bytebuddy.implementation.FixedValue;

@Singleton
public final class ByteBuddyGreetingFactory {

private static final String GENERATED_CLASS_PREFIX = "demo.crema.generated.ConferencePlugin";

private final AtomicLong classSequence = new AtomicLong();

public GreetingResult createGreeting(PluginSpec plugin) {
Objects.requireNonNull(plugin, "plugin");

Class<?> generatedType = new ByteBuddy(ClassFileVersion.JAVA_V25)
.subclass(Object.class)
.name(GENERATED_CLASS_PREFIX + classSequence.incrementAndGet())
.defineMethod("message", String.class, Visibility.PUBLIC)
.intercept(FixedValue.value(plugin.message()))
.make()
.load(ByteBuddyGreetingFactory.class.getClassLoader(), ClassLoadingStrategy.Default.WRAPPER)
.getLoaded();

return invokeGreeting(generatedType);
}

private static GreetingResult invokeGreeting(Class<?> generatedType) {
try {
MethodHandles.Lookup lookup = MethodHandles.publicLookup();
MethodHandle constructor = lookup.findConstructor(generatedType, MethodType.methodType(void.class));
Object greeting = constructor.invoke();
MethodHandle message = lookup.findVirtual(generatedType, "message", MethodType.methodType(String.class));

return new GreetingResult(generatedType.getName(), (String) message.invoke(greeting));
} catch (RuntimeException | Error exception) {
throw exception;
} catch (Throwable exception) {
throw new IllegalStateException("Could not invoke generated greeting", exception);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
package demo.crema;

public record GreetingResult(String generatedClassName, String message) {
}
Loading