今日は Akka HTTP Quickstart for Java を楽しんだ

昨日は、Hello Akka 的なやつをやった。

bufferings.hatenablog.com

ので、今日は HTTP のやつを触った。内容が分かってるわけじゃなくて、とりあえず動いたなぁ、っていう感じ。雰囲気は理解した。

Akka HTTP Quickstart for Java · Lightbend Tech Hub

3日で忘れるので、自分用メモ。

まずは動かす

また今日も Records を使おうと思ってるから、Java 15 で動くように build.gradle を少し書き換えてから実行。

ユーザーを追加して:

❯ curl -H "Content-type: application/json" -X POST -d '{"name": "MrX", "age": 31, "countryOfResidence": "Canada"}' http://localhost:8080/users       
{"description":"User MrX created."}

❯ curl -H "Content-type: application/json" -X POST -d '{"name": "Anonymous", "age": 55, "countryOfResidence": "Iceland"}' http://localhost:8080/users
{"description":"User Anonymous created."}

❯ curl -H "Content-type: application/json" -X POST -d '{"name": "Bill", "age": 67, "countryOfResidence": "USA"}' http://localhost:8080/users
{"description":"User Bill created."}

追加されたことを確認:

❯ curl -s http://localhost:8080/users                                                                                                  
{"users":[{"age":31,"name":"MrX","countryOfResidence":"Canada"},{"age":55,"name":"Anonymous","countryOfResidence":"Iceland"},{"age":67,"name":"Bill","countryOfResidence":"USA"}]}

OK。3人登録されてるなー。面白いなー。

じゃ、個別で取得してみよう:

❯ curl -s http://localhost:8080/users/Bill       
{"empty":false,"present":true}

お。思ってたんと違う。ソースを見てみたら、Optional のフィールドだからだな:

  public final static class GetUserResponse {
    public final Optional<User> maybeUser;
    public GetUserResponse(Optional<User> maybeUser) {
      this.maybeUser = maybeUser;
    }
  }

ふーん。それはとりあえずおいといて、DELETEもチェックしといた。これはOK。

❯ curl -X DELETE http://localhost:8080/users/Bill
{"description":"User Bill deleted."}

❯ curl -s http://localhost:8080/users            
{"users":[{"age":55,"name":"Anonymous","countryOfResidence":"Iceland"},{"age":31,"name":"MrX","countryOfResidence":"Canada"}]}

Optional を扱えるように

さて。Optional を扱えるようにしようかな。そもそも Optional をフィールドに持ってそれを返すのってありなのかなぁ?とは思うけど、Quickstart だからそのまま進めよう。

Jackson の ObjectMapper に設定を追加すれば良いんだよなぁ。と思いつつ Akka のそれっぽいドキュメントを読んでみる。

Serialization with Jackson • Akka Documentation

ん? Jdk8Module はデフォルトで有効っぽい?

The following Jackson modules are enabled by default:

akka.serialization.jackson {

  # The Jackson JSON serializer will register these modules.
  jackson-modules += "akka.serialization.jackson.AkkaJacksonModule"
  # AkkaTypedJacksonModule optionally included if akka-actor-typed is in classpath
  jackson-modules += "akka.serialization.jackson.AkkaTypedJacksonModule"
  # AkkaStreamsModule optionally included if akka-streams is in classpath
  jackson-modules += "akka.serialization.jackson.AkkaStreamJacksonModule"
  jackson-modules += "com.fasterxml.jackson.module.paramnames.ParameterNamesModule"
  jackson-modules += "com.fasterxml.jackson.datatype.jdk8.Jdk8Module"
  jackson-modules += "com.fasterxml.jackson.datatype.jsr310.JavaTimeModule"
  jackson-modules += "com.fasterxml.jackson.module.scala.DefaultScalaModule"
}

でも、実際は効いてないわけだから・・・。ふーむ。コード見てみるか。

この Quickstart で使ってる ObjectMapper はこれだな?

akka-http/Jackson.java at master · akka/akka-http · GitHub

  private static final ObjectMapper defaultObjectMapper =
    new ObjectMapper().enable(MapperFeature.SORT_PROPERTIES_ALPHABETICALLY);

設定が入ってるようには見えないな。

ということは、Akka HTTP と akka-serialization-jackson は関係ないってことか?と思ってうろうろしたら見つけた:

akka-http-jackson and akka-serialization-jackson shall share the same jackson-json ObjectMapper. · Issue #3168 · akka/akka-http · GitHub

なるほど。akka-serialization-jackson には設定が色々入ってるけど、Akka HTTP はそれとは関係ないってことか。

