SubstrateVM Reflection Configuration in Java

Introduction

SubstrateVM (used in GraalVM Native Image) requires explicit configuration for reflection usage since it performs closed-world analysis during native image generation. Reflection configuration tells the native image builder which classes, methods, and fields need to be accessible via reflection at runtime.

Understanding Reflection in Native Image

The Reflection Challenge

public class ReflectionChallenge {
// This will fail in native image without configuration
public void problematicReflection() {
try {
Class<?> clazz = Class.forName("com.example.HiddenClass");
Method method = clazz.getDeclaredMethod("hiddenMethod");
method.setAccessible(true);
Object instance = clazz.getDeclaredConstructor().newInstance();
method.invoke(instance);
} catch (Exception e) {
// This will fail in native image without configuration
e.printStackTrace();
}
}
// Common reflection patterns that need configuration
public void commonReflectionPatterns() {
// 1. Class.forName()
Class<?> clazz = Class.forName("fully.qualified.ClassName");
// 2. getMethod() / getDeclaredMethod()
Method method = clazz.getDeclaredMethod("methodName", String.class);
// 3. getField() / getDeclaredField()
Field field = clazz.getDeclaredField("fieldName");
// 4. getConstructor()
Constructor<?> constructor = clazz.getDeclaredConstructor();
// 5. Array.newInstance()
Object array = Array.newInstance(clazz, 10);
// 6. Resource loading
InputStream resource = clazz.getResourceAsStream("/config.properties");
}
}

Manual Reflection Configuration

JSON Configuration Files

// META-INF/native-image/reflect-config.json
[
{
"name": "com.example.User",
"allDeclaredConstructors": true,
"allPublicConstructors": true,
"allDeclaredMethods": true,
"allPublicMethods": true,
"allDeclaredFields": true,
"allPublicFields": true
},
{
"name": "com.example.Product",
"methods": [
{
"name": "getName",
"parameterTypes": []
},
{
"name": "setPrice",
"parameterTypes": ["double"]
}
],
"fields": [
{
"name": "id",
"type": "long"
},
{
"name": "name", 
"type": "java.lang.String"
}
]
},
{
"name": "com.example.Service",
"queriedMethods": [
{
"name": "process",
"parameterTypes": ["java.lang.String", "int"]
}
]
}
]

Comprehensive Configuration Structure

// Complete reflect-config.json example
[
{
"name": "com.example.dto.UserDTO",
"allDeclaredClasses": true,
"allDeclaredConstructors": true,
"allDeclaredMethods": true,
"allDeclaredFields": true,
"allPublicClasses": true,
"allPublicConstructors": true,
"allPublicMethods": true,
"allPublicFields": true
},
{
"name": "com.example.util.ReflectionHelper",
"methods": [
{
"name": "<init>",
"parameterTypes": []
},
{
"name": "invokePrivateMethod",
"parameterTypes": ["java.lang.Object", "java.lang.String", "java.lang.Class[]"]
}
]
},
{
"name": "java.util.ArrayList",
"fields": [
{
"name": "elementData",
"type": "java.lang.Object[]"
},
{
"name": "size",
"type": "int"
}
]
},
{
"name": "com.example.enums.Status",
"allDeclaredFields": true,
"allDeclaredMethods": true
}
]

Automated Configuration Generation

Runtime Reflection Registration

