Loaders can provide support for persistent objectslong lived objects stored on disk in the file system or in a database.
Loaders are also called when an object reference enters an address space, and not just when a "missing object" is the target of a request. This can arise in a number of ways:
_bind()
or string_to_object()
is made from within a process.
in
parameter.
out
or inout
parameter, or a return value.
LoaderClass
(defined in package IE.Iona.Orbix2.CORBA
), and they are installed by creating an instance of that new class. LoaderClass
provides the following methods:
load()
: OrbixWeb uses this method to inform a loader of an object fault. The loader is given the marker of the missing object so that it can identify which object to load.
save()
: When a process terminates, the objects in its address space can be saved by its loaders. To allow this, OrbixWeb supplies a finalize()
method, which should be called on the _CORBA.Orbix
object before process termination. _CORBA.Orbix.finalize()
makes an individual call to save()
for each object managed by a loader. The save()
method can also be explicitly called through the _ObjectRef._save()
method, which all OrbixWeb objects implement.
record()
and rename()
: These methods are used to control naming of objects, and they are explained in section 23.2.
LoaderClass
are given in Part II, "Class and Interface Reference" of the OrbixWeb Reference Guide.
The constructor of
LoaderClass
(the base class of all loaders) takes an optional boolean
parameter. When creating a loader object, this parameter must be true
if the load()
method of the new loader is to be called by OrbixWeb.
23.1 Specifying a Loader for an Object
Each object is associated with a loader object, which is informed when the object is named or renamed (section 23.2) and when the object is to be saved (section 23.4). If no loader is explicitly specified for an object, then it is associated with a default loader, implemented by OrbixWeb.
An object's loader can be specified as the object is being created, either by the TIE or by the BOAImpl approach.
// Java // myLoader is a loader object: _BankRef bRef = new _tie_Bank (new BankImplementation (), "College Green", myLoader);In the BOAImpl approach, this can be done by declaring the implementation class's constructor to take a loader object parameter; and by defining this constructor to pass on this object as the second parameter to its BOAImpl class's constructor. For example:
// Java import IE.Iona.Orbix2.CORBA.SystemException; import IE.Iona.Orbix2.CORBA.LoaderClass; class BankImplementation extends _boaimpl_Bank { ... public BankImplementation (String marker, LoaderClass loader) throws SystemException { super (marker, loader); ... } }Each object is associated with a simple default loader if no loader is specified for it. This loader does not support persistence.
An object's loader can be retrieved by calling:
// Java // In package IE.Iona.Orbix2.CORBA, // in interface _ObjectRef. public LoaderClass _loader () throws SystemException;
Recall that objects can be named in a number of ways:
_BankRef bRef = new _tie_Bank (new BankImplementation (), "College Green", myLoader);
BankImplementation bImpl; try { bImpl = new BankImplementation ("College Green", myLoader); } ...
_ObjectRef._marker(String)
, for example:
bRef._marker ("Foster Place");In all cases, OrbixWeb calls the object's loader to confirm the chosen name, thus giving the loader a chance to override the choice. In the first two cases above, OrbixWeb calls
record()
; in the last case it calls rename()
because the object already exists.OrbixWeb executes the following algorithm when an object is created, or an object's existing marker is changed:
record()
or rename()
). The loader can accept the name by not changing it; alternatively, the loader can reject it by changing it to a new name. If the loader changes the name, OrbixWeb again checks that the new name is not already in use within the current process; if it is already in use, the object will not be correctly registered.
record()
or rename()
) which must then choose a name. OrbixWeb will then check the chosen name; the object will not be correctly registered if this chosen name is already in use.
record()
and rename()
can, if necessary, raise an exception.The implementations of
rename()
and record()
in LoaderClass
both return without changing the suggested name. (It's implementation of load()
and save()
carry out no actions.)The default loader (which is associated with all objects which are not explicitly associated with another loader) is an instance of
NullLoaderClass
, a derived class of LoaderClass
. This class inherits load()
, save()
and rename()
from LoaderClass
. It implements record()
so that if no marker name is suggested it chooses one that is a string of decimal digits, different to any it generated before in the current process.
load()
method is called on each loader in turn until one of them successfully returns the address of the object, or until they have all returned null
.The responsibilities of the
load()
method are:
load()
method is given the following information:
boolean
value, set as follows depending on why the object fault occurred:
p = I1._bind( <parameters> );
load()
will be "I1
".(If the first parameter to the
_bind()
is a full object reference string, OrbixWeb will return an exception if the reference's interface field is not I1
or a derived interface of I1
.).
p = _CORBA.Orbix.string_to_object ( <full object reference string> );
load()
will be that extracted from the full object reference string.
in
, out
or inout
parameter, a return value, or as the target object of an operation call), then the interface name in load()
will be the interface name extracted from the object reference.
save()
method on the loader associated with each object. A loader may save the object to persistent storage (either by calling a method on the object, or by accessing the object's data and writing this data itself).A programmer can also explicitly cause the
save()
method to be called by invoking an object's _save()
method. The _save()
method simply calls the save()
method on the object's loader. _save()
must be called in the same address space as the target object: calling it in a client process, that is, on a proxy, will have no effect.The two alternative reasons for the invocation of
save()
are distinguished by its second parameter. This parameter is of type int
, and takes one of the following values:
load()
method is redefined to do the main work of the loader, that is, to load the object on demand. The object's marker is normally used to find the object in the persistent store.
save()
method is redefined so that it saves its objects on process termination, and also if _save()
is called.
record()
and rename()
methods are normally redefined. Often, record()
chooses the marker for a new object; and rename()
is sometimes written to simply prevent an object's marker being changed. However, record()
and rename()
are sometimes not redefined in a simple application, where the code that chooses markers at the application level can be trusted to choose correct values.
per_simp
demonstration directory.Two interfaces are involved in the application:
// IDL // In file bank.idl. interface Account { readonly attribute float balance; void makeDeposit(in float f); void makeWithdrawal(in float f); }; interface Bank { Account newAccount(in string name); };Throughout this simple example, we will assume that these definitions have been compiled using the IDL
-jP
switch as follows:
idl -jP per_simp bank.idlThe classes output by the IDL compiler are within the scope of the
per_simp
Java package.Interfaces
Account
and Bank
are implemented by classes AccountImplementation
and BankImplementation
, respectively. Instances of class AccountImplementation
are made persistent using a loader (of class Loader
). The persistence mechanism used is very primitive because it uses one file per account object. Nevertheless, the example acts as a simple introduction to loaders. The implementation of class Loader
is shown later, but first the implementations of classes AccountImplementation
and BankImplementation
are shown.
Class
AccountImplementation
is implemented as follows:
// Java // In file AccountImplementation.java. package per_simp; import IE.Iona.Orbix2.CORBA._ObjectRef; import IE.Iona.Orbix2.CORBA.LoaderClass; import IE.Iona.Orbix2.CORBA.SystemException; public class AccountImplementation implements _AccountOperations { protected String m_name; protected float m_balance; protected String m_accountNr; public AccountImplementation (float initialBalance, String name, String nr) { // Initialise member variable values. // Details omitted. } // Methods to implement IDL operations: public float get_balance () throws SystemException { return m_balance; } public void makeLodgement (float f) throws SystemException { m_balance += f; } public void makeWithdrawal (float f) throws SystemException { m_balance -= f; } // Methods for supporting persistence. public static _ObjectRef loadMe (String file_name, LoaderClass loader) throws SystemException { // Details shown later. } public void saveMe (String file_name) { // Details shown later. } };Two methods have been added to the implementation class. The static method
loadMe()
is called from the load()
method of the loader. It is given the name of the file to load the account from. The method saveMe()
writes the member variables of an account to a specified file. These methods can be coded as follows:
public static _ObjectRef loadMe (String file_name, LoaderClass loader) throws SystemException { ... RandomAccessFile file = null; String name = null; float bal = 0; try { file = new RandomAccessFile (file_name, "r"); name = file.readLine (); bal = file.readFloat (); file.close(); } catch (java.io.IOException ex) { ... System.exit (1); } AccountImplementation aImpl = new AccountImplementation (bal, name, file_name); _AccountRef aRef = new _tie_Account (aImpl, file_name, loader); return aRef; } public void saveMe (String file_name) { ... RandomAccessFile file = null; try { file = new RandomAccessFile (file_name, "rw"); file.seek (0); file.writeBytes (m_name + "\n"); file.writeFloat (m_balance); f.close(); } catch (java.io.IOException ex) { ... System.exit(1); } }The statement:
_AccountRef aRef = new _tie_Account (aImpl, file_name, loader);in
AccountImplementation.loadMe()
creates a new TIE for the implementation object accImpl
, and specifies its marker to be file_name
and its loader to be the loader object referenced by parameter loader
(Actually, our example creates only a single loader object, as we will see.)Class
BankImplementation
is implemented as follows:
// Java // In file BankImplementation.java. package per_simp; import IE.Iona.Orbix2.CORBA.LoaderClass; import IE.Iona.Orbix2.CORBA.SystemException; public class BankImplementation implements _BankOperations { protected int m_sortCode; protected int m_lastAc; protected LoaderClass m_loader; public BankImplementation (long sortCode, LoaderClass loader) { m_sortCode = sortCode; m_loader = loader; m_lastAc = 0; // Number of previous account. } // Method to implement IDL operation: public _AccountRef newAccount (String name) throws SystemException { String accountNr = new String ("a" + m_sortCode + "-" + (++m_lastAc)); AccountImplementation aImpl = null; try { aImpl = new AccountImplementation (100, name, accountNr); } catch (SystemException se) { ... } _AccountRef aRef = new _tie_Account (aImpl, accountNr, m_loader); return aRef; } }A single loader object, of class
Loader
, is created in the main method and each Account
object created is assigned this loader. Each BankImplementation
object holds its sort code (a unique number for each bank, for example 1234
), and also a reference to the loader object to associate with each Account
object as it is created. Each account is assigned a unique account number, constructed from its bank's sort code and a unique counter value. The first account in the bank with sort code 1234
is therefore given the number "a1234-1
". The marker of each account is its account number, for example "a1234-1
". This ability to choose markers is an important feature for persistence.The statement:
_AccountRef aRef = new _tie_Account (aImpl, accountNr, m_loader);creates a new TIE for the
AccountImplementation
object assigning it the marker accountNr
and the loader referenced by m_loader
. (The bank objects are not associated with an application level loader, so they are implicitly associated with the OrbixWeb default loader.)The server application class must create a loader and a bank; for example:
// Java // In file BankServer.java. package per_simp; import IE.Iona.Orbix2.CORBA.SystemException; import IE.Iona.Orbix2._CORBA; public class BankServer { public static void main (String args[]) { Loader myLoader = new Loader (); BankImplementation bankImpl = new BankImplementation (1234, myLoader); _BankRef bRef; try { bRef = new _tie_Bank (bankImpl, "b1234"); } catch (SystemException se) { ... } ... } }
Loader
can be implemented as follows:
// Java // In file Loader.java. package per_simp; import IE.Iona.Orbix2._CORBA; import IE.Iona.Orbix2.CORBA.SystemException; import IE.Iona.Orbix2.CORBA.LoaderClass; import IE.Iona.Orbix2.CORBA._ObjectRef; class Loader extends LoaderClass { public Loader() { super (true); } public _ObjectRef load (String interface, String marker, boolean isBind) throws SystemException { // We will always have an interface; // but the marker may be the null string. if (marker!=null && !marker.equals ("") && marker.charAt (0)=='a' && interface.equals ("Account")) return AccountImplementation.loadMe (marker, this); return null; } public void save (_ObjectRef obj, int reason) { throws SystemException { String marker = obj._marker (); if (reason == _CORBA.processTermination) { AccountImplementation aImpl = (AccountImplementation)(obj._deref()); aImpl.saveMe (marker); } } }Note that the constructor of
LoaderClass
takes a parameter to indicate whether or not the loader being created should be included in the list of loaders that are tried when an object fault occurs. By default, this value is false
; so our loader class's constructor passes a value of true
to the LoaderClass
constructor to indicate that instances of Loader
should be added to this list.Note that the
AccountImplementation.loadMe()
method assigns the correct marker to the newly created object. If it failed to do this, then subsequent calls on the same object would result in further object faults and calls to the Loader.load()
method.It would have been possible for the
Loader.load()
method to have read the data itself, rather than calling the static method AccountImplementation.loadMe()
. However, to construct the object, load()
would be dependent on there being a constructor on class AccountImplementation
that takes all of an account's state as parameters. Since this will not be the case for all classes, it is safer to introduce a method such as loadMe()
. Equally, Loader.save()
could have accessed the account's data and written it out, rather than calling AccountImplementation.saveMe()
. However, it would then be dependent on AccountImplementation
providing some means to access all of its state.In any case, having
loadMe()
and saveMe()
within class AccountImplementation
provides a sensible split of functionality between the application level class, AccountImplementation
, and the loader class.
// Java _BankRef bRef; _AccountRef aRef; try { // Find the bank somehow; for example, // using _bind(): bRef = Bank._bind (":per_simp", host); aRef = bRef.newAccount ("John"); } catch (SystemException se) { ... }A client that wishes to manipulate an account could execute the following:
// Java // To access Account with account // number "a1234-1". _AccountRef aRef; float bal; try { aRef = Account._bind ("a1234-1:per_simp", host); bal = aRef.balance (); aRef.makeWithdrawal (100.00); } catch (SystemException se) { ... }If the target account is not already present in the server then the
load()
method of the loader object will be called. If the loader recognises the object, it will handle the object fault by re-creating the object from the saved data. If the load request cannot be handled by that loader, then in this case the default loader will be tried next and this will always indicate that it cannot load the object. This will finally result in an INV_OBJREF
exception being returned to the caller.
I1._bind()
but where the object's actual interface is in fact a derived interface of I1
.The class of the target object (and therefore how to load it) must therefore be determined either from the marker passed to the loader, or from the data used to load the target object. The demonstration code for loaders shows the marker names being used to distinguish the real interface of an object (using the first character of each marker). This is a simple approach, but it is probably better in a large system to use some information stored with the persistent data of each object.
It must also be remembered that distinguishing the real interface of an object may not be necessary in all applications and for all interfaces. If the correct interface name is always used in calls to
_bind()
(that is, I1._bind()
is always used when binding to an object with interface I1
) then handling polymorphism is not required. This will also be the case if _bind()
is not used for a given interface: for example, all object references to accounts may be obtained by searching (say, using an owner name) in a bank, rather than using _bind()
.It is however possible that, because of programmer error, the actual interface of the target object is not the same or a derived interface of correct one. This should be detected by a loader.
The information provided to a loader on an object fault comprises the object's marker and the interface name. The loader must be able to find the requested object using these two pieces of information. It must also be able to determine the implementation class of the target objectso that it can create an object of the correct class. Naturally, this implementation class must implement the required interface or one of its derived interfaces.
It is normal, therefore, to use the marker as a key to find the object, and either to encode the target object's implementation class in the marker, or to first find the object's persistent state and determine the implementation class from that data.
For example, a prefix of the marker could indicate the implementation class and the remainder of the marker (or indeed the whole marker) could be the name of the file that holds the object's persistent state.
The following are some of the choices available when using loaders to support persistent objects:
load()
method will be called in turn until one indicates that it can load the target object. Although this approach is simple to implement, such a linear search may be inefficient if a process handles a large number of interfaces. One efficient mechanism is to install a master loader, with which the other loaders can register. Each registration would give some key that indicates when the registering loader's load()
method is to be called by the master loader; a key could be a marker prefix and an interface name. Another reason for having more than one loader is that a process may use objects from separate subsystemseach of which installs its own loader(s). These loaders must of course be able to distinguish requests to load their own objects. Confusion can be avoided if the subsystems handle disjoint interfaces (recall that the interface name is passed to a loader); however, some co-operation between the subsystems is required if they handle the same interfaces, or interfaces which have a common base interface.1 Each subsystem must be able to distinguish its objects based on their markers or their persistent state.
In particular, the subsystems must choose disjoint markers.
23.9 Disabling the Loaders
On occasion, it is useful to be able to disable the loaders for a period. If, when binding to an object, the caller knows that the object will be already loaded if it exists, then it might be worthwhile to avoid involving the loaders if the object cannot be found.
// Java // In package IE.Iona.Orbix2.CORBA, // in class BOA. public boolean enableLoaders (boolean b) throws SystemException;on the
_CORBA.Orbix
object, with a false
parameter value. This returns the previous setting; the default is to have loaders enabled.
I1
is a base interface of I2
and I3
, then the objects of interfaces I2
and I3
must be distinguishable to avoid confusion when "I1
" is passed as an interface name to load()
.