I'm writing some gradle plugin and now I've faced with a problem that DriverManager doesn't see JDBC driver defined in buildscript dependency:
I have next build.gradle file:
buildscript {
repositories {
mavenCentral()
}
dependencies {
classpath("com.h2database:h2:1.4.196")
}
}
plugins {
id "someplugin"
}
apply plugin: 'groovy'
And when I invoke command defined in task that extends DefaultTask
DriverManager.getConnection("jdbc:h2:mem:", "sa", "")
I receive exception
No suitable driver found for jdbc:h2:mem:
There is no problem when I unit-test these classes - it happens only when I invoke plugin's task that invokes DriverManager.getConnection.
What am I missing here? Thanks!
Well, there is an answer on why DriverManager
does not work like you use it and how to make it work and there is an answer on how to use SQL from Groovy (which is the base for Gradle scripts) properly. I'll start with the proper way to use SQL from Groovy:
Properly use SQL from Gradle / Groovy:
Adding the driver to the buildscript classpath is not enough unfortunately to use the Groovy Sql class, you need to add the driver to the right classloader instead, otherwise it will not work properly.
Besides adding the driver to the JVMs ext dir, you can do this dynamically like this:
configurations { jdbc }
jdbc 'com.h2database:h2:1.4.196'
def sqlClassLoader = Sql.classLoader
configurations.jdbc.each { sqlClassLoader.addURL it.toURI().toURL() }
Sql.withInstance('jdbc:h2:mem:', 'sa', '', 'org.h2.Driver') {
it.execute 'your sql here'
}
Properly using the DriverManager
:
You cannot use the DriverManager
properly like you did, because of the dynamicness of Groovy. In the method you used, the DriverManager
tries to dynamically find the caller class from the callstack and then uses the classloader of that class for finding the database driver. With Groovy, this is some dynamic proxy class that is found and thus the database driver is not found in its class loader.
If you instead give the DriverManager
the proper caller class explicitly like DriverManager.getConnection("jdbc:h2:mem:", [user: "sa", password: ""] as Properties, getClass())
, it works properly. Alternatively it would also work to not give any caller class like DriverManager.getConnection("jdbc:h2:mem:", [user: "sa", password: ""] as Properties, null)
in which case the current threads context class loader is used, which is also fine.
Problem with automatic driver lookup in Gradle:
When the class DriverManager
is loaded, it scans through the system property jdbc.drivers
and all services that provide the java.sql.Driver
service. It iterates through those found classes and instantiates them. The drivers themselves are usually responsive for registering themselves with the DriverManager
which they can do at this point in time for being available for automatic lookup later on like I advised above.
Now the problem is, if you are using the Gradle Daemon (which is the default by now), and run any build in that daemon that loads the DriverManager
(e. g. in your former tries), then the class is already loaded. If you then later on add the buildscript dependency to H2 (or run a build where it is present after one where it not was present but DriverManager
got loaded), then the class is already loaded and does not lookup drivers that are now in the classpath.
Here some possible workarounds without explicitly naming the driver class, from probably worst to probably best solution:
- Disable the daemon for your build and make sure noone using your build enables the daemon. This is hard to control and enforce and degrades build performance.
- Use the private method
DriverManager.loadInitialDrivers()
before calling getConnection
to make sure the lookup is done again and the H2 driver added automatically. Better but uses a private method.
- Use the
ServiceLoader
yourself to load all Driver
classes in the classpath to make them self register with the DriverManager
like ServiceLoader.load(Driver.class).collect()
before the getConnection
call. Probably the most elegant solution.
Here some possible workarounds with explicitly naming the driver class:
- Simply load the class before using
getConnection()
to make it self-register with org.h2.Driver.toString()
- Simply load the class before using
getConnection()
to make it self-register with Class.forName 'org.h2.Driver'