javacしか知らなかった原始人がGradleというビルドツールで現代人を目指した話

こんにちは、平野です。

この年末年始、ずっと理解したいと思ってたものに色々と触る時間が取れたので、 非常に基本的な事柄ですが、自分なりに吸収したことをアウトプットして行きたいと思います!

ということで、Gradleを使ってJavaのプログラムをビルドして、 ビルドツールとかぜんぜん使ったことない原始人から現代人への進化を目指します! なお、IDEは使わないでコマンドラインからの確認を行います。 (だから現代人になれないのでは?)

スタート地点

この記事の内容を行う前の私の状況です。

  • Javaのプログラムの基本的な部分はまあまあ理解している
  • Javaのビルドと言えばCLASSPATH設定してコマンドラインでjavacするの一択
    • そのあとjarコマンドで固める
  • Eclipseはちょっと使ったことあるけど、CLASSPATHの設定の仕方も思い出せない
  • Gradleは触ったことあるけど、人が書いたものを実行しただけ

また他のビルドツールについてもAnt?Maven?なにそれ、新しいポケモン?な感じです。

Gradleのインストール&検証環境

Macならインストールは以下のコマンドだけのはずです。他は特に行っていません (と思います。実際インストールしたのは結構前なので・・・)。

brew install gradle

検証に使用したバージョンは以下です。

$ gradle -v

------------------------------------------------------------
Gradle 6.0.1
------------------------------------------------------------

Build time:   2019-11-18 20:25:01 UTC
Revision:     fad121066a68c4701acd362daf4287a7c309a0f5

Kotlin:       1.3.50
Groovy:       2.5.8
Ant:          Apache Ant(TM) version 1.10.7 compiled on September 1 2019
JVM:          1.8.0_181 (Oracle Corporation 25.181-b13)
OS:           Mac OS X 10.14.6 x86_64

一番最初のビルド

早速最小限に近いようなプログラムのビルドを行ってみます。 以下のようなJava初学者が最初に書くプログラムを用意します。

public class Test {
    public static void main(String[] args) {
        System.out.println("HelloWorld");
    }
}

(私が知っている唯一の)ビルド&実行の方法は以下のような感じですね。

$ javac Test.java && java Test
HelloWorld

まずはこれをGradleを使って行うことが最初のゴールです。

このファイルを以下のパスに設置します。 (以降、今回の作業の起点となるディレクトリをsample02とした内容になっています)

src/main/java/Test.java

この場所はGradleの決まりごとで、無視しても良いことは特になさそうなので、 初めからこの場所に置いておくことにします。

次にbuild.gradleという名前で、Gradleで行うビルド内容の定義を書いていきます。 まずはこれだけ書きます。

apply plugin: 'java'

最初、「基本的なプログラムのビルドぐらい、設定ファイルになにも書かないでもできないもんかな?」と思ったのですが、

Gradleのコア部分には、実際の自動化に役に立つような機能は含まれていません。 これは意図的なもので、Javaのコードをコンパイルしたりといった便利な機能は、全てプラグインにより追加されます。
Gradle User Guide - 第21章 Gradleのプラグインについて

のように書かれており、Gradleの思想的に、 あくまでもJavaの基本的なコンパイルもプラグインで実行するというスタンスのようです。

また、このファイルの書式はgroovyというプログラミング言語とのことですが、 少なくとも最初はそんなことは意識する必要はなく、 ただの設定ファイルだと思っておいて問題ないかと思います。

閑話休題、ここまでを準備した状態でgradle tasksと実行することで、 実行できるサブコマンドの一覧を得ることができます。

$ gradle tasks

> Task :tasks

------------------------------------------------------------
Tasks runnable from root project
------------------------------------------------------------

Build tasks
-----------
assemble - Assembles the outputs of this project.
build - Assembles and tests this project.
buildDependents - Assembles and tests this project and all projects that depend on it.
buildNeeded - Assembles and tests this project and all projects it depends on.
classes - Assembles main classes.
clean - Deletes the build directory.
jar - Assembles a jar archive containing the main classes.
testClasses - Assembles test classes.

(以下略)

