Wednesday, December 24, 2008

JNDI lookup on Tomcat and JBoss using Spring

Unfortunately Java EE specs does not specify any standard way of JNDI naming conventions, hence most of the application servers have their own way of JNDI naming. On specifying a Datasource's JNDI name as 'jdbc/myDatasource', Tomcat (6) binds that as 'java:/comp/env/jdbc/myDatasource' while JBoss (5) binds as 'java:/jdbc/myDatasource'. So if one wants to deploy the application on multiple application servers then at least JNDI names has to be changed. I was wondering if Spring Framework has a solution for this, so tried to do lookup using <jee:jndi-lookup ...> as shown below-

<jee:jndi-lookup id="dataSource" jndi-name="jdbc/myDatasource" resource-ref="true"/>
it works well with Tomcat but fails on JBoss. Reason, the JNDI prefix is hard coded in Spring to 'java:comp/env/'.
package org.springframework.jndi;

public abstract class JndiLocatorSupport extends JndiAccessor {

    /** JNDI prefix used in a J2EE container */
    public static final String CONTAINER_PREFIX = "java:comp/env/";
    . . .
}
So it is obvious that it will fail on JBoss. To overcome this limitation, I came up with following solution-
public final class ServiceLocator {

    private static final Map<String, Object> services = new ConcurrentHashMap<String, Object>();

    private static ServiceLocator instance;

    private static Context context;

    static {
        try {
            Context initContext = new InitialContext();
            if (ServerDetector.isJBoss()) {
                context = (Context) initContext.lookup("java:");
            } else if (ServerDetector.isTomcat()) {
                context = (Context) initContext.lookup("java:/comp/env");
            } else {
                context = initContext;
                // or add more 'else if' blocks according to servers to be supported
            }
        } catch (Exception e) {
            throw new ExceptionInInitializerError(e);
        }
    }

    public DataSource getDataSource(String name) throws Exception {
        if (name == null || name.length() <= 0)
            throw new IllegalArgumentException("name");

        if (services.containsKey(name))
            return (DataSource) services.get(name);

        DataSource ds = (DataSource) context.lookup(name);

        services.put(name, ds);
        return ds;
    }
}

Here ServerDetector is a good utility class I found in Liferay code base. Now ServiceLocator takes care of application server specific JNDI prefixes and avoids hassles of changing configuration or code to deploy it on a specific server. In case of Spring instead of using <jee:jndi-lookup ...> one can do the lookup using above-mentioned ServiceLocator as shown below-
    <bean id="serviceLocator" class="com.vinodsingh.ServiceLocator" factory-method="getInstance" />

    <bean id="dataSource" factory-bean="serviceLocator" factory-method="getDataSource">
        <constructor-arg value="jdbc/myDatasource" />
    </bean>
Now entire code (including configuration files) becomes truly portable, at least for JNDI lookups :-)

6 Comments:

Anonymous said...

Or you could use the more standard way of declaring the resource-ref jdbc/myDatasource in web.xml and adding a jboss-web.xml that maps the ref to a global jndi name. Now both containers are using the jndi name java:/comp/env/jdbc/myDatasource.

Anonymous said...

My intention is to make it work across application servers without any changes. The jboss-web.xml is JBoss specific stuff, which I do not want.

Per Lilja said...

You could also use a SimpleJndiBeanFactory where you can specify if you want the java:comp/env to be added or not, with the setResourceRef method, see the Spring API.

andyb said...

Thanks for this post. I was having a nightmare with this issue across different app servers but this has made things very clear to me now. I ended up using the jboss-web.xml solution of mapping the jndi name in Jboss.
Thanks!

Anonymous said...

The first anon commenter actually has the correct answer. You should be defining your resource mappings in your web xml.. then upon deployment to the application server (Jboss, WAS, etc) you should be binding the resource declaration with the app server's specific jndi tree. I know it is all the rage to be more "portable" than jee but writing a class like this is really unnecessary. Of course this requires that you use the indirection already built into the jee spec. I feel like developers don't usually get this one right because they aren't usually in charge of deploying many applications on an app server. The step (putting resources in web.xml) feels unnecessary to developers because they don't really get why you should do it.

Anonymous said...

hey Matt, you are absolutely right. jboss-web.xml really saved my day. Thank you.