HomeDigital EditionSys-Con RadioSearch Java Cd
Advanced Java AWT Book Reviews/Excerpts Client Server Corba Editorials Embedded Java Enterprise Java IDE's Industry Watch Integration Interviews Java Applet Java & Databases Java & Web Services Java Fundamentals Java Native Interface Java Servlets Java Beans J2ME Libraries .NET Object Orientation Observations/IMHO Product Reviews Scalability & Performance Security Server Side Source Code Straight Talking Swing Threads Using Java with others Wireless XML

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
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 [email protected] 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. [email protected]

	


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) ;
    }
}

  
 

All Rights Reserved
Copyright ©  2004 SYS-CON Media, Inc.
  E-mail: [email protected]

Java and Java-based marks are trademarks or registered trademarks of Sun Microsystems, Inc. in the United States and other countries. SYS-CON Publications, Inc. is independent of Sun Microsystems, Inc.