Saturday, January 26, 2008

» Loading a JDBC Driver at runtime

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 urlthrows SQLException {         return getDelegate().acceptsURL(url);     }     public Connection connect(String url, Properties infothrows 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 = (Drivernew 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:

3 Comments:

Blogger Jaan Vajakas said...

It is not documented in the Java API documentation that each Driver must have a no-argument constructor. So, using newInstance() to create the driver might fail.

15:33  
Blogger Jaan Vajakas said...

Sorry, it seems that I was wrong.

The JDBC 4.0 specification contains the sentence "To insure that drivers can be loaded using this mechanism, drivers are required to
provide a no-argument constructor." and also that "The DriverManager.getConnection method has been enhanced to support the
Java Standard Edition Service Provider mechanism.", and according to the JAR File Specification (http://java.sun.com/javase/6/docs/technotes/guides/jar/jar.html#Service%20Provider) each service implementation must have a zero-argument constructor ("The only requirement enforced here is that provider classes must have a zero-argument constructor so that they may be instantiated during lookup.").

However, it is not explicitly stated neither in the JDBC Spec nor in the JAR Spec that the constructor must be public (although java.util.ServiceLoader uses newInstance() to create the instance and throws an exception when the service constructor is not public).

It would be nice of Sun if it were also mentioned in the Javadoc of java.sql.Driver that a Driver implementation must have a public constructor that can be used to instantiate the driver.

14:47  
Blogger Esperan├ža said...

Hi i'm trying to use your code to load mysql connector but i think i'm doing something wrong can you give a concrete example??? i'm using a java aplication not a web application...

13:07  

Post a Comment

Links to this post:

Create a Link

<< Home