これは、なにをしたくて書いたもの?
Quarkusを使って、Functionを作る方法をちょっと見ておきたいな、と思いまして。
ガイドを見ていると、いくつか選択肢があるようです。
今回は、Funqyというものを見ていきたいと思います。
Funqy?
Funqyのガイドはこちら。
Funqyというのは、Quarkusのサーバーレスのひとつであり、AWS Lambda、Azure Functions、Google Cloud Functions、
Knativeといった、様々なFaaS環境にデプロイできるAPIとなることを目指したもののようです。また、スタンドアロンでも
使えます。
とはいえ、このような複数の環境にまたがる抽象化を行っているため、持っている機能が非常に単純であり、他の方法に比べて
機能的に不足することもあるようです。反対に、可能な限り最適化され、小さく作られたフレームワークでもあります。
それ以外の特徴としては、以下のようなものがあります。
@Funq
を付与したメソッドをFunctionとして扱う- リアクティブ(SmallRye Mutiny)をサポート
- DI(CDIまたはSpring DI)を利用可
Funqy自体は、各FaaS環境にデプロイできるものを含め、以下で開発されているようです。
https://github.com/quarkusio/quarkus/tree/1.12.1.Final/extensions/funqy
共通部分はこちらですね。
https://github.com/quarkusio/quarkus/tree/1.12.1.Final/extensions/funqy/funqy-server-common
そして、今回はスタンドアロンで使うFunqy HTTPを使ってみたいと思います。
Quarkus - Funqy HTTP Binding (Standalone)
Funqy HTTP
Funqy HTTPは、FunqyをスタンドアロンにデプロイしてHTTPを使ってFunctionを呼び出す仕組みになります。
Quarkus - Funqy HTTP Binding (Standalone)
ソースコードは、以下ですね。
https://github.com/quarkusio/quarkus/tree/1.12.1.Final/extensions/funqy/funqy-http
unqy HTTPやFunqyを使って作られた、各プラットフォーム向けのExtensionもあるようです。
Quarkus - Funqy HTTP Binding with Amazon Lambda
Quarkus - Funqy HTTP Binding with Azure Functions
Quarkus - Funqy HTTP Binding with Google Cloud Functions
Quarkus - Funqy Amazon Lambda Binding
Quarkus - Funqy Google Cloud Functions
Quarkus - Funqy Knative Events Binding
とはいえ、特にHTTPのものを見ていると、リクエストパラメーターとしてQueryStringやJSONを受け取ることができるものの
REST APIとしての機能やHTTPに関するサポートは他の方法に比べると劣ると各ガイドには書かれています。
それが問題になる場合は、各環境に適したFunctionのサポートを利用すべきなようです。
Quarkus - Amazon Lambda with RESTEasy, Undertow, or Vert.x Web
Quarkus - Azure Functions (Serverless) with RESTEasy, Undertow, or Vert.x Web
Quarkus - Google Cloud Functions (Serverless)
Quarkus - Google Cloud Functions (Serverless) with RESTEasy, Undertow, or Vert.x Web
といっても、いずれもpreviewまたはexperimentalな状態ですけどね。
説明はこれくらいにして、Funqy HTTPを試してみましょう。
環境
今回の環境は、こちらです。
$ java --version openjdk 11.0.10 2021-01-19 OpenJDK Runtime Environment (build 11.0.10+9-Ubuntu-0ubuntu1.20.04) OpenJDK 64-Bit Server VM (build 11.0.10+9-Ubuntu-0ubuntu1.20.04, mixed mode, sharing) $ mvn --version Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f) Maven home: $HOME/.sdkman/candidates/maven/current Java version: 11.0.10, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64 Default locale: ja_JP, platform encoding: UTF-8 OS name: "linux", version: "5.4.0-66-generic", arch: "amd64", family: "unix"
Quarkusは、1.12.1.Finalを使用します。
プロジェクトを作成する
Funqy HTTPを使うプロジェクトを作成します。Extensionとしては、funqy-http
を指定すればOKです。
$ mvn io.quarkus:quarkus-maven-plugin:1.12.1.Final:create \ -DprojectGroupId=org.littlewings \ -DprojectArtifactId=funqy-http-getting-started \ -DprojectVersion=0.0.1-SNAPSHOT \ -Dextensions="funqy-http"
選択されたExtensionおよびCodestarts。
----------- selected extensions: - io.quarkus:quarkus-funqy-http applying codestarts... 🔠 java 🧰 maven 🗃 quarkus 📜 config-properties 🛠 dockerfiles 🛠 maven-wrapper 🐒 funqy-http-example
完了したら、プロジェクト内へ移動。
$ cd funqy-http-getting-started
中身は、こうなっています。
$ tree -a . ├── .dockerignore ├── .gitignore ├── .mvn │ └── wrapper │ ├── MavenWrapperDownloader.java │ └── maven-wrapper.properties ├── README.md ├── mvnw ├── mvnw.cmd ├── pom.xml └── src ├── main │ ├── docker │ │ ├── Dockerfile.jvm │ │ ├── Dockerfile.legacy-jar │ │ ├── Dockerfile.native │ │ └── Dockerfile.native-distroless │ ├── java │ │ └── org │ │ └── littlewings │ │ └── funqy │ │ └── Funqy.java │ └── resources │ ├── META-INF │ │ └── resources │ │ └── index.html │ └── application.properties └── test └── java └── org └── littlewings └── funqy ├── FunqyIT.java └── FunqyTest.java 17 directories, 17 files
Maven依存関係。
<dependencies> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-funqy-http</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-arc</artifactId> </dependency> <dependency> <groupId>io.quarkus</groupId> <artifactId>quarkus-junit5</artifactId> <scope>test</scope> </dependency> <dependency> <groupId>io.rest-assured</groupId> <artifactId>rest-assured</artifactId> <scope>test</scope> </dependency> </dependencies>
もう少し詳しく見てみましょう。
$ mvn dependency:tree -Dscope=compile
こうなりました。
[INFO] --- maven-dependency-plugin:2.8:tree (default-cli) @ funqy-http-getting-started --- [INFO] org.littlewings:funqy-http-getting-started:jar:0.0.1-SNAPSHOT [INFO] +- io.quarkus:quarkus-funqy-http:jar:1.12.1.Final:compile [INFO] | +- io.quarkus:quarkus-vertx-http:jar:1.12.1.Final:compile [INFO] | | +- io.quarkus:quarkus-security-runtime-spi:jar:1.12.1.Final:compile [INFO] | | +- io.quarkus:quarkus-vertx-http-dev-console-runtime-spi:jar:1.12.1.Final:compile [INFO] | | +- io.quarkus.security:quarkus-security:jar:1.1.3.Final:compile [INFO] | | +- io.quarkus:quarkus-vertx-core:jar:1.12.1.Final:compile [INFO] | | | +- io.quarkus:quarkus-netty:jar:1.12.1.Final:compile [INFO] | | | | +- io.netty:netty-codec:jar:4.1.49.Final:compile [INFO] | | | | \- io.netty:netty-handler:jar:4.1.49.Final:compile [INFO] | | | \- io.vertx:vertx-core:jar:3.9.5:compile [INFO] | | | +- io.netty:netty-common:jar:4.1.49.Final:compile [INFO] | | | +- io.netty:netty-buffer:jar:4.1.49.Final:compile [INFO] | | | +- io.netty:netty-transport:jar:4.1.49.Final:compile [INFO] | | | +- io.netty:netty-handler-proxy:jar:4.1.49.Final:compile [INFO] | | | | \- io.netty:netty-codec-socks:jar:4.1.49.Final:compile [INFO] | | | +- io.netty:netty-codec-http:jar:4.1.49.Final:compile [INFO] | | | +- io.netty:netty-codec-http2:jar:4.1.49.Final:compile [INFO] | | | +- io.netty:netty-resolver:jar:4.1.49.Final:compile [INFO] | | | \- io.netty:netty-resolver-dns:jar:4.1.49.Final:compile [INFO] | | | \- io.netty:netty-codec-dns:jar:4.1.49.Final:compile [INFO] | | \- io.vertx:vertx-web:jar:3.9.5:compile [INFO] | | +- io.vertx:vertx-web-common:jar:3.9.5:compile [INFO] | | +- io.vertx:vertx-auth-common:jar:3.9.5:compile [INFO] | | \- io.vertx:vertx-bridge-common:jar:3.9.5:compile [INFO] | +- io.quarkus:quarkus-funqy-server-common:jar:1.12.1.Final:compile [INFO] | | \- io.smallrye.reactive:mutiny:jar:0.13.0:compile [INFO] | | +- org.reactivestreams:reactive-streams:jar:1.0.3:compile [INFO] | | \- io.smallrye.common:smallrye-common-annotation:jar:1.5.0:compile [INFO] | \- io.quarkus:quarkus-jackson:jar:1.12.1.Final:compile [INFO] | +- com.fasterxml.jackson.core:jackson-databind:jar:2.12.1:compile [INFO] | | +- com.fasterxml.jackson.core:jackson-annotations:jar:2.12.1:compile [INFO] | | \- com.fasterxml.jackson.core:jackson-core:jar:2.12.1:compile [INFO] | +- com.fasterxml.jackson.datatype:jackson-datatype-jsr310:jar:2.12.1:compile [INFO] | +- com.fasterxml.jackson.datatype:jackson-datatype-jdk8:jar:2.12.1:compile [INFO] | \- com.fasterxml.jackson.module:jackson-module-parameter-names:jar:2.12.1:compile [INFO] \- io.quarkus:quarkus-arc:jar:1.12.1.Final:compile [INFO] +- io.quarkus.arc:arc:jar:1.12.1.Final:compile [INFO] | +- jakarta.enterprise:jakarta.enterprise.cdi-api:jar:2.0.2:compile [INFO] | | +- jakarta.el:jakarta.el-api:jar:3.0.3:compile [INFO] | | \- jakarta.interceptor:jakarta.interceptor-api:jar:1.2.5:compile [INFO] | +- jakarta.annotation:jakarta.annotation-api:jar:1.3.5:compile [INFO] | +- jakarta.transaction:jakarta.transaction-api:jar:1.3.3:compile [INFO] | \- org.jboss.logging:jboss-logging:jar:3.4.1.Final:compile [INFO] +- io.quarkus:quarkus-core:jar:1.12.1.Final:compile [INFO] | +- jakarta.inject:jakarta.inject-api:jar:1.0:compile [INFO] | +- io.quarkus:quarkus-ide-launcher:jar:1.12.1.Final:compile [INFO] | +- io.quarkus:quarkus-development-mode-spi:jar:1.12.1.Final:compile [INFO] | +- io.smallrye.config:smallrye-config:jar:1.10.2:compile [INFO] | | +- io.smallrye.config:smallrye-config-common:jar:1.10.2:compile [INFO] | | | \- org.eclipse.microprofile.config:microprofile-config-api:jar:1.4:compile [INFO] | | +- io.smallrye.common:smallrye-common-expression:jar:1.5.0:compile [INFO] | | | \- io.smallrye.common:smallrye-common-function:jar:1.5.0:compile [INFO] | | +- io.smallrye.common:smallrye-common-constraint:jar:1.5.0:compile [INFO] | | \- io.smallrye.common:smallrye-common-classloader:jar:1.5.0:compile [INFO] | +- org.jboss.logmanager:jboss-logmanager-embedded:jar:1.0.6:compile [INFO] | +- org.jboss.logging:jboss-logging-annotations:jar:2.2.0.Final:compile [INFO] | +- org.jboss.threads:jboss-threads:jar:3.2.0.Final:compile [INFO] | +- org.slf4j:slf4j-api:jar:1.7.30:compile [INFO] | +- org.jboss.slf4j:slf4j-jboss-logmanager:jar:1.1.0.Final:compile [INFO] | +- org.graalvm.sdk:graal-sdk:jar:21.0.0:compile [INFO] | +- org.wildfly.common:wildfly-common:jar:1.5.4.Final-format-001:compile [INFO] | \- io.quarkus:quarkus-bootstrap-runner:jar:1.12.1.Final:compile [INFO] \- org.eclipse.microprofile.context-propagation:microprofile-context-propagation-api:jar:1.0.1:compile
HTTPに関してはVert.xを使い、JSONはJacksonを使いそうな感じですね。
ソースコードも見てみましょう。src/main/java
側。
src/main/java/org/littlewings/funqy/Funqy.java
package org.littlewings.funqy; import io.quarkus.funqy.Funq; import java.util.Random; public class Funqy { private static final String CHARM_QUARK_SYMBOL = "c"; @Funq public String charm(Answer answer) { return CHARM_QUARK_SYMBOL.equalsIgnoreCase(answer.value) ? "You Quark!" : "👻 Wrong answer"; } public static class Answer { public String value; } }
テストコード。
src/test/java/org/littlewings/funqy/FunqyTest.java
package org.littlewings.funqy; import io.quarkus.test.junit.QuarkusTest; import org.junit.jupiter.api.Test; import static io.restassured.RestAssured.given; import static org.hamcrest.CoreMatchers.containsString; @QuarkusTest public class FunqyTest { @Test public void testCharm() { given() .contentType("application/json") .body("{\"value\": \"c\"}") .post("/charm") .then() .statusCode(200) .body(containsString("You Quark!")); } }
src/test/java/org/littlewings/funqy/FunqyIT.java
package org.littlewings.funqy; import io.quarkus.test.junit.NativeImageTest; @NativeImageTest public class FunqyIT extends FunqyTest { // Run the same tests }
動かしてみる
1度、このまま動かしてみましょう。パッケージング。
$ mvn package
起動。
$ java -jar target/quarkus-app/quarkus-run.jar
ポート8080を使ってQuarkusが起動するので、アクセスしてみます。
$ curl localhost:8080/charm -d '{"value": "c"}' "You Quark!" $ curl localhost:8080/charm "👻 Wrong answer"
URLのパスは、メソッド名が反映されます。作成されたプロジェクト内のメソッド名は、charm
だったので
http://localhost:8080/charm
でアクセスするできます、と。
Funqy HTTP / Execute Funqy HTTP functions
関数呼び出しまでのトレースを見てみましょう。Thread#dumpStack
を追加してみます。
src/main/java/org/littlewings/funqy/Funqy.java
@Funq public String charm(Answer answer) { Thread.dumpStack(); return CHARM_QUARK_SYMBOL.equalsIgnoreCase(answer.value) ? "You Quark!" : "👻 Wrong answer"; }
得られる出力は、こんな感じです。ほとんど間になにもありませんね。
java.lang.Exception: Stack trace at java.base/java.lang.Thread.dumpStack(Thread.java:1388) at org.littlewings.funqy.Funqy.charm(Funqy.java:13) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke0(Native Method) at java.base/jdk.internal.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62) at java.base/jdk.internal.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) at java.base/java.lang.reflect.Method.invoke(Method.java:566) at io.quarkus.funqy.runtime.FunctionInvoker.invoke(FunctionInvoker.java:120) at io.quarkus.funqy.runtime.bindings.http.VertxRequestHandler.dispatch(VertxRequestHandler.java:141) at io.quarkus.funqy.runtime.bindings.http.VertxRequestHandler.lambda$handle$0(VertxRequestHandler.java:98) at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2415) at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1452) at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29) at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29) at java.base/java.lang.Thread.run(Thread.java:834) at org.jboss.threads.JBossThread.run(JBossThread.java:501)
FunqyもFunqy HTTPも、ソースコードを見るとけっこう小さいフレームワークなんですよね。
https://github.com/quarkusio/quarkus/tree/1.12.1.Final/extensions/funqy/funqy-server-common
https://github.com/quarkusio/quarkus/tree/1.12.1.Final/extensions/funqy/funqy-http
スレッドダンプを取ってみると、Vert.xのスレッドの存在を確認できます。
$ jcmd [PID] Thread.print | grep '^"' "main" #1 prio=5 os_prio=0 cpu=679.82ms elapsed=144.20s tid=0x00007fc5e8016800 nid=0x5e9b waiting on condition [0x00007fc5ec8bd000] "Reference Handler" #2 daemon prio=10 os_prio=0 cpu=0.44ms elapsed=144.17s tid=0x00007fc5e8237000 nid=0x5ea2 waiting on condition [0x00007fc5c01ab000] "Finalizer" #3 daemon prio=8 os_prio=0 cpu=0.46ms elapsed=144.17s tid=0x00007fc5e8239000 nid=0x5ea3 in Object.wait() [0x00007fc5b37fe000] "Signal Dispatcher" #4 daemon prio=9 os_prio=0 cpu=0.31ms elapsed=144.17s tid=0x00007fc5e823e800 nid=0x5ea4 runnable [0x0000000000000000] "Service Thread" #5 daemon prio=9 os_prio=0 cpu=0.05ms elapsed=144.17s tid=0x00007fc5e8240800 nid=0x5ea5 runnable [0x0000000000000000] "C2 CompilerThread0" #6 daemon prio=9 os_prio=0 cpu=254.84ms elapsed=144.17s tid=0x00007fc5e8242800 nid=0x5ea6 waiting on condition [0x0000000000000000] "C1 CompilerThread0" #9 daemon prio=9 os_prio=0 cpu=397.30ms elapsed=144.17s tid=0x00007fc5e8244800 nid=0x5ea7 waiting on condition [0x0000000000000000] "Sweeper thread" #10 daemon prio=9 os_prio=0 cpu=0.10ms elapsed=144.17s tid=0x00007fc5e8246800 nid=0x5ea8 runnable [0x0000000000000000] "Common-Cleaner" #11 daemon prio=8 os_prio=0 cpu=0.70ms elapsed=144.15s tid=0x00007fc5e8291000 nid=0x5eaa in Object.wait() [0x00007fc5b2a83000] "vertx-blocked-thread-checker" #16 daemon prio=5 os_prio=0 cpu=9.63ms elapsed=143.57s tid=0x00007fc5e88dd800 nid=0x5eb3 in Object.wait() [0x00007fc5b2481000] "vert.x-eventloop-thread-0" #18 prio=5 os_prio=0 cpu=3.11ms elapsed=143.50s tid=0x00007fc5e8983800 nid=0x5eb4 runnable [0x00007fc5b1b75000] "vert.x-eventloop-thread-1" #19 prio=5 os_prio=0 cpu=2.10ms elapsed=143.50s tid=0x00007fc5e8985000 nid=0x5eb5 runnable [0x00007fc5b1a74000] "vert.x-eventloop-thread-2" #20 prio=5 os_prio=0 cpu=144.07ms elapsed=143.50s tid=0x00007fc5e8987000 nid=0x5eb6 runnable [0x00007fc5b1973000] "vert.x-eventloop-thread-3" #21 prio=5 os_prio=0 cpu=2.90ms elapsed=143.50s tid=0x00007fc5e8988800 nid=0x5eb7 runnable [0x00007fc5b1872000] "vert.x-eventloop-thread-4" #22 prio=5 os_prio=0 cpu=1.47ms elapsed=143.50s tid=0x00007fc5e898a800 nid=0x5eb8 runnable [0x00007fc5b1771000] "vert.x-eventloop-thread-5" #23 prio=5 os_prio=0 cpu=1.91ms elapsed=143.50s tid=0x00007fc5e898c000 nid=0x5eb9 runnable [0x00007fc5b1670000] "vert.x-eventloop-thread-6" #24 prio=5 os_prio=0 cpu=1.56ms elapsed=143.50s tid=0x00007fc5e898e000 nid=0x5eba runnable [0x00007fc5b156f000] "vert.x-eventloop-thread-7" #25 prio=5 os_prio=0 cpu=1.10ms elapsed=143.50s tid=0x00007fc5e898f800 nid=0x5ebb runnable [0x00007fc5b146e000] "vert.x-eventloop-thread-8" #26 prio=5 os_prio=0 cpu=1.66ms elapsed=143.50s tid=0x00007fc5e8991800 nid=0x5ebc runnable [0x00007fc5b136d000] "vert.x-eventloop-thread-9" #27 prio=5 os_prio=0 cpu=2.51ms elapsed=143.50s tid=0x00007fc5e8993800 nid=0x5ebd runnable [0x00007fc5b126c000] "vert.x-eventloop-thread-10" #28 prio=5 os_prio=0 cpu=3.83ms elapsed=143.50s tid=0x00007fc5e8995000 nid=0x5ebe runnable [0x00007fc5b116b000] "vert.x-eventloop-thread-11" #29 prio=5 os_prio=0 cpu=1.38ms elapsed=143.50s tid=0x00007fc5e8997000 nid=0x5ebf runnable [0x00007fc5b106a000] "vert.x-eventloop-thread-12" #30 prio=5 os_prio=0 cpu=1.56ms elapsed=143.50s tid=0x00007fc5e8998800 nid=0x5ec0 runnable [0x00007fc5b0f69000] "vert.x-eventloop-thread-13" #31 prio=5 os_prio=0 cpu=1.27ms elapsed=143.50s tid=0x00007fc5e899a800 nid=0x5ec1 runnable [0x00007fc5b0e68000] "vert.x-eventloop-thread-14" #32 prio=5 os_prio=0 cpu=5.78ms elapsed=143.50s tid=0x00007fc5e899c800 nid=0x5ec2 runnable [0x00007fc5b0d67000] "vert.x-eventloop-thread-15" #33 prio=5 os_prio=0 cpu=1.30ms elapsed=143.50s tid=0x00007fc5e899e800 nid=0x5ec3 runnable [0x00007fc5b0c66000] "vert.x-acceptor-thread-0" #34 prio=5 os_prio=0 cpu=13.89ms elapsed=143.43s tid=0x00007fc55c05c000 nid=0x5ec4 runnable [0x00007fc5b0965000] "executor-thread-2" #35 daemon prio=5 os_prio=0 cpu=8.70ms elapsed=141.73s tid=0x00007fc57c029000 nid=0x5ec8 waiting on condition [0x00007fc5b0664000] "Attach Listener" #36 daemon prio=9 os_prio=0 cpu=1.10ms elapsed=103.47s tid=0x00007fc598001000 nid=0x5f0e waiting on condition [0x0000000000000000] "VM Thread" os_prio=0 cpu=40.89ms elapsed=144.17s tid=0x00007fc5e8234000 nid=0x5ea1 runnable "GC Thread#0" os_prio=0 cpu=19.25ms elapsed=144.19s tid=0x00007fc5e802f800 nid=0x5e9c runnable "GC Thread#1" os_prio=0 cpu=17.08ms elapsed=143.64s tid=0x00007fc5a8001000 nid=0x5eae runnable "GC Thread#2" os_prio=0 cpu=17.22ms elapsed=143.64s tid=0x00007fc5a8002800 nid=0x5eaf runnable "GC Thread#3" os_prio=0 cpu=17.28ms elapsed=143.64s tid=0x00007fc5a8004000 nid=0x5eb0 runnable "GC Thread#4" os_prio=0 cpu=17.62ms elapsed=143.64s tid=0x00007fc5a8005800 nid=0x5eb1 runnable "GC Thread#5" os_prio=0 cpu=17.36ms elapsed=143.64s tid=0x00007fc5a8007000 nid=0x5eb2 runnable "G1 Main Marker" os_prio=0 cpu=0.42ms elapsed=144.19s tid=0x00007fc5e808d800 nid=0x5e9d runnable "G1 Conc#0" os_prio=0 cpu=0.10ms elapsed=144.19s tid=0x00007fc5e808f800 nid=0x5e9e runnable "G1 Refine#0" os_prio=0 cpu=0.31ms elapsed=144.18s tid=0x00007fc5e818f000 nid=0x5e9f runnable "G1 Young RemSet Sampling" os_prio=0 cpu=32.18ms elapsed=144.18s tid=0x00007fc5e8191000 nid=0x5ea0 runnable "VM Periodic Task Thread" os_prio=0 cpu=119.81ms elapsed=144.16s tid=0x00007fc5e828f000 nid=0x5ea9 waiting on condition
少しは実行環境の雰囲気はわかったかなと思います。
ここで、生成されたソースコードおよびテストコードを削除して、自分でソースコードを書いていきましょう。
$ rm src/main/java/org/littlewings/funqy/Funqy.java src/test/java/org/littlewings/funqy/Funqy*
自分でFunctionを書いてみる
次は、自分でFunctionを書いてみましょう。
こちらを見て進めていきます。
Quarkus - Funqy HTTP Binding (Standalone)
以降、ソースコードの紹介ごとに以下のコマンドを実行して確認しているものとします。
$ mvn package $ java -jar target/quarkus-app/quarkus-run.jar
また、SmallRye Mutinyを使って関数は作成します。
まずは、こんなクラスを作成。
src/main/java/org/littlewings/funqy/function/MyFunctions.java
package org.littlewings.funqy.function; import io.quarkus.funqy.Funq; import io.smallrye.mutiny.Uni; public class MyFunctions { @Funq public Uni<String> hello() { return Uni.createFrom().item("Hello Funqy!!"); } @Funq("MyFunction") public Uni<String> annotationSpecFunction() { return Uni.createFrom().item("annotation spec function name"); } }
関数には@Funqy
アノテーションを付与します。ガイドを振り返ると、関数にアクセスする際はデフォルトでは
@Funqy
アノテーションを付与したメソッド名がパスに反映されるという話でした。
Funqy HTTP / Execute Funqy HTTP functions
というわけで、こちらのメソッドには/hello
というパスでアクセスできるはずです。
@Funq public Uni<String> hello() { return Uni.createFrom().item("Hello Funqy!!"); }
確認。
$ curl -i localhost:8080/hello HTTP/1.1 200 OK Content-Type: application/json content-length: 15 "Hello Funqy!!"
また、@Funqy
アノテーションに値を指定することで、その名前を関数名とすることもできます。
@Funq("MyFunction") public Uni<String> annotationSpecFunction() { return Uni.createFrom().item("annotation spec function name"); }
確認。
$ curl -i localhost:8080/MyFunction HTTP/1.1 200 OK Content-Type: application/json content-length: 31 "annotation spec function name"
なお、関数名は大文字・小文字を区別します。
$ curl -i localhost:8080/myfunction HTTP/1.1 404 Not Found content-type: text/html; charset=utf-8 content-length: 53 <html><body><h1>Resource not found</h1></body></html>
次は、パラメーターを受け取るようにしてみましょう。GETの場合はQueryStringを、POSTの場合はHTTP Bodyを受け取ることが
できます。
Funqy HTTP / GET Query Parameter Mapping
src/main/java/org/littlewings/funqy/function/ParameterFunctions.java
package org.littlewings.funqy.function; import java.util.Map; import io.quarkus.funqy.Funq; import io.smallrye.mutiny.Uni; public class ParameterFunctions { @Funq public Uni<String> map(Map<String, String> request) { return Uni .createFrom() .item(String.format("Hello[map], %s %s: %s", request.get("firstName"), request.get("lastName"), request.get("age"))); } @Funq public Uni<Result> bean(Person p) { return Uni .createFrom() .item(new Result("bean", String.format("%s %s: %d", p.getFirstName(), p.getLastName(), p.age))); } }
パラメーターはMap
で受け取るか、Javaオブジェクトとして受け取ることが可能です。
2つ目のメソッドはJavaオブジェクトを受け取るようにしてありますが、その定義はこちら。
src/main/java/org/littlewings/funqy/function/Person.java
package org.littlewings.funqy.function; public class Person { String firstName; String lastName; int age; 〜getter/setterは省略〜 }
また、レスポンスも自分で定義したクラスにしてあります。
src/main/java/org/littlewings/funqy/function/Result.java
package org.littlewings.funqy.function; public class Result { String message; public Result(String prefix, String message) { this.message = String.format("Hello[%s] %s", prefix, message); } public String getMessage() { return message; } }
まずは、Map
で受け取る方から確認。
@Funq public Uni<String> map(Map<String, String> request) { return Uni .createFrom() .item(String.format("Hello[map], %s %s: %s", request.get("firstName"), request.get("lastName"), request.get("age"))); }
結果。
$ curl -i 'localhost:8080/map?firstName=katsuo&lastName=isono&age=11' HTTP/1.1 200 OK Content-Type: application/json content-length: 30 "Hello[map], katsuo isono: 11"
QueryStringで渡されたパラメーターを、Javaオブジェクトにマッピングして受け取ることもできます。
$ curl -i 'localhost:8080/bean?firstName=katsuo&lastName=isono&age=11' HTTP/1.1 200 OK Content-Type: application/json content-length: 42 {"message":"Hello[bean] katsuo isono: 11"}
こちらは、レスポンスもオブジェクト形式になりました。
次は、POSTでJSONを送信してみましょう。
@Funq public Uni<Result> bean(Person p) { return Uni .createFrom() .item(new Result("bean", String.format("%s %s: %d", p.getFirstName(), p.getLastName(), p.age))); }
こちらも、Map
およびJavaオブジェクトのどちらでも受け取れます。
$ curl -i -XPOST -H 'Content-Type:application/json' localhost:8080/map -d '{"firstName": "カツオ", "lastName": "磯野", "age": 11}' HTTP/1.1 200 OK Content-Type: application/json content-length: 34 "Hello[map], カツオ 磯野: 11" $ curl -i -XPOST -H 'Content-Type:application/json' localhost:8080/bean -d '{"firstName": "カツオ", "lastName": "磯野", "age": 11}' HTTP/1.1 200 OK Content-Type: application/json content-length: 46 {"message":"Hello[bean] カツオ 磯野: 11"}
最後は、DIを使いましょう。今回はCDIを使います。
なのですが、まずはその前に関数を持ったクラスのインスタンスがどういうライフサイクルになっているか知りたいところ。
ガイドによると、デフォルトでは@Dependent
と同等だそうです。
The default object lifecycle for a Funqy class is @Dependent.
最初に作ったクラスのコンストラクタに、Thread#dumpStack
を仕込んで確認してみます。
public MyFunctions() {
Thread.dumpStack();
}
すると、curl
でのアクセスごとにスタックトレースが出力されます。
java.lang.Exception: Stack trace at java.base/java.lang.Thread.dumpStack(Thread.java:1388) at org.littlewings.funqy.function.MyFunctions.<init>(MyFunctions.java:8) at org.littlewings.funqy.function.MyFunctions_Bean.create(MyFunctions_Bean.zig:104) at org.littlewings.funqy.function.MyFunctions_Bean.get(MyFunctions_Bean.zig:134) at org.littlewings.funqy.function.MyFunctions_Bean.get(MyFunctions_Bean.zig:169) at io.quarkus.arc.impl.ArcContainerImpl.beanInstanceHandle(ArcContainerImpl.java:430) at io.quarkus.arc.impl.ArcContainerImpl.beanInstanceHandle(ArcContainerImpl.java:443) at io.quarkus.arc.impl.ArcContainerImpl$1.get(ArcContainerImpl.java:266) at io.quarkus.arc.impl.ArcContainerImpl$1.get(ArcContainerImpl.java:263) at io.quarkus.arc.runtime.BeanContainerImpl$1.create(BeanContainerImpl.java:35) at io.quarkus.funqy.runtime.FunctionConstructor.construct(FunctionConstructor.java:18) at io.quarkus.funqy.runtime.FunctionInvoker.invoke(FunctionInvoker.java:118) at io.quarkus.funqy.runtime.bindings.http.VertxRequestHandler.dispatch(VertxRequestHandler.java:141) at io.quarkus.funqy.runtime.bindings.http.VertxRequestHandler.lambda$handle$0(VertxRequestHandler.java:98) at io.quarkus.runtime.CleanableExecutor$CleaningRunnable.run(CleanableExecutor.java:231) at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515) at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) at org.jboss.threads.EnhancedQueueExecutor$Task.run(EnhancedQueueExecutor.java:2415) at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1452) at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29) at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:29) at java.base/java.lang.Thread.run(Thread.java:834) at org.jboss.threads.JBossThread.run(JBossThread.java:501)
このあたりのソースコードは、こちら。
では、@ApplicationScoped
アノテーションを付与してみます。
@ApplicationScoped public class MyFunctions { public MyFunctions() { Thread.dumpStack(); }
すると、ビルドに失敗するようになります…。二重でスコープ定義が入った感じになっているようですが…。
[ERROR] [error]: Build step io.quarkus.arc.deployment.ArcProcessor#registerBeans threw an exception: javax.enterprise.inject.spi.DefinitionException: Bean class org.littlewings.funqy.function.MyFunctions declares multiple scope type annotations: javax.enterprise.context.ApplicationScoped, javax.enterprise.context.Dependent
というわけで、スコープ定義は明示的に入れないことにします。気を取り直して。
CDI管理Beanを作成。
src/main/java/org/littlewings/funqy/service/MessageService.java
package org.littlewings.funqy.service; import javax.enterprise.context.ApplicationScoped; import io.smallrye.mutiny.Uni; @ApplicationScoped public class MessageService { public Uni<String> format(String firstName, String lastName, int age) { return Uni .createFrom() .item(String.format("%s %s: %d", firstName, lastName, age)); } }
こちらを@Inject
して使用する、関数を持ったクラスを作成。
src/main/java/org/littlewings/funqy/function/CdiFunctions.java
package org.littlewings.funqy.function; import javax.inject.Inject; import io.quarkus.funqy.Funq; import io.smallrye.mutiny.Uni; import org.littlewings.funqy.service.MessageService; public class CdiFunctions { @Inject MessageService messageService; @Funq public Uni<Result> cdi(Person p) { return Uni .combine() .all() .unis( Uni.createFrom().item("cdi"), messageService.format(p.getFirstName(), p.getLastName(), p.getAge()) ) .combinedWith((s1, s2) -> new Result(s1, s2)); } }
リクエストおよびレスポンスは、先ほどパラメーターを扱った時に作成したクラスを利用しています。
確認。
$ curl -i -XPOST -H 'Content-Type: application/json' localhost:8080/cdi -d '{"firstName": "カツオ", "lastName": "磯野", "age": 11}' HTTP/1.1 200 OK Content-Type: application/json content-length: 45 {"message":"Hello[cdi] カツオ 磯野: 11"}
OKですね。
あと、試していない機能としてはContext Injectionがあるのですが、今回はパス。
なんとなく、使い方、雰囲気はわかりました。
テストコード
最後に、ここまでcurl
で確認してきた内容と同じことをするテストコードを載せておきます。
src/test/java/org/littlewings/funqy/function/MyFunctionsTest.java
package org.littlewings.funqy.function; import io.quarkus.test.junit.QuarkusTest; import org.apache.http.entity.ContentType; import org.junit.jupiter.api.Test; import static io.restassured.RestAssured.given; import static org.hamcrest.CoreMatchers.is; @QuarkusTest class MyFunctionsTest { @Test public void helloTest() { given() .contentType(ContentType.APPLICATION_JSON.getMimeType()) .when() .get("/hello") .then() .assertThat() .statusCode(200) .body(is("\"Hello Funqy!!\"")); } @Test public void annotationSpecFunctionTest() { given() .contentType(ContentType.APPLICATION_JSON.getMimeType()) .when() .get("/MyFunction") .then() .assertThat() .statusCode(200) .body(is("\"annotation spec function name\"")); } @Test public void caseSensitiveTest() { given() .contentType(ContentType.APPLICATION_JSON.getMimeType()) .get("/myfunction") .then() .assertThat() .statusCode(404); } }
src/test/java/org/littlewings/funqy/function/ParameterFunctionsTest.java
package org.littlewings.funqy.function; import io.quarkus.test.junit.QuarkusTest; import org.apache.http.entity.ContentType; import org.junit.jupiter.api.Test; import static io.restassured.RestAssured.given; import static org.hamcrest.CoreMatchers.is; @QuarkusTest class ParameterFunctionsTest { @Test public void mapQueryTest() { given() .contentType(ContentType.APPLICATION_JSON.getMimeType()) .queryParam("firstName", "カツオ") .queryParam("lastName", "磯野") .queryParam("age", 11) .when() .get("/map") .then() .assertThat() .statusCode(200) .body(is("\"Hello[map], カツオ 磯野: 11\"")); } @Test public void beanQueryTest() { given() .contentType(ContentType.APPLICATION_JSON.getMimeType()) .queryParam("firstName", "カツオ") .queryParam("lastName", "磯野") .queryParam("age", 11) .when() .get("/bean") .then() .assertThat() .statusCode(200) .body("message", is("Hello[bean] カツオ 磯野: 11")); } @Test public void mapJsonTest() { Person request = new Person(); request.setFirstName("カツオ"); request.setLastName("磯野"); request.setAge(11); given() .contentType(ContentType.APPLICATION_JSON.getMimeType()) .body(request) .when() .post("/map") .then() .assertThat() .statusCode(200) .body(is("\"Hello[map], カツオ 磯野: 11\"")); } @Test public void beanTest() { Person request = new Person(); request.setFirstName("カツオ"); request.setLastName("磯野"); request.setAge(11); given() .contentType(ContentType.APPLICATION_JSON.getMimeType()) .body(request) .when() .post("/bean") .then() .assertThat() .statusCode(200) .body("message", is("Hello[bean] カツオ 磯野: 11")); } }
src/test/java/org/littlewings/funqy/function/CdiFunctionsTest.java
package org.littlewings.funqy.function; import io.quarkus.test.junit.QuarkusTest; import org.apache.http.entity.ContentType; import org.junit.jupiter.api.Test; import static io.restassured.RestAssured.given; import static org.hamcrest.CoreMatchers.is; @QuarkusTest class CdiFunctionsTest { @Test public void cdiTest() { Person request = new Person(); request.setFirstName("カツオ"); request.setLastName("磯野"); request.setAge(11); given() .contentType(ContentType.APPLICATION_JSON.getMimeType()) .body(request) .when() .post("/cdi") .then() .assertThat() .statusCode(200) .body("message", is("Hello[cdi] カツオ 磯野: 11")); } }