Recover from error during SpEL MIXED mode compilation

Prior to this commit, SpEL was able to recover from an error that
occurred while running a CompiledExpression; however, SpEL was not able
to recover from an error that occurred while compiling the expression
(such as a java.lang.VerifyError). The latter can occur when multiple
threads concurrently change types involved in the expression, such as
the concrete type of a custom variable registered via
EvaluationContext.setVariable(...), which can result in SpEL generating
invalid bytecode.

This commit addresses this issue by catching exceptions thrown while
compiling an expression and updating the `failedAttempts` and
`interpretedCount` counters accordingly. If an exception is caught
while operating in SpelCompilerMode.IMMEDIATE mode, the exception will
be propagated via a SpelEvaluationException with a new
SpelMessage.EXCEPTION_COMPILING_EXPRESSION error category.

Closes gh-28043
This commit is contained in:
Sam Brannen 2022-02-18 15:31:59 +01:00
parent ff20a06876
commit 94af2ca06b
4 changed files with 77 additions and 14 deletions

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2021 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -27,10 +27,11 @@ import java.text.MessageFormat;
* <p>When a message is formatted, it will have this kind of form, capturing the prefix
* and the error kind:
*
* <pre class="code">EL1004E: Type cannot be found 'String'</pre>
* <pre class="code">EL1005E: Type cannot be found 'String'</pre>
*
* @author Andy Clement
* @author Juergen Hoeller
* @author Sam Brannen
* @since 3.0
*/
public enum SpelMessage {
@ -255,7 +256,11 @@ public enum SpelMessage {
/** @since 4.3.17 */
FLAWED_PATTERN(Kind.ERROR, 1073,
"Failed to efficiently evaluate pattern ''{0}'': consider redesigning it");
"Failed to efficiently evaluate pattern ''{0}'': consider redesigning it"),
/** @since 5.3.16 */
EXCEPTION_COMPILING_EXPRESSION(Kind.ERROR, 1074,
"An exception occurred while compiling an expression");
private final Kind kind;

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -110,7 +110,8 @@ public final class SpelCompiler implements Opcodes {
return ReflectionUtils.accessibleConstructor(clazz).newInstance();
}
catch (Throwable ex) {
throw new IllegalStateException("Failed to instantiate CompiledExpression", ex);
throw new IllegalStateException("Failed to instantiate CompiledExpression for expression: " +
expression.toStringAST(), ex);
}
}
}

View File

@ -1,5 +1,5 @@
/*
* Copyright 2002-2020 the original author or authors.
* Copyright 2002-2022 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@ -44,6 +44,7 @@ import org.springframework.util.Assert;
*
* @author Andy Clement
* @author Juergen Hoeller
* @author Sam Brannen
* @since 3.0
*/
public class SpelExpression implements Expression {
@ -522,17 +523,34 @@ public class SpelExpression implements Expression {
// Compiled by another thread before this thread got into the sync block
return true;
}
SpelCompiler compiler = SpelCompiler.getCompiler(this.configuration.getCompilerClassLoader());
compiledAst = compiler.compile(this.ast);
if (compiledAst != null) {
// Successfully compiled
this.compiledAst = compiledAst;
return true;
try {
SpelCompiler compiler = SpelCompiler.getCompiler(this.configuration.getCompilerClassLoader());
compiledAst = compiler.compile(this.ast);
if (compiledAst != null) {
// Successfully compiled
this.compiledAst = compiledAst;
return true;
}
else {
// Failed to compile
this.failedAttempts.incrementAndGet();
return false;
}
}
else {
catch (Exception ex) {
// Failed to compile
this.failedAttempts.incrementAndGet();
return false;
// If running in mixed mode, revert to interpreted
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
this.compiledAst = null;
this.interpretedCount.set(0);
return false;
}
else {
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
throw new SpelEvaluationException(ex, SpelMessage.EXCEPTION_COMPILING_EXPRESSION);
}
}
}
}

View File

@ -76,6 +76,21 @@ class SpelCompilerTests {
assertThat(expression.getValue(context)).asInstanceOf(BOOLEAN).isTrue();
}
@Test // gh-28043
void changingRegisteredVariableTypeDoesNotResultInFailureInMixedMode() {
SpelParserConfiguration config = new SpelParserConfiguration(SpelCompilerMode.MIXED, null);
SpelExpressionParser parser = new SpelExpressionParser(config);
Expression sharedExpression = parser.parseExpression("#bean.value");
StandardEvaluationContext context = new StandardEvaluationContext();
Object[] beans = new Object[] {new Bean1(), new Bean2(), new Bean3(), new Bean4()};
IntStream.rangeClosed(1, 1_000_000).parallel().forEach(count -> {
context.setVariable("bean", beans[count % 4]);
assertThat(sharedExpression.getValue(context)).asString().startsWith("1");
});
}
static class OrderedComponent implements Ordered {
@ -121,4 +136,28 @@ class SpelCompilerTests {
boolean hasSomeProperty();
}
public static class Bean1 {
public String getValue() {
return "11";
}
}
public static class Bean2 {
public Integer getValue() {
return 111;
}
}
public static class Bean3 {
public Float getValue() {
return 1.23f;
}
}
public static class Bean4 {
public Character getValue() {
return '1';
}
}
}