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:
Properly using the
DriverManager
:You cannot use the
DriverManager
properly like you did, because of the dynamicness of Groovy. In the method you used, theDriverManager
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 likeDriverManager.getConnection("jdbc:h2:mem:", [user: "sa", password: ""] as Properties, getClass())
, it works properly. Alternatively it would also work to not give any caller class likeDriverManager.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 propertyjdbc.drivers
and all services that provide thejava.sql.Driver
service. It iterates through those found classes and instantiates them. The drivers themselves are usually responsive for registering themselves with theDriverManager
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 butDriverManager
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:
DriverManager.loadInitialDrivers()
before callinggetConnection
to make sure the lookup is done again and the H2 driver added automatically. Better but uses a private method.ServiceLoader
yourself to load allDriver
classes in the classpath to make them self register with theDriverManager
likeServiceLoader.load(Driver.class).collect()
before thegetConnection
call. Probably the most elegant solution.Here some possible workarounds with explicitly naming the driver class:
getConnection()
to make it self-register withorg.h2.Driver.toString()
getConnection()
to make it self-register withClass.forName 'org.h2.Driver'