Thursday, 8 October 2009

JNDI in your Applications

Lately I had to make several services to be used from an application server, as a webservice or a service used from BPEL.

I find it convenient to first build the application or service as a java-application and test it from jDeveloper and/or using an ant-script. Although I know that it's possible to test an application from the application server (but never did it myself) it surely is more convenient to have it tested from an ordinary java-application. Also it enables you to use the service in other topologies. Actually it's my opinion that for example a Webservice should not be more then an interface or just another exposure of an existing service or application.

If your service need a database then from an application you would just create a connection using a driver, jdbc-connection and username/password. Probably you would get those values from a property-file. But within an Application Server you would use JNDI to get a DataSource and from that retrieve a Connection.

But the methods that are called from your services within the application server should somehow get a connection in a uniform manner. Because, how would you determine if the method is called from a running standalone application or from an Application Server? The preferred approach, as I see it, is to make the application in fact agnostic of it. The retrieval of a database connection should in both cases be the same. So how to achieve this?

The solution that I found is to have the methods retrieve the database connection in all cases from a JNDI Context. To enable this, you should take care of having a DataSource registered upfront, before calling the service-implementing-methods.

JNDI Context
To understand JNDI you could take a look into the JNDI Tutorial.
It all starts with retrieving a JNDI Context. From a context you could retrieve every type of Object that is registered with it. So it not necessarly be a Database connection or DataSource, although I believe this is one of the most obvious usages of JNDI.

To retrieve a JNDI Context I create a helper class:
package nl.darwin-it.crmi.jndi;
/**
* Class providing and initializing JndiContext.
*
* @author Martien van den Akker
* @author Darwin IT Professionals
*/
import java.util.Hashtable;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import nl.darwin-it.crmi.CrmiBase;

import oracle.jdbc.pool.OracleDataSource;

public class JndiContextProvider extends CrmiBase {
   public static final String DFT_CONTEXT_FACTORY = "com.sun.jndi.fscontext.RefFSContextFactory";
   public static final String DFT = "Default";
   private Context jndiContext = null;

   public JndiContextProvider() {
   }

   /**
    * Get an Initial Context based on the jndiContextFactory. If jndiContextFactory is null
    * the Initial context is fetched from the J2EE environment. Otherwise if jndiContextFactory contains the keyword
    * "Default" (in any case) the DFT_CONTEXT_FACTORY is used as an Initial Context Factory. Otherwise
    * jndiContextFactory is used.
    *
    *
    * @param jndiContextFactory
    * @return
    * @throws NamingException
    */
   public Context getInitialContext() throws NamingException {
       final String methodName = "getInitialContext";
       debug("Start "+methodName);
       Context  ctx = new InitialContext();
       debug("End "+methodName);
       return ctx;
   }

   /**
    * Bind the Oracle Datasource to the JndiName
    * @param ods
    * @param jndiName
    * @throws NamingException
    */
   public void bindOdsWithJNDI(OracleDataSource ods, String jndiName) throws NamingException {
       final String methodName = "bindOdsWithJNDI";
       debug("Start "+methodName);
       //Registering the data source with JNDI
       Context ctx = getJndiContext();
       ctx.bind(jndiName, ods);
       debug("End "+methodName);
   }

   /**
    * Unbind jndiName
    * @param jndiName
    * @throws NamingException
    */
   public void unbindJNDIName(String jndiName) throws NamingException {
       final String methodName = "unbindJNDIName";
       debug("Start "+methodName);
       //UnRegistering the data source with JNDI
       Context ctx = getJndiContext();
       ctx.unbind(jndiName);
       debug("End "+methodName);
   }

   /**
    * Set jndiContext;
    */
   public void setJndiContext(Context jndiContext) {
       this.jndiContext = jndiContext;
   }
   /**
    * Get jndiContext
    */
   public Context getJndiContext() throws NamingException {
   if (jndiContext==null){
       Context context = getInitialContext();
       setJndiContext(context);
   }
       return jndiContext;
   }
}

This package gives a Jndi Context that can be used to retrieve a DataSource kunt ophalen, using JndiContextProvider.getJndiContext().
This method retrieves an InitialContext that is cached in a private variable.
From within the application server the InitialContext is provided by the Application Server.
But when running as a standalone application a default InitialContext is provided. But this one does not have any content. Also it has no Service Provider that can deliver any objects. You could get an InitialContext by providing the InitialContext() constructor with a hash table with the java.naming.factory.initial property set to com.sun.jndi.fscontext.RefFSContextFactory.

The problem is that if you use the HashTable option, the getting of the InitialContext is not the same as within the Application Server. Fortunately if you don't provide the HashTable, the constructor is looking for the jndi.properties file, within the classpath. This file can contain several properties, but for our situation only the following is needed:
java.naming.factory.initial=com.sun.jndi.fscontext.RefFSContextFactory



