見出し画像

JAR内のリソースファイルにアクセスする

Javaで開発をしてからは随分と長い。少なくとも自分のITエンジニアとしてのキャリアの中心かそれに近い位置にJavaという言語があることは否めない。
......のだけれど、つい最近初めて知った、知ってそうで知らなかったことがある。
それが、JARやZIPファイル内へのプログラムからのアクセス方法。

通常のリソースファイルをsrc/main/resources直下においたとすると、こんな感じでアクセスできる。

final String targetFilePath = "/xxx.zip";
URL url = this.getClass().getClassLoader().getResource(projectFilePath);
File downloadable = new File(url.getPath());

が、これをJARファイルにしたアプリで実行すると、urlがnullになりNullPointer Exception(NPE)が発生する。
自分の場合はJARにしたファイルをコンテナに乗せて動かすことが前提になっていたので、/deployments/app.jarへのアクションを前提として書く。


final String jarLocationPath = "jar:file:/deployments/app.jar!/xxx.zip";
final String copyTargetPath = "/deployments/xxx.zip";
URI uri = null;

try {
   URL url;
   url = new URL(jarLocationPath + projectFilePath);
   JarURLConnection connection = (JarURLConnection) url.openConnection();
   connection.connect();
   uri = URI.create(url.toString());
} catch (IOException e) {
   e.printStackTrace();
}

Map<String, String> env = new HashMap<>();
env.put("create", "true");
File downloadable = null;

if (Objects.nonNull(uri)) {
   try (FileSystem jarFs = FileSystems.newFileSystem(URI.create(uri.toString().split("!")[0]), env)) {
       // jarファイルは"!"で区切る
       Path pathInJarFile = jarFs.getPath(uri.toString().split("!")[1]);
       downloadable = new File(copyTargetPath);
       Path target = downloadable.toPath();
       // copy from jar inside to local
       Files.copy(pathInJarFile, target, StandardCopyOption.REPLACE_EXISTING);
   } catch (Exception e) {
       e.printStackTrace();
   }
}

FileSystemの箇所はカスタムファイルシステムプロバイダと呼ばれているもので、この仕組みでJARファイルの中身を一つのファイルシステムとして扱うことで、参照を実現している様子。
リソース内のファイル参照は、まだWARが多くの場所で必要とされていた頃にしていただけだったので、全然わからなかったけれど、実はJava 7で導入されていたのね。

次のリンクが参考になった。
JARからファイルが参照できるようになったので、そのままそのPathをnew File()に突っ込んだら参照できなかった。
検索してきて出てきた例が度々、getResource()じゃなくてgetResourceAsStream()だったのはそういうことか......!と最後の最後で合点がいった。

そもそもJAR内部のリソースにアクセスさせているのは一時的な理由に伴うものだったのでさらっと済ませたかったのだけど、思ったよりは調べてテストして、というあたりに時間かかったな。
あと、Zip ファイルシステムプロバイダ、というリンクのJava 11バージョンを見つけることができなかった。その中に記載のコード例が結構役に立ったのだけど。
ただ、カスタムファイルシステムプロバイダに渡している値がどう動いているのかはまだ確認できていないので、また別途。

想定よりも時間がかかったけど、たまに使うかもしれない内容なので解決してすっきり。
ローカルでのテスト(e.g. ./gradlew test)が、上記を対象にすると失敗するはずなので、書くテストの種類にもよるけれど、uriが見つからなかった場合にはresources下の対象ファイルを見に行く、といった書き方などで回避するのが良いかも。

posted on: https://blog.tkhm.dev/2020/10/jar.html