Affects: 5.2.13.RELEASE


I am testing spel performance, when I share a SpelExpression instance between multiple threads.

I tried set SpelCompilerMode to MIXED or IMMEDIATE and put a variable with different type in the context, then I triggered an exception.

If I set SpelCompilerMode to OFF, it works correctly. So I am confused, is spel thread safe?

I didn't find any description of spel's thread safety in the spring project document. I want to ask it's a spel thread safe bug or spel is not thread safe when compile is enable.

Here is my test code:

import java.util.concurrent.atomic.AtomicInteger;

import org.springframework.expression.EvaluationContext;
import org.springframework.expression.Expression;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.SpelCompilerMode;
import org.springframework.expression.spel.SpelParserConfiguration;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.DataBindingMethodResolver;
import org.springframework.expression.spel.support.SimpleEvaluationContext;

import lombok.AllArgsConstructor;
import lombok.Data;

/**
 * @author happier233
 * @version Main.java, v 0.1 2021年08月18日 10:02 上午 happier233
 */
public class Main {

    public static String                  name                    = "default";

    public static Context                 root                    = new Context();

    public static SpelParserConfiguration spelParserConfiguration = new SpelParserConfiguration(
        SpelCompilerMode.MIXED, Main.class.getClassLoader());
    public static ExpressionParser        parser                  = new SpelExpressionParser(
        spelParserConfiguration);

    public static Expression              expression              = parser
        .parseExpression("#bean.name + '234'");

    public static void main(String[] args) {
        calc(() -> {
            for (int i = 0; i < 10; i++) {
                calc(() -> run(true));
            }
        });
        new Thread(Main::run).start();
        new Thread(Main::run).start();
    }

    public static void run() {
        calc(() -> {
            for (int i = 0; i < 1000000; i++) {
                run(false);
            }
        });
    }

    private static final AtomicInteger cc = new AtomicInteger();

    private static void run(boolean flag) {
        EvaluationContext context = SimpleEvaluationContext.forReadOnlyDataBinding()
            .withRootObject(root)
            .withMethodResolvers(DataBindingMethodResolver.forInstanceMethodInvocation()).build();
        if ((cc.incrementAndGet() & 1) == 0) {
            context.setVariable("bean", new Bean("test"));
        } else {
            context.setVariable("bean", new Bean2(123));
        }
        Object value = expression.getValue(context);
        if (flag) {
            System.out.println(value);
        }
    }

    public static void calc(Runnable runnable) {
        long start = System.currentTimeMillis();
        runnable.run();
        long end = System.currentTimeMillis();
        System.out.println("time[" + name + "]: " + (end - start));
    }

    @Data
    public static class Context {
        private String        title = "233";
        private AtomicInteger count = new AtomicInteger(0);
    }

    @Data
    @AllArgsConstructor
    public static class Bean {
        private String name;
    }

    @Data
    @AllArgsConstructor
    public static class Bean2 {
        private Integer name;
    }
}

Here is the excpetion stack:

Exception in thread "Thread-1" java.lang.IllegalStateException: Failed to instantiate CompiledExpression
    at org.springframework.expression.spel.standard.SpelCompiler.compile(SpelCompiler.java:113)
    at org.springframework.expression.spel.standard.SpelExpression.compileExpression(SpelExpression.java:526)
    at org.springframework.expression.spel.standard.SpelExpression.checkCompile(SpelExpression.java:497)
    at org.springframework.expression.spel.standard.SpelExpression.getValue(SpelExpression.java:273)
    at org.example.acm.Main.run(Main.java:65)
    at org.example.acm.Main.lambda$run$2(Main.java:49)
    at org.example.acm.Main.calc(Main.java:73)
    at org.example.acm.Main.run(Main.java:47)
    at java.lang.Thread.run(Thread.java:750)
Caused by: java.lang.VerifyError: (class: spel/Ex27, method: getValue signature: (Ljava/lang/Object;Lorg/springframework/expression/EvaluationContext;)Ljava/lang/Object;) Incompatible object argument for function call
    at java.lang.Class.getDeclaredConstructors0(Native Method)
    at java.lang.Class.privateGetDeclaredConstructors(Class.java:2671)
    at java.lang.Class.getConstructor0(Class.java:3075)
    at java.lang.Class.getDeclaredConstructor(Class.java:2178)
    at org.springframework.util.ReflectionUtils.accessibleConstructor(ReflectionUtils.java:185)
    at org.springframework.expression.spel.standard.SpelCompiler.compile(SpelCompiler.java:110)
    ... 8 more

Comment From: happier233

I made a mistake, this problem is not happened between multiple threads. I remove the multi thread code, I still happened. Just When I put a variable with different type in the context, the expcetion will happen.

Comment From: sbrannen

Hi @happier233,

Thanks for reporting your first issue for the Spring Framework. 👍

This appears to be a bug in SpEL with regard to state tracking for the SpelExpression.interpretedCount field.

Specifically, interpretedCount is always incremented in checkCompile(ExpressionState) even if the call to compileExpression() fails with an exception.

We'll fix it.

Comment From: sbrannen

I made a mistake, this problem is not happened between multiple threads. I remove the multi thread code, I still happened.

Are you positive that the same error occurs without concurrent use of the shared expression instance?

If so, could you please share an example without concurrent access which demonstrates that?

Comment From: sbrannen

Update

Using a simplified version of the original example, I can reliably reproduce a stack trace similar to the following.

