Support SpEL compilation of #root or #this with non-public type
Prior to this commit, if a Spring Expression Language (SpEL) expression referenced the root context object via the #root or #this variable, we inserted a checkcast in the generated byte code that cast the object to its concrete type. However if the root context object's type was non-public, that resulted in an IllegalAccessError when the compiled byte code was executed. VariableReference.getValueInternal() already contains a solution for global variables which inserts a checkcast to Object in the generated byte code instead of to the object's concrete non-public type. This commit therefore applies the same logic to #root (or #this when used to reference the root context object) that is already applied to global variables. Closes gh-32356
This commit is contained in:
		
							parent
							
								
									5cba32df32
								
							
						
					
					
						commit
						380184e85a
					
				|  | @ -20,6 +20,15 @@ characters. | |||
| * dollar sign: `$` | ||||
| ==== | ||||
| 
 | ||||
| [TIP] | ||||
| ==== | ||||
| When setting a variable or root context object in the `EvaluationContext`, it is advised | ||||
| that the type of the variable or root context object be `public`. | ||||
| 
 | ||||
| Otherwise, certain types of SpEL expressions involving a variable or root context object | ||||
| with a non-public type may fail to evaluate or compile. | ||||
| ==== | ||||
| 
 | ||||
| [WARNING] | ||||
| ==== | ||||
| Since variables share a common namespace with | ||||
|  |  | |||
|  | @ -70,16 +70,23 @@ public class VariableReference extends SpelNodeImpl { | |||
| 
 | ||||
| 	@Override | ||||
| 	public TypedValue getValueInternal(ExpressionState state) throws SpelEvaluationException { | ||||
| 		TypedValue result; | ||||
| 		if (THIS.equals(this.name)) { | ||||
| 			return state.getActiveContextObject(); | ||||
| 			result = state.getActiveContextObject(); | ||||
| 			// If the active context object (#this) is not the root context object (#root), | ||||
| 			// that means that #this is being evaluated within a nested scope (for example, | ||||
| 			// collection selection or collection project), which is not a compilable | ||||
| 			// expression, so we return the result without setting the exit type descriptor. | ||||
| 			if (result != state.getRootContextObject()) { | ||||
| 				return result; | ||||
| 			} | ||||
| 		} | ||||
| 		if (ROOT.equals(this.name)) { | ||||
| 			TypedValue result = state.getRootContextObject(); | ||||
| 			this.exitTypeDescriptor = CodeFlow.toDescriptorFromObject(result.getValue()); | ||||
| 			return result; | ||||
| 		else if (ROOT.equals(this.name)) { | ||||
| 			result = state.getRootContextObject(); | ||||
| 		} | ||||
| 		else { | ||||
| 			result = state.lookupVariable(this.name); | ||||
| 		} | ||||
| 
 | ||||
| 		TypedValue result = state.lookupVariable(this.name); | ||||
| 		setExitTypeDescriptor(result.getValue()); | ||||
| 
 | ||||
| 		// A null value in the returned TypedValue will mean either the value was | ||||
|  | @ -132,7 +139,7 @@ public class VariableReference extends SpelNodeImpl { | |||
| 
 | ||||
| 	@Override | ||||
| 	public void generateCode(MethodVisitor mv, CodeFlow cf) { | ||||
| 		if (ROOT.equals(this.name)) { | ||||
| 		if (THIS.equals(this.name) || ROOT.equals(this.name)) { | ||||
| 			mv.visitVarInsn(ALOAD, 1); | ||||
| 		} | ||||
| 		else { | ||||
|  |  | |||
|  | @ -19,6 +19,7 @@ package org.springframework.expression.spel; | |||
| import java.io.IOException; | ||||
| import java.lang.reflect.Field; | ||||
| import java.lang.reflect.Method; | ||||
| import java.lang.reflect.Modifier; | ||||
| import java.util.ArrayList; | ||||
| import java.util.Arrays; | ||||
| import java.util.Collection; | ||||
|  | @ -52,7 +53,6 @@ import org.springframework.expression.spel.testdata.PersonInOtherPackage; | |||
| 
 | ||||
| import static java.util.stream.Collectors.joining; | ||||
| import static org.assertj.core.api.Assertions.assertThat; | ||||
| import static org.assertj.core.api.Assertions.assertThatException; | ||||
| import static org.assertj.core.api.Assertions.assertThatExceptionOfType; | ||||
| import static org.assertj.core.api.Assertions.within; | ||||
| import static org.assertj.core.api.InstanceOfAssertFactories.BOOLEAN; | ||||
|  | @ -124,7 +124,7 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests { | |||
| 	 * Further TODOs for compilation: | ||||
| 	 * | ||||
| 	 * - OpMinus with a single literal operand could be treated as a negative literal. Will save a | ||||
| 	 *   pointless loading of 0 and then a subtract instruction in code geneneration. | ||||
| 	 *   pointless loading of 0 and then a subtract instruction in code generation. | ||||
| 	 * | ||||
| 	 * - allow other accessors/resolvers to participate in compilation and create their own code. | ||||
| 	 * | ||||
|  | @ -143,6 +143,84 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests { | |||
| 	private SpelNodeImpl ast; | ||||
| 
 | ||||
| 
 | ||||
| 	@Nested | ||||
| 	class VariableReferenceTests { | ||||
| 
 | ||||
| 		@ParameterizedTest  // gh-32356 | ||||
| 		@ValueSource(strings = { "#root", "#this" }) | ||||
| 		void rootVariableWithPublicType(String spel) { | ||||
| 			String string = "hello"; | ||||
| 			expression = parser.parseExpression(spel); | ||||
| 			Object result = expression.getValue(string, String.class); | ||||
| 			assertThat(result).isEqualTo(string); | ||||
| 			assertCanCompile(expression); | ||||
| 			result = expression.getValue(string, String.class); | ||||
| 			assertThat(result).isEqualTo(string); | ||||
| 
 | ||||
| 			Integer number = 42; | ||||
| 			expression = parser.parseExpression(spel); | ||||
| 			result = expression.getValue(number, Integer.class); | ||||
| 			assertThat(result).isEqualTo(number); | ||||
| 			assertCanCompile(expression); | ||||
| 			result = expression.getValue(number, Integer.class); | ||||
| 			assertThat(result).isEqualTo(number); | ||||
| 		} | ||||
| 
 | ||||
| 		@ParameterizedTest  // gh-32356 | ||||
| 		@ValueSource(strings = { | ||||
| 			"#root.empty ? 0 : #root.size", | ||||
| 			"#this.empty ? 0 : #this.size" | ||||
| 		}) | ||||
| 		void rootVariableWithNonPublicType(String spel) { | ||||
| 			Map<String, Integer> map = Map.of("a", 13, "b", 42); | ||||
| 
 | ||||
| 			// Prerequisite: root type must not be public for this use case. | ||||
| 			assertThat(Modifier.isPublic(map.getClass().getModifiers())).isFalse(); | ||||
| 
 | ||||
| 			expression = parser.parseExpression(spel); | ||||
| 			Integer result = expression.getValue(map, Integer.class); | ||||
| 			assertThat(result).isEqualTo(2); | ||||
| 			assertCanCompile(expression); | ||||
| 			result = expression.getValue(map, Integer.class); | ||||
| 			assertThat(result).isEqualTo(2); | ||||
| 		} | ||||
| 
 | ||||
| 		@Test | ||||
| 		void userDefinedVariable() { | ||||
| 			EvaluationContext ctx = new StandardEvaluationContext(); | ||||
| 			ctx.setVariable("target", "abc"); | ||||
| 			expression = parser.parseExpression("#target"); | ||||
| 			assertThat(expression.getValue(ctx)).isEqualTo("abc"); | ||||
| 			assertCanCompile(expression); | ||||
| 			assertThat(expression.getValue(ctx)).isEqualTo("abc"); | ||||
| 			ctx.setVariable("target", "123"); | ||||
| 			assertThat(expression.getValue(ctx)).isEqualTo("123"); | ||||
| 
 | ||||
| 			// Changing the variable type from String to Integer results in a | ||||
| 			// ClassCastException in the compiled code. | ||||
| 			ctx.setVariable("target", 42); | ||||
| 			assertThatExceptionOfType(SpelEvaluationException.class) | ||||
| 				.isThrownBy(() -> expression.getValue(ctx)) | ||||
| 				.withCauseInstanceOf(ClassCastException.class); | ||||
| 
 | ||||
| 			ctx.setVariable("target", "abc"); | ||||
| 			expression = parser.parseExpression("#target.charAt(0)"); | ||||
| 			assertThat(expression.getValue(ctx)).isEqualTo('a'); | ||||
| 			assertCanCompile(expression); | ||||
| 			assertThat(expression.getValue(ctx)).isEqualTo('a'); | ||||
| 			ctx.setVariable("target", "1"); | ||||
| 			assertThat(expression.getValue(ctx)).isEqualTo('1'); | ||||
| 
 | ||||
| 			// Changing the variable type from String to Integer results in a | ||||
| 			// ClassCastException in the compiled code. | ||||
| 			ctx.setVariable("target", 42); | ||||
| 			assertThatExceptionOfType(SpelEvaluationException.class) | ||||
| 				.isThrownBy(() -> expression.getValue(ctx)) | ||||
| 				.withCauseInstanceOf(ClassCastException.class); | ||||
| 		} | ||||
| 
 | ||||
| 	} | ||||
| 
 | ||||
| 	@Nested | ||||
| 	class IndexingTests { | ||||
| 
 | ||||
|  | @ -466,10 +544,13 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests { | |||
| 			assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); | ||||
| 		} | ||||
| 
 | ||||
| 		@Test | ||||
| 		@Test  // gh-32356 | ||||
| 		void indexIntoMapOfPrimitiveIntArray() { | ||||
| 			Map<String, int[]> map = Map.of("foo", new int[] { 1, 2, 3 }); | ||||
| 
 | ||||
| 			// Prerequisite: root type must not be public for this use case. | ||||
| 			assertThat(Modifier.isPublic(map.getClass().getModifiers())).isFalse(); | ||||
| 
 | ||||
| 			// map key access | ||||
| 			expression = parser.parseExpression("['foo']"); | ||||
| 
 | ||||
|  | @ -479,22 +560,37 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests { | |||
| 			assertThat(stringify(expression.getValue(map))).isEqualTo("1 2 3"); | ||||
| 			assertThat(getAst().getExitDescriptor()).isEqualTo("Ljava/lang/Object"); | ||||
| 
 | ||||
| 			// map key access & array index | ||||
| 			// map key access via implicit #root & array index | ||||
| 			expression = parser.parseExpression("['foo'][1]"); | ||||
| 
 | ||||
| 			assertThat(expression.getValue(map)).isEqualTo(2); | ||||
| 			assertCanCompile(expression); | ||||
| 			assertThat(expression.getValue(map)).isEqualTo(2); | ||||
| 
 | ||||
| 			// map key access via explicit #root & array index | ||||
| 			expression = parser.parseExpression("#root['foo'][1]"); | ||||
| 
 | ||||
| 			assertThat(expression.getValue(map)).isEqualTo(2); | ||||
| 			assertCanCompile(expression); | ||||
| 			assertThat(expression.getValue(map)).isEqualTo(2); | ||||
| 
 | ||||
| 			// map key access via explicit #this & array index | ||||
| 			expression = parser.parseExpression("#this['foo'][1]"); | ||||
| 
 | ||||
| 			assertThat(expression.getValue(map)).isEqualTo(2); | ||||
| 			assertCanCompile(expression); | ||||
| 			assertThat(expression.getValue(map)).isEqualTo(2); | ||||
| 		} | ||||
| 
 | ||||
| 		@Test | ||||
| 		@Test  // gh-32356 | ||||
| 		void indexIntoMapOfPrimitiveIntArrayWithCompilableMapAccessor() { | ||||
| 			StandardEvaluationContext context = new StandardEvaluationContext(); | ||||
| 			context.addPropertyAccessor(new CompilableMapAccessor()); | ||||
| 
 | ||||
| 			// Map<String, int[]> map = Map.of("foo", new int[] { 1, 2, 3 }); | ||||
| 			Map<String, int[]> map = new HashMap<>(); | ||||
| 			map.put("foo", new int[] { 1, 2, 3 }); | ||||
| 			Map<String, int[]> map = Map.of("foo", new int[] { 1, 2, 3 }); | ||||
| 
 | ||||
| 			// Prerequisite: root type must not be public for this use case. | ||||
| 			assertThat(Modifier.isPublic(map.getClass().getModifiers())).isFalse(); | ||||
| 
 | ||||
| 			// map key access | ||||
| 			expression = parser.parseExpression("['foo']"); | ||||
|  | @ -516,12 +612,13 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests { | |||
| 
 | ||||
| 			assertThat(expression.getValue(context, map)).isEqualTo(2); | ||||
| 			assertCanCompile(expression); | ||||
| 			// TODO If map is created via Map.of(), the following fails with an IllegalAccessError. | ||||
| 			// | ||||
| 			// IllegalAccessError: failed to access class java.util.ImmutableCollections$Map1 from class | ||||
| 			// spel.Ex2774 (java.util.ImmutableCollections$Map1 is in module java.base of loader 'bootstrap'; | ||||
| 			// spel.Ex2774 is in unnamed module of loader | ||||
| 			// org.springframework.expression.spel.standard.SpelCompiler$ChildClassLoader @359b650b) | ||||
| 			assertThat(expression.getValue(context, map)).isEqualTo(2); | ||||
| 
 | ||||
| 			// custom CompilableMapAccessor via explicit #this & array index | ||||
| 			expression = parser.parseExpression("#this.foo[1]"); | ||||
| 
 | ||||
| 			assertThat(expression.getValue(context, map)).isEqualTo(2); | ||||
| 			assertCanCompile(expression); | ||||
| 			assertThat(expression.getValue(context, map)).isEqualTo(2); | ||||
| 
 | ||||
| 			// map key access & array index | ||||
|  | @ -644,7 +741,9 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests { | |||
| 		assertThat(expression.getValue()).isEqualTo(boolean.class); | ||||
| 
 | ||||
| 		expression = parse("T(Missing)"); | ||||
| 		assertGetValueFail(expression); | ||||
| 		assertThatExceptionOfType(SpelEvaluationException.class) | ||||
| 				.isThrownBy(expression::getValue) | ||||
| 				.withMessageEndingWith("Type cannot be found 'Missing'"); | ||||
| 		assertCantCompile(expression); | ||||
| 	} | ||||
| 
 | ||||
|  | @ -915,16 +1014,20 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests { | |||
| 
 | ||||
| 		// Code gen is different for -1 .. 6 because there are bytecode instructions specifically for those values | ||||
| 
 | ||||
| 		// Not an int literal but an opminus with one operand: | ||||
| 		// expression = parser.parseExpression("-1"); | ||||
| 		// assertCanCompile(expression); | ||||
| 		// assertEquals(-1, expression.getValue()); | ||||
| 		// Not an int literal but an opMinus with one operand: | ||||
| 		expression = parser.parseExpression("-1"); | ||||
| 		expression.getValue(Integer.class); | ||||
| 		assertCanCompile(expression); | ||||
| 		assertThat(expression.getValue()).isEqualTo(-1); | ||||
| 
 | ||||
| 		expression = parser.parseExpression("0"); | ||||
| 		assertCanCompile(expression); | ||||
| 		assertThat(expression.getValue()).isEqualTo(0); | ||||
| 
 | ||||
| 		expression = parser.parseExpression("2"); | ||||
| 		assertCanCompile(expression); | ||||
| 		assertThat(expression.getValue()).isEqualTo(2); | ||||
| 
 | ||||
| 		expression = parser.parseExpression("7"); | ||||
| 		assertCanCompile(expression); | ||||
| 		assertThat(expression.getValue()).isEqualTo(7); | ||||
|  | @ -1374,23 +1477,6 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests { | |||
| 		assertCanCompile(expression); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	void variableReference_root() { | ||||
| 		String s = "hello"; | ||||
| 		Expression expression = parser.parseExpression("#root"); | ||||
| 		String resultI = expression.getValue(s, String.class); | ||||
| 		assertCanCompile(expression); | ||||
| 		String resultC = expression.getValue(s, String.class); | ||||
| 		assertThat(resultI).isEqualTo(s); | ||||
| 		assertThat(resultC).isEqualTo(s); | ||||
| 
 | ||||
| 		expression = parser.parseExpression("#root"); | ||||
| 		int i = (Integer) expression.getValue(42); | ||||
| 		assertThat(i).isEqualTo(42); | ||||
| 		assertCanCompile(expression); | ||||
| 		i = (Integer) expression.getValue(42); | ||||
| 		assertThat(i).isEqualTo(42); | ||||
| 	} | ||||
| 
 | ||||
| 	public static String concat(String a, String b) { | ||||
| 		return a+b; | ||||
|  | @ -1776,34 +1862,6 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests { | |||
| 		assertThat(expression.getValue(ctx)).isEqualTo("abc"); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	void variableReference_userDefined() { | ||||
| 		EvaluationContext ctx = new StandardEvaluationContext(); | ||||
| 		ctx.setVariable("target", "abc"); | ||||
| 		expression = parser.parseExpression("#target"); | ||||
| 		assertThat(expression.getValue(ctx)).isEqualTo("abc"); | ||||
| 		assertCanCompile(expression); | ||||
| 		assertThat(expression.getValue(ctx)).isEqualTo("abc"); | ||||
| 		ctx.setVariable("target", "123"); | ||||
| 		assertThat(expression.getValue(ctx)).isEqualTo("123"); | ||||
| 		ctx.setVariable("target", 42); | ||||
| 		assertThatExceptionOfType(SpelEvaluationException.class) | ||||
| 			.isThrownBy(() -> expression.getValue(ctx)) | ||||
| 			.withCauseInstanceOf(ClassCastException.class); | ||||
| 
 | ||||
| 		ctx.setVariable("target", "abc"); | ||||
| 		expression = parser.parseExpression("#target.charAt(0)"); | ||||
| 		assertThat(expression.getValue(ctx)).isEqualTo('a'); | ||||
| 		assertCanCompile(expression); | ||||
| 		assertThat(expression.getValue(ctx)).isEqualTo('a'); | ||||
| 		ctx.setVariable("target", "1"); | ||||
| 		assertThat(expression.getValue(ctx)).isEqualTo('1'); | ||||
| 		ctx.setVariable("target", 42); | ||||
| 		assertThatExceptionOfType(SpelEvaluationException.class) | ||||
| 			.isThrownBy(() -> expression.getValue(ctx)) | ||||
| 			.withCauseInstanceOf(ClassCastException.class); | ||||
| 	} | ||||
| 
 | ||||
| 	@Test | ||||
| 	void opLt() { | ||||
| 		expression = parse("3.0d < 4.0d"); | ||||
|  | @ -5402,21 +5460,23 @@ public class SpelCompilationCoverageTests extends AbstractExpressionTests { | |||
| 	} | ||||
| 
 | ||||
| 	private void assertCanCompile(Expression expression) { | ||||
| 		assertThat(SpelCompiler.compile(expression)).isTrue(); | ||||
| 		assertThat(SpelCompiler.compile(expression)) | ||||
| 				.as(() -> "Expression <%s> should be compilable" | ||||
| 						.formatted(((SpelExpression) expression).toStringAST())) | ||||
| 				.isTrue(); | ||||
| 	} | ||||
| 
 | ||||
| 	private void assertCantCompile(Expression expression) { | ||||
| 		assertThat(SpelCompiler.compile(expression)).isFalse(); | ||||
| 		assertThat(SpelCompiler.compile(expression)) | ||||
| 				.as(() -> "Expression <%s> should not be compilable" | ||||
| 						.formatted(((SpelExpression) expression).toStringAST())) | ||||
| 				.isFalse(); | ||||
| 	} | ||||
| 
 | ||||
| 	private Expression parse(String expression) { | ||||
| 		return parser.parseExpression(expression); | ||||
| 	} | ||||
| 
 | ||||
| 	private void assertGetValueFail(Expression expression) { | ||||
| 		assertThatException().isThrownBy(expression::getValue); | ||||
| 	} | ||||
| 
 | ||||
| 
 | ||||
| 	// Nested types | ||||
| 
 | ||||
|  |  | |||
		Loading…
	
		Reference in New Issue