VOOZH about

URL: https://www.javacodegeeks.com/2025/09/migrating-a-legacy-rmi-application-to-websockets-a-pragmatic-guide.html

⇱ Migrating a Legacy RMI Application to WebSockets — A Pragmatic Guide - Java Code Geeks


Remote Method Invocation (RMI) was once the straightforward way for Java apps to talk across a network. But it’s tightly coupled to the JVM, brittle across NAT and firewalls, and awkward to expose on the public internet. Modern systems prefer WebSockets: a simple, bidirectional, cross-platform channel that works well from browsers, mobile apps, and servers alike (via RFC 6455). This guide walks you through how to migrate from RMI to WebSockets without rewriting your entire system at once. We’ll keep the tone practical, use small, realistic code slices, and point to battle-tested docs along the way.

Why move off RMI?

  1. Connectivity and firewalls. RMI often struggles behind NAT and corporate firewalls and needs custom socket factories or extra plumbing; callbacks are especially painful. WebSockets ride over HTTP(S) and upgrade to a persistent TCP connection, which tends to pass through proxies and load balancers much more reliably.
  2. Interoperability. RMI ties you to Java. WebSockets are language-agnostic: browsers, Node.js, Python, Go, Java—everything speaks it. With subprotocols like STOMP, you can add routing and pub/sub semantics.
  3. Scalability and operations. Long-lived, full-duplex connections with less overhead than HTTP long polling. Modern proxies and cloud load balancers support WebSockets out of the box.

Migration strategy in three phases

The safest path is incremental. You don’t “flip a switch”; you strangle the RMI endpoints behind a WebSocket façade and move features gradually.

Phase 1 — Introduce a WebSocket edge in front of RMI

Keep your RMI services running, but expose a WebSocket gateway that translates JSON messages into RMI calls. New clients (or the parts of your app you’re actively modernizing) connect via WebSocket; the gateway invokes legacy RMI under the hood.

[ Browser / Mobile / New Services ] <--WebSocket--> [ Gateway ] <--RMI--> [ Legacy Services ]

Benefits: no risky big-bang rewrite; you can test path by path.

Phase 2 — Carve out services

As you modernize internals, replace RMI implementations with native WebSocket handlers (or message-brokered services) one by one. The gateway keeps the protocol stable while you swap the backend.

Phase 3 — Retire RMI

Once traffic is fully moved, decommission the RMI registry and stubs. Remove compatibility layers and simplify message models.

