Designing RMI Applications

We present some of the ideas from: Code examples were downloaded from and belong to O'Reilly [2].

1 Sketch a Rough Architecture

  1. Figure out what you are going to build. Do your requirements analysis.
  2. Find a basic use case that will motivate the rough architecture.
  3. Figure out what you can safely ignore for now (often scalability and security).
  4. Determine which design decisions are forced by the environment.
  5. Narrow down the servers to as few as possible.

2 Bank Example

Bank example

3 Two Choices


//One instance of Bank for all clients
class Bank {
  public Money getBalance(Account account) throws RemoteException;
  public void makeDeposit(Account account)
    throws RemoteException, NegativeAmountException;
  public void makeWithdrawal(Account account, Money amount)
    throws RemoteException, OverdraftException, NegativeAmountException;
}

//One instance of Account for each account
class Account {
  public Money getBalance() throws RemoteException;
  public void makeDeposit(Money amount)
    throws RemoteException, NegativeAmountException;
  public void makeWithdrawal(Money amount)
    throws RemoteException, OverdraftException, NegativeAmountException;
}
  

4 Choosing

4.1 Does Each Instance Require Shared Resource?

4.2 How Well Server Scales or Replicates to Multiple Machines?

4.3 Is Single Server Enough?

4.4 How to Handle Multiple Simultaneous Clients?

4.5 Is Code Correct?

4.6 How Fatal is Server Crash?

4.7 How Easy Adding Functionality?

5 Writing the Interface

5.1 Should We Pass Method Objects?

public interface Account extends Remote {
  public Money getBalance() throws RemoteException;
  public void postTransaction(Transaction transaction)
    throws RemoteException, TransactionException;
}

5.2 Pass Objects As Arguments or Use Primitive Values?

5.3 Return Values: Objects or Primitive Values?

5.4 Do Individual Method Calls Waste Bandwidth?

5.5 Is Each Conceptual Operation A Single Method Call?

5.6 Does The Interface Expose The Right Amount Of Metadata?

5.7 Have We Identified A Reasonable Set Of Distributed Exceptions?

6 Building Data Objects

import java.io.*;


public class Money extends ValueObject {
  protected int _cents;

  public Money(Integer cents) {
    this (cents.intValue());
  }

  public Money(int cents) {
    super (cents + " cents.");
    _cents = cents;
  }

  public int getCents() {
    return _cents;
  }

  public void add(Money otherMoney) {
    _cents += otherMoney.getCents();
  }

  public void subtract(Money otherMoney) {
    _cents -= otherMoney.getCents();
  }

  public boolean greaterThan(Money otherMoney) {
    if (_cents > otherMoney.getCents()) {
      return true;
    }
    return false;
  }

  public boolean isNegative() {
    return _cents < 0;
  }

  public boolean equals(Object object) {
    if (object instanceof Money) {
      Money otherMoney = (Money) object;

      return (_cents == otherMoney.getCents());
    }
    return false;
  }
}

7 Server Implementation

import java.rmi.server.*;

public class Account_Impl extends UnicastRemoteObject implements Account {
  private Money _balance;
  public Account_Impl(Money startingBalance)
    throws RemoteException {
    _balance = startingBalance;
  }

  public Money getBalance()
    throws RemoteException {
    return _balance;
  }

  public void makeDeposit(Money amount)
    throws RemoteException, NegativeAmountException {
    checkForNegativeAmount(amount);
    _balance.add(amount);
    return;
  }

  public void makeWithdrawal(Money amount)
    throws RemoteException, OverdraftException, NegativeAmountException {
    checkForNegativeAmount(amount);
    checkForOverdraft(amount);
    _balance.subtract(amount);
    return;
  }

  private void checkForNegativeAmount(Money amount)
    throws NegativeAmountException {
    int cents = amount.getCents();

    if (0 > cents) {
      throw new NegativeAmountException();
    }
  }

  private void checkForOverdraft(Money amount)
    throws OverdraftException {
    if (amount.greaterThan(_balance)) {
      throw new OverdraftException(false);
    }
    return;
  }
} 

8 Server Implementation 2

import java.rmi.server.*;
/*
 The only difference between this and Account_Impl is that
 Account_Impl extends UnicastRemote.
 */
public class Account_Impl2 implements Account {
  private Money _balance;
  public Account_Impl2(Money startingBalance)
    throws RemoteException {
    _balance = startingBalance;
  }