public class RuntimeReflectionRegistrar {
public static void registerForReflection() {
// Register classes for reflection
registerClass("com.example.User");
registerClass("com.example.Product");
registerClass("com.example.Order");
// Register specific methods
registerMethod("com.example.Service", "processOrder", String.class, int.class);
registerMethod("com.example.Util", "convertToJson", Object.class);
// Register fields
registerField("com.example.Config", "apiKey");
registerField("com.example.Config", "timeout");
}
private static void registerClass(String className) {
try {
Class<?> clazz = Class.forName(className);
RuntimeReflection.register(clazz);
RuntimeReflection.register(clazz.getDeclaredConstructors());
RuntimeReflection.register(clazz.getDeclaredMethods());
RuntimeReflection.register(clazz.getDeclaredFields());
} catch (ClassNotFoundException e) {
System.err.println("Class not found for reflection registration: " + className);
}
}
private static void registerMethod(String className, String methodName, Class<?>... parameterTypes) {
try {
Class<?> clazz = Class.forName(className);
Method method = clazz.getDeclaredMethod(methodName, parameterTypes);
RuntimeReflection.register(method);
} catch (Exception e) {
System.err.println("Method not found for reflection registration: " + 
className + "." + methodName);
}
}
private static void registerField(String className, String fieldName) {
try {
Class<?> clazz = Class.forName(className);
Field field = clazz.getDeclaredField(fieldName);
RuntimeReflection.register(field);
} catch (Exception e) {
System.err.println("Field not found for reflection registration: " + 
className + "." + fieldName);
}
}
// Register all classes in a package
public static void registerPackage(String packageName) {
try {
ClassPath classpath = ClassPath.from(Thread.currentThread().getContextClassLoader());
for (ClassPath.ClassInfo classInfo : classpath.getTopLevelClasses(packageName)) {
registerClass(classInfo.getName());
}
} catch (IOException e) {
System.err.println("Failed to scan package: " + packageName);
}
}
}

Feature Interface for Automatic Registration

import org.graalvm.nativeimage.hosted.Feature;
import org.graalvm.nativeimage.hosted.RuntimeReflection;
public class ReflectionFeature implements Feature {
@Override
public void beforeAnalysis(BeforeAnalysisAccess access) {
System.out.println("Registering classes for reflection...");
// Register core application classes
registerApplicationClasses();
// Register framework classes
registerFrameworkClasses();
// Register dynamic classes
registerDynamicReflection();
}
private void registerApplicationClasses() {
// Register all DTOs
registerClassAndMembers("com.example.dto.UserDTO");
registerClassAndMembers("com.example.dto.ProductDTO");
registerClassAndMembers("com.example.dto.OrderDTO");
// Register service classes
registerClassAndMembers("com.example.service.UserService");
registerClassAndMembers("com.example.service.ProductService");
// Register entity classes
registerClassAndMembers("com.example.entity.User");
registerClassAndMembers("com.example.entity.Product");
}
private void registerFrameworkClasses() {
// Jackson JSON processing
registerClass("com.fasterxml.jackson.databind.ObjectMapper");
registerClass("com.fasterxml.jackson.annotation.JsonProperty");
// Spring Framework
registerClass("org.springframework.stereotype.Component");
registerClass("org.springframework.web.bind.annotation.RestController");
}
private void registerDynamicReflection() {
// Register classes that might be loaded dynamically
try {
Class<?> dynamicClass = Class.forName("com.example.dynamic.DynamicProcessor");
RuntimeReflection.register(dynamicClass);
} catch (ClassNotFoundException e) {
// Ignore - class might not be available at build time
}
}
private void registerClassAndMembers(String className) {
try {
Class<?> clazz = Class.forName(className);
RuntimeReflection.register(clazz);
RuntimeReflection.register(clazz.getDeclaredConstructors());
RuntimeReflection.register(clazz.getDeclaredMethods());
RuntimeReflection.register(clazz.getDeclaredFields());
} catch (ClassNotFoundException e) {
System.err.println("Class not found: " + className);
}
}
private void registerClass(String className) {
try {
Class<?> clazz = Class.forName(className);
RuntimeReflection.register(clazz);
} catch (ClassNotFoundException e) {
System.err.println("Class not found: " + className);
}
}
@Override
public String getDescription() {
return "Automatic reflection registration for application classes";
}
}

Framework-Specific Configuration

Spring Boot Native Image Configuration

