I have a process that needs to call a method and return its value. However, there are several different methods that this process may need to call, depending on the situation. If I could pass the method and its arguments to the process (like in Python), then this would be no problem. However, I don't know of any way to do this in Java.
Here's a concrete example. (This example uses Apache ZooKeeper, but you don't need to know anything about ZooKeeper to understand the example.)
The ZooKeeper object has several methods that will fail if the network goes down. In this case, I always want to retry the method. To make this easy, I made a "BetterZooKeeper" class that inherits the ZooKeeper class, and all of its methods automatically retry on failure.
This is what the code looked like:
public class BetterZooKeeper extends ZooKeeper {
private void waitForReconnect() {
// logic
}
@Override
public Stat exists(String path, Watcher watcher) {
while (true) {
try {
return super.exists(path, watcher);
} catch (KeeperException e) {
// We will retry.
}
waitForReconnect();
}
}
@Override
public byte[] getData(String path, boolean watch, Stat stat) {
while (true) {
try {
return super.getData(path, watch, stat);
} catch (KeeperException e) {
// We will retry.
}
waitForReconnect();
}
}
@Override
public void delete(String path, int version) {
while (true) {
try {
super.delete(path, version);
return;
} catch (KeeperException e) {
// We will retry.
}
waitForReconnect();
}
}
}
(In the actual program there is much more logic and many more methods that I took out of the example for simplicity.)
We can see that I'm using the same retry logic, but the arguments, method call, and return type are all different for each of the methods.
Here's what I did to eliminate the duplication of code:
public class BetterZooKeeper extends ZooKeeper {
private void waitForReconnect() {
// logic
}
@Override
public Stat exists(final String path, final Watcher watcher) {
return new RetryableZooKeeperAction<Stat>() {
@Override
public Stat action() {
return BetterZooKeeper.super.exists(path, watcher);
}
}.run();
}
@Override
public byte[] getData(final String path, final boolean watch, final Stat stat) {
return new RetryableZooKeeperAction<byte[]>() {
@Override
public byte[] action() {
return BetterZooKeeper.super.getData(path, watch, stat);
}
}.run();
}
@Override
public void delete(final String path, final int version) {
new RetryableZooKeeperAction<Object>() {
@Override
public Object action() {
BetterZooKeeper.super.delete(path, version);
return null;
}
}.run();
return;
}
private abstract class RetryableZooKeeperAction<T> {
public abstract T action();
public final T run() {
while (true) {
try {
return action();
} catch (KeeperException e) {
// We will retry.
}
waitForReconnect();
}
}
}
}
The RetryableZooKeeperAction is parameterized with the return type of the function. The run() method holds the retry logic, and the action() method is a placeholder for whichever ZooKeeper method needs to be run. Each of the public methods of BetterZooKeeper instantiates an anonymous inner class that is a subclass of the RetryableZooKeeperAction inner class, and it overrides the action() method. The local variables are (strangely enough) implicitly passed to the action() method, which is possible because they are final.
In the end, this approach does work and it does eliminate the duplication of the retry logic. However, it has two major drawbacks: (1) it creates a new object every time a method is called, and (2) it's ugly and hardly readable. Also I had to workaround the 'delete' method which has a void return value.
So, here is my question: is there a better way to do this in Java? This can't be a totally uncommon task, and other languages (like Python) make it easier by allowing methods to be passed. I suspect there might be a way to do this through reflection, but I haven't been able to wrap my head around it.