问题
I have a method with the following signature:
public Optional<String> doSomething() {
...
}
If I get an empty Optional I'd like to retry this method and only after 3 times return the empty Optional.
I've looked and found the Retryable spring annotation, but it seems to only work on Exceptions.
If possible I'd like to use a library for this, and avoid:
- Creating and throwing an exception.
- Writing the logic myself.
回答1:
I have been using failsafe build in retry. You can retry based on predicates and exceptions.
Your code would look like this:
private Optional<String> doSomethingWithRetry() {
RetryPolicy<Optional> retryPolicy = new RetryPolicy<Optional>()
.withMaxAttempts(3)
.handleResultIf(result -> {
System.out.println("predicate");
return !result.isPresent();
});
return Failsafe
.with(retryPolicy)
.onSuccess(response -> System.out.println("ok"))
.onFailure(response -> System.out.println("no ok"))
.get(() -> doSomething());
}
private Optional<String> doSomething() {
return Optional.of("result");
}
If the optional is not empty the output is:
predicate
ok
Otherwise looks like:
predicate
predicate
predicate
no ok
回答2:
@Retryable (and the underlying RetryTemplate) are purely based on exceptions.
You could subclass RetryTemplate, overriding doExecute() to check the return value.
You would probably have to replicate much of the code in the method; it's not really designed for overriding just the retryCallback.doWithRetry() call.
You can use a custom RetryTemplate in a RetryOperationsInterceptor (specified in the @Retryable in the interceptor property).
EDIT
The current RetryTemplate code looks like this...
while (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {
try {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Retry: count=" + context.getRetryCount());
}
// Reset the last exception, so if we are successful
// the close interceptors will not think we failed...
lastException = null;
return retryCallback.doWithRetry(context);
}
catch (Throwable e) {
lastException = e;
try {
registerThrowable(retryPolicy, state, context, e);
}
catch (Exception ex) {
throw new TerminatedRetryException("Could not register throwable",
ex);
}
finally {
doOnErrorInterceptors(retryCallback, context, e);
}
...
}
You would need to change it to something like...
while (canRetry(retryPolicy, context) && !context.isExhaustedOnly()) {
try {
if (this.logger.isDebugEnabled()) {
this.logger.debug("Retry: count=" + context.getRetryCount());
}
// Reset the last exception, so if we are successful
// the close interceptors will not think we failed...
lastException = null;
T result = retryCallback.doWithRetry(context);
if (((Optional<String>) result).get() == null) {
try {
registerThrowable(retryPolicy, state, context, someDummyException);
}
catch (Exception ex) {
throw new TerminatedRetryException("Could not register throwable",
ex);
}
finally {
doOnErrorInterceptors(retryCallback, context, e);
}
...
}
else {
return result;
}
}
catch (Throwable e) {
...
}
Where someDummyException is to fool the context into incrementing the counter. It can be a static field, just created once.
回答3:
I currently wrote a util for this myself (vanilla java), other answers are more than welcome:
import java.util.function.Predicate;
import java.util.function.Supplier;
public class Retryable<T> {
private Supplier<T> action = () -> null;
private Predicate<T> successCondition = ($) -> true;
private int numberOfTries = 3;
private long delay = 1000L;
private Supplier<T> fallback = () -> null;
public static <A> Retryable<A> of(Supplier<A> action) {
return new Retryable<A>().run(action);
}
public Retryable<T> run(Supplier<T> action) {
this.action = action;
return this;
}
public Retryable<T> successIs(Predicate<T> successCondition) {
this.successCondition = successCondition;
return this;
}
public Retryable<T> retries(int numberOfTries) {
this.numberOfTries = numberOfTries;
return this;
}
public Retryable<T> delay(long delay) {
this.delay = delay;
return this;
}
public Retryable<T> orElse(Supplier<T> fallback) {
this.fallback = fallback;
return this;
}
public T execute() {
for (int i = 0; i < numberOfTries; i++) {
T t = action.get();
if (successCondition.test(t)) {
return t;
}
try {
Thread.sleep(delay);
} catch (InterruptedException e) {
// do nothing
}
}
return fallback.get();
}
}
With this code, my method looks like this:
public Optional<String> doSomething() {
return Retryable
.of(() -> actualDoSomething())
.successIs(Optional::isPresent)
.retries(3)
.delay(1000L)
.orElse(Optional::empty)
.execute();
}
回答4:
Just throw Exception if your result is not desired
来源:https://stackoverflow.com/questions/54521486/retry-a-method-based-on-result-instead-of-exception