Replace build-time initialization by constant fields

This commit leverages a subset of @philwebb initial experimentation
to compute at build time the value of specific boolean static fields
in native images. This enhancement is implemented for now as a
GraalVM feature.

The goal here is to keep an optimized footprint via build time code
removal without leveraging build-time class initialization which is known
for the blocking compatibility issues it introduces due to its viral nature.

For now, the static fields initialized at build time with native are:
 - NativeDetector#imageCode
 - Fields with a name ending by "Present" in "org.springframework" package
   typically used for classpath check with ClassUtils#isPresent

Closes gh-28624
This commit is contained in:
Sébastien Deleuze
2022-06-22 12:17:40 +02:00
parent dc4ae559c5
commit 22a750f378
16 changed files with 413 additions and 15 deletions

View File

@@ -0,0 +1,48 @@
description = "Spring Core GraalVM feature"
configurations {
classesOnlyElements {
canBeConsumed = true
canBeResolved = false
}
}
artifacts {
classesOnlyElements(compileJava.destinationDirectory)
classesOnlyElements(sourceSets.main.resources.srcDirs)
}
tasks.withType(JavaCompile) {
options.compilerArgs += [
"--add-modules",
"jdk.internal.vm.ci",
"--add-exports",
"jdk.internal.vm.ci/jdk.vm.ci.meta=ALL-UNNAMED"
]
}
eclipse.classpath.file {
whenMerged {
entries.find{ it.path ==~ '.*JRE_CONTAINER.*' }.each {
it.entryAttributes['module'] = true
it.entryAttributes['add-exports'] = 'jdk.internal.vm.ci/jdk.vm.ci.meta=ALL-UNNAMED'
it.entryAttributes['limit-modules'] = 'java.se,jdk.accessibility,jdk.attach,jdk.compiler,jdk.httpserver,jdk.jartool,jdk.jconsole,jdk.jdi,jdk.management,jdk.sctp,jdk.security.auth,jdk.security.jgss,jdk.unsupported,jdk.dynalink,jdk.incubator.foreign,jdk.incubator.vector,jdk.javadoc,jdk.jfr,jdk.jshell,jdk.management.jfr,jdk.net,jdk.nio.mapmode,jdk.unsupported.desktop,jdk.jsobject,jdk.xml.dom,jdk.internal.vm.ci'
}
}
}
dependencies {
compileOnly("org.graalvm.nativeimage:svm")
}
tasks.withType(PublishToMavenRepository) {
enabled = false
}
tasks.withType(PublishToMavenLocal) {
enabled = false
}
tasks.withType(Javadoc) {
enabled = false
}

View File

@@ -0,0 +1,51 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.graalvm;
import com.oracle.svm.core.annotate.AutomaticFeature;
import com.oracle.svm.hosted.FeatureImpl.DuringSetupAccessImpl;
import org.graalvm.compiler.debug.DebugContext;
import org.graalvm.nativeimage.hosted.Feature;
/**
* GraalVM {@link Feature} that substitutes field values that match a certain pattern
* with constants without causing build-time initialization.
*
* @author Phillip Webb
* @author Sebastien Deleuze
* @since 6.0
*/
@AutomaticFeature
class ConstantFieldFeature implements Feature {
@Override
public void duringSetup(DuringSetupAccess access) {
duringSetup((DuringSetupAccessImpl) access);
}
private void duringSetup(DuringSetupAccessImpl access) {
DebugContext debug = access.getDebugContext();
try (DebugContext.Scope scope = debug.scope("ConstantFieldFeature.duringSetup")) {
debug.log("Installing constant field substitution processor : " + scope);
ClassLoader applicationClassLoader = access.getApplicationClassLoader();
ConstantFieldSubstitutionProcessor substitutionProcessor =
new ConstantFieldSubstitutionProcessor(debug, applicationClassLoader);
access.registerSubstitutionProcessor(substitutionProcessor);
}
}
}

View File

