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