Quarkus+Redisで簡単なアプリケーションを作る

## 概要
最近QuarkusというJavaフレームワークの存在を知りました。
> Quarkus は、Java 仮想マシン (JVM) およびネイティブコンパイルのために作成されたフルスタックの Kubernetes ネイティブ Java フレームワークで、Java をコンテナに最適化し、サーバーレス、クラウド、Kubernetes の各環境で効果的なプラットフォームとして使用できるようにします。
https://www.redhat.com/ja/topics/cloud-native-apps/what-is-quarkus

だそうです。

KubernetesネイティブなJavaフレームワークということで、マイクロサービスが流行りつつある今、個人的には将来性のあるフレームワークなのかな、と思っています。
特徴や詳しい使い方は他の方に任せます。（

こちらの記事では、Quarkusのサイトで紹介されているredis-clientを使ったサンプルアプリケーションの作り方をまとめました。
Redisと接続してCRUDを行うRESTful APIアプリケーションです。

↓のページに記載されている内容を、私の方でいくつか加（減）筆したものになります。
https://quarkus.io/guides/redis



## 手順
### Maven Projectを作る
quickに新規でQuarkusプロジェクトを新規で作成するのであれば、以下のコマンドを実行するだけです。
`-DprojectGroupId`や`-DprojectArtifactId`はお好みで変更してください。

```bash
mvn io.quarkus:quarkus-maven-plugin:1.13.4.Final:create \
    -DprojectGroupId=org.acme \
    -DprojectArtifactId=redis-quickstart \
    -Dextensions="redis-client,resteasy-jackson,resteasy-mutiny" \
    -DnoExamples
cd redis-quickstart
```
※Quarkusのバージョン1.13.4時点では、3.6.2以降のバージョンのmavenが必要です。

`-Dextensions`で、Quarkus+Redisなアプリケーションを実装するのに必要なライブラリが指定されています。

* redis-client: Redisのクライアントライブラリです。
* resteasy-jackson, resteasy-mutiny: REST APIを作るのに必要なライブラリです。

既に作成されているMavenプロジェクトにライブラリを追加する場合は、以下のコマンドを実行するか、

```bash
./mvnw quarkus:add-extension -Dextensions="redis-client"
```
pom.xmlに以下を追記してください。

```xml:pom.xml
<dependency>
    <groupId>io.quarkus</groupId>
    <artifactId>quarkus-redis-client</artifactId>
</dependency>
```

### Redisサーバを用意する
Redisサーバを用意されていない方は、DockerでRedisコンテナを用意しておきましょう。

``` bash
docker run --ulimit memlock=-1:-1 -it --rm=true --memory-swappiness=0 --name redis_quarkus_test -p 6379:6379 redis:5.0.6
```


### アプリケーションの作成
ここからソースコードを触っていきます。
事前に下記コマンドで必要なディレクトリを作成しておきます。

```bash
# redis-quickstart配下で
mkdir -p src/{main,test}/java/org/acme/redis/
```


#### application.properties
application.propertiesにRedisサーバのURLを記述します。

```
quarkus.redis.hosts=redis://localhost:6379
```

#### POJO
ソースコードの解説は時間があるときにやります（やらないとは言っていない…言ってない！）

```java:src/main/java/org/acme/redis/Increment.java
package org.acme.redis;

public class Increment {
    public String key; 
    public int value; 

    public Increment(String key, int value) {
        this.key = key;
        this.value = value;
    }

    public Increment() {
    }
}
```

#### Service
```java:src/main/java/org/acme/redis/IncrementService.java
package org.acme.redis;

import io.quarkus.redis.client.RedisClient;
import io.quarkus.redis.client.reactive.ReactiveRedisClient;
import io.smallrye.mutiny.Uni;

import io.vertx.mutiny.redis.client.Response;

import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;

import javax.inject.Inject;
import javax.inject.Singleton;

@Singleton
class IncrementService {

    @Inject
    RedisClient redisClient; 

    @Inject
    ReactiveRedisClient reactiveRedisClient; 

    Uni<Void> del(String key) {
        return reactiveRedisClient.del(Arrays.asList(key))
                .map(response -> null);
    }

    String get(String key) {
        return redisClient.get(key).toString();
    }

    void set(String key, Integer value) {
        redisClient.set(Arrays.asList(key, value.toString()));
    }

    void increment(String key, Integer incrementBy) {
        redisClient.incrby(key, incrementBy.toString());
    }

    Uni<List<String>> keys() {
        return reactiveRedisClient
                .keys("*")
                .map(response -> {
                    List<String> result = new ArrayList<>();
                    for (Response r : response) {
                        result.add(r.toString());
                    }
                    return result;
                });
    }
}
```

