: jdbc courier overview

JDBC Courier Technology

On this page we're going to delve a bit deeper into the technologies that underpin JDBC Courier.

Let's take a look at the architecture of an integrated database solution. The image below shows three applications, one written in Java, one in C++, and one in .NET. All three use exactly the same JDBC driver to communicate with a database. The product architecture

The first thing we want to point out is that the Java application (on the left), talks to the database through the standardized JDBC API. When done correctly, all you need to do to switch to a different JDBC implementation is to change some runtime configuration settings.

The Picture on the C++ Side

Now let's take a look at the C++ client application. Please note that all the components within it run in the same process, implying a secure, tight, highly performant, combined solution.

When compared with the Java client, you have the identical picture at the bottom two layers of the stack. This means that—as far as the database is concerned—it appears to be talking to a Java client application.

At the top you have C++ application code instead of Java application code. Of course that's the whole point of the integration: your C++ developers want to write C++ code to communicate with JDBC.

In the middle, JDBC Courier provides a high-quality C++ API to the C++ side and a highly reliable and performant cross-language gateway to the Java side.

You probably want to know what this cross-language gateway looks like at runtime. The diagram below illustrates this.

The product at runtime

We use the Java Native Interface (JNI) to cross the language barrier. JNI has a poor reputation for performance and reliability, which is entirely undeserved as far as its capabilities are concerned and totally deserved as far as its usability is concerned. If you have to write JNI code by hand you invariably end up with crashing and poorly performing applications, both usually due to memory leaks. Yet the Java Runtime integrates with the underlying operating system through JNI, which tells you that it must be possible to use JNI effectively.

We use JNI through a carefully crafted set of utility APIs that itself is used by generated code. You never deal with JNI directly and that is the key to JDBC Courier's speed and reliability.

Let's take a look at some example C++ code that uses the JDBC Courier C++ proxy types to use an embedded hsqldb database.

#include "java_lang_pkg.h"
#include "java_util_pkg.h"
#include "java_sql_pkg.h"
#include "shared.h"
#include <iostream>


extern "C" int main( int argc, char * argv[] ) {
    std::cerr << "Running JDBC query example" << std::endl;

    // parse the commandline arguments to take optional configuration settings into account
    if( !init_java_runtime(argc, argv)) {
        return 1;
    }
    // try to load the Java runtime into the process
    if( !load_java_runtime()) {
        return 1;
    }

    std::cerr << "Loaded Jvm!" << std::endl;
    std::cerr << "About to connect to DB..." << std::endl;

    Connection      conn = null;

    // we execute our entire Java code in a try block and catch any exceptions
    // that might be thrown at this level.  In a more sophisticated application
    // you might have local try/catch clauses in other places.
    try {
        std::string db = std::string( "jdbc:hsqldb:file:" ) + get_example_dir() + "/sampledb";
        String      user = "SA";
        String      password = "";

        conn = DriverManager::getConnection( db.c_str(), user, password );
        std::cerr << "Connected to DB!" << std::endl;

        Statement   stmt = conn.createStatement();
        ResultSet   rs = stmt.executeQuery( "SELECT FIRSTNAME, LASTNAME FROM CUSTOMER" );

        while( rs.next() ) {
            String  firstName = rs.getString( "FIRSTNAME" );
            String  lastName = rs.getString( "LASTNAME" );

            std::cout << firstName.to_chars() << " " << lastName.to_chars() << std::endl;
        }

        rs.close();
        stmt.close();
    }
    catch( Throwable t ) {
        std::cerr << "*** Caught Java exception through Throwable: " << t.toString().to_chars() << std::endl;
    }
    catch( xmog_exception xe ) {
        char * message = xe.get_message_chars();
        std::cerr << "*** Caught framework exception: " << message << std::endl;
        xmog_java_string::free( message );
    }

    if( conn != null ) {
        try {
            conn.close();
        }
        catch(...) {
        }
    }

    std::cerr << "Done with JDBC query example!" << std::endl;
    return 0;
}

This code should strike you as looking remarkably similar to the corresponding Java code.

Your C++ developers can be productive JDBC developers with a few hours of training.

