A roadmap to flexibly configurable apps

Give your client/server programs a variety of user interfaces and access methods

Any program that processes user requests can be viewed as a client and a server. The client consists of feature code, UI code, and a UI layout. The server consists of server code and usually a database. UI code is responsible for acquiring user input and presenting user output. Feature code is the UI-independent part of the client; it fulfills user-requested transactions by invoking methods on one or more servers. Server code does the real work, such as managing a bank account, so it is often called the program's business logic.

You can configure these code pieces -- UI code, UI layout, and so forth -- in different ways across nodes. Figure 1 shows two common approaches. The upper part of Figure 1 shows one approach: The client code is an app on a desktop machine; the UI layout is created using Swing; and the server code is part of the app. (The term app is used here to mean an applet or an application.)

The bottom part shows another approach: The UI layout is HTML running in a browser on a desktop machine; the UI code and feature code are in a servlet running on a Web-server machine; and the server code and database run on a server-application machine. (Note: The term servlet derives from the fact that it runs in a Web server, not that it contains server code. Although a servlet can contain server code, its main function is UI-related -- namely, it interfaces to browsers.)

Figure 1. Two common program configurations

When you first start to develop a program, these code pieces often correspond one to one, so there is no strong motivation to keep them distinct. For instance, developers often intermix UI code and feature code. But as a program evolves, you may need to configure its pieces in different ways, support multiple UI styles, or provide features that use multiple servers. So keeping these pieces distinct from day one can provide great benefits down the road. However, that is easier said than done, for the following reasons:

  • A servlet gets and presents UI data using code that's different from a client app's code.

  • A client app supports a single user, whereas a servlet supports multiple users, one per servlet thread.

  • Extra machinery needs to be written in clients and servers to allow the use of multiple communication protocols.

  • The sheer complexity of delivering fast, robust, distributed programs can keep you too busy to attempt to keep the code pieces distinct.

This article presents guidelines on how to develop distributed programs that are flexibly configurable, fast, and robust. There are two levels of guidelines. The general guidelines help you effectively organize your program and, as a side effect, enable you to take full advantage of the Servit package (see Resources). The remaining guidelines explain how to use the Servit package. The main benefits of this server invocation tool are:

  • It can start multiple servers and the Remote Method Invocation (RMI) machinery from a single command line.

  • It takes care of the detailed machinery of using a server, like locating and instantiating server objects.

  • It helps you share feature code among servlets, applets, and applications.

  • It lets you define your server configuration through properties set outside your code. For example, during testing you might want to have an in-process server to facilitate debugging, but then use an RMI server when you release your program.

  • It optimizes server-object use by detecting session-stateless servers and by maintaining connection pools within multiple server-object clients (such as servlets).

  • It provides a framework for recovering from network errors.

Developing servers

Granularity of server methods

Accessing a "small" method in a remote server takes milliseconds rather than microseconds. Thus, you should define high-level server methods. If a single server can perform a user transaction, you should probably create a single-server method to do the work of this transaction. If the transaction calculates a number of output items, you should define a class that contains those items and return an object of that class, rather than using individual accessor methods (for example,

getAccountBalance

) to retrieve each output item.

Modifiable server state

Servers are usually multithreaded so that multiple client requests can be processed in parallel. If your server code can modify fields in a server thread, you must synchronize the code sections that use the modifiable state. Obviously, synchronization reduces the degree of possible client parallelism. So you should define a modifiable server state only when necessary. For example, a stock price monitor needs to maintain a database of those who register to receive stock price changes. On the other hand, servers that manipulate client-specific data, like client bank accounts, could often be developed without a modifiable state.

Normally, you would have to use synchronized methods or blocks to achieve any needed thread synchronization. But if your servers are accessed in process and only from servlets, a second synchronization mechanism is available to you.

Session state

You need to decide how the session state is managed. You essentially have two choices as to how to organize your server. It can be

  • Session stateless. This kind of server receives the client state only from method arguments and does not cache any data in server fields during a transaction. Thus, two client transactions executing in parallel can safely use the same server object. (In RMI terms, this means that all clients can perform operations on the server object returned by Naming.lookup; the clients do not each acquire a unique server object.)

  • Session stateful. This kind of server maintains the client state in the server object's fields. Servit will treat a server as session stateful if it has a public Server createSession() method. That method's job is to return a unique server object. Its code can be a simple return new ServerImpl();. The reference object used with createSession is called a factory object.

Loosely speaking, there are two types of session state:

  • Internal state. If a server object contains just an internal state, the only issue is the transaction integrity. That is, each client transaction needs its own server object so that two client transactions executing in parallel do not step on each other's state.

  • Identifying state. If a server object contains an identifying state, even a single user could need to access multiple server objects. For example, a user could need to access two bank account objects, one for checking and one for savings. A client inserts an identifying state into a server object after constructing it because there is no way to specify an identifying state while the server object is constructed. The bank account case might incorporate a setAccount method:

    Servit handle = factoryBank.startSession();
    Bank acct = (Bank)handle.getServer();
    acct.setAccount("Acct#");

