diff --git a/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineList.java b/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineList.java index 6c674bac178..fc4d9eab3de 100644 --- a/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineList.java +++ b/spring-expression/src/main/java/org/springframework/expression/spel/ast/InlineList.java @@ -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;"); + } } } diff --git a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java index 611d7170ff6..c8c0553add0 100644 --- a/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java +++ b/spring-expression/src/test/java/org/springframework/expression/spel/SpelCompilationCoverageTests.java @@ -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 interpreted = (List) expression.getValue(); + assertThat(interpreted).containsExactly(1, 2, 3); + + // Interpreted --> unmodifiable + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> interpreted.add("boom")); + + assertCanCompile(expression); + + List compiled = (List) 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 interpreted = (List) expression.getValue(); + assertThat(interpreted.toString()).isEqualTo("[[1, 2], [3, 4]]"); + + // Interpreted --> unmodifiable + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> interpreted.add("boom")); + List nested1Interpreted = (List) interpreted.get(0); + List nested2Interpreted = (List) interpreted.get(1); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> nested1Interpreted.add("boom")); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> nested2Interpreted.add("boom")); + + assertCanCompile(expression); + + List compiled = (List) expression.getValue(); + assertThat(compiled.toString()).isEqualTo("[[1, 2], [3, 4]]"); + + // Compiled --> unmodifiable + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> compiled.add("boom")); + List nested1Compiled = (List) compiled.get(0); + List nested2Compiled = (List) compiled.get(1); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> nested1Compiled.add("boom")); + assertThatExceptionOfType(UnsupportedOperationException.class).isThrownBy(() -> nested2Compiled.add("boom")); + } } @Nested