Browse Source

Introduce Builder API and factory methods for RetryPolicy

Prior to this commit, we had three concrete RetryPolicy implementations.

- MaxRetryAttemptsPolicy
- MaxDurationAttemptsPolicy
- PredicateRetryPolicy

However, there was no way to combine the behavior of those policies.
Furthermore, the PredicateRetryPolicy was practically useless as a
standalone policy, since it did not have a way to end an infinite loop
for a Retryable that continually throws an exception which matches the
predicate.

This commit therefore replaces the current built-in RetryPolicy
implementations with a fluent Builder API and dedicated factory methods
for common use cases.

In addition, this commit also introduces built-in support for
specifying include/exclude lists.

Examples:

new MaxRetryAttemptsPolicy(5) -->

    RetryPolicy.withMaxAttempts(5)

new MaxDurationAttemptsPolicy(Duration.ofSeconds(5)) -->

    RetryPolicy.withMaxDuration(Duration.ofSeconds(5))

new PredicateRetryPolicy(IOException.class::isInstance) -->

    RetryPolicy.builder()
        .maxAttempts(3)
        .predicate(IOException.class::isInstance)
        .build();

The following example demonstrates all supported features of the builder.

RetryPolicy.builder()
    .maxAttempts(5)
    .maxDuration(Duration.ofMillis(100))
    .includes(IOException.class)
    .excludes(FileNotFoundException.class)
    .predicate(t -> t.getMessage().contains("Unexpected failure"))
    .build();

Closes gh-35058
pull/35061/head
Sam Brannen 8 months ago
parent
commit
97522cfa36
  1. 167
      spring-core/src/main/java/org/springframework/core/retry/DefaultRetryPolicy.java
  2. 146
      spring-core/src/main/java/org/springframework/core/retry/RetryPolicy.java
  3. 10
      spring-core/src/main/java/org/springframework/core/retry/RetryTemplate.java
  4. 100
      spring-core/src/main/java/org/springframework/core/retry/support/MaxRetryAttemptsPolicy.java
  5. 103
      spring-core/src/main/java/org/springframework/core/retry/support/MaxRetryDurationPolicy.java
  6. 75
      spring-core/src/main/java/org/springframework/core/retry/support/PredicateRetryPolicy.java
  7. 188
      spring-core/src/test/java/org/springframework/core/retry/MaxAttemptsDefaultRetryPolicyTests.java
  8. 65
      spring-core/src/test/java/org/springframework/core/retry/MaxDurationDefaultRetryPolicyTests.java
  9. 34
      spring-core/src/test/java/org/springframework/core/retry/NumberFormatExceptionMatcher.java
  10. 120
      spring-core/src/test/java/org/springframework/core/retry/RetryTemplateTests.java
  11. 114
      spring-core/src/test/java/org/springframework/core/retry/support/MaxRetryAttemptsPolicyTests.java
  12. 58
      spring-core/src/test/java/org/springframework/core/retry/support/MaxRetryDurationPolicyTests.java
  13. 70
      spring-core/src/test/java/org/springframework/core/retry/support/PredicateRetryPolicyTests.java

167
spring-core/src/main/java/org/springframework/core/retry/DefaultRetryPolicy.java

@ -0,0 +1,167 @@ @@ -0,0 +1,167 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.retry;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Set;
import java.util.StringJoiner;
import java.util.function.Predicate;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
/**
* Default {@link RetryPolicy} created by {@link RetryPolicy.Builder}.
*
* @author Sam Brannen
* @author Mahmoud Ben Hassine
* @since 7.0
*/
class DefaultRetryPolicy implements RetryPolicy {
private final int maxAttempts;
private final @Nullable Duration maxDuration;
private final Set<Class<? extends Throwable>> includes;
private final Set<Class<? extends Throwable>> excludes;
private final @Nullable Predicate<Throwable> predicate;
DefaultRetryPolicy(int maxAttempts, @Nullable Duration maxDuration, Set<Class<? extends Throwable>> includes,
Set<Class<? extends Throwable>> excludes, @Nullable Predicate<Throwable> predicate) {
Assert.isTrue((maxAttempts > 0 || maxDuration != null), "Max attempts or max duration must be specified");
this.maxAttempts = maxAttempts;
this.maxDuration = maxDuration;
this.includes = includes;
this.excludes = excludes;
this.predicate = predicate;
}
@Override
public RetryExecution start() {
return new DefaultRetryPolicyExecution();
}
@Override
public String toString() {
StringJoiner result = new StringJoiner(", ", "DefaultRetryPolicy[", "]");
if (this.maxAttempts > 0) {
result.add("maxAttempts=" + this.maxAttempts);
}
if (this.maxDuration != null) {
result.add("maxDuration=" + this.maxDuration.toMillis() + "ms");
}
if (!this.includes.isEmpty()) {
result.add("includes=" + names(this.includes));
}
if (!this.excludes.isEmpty()) {
result.add("excludes=" + names(this.excludes));
}
if (this.predicate != null) {
result.add("predicate=" + this.predicate.getClass().getSimpleName());
}
return result.toString();
}
private static String names(Set<Class<? extends Throwable>> types) {
StringJoiner result = new StringJoiner(", ", "[", "]");
for (Class<? extends Throwable> type : types) {
String name = type.getCanonicalName();
result.add(name != null? name : type.getName());
}
return result.toString();
}
/**
* {@link RetryExecution} for {@link DefaultRetryPolicy}.
*/
private class DefaultRetryPolicyExecution implements RetryExecution {
private final LocalDateTime retryStartTime = LocalDateTime.now();
private int retryCount;
@Override
public boolean shouldRetry(Throwable throwable) {
if (DefaultRetryPolicy.this.maxAttempts > 0 &&
this.retryCount++ >= DefaultRetryPolicy.this.maxAttempts) {
return false;
}
if (DefaultRetryPolicy.this.maxDuration != null) {
Duration retryDuration = Duration.between(this.retryStartTime, LocalDateTime.now());
if (retryDuration.compareTo(DefaultRetryPolicy.this.maxDuration) > 0) {
return false;
}
}
if (!DefaultRetryPolicy.this.excludes.isEmpty()) {
for (Class<? extends Throwable> excludedType : DefaultRetryPolicy.this.excludes) {
if (excludedType.isInstance(throwable)) {
return false;
}
}
}
if (!DefaultRetryPolicy.this.includes.isEmpty()) {
boolean included = false;
for (Class<? extends Throwable> includedType : DefaultRetryPolicy.this.includes) {
if (includedType.isInstance(throwable)) {
included = true;
break;
}
}
if (!included) {
return false;
}
}
return DefaultRetryPolicy.this.predicate == null || DefaultRetryPolicy.this.predicate.test(throwable);
}
@Override
public String toString() {
StringJoiner result = new StringJoiner(", ", "DefaultRetryPolicyExecution[", "]");
if (DefaultRetryPolicy.this.maxAttempts > 0) {
result.add("maxAttempts=" + DefaultRetryPolicy.this.maxAttempts);
result.add("retryCount=" + this.retryCount);
}
if (DefaultRetryPolicy.this.maxDuration != null) {
result.add("maxDuration=" + DefaultRetryPolicy.this.maxDuration.toMillis() + "ms");
result.add("retryStartTime=" + this.retryStartTime);
}
if (!DefaultRetryPolicy.this.includes.isEmpty()) {
result.add("includes=" + names(DefaultRetryPolicy.this.includes));
}
if (!DefaultRetryPolicy.this.excludes.isEmpty()) {
result.add("excludes=" + names(DefaultRetryPolicy.this.excludes));
}
if (DefaultRetryPolicy.this.predicate != null) {
result.add("predicate=" + DefaultRetryPolicy.this.predicate.getClass().getSimpleName());
}
return result.toString();
}
}
}