Session-stateless servers initialize faster and use fewer resources. If your server can be coded this way, the extra work of maintaining all the states in arguments and local variables may well be worth it.

Server-access methods

This section describes how to write servers that can be accessed in process, by RMI, and by sockets. For in-process access, server objects are constructed within the client's Java Virtual Machine. RMI access means that the client can invoke methods on remote server objects. With a socket-based server, the client and server communicate via application-defined socket messages. For example, see the code in

Broker*.java

in the Servit kit's demo (see

Resources

).

There are some general rules that you must follow. First, you must create a public interface named Server.It should contain declarations of all the client-accessible methods of your server. Also, any method that passes or returns a server object should specify Server (and not ServerImpl). You must also create a public class that implements Server -- this is the class that actually contains your server code. It must be named ServerImpl.

The remaining rules are access-method specific. If you follow all these rules, your server can be accessed all three ways.

  1. For RMI access: Your Server interface must have an extends java.rmi.Remote clause. Each of its methods must have a throws java.rmi.RemoteException clause. Your Server implementation class must extend RemoteObject or contain equivalent logic. The two main ways of doing this are:

    • Declaring the class with an extends java.rmi.server.UnicastRemoteObject clause and defining a no-argument constructor that has a throws java.rmi.RemoteException clause. That will create a permanent RMI server.

    • Declaring the class with an extends java.rmi.activation.Activatable clause and defining a two-argument constructor that has a throws java.rmi.RemoteException clause. The constructor's signature should be Server(ActivationId id, MarshalledObject data), and it should start with super(id, 0). That will create an activatable RMI server, which requires Java 2.

    You may also need to specify a security manager. (See the discussion of StartServers on page 5 for more information.) If your server does not need to download RMI stub files (for example, if it does no RMI callbacks to a client), you don't need to specify a security manager. If your server does not use any external resources, such as files or custom sockets, you can specify RMISecurityManager. If you need to do downloading and use external resources, you need to implement and specify your own security manager.

    Finally, you must run rmic ServerImpl to create ServerImpl_Stub and ServerImpl_Skel, the server class's RMI stub and skeleton classes. For more background on these steps, see the link to the RMI tutorial and the link to Java 2's remote object activation tutorials in Resources.

  2. For in-process access: If you follow the RMI-access rules, your Server implementation class's rmic-created stub class needs to be accessible via the client machine's class path. Additionally, if your Server implementation class extends java.rmi.activation.Activatable, you must define a no-argument constructor that starts with super(null, 0).

  3. For socket access: You must write some additional code, namely the ServerSocketStub and ServerSocketSkel classes (see the link to the Servit documentation in Resources for details). In effect, these classes implement what RMI does for you automatically. Conversely, these classes give you complete control over how arguments and output values are manipulated, as well as complete control over socket settings.

Developing clients

This section describes how to create servlets and single-user client apps. Note that the UI-control structure of an app is very different from the UI-control structure of a servlet-based client. For example, an app has a top-level class that defines the app's UI and explains how to dispatch to each menu item's code. Menu-item code in turn displays the UI layout of its feature. Finally, a feature's UI code is invoked when the user clicks on a button like OK. In addition, a client implemented by means of servlets has no such top-level class. Instead, hyperlinks and HTML pages replace the menu system and the menu-item code. Finally, a feature's UI code (for example, the servlet) is invoked when the user clicks on a button like Submit in an HTML page.

You should organize your client such that it has a utility class that consists of the utility methods needed by multiple features, and a field containing a Servit object for each server used by your client. It should also have a class for each feature. The class should be shared by all the UI styles implemented for the feature, and it should contain a constructor that connects to each server needed by that feature. The class should also contain a work method that calls the public server methods needed by that feature (see the first rule under "Server-access methods" on page 2). It should avoid transmitting unnecessary fields when objects are passed to and from remote servers, by declaring such fields transient. It can be named Feature only if that is not the name of a server interface. (The Servit demo sidesteps this potential collision by naming its feature classes DoFeature -- namely, DoCalcRet and DoGetPort.)

Finally, the client should contain a UI-code class for each style of UI you want a feature to support. Each such class should acquire the feature's input fields and store them in a feature object (or store them in locals and pass them all to the work method). It should also call the feature's work method, and process the error or output data returned by the feature's work method.

Figure 2 shows how these rules were applied within the Servit kit's demo.

Figure 2. Organization of the Servit demo's client

Applications and applets

To create a single-user client that can be run as an application or applet, first define a top-level class, like

retire.java

, that contains an

extends java.applet.Applet

clause. The class should have a

main

method that creates a frame for the applet, creates an instance of the applet, adds the applet to the frame, and calls

init

and

start

. It should also have an

init

method that

1 2 3 Page 1
Page 1 of 3