For the past few years I've participated in several projects
to update existing Java applications. While working on those projects
I often wanted to be able to add new functionality to a class without
recompiling it.
Some of the reasons for this were:
- I didn't have the right to access or modify a class's source
file if, for example, the class was developed by a tier supplier.
- The class was used in many distributed sites and the new
functionality was useful for a small number of sites only.
- There were some persistent objects and I didn't want to
manage several versions of the class or update all the persistent
objects using a batch mechanism.
- I didn't want to touch a tested class that had been working
well for many months or years.
- Other developers were working on the same class at the same
time and we had to coordinate our modifications to avoid any
incompatibility as well as avoid scratching our modifications. It
would have been a lot easier if each developer had worked on a
different source file associated with its own functionality.
After much thought I decided I didn't want a completely
dynamic solution, such as JavaScript, where it's possible to add new
functions to a class at runtime. In that kind of language the
performance is affected too much and the application architecture
becomes difficult to master.
Finally, I created Dynamic Java Binder (DJBinder), a tool
that enables you to dynamically attach interface implementations to a
class without changing its source file. The dynamically attached
interfaces are equivalent to the interfaces listed after the
"implements" keywords.
DJBinder uses the class loading mechanism of the Java 2
platform to create the link between the classes and the interfaces at
runtime.
How DJBinder Works
An Example Application
I have an application that creates objects of type Man and
Woman. Now it's time to add a new print functionality that writes the
name and age of each person to the standard output. To reduce the
regression risk, the solution can't modify the existing application
code.
Listing 1 shows all the definitions used in this example.
Note: These classes have not been designed with the intention of
using any dynamic mechanism.
First I'll present the Java code you need to write to solve
this problem using DJBinder, then I'll explain how DJBinder handles
the internal details.
The following statements write the person "p" data to the
standard output using the Print interface:
Person p = ... ;
((Print) p).toStandardOutput ();
The cast operation is needed because the Print interface
isn't directly implemented by the Person class. The Java compiler
assumes that the Print interface is implemented by a Person subclass,
but we know this isn't true since neither Man nor Woman implements
the Print interface.
The DJBinder method that implements the Print interface
without changing the Person class is a new abstract class named
DI_Person__Print:
public abstract class
DI_Person__Print
implements Print {
public void toStandardOutput() {
Person p= (Person) (Object) this;
System.out.println (p.getName());
}
}
The class name is very important because it creates the link
between the Person class and the Print interface. The class is
abstract since it can't be directly instantiated; its instances are
implicitly created by DJBinder according to the cast operations.
The main line of the toStandardOutput() method is:
Person p= (Person) (Object)this;
This enables you to get the Person object that's associated
with this interface implementation. The cast isn't done directly
because the Java compiler forbids a cast operation between two
classes that don't belong to the same hierarchy. The trick is to use
an Object class, such as bridge, which is legal because the Object
class is the root of all the class hierarchies.
Writing the age is a little more difficult because the age
member has a protected visibility that forbids access from the
DI_Person__Print class.
The solution proposed by DJBinder is to create a new class
with all the protected and private members of the Person class that
may be accessed from the DI_Person__* classes. The name of this class
must be DA_Person. For example, the following class authorizes access
to the age member:
abstract class DA_Person{
public int age;
}
This class makes it possible to use the age member in the toStandard-
Output() method :
public abstract class
DI_Person__Print implements Print
{
public void toStandardOutput() {
Person p = (Person) (Object)
this ;
System.out.println
(p.getName()) ;
DA_Person pp = (DA_Person)
(Object) this;
System.out.println (pp.age) ;
}
}
Notice that the object referred to by the pseudo variable
"this" is cast to the DA_Person class to access the protected member
age. An equivalent alternative would be to cast the variable "p" to
the DA_Person class :
DA_Person pp = (DA_Person)
(Object)p;
The final step of my example is to loop over all the persons
created by the existing application and cast each person to the Print
interface (see Listing 2).
The mechanism implemented by DJBinder is similar to the inner
classes of Java. The DI_* classes are similar to inner classes. The
most important difference is that each DI_* class is defined in a
different source file and can belong to a different JAR file.
The fact that each DI_* class can be extended from another
class offers an elegant way to implement the multiple heritage in
Java without the problems of other languages such as C++.
The code shown in Listing 2 compiles well, but at runtime
there would be several errors if DJBinder didn't use the class
loading mechanism to change the bytecode on the fly.
The class loading mechanism works in the following way: every
time the Java Virtual Machine needs a new class, it requests the
current class loader to return the corresponding bytecode. For
example, the default class loader returns the content of a file named
className.class. This file is searched in the directories listed in the
CLASSPATH environment variable.
The DJBinder class loader works in a different way. First, it
gets the original bytecode using the class loader that's normally
used by the application, changes it, then returns the modified
bytecode to the virtual machine.
The DJBinder tool requires that the JVM uses the DJBinder
class loader. For the console programs you simply need to replace the
traditional command:
virtualMachinePath javaParameters
applicationName
applicationParameters
with
virtualMachinePath javaParameters
amslib.djbinder.Start
applicationName
applicationParameters
The amslib.djbinder.Start class runs your application using
the DJBinder class loader.
For other kinds of Java programs (applets, servlets, JSPs,
etc.) there's a similar procedure based on the runtime environment
characteristics. For example, if you run your Java application server
using the amslib.djbinder.Start class, the beans and servlets become
DJBinder aware and can use any interface that's dynamically
implemented.
The DJBinder class loader changes the original bytecode in four places:
1.The cast operations with an interface of target type
The exceptions eventually thrown by the cast operations are
caught and DJBinder tries to find a class named
DI_objectClass__interface or DI_objectSuperClass__interface. If one correct class is found, a dynamic
interface implementation object (DI_* object) is created and
returned, otherwise the original exception is rethrown. The
objectSuperClass token represents any class in the hierarchy up to
the java.lang.Object.
If the same cast is done twice for the same object, the
previous DI_* object is returned instead of creating a new object.
This allows you to keep some information within the DI_* objects.
In my example this bytecode change prevents the
ClassCastException that should be thrown by the following statement
of the PrintAllPersons class (see Listing 2):
Print d= (Print)e.nextElement();
The object returned by the expression e.nextElement() is an
instance of Person; normally it can't be cast to the Print interface,
but DJBinder creates the corresponding DI_* object, which becomes the
result of the cast operation.
2. The cast operations that have a class as target type
If the casted object is a dynamic interface implementation object, DJ-
Binder replaces the cast by a reference to the main object, that is,
the object that triggered the creation of the DI_* object.
In my example this bytecode change prevents the exceptions
that should be thrown by the statements:
Person p= (Person) (Object)this;
DA_Person pp =
(DA_Person) (Object) this;
3. The references to a DA_* class
The DA_* classes are a buildtime artifice and don't exist in
the runtime environment. All the references to a DA_XXX class are
converted into a reference to the XXX class. In addition, special
methods are added to the XXX class to enable controlled access to the
private and protected members. The access is allowed from the
DI_XXX__* classes only.
In my example this bytecode change enables you to access the
protected age member of the Person class.
4. The instanceof and == operations
These operations are modified to get a behavior that's
compatible with the result of the cast operation: if a cast operation
throws an exception, the corresponding instanceof operation must
return false; if a cast operation works silently, the corresponding
instanceof operation must return true.
Let's assume that the classes X, DI_X__I, and DI_X__J exist
and the "v" variable refers to an object of type X, therefore the
cast operations:
(I) v
(J) v
do not throw an exception, and the operations:
v instanceof I
v == (I) v
(J) v == (I) v
return "true."
These changes create the illusion that the main object and
the dynamic interface implementation objects are a single object.
This simple fact makes programming a lot easier. Figure 1 shows the
software architecture allowed by DJBinder.
Figure 1:
A final requirement of DJBinder is that the DI_* and DA_*
classes must belong to the same package of the main class or to a
subpackage named djbinder. For example, if the Person class belongs
to the acme.applis
package, the DI_Person__Print and DA_Person classes must belong to
the acme.applis package or the acme
applis.djbinder package.
The djbinder subpackage allows you to use the DJBinder
mechanism for classes that belong to a sealed package - a package
that can't be modified.
What do software developers and application administrators
gain from using DJBinder?
For developers the modification unit is usually the source
file. Each source file has a creation and modification date. In large
software projects with several developers it's necessary to establish
a reservation mechanism to prevent two developers from changing the
same file simultaneously, otherwise one of the modifications would be
lost. DJBinder allows you to distribute the tasks and
responsibilities among the developers better, so each developer can
work on a well-defined set of functionalities (grouped within an
interface). This feature facilitates the parallel work of several
developers (increasing the concurrent engineering), thus reducing the
duration of the project.
For customers and administrators the modification unit is the
executable file. In a Java application the executable files are the
JAR files. DJBinder allows you to build JAR files associated with
each functional modification. New functionalities become available
when the corresponding JAR file is added to the runtime environment.
The JAR files already delivered with the previous versions of the
application don't have to be modified. This feature facilitates the
administration and reassures customers who are afraid of regressions.
A feature that can be easily designed using DJBinder is
virtual typing - using a type that's different from the Java class
name to look for a dynamic interface implementation. For example, you
could assign the virtual type "French" to an instance of the Person
class. In that case the Print interface could be implemented by the
DI_French__Print class and include some extra fields. Different
instances of the same Person class could have different virtual
types. This extra flexibility is very useful, especially to
communicate with legacy applications or legacy databases.
Conclusion
DJBinder enables a functionality-driven software architecture
that's particularly flexible and makes the evolution of any Java
application easier, including existing applications, without
recompiling them. DJBinder uses the Java class loading mechanism and
doesn't need any special compiler or virtual machine.
For more information about the class loading mechanism see http://java.sun.com/products/jdk/1.2/docs/api/
java/lang/ClassLoader.html, or
contact me at alvaro.schwarzberg@amslib.com.
You can download a full version of DJBinder from www.amslib.com/djbinder; however, this version can't be used for commercial
development without my written authorization.
Author Bio
Alvaro Schwarzberg works in software development for Dassault
Systemes, and other international companies based in Colombia,
Brazil, and the U.S. He has experience working with multitiered
object-oriented applications mixing Java, C++, and databases.
alvaro.schwarzberg@free.fr
Listing 1: Definitions used in the example
class Person
{
public Person (String n, int a)
{
name=n; age=a; allPersons.add(this);
};
public static Enumeration getAll()
{
return allPersons.elements();
};
public String getName()
{
return name;
} ;
protected static Vector allPersons =
new Vector();
protected String name ;
protected int age ;
}
public class Man extends Person
{
public Man (String n, int a) {super(n, a);}
}
public class Woman extends Person
{
public Woman (String n, int a) {super(n, a);}
}
public interface Print
{
void toStandardOutput() ;
}
Listing 2: Full solution public class
PrintAllPersons
{
public static void main (String [] arg)
{
Enumeration e = Person.getAll();
while (e.hasMoreElements())
{
Print d = (Print) e.nextElement() ;
d.toStandardOutput () ;
}
}
}
abstract class DA_Person
{
public int age;
}
public abstract class DI_Person__Print
implements Print
{
public void toStandardOutput()
{
Person p = (Person) (Object) this ;
System.out.println (p.getName()) ;
DA_Person pp = (DA_Person) (Object) this ;
System.out.println (pp.age) ;
}
}