Skip to content

Commit 6238228

Browse files
committed
Ensure alias resolution in SimpleAliasRegistry depends on registration order
Closes spring-projectsgh-32024
1 parent 6691ff2 commit 6238228

File tree

2 files changed

+30
-70
lines changed

2 files changed

+30
-70
lines changed

Diff for: spring-core/src/main/java/org/springframework/core/SimpleAliasRegistry.java

+14-3
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,6 @@
1717
package org.springframework.core;
1818

1919
import java.util.ArrayList;
20-
import java.util.HashMap;
2120
import java.util.List;
2221
import java.util.Map;
2322
import java.util.concurrent.ConcurrentHashMap;
@@ -50,6 +49,9 @@ public class SimpleAliasRegistry implements AliasRegistry {
5049
/** Map from alias to canonical name. */
5150
private final Map<String, String> aliasMap = new ConcurrentHashMap<>(16);
5251

52+
/** List of alias names, in registration order. */
53+
private volatile List<String> aliasNames = new ArrayList<>(16);
54+
5355

5456
@Override
5557
public void registerAlias(String name, String alias) {
@@ -58,6 +60,7 @@ public void registerAlias(String name, String alias) {
5860
synchronized (this.aliasMap) {
5961
if (alias.equals(name)) {
6062
this.aliasMap.remove(alias);
63+
this.aliasNames.remove(alias);
6164
if (logger.isDebugEnabled()) {
6265
logger.debug("Alias definition '" + alias + "' ignored since it points to same name");
6366
}
@@ -80,6 +83,7 @@ public void registerAlias(String name, String alias) {
8083
}
8184
checkForAliasCircle(name, alias);
8285
this.aliasMap.put(alias, name);
86+
this.aliasNames.add(alias);
8387
if (logger.isTraceEnabled()) {
8488
logger.trace("Alias definition '" + alias + "' registered for name '" + name + "'");
8589
}
@@ -111,6 +115,7 @@ public boolean hasAlias(String name, String alias) {
111115
public void removeAlias(String alias) {
112116
synchronized (this.aliasMap) {
113117
String name = this.aliasMap.remove(alias);
118+
this.aliasNames.remove(alias);
114119
if (name == null) {
115120
throw new IllegalStateException("No alias '" + alias + "' registered");
116121
}
@@ -155,19 +160,22 @@ private void retrieveAliases(String name, List<String> result) {
155160
public void resolveAliases(StringValueResolver valueResolver) {
156161
Assert.notNull(valueResolver, "StringValueResolver must not be null");
157162
synchronized (this.aliasMap) {
158-
Map<String, String> aliasCopy = new HashMap<>(this.aliasMap);
159-
aliasCopy.forEach((alias, registeredName) -> {
163+
List<String> aliasNamesCopy = new ArrayList<>(this.aliasNames);
164+
aliasNamesCopy.forEach(alias -> {
165+
String registeredName = this.aliasMap.get(alias);
160166
String resolvedAlias = valueResolver.resolveStringValue(alias);
161167
String resolvedName = valueResolver.resolveStringValue(registeredName);
162168
if (resolvedAlias == null || resolvedName == null || resolvedAlias.equals(resolvedName)) {
163169
this.aliasMap.remove(alias);
170+
this.aliasNames.remove(alias);
164171
}
165172
else if (!resolvedAlias.equals(alias)) {
166173
String existingName = this.aliasMap.get(resolvedAlias);
167174
if (existingName != null) {
168175
if (existingName.equals(resolvedName)) {
169176
// Pointing to existing alias - just remove placeholder
170177
this.aliasMap.remove(alias);
178+
this.aliasNames.remove(alias);
171179
return;
172180
}
173181
throw new IllegalStateException(
@@ -177,10 +185,13 @@ else if (!resolvedAlias.equals(alias)) {
177185
}
178186
checkForAliasCircle(resolvedName, resolvedAlias);
179187
this.aliasMap.remove(alias);
188+
this.aliasNames.remove(alias);
180189
this.aliasMap.put(resolvedAlias, resolvedName);
190+
this.aliasNames.add(resolvedAlias);
181191
}
182192
else if (!registeredName.equals(resolvedName)) {
183193
this.aliasMap.put(alias, resolvedName);
194+
this.aliasNames.add(alias);
184195
}
185196
});
186197
}

Diff for: spring-core/src/test/java/org/springframework/core/SimpleAliasRegistryTests.java

+16-67
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,6 @@
1818

1919
import java.util.Map;
2020

21-
import org.junit.jupiter.api.Disabled;
2221
import org.junit.jupiter.api.Test;
2322
import org.junit.jupiter.params.ParameterizedTest;
2423
import org.junit.jupiter.params.provider.ValueSource;
@@ -49,10 +48,6 @@ class SimpleAliasRegistryTests {
4948
private static final String ALIAS1 = "alias1";
5049
private static final String ALIAS2 = "alias2";
5150
private static final String ALIAS3 = "alias3";
52-
// TODO Determine if we can make SimpleAliasRegistry.resolveAliases() reliable.
53-
// See https://github.com/spring-projects/spring-framework/issues/32024.
54-
// When ALIAS4 is changed to "test", various tests fail due to the iteration
55-
// order of the entries in the aliasMap in SimpleAliasRegistry.
5651
private static final String ALIAS4 = "alias4";
5752
private static final String ALIAS5 = "alias5";
5853

@@ -213,35 +208,37 @@ void resolveAliasesWithPlaceholderReplacementConflict() {
213208
"It is already registered for name '%s'.", ALIAS2, ALIAS1, NAME1, NAME2);
214209
}
215210

216-
@Test
217-
void resolveAliasesWithComplexPlaceholderReplacement() {
211+
@ParameterizedTest
212+
@ValueSource(strings = {"alias4", "test", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"})
213+
void resolveAliasesWithComplexPlaceholderReplacementWithAliasSwitching(String aliasX) {
218214
StringValueResolver valueResolver = new StubStringValueResolver(Map.of(
219215
ALIAS3, ALIAS1,
220-
ALIAS4, ALIAS5,
216+
aliasX, ALIAS5,
221217
ALIAS5, ALIAS2
222218
));
223219

220+
// Since SimpleAliasRegistry ensures that aliases are processed in declaration
221+
// order, we need to register ALIAS5 *before* aliasX to support our use case.
224222
registerAlias(NAME3, ALIAS3);
225-
registerAlias(NAME4, ALIAS4);
226223
registerAlias(NAME5, ALIAS5);
224+
registerAlias(NAME4, aliasX);
227225

228226
// Original state:
229-
// WARNING: Based on ConcurrentHashMap iteration order!
230227
// ALIAS3 -> NAME3
231228
// ALIAS5 -> NAME5
232-
// ALIAS4 -> NAME4
229+
// aliasX -> NAME4
233230

234231
// State after processing original entry (ALIAS3 -> NAME3):
235232
// ALIAS1 -> NAME3
236233
// ALIAS5 -> NAME5
237-
// ALIAS4 -> NAME4
234+
// aliasX -> NAME4
238235

239236
// State after processing original entry (ALIAS5 -> NAME5):
240237
// ALIAS1 -> NAME3
241238
// ALIAS2 -> NAME5
242-
// ALIAS4 -> NAME4
239+
// aliasX -> NAME4
243240

244-
// State after processing original entry (ALIAS4 -> NAME4):
241+
// State after processing original entry (aliasX -> NAME4):
245242
// ALIAS1 -> NAME3
246243
// ALIAS2 -> NAME5
247244
// ALIAS5 -> NAME4
@@ -252,72 +249,24 @@ void resolveAliasesWithComplexPlaceholderReplacement() {
252249
assertThat(registry.getAliases(NAME5)).containsExactly(ALIAS2);
253250
}
254251

255-
// TODO Remove this test once we have implemented reliable processing in SimpleAliasRegistry.resolveAliases().
256-
// See https://github.com/spring-projects/spring-framework/issues/32024.
257-
// This method effectively duplicates the @ParameterizedTest version below,
258-
// with aliasX hard coded to ALIAS4; however, this method also hard codes
259-
// a different outcome that passes based on ConcurrentHashMap iteration order!
260-
@Test
261-
void resolveAliasesWithComplexPlaceholderReplacementAndNameSwitching() {
262-
StringValueResolver valueResolver = new StubStringValueResolver(Map.of(
263-
NAME3, NAME4,
264-
NAME4, NAME3,
265-
ALIAS3, ALIAS1,
266-
ALIAS4, ALIAS5,
267-
ALIAS5, ALIAS2
268-
));
269-
270-
registerAlias(NAME3, ALIAS3);
271-
registerAlias(NAME4, ALIAS4);
272-
registerAlias(NAME5, ALIAS5);
273-
274-
// Original state:
275-
// WARNING: Based on ConcurrentHashMap iteration order!
276-
// ALIAS3 -> NAME3
277-
// ALIAS5 -> NAME5
278-
// ALIAS4 -> NAME4
279-
280-
// State after processing original entry (ALIAS3 -> NAME3):
281-
// ALIAS1 -> NAME4
282-
// ALIAS5 -> NAME5
283-
// ALIAS4 -> NAME4
284-
285-
// State after processing original entry (ALIAS5 -> NAME5):
286-
// ALIAS1 -> NAME4
287-
// ALIAS2 -> NAME5
288-
// ALIAS4 -> NAME4
289-
290-
// State after processing original entry (ALIAS4 -> NAME4):
291-
// ALIAS1 -> NAME4
292-
// ALIAS2 -> NAME5
293-
// ALIAS5 -> NAME3
294-
295-
registry.resolveAliases(valueResolver);
296-
assertThat(registry.getAliases(NAME3)).containsExactly(ALIAS5);
297-
assertThat(registry.getAliases(NAME4)).containsExactly(ALIAS1);
298-
assertThat(registry.getAliases(NAME5)).containsExactly(ALIAS2);
299-
}
300-
301-
@Disabled("Fails for some values unless alias registration order is honored")
302252
@ParameterizedTest // gh-32024
303253
@ValueSource(strings = {"alias4", "test", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"})
304-
void resolveAliasesWithComplexPlaceholderReplacementAndNameSwitching(String aliasX) {
254+
void resolveAliasesWithComplexPlaceholderReplacementWithAliasAndNameSwitching(String aliasX) {
305255
StringValueResolver valueResolver = new StubStringValueResolver(Map.of(
306-
NAME3, NAME4,
307-
NAME4, NAME3,
308256
ALIAS3, ALIAS1,
309257
aliasX, ALIAS5,
310-
ALIAS5, ALIAS2
258+
ALIAS5, ALIAS2,
259+
NAME3, NAME4,
260+
NAME4, NAME3
311261
));
312262

313-
// If SimpleAliasRegistry ensures that aliases are processed in declaration
263+
// Since SimpleAliasRegistry ensures that aliases are processed in declaration
314264
// order, we need to register ALIAS5 *before* aliasX to support our use case.
315265
registerAlias(NAME3, ALIAS3);
316266
registerAlias(NAME5, ALIAS5);
317267
registerAlias(NAME4, aliasX);
318268

319269
// Original state:
320-
// WARNING: Based on LinkedHashMap iteration order!
321270
// ALIAS3 -> NAME3
322271
// ALIAS5 -> NAME5
323272
// aliasX -> NAME4

0 commit comments

Comments
 (0)