2019 年の Java アドベントカレンダーの2日目のエントリーです。この前に書いた ServiceTalk の記事の続きです。今回は ServiceTalk の上で JAX-RS アプリケーションを動かします。
続きを読むEnumeration を使いやすくする(Iterable にする/Iterator にする)
TL で Enumeration
が使いづらいというツイートが流れてたら、鮮やかに解決するツイートも流れてきた。
なるほどなー、これ実際にはそのままダイレクトには書けないですけど、明示的にキャストしてあげれば書けますね
— がくぞ (@gakuzzzz) November 28, 2019
for (String name : (Iterable<String>) request.getHeaderNames()::asIterator) {
...
}https://t.co/NTL5iUFzL9
Enumeration<String> enumeration = ...;
Iterable<String> iterable = enumeration::asIterator;
asIterator
というメソッドが Java9 から生えていたらしい。
残念なことに 拡張 for 文のソースの部分では推論がうまくできないため、コンパイルエラーになる
for (String s: enumeration::asIterator) { System.out.println(s); }
JJUG CCC 2019 Fall で発表した #jjug_ccc
表記の通り、 JJUG CCC 2019 で発表してきた。
もともと発表資料を Key Note で作っていたものの、前日に予行演習した際に 90 分かかってしまい(セッションの時間は45分)、資料中に無駄な要素と有用な要素が分かちがたく結びついているなどで資料を削るのも難しいと判断、前日の 22:30 頃に 0 から資料を作り直すことにしました。極力短時間で書きたいため、レイアウトなどを考えなくてよいマークダウンで記述した結果、 GitHub のレポジトリーに発表資料ができる次第になりました。
発表中のツイートをまとめたのがこちらです
いつも参加された勉強会の発表をイラストでまとめてくださる 中山さん さんにまとめていただけました。光栄です!!!!
セッションについて
セッションでは時間が足らずにいくつか説明できなかったものがあるので、手短に雑に解説します
3-4. 概念に適切な例外を用いる
ざっくりいうと、
- モデルとかレポジトリーインターフェースに実装依存の例外クラスを使わない(垂直方向に適切な例外を使う)
- 業務内容的におかしな例外は投げない(ユーザーの認証モジュールが在庫モジュールの例外を投げてたらおかしい)(水平方向に適切な例外を使う)
3-6. 本当に必要なところだけに例外を使う
内容的にはバリデーションで引っかかったものについては例外を使わないですが、遠巻きに例外を使うなという過激なことを言っています
業務において期待した動作が不可能なのは
- 入力が業務の入力値的に許容されない(バリデーション)
- 業務的に操作が許可されない(パーミッション等DBへのアクセスが必要なものなど)
の 2 パターンだと思われますが、これらの違反状態は機能仕様として想定の範囲内だと思われます。したがって、これらは決して想定できない例外状態ではないので、例外で表現しないほうがよいだろうという主張をする予定でした。
発表の後に、技術的な例外と業務的な例外をうまく区別して扱う方法がないかという質問をうけて、 3-4. の回答をしましたが、こちらの業務には例外使わなくてもよいのではないかという回答でも良かったかと思っています。
最後に参考文献について
もともとは自分の経験を元に例外について喋っていこうと考えていました(CFPの時点では)。ところが、僕は語彙が圧倒的に足りないのか「例外とは何か?」という根本的な質問に対する適切な回答ができませんでした。 『Effective Java 第3版』 を読み返してみると、「契約による設計」というキーワードがそこかしこに見られ、また以前参加した java-ja の 「LOG.debug("nice catch!")」 での t_wada さんの資料と西尾さんの記録(はてな)を読み返すと、「契約」「事前条件」「事後条件」といったキーワードもあり、バートランド・メイヤーの 『オブジェクト指向入門 第2版 原則・コンセプト』 を読むことにしました。
『オブジェクト指向入門』には例外の定義と例外処理の原則が書かれており、 Java や Golang などの実装から想定される例外に慣れていると考えもしないようなことを反省できます。その最たる例は「回復」というキーワードだと思っています。僕らの大好き『Effective Java』の項目70(第2版は項目58)に 「回復可能な状態にはチェックされる例外」という見出しが設定されており、これをもとにそこかしこで検査例外と非検査例外の使い分け条件として「回復」可能性を根拠としています。ところが、この回復についてちゃんと定義している人は『Effective Java』を含めてほとんどいないのです。『オブジェクト指向入門』では「失敗するケース」に「回復」について書いてあります。
ルーチンの実行中に例外が起き、ルーチンがその例外から回復しない場合に限り、そのルーチンコールは失敗となる。
『オブジェクト指向入門 第2版 原則・コンセプト』p.530
これは逆に言うと、ルーチンから回復するとルーチンは成功になるということを意味します。ここから制御された例外処理の原則(『オブジェクト指向入門』p.534)をあわせて考えると、「回復」というのが「阻害されてしまった事後条件の達成を、再び獲得しようとする試み」と定義できる。また、「12.4.2 ハードウェアあるいはオペレーティングシステムの例外から回復する」(p.542)では浮動小数点数の 0 付近の値の除算命令の実行に対して、不可能な場合にはデフォルト値 0
を返すという回復の具体例があげられる。これはデフォルト値を返すことによって事後条件を達成するという「回復」を示す例である。
ここからは、芋づる式に例外処理ですべきことが明確かつ原理に即して(経験とか適当な推量ではなく)説明できるようになります。広義の事後条件の中にはクラスの不変条件が含まれており(『オブジェクト指向入門』p.472)、結果が成功であれ失敗であれルーチンの終了には不変条件の再構築が求められる(同p.549)(。『Effective Java』では、このことを項目76(旧64)「エラーアトミック性に務める」と言い直しています)。こうして、発表資料の 1-4.例外処理でやるべきこと や 3-5. 検査例外の使い分け の記述を書いていきました。
今回の発表資料は自分の無知からはじまって、『オブジェクト指向入門』を頼りに構成していきました。勢いと勘で書けばそれほど時間かからずに資料は作れたと思います。しかし今回はわからないことへの不安が強かったため、レンガを一つ一つ積み上げていくようなやり方で資料を書き上げました。そのため非常に時間がかかってしまい、所属する会社の皆様に協力をしていただくなどしました。大変感謝しております。
Apple 製の ネットワークアプリケーションフレームワーク ServiceTalk
2019/11/6 くらいに apple が servicetalk という Netty ベースのネットワークフレームワークを発表していたので、この数日間さわっていました。
さわった印象は、SpringBoot(Web) や micronaut 、 Helidon 、 Quarkus といった軽量 Web フレームワークに比べると明らかに機能が少ない(ネットワークフレームワークなので
DI はない)ですが、 Netty をコントロールする煩わしさを解消(ByteBuf
は Buffer
というインターフェースで抽象化していて、さらにユーザーはそれを気にしなくて良い)して、より上位のレイヤーでアプリケーションを組めるようなフレームワークになっています。
Hello World
それでは簡単なアプリケーションを書いてみます。
Gradle は次のような感じにします。なお、まだ Maven Central にはないようなので、 Bintray から jar を取得します。
plugins { id 'java' id 'application' } repositories { jcenter() maven { url "https://dl.bintray.com/servicetalk/servicetalk/" } } dependencies { implementation( "org.slf4j:slf4j-api:1.7.28", "io.servicetalk:servicetalk-http-netty:0.20.0", "io.servicetalk:servicetalk-annotations:0.20.0", ) runtimeOnly "ch.qos.logback:logback-classic:1.2.3" testImplementation 'org.junit.jupiter:junit-jupiter-api:5.4.2' testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.4.2' } application { mainClassName = "com.example.App" } test { useJUnitPlatform() }
つづいてアプリケーションクラス
package com.example; import io.servicetalk.concurrent.api.Single; import io.servicetalk.http.api.*; import io.servicetalk.http.netty.HttpServers; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import java.util.stream.Collectors; import java.util.stream.StreamSupport; import static io.servicetalk.http.api.HttpSerializationProviders.textSerializer; public class App { private static final Logger logger = LoggerFactory.getLogger(App.class); public static void main(String[] args) throws Exception { HttpServers.forPort(8080) // (1) .listenAndAwait((ctx, request, responseFactory) -> hello(request, responseFactory)) // (2) .awaitShutdown(); // (3) } private static Single<HttpResponse> hello( HttpRequest request, HttpResponseFactory responseFactory) { Iterable<String> names = request.queryParameters("name"); // (4) String message = StreamSupport.stream(names.spliterator(), false) .collect(Collectors.joining(", ", "Hello, ", ".")); logger.info("message: {}", message); return Single.succeeded( // (5) responseFactory // (6) .ok() .setHeader("Content-Type", "plain/text") .payloadBody(message, textSerializer())); // (7) } }
- http を ポート 8080 で listen するように指定します
HttpService
を指定して、アプリケーションを起動します- アプリケーションが終了するまで待ちます
- リクエストの情報(パス、ヘッダー、クエリ、ボディなど)を
HttpRequest
オブジェクトから取得します - レスポンスは
Single<HttpResponse>
で返します。これは Reactor のMono
のような 0 個または 1個のストリームをあらわすオブジェクトです - レスポンスのデータは
HttpResponseFactory
経由で作成します。おそらく、これはByteBuf
やByteBufAllocator
を直接操作しなくて良いようにするためだと思われます。 HttpSerializer
とレスポンスのオブジェクトをボディに指定します。HttpSerializer
はレスポンスのオブジェクトをBuffer
に流し込むためのオブジェクトです。ここではテキスト(plain/text
)専用のものを使っていますが、他にも json 用などがあります
というわけで、動かすとこんな感じになります
Apple の servicetalk 動かしてみた pic.twitter.com/7f1BDeAuaK
— 石◯王 もちだ (@mike_neck) November 7, 2019
最近の Web アプリケーションフレームワークもこのような感じで書けるので、特に驚くほどではないかと思います…
Spring Integration
先述の通り、 ServiceTalk はネットワークフレームワークなので、 DI 機能はありません。したがって、足りない機能を Spring で補います。
Gradle のスクリプトは、 https://start.spring.io からとってきたものに ServiceTalk のものを追加した感じになります。
plugins { id 'org.springframework.boot' version '2.2.1.RELEASE' id 'io.spring.dependency-management' version '1.0.8.RELEASE' id 'java' } group = 'com.example' version = '0.0.1-SNAPSHOT' sourceCompatibility = JavaVersion.VERSION_12 ext { servicetalkVersion = '0.20.0' } repositories { mavenCentral() maven { url "https://dl.bintray.com/servicetalk/servicetalk/" } } dependencies { implementation ( 'org.springframework.boot:spring-boot-starter', "io.servicetalk:servicetalk-http-netty:$servicetalkVersion", "io.servicetalk:servicetalk-annotations:$servicetalkVersion", "io.servicetalk:servicetalk-http-router-predicate:$servicetalkVersion", "io.servicetalk:servicetalk-data-jackson:$servicetalkVersion", ) testImplementation('org.springframework.boot:spring-boot-starter-test') { exclude group: 'org.junit.vintage', module: 'junit-vintage-engine' } } test { useJUnitPlatform() }
アプリケーションのメインクラスです。 ApplicationContextInitializer
を用意していますが、普通にやる分には必要ないです。
@SpringBootApplication public class App implements ApplicationContextInitializer<GenericApplicationContext> { private static final Logger logger = LoggerFactory.getLogger(App.class); public static void main(String[] args) { SpringApplication.run(App.class, args); } @Override public void initialize(GenericApplicationContext applicationContext) { logger.info("initializing application"); applicationContext.registerBean( HttpSerializationProvider.class, () -> HttpSerializationProviders.jsonSerializer(new JacksonSerializationProvider())); applicationContext.registerBean(MyHandler.class); applicationContext.registerBean(ServiceTalkRunner.class); } }
ServiceTalk の設定・起動は、 CommandLineRunner
の実装クラスで行います。
public class ServiceTalkRunner implements CommandLineRunner { private static final Logger logger = LoggerFactory.getLogger(ServiceTalkRunner.class); private final MyHandler handler; public ServiceTalkRunner(MyHandler handler) { this.handler = handler; } @Override public void run(String... args) throws Exception { logger.info("starting service on http://localhost:8080"); ServerContext serverContext = HttpServers.forPort(8080) .listenStreamingAndAwait(router()); serverContext.awaitShutdown(); } private StreamingHttpService router() { return new HttpPredicateRouterBuilder() .whenMethod(HttpRequestMethod.GET) .andPathMatches(Pattern.compile("/runner/\\p{N}+")) .thenRouteTo((ctx, req, factory) -> handler.handle(req, factory)) .buildStreaming(); } }
ハンドラーは次のような感じ
class MyHandler { static final Logger logger = LoggerFactory.getLogger(MyHandler.class); final HttpSerializationProvider provider; MyHandler(HttpSerializationProvider provider) { this.provider = provider; } Single<HttpResponse> handle(HttpRequest request, HttpResponseFactory factory) { logger.info("request: {}", request.path()); String num = request.path().split("/")[2]; return Single.succeeded( factory.ok() .payloadBody( Map.of("value", Integer.parseInt(num), "message", "hello"), provider.serializerFor(new TypeHolder<Map<String, Object>>(){}))); } }
これを動かしてみると、次のツイートのようになります。
servicetalk x SpringBoot やってみた pic.twitter.com/OqEIdwQaDZ
— 石◯王 もちだ (@mike_neck) November 10, 2019
これ以外にも、 http2 / gRPC / JAX-RS などがあり、まだまだ遊び足りないくらいですが、とりあえず紹介だけしてみました。
なお、僕が遊んでみたコードは次のレポジトリーにあります。