The Picture on the .NET Side

Everything we said about the C++ client holds true for the .NET client as well, of course with your favorite .NET language taking the place of C++. The stack is one layer higher because the JDBC Courier .NET Proxies talk to a managed .NET runtime assembly, which in turn talks to the C++ runtime library from our C++ use case.

This results in the following runtime illustration for the .NET side:

The product at runtime

The hand-off from .NET to C++ is done through P/Invoke, .NET's native escape hatch from the world of managed code.

Now let's look at some .NET code that uses the JDBC Courier .NET proxy types.

using System;
using Java.Util;
using Java.Sql;

namespace Codemesh
{
    namespace Examples
    {
        public class Query : Shared
        {
            public static int Main( string[] args )
            {
                System.Console.WriteLine("Running  JDBC query example");

                // parse the commandline arguments to take optional configuration settings into account,
                // then  try to load the Java runtime into the process.
                // Both of these functions are implemented in the example basetype in 'Shared.cs' 
                if ( !InitJavaRuntime(args) || !LoadJavaRuntime() )
                {
                    System.Console.WriteLine("ERROR: could not set up the Java Runtime!");
                    return 1;
                }

                System.Console.WriteLine("Loaded Jvm!");
                System.Console.WriteLine("About to connect to DB...");

                Connection conn = null;

                try
                {
                    string  db = "jdbc:hsqldb:file:" + ExampleDir + "/sampledb";
                    string  user = "SA";
                    string  password = "";

                    conn = DriverManager.GetConnection(db, user, password);
                    System.Console.WriteLine("Connected to DB!");

                    Statement stmt = conn.CreateStatement();
                    ResultSet rs = stmt.ExecuteQuery("SELECT FIRSTNAME, LASTNAME FROM CUSTOMER");

                    while (rs.Next())
                    {
                        string firstName = rs.GetString("FIRSTNAME");
                        string lastName = rs.GetString("LASTNAME");

                        System.Console.WriteLine( "{0} {1}", firstName, lastName );
                    }

                    rs.Close();
                    stmt.Close();
                }
                catch ( System.Exception se )
                {
                    System.Console.WriteLine(se.ToString());
                }

                if (conn != null)
                {
                    try
                    {
                        conn.Close();
                    }
                    catch ( System.Exception se )
                    {
                        System.Console.WriteLine(se.ToString());
                    }
                }

                System.Console.WriteLine("Done with JDBC query example!");
                return 0;
            }
        }
    }
}
        

That code should look incredibly familiar to anyone who has used JDBC in Java.

Your .NET developers can be productive JDBC users in a matter of hours.

Fastest Possible

At runtime, all these calls happen blazingly fast when compared to alternative integration approaches. JNI has a poor reputation when it comes to reliability and performance. Yet the fact remains that it is the fastest way of crossing the language boundary by far. By using a proven and tested runtime library and generated code to invoke the Java Native Interface we also make sure to obey best practices and avoid many of the performance traps and all of the memory leaks and crashes that plague hand-written JNI code.

As you are talking to an enterprise messaging API, you will hardly see any performance degradation at all in the integrated application. The cross-language call overhead will be negligible in comparison to the time it takes to deliver or receive the message.

It goes without saying that your JDBC vendor can implement a hand-optimized native driver that is faster and has lower latency than what we can do through JNI. If you need the utmost of speed you might not have a choice. Just be aware that you will be facing vendor lock-in and the proprietary native gateway will probably not be feature-complete. Nothing comes without a price.

Most Secure

The in-process nature of the integration also means that you do not have to worry about new security vulnerabilities due to the integration. There are no ports to be opened, firewalls to be configured, or certificates to be installed just to secure the integration solution. As long as you take a little bit of care with your runtime configuration, your threat profile is essentially the superset of a pure native application's and a pure Java JDBC application's threat profiles.

Mature Configuration Framework

The Java Runtime Environment has many options—classpath is just the best known one—and you might have to use many of them to configure your integrated application's JVM. The JunC++ion runtime offers a mature configuration API that allows you to hardcode settings in your application, use configuration files, pick up settings from the environment, or even from shared libraries when they get loaded.