少年易酔學難成

IT/技術的な話題について書きます

JShellでライブラリを追加しデバッグする

概要

Javaアプリケーションで問題が発生したときに、状況を再現できる最小のライブラリを使って対話的にデバッグしたくなることがあります。

JShellでは、jshell --class-path <<クラスパス>>でクラスパスを指定することで外部ライブラリを使用できます。が、使いたいライブラリが1つであっても、大抵はそのライブラリの動作させるためにさらに多くのライブラリが必要になります。

ライブラリの依存関係を調べて必要なライブラリを手で全てダウンロードするには人生は短すぎるので、これらの作業はビルドツールを使用した方が良さそうです。

今回はApache Mavenを使ってライブラリをダウンロードし、必要なクラスパスをJShellに渡して起動する方法を調べました。

手順

手順は以下のステップで行います。

  1. 使いたいライブラリを記載したpom.xmlを作成する
  2. 依存ライブラリをダウンロードし、クラスパスを出力する
  3. jshellに2で出力したクラスパスを食わせる

順番に見ていきます。

1. 使いたいライブラリを記載したpom.xmlを作成する

今回は例として、JavaのMinIOクライアントの動作を確認するために以下のようなpom.xmlを作成しました。

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
 
  <groupId>example</groupId>
  <artifactId>miniodebug</artifactId>
  <version>1.0</version>
 
  <dependencies>
    <dependency>
      <groupId>io.minio</groupId>
      <artifactId>minio</artifactId>
      <version>8.2.1</version>
    </dependency>
  </dependencies>
</project>

2. 依存ライブラリをダウンロードし、クラスパスを出力する

依存関係のダウンロードとクラスパスの出力はApache Maven Dependency Pluginを使います。 このプラグインにはbuild-classpathというゴールを設定でき、これはまさにここでやりたいことをやってくれます。 以下のコマンドでdep.txtにクラスパスを出力します。

$ mvn dependency:build-classpath -DincludeTypes=jar -Dmdep.outputFile=dep.txt

dep.txtを確認してみます。

$ cat dep.txt
/Users/tsatow/.m2/repository/org/jetbrains/kotlin/kotlin-stdlib-common/1.3.70/kotlin-stdlib-common-1.3.70.jar:/Users/tsatow/.m2/repository/com/google/code/findbugs/jsr305/3.0.2/jsr305-3.0.2.jar:/Users/tsatow/.m2/repository/com/fasterxml/jackson/core/jackson-annotations/2.11.2/jackson-annotations-2.11.2.jar:/Users/tsatow/.m2/repository/com/google/guava/guava/29.0-jre/guava-29.0-jre.jar:/Users/tsatow/.m2/repository/com/google/guava/failureaccess/1.0.1/failureaccess-1.0.1.jar:/Users/tsatow/.m2/repository/com/fasterxml/jackson/core/jackson-databind/2.11.2/jackson-databind-2.11.2.jar:/Users/tsatow/.m2/repository/com/google/guava/listenablefuture/9999.0-empty-to-avoid-conflict-with-guava/listenablefuture-9999.0-empty-to-avoid-conflict-with-guava.jar:/Users/tsatow/.m2/repository/com/carrotsearch/thirdparty/simple-xml-safe/2.7.1/simple-xml-safe-2.7.1.jar:/Users/tsatow/.m2/repository/com/fasterxml/jackson/core/jackson-core/2.11.2/jackson-core-2.11.2.jar:/Users/tsatow/.m2/repository/com/google/errorprone/error_prone_annotations/2.3.4/error_prone_annotations-2.3.4.jar:/Users/tsatow/.m2/repository/io/minio/minio/8.2.1/minio-8.2.1.jar:/Users/tsatow/.m2/repository/org/checkerframework/checker-qual/2.11.1/checker-qual-2.11.1.jar:/Users/tsatow/.m2/repository/com/squareup/okhttp3/okhttp/4.8.1/okhttp-4.8.1.jar:/Users/tsatow/.m2/repository/org/jetbrains/kotlin/kotlin-stdlib/1.3.72/kotlin-stdlib-1.3.72.jar:/Users/tsatow/.m2/repository/com/google/j2objc/j2objc-annotations/1.3/j2objc-annotations-1.3.jar:/Users/tsatow/.m2/repository/com/squareup/okio/okio/2.7.0/okio-2.7.0.jar:/Users/tsatow/.m2/repository/org/jetbrains/annotations/13.0/annotations-13.0.jar

3. JShellに2で出力したクラスパスを食わせる

あとは先程出力したdep.txtを使ってJShellにクラスパスを食わせるだけです。

$ jshell --class-path `cat dep.txt`
|  JShellへようこそ -- バージョン11.0.6
|  概要については、次を入力してください: /help intro

jshell> import io.minio.MinioClient

jshell> var minioClient = MinioClient.builder().endpoint("http://xxxxx:9000").credentials("user", "pass").build()
    minioClient ==> io.minio.MinioClient@44c03695
...

ここまで来たら、あとは色々試しながらデバッグすることができます。

応用

コンテナ環境で使う

この方法は自分のPCで試すのにも便利ですが、例えばKubernetesクラスタ内で作業を行いたい場合にも便利です。 殆ど手数が増えずにデバッグを開始できますし、試行錯誤の際にいちいちimageをbuildしてpushし直す必要もありません。

$ kubectl run minio-debug -it --rm --restart=Never --image=maven:3.8.3-jdk-11 --command -- bash
root@minio-debug:/# cat <<EOF > ./pom.xml
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
 
  <groupId>example</groupId>
  <artifactId>miniodebug</artifactId>
  <version>1.0</version>
 
  <dependencies>
    <dependency>
      <groupId>io.minio</groupId>
      <artifactId>minio</artifactId>
      <version>8.2.1</version>
    </dependency>
  </dependencies>
</project>
EOF
root@minio-debug:/# mvn dependency:build-classpath -DincludeTypes=jar -Dmdep.outputFile=dep.txt
root@minio-debug:/# jshell --class-path `cat dep.txt`
|  Welcome to JShell -- Version 11.0.13
|  For on introduction type: /help intro
    
jshell> ...

実はこれがやりたかったので今回の方法を調べたのでした。

既存のMavenプロジェクトに適用する

今回はデバッグするのに必要な最小構成のMavenプロジェクトを新規に作成する方法を紹介しました。 しかし、既存のMavenプロジェクトを使って手順の2以降を実行すれば、プロジェクトの依存ライブラリ全てを使える状態でJShellを起動することができます。

Gradleでも同様のことを行う

最近のJavaプロジェクトでは、MavenよりもGradleが使われていることが多いように思います。 既存のプロジェクトへの適用を考えると、Gradleで同様のことを行う方法ができるといいのですが... 残念ながら私はGradleがよくわからないので1、誰か教えてください。

まとめ

今回はMavenでライブラリを追加しJShellでデバッグする方法方法について調べてみました。 問題が起きてる環境がローカルではなくステージングだった場合、デバッグコードをアプリに埋め込んだりデバッグ用のプロジェクトを立ち上げるのは結構大変です。 そういった場合に、問題を再現可能な最小構成でJShellを使って対話的にデバッグできると便利なのではないかと思います。

よりスマート方法があれば、是非コメントで教えてください。


  1. じゃあMavenはわかるのかというと、Mavenも同様にわかりません