  public Money getBalance()
    throws RemoteException {
    return _balance;
  }

  public void makeDeposit(Money amount)
    throws RemoteException, NegativeAmountException {
    checkForNegativeAmount(amount);
    _balance.add(amount);
    return;
  }

  public void makeWithdrawal(Money amount)
    throws RemoteException, OverdraftException, NegativeAmountException {
    checkForNegativeAmount(amount);
    checkForOverdraft(amount);
    _balance.subtract(amount);
    return;
  }

  /** We must define this function */
  public boolean equals(Object object) {
    // three cases. Either it's us, or it's our stub, or it's
    // not equal.

    if (object instanceof Account_Impl2) {
      return (object == this);
    }
    if (object instanceof RemoteStub) {
      try {
        RemoteStub ourStub = (RemoteStub) RemoteObject.toStub(this);

        return ourStub.equals(object);
      } catch (NoSuchObjectException e) {
        // we're not listening on a port, therefore it's not our
        // stub
      }
    }
    return false;
  }

  /** We must define this function */
  public int hashCode() {
    try {
      Remote ourStub = RemoteObject.toStub(this);

      return ourStub.hashCode();
    } catch (NoSuchObjectException e) {
    }
    return super.hashCode();
  }
    
  private void checkForNegativeAmount(Money amount)
    throws NegativeAmountException {
    int cents = amount.getCents();

    if (0 > cents) {
      throw new NegativeAmountException();
    }
  }

  private void checkForOverdraft(Money amount)
    throws OverdraftException {
    if (amount.greaterThan(_balance)) {
      throw new OverdraftException(false);
    }
    return;
  }
} 

9 Launch Code

public class ImplLauncher {
  public static void main(String[] args) {
    Collection nameBalancePairs = getNameBalancePairs(args);
    Iterator i = nameBalancePairs.iterator();

    while (i.hasNext()) {
      NameBalancePair nextNameBalancePair = (NameBalancePair) i.next();

      launchServer(nextNameBalancePair);
    }
  }

  private static void launchServer(NameBalancePair serverDescription) {
    try {
      Account_Impl newAccount = new Account_Impl(serverDescription.balance);

      Naming.rebind(serverDescription.name, newAccount);
      System.out.println("Account " + serverDescription.name + " successfully launched.");
    } catch (Exception e) {
    }
  }

  private static Collection getNameBalancePairs(String[] args) {
    int i;
    ArrayList returnValue = new ArrayList();

    for (i = 0; i < args.length; i += 2) {
      NameBalancePair nextNameBalancePair = new NameBalancePair();

      nextNameBalancePair.name = args[i];
      int cents = (new Integer(args[i + 1])).intValue();

      nextNameBalancePair.balance = new Money(cents);
      returnValue.add(nextNameBalancePair);
    }
    return returnValue;
  }

  private static class NameBalancePair {
    String name;
    Money balance;
  }
}

10 Building the Client

import java.awt.*;
import java.awt.event.*;
import javax.swing.*;
import java.rmi.*;

public class BankClient {
  public static void main(String[] args) {
    (new BankClientFrame()).show();
  }
}

public class BankClientFrame extends ExitingFrame {
  private JTextField _accountNameField;
  private JTextField _balanceTextField;
  private JTextField _withdrawalTextField;
  private JTextField _depositTextField;
  private Account _account;

  protected void buildGUI() {
    JPanel contentPane = new JPanel(new BorderLayout());

    contentPane.add(buildActionPanel(), BorderLayout.CENTER);
    contentPane.add(buildBalancePanel(), BorderLayout.SOUTH);
    setContentPane(contentPane);
    setSize(250, 100);
  }

  private void resetBalanceField() {
    try {
      Money balance = _account.getBalance();

      _balanceTextField.setText("Balance: " + balance.toString());
    } catch (Exception e) {
      System.out.println("Error occurred while getting account balance\n" + e);
    }
  }

