April 23, 2014
Hot Topics:
RSS RSS feed Download our iPhone app

A simple four-tier solution using RMI and LiveConnect

  • December 21, 1998
  • By Jason Bloomberg
  • Send Email »
  • More Articles »

Building a four-tier Web solution is not as difficult as it sounds. Two of today's Internet technologies make piecing together a scalable, robust Web interface to a database a simple matter of assembling small, customized elements. All the hard work goes on behind the scenes.

The two technologies I am talking about are Java's Remote Method Invocation (RMI), and Netscape's LiveConnect. RMI allows you to access the methods of objects on remote servers as though they were objects on your local machine. LiveConnect is the Java-to-JavaScript communication built into the Netscape Enterprise Server. (LiveConnect is also built into today's browsers, but I will be talking about server-side LiveConnect.)

What is a four-tier solution?

Before I get into the how-to, let's talk about what a four-tier solution is, and why you might need one. Two-tier solutions, known as client/server, are not scalable and require fat clients: each solution typically must have its own proprietary client, and each server can only handle so many clients.

Three-tier solutions put a Web server between the database and the client. Now the client is a Web browser -- a thin client -- which means that the client is free, and it's already on user's computers. However, this solution is still not scalable, because Web servers can only handle so many hits at a time.

Enter the four-tier solution. Put a piece of middleware, typically called an application server, between the Web server and the database. Now you can add additional Web servers as traffic grows, providing a scalable solution. In addition, the application server can talk to several sources of data, be they databases, mainframes, or other legacy systems.

There are now many commercial four-tier solutions on the market, which typically cost $50,000 and up, and also require a steep learning curve. However, it is not necessary to spend such money and time to set up a simple four-tier solution. Here's how to get started on creating your own custom four-tier solution.

How to build it

My application server talks to an object called a DatabasePeer, which uses Java Database Connectivity (JDBC) to make requests of the database. When I perform a query on the database, I get back a result set that is simply one or more rows out of the database. I want to put these rows into a two-dimensional JavaScript Array on the Web server, so I have the DatabasePeer populate a Java object I call a Table. Think of a Table as a two-dimensional array of variable size (which in Java is actually a Vector of Vectors). Here's the framework for the Table object. The DatabasePeer populates the Table by calling its setElement method. My application server will then pass the Table to the Web server.

import java.util.*;

public class Table extends Vector
	{
	// a table is a Vector of Vectors which will contain all the elements 
	// in a ResultSet returned from the database, stored as Strings

	public int getWidth ()
		{
		// returns how many fields are returned by the database query
		}

	public int getLength ()
		{
		// returns how many rows are returned by the database query
		}

	public String getElement (int i, int j)
		{
		// returns a particular element of the Table
		}

	public void setElement (int i, int j, String val)
		{
		// allows the DatabasePeer to set elements in the table
		}

	}
I use RMI to make my application server available remotely on the Web servers. The application server must extend the UnicastRemoteObject class and implement an interface that extends the Remote class. By incorporating these two classes included in the RMI package, I automatically build in all the code necessary to create the local (skeleton) and remote (stub) pieces of the application server, and to handle all the communication between them. You can see from the application server code that there is little more to this code than creating an instance of the Table class and calling one of the DatabasePeer's methods to populate data into the Table.

import java.rmi.*;
import java.util.*;

public interface AppServerInterface extends Remote
	{
	// required by RMI
	public Table getTable (String arguments) throws RemoteException;
	}

import java.io.*;
import java.util.*;
import java.rmi.*;
import java.rmi.server.*;

public class AppServer extends UnicastRemoteObject implements AppServerInterface
	{
	static DatabasePeer peer;
	// all you have to know about DatabasePeer here is its 
	// fetch () method returns a Table

	public synchronized Table getTable  (String arguments)
		{
		Table t = new Table ();
		t = peer.fetch (arguments);
		return t;
		}
	}
The Web server needs to have a class that can do two things: connect to the application server and get the Table. I call this class Query. Query's constructor looks up the remote application server in the RMI registry (all provided with the RMI code in Sun's Java JDK, or Java 2). Query's getTable method then gets the Table object from the application server and casts it back to an instance of the Table class. (Behind the scenes, RMI is serializing the Table so that it can send it as a stream of bytes from a socket on the application server machine to sockets on one or more Web server machines. Fortunately, all the low-level communications are handled automatically.)

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

public class Query
	{
	AppServerInterface remoteAppServer = null;
	Table t = new Table ();
	String name = "rmi://rmi.domain.com/AppServer";

	public Query ()
		{
		// constructor handles RMI lookup
		try
			{
			remoteAppServer = (AppServerInterface) Naming.lookup (name);
			}

		catch (RemoteException e)
			{
			System.err.println (e);
			}
		}

	public synchronized Table getTable (String arguments)
		{
		try
			{
			t =  (Table) remoteAppServer.getTable (arguments);
			}
		catch (RemoteException e)
			{
			System.err.println (e);
			}
		}
	}