#### Resource


```java:src/main/java/org/acme/redis/IncrementResource.java
package org.acme.redis;

import javax.inject.Inject;
import javax.ws.rs.GET;
import javax.ws.rs.PathParam;
import javax.ws.rs.PUT;
import javax.ws.rs.Path;
import javax.ws.rs.POST;
import javax.ws.rs.DELETE;
import java.util.List;

import io.smallrye.mutiny.Uni;

@Path("/increments")
public class IncrementResource {

    @Inject
    IncrementService service;

    @GET
    public Uni<List<String>> keys() {
        return service.keys();
    }

    @POST
    public Increment create(Increment increment) {
        service.set(increment.key, increment.value);
        return increment;
    }

    @GET
    @Path("/{key}")
    public Increment get(@PathParam("key") String key) {
        return new Increment(key, Integer.valueOf(service.get(key)));
    }

    @PUT
    @Path("/{key}")
    public void increment(@PathParam("key") String key, Integer value) {
        service.increment(key, value);
    }

    @DELETE
    @Path("/{key}")
    public Uni<Void> delete(@PathParam("key") String key) {
        return service.del(key);
    }
}
```

#### Test
こちらはオプショナルです。

```java:src/test/java/org/acme/redis/IncrementResourceTest.java
package org.acme.redis;

import static org.hamcrest.Matchers.is;

import org.junit.jupiter.api.Test;

import io.quarkus.test.junit.QuarkusTest;

import static io.restassured.RestAssured.given;

import io.restassured.http.ContentType;

@QuarkusTest
public class IncrementResourceTest {

    @Test
    public void testRedisOperations() {
        // verify that we have nothing
        given()
                .accept(ContentType.JSON)
                .when()
                .get("/increments")
                .then()
                .statusCode(200)
                .body("size()", is(0));

        // create a first increment key with an initial value of 0
        given()
                .contentType(ContentType.JSON)
                .accept(ContentType.JSON)
                .body("{\"key\":\"first-key\",\"value\":0}")
                .when()
                .post("/increments")
                .then()
                .statusCode(200)
                .body("key", is("first-key"))
                .body("value", is(0));

        // create a second increment key with an initial value of 10
        given()
                .contentType(ContentType.JSON)
                .accept(ContentType.JSON)
                .body("{\"key\":\"second-key\",\"value\":10}")
                .when()
                .post("/increments")
                .then()
                .statusCode(200)
                .body("key", is("second-key"))
                .body("value", is(10));

        // increment first key by 1
        given()
                .contentType(ContentType.JSON)
                .body("1")
                .when()
                .put("/increments/first-key")
                .then()
                .statusCode(204);

        // verify that key has been incremented
        given()
                .accept(ContentType.JSON)
                .when()
                .get("/increments/first-key")
                .then()
                .statusCode(200)
                .body("key", is("first-key"))
                .body("value", is(1));

        // increment second key by 1000
        given()
                .contentType(ContentType.JSON)
                .body("1000")
                .when()
                .put("/increments/second-key")
                .then()
                .statusCode(204);

        // verify that key has been incremented
        given()
                .accept(ContentType.JSON)
                .when()
                .get("/increments/second-key")
                .then()
                .statusCode(200)
                .body("key", is("second-key"))
                .body("value", is(1010));

        // verify that we have two keys in registered
        given()
                .accept(ContentType.JSON)
                .when()
                .get("/increments")
                .then()
                .statusCode(200)
                .body("size()", is(2));

        // delete first key
        given()
                .accept(ContentType.JSON)
                .when()
                .delete("/increments/first-key")
                .then()
                .statusCode(204);

        // verify that we have one key left after deletion
        given()
                .accept(ContentType.JSON)
                .when()
                .get("/increments")
                .then()
                .statusCode(200)
                .body("size()", is(1));

        // delete second key
        given()
                .accept(ContentType.JSON)
                .when()
                .delete("/increments/second-key")
                .then()
                .statusCode(204);

        // verify that there is no key left
        given()
                .accept(ContentType.JSON)
                .when()
                .get("/increments")
                .then()
                .statusCode(200)
                .body("size()", is(0));
    }
}
```
※手順に従って進めていましたが、私の場合は`io.hamcrest`と`io.restassured`のライブラリが見つからず、エラーとなりました。
エラーが発生した場合は、以下を`pom.xml`に追加してください。