  private JPanel buildActionPanel() {
    JPanel actionPanel = new JPanel(new GridLayout(3, 3));

    actionPanel.add(new JLabel("Account Name:"));
    _accountNameField = new JTextField();
    actionPanel.add(_accountNameField);
    JButton getBalanceButton = new JButton("Get Balance");

    getBalanceButton.addActionListener(new GetBalanceAction());
    actionPanel.add(getBalanceButton);
    actionPanel.add(new JLabel("Withdraw"));
    _withdrawalTextField = new JTextField();
    actionPanel.add(_withdrawalTextField);
    JButton withdrawalButton = new JButton("Do it");

    withdrawalButton.addActionListener(new WithdrawAction());
    actionPanel.add(withdrawalButton);
    actionPanel.add(new JLabel("Deposit"));
    _depositTextField = new JTextField();
    actionPanel.add(_depositTextField);
    JButton depositButton = new JButton("Do it");

    depositButton.addActionListener(new DepositAction());
    actionPanel.add(depositButton);
    return actionPanel;
  }

  private JPanel buildBalancePanel() {
    JPanel balancePanel = new JPanel(new GridLayout(1, 2));

    balancePanel.add(new JLabel("Current Balance:"));
    _balanceTextField = new JTextField();
    _balanceTextField.setEnabled(false);
    balancePanel.add(_balanceTextField);
    return balancePanel;
  }

  private void getAccount() {
    try {
      _account = (Account) Naming.lookup(_accountNameField.getText());
    } catch (Exception e) {
      System.out.println("Couldn't find account. Error was \n " + e);
      e.printStackTrace();
    }
    return;
  }

  private void releaseAccount() {
    _account = null;
  }

  private Money readTextField(JTextField moneyField) {
    try {
      Float floatValue = new Float(moneyField.getText());
      float actualValue = floatValue.floatValue();
      int cents = (int) (actualValue * 100);

      return new PositiveMoney(cents);
    } catch (Exception e) {
      System.out.println("Field doesn't contain a valid value");
    }
    return null;
  }

  private class GetBalanceAction implements ActionListener {
    public void actionPerformed(ActionEvent event) {
      try {
        getAccount();
        resetBalanceField();
        releaseAccount();
      } catch (Exception exception) {
        System.out.println("Couldn't talk to account. Error was \n " + exception);
        exception.printStackTrace();
      }
    }
  }


  private class WithdrawAction implements ActionListener {
    public void actionPerformed(ActionEvent event) {
      try {
        getAccount();
        Money withdrawalAmount = readTextField(_withdrawalTextField);

        _account.makeWithdrawal(withdrawalAmount);
        _withdrawalTextField.setText("");
        resetBalanceField();
        releaseAccount();
      } catch (Exception exception) {
        System.out.println("Couldn't talk to account. Error was \n " + exception);
        exception.printStackTrace();
      }
    }
  }


  private class DepositAction implements ActionListener {
    public void actionPerformed(ActionEvent event) {
      try {
        getAccount();
        Money depositAmount = readTextField(_depositTextField);

        _account.makeDeposit(depositAmount);
        _depositTextField.setText("");
        resetBalanceField();
        releaseAccount();
      } catch (Exception exception) {
        System.out.println("Couldn't talk to account. Error was \n " + exception);
        exception.printStackTrace();
      }
    }
  }
}

11 Serialization

11.1 Using Serialization

11.2 Making a Class Serializable

  1. Implement the Serializable interface.
    1. Add implements Serializable to class definition.
  2. Make sure that instance-level locally defined state is serialized properly.
  3. Make sure that superclass state is properly serialized.
  4. Override equals() and hashCode().

11.2.1 Ensure Serialization of Instance-Level State

  1. Make the variable transient:
    private transient Object myobject[];
    so it will not get serialized (be careful!)
  2. Declare which variables should be stored by defining a special variable:
    private static final ObjectSreamField[] serialPersistentFields =
        { new ObjectStreamField("size", Integer.Type), ... };
  3. Do your own serialization by implementing
    private void writeObject(java.io.ObjectOutputStream out) throws IOException;
    private void readOject(java.io.ObjectInputStream in) 
           throws IOException, ClassNotFoundException;
    these methods can invoke defaultWriteObject() to invoke default serialization on the non-transient members.

11.2.2 Ensure Superclass State is Handled Correctly

11.2.3 Override equals() and hashCode()

12 Threading

12.1 Ensure Data Integrity

import java.rmi.*;
import java.rmi.server.*;

public class Account_Impl extends UnicastRemoteObject implements Account {
  private Money _balance;

  public synchronized Money getBalance()
    throws RemoteException {
    return _balance;
  }

