Integrate your legacy applications with JNI

Learn how to integrate a legacy accounting application and a new sales force automation program using JNI

IT professionals must often marry old and new technologies, a challenge requiring the technical knowledge to evaluate which tools are best for a particular project. In this article, I will show you how to integrate a legacy accounting application (which we will call Beetle) and a new sales force automation program (Aphid), using Java Native Interface (JNI) as our tool of choice.

Beetle is a double-entry accounting system that tracks A/R, A/P, and payroll. Sales representatives use Aphid to track their calls, appointments, and sales by account. In accounting, the term double entry means that a credit is balanced by a debit and vice versa, so Beetle's double-entry system ensures that balance is achieved.

In application development, however, double entry means that the same information is manually entered twice, usually on two separate systems. That is not only inefficient but also a basis for human error; our goal is to eliminate that form of double entry by integrating Aphid and Beetle. When a sales representative completes a sale, Aphid will enter the invoice into Beetle, along with the representative's 5 percent commission. To help motivate our sales force, Aphid will display all the representative's commissions, with payment status extracted from Beetle.

Assess the technology

The first step in our development project is to determine the appropriate technology for the job. Because platform, connectivity, and storage decisions are usually predetermined, often mandated from above, our best bet is to use a software model that can perform independent of, or at least very loosely dependent on, the technology. When it comes to legacy applications, we don't have too many options: Beetle was already written in C, using ODBC to manipulate an Access database. A secondary consideration is a movement elsewhere in the company to migrate to Oracle, but that's a decision over which we have no control right now.

In this case, using Java makes the most sense. Because we need to integrate with a C application, C++ might appear an attractive choice at first glance. But the sales representatives travel quite frequently, and they need access, either disconnected or dialed-in from a hotel room, to the application from their laptops. For Aphid, we want to take advantage of Java's power on both the server and the client, and use XML for storage and transmission; the need for an XML-based disconnected client/server solution dictates Java as our tool of choice. When choosing a technology, remember to weigh all the project's requirements and not let one specific detail -- such as C integration -- force us into a particular implementation.

Fortunately, Java Native Interface gives us the flexibility that we require. JNI was developed as a way for Java applications to take advantage of platform-native resources; it allows the JVM to interoperate with applications and libraries written in C, C++, assembly, and several other languages. With JNI, not only can Java call native code but the native code can also create and manipulate Java objects. The result is that JNI allows full language interoperability, making it an ideal candidate for legacy application integration.

To integrate Aphid with Beetle, we could have chosen to access the database directly through JDBC, which would have yielded disastrous consequences. Because much of the business logic that is necessary for proper operation (such as balance-based double entry) is actually encoded within Beetle, circumventing the application would require us to duplicate the business logic in Aphid.

Moreover, the possibility of an Oracle migration means that we can't be sure of the future database schema, and much of the business logic could eventually be pushed down into stored procedures. Coding Aphid directly to the database opens it up to maintenance problems down the road. Going through the JNI interface, though, localizes future changes and isolates Aphid from significant maintenance concerns.

Model the information

Once we have decided upon the technology, the next step is to create the information model, otherwise known as the software model. Beetle is based on a relational database, so we can simply extract the entity relationship diagram in Figure 1. The most important tables are Transaction and Entry. Each transaction consists of two or more entries, each a credit or debit to a particular account. The total credits for any given transaction must equal the total debits, and Beetle's C code ensures that balance is always met.

Figure 1. Beetle's entity relationship diagram. A transaction consists of a number of entries, each a credit or debit to a particular account.

Because Aphid is written in an object-oriented language, its information model is most easily expressed in UML. The model for Aphid appears in the left side of Figure 2. UML's diamond notation denotes ownership instead of aggregation -- ownership is a more rigorous relationship than aggregation, as each object (except a singleton) has exactly one owner. In Aphid, the Company singleton owns everything else.

Figure 2. Aphid's UML structure diagram. The pieces of the Beetle model that are important to Aphid are included, trans- lated from ERD to UML.

To illustrate the relationship between the two applications, the important pieces of the Beetle ERD are translated into UML on the right side of Figure 2. To fulfill the rule of ownership, the Beetle model also includes a Company singleton. (As we will see when we start coding, that extra object will come in quite handy.) Then we map the Aphid objects to their corresponding Beetle objects: a representative is an employee of the company, an account is the rep's term for a customer, and a sale generates both an invoice and a commission.

Aphid has its own information model, separate from -- but connected to -- the Beetle information model. Aphid will need its own storage mechanism as well. We've already decided to use XML for storage, but we still need to figure out how to join Aphid's XML document to Beetle's relational database. Beetle must share a secret about each object in which Aphid is interested, so that Aphid can persist the connection and request the object the next time it is needed. Beetle must share each record's key and, to avoid excessive dependence on Beetle's database schema, Aphid will treat all keys as text strings, or cookies. Aphid will retrieve its cookies from Beetle, return its cookies to Beetle, and make no further assumptions about the information.

Set up the environment

Now that we have the technology assessment and information models in place, we can assemble our development environment. (See Resources to download sample code.) Please note that the attached sample code does not implement Aphid's client/server behavior, its disconnected operation, or its XML transactions. In the sample, both the Aphid GUI and Beetle UI run on the same machine, which would not hold true in actual production.

