2019/11/6 くらいに apple が servicetalk という Netty ベースのネットワークフレームワークを発表していたので、この数日間さわっていました。
さわった印象は、SpringBoot(Web) や micronaut 、 Helidon 、 Quarkus といった軽量 Web フレームワークに比べると明らかに機能が少ない(ネットワークフレームワークなので
DI はない)ですが、 Netty をコントロールする煩わしさを解消(ByteBuf
は Buffer
というインターフェースで抽象化していて、さらにユーザーはそれを気にしなくて良い)して、より上位のレイヤーでアプリケーションを組めるようなフレームワークになっています。
github.com
apple.github.io
それでは簡単なアプリケーションを書いてみます。
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)
.listenAndAwait((ctx, request, responseFactory) ->
hello(request, responseFactory))
.awaitShutdown();
}
private static Single<HttpResponse> hello(
HttpRequest request,
HttpResponseFactory responseFactory) {
Iterable<String> names = request.queryParameters("name");
String message = StreamSupport.stream(names.spliterator(), false)
.collect(Collectors.joining(", ", "Hello, ", "."));
logger.info("message: {}", message);
return Single.succeeded(
responseFactory
.ok()
.setHeader("Content-Type", "plain/text")
.payloadBody(message, textSerializer()));
}
}
- http を ポート 8080 で listen するように指定します
HttpService
を指定して、アプリケーションを起動します
- アプリケーションが終了するまで待ちます
- リクエストの情報(パス、ヘッダー、クエリ、ボディなど)を
HttpRequest
オブジェクトから取得します
- レスポンスは
Single<HttpResponse>
で返します。これは Reactor の Mono
のような 0 個または 1個のストリームをあらわすオブジェクトです
- レスポンスのデータは
HttpResponseFactory
経由で作成します。おそらく、これは ByteBuf
や ByteBufAllocator
を直接操作しなくて良いようにするためだと思われます。
HttpSerializer
とレスポンスのオブジェクトをボディに指定します。 HttpSerializer
はレスポンスのオブジェクトを Buffer
に流し込むためのオブジェクトです。ここではテキスト(plain/text
)専用のものを使っていますが、他にも json 用などがあります
というわけで、動かすとこんな感じになります
最近の 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>>(){})));
}
}
これを動かしてみると、次のツイートのようになります。
これ以外にも、 http2 / gRPC / JAX-RS などがあり、まだまだ遊び足りないくらいですが、とりあえず紹介だけしてみました。
なお、僕が遊んでみたコードは次のレポジトリーにあります。
github.com