简体   繁体   中英

SpEL - disable short circuit evaluation

In my application, I have objects with several SpEL expressions that usually contains signatures of methods with boolean return type to invoke and logical operators. Before these objects are cached, I check the consistency of the expression by simply executing the parsed expression. When an exception is thrown, I set an appropriate flag inside the object to indicate that the expression is invalid to avoid further execution.

I am executing the expression on an EvaluationContext that implements all methods that are permitted to be a part of the expression. All these methods return false . I have come across a problem that involves a short circuit evaluation.

Given methodOne and methodTwo are the only permitted methods to invoke, this expression correctly sets the inconsistency flag

methodERROROne("arg") AND methodTwo("arg")

this one, however, does not because methodOne returns false , Spring uses short circuit evaluation and does not execute the remaining operands. This causes the expression to fail when it is executed on real EvaluationContext and the methodOne returns true

methodOne("arg") AND methodERRORTwo("arg")

Is there a way to disable short circuit evaluation is Spring expression language?

No; the OpAnd operator always short-circuits...

@Override
public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
    if (!getBooleanValue(state, getLeftOperand())) {
        // no need to evaluate right operand
        return BooleanTypedValue.FALSE;
    }
    return BooleanTypedValue.forValue(getBooleanValue(state, getRightOperand()));
}

...there is no equivalent to Java's & operator.

EDIT

All these methods return false

If they all return false can't you use "!(!m1() and !m2())" ?

or even

"!((!m1() or m1()) and (!m2() or m2()))"

If you really want to disable short circuit evaluation it is possible, but you have to get your hands dirty. As Gary pointed out logic in OpAnd (OpOr) is fixed. So we have to change/override implementation of OpAnd/OpOr.getValueInternal(ExpressionState state). This requies some source code copying, so source code can be found here . My samples may differ a bit since I implemented it and tested on different version of maven dependency:

<groupId>org.springframework</groupId>
<artifactId>spring-expression</artifactId>
<version>5.1.5.RELEASE</version>

First of all, we have to implement our own version of OpAnd and OpOr. Since OpAnd.getBooleanValue(ExpressionState state, SpelNodeImpl operand) and SpelNodeImpl.getValue(ExpressionState state, Class desiredReturnType) are private, we have to implement our version of getBooleanValue.

import org.springframework.expression.EvaluationException;
import org.springframework.expression.TypedValue;
import org.springframework.expression.common.ExpressionUtils;
import org.springframework.expression.spel.ExpressionState;
import org.springframework.expression.spel.SpelEvaluationException;
import org.springframework.expression.spel.SpelMessage;
import org.springframework.expression.spel.ast.OpAnd;
import org.springframework.expression.spel.ast.SpelNodeImpl;
import org.springframework.expression.spel.support.BooleanTypedValue;
import org.springframework.lang.Nullable;

public class DomailOpAnd extends OpAnd {

    public static boolean getBooleanValue(ExpressionState state, SpelNodeImpl operand) {
        try {
            Boolean value = ExpressionUtils.convertTypedValue(state.getEvaluationContext(), operand.getValueInternal(state), Boolean.class);
            //Boolean value = operand.getValue(state, Boolean.class);
            assertValueNotNull(value);
            return value;
        }
        catch (SpelEvaluationException ex) {
            ex.setPosition(operand.getStartPosition());
            throw ex;
        }
    }
    
    private static void assertValueNotNull(@Nullable Boolean value) {
        if (value == null) {
            throw new SpelEvaluationException(SpelMessage.TYPE_CONVERSION_ERROR, "null", "boolean");
        }
    }
    
    public DomailOpAnd(int pos, SpelNodeImpl ... operands) {
        super(pos, operands);
    }
    
    @Override
    public TypedValue getValueInternal(ExpressionState state) throws EvaluationException {
        //HERE is our non-short-circuiting logic:
        boolean left = getBooleanValue(state, getLeftOperand());
        boolean right = getBooleanValue(state, getRightOperand());
        return BooleanTypedValue.forValue(left && right);
    }
}

