BT

Facilitating the Spread of Knowledge and Innovation in Professional Software Development

Write for InfoQ

Topics

Choose your language

InfoQ Homepage Articles Migrate a RMI-Based Legacy Application to WebSocket

Migrate a RMI-Based Legacy Application to WebSocket

This item in japanese

Bookmarks

Key Takeaways

  • Technical debt, especially in enterprise software, is a relevant problem that we recurrently have to face. This article provides a use case related to how I’ve dealt with removing a technical debt in a large enterprise application based on an old fashioned Remote Method Invocation (RMI) protocol, migrating it toward modern cloud-aware communication technologies.
  • In the use case provided, the experience in selecting a suitable open source project, in forking it and in extending it to fit in the purpose. The ability in selecting, using, extending open source software is strategic in a modern Application Lifecycle Management.
  • The emerging reactive programming paradigm in the cloud era, based on a functional approach, seems a better fit in the new software development challenges. In the Java world, the reactive-stream is one of the attempts to standardize reactive approaches in Java development. One of the most important parts faced in migration has been switching from a classical sync request/response to a reactive one.
  • JakartaEE is the heir of JavaEE and it is the reference point for Enterprise Applications. One of the most important goals of such a migration is to make the final assets deployable within a JakartaEE container (e.g., Tomcat, WildFly, etc.).

What is Remote Method Invocation (RMI)?

RMI was first introduced in J2SE 5.0 to provide an all-in-one Java-based solution for application interoperability across networks. Its fundamentals were:

  • Remote Procedure Call (RPC) based on Client-Server model
  • Synchronous by design
  • TCP Socket as transport protocol
  • Java binary (built-in) serialization as application protocol
  • Allow bidirectional communication protocol
    • clients call servers and vice-versa (i.e., callback)

At that time, this was quite a revolution in the Java world because interoperability became easier to achieve in a period where the internet was rising in the IT landscape.

This implied that a lot of client-server applications were developed based on this new and compelling technology.

Obviously looking at RMI now, in the modern Internet era, such technology seems old and out-of-scope considering that in the modern web-based architecture, the front-end is mostly based on internet browsers and protocols are based on open standards that must be platforms and technologies independent.

Target Readers

For what is described below, this article is targeted to Java Developers and/or Architects that, for whatever reasons, are dealing with modernization of large legacy RMI applications.

I’ve had such a challenge and I'd like to share my experience in facing it.

Migration vs Rewritten

First of all,there is one thing that I would like to emphasize for the reader. By "migrate," I absolutely do not mean "rewrite," so if you are interested in such "migration," this could be a new hope to bring a legacy Java application one step closer to its modernization.

To give you a comprehensive view of which tasks are needed and the implications behind such migration, I’ll present to you a real use case that I’ve tackled in one of my works.

Use Case - Evolve an Old Full-Stack Java Application Based on RMI

I had an old, but perfectly working, large client/server java application ported on JDK/JRE 8 with a front-end based on Swing/AWT using RMI as the underlying communication protocol.

Requirement

  1. Move the application to the Cloud using Docker/Kubernetes
  2. Move toward modern web compliant transport protocol like HTTP/Websocket/gRPC

Challenge: RMI Tunneling Over HTTP

The first evaluation was to use RMI HTTP tunneling, but right from the start, it seemed a bit too complicated and since the application heavily uses RMI Callbacks, the one-way protocol like HTTP was not suitable for the purpose.

