Skip to content

Commit b6a7632

Browse files
committed
spring-projectsGH-184: Expression Evaluation at Runtime
Resolves spring-projects#184
1 parent 7a86c9e commit b6a7632

17 files changed

+716
-132
lines changed

src/main/java/org/springframework/retry/annotation/AnnotationAwareRetryOperationsInterceptor.java

Lines changed: 131 additions & 67 deletions
Original file line numberDiff line numberDiff line change
@@ -40,9 +40,11 @@
4040
import org.springframework.core.annotation.AnnotatedElementUtils;
4141
import org.springframework.core.annotation.AnnotationAwareOrderComparator;
4242
import org.springframework.core.annotation.AnnotationUtils;
43+
import org.springframework.expression.Expression;
4344
import org.springframework.expression.common.TemplateParserContext;
4445
import org.springframework.expression.spel.standard.SpelExpressionParser;
4546
import org.springframework.expression.spel.support.StandardEvaluationContext;
47+
import org.springframework.retry.RetryContext;
4648
import org.springframework.retry.RetryListener;
4749
import org.springframework.retry.RetryPolicy;
4850
import org.springframework.retry.backoff.BackOffPolicy;
@@ -59,6 +61,8 @@
5961
import org.springframework.retry.policy.MapRetryContextCache;
6062
import org.springframework.retry.policy.RetryContextCache;
6163
import org.springframework.retry.policy.SimpleRetryPolicy;
64+
import org.springframework.retry.support.Args;
65+
import org.springframework.retry.support.RetrySynchronizationManager;
6266
import org.springframework.retry.support.RetryTemplate;
6367
import org.springframework.util.ConcurrentReferenceHashMap;
6468
import org.springframework.util.ReflectionUtils;
@@ -228,8 +232,8 @@ private MethodInterceptor getStatefulInterceptor(Object target, Method method, R
228232
if (circuit != null) {
229233
RetryPolicy policy = getRetryPolicy(circuit);
230234
CircuitBreakerRetryPolicy breaker = new CircuitBreakerRetryPolicy(policy);
231-
breaker.setOpenTimeout(getOpenTimeout(circuit));
232-
breaker.setResetTimeout(getResetTimeout(circuit));
235+
openTimeout(breaker, circuit);
236+
resetTimeout(breaker, circuit);
233237
template.setRetryPolicy(breaker);
234238
template.setBackOffPolicy(new NoBackOffPolicy());
235239
String label = circuit.label();
@@ -248,45 +252,39 @@ private MethodInterceptor getStatefulInterceptor(Object target, Method method, R
248252
.recoverer(getRecoverer(target, method)).build();
249253
}
250254

251-
private long getOpenTimeout(CircuitBreaker circuit) {
255+
private void openTimeout(CircuitBreakerRetryPolicy breaker, CircuitBreaker circuit) {
252256
if (StringUtils.hasText(circuit.openTimeoutExpression())) {
253-
Long value = null;
254-
if (isTemplate(circuit.openTimeoutExpression())) {
255-
value = PARSER.parseExpression(resolve(circuit.openTimeoutExpression()), PARSER_CONTEXT)
256-
.getValue(this.evaluationContext, Long.class);
257+
Expression parsed = parse(circuit.openTimeoutExpression());
258+
if (Evaluation.INITIALIZATION.equals(circuit.expressionEvaluation())) {
259+
Long value = parsed.getValue(this.evaluationContext, Long.class);
260+
if (value != null) {
261+
breaker.setOpenTimeout(value);
262+
return;
263+
}
257264
}
258265
else {
259-
value = PARSER.parseExpression(resolve(circuit.openTimeoutExpression()))
260-
.getValue(this.evaluationContext, Long.class);
261-
}
262-
if (value != null) {
263-
return value;
266+
breaker.setOpenTimeout(() -> evaluate(parsed, Long.class));
267+
return;
264268
}
265269
}
266-
return circuit.openTimeout();
270+
breaker.setOpenTimeout(circuit.openTimeout());
267271
}
268272

269-
private long getResetTimeout(CircuitBreaker circuit) {
273+
private void resetTimeout(CircuitBreakerRetryPolicy breaker, CircuitBreaker circuit) {
270274
if (StringUtils.hasText(circuit.resetTimeoutExpression())) {
271-
Long value = null;
272-
if (isTemplate(circuit.openTimeoutExpression())) {
273-
value = PARSER.parseExpression(resolve(circuit.resetTimeoutExpression()), PARSER_CONTEXT)
274-
.getValue(this.evaluationContext, Long.class);
275+
Expression parsed = parse(circuit.resetTimeoutExpression());
276+
if (Evaluation.INITIALIZATION.equals(circuit.expressionEvaluation())) {
277+
Long value = parsed.getValue(this.evaluationContext, Long.class);
278+
if (value != null) {
279+
breaker.setResetTimeout(value);
280+
return;
281+
}
275282
}
276283
else {
277-
value = PARSER.parseExpression(resolve(circuit.resetTimeoutExpression()))
278-
.getValue(this.evaluationContext, Long.class);
279-
}
280-
if (value != null) {
281-
return value;
284+
breaker.setResetTimeout(() -> evaluate(parsed, Long.class));
282285
}
283286
}
284-
return circuit.resetTimeout();
285-
}
286-
287-
private boolean isTemplate(String expression) {
288-
return expression.contains(PARSER_CONTEXT.getExpressionPrefix())
289-
&& expression.contains(PARSER_CONTEXT.getExpressionSuffix());
287+
breaker.setResetTimeout(circuit.resetTimeout());
290288
}
291289

292290
private RetryTemplate createTemplate(String[] listenersBeanNames) {
@@ -340,21 +338,24 @@ private RetryPolicy getRetryPolicy(Annotation retryable) {
340338
Class<? extends Throwable>[] excludes = (Class<? extends Throwable>[]) attrs.get("exclude");
341339
Integer maxAttempts = (Integer) attrs.get("maxAttempts");
342340
String maxAttemptsExpression = (String) attrs.get("maxAttemptsExpression");
341+
Expression parsedExpression = null;
343342
if (StringUtils.hasText(maxAttemptsExpression)) {
344-
if (ExpressionRetryPolicy.isTemplate(maxAttemptsExpression)) {
345-
maxAttempts = PARSER.parseExpression(resolve(maxAttemptsExpression), PARSER_CONTEXT)
346-
.getValue(this.evaluationContext, Integer.class);
347-
}
348-
else {
349-
maxAttempts = PARSER.parseExpression(resolve(maxAttemptsExpression)).getValue(this.evaluationContext,
350-
Integer.class);
343+
parsedExpression = parse(maxAttemptsExpression);
344+
if (Evaluation.INITIALIZATION.equals(attrs.get("expressionEvaluation"))) {
345+
maxAttempts = parsedExpression.getValue(this.evaluationContext, Integer.class);
351346
}
352347
}
348+
final Expression expression = parsedExpression;
353349
if (includes.length == 0 && excludes.length == 0) {
354350
SimpleRetryPolicy simple = hasExpression
355351
? new ExpressionRetryPolicy(resolve(exceptionExpression)).withBeanFactory(this.beanFactory)
356352
: new SimpleRetryPolicy();
357-
simple.setMaxAttempts(maxAttempts);
353+
if (Evaluation.RUNTIME.equals(attrs.get("expressionEvaluation"))) {
354+
simple.setMaxAttempts(() -> evaluate(expression, Integer.class));
355+
}
356+
else {
357+
simple.setMaxAttempts(maxAttempts);
358+
}
358359
return simple;
359360
}
360361
Map<Class<? extends Throwable>, Boolean> policyMap = new HashMap<>();
@@ -370,63 +371,126 @@ private RetryPolicy getRetryPolicy(Annotation retryable) {
370371
.withBeanFactory(this.beanFactory);
371372
}
372373
else {
373-
return new SimpleRetryPolicy(maxAttempts, policyMap, true, retryNotExcluded);
374+
SimpleRetryPolicy policy = new SimpleRetryPolicy(maxAttempts, policyMap, true, retryNotExcluded);
375+
if (Evaluation.RUNTIME.equals(attrs.get("expressionEvaluation"))) {
376+
policy.setMaxAttempts(() -> evaluate(expression, Integer.class));
377+
}
378+
return policy;
374379
}
375380
}
376381

377382
private BackOffPolicy getBackoffPolicy(Backoff backoff) {
378383
Map<String, Object> attrs = AnnotationUtils.getAnnotationAttributes(backoff);
379384
long min = backoff.delay() == 0 ? backoff.value() : backoff.delay();
385+
boolean evalInit = Evaluation.INITIALIZATION.equals(attrs.get("expressionEvaluation"));
380386
String delayExpression = (String) attrs.get("delayExpression");
387+
Expression parsedMinExp = null;
381388
if (StringUtils.hasText(delayExpression)) {
382-
if (ExpressionRetryPolicy.isTemplate(delayExpression)) {
383-
min = PARSER.parseExpression(resolve(delayExpression), PARSER_CONTEXT).getValue(this.evaluationContext,
384-
Long.class);
385-
}
386-
else {
387-
min = PARSER.parseExpression(resolve(delayExpression)).getValue(this.evaluationContext, Long.class);
389+
parsedMinExp = parse(delayExpression);
390+
if (evalInit) {
391+
min = parsedMinExp.getValue(this.evaluationContext, Long.class);
388392
}
389393
}
390394
long max = backoff.maxDelay();
391395
String maxDelayExpression = (String) attrs.get("maxDelayExpression");
396+
Expression parsedMaxExp = null;
392397
if (StringUtils.hasText(maxDelayExpression)) {
393-
if (ExpressionRetryPolicy.isTemplate(maxDelayExpression)) {
394-
max = PARSER.parseExpression(resolve(maxDelayExpression), PARSER_CONTEXT)
395-
.getValue(this.evaluationContext, Long.class);
396-
}
397-
else {
398-
max = PARSER.parseExpression(resolve(maxDelayExpression)).getValue(this.evaluationContext, Long.class);
398+
parsedMaxExp = parse(maxDelayExpression);
399+
if (evalInit) {
400+
max = parsedMaxExp.getValue(this.evaluationContext, Long.class);
399401
}
400402
}
401403
double multiplier = backoff.multiplier();
402404
String multiplierExpression = (String) attrs.get("multiplierExpression");
405+
Expression parsedMultExp = null;
403406
if (StringUtils.hasText(multiplierExpression)) {
404-
if (ExpressionRetryPolicy.isTemplate(multiplierExpression)) {
405-
multiplier = PARSER.parseExpression(resolve(multiplierExpression), PARSER_CONTEXT)
406-
.getValue(this.evaluationContext, Double.class);
407-
}
408-
else {
409-
multiplier = PARSER.parseExpression(resolve(multiplierExpression)).getValue(this.evaluationContext,
410-
Double.class);
407+
parsedMultExp = parse(multiplierExpression);
408+
if (evalInit) {
409+
multiplier = parsedMultExp.getValue(this.evaluationContext, Double.class);
411410
}
412411
}
413412
boolean isRandom = false;
413+
String randomExpression = (String) attrs.get("randomExpression");
414+
Expression parsedRandomExp = null;
414415
if (multiplier > 0) {
415416
isRandom = backoff.random();
416-
String randomExpression = (String) attrs.get("randomExpression");
417417
if (StringUtils.hasText(randomExpression)) {
418-
if (ExpressionRetryPolicy.isTemplate(randomExpression)) {
419-
isRandom = PARSER.parseExpression(resolve(randomExpression), PARSER_CONTEXT)
420-
.getValue(this.evaluationContext, Boolean.class);
421-
}
422-
else {
423-
isRandom = PARSER.parseExpression(resolve(randomExpression)).getValue(this.evaluationContext,
424-
Boolean.class);
418+
parsedRandomExp = parse(randomExpression);
419+
if (evalInit) {
420+
isRandom = parsedRandomExp.getValue(this.evaluationContext, Boolean.class);
425421
}
426422
}
427423
}
428-
return BackOffPolicyBuilder.newBuilder().delay(min).maxDelay(max).multiplier(multiplier).random(isRandom)
429-
.sleeper(this.sleeper).build();
424+
if (!evalInit) {
425+
return buildWithRuntimeExpressions(min, parsedMinExp, max, parsedMaxExp, multiplier, parsedMultExp,
426+
isRandom, parsedRandomExp);
427+
}
428+
else {
429+
return BackOffPolicyBuilder.newBuilder().delay(min).maxDelay(max).multiplier(multiplier).random(isRandom)
430+
.sleeper(this.sleeper).build();
431+
}
432+
}
433+
434+
private BackOffPolicy buildWithRuntimeExpressions(long min, Expression parsedMinExp, long max,
435+
Expression parsedMaxExp, double multiplier, Expression parsedMultExp, boolean isRandom,
436+
Expression parsedRandomExp) {
437+
438+
BackOffPolicyBuilder builder = BackOffPolicyBuilder.newBuilder();
439+
if (parsedMinExp != null) {
440+
Expression expression = parsedMinExp;
441+
builder.delaySupplier(() -> evaluate(expression, Long.class));
442+
}
443+
else {
444+
builder.delay(min);
445+
}
446+
if (parsedMaxExp != null) {
447+
Expression expression = parsedMaxExp;
448+
builder.maxDelaySupplier(() -> evaluate(expression, Long.class));
449+
}
450+
else {
451+
builder.maxDelay(max);
452+
}
453+
if (parsedMultExp != null) {
454+
Expression expression = parsedMultExp;
455+
builder.multiplierSupplier(() -> evaluate(expression, Double.class));
456+
}
457+
else {
458+
builder.multiplier(multiplier);
459+
}
460+
if (parsedRandomExp != null) {
461+
Expression expression = parsedRandomExp;
462+
builder.randomSupplier(() -> evaluate(expression, Boolean.class));
463+
}
464+
else {
465+
builder.random(isRandom);
466+
}
467+
return builder.build();
468+
}
469+
470+
private Expression parse(String expression) {
471+
if (isTemplate(expression)) {
472+
return PARSER.parseExpression(resolve(expression), PARSER_CONTEXT);
473+
}
474+
else {
475+
return PARSER.parseExpression(resolve(expression));
476+
}
477+
}
478+
479+
private boolean isTemplate(String expression) {
480+
return expression.contains(PARSER_CONTEXT.getExpressionPrefix())
481+
&& expression.contains(PARSER_CONTEXT.getExpressionSuffix());
482+
}
483+
484+
private <T> T evaluate(Expression expression, Class<T> type) {
485+
RetryContext context = RetrySynchronizationManager.getContext();
486+
Args args = null;
487+
if (context != null) {
488+
args = (Args) context.getAttribute("ARGS");
489+
}
490+
if (args == null) {
491+
args = Args.NO_ARGS;
492+
}
493+
return expression.getValue(this.evaluationContext, args, type);
430494
}
431495

432496
/**

src/main/java/org/springframework/retry/annotation/Backoff.java

Lines changed: 16 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/*
2-
* Copyright 2012-2013 the original author or authors.
2+
* Copyright 2012-2022 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.
@@ -22,6 +22,7 @@
2222
import java.lang.annotation.RetentionPolicy;
2323
import java.lang.annotation.Target;
2424

25+
import org.springframework.core.annotation.AliasFor;
2526
import org.springframework.retry.backoff.BackOffPolicy;
2627

2728
/**
@@ -53,16 +54,18 @@
5354
* element is ignored, otherwise value of this element is taken.
5455
* @return the delay in milliseconds (default 1000)
5556
*/
57+
@AliasFor("delay")
5658
long value() default 1000;
5759

5860
/**
5961
* A canonical backoff period. Used as an initial value in the exponential case, and
6062
* as a minimum value in the uniform case. When the value of this element is 0, value
6163
* of element {@link #value()} is taken, otherwise value of this element is taken and
6264
* {@link #value()} is ignored.
63-
* @return the initial or canonical backoff period in milliseconds (default 0)
65+
* @return the initial or canonical backoff period in milliseconds (default 1000)
6466
*/
65-
long delay() default 0;
67+
@AliasFor("value")
68+
long delay() default 1000;
6669

6770
/**
6871
* The maximum wait (in milliseconds) between retries. If less than the
@@ -125,4 +128,14 @@
125128
*/
126129
String randomExpression() default "";
127130

131+
/**
132+
* Determine when expressions in this annotation should be evaluated. Default
133+
* {@link Evaluation#INITIALIZATION}. A side effect of setting this to
134+
* {@link Evaluation#RUNTIME} is the backoff context will not be serializable, so
135+
* can't be used in a distributed cache that requires serialization.
136+
* @return the evaluation point.
137+
* @since 2.0
138+
*/
139+
Evaluation expressionEvaluation() default Evaluation.INITIALIZATION;
140+
128141
}

src/main/java/org/springframework/retry/annotation/CircuitBreaker.java

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -133,4 +133,14 @@
133133
*/
134134
String exceptionExpression() default "";
135135

136+
/**
137+
* Determine when expressions in this annotation should be evaluated. Default
138+
* {@link Evaluation#INITIALIZATION}. A side effect of setting this to
139+
* {@link Evaluation#RUNTIME} is the retry policy will not be serializable, so can't
140+
* be used in a distributed cache that requires serialization.
141+
* @return the evaluation point.
142+
* @since 2.0
143+
*/
144+
Evaluation expressionEvaluation() default Evaluation.INITIALIZATION;
145+
136146
}
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
/*
2+
* Copyright 2022 the original author or authors.
3+
*
4+
* Licensed under the Apache License, Version 2.0 (the "License");
5+
* you may not use this file except in compliance with the License.
6+
* You may obtain a copy of the License at
7+
*
8+
* https://www.apache.org/licenses/LICENSE-2.0
9+
*
10+
* Unless required by applicable law or agreed to in writing, software
11+
* distributed under the License is distributed on an "AS IS" BASIS,
12+
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
* See the License for the specific language governing permissions and
14+
* limitations under the License.
15+
*/
16+
17+
package org.springframework.retry.annotation;
18+
19+
/**
20+
* Determine when expressions are evaluated.
21+
*
22+
* @author Gary Russell
23+
*
24+
* @since 2.0
25+
*/
26+
public enum Evaluation {
27+
28+
/**
29+
* Evaluate expressions at runtime.
30+
*/
31+
RUNTIME,
32+
33+
/**
34+
* Evaluate expressions once, during initialization.
35+
*/
36+
INITIALIZATION
37+
38+
}

0 commit comments

Comments
 (0)