The Web server uses Server-Side JavaScript (SSJS) to instantiate the class Query (thus calling its constructor code) and then places the reference to the Query in SSJS's project object. Here's a look at the SSJS code used to build a Web page. The project object is shared by all the clients of the SSJS application and can exist as long as the application is running. Once the instance of Query is stored in the project object, its methods can be called from SSJS. In particular, we can call Query's getTable method, which retrieves a Table from the application server.

<server>
function RMIconnect ()
	{
	// instantiates Query and places its reference in project object
	result = "Connection already exists";
	if (project.connection == null)
		{
		project.connection = new Packages.Query ();
		result = "Connection created";
		}
	return result;
	}

function tableObject (arguments)
	{
	// JavaScript object constructor which places data from database 
	// into JavaScript Array
	thistable = project.connection.getTable (arguments);

	this.width = thistable.getWidth ();
	this.length = thistable.getLength ();
	this.element = new Array (this.length);

	for (i = 0; i < this.length; i++)
		{
		this.element[i] = new Array (this.width);
		for (j = 0; j < this.width; j++)
			this.element[i][j] = "" + thistable.getElement (i, j);
		}
	return this;
	}

// the following code goes directly on the Web page
project.lock ();
write (RMIconnect ());
project.unlock ();

arguments = [a string describing which data we want];

// the following assignment actually executes a query from the database
mytable = new tableObject (arguments);

// one example of how to display the data on the Web page
for (var i = 0; i < mytable.width; i++)
	for (var j = 0; j < mytable.length; j++)
		write (mytable.element (i, j));
</server>
The secret to the SSJS code is that the Table is represented as a SSJS custom object (called tableObject) that is itself instantiated on the Web page. The tableObject constructor defines SSJS methods that call the corresponding Java methods in Query. In my example, for instance, "element" is a property of a tableObject that is a two-dimensional array. It contains the elements of the Table exposed by Table's getElement method. Once the Web page calls
new tableObject ()
, that Web page can refer to its tableObject instance's methods, which in turn call Query's methods.

The control flow

All right, I've presented the pieces, now let's put them all in order. Here's the blow-by-blow on what really happens when a user loads a Web page.

First, the user hits the Web page. (Nothing happens until a user loads a page.) The Web server sees if project.connection (where Query is stored) is null. If it is, it instantiates the object Query, which looks up the application server in the RMI registry. The Web server then stores a reference to the Query instance in the project object.

Second, the Web server executes the statement:

mytable = new tableObject (arguments);

The string "arguments" identifies what data we want from the database (you can have more than one argument here, but I suggest you use only strings). SSJS creates a new custom object by calling the getTable method of Query, which is stored in the project.connection object. The getTable method of Query, in turn, calls the getTable method of the application server via RMI. This final getTable method executes the database access method in the DatabasePeer, which executes the SQL (or other data access code) that obtains the data from the database. The DatabasePeer puts the data into a Table, which it returns to the application server, which passes it on to Query.

Now that the Table, which contains the data from the database, is in the Web server's project object, the SSJS on the Web page can call the methods of the custom SSJS object tableObject, without having to instantiate Query again. In particular, I can write

mytable.element (i, j)
, and I get the
i x j
value out of the Table instance returned from the database.

Conclusion

It doesn't matter how many Web servers you have. Put the SSJS code and the Query class on a Web server, and it can talk to the application server. Hence this solution is scalable. Furthermore, I left the DatabasePeer up to you. Does it talk JDBC to Oracle? Does it talk CORBA to an object broker? It doesn't matter. The application server will work the same way as long as the DatabasePeer knows how to populate the values in a Table.

My Table is a Vector of Vectors, essentially a two-dimensional array with flexible dimensions. Could your Table be more sophisticated? Sure. SSJS is very string-friendly, so I recommend that the elements of the table be strings, but other data types are possible.

Can the application server do more than pass along Tables? Of course. You can build any methods you like in the application server and expose them in the Web server just as we exposed the getTable method. I'm building one now that retrieves localized text translations out of a resource file, sends requests to an LDAP directory server, and sends e-mail.

Is there more to it? Again, of course. This example is single-threaded; it can handle only one user request at a time. To be truly scalable, the application server would have to be multi-threaded, allowing it to handle several simultaneous requests, each one of which accessing a separate database connection. In addition, this example is stateless: it forgets who a user is after a request is complete, and it also forgets what data was just requested. If an identical request comes through, it happily makes the same request of the database, oblivious to the fact that it just made the same request.

Nevertheless, if you understand this article, then you can build your own four-tier solution, from scratch, from the ground up. It won't do everything the $50,000 products will, but it won't cost an arm and a leg, either.

Resources

Sitemap | Contact Us

Rocket Fuel