Substitution
Due to limitations of Native Image, some code needs to be changed to run in native images. This is done by substituting existing classes with new classes during native image generation. Using substitutions is the most powerful technique to support native images.
Substitutions are declared using annotations in the com.oracle.svm.core.annotate
package of GraalVM SDK. These annotated classes are used by native-image
tool during the generation of native images.
Source code from the GraalVM SDK is used to demonstrate the usage of substitutions.
Target Class
The first step is to identify the class to substitute. @TargetClass
annotation is used to annotate a class that modifies methods or fields of another class. The class to be modified is called the original class. The table below shows attributes of @TargetClass
.
Attribute | Type | Default value | Description |
---|---|---|---|
value | Class<?> | TargetClass.class | Original class to substitute |
className | String | "" | Original class substitute |
classNameProvider | Class<? extends Function<TargetClass, String>> | NoClassNameProvider.class | Use a function to provide the original class to substitute |
innerClass | String[] | {} | The suffix of the original class name when it is an inner class |
onlyWith | Class<?>[] | TargetClass.AlwaysIncluded.class | Substitute only if all provided predicates are true. The classes must either implement BooleanSupplier or Predicate<String> |
value
, className
and classNameProvider
attributes are different ways to provide the original class to substitute.
The original class can be modified in different ways, depends on the annotations present on the class annotated with @TargetClass
.
You are free to use any class names for substitutions. The convention is to name the substitution class after the target class. For example, Target_java_lang_Thread
is the recommended name for the java.lang.Thread
class.
Alias
If no @Delete
or @Substitute
annotations present, the annotated class is an alias for the original class. All methods and fields for the annotated class must be annotated with @Alias
, @Delete
, @Substitute
, or @AnnotateOriginal
.
@Alias
When @Alias
is present, the annotated method or field is an alias for the original element. The annotated element doesn't exist in the runtime. All usages of the annotated element reference the original element. By using @Alias
, we can refer to fields and methods that are inaccessible due to Java language access control rules.
The Target_java_util_Optional
class comes from GraalVM SDK. It substitutes java.util.Optional
class. The value
field is annotated with @Alias
. This field is an alias of the private final T value
field from java.util.Optional
. Then this value
field can be used in the toString
method.
@TargetClass(Optional.class)
final class Target_java_util_Optional<T> {
@Alias T value;
@Override
@Substitute
public String toString() {
return value != null
? "Optional[" + value + "]"
: "Optional.empty";
}
}
@Delete
When @Delete
is present, the annotated element doesn't exist in the runtime.
The Target_java_net_URL
class comes from GraalVM SDK. The handlers
field from java.net.URL
doesn't exist in the runtime.
@TargetClass(java.net.URL.class)
final class Target_java_net_URL {
@Delete private static Hashtable<?, ?> handlers;
@Substitute
private static URLStreamHandler getURLStreamHandler(String protocol) throws MalformedURLException {
return JavaNetSubstitutions.getURLStreamHandler(protocol);
}
@Substitute
@SuppressWarnings("unused")
public static void setURLStreamHandlerFactory(URLStreamHandlerFactory fac) {
VMError.unsupportedFeature("Setting a custom URLStreamHandlerFactory.");
}
}
@Substitute
When @Substitute
is present, the annotated element is a substitute of the same element in the target class. We already see the examples of method substitutions in the Target_java_util_Optional
and Target_java_net_URL
classes.
When a method is annotated with @Substitute
, the method to substitute is matched by the name and the list of parameter types. We can provide a different method name using the @TargetElement
annotation. However, the list of parameter types must match.
@AnnotateOriginal
Whhen @AnnotateOriginal
is present, all annotations on the annotated method are also present in the original method.
The Target_java_lang_Enum
class comes from GraalVM SDK. With @AnnotateOriginal
, the @Uninterruptible
annotation is added to the ordinal
method.
@TargetClass(java.lang.Enum.class)
final class Target_java_lang_Enum {
@Substitute
private static Enum<?> valueOf(Class<Enum<?>> enumType, String name) {
// code omitted
}
@AnnotateOriginal
@Uninterruptible(reason = "Called from uninterruptible code.", mayBeInlined = true)
public native int ordinal();
}
Delete
If @Delete
annotation is present, the annotated class and the original class don't exist.
In the code below, java.lang.ref.Finalizer
class is deleted because Substrate VM does not support Finalizer
references.
@TargetClass(className = "java.lang.ref.Finalizer")
@Delete
final class Target_java_lang_ref_Finalizer {
}
Replace
If @Substitute
annotation is present, the annotated class replaces the original class. All methods and fields of the original class are treated as deleted by default, with the following exception:
- A method annotated with
@Substitute
replaces the original method. - A method annotated with
@KeepOriginal
keeps the original method.
Target Element
@TargetElement
specifies additional properties for an element which is also annotated with @Alias
, @Delete
, @Substitute
, @AnnotateOriginal
, or @KeepOriginal
.
The table below shows attributes of @TargetElement
.
Attribute | Type | Default value | Description |
---|---|---|---|
name | String | "" | The name of the field or method in the original class |
onlyWith | Class<?>[] | TargetClass.AlwaysIncluded.class | Substitute only if all provided predicates are true. The classes must either implement BooleanSupplier or Predicate<String> |
In the code below, the getExtendedNPEMessage
is a substitution of the same method in java.lang.NullPointerException
, but only in JDK 17 or later.
@TargetClass(java.lang.NullPointerException.class)
final class Target_java_lang_NullPointerException {
@Substitute
@TargetElement(onlyWith = JDK17OrLater.class)
@SuppressWarnings("static-method")
private String getExtendedNPEMessage() {
return null;
}
}
Recompute Field Value
By default, field values in the native image heap of the Substrate VM are the same as in the host VM. We can recompute field values using the @RecomputeFieldValue
annotation. The table below shows attributes of @RecomputeFieldValue
.
Attribute | Type | Default value | Description |
---|---|---|---|
kind | Kind | Kind of the recomputation | |
declClass | Class<?> | RecomputeFieldValue.class | The class parameter for the recomputation |
declClassName | String | "" | The class parameter for the recomputation |
name | String | "" | The name parameter for the recomputation |
isFinal | boolean | false | Whether the value should be treated as final |
disableCaching | boolean | false | Whether the caching of computed values should be disabled |
Target Class
When determining the class parameter for the recomputation, the following algorithm is used:
- Check the
declClass
attribute. - Check the
declClassName
attribute. - Use the class specified by the
@TargetClass
annotation.
Kind
The most important attribute of @RecomputeFieldValue
is kind
. The type Kind
is an enum with following values.
Value | Description |
---|---|
None | The field value is not modified. |
Reset | The field is rest to the default value. |
NewInstance | The field is set to an instance of declClass created by calling the default constructor. |
FromAlias | The field is set to the value assigned to the @Alias field. |
FieldOffset | The int or long field is set to the offset of the field named name() of the class declClass , as it would be computed by sun.misc.Unsafe.objectFieldOffset . |
ArrayBaseOffset | The int or long field is set to the offset of the first array element of the array class declClass , as it would be computed by sun.misc.Unsafe.arrayBaseOffset(Class) . |
ArrayIndexScale | The int or long field is set to the element size array class declClass , as it would be computed by sun.misc.Unsafe.arrayIndexScale(Class) . |
ArrayIndexShift | The int or long field is set to the log2 of ArrayIndexScale . |
AtomicFieldUpdaterOffset | Set field offsets used by java.util.concurrent.atomic.AtomicXxxFieldUpdater . |
TranslateFieldOffset | The field offset stored in this int or long field is updated. The original value must be a valid field offset in the hosted universe, and the new value is the field offset of the same field in the substrate universe. |
Manual | Use manual logic. |
Custom | Use a RecomputeFieldValue.CustomFieldValueComputer or RecomputeFieldValue.CustomFieldValueTransformer , which is specified as the target class. |
Reset
In the code below, value of the otherParents
field is reset to the default value.
@TargetClass(java.io.FileDescriptor.class)
final class Target_java_io_FileDescriptor {
@Alias @RecomputeFieldValue(kind = Kind.Reset)//
private List<Closeable> otherParents;
}
NewInstance
In the code below, value of the instances
field is set to a new instance of ConcurrentHashMap
.
@TargetClass(java.util.Currency.class)
final class Target_java_util_Currency {
@Alias//
@RecomputeFieldValue(kind = Kind.NewInstance, declClass = ConcurrentHashMap.class)//
private static ConcurrentMap<String, Currency> instances;
}
FromAlias
In the code below, the value of powerCache
field is assigned in the static initializer of the current class.
@TargetClass(value = java.math.BigInteger.class)
final class Target_java_math_BigInteger {
@Alias @RecomputeFieldValue(kind = FromAlias)//
private static BigInteger[][] powerCache;
static {
/*
* BigInteger code changes the powerCache to add values lazily. We need a stable field value
* throughout native image generation, so we use our own value (which is computed by code
* duplicated from the static initializer of BigInteger).
*/
powerCache = new BigInteger[Character.MAX_RADIX + 1][];
for (int i = Character.MIN_RADIX; i <= Character.MAX_RADIX; i++) {
powerCache[i] = new BigInteger[]{BigInteger.valueOf(i)};
}
}
}
FieldOffset
In the code below, value of the SYNC_STATE_FIELD_OFFSET
field in the MultiThreadedMonitorSupport
class is set to the field offset of the state
field in the AbstractQueuedSynchronizer
class.
@TargetClass(MultiThreadedMonitorSupport.class)
final class Target_com_oracle_svm_core_monitor_MultiThreadedMonitorSupport {
@Alias @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.FieldOffset, name = "objectMonitorCondition", declClass = Target_java_util_concurrent_locks_ReentrantLock_NonfairSync.class) //
static long SYNC_MONITOR_CONDITION_FIELD_OFFSET;
@Alias @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.FieldOffset, name = "state", declClass = AbstractQueuedSynchronizer.class) //
static long SYNC_STATE_FIELD_OFFSET;
}
ArrayBaseOffset
and ArrayIndexShift
The code below shows an example of ArrayBaseOffset
and ArrayIndexShift
. The target class is the Array
inner class of java.lang.invoke.VarHandleInts
.
@TargetClass(className = "java.lang.invoke.VarHandleInts", innerClass = "Array", onlyWith = JDK11OrLater.class)
final class Target_java_lang_invoke_VarHandleInts_Array {
@Alias @RecomputeFieldValue(kind = Kind.ArrayBaseOffset, declClass = int[].class) //
int abase;
@Alias @RecomputeFieldValue(kind = Kind.ArrayIndexShift, declClass = int[].class) //
int ashift;
}
AtomicFieldUpdaterOffset
AtomicFieldUpdaterOffset
is specific for atomic field updaters in the java.util.concurrent.atomic
package.
@TargetClass(className = "java.util.concurrent.atomic.AtomicIntegerFieldUpdater$AtomicIntegerFieldUpdaterImpl")
final class Target_java_util_concurrent_atomic_AtomicIntegerFieldUpdater_AtomicIntegerFieldUpdaterImpl {
@Alias @RecomputeFieldValue(kind = AtomicFieldUpdaterOffset) //
private long offset;
/** the same as tclass, used for checks */
@Alias private Class<?> cclass;
/** class holding the field */
@Alias private Class<?> tclass;
// simplified version of the original constructor
@SuppressWarnings("unused")
@Substitute
Target_java_util_concurrent_atomic_AtomicIntegerFieldUpdater_AtomicIntegerFieldUpdaterImpl(final Class<?> tclass,
final String fieldName, final Class<?> caller) {
final Field field;
final int modifiers;
try {
field = tclass.getDeclaredField(fieldName);
modifiers = field.getModifiers();
} catch (Exception ex) {
throw new RuntimeException(ex);
}
if (field.getType() != int.class)
throw new IllegalArgumentException("Must be integer type");
if (!Modifier.isVolatile(modifiers))
throw new IllegalArgumentException("Must be volatile type");
// access checks are disabled
this.cclass = tclass;
this.tclass = tclass;
this.offset = GraalUnsafeAccess.getUnsafe().objectFieldOffset(field);
}
}
Manual
In the code below, value of the pollingAddress
field is computed using manual logic.
@Platforms(Platform.WINDOWS.class)
public final class WindowsJavaNIOSubstitutions {
@TargetClass(className = "sun.nio.fs.Cancellable")
@Platforms({Platform.WINDOWS.class})
static final class Target_sun_nio_fs_Cancellable {
@Alias @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Manual)//
private long pollingAddress;
}
}
Custom
When using the Custom
kind, value of the field is recomputed using an instance of CustomFieldValueComputer
or CustomFieldValueTransformer
interface. The class name is specified as the target class.
CustomFieldValueComputer
and CustomFieldValueTransformer
have a similar interface. The difference is that CustomFieldValueTransformer
can access the original value of the field.
interface CustomFieldValueComputer {
Object compute(MetaAccessProvider metaAccess, ResolvedJavaField original, ResolvedJavaField annotated, Object receiver);
}
interface CustomFieldValueTransformer {
Object transform(MetaAccessProvider metaAccess, ResolvedJavaField original, ResolvedJavaField annotated, Object receiver, Object originalValue);
}
The table below shows the list of possible parameters used in these two interfaces.
Parameter | Type | Description |
---|---|---|
metaAccess | MetaAccessProvider | The AnalysisMetaAccess instance during the analysis or HostedMetaAccess instance after the analysis. |
original | ResolvedJavaField | The original field (if @RecomputeFieldValue is used for an @Alias field). |
annotated | ResolvedJavaField | The field annotated with @RecomputeFieldValue . |
receiver | Object | The original object for instance fields, or null for static fields. |
originalValue | Object | The original value of the field. |
In the code below, value of the field previousKey
is computed using the ServiceKeyComputer
class which implements CustomFieldValueComputer
.
@TargetClass(value = java.security.Provider.class)
final class Target_java_security_Provider {
@Alias @RecomputeFieldValue(kind = RecomputeFieldValue.Kind.Custom, declClass = ServiceKeyComputer.class) //
private static Target_java_security_Provider_ServiceKey previousKey;
}
@Platforms(Platform.HOSTED_ONLY.class)
class ServiceKeyComputer implements RecomputeFieldValue.CustomFieldValueComputer {
@Override
public Object compute(MetaAccessProvider metaAccess, ResolvedJavaField original, ResolvedJavaField annotated, Object receiver) {
try {
// Checkstyle: stop do not use dynamic class loading
Class<?> serviceKey = Class.forName("java.security.Provider$ServiceKey");
// Checkstyle: resume
Constructor<?> constructor = ReflectionUtil.lookupConstructor(serviceKey, String.class, String.class, boolean.class);
return constructor.newInstance("", "", false);
} catch (InstantiationException | IllegalAccessException | InvocationTargetException | ClassNotFoundException e) {
throw VMError.shouldNotReachHere(e);
}
}
}
Inject Accessor
For fields annotated with the @Alias
annotation, their values can be get or set using accessors. These accessors are specified using the @InjectAccessors
annotation. @InjectAccessors
only has one value
attribute to provide the class which provides accessors.
The accessor methods are static methods in the accessor class. The method names can be:
set
orget
for a single fieldsetFoo
orgetFoo
for a field namefoo
if the accessor class can be used for multiple fields
Depending on the kind of accessor, the accessor method can have 0
, 1
or 2
parameters.
Accessor | Parameters |
---|---|
get for a static field | No parameters |
set for a static field | One parameter with the value to set |
get for a non-static field | One parameter with the object to access |
set for a non-static field | Two parameters, the first parameter is the object to access, the second parameter is the value to set |
In the code below, CompletableFutureAsyncPoolAccessor
is used to get the value of asyncPool
or ASYNC_POOL
fields. CompletableFutureAsyncPoolAccessor
has only one get
method to get the Executor
instance.
@TargetClass(java.util.concurrent.CompletableFuture.class)
final class Target_java_util_concurrent_CompletableFuture {
@Alias @InjectAccessors(CompletableFutureAsyncPoolAccessor.class) //
@TargetElement(onlyWith = JDK8OrEarlier.class) //
private static Executor asyncPool;
@Alias @InjectAccessors(CompletableFutureAsyncPoolAccessor.class) //
@TargetElement(onlyWith = JDK11OrLater.class) //
private static Executor ASYNC_POOL;
}
class CompletableFutureAsyncPoolAccessor {
static Executor get() {
return ForkJoinPoolCommonAccessor.get();
}
}
Inject
New fields can be injected into the target class using the @Inject
annotation. The annotated field must not be static. The @RecomputeFieldValue
annotation is usually used with @Inject
to provide the value for the new field.
In the code below, the new addedShutdownHook
field is injected into the java.util.logging.LogManager
class with the value of a new instance of AtomicBoolean
. This new field is used in the substituted getLogManager
method.
@TargetClass(value = LogManager.class)
final class Target_java_util_logging_LogManager {
@Inject @RecomputeFieldValue(kind = Kind.NewInstance, declClass = AtomicBoolean.class) private AtomicBoolean addedShutdownHook = new AtomicBoolean();
@Alias static LogManager manager;
@Alias
native void ensureLogManagerInitialized();
@Substitute
public static LogManager getLogManager() {
/* First performing logic originally in getLogManager. */
if (manager == null) {
return manager;
}
Target_java_util_logging_LogManager managerAlias = SubstrateUtil.cast(manager, Target_java_util_logging_LogManager.class);
managerAlias.ensureLogManagerInitialized();
/* Logic for adding shutdown hook. */
if (!managerAlias.addedShutdownHook.getAndSet(true)) {
/* Add a shutdown hook to close the global handlers. */
try {
Runtime.getRuntime().addShutdownHook(SubstrateUtil.cast(new Target_java_util_logging_LogManager_Cleaner(managerAlias), Thread.class));
} catch (IllegalStateException e) {
/* If the VM is already shutting down, we do not need to register shutdownHook. */
}
}
return manager;
}
}
Example
See Flyway native support for how substitutions are used.