@NativeHint(
trigger = SpringBootApplication.class,
types = {
@TypeHint(types = {
org.springframework.web.bind.annotation.RestController.class,
org.springframework.stereotype.Service.class,
org.springframework.stereotype.Component.class
}, access = TypeAccess.DECLARED_CONSTRUCTORS),
@TypeHint(
types = com.example.dto.UserDTO.class,
access = {TypeAccess.PUBLIC_CONSTRUCTORS, TypeAccess.PUBLIC_METHODS}
)
},
options = {"--enable-https"}
)
@Configuration
public class SpringNativeConfiguration {
@Bean
@NativeHint(types = @TypeHint(types = ObjectMapper.class))
public ObjectMapper objectMapper() {
return new ObjectMapper();
}
}
// Alternative using Spring Native 0.11+ with @RegisterReflectionForBinding
@SpringBootApplication
@RegisterReflectionForBinding({
UserDTO.class,
ProductDTO.class, 
OrderDTO.class
})
public class NativeApplication {
public static void main(String[] args) {
SpringApplication.run(NativeApplication.class, args);
}
}

Jackson JSON Reflection Configuration

public class JacksonReflectionConfig {
// JSON configuration for Jackson
/*
META-INF/native-image/jackson-config.json
*/
public static String getJacksonReflectionConfig() {
return """
[
{
"name": "com.example.dto.UserDTO",
"allDeclaredConstructors": true,
"allDeclaredMethods": true,
"allDeclaredFields": true
},
{
"name": "com.example.dto.ProductDTO",
"allDeclaredConstructors": true,
"allDeclaredMethods": true,
"allDeclaredFields": true
},
{
"name": "com.fasterxml.jackson.databind.ObjectMapper",
"methods": [
{"name": "<init>", "parameterTypes": []},
{"name": "readValue", "parameterTypes": ["java.lang.String", "java.lang.Class"]}
]
}
]
""";
}
// Runtime registration for Jackson
public static void registerJacksonForReflection() {
try {
// Register ObjectMapper
Class<?> objectMapperClass = Class.forName("com.fasterxml.jackson.databind.ObjectMapper");
RuntimeReflection.register(objectMapperClass);
RuntimeReflection.register(objectMapperClass.getDeclaredConstructors());
// Register common Jackson annotations
Class<?> jsonPropertyClass = Class.forName("com.fasterxml.jackson.annotation.JsonProperty");
RuntimeReflection.register(jsonPropertyClass);
// Register your DTOs
registerDTOClasses();
} catch (ClassNotFoundException e) {
System.err.println("Jackson classes not found for reflection registration");
}
}
private static void registerDTOClasses() {
String[] dtoClasses = {
"com.example.dto.UserDTO",
"com.example.dto.ProductDTO", 
"com.example.dto.OrderDTO",
"com.example.dto.AddressDTO"
};
for (String className : dtoClasses) {
try {
Class<?> clazz = Class.forName(className);
RuntimeReflection.register(clazz);
RuntimeReflection.register(clazz.getDeclaredConstructors());
RuntimeReflection.register(clazz.getDeclaredMethods());
RuntimeReflection.register(clazz.getDeclaredFields());
} catch (ClassNotFoundException e) {
System.err.println("DTO class not found: " + className);
}
}
}
}

Dynamic Proxy Configuration

Proxy Configuration Files

// META-INF/native-image/proxy-config.json
[
{
"interfaces": ["java.lang.Runnable", "java.util.concurrent.Callable"]
},
{
"interfaces": ["com.example.service.UserService", "org.springframework.stereotype.Component"]
},
{
"interfaces": ["java.io.Serializable", "com.example.dto.Transferable"]
}
]

Runtime Proxy Registration

