Skip to content

Commit bcfae82

Browse files
committed
Merge branch 'main' into 34651
2 parents 5d51631 + 8f41079 commit bcfae82

File tree

8 files changed

+115
-52
lines changed

8 files changed

+115
-52
lines changed

framework-docs/modules/ROOT/pages/integration/rest-clients.adoc

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1201,12 +1201,12 @@ annotate HTTP interfaces as follows:
12011201
[source,java,indent=0,subs="verbatim,quotes"]
12021202
----
12031203
@HttpServiceClient("echo")
1204-
public class EchoServiceA {
1204+
public interface EchoServiceA {
12051205
// ...
12061206
}
12071207
12081208
@HttpServiceClient("echo")
1209-
public class EchoServiceB {
1209+
public interface EchoServiceB {
12101210
// ...
12111211
}
12121212
----

spring-context/src/main/java/org/springframework/resilience/retry/AbstractRetryInterceptor.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
import reactor.core.publisher.Mono;
2727
import reactor.util.retry.Retry;
2828

29+
import org.springframework.aop.ProxyMethodInvocation;
2930
import org.springframework.core.ReactiveAdapter;
3031
import org.springframework.core.ReactiveAdapterRegistry;
3132
import org.springframework.core.retry.RetryException;
@@ -103,7 +104,8 @@ public AbstractRetryInterceptor() {
103104
return retryTemplate.execute(new Retryable<>() {
104105
@Override
105106
public @Nullable Object execute() throws Throwable {
106-
return invocation.proceed();
107+
return (invocation instanceof ProxyMethodInvocation pmi ?
108+
pmi.invocableClone().proceed() : invocation.proceed());
107109
}
108110
@Override
109111
public String getName() {

spring-context/src/test/java/org/springframework/resilience/RetryInterceptorTests.java

Lines changed: 34 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -17,18 +17,21 @@
1717
package org.springframework.resilience;
1818

1919
import java.io.IOException;
20+
import java.lang.reflect.InvocationTargetException;
2021
import java.lang.reflect.Method;
2122
import java.nio.file.AccessDeniedException;
2223
import java.time.Duration;
2324
import java.util.Properties;
2425
import java.util.concurrent.atomic.AtomicInteger;
2526

27+
import org.aopalliance.intercept.MethodInterceptor;
2628
import org.junit.jupiter.api.Test;
2729

2830
import org.springframework.aop.framework.AopProxyUtils;
2931
import org.springframework.aop.framework.ProxyConfig;
3032
import org.springframework.aop.framework.ProxyFactory;
3133
import org.springframework.aop.framework.autoproxy.AutoProxyUtils;
34+
import org.springframework.aop.interceptor.SimpleTraceInterceptor;
3235
import org.springframework.aop.support.AopUtils;
3336
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
3437
import org.springframework.beans.factory.support.RootBeanDefinition;
@@ -59,7 +62,30 @@ void withSimpleInterceptor() {
5962
pf.setTarget(target);
6063
pf.addAdvice(new SimpleRetryInterceptor(
6164
new MethodRetrySpec((m, t) -> true, 5, Duration.ofMillis(10))));
62-
NonAnnotatedBean proxy = (NonAnnotatedBean) pf.getProxy();
65+
pf.addAdvice(new SimpleTraceInterceptor());
66+
PlainInterface proxy = (PlainInterface) pf.getProxy();
67+
68+
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("6");
69+
assertThat(target.counter).isEqualTo(6);
70+
}
71+
72+
@Test
73+
void withSimpleInterceptorAndNoTarget() {
74+
NonAnnotatedBean target = new NonAnnotatedBean();
75+
ProxyFactory pf = new ProxyFactory();
76+
pf.addAdvice(new SimpleRetryInterceptor(
77+
new MethodRetrySpec((m, t) -> true, 5, Duration.ofMillis(10))));
78+
pf.addAdvice(new SimpleTraceInterceptor());
79+
pf.addAdvice((MethodInterceptor) invocation -> {
80+
try {
81+
return invocation.getMethod().invoke(target, invocation.getArguments());
82+
}
83+
catch (InvocationTargetException ex) {
84+
throw ex.getTargetException();
85+
}
86+
});
87+
pf.addInterface(PlainInterface.class);
88+
PlainInterface proxy = (PlainInterface) pf.getProxy();
6389

6490
assertThatIOException().isThrownBy(proxy::retryOperation).withMessage("6");
6591
assertThat(target.counter).isEqualTo(6);
@@ -237,7 +263,7 @@ void withEnableAnnotation() throws Exception {
237263
}
238264

239265

240-
static class NonAnnotatedBean {
266+
static class NonAnnotatedBean implements PlainInterface {
241267

242268
int counter = 0;
243269

@@ -248,6 +274,12 @@ public void retryOperation() throws IOException {
248274
}
249275

250276

277+
public interface PlainInterface {
278+
279+
void retryOperation() throws IOException;
280+
}
281+
282+
251283
static class AnnotatedMethodBean {
252284

253285
int counter = 0;

spring-core/src/main/java/org/springframework/core/retry/RetryListener.java

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
*
3030
* @author Mahmoud Ben Hassine
3131
* @author Sam Brannen
32+
* @author Juergen Hoeller
3233
* @since 7.0
3334
* @see CompositeRetryListener
3435
*/
@@ -64,9 +65,13 @@ default void onRetryFailure(RetryPolicy retryPolicy, Retryable<?> retryable, Thr
6465
* Called if the {@link RetryPolicy} is exhausted.
6566
* @param retryPolicy the {@code RetryPolicy}
6667
* @param retryable the {@code Retryable} operation
67-
* @param throwable the last exception thrown by the {@link Retryable} operation
68+
* @param exception the resulting {@link RetryException}, including the last operation
69+
* exception as a cause and all earlier operation exceptions as suppressed exceptions
70+
* @see RetryException#getCause()
71+
* @see RetryException#getSuppressed()
72+
* @see RetryException#getRetryCount()
6873
*/
69-
default void onRetryPolicyExhaustion(RetryPolicy retryPolicy, Retryable<?> retryable, Throwable throwable) {
74+
default void onRetryPolicyExhaustion(RetryPolicy retryPolicy, Retryable<?> retryable, RetryException exception) {
7075
}
7176

7277
}

spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java

Lines changed: 17 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,14 @@ public void setRetryPolicy(RetryPolicy retryPolicy) {
9090
this.retryPolicy = retryPolicy;
9191
}
9292

93+
/**
94+
* Return the current {@link RetryPolicy} that is in use
95+
* with this template.
96+
*/
97+
public RetryPolicy getRetryPolicy() {
98+
return this.retryPolicy;
99+
}
100+
93101
/**
94102
* Set the {@link RetryListener} to use.
95103
* <p>If multiple listeners are needed, use a
@@ -102,6 +110,14 @@ public void setRetryListener(RetryListener retryListener) {
102110
this.retryListener = retryListener;
103111
}
104112

113+
/**
114+
* Return the current {@link RetryListener} that is in use
115+
* with this template.
116+
*/
117+
public RetryListener getRetryListener() {
118+
return this.retryListener;
119+
}
120+
105121

106122
/**
107123
* Execute the supplied {@link Retryable} operation according to the configured
@@ -176,7 +192,7 @@ public void setRetryListener(RetryListener retryListener) {
176192
"Retry policy for operation '%s' exhausted; aborting execution".formatted(retryableName),
177193
exceptions.removeLast());
178194
exceptions.forEach(retryException::addSuppressed);
179-
this.retryListener.onRetryPolicyExhaustion(this.retryPolicy, retryable, lastException);
195+
this.retryListener.onRetryPolicyExhaustion(this.retryPolicy, retryable, retryException);
180196
throw retryException;
181197
}
182198
}

spring-core/src/main/java/org/springframework/core/retry/support/CompositeRetryListener.java

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121

2222
import org.jspecify.annotations.Nullable;
2323

24+
import org.springframework.core.retry.RetryException;
2425
import org.springframework.core.retry.RetryListener;
2526
import org.springframework.core.retry.RetryPolicy;
2627
import org.springframework.core.retry.RetryTemplate;
@@ -34,6 +35,7 @@
3435
* <p>This class is used to compose multiple listeners within a {@link RetryTemplate}.
3536
*
3637
* @author Mahmoud Ben Hassine
38+
* @author Juergen Hoeller
3739
* @since 7.0
3840
*/
3941
public class CompositeRetryListener implements RetryListener {
@@ -82,8 +84,8 @@ public void onRetryFailure(RetryPolicy retryPolicy, Retryable<?> retryable, Thro
8284
}
8385

8486
@Override
85-
public void onRetryPolicyExhaustion(RetryPolicy retryPolicy, Retryable<?> retryable, Throwable throwable) {
86-
this.listeners.forEach(listener -> listener.onRetryPolicyExhaustion(retryPolicy, retryable, throwable));
87+
public void onRetryPolicyExhaustion(RetryPolicy retryPolicy, Retryable<?> retryable, RetryException exception) {
88+
this.listeners.forEach(listener -> listener.onRetryPolicyExhaustion(retryPolicy, retryable, exception));
8789
}
8890

8991
}

spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java

Lines changed: 46 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,12 @@ void configureRetryTemplate() {
6868
retryTemplate.setRetryListener(retryListener);
6969
}
7070

71+
@Test
72+
void checkRetryTemplateConfiguration() {
73+
assertThat(retryTemplate.getRetryPolicy()).isSameAs(retryPolicy);
74+
assertThat(retryTemplate.getRetryListener()).isSameAs(retryListener);
75+
}
76+
7177
@Test
7278
void retryWithImmediateSuccess() throws Exception {
7379
AtomicInteger invocationCount = new AtomicInteger();
@@ -99,10 +105,9 @@ void retryWithInitialFailureAndZeroRetriesRetryPolicy() {
99105
.withMessageMatching("Retry policy for operation '.+?' exhausted; aborting execution")
100106
.withCause(exception)
101107
.satisfies(throwable -> assertThat(throwable.getSuppressed()).isEmpty())
102-
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero());
108+
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero())
109+
.satisfies(throwable -> inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable));
103110

104-
// RetryListener interactions:
105-
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, exception);
106111
verifyNoMoreInteractions(retryListener);
107112
}
108113

@@ -122,10 +127,9 @@ void retryWithInitialFailureAndZeroRetriesFixedBackOffPolicy() {
122127
.withMessageMatching("Retry policy for operation '.+?' exhausted; aborting execution")
123128
.withCause(exception)
124129
.satisfies(throwable -> assertThat(throwable.getSuppressed()).isEmpty())
125-
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero());
130+
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero())
131+
.satisfies(throwable -> inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable));
126132

127-
// RetryListener interactions:
128-
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, exception);
129133
verifyNoMoreInteractions(retryListener);
130134
}
131135

@@ -145,10 +149,9 @@ void retryWithInitialFailureAndZeroRetriesBackOffPolicyFromBuilder() {
145149
.withMessageMatching("Retry policy for operation '.+?' exhausted; aborting execution")
146150
.withCause(exception)
147151
.satisfies(throwable -> assertThat(throwable.getSuppressed()).isEmpty())
148-
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero());
152+
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isZero())
153+
.satisfies(throwable -> inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable));
149154

150-
// RetryListener interactions:
151-
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, exception);
152155
verifyNoMoreInteractions(retryListener);
153156
}
154157

@@ -194,18 +197,19 @@ public String getName() {
194197
assertThatExceptionOfType(RetryException.class)
195198
.isThrownBy(() -> retryTemplate.execute(retryable))
196199
.withMessage("Retry policy for operation 'test' exhausted; aborting execution")
197-
.withCause(new CustomException("Boom 4"));
200+
.withCause(new CustomException("Boom 4"))
201+
.satisfies(throwable -> {
202+
invocationCount.set(1);
203+
repeat(3, () -> {
204+
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
205+
inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable,
206+
new CustomException("Boom " + invocationCount.incrementAndGet()));
207+
});
208+
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable);
209+
});
198210
// 4 = 1 initial invocation + 3 retry attempts
199211
assertThat(invocationCount).hasValue(4);
200212