  public synchronized void makeDeposit(Money amount)
    throws RemoteException, NegativeAmountException {
    checkForNegativeAmount(amount);
    _balance.add(amount);
    return;
  }

  public synchronized void makeWithdrawal(Money amount)
    throws RemoteException, OverdraftException, NegativeAmountException {
    checkForNegativeAmount(amount);
    checkForOverdraft(amount);
    _balance.subtract(amount);
    return;
  }
}

12.2 Client Maintains Lock

import java.rmi.*;
import java.rmi.server.*;

public class Account2_Impl extends UnicastRemoteObject
  implements Account2 {
  private Money _balance;
  private String _currentClient;

  public Account2_Impl(Money startingBalance)
    throws RemoteException {
    _balance = startingBalance;
  }

  /** The client can use this to get a lock on the account */
  public synchronized void getLock()
    throws RemoteException, LockedAccountException {
    if (false == becomeOwner()) {
      throw new LockedAccountException();
    }
    return;
  }

  /** The client can use this to release a lock on the account */
  public synchronized void releaseLock() throws RemoteException {
    String clientHost = wrapperAroundGetClientHost();

    if ((null != _currentClient) && (_currentClient.equals(clientHost))) {
      _currentClient = null;
    }
  }

  private boolean becomeOwner() {
    String clientHost = wrapperAroundGetClientHost();

    if (null != _currentClient) {
      if (_currentClient.equals(clientHost)) {
        return true;
      }
    } else {
      _currentClient = clientHost;
      return true;
    }
    return false;
  }

  private void checkAccess() throws LockedAccountException {
    String clientHost = wrapperAroundGetClientHost();

    if ((null != _currentClient) && (_currentClient.equals(clientHost))) {
      return;
    }
    throw new LockedAccountException();
  }

  private String wrapperAroundGetClientHost() {
    String clientHost = null;

    try {
      clientHost = getClientHost();
    } catch (ServerNotActiveException ignored) {
    }
    return clientHost;
  }

  public synchronized Money getBalance()
    throws RemoteException, LockedAccountException {
    checkAccess();
    return _balance;
  }

  public synchronized void makeDeposit(Money amount)
    throws RemoteException, LockedAccountException, NegativeAmountException {
    checkAccess();
    checkForNegativeAmount(amount);
    _balance.add(amount);
    return;
  }

  public synchronized void makeWithdrawal(Money amount)
    throws RemoteException, OverdraftException, LockedAccountException, NegativeAmountException {
    checkAccess();
    checkForNegativeAmount(amount);
    checkForOverdraft(amount);
    _balance.subtract(amount);
    return;
  }

  private void checkForNegativeAmount(Money amount)
    throws NegativeAmountException {
    int cents = amount.getCents();

    if (0 > cents) {
      throw new NegativeAmountException();
    }
  }

  private void checkForOverdraft(Money amount)
    throws OverdraftException {
    if (amount.greaterThan(_balance)) {
      throw new OverdraftException(false);
    }
    return;
  }
} 

12.3 Using a Lock Expiry Thread

import java.rmi.*;
import java.rmi.server.*;
/*
 Has timer-based lock management on server-side
 */

public class Account3_Impl extends UnicastRemoteObject implements Account3 {
  private static final int TIMER_DURATION = 120000; // Two minutes
  private static final int THREAD_SLEEP_TIME = 10000; // 10 seconds

  private Money _balance;
  private String _currentClient;
  private int _timeLeftUntilLockIsReleased;

  public Account3_Impl(Money startingBalance)
    throws RemoteException {
    _balance = startingBalance;
    _timeLeftUntilLockIsReleased = 0;
    new Thread(new CountDownTimer()).start();
  }

  public synchronized Money getBalance()
    throws RemoteException, LockedAccountException {
    checkAccess();
    return _balance;
  }

  public synchronized void makeDeposit(Money amount)
    throws RemoteException, LockedAccountException, NegativeAmountException {
    checkAccess();
    checkForNegativeAmount(amount);
    _balance.add(amount);
    return;
  }

  public synchronized void makeWithdrawal(Money amount)
    throws RemoteException, OverdraftException, LockedAccountException, NegativeAmountException {
    checkAccess();
    checkForNegativeAmount(amount);
    checkForOverdraft(amount);
    _balance.subtract(amount);
    return;
  }

