Search code examples
javaspring-bootjdbc

Microsoft JDBC driver not loading in Spring Boot


I have a Spring Boot app which creates a database connection. It's fairly standard code:

SQLServerConnection sqlServerConnection = (SQLServerConnection) DriverManager.getConnection(url);

I checked and the URL is correct and contains the right connection string.

It used to work fine, but after some configuration updates (changed the Gradle version and some other seemingly unrelated things) the code above throws the exception that the driver can't be found.

After some experimenting the fix turned out to be adding this line before trying to establish a connection:

Class.forName("com.microsoft.sqlserver.jdbc.SQLServerDriver");

Some background - the app is Spring Boot based. We generate a fat jar and I checked it - it contains the Microsoft driver jar:

$ jar tf app.jar | grep jdbc
BOOT-INF/lib/mssql-jdbc-12.4.1.jre11.jar

My question is - what's happening? From my understanding the Class.forName() is not required since 1.6.


Solution

  • The reason the Class.forName is not required, is that JDBC drivers are loaded via the SPI system. This involves asking all entries on the classpath for the resource META-INF/resources/java.sql.Driver, which is a text file listing one (or more) fully qualified driver class per file. It then loads them all. As if you wrote Class.forName() for each of them.

    Given that your code works fine if you add the forName but it does not if you don't, your fatjarring has messed up that file. The obvious explanations are:

    • The fatjar thing attempted to 'fatjar' (e.g. name-mangle) that file. The file name has to be exact, if you mangle it, the SPI system doesn't work. The fatjar thing would mess up any SPI loaded anything. You may want to file a bug or give up on it - too many caveats to be useful.
    • The fatjar thing knows it can't do that, but you have multiple JDBC drivers that are fatjarred together and one arbitrary one 'wins' - it's the last one in the lineup. Because they all overwrite that file, so only one file survives. The fatjar tool is also broken in this case; it should know that files in META-INF/services need to be merged (simply combine them all into a single file).

    In general fatjarring is mostly useless. jars can have a classpath. fatjars are big, it takes some time to make them (big files tend to, after all), and thus not using them makes it easier to manage your deps and makes deployments go way, way faster. There is some slight benefit in having a single jar file you can ship around but this is barely a benefit - generally 'here is a jar file, you figure out how to run it' is not appropriate and hasn't been for a decade: The 'modern' (at this point 10 years old) recommended client-side java deployment setup is that you ensure an appropriate JVM is available. Thus, the distro model of 'ship one single jar around' is deprecated as you can't ensure a JVM that way.

    TL;DR: Stop fatjarring, it caused this, and many other problems, and adds nothing useful. Alternatively, find a less bad tool / manually fix its mangling of the JDBC driver SPI file / configure it so it no longer does this / remove all but one JDBC driver from the fatjar stack / just roll with the Class.forName thing... and know that anything else SPI based is probably also broken. I hope you didn't try to add any security or charset providers...