Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

DriverManager doesn't see dependency in gradle custom plugin's task

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!

like image 601
Evgeny Karpov Avatar asked Jan 04 '23 19:01

Evgeny Karpov


1 Answers

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'
like image 150
Vampire Avatar answered Jan 13 '23 11:01

Vampire