public class DynamicProxyRegistrar {
public static void registerProxies() {
// Register common proxy interfaces
registerProxyInterfaces(
"java.lang.Runnable",
"java.util.concurrent.Callable",
"java.io.Serializable"
);
// Register Spring proxies
registerProxyInterfaces(
"org.springframework.stereotype.Component",
"org.springframework.context.annotation.Bean"
);
// Register application-specific proxies
registerProxyInterfaces(
"com.example.service.UserService",
"com.example.service.ProductService"
);
}
private static void registerProxyInterfaces(String... interfaceNames) {
List<Class<?>> interfaces = new ArrayList<>();
for (String interfaceName : interfaceNames) {
try {
Class<?> interfaceClass = Class.forName(interfaceName);
interfaces.add(interfaceClass);
} catch (ClassNotFoundException e) {
System.err.println("Interface not found: " + interfaceName);
}
}
if (!interfaces.isEmpty()) {
RuntimeReflection.register(interfaces.toArray(new Class<?>[0]));
}
}
// Register proxy for specific interface combinations
public static void registerSpecificProxy(Class<?>... interfaces) {
RuntimeReflection.register(interfaces);
}
}

Resource Configuration

Resource Configuration Files

// META-INF/native-image/resource-config.json
{
"resources": {
"includes": [
{
"pattern": ".*\\\\.properties$"
},
{
"pattern": ".*\\\\.json$"
},
{
"pattern": ".*\\\\.xml$"
},
{
"pattern": "META-INF/services/.*"
},
{
"pattern": "templates/.*"
}
],
"excludes": [
{
"pattern": ".*\\\\.gitignore$"
},
{
"pattern": ".*\\\\.md$"
}
]
},
"bundles": [
{
"name": "com.sun.org.apache.xerces.internal.impl.msg.XMLMessages"
},
{
"name": "sun.util.resources.CurrencyNames"
}
]
}

Runtime Resource Registration

public class ResourceRegistration {
public static void registerResources() {
// Register property files
registerResourcePattern(".*\\\\.properties");
// Register configuration files
registerResourcePattern("application.*");
registerResourcePattern("bootstrap.*");
// Register service files
registerResourcePattern("META-INF/services/.*");
// Register template files
registerResourcePattern("templates/.*");
registerResourcePattern("static/.*");
}
private static void registerResourcePattern(String pattern) {
try {
RuntimeResourceAccess.addResourcePattern(pattern);
} catch (Exception e) {
System.err.println("Failed to register resource pattern: " + pattern);
}
}
// Register specific resources
public static void registerSpecificResources(String... resources) {
for (String resource : resources) {
try {
RuntimeResourceAccess.addResource(resource);
} catch (Exception e) {
System.err.println("Failed to register resource: " + resource);
}
}
}
// Register resource bundles
public static void registerResourceBundles() {
String[] bundles = {
"sun.util.resources.CurrencyNames",
"com.sun.org.apache.xerces.internal.impl.msg.XMLMessages",
"java.util.PropertyResourceBundle"
};
for (String bundle : bundles) {
try {
RuntimeResourceAccess.addResourceBundle(bundle);
} catch (Exception e) {
System.err.println("Failed to register resource bundle: " + bundle);
}
}
}
}

JNI Configuration

JNI Configuration Files

// META-INF/native-image/jni-config.json
[
{
"name": "java.lang.System",
"methods": [
{
"name": "loadLibrary",
"parameterTypes": ["java.lang.String"]
},
{
"name": "load",
"parameterTypes": ["java.lang.String"]
}
]
},
{
"name": "java.lang.Runtime",
"methods": [
{
"name": "loadLibrary0",
"parameterTypes": ["java.lang.Class", "java.lang.String"]
}
]
},
{
"name": "com.example.NativeLibrary",
"allDeclaredMethods": true,
"allDeclaredConstructors": true
}
]

Testing Reflection Configuration

Reflection Testing Utility

