Skip to content

Commit e7013e6

Browse files
committed
Avoid too much transitive class build time initializations
Leverage a subset of @philwebb https://github.com/philwebb/scratch-graal-conditions/tree/annotations to set the value of some boolean static field at build time for native images without leveraging class build-time initialization in order to avoid related compatibility issues. Closes spring-projectsgh-28624
1 parent 16c43c2 commit e7013e6

File tree

16 files changed

+413
-15
lines changed

16 files changed

+413
-15
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,7 @@ classes/
2222
buildSrc/build
2323
/spring-*/build
2424
/spring-core/kotlin-coroutines/build
25+
/spring-core/graalvm/build
2526
/framework-bom/build
2627
/integration-tests/build
2728
/src/asciidoc/build

build.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -240,6 +240,8 @@ configure(allprojects) { project ->
240240
dependency "org.glassfish:jakarta.el:4.0.2"
241241
dependency "org.glassfish.tyrus:tyrus-container-servlet:2.0.1"
242242
dependency "org.eclipse.persistence:org.eclipse.persistence.jpa:3.0.2"
243+
244+
dependency "org.graalvm.nativeimage:svm:22.1.0.1"
243245
}
244246
generatedPomCustomization {
245247
enabled = false

framework-bom/framework-bom.gradle

+1-1
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ group = "org.springframework"
77

88
dependencies {
99
constraints {
10-
parent.moduleProjects.sort { "$it.name" }.each {
10+
parent.moduleProjects.findAll{ it.name != 'spring-core-graalvm' }.sort { "$it.name" }.each {
1111
api it
1212
}
1313
}

gradle/docs.gradle

+10-6
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,7 @@
1+
ext {
2+
documentedProjects = moduleProjects.findAll { it.name != 'spring-core-graalvm' }
3+
}
4+
15
configurations {
26
asciidoctorExt
37
}
@@ -24,7 +28,7 @@ task api(type: Javadoc) {
2428
title = "${rootProject.description} ${version} API"
2529

2630
dependsOn {
27-
moduleProjects.collect {
31+
documentedProjects.collect {
2832
it.tasks.getByName("jar")
2933
}
3034
}
@@ -33,7 +37,7 @@ task api(type: Javadoc) {
3337
// ensure the javadoc process can resolve types compiled from .aj sources
3438
project(":spring-aspects").sourceSets.main.output
3539
)
36-
classpath += files(moduleProjects.collect { it.sourceSets.main.compileClasspath })
40+
classpath += files(documentedProjects.collect { it.sourceSets.main.compileClasspath })
3741
}
3842

3943
options {
@@ -48,7 +52,7 @@ task api(type: Javadoc) {
4852
addBooleanOption('Xdoclint:syntax', true) // only check syntax with doclint
4953
addBooleanOption('Werror', true) // fail build on Javadoc warnings
5054
}
51-
source moduleProjects.collect { project ->
55+
source documentedProjects.collect { project ->
5256
project.sourceSets.main.allJava
5357
}
5458
maxMemory = "1024m"
@@ -180,7 +184,7 @@ task schemaZip(type: Zip) {
180184
description = "Builds -${archiveClassifier} archive containing all " +
181185
"XSDs for deployment at https://springframework.org/schema."
182186
duplicatesStrategy DuplicatesStrategy.EXCLUDE
183-
moduleProjects.each { module ->
187+
documentedProjects.each { module ->
184188
def Properties schemas = new Properties();
185189

186190
module.sourceSets.main.resources.find {
@@ -230,7 +234,7 @@ task distZip(type: Zip, dependsOn: [docsZip, schemaZip]) {
230234
into "${baseDir}/schema"
231235
}
232236

233-
moduleProjects.each { module ->
237+
documentedProjects.each { module ->
234238
into ("${baseDir}/libs") {
235239
from module.jar
236240
if (module.tasks.findByPath("sourcesJar")) {
@@ -243,4 +247,4 @@ task distZip(type: Zip, dependsOn: [docsZip, schemaZip]) {
243247
}
244248
}
245249

246-
distZip.mustRunAfter moduleProjects.check
250+
distZip.mustRunAfter documentedProjects.check

settings.gradle

+2
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,8 @@ include "spring-context"
1818
include "spring-context-indexer"
1919
include "spring-context-support"
2020
include "spring-core"
21+
include "spring-core-graalvm"
22+
project(':spring-core-graalvm').projectDir = file('spring-core/graalvm')
2123
include "spring-core-test"
2224
include "spring-expression"
2325
include "spring-instrument"
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
description = "Spring Core GraalVM feature"
2+
3+
configurations {
4+
classesOnlyElements {
5+
canBeConsumed = true
6+
canBeResolved = false
7+
}
8+
}
9+
10+
artifacts {
11+
classesOnlyElements(compileJava.destinationDirectory)
12+
classesOnlyElements(sourceSets.main.resources.srcDirs)
13+
}
14+
15+
tasks.withType(JavaCompile) {
16+
options.compilerArgs += [
17+
"--add-modules",
18+
"jdk.internal.vm.ci",
19+
"--add-exports",
20+
"jdk.internal.vm.ci/jdk.vm.ci.meta=ALL-UNNAMED"
21+
]
22+
}
23+
24+
eclipse.classpath.file {
25+
whenMerged {
26+
entries.find{ it.path ==~ '.*JRE_CONTAINER.*' }.each {
27+
it.entryAttributes['module'] = true
28+
it.entryAttributes['add-exports'] = 'jdk.internal.vm.ci/jdk.vm.ci.meta=ALL-UNNAMED'
29+
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'
30+
}
31+
}
32+
}
33+
34+
dependencies {
35+
compileOnly("org.graalvm.nativeimage:svm")
36+
}
37+
38+
tasks.withType(PublishToMavenRepository) {
39+
enabled = false
40+
}
41+
42+
tasks.withType(PublishToMavenLocal) {
43+
enabled = false
44+
}
45+
46+
tasks.withType(Javadoc) {
47+
enabled = false
48+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.aot.graalvm;
18+
19+
import com.oracle.svm.core.annotate.AutomaticFeature;
20+
import com.oracle.svm.hosted.FeatureImpl.DuringSetupAccessImpl;
21+
import org.graalvm.compiler.debug.DebugContext;
22+
import org.graalvm.nativeimage.hosted.Feature;
23+
24+
/**
25+
* GraalVM {@link Feature} that substitutes field values that match a certain pattern
26+
* with constants without causing build-time initialization.
27+
*
28+
* @author Phillip Webb
29+
* @author Sebastien Deleuze
30+
* @since 6.0
31+
*/
32+
@AutomaticFeature
33+
class ConstantFieldFeature implements Feature {
34+
35+
@Override
36+
public void duringSetup(DuringSetupAccess access) {
37+
duringSetup((DuringSetupAccessImpl) access);
38+
}
39+
40+
private void duringSetup(DuringSetupAccessImpl access) {
41+
DebugContext debug = access.getDebugContext();
42+
try (DebugContext.Scope scope = debug.scope("ConstantFieldFeature.duringSetup")) {
43+
debug.log("Installing constant field substitution processor : " + scope);
44+
ClassLoader applicationClassLoader = access.getApplicationClassLoader();
45+
ConstantFieldSubstitutionProcessor substitutionProcessor =
46+
new ConstantFieldSubstitutionProcessor(debug, applicationClassLoader);
47+
access.registerSubstitutionProcessor(substitutionProcessor);
48+
}
49+
}
50+
51+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
/*
2+
* Copyright 2002-2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.aot.graalvm;
18+
19+
import java.lang.reflect.Field;
20+
import java.util.regex.Pattern;
21+
22+
import com.oracle.graal.pointsto.infrastructure.SubstitutionProcessor;
23+
import com.oracle.svm.core.meta.SubstrateObjectConstant;
24+
import com.oracle.svm.core.util.UserError;
25+
import jdk.vm.ci.meta.JavaConstant;
26+
import jdk.vm.ci.meta.JavaKind;
27+
import jdk.vm.ci.meta.ResolvedJavaField;
28+
import jdk.vm.ci.meta.ResolvedJavaType;
29+
import org.graalvm.compiler.debug.DebugContext;
30+
31+
/**
32+
* {@link SubstitutionProcessor} to compute at build time the value of the
33+
* boolean static fields identified by {@link #patterns} in order to allow
34+
* efficient code shrinking without using class build time initialization.
35+
*
36+
* @author Phillip Webb
37+
* @author Sebastien Deleuze
38+
* @since 6.0
39+
*/
40+
class ConstantFieldSubstitutionProcessor extends SubstitutionProcessor {
41+
42+
// Later should be an explicit signal, like an annotation or even a Java keyword
43+
private static Pattern[] patterns = {
44+
Pattern.compile(Pattern.quote("org.springframework.core.NativeDetector#imageCode")),
45+
Pattern.compile(Pattern.quote("org.springframework.") + ".*#.*Present"),
46+
};
47+
48+
private final ThrowawayClassLoader throwawayClassLoader;
49+
50+
51+
ConstantFieldSubstitutionProcessor(DebugContext debug, ClassLoader applicationClassLoader) {
52+
this.throwawayClassLoader = new ThrowawayClassLoader(applicationClassLoader);
53+
}
54+
55+
56+
@Override
57+
public ResolvedJavaField lookup(ResolvedJavaField field) {
58+
ResolvedJavaType declaringClass = field.getDeclaringClass();
59+
if (field.getType().getJavaKind() == JavaKind.Boolean && field.isStatic()) {
60+
String fieldIdentifier = declaringClass.toJavaName() + "#" + field.getName();
61+
for (Pattern pattern : patterns) {
62+
if (pattern.matcher(fieldIdentifier).matches()) {
63+
JavaConstant constant = lookupConstant(declaringClass.toJavaName(), field.getName());
64+
if (constant != null) {
65+
// TODO Use proper logging only when --verbose is specified when https://github.com/oracle/graal/issues/4669 will be fixed
66+
System.out.println("Field " + fieldIdentifier + " set to " + constant.toValueString() + " at build time");
67+
return new ConstantReadableJavaField(field, constant);
68+
}
69+
}
70+
}
71+
}
72+
return super.lookup(field);
73+
}
74+
75+
private JavaConstant lookupConstant(String className, String fieldName) {
76+
try {
77+
Class<?> throwawayClass = this.throwawayClassLoader.loadClass(className);
78+
Field field = throwawayClass.getDeclaredField(fieldName);
79+
field.setAccessible(true);
80+
Object value = field.get(null);
81+
if (!(value instanceof Boolean)) {
82+
throw UserError.abort("Unable to get the value of " + className + "." + fieldName);
83+
}
84+
return SubstrateObjectConstant.forBoxedValue(JavaKind.Boolean, value);
85+
}
86+
catch (Exception ex) {
87+
throw new IllegalStateException("Unable to read value from " + className + "." + fieldName, ex);
88+
}
89+
}
90+
91+
}

0 commit comments

Comments
 (0)