Skip to content

Commit eaf7a28

Browse files
committed
Write runtime hints with deterministic order
This commit updates the JSON writers to use a deterministic order for arrays. Previously, the order could change with the same content, breaking caching. Closes gh-31852
1 parent 7965c19 commit eaf7a28

File tree

11 files changed

+183
-42
lines changed

11 files changed

+183
-42
lines changed

spring-core/src/main/java/org/springframework/aot/hint/AbstractTypeReference.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,10 @@ protected String addPackageIfNecessary(String part) {
7979

8080
protected abstract boolean isPrimitive();
8181

82+
@Override
83+
public int compareTo(TypeReference other) {
84+
return this.getCanonicalName().compareToIgnoreCase(other.getCanonicalName());
85+
}
8286

8387
@Override
8488
public boolean equals(@Nullable Object other) {

spring-core/src/main/java/org/springframework/aot/hint/ExecutableHint.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,8 +19,10 @@
1919
import java.lang.reflect.Constructor;
2020
import java.lang.reflect.Executable;
2121
import java.lang.reflect.Method;
22+
import java.util.Comparator;
2223
import java.util.List;
2324
import java.util.function.Consumer;
25+
import java.util.stream.Collectors;
2426

2527
import org.springframework.lang.Nullable;
2628
import org.springframework.util.Assert;
@@ -32,7 +34,7 @@
3234
* @author Stephane Nicoll
3335
* @since 6.0
3436
*/
35-
public final class ExecutableHint extends MemberHint {
37+
public final class ExecutableHint extends MemberHint implements Comparable<ExecutableHint> {
3638

3739
private final List<TypeReference> parameterTypes;
3840

@@ -91,6 +93,17 @@ public static Consumer<Builder> builtWith(ExecutableMode mode) {
9193
return builder -> builder.withMode(mode);
9294
}
9395

96+
@Override
97+
public int compareTo(ExecutableHint other) {
98+
return Comparator.comparing(ExecutableHint::getName, String::compareToIgnoreCase)
99+
.thenComparing(ExecutableHint::getParameterTypes, Comparator.comparingInt(List::size))
100+
.thenComparing(ExecutableHint::getParameterTypes, (params1, params2) -> {
101+
String left = params1.stream().map(TypeReference::getCanonicalName).collect(Collectors.joining(","));
102+
String right = params2.stream().map(TypeReference::getCanonicalName).collect(Collectors.joining(","));
103+
return left.compareTo(right);
104+
}).compare(this, other);
105+
}
106+
94107
/**
95108
* Builder for {@link ExecutableHint}.
96109
*/

spring-core/src/main/java/org/springframework/aot/hint/TypeReference.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@
2929
* @author Sebastien Deleuze
3030
* @since 6.0
3131
*/
32-
public interface TypeReference {
32+
public interface TypeReference extends Comparable<TypeReference> {
3333

3434
/**
3535
* Return the fully qualified name of this type reference.

spring-core/src/main/java/org/springframework/aot/nativex/ProxyHintsWriter.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,11 +16,14 @@
1616

1717
package org.springframework.aot.nativex;
1818

19+
import java.util.Comparator;
1920
import java.util.LinkedHashMap;
2021
import java.util.Map;
22+
import java.util.stream.Collectors;
2123

2224
import org.springframework.aot.hint.JdkProxyHint;
2325
import org.springframework.aot.hint.ProxyHints;
26+
import org.springframework.aot.hint.TypeReference;
2427

2528
/**
2629
* Write {@link JdkProxyHint}s contained in a {@link ProxyHints} to the JSON
@@ -38,8 +41,18 @@ class ProxyHintsWriter {
3841

3942
public static final ProxyHintsWriter INSTANCE = new ProxyHintsWriter();
4043

44+
private static final Comparator<JdkProxyHint> JDK_PROXY_HINT_COMPARATOR =
45+
(left, right) -> {
46+
String leftSignature = left.getProxiedInterfaces().stream()
47+
.map(TypeReference::getCanonicalName).collect(Collectors.joining(","));
48+
String rightSignature = right.getProxiedInterfaces().stream()
49+
.map(TypeReference::getCanonicalName).collect(Collectors.joining(","));
50+
return leftSignature.compareTo(rightSignature);
51+
};
52+
4153
public void write(BasicJsonWriter writer, ProxyHints hints) {
42-
writer.writeArray(hints.jdkProxyHints().map(this::toAttributes).toList());
54+
writer.writeArray(hints.jdkProxyHints().sorted(JDK_PROXY_HINT_COMPARATOR)
55+
.map(this::toAttributes).toList());
4356
}
4457

4558
private Map<String, Object> toAttributes(JdkProxyHint hint) {

spring-core/src/main/java/org/springframework/aot/nativex/ReflectionHintsWriter.java

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -17,6 +17,7 @@
1717
package org.springframework.aot.nativex;
1818

1919
import java.util.Collection;
20+
import java.util.Comparator;
2021
import java.util.LinkedHashMap;
2122
import java.util.List;
2223
import java.util.Map;
@@ -49,7 +50,9 @@ class ReflectionHintsWriter {
4950
public static final ReflectionHintsWriter INSTANCE = new ReflectionHintsWriter();
5051

5152
public void write(BasicJsonWriter writer, ReflectionHints hints) {
52-
writer.writeArray(hints.typeHints().map(this::toAttributes).toList());
53+
writer.writeArray(hints.typeHints()
54+
.sorted(Comparator.comparing(TypeHint::getType))
55+
.map(this::toAttributes).toList());
5356
}
5457

5558
private Map<String, Object> toAttributes(TypeHint hint) {
@@ -58,7 +61,8 @@ private Map<String, Object> toAttributes(TypeHint hint) {
5861
handleCondition(attributes, hint);
5962
handleCategories(attributes, hint.getMemberCategories());
6063
handleFields(attributes, hint.fields());
61-
handleExecutables(attributes, Stream.concat(hint.constructors(), hint.methods()).toList());
64+
handleExecutables(attributes, Stream.concat(
65+
hint.constructors(), hint.methods()).sorted().toList());
6266
return attributes;
6367
}
6468

@@ -71,7 +75,9 @@ private void handleCondition(Map<String, Object> attributes, TypeHint hint) {
7175
}
7276

7377
private void handleFields(Map<String, Object> attributes, Stream<FieldHint> fields) {
74-
addIfNotEmpty(attributes, "fields", fields.map(this::toAttributes).toList());
78+
addIfNotEmpty(attributes, "fields", fields
79+
.sorted(Comparator.comparing(FieldHint::getName, String::compareToIgnoreCase))
80+
.map(this::toAttributes).toList());
7581
}
7682

7783
private Map<String, Object> toAttributes(FieldHint hint) {
@@ -97,7 +103,7 @@ private Map<String, Object> toAttributes(ExecutableHint hint) {
97103
}
98104

99105
private void handleCategories(Map<String, Object> attributes, Set<MemberCategory> categories) {
100-
categories.forEach(category -> {
106+
categories.stream().sorted().forEach(category -> {
101107
switch (category) {
102108
case PUBLIC_FIELDS -> attributes.put("allPublicFields", true);
103109
case DECLARED_FIELDS -> attributes.put("allDeclaredFields", true);

spring-core/src/main/java/org/springframework/aot/nativex/ResourceHintsWriter.java

Lines changed: 19 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
package org.springframework.aot.nativex;
1818

1919
import java.util.Collection;
20+
import java.util.Comparator;
2021
import java.util.LinkedHashMap;
2122
import java.util.List;
2223
import java.util.Map;
@@ -44,6 +45,13 @@ class ResourceHintsWriter {
4445

4546
public static final ResourceHintsWriter INSTANCE = new ResourceHintsWriter();
4647

48+
private static final Comparator<ResourcePatternHint> RESOURCE_PATTERN_HINT_COMPARATOR =
49+
Comparator.comparing(ResourcePatternHint::getPattern);
50+
51+
private static final Comparator<ResourceBundleHint> RESOURCE_BUNDLE_HINT_COMPARATOR =
52+
Comparator.comparing(ResourceBundleHint::getBaseName);
53+
54+
4755
public void write(BasicJsonWriter writer, ResourceHints hints) {
4856
Map<String, Object> attributes = new LinkedHashMap<>();
4957
addIfNotEmpty(attributes, "resources", toAttributes(hints));
@@ -53,15 +61,21 @@ public void write(BasicJsonWriter writer, ResourceHints hints) {
5361

5462
private Map<String, Object> toAttributes(ResourceHints hint) {
5563
Map<String, Object> attributes = new LinkedHashMap<>();
56-
addIfNotEmpty(attributes, "includes", hint.resourcePatternHints().map(ResourcePatternHints::getIncludes)
57-
.flatMap(List::stream).distinct().map(this::toAttributes).toList());
58-
addIfNotEmpty(attributes, "excludes", hint.resourcePatternHints().map(ResourcePatternHints::getExcludes)
59-
.flatMap(List::stream).distinct().map(this::toAttributes).toList());
64+
addIfNotEmpty(attributes, "includes", hint.resourcePatternHints()
65+
.map(ResourcePatternHints::getIncludes).flatMap(List::stream).distinct()
66+
.sorted(RESOURCE_PATTERN_HINT_COMPARATOR)
67+
.map(this::toAttributes).toList());
68+
addIfNotEmpty(attributes, "excludes", hint.resourcePatternHints()
69+
.map(ResourcePatternHints::getExcludes).flatMap(List::stream).distinct()
70+
.sorted(RESOURCE_PATTERN_HINT_COMPARATOR)
71+
.map(this::toAttributes).toList());
6072
return attributes;
6173
}
6274

6375
private void handleResourceBundles(Map<String, Object> attributes, Stream<ResourceBundleHint> resourceBundles) {
64-
addIfNotEmpty(attributes, "bundles", resourceBundles.map(this::toAttributes).toList());
76+
addIfNotEmpty(attributes, "bundles", resourceBundles
77+
.sorted(RESOURCE_BUNDLE_HINT_COMPARATOR)
78+
.map(this::toAttributes).toList());
6579
}
6680

6781
private Map<String, Object> toAttributes(ResourceBundleHint hint) {

spring-core/src/main/java/org/springframework/aot/nativex/SerializationHintsWriter.java

Lines changed: 8 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.aot.nativex;
1818

19+
import java.util.Comparator;
1920
import java.util.LinkedHashMap;
2021
import java.util.Map;
2122

@@ -38,8 +39,13 @@ class SerializationHintsWriter {
3839

3940
public static final SerializationHintsWriter INSTANCE = new SerializationHintsWriter();
4041

42+
private static final Comparator<JavaSerializationHint> JAVA_SERIALIZATION_HINT_COMPARATOR =
43+
Comparator.comparing(JavaSerializationHint::getType);
44+
4145
public void write(BasicJsonWriter writer, SerializationHints hints) {
42-
writer.writeArray(hints.javaSerializationHints().map(this::toAttributes).toList());
46+
writer.writeArray(hints.javaSerializationHints()
47+
.sorted(JAVA_SERIALIZATION_HINT_COMPARATOR)
48+
.map(this::toAttributes).toList());
4349
}
4450

4551
private Map<String, Object> toAttributes(JavaSerializationHint serializationHint) {

spring-core/src/test/java/org/springframework/aot/nativex/ProxyHintsWriterTests.java

Lines changed: 17 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2022 the original author or authors.
2+
* Copyright 2002-2023 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -19,6 +19,7 @@
1919
import java.io.StringWriter;
2020
import java.util.function.Consumer;
2121
import java.util.function.Function;
22+
import java.util.function.Supplier;
2223

2324
import org.json.JSONException;
2425
import org.junit.jupiter.api.Test;
@@ -32,8 +33,9 @@
3233
* Tests for {@link ProxyHintsWriter}.
3334
*
3435
* @author Sebastien Deleuze
36+
* @author Stephane Nicoll
3537
*/
36-
public class ProxyHintsWriterTests {
38+
class ProxyHintsWriterTests {
3739

3840
@Test
3941
void empty() throws JSONException {
@@ -63,6 +65,18 @@ void shouldWriteMultipleEntries() throws JSONException {
6365
]""", hints);
6466
}
6567

68+
@Test
69+
void shouldWriteEntriesInNaturalOrder() throws JSONException {
70+
ProxyHints hints = new ProxyHints();
71+
hints.registerJdkProxy(Supplier.class);
72+
hints.registerJdkProxy(Function.class);
73+
assertEquals("""
74+
[
75+
{ "interfaces": [ "java.util.function.Function" ] },
76+
{ "interfaces": [ "java.util.function.Supplier" ] }
77+
]""", hints);
78+
}
79+
6680
@Test
6781
void shouldWriteInnerClass() throws JSONException {
6882
ProxyHints hints = new ProxyHints();
@@ -88,7 +102,7 @@ private void assertEquals(String expectedString, ProxyHints hints) throws JSONEx
88102
StringWriter out = new StringWriter();
89103
BasicJsonWriter writer = new BasicJsonWriter(out, "\t");
90104
ProxyHintsWriter.INSTANCE.write(writer, hints);
91-
JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.NON_EXTENSIBLE);
105+
JSONAssert.assertEquals(expectedString, out.toString(), JSONCompareMode.STRICT);
92106
}
93107

94108
interface Inner {

0 commit comments

Comments
 (0)