Friday, March 30, 2012

How to use JDBC driver in OSGi container

One frequently asked question on the FuseSource forum and several Apache projects like Apache servicemix/Karaf/Camel/CXF which I'm working on is that how to use JDBC driver in OSGi container? why I always encounter ClassNotFoundException?

Users usually use some third party libs like spring-jdbc or commons-dbcp to maintain the connection pool, so they configure it like
<bean id="databaseConnection" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver" />
<property name="url" value="jdbc:mysql://localhost:3306/schema" />
<property name="username" value="***" />
<property name="password" value="***" />


<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driver.class}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.user}"/>
<property name="password" value="${jdbc.password}"/>

Those beans always work in standalone  mode, but when users deploy it in OSGi container like FUSE ESB or Apache Servicemix/Karaf, they will see the JDBC driver ClassNotFoundException.

This error actually is from how OSGi classloader works, say, if bundleA need use some package, it need explicitly import it before hand.

However,  if a bundle(let's say it bundle A) using some code like classForName(jdbc_driver_classname) to init a jdbc driver class, this is a big issue in OSGi world, as if BundleA not import package for jdbc_driver_classname, you'll see the problem. Unfortunately BundleA actually can't know about package name for jdbc_driver_classname beforehand, as the jdbc_driver_classname generally is passed in through configuration during runtime, it can't be determined during build time, so you can't add correct package import when you generate bundle A.

Back to our original issue, spring-jdbc and  commons-dbcp is Bundle A here.

How to resolve it?

Well, we have several options

1.  Put all possible necessary packages in Import-Package OSGi metadata header, this is exactly how spring-jdbc do .  spring-jdbc optionally import packages like;resolution:=optional;version=0,;resolution:=optional;version=0,
    com.mchange.v2.c3p0;resolution:=optional;version="[0.9.1, 2.0.0)",
    oracle.sql;resolution:=optional;version=0,;resolution:=optional;version="[10.5.0, 11.0.0)",
    org.apache.derby.jdbc;resolution:=optional;version="[10.5.0, 11.0.0)",
    org.h2;resolution:=optional;version="[1.0.0, 2.0.0)",
    org.hsqldb;resolution:=optional;version="[1.8.0, 2.0.0)",
    com.sun.rowset;resolution:=optional;version="[1.0.1, 2.0.0)",
pretty much all popular used JDBC drivers is available, but not include  mysql.
Of course we can hack the bundleA to add any Import-Package we want,  it's not a big deal as we always hack, but it's not very graceful, especially sometimes we can't hack  some bundles for license reason.

2. Make bundleA have DynamicImport-Package OSGi headers,  this is the way how Servicemix wrapped commons-dbcp do.It works, but we all know using DynamicImport-Package isn't the best practice in OSGi world. To support JDBC out of the box, we have to do some trade-off.

3. What I prefer to is the fragment bundle way, that said, you create JDBC driver as a fragment bundle, and attach it to bundle A(bundle A is host bundle now), so that all resource from fragment bundle is available for the host bundle. This way you needn't hack bundleA.

Finally, I'd say the fragment bundle is very useful when you try to get around some legacy libs in OSGi, it allow you pass the classname during runtime between bundle(like the JDBC driver we discuss here), it also can allow you to pass some resource files between bundles, I hope I can find some time to elaborate this issue later one.