assemblebuildなど、似たようなものが並んでいます。 今回はテストとか全然関係ないのでgradle assembleを行ってみます。

$ gradle assemble

BUILD SUCCESSFUL in 546ms
2 actionable tasks: 2 executed

成功しました。 buildディレクトリが作成され、中にclassとJarファイルが作成されました。

build/classes/java/main/Test.class
build/libs/sample02.jar

期待したものが出力されたので、それぞれ確認してみます。

$ java -cp build/classes/java/main/ Test
HelloWorld
$ jar tvf build/libs/sample02.jar
     0 Mon Dec 16 12:17:30 JST 2019 META-INF/
    25 Mon Dec 16 12:17:30 JST 2019 META-INF/MANIFEST.MF
   514 Mon Dec 16 12:17:30 JST 2019 Test.class
$ java -cp build/libs/sample02.jar Test
HelloWorld

classファイル、Jarファイル共に期待通りの動作になっています。 ということで、もっとも単純なjavac Test.javaと同じことができました!

外部ライブラリ

さて、これだけだとGradleを使う旨味がないので、外部ライブラリを使用してみます。 今回はちょっといきなり飛躍しますが、AWS SDKを使ってみようと思います。

Javaのプログラムとして以下のTest.javaを準備します。

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3Client;
import com.amazonaws.services.s3.model.Bucket;

public class Test {
    public static void main(String args[]) {
        AmazonS3 s3Client = new AmazonS3Client();
        for (Bucket bucket: s3Client.listBuckets()) {
            System.out.println(bucket.getName());
        }
    }
}

S3のクライアントを準備して、バケットの名前一覧を表示するプログラムです。 あとはimportしているクラスが見つかれば正しく実行ができるはずです。

とりあえずそのままビルドして失敗してみます。

$ gradle assemble

> Task :compileJava FAILED
/Users/hirano.shigetoshi/study/20191213-java-gradle/sample02/src/main/java/Test.java:1: エラー: パッケージcom.amazonaws.services.s3は存在しません
import com.amazonaws.services.s3.AmazonS3;
                                ^
(以下略)

クラスが見つからない、という期待通りのエラーが返ってきました。 こういうのを一つ一つ確認するのは大事です。 ということで、AWS SDKのクラスと紐付けてあげなくてはいけません。

もっとも基本的なやり方としてはJarファイルをダウンロードしてきて、 それにCLASSPATHを通して実行する形になります。

しかし、イマドキはそんなことをする必要はないようです! Gradleではbuild.gradleに どのライブラリが必要か、 どのリポジトリから取得するか、 を書く事で、必要なものを自動的に取得してビルドを行うことができます! すごいですねー(普通?)

ということで、build.gradleに以下のように記述します。

apply plugin: 'java'

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.amazonaws:aws-java-sdk:1.11.692'
}

repositoriesにライブラリを見つけに行くリポジトリ、 dependenciesに必要なライブラリを記述します。

mavenCentral()については、他に例えばどんなものが指定できるのかまだ特に調べていませんが、 少なくともここで見つからないものが出てくるまではこのまま特に変更しなくても良さそうです。

dependenciesに書いた内容はこちらに記載されているものをそのまま書いています。 dependenciesに書くものの探し方としては、検索窓にimportするクラスを入れ、 見つからなければ末端(右側)を少しずつ削っていき、 一番それっぽいものを指定すれば大丈夫そうです。

com.amazonaws.services.s3.AmazonS3
com.amazonaws.services.s3
com.amazonaws.services
com.amazonaws              # ここで見つかった

早速これでビルドして行きます。 一応前回の結果のクリアのため、gradle cleanも行います。 gradle clean assembleとするとcleanしてからassembleを行います。

$ gradle clean assemble

> Task :compileJava
注意:/Users/hirano.shigetoshi/study/20191213-java-gradle/sample02/src/main/java/Test.javaは非推奨のAPIを使用またはオーバーライドしています。
注意:詳細は、-Xlint:deprecationオプションを指定して再コンパイルしてください。

BUILD SUCCESSFUL in 687ms
3 actionable tasks: 3 executed

問題なくビルド完了です。 古いクラスを使っている警告が出ていますが、今回は目をつぶってください。

さて、ビルドできたので実行!と行きたいのですが、 残念ながら必要なライブラリが手元にないので、それらがないとやはり実行はできません。

Fat Jarの作成

上記の解決策の一つとして、必要なクラス全部入りのJarを作ってみます。 このようなJarファイルはFat Jarと呼ばれるようです。 そのためにGradle Shadowというプラグインを使います。 使い方として、build.gradleを以下のように書き換えます。

plugins {
    id 'java'
    id 'com.github.johnrengelman.shadow' version '2.0.3'
}

repositories {
    mavenCentral()
}

dependencies {
    implementation 'com.amazonaws:aws-java-sdk:1.11.692'
}

shadowJar {
    zip64 true
}

pluginsは、従来からあったjavaと合わせて、上記のように書き換えます。 これでJavaとGradle Shadowと2つのプラグインを使うという意味になります。

shadowJarzip64 trueの部分は(大事そうに見えるけど)本質ではないです。 そのままビルドすると容量が大きくて以下のように言われるので、つけました。

To build this archive, please enable the zip64 extension.

これで準備完了。ビルドを行います。 従来のgradle assemble等のコマンドではFat Jarは作成できないのでgradle shadowJarと実行します。 このコマンドの存在はgradle tasksすると確認することができます。

Shadow tasks
------------
knows - Do you know who knows?
shadowJar - Create a combined JAR of project and runtime dependencies

では、ビルドします。

$ gradle clean shadowJar

> Task :compileJava
注意:/Users/hirano.shigetoshi/study/20191213-java-gradle/sample02/src/main/java/Test.javaは非推奨のAPIを使用またはオーバーライドしています。
注意:詳細は、-Xlint:deprecationオプションを指定して再コンパイルしてください。

Deprecated Gradle features were used in this build, making it incompatible with Gradle 7.0.
Use '--warning-mode all' to show the individual deprecation warnings.
See https://docs.gradle.org/6.0.1/userguide/command_line_interface.html#sec:command_line_warnings

BUILD SUCCESSFUL in 24s
3 actionable tasks: 3 executed

24秒ほどと、ちょっと時間がかかってビルド完了です。

作成されるJarファイルは変わらずbuild/libs/sample02.jarですが、 容量を見てみると153MBありました。 ちなみにFat Jarでないときは1kB未満だったので、 かなりたくさんのclassが含まれていることがわかります。

さて、実行です。

$ java -cp build/libs/sample02.jar Test
shigetoshi-amazon-s3                    # S3にあるバケット一覧が表示された

単独のjarファイルをCLASSPATHに通しただけですが、 AWS SDKを使用してAWSのリソースにアクセスできました!!

使ってみた感想

最初Gradleを使う前に、自力でJarファイルをダウンロードしてCLASSPATHを通して実行したのですが、 これだと、AWS SDKのクラスが使う別のJarも探してくる必要があり、大変でした。

まぁそれでも5個くらいのJarファイルを用意したことでClassNotFoundは解消されたのですが、 どうやらバージョンが適合していないようで、よくわからない所でエラーが出てしまっていました。 もちろんバージョンも正しいものが準備できたら問題なく動作するのでしょうが、 どのjarが問題なのかを切り分けるのも大変だし、、、と考えていて、

「そもそも(ある程度世の中で知られている)必要なクラスを自力で探してくるのって多分機械的にできるし、 わざわざ人手でやらない方がよくない?」 ということに気づいたのでした(遅っっっっっっっっっっっっっっ!

ビルドツールはこの辺をやってくれるので本当に便利です。 欲しいクラスは数個なのに芋づる式に複数のクラスをたくさん探して来なくちゃいけないのは本当に心が折れますし、 こんなことに時間使いたくないなぁという思いが解消されるのは素晴らしいです!

まとめ

JavaのビルドツールとしてGradleを手探りで触ってみました。

天下り的な所もあるものの、最小限に設定を追加していくことで Gradleの使い方がだいぶわかってきました。

これで少し現代人に近づけた気がします。 やっぱり機械的にできるものはどんどん自動化して行きたいですね!

参照リンク