public class DomailOpOr extends OpOr {

    public DomailOpOr(int pos, SpelNodeImpl ... operands) {
        super(pos, operands);
    }

    @Override
    public BooleanTypedValue getValueInternal(ExpressionState state) throws EvaluationException {
        Boolean left = DomailOpAnd.getBooleanValue(state, getLeftOperand());
        Boolean right = DomailOpAnd.getBooleanValue(state, getRightOperand());
        return BooleanTypedValue.forValue(left || right);
    }
}

Now we have to use our versions of OpAnd and OpOr where they are created in InternalSpelExpressionParser. But InternalSpelExpressionParser is not a public class so we cannot use override as with OpAnd. We have to copy InternalSpelExpressionParser source code and create own class. Here I show only edited parts:

/**
 * Hand-written SpEL parser. Instances are reusable but are not thread-safe.
 *
 * @author Andy Clement
 * @author Juergen Hoeller
 * @author Phillip Webb
 * @since 3.0
 */
class DomailInternalExpressionParser extends TemplateAwareExpressionParser {
//rest of class ...
    @Nullable
    private SpelNodeImpl eatLogicalOrExpression() {
        SpelNodeImpl expr = eatLogicalAndExpression();
        while (peekIdentifierToken("or") || peekToken(TokenKind.SYMBOLIC_OR)) {
            Token t = takeToken();  //consume OR
            SpelNodeImpl rhExpr = eatLogicalAndExpression();
            checkOperands(t, expr, rhExpr);
            expr = new DomailOpOr(toPos(t), expr, rhExpr);
        }
        return expr;
    }

    @Nullable
    private SpelNodeImpl eatLogicalAndExpression() {
        SpelNodeImpl expr = eatRelationalExpression();
        while (peekIdentifierToken("and") || peekToken(TokenKind.SYMBOLIC_AND)) {
            Token t = takeToken();  // consume 'AND'
            SpelNodeImpl rhExpr = eatRelationalExpression();
            checkOperands(t, expr, rhExpr);
            expr = new DomailOpAnd(toPos(t), expr, rhExpr);
        }
        return expr;
    }
//rest of class ... 

This also requires to copy source of other non public classes to be available:

  • org.springframework.expression.spel.standard.Token
  • org.springframework.expression.spel.standard.Tokenizer
  • org.springframework.expression.spel.standard.TokenKind

Finally we swap implementation of parser in SpelExpression Parser:

import org.springframework.expression.ParseException;
import org.springframework.expression.ParserContext;
import org.springframework.expression.spel.SpelParserConfiguration;
import org.springframework.expression.spel.standard.SpelExpression;
import org.springframework.expression.spel.standard.SpelExpressionParser;

public class DomailExpressionParser extends SpelExpressionParser {

    private final SpelParserConfiguration configuration;
    public DomailExpressionParser(SpelParserConfiguration configuration) {
        super(configuration);
        this.configuration = configuration;
    }
    
    // we cannot use this because configuration is not visible
    //  public DomailExpressionParser() {
    //      super();
    //  }

    @Override
    protected SpelExpression doParseExpression(String expressionString, ParserContext context) throws ParseException {
        return new DomailInternalExpressionParser(this.configuration).doParseExpression(expressionString, context);
    }

}

and use our version of parser as if it would be SpelExpressionParser:

SpelParserConfiguration engineConfig = new SpelParserConfiguration();
ExpressionParser engine = new DomailExpressionParser(engineConfig);
Expression parsedScript = engine.parseExpression(script);
T result = parsedScript.getValue(scriptCtx, resultType);

We can test it on simple expression "false && blabla":

  • with SpelExpressionParser there is no error
  • with DomailExpressionParser I get org.springframework.expression.spel.SpelEvaluationException: EL1008E: Property or field 'blabla' cannot be found on object of type 'sk.dominanz.domic.common.eml.service.EmlScriptRootObject' - maybe not public or not valid?

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM