CLOVER🍀

That was when it all began.

Node.jsアプリケーションのログ出力に、winstonを使ってみる

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

Node.jsでアプリケーションを書く時のロギングライブラリは、どういうものがあるのかちょっと気になりまして。

調べた感じ、以下のようなものがありそうです。

GitHub - winstonjs/winston: A logger for just about everything.

GitHub - trentm/node-bunyan: a simple and fast JSON logging module for node.js services

GitHub - log4js-node/log4js-node: A port of log4js to node.js

GitHub - npm/npmlog: The logger that npm uses

今回は、この中でもstar数が多いwinstonを試してみることにしました。

winston

winstonは、複数のトランスポート(出力先)をサポートするロギングライブラリです。

GitHub - winstonjs/winston: A logger for just about everything.

ロガーを作成する時に、

  • 出力先
  • ログフォーマット
  • 出力するログレベル

などを設定できますが、カテゴリ別にロガーを作成したり、デフォルトロガーを設定したりもできます。

Working with multiple Loggers in winston

Using the Default Logger

ログの出力先には、ビルトインではコンソール、ファイル、HTTP、Streamが選べ、その他にもコミュニティなどでいろいろと
実装されているようです。

winston/transports.md at 3.2.1 · winstonjs/winston · GitHub

フォーマットについては、logformでのものが扱えるようです。

Formats

GitHub - winstonjs/logform: An mutable object format designed for chaining & objectMode streams

Logstashフォーマットとかもあったりします。

とりあえず、今回は簡単に試してみるとしましょう。

環境

今回の環境は、こちら。

$ node -v
v10.15.3


$ npm -v
6.4.1

準備

winstonのインストール。

$ npm i winston

インストールされたwinstonのバージョン。

  "dependencies": {
    "winston": "^3.2.1"
  }

今回は、こちらを使っていきたいと思います。

winstonを使っていろいろ試す

それでは、ここからいくつか簡単にwinstonを使ったコードを書いていきましょう。

Getting Started的な

なにはともあれ、まずは動かしてみます。

こんなコードを用意。
winston-example1.js

const winston = require('winston');

const logger = winston.createLogger({
    level: 'info',
    format: winston.format.json(),  // default
    transports: [
        new winston.transports.Console()
    ]
});

logger.debug('Debug Message');
logger.info('Info Message');
logger.warn('Warn Message');
logger.error('Error Message');

ログ出力先をコンソール、出力するログレベルをinfo以上に、ログフォーマットをJSONにしました。ログ出力先(Transport)は、
指定しないとエラーになります。

ロガーのデフォルト設定は以下に記載がありますが、ログフォーマットはJSON、ログ出力レベルはinfo以上みたいです。

Creating your own Logger

実行。

$ node winston-example1.js 
{"message":"Info Message","level":"info"}
{"message":"Warn Message","level":"warn"}
{"message":"Error Message","level":"error"}

ログ出力レベルがinfo以上なので、debugのものは出力されていませんね。また、ログフォーマットはJSONです。

ちなみに、ログレベルの一覧はこちら。

Logging Levels

今回はdebug、info、warn、errorしか使っていませんが、もっとたくさんあることがわかります。

ログにタイムスタンプを入れ、メッセージをフォーマット指定できるようにしてみる

先ほどのログには、実行時のタイムスタンプがありません。できれば、タイムスタンプを表示したいところです。

また、ログメッセージに%sのようなフォーマット指定もできるようなので、こちらも合わせて試してみました。
winston-example2.js

const winston = require('winston');
const format = winston.format;

const logger = winston.createLogger({
    level: 'info',
    format: format.combine(
        format.timestamp(),  // timestampを出力する
        format.splat(),  // String interpolation splat for %d %s-style messages.
        format.json()
    ),
    transports: [
        new winston.transports.Console()
    ]
});

logger.debug('Debug Message');
logger.info('Info Message');
logger.warn('Warn Message');
logger.error('Error Message');

logger.debug('%s %s', 'Debug','Message');
logger.info('%s %s', 'Info', 'Message');
logger.warn('%s %s', 'Warn', 'Message');
logger.error('%s %s', 'Error', 'Message');

こちらは、フォーマットをcombineで組み合わせることで設定しています。

    format: format.combine(
        format.timestamp(),  // timestampを出力する
        format.splat(),  // String interpolation splat for %d %s-style messages.
        format.json()
    ),

この指定する順番が大事みたいで、jsonの後にtimestampを入れたりすると、タイムスタンプは出力されません…。

Combine

詳細は、logformを見てみるとよいでしょう。

GitHub - winstonjs/logform: An mutable object format designed for chaining & objectMode streams

確認。

$ node winston-example2.js 
{"message":"Info Message","level":"info","timestamp":"2019-05-21T14:21:17.627Z"}
{"message":"Warn Message","level":"warn","timestamp":"2019-05-21T14:21:17.628Z"}
{"message":"Error Message","level":"error","timestamp":"2019-05-21T14:21:17.628Z"}
{"level":"info","message":"Info Message","timestamp":"2019-05-21T14:21:17.629Z"}
{"level":"warn","message":"Warn Message","timestamp":"2019-05-21T14:21:17.629Z"}
{"level":"error","message":"Error Message","timestamp":"2019-05-21T14:21:17.629Z"}

タイムスタンプがログに入り、%sの部分も置換されるようになっています。

ところで、タイムゾーンがUTC感。

タイムスタンプのフォーマットを変えたい場合は、timestampにformatで指定すればよいみたいです。

Timestamp

ログフォーマットに任意の項目を追加する

ログフォーマットに、任意の項目を追加してみましょう。要するに、カスタムフォーマットの作り方です。

Creating custom formats

今回は、「hostname」という項目を足してみました。
winston-example3.js

const winston = require('winston');
const format = winston.format;

const hostname = format((info, opts = {}) => {
    let value;
    
    if (!opts.hostname) {
        value = 'myhost';
    } else {
        value = opts.hostname;
    }

    if (!opts.alias) {
        info.hostname = value;
    } else {
        info[opts.alias] = value;
    }

    return info;
});

const logger = winston.createLogger({
    level: 'info',
    format: format.combine(
        format.timestamp(),
        hostname(),  // custom format
        format.json()
    ),
    transports: [
        new winston.transports.Console()
    ]
});

logger.debug('Debug Message');
logger.info('Info Message');
logger.warn('Warn Message');
logger.error('Error Message');

フォーマットは、format関数で作成します。今回は、ホスト名(固定ですが)を入れることにしました。

const hostname = format((info, opts = {}) => {
    let value;
    
    if (!opts.hostname) {
        value = 'myhost';
    } else {
        value = opts.hostname;
    }

    if (!opts.alias) {
        info.hostname = value;
    } else {
        info[opts.alias] = value;
    }

    return info;
});

この関数を、formatオプションで指定します。

    format: format.combine(
        format.timestamp(),
        hostname(),  // custom format
        format.json()
    ),

確認。

$ node winston-example3.js 
{"message":"Info Message","level":"info","timestamp":"2019-05-21T14:25:30.017Z","hostname":"myhost"}
{"message":"Warn Message","level":"warn","timestamp":"2019-05-21T14:25:30.018Z","hostname":"myhost"}
{"message":"Error Message","level":"error","timestamp":"2019-05-21T14:25:30.018Z","hostname":"myhost"}

「hostname」という項目が増えましたね。

今回は使っていませんが、このhostnameにオプションを与えると、動作のカスタマイズができるようになります。以下のコードで、
infoは実際に出力するログの情報で、optsはformatを作成する時のオプションです。

const hostname = format((info, opts = {}) => {
    let value;
    
    if (!opts.hostname) {
        value = 'myhost';
    } else {
        value = opts.hostname;
    }

    if (!opts.alias) {
        info.hostname = value;
    } else {
        info[opts.alias] = value;
    }

    return info;
});

オプションは、こんな感じで指定します。

        hostname({ alias: 'servername' }),  // custom format
ログを複数の出力先に出力する

出力先(Transport)を複数指定することで、ログを複数のターゲットに出力することができます。

たとえば、コンソールとファイルに出力する場合。
winston-example4.js

const winston = require('winston');

const logger = winston.createLogger({
    level: 'info',
    transports: [
        new winston.transports.Console({ level: 'warn' }),
        new winston.transports.File({ filename: 'app.log', level: 'debug' })
    ]
});

logger.debug('Debug Message');
logger.info('Info Message');
logger.warn('Warn Message');
logger.error('Error Message');

出力先(Transport)単位で、設定を変更することもできます。

    transports: [
        new winston.transports.Console({ level: 'warn' }),
        new winston.transports.File({ filename: 'app.log', level: 'debug' })
    ]

今回はデフォルトのログレベルはinfoにしていますが、コンソールはwarn、ファイルはdebugにしてみました。また、ファイルの方の
出力先はapp.logとします。

確認。

$ node winston-example4.js 
{"message":"Warn Message","level":"warn"}
{"message":"Error Message","level":"error"}

コンソールには、infoも出なくなりました。ファイルのログを見てみましょう。

$ cat app.log 
{"message":"Debug Message","level":"debug"}
{"message":"Info Message","level":"info"}
{"message":"Warn Message","level":"warn"}
{"message":"Error Message","level":"error"}

こちらは、debugレベルまで出力されています。

Transportは、他にもデイリーでローテーションするものだったりといろいろあるので、確認してみるとよいでしょう。

GitHub - winstonjs/winston-daily-rotate-file: A transport for winston which logs to a rotating file each day.

winston/transports.md at 3.2.1 · winstonjs/winston · GitHub

CLIログフォーマットにする

ここまでJSONフォーマットでログを出力していましたが、もっとCLIっぽいログでも出力してみようかなと。

こんな感じのフォーマットでどうでしょう。
winston-example5.js

const winston = require('winston');
const format = winston.format;

const logger = winston.createLogger({
    level: 'info',
    format: format.combine(
        format.timestamp(),  // timestampを出力する
        format.cli(),
        format.printf(info => `[${info.timestamp}] ${info.level} ${info.message}`)
    ),
    transports: [
        new winston.transports.Console()
    ]
});

logger.debug('Debug Message');
logger.info('Info Message');
logger.warn('Warn Message');
logger.error('Error Message');

タイムスタンプを入れたCLIフォーマットで、追加したタイムスタンプの位置はprintfで決定します。

確認。

$ node winston-example5.js
[2019-05-21T14:33:57.427Z] info     Info Message
[2019-05-21T14:33:57.428Z] warn     Warn Message
[2019-05-21T14:33:57.428Z] error    Error Message

ログレベルは、カラーで出力されます。

CLIフォーマットの正体は、colorize、padLevelsを組み合わせたものです。

CLI

カラー表示をやめたければ、cliの部分をsimpleにするとよいでしょう。

    format: format.combine(
        format.timestamp(),  // timestampを出力する
        format.simple(),
        format.printf(info => `[${info.timestamp}] ${info.level} ${info.message}`)
    ),
ログにErrorを出力させる

ログ出力の際には、try〜catchしたErrorを出力させたいこともあるでしょう。

あんまりドキュメントにそれっぽいことが書かれていなかったので、試してみましたがこんな感じっぽいです。 winston-example6.js

const winston = require('winston');

const logger = winston.createLogger({
    transports: [
        new winston.transports.Console()
    ]
});

logger.error('error: ', new Error('Oops!!'));  // OK
logger.error(new Error('Oops!!'));  // NG

2つ目のログ出力は、NG例です。

確認。

$ node winston-example6.js 
{"level":"error","message":"error: Oops!!","stack":"Error: Oops!!\n    at Object.<anonymous> (/path/to/winston-example6.js:9:25)\n    at Module._compile (internal/modules/cjs/loader.js:701:30)\n    at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10)\n    at Module.load (internal/modules/cjs/loader.js:600:32)\n    at tryModuleLoad (internal/modules/cjs/loader.js:539:12)\n    at Function.Module._load (internal/modules/cjs/loader.js:531:3)\n    at Function.Module.runMain (internal/modules/cjs/loader.js:754:12)\n    at startup (internal/bootstrap/node.js:283:19)\n    at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3)"}
{"level":"error"}

ひとつ目のログは、こんな感じに出力されています。

{"level":"error","message":"error: Oops!!","stack":"Error: Oops!!\n    at Object.<anonymous> (/path/to/winston-example6.js:9:25)\n    at Module._compile (internal/modules/cjs/loader.js:701:30)\n    at Object.Module._extensions..js (internal/modules/cjs/loader.js:712:10)\n    at Module.load (internal/modules/cjs/loader.js:600:32)\n    at tryModuleLoad (internal/modules/cjs/loader.js:539:12)\n    at Function.Module._load (internal/modules/cjs/loader.js:531:3)\n    at Function.Module.runMain (internal/modules/cjs/loader.js:754:12)\n    at startup (internal/bootstrap/node.js:283:19)\n    at bootstrapNodeJSCore (internal/bootstrap/node.js:622:3)"}

よくよく見ると、指定したメッセージに、さらにErrorのメッセージが追加されていますね。

"message":"error: Oops!!"

これは、このあたりで行われているようです。

https://github.com/winstonjs/winston/blob/3.2.1/lib/winston/logger.js#L241-L242

また、2つ目のログは、Errorの情報がキレイに無視されています。

{"level":"error"}

というわけで、メッセージとは別にErrorを指定しましょうということで。

logger.error('error: ', new Error('Oops!!'));  // OK

その他

カテゴリーとかも便利かなぁとは思いますが、今回はいいかなぁと。

Working with multiple Loggers in winston

メモとしては、書き留めておく程度にしておきます。

とりあえず、基本的な使い方としてはこんなところではないでしょうか。

QuarkusのInfinispan Client(Hot Rod) Extensionを試す

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

QuarkusにInfinispan向けのExtensionがあるというので、ちょっと試してみようかと。

Infinispan Client Guide

Infinispan Client Extension

Infinispan Client Extensionということで、Embedded Modeではなく、いわゆるClient/Server Mode(Hot Rod)になります。

なので、RemoteCacheを使ったものが基本になります。

今回は、Quarkus 0.14.0を使います。対応するInfinispanは、なんと10.0.0.Beta2です。思っていたよりも、だいぶ前のめりですね。

ざっと見た感じ、通常のKey/Valueでのアクセス、CDI、Remote Query、Counter、Near Cache、Encryption、Authenticationが
使えるようです。

Infinispan Client Guide

quarkus/infinispan-client-guide.adoc at 0.14.0 · quarkusio/quarkus · GitHub

今回は、簡単にKey/Valueでのアクセスで試してみましょう。

環境

今回の環境は、こちら。

$ java -version
openjdk version "1.8.0_212"
OpenJDK Runtime Environment (build 1.8.0_212-8u212-b03-0ubuntu1.18.04.1-b03)
OpenJDK 64-Bit Server VM (build 25.212-b03, mixed mode)


$ mvn -version
Apache Maven 3.6.1 (d66c9c0b3152b2e69ee9bac180bb8fcc8e6af555; 2019-04-05T04:00:29+09:00)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 1.8.0_212, vendor: Oracle Corporation, runtime: /usr/lib/jvm/java-8-openjdk-amd64/jre
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "4.15.0-48-generic", arch: "amd64", family: "unix"


$ $GRAALVM_HOME/bin/native-image --version
GraalVM Version 1.0.0-rc16 CE

Infinispan Serverは、10.0.0.Beta2のものを起動させておきます。IPアドレスは、172.17.0.2とします。

サンプルプログラムの作成

では、サンプルプログラムを作ってみましょう。

まずはプロジェクトの作成。Infinispan Client Extensionと、JSONも使うのでRESTEasy JSONBをExtensionとして追加します。

$ mvn io.quarkus:quarkus-maven-plugin:0.14.0:create \
    -DprojectGroupId=org.littlewings \
    -DprojectArtifactId=infinispan-client-basic \
    -DclassName="org.littlewings.quarkus.infinispan.InfinispanResource" \
    -Dpath="/infinispan" \
    -Dextensions="resteasy-jsonb,infinispan-client"

今回は、書籍データをお題にJSONデータを扱うJAX-RSリソースクラスを作ってみましょう。

何気なく、Infinispan Clientを使うノリでこんなクラスを用意。
※なお、このコードと次のJAX-RSリソースクラスの組み合わせでは失敗します src/main/java/org/littlewings/quarkus/infinispan/Book.java

package org.littlewings.quarkus.infinispan;

import java.io.Serializable;

public class Book implements Serializable {
    private static final long serialVersionUID = 1L;

    private String isbn;
    private String title;
    private int price;

    public Book() {
    }

    // getter/setterは省略
}

こんな感じの、JAX-RSリソースクラスを用意します。
src/main/java/org/littlewings/quarkus/infinispan/InfinispanResource.java

package org.littlewings.quarkus.infinispan;

import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import io.quarkus.infinispan.client.runtime.Remote;
import org.infinispan.client.hotrod.RemoteCache;

@Path("/infinispan")
public class InfinispanResource {
    @Inject
    @Remote
    RemoteCache<String, Book> defaultCache;

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.TEXT_PLAIN)
    public String register(Book book) {
        defaultCache.put(book.getIsbn(), book);

        return "ok!!";
    }

    @GET
    @Path("{isbn}")
    @Produces(MediaType.APPLICATION_JSON)
    public Book findByIsbn(@PathParam("isbn") String isbn) {
        return defaultCache.get(isbn);
    }
}

RemoteCacheは、こんな感じでCDIによりインジェクションできます。

    @Inject
    @Remote
    RemoteCache<String, Book> defaultCache;

@Remoteアノテーションになにも指定しないとデフォルトのRemoteCacheを使い、名前を指定するとその名前に応じた
RemoteCacheを利用するようになります。

Infinispan Serverは、別のサーバーで動いているので、接続先を指定する必要があります。

その設定は、application.propertiesに書きます。

Configuration

接続先は、「quarkus.infinispan-client.server-list」で定義します。Infinispanのhotrod-client.propertiesとは、ちょっと違う名前ですね。
src/main/resources/application.properties

# Configuration file
# key = value

quarkus.infinispan-client.server-list=172.17.0.2:11222

この他に設定できるのは、「quarkus.infinispan-client.near-cache-max-entries」のようです。

では、それ以外の項目はどうするかというと、hotrod-client.propertiesで指定します。

ただ、通常のInfinispanのHot Rod Clientを使う時と異なり、「META-INF/hotrod-client.properties」に置く必要があります。 src/main/resources/META-INF/hotrod-client.properties

infinispan.client.hotrod.server_list=172.17.0.2:11222

「META-INF/hotrod-client.properties」に関する記述は、こちら。

https://github.com/quarkusio/quarkus/blob/0.14.0/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientProcessor.java#L69

※通常はこちら
infinispan/RemoteCacheManager.java at 10.0.0.Beta2 · infinispan/infinispan · GitHub

「META-INF/hotrod-client.properties」を使う場合は、もともとのHot Rod Clientの設定項目名が使えます。設定の一覧は、こちらです。

org.infinispan.client.hotrod.configuration (Infinispan JavaDoc All 10.0.0.Beta3 API)

では、パッケージングして動かしてみましょう。プロジェクト作成時にテストコードもできていますが、今回は修正していないので
テストはスキップします。

$ mvn package -DskipTests=true

起動。

$ java -jar target/infinispan-client-basic-1.0-SNAPSHOT-runner.jar

こんなJSONファイルを作成して、アクセスしてみます。
book1.json

{
  "isbn": "978-4621303252",
  "title": "Effective Java 第3版",
  "price": 4320
}

実行。

$ curl -XPOST -H 'Content-Type: application/json' localhost:8080/infinispan -d @book1.json
<html><head><title>Error</title></head><body>Internal Server Error</body></html>

コケてしまいました。

2019-05-15 23:41:00,552 ERROR [io.und.request] (executor-thread-1) UT005023: Exception handling request to /infinispan: org.jboss.resteasy.spi.UnhandledException: java.lang.IllegalArgumentException: No marshaller registered for org.littlewings.quarkus.infinispan.Book
    at org.jboss.resteasy.core.ExceptionHandler.handleApplicationException(ExceptionHandler.java:106)
    at org.jboss.resteasy.core.ExceptionHandler.handleException(ExceptionHandler.java:372)
    at org.jboss.resteasy.core.SynchronousDispatcher.writeException(SynchronousDispatcher.java:209)
    at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:496)
    at org.jboss.resteasy.core.SynchronousDispatcher.lambda$invoke$4(SynchronousDispatcher.java:252)
    at org.jboss.resteasy.core.SynchronousDispatcher.lambda$preprocess$0(SynchronousDispatcher.java:153)
    at org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:362)
    at org.jboss.resteasy.core.SynchronousDispatcher.preprocess(SynchronousDispatcher.java:156)
    at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:238)
    at org.jboss.resteasy.plugins.server.servlet.ServletContainerDispatcher.service(ServletContainerDispatcher.java:234)
    at io.quarkus.resteasy.runtime.ResteasyFilter.doFilter(ResteasyFilter.java:45)
    at io.undertow.servlet.core.ManagedFilter.doFilter(ManagedFilter.java:61)
    at io.undertow.servlet.handlers.FilterHandler$FilterChainImpl.doFilter(FilterHandler.java:131)
    at io.undertow.servlet.handlers.FilterHandler.handleRequest(FilterHandler.java:84)
    at io.undertow.servlet.handlers.security.ServletSecurityRoleHandler.handleRequest(ServletSecurityRoleHandler.java:62)
    at io.undertow.servlet.handlers.ServletChain$1.handleRequest(ServletChain.java:68)
    at io.undertow.servlet.handlers.ServletDispatchingHandler.handleRequest(ServletDispatchingHandler.java:36)
    at io.undertow.servlet.handlers.security.SSLInformationAssociationHandler.handleRequest(SSLInformationAssociationHandler.java:132)
    at io.undertow.servlet.handlers.security.ServletAuthenticationCallHandler.handleRequest(ServletAuthenticationCallHandler.java:57)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at io.undertow.security.handlers.AbstractConfidentialityHandler.handleRequest(AbstractConfidentialityHandler.java:46)
    at io.undertow.servlet.handlers.security.ServletConfidentialityConstraintHandler.handleRequest(ServletConfidentialityConstraintHandler.java:64)
    at io.undertow.security.handlers.AuthenticationMechanismsHandler.handleRequest(AuthenticationMechanismsHandler.java:60)
    at io.undertow.servlet.handlers.security.CachedAuthenticatedSessionHandler.handleRequest(CachedAuthenticatedSessionHandler.java:77)
    at io.undertow.security.handlers.AbstractSecurityContextAssociationHandler.handleRequest(AbstractSecurityContextAssociationHandler.java:43)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at io.undertow.server.handlers.PredicateHandler.handleRequest(PredicateHandler.java:43)
    at io.undertow.servlet.handlers.ServletInitialHandler.handleFirstRequest(ServletInitialHandler.java:292)
    at io.undertow.servlet.handlers.ServletInitialHandler.access$100(ServletInitialHandler.java:81)
    at io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:138)
    at io.undertow.servlet.handlers.ServletInitialHandler$2.call(ServletInitialHandler.java:135)
    at io.undertow.servlet.core.ServletRequestContextThreadSetupAction$1.call(ServletRequestContextThreadSetupAction.java:48)
    at io.undertow.servlet.core.ContextClassLoaderSetupAction$1.call(ContextClassLoaderSetupAction.java:43)
    at io.quarkus.undertow.runtime.UndertowDeploymentTemplate$7$1$1.call(UndertowDeploymentTemplate.java:437)
    at io.undertow.servlet.handlers.ServletInitialHandler.dispatchRequest(ServletInitialHandler.java:272)
    at io.undertow.servlet.handlers.ServletInitialHandler.access$000(ServletInitialHandler.java:81)
    at io.undertow.servlet.handlers.ServletInitialHandler$1.handleRequest(ServletInitialHandler.java:104)
    at io.undertow.server.Connectors.executeRootHandler(Connectors.java:364)
    at io.undertow.server.HttpServerExchange$1.run(HttpServerExchange.java:830)
    at org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
    at org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1998)
    at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1525)
    at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1382)
    at org.jboss.threads.DelegatingRunnable.run(DelegatingRunnable.java:29)
    at org.jboss.threads.ThreadLocalResettingRunnable.run(ThreadLocalResettingRunnable.java:32)
    at java.lang.Thread.run(Thread.java:748)
    at org.jboss.threads.JBossThread.run(JBossThread.java:479)
Caused by: java.lang.IllegalArgumentException: No marshaller registered for org.littlewings.quarkus.infinispan.Book
    at org.infinispan.protostream.impl.SerializationContextImpl.getMarshallerDelegate(SerializationContextImpl.java:288)
    at org.infinispan.protostream.WrappedMessage.writeMessage(WrappedMessage.java:240)
    at org.infinispan.protostream.ProtobufUtil.toWrappedByteArray(ProtobufUtil.java:175)
    at org.infinispan.query.remote.client.BaseProtoStreamMarshaller.objectToBuffer(BaseProtoStreamMarshaller.java:57)
    at org.infinispan.commons.marshall.AbstractMarshaller.objectToByteBuffer(AbstractMarshaller.java:70)
    at org.infinispan.client.hotrod.marshall.MarshallerUtil.obj2bytes(MarshallerUtil.java:74)
    at org.infinispan.client.hotrod.DataFormat.valueToBytes(DataFormat.java:98)
    at org.infinispan.client.hotrod.impl.RemoteCacheImpl.valueToBytes(RemoteCacheImpl.java:549)
    at org.infinispan.client.hotrod.impl.RemoteCacheImpl.putAsync(RemoteCacheImpl.java:365)
    at org.infinispan.client.hotrod.impl.RemoteCacheImpl.put(RemoteCacheImpl.java:334)
    at org.infinispan.client.hotrod.impl.RemoteCacheSupport.put(RemoteCacheSupport.java:79)
    at org.littlewings.quarkus.infinispan.InfinispanResource.register(InfinispanResource.java:25)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:151)
    at org.jboss.resteasy.core.MethodInjectorImpl.lambda$invoke$3(MethodInjectorImpl.java:122)
    at java.util.concurrent.CompletableFuture.uniApply(CompletableFuture.java:602)
    at java.util.concurrent.CompletableFuture.uniApplyStage(CompletableFuture.java:614)
    at java.util.concurrent.CompletableFuture.thenApply(CompletableFuture.java:1983)
    at java.util.concurrent.CompletableFuture.thenApply(CompletableFuture.java:110)
    at org.jboss.resteasy.core.MethodInjectorImpl.invoke(MethodInjectorImpl.java:122)
    at org.jboss.resteasy.core.ResourceMethodInvoker.internalInvokeOnTarget(ResourceMethodInvoker.java:568)
    at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTargetAfterFilter(ResourceMethodInvoker.java:442)
    at org.jboss.resteasy.core.ResourceMethodInvoker.lambda$invokeOnTarget$2(ResourceMethodInvoker.java:396)
    at org.jboss.resteasy.core.interception.jaxrs.PreMatchContainerRequestContext.filter(PreMatchContainerRequestContext.java:362)
    at org.jboss.resteasy.core.ResourceMethodInvoker.invokeOnTarget(ResourceMethodInvoker.java:398)
    at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:367)
    at org.jboss.resteasy.core.ResourceMethodInvoker.lambda$invoke$1(ResourceMethodInvoker.java:341)
    at java.util.concurrent.CompletableFuture.uniComposeStage(CompletableFuture.java:981)
    at java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:2124)
    at java.util.concurrent.CompletableFuture.thenCompose(CompletableFuture.java:110)
    at org.jboss.resteasy.core.ResourceMethodInvoker.invoke(ResourceMethodInvoker.java:341)
    at org.jboss.resteasy.core.SynchronousDispatcher.invoke(SynchronousDispatcher.java:477)
    ... 43 more

Marshallerがないよ、と言われています。

Caused by: java.lang.IllegalArgumentException: No marshaller registered for org.littlewings.quarkus.infinispan.Book

https://github.com/quarkusio/quarkus/blob/0.14.0/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/InfinispanClientProducer.java#L96-L97

どうやら、デフォルトでProtoStreamMarshaller(Protocol Buffers)向けのMarshallerを使うようです。

確かに、ガイドにもそれっぽいことが書かれていました。

The default serialization is done using a library based on protobuf. We need to define the proto buf schema and a marshaller for each user type(s).

Serialization (Key Value types support)

ちなみに、Stringやプリミティブのラッパーはbyte配列に変換してくれるようです。

By default the client will support keys and values of the following types: byte[], primitive wrappers (eg. Integer, Long, Double etc.), String, Date and Instant.

対応する範囲は、このあたりのようですね。

infinispan/BaseProtoStreamMarshaller.java at 10.0.0.Beta2 · infinispan/infinispan · GitHub