akka-serialization-jackson の ObjectMapper を使う

なら、Akka HTTP デフォルトの ObjectMapper を使うのをやめて、Jdk8Module を設定したものから Marshaller を作れば良さそう。

  // Quickstart ではこっちを使ってる
  public static <T> Marshaller<T, RequestEntity> marshaller() {
    return marshaller(defaultObjectMapper);
  }

  // こっちを使って設定済みの ObjectMapper を渡せば良さそう
  public static <T> Marshaller<T, RequestEntity> marshaller(ObjectMapper mapper) {
    return Marshaller.wrapEntity(
      u -> toJSON(mapper, u),
      Marshaller.stringToEntity(),
      MediaTypes.APPLICATION_JSON
    );
  }

自前で用意するの面倒くさいから、さっきの akka-serialization-jackson から ObjectMapper を取ってこれないかなぁ?と思ったら取れた。でも、これが正しいやり方なのかどうかはよく分かってない。

    if (!(system.classicSystem() instanceof ExtendedActorSystem extendedActorSystem)) {
      throw new IllegalArgumentException("Failed to get object mapper.");
    }
    objectMapper = new JacksonObjectMapperProvider(extendedActorSystem).getOrCreate("akka-http", Optional.empty());

使ってる system が akka-actor-typed のもので、キャストできないって怒られたからしばらく悩んで、ぼーっと ActorSystem のメソッドを眺めてたら classicSystem() ってメソッドがあって、これでキャストできた。

ところで、折角なので instanceof のパターンマッチング使ってみた。Records 同様 Java 15 で Second Preview みたいで、Java 16 で正式に導入されるのかなぁ。

JEP 394: Pattern Matching for instanceof

こういうときは if の後ろのブロックでこの変数を使えるっての面白いなー。良い。

https://docs.oracle.com/javase/jp/15/docs/specs/patterns-instanceof-jls.html#jls-6.3.2

はい動いたー。

❯ curl -s http://localhost:8080/users/Bill                                                                                                           
{"name":"Bill","age":67,"countryOfRecidence":"USA"}

Records に変えてみる

昨日に引き続き、メッセージ用のオブジェクトを Records に変更してみる。けど、HTTP のレスポンスに使ってるオブジェクトもあるから「はて?Jackson は Records に対応しているのだろうか?」と思いつつ実行。

ちなみに IntelliJ IDEA さん、よくできる子やで。

static final がつくけど要らないかなぁって思うのでそれだけ消しておいた。

からの実行!

❯ curl -H "Content-type: application/json" -X POST -d '{"name": "Bill", "age": 67, "countryOfResidence": "USA"}' http://localhost:8080/users  
The request content was malformed:
Cannot unmarshal JSON as User

お。JSON の unmarshal ができないって言われた。ふむふむ。Jackson の Records 対応を見てみる。

Jackson 2.12 Most Wanted (5/5):. Support ‘java.lang.Record’ | by @cowtowncoder | Dec, 2020 | Medium

2.12 で対応したみたいだな。Akka HTTP は 2.10 を使ってるみたいだから、2.12 を使うようにしてみるか。

    implementation 'com.fasterxml.jackson.core:jackson-databind:2.12.1'

からの再実行:

❯ curl -H "Content-type: application/json" -X POST -d '{"name": "MrX", "age": 31, "countryOfResidence": "Canada"}' http://localhost:8080/users
{"description":"User MrX created."}

❯ curl -H "Content-type: application/json" -X POST -d '{"name": "Bill", "age": 67, "countryOfResidence": "USA"}' http://localhost:8080/users
{"description":"User Bill created."}

❯ curl -H "Content-type: application/json" -X POST -d '{"name": "Anonymous", "age": 55, "countryOfResidence": "Iceland"}' http://localhost:8080/users
{"description":"User Anonymous created."}

❯ curl -s http://localhost:8080/users                                                                                                  
{"users":[{"name":"MrX","age":31,"countryOfResidence":"Canada"},{"name":"Bill","age":67,"countryOfResidence":"USA"},{"name":"Anonymous","age":55,"countryOfResidence":"Iceland"}]}

❯ curl -s http://localhost:8080/users/Bill       
{"name":"Bill","age":67,"countryOfResidence":"USA"}

❯ curl -X DELETE http://localhost:8080/users/Bill
{"description":"User Bill deleted."}

❯ curl -s http://localhost:8080/users            
{"users":[{"name":"MrX","age":31,"countryOfResidence":"Canada"},{"name":"Anonymous","age":55,"countryOfResidence":"Iceland"}]}

動いたー。面白かったヽ(=´▽`=)ノ