public class ReflectionTestUtil {
public static void testReflectionAccess(String className) {
try {
Class<?> clazz = Class.forName(className);
System.out.println("Testing reflection for: " + className);
// Test constructors
testConstructors(clazz);
// Test methods
testMethods(clazz);
// Test fields
testFields(clazz);
System.out.println("✓ All reflection tests passed for: " + className);
} catch (ClassNotFoundException e) {
System.err.println("✗ Class not found: " + className);
} catch (Exception e) {
System.err.println("✗ Reflection test failed for: " + className + " - " + e.getMessage());
}
}
private static void testConstructors(Class<?> clazz) {
try {
Constructor<?>[] constructors = clazz.getDeclaredConstructors();
for (Constructor<?> constructor : constructors) {
constructor.setAccessible(true);
// Try to access constructor
System.out.println("  ✓ Constructor: " + constructor);
}
} catch (Exception e) {
throw new RuntimeException("Constructor access failed", e);
}
}
private static void testMethods(Class<?> clazz) {
try {
Method[] methods = clazz.getDeclaredMethods();
for (Method method : methods) {
method.setAccessible(true);
// Try to access method
System.out.println("  ✓ Method: " + method.getName());
}
} catch (Exception e) {
throw new RuntimeException("Method access failed", e);
}
}
private static void testFields(Class<?> clazz) {
try {
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
// Try to access field
System.out.println("  ✓ Field: " + field.getName());
}
} catch (Exception e) {
throw new RuntimeException("Field access failed", e);
}
}
// Test specific method invocation
public static void testMethodInvocation(String className, String methodName, Class<?>[] paramTypes, Object... args) {
try {
Class<?> clazz = Class.forName(className);
Method method = clazz.getDeclaredMethod(methodName, paramTypes);
method.setAccessible(true);
Object instance = clazz.getDeclaredConstructor().newInstance();
Object result = method.invoke(instance, args);
System.out.println("✓ Method invocation successful: " + methodName + " -> " + result);
} catch (Exception e) {
throw new RuntimeException("Method invocation failed: " + methodName, e);
}
}
// Run all reflection tests
public static void runAllTests() {
String[] testClasses = {
"com.example.dto.UserDTO",
"com.example.service.UserService",
"com.example.util.ValidationUtil"
};
for (String className : testClasses) {
testReflectionAccess(className);
}
}
}

Build-Time Configuration

Maven Configuration for Native Image

<!-- pom.xml configuration for GraalVM Native Image -->
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>0.9.19</version>
<executions>
<execution>
<id>build-native</id>
<goals>
<goal>build</goal>
</goals>
<phase>package</phase>
</execution>
</executions>
<configuration>
<mainClass>com.example.Application</mainClass>
<buildArgs>
<buildArg>--verbose</buildArg>
<buildArg>--no-fallback</buildArg>
<buildArg>-H:EnableURLProtocols=http,https</buildArg>
<buildArg>-H:+ReportExceptionStackTraces</buildArg>
<buildArg>-H:ReflectionConfigurationFiles=reflect-config.json</buildArg>
<buildArg>-H:ResourceConfigurationFiles=resource-config.json</buildArg>
<buildArg>-H:JNIConfigurationFiles=jni-config.json</buildArg>
</buildArgs>
</configuration>
</plugin>
<!-- Agent for automatic configuration generation -->
<plugin>
<groupId>org.graalvm.buildtools</groupId>
<artifactId>native-maven-plugin</artifactId>
<version>0.9.19</version>
<executions>
<execution>
<id>test-native</id>
<goals>
<goal>test</goal>
</goals>
<phase>test</phase>
</execution>
</executions>
<configuration>
<agent>true</agent>
</configuration>
</plugin>

Gradle Configuration

// build.gradle for Native Image
plugins {
id 'org.graalvm.buildtools.native' version '0.9.19'
}
graalvmNative {
binaries {
main {
buildArgs.addAll(
'--verbose',
'--no-fallback',
'-H:EnableURLProtocols=http,https',
'-H:+ReportExceptionStackTraces',
'-H:ReflectionConfigurationFiles=reflect-config.json',
'-H:ResourceConfigurationFiles=resource-config.json'
)
}
}
agent {
defaultMode = "standard"
}
}
// Task to generate reflection configuration
task generateReflectionConfig(type: JavaExec) {
classpath = sourceSets.main.runtimeClasspath
mainClass = 'com.example.ReflectionConfigGenerator'
}