くどくなるので実行結果は載せませんが、以下のようにStringをRemoteCacheに入れるようなコードは、特になにも用意せずとも
動作します。
src/main/java/org/littlewings/quarkus/infinispan/InfinispanStringResource.java

package org.littlewings.quarkus.infinispan;

import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;

import io.quarkus.infinispan.client.runtime.Remote;
import org.infinispan.client.hotrod.RemoteCache;

@Path("/infinispan-string")
public class InfinispanStringResource {
    @Inject
    @Remote
    RemoteCache<String, String> defaultCache;

    @GET
    @Path("register")
    @Produces(MediaType.TEXT_PLAIN)
    public String register(@QueryParam("key") String key, @QueryParam("value") String value) {
        defaultCache.put(key, value);

        return "ok!!";
    }

    @GET
    @Path("{key}")
    @Produces(MediaType.TEXT_PLAIN)
    public String find(@PathParam("key") String isbn) {
        return defaultCache.get(isbn);
    }
}

Protocol Buffers用のMarshallerを書く

というわけで、自分で定義したクラスをRemoteCacheに登録するには、Protocol Bufferesを使うことになります。
ちなみに、今のInfinispanでProtocol Buffersを使う場合、Protocol Bufferes 2を使います。

気を取り直して、新しいクラスで定義し直すことにしましょう。 src/main/java/org/littlewings/quarkus/infinispan/ProtoBook.java

package org.littlewings.quarkus.infinispan;

public class ProtoBook {
    private String isbn;
    private String title;
    private int price;

    public static ProtoBook create(String isbn, String title, int price) {
        ProtoBook protoBook = new ProtoBook();
        protoBook.setIsbn(isbn);
        protoBook.setTitle(title);
        protoBook.setPrice(price);
        return protoBook;
    }

    public ProtoBook() {
    }

    // getter、setterは省略
}

このクラスに対応する、Protocol Buffersのスキーマ定義を書きます。 src/main/resources/META-INF/library.proto

package proto_book;

message ProtoBook {
    required string isbn = 1;
    required string title = 2;
    required int32 price = 3;
}

META-INF配下に「.proto」拡張子のファイルを置いておくと、ビルド時にQuarkusが検出してくれます。

https://github.com/quarkusio/quarkus/blob/0.14.0/extensions/infinispan-client/deployment/src/main/java/io/quarkus/infinispan/client/deployment/InfinispanClientProcessor.java#L122-L125

作成したクラスおよびprotoファイルに対する、Marshallerを作成します。 src/main/java/org/littlewings/quarkus/infinispan/ProtoBookMarshaller.java

package org.littlewings.quarkus.infinispan;

import java.io.IOException;

import org.infinispan.protostream.MessageMarshaller;

public class ProtoBookMarshaller implements MessageMarshaller<ProtoBook> {
    @Override
    public ProtoBook readFrom(ProtoStreamReader reader) throws IOException {
        return ProtoBook.create(
                reader.readString("isbn"),
                reader.readString("title"),
                reader.readInt("price")
        );
    }

    @Override
    public void writeTo(ProtoStreamWriter writer, ProtoBook protoBook) throws IOException {
        writer.writeString("isbn", protoBook.getIsbn());
        writer.writeString("title", protoBook.getTitle());
        writer.writeInt("price", protoBook.getPrice());
    }

    @Override
    public Class<? extends ProtoBook> getJavaClass() {
        return ProtoBook.class;
    }

    @Override
    public String getTypeName() {
        return "proto_book.ProtoBook";
    }
}

そして、このMarshallerに対するProducerを作成します。
src/main/java/org/littlewings/quarkus/infinispan/Bootstrap.java

package org.littlewings.quarkus.infinispan;

import javax.enterprise.context.ApplicationScoped;
import javax.enterprise.inject.Produces;

import org.infinispan.protostream.MessageMarshaller;

@ApplicationScoped
public class Bootstrap {
    @Produces
    MessageMarshaller protoBookMessageMarshaller() {
    // MessageMarshaller<ProtoBook> protoBookMessageMarshaller() {  // これはダメ
        return new ProtoBookMarshaller();
    }
}

ここまでやって、やっとMarshallerが利用できるようになります。

なお、コメントに書いていますが、メソッドの戻り値のMessageMarshallerの型に対して、変に型パラメーターを与えてはいけません。

    // MessageMarshaller<ProtoBook> protoBookMessageMarshaller() {  // これはダメ

Quarkus内でMessageMarshaller型のCDI管理Beanを取得する時に、失敗するようになります。

https://github.com/quarkusio/quarkus/blob/0.14.0/extensions/infinispan-client/runtime/src/main/java/io/quarkus/infinispan/client/runtime/InfinispanClientProducer.java#L186

結果、こんな感じで見つけられなくなってしまうので、ご注意を。

23:03:33,082 ERROR [io.qua.dev.DevModeMain] Failed to start quarkus: java.lang.RuntimeException: io.quarkus.builder.BuildException: Build failure: Build failed due to errors
    [error]: Build step io.quarkus.infinispan.client.deployment.InfinispanClientProcessor#setup threw an exception: java.lang.IllegalStateException: java.lang.ClassNotFoundException: org.littlewings.quarkus.infinispan.ProtoBookBinaryMarshaller
    at io.quarkus.runner.RuntimeRunner.run(RuntimeRunner.java:137)
    at io.quarkus.dev.DevModeMain.doStart(DevModeMain.java:131)
    at io.quarkus.dev.DevModeMain.main(DevModeMain.java:84)
Caused by: io.quarkus.builder.BuildException: Build failure: Build failed due to errors
    [error]: Build step io.quarkus.infinispan.client.deployment.InfinispanClientProcessor#setup threw an exception: java.lang.IllegalStateException: java.lang.ClassNotFoundException: org.littlewings.quarkus.infinispan.ProtoBookBinaryMarshaller
    at io.quarkus.builder.Execution.run(Execution.java:124)
    at io.quarkus.builder.BuildExecutionBuilder.execute(BuildExecutionBuilder.java:137)
    at io.quarkus.deployment.QuarkusAugmentor.run(QuarkusAugmentor.java:108)
    at io.quarkus.runner.RuntimeRunner.run(RuntimeRunner.java:102)
    ... 2 more
Caused by: java.lang.IllegalStateException: java.lang.ClassNotFoundException: org.littlewings.quarkus.infinispan.ProtoBookBinaryMarshaller
    at io.quarkus.deployment.ExtensionLoader$1.execute(ExtensionLoader.java:516)
    at io.quarkus.builder.BuildContext.run(BuildContext.java:414)
    at org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
    at org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1998)
    at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1525)
    at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1416)
    at java.lang.Thread.run(Thread.java:748)
    at org.jboss.threads.JBossThread.run(JBossThread.java:479)
Caused by: java.lang.ClassNotFoundException: org.littlewings.quarkus.infinispan.ProtoBookBinaryMarshaller
    at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:264)
    at io.quarkus.infinispan.client.runtime.InfinispanClientProducer.replaceProperties(InfinispanClientProducer.java:93)
    at io.quarkus.infinispan.client.deployment.InfinispanClientProcessor.setup(InfinispanClientProcessor.java:111)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at io.quarkus.deployment.ExtensionLoader$1.execute(ExtensionLoader.java:507)
    ... 7 more

なお、現在のHot Rod Clientは、アノテーションでこのあたりのMarshallerなどの処理は書けるのですが、Quarkusではまだサポート
していないようです。

Annotation based proto stream marshalling is not yet supported in the Quarkus Infinispan client. This will be added soon, allowing you to only annotate your classes, skipping the following steps.

では、このクラスを使うJAX-RSリソースクラスを作成します。

src/main/java/org/littlewings/quarkus/infinispan/InfinispanProtoResource.java

package org.littlewings.quarkus.infinispan;

import javax.inject.Inject;
import javax.ws.rs.Consumes;
import javax.ws.rs.GET;
import javax.ws.rs.POST;
import javax.ws.rs.Path;
import javax.ws.rs.PathParam;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import io.quarkus.infinispan.client.runtime.Remote;
import org.infinispan.client.hotrod.RemoteCache;

@Path("/infinispan-proto")
public class InfinispanProtoResource {
    @Inject
    @Remote("protoBookCache")
    RemoteCache<String, ProtoBook> protoBookCache;

    @POST
    @Consumes(MediaType.APPLICATION_JSON)
    @Produces(MediaType.TEXT_PLAIN)
    public String register(ProtoBook book) {
        protoBookCache.put(book.getIsbn(), book);

        return "ok!!";
    }

    @GET
    @Path("{isbn}")
    @Produces(MediaType.APPLICATION_JSON)
    public ProtoBook findByIsbn(@PathParam("isbn") String isbn) {
        return protoBookCache.get(isbn);
    }
}

せっかくなので、新しいRemoteCacheを作成しましょうか。

    @Inject
    @Remote("protoBookCache")
    RemoteCache<String, ProtoBook> protoBookCache;

Infinispan Server側で、「protoBookCache:add」というCacheを作成。

$ bin/ispn-cli.sh -c --command='/subsystem=datagrid-infinispan/cache-container=clustered/configurations=CONFIGURATIONS/distributed-cache-configuration=protoBookCacheConfiguration:add(start=EAGER,mode=SYNC)'
WARN: can't find jboss-cli.xml. Using default configuration values.
{"outcome" => "success"}
$ bin/ispn-cli.sh -c --command='/subsystem=datagrid-infinispan/cache-container=clustered/distributed-cache=protoBookCache:add(configuration=protoBookCacheConfiguration)'
WARN: can't find jboss-cli.xml. Using default configuration values.
{"outcome" => "success"}

なお、Quarkusのサンプルにあるように、APIでもCacheは作れるようですが、今回は試していません。

https://github.com/quarkusio/quarkus-quickstarts/blob/0.14.0/infinispan-client/src/main/java/org/acme/infinispanclient/InfinispanClientApp.java#L24

ここまで準備すると、動作するようになります。

ビルドして、起動。

$ mvn package -DskipTests=true
$ java -jar target/infinispan-client-basic-1.0-SNAPSHOT-runner.jar

今度はOKです。

$ curl -XPOST -H 'Content-Type: application/json' localhost:8080/infinispan-proto -d @book1.json
ok!!

$ curl localhost:8080/infinispan-proto/978-4621303252
{"isbn":"978-4621303252","price":4320,"title":"Effective Java 第3版"}

ネイティブイメージでも、確認してみましょう。

$ mvn -Pnative package -DskipTests=true
$ ./target/infinispan-client-basic-1.0-SNAPSHOT-runner

同じ結果になります。

$ curl -XPOST -H 'Content-Type: application/json' localhost:8080/infinispan-proto -d @book1.json
ok!!

$ curl localhost:8080/infinispan-proto/978-4621303252
{"isbn":"978-4621303252","price":4320,"title":"Effective Java 第3版"}

というわけで、簡単な範囲ですが確認できました。

通常のInfinispanのHot Rod Clientとは、少し勝手が違うんですねー。

Marshallerについて補足

ところで、ドキュメントには「org.infinispan.commons.marshaller.Marshaller」インターフェースを実装したクラスを作成して、
設定してもいいよ、みたいなことが書かれています。

Providing your own Marshaller

最初に失敗したBookクラスを、シリアライズできるようにしてみましょう。

Marshallerを作成します。めちゃくちゃその場しのぎ感がすごいですが…。
src/main/java/org/littlewings/quarkus/infinispan/BookMarshaller.java

package org.littlewings.quarkus.infinispan;

import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.nio.charset.StandardCharsets;

import org.infinispan.commons.dataconversion.MediaType;
import org.infinispan.commons.io.ByteBuffer;
import org.infinispan.commons.io.ByteBufferImpl;
import org.infinispan.commons.marshall.AbstractMarshaller;
import org.infinispan.commons.marshall.Marshaller;
import org.infinispan.commons.marshall.StringMarshaller;

public class BookMarshaller extends AbstractMarshaller {
    Marshaller marshaller = new StringMarshaller(StandardCharsets.UTF_8);

    @Override
    protected ByteBuffer objectToBuffer(Object o, int estimatedSize) throws IOException, InterruptedException {
        if (o instanceof String) {
            return marshaller.objectToBuffer(o);
        }

        ByteArrayOutputStream baos = new ByteArrayOutputStream();

        try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
            oos.writeUTF(((Book) o).getIsbn());
            oos.writeUTF(((Book) o).getTitle());
            oos.writeInt(((Book) o).getPrice());
        }

        return new ByteBufferImpl(baos.toByteArray());
    }

    @Override
    public Object objectFromByteBuffer(byte[] buf, int offset, int length) throws IOException, ClassNotFoundException {
        ByteArrayInputStream bais = new ByteArrayInputStream(buf);

        Book book = new Book();

        try (ObjectInputStream ois = new ObjectInputStream(bais)) {
            book.setIsbn(ois.readUTF());
            book.setTitle(ois.readUTF());
            book.setPrice(ois.readInt());
        }

        return book;
    }

    @Override
    public boolean isMarshallable(Object o) throws Exception {
        return o instanceof Book;
    }

    @Override
    public MediaType mediaType() {
        return MediaType.APPLICATION_SERIALIZED_OBJECT;
    }
}

ObjectOutputStream.writeObjectなどは(ネイティブイメージにすると)使えないので、今回はこんな感じに…。

途中で、「もうProtocol Buffersでいいや」という気分になったので、ほんっと適当です。

このクラスを、「infinispan.client.hotrod.marshaller」に登録します。
src/main/resources/META-INF/hotrod-client.properties

infinispan.client.hotrod.marshaller=org.littlewings.quarkus.infinispan.BookMarshaller

ビルドして起動。

$ mvn package -DskipTests=true
$ java -jar target/infinispan-client-basic-1.0-SNAPSHOT-runner.jar

すると、最初に失敗したJAX-RSリソースクラスも動作するようになります。

$ curl -XPOST -H 'Content-Type: application/json' localhost:8080/infinispan -d @book1.json
ok!!


$ curl localhost:8080/infinispan/978-4621303252
{"isbn":"978-4621303252","price":4320,"title":"Effective Java 第3版"}

ネイティブイメージにしても、問題なく動きます。

なのですが、「quarkus:dev」だと

$ mvn compile quarkus:dev

作成したMarshallerが見つけられなくなります…。

00:53:49,730 ERROR [io.qua.dev.DevModeMain] Failed to start quarkus: java.lang.RuntimeException: io.quarkus.builder.BuildException: Build failure: Build failed due to errors
    [error]: Build step io.quarkus.infinispan.client.deployment.InfinispanClientProcessor#setup threw an exception: java.lang.IllegalStateException: java.lang.ClassNotFoundException: org.littlewings.quarkus.infinispan.BookMarshaller
    at io.quarkus.runner.RuntimeRunner.run(RuntimeRunner.java:137)
    at io.quarkus.dev.DevModeMain.doStart(DevModeMain.java:131)
    at io.quarkus.dev.DevModeMain.main(DevModeMain.java:84)
Caused by: io.quarkus.builder.BuildException: Build failure: Build failed due to errors
    [error]: Build step io.quarkus.infinispan.client.deployment.InfinispanClientProcessor#setup threw an exception: java.lang.IllegalStateException: java.lang.ClassNotFoundException: org.littlewings.quarkus.infinispan.BookMarshaller
    at io.quarkus.builder.Execution.run(Execution.java:124)
    at io.quarkus.builder.BuildExecutionBuilder.execute(BuildExecutionBuilder.java:137)
    at io.quarkus.deployment.QuarkusAugmentor.run(QuarkusAugmentor.java:108)
    at io.quarkus.runner.RuntimeRunner.run(RuntimeRunner.java:102)
    ... 2 more
Caused by: java.lang.IllegalStateException: java.lang.ClassNotFoundException: org.littlewings.quarkus.infinispan.BookMarshaller
    at io.quarkus.deployment.ExtensionLoader$1.execute(ExtensionLoader.java:516)
    at io.quarkus.builder.BuildContext.run(BuildContext.java:414)
    at org.jboss.threads.ContextClassLoaderSavingRunnable.run(ContextClassLoaderSavingRunnable.java:35)
    at org.jboss.threads.EnhancedQueueExecutor.safeRun(EnhancedQueueExecutor.java:1998)
    at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.doRunTask(EnhancedQueueExecutor.java:1525)
    at org.jboss.threads.EnhancedQueueExecutor$ThreadBody.run(EnhancedQueueExecutor.java:1416)
    at java.lang.Thread.run(Thread.java:748)
    at org.jboss.threads.JBossThread.run(JBossThread.java:479)
Caused by: java.lang.ClassNotFoundException: org.littlewings.quarkus.infinispan.BookMarshaller
    at java.net.URLClassLoader.findClass(URLClassLoader.java:382)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:424)
    at sun.misc.Launcher$AppClassLoader.loadClass(Launcher.java:349)
    at java.lang.ClassLoader.loadClass(ClassLoader.java:357)
    at java.lang.Class.forName0(Native Method)
    at java.lang.Class.forName(Class.java:264)
    at io.quarkus.infinispan.client.runtime.InfinispanClientProducer.replaceProperties(InfinispanClientProducer.java:93)
    at io.quarkus.infinispan.client.deployment.InfinispanClientProcessor.setup(InfinispanClientProcessor.java:111)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
    at java.lang.reflect.Method.invoke(Method.java:498)
    at io.quarkus.deployment.ExtensionLoader$1.execute(ExtensionLoader.java:507)
    ... 7 more

いろいろ遠いですねぇ…。