DISTRIBUTED OBJECT TECHNOLOGIES COMPARED:
A REMOTE OBJECT PERSISTENCE MECHANISM
by
Dare Obasanjo
and
Sanjay Bhatia
Client-server computing is an extension of the division of programming tasks into modular pieces which spawned object oriented programming and component programming taken to another extreme. In client-server computing, an architecture is created that allows a certain amount of work to be split amongst disparate processes on one or more machines with some means of communication between the processes being established. Typical client-server architecture consists of a “client” process which requests services and a “server” process which satisfies requests for services. The client and server processes are traditionally on different machines but can also exist as separate processes on a single machine.
Client-server programming evolved from file sharing applications where a client downloads files from a shared location and then the user jobs interacts with the downloaded file. File sharing architectures work if shared usage and the volume of data transfer is low but have difficulty scaling to a large number of users. Such file sharing architectures were soon superseded by client-server architectures. Early client-server architectures replaced the file server with a relational database server which to replace the file server. Transaction times were improved because instead of transferring entire files every time the client made a request, the relational database management system (RDBMS) could directly answer the client’s request and only send the data that was needed. Thus providing a query response rather than complete file transfer reduced network traffic, also multiple users updating a single data file could now be handled in a more effective manner.
Unfortunately two-tier client-server architectures consisting of a user system (usually a GUI) directly communicating with a database system has difficulty dealing with a large number of users because the database serves usually maintain a separate database connection for each client. Maintaining a separate connection for each client means that the server runs out of resources once the number of connected clients reaches a certain ceiling. Also two-tier architectures often lead to vendor lock-in because clients would create specialized stored procedures that improved the performance of database queries but were not portable between database systems. The solution to this problem was the three-tier architecture where a middle-tier was placed in between the client and the RDBMS. The middle-tier provides business logic as well acting as a communication broker between the client and the server. With a three-tier system it is no longer necessary to have a single database connection for each user and instead database connections can be pooled and shared amongst the different users. Another advantage of the three-tier architecture is that it is more portable as presentation logic, business logic, and data-access logic are all kept separate. Distributed object technologies are a popular way to implement three-tier client-server architectures because they enhance the maintainability of such systems and are also interoperable between platforms and/or languages.
PersistableRemoteObject has the following methods.
getCreationID()
Obtains the value of the creation ID for the object. When a PersistableRemoteObject is sent to a client, a unique ID is generated which is used to identify the client that instantiated the object and is currently using it.
getObjectID()
Obtains the value of the object ID. This is a unique identifier used to locate the object in the database, it is similar to a primary key in database parlance.
getXMLRepresentation()
Generates an XML representation of the Object. This XML
representation is what is stored in the database instead of the actual object binary.
loadFromXML(xmlString)
Populates the data/attributes in the object from an XML string.
setCreationID(creation_id)
Sets the value of the creation ID.
setObjectID(object_id)
Sets the value of the object ID.
The PersistableRemoteObjects are stored in a relational database table with the following metadata.
|
Name |
Datatype |
Constraints |
|
object_id |
varchar(100) |
primary key |
|
xml_representation |
varchar(7250) |
not null |
|
object_type |
varchar(500) |
not null |
|
Locked |
Bit |
not null |
|
lock_owner_id |
varchar(20) |
RemoteObjectFactory has the following methods:
createObject(object_id, object_metadata):
Checks to see if the object ID is not in use and if it isn't will create a new instance of the class, which must be confirmed to be a PersistableRemoteObject, and then stores that freshly minted object in the remote store before returning it to the calling function.
isLocked(String object_id):
Checks to see if an object is locked. The Remote Object Persistence Mechanism supports a simple transactional system where a specific client can lock an object thus preventing updates to the object’s persistent state on the server until unlocked by the client that initiated the lock. While an object is locked in can still be loaded, and thus shared, by other clients but it can only accept updates from one client.
loadObject(object_id, object_metadata)
Obtains an object from the remote object store, if it exists.
lock( object_to_lock)
Prevents any updates to the object until the client that initiated the lock unlocks the object.
storeObject(PersistableRemoteObject pro)
This stores the specified PersistableRemoteObject in the remote object store. If the object has never been stored before a new entry is created for it and if it already exists then it is updated. This method fails if the object is currently locked by another client.
unlock(object_to_unlock)
Releases a lock placed on an object, if its creation ID matches that of the client that originally locked the object.
With the above framework it is possible to create rich, multi-user, networked applications such as workflow software, email programs, scheduling software, group conferencing programs, group scheduling software, shareable multimedia applications, etc. For our comparison of the various distributed object technologies we decided to implement an online jargon file (dictionary) that is viewable by several users at once who can add and modify entries stored on a remote machine.
. We chose to use standards compliant parsers for XML manipulation to take advantage of performance as well as support Document Type Definitions. Document Type Definitions are declarative frameworks of rules that define how to validate XML. For our word component, the XML representation of an entry in the jargon file would look like this:
<?xml version="1.0" ?>The DTD for the word component looks like this:
This declares the word XML contains a word tag with a name attribute inside of which are embedded one or more definition tags. These definition tags contain no attributes, but wrap the definition data. Technologies like DTD add high-level verification to our framework which makes it more secure and reliable. If bad data somehow got into the database or one of the data objects, the system would reject it since it would not pass DTD verification.
The CORBA implementation of ROPEM was implemented in Java because writing CORBA code in Java is less complex than in most other languages for which CORBA bindings exist. Below is the interface definition for the ROPEM which is described in a file called RemoteObjectStore.idl
module RemoteObjectStore{
exception XMLLoadException{};
interface PersistableRemoteObject{
//the time the object was created on the server, used to
determine which
//of the object instances this one is
attribute string creationID;
//unique object identifier
attribute string objectID;
//Generates an XML representation of the Object
string getXMLRepresentation();
//Populates the data in the object from an XML string
void loadFromXML(in string xmlString) raises
(XMLLoadException);
};
exception ObjectAlreadyExistsException{};
exception ObjectNotFoundException{};
exception ObjectLockedException{};
interface RemoteObjectFactory{
//Checks to see if the object ID is not in use and if it isn't
will
//populate the object passed in with the data for the class
void createObject(in string objectID, inout
PersistableRemoteObject pro)
raises (ObjectAlreadyExistsException);
//checks to see if the object is locked
boolean isLocked(in string objectID) raises
(ObjectNotFoundException);
//Obtains an object from the remote object store.
void loadObject(in string objectID, inout
PersistableRemoteObject pro)
raises (ObjectLockedException,
ObjectNotFoundException);
//This stores the specified PersistableRemoteObject in the
remote object store
void storeObject(in PersistableRemoteObject pro)
raises (ObjectLockedException);
//Prevents any updates to the object until the object is
unlocked
void lock(in PersistableRemoteObject pro)
raises (ObjectLockedException,
ObjectNotFoundException);
// Releases a lock placed on an object, if unlocked by the
original locker
void unlock(in PersistableRemoteObject pro)
raises (ObjectLockedException,
ObjectNotFoundException);
};
};
The IDL file specifies the operations and attributes that the remote objects support as well as the exceptions raised by each operation. Data types that an operation returns, its parameters as well as the object's attributes are also described in the IDL file. The idltojava compiler generates a number of Java files when run on the IDL file including numerous exception classes that are subclasses of org.omg.CORBA.UserException , client-side stubs and server-side skeletons of the interfaces described in the IDL , Helper classes which contain methods for manipulating IDL types such as the narrow() which is used for static typecasts, Holder classes which are used for out and inout parameters as well as the interface classes for RemoteObjectFactory and PersistableRemoteObject. It should be noted that the idltojava compiler changes attributes listed in the IDL interfaces to accessor and modifier methods in the generated Java interfaces as can be seen in the PersistableRemoteObject interface above with regards to creationID and objectID
import RemoteObjectStore.*;
import org.xml.sax.*;
import org.xml.sax.helpers.*;
import java.util.*;
import java.io.*;
public class JargonFileEntry extends
_PersistableRemoteObjectImplBase
implements DocumentHandler {
/*=================================================================*/
/*
C L A S S V A R I A B L E
S
*/
/*=================================================================*/
final private static String parserName =
"com.jclark.xml.sax.Driver";
/*=================================================================*/
/*
M E M B E R V A R I A B L E
S
*/
/*=================================================================*/
/**
* This is used for associating a SAX
event with a document location.
*/
private Locator locator=null;
private String objectID=null;
private String creationID=null;
private ArrayList definitions = new
ArrayList();
private boolean in_data_tag = false;
/*=================================================================*/
/*
C O N S T R U C T O R
S
*/
/*=================================================================*/
/**
* Default constructor.
*/
public JargonFileEntry() {;}
/*==================================================================*/
/*
M E M B E R F U N C T I O N
S
*/
/*=================================================================*/
public void startDocument (){;}
public void endDocument(){;}
public void ignorableWhitespace(char[] ch,
int start, int length){; }
public void
processingInstruction(java.lang.String target,
java.lang.String data){;}
public void setDocumentLocator(Locator
locator){this.locator = locator;}
public void startElement (String name,
AttributeList atts)
{
//System.out.println("Start element: " + name);
if(name.equals("jargon_file_entry")==true)
this.objectID = atts.getValue(0);
else
if(name.equals("definition")==true)
in_data_tag = true;
}
public void endElement (String name){
in_data_tag = false;
}/* endElement(String) */
public void characters(char ch[],int start,
int length){
if(in_data_tag == true){
definitions.add(new String(ch, start,
length));
}
} /* characters(char[], int, int) */
public String objectID() {return
objectID;}
public void objectID(String v)
{this.objectID = v;}
public String creationID(){ return
this.creationID; };
public void creationID(String
v){this.creationID = v;}
public String getXMLRepresentation(){
StringBuffer toReturn = new StringBuffer("<?xml
version=\"1.0\"?>\n");
//beginning tag
toReturn.append("<jargon_file_entry word=\"");
toReturn.append(this.objectID ==null? "" : this.objectID
);
toReturn.append("\">\n");
//definitions
int num_defs = this.definitions.size();
for(int i=0; i < num_defs; i++){
toReturn.append("<definition>");
toReturn.append(definitions.get(i).toString());
toReturn.append("</definition>\n");
}
//end tag
toReturn.append("</jargon_file_entry>");
return toReturn.toString();
}/* getXMLRepresentation() */
public void loadFromXML(String
xmlString)throws XMLLoadException{
try{
Parser parser =
ParserFactory.makeParser(parserName);
parser.setDocumentHandler(this);
parser.parse(new InputSource(
new
CharArrayReader(xmlString.toCharArray())
));
}catch(Exception e){
throw (XMLLoadException) e.fillInStackTrace();
}
}/* loadFromXML(String) */
public int getNumDefinitions(){ return
definitions.size(); }
public void addDefinition(String def){
definitions.add(def); }
public String getDefinitionAt(int index){
try{
return (String)
definitions.get(index);
}catch(IndexOutOfBoundsException ioobe){
return null;
}
}/* getDefinitionAt(int) */
public String eraseDefinitionAt(int
index){
try{
return (String)
definitions.remove(index);
}catch(IndexOutOfBoundsException ioobe){
return null;
}
}/* eraseDefinitionAt(int) */
public String replaceDefinitionAt(int index,
String newDef){
try{
return (String) definitions.set(index,
newDef);
}catch(IndexOutOfBoundsException ioobe){
return null;
}
}/* replaceDefinitionAt(int, String) */
} // JargonFileEntry
The JargonFileEntry class extends the_PersistableRemoteObjectImplBase which implements the PersistableRemoteObject interface. The JargonFile entry class uses the event-based Simple API for XML parsing (SAX) API to populate its attributes from an XML string. The SAX API was chosen because it is more efficient for dealing with large XML structures, and hence large objects, because it doesn’t have to store the entire string in memory all at once. The JargonFileEntry classes implements the DocumentHandler interface and thus contains methods used in parsing an XML string that are and populating its attributes from the XML string. The RemoteObjectServer that retrieves and stores the JargonFileEntry uses JDBC and Sun's JDBC-ODBC bridge to interact with the database. Below is a snippet from the RemoteObjectServer.java file showing the loadObject() method.
import RemoteObjectStore.*;
import org.omg.CORBA.*;
import org.omg.CosNaming.*;
import org.omg.CosNaming.NamingContextPackage.*;
import java.sql.*;
import java.util.*;
import sun.jdbc.odbc.JdbcOdbcDriver;
/**
* This class sits on a server and satisfies remote
requests for
* PersistableRemoteObjects.
*/
public class RemoteObjectServer extends
_RemoteObjectFactoryImplBase {
private static final String dsn =
"jdbc:odbc:Object_Store";
private static final String user =
"object_manager";
private static final String pswd =
"remote";
/* NOTE!!! LOTS OF CODE IN THIS CLASS HAS BEEN OMITTED FOR
CLARITY */
/**
* Calls parent class constructor and
loads new instance of Sun's
* JDBC-ODBC bridge for interaction with
SQL Server database.
* @see
_RemoteObjectFactoryImplBase#_RemoteObjectFactoryImplBase()
*/
public RemoteObjectServer(){
super();
try
{
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver").newInstance();
System.out.println("Remote Object Server Launched");
}
catch(Exception e)
{
throw new
RuntimeException("JDBC-ODBC Driver load failure");
}
} /* Constructor() */
/**
* Obtains an object from the remote
object store. If the object does not
* exist in the DB, a RuntimeException
will be thrown.
* @param objectID the object ID of the
object to retrieve from the remote
* object store.
* @param proh this is a holder class
that contains an empty
* PersistableRemoteObject that will be
populated with the object
* created.
* @exception ObjectLockedException if
the objectID is currently locked
* by another user.
* @exception ObjectNotFoundException if
the objectID could not be found
* in the database.
*/
public void loadObject(String objectID,
PersistableRemoteObjectHolder proh) throws
ObjectLockedException,
ObjectNotFoundException{
try{
Class objectClass = proh.value.getClass();
/* connect to database */
Connection conn = DriverManager.getConnection(dsn, user,
pswd);
/* search for object in DB */
Statement sidStmt = conn.createStatement();
// StringBuffer preferable to concatenating Strings with '+'
StringBuffer sqlStmt =
new StringBuffer("select * from object_table where
object_id = '");
sqlStmt.append(objectID); sqlStmt.append("'");
ResultSet rset = sidStmt.executeQuery(sqlStmt.toString());
boolean in_database = rset.next();
if(!in_database){
throw new ObjectNotFoundException();
}
/* create object that will be returned */
String xmlString = rset.getString(2);
String objectType = rset.getString(3);
// but first check to see if the object is locked
if(rset.getInt("locked")==1)
throw new ObjectLockedException();
if(objectClass.getName().equals(objectType.trim()) == false){
System.out.println("Class object doesn't match
that in store");
throw new RuntimeException("Class object doesn't
match that in store");
}
PersistableRemoteObject pro = proh.value;
pro.objectID(objectID);
pro.loadFromXML(xmlString);
pro.creationID("" + System.currentTimeMillis());
/* close connections to DB */
sidStmt.close();
conn.close();
//
proh.value = pro;
}catch(SQLException sqle){
sqle.printStackTrace();
throw new RuntimeException(sqle + "");
}catch(RuntimeException re){
System.out.println(re.getMessage());
throw re;
}catch(ObjectLockedException ole){
ole.printStackTrace();
throw ole;
}catch(ObjectNotFoundException onfe){
onfe.printStackTrace();
throw onfe;
}catch(Exception e){
e.printStackTrace();
throw new RuntimeException("Occured while in
loadFromXML()" + e);
}
}/* loadObject(String) */
}
Upon creation RemoteObjectServer the remote object server makes a call to its super class’s (RemoteObjectFactoryImplBase) constructor which in turn makes a call to its super class's constructor (org.omg.CORBA.DynamicImplementation). Also an attempt is made to load the JDBC-ODBC driver and if it fails the server will not start because a connection to the database cannot be made. The loadObject() method uses a PersistableRemoteObjectHolder as a parameter because Java does not support inout or out parameters due to the fact that parameters as passed by constant value. A check is made to see if the classname of the object held in the PersistableRemoteObjectHolder matches that of the object stored in the database with the requested object ID before returning it to the client. It is possible to have implemented this function by using CORBA Introspection and instantiating the object via metadata passed instead of populating an object that was passed to the method.
The JargonFileClient locates the RemoteObjectServer via the CORBA COS (Common Object Services) Naming Service, which provides a tree-like directory for object references in the same way that a filesystem provides a directory structure for files. There is a Naming Service provided with Java IDL that ships standard with the Java 1.3 SDK which is a simple implementation of the COS Naming Service specification.
Object references are stored in the namespace by name and each object reference-name pair is called a name binding. Name bindings may be organized under naming contexts. Naming contexts are themselves name bindings and serve the same organizational function as a file system subdirectory. All bindings are stored under the initial naming context. The initial naming context is the only persistent binding in the namespace; the rest of the namespace is lost if the Java IDL name server process halts and restarts.
For the JargonFileClient to use COS naming, its ORB must know the name and port of a host running a naming service or have access to a stringified initial naming context for that name server. The naming service can be either the Java IDL name server or another COS-compliant name service. A code snippet showing how the JargonFileClient locates the RemoteObjectServer and also how the it uses the RemoteObjectServer’s loadObject() method described above follows:
import RemoteObjectStore.*;
import org.omg.CORBA.*;
import org.omg.CosNaming.*;
import org.omg.CosNaming.NamingContextPackage.*;
/**
* This class provides an means to interact with a jargon
file whose elements are stored
* as objects in a remote object store over a network.
*/
public class JargonFileClient {
/**
* We will retrieve objects from the
remote object store via this object.
*/
public static RemoteObjectFactory
objFactory;
/* NOTE!!! LOTS OF CODE IN THIS CLASS HAS BEEN OMITTED FOR
CLARITY */
/**
* This is the word currently being
looked at by the user.
*/
public static JargonFileEntry
currentEntry;
/**
* Retrieves a specified entry from the
Jargon File
*/
public static void locateEntry(){
System.out.print(strSEARCH);
String strUserInput = IOHelper.readLine();
//we need to create an unused class so as to get a store
//the JargonFileEntry class we will get back from the server
JargonFileEntry classProxy = new JargonFileEntry();
PersistableRemoteObjectHolder proh =
new
PersistableRemoteObjectHolder(classProxy);
try{
objFactory.loadObject(strUserInput, proh);
/* unlock old JargonFileEntry */
if(currentEntry!= null){
objFactory.unlock(currentEntry);
}
currentEntry = (JargonFileEntry) proh.value;
/* lock new JargonFileEntry */
objFactory.lock(currentEntry);
classProxy = null; /* no longer needed */
viewEntry();
}catch(ObjectNotFoundException onfe){
System.out.println(strDOESNT_EXIST);
}catch(ObjectLockedException ole){
System.out.println(strIS_LOCKED);
}catch(RuntimeException re){
System.out.println(re);
re.printStackTrace();
}
}/* locateEntry() */
/**
* Interacts with user and retrieves
word-definition pairs from remote
* object store.
* @param argv ignored.
*/
public static void main(String[] args) {
try {
/* Create and initialize the ORB */
ORB orb = ORB.init(args, null);
// Get the root naming context
org.omg.CORBA.Object objRef =
orb.resolve_initial_references("NameService");
NamingContext ncRef =
NamingContextHelper.narrow(objRef);
/* Resolve the object reference in naming */
NameComponent nc = new
NameComponent("RemoteObjectFactory", "");
NameComponent path[] = {nc};
objFactory =
RemoteObjectFactoryHelper.narrow(ncRef.resolve(path));
/* display user menu */
displayMenu();
} catch (Exception e)
{
e.printStackTrace();
}
}/* main(String[]) */
} // JargonFileClient
RMI Implementation
The RMI interfaces for the ROPEM system are shown below.
package remote_obj_store;
import java.rmi.Remote;
import java.rmi.RemoteException;
/**
* This Object stores and retrieves
PersistableRemoteObject's to and from a
* remote location.
*/
public interface RemoteObjectFactory extends Remote {
PersistableRemoteObject createObject(String
objectID, Class objectClass)
throws RemoteException, ClassCastException;
void storeObject(PersistableRemoteObject pro)
throws RemoteException;
PersistableRemoteObject loadObject(String
objectID, Class objectClass)
throws RemoteException;
void lock(PersistableRemoteObject pro) throws
RemoteException;
void unlock(PersistableRemoteObject pro)
throws RemoteException;
boolean isLocked(String objectID) throws
RemoteException;
} // RemoteObjectFactory
package remote_obj_store;
import java.io.Serializable;
import org.xml.sax.*;
import org.xml.sax.helpers.*;
/**
* This is an interface for an Object that can generate
XML representations
* of itself and has an Object ID that can be used to
uniquely identify it.
*/
public interface PersistableRemoteObject extends Serializable
{
public String getCreationID();
public void setCreationID(String
v);
String getObjectID();
void setObjectID(String v);
String getXMLRepresentation();
void loadFromXML(String xmlString) throws
Exception;
} // PersistableRemoteObject
One noticeable difference between the interfaces specified in the RMI version of ROPEM and the CORBA version is that the CORBA version throws lots of specific Exceptions while the RMI version throws mainly RemoteException. The reason that the RMI version can get away with throwing RemoteException is that the RemoteException class supports storing error messages in it upon initialization while CORBA Exception classes do not. The JargonFileEntry class is very similar to that in the CORBA implementation except that it implements the PersistableRemoteObject interface directly as well as extends the HandlerBase class from the org.xml.sax package to avoid having to create functions that do nothing to fulfill interface requirements.
On the other hand there is considerable difference between the implementations of methods like loadObject() and createObject() in the CORBA vs. RMI versions.
package remote_obj_server;
import java.rmi.*;
import java.rmi.server.*;
import remote_obj_store.*;
import java.sql.*;
import java.util.*;
import sun.jdbc.odbc.JdbcOdbcDriver;
/**
* This class sits on a server and satisfies remote
requests for
* PersistableRemoteObjects.
*/
public class RemoteObjectServer extends UnicastRemoteObject
implements RemoteObjectFactory {
private static final String dsn =
"jdbc:odbc:Object_Store";
private static final String user =
"object_manager";
private static final String pswd =
"remote";
public RemoteObjectServer() throws
RemoteException{
super(6661);
try
{
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver").newInstance();
System.out.println("Remote Object Server Launched");
}
catch(Exception e)
{
e.printStackTrace();
throw new RemoteException("Could not
register JDBC-ODBC bridge");
}
} /* Constructor() */
/* NOTE!!! LOTS OF CODE IN THIS CLASS HAS BEEN OMITTED FOR
CLARITY */
/**
* Obtains an object from the remote
object store. If the object does not
* exist in the DB, a RemoteException
will be thrown.
* @param objectID the object ID of the
object to retrieve from the remote
* object store.
* @param objectClass the Class object
for the object that shall be
* retrieved.
* @exception RemoteException see
RemoteObjectFactory for details.
*/
public PersistableRemoteObject loadObject(String
objectID,Class objectClass)
throws RemoteException{
try{
/* connect to database */
Connection conn = DriverManager.getConnection(dsn, user,
pswd);
/* search for object in DB */
Statement sidStmt = conn.createStatement();
// StringBuffer preferable to concatenating Strings with '+'
StringBuffer sqlStmt =
new StringBuffer("select * from object_table where
object_id = '");
sqlStmt.append(objectID); sqlStmt.append("'");
ResultSet rset = sidStmt.executeQuery(sqlStmt.toString());
boolean in_database = rset.next();
if(!in_database){
StringBuffer errStr = new
StringBuffer("ObjectID: ");
errStr.append(objectID);
errStr.append(" does not exist in remote object
store");
throw new
RemoteException(errStr.toString());
}
/* create object that will be returned */
String xmlString = rset.getString(2);
String objectType = rset.getString(3);
// but first check to see if the object is locked
if(rset.getInt(4)==1){
StringBuffer errStr = new
StringBuffer("ObjectID: ");
errStr.append(objectID);
errStr.append(" is currently locked");
throw new
RemoteException(errStr.toString());
}
//check to see if the Class object passed in is for the class in
the
//object store
// NOTE: eliminate whitespace from string.
if(objectClass.getName().equals(objectType.trim()) == false)
throw new RemoteException("Class object doesn't match that
in store");
PersistableRemoteObject pro =
(PersistableRemoteObject)
objectClass.newInstance();
pro.setObjectID(objectID);
pro.loadFromXML(xmlString);
pro.setCreationID("" + System.currentTimeMillis());
/* close connections to DB */
sidStmt.close();
conn.close();
return pro;
}catch(SQLException sqle){
System.out.println(sqle);
throw new RemoteException(sqle + "");
}catch(InstantiationException ie){
System.out.println(ie);;
throw new RemoteException(ie + "");
}catch(ClassNotFoundException cnfe){
System.out.println(cnfe);
throw new RemoteException(cnfe + "");
}catch(IllegalAccessException iae){
System.out.println(iae);
throw new RemoteException(iae + "");
}catch(RemoteException re){
throw re;
}catch(Exception e){
System.out.println(e);
throw new RemoteException("Occured while in
loadFromXML()" + e);
}
}/* loadObject(String) */
} // RemoteObjectServer
A marked differences exist in the RMI implementation of loadObject() method from the CORBA implementation such as the fact that a Class object is passed to the method instead of a PersistableRemoteObjectHolder or similar means of getting an out parameter. Instead of using an out paramater the java.lang.Class object containing the metadata for that particular PersistableRemoteObject is marshaled and sent to the server (which is possible because it implements java.io.Serializable) . The Class object is then used to instantiate the PersistableRemoteObject which is returned to the client after being populated with the XML string from the database. Due to the fact that RMI supports dynamically loading classes across Java Virtual Machines, the class whose Class object is sent across the network doesn’t have to exist in the server environment to be instantiated but can be created entirely from the metadata contained in its Class object.
The DCOM implementation had two major differences with the RMI and CORBA versions. Firstly, we chose C++ rather than Java since it allows more fine-grained control of COM internals and direct access to IUnknown. Secondly, XML parsing was handled by Microsoft's XMLDOM parser. Since this parser uses the Document Object Model (DOM) API, it creates a complete representation of the XML document object in memory leading to increased memory overhead. Initial parsing costs are also greater, but subsequent access should be much faster than with SAX.
// Dictionary.idl : IDL source for Dictionary.dll
//
// This file will be processed by the MIDL tool to
// produce the type library (Dictionary.tlb) and marshalling
code.
import "oaidl.idl";
import "ocidl.idl";
[
object,
uuid(EC12584D-93C5-4359-A152-53C70F5391FE),
dual,
helpstring("IWord Interface"),
pointer_default(unique)
]
interface IWord : IDispatch
{
[id(1), helpstring("method AddDefinitoin")] HRESULT
AddDefinitoin(BSTR bstrDefinition);
[id(2), helpstring("method GetNumDefinitions")] HRESULT
GetNumDefinitions([out,retval] long * pDefinitions);
[id(3), helpstring("method GetDefinitionAt")] HRESULT
GetDefinitionAt(long index, [out,retval] BSTR * bstrDef);
[id(4), helpstring("method EraseDefinitionAt")] HRESULT
EraseDefinitionAt(long index);
[id(5), helpstring("method ReplaceDefinitionAt")] HRESULT
ReplaceDefinitionAt(long index, BSTR bstrDef);
};
[
object,
uuid(EC12584D-93C5-4359-A152-53C70F5391FA),
dual,
helpstring("IPersistableRemoteObject Interface"),
pointer_default(unique)
]
interface IPersistableRemoteObject : IUnknown
{
[propget, id(2), helpstring("property ObjectID")] HRESULT
ObjectID([out, retval] BSTR *pVal);
[propput, id(2), helpstring("property ObjectID")] HRESULT
ObjectID([in] BSTR newVal);
[propget, id(3), helpstring("property CreationID")] HRESULT
CreationID([out, retval] BSTR *pVal);
[propput, id(3), helpstring("property CreationID")] HRESULT
CreationID([in] BSTR newVal);
[id(4), helpstring("method GetXMLRepresentation")] HRESULT
GetXMLRepresentation(BSTR* bstrXML);
[id(5), helpstring("method LoadFromXML")] HRESULT LoadFromXML(BSTR
bstrXML);
};
[
object,
uuid(FE9A7B15-587D-4C6F-B6D0-070E6B7F769F),
dual,
helpstring("IRemoteObjectServer Interface"),
pointer_default(unique)
]
interface IRemoteObjectServer : IDispatch
{
[id(1), helpstring("method CreateObject")] HRESULT
CreateObject(BSTR bstrObjectID, BSTR bstrType, [out,retval]
IPersistableRemoteObject ** pRetVal);
[id(3), helpstring("method UnlockObject")] HRESULT
UnlockObject(IPersistableRemoteObject * pObject);
[id(4), helpstring("method StoreObject")] HRESULT
StoreObject(IPersistableRemoteObject* pObject);
[id(5), helpstring("method LoadObject")] HRESULT
LoadObject(IPersistableRemoteObject* pObject);
[id(6), helpstring("method IsLocked")] HRESULT
IsLocked(IPersistableRemoteObject* pObject, [out,retval] BOOL*
pbLocked);
[id(7), helpstring("method LockObject")] HRESULT
LockObject(IPersistableRemoteObject *pObject);
};
[id(4), helpstring("method LockObject")] HRESULT
LockObject(IPersistableRemoteObject* pObject);
[
uuid(FCF8BF7C-C396-4747-AF49-EFB69C34A8CA),
version(1.0),
helpstring("Dictionary 1.0 Type Library")
]
library DICTIONARYLib
{
importlib("stdole32.tlb");
importlib("stdole2.tlb");
[
uuid(A8DBEA02-129C-4BC0-BF62-0E686221DE88),
helpstring("Word Class")
]
coclass Word
{
[default] interface IWord;
};
[
uuid(556A2C3D-FCAD-4743-9D2D-C31FA49FF55E),
helpstring("RemoteObjectServer Class")
]
coclass RemoteObjectServer
{
[default] interface IRemoteObjectServer;
};
};
The DCOM version of the Dictionary component uses multiple interfaces to support both client requirements and the distributed nature of the object. DCOM uses a technology known as Type Libraries to store metadata about the component and it's methods. The type library maps perfectly to the component's definition which is specified in IDL. The type library also provides the operating system with identification data so that clients can instantiate the component knowing only it's interface and coclass name. A coclass defines the class that maps to a specific identifier. Here, the CWord class maps to the Word identifier. The Word component actually supports two different custom interfaces. It supports IWord and IPersistableRemoteObject. Clients that work with the word component fall into two categories. C++ clients, which have full access to both the component and COM, simply request the particular interface which they want through QueryInterface. Non-C++ clients usually only see the interface marked as [default]. Some languages, like Microsoft's version of java, are capable to accessing other interfaces by casting. One of the many advantages of DCOM is the support for multiple languages. This component is accessible through Java, JavaScript, Visual Basic as well as any other COM compliant language in addition to C++. This is possible through marshalling, type libraries and automation. Marshalling consists of converting a method invocation into a binary string.
Marshalling help support both the distributed nature of the component as well as intra-language functionality. This works since COM can marshal the method call to and from the Microsoft Java Virtual Machine.
The DCOM version of the persistent object framework and the word component required a deep understanding of the fundamentals of DCOM as well as the specifics of various technologies required to actually develop working components. One of the key aspects of our frameworks consists of persisting objects into a central repository as XML text. In order to use the XML parser and its support for verification, our components needed to act as a DCOM client for the XMLDOM component. Since we did not have the headers, libraries or source code for the XMLDOM component, we used ATL as well as the compiler’s built in COM import feature. ATL stands for ActiveX Template Library and it consists of several C++ template classes that make working with type libraries possible in C++ without the original headers or source.
STDMETHODIMP
CRemoteObjectServer::LoadObject(IPersistableRemoteObject
*pObject)
{
if(!pObject) return E_POINTER;
// Hack. Use the xml parser to load from a url.
try
{
_ConnectionPtr pConn("ADODB.Connection");
_RecordsetPtr pRs("ADODB.Recordset");
_CommandPtr pCmd("ADODB.Command");
// Open a connection
pConn->Open("dsn=PersistableRemoteObject;", "", "",
adConnectUnspecified);
// Create query
BSTR bstrObjectID = NULL;
pObject->get_ObjectID(&bstrObjectID);
CComBSTR bstrSQL = "select * from Object where object_id = '";
bstrSQL += bstrObjectID;
bstrSQL += "';";
SysFreeString(bstrObjectID);
// Create a command
pCmd->CommandText = bstrSQL.m_str; // add
where clause for object_id
pCmd->ActiveConnection = pConn;
// Execute the command
pRs->CursorLocation = adUseClient;
pRs->Open((IDispatch *) pCmd, vtMissing,
adOpenStatic,
adLockBatchOptimistic, adCmdUnspecified);
long iRecords = 0;
pRs->get_RecordCount(&iRecords);
if(iRecords != 1) return E_UNEXPECTED; //No Object
Fields* pFields = NULL;
pRs->get_Fields(&pFields);
Field* pXMLField = NULL;
_variant_t varXML = CComBSTR("xml");
pFields->get_Item(varXML, &pXMLField);
_variant_t var;
pXMLField->get_Value(&var);
if(var.vt != VT_BSTR) return E_FAIL;
pObject->LoadFromXML(var.bstrVal);
}
catch(...)
{
return E_FAIL;
}
return S_OK;
}
Here, _ ConnectionPtr is actually a smart pointer that overrides the C++'s indirection operator. Whenever a method like pRS->Open () is called, the overridden indirection or arrow operator makes the proper call to the component based on the type library. The compiler generates the class definition for the smart pointer at compile time. This definition consists of the exact interface that is found in the type library. Therefore, dynamically generated ATL smart pointers makes interacting with other DCOM components seamless to the C++ code. Without ATL and dynamic COM import support, this would only be possible with source headers. In addition to ADO database components, our framework also uses ATL to access the XMLDOM component.
It should be noted that DCOM’s primary means of handling remote errors is via the HRESULT which is returned by every remote function. The HRESULT is merely a numeric assignation which has various pre-defined values (S_OK for success, E_FAIL for error, etc). Due to the fact that all DCOM functions must return an HRESULT, all DCOM functions that want to return a value to the client must use out parameters. For richer error handling functionality Error Objects that implement IErrorInfo can be used to return more contextual error data to the client. If the server plans to support Error objects it must implement the ISupportErrorInfo interface.
Our framework fully supports multi-tier distributed applications. Although in theory an application could live on multiple servers scattered around the world and take advantage of our framework for persistence, it will not scale well. An analysis of bottlenecks revealed that most of the problems were in the database connection and locking. This is unfortunate because it meant that all the benchmark numbers we obtained to compare the performance of the same system written in CORBA, DCOM, and RMI were meaningless. Actually accessing the database was generally much faster than xml parsing or any other operation. Secondly, our locking mechanism worked through polling. If an object were locked, the code would keep trying again until the object was available. The solution consists of using a mutex or messaging framework. Messaging frameworks such as Microsoft Message Queue (MSMQ) or Java Message Services (JMS) provide loosely coupled transactional events that get queued by the component system until the proper resource is available. On a lower level, an operating system mutex could be used to coordinate multiple threads awaiting the same resource. Mutexes, however would not truly work in a distributed environment and would only make a single process more efficient. Since the database was the major bottleneck, caching proved to be the most effective way to improve performance. In a single process environment, moving a percentage of the objects to an in-memory data structure and then synchronizing the data structure with the database using a write-back scheme showed an order of magnitude improvement dependent on how often the same object was requested by clients. Unfortunately, the strategy used would not scale to larger systems since a distributed cluster does not share memory. With an event based synchronization, however, parts of the database could be transferred to whatever server needs the data most.
At the low level, component systems provide a component with language independence as well as distributed capabilities like inter-process method invocation. Component systems can also provide functionality at much higher levels of abstraction. A classic example is transactions. Most enterprise systems rely on transactions for nearly all their functionality. A transaction is a distributed atomic computational event. This means that the computation is relevant to multiple systems at the same time and the transaction is either completed or not. There is no such thing as a partial transaction. Transactional operations usually consist of a set of smaller operations. For instance, a commerce transaction consists of removing money from one account and adding it to another. If only one of these operations succeeds, the transaction violates the system’s conceptual integrity. It does not make sense for money to be removed from one account without being added to another. Therefore, all transactional systems require rollback or an ability to undo operations until all interested parties have confirmed that they are satisfied with the state of the transaction. Transactional component systems provide a set of distributed components with a transaction context. This context stores relevant information about the transaction as well as success state. The context may also know what components are interested in the transaction.
One strategy for using transactional contexts is multi-phase commits. The components that work on a transaction are part of a pipeline. At any stage in the pipeline, the component may chose to abort the transaction. If an abortion occurs, all previous components are told the operation failed and simply ignore any operations they have queued. Once all pipeline stages complete, all components are told to get ready to commit. Once they are all ready, the transactional system tells them to commit. At this point, the chance of failure is very low. Each component then sends the transactional system a confirmation. If all confirmations are received, the transactional system proceeds to the next operation. If at least one confirmation is not received, the transactional system tells the components to rollback or undo the work. Therefore, either all interested components complete the transaction or return to their previous state. In such a transactional system, there is no chance of the system integrity being compromised. The transaction either goes through perfectly on all parts of the system or it fails completely. Another advantage of transactional contexts is that they hold state.
On web services with hundreds of servers spread across the globe serving millions of users, it would be very difficult to synchronize the data in a central place. The solution consists of storing the transactional context in the web browser’s cookie. Whatever server the web user’s action connects to can then use the cookie to recreate all relevant state. Two technologies that add transactional support to distributed component system are Microsoft Transaction Server and Sun’s Enterprise Java Beans Transactions. Transactional components must work with events that adhere to ACID properties.
|
Atomicity |
A transaction can not complete in part. It must either succeed or fail. |
|
Consistency |
If a transaction succeeds, all parts of the system agree on what happened. |
|
Isolations |
The order in which two transactions occur should not affect the outcome. |
|
Durability |
The transactional state is maintained in a form of media that can survive power or network outages. |
MTS allows COM components to be part of distributed transaction. The key feature MTS provides is context objects. A context object handles database rollback as well as context. When a transactional component is instantiated the Distributed Transaction Coordinator(DTC) provides it with a context object. In C++ a component may get this object by calling GetObjectContext(). The DTC links this context object with a transaction provided. One example of a transaction provider is SQL server. When a transactional component accesses the database, the updates are queued instead of written. The component thinks the database has been modified and tell MTS that it has completed successfully through the SetComplete() method on the context object. If all phases of the transactional pipeline complete, the queued changes are committed to the database. Otherwise they are ignored and the transaction is aborted. Note that each transactional component does not need to deal with the details of rolling back their changes to the database.
Transactions in CORBA as well as the Java Transaction Architecture are modeled after the X/Open Distributed Transaction Processing Model. This model lays the theoretical groundwork for transactions as well as defining specific interfaces that a transactional framework should support. One aspect of this model is the transactional requirements attached to a every component. X/Open describes several transactional modes that indicate how a component may accept or work with a transaction.
|
Transactions are not supported |
|
|
Dirty reads are prevented; non-repeatable reads and phantom reads can occur |
|
|
Dirty reads, non-repeatable reads and phantom reads can occur |
|
|
Dirty reads and non-repeatable reads are prevented; phantom reads can occur |
|
|
Dirty reads, non-repeatable reads and phantom reads are prevented |
|
TX_REQUIRED |
Must have a transactional context |
|
TX_SUPPORTS |
May use a transactional context if it exists |
|
TX_REQUIRES_NEW |
Requires new transactional context |
|
TX_MANDATORY |
Must have a unfinished transactional context |
Giving all components a set of transactional requirements sets up constraints to prevents certain types of invalid subtransactions. For instance, the component that subtracts money from a bank account should always get a new context. If it gets an older context, then some invalid operations have been performed. This ensures that deductions happen before anything else. It is better to temporarily destroy money than it is to create it since you can give someone a refund but can’t automatically ask for money back. The X/Open method supports both one-phase and two-phase commits. The Object Transaction Service handles commits.
The purpose of our investigations was to learn about Distributed Object systems and how they relate to building robust three-tier applications. In our explorations we discovered how transactional systems work, how to write programs using the three most popular distributed object technologies as well as the complexities involved in creating a robust error handling mechanism in a distributed environment. In this regard we feel that our independent study was successful and look forward to continuing the development of ROPEM in the future.
Appendix A: Source code for CORBA implementation of ROPEM
I. RemoteObjectStore.idl
module RemoteObjectStore{
exception XMLLoadException{};
interface PersistableRemoteObject{
//the time the object was created on the server, used to
determine which
//of the object instances this one is
attribute string creationID;
//unique object identifier
attribute string objectID;
//Generates an XML representation of the Object
string getXMLRepresentation();
//Populates the data in the object from an XML string
void loadFromXML(in string xmlString) raises
(XMLLoadException);
};
exception ObjectAlreadyExistsException{};
exception ObjectNotFoundException{};
exception ObjectLockedException{};
interface RemoteObjectFactory{
//Checks to see if the object ID is not in use and if it isn't
will
//populate the object passed in with the data for the class
void createObject(in string objectID, inout
PersistableRemoteObject pro)
raises (ObjectAlreadyExistsException);
//checks to see if the object is locked
boolean isLocked(in string objectID) raises
(ObjectNotFoundException);
//Obtains an object from the remote object store.
void loadObject(in string objectID, inout
PersistableRemoteObject pro)
raises (ObjectLockedException,
ObjectNotFoundException);
//This stores the specified PersistableRemoteObject in the
remote object store
void storeObject(in PersistableRemoteObject pro)
raises (ObjectLockedException);
//Prevents any updates to the object until the object is
unlocked
void lock(in PersistableRemoteObject pro)
raises (ObjectLockedException,
ObjectNotFoundException);
// Releases a lock placed on an object, if unlocked by the
original locker
void unlock(in PersistableRemoteObject pro)
raises (ObjectLockedException,
ObjectNotFoundException);
};
};
//idltojava -fno-cpp RemoteObjectStore.idl
II. RemoteObjectServer.java
import RemoteObjectStore.*;
import org.omg.CORBA.*;
import org.omg.CosNaming.*;
import org.omg.CosNaming.NamingContextPackage.*;
import java.sql.*;
import java.util.*;
import sun.jdbc.odbc.JdbcOdbcDriver;
/**
* RemoteObjectServer.java
*
*
* Created: Fri Nov 17 09:11:05 2000
*
* @author <a href="mailto:kpako@hotmail.com">Dare
Obasanjo</a>
* @version 1.0
*/
/**
* This class sits on a server and satisfies remote
requests for
* PersistableRemoteObjects.
*/
public class RemoteObjectServer extends
_RemoteObjectFactoryImplBase {
/*=================================================================*/
/*
I N S T A N C E V A R I A B L E
S
*/
/*=================================================================*/
/*=================================================================*/
/*
S T A T I C V A R I A B L
E
S
*/
/*=================================================================*/
/**
* This is the name of the dsn that will
be used when attempting to
* connect to the database
*/
private static final String dsn =
"jdbc:odbc:Object_Store";
/**
* This is the name of the user
name that will be used when attempting to
* connect to the database
*/
private static final String user =
"object_manager";
/**
* This is the name of the
password that will be used when attempting to
* connect to the database
*/
private static final String pswd =
"remote";
/*=================================================================*/
/*
C O N S T R U C T O
R
*/
/*=================================================================*/
/**
* Calls parent class constructor and
loads new instance of Sun's
* JDBC-ODBC bridge for interaction with
SQL Server database.
* @see
_RemoteObjectFactoryImplBase#_RemoteObjectFactoryImplBase()
*/
public RemoteObjectServer(){
super();
try
{
Class.forName("sun.jdbc.odbc.JdbcOdbcDriver").newInstance();
System.out.println("Remote Object Server Launched");
}
catch(Exception e)
{
throw new
RuntimeException("JDBC-ODBC Driver load failure");
}
} /* Constructor() */
/*=================================================================*/
/*
I N S T A N C E M E T H O D
S
*/
/*=================================================================*/
/**
* Creates a new PersistableRemoteObject
and stores on the it in the
* database.
* @param objectID the objectID for the
object that will be stored in the
* database.
* @param proh this is a holder class
that contains an empty
* PersistableRemoteObject that will be
populated with the object
* created.
* @exception
ObjectAlreadyExistsException if the objectID already exists
* in the database.
*/
public void createObject(String objectID,
PersistableRemoteObjectHolder proh)
throws
ObjectAlreadyExistsException{
try{
Class objectClass = proh.value.getClass();
/* connect to database */
Connection conn =
DriverManager.getConnection(dsn, user, pswd);
//Turn off autocommit so that if a crash occurs
in the middle of
//insertions there won't be data corruption.
conn.setAutoCommit(false);
/* search for object ID in DB */
Statement sidStmt = conn.createStatement();
// StringBuffer preferable to concatenating Strings with '+'
StringBuffer sqlStmt =
new StringBuffer("select * from object_table where
object_id = '");
sqlStmt.append(objectID); sqlStmt.append("'");
ResultSet rset = sidStmt.executeQuery(sqlStmt.toString());
boolean in_database = rset.next();
sidStmt.close();
if(in_database){
throw new ObjectAlreadyExistsException();
}
/* create new instance of class */
PersistableRemoteObject pro = proh.value;
pro.objectID(objectID);
pro.creationID("" + System.currentTimeMillis());
/* store default representation in DB */
PreparedStatement object_table_insrt =
conn.prepareStatement("insert into object_table
values(?,?,?,?,?)");
object_table_insrt.setString(1, objectID);
object_table_insrt.setString(2, pro.getXMLRepresentation());
object_table_insrt.setString(3, objectClass.getName());
object_table_insrt.setInt(4, 0);
object_table_insrt.setString(5, pro.creationID());
object_table_insrt.executeUpdate();
/* commit the changes to the database */
conn.commit();
conn.setAutoCommit(true);
/* close all connections to the database */
object_table_insrt.close();
conn.close();
//proh.value = pro;
}catch(SQLException sqle){
sqle.printStackTrace();
throw new RuntimeException("" + sqle);
}catch(ObjectAlreadyExistsException oaee){
oaee.printStackTrace();
throw oaee;
}catch(Exception sqle){
sqle.printStackTrace();
throw new RuntimeException("" + sqle);
}
}/* createObject(String, Class) */
/**
* Obtains an object from the remote
object store. If the object does not
* exist in the DB, a RuntimeException
will be thrown.