  private void checkAccess() throws LockedAccountException {
    String clientHost = wrapperAroundGetClientHost();

    if (null == _currentClient) {
      _currentClient = clientHost;
    } else {
      if (!_currentClient.equals(clientHost)) {
        throw new LockedAccountException();
      }
    }
    resetCounter();
    return;
  }

  private void resetCounter() {
    _timeLeftUntilLockIsReleased = TIMER_DURATION;
  }

  private void releaseLock() {
    if (null != _currentClient) {
      _currentClient = null;
    }
  }

  private String wrapperAroundGetClientHost() {
    String clientHost = null;

    try {
      clientHost = getClientHost();
    } catch (ServerNotActiveException ignored) {
    }
    return clientHost;
  }

  private void checkForNegativeAmount(Money amount)
    throws NegativeAmountException {
    int cents = amount.getCents();

    if (0 > cents) {
      throw new NegativeAmountException();
    }
  }

  private void checkForOverdraft(Money amount)
    throws OverdraftException {
    if (amount.greaterThan(_balance)) {
      throw new OverdraftException(false);
    }
    return;
  }

  /** The expire thread */
  private class CountDownTimer implements Runnable {
    public void run() {
      while (true) {
        try {
          Thread.sleep(THREAD_SLEEP_TIME);
        } catch (Exception ignored) {
        }
        synchronized (Account3_Impl.this) {
          if (_timeLeftUntilLockIsReleased > 0) {
            _timeLeftUntilLockIsReleased -= THREAD_SLEEP_TIME;
          } else {
            releaseLock();
          }
        }
      }
    }
  }
} 

12.4 Minimize Time in Synchronized Blocks.

12.5 Be Careful When Using Container Classes.

import java.util.*;
public synchronized void insertIfAbsent(Vector vector, Object object){
  if (vector.contains(object)) {
    return;
  } 
  vector.add(object);
}

12.6 Use Containers To Mediate Inter-thread Communication.

13 Testing

13.1 Test Strategy

  1. Build tests objects that test unit functionality, e.g., one function.
  2. Build aggregate tests for entire use case.
  3. Build threaded tester that does many of these, in sequence.
  4. Build a container that launches many testers. Simulate users.
  5. Build a reporting mechanism.
  6. Run many tests.
  7. Profile performance using tester containers.
Note:

I realize that these instructions are overkill for your typical problem set project. However, any real-world project will likely be ten times larger and involve five times as many developers. Unit testing and automated testing are indispensable for any real project. Frequent automated testing forms the basis of all major software development companies' culture. In fact, many of them have nightly builds/tests. If any bugs are found then fixing it becomes the developer's first priority.

14 Naming Services

Naming
Note:

LDAP provides an excellent example of the kind of sophisticated naming service that will likely be needed to replace the rmiregistry's limited search ability. If we envision a world of different remote objects offered by different companies then the need for this type of service is obvious. Web services have been trying to provide a solution to this same problem via the use of UDDI and WSDL (later in class).

15 RMI Garbage Collection

16 RMI Logging

16.1 Specialized Logs

17 Security Policy

"Making a distributed system secure is a mindnumbingly difficult task."—William Grosso

17.1 Security Manager Permissions

17.2 The Security Manager

18 HTTP Tunneling

Tunnel
  1. Contact directly using JRMP.
  2. Make direct HTTP connection to server, encapsulate method call in HTTP request.
  3. Assume firewall is proxy server, ask it to forward the request to appropriate port on server. Firewall forwards request as HTTP request.
  4. Connect to port 80 on server machine and send request to URL beginning with /cgi-bin/java-rmi.cgi. Hope request gets forwarded to proper port on server machine.
  5. Connect to port 80 of firewall machine and send request to URL beginning with /cgi-bin/java-rmi.cgi. Hope request gets forwarded to server.

URLs

  1. Java RMI, http://www.amazon.com/exec/obidos/ASIN/1565924525/multiagentcom/
  2. O'Reilly, http://www.oreilly.com/catalog/javarmi/
  3. wikipedia:Adapter_pattern, http://www.wikipedia.org/wiki/Adapter_pattern
  4. wikipedia:LDAP, http://www.wikipedia.org/wiki/LDAP

This talk available at http://jmvidal.cse.sc.edu/talks/rmidesign/
Copyright © 2009 José M. Vidal . All rights reserved.

17 March 2004, 09:42AM