In the context of a nice mini-framework (just a few classes actually) for integration tests for Java using
JUnit,
DBUnit (for database initialisation and comparison against expected data) and
Spring, I found myself confronted with somewhat less elegant requirement of having to specify the JDBC Driver jar filename in the
CLASSPATH and having to change it depending on the target database. Whereas changing the JDBC database URL, username and password is easy (just using
Spring's PropertyPlaceholderConfigurer and a
.properties file), changing the
CLASSPATH is annoying, because it has to be changed in the Eclipse build path (e.g. using a classpath variable) as well as in the build configuration (be it
Ant or
Maven).
I wanted to specify the filename of the JDBC
Driver jar in the same
.properties file as the JDBC URL, username and password.
While this may sound trivial to some, it isn't, because you cannot load jars at runtime using the default
ClassLoader.
This code snippet shows how one can (ab)use the
URLClassLoader to load jars at runtime.
But the problem is that I didn't want to set a new default
ClassLoader nor pass JVM parameters at startup, i.e. use the pristine Eclipse and Ant/Maven environment and do it purely through Java code at runtime.
The trick is quite simple, actually:
1) write a
delegate implementation of JDBC's
java.sql.Driver class, that passes each method call to a
static (singleton)
Driver
2) use the name of the delegate class above as the name of the JDBC Driver class
3) set the static Driver in the delegate Driver class above to an instance of the real JDBC Driver class
Let's start with the delegate:
package sample;
import java.sql.Connection;
import java.sql.Driver;
import java.sql.DriverManager;
import java.sql.DriverPropertyInfo;
import java.sql.SQLException;
import java.util.Properties;
public class DelegateDriver implements Driver {
static {
try {
DriverManager.registerDriver(new DelegateDriver());
} catch (SQLException e) {
throw new RuntimeException(new StringBuffer()
.append("failed to register ").append(DelegateDriver.class.getName())
.append(" with the JDBC ").append(DriverManager.class.getName())
.append(": ").append(e.getMessage()).toString(), e);
}
}
public static Driver DELEGATE = null;
private static Driver getDelegate() {
if (DELEGATE == null) {
throw new IllegalStateException("delegate driver not set");
}
return DELEGATE;
}
public boolean acceptsURL(String url) throws SQLException {
return getDelegate().acceptsURL(url);
}
public Connection connect(String url, Properties info) throws SQLException {
return getDelegate().connect(url, info);
}
public int getMajorVersion() {
return getDelegate().getMajorVersion();
}
public int getMinorVersion() {
return getDelegate().getMinorVersion();
}
public DriverPropertyInfo[] getPropertyInfo(String url, Properties info)
throws SQLException {
return getDelegate().getPropertyInfo(url, info);
}
public boolean jdbcCompliant() {
return getDelegate().jdbcCompliant();
}
}
And here is the class to use to configure the actual JDBC Driver class as well as the JDBC driver jar file, shaped as a Spring-ready singleton bean:
package sample;
import java.io.File;
import java.net.URL;
import java.net.URLClassLoader;
import java.sql.Driver;
public class JDBCDriverLoader {
private String jdbcDriverClass;
private File jdbcDriverFile;
/** Configure using Spring or Java code: */
public void setJdbcDriverFile(File jdbcDriverFile) {
this.jdbcDriverFile = jdbcDriverFile;
}
/** Configure using Spring or Java code: */
public void setJdbcDriverClass(String jdbcDriverClass) {
this.jdbcDriverClass = jdbcDriverClass;
}
public void initialize() throws Exception {
// TODO throw IllegalStateException if jdbcDriverFile or jdbcDriverClass is null
DelegateDriver.DELEGATE = (Driver) new URLClassLoader(new URL[]{}, this.getClass().getClassLoader()) {{
// Have to use a subclass because addURL() is protected.
// See http://snippets.dzone.com/posts/show/3574
addURL(new URL("jar:file://" + jdbcDriverFile.getPath() + "!/"));
}}.loadClass(jdbcDriverClass).newInstance();
}
}
All you need to do now is to use
sample.DelegateDriver as the name of the JDBC Driver class (e.g. in your Apache Commons DBCP connection pool).
Jumping through those hoops is needed because it's a different
ClassLoader.
I'll leave the rest of the glue as an exercise to the reader
;)Labels: java