CLOVER🍀

That was when it all began.

Funqyを使ってQuarkusでFunctionを書いてみる

これは、なにをしたくて書いたもの?

Quarkusを使って、Functionを作る方法をちょっと見ておきたいな、と思いまして。

ガイドを見ていると、いくつか選択肢があるようです。

Guides / Cloud

今回は、Funqyというものを見ていきたいと思います。

Funqy?

Funqyのガイドはこちら。

Quarkus - 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

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アノテーションに値を指定することで、その名前を関数名とすることもできます。

Funqy / Function Names

    @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

https://github.com/quarkusio/quarkus/blob/1.12.1.Final/extensions/funqy/funqy-http/runtime/src/main/java/io/quarkus/funqy/runtime/bindings/http/VertxRequestHandler.java#L84-L119

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を使います。

Funqy / Funqy DI

なのですが、まずはその前に関数を持ったクラスのインスタンスがどういうライフサイクルになっているか知りたいところ。

ガイドによると、デフォルトでは@Dependentと同等だそうです。

The default object lifecycle for a Funqy class is @Dependent.

Funqy / Funqy DI

最初に作ったクラスのコンストラクタに、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)

リクエストごとにインスタンス化されているようです。

このあたりのソースコードは、こちら。

https://github.com/quarkusio/quarkus/blob/1.12.1.Final/extensions/funqy/funqy-http/runtime/src/main/java/io/quarkus/funqy/runtime/bindings/http/VertxRequestHandler.java#L141

https://github.com/quarkusio/quarkus/blob/1.12.1.Final/extensions/funqy/funqy-server-common/runtime/src/main/java/io/quarkus/funqy/runtime/FunctionInvoker.java#L118

https://github.com/quarkusio/quarkus/blob/1.12.1.Final/extensions/funqy/funqy-server-common/runtime/src/main/java/io/quarkus/funqy/runtime/FunctionConstructor.java#L18

では、@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があるのですが、今回はパス。

Funqy / 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"));
    }
}