This Context factory is one of the jndi-provider-factories delivered with the standard JNDI implementation. It is a simple FileSystemContextFactory,a JNDI factory that refers to the filesystem.

Now the only thing you need to do is to bind a DataSource to the Context, right before your call the possible method, for example in the main method of your applicatoin.
That DataSource you should create using the properties from the configuration file.

To create a datasource I created another helper class: JDBCConnectionProvider:
package nl.darwin-it.crmi.jdbc;
/**
 * Class providing JdbcConnections
 * 
 * @author Martien van den Akker
 * @author Darwin IT Professionals
 */
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.SQLException;

import java.util.Hashtable;

import javax.naming.Context;

import javax.naming.InitialContext;

import javax.naming.NamingException;

import javax.naming.NoInitialContextException;

import oracle.jdbc.pool.OracleDataSource;

public abstract class JDBCConnectionProvider {
    public static final String ORCL_NET_TNS_ADMIN = "oracle.net.tns_admin";

    /**
     * Create a database Connection
     * 
     * @param connectString
     * @param userName
     * @param Password
     * @return
     * @throws ClassNotFoundException
     * @throws SQLException
     */
    public static Connection createConnection(String driver, String connectString, String userName, 
                                              String password) throws ClassNotFoundException, SQLException {
        Connection connection = null;
        Class.forName(driver);
        // DriverManager.registerDriver(new oracle.jdbc.driver.OracleDriver());
        // @machineName:port:SID, userid, password
        connection = DriverManager.getConnection(connectString, userName, password);
        return connection;
    }

    /**
     * Create a database Connection after setting TNS_ADMIN property.
     * 
     * @param driver
     * @param tnsNames
     * @param connectString
     * @param userName
     * @param password
     * @return
     * @throws ClassNotFoundException
     * @throws SQLException
     */
    public static Connection createConnection(String driver, String tnsAdmin, String connectString, String userName, 
                                              String password) throws ClassNotFoundException, SQLException {
        if (tnsAdmin != null) {
            System.setProperty(ORCL_NET_TNS_ADMIN, tnsAdmin);
        }
        Connection connection = createConnection(driver, connectString, userName, password);
        return connection;
    }
    /**
     * Create an Oracle DataSource
     * @param driverType
     * @param connectString
     * @param dbUser
     * @param dbPassword
     * @return
     * @throws SQLException
     */
    public static OracleDataSource createDataSource(String driverType, String connectString, String dbUser, 
                                                 String dbPassword) throws SQLException {
        OracleDataSource ods = new OracleDataSource();
        ods.setDriverType(driverType);
        ods.setURL(connectString);
        ods.setUser(dbUser);
        ods.setPassword(dbPassword);
        return ods;
    }
    public static Connection getConnection(Context context, String jndiName) throws NamingException, SQLException {
        //Performing a lookup for a pool-enabled data source registered in JNDI tree
        Connection conn = null;
        OracleDataSource ods = (OracleDataSource)context.lookup(jndiName);
        conn = ods.getConnection();
        return conn;
    }
}


The method createDataSource will create an OracleDataSource. Today with trial&error I discovered that from a BPEL 10.1.2 server, you should get a plain java.sql.DataSource.

Then with JndiContextProvider.bindOdsWithJNDI(OracleDataSource ods, String jndiName) you can bind the DataSource with the Filesystem Context Provider.

Having done that from your implementation code you can get a database connection using JDBCConnectionProvider.getConnection(Context context, String jndiName)

I haven't discovered how yet, but somewhere (in the filesystem I presume) the DataSource is registered. So a subsequent bind will throw an exception. But since you might want to change your connection in your property file, it might be convenient to unbind the DataSource afterwards at the end of the main-method.The method JndiContextProvider.unbindJNDIName(String jndiName) takes care of that.

What took me also quite a long time to figure out is what jar-files you need to get this working when running in a standalone application. Somehow I found quite a lot examples but no list of jar-files. I even understood (wrongly as it turned out) that the base J2SE5 or 6 should deliver these packages.


The jars you need are:
  • jndi.jar
  • fscontext.jar
  • providerutil.jar


You can find them from the BPEL PM 10.1.2 tree and probably also in a Oracle 10.1.3.x Application Server or Weblogic 10.3 (or nowadays 11g). But the JSE ones can be found via
http://java.sun.com/products/jndi/downloads/index.html

There you'll find the following packages:
  • jndi-1_2_1.zip
  • fscontext-1_2-beta3.zip


Conclusion
This approach gave me a database connection within the application server or from a standalone application. The name of the JNDI I fetched from a property file still. That way it is configurable from which JNDI source the connection have to be fetched (I don't like hardcodings).
In my search I found that from Oracle 11g onwards you can use the Universal Connection Pool library: http://www.oracle.com/technology/pub/articles/vasiliev-oracle-jdbc.html to have a connection pool from your application.

No comments:

Post a Comment