201-
// RetryListener interactions:
202-
invocationCount.set(1);
203-
repeat(3, () -> {
204-
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
205-
inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable,
206-
new CustomException("Boom " + invocationCount.incrementAndGet()));
207-
});
208-
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, new CustomException("Boom 4"));
209213
verifyNoMoreInteractions(retryListener);
210214
}
211215

@@ -240,16 +244,17 @@ public String getName() {
240244
assertThatExceptionOfType(RetryException.class)
241245
.isThrownBy(() -> retryTemplate.execute(retryable))
242246
.withMessage("Retry policy for operation 'always fails' exhausted; aborting execution")
243-
.withCause(exception);
247+
.withCause(exception)
248+
.satisfies(throwable -> {
249+
repeat(5, () -> {
250+
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
251+
inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable, exception);
252+
});
253+
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable);
254+
});
244255
// 6 = 1 initial invocation + 5 retry attempts
245256
assertThat(invocationCount).hasValue(6);
246257

247-
// RetryListener interactions:
248-
repeat(5, () -> {
249-
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
250-
inOrder.verify(retryListener).onRetryFailure(retryPolicy, retryable, exception);
251-
});
252-
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, exception);
253258
verifyNoMoreInteractions(retryListener);
254259
}
255260

@@ -291,17 +296,17 @@ public String getName() {
291296
suppressed1 -> assertThat(suppressed1).isExactlyInstanceOf(FileNotFoundException.class),
292297
suppressed2 -> assertThat(suppressed2).isExactlyInstanceOf(IOException.class)
293298
))
294-
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isEqualTo(2));
299+
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isEqualTo(2))
300+
.satisfies(throwable -> {
301+
repeat(2, () -> {
302+
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
303+
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(Exception.class));
304+
});
305+
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable);
306+
});
295307
// 3 = 1 initial invocation + 2 retry attempts
296308
assertThat(invocationCount).hasValue(3);
297309

