Expand hints conditions support in RuntimeHints

Prior to this commit, the hints conditions were only supported for the
`TypeHint` case. GraalVM generally expanded this concept to all hints
and we should do the same.

Right now, only the `typeReachable` condition is available but we should
design for possible future additions.

This commit introduces a new `ConditionalHint` contract implemented by
all hints compatible with this approach. The condition information is
also used in all configuration writers as a result.

Closes gh-28126
This commit is contained in:
Brian Clozel
2022-06-02 11:12:09 +02:00
parent 2269c0063a
commit 8af1496b37
29 changed files with 806 additions and 275 deletions

View File

@@ -32,24 +32,25 @@ import static org.assertj.core.api.Assertions.assertThat;
* Tests for {@link ClassProxyHint}.
*
* @author Stephane Nicoll
* @author Brian Clozel
*/
class ClassProxyHintTests {
@Test
void equalsWithWithSameInstanceIsTrue() {
void equalsWithSameInstanceIsTrue() {
ClassProxyHint hint = ClassProxyHint.of(Properties.class).build();
assertThat(hint).isEqualTo(hint);
}
@Test
void equalsWithWithSameTargetClassIsTrue() {
void equalsWithSameTargetClassIsTrue() {
ClassProxyHint first = ClassProxyHint.of(Properties.class).build();
ClassProxyHint second = ClassProxyHint.of(TypeReference.of(Properties.class)).build();
assertThat(first).isEqualTo(second);
}
@Test
void equalsWithWithSameProxiedInterfacesIsTrue() {
void equalsWithSameProxiedInterfacesIsTrue() {
ClassProxyHint first = ClassProxyHint.of(Properties.class)
.proxiedInterfaces(Serializable.class).build();
ClassProxyHint second = ClassProxyHint.of(Properties.class)
@@ -58,14 +59,14 @@ class ClassProxyHintTests {
}
@Test
void equalsWithWithDifferentTargetClassIsFalse() {
void equalsWithDifferentTargetClassIsFalse() {
ClassProxyHint first = ClassProxyHint.of(Properties.class).build();
ClassProxyHint second = ClassProxyHint.of(Hashtable.class).build();
assertThat(first).isNotEqualTo(second);
}
@Test
void equalsWithWithSameProxiedInterfacesDifferentOrderIsFalse() {
void equalsWithSameProxiedInterfacesDifferentOrderIsFalse() {
ClassProxyHint first = ClassProxyHint.of(Properties.class)
.proxiedInterfaces(Serializable.class, Closeable.class).build();
ClassProxyHint second = ClassProxyHint.of(Properties.class)
@@ -75,7 +76,7 @@ class ClassProxyHintTests {
}
@Test
void equalsWithWithDifferentProxiedInterfacesIsFalse() {
void equalsWithDifferentProxiedInterfacesIsFalse() {
ClassProxyHint first = ClassProxyHint.of(Properties.class)
.proxiedInterfaces(Serializable.class).build();
ClassProxyHint second = ClassProxyHint.of(Properties.class)
@@ -90,4 +91,11 @@ class ClassProxyHintTests {
assertThat(first).isNotEqualTo(second);
}
@Test
void equalsWithDifferentConditionIsFalse() {
ClassProxyHint first = ClassProxyHint.of(Properties.class).build();
ClassProxyHint second = ClassProxyHint.of(Properties.class).onReachableType(TypeReference.of("org.example.test")).build();
assertThat(first).isNotEqualTo(second);
}
}

View File

@@ -33,13 +33,13 @@ import static org.assertj.core.api.Assertions.assertThat;
class JdkProxyHintTests {
@Test
void equalsWithWithSameInstanceIsTrue() {
void equalsWithSameInstanceIsTrue() {
JdkProxyHint hint = new Builder().proxiedInterfaces(Function.class, Consumer.class).build();
assertThat(hint).isEqualTo(hint);
}
@Test
void equalsWithWithSameProxiedInterfacesIsTrue() {
void equalsWithSameProxiedInterfacesIsTrue() {
JdkProxyHint first = new Builder().proxiedInterfaces(Function.class, Consumer.class).build();
JdkProxyHint second = new Builder().proxiedInterfaces(TypeReference.of(Function.class.getName()),
TypeReference.of(Consumer.class)).build();
@@ -47,7 +47,16 @@ class JdkProxyHintTests {
}
@Test
void equalsWithWithSameProxiedInterfacesDifferentOrderIsFalse() {
void equalsWithSameProxiedInterfacesAndDifferentConditionIsFalse() {
JdkProxyHint first = new Builder().proxiedInterfaces(Function.class, Consumer.class)
.onReachableType(TypeReference.of(String.class)).build();
JdkProxyHint second = new Builder().proxiedInterfaces(TypeReference.of(Function.class.getName()),
TypeReference.of(Consumer.class)).onReachableType(TypeReference.of(Function.class)).build();
assertThat(first).isNotEqualTo(second);
}
@Test
void equalsWithSameProxiedInterfacesDifferentOrderIsFalse() {
JdkProxyHint first = new Builder().proxiedInterfaces(Function.class, Consumer.class).build();
JdkProxyHint second = new Builder().proxiedInterfaces(TypeReference.of(Consumer.class),
TypeReference.of(Function.class.getName())).build();
@@ -55,7 +64,7 @@ class JdkProxyHintTests {
}
@Test
void equalsWithWithDifferentProxiedInterfacesIsFalse() {
void equalsWithDifferentProxiedInterfacesIsFalse() {
JdkProxyHint first = new Builder().proxiedInterfaces(Function.class).build();
JdkProxyHint second = new Builder().proxiedInterfaces(TypeReference.of(Function.class.getName()),
TypeReference.of(Consumer.class)).build();

View File

@@ -84,8 +84,8 @@ class ResourceHintsTests {
@Test
void registerPatternWithIncludesAndExcludes() {
this.resourceHints.registerPattern("com/example/*.properties",
resourceHint -> resourceHint.excludes("com/example/to-ignore.properties"));
this.resourceHints.registerPattern(resourceHint ->
resourceHint.includes("com/example/*.properties").excludes("com/example/to-ignore.properties"));
assertThat(this.resourceHints.resourcePatterns()).singleElement().satisfies(patternOf(
List.of("com/example/*.properties"),
List.of("com/example/to-ignore.properties")));
@@ -107,7 +107,7 @@ class ResourceHintsTests {
}
private Consumer<ResourcePatternHint> patternOf(String... includes) {
private Consumer<ResourcePatternHints> patternOf(String... includes) {
return patternOf(Arrays.asList(includes), Collections.emptyList());
}
@@ -115,10 +115,10 @@ class ResourceHintsTests {
return resourceBundleHint -> assertThat(resourceBundleHint.getBaseName()).isEqualTo(baseName);
}
private Consumer<ResourcePatternHint> patternOf(List<String> includes, List<String> excludes) {
private Consumer<ResourcePatternHints> patternOf(List<String> includes, List<String> excludes) {
return pattern -> {
assertThat(pattern.getIncludes()).containsExactlyElementsOf(includes);
assertThat(pattern.getExcludes()).containsExactlyElementsOf(excludes);
assertThat(pattern.getIncludes()).map(ResourcePatternHint::getPattern).containsExactlyElementsOf(includes);
assertThat(pattern.getExcludes()).map(ResourcePatternHint::getPattern).containsExactlyElementsOf(excludes);
};
}

View File

@@ -48,15 +48,16 @@ class RuntimeHintsTests {
void resourceHintWithClass() {
this.hints.resources().registerType(String.class);
assertThat(this.hints.resources().resourcePatterns()).singleElement().satisfies(resourceHint -> {
assertThat(resourceHint.getIncludes()).containsExactly("java/lang/String.class");
assertThat(resourceHint.getIncludes()).map(ResourcePatternHint::getPattern).containsExactly("java/lang/String.class");
assertThat(resourceHint.getExcludes()).isEmpty();
});
}
@Test
void javaSerializationHintWithClass() {
this.hints.javaSerialization().registerType(String.class);
assertThat(this.hints.javaSerialization().types()).containsExactly(TypeReference.of(String.class));
this.hints.serialization().registerType(String.class);
assertThat(this.hints.serialization().javaSerialization().map(JavaSerializationHint::getType))
.containsExactly(TypeReference.of(String.class));
}
@Test

View File

@@ -23,20 +23,20 @@ import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link JavaSerializationHints}.
* Tests for {@link SerializationHints}.
*
* @author Stephane Nicoll
*/
class JavaSerializationHintsTests {
class SerializationHintsTests {
private final JavaSerializationHints javaSerializationHints = new JavaSerializationHints();
private final SerializationHints serializationHints = new SerializationHints();
@Test
void registerTypeTwiceExposesOneHint() {
this.javaSerializationHints.registerType(URL.class);
this.javaSerializationHints.registerType(TypeReference.of(URL.class.getName()));
assertThat(this.javaSerializationHints.types()).singleElement()
.isEqualTo(TypeReference.of(URL.class));
this.serializationHints.registerType(URL.class);
this.serializationHints.registerType(TypeReference.of(URL.class.getName()));
assertThat(this.serializationHints.javaSerialization()).singleElement()
.extracting(JavaSerializationHint::getType).isEqualTo(TypeReference.of(URL.class));
}
}

View File

@@ -32,12 +32,12 @@ import org.skyscreamer.jsonassert.JSONAssert;
import org.skyscreamer.jsonassert.JSONCompareMode;
import org.springframework.aot.hint.ExecutableMode;
import org.springframework.aot.hint.JavaSerializationHints;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.ProxyHints;
import org.springframework.aot.hint.ReflectionHints;
import org.springframework.aot.hint.ResourceHints;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.SerializationHints;
import org.springframework.aot.hint.TypeReference;
import org.springframework.core.codec.StringDecoder;
import org.springframework.util.MimeType;
@@ -66,7 +66,7 @@ public class FileNativeConfigurationWriterTests {
void serializationConfig() throws IOException, JSONException {
FileNativeConfigurationWriter generator = new FileNativeConfigurationWriter(tempDir);
RuntimeHints hints = new RuntimeHints();
JavaSerializationHints serializationHints = hints.javaSerialization();
SerializationHints serializationHints = hints.serialization();
serializationHints.registerType(Integer.class);
serializationHints.registerType(Long.class);
generator.write(hints);

View File

@@ -26,6 +26,7 @@ import org.skyscreamer.jsonassert.JSONAssert;
import org.skyscreamer.jsonassert.JSONCompareMode;
import org.springframework.aot.hint.ProxyHints;
import org.springframework.aot.hint.TypeReference;
/**
* Tests for {@link ProxyHintsWriter}.
@@ -41,7 +42,7 @@ public class ProxyHintsWriterTests {
}
@Test
void one() throws JSONException {
void shouldWriteOneEntry() throws JSONException {
ProxyHints hints = new ProxyHints();
hints.registerJdkProxy(Function.class);
assertEquals("""
@@ -51,7 +52,7 @@ public class ProxyHintsWriterTests {
}
@Test
void two() throws JSONException {
void shouldWriteMultipleEntries() throws JSONException {
ProxyHints hints = new ProxyHints();
hints.registerJdkProxy(Function.class);
hints.registerJdkProxy(Function.class, Consumer.class);
@@ -62,6 +63,17 @@ public class ProxyHintsWriterTests {
]""", hints);
}
@Test
void shouldWriteCondition() throws JSONException {
ProxyHints hints = new ProxyHints();
hints.registerJdkProxy(builder -> builder.proxiedInterfaces(Function.class)
.onReachableType(TypeReference.of("org.example.Test")));
assertEquals("""
[
{ "condition": { "typeReachable": "org.example.Test"}, "interfaces": [ "java.util.function.Function" ] }
]""", hints);
}
private void assertEquals(String expectedString, ProxyHints hints) throws JSONException {
StringWriter out = new StringWriter();
BasicJsonWriter writer = new BasicJsonWriter(out, "\t");

View File

@@ -24,11 +24,13 @@ import org.skyscreamer.jsonassert.JSONAssert;
import org.skyscreamer.jsonassert.JSONCompareMode;
import org.springframework.aot.hint.ResourceHints;
import org.springframework.aot.hint.TypeReference;
/**
* Tests for {@link ResourceHintsWriter}.
*
* @author Sebastien Deleuze
* @author Brian Clozel
*/
public class ResourceHintsWriterTests {
@@ -71,18 +73,32 @@ public class ResourceHintsWriterTests {
@Test
void registerPatternWithIncludesAndExcludes() throws JSONException {
ResourceHints hints = new ResourceHints();
hints.registerPattern("com/example/*.properties", hint -> hint.excludes("com/example/to-ignore.properties"));
hints.registerPattern("org/example/*.properties", hint -> hint.excludes("org/example/to-ignore.properties"));
hints.registerPattern(hint -> hint.includes("com/example/*.properties").excludes("com/example/to-ignore.properties"));
hints.registerPattern(hint -> hint.includes("org/other/*.properties").excludes("org/other/to-ignore.properties"));
assertEquals("""
{
"resources": {
"includes": [
{ "pattern": "\\\\Qcom/example/\\\\E.*\\\\Q.properties\\\\E"},
{ "pattern": "\\\\Qorg/example/\\\\E.*\\\\Q.properties\\\\E"}
{ "pattern": "\\\\Qorg/other/\\\\E.*\\\\Q.properties\\\\E"}
],
"excludes": [
{ "pattern": "\\\\Qcom/example/to-ignore.properties\\\\E"},
{ "pattern": "\\\\Qorg/example/to-ignore.properties\\\\E"}
{ "pattern": "\\\\Qorg/other/to-ignore.properties\\\\E"}
]
}
}""", hints);
}
@Test
void registerWithReachableTypeCondition() throws JSONException {
ResourceHints hints = new ResourceHints();
hints.registerPattern(builder -> builder.includes(TypeReference.of("com.example.Test"), "com/example/test.properties"));
assertEquals("""
{
"resources": {
"includes": [
{ "condition": { "typeReachable": "com.example.Test"}, "pattern": "\\\\Qcom/example/test.properties\\\\E"}
]
}
}""", hints);

View File

@@ -23,26 +23,26 @@ import org.junit.jupiter.api.Test;
import org.skyscreamer.jsonassert.JSONAssert;
import org.skyscreamer.jsonassert.JSONCompareMode;
import org.springframework.aot.hint.JavaSerializationHints;
import org.springframework.aot.hint.SerializationHints;
import org.springframework.aot.hint.TypeReference;
import org.springframework.core.env.Environment;
/**
* Tests for {@link JavaSerializationHintsWriter}.
* Tests for {@link SerializationHintsWriter}.
*
* @author Sebastien Deleuze
*/
public class JavaSerializationHintsWriterTests {
public class SerializationHintsWriterTests {
@Test
void empty() throws JSONException {
JavaSerializationHints hints = new JavaSerializationHints();
void shouldWriteEmptyHint() throws JSONException {
SerializationHints hints = new SerializationHints();
assertEquals("[]", hints);
}
@Test
void one() throws JSONException {
JavaSerializationHints hints = new JavaSerializationHints().registerType(TypeReference.of(String.class));
void shouldWriteSingleHint() throws JSONException {
SerializationHints hints = new SerializationHints().registerType(TypeReference.of(String.class));
assertEquals("""
[
{ "name": "java.lang.String" }
@@ -50,8 +50,8 @@ public class JavaSerializationHintsWriterTests {
}
@Test
void two() throws JSONException {
JavaSerializationHints hints = new JavaSerializationHints()
void shouldWriteMultipleHints() throws JSONException {
SerializationHints hints = new SerializationHints()
.registerType(TypeReference.of(String.class))
.registerType(TypeReference.of(Environment.class));
assertEquals("""
@@ -61,10 +61,20 @@ public class JavaSerializationHintsWriterTests {
]""", hints);
}
private void assertEquals(String expectedString, JavaSerializationHints hints) throws JSONException {
@Test
void shouldWriteSingleHintWithCondition() throws JSONException {
SerializationHints hints = new SerializationHints().registerType(TypeReference.of(String.class),
builder -> builder.onReachableType(TypeReference.of("org.example.Test")));
assertEquals("""
[
{ "condition": { "typeReachable": "org.example.Test" }, "name": "java.lang.String" }
]""", hints);
}
private void assertEquals(String expectedString, SerializationHints hints) throws JSONException {
StringWriter out = new StringWriter();
BasicJsonWriter writer = new BasicJsonWriter(out, "\t");
JavaSerializationHintsWriter.INSTANCE.write(writer, hints);
SerializationHintsWriter.INSTANCE.write(writer, hints);
JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.NON_EXTENSIBLE);
}

View File

@@ -20,6 +20,7 @@ import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.aot.hint.MemberCategory;
import org.springframework.aot.hint.ResourcePatternHint;
import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.aot.hint.TypeHint;
@@ -48,7 +49,7 @@ class SpringFactoriesLoaderRuntimeHintsRegistrarTests {
@Test
void resourceLocationHasHints() {
assertThat(this.hints.resources().resourcePatterns())
.anySatisfy(hint -> assertThat(hint.getIncludes())
.anySatisfy(hint -> assertThat(hint.getIncludes()).map(ResourcePatternHint::getPattern)
.contains(SpringFactoriesLoader.FACTORIES_RESOURCE_LOCATION));
}