|
|
Optimize with a SATA RAID Storage Solution
Range of capacities as low as $1250 per TB. Ideal if you currently rely on servers/disks/JBODs
Page 5 of 5
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.
Making use of a good configuration management tool at this point is crucial; check everything in after each successful test, and don't be afraid to go back to the last stable build if it's not working. Ideally, business logic should function independently of user interface, but if you find that the business logic makes calls back to the user interface, you may have to use C function pointers or C++ abstract base classes to make the DLL call the EXE. (Such techniques, while not terribly difficult, are beyond the scope of this article.)
To complete the first native method, we must break out the business logic that loads an Employee record from the database.
We put that into a function in BeetleBL.dll called LoadEmployee, which takes the social security number and returns the name and a success code. Our native method calls that new business
logic function, constructs a new Employee object, and maps the new object to its key so that it won't have to be loaded again.
The code appears below:
_BEETLEJNI_EXPORT_ jobject JNICALL Java_Beetle_Company_loadEmployee(
JNIEnv *env,
jobject obj,
jlong nEmployeeSSN)
{
// Lookup employee information.
char szName[51];
if ( LoadEmployee( (unsigned long)nEmployeeSSN, szName ) )
{
// Create a new Employee object.
jclass clsEmployee = env->FindClass( "Beetle/Employee" );
jmethodID constructor = env->GetMethodID(
clsEmployee,
"<init>",
"(JLjava/lang/String;)V" );
jobject objEmployee = env->NewObject( clsEmployee, constructor,
(jlong)nEmployeeSSN,
(jobject)env->NewStringUTF(szName) );
// Map the new Employee object.
jmethodID mapEmployee = env->GetMethodID(
env->GetObjectClass(obj),
"mapEmployee",
"(JLBeetle/Employee;)V" );
env->CallVoidMethod( obj, mapEmployee,
(jlong)nEmployeeSSN,
objEmployee );
return objEmployee;
}
// Invalid SSN.
return NULL;
}
To create the new object, the native code must identify both the class and the constructor to be used. The FindClass function takes the fully qualified name of the class, using a forward slash to separate package and class names. The GetMethodID function takes class, and the name and signature of the method. (Please see Sidebar 2 for details on method signatures.) The NewObject function takes the class and method ID, as well as all constructor parameters. The constructor we've chosen to call takes
a long integer and a string. We can't just send a character pointer to Java code; we must first package it in a String object.
Fortunately, the NewStringUTF function takes care of that for us.
We want to ensure that only one Employee object is created for each record in the database; to enforce that limit, we map all new Employee objects by their database keys. We write the code to handle the map in Java, so that we can take advantage of the Hashtable class. Our native code must therefore call a Java method on the current Company object. Again, we use GetMethodID to look up that method by class, name, and signature. This time, though, we use CallVoidMethod instead of NewObject because we are not calling a constructor; the object already exists. (Note that if the method returned a value, we would
use a different function, such as CallIntMethod, CallFloatMethod, and CallObjectMethod.)
We have completed our first native method. Compile, set breakpoints, test, and celebrate. We've already experienced three different features of JNI: We have implemented a native method in C++ and called it from Java; we have created a Java object from C++; and we have invoked a Java method from C++. We've seen the central aspects of JNI, and we've only implemented one method! Now it's time to take what we've learned, replicate the pattern (via clipboard code reuse), and implement the next few native methods.
After cruising along for a while, we feel pretty confident in our ability to write native methods. We've loaded employees,
customers, invoices, and commissions -- all with very similar code. But then we hit a snag; when a representative makes a
sale, Aphid must create a new Beetle.TransactionInvoice object, populate its fields, and commit it to the database. That's simple, right? Just write commit as a native method and extract the information from the object's fields.
But it's not that straightforward. The date is stored in a Date field, and we have to go through the GregorianCalendar class to gain access to the month, day, and year values. The code is simple enough to write in Java, but not in C++. We would
have to use FindClass, NewObject, GetMethodID, and CallIntMethod repeatedly, creating a jumbled mess of unreadable code, which would become a maintenance nightmare.
Relax. If it's easier to write something in Java, then write it in Java. Instead of implementing commit directly in native code, we can have it do all the footwork and then call a native method. In that case, we define the native
method as follows:
native private static long createInvoice(
int nDay, int nMonth, int nYear, long nCustomerID, float fAmount );
In C++, we declare this:
extern "C"
{
_BEETLEJNI_EXPORT_ jlong JNICALL Java_Beetle_TransactionInvoice_createInvoice(
JNIEnv *env,
jclass cls,
jint nDay,
jint nMonth,
jint nYear,
jlong nCustomerID,
jfloat fAmount );
}
Notice that we made the native method static. Since we have gathered all the necessary information in our Java method, the
native method doesn't need access to the TransactionInvoice instance. The only impact that has on our C++ code is the second parameter; instead of getting a jobject, our native code gets a jclass. As it happens, we don't even need that parameter because the body of the native method makes no JNI calls at all! We've
taken a potentially hairy situation and turned it into our simplest native method yet.
We're on the home stretch of our application development, and so far we've vaulted every hurdle. One last requirement stands
in our way to the finish line. Naturally, the sales representatives are interested in the status of their commissions, and
they would like to know when they will get paid. Beetle already generates a list of payable commissions for a report; we want
to use that same business logic to display commission status in Aphid. Past experience has taught us that we can write most
of the code in the Java method Beetle.Company.updateCommissionStatus. For a given employee, that method loops through all commissions and updates their status. To obtain the list of payable
commissions, we define the following native method, static to the Beetle.Company class:
native private static long[] getCommissionsPayable( long nEmployeeSSN );
That is the first native method we've encountered that needs to work with an array. Arrays aren't too problematic as long
as you follow a few rules. In native code -- just as in Java -- arrays must be constructed before they are used. They must
be given a size at construction time and cannot be resized afterward. To access the array's elements, native code must first
call Get<type>ArrayElements to obtain a pointer, then call Release<type>ArrayElements when it is finished with the pointer. The following native implementation obeys these rules:
_BEETLEJNI_EXPORT_ jlongArray JNICALL
Java_Beetle_Company_getCommissionsPayable(
JNIEnv *env,
jclass cls,
jlong nEmployeeSSN)
{
// Query for all unpaid commissions.
unsigned long *pCommissionID = NULL;
int nCount = GetCommissionsPayable(
(unsigned long)nEmployeeSSN, &pCommissionID );
// Allocate an array of longs.
jlongArray arrayID = env->NewLongArray( nCount );
jlong *pID = env->GetLongArrayElements( arrayID, NULL );
// Copy the data.
int nIndex;
for ( nIndex = 0; nIndex < nCount; nIndex++ )
{
pID[nIndex] = pCommissionID[nIndex];
}
// Unlock the arrays.
env->ReleaseLongArrayElements( arrayID, pID, 0 );
FreeCommissionsPayable( pCommissionID );
return arrayID;
}
Notice the call to FreeCommissionsPayable. That is a BeetleBL.dll function, not a JNI function. Because BeetleBL.dll allocates the array of commission IDs, it must also be the one to free the array.
We've done it! The application is complete; the sales representatives are eager to start using Aphid; and we can tell the Oracle migration team that we've done half their work for them by separating the Beetle business logic. Let's exercise the system to see what we've accomplished.
First, let's recap our machine configuration. We have an Access database for which we've created an ODBC data source named
BEETLE. We've built all of the C/C++ binaries into the devpath directory, which we have added to the path. Finally, we have built the Java binaries, the main one being Aphid\App.class, into Java\classes directory.
Now, let's run Aphid. Launching it either from the Java IDE or by using javaw.exe, we see the main split-screen appear. On top, it lists all account activities for our test representative, Jill. At the bottom,
it lists Jill's commissions, and we see that the commission from her ABC sale is still payable. Now we launch Beetle by running
BeetleUI.exe and let it process Jill's payroll. Returning to Aphid, we choose Refresh from the File menu. (Automatic update, while entirely
feasible, is also beyond the scope of this article.) The indicator shows that Jill's commission is now paid.
Opening "Beetle.mdb" in Access, the Customer table shows that only ABC is a customer, because Jill is still working on her XYZ account. But let's return to Aphid and suppose that Jill makes a sale. We choose New, Sale from the menu, enter a modest sale for ,000 to XYZ, and press OK. Now when we look at the Customer account, we see that XYZ has been added. Running the GL query, we can also see a ,000 invoice, a 00 commission, and no double entry -- the accountants will be pleased!