We now identify four target modules. Both applications run independently, so they are represented as top-level modules, Aphid\App.class and BeetleUI.exe. To access the native code from a standalone Java program, JNI loads a DLL, and the Beetle business logic is extracted to a separate module, BeetleBL.dll. To localize the JNI support, we create a wrapper module, BeetleJNI.dll. The module relationship diagram appears in Figure 3. Notice that only the Beetle business logic module has direct access to the database.

Figure 3: Module relationship diagram for both Aphid and Beetle. Beetle's busi ness logic is extracted so that both appli- cations can share the code.

Because Beetle was originally written in C, we will use C for both BeetleUI.exe and BeetleBL.dll. However, JNI code is slightly more readable when written in C++, so we will use C++ for BeetleJNI.dll. (I used Microsoft Visual C++ 6.0 and Borland JBuilder Foundation 3.5 to create those modules, though I attempted to avoid anything compiler-specific. You will need a C/C++ compiler, a Java 1.2.2 compiler and VM, and ODBC. Create an ODBC data source named BEETLE, which references the database "Beetle.mdb" included in the sample source code.)

Since we are starting with a legacy application, we already have a module called Beetle.exe. First, we want to rename that project BeetleUI.exe to reflect the new separation of user interface from business logic, and then we want to create empty projects for each of the other modules. We design a directory structure that separates C code from Java code, Java source files from Java class files, and the various C modules from the database (please see sample code).

One important consideration when developing a JNI project is that JNI searches for native modules within CLASSPATH. Therefore, we must create a separate directory called devpath for all the C modules and put it in our PATH environment variable. We then instruct the C/C++ compiler to write all output files into devpath. A quick reboot makes the new PATH take effect, and now we are ready to code.

Connect the modules

First, to instruct JNI to load our new module, we must place a call to the static function System.loadLibrary in an appropriate place. Because the class Beetle.Company is the top of the JNI-specific code, putting the call in its static initializer makes sense. The code is as follows:

public class Company {
  ...
  static {
    System.loadLibrary("BeetleJNI");
  }
  ...
}

The Java runtime searches for BeetleJNI.dll in CLASSPATH and automatically matches up all native methods. But wait -- we haven't written any native methods.

Once again, from the beginning: When a sales representative starts Aphid, the Aphid.Representative object must request the Beetle.Employee object to which it is connected. All it has is a cookie. Because Beetle.Company is the owner of Beetle.Employee, Aphid.Representative passes the cookie to Beetle.Company. Beetle.Company must access the business logic layer to complete the request, so that is where we require our first native method. The code on the Java side looks like this:

  native private Employee loadEmployee( long nEmployeeSS );

Of course, we want to hide the fact that the cookie is really the employee's social security number encoded as a long integer from Aphid. We make the method private so that Aphid.Representative can't call it directly -- it has to go through the public method Employee lookupEmployee( String cookie ).

Notice that the Java method does not include an implementation; the implementation appears in the C++ module BeetleJNI.dll. The function must be declared precisely as follows so that JNI can match it to the proper native method declaration:

extern "C"
{
_BEETLEJNI_EXPORT_ jobject JNICALL Java_Beetle_Company_loadEmployee(
  JNIEnv *env,
  jobject obj,
  jlong nEmployeeSSN);
}

A little explanation is in order here: The function name identifies itself as JNI native code (Java_) and identifies the package (Beetle_), class (Company_), and method (loadEmployee). It takes a JNI environment object (JNIEnv *env) (which we will use to communicate with the VM), a reference to the Company instance (jobject obj), and the long parameter that we declared in Java (jlong nEmployeeSSN). Because we declared that the native method returns an Employee object in Java, the C++ function returns jobject. The extern "C" and JNICALL modifiers ensure that the name is not mangled and that the proper calling convention is used. And of course, _BEETLEJNI_EXPORT_ is a macro that instructs the compiler to export the function from the DLL. (See Sidebar 1 for more information about native method naming.)

To make sure that everything is working properly thus far, we create an empty stub for the native code -- just enough to hold a breakpoint. Then we implement the Java code to invoke our native method. (Calling a native method in Java is exactly the same as calling a Java method.) We set up the debugger to run BeetleJNI.dll by way of Aphid. In Visual C++, that can be accomplished in the Debug panel of the Project Settings dialog by specifying "D:\JBuilder35\jdk1.2.2\bin\javaw.exe" as the "Executable for debug session" and "-classpath D:\Dev\Laijni\Java\classes Aphid.App" as the program arguments. (I have the JDK installed in "D:\JBuilder35\jdk1.2.2" and the source code starting in "D:\Dev\Laijni"; your configuration may differ.) We can then set the breakpoint and run the program, and we shouldn't continue until the breakpoint is hit, confirming that we have correctly connected the modules.

Break out the code

With our framework firmly in place, we can start hanging code from the rafters. We must complete the first native method before moving on to the next, which requires that we modify code in all four modules on each iteration. That sequential movement also allows us to compile and test a stable build before moving on to the next change. The new development in Aphid and BeetleJNI.dll is not the risky part -- it's the legacy code that's perilous ground.

The separation of the Beetle business logic from its user interface is the most difficult undertaking in our dev project. Beetle was originally developed as a single standalone application, and calling across module boundaries is not as trivial as calling within one module. The best advice is to take it slow, and don't try to rip out all the business logic at once. Instead, factor out small chunks as you need them, recompiling and testing everything at each step.

1 2 Page 1