298-
// RetryListener interactions:
299-
repeat(2, () -> {
300-
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
301-
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(Exception.class));
302-
});
303-
inOrder.verify(retryListener).onRetryPolicyExhaustion(
304-
eq(retryPolicy), eq(retryable), any(IllegalStateException.class));
305310
verifyNoMoreInteractions(retryListener);
306311
}
307312

@@ -354,17 +359,17 @@ public String getName() {
354359
suppressed1 -> assertThat(suppressed1).isExactlyInstanceOf(IOException.class),
355360
suppressed2 -> assertThat(suppressed2).isExactlyInstanceOf(IOException.class)
356361
))
357-
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isEqualTo(2));
362+
.satisfies(throwable -> assertThat(throwable.getRetryCount()).isEqualTo(2))
363+
.satisfies(throwable -> {
364+
repeat(2, () -> {
365+
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
366+
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(IOException.class));
367+
});
368+
inOrder.verify(retryListener).onRetryPolicyExhaustion(retryPolicy, retryable, throwable);
369+
});
358370
// 3 = 1 initial invocation + 2 retry attempts
359371
assertThat(invocationCount).hasValue(3);
360372

361-
// RetryListener interactions:
362-
repeat(2, () -> {
363-
inOrder.verify(retryListener).beforeRetry(retryPolicy, retryable);
364-
inOrder.verify(retryListener).onRetryFailure(eq(retryPolicy), eq(retryable), any(IOException.class));
365-
});
366-
inOrder.verify(retryListener).onRetryPolicyExhaustion(
367-
eq(retryPolicy), eq(retryable), any(CustomFileNotFoundException.class));
368373
verifyNoMoreInteractions(retryListener);
369374
}
370375

spring-core/src/test/java/org/springframework/core/retry/support/CompositeRetryListenerTests.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@
2121
import org.junit.jupiter.api.BeforeEach;
2222
import org.junit.jupiter.api.Test;
2323

24+
import org.springframework.core.retry.RetryException;
2425
import org.springframework.core.retry.RetryListener;
2526
import org.springframework.core.retry.RetryPolicy;
2627
import org.springframework.core.retry.Retryable;
@@ -83,7 +84,7 @@ void onRetryFailure() {
8384

8485
@Test
8586
void onRetryPolicyExhaustion() {
86-
Exception exception = new Exception();
87+
RetryException exception = new RetryException("", new Exception());
8788
compositeRetryListener.onRetryPolicyExhaustion(retryPolicy, retryable, exception);
8889

8990
verify(listener1).onRetryPolicyExhaustion(retryPolicy, retryable, exception);

0 commit comments

Comments
 (0)