In such a sense, other than Socket, Websocket seemed the best fit for purpose protocol, but even if I’ve spent enough effort to understand how to plug Websocket as the underlying protocol of RMI, the results were a waste of time :(.

Challenge: Evaluate an Open Source Alternative RMI Implementation

So another possible solution has been to evaluate alternative RMI implementations. I've been searching for them trying to identify an open source semi-finished product that is easy to understand and with a flexible and adaptable architecture allowing to plug a new protocol.

Evolving Selected Open Source Project: LipeRMI

During my Internet surfing, I’ve landed on a GitHub hosted project named LipeRMI defined as a light-weight RMI alternative implementation for Java. It seemed to me that LipeRMI had the expected requirements. I tested it with a simple, but complete application and it worked. And amazingly, it also supported, very well, the RMI Callbacks.

Even if the original implementation was based upon socket, its architecture was flexible enough to allow me to be confident in the possibility to extend and enhance it to accomplish my needs, and so my journey began.

Understand : LipeRMI "The Original"

In the picture below, there is the High Level Architecture as presented in the original project.

Original High Level Architecture

As you can see, it is pretty simple. The main component is the CallHandler which knows application interfaces and implementations. Both client and server use CallHandler and directly involve Sockets to make a connection’s session between them.

Evolving LipeRMI - "The Fork"

As a first step, I forked the project and converted it into a Maven multimodule project to allow better management by simplifying both the extension model and the tests.

As result of such refactoring, I got the following modules:

Module Summary
core the core implementation
socket core extension implementing synch socket protocol
websocket core reactive extension implementing async websocket protocol
rmi-emul core extension to emulate RMI API
examples various examples
cheerpj WebAssembly frontend based upon CheerpJ (EXPERIMENTAL)

In this article, I’m going to focus on core, socket and websocket, where core+socket should be considered a modular re-interpretation that the original project while websocket is a completely new implementation that takes advantage of the introduced core reactive protocol abstraction with usage of a reactive-stream.

Core Module

Protocol Abstraction

In the core module, I’ve placed a greater part of the original project’s code. Taking a look at the original architecture, one of the main goals was decoupling/abstracting the underlying protocol, so I introduced some interfaces like IServer, iClient and IRemoteCaller in order to achieve this and as consequence in core module there aren’t any specific protocol implementations.

In the picture below, there is an overview of the new architecture allowing protocol abstraction.

Class Diagram with protocol abstraction

Socket Module

In the socket module, I’ve simply implemented all the synchronous abstractions provided by the core module that essentially reuses code from the original project, but putting it in the new architecture.

Class Diagram using socket implementation

Code Examples

To give you an idea of the complexity in using the new LipeRMI implementation, consider the code snippets below that I’ve extracted by working examples.

Remotable Interfaces

// Remotable Interface
public interface IAnotherObject extends Serializable {
    int getNumber();    
}

// Remotable Interface
public interface ITestService extends Serializable {

    public String letsDoIt();
    
    public IAnotherObject getAnotherObject();
    
    public void throwAExceptionPlease();
}

Server

// Server
public class TestSocketServer implements Constants {

    // Remotable Interface Implementation
    static class TestServiceImpl implements ITestService {
        final CallHandler callHandler;
        int anotherNumber = 0;

        public TestServiceImpl(CallHandler callHandler) {
            this.callHandler = callHandler;
        }

        @Override
        public String letsDoIt() {
            log.info("letsDoIt() done.");
            return "server saying hi";
        }

        @Override
        public IAnotherObject getAnotherObject() {
            log.info("building AnotherObject with anotherNumber= {}", anotherNumber);
            
            IAnotherObject ao = new AnotherObjectImpl(anotherNumber++);

            callHandler.exportObject(IAnotherObject.class, ao);

            return ao;
        }

        @Override
        public void throwAExceptionPlease() {
            throw new AssertionError("take it easy!");
        }
    }

    public TestSocketServer() throws Exception {

        log.info("Creating Server");
        SocketServer server = new SocketServer();

        final ITestService service = new TestServiceImpl(server.getCallHandler());

        log.info("Registering implementation");
        server.getCallHandler().registerGlobal(ITestService.class, service);

        server.start(PORT, GZIPProtocolFilter.Shared);

        log.info("Server listening");
    
    }

    public static void main(String[] args) throws Exception{
        new TestSocketServer();
    }


}

Client

// Client
public class TestSocketClient implements Constants {
    
    public static void main(String... args) {

        log.info("Creating Client");
        try( final SocketClient client = new SocketClient("localhost", PORT, GZIPProtocolFilter.Shared)) {

            log.info("Getting proxy");
            final ITestService myServiceCaller = client.getGlobal(ITestService.class);

            log.info("Calling the method letsDoIt(): {}", myServiceCaller.letsDoIt());

            try {
                log.info("Calling the method throwAExceptionPlease():");
                myServiceCaller.throwAExceptionPlease();
            }
            catch (AssertionError e) {
                log.info("Catch! {}", e.getMessage());
            }

            final IAnotherObject ao = myServiceCaller.getAnotherObject();

            log.info("AnotherObject::getNumber(): {}", ao.getNumber());
                            
        }
        
    }
    
}

Evolving LipeRMI : Add Reactivity to the Framework with reactive-stream

Unfortunately, the socket promotes a synchronous programming model that does not fit very well with the asynchronous one promoted by websocket, so I decided to move the framework toward a reactive approach using the Reactive Streams standard.

Design Guideline

The basic idea was to simply decouple the request and response using events so the request comes out from a publisher while the response obtained by subscriber and the entire Lifecycle request/response would be managed by a CompletableFuture (essentially the Java Promise Design Pattern).

Reactive Protocol Abstraction (Asynchronous)

As previously mentioned. I’ve introduced using reactive streams in the core module that is a standard for asynchronous stream processing with non-blocking back pressure that encompasses efforts aimed at runtime environments as well as network protocols.

Class Diagram of reactive stream

Interface Description
Processor<T,R> A Processor represents a processing stage - which is both a Subscriber and a Publisher and obeys the contracts of both.
Publisher<T> A Publisher is a provider of a potentially unbounded number of sequenced elements, publishing them according to the demand received from its Subscriber(s).
Subscriber<T> Will receive a call to the Subscriber.onSubscribe(Subscription) method once after passing an instance of Subscriber to the Publisher.subscribe(Subscriber) method.
Subscription A Subscription represents a one-to-one lifecycle of a Subscriber subscribing to a Publisher.

Below is the new core architecture that include the ReactiveClient abstraction:

Class Diagram with Reactive protocol abstraction

The implementation of the reactive client is contained by the abstract ReactiveClient class. It is based on the RemoteCallProcessor class, which is an implementation of the reactive flow Processor. This acts as both a Publisher by publishing the event to trigger the remote call, and as a Subscriber by receiving the event containing the result of such remote call. Finally, the events’ interactions are coordinated by ReactiveRemoteCaller.

Finally Implements Websocket Module

After introducing a reactive-stream implementation, switching from socket to websocket has been a simple and rewarding coding exercise.

To quickly verify and provide a proof of concept, I’ve decided to use a simple open source micro-framework, Java-WebSocket, providing a websocket implementation which is both simple and effective but my real target is to plug it into Jakarta EE using its Websocket specification. The final part of this article will be dedicated on how to enable whatever Jakarta EE compatible product to be compatible with RMI protocol guaranteeing, at the same time, for your application evolution and a smooth migration.

Class Diagram using WebSocket implementation

As you see from the above class diagram, there are two new handler classes, WSClientConnectionHandler and WSServerConnectionHandler, that manage respectively for client and server the events that come in and out managing, in the same time, their consistency over each call.

Code Examples

Amazingly, the code examples presented above also works essentially in the same way for the websocket. It is enough simply to move from SocketClient to LipeRMIWebSocketClient for the client and from SocketServer to LipeRMIWebSocketServer for the server. That's all!

// Client
try( LipeRMIWebSocketClient client = new LipeRMIWebSocketClient(new URI(format( "ws://localhost:%d", PORT)), GZIPProtocolFilter.Shared)) 
{ 
    // use client
}

// Server
final LipeRMIWebSocketServer server = new LipeRMIWebSocketServer();

Plug LipeRMI Websocket in Jakarta EE using TomEE runtime

JakartaEE is currently the de-facto standard in developing enterprise applications. This makes strategic LipeRMI integration with it.

Jakarta EE is essentially a specification for realizing Java Application Server; this implies that we have to choose an Application Server compliant with such a specification. To experiment with LipeRMI integration, I’ve chosen Apache TomEE, a certified Jakarta EE Web Profile!

Since RMI provides a built-in service broker, it was designed to work without an application server, but with LipeRMI, we have decoupled remote method invocation from service brokering and such separation allows us to plug RMI processing within a JakartaEE container.

ServerEndPoint

Let’s start to integrate the Jakarta WebSocket specification by creating a ServerEndpoint that will manage all websocket sessions opened by clients.

@ServerEndpoint( value = "/lipermi" )
public class WSServerConnectionHandler {

}

The most important behavior to implement is handling the binary message that could be of two different types: a RemoteCall or RemoteReturn depending if the client is requesting a method invocation (RemoteCall) or the incoming message is a callback invocation result (RemoteReturn) .

A simplified sequence diagram that shows the main tasks performed on an incoming websocket message is shown below:

This is an extract of the original code to give you an idea of the complexity in handling request and response over websocket sessions.

@ServerEndpoint( value = "/lipermi" )
public class WSServerSessionHandler {

 @OnMessage
 public void onMessage(ByteBuffer buffer, Session webSocket) {
   try (final ByteArrayInputStream bais = new ByteArrayInputStream(buffer.array());
        final ObjectInputStream input = new ObjectInputStream(bais))
   {

       final Object objFromStream = input.readUnshared();
       final IRemoteMessage remoteMessage = filter.readObject(objFromStream);

       if (remoteMessage instanceof RemoteCall) {
           this.handleRemoteCall( webSocket, (RemoteCall)remoteMessage );
       } else if (remoteMessage instanceof RemoteReturn) {
           remoteReturnManager.handleRemoteReturn( (RemoteReturn) remoteMessage);
       } else {
           log.warn("Unknown IRemoteMessage type");
       }
   }
   catch( Exception ex ) {
       log.warn("error reading message", ex );
   }

 }
}

ClientEndpoint

JakartaEE , from the client side, makes available a ContainerProvider to acquire a WebSocketContainer that allows connecting to a websocket server getting a new session.

WebSocketContainer container = ContainerProvider.getWebSocketContainer();
session = container.connectToServer(this, serverUri);

A sequence diagram that represents a complete remote method invocation flow that puts in evidence the reactive-stream implementation and the tasks involving the JakartaEE websocket client API is shown below:

The diagram above can be split in three macro tasks:

  1. Acquire a remote service proxy, invoking a method that starts a CompletableFuture that will manage the asynchronous request and response
  2. Send a request to the remote call processor and create a subscription to the remote return publisher for the management of the result
  3. Send the binary request over a websocket session and wait for result through the OnMessage websocket handler

Experiments and Evolution

Once we have moved the RMI implementation over websocket and make it compliant with a JakartaEE container, we can imagine evolving the original architecture of RMI into a more modern fashion.

RMI Design Limit

RMI itself was designed implying that the client and server were developed using Java especially because the RMI application data protocol relies on the built-in Java serialization. Nowadays, modern applications privileges Web Client, and with RMI technology, this does not seem achievable also because the Java Applet technology has been deprecated. But a new cutting edge technology that could help us is WebAssembly.

WebAssembly to the Rescue

With WebAssembly, the technological barriers in the browser have been removed. So not only Javascript language can be performed within the browser context, but also all the programming languages whose compiler is able to produce WebAssembly-compliant bytecode (wasm).

Currently, one of the most famous programming languages that generate WebAssembly is Rust, but other more mature languages, such as C# and Swift, are providing WebAssembly generation. But what about Java?

Java to WebAssembly

One of the most interesting projects allowing you to compile Java to WebAssembly is CheerpJ. It also supports Java Swing/AWT and Serialization. I’ve experimented on it and the results are very promising. In fact, I’ve successfully developed a simple chat using LipeRMI and I've deployed the Java client directly inside the browser through CheerpJ.

However, going in-depth with CheerpJ and WebAssembly is out of scope of this article, but is probably a very interesting material for a next one.

Conclusion

I’ve started to migrate a legacy project and it is going fine. Just keep in mind that it requires a lot of effort, but the results are very promising. Moreover, switching over to the websocket protocol opens new unexpected and exciting scenarios.

My idea is to work on the LipeRMI fork to use JSON-based serialization instead of the proprietary Java one. This will allow you, once the application migration will be accomplished, to develop clients with other technology such as JavaScript/React..

I hope this article can be of use for someone that has the same challenge as mine to tackle. In the meanwhile, good programming!

References

About the Author

Rate this Article

Adoption
Style

BT