2

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

Community
  • 1
  • 1
Timofei Davydik
  • 7,244
  • 7
  • 33
  • 59

1 Answers1

6

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.

Subodh Joshi
  • 12,717
  • 29
  • 108
  • 202
Mark Thomas
  • 16,339
  • 1
  • 39
  • 60
  • I know that. As you can see from the code, when common loader fails to load the driver, then context classloader (which refers to webapp classloader) loads the driver successfully. That's why I'm confused – Timofei Davydik Oct 17 '13 at 15:46
  • Hmm. Let me go and dig into the source code. Are you sure you are using Tomcat 7? Exactly which version is it? (Tomcat 8 has the code above but Tomcat 7 doesn't - where did that source snippet come from?) – Mark Thomas Oct 17 '13 at 16:27
  • I'm using 7.0.27 version. Moreover, i've checked version 6 and saw the same part of code there – Timofei Davydik Oct 17 '13 at 17:41
  • Ah - found it. And it was me that made the change to DBCP. Let up update my answer. – Mark Thomas Oct 17 '13 at 18:40
  • Thanks, Mark! I've checked another version (6.0.26) and noticed, that there's no fall back to another class loader. Now it's clear for me :) – Timofei Davydik Oct 18 '13 at 08:18