🐘

Gradle Shadow Pluginで作成したfat/uber JARで、複数のJDBCドライバがロードできない

2021/04/19に公開

fat/uber JAR は、依存するライブラリを含めて一つのjarにまとめたものです。一つのjarにまとめることで配布/実行が簡単になります。
これを実現するGradleのプラグインとして、Gradle Shadow Pluginがあります。

先日Gradle Shadow Pluginを使っていて嵌ることがあり、この記事に残すことにしました。

問題内容

依存ライブラリとして複数データベース(PostgreSQL/MySQL)のJDBCドライバを含めたプロジェクト(ちょっとしたツール)を作っていたところ、IDEではそれぞれのデータベースへのコネクションが問題なく取れるけど、fat JARにしてから実行すると、片方しか取れないといった現象が発生しました。

fat JARで実行すると、該当するJDBCがないといったエラーが発生します。

java.sql.SQLException: No suitable driver found for jdbc:mysql://192.168.33.11/testdb
	at java.sql.DriverManager.getConnection(DriverManager.java:689)
	at java.sql.DriverManager.getConnection(DriverManager.java:247)

JDBCドライバがロードされる仕組み

問題内容を説明する前に、JDBCドライバがロードされる仕組みを説明しておきたいと思います。

JDBCドライバですが、Service Provider Interface(SPI)という仕組みを使ってロードされます。
大昔(Java1.4とか)は、Class.forName("org.postgresql.Driver")で明示的にロードする必要がありましたが、SPIを使ってロードされるようになったことで不要となりました。

SPIでは、サービスとなるインタフェース(JDBCだとjava.sql.Driver)を定義し、そのインタフェースを実装したものがサービスプロバイダとなります。
サービスプロバイダをロードする仕組みはjava.util.ServiceLoaderにて提供されています。

サービスプロバイダは、jar内のMETA-INF/services/配下にプロバイダ構成ファイルを配置することによって識別されます。
ファイルの名前は、サービスの名前(インタフェースの完全修飾名)となります。そこにプロバイダとなるクラスの完全修飾名を書くことで、識別、ロードされるようになります。

JDBCドライバだと、各jarファイル内にMETA-INF/services/java.sql.Driverというファイルが用意され、ここに各JDBCドライバの実装クラスが記載されることになります。
たとえばPostgreSQLのJDBCドライバだと、下記のような内容が記載されており、jarファイルをクラスパスに配置しただけで、PostgreSQLのJDBCドライバの実装となるorg.postgresql.Driverがロードされることになります。

org.postgresql.Driver

このようにSPIを使うとプラグイン的な機能を簡単に提供できるので、アプリケーションで独自のプラグイン機構を提供する時なども利用されています。

問題原因

Gradle Shadow Pluginでは、各jarの内容を一つのjarファイルとしてまとめる際に、デフォルトではMETA-INF/services/配下のファイルを上書きするような動作になっています。
そのため、各JDBCドライバのjar内にあったMETA-INF/services/java.sql.Driverが上書きされてしまい、上書きされてしまったものはロードできなくなってしまっていました。

解決策

設定でMETA-INF/services/配下のファイルを上書きするのではなく、マージするといった機能があり、これを使うことによって回避できます。

build.gradleで下記のように設定します。

// Merging Service Files
shadowJar {
  mergeServiceFiles()
}

上書きされてうれしいシチュエーションが思いつかないので、デフォルトマージで良いのでは、、と思いましたが、残念ながらそのようになっていませんでした。
また忘れたころに、同じ問題に嵌りそうな気がしています。

Discussion