@@ -0,0 +1,91 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.graalvm;
import java.lang.reflect.Field;
import java.util.regex.Pattern;
import com.oracle.graal.pointsto.infrastructure.SubstitutionProcessor;
import com.oracle.svm.core.meta.SubstrateObjectConstant;
import com.oracle.svm.core.util.UserError;
import jdk.vm.ci.meta.JavaConstant;
import jdk.vm.ci.meta.JavaKind;
import jdk.vm.ci.meta.ResolvedJavaField;
import jdk.vm.ci.meta.ResolvedJavaType;
import org.graalvm.compiler.debug.DebugContext;
/**
* {@link SubstitutionProcessor} to compute at build time the value of the
* boolean static fields identified by {@link #patterns} in order to allow
* efficient code shrinking without using class build time initialization.
*
* @author Phillip Webb
* @author Sebastien Deleuze
* @since 6.0
*/
class ConstantFieldSubstitutionProcessor extends SubstitutionProcessor {
// Later should be an explicit signal, like an annotation or even a Java keyword
private static Pattern[] patterns = {
Pattern.compile(Pattern.quote("org.springframework.core.NativeDetector#imageCode")),
Pattern.compile(Pattern.quote("org.springframework.") + ".*#.*Present"),
};
private final ThrowawayClassLoader throwawayClassLoader;
ConstantFieldSubstitutionProcessor(DebugContext debug, ClassLoader applicationClassLoader) {
this.throwawayClassLoader = new ThrowawayClassLoader(applicationClassLoader);
}
@Override
public ResolvedJavaField lookup(ResolvedJavaField field) {
ResolvedJavaType declaringClass = field.getDeclaringClass();
if (field.getType().getJavaKind() == JavaKind.Boolean && field.isStatic()) {
String fieldIdentifier = declaringClass.toJavaName() + "#" + field.getName();
for (Pattern pattern : patterns) {
if (pattern.matcher(fieldIdentifier).matches()) {
JavaConstant constant = lookupConstant(declaringClass.toJavaName(), field.getName());
if (constant != null) {
// TODO Use proper logging only when --verbose is specified when https://github.com/oracle/graal/issues/4669 will be fixed
System.out.println("Field " + fieldIdentifier + " set to " + constant.toValueString() + " at build time");
return new ConstantReadableJavaField(field, constant);
}
}
}
}
return super.lookup(field);
}
private JavaConstant lookupConstant(String className, String fieldName) {
try {
Class<?> throwawayClass = this.throwawayClassLoader.loadClass(className);
Field field = throwawayClass.getDeclaredField(fieldName);
field.setAccessible(true);
Object value = field.get(null);
if (!(value instanceof Boolean)) {
throw UserError.abort("Unable to get the value of " + className + "." + fieldName);
}
return SubstrateObjectConstant.forBoxedValue(JavaKind.Boolean, value);
}
catch (Exception ex) {
throw new IllegalStateException("Unable to read value from " + className + "." + fieldName, ex);
}
}
}

View File

@@ -0,0 +1,112 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.graalvm;
import java.lang.annotation.Annotation;
import com.oracle.svm.core.meta.ReadableJavaField;
import jdk.vm.ci.meta.JavaConstant;
import jdk.vm.ci.meta.JavaType;
import jdk.vm.ci.meta.MetaAccessProvider;
import jdk.vm.ci.meta.ResolvedJavaField;
import jdk.vm.ci.meta.ResolvedJavaType;
/**
* {@link ReadableJavaField} for a constant value.
*
* @author Phillip Webb
* @since 6.0
*/
class ConstantReadableJavaField implements ReadableJavaField {
private final ResolvedJavaField original;
private final JavaConstant constant;
public ConstantReadableJavaField(ResolvedJavaField original, JavaConstant constant) {
this.original = original;
this.constant = constant;
}
@Override
public <T extends Annotation> T getAnnotation(Class<T> annotationClass) {
return this.original.getAnnotation(annotationClass);
}
@Override
public Annotation[] getAnnotations() {
return this.original.getAnnotations();
}
@Override
public Annotation[] getDeclaredAnnotations() {
return this.original.getDeclaredAnnotations();
}
@Override
public ResolvedJavaType getDeclaringClass() {
return this.original.getDeclaringClass();
}
@Override
public int getModifiers() {
return this.original.getModifiers();
}
@Override
public String getName() {
return this.original.getName();
}
@Override
public int getOffset() {
return this.original.getOffset();
}
@Override
public JavaType getType() {
return this.original.getType();
}
@Override
public boolean isInternal() {
return this.original.isInternal();
}
@Override
public boolean isSynthetic() {
return this.original.isSynthetic();
}
@Override
public JavaConstant readValue(MetaAccessProvider metaAccess, JavaConstant receiver) {
return this.constant;
}
@Override
public boolean allowConstantFolding() {
return true;
}
@Override
public boolean injectFinalForRuntimeCompilation() {
return true;
}
}