```Exception in thread "main" java.lang.IllegalStateException: java.lang.IllegalStateException: Failed to instantiate CompiledExpression at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at java.util.concurrent.ForkJoinTask.getThrowableException(ForkJoinTask.java:593) at java.util.concurrent.ForkJoinTask.reportException(ForkJoinTask.java:677) at java.util.concurrent.ForkJoinTask.invoke(ForkJoinTask.java:735) at java.util.stream.ForEachOps$ForEachOp.evaluateParallel(ForEachOps.java:159) at java.util.stream.ForEachOps$ForEachOp$OfInt.evaluateParallel(ForEachOps.java:188) at java.util.stream.AbstractPipeline.evaluate(AbstractPipeline.java:233) at java.util.stream.IntPipeline.forEach(IntPipeline.java:427) at java.util.stream.IntPipeline$Head.forEach(IntPipeline.java:584) at example.Main.main(Main.java:37) Caused by: java.lang.IllegalStateException: Failed to instantiate CompiledExpression at org.springframework.expression.spel.standard.SpelCompiler.compile(SpelCompiler.java:114) at org.springframework.expression.spel.standard.SpelExpression.compileExpression(SpelExpression.java:527) at org.springframework.expression.spel.standard.SpelExpression.checkCompile(SpelExpression.java:498) at org.springframework.expression.spel.standard.SpelExpression.getValue(SpelExpression.java:274) at example.Main.evaluateExpression(Main.java:67) at java.util.stream.ForEachOps$ForEachOp$OfInt.accept(ForEachOps.java:204) at java.util.stream.Streams$RangeIntSpliterator.forEachRemaining(Streams.java:110) at java.util.Spliterator$OfInt.forEachRemaining(Spliterator.java:693) at java.util.stream.AbstractPipeline.copyInto(AbstractPipeline.java:482) at java.util.stream.ForEachOps$ForEachTask.compute(ForEachOps.java:290) at java.util.concurrent.CountedCompleter.exec(CountedCompleter.java:731) at java.util.concurrent.ForkJoinTask.doExec(ForkJoinTask.java:289) at java.util.concurrent.ForkJoinPool$WorkQueue.runTask(ForkJoinPool.java:1056) at java.util.concurrent.ForkJoinPool.runWorker(ForkJoinPool.java:1692) at java.util.concurrent.ForkJoinWorkerThread.run(ForkJoinWorkerThread.java:175) Caused by: java.lang.VerifyError: Bad type on operand stack Exception Details: Location: spel/Ex10.getValue(Ljava/lang/Object;Lorg/springframework/expression/EvaluationContext;)Ljava/lang/Object; @11: invokevirtual Reason: Type 'example/Main$Bean2' (current frame, stack[0]) is not assignable to 'example/Main$Bean1' Current Frame: bci: @11 flags: { } locals: { 'spel/Ex10', 'java/lang/Object', 'org/springframework/expression/EvaluationContext' } stack: { 'example/Main$Bean2' } Bytecode: 0x0000000: 2c12 0eb9 0014 0200 c000 16b6 001c b0

at java.lang.Class.getDeclaredConstructors0(Native Method)
at java.lang.Class.privateGetDeclaredConstructors(Class.java:2671)
at java.lang.Class.getConstructor0(Class.java:3075)
at java.lang.Class.getDeclaredConstructor(Class.java:2178)
at org.springframework.util.ReflectionUtils.accessibleConstructor(ReflectionUtils.java:185)
at org.springframework.expression.spel.standard.SpelCompiler.compile(SpelCompiler.java:111)
... 14 more

```

The key thing to note is:

Type 'example/Main$Bean2' (current frame, stack[0]) is not assignable to 'example/Main$Bean1'

Based on my current understanding of the issue, this means that the compiled expression Class (spel/Ex10) generated by the SpelCompiler contains references to types from custom registered variables from two different evaluations (in this case Bean1 and Bean2) of the shared SpelExpression. Thus, there appears to be a concurrency issue with regard to the generation of the byte code for the compiled class.

Comment From: sbrannen

Related issue:

  • 24265

Comment From: happier233

Thus, there appears to be a concurrency issue with regard to the generation of the byte code for the compiled class.

Yes, I agree with that. In my project, I have an interface "Order" and there are multiple implementations. So I may invoke one SpelExpression with different implementations.

Comment From: happier233

In the implementation org.springframework.expression.spel.standard.SpelExpression#getValue, all exception will be caught, include compile error. When the compile model is MIXED, it will be failover to normal ast to get value. But in SpelExpression#checkCompile, there is no exception catahing.

Comment From: sbrannen

In my project, I have an interface "Order" and there are multiple implementations. So I may invoke one SpelExpression with different implementations.

Do the evaluated/compiled expressions all invoke only methods defined in your Order interface, and if so are there any generics involved in the method signatures?

Or does your expression invoke any method that is unique to a particular concrete implementation?

Comment From: sbrannen

This has been fixed in 5.3.x (for inclusion in 5.3.17) and main. See commit 94af2ca06bf711de03286999b1c64a703d6de552 for details.

Although we recommend that you not use the MIXED compiler mode in SpEL when frequently changing types used in the expression (such as a custom variable), this fix should allow you to do so. However, keep in mind that the compiler will continually try to compile expressions (generating classes for them on the fly) only to throw them away later, and at some point the compiler will determine that it does not make sense to continue trying (due to the failedAttempts threshold).

In other words, if you used MIXED mode in such scenarios you will not actually benefit from compiled expressions.

In the particular example provided, you could introduce a common interface that each of the types used as the bean variable can implement. If the method invoked on the bean variable comes from the common interface, SpEL should be able to reliably compile and reuse an expression based on that interface.