Designing the message API (RMI method ⇒ WebSocket message

RMI exposes strongly typed Java interfaces; WebSocket sends text (often JSON) or binary frames. The trick is to codify your methods as messages:

RMI interface (legacy):

public interface OrderService extends Remote {
 Order submit(OrderRequest req) throws RemoteException;
 Order get(String orderId) throws RemoteException;
}

WebSocket message shapes (new):

// Request (client -> server)
{ "type": "Order.Submit", "payload": { "sku": "ABC-123", "qty": 2 } }

// Response (server -> client)
{ "type": "Order.Submitted", "payload": { "id": "o-987", "status": "PENDING" } }

// Query
{ "type": "Order.Get", "payload": { "id": "o-987" } }

// Result
{ "type": "Order.Snapshot", "payload": { "id": "o-987", "status": "PENDING" } }

You can evolve this into a small schema: type, correlationId, payload, and optional errors.

A minimal WebSocket server in Java (Jakarta/JSR-356)

JSR-356 is the standard Java API for WebSockets and is supported in modern Java servers (Tomcat, Jetty, Undertow, etc.). Here’s a dead-simple endpoint you can drop into a servlet container.

import jakarta.websocket.*;
import jakarta.websocket.server.ServerEndpoint;
import com.fasterxml.jackson.databind.ObjectMapper;

@ServerEndpoint("/ws")
public class GatewayEndpoint {
 private static final ObjectMapper MAPPER = new ObjectMapper();

 @OnOpen
 public void onOpen(Session session) {
 // Could auth here (cookies/JWT), start heartbeats, etc.
 }

 @OnMessage
 public void onMessage(String raw, Session session) throws Exception {
 Message msg = MAPPER.readValue(raw, Message.class);
 switch (msg.type()) {
 case "Order.Submit" -> {
 OrderRequest req = MAPPER.convertValue(msg.payload(), OrderRequest.class);
 // bridge to legacy RMI:
 OrderService rmi = RmiLocator.lookup();
 Order order = rmi.submit(req);
 send(session, new Message("Order.Submitted", order, msg.correlationId()));
 }
 case "Order.Get" -> {
 OrderService rmi = RmiLocator.lookup();
 Order order = rmi.get((String) msg.payload().get("id"));
 send(session, new Message("Order.Snapshot", order, msg.correlationId()));
 }
 default -> send(session, Message.error("UnknownType", msg.correlationId()));
 }
 }

 @OnClose
 public void onClose(Session session, CloseReason reason) { }

 private void send(Session s, Message m) throws Exception {
 s.getBasicRemote().sendText(MAPPER.writeValueAsString(m));
 }
}

If you’re already on Spring, its WebSocket+STOMP support gives you topic routing, @MessageMapping handlers, and a built-in broker—handy when transitioning from RPC-style calls to events.

A minimal browser client

<script>
 const ws = new WebSocket("wss://example.com/ws");
 const pending = new Map();

 ws.onopen = () => {
 call("Order.Submit", { sku: "ABC-123", qty: 2 })
 .then(console.log)
 .catch(console.error);
 };

 ws.onmessage = (e) => {
 const msg = JSON.parse(e.data);
 if (msg.correlationId && pending.has(msg.correlationId)) {
 const { resolve, reject } = pending.get(msg.correlationId);
 pending.delete(msg.correlationId);
 msg.error ? reject(msg) : resolve(msg.payload);
 } else {
 // unsolicited push: status updates, notifications, etc.
 console.log("event", msg);
 }
 };

 function call(type, payload) {
 const correlationId = crypto.randomUUID();
 ws.send(JSON.stringify({ type, payload, correlationId }));
 return new Promise((resolve, reject) => pending.set(correlationId, { resolve, reject }));
 }
</script>

Handling authentication, sessions, and security

  • TLS everywhere: use wss:// and terminate TLS at your reverse proxy or cloud load balancer. WebSockets upgrade from HTTP(S), so your existing cert story applies. Modern load balancers like AWS ALB support WebSockets natively.
  • Auth tokens: reuse your HTTP auth (cookies or JWT). Send JWT in the initial HTTP Upgrade request (cookie or Authorization header) and validate during @OnOpen. Spring also has first-class integration when using STOMP + Spring Security.
  • Cross-origin: apply CORS-like checks before accepting a session; for browsers, origin checking still matters even with WebSockets.

Operating WebSockets in production (proxies, timeouts, scaling)

Reverse proxy / LB. NGINX and many others support WebSockets; ensure you forward Upgrade and Connection headers and configure timeouts to avoid idle disconnects. With NGINX, be explicit about proxy timeouts; don’t confuse connect timeout (handshake) with read timeout (idle).

Cloud LBs. AWS Application Load Balancer supports WebSockets and uses the HTTP/1.1 upgrade path; use target groups just like HTTP services.

Sticky sessions? If your server relies on in-memory session state, use session affinity or move state to a shared store so any instance can resume a connection after reconnect.

Backpressure. Browsers don’t give you perfect receive-side backpressure; plan for bufferedAmount, server-side rate limiting, and flow control semantics in your protocol (acks, window sizes). Reactive frameworks help when you own both ends.

Mapping exceptions and versioning

RMI propagates checked exceptions across the wire. Over WebSockets, normalize errors:

{ "type": "Error", "correlationId": "123", "payload": { "code": "Order.NotFound", "message": "No order o-987" } }

Introduce v (version) in your messages early:
{ "type": "Order.Submit", "v": 2, "payload": { ... } }
This lets you evolve payloads without breaking old clients.

Performance notes (what to expect)

  • WebSockets avoid the request/response churn of long polling and typically reduce latency and server CPU at scale. Expect fewer context switches and lower header overhead per message.
  • Batch small updates, prefer binary frames for large payloads, and compress carefully (permessage-deflate) to avoid CPU spikes.

Testing the migration

  1. Contract tests for each message type (round-trip JSON ↔ DTO) to ensure you didn’t change payload shapes inadvertently.
  2. Soak tests through the LB to validate idle timeouts and reconnection behavior.
  3. Chaos tests: drop connections to confirm idempotency of retried messages (your gateway should handle “at-least-once” semantics cleanly).

Example: Bridging a single RMI call

RMI lookup utility in the gateway:

import java.rmi.Naming;

public final class RmiLocator {
 private RmiLocator() {}
 public static OrderService lookup() {
 try {
 // rmi://host:1099/OrderService
 return (OrderService) Naming.lookup(System.getenv("RMI_URL"));
 } catch (Exception e) {
 throw new RuntimeException(e);
 }
 }
}

Converting DTOs:

  • Keep the legacy DTOs, but annotate a Web DTO that mirrors fields and is stable across clients. Use Jackson to map between them; this lets you slowly reshape the public API while the RMI types stay untouched.

Example: Moving from RPC to events (with Spring + STOMP)

If your RMI callbacks pushed updates, map them to topics:

Server:

@Controller
public class OrderController {

 @MessageMapping("/order.submit")
 @SendTo("/topic/orders")
 public OrderSubmitted submit(OrderRequest request) {
 // call legacy or new service
 return new OrderSubmitted(/*...*/);
 }
}

Client (browser):

const sock = new SockJS("/ws-endpoint");
const stomp = Stomp.over(sock);
stomp.connect({}, () => {
 stomp.subscribe("/topic/orders", (msg) => console.log(JSON.parse(msg.body)));
 stomp.send("/app/order.submit", {}, JSON.stringify({ sku: "ABC-123", qty: 2 }));
});

Spring’s guide and reference explain STOMP frames, destinations, and message converters in detail.

Cutover checklist

  • WebSocket gateway deployed behind a proxy/LB that supports Upgrade.
  • Health checks and metrics: connection counts, sends/receives per second, average bufferedAmount, reconnect rates.
  • Heartbeats/pings to detect dead peers; configure proxy/LB idle timeouts accordingly.
  • Error model and versioning documented.
  • Rollback: clients can still speak RMI (or your gateway still routes to RMI) during the early phases.

Common pitfalls (and fixes)

  • 502/101 errors through chained proxies. Ensure both hops preserve Connection: Upgrade and Upgrade: websocket. Validate with curl -i -N -H "Connection: Upgrade" -H "Upgrade: websocket" .... Double proxies are a classic gotcha. Server Fault
  • Serialization differences. Spring’s @MessageMapping doesn’t always reuse your MVC Jackson config; wire a shared ObjectMapper or configure message converters for WS specifically. Stack Overflow
  • NAT traversal assumptions. One reason RMI was painful was callbacks through NAT/firewalls; WebSockets help because the client initiates and keeps the TCP connection open. Still, plan for reconnects and stateless handlers. Oracle Forums

Closing thoughts

The jump from Java-only RPC (RMI) to open, event-friendly WebSockets is less scary when you start with a gateway, freeze your message schema early, and iterate. You’ll gain simpler networking, better interoperability, and a cleaner path to web and mobile clients.

Useful links (deep dives & docs)

Do you want to know how to develop your skillset to become a Java Rockstar?
Subscribe to our newsletter to start Rocking right now!
To get you started we give you our best selling eBooks for FREE!
1. JPA Mini Book
2. JVM Troubleshooting Guide
3. JUnit Tutorial for Unit Testing
4. Java Annotations Tutorial
5. Java Interview Questions
6. Spring Interview Questions
7. Android UI Design
and many more ....
I agree to the Terms and Privacy Policy

Thank you!

We will contact you soon.

👁 Photo of Eleftheria Drosopoulou
Eleftheria Drosopoulou
September 4th, 2025Last Updated: August 26th, 2025
0 371 6 minutes read

Eleftheria Drosopoulou

Eleftheria is an Experienced Business Analyst with a robust background in the computer software industry. Proficient in Computer Software Training, Digital Marketing, HTML Scripting, and Microsoft Office, they bring a wealth of technical skills to the table. Additionally, she has a love for writing articles on various tech subjects, showcasing a talent for translating complex concepts into accessible content.
Subscribe

This site uses Akismet to reduce spam. Learn how your comment data is processed.

0 Comments
Oldest
Newest Most Voted
Back to top button
Close
wpDiscuz