2
votes

I'm trying to use tomcat jdbc connection pool and I define it in my application context.xml file.

<Context>
    <Resource auth="Container" name="jdbc/iup" type="javax.sql.DataSource"
              maxActive="300" maxIdle="30" maxWait="20000"
              username="${db.username}" password="${db.password}" driverClassName="net.sf.log4jdbc.DriverSpy"
              url="jdbc:log4jdbc:sqlserver://${db.server};databaseName=${db.name}"/>
</Context>

Class net.sf.log4jdbc.DriverSpy is defined in log4jdbc4-1.2.jar, which is placed in my application lib folder. And it works fine for me. But here it's said, that jar with driver class should be placed ONLY in tomcat lib folder.

Tomcat uses it's BasicDataSource class to load driver:

if (driverClassName != null) {
            try {
                try {
                    if (driverClassLoader == null) {
                        Class.forName(driverClassName);
                    } else {
                        Class.forName(driverClassName, true, driverClassLoader);
                    }
                } catch (ClassNotFoundException cnfe) {
                    driverFromCCL = Thread.currentThread(
                            ).getContextClassLoader().loadClass(
                                    driverClassName);
                }
            } catch (Throwable t) {
                String message = "Cannot load JDBC driver class '" +
                    driverClassName + "'";
                logWriter.println(message);
                t.printStackTrace(logWriter);
                throw new SQLNestedException(message, t);
            }
        }

driverClassLoader is null, and driver class is trying to be loaded via Class.forName(driverClassName). As I understand, in this this case driver class is being loaded with the same classloader instance as BasicDataSource is. And this is StandardClassLoader, which will load the class if my jar is in tomcat libs. In my case exception is thrown and Thread.currentThread().getContextClassLoader() is used, which is WebappClassLoader instance and can load classes from webapp lib and it does. So I'm confused. Why is it said, that I must put my driver class in tomcat libs if I use datasource from container resource.

Please, explain, thanks

1

1 Answers

6
votes

Tomcat automatically adds container managed connection pooling to every resource of type jaxaz.sql.DataSource. The library that provides this pooling (a package renamed version of Commons DBCP) is loaded by the shared class loader (which in a default config is the same as the common loader). The pooling implementation needs to be able to load the configured JDBC driver and the shared (and common) loader does not have visibility into the web application class loaders. Therefore, the JAR with the JDBC driver needs to be in the $CATALINA_BASE/lib directory so it can be loaded.

However, as of r754776, DBCP falls back to the Thread's context class loader if it can't load the specified Driver. If the thread context class loader is set to the web application's class loader then the Driver can be loaded. This change was included in DBCP 1.3 and 1.4 onwards which means it was included in 5.5.30 onwards, 6.0.27 onwards and every 7.0.x release. It will also be in every 8.0.x release.

A fairly unscientific look at query volumes with MarkMail suggests that there has been some reduction in ClassNotFoundException questions on the Tomcat users mailing list but it could equally be down to people being more aware of the issue.

I guess the fundamental question is is this reliable? If the DataSource is always instantiated when the thread context class loader is the web application class loader then it will be reliable. Access to those resources is through JNDI and that depends on the thread context class loader being correctly set. If it isn't - JNDI won't be able to locate a web applications resources. On that basis this should just work.

Global resources will (obviously) still need the JDBC driver to be located in $CATALINA_HOME/lib

The scenario that is likely to cause problems is if the JDBC driver JAR is present in $CATALINA_HOME/lib and WEB-INF/lib. If the web application attempts to cast to a database specific object then things are going to fail as that would be an attempt to cast a class loaded by the shared loader to a class of the same name loaded by the web application class loader which will always fail.

So in short:

  • The long standing advice not to have the JDBC driver in WEB-INF/lib and $CATALINA_[HOME|BASE]/lib stands
  • As of 6.0.27 onwards, it is possible to package the JDBC driver in the web application and everything will still work.

Apologies for the initial wrong/incomplete answer. It isn't the first time I have completely forgotten about a commit I made and I suspect it won't be the last.