Skip to content

Commit 6d5bf6d

Browse files
committed
Ensure alias resolution in SimpleAliasRegistry depends on registration order
Closes gh-32024
1 parent 6b482df commit 6d5bf6d

File tree

2 files changed

+46
-71
lines changed

2 files changed

+46
-71
lines changed

spring-core/src/main/java/org/springframework/core/SimpleAliasRegistry.java

Lines changed: 14 additions & 3 deletions
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
}

spring-core/src/test/java/org/springframework/core/SimpleAliasRegistryTests.java

Lines changed: 32 additions & 68 deletions
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;
@@ -29,6 +28,7 @@
2928
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
3029
import static org.assertj.core.api.Assertions.assertThatIllegalStateException;
3130
import static org.assertj.core.api.Assertions.assertThatNoException;
31+
import static org.assertj.core.api.Assertions.assertThatNullPointerException;
3232

3333
/**
3434
* Tests for {@link SimpleAliasRegistry}.
@@ -49,10 +49,6 @@ class SimpleAliasRegistryTests {
4949
private static final String ALIAS1 = "alias1";
5050
private static final String ALIAS2 = "alias2";
5151
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.
5652
private static final String ALIAS4 = "alias4";
5753
private static final String ALIAS5 = "alias5";
5854

@@ -94,7 +90,21 @@ void aliasChainingWithMultipleAliases() {
9490
}
9591

9692
@Test
97-
void removeAlias() {
93+
void removeNullAlias() {
94+
assertThatNullPointerException().isThrownBy(() -> registry.removeAlias(null));
95+
}
96+
97+
@Test
98+
void removeNonExistentAlias() {
99+
String alias = NICKNAME;
100+
assertDoesNotHaveAlias(REAL_NAME, alias);
101+
assertThatIllegalStateException()
102+
.isThrownBy(() -> registry.removeAlias(alias))
103+
.withMessage("No alias '%s' registered", alias);
104+
}
105+
106+
@Test
107+
void removeExistingAlias() {
98108
registerAlias(REAL_NAME, NICKNAME);
99109
assertHasAlias(REAL_NAME, NICKNAME);
100110

@@ -213,35 +223,37 @@ void resolveAliasesWithPlaceholderReplacementConflict() {
213223
"It is already registered for name '%s'.", ALIAS2, ALIAS1, NAME1, NAME2);
214224
}
215225

216-
@Test
217-
void resolveAliasesWithComplexPlaceholderReplacement() {
226+
@ParameterizedTest
227+
@ValueSource(strings = {"alias4", "test", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"})
228+
void resolveAliasesWithComplexPlaceholderReplacementWithAliasSwitching(String aliasX) {
218229
StringValueResolver valueResolver = new StubStringValueResolver(Map.of(
219230
ALIAS3, ALIAS1,
220-
ALIAS4, ALIAS5,
231+
aliasX, ALIAS5,
221232
ALIAS5, ALIAS2
222233
));
223234

235+
// Since SimpleAliasRegistry ensures that aliases are processed in declaration
236+
// order, we need to register ALIAS5 *before* aliasX to support our use case.
224237
registerAlias(NAME3, ALIAS3);
225-
registerAlias(NAME4, ALIAS4);
226238
registerAlias(NAME5, ALIAS5);
239+
registerAlias(NAME4, aliasX);
227240

228241
// Original state:
229-
// WARNING: Based on ConcurrentHashMap iteration order!
230242
// ALIAS3 -> NAME3
231243
// ALIAS5 -> NAME5
232-
// ALIAS4 -> NAME4
244+
// aliasX -> NAME4
233245

234246
// State after processing original entry (ALIAS3 -> NAME3):
235247
// ALIAS1 -> NAME3
236248
// ALIAS5 -> NAME5
237-
// ALIAS4 -> NAME4
249+
// aliasX -> NAME4
238250

239251
// State after processing original entry (ALIAS5 -> NAME5):
240252
// ALIAS1 -> NAME3
241253
// ALIAS2 -> NAME5
242-
// ALIAS4 -> NAME4
254+
// aliasX -> NAME4
243255

244-
// State after processing original entry (ALIAS4 -> NAME4):
256+
// State after processing original entry (aliasX -> NAME4):
245257
// ALIAS1 -> NAME3
246258
// ALIAS2 -> NAME5
247259
// ALIAS5 -> NAME4
@@ -252,72 +264,24 @@ void resolveAliasesWithComplexPlaceholderReplacement() {
252264
assertThat(registry.getAliases(NAME5)).containsExactly(ALIAS2);
253265
}
254266

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")
302267
@ParameterizedTest // gh-32024
303268
@ValueSource(strings = {"alias4", "test", "xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"})
304-
void resolveAliasesWithComplexPlaceholderReplacementAndNameSwitching(String aliasX) {
269+
void resolveAliasesWithComplexPlaceholderReplacementWithAliasAndNameSwitching(String aliasX) {
305270
StringValueResolver valueResolver = new StubStringValueResolver(Map.of(
306-
NAME3, NAME4,
307-
NAME4, NAME3,
308271
ALIAS3, ALIAS1,
309272
aliasX, ALIAS5,
310-
ALIAS5, ALIAS2
273+
ALIAS5, ALIAS2,
274+
NAME3, NAME4,
275+
NAME4, NAME3
311276
));
312277

313-
// If SimpleAliasRegistry ensures that aliases are processed in declaration
278+
// Since SimpleAliasRegistry ensures that aliases are processed in declaration
314279
// order, we need to register ALIAS5 *before* aliasX to support our use case.
315280
registerAlias(NAME3, ALIAS3);
316281
registerAlias(NAME5, ALIAS5);
317282
registerAlias(NAME4, aliasX);
318283

319284
// Original state:
320-
// WARNING: Based on LinkedHashMap iteration order!
321285
// ALIAS3 -> NAME3
322286
// ALIAS5 -> NAME5
323287
// aliasX -> NAME4

0 commit comments

Comments
 (0)