Refine GraalVM tracing agent detection

This commit refines how GraalVM tracing agent detection works
for both test and application executions.

It rolls back the introduction of TestAotDetector done in 111309605c
and instead updates AotDetector.useGeneratedArtifacts()
to only detect "buildtime" and "runtime" imagecode system
property values by leveraging a new method
NativeDetector.inNativeImage(NativeDetector.Context...).

This commit also adds a workaround for
https://github.com/oracle/graal/issues/6691.

Closes gh-30511
This commit is contained in:
Sébastien Deleuze 2023-05-25 13:50:03 +02:00
parent 8b8d147480
commit 1cf6d1dd9d
16 changed files with 107 additions and 166 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2022 the original author or authors.
* Copyright 2002-2023 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.
@ -19,11 +19,14 @@ package org.springframework.aot;
import org.springframework.core.NativeDetector;
import org.springframework.core.SpringProperties;
import static org.springframework.core.NativeDetector.Context;
/**
* Utility for determining if AOT-processed optimizations must be used rather
* than the regular runtime. Strictly for internal use within the framework.
*
* @author Stephane Nicoll
* @author Sebastien Deleuze
* @since 6.0
*/
public abstract class AotDetector {
@ -36,6 +39,8 @@ public abstract class AotDetector {
*/
public static final String AOT_ENABLED = "spring.aot.enabled";
private static final boolean inNativeImage = NativeDetector.inNativeImage(Context.RUNTIME, Context.BUILD_TIME);
/**
* Determine whether AOT optimizations must be considered at runtime. This
* is mandatory in a native image but can be triggered on the JVM using
@ -43,7 +48,7 @@ public abstract class AotDetector {
* @return whether AOT optimizations must be considered
*/
public static boolean useGeneratedArtifacts() {
return (NativeDetector.inNativeImage() || SpringProperties.getFlag(AOT_ENABLED));
return (inNativeImage || SpringProperties.getFlag(AOT_ENABLED));
}
}

View File

@ -33,8 +33,9 @@ import org.graalvm.nativeimage.hosted.Feature;
class PreComputeFieldFeature implements Feature {
private static Pattern[] patterns = {
Pattern.compile(Pattern.quote("org.springframework.core.NativeDetector#imageCode")),
Pattern.compile(Pattern.quote("org.springframework.cglib.core.AbstractClassGenerator#imageCode")),
Pattern.compile(Pattern.quote("org.springframework.core.NativeDetector#inNativeImage")),
Pattern.compile(Pattern.quote("org.springframework.cglib.core.AbstractClassGenerator#inNativeImage")),
Pattern.compile(Pattern.quote("org.springframework.aot.AotDetector#inNativeImage")),
Pattern.compile(Pattern.quote("org.springframework.") + ".*#.*Present"),
Pattern.compile(Pattern.quote("org.springframework.") + ".*#.*PRESENT"),
Pattern.compile(Pattern.quote("reactor.") + ".*#.*Available"),

View File

@ -43,8 +43,12 @@ abstract public class AbstractClassGenerator<T> implements ClassGenerator {
private static final boolean DEFAULT_USE_CACHE =
Boolean.parseBoolean(System.getProperty("cglib.useCache", "true"));
// See https://github.com/oracle/graal/blob/master/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/ImageInfo.java
private static final boolean imageCode = (System.getProperty("org.graalvm.nativeimage.imagecode") != null);
private static final boolean inNativeImage;
static {
String imageCode = System.getProperty("org.graalvm.nativeimage.imagecode");
inNativeImage = "buildtime".equals(imageCode) || "runtime".equals(imageCode);
}
private GeneratorStrategy strategy = DefaultGeneratorStrategy.INSTANCE;
@ -354,7 +358,7 @@ abstract public class AbstractClassGenerator<T> implements ClassGenerator {
}
}
// SPRING PATCH BEGIN
if (imageCode) {
if (inNativeImage) {
throw new UnsupportedOperationException("CGLIB runtime enhancement not supported on native image. " +
"Make sure to include a pre-generated class on the classpath instead: " + getClassName());
}

View File

@ -16,6 +16,8 @@
package org.springframework.core;
import org.springframework.lang.Nullable;
/**
* A common delegate for detecting a GraalVM native image environment.
*
@ -25,12 +27,61 @@ package org.springframework.core;
public abstract class NativeDetector {
// See https://github.com/oracle/graal/blob/master/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/ImageInfo.java
private static final boolean imageCode = (System.getProperty("org.graalvm.nativeimage.imagecode") != null);
@Nullable
private static final String imageCode = System.getProperty("org.graalvm.nativeimage.imagecode");
private static final boolean inNativeImage = (imageCode != null);
/**
* Returns {@code true} if invoked in the context of image building or during image runtime, else {@code false}.
* Returns {@code true} if running in a native image context (for example {@code buildtime}, {@code runtime} or
* {@code agent}) expressed by setting {@code org.graalvm.nativeimage.imagecode} system property to any value, else {@code false}.
*/
public static boolean inNativeImage() {
return imageCode;
return inNativeImage;
}
/**
* Returns {@code true} if running in any of the specified native image context(s), else {@code false}.
* @param contexts the native image context(s)
* @since 6.0.10
*/
public static boolean inNativeImage(Context... contexts) {
for (Context context: contexts) {
if (context.key.equals(imageCode)) {
return true;
}
}
return false;
}
/**
* Native image context as defined in
* <a href="https://github.com/oracle/graal/blob/master/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/ImageInfo.java">ImageInfo.java</a>.
*
* @since 6.0.10
*/
public enum Context {
/**
* The code is executing in the context of image building.
*/
BUILD_TIME("buildtime"),
/**
* The code is executing at image runtime.
*/
RUNTIME("runtime");
private final String key;
Context(final String key) {
this.key = key;
}
@Override
public String toString() {
return this.key;
}
}
}

View File

@ -16,6 +16,7 @@
package org.springframework.test.context.aot;
import org.springframework.aot.AotDetector;
import org.springframework.lang.Nullable;
/**
@ -26,7 +27,7 @@ import org.springframework.lang.Nullable;
* and run-time. At build time, test components can {@linkplain #setAttribute contribute}
* attributes during the AOT processing phase. At run time, test components can
* {@linkplain #getString(String) retrieve} attributes that were contributed at
* build time. If {@link TestAotDetector#useGeneratedArtifacts()} returns {@code true},
* build time. If {@link AotDetector#useGeneratedArtifacts()} returns {@code true},
* run-time mode applies.
*
* <p>For example, if a test component computes something at build time that
@ -43,7 +44,7 @@ import org.springframework.lang.Nullable;
* &mdash; can choose to contribute an attribute at any point in time. Note that
* contributing an attribute during standard JVM test execution will not have any
* adverse side effect since AOT attributes will be ignored in that scenario. In
* any case, you should use {@link TestAotDetector#useGeneratedArtifacts()} to determine
* any case, you should use {@link AotDetector#useGeneratedArtifacts()} to determine
* if invocations of {@link #setAttribute(String, String)} and
* {@link #removeAttribute(String)} are permitted.
*
@ -70,12 +71,12 @@ public interface AotTestAttributes {
* @param name the unique attribute name
* @param value the associated attribute value
* @throws UnsupportedOperationException if invoked during
* {@linkplain TestAotDetector#useGeneratedArtifacts() AOT run-time execution}
* {@linkplain AotDetector#useGeneratedArtifacts() AOT run-time execution}
* @throws IllegalArgumentException if the provided value is {@code null} or
* if an attempt is made to override an existing attribute
* @see #setAttribute(String, boolean)
* @see #removeAttribute(String)
* @see TestAotDetector#useGeneratedArtifacts()
* @see AotDetector#useGeneratedArtifacts()
*/
void setAttribute(String name, String value);
@ -87,13 +88,13 @@ public interface AotTestAttributes {
* @param name the unique attribute name
* @param value the associated attribute value
* @throws UnsupportedOperationException if invoked during
* {@linkplain TestAotDetector#useGeneratedArtifacts() AOT run-time execution}
* {@linkplain AotDetector#useGeneratedArtifacts() AOT run-time execution}
* @throws IllegalArgumentException if an attempt is made to override an
* existing attribute
* @see #setAttribute(String, String)
* @see #removeAttribute(String)
* @see Boolean#toString(boolean)
* @see TestAotDetector#useGeneratedArtifacts()
* @see AotDetector#useGeneratedArtifacts()
*/
default void setAttribute(String name, boolean value) {
setAttribute(name, Boolean.toString(value));
@ -103,8 +104,8 @@ public interface AotTestAttributes {
* Remove the attribute stored under the provided name.
* @param name the unique attribute name
* @throws UnsupportedOperationException if invoked during
* {@linkplain TestAotDetector#useGeneratedArtifacts() AOT run-time execution}
* @see TestAotDetector#useGeneratedArtifacts()
* {@linkplain AotDetector#useGeneratedArtifacts() AOT run-time execution}
* @see AotDetector#useGeneratedArtifacts()
* @see #setAttribute(String, String)
*/
void removeAttribute(String name);

View File

@ -19,6 +19,7 @@ package org.springframework.test.context.aot;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import org.springframework.aot.AotDetector;
import org.springframework.lang.Nullable;
/**
@ -39,7 +40,7 @@ final class AotTestAttributesFactory {
/**
* Get the underlying attributes map.
* <p>If the map is not already loaded, this method loads the map from the
* generated class when running in {@linkplain TestAotDetector#useGeneratedArtifacts()
* generated class when running in {@linkplain AotDetector#useGeneratedArtifacts()
* AOT execution mode} and otherwise creates a new map for storing attributes
* during the AOT processing phase.
*/
@ -49,7 +50,7 @@ final class AotTestAttributesFactory {
synchronized (AotTestAttributesFactory.class) {
attrs = attributes;
if (attrs == null) {
attrs = (TestAotDetector.useGeneratedArtifacts() ? loadAttributesMap() : new ConcurrentHashMap<>());
attrs = (AotDetector.useGeneratedArtifacts() ? loadAttributesMap() : new ConcurrentHashMap<>());
attributes = attrs;
}
}

View File

@ -19,6 +19,7 @@ package org.springframework.test.context.aot;
import java.util.Map;
import java.util.function.Supplier;
import org.springframework.aot.AotDetector;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.lang.Nullable;
@ -29,7 +30,7 @@ import org.springframework.lang.Nullable;
*
* <p>Intended solely for internal use within the framework.
*
* <p>If we are not running in {@linkplain TestAotDetector#useGeneratedArtifacts()
* <p>If we are not running in {@linkplain AotDetector#useGeneratedArtifacts()
* AOT mode} or if a test class is not {@linkplain #isSupportedTestClass(Class)
* supported} in AOT mode, {@link #getContextInitializer(Class)} and
* {@link #getContextInitializerClass(Class)} will return {@code null}.

View File

@ -19,6 +19,7 @@ package org.springframework.test.context.aot;
import java.util.Map;
import java.util.function.Supplier;
import org.springframework.aot.AotDetector;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.lang.Nullable;
@ -44,7 +45,7 @@ final class AotTestContextInitializersFactory {
/**
* Get the underlying map.
* <p>If the map is not already loaded, this method loads the map from the
* generated class when running in {@linkplain TestAotDetector#useGeneratedArtifacts()
* generated class when running in {@linkplain AotDetector#useGeneratedArtifacts()
* AOT execution mode} and otherwise creates an immutable, empty map.
*/
static Map<String, Supplier<ApplicationContextInitializer<ConfigurableApplicationContext>>> getContextInitializers() {
@ -53,7 +54,7 @@ final class AotTestContextInitializersFactory {
synchronized (AotTestContextInitializersFactory.class) {
initializers = contextInitializers;
if (initializers == null) {
initializers = (TestAotDetector.useGeneratedArtifacts() ? loadContextInitializersMap() : Map.of());
initializers = (AotDetector.useGeneratedArtifacts() ? loadContextInitializersMap() : Map.of());
contextInitializers = initializers;
}
}
@ -67,7 +68,7 @@ final class AotTestContextInitializersFactory {
synchronized (AotTestContextInitializersFactory.class) {
initializerClasses = contextInitializerClasses;
if (initializerClasses == null) {
initializerClasses = (TestAotDetector.useGeneratedArtifacts() ? loadContextInitializerClassesMap() : Map.of());
initializerClasses = (AotDetector.useGeneratedArtifacts() ? loadContextInitializerClassesMap() : Map.of());
contextInitializerClasses = initializerClasses;
}
}

View File

@ -18,6 +18,7 @@ package org.springframework.test.context.aot;
import java.util.Map;
import org.springframework.aot.AotDetector;
import org.springframework.lang.Nullable;
import org.springframework.util.Assert;
@ -60,7 +61,7 @@ class DefaultAotTestAttributes implements AotTestAttributes {
private static void assertNotInAotRuntime() {
if (TestAotDetector.useGeneratedArtifacts()) {
if (AotDetector.useGeneratedArtifacts()) {
throw new UnsupportedOperationException(
"AOT attributes cannot be modified during AOT run-time execution");
}

View File

@ -1,58 +0,0 @@
/*
* Copyright 2002-2023 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.test.context.aot;
import org.springframework.aot.AotDetector;
import org.springframework.core.SpringProperties;
import org.springframework.util.StringUtils;
/**
* TestContext framework specific utility for determining if AOT-processed
* optimizations must be used rather than the regular runtime.
*
* <p>Strictly for internal use within the framework.
*
* @author Sam Brannen
* @since 6.0.9
*/
public abstract class TestAotDetector {
/**
* Determine whether AOT optimizations must be considered at runtime.
* <p>This can be triggered using the {@value AotDetector#AOT_ENABLED}
* Spring property or via GraalVM's {@code "org.graalvm.nativeimage.imagecode"}
* JVM system property (if set to any non-empty value other than {@code agent}).
* @return {@code true} if AOT optimizations must be considered
* @see <a href="https://github.com/oracle/graal/blob/master/sdk/src/org.graalvm.nativeimage/src/org/graalvm/nativeimage/ImageInfo.java">GraalVM's ImageInfo.java</a>
* @see AotDetector#useGeneratedArtifacts()
*/
public static boolean useGeneratedArtifacts() {
return (SpringProperties.getFlag(AotDetector.AOT_ENABLED) || inNativeImage());
}
/**
* Determine if we are currently running within a GraalVM native image from
* the perspective of the TestContext framework.
* @return {@code true} if the {@code org.graalvm.nativeimage.imagecode} JVM
* system property has been set to any value other than {@code agent}.
*/
private static boolean inNativeImage() {
String imageCode = System.getProperty("org.graalvm.nativeimage.imagecode");
return (StringUtils.hasText(imageCode) && !"agent".equalsIgnoreCase(imageCode.trim()));
}
}

View File

@ -26,6 +26,7 @@ import java.util.stream.Stream;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aot.AotDetector;
import org.springframework.aot.generate.ClassNameGenerator;
import org.springframework.aot.generate.DefaultGenerationContext;
import org.springframework.aot.generate.GeneratedClasses;
@ -122,7 +123,7 @@ public class TestContextAotGenerator {
* @throws TestContextAotException if an error occurs during AOT processing
*/
public void processAheadOfTime(Stream<Class<?>> testClasses) throws TestContextAotException {
Assert.state(!TestAotDetector.useGeneratedArtifacts(), "Cannot perform AOT processing during AOT run-time execution");
Assert.state(!AotDetector.useGeneratedArtifacts(), "Cannot perform AOT processing during AOT run-time execution");
try {
resetAotFactories();

View File

@ -21,6 +21,7 @@ import java.util.List;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.aot.AotDetector;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextInitializer;
import org.springframework.context.ConfigurableApplicationContext;
@ -35,7 +36,6 @@ import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.SmartContextLoader;
import org.springframework.test.context.aot.AotContextLoader;
import org.springframework.test.context.aot.AotTestContextInitializers;
import org.springframework.test.context.aot.TestAotDetector;
import org.springframework.test.context.aot.TestContextAotException;
import org.springframework.test.context.util.TestContextSpringFactoriesUtils;
import org.springframework.util.Assert;
@ -248,7 +248,7 @@ public class DefaultCacheAwareContextLoaderDelegate implements CacheAwareContext
*/
@SuppressWarnings("unchecked")
private MergedContextConfiguration replaceIfNecessary(MergedContextConfiguration mergedConfig) {
if (TestAotDetector.useGeneratedArtifacts()) {
if (AotDetector.useGeneratedArtifacts()) {
Class<?> testClass = mergedConfig.getTestClass();
Class<? extends ApplicationContextInitializer<?>> contextInitializerClass =
this.aotTestContextInitializers.getContextInitializerClass(testClass);

View File

@ -21,6 +21,7 @@ import java.lang.reflect.InvocationTargetException;
import org.apache.commons.logging.Log;
import org.apache.commons.logging.LogFactory;
import org.springframework.core.NativeDetector;
import org.springframework.core.io.support.SpringFactoriesLoader.FailureHandler;
/**
@ -53,6 +54,13 @@ class TestContextFailureHandler implements FailureHandler {
available.""".formatted(factoryType.getSimpleName(), factoryImplementationName), ex);
}
}
// Workaround for https://github.com/oracle/graal/issues/6691
else if (NativeDetector.inNativeImage() && ex instanceof IllegalStateException) {
if (logger.isDebugEnabled()) {
logger.debug("Skipping candidate %1$s [%2$s] due to an error when loading it in a native image."
.formatted(factoryType.getSimpleName(), factoryImplementationName));
}
}
else {
if (ex instanceof RuntimeException runtimeException) {
throw runtimeException;

View File

@ -1,76 +0,0 @@
/*
* Copyright 2002-2023 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.test.context.aot;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.params.ParameterizedTest;
import org.junit.jupiter.params.provider.CsvSource;
import org.springframework.aot.generate.InMemoryGeneratedFiles;
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
import static org.assertj.core.api.Assertions.assertThatNoException;
/**
* Tests for error cases in {@link TestContextAotGenerator}.
*
* @author Sam Brannen
* @since 6.0.9
*/
class TestContextAotGeneratorErrorCaseTests {
@ParameterizedTest
@CsvSource(delimiter = '=', textBlock = """
'spring.aot.enabled' = 'true'
'org.graalvm.nativeimage.imagecode' = 'buildtime'
'org.graalvm.nativeimage.imagecode' = 'runtime'
'org.graalvm.nativeimage.imagecode' = 'bogus'
""")
void attemptToProcessWhileRunningInAotMode(String property, String value) {
try {
System.setProperty(property, value);
assertThatIllegalStateException()
.isThrownBy(() -> generator().processAheadOfTime(Stream.empty()))
.withMessage("Cannot perform AOT processing during AOT run-time execution");
}
finally {
System.clearProperty(property);
}
}
@Test
void attemptToProcessWhileRunningInGraalVmNativeBuildToolsAgentMode() {
final String IMAGECODE = "org.graalvm.nativeimage.imagecode";
try {
System.setProperty(IMAGECODE, "AgenT");
assertThatNoException().isThrownBy(() -> generator().processAheadOfTime(Stream.empty()));
}
finally {
System.clearProperty(IMAGECODE);
}
}
private static TestContextAotGenerator generator() {
InMemoryGeneratedFiles generatedFiles = new InMemoryGeneratedFiles();
return new TestContextAotGenerator(generatedFiles);
}
}

View File

@ -18,6 +18,7 @@ package org.springframework.test.context.aot.samples.basic;
import org.junit.runner.RunWith;
import org.springframework.aot.AotDetector;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.ApplicationContext;
@ -27,7 +28,6 @@ import org.springframework.test.context.ContextLoader;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.TestPropertySource;
import org.springframework.test.context.aot.AotTestAttributes;
import org.springframework.test.context.aot.TestAotDetector;
import org.springframework.test.context.aot.samples.basic.BasicSpringVintageTests.CustomXmlBootstrapper;
import org.springframework.test.context.aot.samples.common.MessageService;
import org.springframework.test.context.junit4.SpringRunner;
@ -78,7 +78,7 @@ public class BasicSpringVintageTests {
String booleanKey1 = "@SpringBootConfiguration-" + mergedConfig.getTestClass().getName() + "-active1";
String booleanKey2 = "@SpringBootConfiguration-" + mergedConfig.getTestClass().getName() + "-active2";
AotTestAttributes aotAttributes = AotTestAttributes.getInstance();
if (TestAotDetector.useGeneratedArtifacts()) {
if (AotDetector.useGeneratedArtifacts()) {
assertThat(aotAttributes.getString(stringKey))
.as("AOT String attribute must already be present during AOT run-time execution")
.isEqualTo("org.example.Main");

View File

@ -19,6 +19,7 @@ package org.springframework.test.context.aot.samples.basic;
import java.util.Arrays;
import java.util.List;
import org.springframework.aot.AotDetector;
import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.context.annotation.AnnotatedBeanDefinitionReader;
import org.springframework.context.annotation.Import;
@ -27,7 +28,6 @@ import org.springframework.test.context.ContextConfigurationAttributes;
import org.springframework.test.context.ContextCustomizer;
import org.springframework.test.context.ContextCustomizerFactory;
import org.springframework.test.context.MergedContextConfiguration;
import org.springframework.test.context.aot.TestAotDetector;
/**
* Emulates {@code ImportsContextCustomizerFactory} from Spring Boot's testing support.
@ -41,7 +41,7 @@ class ImportsContextCustomizerFactory implements ContextCustomizerFactory {
public ContextCustomizer createContextCustomizer(Class<?> testClass,
List<ContextConfigurationAttributes> configAttributes) {
if (TestAotDetector.useGeneratedArtifacts()) {
if (AotDetector.useGeneratedArtifacts()) {
return null;
}
if (testClass.getName().startsWith("org.springframework.test.context.aot.samples") &&