Skip to content

Commit 33dc3b0

Browse files
committed
Thread-safe compiled expression evaluation in SpelExpression
Closes gh-24265
1 parent a10f9f2 commit 33dc3b0

File tree

1 file changed

+70
-50
lines changed
  • spring-expression/src/main/java/org/springframework/expression/spel/standard

1 file changed

+70
-50
lines changed

spring-expression/src/main/java/org/springframework/expression/spel/standard/SpelExpression.java

Lines changed: 70 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2002-2018 the original author or authors.
2+
* Copyright 2002-2020 the original author or authors.
33
*
44
* Licensed under the Apache License, Version 2.0 (the "License");
55
* you may not use this file except in compliance with the License.
@@ -16,6 +16,8 @@
1616

1717
package org.springframework.expression.spel.standard;
1818

19+
import java.util.concurrent.atomic.AtomicInteger;
20+
1921
import org.springframework.core.convert.TypeDescriptor;
2022
import org.springframework.expression.EvaluationContext;
2123
import org.springframework.expression.EvaluationException;
@@ -62,15 +64,15 @@ public class SpelExpression implements Expression {
6264
private EvaluationContext evaluationContext;
6365

6466
// Holds the compiled form of the expression (if it has been compiled)
65-
private CompiledExpression compiledAst;
67+
private volatile CompiledExpression compiledAst;
6668

6769
// Count of many times as the expression been interpreted - can trigger compilation
6870
// when certain limit reached
69-
private volatile int interpretedCount = 0;
71+
private final AtomicInteger interpretedCount = new AtomicInteger(0);
7072

7173
// The number of times compilation was attempted and failed - enables us to eventually
7274
// give up trying to compile it when it just doesn't seem to be possible.
73-
private volatile int failedAttempts = 0;
75+
private final AtomicInteger failedAttempts = new AtomicInteger(0);
7476

7577

7678
/**
@@ -112,16 +114,17 @@ public String getExpressionString() {
112114

113115
@Override
114116
public Object getValue() throws EvaluationException {
115-
if (this.compiledAst != null) {
117+
CompiledExpression compiledAst = this.compiledAst;
118+
if (compiledAst != null) {
116119
try {
117120
EvaluationContext context = getEvaluationContext();
118-
return this.compiledAst.getValue(context.getRootObject().getValue(), context);
121+
return compiledAst.getValue(context.getRootObject().getValue(), context);
119122
}
120123
catch (Throwable ex) {
121124
// If running in mixed mode, revert to interpreted
122125
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
123-
this.interpretedCount = 0;
124126
this.compiledAst = null;
127+
this.interpretedCount.set(0);
125128
}
126129
else {
127130
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
@@ -139,10 +142,11 @@ public Object getValue() throws EvaluationException {
139142
@SuppressWarnings("unchecked")
140143
@Override
141144
public <T> T getValue(Class<T> expectedResultType) throws EvaluationException {
142-
if (this.compiledAst != null) {
145+
CompiledExpression compiledAst = this.compiledAst;
146+
if (compiledAst != null) {
143147
try {
144148
EvaluationContext context = getEvaluationContext();
145-
Object result = this.compiledAst.getValue(context.getRootObject().getValue(), context);
149+
Object result = compiledAst.getValue(context.getRootObject().getValue(), context);
146150
if (expectedResultType == null) {
147151
return (T) result;
148152
}
@@ -154,8 +158,8 @@ public <T> T getValue(Class<T> expectedResultType) throws EvaluationException {
154158
catch (Throwable ex) {
155159
// If running in mixed mode, revert to interpreted
156160
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
157-
this.interpretedCount = 0;
158161
this.compiledAst = null;
162+
this.interpretedCount.set(0);
159163
}
160164
else {
161165
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
@@ -173,15 +177,16 @@ public <T> T getValue(Class<T> expectedResultType) throws EvaluationException {
173177

174178
@Override
175179
public Object getValue(Object rootObject) throws EvaluationException {
176-
if (this.compiledAst != null) {
180+
CompiledExpression compiledAst = this.compiledAst;
181+
if (compiledAst != null) {
177182
try {
178-
return this.compiledAst.getValue(rootObject, getEvaluationContext());
183+
return compiledAst.getValue(rootObject, getEvaluationContext());
179184
}
180185
catch (Throwable ex) {
181186
// If running in mixed mode, revert to interpreted
182187
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
183-
this.interpretedCount = 0;
184188
this.compiledAst = null;
189+
this.interpretedCount.set(0);
185190
}
186191
else {
187192
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
@@ -200,9 +205,10 @@ public Object getValue(Object rootObject) throws EvaluationException {
200205
@SuppressWarnings("unchecked")
201206
@Override
202207
public <T> T getValue(Object rootObject, Class<T> expectedResultType) throws EvaluationException {
203-
if (this.compiledAst != null) {
208+
CompiledExpression compiledAst = this.compiledAst;
209+
if (compiledAst != null) {
204210
try {
205-
Object result = this.compiledAst.getValue(rootObject, getEvaluationContext());
211+
Object result = compiledAst.getValue(rootObject, getEvaluationContext());
206212
if (expectedResultType == null) {
207213
return (T)result;
208214
}
@@ -214,8 +220,8 @@ public <T> T getValue(Object rootObject, Class<T> expectedResultType) throws Eva
214220
catch (Throwable ex) {
215221
// If running in mixed mode, revert to interpreted
216222
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
217-
this.interpretedCount = 0;
218223
this.compiledAst = null;
224+
this.interpretedCount.set(0);
219225
}
220226
else {
221227
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
@@ -236,15 +242,16 @@ public <T> T getValue(Object rootObject, Class<T> expectedResultType) throws Eva
236242
public Object getValue(EvaluationContext context) throws EvaluationException {
237243
Assert.notNull(context, "EvaluationContext is required");
238244

239-
if (this.compiledAst != null) {
245+
CompiledExpression compiledAst = this.compiledAst;
246+
if (compiledAst != null) {
240247
try {
241-
return this.compiledAst.getValue(context.getRootObject().getValue(), context);
248+
return compiledAst.getValue(context.getRootObject().getValue(), context);
242249
}
243250
catch (Throwable ex) {
244251
// If running in mixed mode, revert to interpreted
245252
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
246-
this.interpretedCount = 0;
247253
this.compiledAst = null;
254+
this.interpretedCount.set(0);
248255
}
249256
else {
250257
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
@@ -264,9 +271,10 @@ public Object getValue(EvaluationContext context) throws EvaluationException {
264271
public <T> T getValue(EvaluationContext context, Class<T> expectedResultType) throws EvaluationException {
265272
Assert.notNull(context, "EvaluationContext is required");
266273

267-
if (this.compiledAst != null) {
274+
CompiledExpression compiledAst = this.compiledAst;
275+
if (compiledAst != null) {
268276
try {
269-
Object result = this.compiledAst.getValue(context.getRootObject().getValue(), context);
277+
Object result = compiledAst.getValue(context.getRootObject().getValue(), context);
270278
if (expectedResultType != null) {
271279
return ExpressionUtils.convertTypedValue(context, new TypedValue(result), expectedResultType);
272280
}
@@ -277,8 +285,8 @@ public <T> T getValue(EvaluationContext context, Class<T> expectedResultType) th
277285
catch (Throwable ex) {
278286
// If running in mixed mode, revert to interpreted
279287
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
280-
this.interpretedCount = 0;
281288
this.compiledAst = null;
289+
this.interpretedCount.set(0);
282290
}
283291
else {
284292
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
@@ -297,15 +305,16 @@ public <T> T getValue(EvaluationContext context, Class<T> expectedResultType) th
297305
public Object getValue(EvaluationContext context, Object rootObject) throws EvaluationException {
298306
Assert.notNull(context, "EvaluationContext is required");
299307

300-
if (this.compiledAst != null) {
308+
CompiledExpression compiledAst = this.compiledAst;
309+
if (compiledAst != null) {
301310
try {
302-
return this.compiledAst.getValue(rootObject, context);
311+
return compiledAst.getValue(rootObject, context);
303312
}
304313
catch (Throwable ex) {
305314
// If running in mixed mode, revert to interpreted
306315
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
307-
this.interpretedCount = 0;
308316
this.compiledAst = null;
317+
this.interpretedCount.set(0);
309318
}
310319
else {
311320
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
@@ -327,9 +336,10 @@ public <T> T getValue(EvaluationContext context, Object rootObject, Class<T> exp
327336

328337
Assert.notNull(context, "EvaluationContext is required");
329338

330-
if (this.compiledAst != null) {
339+
CompiledExpression compiledAst = this.compiledAst;
340+
if (compiledAst != null) {
331341
try {
332-
Object result = this.compiledAst.getValue(rootObject, context);
342+
Object result = compiledAst.getValue(rootObject, context);
333343
if (expectedResultType != null) {
334344
return ExpressionUtils.convertTypedValue(context, new TypedValue(result), expectedResultType);
335345
}
@@ -340,8 +350,8 @@ public <T> T getValue(EvaluationContext context, Object rootObject, Class<T> exp
340350
catch (Throwable ex) {
341351
// If running in mixed mode, revert to interpreted
342352
if (this.configuration.getCompilerMode() == SpelCompilerMode.MIXED) {
343-
this.interpretedCount = 0;
344353
this.compiledAst = null;
354+
this.interpretedCount.set(0);
345355
}
346356
else {
347357
// Running in SpelCompilerMode.immediate mode - propagate exception to caller
@@ -454,48 +464,58 @@ public void setValue(EvaluationContext context, Object rootObject, Object value)
454464
* @param expressionState the expression state used to determine compilation mode
455465
*/
456466
private void checkCompile(ExpressionState expressionState) {
457-
this.interpretedCount++;
467+
this.interpretedCount.incrementAndGet();
458468
SpelCompilerMode compilerMode = expressionState.getConfiguration().getCompilerMode();
459469
if (compilerMode != SpelCompilerMode.OFF) {
460470
if (compilerMode == SpelCompilerMode.IMMEDIATE) {
461-
if (this.interpretedCount > 1) {
471+
if (this.interpretedCount.get() > 1) {
462472
compileExpression();
463473
}
464474
}
465475
else {
466476
// compilerMode = SpelCompilerMode.MIXED
467-
if (this.interpretedCount > INTERPRETED_COUNT_THRESHOLD) {
477+
if (this.interpretedCount.get() > INTERPRETED_COUNT_THRESHOLD) {
468478
compileExpression();
469479
}
470480
}
471481
}
472482
}
473483

474-
475484
/**
476-
* Perform expression compilation. This will only succeed once exit descriptors for all nodes have
477-
* been determined. If the compilation fails and has failed more than 100 times the expression is
478-
* no longer considered suitable for compilation.
485+
* Perform expression compilation. This will only succeed once exit descriptors for
486+
* all nodes have been determined. If the compilation fails and has failed more than
487+
* 100 times the expression is no longer considered suitable for compilation.
488+
* @return whether this expression has been successfully compiled
479489
*/
480490
public boolean compileExpression() {
481-
if (this.failedAttempts > FAILED_ATTEMPTS_THRESHOLD) {
491+
CompiledExpression compiledAst = this.compiledAst;
492+
if (compiledAst != null) {
493+
// Previously compiled
494+
return true;
495+
}
496+
if (this.failedAttempts.get() > FAILED_ATTEMPTS_THRESHOLD) {
482497
// Don't try again
483498
return false;
484499
}
485-
if (this.compiledAst == null) {
486-
synchronized (this.expression) {
487-
// Possibly compiled by another thread before this thread got into the sync block
488-
if (this.compiledAst != null) {
489-
return true;
490-
}
491-
SpelCompiler compiler = SpelCompiler.getCompiler(this.configuration.getCompilerClassLoader());
492-
this.compiledAst = compiler.compile(this.ast);
493-
if (this.compiledAst == null) {
494-
this.failedAttempts++;
495-
}
500+
501+
synchronized (this) {
502+
if (this.compiledAst != null) {
503+
// Compiled by another thread before this thread got into the sync block
504+
return true;
505+
}
506+
SpelCompiler compiler = SpelCompiler.getCompiler(this.configuration.getCompilerClassLoader());
507+
compiledAst = compiler.compile(this.ast);
508+
if (compiledAst != null) {
509+
// Successfully compiled
510+
this.compiledAst = compiledAst;
511+
return true;
512+
}
513+
else {
514+
// Failed to compile
515+
this.failedAttempts.incrementAndGet();
516+
return false;
496517
}
497518
}
498-
return (this.compiledAst != null);
499519
}
500520

501521
/**
@@ -505,8 +525,8 @@ public boolean compileExpression() {
505525
*/
506526
public void revertToInterpreted() {
507527
this.compiledAst = null;
508-
this.interpretedCount = 0;
509-
this.failedAttempts = 0;
528+
this.interpretedCount.set(0);
529+
this.failedAttempts.set(0);
510530
}
511531

512532
/**

0 commit comments

Comments
 (0)