View File

@@ -0,0 +1,85 @@
/*
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.aot.graalvm;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URL;
/**
* {@link ClassLoader} used to load classes without causing build-time
* initialization.
*
* @author Phillip Webb
* @since 6.0
*/
class ThrowawayClassLoader extends ClassLoader {
static {
registerAsParallelCapable();
}
private final ClassLoader resourceLoader;
ThrowawayClassLoader(ClassLoader parent) {
super(parent.getParent());
this.resourceLoader = parent;
}
@Override
protected Class<?> loadClass(String name, boolean resolve) throws ClassNotFoundException {
synchronized (getClassLoadingLock(name)) {
Class<?> loaded = findLoadedClass(name);
if (loaded != null) {
return loaded;
}
try {
return super.loadClass(name, true);
}
catch (ClassNotFoundException ex) {
return loadClassFromResource(name);
}
}
}
private Class<?> loadClassFromResource(String name) throws ClassNotFoundException, ClassFormatError {
String resourceName = name.replace('.', '/') + ".class";
InputStream inputStream = this.resourceLoader.getResourceAsStream(resourceName);
if (inputStream == null) {
return null;
}
try {
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
inputStream.transferTo(outputStream);
byte[] bytes = outputStream.toByteArray();
return defineClass(name, bytes, 0, bytes.length);
}
catch (IOException ex) {
throw new ClassNotFoundException("Cannot load resource for class [" + name + "]", ex);
}
}
@Override
protected URL findResource(String name) {
return this.resourceLoader.getResource(name);
}
}

View File

@@ -0,0 +1,5 @@
/**
* GraalVM implementation specific support which might change at any time, so not considered part of Spring Framework public API.
*/
package org.springframework.aot.graalvm;

View File

@@ -0,0 +1 @@
Args = --initialize-at-build-time=org.springframework.aot.graalvm.ThrowawayClassLoader

View File

@@ -17,6 +17,7 @@ configurations {
cglib
javapoet
objenesis
graalvm
}
task cglibRepackJar(type: ShadowJar) {
@@ -45,6 +46,7 @@ dependencies {
cglib("cglib:cglib:${cglibVersion}@jar")
javapoet("com.squareup:javapoet:${javapoetVersion}@jar")
objenesis("org.objenesis:objenesis:${objenesisVersion}@jar")
graalvm(project(path: ":spring-core-graalvm", configuration: 'classesOnlyElements'))
api(files(cglibRepackJar))
api(files(javapoetRepackJar))
api(files(objenesisRepackJar))
@@ -107,6 +109,8 @@ jar {
from(zipTree(objenesisRepackJar.archivePath)) {
include "org/springframework/objenesis/**"
}
from configurations.graalvm
}
test {

View File

@@ -1,5 +0,0 @@
Args = --initialize-at-build-time=org.springframework.aot.AotDetector,\
org.springframework.core.NativeDetector,\
org.springframework.util.ConcurrentReferenceHashMap,\
org.springframework.core.SpringProperties,\
org.springframework.util.ClassUtils