146
spring-core/src/main/java/org/springframework/core/retry/RetryPolicy.java

@ -16,11 +16,28 @@ @@ -16,11 +16,28 @@
package org.springframework.core.retry;
import java.time.Duration;
import java.util.Collections;
import java.util.LinkedHashSet;
import java.util.Set;
import java.util.function.Predicate;
import org.jspecify.annotations.Nullable;
import org.springframework.util.Assert;
/**
* Strategy interface to define a retry policy.
*
* <p>Also provides factory methods and a fluent builder API for creating retry
* policies with common configurations. See {@link #withMaxAttempts(int)},
* {@link #withMaxDuration(Duration)}, {@link #builder()}, and the configuration
* options in {@link Builder} for details.
*
* @author Sam Brannen
* @author Mahmoud Ben Hassine
* @since 7.0
* @see RetryExecution
*/
public interface RetryPolicy {
@ -30,4 +47,133 @@ public interface RetryPolicy { @@ -30,4 +47,133 @@ public interface RetryPolicy {
*/
RetryExecution start();
/**
* Create a {@link RetryPolicy} configured with a maximum number of retry attempts.
* @param maxAttempts the maximum number of retry attempts; must be greater than zero
* @see Builder#maxAttempts(int)
*/
static RetryPolicy withMaxAttempts(int maxAttempts) {
return builder().maxAttempts(maxAttempts).build();
}
/**
* Create a {@link RetryPolicy} configured with a maximum retry {@link Duration}.
* @param maxDuration the maximum retry duration; must be positive
* @see Builder#maxDuration(Duration)
*/
static RetryPolicy withMaxDuration(Duration maxDuration) {
return builder().maxDuration(maxDuration).build();
}
/**
* Create a {@link Builder} to configure a {@link RetryPolicy} with common
* configuration options.
*/
static Builder builder() {
return new Builder();
}
/**
* Fluent API for configuring a {@link RetryPolicy} with common configuration
* options.
*/
final class Builder {
private int maxAttempts;
private @Nullable Duration maxDuration;
private final Set<Class<? extends Throwable>> includes = new LinkedHashSet<>();
private final Set<Class<? extends Throwable>> excludes = new LinkedHashSet<>();
private @Nullable Predicate<Throwable> predicate;
private Builder() {
// internal constructor
}
/**
* Specify the maximum number of retry attempts.
* @param maxAttempts the maximum number of retry attempts; must be
* greater than zero
* @return this {@code Builder} instance for chained method invocations
*/
public Builder maxAttempts(int maxAttempts) {
Assert.isTrue(maxAttempts > 0, "Max attempts must be greater than zero");
this.maxAttempts = maxAttempts;
return this;
}
/**
* Specify the maximum retry {@link Duration}.
* @param maxDuration the maximum retry duration; must be positive
* @return this {@code Builder} instance for chained method invocations
*/
public Builder maxDuration(Duration maxDuration) {
Assert.isTrue(!maxDuration.isNegative() && !maxDuration.isZero(), "Max duration must be positive");
this.maxDuration = maxDuration;
return this;
}
/**
* Specify the types of exceptions for which the {@link RetryPolicy}
* should retry a failed operation.
* <p>This can be combined with {@link #excludes(Class...)} and
* {@link #predicate(Predicate)}.
* @param types the types of exceptions to include in the policy
* @return this {@code Builder} instance for chained method invocations
*/
@SafeVarargs // Making the method final allows us to use @SafeVarargs.
@SuppressWarnings("varargs")
public final Builder includes(Class<? extends Throwable>... types) {
Collections.addAll(this.includes, types);
return this;
}
/**
* Specify the types of exceptions for which the {@link RetryPolicy}
* should not retry a failed operation.
* <p>This can be combined with {@link #includes(Class...)} and
* {@link #predicate(Predicate)}.
* @param types the types of exceptions to exclude from the policy
* @return this {@code Builder} instance for chained method invocations
*/
@SafeVarargs // Making the method final allows us to use @SafeVarargs.
@SuppressWarnings("varargs")
public final Builder excludes(Class<? extends Throwable>... types) {
Collections.addAll(this.excludes, types);
return this;
}
/**
* Specify a custom {@link Predicate} that the {@link RetryPolicy} will
* use to determine whether to retry a failed operation based on a given
* {@link Throwable}.
* <p>If a predicate has already been configured, the supplied predicate
* will be {@linkplain Predicate#and(Predicate) combined} with the
* existing predicate.
* <p>This can be combined with {@link #includes(Class...)} and
* {@link #excludes(Class...)}.
* @param predicate a custom predicate
* @return this {@code Builder} instance for chained method invocations
*/
public Builder predicate(Predicate<Throwable> predicate) {
this.predicate = (this.predicate != null ? this.predicate.and(predicate) : predicate);
return this;
}
/**
* Build the {@link RetryPolicy} configured via this {@code Builder}.
*/
public RetryPolicy build() {
return new DefaultRetryPolicy(this.maxAttempts, this.maxDuration,
this.includes, this.excludes, this.predicate);
}
}
}

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

@ -24,7 +24,6 @@ import java.util.Iterator; @@ -24,7 +24,6 @@ import java.util.Iterator;
import org.jspecify.annotations.Nullable;
import org.springframework.core.log.LogAccessor;
import org.springframework.core.retry.support.MaxRetryAttemptsPolicy;
import org.springframework.util.Assert;
import org.springframework.util.backoff.BackOff;
import org.springframework.util.backoff.BackOffExecution;
@ -59,7 +58,8 @@ public class RetryTemplate implements RetryOperations { @@ -59,7 +58,8 @@ public class RetryTemplate implements RetryOperations {
private static final LogAccessor logger = new LogAccessor(RetryTemplate.class);
private RetryPolicy retryPolicy = new MaxRetryAttemptsPolicy();
private RetryPolicy retryPolicy = RetryPolicy.withMaxAttempts(3);
private BackOff backOffPolicy = new FixedBackOff(Duration.ofSeconds(1));
@ -98,9 +98,11 @@ public class RetryTemplate implements RetryOperations { @@ -98,9 +98,11 @@ public class RetryTemplate implements RetryOperations {
/**
* Set the {@link RetryPolicy} to use.
* <p>Defaults to {@code new MaxRetryAttemptsPolicy()}.
* <p>Defaults to {@code RetryPolicy.withMaxAttempts(3)}.
* @param retryPolicy the retry policy to use
* @see MaxRetryAttemptsPolicy
* @see RetryPolicy#withMaxAttempts(int)
* @see RetryPolicy#withMaxDuration(Duration)
* @see RetryPolicy#builder()
*/
public void setRetryPolicy(RetryPolicy retryPolicy) {
Assert.notNull(retryPolicy, "Retry policy must not be null");

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

@ -1,100 +0,0 @@ @@ -1,100 +0,0 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.retry.support;
import org.springframework.core.retry.RetryExecution;
import org.springframework.core.retry.RetryPolicy;
import org.springframework.util.Assert;
/**
* A {@link RetryPolicy} based on a maximum number of retry attempts.
*
* @author Mahmoud Ben Hassine
* @author Sam Brannen
* @since 7.0
*/
public final class MaxRetryAttemptsPolicy implements RetryPolicy {
/**
* The default maximum number of retry attempts: {@value}.
*/
public static final int DEFAULT_MAX_RETRY_ATTEMPTS = 3;
private int maxRetryAttempts = DEFAULT_MAX_RETRY_ATTEMPTS;
/**
* Create a new {@code MaxRetryAttemptsPolicy} with the default maximum number
* of retry attempts.
* @see #DEFAULT_MAX_RETRY_ATTEMPTS
*/
public MaxRetryAttemptsPolicy() {
}
/**
* Create a new {@code MaxRetryAttemptsPolicy} with the specified maximum number
* of retry attempts.
* @param maxRetryAttempts the maximum number of retry attempts; must be greater
* than zero
*/
public MaxRetryAttemptsPolicy(int maxRetryAttempts) {
setMaxRetryAttempts(maxRetryAttempts);
}
/**
* Set the maximum number of retry attempts.
* @param maxRetryAttempts the maximum number of retry attempts; must be greater
* than zero
*/
public void setMaxRetryAttempts(int maxRetryAttempts) {
Assert.isTrue(maxRetryAttempts > 0, "Max retry attempts must be greater than zero");
this.maxRetryAttempts = maxRetryAttempts;
}
@Override
public RetryExecution start() {
return new MaxRetryAttemptsPolicyExecution();
}
@Override
public String toString() {
return "MaxRetryAttemptsPolicy[maxRetryAttempts=%d]".formatted(this.maxRetryAttempts);
}
/**
* A {@link RetryExecution} based on a maximum number of retry attempts.
*/
private class MaxRetryAttemptsPolicyExecution implements RetryExecution {
private int retryAttempts;
@Override
public boolean shouldRetry(Throwable throwable) {
return (this.retryAttempts++ < MaxRetryAttemptsPolicy.this.maxRetryAttempts);
}
@Override
public String toString() {
return "MaxRetryAttemptsPolicyExecution[retryAttempts=%d, maxRetryAttempts=%d]"
.formatted(this.retryAttempts, MaxRetryAttemptsPolicy.this.maxRetryAttempts);
}
}
}

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

@ -1,103 +0,0 @@ @@ -1,103 +0,0 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.retry.support;
import java.time.Duration;
import java.time.LocalDateTime;
import org.springframework.core.retry.RetryExecution;
import org.springframework.core.retry.RetryPolicy;
import org.springframework.util.Assert;
/**
* A {@link RetryPolicy} based on a maximum retry {@link Duration}.
*
* @author Mahmoud Ben Hassine
* @author Sam Brannen
* @since 7.0
*/
public final class MaxRetryDurationPolicy implements RetryPolicy {
/**
* The default maximum retry duration: 3 seconds.
*/
public static final Duration DEFAULT_MAX_RETRY_DURATION = Duration.ofSeconds(3);
private Duration maxRetryDuration = DEFAULT_MAX_RETRY_DURATION;
/**
* Create a new {@code MaxRetryDurationPolicy} with the default maximum retry
* {@link Duration}.
* @see #DEFAULT_MAX_RETRY_DURATION
*/
public MaxRetryDurationPolicy() {
}
/**
* Create a new {@code MaxRetryDurationPolicy} with the specified maximum retry
* {@link Duration}.
* @param maxRetryDuration the maximum retry duration; must be positive
*/
public MaxRetryDurationPolicy(Duration maxRetryDuration) {
setMaxRetryDuration(maxRetryDuration);
}
/**
* Set the maximum retry {@link Duration}.
* @param maxRetryDuration the maximum retry duration; must be positive
*/
public void setMaxRetryDuration(Duration maxRetryDuration) {
Assert.isTrue(!maxRetryDuration.isNegative() && !maxRetryDuration.isZero(),
"Max retry duration must be positive");
this.maxRetryDuration = maxRetryDuration;
}
@Override
public RetryExecution start() {
return new MaxRetryDurationPolicyExecution();
}
@Override
public String toString() {
return "MaxRetryDurationPolicy[maxRetryDuration=%dms]".formatted(this.maxRetryDuration.toMillis());
}
/**
* A {@link RetryExecution} based on a maximum retry duration.
*/
private class MaxRetryDurationPolicyExecution implements RetryExecution {
private final LocalDateTime retryStartTime = LocalDateTime.now();
@Override
public boolean shouldRetry(Throwable throwable) {
Duration currentRetryDuration = Duration.between(this.retryStartTime, LocalDateTime.now());
return currentRetryDuration.compareTo(MaxRetryDurationPolicy.this.maxRetryDuration) <= 0;
}
@Override
public String toString() {
return "MaxRetryDurationPolicyExecution[retryStartTime=%s, maxRetryDuration=%dms]"
.formatted(this.retryStartTime, MaxRetryDurationPolicy.this.maxRetryDuration.toMillis());
}
}
}

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

@ -1,75 +0,0 @@ @@ -1,75 +0,0 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.retry.support;
import java.util.function.Predicate;
import org.springframework.core.retry.RetryExecution;
import org.springframework.core.retry.RetryPolicy;
/**
* A {@link RetryPolicy} based on a {@link Predicate}.
*
* @author Mahmoud Ben Hassine
* @author Sam Brannen
* @since 7.0
*/
public final class PredicateRetryPolicy implements RetryPolicy {
private final Predicate<Throwable> predicate;
/**
* Create a new {@code PredicateRetryPolicy} with the given {@link Predicate}.
* @param predicate the predicate to use for determining whether to retry an
* operation based on a given {@link Throwable}
*/
public PredicateRetryPolicy(Predicate<Throwable> predicate) {
this.predicate = predicate;
}
@Override
public RetryExecution start() {
return new PredicateRetryPolicyExecution();
}
@Override
public String toString() {
return "PredicateRetryPolicy[predicate=%s]".formatted(this.predicate.getClass().getSimpleName());
}
/**
* A {@link RetryExecution} based on a {@link Predicate}.
*/
private class PredicateRetryPolicyExecution implements RetryExecution {
@Override
public boolean shouldRetry(Throwable throwable) {
return PredicateRetryPolicy.this.predicate.test(throwable);
}
@Override
public String toString() {
return "PredicateRetryPolicyExecution[predicate=%s]"
.formatted(PredicateRetryPolicy.this.predicate.getClass().getSimpleName());
}
}
}

188
spring-core/src/test/java/org/springframework/core/retry/MaxAttemptsDefaultRetryPolicyTests.java

@ -0,0 +1,188 @@ @@ -0,0 +1,188 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.retry;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.nio.file.FileSystemException;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.Mockito.mock;
/**
* Max attempts tests for {@link DefaultRetryPolicy} and its {@link RetryExecution}.
*
* @author Mahmoud Ben Hassine
* @author Sam Brannen
* @since 7.0
*/
class MaxAttemptsDefaultRetryPolicyTests {
@Test
void invalidMaxAttempts() {
assertThatIllegalArgumentException()
.isThrownBy(() -> RetryPolicy.withMaxAttempts(0))
.withMessage("Max attempts must be greater than zero");
assertThatIllegalArgumentException()
.isThrownBy(() -> RetryPolicy.withMaxAttempts(-1))
.withMessage("Max attempts must be greater than zero");
}
@Test
void maxAttempts() {
var retryPolicy = RetryPolicy.withMaxAttempts(2);
var retryExecution = retryPolicy.start();
var throwable = mock(Throwable.class);
assertThat(retryExecution.shouldRetry(throwable)).isTrue();
assertThat(retryExecution.shouldRetry(throwable)).isTrue();
assertThat(retryExecution.shouldRetry(throwable)).isFalse();
assertThat(retryExecution.shouldRetry(throwable)).isFalse();
}
@Test
void maxAttemptsAndPredicate() {
var retryPolicy = RetryPolicy.builder()
.maxAttempts(4)
.predicate(NumberFormatException.class::isInstance)
.build();
var retryExecution = retryPolicy.start();
// 4 retries
assertThat(retryExecution.shouldRetry(new NumberFormatException())).isTrue();
assertThat(retryExecution.shouldRetry(new IllegalStateException())).isFalse();
assertThat(retryExecution.shouldRetry(new IllegalStateException())).isFalse();
assertThat(retryExecution.shouldRetry(new CustomNumberFormatException())).isTrue();
// After policy exhaustion
assertThat(retryExecution.shouldRetry(new NumberFormatException())).isFalse();
assertThat(retryExecution.shouldRetry(new IllegalStateException())).isFalse();
}
@Test
void maxAttemptsWithIncludesAndExcludes() {
var policy = RetryPolicy.builder()
.maxAttempts(6)
.includes(RuntimeException.class, IOException.class)
.excludes(FileNotFoundException.class, CustomFileSystemException.class)
.build();
var retryExecution = policy.start();
// 6 retries
assertThat(retryExecution.shouldRetry(new IOException())).isTrue();
assertThat(retryExecution.shouldRetry(new RuntimeException())).isTrue();
assertThat(retryExecution.shouldRetry(new FileNotFoundException())).isFalse();
assertThat(retryExecution.shouldRetry(new FileSystemException("file"))).isTrue();
assertThat(retryExecution.shouldRetry(new CustomFileSystemException("file"))).isFalse();
assertThat(retryExecution.shouldRetry(new IOException())).isTrue();
// After policy exhaustion
assertThat(retryExecution.shouldRetry(new IOException())).isFalse();
}
@Test
void toStringImplementations() {
var policy = RetryPolicy.builder()
.maxAttempts(6)
.includes(RuntimeException.class, IOException.class)
.excludes(FileNotFoundException.class, CustomFileSystemException.class)
.build();
assertThat(policy).asString().isEqualTo("""
DefaultRetryPolicy[\
maxAttempts=6, \
includes=[java.lang.RuntimeException, java.io.IOException], \
excludes=[java.io.FileNotFoundException, \
org.springframework.core.retry.MaxAttemptsDefaultRetryPolicyTests.CustomFileSystemException]]""");
var template = """
DefaultRetryPolicyExecution[\
maxAttempts=6, \
retryCount=%d, \
includes=[java.lang.RuntimeException, java.io.IOException], \
excludes=[java.io.FileNotFoundException, \
org.springframework.core.retry.MaxAttemptsDefaultRetryPolicyTests.CustomFileSystemException]]""";
var retryExecution = policy.start();
var count = 0;
assertThat(retryExecution).asString().isEqualTo(template, count++);
retryExecution.shouldRetry(new IOException());
assertThat(retryExecution).asString().isEqualTo(template, count++);
retryExecution.shouldRetry(new IOException());
assertThat(retryExecution).asString().isEqualTo(template, count++);
}
@Test
void toStringImplementationsWithPredicateAsClass() {
var policy = RetryPolicy.builder()
.maxAttempts(1)
.predicate(new NumberFormatExceptionMatcher())
.build();
assertThat(policy).asString()
.isEqualTo("DefaultRetryPolicy[maxAttempts=1, predicate=NumberFormatExceptionMatcher]");
var retryExecution = policy.start();
assertThat(retryExecution).asString()
.isEqualTo("DefaultRetryPolicyExecution[maxAttempts=1, retryCount=0, predicate=NumberFormatExceptionMatcher]");
}
@Test
void toStringImplementationsWithPredicateAsLambda() {
var policy = RetryPolicy.builder()
.maxAttempts(2)
.predicate(NumberFormatException.class::isInstance)
.build();
assertThat(policy).asString()
.matches("DefaultRetryPolicy\\[maxAttempts=2, predicate=MaxAttemptsDefaultRetryPolicyTests.+?Lambda.+?]");
var retryExecution = policy.start();
assertThat(retryExecution).asString()
.matches("DefaultRetryPolicyExecution\\[maxAttempts=2, retryCount=0, predicate=MaxAttemptsDefaultRetryPolicyTests.+?Lambda.+?]");
retryExecution.shouldRetry(new NumberFormatException());
assertThat(retryExecution).asString()
.matches("DefaultRetryPolicyExecution\\[maxAttempts=2, retryCount=1, predicate=MaxAttemptsDefaultRetryPolicyTests.+?Lambda.+?]");
retryExecution.shouldRetry(new NumberFormatException());
assertThat(retryExecution).asString()
.matches("DefaultRetryPolicyExecution\\[maxAttempts=2, retryCount=2, predicate=MaxAttemptsDefaultRetryPolicyTests.+?Lambda.+?]");
retryExecution.shouldRetry(new NumberFormatException());
assertThat(retryExecution).asString()
.matches("DefaultRetryPolicyExecution\\[maxAttempts=2, retryCount=3, predicate=MaxAttemptsDefaultRetryPolicyTests.+?Lambda.+?]");
}
@SuppressWarnings("serial")
private static class CustomNumberFormatException extends NumberFormatException {
}
@SuppressWarnings("serial")
private static class CustomFileSystemException extends FileSystemException {
CustomFileSystemException(String file) {
super(file);
}
}
}

65
spring-core/src/test/java/org/springframework/core/retry/MaxDurationDefaultRetryPolicyTests.java

@ -0,0 +1,65 @@ @@ -0,0 +1,65 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.retry;
import java.time.Duration;
import org.junit.jupiter.api.Test;
import static java.time.Duration.ofSeconds;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Max duration tests for {@link DefaultRetryPolicy} and its {@link RetryExecution}.
*
* @author Mahmoud Ben Hassine
* @author Sam Brannen
* @since 7.0
*/
class MaxDurationDefaultRetryPolicyTests {
@Test
void invalidMaxDuration() {
assertThatIllegalArgumentException()
.isThrownBy(() -> RetryPolicy.withMaxDuration(Duration.ZERO))
.withMessage("Max duration must be positive");
assertThatIllegalArgumentException()
.isThrownBy(() -> RetryPolicy.withMaxDuration(ofSeconds(-1)))
.withMessage("Max duration must be positive");
}
@Test
void toStringImplementations() {
var policy1 = RetryPolicy.withMaxDuration(ofSeconds(3));
var policy2 = RetryPolicy.builder()
.maxDuration(ofSeconds(1))
.predicate(new NumberFormatExceptionMatcher())
.build();
assertThat(policy1).asString()
.isEqualTo("DefaultRetryPolicy[maxDuration=3000ms]");
assertThat(policy2).asString()
.isEqualTo("DefaultRetryPolicy[maxDuration=1000ms, predicate=NumberFormatExceptionMatcher]");
assertThat(policy1.start()).asString()
.matches("DefaultRetryPolicyExecution\\[maxDuration=3000ms, retryStartTime=.+]");
assertThat(policy2.start()).asString()
.matches("DefaultRetryPolicyExecution\\[maxDuration=1000ms, retryStartTime=.+, predicate=NumberFormatExceptionMatcher]");
}
}

34
spring-core/src/test/java/org/springframework/core/retry/NumberFormatExceptionMatcher.java

@ -0,0 +1,34 @@ @@ -0,0 +1,34 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.retry;
import java.util.function.Predicate;
/**
* Predicate that matches {@link NumberFormatException}.
*
* @author Sam Brannen
* @since 7.0
*/
class NumberFormatExceptionMatcher implements Predicate<Throwable> {
@Override
public boolean test(Throwable throwable) {
return (throwable instanceof NumberFormatException);
}
}

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

@ -16,6 +16,8 @@ @@ -16,6 +16,8 @@
package org.springframework.core.retry;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.time.Duration;
import java.util.concurrent.atomic.AtomicInteger;
@ -26,6 +28,7 @@ import org.springframework.util.backoff.FixedBackOff; @@ -26,6 +28,7 @@ import org.springframework.util.backoff.FixedBackOff;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatExceptionOfType;
import static org.assertj.core.api.InstanceOfAssertFactories.array;
/**
* Tests for {@link RetryTemplate}.
@ -74,10 +77,10 @@ class RetryTemplateTests { @@ -74,10 +77,10 @@ class RetryTemplateTests {
@Test
void retryWithExhaustedPolicy() {
AtomicInteger invocationCount = new AtomicInteger();
RuntimeException exception = new RuntimeException("Boom!");
var invocationCount = new AtomicInteger();
var exception = new RuntimeException("Boom!");
Retryable<String> retryable = new Retryable<>() {
var retryable = new Retryable<>() {
@Override
public String execute() {
invocationCount.incrementAndGet();
@ -100,11 +103,11 @@ class RetryTemplateTests { @@ -100,11 +103,11 @@ class RetryTemplateTests {
}
@Test
void retryWithFailingRetryableAndCustomRetryPolicy() {
AtomicInteger invocationCount = new AtomicInteger();
RuntimeException exception = new NumberFormatException();
void retryWithFailingRetryableAndCustomRetryPolicyWithMultiplePredicates() {
var invocationCount = new AtomicInteger();
var exception = new NumberFormatException("Boom!");
Retryable<String> retryable = new Retryable<>() {
var retryable = new Retryable<>() {
@Override
public String execute() {
invocationCount.incrementAndGet();
@ -117,20 +120,111 @@ class RetryTemplateTests { @@ -117,20 +120,111 @@ class RetryTemplateTests {
}
};
AtomicInteger retryCount = new AtomicInteger();
// Custom RetryPolicy that only retries for a NumberFormatException and max 5 retry attempts.
RetryPolicy retryPolicy = () -> throwable -> (retryCount.incrementAndGet() <= 5 && throwable instanceof NumberFormatException);
var retryPolicy = RetryPolicy.builder()
.maxAttempts(5)
.maxDuration(Duration.ofMillis(100))
.predicate(NumberFormatException.class::isInstance)
.predicate(t -> t.getMessage().equals("Boom!"))
.build();
retryTemplate.setRetryPolicy(retryPolicy);
assertThat(invocationCount).hasValue(0);
assertThat(retryCount).hasValue(0);
assertThatExceptionOfType(RetryException.class)
.isThrownBy(() -> retryTemplate.execute(retryable))
.withMessage("Retry policy for operation 'always fails' exhausted; aborting execution")
.withCause(exception);
// 6 = 1 initial invocation + 5 retry attempts
// 6 = 1 initial invocation + 5 retry attempts
assertThat(invocationCount).hasValue(6);
assertThat(retryCount).hasValue(6);
}
@Test
void retryWithExceptionIncludes() throws Exception {
var invocationCount = new AtomicInteger();
var retryable = new Retryable<>() {
@Override
public String execute() throws Exception {
return switch (invocationCount.incrementAndGet()) {
case 1 -> throw new FileNotFoundException();
case 2 -> throw new IOException();
case 3 -> throw new IllegalStateException();
default -> "success";
};
}
@Override
public String getName() {
return "test";
}
};
var retryPolicy = RetryPolicy.builder()
.maxAttempts(Integer.MAX_VALUE)
.includes(IOException.class)
.build();
retryTemplate.setRetryPolicy(retryPolicy);
assertThat(invocationCount).hasValue(0);
assertThatExceptionOfType(RetryException.class)
.isThrownBy(() -> retryTemplate.execute(retryable))
.withMessage("Retry policy for operation 'test' exhausted; aborting execution")
.withCauseExactlyInstanceOf(IllegalStateException.class)
.extracting(Throwable::getSuppressed, array(Throwable[].class))
.satisfiesExactly(
suppressed1 -> assertThat(suppressed1).isExactlyInstanceOf(IOException.class),
suppressed2 -> assertThat(suppressed2).isExactlyInstanceOf(FileNotFoundException.class)
);
// 3 = 1 initial invocation + 2 retry attempts
assertThat(invocationCount).hasValue(3);
}
@Test
void retryWithExceptionExcludes() throws Exception {
var invocationCount = new AtomicInteger();
var retryable = new Retryable<>() {
@Override
public String execute() throws Exception {
return switch (invocationCount.incrementAndGet()) {
case 1 -> throw new IOException();
case 2 -> throw new IOException();
case 3 -> throw new CustomFileNotFoundException();
default -> "success";
};
}
@Override
public String getName() {
return "test";
}
};
var retryPolicy = RetryPolicy.builder()
.maxAttempts(Integer.MAX_VALUE)
.includes(IOException.class)
.excludes(FileNotFoundException.class)
.build();
retryTemplate.setRetryPolicy(retryPolicy);
assertThat(invocationCount).hasValue(0);
assertThatExceptionOfType(RetryException.class)
.isThrownBy(() -> retryTemplate.execute(retryable))
.withMessage("Retry policy for operation 'test' exhausted; aborting execution")
.withCauseExactlyInstanceOf(CustomFileNotFoundException.class)
.extracting(Throwable::getSuppressed, array(Throwable[].class))
.satisfiesExactly(
suppressed1 -> assertThat(suppressed1).isExactlyInstanceOf(IOException.class),
suppressed2 -> assertThat(suppressed2).isExactlyInstanceOf(IOException.class)
);
// 3 = 1 initial invocation + 2 retry attempts
assertThat(invocationCount).hasValue(3);
}
@SuppressWarnings("serial")
private static class CustomFileNotFoundException extends FileNotFoundException {
}
}

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

@ -1,114 +0,0 @@ @@ -1,114 +0,0 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.retry.support;
import org.junit.jupiter.api.Test;
import org.springframework.core.retry.RetryExecution;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
import static org.mockito.Mockito.mock;
/**
* Tests for {@link MaxRetryAttemptsPolicy} and its {@link RetryExecution}.
*
* @author Mahmoud Ben Hassine
* @author Sam Brannen
* @since 7.0
*/
class MaxRetryAttemptsPolicyTests {
@Test
void defaultMaxRetryAttempts() {
// given
MaxRetryAttemptsPolicy retryPolicy = new MaxRetryAttemptsPolicy();
Throwable throwable = mock();
// when
RetryExecution retryExecution = retryPolicy.start();
// then
assertThat(retryExecution.shouldRetry(throwable)).isTrue();
assertThat(retryExecution.shouldRetry(throwable)).isTrue();
assertThat(retryExecution.shouldRetry(throwable)).isTrue();
assertThat(retryExecution.shouldRetry(throwable)).isFalse();
assertThat(retryExecution.shouldRetry(throwable)).isFalse();
}
@Test
void customMaxRetryAttempts() {
// given
MaxRetryAttemptsPolicy retryPolicy = new MaxRetryAttemptsPolicy(2);
Throwable throwable = mock();
// when
RetryExecution retryExecution = retryPolicy.start();
// then
assertThat(retryExecution.shouldRetry(throwable)).isTrue();
assertThat(retryExecution.shouldRetry(throwable)).isTrue();
assertThat(retryExecution.shouldRetry(throwable)).isFalse();
assertThat(retryExecution.shouldRetry(throwable)).isFalse();
}
@Test
void invalidMaxRetryAttempts() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new MaxRetryAttemptsPolicy(0))
.withMessage("Max retry attempts must be greater than zero");
assertThatIllegalArgumentException()
.isThrownBy(() -> new MaxRetryAttemptsPolicy(-1))
.withMessage("Max retry attempts must be greater than zero");
}
@Test
void toStringImplementations() {
MaxRetryAttemptsPolicy policy1 = new MaxRetryAttemptsPolicy();
MaxRetryAttemptsPolicy policy2 = new MaxRetryAttemptsPolicy(1);
assertThat(policy1).asString().isEqualTo("MaxRetryAttemptsPolicy[maxRetryAttempts=3]");
assertThat(policy2).asString().isEqualTo("MaxRetryAttemptsPolicy[maxRetryAttempts=1]");
RetryExecution retryExecution = policy1.start();
assertThat(retryExecution).asString()
.isEqualTo("MaxRetryAttemptsPolicyExecution[retryAttempts=0, maxRetryAttempts=3]");
assertThat(retryExecution.shouldRetry(mock())).isTrue();
assertThat(retryExecution).asString()
.isEqualTo("MaxRetryAttemptsPolicyExecution[retryAttempts=1, maxRetryAttempts=3]");
assertThat(retryExecution.shouldRetry(mock())).isTrue();
assertThat(retryExecution).asString()
.isEqualTo("MaxRetryAttemptsPolicyExecution[retryAttempts=2, maxRetryAttempts=3]");
assertThat(retryExecution.shouldRetry(mock())).isTrue();
assertThat(retryExecution).asString()
.isEqualTo("MaxRetryAttemptsPolicyExecution[retryAttempts=3, maxRetryAttempts=3]");
assertThat(retryExecution.shouldRetry(mock())).isFalse();
assertThat(retryExecution).asString()
.isEqualTo("MaxRetryAttemptsPolicyExecution[retryAttempts=4, maxRetryAttempts=3]");
assertThat(retryExecution.shouldRetry(mock())).isFalse();
assertThat(retryExecution).asString()
.isEqualTo("MaxRetryAttemptsPolicyExecution[retryAttempts=5, maxRetryAttempts=3]");
}
}

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

@ -1,58 +0,0 @@ @@ -1,58 +0,0 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.retry.support;
import java.time.Duration;
import org.junit.jupiter.api.Test;
import org.springframework.core.retry.RetryExecution;
import static org.assertj.core.api.Assertions.assertThat;
import static org.assertj.core.api.Assertions.assertThatIllegalArgumentException;
/**
* Tests for {@link MaxRetryDurationPolicy} and its {@link RetryExecution}.
*
* @author Mahmoud Ben Hassine
* @author Sam Brannen
* @since 7.0
*/
class MaxRetryDurationPolicyTests {
@Test
void invalidMaxRetryDuration() {
assertThatIllegalArgumentException()
.isThrownBy(() -> new MaxRetryDurationPolicy(Duration.ZERO))
.withMessage("Max retry duration must be positive");
}
@Test
void toStringImplementations() {
MaxRetryDurationPolicy policy1 = new MaxRetryDurationPolicy();
MaxRetryDurationPolicy policy2 = new MaxRetryDurationPolicy(Duration.ofSeconds(1));
assertThat(policy1).asString().isEqualTo("MaxRetryDurationPolicy[maxRetryDuration=3000ms]");
assertThat(policy2).asString().isEqualTo("MaxRetryDurationPolicy[maxRetryDuration=1000ms]");
assertThat(policy1.start()).asString()
.matches("MaxRetryDurationPolicyExecution\\[retryStartTime=.+, maxRetryDuration=3000ms\\]");
assertThat(policy2.start()).asString()
.matches("MaxRetryDurationPolicyExecution\\[retryStartTime=.+, maxRetryDuration=1000ms\\]");
}
}

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

@ -1,70 +0,0 @@ @@ -1,70 +0,0 @@
/*
* Copyright 2002-2025 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.core.retry.support;
import java.util.function.Predicate;
import org.junit.jupiter.api.Test;
import org.springframework.core.retry.RetryExecution;
import static org.assertj.core.api.Assertions.assertThat;
/**
* Tests for {@link PredicateRetryPolicy} and its {@link RetryExecution}.
*
* @author Mahmoud Ben Hassine
* @author Sam Brannen
* @since 7.0
*/
class PredicateRetryPolicyTests {
@Test
void predicateRetryPolicy() {
Predicate<Throwable> predicate = NumberFormatException.class::isInstance;
PredicateRetryPolicy retryPolicy = new PredicateRetryPolicy(predicate);
RetryExecution retryExecution = retryPolicy.start();
assertThat(retryExecution.shouldRetry(new NumberFormatException())).isTrue();
assertThat(retryExecution.shouldRetry(new IllegalStateException())).isFalse();
}
@Test
void toStringImplementations() {
PredicateRetryPolicy policy1 = new PredicateRetryPolicy(NumberFormatException.class::isInstance);
PredicateRetryPolicy policy2 = new PredicateRetryPolicy(new NumberFormatExceptionMatcher());
assertThat(policy1).asString().matches("PredicateRetryPolicy\\[predicate=PredicateRetryPolicyTests.+?Lambda.+?\\]");
assertThat(policy2).asString().isEqualTo("PredicateRetryPolicy[predicate=NumberFormatExceptionMatcher]");
assertThat(policy1.start()).asString()
.matches("PredicateRetryPolicyExecution\\[predicate=PredicateRetryPolicyTests.+?Lambda.+?\\]");
assertThat(policy2.start()).asString()
.isEqualTo("PredicateRetryPolicyExecution[predicate=NumberFormatExceptionMatcher]");
}
private static class NumberFormatExceptionMatcher implements Predicate<Throwable> {
@Override
public boolean test(Throwable throwable) {
return (throwable instanceof NumberFormatException);
}
}
}
Loading…
Cancel
Save