From 22a750f378fcdf9d4baab721ec92952280afa207 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20Deleuze?= Date: Wed, 22 Jun 2022 12:17:40 +0200 Subject: [PATCH] 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 --- .gitignore | 1 + build.gradle | 2 + framework-bom/framework-bom.gradle | 2 +- gradle/docs.gradle | 16 ++- settings.gradle | 2 + .../graalvm/spring-core-graalvm.gradle | 48 ++++++++ .../aot/graalvm/ConstantFieldFeature.java | 51 ++++++++ .../ConstantFieldSubstitutionProcessor.java | 91 ++++++++++++++ .../graalvm/ConstantReadableJavaField.java | 112 ++++++++++++++++++ .../aot/graalvm/ThrowawayClassLoader.java | 85 +++++++++++++ .../aot/graalvm/package-info.java | 5 + .../spring-core/native-image.properties | 1 + spring-core/spring-core.gradle | 4 + .../spring-core/native-image.properties | 5 - .../spring-web/native-image.properties | 2 - .../spring-webmvc/native-image.properties | 1 - 16 files changed, 413 insertions(+), 15 deletions(-) create mode 100644 spring-core/graalvm/spring-core-graalvm.gradle create mode 100644 spring-core/graalvm/src/main/java/org/springframework/aot/graalvm/ConstantFieldFeature.java create mode 100644 spring-core/graalvm/src/main/java/org/springframework/aot/graalvm/ConstantFieldSubstitutionProcessor.java create mode 100644 spring-core/graalvm/src/main/java/org/springframework/aot/graalvm/ConstantReadableJavaField.java create mode 100644 spring-core/graalvm/src/main/java/org/springframework/aot/graalvm/ThrowawayClassLoader.java create mode 100644 spring-core/graalvm/src/main/java/org/springframework/aot/graalvm/package-info.java create mode 100644 spring-core/graalvm/src/main/resources/META-INF/native-image/org.springframework/spring-core/native-image.properties delete mode 100644 spring-core/src/main/resources/META-INF/native-image/org.springframework/spring-core/native-image.properties delete mode 100644 spring-web/src/main/resources/META-INF/native-image/org.springframework/spring-web/native-image.properties delete mode 100644 spring-webmvc/src/main/resources/META-INF/native-image/org.springframework/spring-webmvc/native-image.properties diff --git a/.gitignore b/.gitignore index b08c4536d7..f13180d605 100644 --- a/.gitignore +++ b/.gitignore @@ -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 diff --git a/build.gradle b/build.gradle index 37529ab214..33d5c96019 100644 --- a/build.gradle +++ b/build.gradle @@ -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 diff --git a/framework-bom/framework-bom.gradle b/framework-bom/framework-bom.gradle index 840f20537f..30c380c61d 100644 --- a/framework-bom/framework-bom.gradle +++ b/framework-bom/framework-bom.gradle @@ -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 } } diff --git a/gradle/docs.gradle b/gradle/docs.gradle index 470c854cc9..d51206403b 100644 --- a/gradle/docs.gradle +++ b/gradle/docs.gradle @@ -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 diff --git a/settings.gradle b/settings.gradle index 0fef14947e..403fb4f175 100644 --- a/settings.gradle +++ b/settings.gradle @@ -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" diff --git a/spring-core/graalvm/spring-core-graalvm.gradle b/spring-core/graalvm/spring-core-graalvm.gradle new file mode 100644 index 0000000000..58319b2de5 --- /dev/null +++ b/spring-core/graalvm/spring-core-graalvm.gradle @@ -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 +} \ No newline at end of file diff --git a/spring-core/graalvm/src/main/java/org/springframework/aot/graalvm/ConstantFieldFeature.java b/spring-core/graalvm/src/main/java/org/springframework/aot/graalvm/ConstantFieldFeature.java new file mode 100644 index 0000000000..06d67ee991 --- /dev/null +++ b/spring-core/graalvm/src/main/java/org/springframework/aot/graalvm/ConstantFieldFeature.java @@ -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); + } + } + +} diff --git a/spring-core/graalvm/src/main/java/org/springframework/aot/graalvm/ConstantFieldSubstitutionProcessor.java b/spring-core/graalvm/src/main/java/org/springframework/aot/graalvm/ConstantFieldSubstitutionProcessor.java new file mode 100644 index 0000000000..5942c7b6c5 --- /dev/null +++ b/spring-core/graalvm/src/main/java/org/springframework/aot/graalvm/ConstantFieldSubstitutionProcessor.java @@ -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); + } + } + +} diff --git a/spring-core/graalvm/src/main/java/org/springframework/aot/graalvm/ConstantReadableJavaField.java b/spring-core/graalvm/src/main/java/org/springframework/aot/graalvm/ConstantReadableJavaField.java new file mode 100644 index 0000000000..6ee401d6a3 --- /dev/null +++ b/spring-core/graalvm/src/main/java/org/springframework/aot/graalvm/ConstantReadableJavaField.java @@ -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 getAnnotation(Class 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; + } + +} diff --git a/spring-core/graalvm/src/main/java/org/springframework/aot/graalvm/ThrowawayClassLoader.java b/spring-core/graalvm/src/main/java/org/springframework/aot/graalvm/ThrowawayClassLoader.java new file mode 100644 index 0000000000..01cf682be4 --- /dev/null +++ b/spring-core/graalvm/src/main/java/org/springframework/aot/graalvm/ThrowawayClassLoader.java @@ -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); + } + +} diff --git a/spring-core/graalvm/src/main/java/org/springframework/aot/graalvm/package-info.java b/spring-core/graalvm/src/main/java/org/springframework/aot/graalvm/package-info.java new file mode 100644 index 0000000000..f5128d92f9 --- /dev/null +++ b/spring-core/graalvm/src/main/java/org/springframework/aot/graalvm/package-info.java @@ -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; + diff --git a/spring-core/graalvm/src/main/resources/META-INF/native-image/org.springframework/spring-core/native-image.properties b/spring-core/graalvm/src/main/resources/META-INF/native-image/org.springframework/spring-core/native-image.properties new file mode 100644 index 0000000000..46311113d3 --- /dev/null +++ b/spring-core/graalvm/src/main/resources/META-INF/native-image/org.springframework/spring-core/native-image.properties @@ -0,0 +1 @@ +Args = --initialize-at-build-time=org.springframework.aot.graalvm.ThrowawayClassLoader \ No newline at end of file diff --git a/spring-core/spring-core.gradle b/spring-core/spring-core.gradle index 453981cae5..c9fedf4628 100644 --- a/spring-core/spring-core.gradle +++ b/spring-core/spring-core.gradle @@ -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 { diff --git a/spring-core/src/main/resources/META-INF/native-image/org.springframework/spring-core/native-image.properties b/spring-core/src/main/resources/META-INF/native-image/org.springframework/spring-core/native-image.properties deleted file mode 100644 index 6bb35ccfdb..0000000000 --- a/spring-core/src/main/resources/META-INF/native-image/org.springframework/spring-core/native-image.properties +++ /dev/null @@ -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 diff --git a/spring-web/src/main/resources/META-INF/native-image/org.springframework/spring-web/native-image.properties b/spring-web/src/main/resources/META-INF/native-image/org.springframework/spring-web/native-image.properties deleted file mode 100644 index 77c8057c5a..0000000000 --- a/spring-web/src/main/resources/META-INF/native-image/org.springframework/spring-web/native-image.properties +++ /dev/null @@ -1,2 +0,0 @@ -Args = --initialize-at-build-time=org.springframework.http.HttpStatus,\ -org.springframework.web.client.RestTemplate diff --git a/spring-webmvc/src/main/resources/META-INF/native-image/org.springframework/spring-webmvc/native-image.properties b/spring-webmvc/src/main/resources/META-INF/native-image/org.springframework/spring-webmvc/native-image.properties deleted file mode 100644 index 6e63da6d95..0000000000 --- a/spring-webmvc/src/main/resources/META-INF/native-image/org.springframework/spring-webmvc/native-image.properties +++ /dev/null @@ -1 +0,0 @@ -Args = --initialize-at-build-time=org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport