IBM Cloud FunctionsでJavaアプリケーションを作る

IBM Cloud Functions自体の説明は[IBM Cloud FunctionsでPHPアプリケーションを作る - Qiita](https://qiita.com/tmsanrinsha/items/7b67e12c8ffb5692eb49)を参照。

## Hello World

[IntelliJ IDEA](https://www.jetbrains.com/idea/)（以降IntelliJと略す）を使って作る。環境はMacを使用している。

IntelliJを開いて「Create New Project」をクリックするか「File」-「New」-「Project」からプロジェクトを作る。

左側のGradleを選択肢、右側のJavaにチェックをしてNext。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/58084/cbf62769-d065-bee9-749f-cc0ebf862a7f.png)

GroupidはMavenリポジトリにデプロイする予定がない場合は空でよい。　 ArtifactIdはプロジェクト名を入れる。ここではsample-javaとする。Versionはデフォルトのままとする。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/58084/eb58cd50-47c0-43e5-0483-19640798e839.png)

Use auto-importにチェックを入れる。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/58084/0c71331e-6ec3-ce8e-1b6a-af375420504e.png)

Project name, Project locationはそのままでFinish。

FinishするとGradleの初期処理が行われ、以下の方なディレクトリ構成になる。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/58084/2fa20bbe-49cd-3619-e6d6-7e80709abff2.png)

src - main - javaのディレクトリを右クリックしてJava Classを作る。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/58084/3ed1bfc5-a03e-57b5-20b0-581e4ef962ef.png)

名前はSampleとする。

[Cloud Functions アクションの作成と起動にあるJavaのサンプル](https://console.bluemix.net/docs/openwhisk/openwhisk_actions.html#openwhisk_actions_java)のコードを参考にして、

```java
import com.google.gson.JsonObject;
public class Sample {
    public static JsonObject main(JsonObject args) {
        String name = "stranger";
        if (args.has("name"))
            name = args.getAsJsonPrimitive("name").getAsString();
        JsonObject response = new JsonObject();
        response.addProperty("greeting", "Hello " + name + "!");
        return response;
    }
}
```

をSample.javaに書き込む。gsonのimportするために、build.gradleのdependenciesにgsonを追加する。

```groovy
version '1.0-SNAPSHOT'

apply plugin: 'java'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

dependencies {
    compile 'com.google.code.gson:gson:2.6.2'  // 追加する
    testCompile group: 'junit', name: 'junit', version: '4.12'
}
```

追加すると、gsonがインストールされる。

ここで、build.gradleについて見ていく。

versionでこのプロジェクトのバージョンを設定する。

```
apply plugin: 'java'
```

でJavaプラグインを設定する[^1]。これでJavaで使う設定やtaskが使えるようになる。IntelliJのプロジェクトの作成で出来たディレクトリ構成が

```
└── src/
    ├── main/
    │   ├── java/
    │   └── resources/
    └── test/
        ├── java/
        └── resources/
```

になるが、これはJavaプラグインのデフォルトのプロジェクトレイアウト

```
src/main/java       製品のJavaソース
src/main/resources  製品のリソース
src/test/java       テストのJavaソース
src/test/resources  テストのリソース
```

になっている。

repositoriesでMavenセントラルリポジトリを使用する設定をする。

dependenciesに依存ライブラリを書く。compileにはコンパイル時の依存関係を、testCompileにはテストのコンパイル時の追加依存関係を書く。
`compile`と`testCompile`で書き方が違っているが、`compile`の書き方は

```groovy
compile group: 'com.google.code.gson', name: 'gson', version: '2.6.2'
```

の省略版。Gradleの設定はGroovyで書かれるが、Groovyはメソッドの丸括弧を省略できるので、`compile`に丸括弧は付けていない。引数がない場合は省略できないので、`mavenCentral()`には付いている。

ここで設定しているversion, apply, sourceCompatibility, repositories, dependenciesは[Project](https://docs.gradle.org/current/dsl/org.gradle.api.Project.html)で確認できる。

では、jarを作る。IntelliJの右側にあるGradleをクリックすると、Gradle projectsウィンドウが表示される。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/58084/cf52c3c7-f0be-902a-48b1-0ca693349bfb.png)

「Tasks」-「build」-「jar」を実行すると`build/libs`に`sample-java-1.0-SNAPSHOT.jar`というjarができる。

もしくはプロジェクトのディレクトリに`gradlew`というファイルがあるので、これを使い

```
$ ./gradlew jar
```

でもjarを作成することができる。`gradlew`はgradle wrapperと呼ばれるもので、これを使うと必要であればgradleのバイナリをダウンロードしてくれる。

jarの中身を確かめてみよう。

```
$ jar -tf build/libs/sample-java-1.0-SNAPSHOT.jar
META-INF/
META-INF/MANIFEST.MF
Sample.class
```

`-t`をアーカイブの中身を表示するオプション、`-f`はファイルの指定。

jarはzip形式で圧縮したファイルである。

```
$ file build/libs/sample-java-1.0-SNAPSHOT.jar
build/libs/sample-java-1.0-SNAPSHOT.jar: Zip archive data, at least v1.0 to extract
```

ちなみに圧縮されたファイルの内容はVimで見ることができる。

```
$ vim build/libs/sample-java-1.0-SNAPSHOT.jar
" zip.vim version v28
" Browsing zipfile /Users/tmsanrinsha/IdeaProjects/sample-java/build/libs/sample-java-1.0-SNAPSHOT.jar
" Select a file with cursor and press ENTER

META-INF/
META-INF/MANIFEST.MF
Sample.class
```

`META-INF/MANIFEST.MF`の行にカーソルを置いて、Enterを押すと

```
Manifest-Version: 1.0
```

のように中身が表示される。

actionをアップロードする。

```
$ bx wsk action create sample-java build/libs/sample-java-1.0-SNAPSHOT.jar --main Sample
```

実行する。

```
$ bx wsk action invoke sample-java --result
{
    "greeting": "Hello stranger!"
}
```

パラメータを設定して実行する。

```
$ bx wsk action invoke sample-java --result -p name reader
{
    "greeting": "Hello reader!"
}
```

## 依存ライブラリがある場合

```
$ jar -tf build/libs/sample-java-1.0-SNAPSHOT.jar
META-INF/
META-INF/MANIFEST.MF
Sample.class
```

でjarの中身を見るとGsonが入ってない。しかし、GsonはIBM Cloud Functionsで用意されているのでなくても動く。

[Building OpenWhisk actions with Java and Gradle - IBM Cloud Blog](https://www.ibm.com/blogs/bluemix/2017/02/building-openwhisk-actions-java-gradle/)に依存ライブラリがある場合の例が載っている。

Sample.javaを以下のように書き換える。これは受け取ったテキストをQRコードにして返すプログラム。

```java
import java.io.*;
import java.util.Base64;
import com.google.gson.JsonObject;
import com.google.zxing.*;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;

public class Sample {

    public static JsonObject main(JsonObject args) throws Exception {
        String text = args.getAsJsonPrimitive("text").getAsString();

        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        OutputStream b64os = Base64.getEncoder().wrap(baos);

        BitMatrix matrix = new MultiFormatWriter().encode(text, BarcodeFormat.QR_CODE, 300, 300);

        MatrixToImageWriter.writeToStream(matrix, "png", b64os);

        b64os.close();

        String output = baos.toString("utf-8");
        JsonObject response = new JsonObject();
        response.addProperty("qr", output);

        return response;
    }
}
```

`build.gradle`を以下のように書き換える。

```groovy
version '1.0-SNAPSHOT'

apply plugin: 'java'

sourceCompatibility = 1.8

repositories {
    mavenCentral()
}

configurations {
    provided
    compile.extendsFrom provided
}

dependencies {
    provided 'com.google.code.gson:gson:2.6.2'
    compile 'com.google.zxing:core:3.3.0'
    compile 'com.google.zxing:javase:3.3.0'
    testCompile group: 'junit', name: 'junit', version: '4.12'
}

jar {
    dependsOn configurations.runtime
    from {
        (configurations.runtime - configurations.provided).collect {
            it.isDirectory() ? it : zipTree(it)
        }
    }
}
```

このgradleの設定は[Fat Jars with Excluded Dependencies in Gradle](http://kennethjorgensen.com/blog/2014/fat-jars-with-excluded-dependencies-in-gradle)を参考にしたもの。

まず、

```groovy
configurations {
    provided
    compile.extendsFrom provided
}
```

で`provided`というものを定義し、[`extendsFrom`](https://docs.gradle.org/current/dsl/org.gradle.api.artifacts.Configuration.html#org.gradle.api.artifacts.Configuration:extendsFrom(org.gradle.api.artifacts.Configuration[]))を使って、compileをprovidedから継承する。

`dependencies`で、jarに含まなくていいものは`provided`で、含むものは`compile`で定義する。`compile`は`provided`に依存するので、コンパイル時は`provided`に書いた依存も入る。

`jar`で、`dependsOn`しているが、これがなくてもうまくいったので、いるのかわからない。

[`from`](https://docs.gradle.org/current/dsl/org.gradle.api.tasks.bundling.Jar.html#org.gradle.api.tasks.bundling.Jar:from(java.lang.Object[]))を使って、jarに含めるファイルを設定している。`from`に含めるのは`configurations.runtime`から`configurations.provided`を引いたもの。[dependency configurations](https://docs.gradle.org/current/userguide/java_plugin.html#tab:configurations)によると`runtime`は`compile`を継承したものなので、`runtime`には`compile`と`provided`の依存がはいり、それを`provided`で引くので、`compile`の依存だけが残る。

jarを作り、

```
$ ./gradlew jar
```

```
$ jar -tf build/libs/sample-java-1.0-SNAPSHOT.jar
```

をすればGson以外のライブラリが入っているのがわかる。

updateして

```
$ bx wsk action update sample-java build/libs/sample-java-1.0-SNAPSHOT.jar --main Sample
```

実行する

```
$ bx wsk action invoke sample-java --result -p text 'Hello world!'
{
    "qr": "iVBORw0KGgoAAAANSUhEUgAAASwAAAEsAQAAAABRBrPYAAABHElEQVR42u3aPRKCMBCG4VBxDI5KjpojWFIR2ezyEx3Uwiyj86ZwgjxU32w2/IT8ybgFGAwGg8F+ik3BxpDnMOapy9H+6GB+TI9yHGSWepkdT8CcWJRgooalF0
h2sEvYHhbsOiY52QUwf5brQnm3vsEasK2JT1Y3b3o9rAGrRlxPvt73wr7NJgumRDTK4Zj6WX5gfkxGEpG0bo4VBPNidfsIS07lNm+ZZZgfs4iWsMoFs9ZNGDLMj+mYrUaShvW8lYI5MItt1JNldtbrYQ3YoV8Ys9u8HubItvYha5a2871uYG5
sf7xhD8D7sr3qYFcwa+JBbzBOw4I1Zw/vImBubH8ZtNZNeLG+wVqw6qFf/UIC5sX4HAIGg8Fgf8Pu1ACAFLpztaIAAAAASUVORK5CYII="
}
```

とちゃんと結果が帰ってくる。これをpngにするには

```
bx wsk action invoke sample-java --result -p text 'Hello world!' | jq -r .qr | base64 -d > qr.png
```

として、

```
$ open qr.png
```

すればQRコードが表示される。

## デバッグ用のメソッドを作る

`public static JsonObject main(JsonObject args)`の`args`をいじれるように、`public static void main(String[] args)`を追加する。

```java
public class Sample {

    public static void main(String[] args) throws Exception {
        String str;
        if (args.length == 0) {
            str = "{\"text\":\"Hello stranger\"}";
        } else {
            str = args[0];
        }
        JsonObject jsonArgs = new JsonParser().parse(str).getAsJsonObject();
        System.out.println(main(jsonArgs).toString());
    }

    public static JsonObject main(JsonObject args) throws Exception {
// ...
    }
}
```

これで、引数が与えられて実行された場合はその第一引数を、なければ、`str`を使って、`public static JsonObject main(JsonObject args)`が実行される。

`public static void main(String[] args)`のメソッドを作ると、IntelliJのコードの左側に▶マークでついて、それをクリックすると実行できるようになる。

引数を設定したい時は「Run」-「Edit Configurations...」から設定できる。しかし、コードの`str`を修正したほうが楽だろう。

Gradleで実行したい場合はbuild.gradleに以下を追加する。

```groovy
// ...
apply plugin: 'java'
apply plugin: 'application' // 追加

mainClassName = 'Sample' // 追加

sourceCompatibility = 1.8
// ...
```
[Application Plugin](https://docs.gradle.org/current/userguide/application_plugin.html)を使うとアプリケーションを実行する`run`というタスクが使えるようになる。メインのクラス名は`mainClassName`で設定する。

これで

```
$ ./gradlew run
```

で実行することができる。Gradle projectsウィンドウのTasks - application - runから実行しても良い。

しかし`gradle task`で直接引数を渡すことが出来ない。[Gradle task - pass arguments to Java application - Stack Overflow](https://stackoverflow.com/questions/27604283/gradle-task-pass-arguments-to-java-application)によると

```groovy
run {
    if (project.hasProperty("appArgs")) {
        args Eval.me(appArgs)
    }
}
```

をbuild.gradleに追加すると

```
$ ./gradlew run -PappArgs='"{\"text\":\"a\"}"'
```

で引数付きで実行できる。

## Gitで管理する

以下のファイルはいらないので`.gitignore`に書く。

```:.gitignore
.gradle/
.idea/
build/
```

インポートする場合は「File」-「New」-「Project from Version Control」でGitHubのURLなどを指定する。Unlinked Gradle project?と出てくるので、Import Gradle projectしてやる。

Version Controlからインポートするのではなく、`git clone`してきて、「File」-「New」-「Project from Existing Sources...」を選んで、「Import Gradle project」でGradleを選んでもよい。

今回作ったサンプル

- https://github.com/tmsanrinsha/ibm-cloud-functions-sample-java

## 参考資料

- [Gradle入門 - 公式ヘルプ | IntelliJ IDEA](https://pleiades.io/help/idea/getting-started-with-gradle.html)
- [Cloud Functions アクションの作成と起動にあるJavaのサンプル](https://console.bluemix.net/docs/openwhisk/openwhisk_actions.html#openwhisk_actions_java)
- [第23章 Javaプラグイン](http://gradle.monochromeroad.com/docs/userguide/java_plugin.html)
- [The Java Plugin - Gradle User Manual](https://docs.gradle.org/current/userguide/java_plugin.html)
- [第8章 依存関係管理の基本](http://gradle.monochromeroad.com/docs/userguide/artifact_dependencies_tutorial.html)
- [Groovyを知らない人のためのbuild.gradle読み書き入門 - Qiita](https://qiita.com/opengl-8080/items/a0bb31fb20cb6505188b)
- [Project - Gradle DSL Version 4.5](https://docs.gradle.org/current/dsl/org.gradle.api.Project.html)
- [Gradle入門 - Qiita](https://qiita.com/vvakame/items/83366fbfa47562fafbf4)
- [Building OpenWhisk actions with Java and Gradle - IBM Cloud Blog](https://www.ibm.com/blogs/bluemix/2017/02/building-openwhisk-actions-java-gradle/)
- [Fat Jars with Excluded Dependencies in Gradle](http://kennethjorgensen.com/blog/2014/fat-jars-with-excluded-dependencies-in-gradle)
- [Write runnable and deployable code for the IBM Cloud Functions platform](https://www.ibm.com/developerworks/library/wa-write-deployable-code-for-openwhisk/index.html)
- [The Application Plugin - Gradle User Manual](https://docs.gradle.org/current/userguide/application_plugin.html)

[^1]: [第23章 Javaプラグイン](http://gradle.monochromeroad.com/docs/userguide/java_plugin.html), [The Java Plugin - Gradle User Manual](https://docs.gradle.org/current/userguide/java_plugin.html)。日本語は古いので注意。