```xml:pom.xml
    <dependency>
      <groupId>org.hamcrest</groupId>
      <artifactId>hamcrest</artifactId>
      <version>2.1</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>io.rest-assured</groupId>
      <artifactId>rest-assured</artifactId>
      <version>3.0.0</version>
      <scope>test</scope>
    <dependency>
```

ソースコードの編集は以上です。

### アプリケーションを動かしてみる
下記コマンドでアプリケーションを動かしてみましょう。

```bash
./mvnw quarkus:dev
```
起動すると、下記のようなログがコンソールに表示されます。（1.3秒くらいで起動できてますね。）
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/181402/75327bee-7624-3480-76ee-71c86df7cee5.png)


### 作成したアプリケーションを試してみる
起動が完了すると8080ポートをListenするので、curlコマンドで`/increments`にPOSTしてみましょう。

```bash
curl -X POST -H "Content-Type: application/json" -d '{"key":"first","value":10}' http://localhost:8080/increments 
# -> {"key":"first","value":10}
```

`/increments`をGETすると、登録したキーのリストが返ってきます。

```bash
curl http://localhost:8080/increments
# -> ["first"]
```

キーを指定してGETをすれば、key-valueがJsonで返却されます。

```bash
curl http://localhost:8080/increments/first
# -> {"key":"first","value":10}
```

念のため、Redisサーバにアクセスしてレコードを確認してみます。

```bash
docker exec -it redis_quarkus_test redis-cli 
127.0.0.1:6379> keys *
# -> 1) "first"
127.0.0.1:6379> get first
# -> "10"
```
ちゃんと登録されてますね。

PUTをすると、指定したキーの値を指定した分だけ増加させます。

```bash
curl -X PUT -H "Content-Type: application/json" -d '27' http://localhost:8080/increments/first
curl http://localhost:8080/increments/first
# -> {"key":"first","value":37}
```
DELETEをすると、Redisからレコードを削除します。

```bash
curl -X DELETE  http://localhost:8080/increments/first
curl http://localhost:8080/increments
# -> []
```
※リクエストで指定されたキーがRedisサーバにないときのエラーハンドリングはしていないので、キー指定が間違っていると例外が発生します。

```bash
curl http://localhost:8080/increments/second
# -> java.lang.NullPointerException: Cannot invoke "io.vertx.redis.client.Response.toString()" because the return value of "io.quarkus.redis.client.RedisClient.get(String)" is null
```

### mvnパッケージングと実行
#### JVMで動かす場合

```bash
# パッケージング
./mvnw package

# 起動
java -jar target/quarkus-app/quarkus-run.jar
```
#### Nativeで動かす場合
Quarkusを使う利点の１つである、ネイティブコンパイルを行い起動する方法です。
こちらの方法によるパッケージングには少々時間がかかります。（Intel Corei7-10710uで2分半程度。PCがめっちゃ熱くなりました。）

```bash
# パッケージング
./mvnw package -Pnative

# 起動
./target/redis-quickstart-1.0.0-SNAPSHOT-runner
```
### その他の操作
[こちら](https://quarkus.io/guides/redis)のページでは、上述の手順以外に下記のような手順が掲載されています。

- コネクションのヘルスチェック
- 複数Redisクライアント

### コンフィグレーション
工事中