mirror of
https://github.com/spring-projects/spring-framework.git
synced 2026-09-17 16:30:28 +00:00
Ensure SpEL's InlineList is immutable in compiled mode
Prior to this commit, SpEL's InlineList was cached as a mutable list in compiled mode (i.e., in a static field in the generated byte code). To address that, this commit modifies InlineList's generateClinitCode() method so that it wraps both top-level and nested inline lists using Collections.unmodifiableList(), analogous to what we already do in createList(). Closes gh-37001
This commit is contained in:
+16
-3
@@ -176,9 +176,10 @@ public class InlineList extends SpelNodeImpl {
|
||||
else {
|
||||
mv.visitInsn(DUP);
|
||||
}
|
||||
// The children might be further lists if they are not constants. In this
|
||||
// situation do not call back into generateCode() because it will register another clinit adder.
|
||||
// Instead, directly build the list here:
|
||||
// Nested InlineList children are always constant (guaranteed by isCompilable/isConstant).
|
||||
// Thus, we call generateClinitCode() directly rather than generateCode() to avoid registering
|
||||
// a separate static field and clinit entry for each nested list. In other words, we build each
|
||||
// nested list inline within the current clinit sequence.
|
||||
if (this.children[c] instanceof InlineList inlineList) {
|
||||
inlineList.generateClinitCode(clazzname, constantFieldName, mv, codeflow, true);
|
||||
}
|
||||
@@ -192,6 +193,18 @@ public class InlineList extends SpelNodeImpl {
|
||||
mv.visitMethodInsn(INVOKEINTERFACE, "java/util/List", "add", "(Ljava/lang/Object;)Z", true);
|
||||
mv.visitInsn(POP);
|
||||
}
|
||||
// Wrap the mutable ArrayList in an unmodifiable list, matching the behavior
|
||||
// of the interpreted mode (see createList()). For the non-nested case, retrieve
|
||||
// the list from the static field first, then store the wrapped list back. For
|
||||
// the nested case, the list is already on the stack for the caller to use.
|
||||
if (!nested) {
|
||||
mv.visitFieldInsn(GETSTATIC, clazzname, constantFieldName, "Ljava/util/List;");
|
||||
}
|
||||
mv.visitMethodInsn(INVOKESTATIC, "java/util/Collections", "unmodifiableList",
|
||||
"(Ljava/util/List;)Ljava/util/List;", false);
|
||||
if (!nested) {
|
||||
mv.visitFieldInsn(PUTSTATIC, clazzname, constantFieldName, "Ljava/util/List;");
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
+49
@@ -1619,6 +1619,55 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests {
|
||||
o = expression.getValue();
|
||||
assertThat(o).isEqualTo("op");
|
||||
}
|
||||
|
||||
@Test // gh-37001
|
||||
@SuppressWarnings("unchecked")
|
||||
void compiledInlineListIsUnmodifiable() {
|
||||
expression = parser.parseExpression("{1, 2, 3}");
|
||||
List<Object> interpreted = (List<Object>) expression.getValue();
|
||||
assertThat(interpreted).containsExactly(1, 2, 3);
|
||||
|
||||
// Interpreted --> unmodifiable
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> interpreted.add("boom"));
|
||||
|
||||
assertCanCompile(expression);
|
||||
|
||||
List<Object> compiled = (List<Object>) expression.getValue();
|
||||
assertThat(compiled).containsExactly(1, 2, 3);
|
||||
|
||||
// Compiled --> unmodifiable
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> compiled.add("boom"));
|
||||
|
||||
// Same list reference must be returned on every evaluation
|
||||
assertThat(expression.getValue()).isSameAs(compiled);
|
||||
}
|
||||
|
||||
@Test // gh-37001
|
||||
@SuppressWarnings("unchecked")
|
||||
void compiledNestedInlineListsAreUnmodifiable() {
|
||||
expression = parser.parseExpression("{{1, 2}, {3, 4}}");
|
||||
List<Object> interpreted = (List<Object>) expression.getValue();
|
||||
assertThat(interpreted.toString()).isEqualTo("[[1, 2], [3, 4]]");
|
||||
|
||||
// Interpreted --> unmodifiable
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> interpreted.add("boom"));
|
||||
List<Object> nested1Interpreted = (List<Object>) interpreted.get(0);
|
||||
List<Object> nested2Interpreted = (List<Object>) interpreted.get(1);
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> nested1Interpreted.add("boom"));
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> nested2Interpreted.add("boom"));
|
||||
|
||||
assertCanCompile(expression);
|
||||
|
||||
List<Object> compiled = (List<Object>) expression.getValue();
|
||||
assertThat(compiled.toString()).isEqualTo("[[1, 2], [3, 4]]");
|
||||
|
||||
// Compiled --> unmodifiable
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> compiled.add("boom"));
|
||||
List<Object> nested1Compiled = (List<Object>) compiled.get(0);
|
||||
List<Object> nested2Compiled = (List<Object>) compiled.get(1);
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> nested1Compiled.add("boom"));
|
||||
assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> nested2Compiled.add("boom"));
|
||||
}
|
||||
}
|
||||
|
||||
@Nested
|
||||
|
||||
Reference in New Issue
Block a user