Best Practices and Troubleshooting

Reflection Configuration Best Practices

public class ReflectionBestPractices {
// 1. Use specific configuration over broad patterns
public void specificConfiguration() {
// Good: Specific methods and fields
/*
{
"name": "com.example.User",
"methods": [
{"name": "getName", "parameterTypes": []},
{"name": "setEmail", "parameterTypes": ["java.lang.String"]}
],
"fields": [
{"name": "id", "type": "long"}
]
}
*/
// Avoid: Overly broad configuration
/*
{
"name": "com.example.*",
"allDeclaredMethods": true,
"allDeclaredFields": true
}
*/
}
// 2. Register classes used in serialization
public void registerSerializationClasses() {
String[] serializationClasses = {
"com.example.dto.UserDTO",
"com.example.dto.ProductDTO",
"com.example.dto.OrderDTO",
"java.util.ArrayList",
"java.util.HashMap"
};
for (String className : serializationClasses) {
try {
Class<?> clazz = Class.forName(className);
RuntimeReflection.register(clazz);
} catch (ClassNotFoundException e) {
System.err.println("Serialization class not found: " + className);
}
}
}
// 3. Handle dynamic class loading
public void handleDynamicClasses() {
// Use Feature interface to register classes that might be loaded dynamically
// Or use conditional registration based on build-time analysis
}
// 4. Test configuration thoroughly
public void testConfiguration() {
ReflectionTestUtil.runAllTests();
}
// 5. Use the agent for initial configuration
public void useAgentForDiscovery() {
// Run with: -agentlib:native-image-agent=config-output-dir=./config
// This generates configuration files based on runtime behavior
}
}
// Common troubleshooting patterns
public class ReflectionTroubleshooting {
public static void diagnoseReflectionIssues() {
// Check if class is available
checkClassAvailability("com.example.MissingClass");
// Check method accessibility
checkMethodAccessibility("com.example.User", "getEmail");
// Check field accessibility  
checkFieldAccessibility("com.example.User", "password");
}
private static void checkClassAvailability(String className) {
try {
Class<?> clazz = Class.forName(className);
System.out.println("✓ Class available: " + className);
} catch (ClassNotFoundException e) {
System.err.println("✗ Class not available: " + className);
}
}
private static void checkMethodAccessibility(String className, String methodName) {
try {
Class<?> clazz = Class.forName(className);
Method method = clazz.getDeclaredMethod(methodName);
method.setAccessible(true);
System.out.println("✓ Method accessible: " + methodName);
} catch (Exception e) {
System.err.println("✗ Method not accessible: " + methodName + " - " + e.getMessage());
}
}
private static void checkFieldAccessibility(String className, String fieldName) {
try {
Class<?> clazz = Class.forName(className);
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
System.out.println("✓ Field accessible: " + fieldName);
} catch (Exception e) {
System.err.println("✗ Field not accessible: " + fieldName + " - " + e.getMessage());
}
}
}

Conclusion

Proper SubstrateVM reflection configuration is essential for successful native image generation. Key takeaways:

  1. Be Specific - Prefer explicit method/field registration over broad patterns
  2. Use the Agent - Leverage the tracing agent for initial configuration generation
  3. Test Thoroughly - Validate reflection access in both JVM and native modes
  4. Framework Integration - Use framework-specific annotations and hints when available
  5. Runtime Registration - Implement Feature interface for dynamic registration scenarios
  6. Resource Management - Don't forget to configure resources and proxies

By following these patterns and best practices, you can successfully configure reflection for SubstrateVM native images while maintaining application functionality and performance.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper