Java RMI - Better error handling and performance with lambdas
Even though Java RMI is already very old and has some problems, there are still a lot of application that use it. While some applications have no time, money or resources to migrate away from RMI, others might not be able to migrate away, due to the fact that the RMI-Stubs they consume are external services or whatever.
In the product that I work on, we use quite a big RMI-API in order to do
synchronous communication between client and server. Our structure is however
not optimal. Services are split into topics, such as Project
or
User
. Each Service has a server-side interface and a client-side
implementation of that interface that catches RemoteException
s.
Sadly all our try-catch constructs pretty much look like this:
try {
Service.lookup(Service.class).method(parameters);
} catch( RemoteException exception ) {
logger.error("Ooopsie woopsie!!!11!!");
return null;
}
So ... not very useful. The problem with this is, that we can't really
handle any errors that occur during the actual method-call, such as
ConnectException
. Well, it could, but it'd require tons of
boilerplate for every method that you ever want to be able to call. As it
is now however, the call would silently fail and return and incorrect return
value which we can't differentiate from a correct return value.
However, we do handle errors that occur during the lookup of the RMI-stub. But technically another error could occur between retrieving the stub and calling the method on it.
Another problem with this approach is, that looking up the stub is actually a network call as well, meaning each RMI-call has at least doubled latency, ultimately dragging down the performance by a lot.
Finally the most annoying part about the solution, was that we had to write these boilerplate methods to be able to use the RMI-methods at all.
However, behold the lambdas!
Using lambdas we can easily do both the error handling, the call-retrying and won't have to write as much boilerpalte. While lambdas clearly take a bit more memory, I think it's worth the lower pressure on the network.
The interface is rather simple. We have two methods, one for calling methods that don't return a value and one for the ones that do. For the sake of keeping this post a bit smaller, I've simplified it a lot:
public static synchronized <ServiceType extends Remote, ResultType> ResultType get(
final Class<ServiceType> serviceClass,
final ServiceResultFunction<ServiceType, ResultType> methodcall,
final int amountOfRetries)
{
try {
ServiceType service = lookupServiceOrGetFromCache(serviceClass);
return methodcall;
} catch ( ConnectException exception ) {
//Clearly the code doesn't make sense like this, but you get the gist.
if (amountOfRetries == 0) {
throw new ConnectionFailedException(...);
}
get(serviceClass, methodcall, amountOfRetries-1);
} catch ( RemoteExcception exception ) {
//Business Exception
throw new RuntimeException(exception);
}
}
public static synchronized <ServiceType extends Remote, ResultType> ResultType get(
final Class<ServiceType> serviceClass,
final ServiceResultFunction<ServiceType, ResultType> methodcall.
final int amountOfRetries )
{
...
}
Now you can call any of your RMI-stub-methods without having to write additional boilerplate:
User user = Services.get(UserService.class, userService -> userService.findByID(4));
System.out.println(user.getRegisteredDate());
I hope this will help someone in the future ;)