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

1
.gitignore vendored
View File

@ -22,6 +22,7 @@ classes/
buildSrc/build
/spring-*/build
/spring-core/kotlin-coroutines/build
/spring-core/graalvm/build
/framework-bom/build
/integration-tests/build
/src/asciidoc/build

View File

@ -240,6 +240,8 @@ configure(allprojects) { project ->
dependency "org.glassfish:jakarta.el:4.0.2"
dependency "org.glassfish.tyrus:tyrus-container-servlet:2.0.1"
dependency "org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.2"
dependency "org.graalvm.nativeimage:svm:22.1.0.1"
}
generatedPomCustomization {
enabled = false

View File

@ -7,7 +7,7 @@ group = "org.springframework"
dependencies {
constraints {
parent.moduleProjects.sort { "$it.name" }.each {
parent.moduleProjects.findAll{ it.name != 'spring-core-graalvm' }.sort { "$it.name" }.each {
api it
}
}

View File

@ -1,3 +1,7 @@
ext {
documentedProjects = moduleProjects.findAll { it.name != 'spring-core-graalvm' }
}
configurations {
asciidoctorExt
}
@ -24,7 +28,7 @@ task api(type: Javadoc) {
title = "${rootProject.description} ${version} API"
dependsOn {
moduleProjects.collect {
documentedProjects.collect {
it.tasks.getByName("jar")
}
}
@ -33,7 +37,7 @@ task api(type: Javadoc) {
// ensure the javadoc process can resolve types compiled from .aj sources
project(":spring-aspects").sourceSets.main.output
)
classpath += files(moduleProjects.collect { it.sourceSets.main.compileClasspath })
classpath += files(documentedProjects.collect { it.sourceSets.main.compileClasspath })
}
options {
@ -48,7 +52,7 @@ task api(type: Javadoc) {
addBooleanOption('Xdoclint:syntax', true) // only check syntax with doclint
addBooleanOption('Werror', true) // fail build on Javadoc warnings
}
source moduleProjects.collect { project ->
source documentedProjects.collect { project ->
project.sourceSets.main.allJava
}
maxMemory = "1024m"
@ -180,7 +184,7 @@ task schemaZip(type: Zip) {
description = "Builds -${archiveClassifier} archive containing all " +
"XSDs for deployment at https://springframework.org/schema."
duplicatesStrategy DuplicatesStrategy.EXCLUDE
moduleProjects.each { module ->
documentedProjects.each { module ->
def Properties schemas = new Properties();
module.sourceSets.main.resources.find {
@ -230,7 +234,7 @@ task distZip(type: Zip, dependsOn: [docsZip, schemaZip]) {
into "${baseDir}/schema"
}
moduleProjects.each { module ->
documentedProjects.each { module ->
into ("${baseDir}/libs") {
from module.jar
if (module.tasks.findByPath("sourcesJar")) {
@ -243,4 +247,4 @@ task distZip(type: Zip, dependsOn: [docsZip, schemaZip]) {
}
}
distZip.mustRunAfter moduleProjects.check
distZip.mustRunAfter documentedProjects.check

View File

@ -18,6 +18,8 @@ include "spring-context"
include "spring-context-indexer"
include "spring-context-support"
include "spring-core"
include "spring-core-graalvm"
project(':spring-core-graalvm').projectDir = file('spring-core/graalvm')
include "spring-core-test"
include "spring-expression"
include "spring-instrument"

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

View File

@ -1,2 +0,0 @@
Args = --initialize-at-build-time=org.springframework.http.HttpStatus,\
org.springframework.web.client.RestTemplate

View File

@ -1 +0,0 @@
Args = --initialize-at-build-time=org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport