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:
parent
dc4ae559c5
commit
22a750f378
|
@ -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
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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"
|
||||
|
|
|
@ -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
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
|
|
@ -0,0 +1 @@
|
|||
Args = --initialize-at-build-time=org.springframework.aot.graalvm.ThrowawayClassLoader
|
|
@ -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 {
|
||||
|
|
|
@ -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
|
|
@ -1,2 +0,0 @@
|
|||
Args = --initialize-at-build-time=org.springframework.http.HttpStatus,\
|
||||
org.springframework.web.client.RestTemplate
|
|
@ -1 +0,0 @@
|
|||
Args = --initialize-at-build-time=org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport
|
Loading…
Reference in New Issue