CLOVER🍀

That was when it all began.

Infinispan 10.1のCacheLoader/CacheWriter(ExternalStore)を試す

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

最近、久しぶりにInfinispanのCacheStoreまわりのAPIをちょっと眺めてみたら、特にAdvancedCacheLoader/AdvancedCacheWriterまわりが
だいぶ変わっているのに驚きまして。

いい機会なので、もう1度CacheStoreまわりのAPIを見ていこうかなと。

いきなりAdvanced〜の方にいくとハードルが高いので、今回はまずCacheLoader/CacheWriterから使っていきます。

Persistence

こちらは、Infinispan 6.0の頃に書いたエントリです。

InfinispanのCacheLoader/CacheWriterを使ってみる - CLOVER🍀

今回はInfinispan 10.1で見直してみます。

Setting Up Persistent Storage

InfinispanからいくつかCacheStoreの実装は提供されていますが、今回は自分でCacheStoreを実装する方を見ていきます。
これが、Infinispan Persistence SPIで、外部ストレージへの読み書きを行うためのAPIとなります。

Infinispan Persistence SPIs

Infinispan Persistence SPIは、以下の特徴を持つと書かれています。

  • 標準に準拠 … JSR-107仕様に準拠したCacheWriter、CacheLoaderインターフェース(※1)
  • トランザクションへの統合 … Infinispan側でロックを扱うため、CacheStoreの実装は永続ストアへの同時アクセスを考慮しなくてよい(※2)
  • Parallel Iteration … マルチスレッドを使った並列処理で永続ストアにあるエントリを扱うことができる
  • シリアライズの削減 … Infinispanは保存されたエントリを、リモートに送信可能なシリアライズされた形式で公開しているため、永続ストアからの取得時のデシリアライズ、書き込み時の再度のシリアライズは不要

※1 … そう?
※2 … 永続ストア自体は、複数のスレッドからアクセスされることを許容する必要がある

Infinispan Persistence SPIは、以下のAPIから構成されます。

  • ByteBuffer … シリアライズされたオブジェクトを抽象化したもの
  • MarshallableEntry … Cacheに登録されたキー/値を永続ストア内に保持するために抽象化したもので、シリアライズ/デシリアライズされた形式の両方が扱える。なお、シリアライズは遅延処理される
  • CacheWriter/CacheLoader … 永続ストアへの読み書きに対する基本的な機能を実現
  • AdvancedCacheLoader/AdvancedCacheWriter … 並列でのイテレーション、有効期限切れエントリのパージ、クリア、サイズ取得といった、永続ストレージに対するバルク操作を実現
  • SegmentedAdvancedLoadWriteStore … セグメントを扱う操作を実現

今回はCacheLoaderおよびCacheWriterを扱うのですが、実際に使うにはAdvancedCacheLoader/AdvancedCacheWriterがないと
機能的には足りないことがわかります。AdvancedCacheLoaderを実装していない場合はイテレーションができず、Expireにも対応
できません。AdvancedCacheWriterを実装していない場合は、有効期限切れのエントリをパージしたりクリアもできません。

なのですが、Advanced〜に関するAPIは割と難しいので、また機会を改めて。

あと、SegmentedAdvancedLoadWriteStoreが追加されていたのは知らなかったですね。Infinispan 9.4からのようです。

では、説明はこれくらいにして、今回はCacheLoader/CacheWriterを使っていってみます。

環境

今回の環境は、こちら。

$ java --version
openjdk 11.0.6 2020-01-14
OpenJDK Runtime Environment (build 11.0.6+10-post-Ubuntu-1ubuntu118.04.1)
OpenJDK 64-Bit Server VM (build 11.0.6+10-post-Ubuntu-1ubuntu118.04.1, mixed mode, sharing)


$ mvn --version
Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 11.0.6, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "4.18.0-25-generic", arch: "amd64", family: "unix"

準備

Maven依存関係は、こちら。

    <dependencies>
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-core</artifactId>
            <version>10.1.5.Final</version>
        </dependency>

        <dependency>
            <groupId>org.junit.platform</groupId>
            <artifactId>junit-platform-launcher</artifactId>
            <version>1.6.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.junit.jupiter</groupId>
            <artifactId>junit-jupiter-engine</artifactId>
            <version>5.6.1</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.15.0</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

Infinispanは10.1.5.Finalを使用します。また、JUnit 5とAssertJはテストコード用です。

お題

今回は、CacheStoreが使う永続ストレージとしてMap(永続じゃないですが)を使うことにします。

極めて簡易なものですが、とりあえず基本的な使い方ということで。

作成するものは、設定まわりでStoreConfiguration、StoreConfigurationBuilderの実装、そして永続化を扱うためにCacheLoader/CacheWriterの
実装です。

設定まわりのクラスを作る

では、最初に設定まわりのクラスを作成します。

StoreConfiguration (Infinispan JavaDoc All 10.1.5.Final API)

StoreConfigurationBuilder (Infinispan JavaDoc All 10.1.5.Final API)

StoreConfigurationの実装。
src/main/java/org/littlewings/infinispan/persistence/SimpleMapCacheStoreConfiguration.java

package org.littlewings.infinispan.persistence;

import org.infinispan.commons.configuration.BuiltBy;
import org.infinispan.commons.configuration.ConfigurationFor;
import org.infinispan.commons.configuration.attributes.AttributeSet;
import org.infinispan.configuration.cache.AbstractStoreConfiguration;
import org.infinispan.configuration.cache.AsyncStoreConfiguration;

@BuiltBy(SimpleMapCacheStoreConfigurationBuilder.class)
@ConfigurationFor(SimpleMapCacheStore.class)
public class SimpleMapCacheStoreConfiguration extends AbstractStoreConfiguration {
    public SimpleMapCacheStoreConfiguration(AttributeSet attributes, AsyncStoreConfiguration async) {
        super(attributes, async);
    }
}

@BuildByアノテーションでStoreConfigurationBuilderの実装を、@ConfigurationForアノテーションでCacheStoreの実装を指定します。

StoreConfigurationBuilderは、CacheStoreの設定を行うためのBuilderですね。Infinispanの設定をAPIで行おうとすると、
ConfigurationBuilderからbuildしてConfigurationを作るという流れが登場するのですが、自作のCacheStore用に作成している
感じです。

StoreConfigurationBuilder側。
src/main/java/org/littlewings/infinispan/persistence/SimpleMapCacheStoreConfigurationBuilder.java

package org.littlewings.infinispan.persistence;

import org.infinispan.commons.configuration.ConfigurationBuilderInfo;
import org.infinispan.configuration.cache.AbstractStoreConfiguration;
import org.infinispan.configuration.cache.AbstractStoreConfigurationBuilder;
import org.infinispan.configuration.cache.PersistenceConfigurationBuilder;

public class SimpleMapCacheStoreConfigurationBuilder extends AbstractStoreConfigurationBuilder<SimpleMapCacheStoreConfiguration, SimpleMapCacheStoreConfigurationBuilder> implements ConfigurationBuilderInfo {
    public SimpleMapCacheStoreConfigurationBuilder(PersistenceConfigurationBuilder builder) {
        super(builder, AbstractStoreConfiguration.attributeDefinitionSet());
    }

    @Override
    public SimpleMapCacheStoreConfiguration create() {
        return new SimpleMapCacheStoreConfiguration(attributes.protect(), async.create());
    }

    @Override
    public SimpleMapCacheStoreConfigurationBuilder self() {
        return this;
    }
}

クラス宣言時のジェネリクスがややこしいですが、まあ、こんな感じです。

なお、StoreConfigurationもStoreConfigurationBuilderも、最低限の実装です。この内容なら、実は作成しなくてもよいくらいですが、
それは後で少し紹介します。

設定とかをちゃんと定義したい場合は、既存のCacheStoreを参考にしましょう。

https://github.com/infinispan/infinispan/tree/10.1.5.Final/core/src/main/java/org/infinispan/configuration/cache

https://github.com/infinispan/infinispan/tree/10.1.5.Final/core/src/main/java/org/infinispan/persistence/file

https://github.com/infinispan/infinispan/tree/10.1.5.Final/core/src/main/java/org/infinispan/persistence/cluster

https://github.com/infinispan/infinispan/tree/10.1.5.Final/persistence/soft-index

https://github.com/infinispan/infinispan/tree/10.1.5.Final/persistence/rocksdb

https://github.com/infinispan/infinispan/tree/10.1.5.Final/persistence/jdbc

https://github.com/infinispan/infinispan/tree/10.1.5.Final/persistence/jpa

https://github.com/infinispan/infinispan/tree/10.1.5.Final/persistence/remote

https://github.com/infinispan/infinispan/tree/10.1.5.Final/persistence/rest

CacheLoader/CacheWriterの実装を作る

続いて、先ほどのStoreConfiguration、StoreConfigurationBuilderにより作成される、CacheLoader/CacheWriterの実装クラスを
作成します。

CacheLoader (Infinispan JavaDoc All 10.1.5.Final API)

CacheWriter (Infinispan JavaDoc All 10.1.5.Final API)

この2つのインターフェースを実装したクラスを作成するのですが、今回はこの2つのインターフェースを継承したExternalStore
インターフェースを実装することにします。

ExternalStore (Infinispan JavaDoc All 10.1.5.Final API)

で、作ったのはこんな感じ。
src/main/java/org/littlewings/infinispan/persistence/SimpleMapCacheStore.java

package org.littlewings.infinispan.persistence;

import java.io.IOException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

import org.infinispan.commons.configuration.ConfiguredBy;
import org.infinispan.commons.io.ByteBuffer;
import org.infinispan.commons.marshall.Marshaller;
import org.infinispan.commons.persistence.Store;
import org.infinispan.configuration.cache.CustomStoreConfiguration;
import org.infinispan.persistence.spi.ExternalStore;
import org.infinispan.persistence.spi.InitializationContext;
import org.infinispan.persistence.spi.MarshallableEntry;
import org.infinispan.persistence.spi.MarshallableEntryFactory;
import org.infinispan.persistence.spi.PersistenceException;
import org.jboss.logging.Logger;

@Store
@ConfiguredBy(SimpleMapCacheStoreConfiguration.class)
public class SimpleMapCacheStore<K, V> implements ExternalStore<K, V> {
    Logger logger = Logger.getLogger(getClass());

    SimpleMapCacheStoreConfiguration storeConfiguration;
    //CustomStoreConfiguration storeConfiguration;
    String storeName;

    Map<ByteBuffer, EntryData> store;

    InitializationContext ctx;

    @Override
    public void init(InitializationContext ctx) {
        this.ctx = ctx;
        storeConfiguration = ctx.getConfiguration();

        if (storeConfiguration.properties().getProperty("storeName") != null) {
            storeName = storeConfiguration.properties().getProperty("storeName");
        } else {
            storeName = "defaultStoreName";
        }

        logging("initialized");
    }

    @Override
    public void write(MarshallableEntry<? extends K, ? extends V> entry) {
        ByteBuffer keyAsBinary = entry.getKeyBytes();
        ByteBuffer valueAsBinary = entry.getValueBytes();

        long created = entry.created();
        long expiryTime = entry.expiryTime();
        long lastUsed = entry.lastUsed();

        ByteBuffer metadataAsBinary = entry.getMetadataBytes();

        EntryData entryData = EntryData.create(valueAsBinary, created, expiryTime, lastUsed, metadataAsBinary);

        store.put(keyAsBinary, entryData);

        logging(
                "write entry: %s / %s, created = %d, expiryTime = %d, lastUsed = %d, metadata = %s",
                entry.getKey(),
                entry.getValue(),
                entry.created(),
                entry.expiryTime(),
                entry.lastUsed(),
                entry.getMetadata()
        );

    }

    @Override
    public MarshallableEntry<K, V> loadEntry(Object key) {
        MarshallableEntryFactory<K, V> marshallableEntryFactory = ctx.getMarshallableEntryFactory();
        Marshaller marshaller = ctx.getPersistenceMarshaller().getUserMarshaller();

        try {
            ByteBuffer keyAsBinary = marshaller.objectToBuffer(key);
            EntryData entryData = store.get(keyAsBinary);

            if (entryData != null) {
                MarshallableEntry<K, V> marshallableEntry =
                        marshallableEntryFactory.create(keyAsBinary, entryData.valueAsBinry, entryData.metadataAsBinary, entryData.getCreated(), entryData.getLastUsed());

                logging(
                        "load entry: %s / %s, created = %d, expiryTime = %d, lastUsed = %d, metadata = %s",
                        marshallableEntry.getKey(),
                        marshallableEntry.getValue(),
                        marshallableEntry.created(),
                        marshallableEntry.expiryTime(),
                        marshallableEntry.lastUsed(),
                        marshallableEntry.getMetadata()
                );
                return marshallableEntry;
            } else {
                logging("missing entry: %s", key);
                return null;
            }
        } catch (IOException e) {
            throw new PersistenceException(e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return null;
        }
    }

    @Override
    public boolean delete(Object key) {
        try {
            Marshaller marshaller = ctx.getPersistenceMarshaller();
            ByteBuffer keyAsBinary = marshaller.objectToBuffer(key);
            EntryData deleted = store.remove(keyAsBinary);

            logging("deleted? %b, key = %s", deleted != null, key);

            return deleted != null;
        } catch (IOException e) {
            throw new PersistenceException(e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }

    @Override
    public boolean contains(Object key) {
        try {
            Marshaller marshaller = ctx.getPersistenceMarshaller();
            ByteBuffer keyAsBinary = marshaller.objectToBuffer(key);

            boolean contains = store.containsKey(keyAsBinary);

            logging("contains? %b, key = %s", contains, key);

            return contains;
        } catch (IOException e) {
            throw new PersistenceException(e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }

    @Override
    public void start() {
        store = new ConcurrentHashMap<>();

        logging("cache store, startup");
    }

    @Override
    public void stop() {
        store.clear();

        logging("cache store, stopped");
    }

    void logging(String messageTemplate, Object... params) {
        List<Object> mergeParams = new ArrayList<>();
        mergeParams.add(ctx.getCache().getAdvancedCache().getDistributionManager().getCacheTopology().getLocalAddress());
        mergeParams.add(getClass().getSimpleName());
        mergeParams.add(storeName);
        mergeParams.addAll(Arrays.asList(params));

        logger.infof("[%s - %s - %s] " + messageTemplate, mergeParams.toArray());
    }
}

データの持ち先は、Mapにします。

    Map<ByteBuffer, EntryData> store;

簡単にConcurrentHashMapということで。ライフサイクルメソッドで、初期化とクリアを仕込み。

    @Override
    public void start() {
        store = new ConcurrentHashMap<>();

        logging("cache store, startup");
    }

    @Override
    public void stop() {
        store.clear();

        logging("cache store, stopped");
    }

initは、CacheLoader/CacheWriterの初期化メソッドです。この引数で受け取るInitializationContextから、設定やMarshallerなどを
取得することができます。

    @Override
    public void init(InitializationContext ctx) {
        this.ctx = ctx;
        storeConfiguration = ctx.getConfiguration();

        if (storeConfiguration.properties().getProperty("storeName") != null) {
            storeName = storeConfiguration.properties().getProperty("storeName");
        } else {
            storeName = "defaultStoreName";
        }

        logging("initialized");
    }

また、今回はストアの名前をプロパティとして受け取ることにしました。

CacheStore操作時のログは、Node名、クラス名、プロパティで指定されたストア名を出力するように実装。

    void logging(String messageTemplate, Object... params) {
        List<Object> mergeParams = new ArrayList<>();
        mergeParams.add(ctx.getCache().getAdvancedCache().getDistributionManager().getCacheTopology().getLocalAddress());
        mergeParams.add(getClass().getSimpleName());
        mergeParams.add(storeName);
        mergeParams.addAll(Arrays.asList(params));

        logger.infof("[%s - %s - %s] " + messageTemplate, mergeParams.toArray());
    }

エントリの書き込み、読み込み、削除。

    @Override
    public void write(MarshallableEntry<? extends K, ? extends V> entry) {
        ByteBuffer keyAsBinary = entry.getKeyBytes();
        ByteBuffer valueAsBinary = entry.getValueBytes();

        long created = entry.created();
        long expiryTime = entry.expiryTime();
        long lastUsed = entry.lastUsed();

        ByteBuffer metadataAsBinary = entry.getMetadataBytes();

        EntryData entryData = EntryData.create(valueAsBinary, created, expiryTime, lastUsed, metadataAsBinary);

        store.put(keyAsBinary, entryData);

        logging(
                "write entry: %s / %s, created = %d, expiryTime = %d, lastUsed = %d, metadata = %s",
                entry.getKey(),
                entry.getValue(),
                entry.created(),
                entry.expiryTime(),
                entry.lastUsed(),
                entry.getMetadata()
        );

    }

    @Override
    public MarshallableEntry<K, V> loadEntry(Object key) {
        MarshallableEntryFactory<K, V> marshallableEntryFactory = ctx.getMarshallableEntryFactory();
        Marshaller marshaller = ctx.getPersistenceMarshaller().getUserMarshaller();

        try {
            ByteBuffer keyAsBinary = marshaller.objectToBuffer(key);
            EntryData entryData = store.get(keyAsBinary);

            if (entryData != null) {
                MarshallableEntry<K, V> marshallableEntry =
                        marshallableEntryFactory.create(keyAsBinary, entryData.valueAsBinry, entryData.metadataAsBinary, entryData.getCreated(), entryData.getLastUsed());

                logging(
                        "load entry: %s / %s, created = %d, expiryTime = %d, lastUsed = %d, metadata = %s",
                        marshallableEntry.getKey(),
                        marshallableEntry.getValue(),
                        marshallableEntry.created(),
                        marshallableEntry.expiryTime(),
                        marshallableEntry.lastUsed(),
                        marshallableEntry.getMetadata()
                );
                return marshallableEntry;
            } else {
                logging("missing entry: %s", key);
                return null;
            }
        } catch (IOException e) {
            throw new PersistenceException(e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return null;
        }
    }

    @Override
    public boolean delete(Object key) {
        try {
            Marshaller marshaller = ctx.getPersistenceMarshaller();
            ByteBuffer keyAsBinary = marshaller.objectToBuffer(key);
            EntryData deleted = store.remove(keyAsBinary);

            logging("deleted? %b, key = %s", deleted != null, key);

            return deleted != null;
        } catch (IOException e) {
            throw new PersistenceException(e);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
            return false;
        }
    }

MarshallableEntryというクラスが、マーシャリングした形式のデータを持っているので、こちらを永続ストア(今回はMapですが)に
保存します。

    @Override
    public void write(MarshallableEntry<? extends K, ? extends V> entry) {
        ByteBuffer keyAsBinary = entry.getKeyBytes();
        ByteBuffer valueAsBinary = entry.getValueBytes();

        long created = entry.created();
        long expiryTime = entry.expiryTime();
        long lastUsed = entry.lastUsed();

        ByteBuffer metadataAsBinary = entry.getMetadataBytes();

        EntryData entryData = EntryData.create(valueAsBinary, created, expiryTime, lastUsed, metadataAsBinary);

なお、EntryDataというのは値と各種メタデータを保存するために、今回作成したクラスです。
src/main/java/org/littlewings/infinispan/persistence/EntryData.java

package org.littlewings.infinispan.persistence;

import org.infinispan.commons.io.ByteBuffer;
import org.infinispan.metadata.Metadata;

public class EntryData {
    ByteBuffer valueAsBinry;
    long created;
    long expiryTime;
    long lastUsed;
    ByteBuffer metadataAsBinary;

    public static EntryData create(ByteBuffer valueAsBinary, long created, long expiryTime, long lastUsed, ByteBuffer metadataAsBinary) {
        EntryData data = new EntryData();
        data.valueAsBinry = valueAsBinary;
        data.created = created;
        data.expiryTime = expiryTime;
        data.lastUsed = lastUsed;
        data.metadataAsBinary = metadataAsBinary;

        return data;
    }

    // getter/setterは省略
}

読み込みの場合は、キーが渡ってくるのでMarshallerおよびMarshallableEntryFactoryを使ってMarshallableEntryにデータを変換してから
返却します。

    @Override
    public MarshallableEntry<K, V> loadEntry(Object key) {
        MarshallableEntryFactory<K, V> marshallableEntryFactory = ctx.getMarshallableEntryFactory();
        Marshaller marshaller = ctx.getPersistenceMarshaller().getUserMarshaller();

        try {
            ByteBuffer keyAsBinary = marshaller.objectToBuffer(key);
            EntryData entryData = store.get(keyAsBinary);

            if (entryData != null) {
                MarshallableEntry<K, V> marshallableEntry =
                        marshallableEntryFactory.create(keyAsBinary, entryData.valueAsBinry, entryData.metadataAsBinary, entryData.getCreated(), entryData.getLastUsed());

                logging(
                        "load entry: %s / %s, created = %d, expiryTime = %d, lastUsed = %d, metadata = %s",
                        marshallableEntry.getKey(),
                        marshallableEntry.getValue(),
                        marshallableEntry.created(),
                        marshallableEntry.expiryTime(),
                        marshallableEntry.lastUsed(),
                        marshallableEntry.getMetadata()
                );
                return marshallableEntry;

ここで返したMarshallableEntryは、DataContainer側で保持されることになります。

削除については、省略。containsは実装はしていますが、呼ばれる気がしませんが…?

確認

では、テストコードで確認してみましょう。といっても、CacheStoreの動作自体はログで見るのですが。

テストコードの雛形。
src/test/java/org/littlewings/infinispan/persistence/SimpleMapStoreTest.java

package org.littlewings.infinispan.persistence;

import java.io.IOException;
import java.io.UncheckedIOException;
import java.util.List;
import java.util.function.Consumer;
import java.util.stream.Collectors;
import java.util.stream.IntStream;

import org.infinispan.Cache;
import org.infinispan.configuration.cache.CacheMode;
import org.infinispan.configuration.cache.Configuration;
import org.infinispan.configuration.cache.ConfigurationBuilder;
import org.infinispan.configuration.cache.PersistenceConfigurationBuilder;
import org.infinispan.manager.DefaultCacheManager;
import org.infinispan.manager.EmbeddedCacheManager;
import org.junit.jupiter.api.Test;

import static org.assertj.core.api.Assertions.assertThat;

public class SimpleMapStoreTest {
    <K, V> void withCache(String cacheName, int numInstances, Consumer<Cache<K, V>> func) {
        List<EmbeddedCacheManager> managers =
                IntStream
                        .rangeClosed(1, numInstances)
                        .mapToObj(i -> {
                                    try {
                                        return new DefaultCacheManager("infinispan.xml");
                                    } catch (IOException e) {
                                        throw new UncheckedIOException(e);
                                    }
                                }
                        )
                        .collect(Collectors.toList());

        try {
            managers.forEach(m -> m.getCache(cacheName));

            Cache<K, V> cache = managers.get(0).getCache(cacheName);
            func.accept(cache);
        } finally {
            managers.forEach(EmbeddedCacheManager::stop);
        }
    }

    // ここに、テストを書く!
}

簡単にEmbeddedなクラスタを作成できるメソッド付きです。

Infinispanの設定。
src/test/resources/infinispan.xml

<?xml version="1.0" encoding="UTF-8"?>
<infinispan
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="urn:infinispan:config:10.1 https://infinispan.org/schemas/infinispan-config-10.1.xsd"
        xmlns="urn:infinispan:config:10.1">
    <cache-container>
        <transport stack="udp"/>

        <distributed-cache name="declarativeSimpleStoreCache">
            <persistence>
                <store class="org.littlewings.infinispan.persistence.SimpleMapCacheStore">
                    <property name="storeName">declarativeStore</property>
                </store>
            </persistence>
        </distributed-cache>
    </cache-container>
</infinispan>

persistence/storeで、自分で作成したCacheStoreの実装クラスを指定。プロパティも設定しておきます。

            <persistence>
                <store class="org.littlewings.infinispan.persistence.SimpleMapCacheStore">
                    <property name="storeName">declarativeStore</property>
                </store>
            </persistence>

まずは、設定ファイルで定義したCacheを使ってみます。

    @Test
    public void withDeclarativeSimpleCacheStore() {
        this.<String, String>withCache("declarativeSimpleStoreCache", 3, cache -> {
            System.out.println("============================== start ==============================");

            System.out.println("[Data Put] start");
            IntStream.rangeClosed(1, 10).forEach(i -> cache.put("key" + i, "value" + i));
            System.out.println("[Data Put] end");

            System.out.println("[Data Get] start");
            assertThat(cache.get("key1")).isEqualTo("value1");
            System.out.println("[Data Get] end");

            assertThat(cache.containsKey("key2")).isTrue();

            System.out.println("[Data Remove] start");
            cache.remove("key3");
            System.out.println("[Data Remove] end");

            System.out.println("[Data Clear] start");
            cache.clear();
            System.out.println("[Data Clear] end");

            assertThat(cache.containsKey("key2")).isTrue();

            System.out.println("[Data Get] start");
            IntStream.rangeClosed(1, 10).forEach(i -> cache.get("key" + i));
            System.out.println("[Data Get] end");

            System.out.println("============================== end ==============================");
        });
    }

ログを追ってみてみます。

まずはデータのput。

============================== start ==============================
[Data Put] start
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] missing entry: key1
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] write entry: key1 / value1, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] write entry: key1 / value1, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] missing entry: key2
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] write entry: key2 / value2, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] write entry: key2 / value2, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-17537 - SimpleMapCacheStore - declarativeStore] missing entry: key3
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] write entry: key3 / value3, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-17537 - SimpleMapCacheStore - declarativeStore] write entry: key3 / value3, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] missing entry: key4
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] write entry: key4 / value4, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-17537 - SimpleMapCacheStore - declarativeStore] write entry: key4 / value4, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-17537 - SimpleMapCacheStore - declarativeStore] missing entry: key5
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] write entry: key5 / value5, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-17537 - SimpleMapCacheStore - declarativeStore] write entry: key5 / value5, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] missing entry: key6
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] write entry: key6 / value6, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-17537 - SimpleMapCacheStore - declarativeStore] write entry: key6 / value6, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] missing entry: key7
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] write entry: key7 / value7, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] write entry: key7 / value7, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] missing entry: key8
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] write entry: key8 / value8, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] write entry: key8 / value8, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] missing entry: key9
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] write entry: key9 / value9, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] write entry: key9 / value9, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] missing entry: key10
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] write entry: key10 / value10, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] write entry: key10 / value10, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
[Data Put] end

CacheStoreに書き込みする前に、1度データをロードしようとしていますね。

3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] missing entry: key1

書き込みは、2回発生。バックアップ分があるからですね。

3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] write entry: key1 / value1, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] write entry: key1 / value1, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}

この状態でデータを取得すると、CacheStore側までアクセスが来ません。

[Data Get] start
[Data Get] end

InfinispanのDataContainer側(InfinispanのCacheの内部)にデータがある場合は、CacheStoreまで来ないからです。

削除。こちらはCacheStoreまでアクセスが来ます。

[Data Remove] start
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] deleted? true, key = key3
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-17537 - SimpleMapCacheStore - declarativeStore] deleted? true, key = key3
[Data Remove] end

ここで、1度データをクリアします。この時、CacheStore側にはなにも起こりません(起こしたい場合はAdvanced〜が必要)。

[Data Clear] start
[Data Clear] end

この状態だと、DataContainerにはデータはないけれど、CacheStoreにはデータが残ったまま(明示的に削除したものを除く)
という状態になっているので、これを利用してデータをCacheStoreからロードしてみます。

1件取得。データをCacheStoreからロードしに来たことがわかります。

3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] load entry: key2 / value2, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}

ループで取得。

[Data Get] start
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] load entry: key1 / value1, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-17537 - SimpleMapCacheStore - declarativeStore] missing entry: key3
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-17537 - SimpleMapCacheStore - declarativeStore] load entry: key4 / value4, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-17537 - SimpleMapCacheStore - declarativeStore] load entry: key5 / value5, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-17537 - SimpleMapCacheStore - declarativeStore] load entry: key6 / value6, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] load entry: key7 / value7, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] load entry: key8 / value8, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] load entry: key9 / value9, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore] load entry: key10 / value10, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}
[Data Get] end
============================== end ==============================

データの取得の場合は、バックアップ分は動かないんですね。1回しかCacheStoreが動いていません。

あとは、ログにプロパティで指定したCacheStoreの名前が反映されていることを確認。

INFO: [xxxxx-59059 - SimpleMapCacheStore - declarativeStore]

続いて、CacheをAPIで定義する時にCacheStoreを設定する方法を見てみましょう。

    @Test
    public void withProgrammaticallyDeclarativeSimpleCacheStore() throws IOException {
        try (EmbeddedCacheManager manager = new DefaultCacheManager("infinispan.xml")) {

            PersistenceConfigurationBuilder persistenceConfigurationBuilder =
                    new ConfigurationBuilder()
                            .clustering()
                            .cacheMode(CacheMode.DIST_SYNC)
                            .persistence();

            Configuration configuration =
                    persistenceConfigurationBuilder
                            .addStore(new SimpleMapCacheStoreConfigurationBuilder(persistenceConfigurationBuilder))
                            .addProperty("storeName", "programmaticallyStore")
                            .build();

            manager.defineConfiguration("programmaticallySimpleStoreCache", configuration);

            Cache<String, String> cache = manager.getCache("programmaticallySimpleStoreCache");

            System.out.println("============================== start ==============================");

            System.out.println("[Data Put] start");
            IntStream.rangeClosed(1, 10).forEach(i -> cache.put("key" + i, "value" + i));
            System.out.println("[Data Put] end");

            System.out.println("[Data Get] start");
            assertThat(cache.get("key1")).isEqualTo("value1");
            System.out.println("[Data Get] end");

            assertThat(cache.containsKey("key2")).isTrue();

            System.out.println("[Data Remove] start");
            cache.remove("key3");
            System.out.println("[Data Remove] end");

            System.out.println("[Data Clear] start");
            cache.clear();
            System.out.println("[Data Clear] end");

            assertThat(cache.containsKey("key2")).isTrue();

            System.out.println("[Data Get] start");
            IntStream.rangeClosed(1, 10).forEach(i -> cache.get("key" + i));
            System.out.println("[Data Get] end");

            System.out.println("============================== end ==============================");
        }
    }

テストコード側は先ほどと同じなので省略しますが、CacheStoreの設定方法はこんな感じですね。

            PersistenceConfigurationBuilder persistenceConfigurationBuilder =
                    new ConfigurationBuilder()
                            .clustering()
                            .cacheMode(CacheMode.DIST_SYNC)
                            .persistence();

            Configuration configuration =
                    persistenceConfigurationBuilder
                            .addStore(new SimpleMapCacheStoreConfigurationBuilder(persistenceConfigurationBuilder))
                            .addProperty("storeName", "programmaticallyStore")
                            .build();

            manager.defineConfiguration("programmaticallySimpleStoreCache", configuration);

ログにプロパティの内容が反映されていることを確認。

INFO: [xxxxx-30918 - SimpleMapCacheStore - declarativeStore]

OKそうですね。これで、確認できました、と。

あとは、少しオマケを書いていきます。

CustomStoreConfiguration

先に少し書きましたが、今回はCacheStoreの設定としてプロパティくらいしか使わないので、実はStoreConfigurationは作る必要は
なかったりします。

今回のCacheStoreの実装では、@ConfiguredByアノテーションを外し、StoreConfigurationの型をCustomStoreConfigurationに変更するだけで
設定ファイルのCacheを使っている方のテストコードは動作します。

@Store
// @ConfiguredBy(SimpleMapCacheStoreConfiguration.class)  // ココ
public class SimpleMapCacheStore<K, V> implements ExternalStore<K, V> {
    Logger logger = Logger.getLogger(getClass());

    //SimpleMapCacheStoreConfiguration storeConfiguration;
    CustomStoreConfiguration storeConfiguration;  // ココ
    String storeName;

    Map<ByteBuffer, EntryData> store;

    InitializationContext ctx;

    @Override
    public void init(InitializationContext ctx) {
        this.ctx = ctx;
        storeConfiguration = ctx.getConfiguration();

こっちは、StoreConfigurationBuilderを直接使っているのでダメですが。

            Configuration configuration =
                    persistenceConfigurationBuilder
                            .addStore(new SimpleMapCacheStoreConfigurationBuilder(persistenceConfigurationBuilder))
                            .addProperty("storeName", "programmaticallyStore")
                            .build();

このような動作になるのは、@ConfiguredByアノテーションが指定されていない場合は、CustomStoreConfigurationBuilderおよび

CustomStoreConfigurationが使われるからです。

https://docs.jboss.org/infinispan/10.1/apidocs/org/infinispan/configuration/cache/CustomStoreConfiguration.html

https://docs.jboss.org/infinispan/10.1/apidocs/org/infinispan/configuration/cache/CustomStoreConfigurationBuilder.html

https://github.com/infinispan/infinispan/blob/10.1.5.Final/core/src/main/java/org/infinispan/configuration/parsing/Parser.java#L2546-L2563

Interceptor

CacheStoreが呼ばれるようになるのは、Persistenceの設定を行うとInterceptorが追加されるからです。

https://github.com/infinispan/infinispan/blob/10.1.5.Final/core/src/main/java/org/infinispan/factories/InterceptorChainFactory.java#L247-L280

https://github.com/infinispan/infinispan/blob/10.1.5.Final/core/src/main/java/org/infinispan/interceptors/impl/CacheLoaderInterceptor.java

https://github.com/infinispan/infinispan/blob/10.1.5.Final/core/src/main/java/org/infinispan/interceptors/impl/CacheWriterInterceptor.java

DataContainer側になかった場合に、CacheStoreからロードしようとするのは、このあたりの挙動ですね。

https://github.com/infinispan/infinispan/blob/10.1.5.Final/core/src/main/java/org/infinispan/interceptors/impl/CacheLoaderInterceptor.java#L204-L206

https://github.com/infinispan/infinispan/blob/10.1.5.Final/core/src/main/java/org/infinispan/interceptors/impl/CacheLoaderInterceptor.java#L390-L450

繰り返しますが、ここでロードしたデータはDataContainer側に保持されるようになっています。

エントリの作成時間やlifespanなどについて

ところで、ログ出力時にcreatedやexpiryTime、lastUsedなどが軒並み-1になっていたと思います。

3月 25, 2020 11:57:46 午後 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-61331 - SimpleMapCacheStore - declarativeStore] write entry: key1 / value1, created = -1, expiryTime = -1, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=-1, maxIdle=-1}

Metadata(lifespan、maxIdle)の部分に関しては、ここで設定されています。いずれも、なにも指定しない場合の初期値は-1です。

https://github.com/infinispan/infinispan/blob/10.1.5.Final/core/src/main/java/org/infinispan/cache/impl/CacheImpl.java#L195-L196

created、expiryTime、lastUsedなどは、expirationの設定を行うと設定されるようになります。

https://github.com/infinispan/infinispan/blob/10.1.5.Final/core/src/main/java/org/infinispan/container/impl/AbstractInternalDataContainer.java#L150

https://github.com/infinispan/infinispan/blob/10.1.5.Final/core/src/main/java/org/infinispan/container/impl/InternalEntryFactoryImpl.java#L46-L61

たとえば、Cacheの設定にlifespanを入れると

        <distributed-cache name="declarativeSimpleStoreCache">
            <expiration lifespan="10000"/>
            <persistence>
                <store class="org.littlewings.infinispan.persistence.SimpleMapCacheStore">
                    <property name="storeName">declarativeStore</property>
                </store>
            </persistence>
        </distributed-cache>

値が入るようになります。

3月 26, 2020 12:26:14 午前 org.littlewings.infinispan.persistence.SimpleMapCacheStore logging
INFO: [xxxxx-22348 - SimpleMapCacheStore - declarativeStore] write entry: key1 / value1, created = 1585149974089, expiryTime = 1585149984089, lastUsed = -1, metadata = EmbeddedExpirableMetadata{version=null, lifespan=10000, maxIdle=-1}

まとめ

InfinispanのCacheStoreまわりのAPIが知らないうちにいろいろ変わっていたので、まずはCacheLoader/CacheWriterと設定の基本まわりを
おさらいしてみました。

前に見た時よりも、だいぶ中も追えたのではないでしょうか?

今回作成したコードは、こちらに置いています。

https://github.com/kazuhira-r/infinispan-getting-started/tree/master/embedded-cachestore

Jib Coreで、Dockerコンテナイメージを作ってみる

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

そういえば、Jibというものがあったけど、全然使ったことがないなと思い、1度試しておこうかなと。

Javaアプリケーションを自動的にコンテナイメージにビルドするツール「Jib」がバージョン1.0に到達 - Publickey

Jib、Google提供のJavaコンテナイメージビルダ

名前や情報自体は、時々見ていたんですけどね。

Jib?

Jibとは、Googleの開発したコンテナイメージをビルドするツールです。Javaアプリケーション向けに、最適化されているようです。

Build container images for your Java applications.

GitHub - GoogleContainerTools/jib: 🏗 Build container images for your Java applications.

Jibとはなにか、その説明を見てみます。

What is Jib?

以下を特徴にしています。

  • Dockerデーモンなしでコンテナイメージを作成できる
  • Dockerイメージを作成するための、ベストプラクティスを熟知する必要がない
  • Javaアプリケーション向けに最適化されたコンテナイメージを作成できる
  • コンテナイメージのフォーマットは、DockerまたはOCIが選択可能
  • Maven、GradleのプラグインCLIJavaライブラリの4形態で利用可能

Google Cloud Platform Blog: Introducing Jib — build Java Docker images better

Build containers faster with Jib, a Google image build tool for Java applications - Speaker Deck

また、高速であること、再現性、デーモンレスを目標として掲げています。

Goals

Dockerレジストリへのpushなどもできるようです。

FAQは、こちら。

jib/faq.md at master · GoogleContainerTools/jib · GitHub

Jib Core

今回は、Javaライブラリとして提供される、Jib Coreを使用してみます。MavenプラグインやGradleプラグインを使うと便利だとは
思うのですが、まずはこちらを1度使ってみた方が理解が進むかな、と。

https://github.com/GoogleContainerTools/jib/tree/v0.13.1-core/jib-core

利用するバージョンは、0.13.1です。

APIリファレンスは、こちら。

com.google.cloud.tools.jib.api (jib-core 0.13.1 API)

Jib Core自体は、汎用的なコンテナイメージのビルドツールのようです。Javaアプリケーション向けのAPIも備えてはいますが、
README.mdに沿ってAPIを使っていくと、割と汎用的なイメージを作っているような感覚を覚えるでしょう。

実際、そうみたいですし。

ちなみに、APIはまだベータ段階なので、今後大幅に変更される可能性があることに注意してください。

The API is currently in beta and may change substantially.

もちろん、MavenプラグインやGradleプラグインのベースにもなっています。

まあ、説明はこのあたりにして使っていってみましょう。

環境

今回の環境は、こちらです。

$ java --version
openjdk 11.0.6 2020-01-14
OpenJDK Runtime Environment (build 11.0.6+10-post-Ubuntu-1ubuntu118.04.1)
OpenJDK 64-Bit Server VM (build 11.0.6+10-post-Ubuntu-1ubuntu118.04.1, mixed mode, sharing)


$ mvn --version
Apache Maven 3.6.3 (cecedd343002696d0abb50b32b541b8a6ba2883f)
Maven home: $HOME/.sdkman/candidates/maven/current
Java version: 11.0.6, vendor: Ubuntu, runtime: /usr/lib/jvm/java-11-openjdk-amd64
Default locale: ja_JP, platform encoding: UTF-8
OS name: "linux", version: "4.18.0-25-generic", arch: "amd64", family: "unix"

サンプルアプリケーション

まずは、JibでDockerイメージを作るためのお題となるアプリケーションを作りましょう。

簡単なJAX-RSアプリケーションを作ります。

pom.xmlの抜粋。

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.jboss.resteasy</groupId>
            <artifactId>resteasy-jdk-http</artifactId>
            <version>4.5.2.Final</version>
        </dependency>
    </dependencies>

ソースコード
src/main/java/org/littlewings/jaxrs/Server.java

package org.littlewings.jaxrs;

import java.io.IOException;
import java.net.InetSocketAddress;
import java.util.Optional;
import java.util.concurrent.TimeUnit;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.QueryParam;
import javax.ws.rs.core.MediaType;

import com.sun.net.httpserver.HttpServer;
import org.jboss.logging.Logger;
import org.jboss.resteasy.plugins.server.sun.http.HttpContextBuilder;

public class Server {
    public static void main(String... args) throws IOException {
        Logger logger = Logger.getLogger(Server.class);

        HttpServer server = HttpServer.create(new InetSocketAddress(8080), 10);

        try {
            HttpContextBuilder builder = new HttpContextBuilder();
            builder.getDeployment().getActualResourceClasses().add(HelloResource.class);

            builder.bind(server);

            server.start();

            logger.info("server start.");

            while (true) {
                try {
                    TimeUnit.SECONDS.sleep(1L);
                } catch (InterruptedException e) {
                    // ignore
                }
            }
        } finally {
            server.stop(0);
            logger.info("server stop.");
        }
    }

    @Path("hello")
    public static class HelloResource {
        @GET
        @Produces(MediaType.TEXT_PLAIN)
        public String message(@QueryParam("value") String value) {
            return "Hello " + Optional.ofNullable(value).orElse("World") + "!!";
        }
    }
}

QueryStringで受け取ったパラメーターを使って、メッセージを返すだけのJAX-RSリソースクラスが動作します。

簡単に動作確認してみましょう。ビルドしてサーバーを起動。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.jaxrs.Server

アクセス。

$ curl localhost:8080/hello
Hello World!!


$ curl localhost:8080/hello?value=JAX-RS
Hello JAX-RS!!

OKですね。では、こちらをJibを使ったDockerイメージにすることを目標に、進めていきましょう。

Jib Coreを使う

それでは、Jib Coreを使ってDockerイメージを作成していきます。

先ほど作ったJAX-RSアプリケーションとは、別のMavenプロジェクトを用意します。依存関係は、こちら。

        <dependency>
            <groupId>com.google.cloud.tools</groupId>
            <artifactId>jib-core</artifactId>
            <version>0.13.1</version>
        </dependency>

まずは、雛形を用意しましょう。
src/main/java/org/littlewings/jib/Builder.java

package org.littlewings.jib;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.Instant;
import java.util.Arrays;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;

import com.google.cloud.tools.jib.api.AbsoluteUnixPath;
import com.google.cloud.tools.jib.api.CacheDirectoryCreationException;
import com.google.cloud.tools.jib.api.Containerizer;
import com.google.cloud.tools.jib.api.DockerDaemonImage;
import com.google.cloud.tools.jib.api.InvalidImageReferenceException;
import com.google.cloud.tools.jib.api.Jib;
import com.google.cloud.tools.jib.api.Port;
import com.google.cloud.tools.jib.api.RegistryException;

public class Builder {
    public static void main(String... args) throws InvalidImageReferenceException, InterruptedException, ExecutionException, RegistryException, CacheDirectoryCreationException, IOException {

        // ここに、Jibを使ったコードを書く

    }
}

こちらのmainメソッドの中身を、README.mdのExamplesを参考に埋めていきます。ちなみに、Dockerレジストリへのpushは行いません。

Examples

ベースイメージを決めて、Dockerイメージを作る

最初に行うのは、ベースイメージの決定と、作成するコンテナイメージの名前付けですね。Jibクラスをエントリポイントとして行います。

        Jib
                .from("adoptopenjdk:11-jre-hotspot")
                .containerize(
                        Containerizer
                                .to(
                                        DockerDaemonImage.named("kazuhira/simple-jaxrs-server")
                                )
                );

これで、AdoptOpenJDK 11 HotSpot(JREのみ)をベースイメージとして、Dockerイメージ名「kazuhira/simple-jaxrs-server」で
コンテナイメージが作成されます。なお、バージョンタグは「latest」となります。

実行してみましょう(

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.jib.Builder

できました。作成時間は、50年前になっていますけど。

$ docker image ls | grep simple-jaxrs-server
kazuhira/simple-jaxrs-server       latest              f50fab05cd8b        50 years ago        225MB

ベースがAdoptOpenJDKのコンテナイメージなので、実行するとシェルが起動します。

$ docker container run -it --rm kazuhira/simple-jaxrs-server:latest 
root@1439d07d732b:/# 

中身はさておき、Jib Coreを使ってDockerコンテナイメージが作れたことになります。

ここから、ちょっとずつ中身を変えていってみましょう。

今回作成したDockerイメージは、削除しておきます。

$ docker image rm kazuhira/simple-jaxrs-server:latest 
タグを指定する

タグが「latest」になるのはイマイチですね。「0.0.1」にしてみます。

        Jib
                .from("adoptopenjdk:11-jre-hotspot")
                .containerize(
                        Containerizer
                                .to(
                                        DockerDaemonImage.named("kazuhira/simple-jaxrs-server:0.0.1")
                                )
                );

コンテナを再作成すると、タグが変わったことが確認できますね。

$ docker image ls | grep simple-jaxrs-server
kazuhira/simple-jaxrs-server       0.0.1               f50fab05cd8b        50 years ago        225MB

ここで指定する文字列のフォーマットは、内部的に呼び出されるImageReference#parseの説明を見ることで確認できます。

https://www.javadoc.io/static/com.google.cloud.tools/jib-core/0.13.1/com/google/cloud/tools/jib/api/ImageReference.html#parse-java.lang.String-

ちなみに、DockerDaemonImage#namedなどに、直接ImageReference#parseの結果を渡すことも可能です。

作成したイメージは、どの環境向けか?

ところで、先ほどからDockerデーモンレスと言いながらdockerコマンドで作成したイメージの情報を見ていますが、
これはContainerizer#toで指定しているものがポイントになっています。

                        Containerizer
                                .to(
                                        DockerDaemonImage.named("kazuhira/simple-jaxrs-server:0.0.1")
                                )

Containerizer (jib-core 0.13.1 API)

今回はDockerDaemonImageを選択しましたが、他にRegistryImage、TarImageがあり、どのような環境向けにイメージを作成するかを
指定することができます。

DockerDaemonImage (jib-core 0.13.1 API)

RegistryImage (jib-core 0.13.1 API)

TarImage (jib-core 0.13.1 API)

たとえば、レジストリにpushしたかったら、RegistryImageを使います。Jib CoreのExamplesでは、RegistryImageを使うように
なっていますね。

ENTRYPOINTを指定する

次に、ENTRYPOINTを指定してみましょう。

たとえば、「java --version」を実行するようにEntrypointを設定してみましょう。

        Jib
                .from("adoptopenjdk:11-jre-hotspot")
                .setEntrypoint(Arrays.asList("java", "--version"))
                .containerize(
                        Containerizer
                                .to(
                                        DockerDaemonImage.named("kazuhira/simple-jaxrs-server:0.0.1")
                                )
                );

確認。

$ docker container run -it --rm kazuhira/simple-jaxrs-server:0.0.1
openjdk 11.0.6 2020-01-14
OpenJDK Runtime Environment AdoptOpenJDK (build 11.0.6+10)
OpenJDK 64-Bit Server VM AdoptOpenJDK (build 11.0.6+10, mixed mode)

反映されましたね。

ちなみに、ProgramArgumentsを使うことで、ENTRYPOINTに対する引数を設定することも可能です。

        Jib
                .from("adoptopenjdk:11-jre-hotspot")
                .setEntrypoint(Arrays.asList("java"))
                .setProgramArguments(Arrays.asList("--version"))
                .containerize(
                        Containerizer
                                .to(
                                        DockerDaemonImage.named("kazuhira/simple-jaxrs-server:0.0.1")
                                )
                );
イメージの作成時刻を変更する

最初にイメージを作った時に確認しましたが、作成時刻が50年前になっています。

これを変更する場合は、CreationTimeを指定します。実行時の時刻を使うようにしてみましょう。

        Jib
                .from("adoptopenjdk:11-jre-hotspot")
                .setCreationTime(Instant.now())
                .setEntrypoint(Arrays.asList("java", "--version"))
                .containerize(
                        Containerizer
                                .to(
                                        DockerDaemonImage.named("kazuhira/simple-jaxrs-server:0.0.1")
                                )
                );

ビルド後にイメージを確認すると、イメージの作成時刻が変更されました。

Jibで作成したイメージの時刻がやたら古い理由は、FAQに記載があります。

Why is my image created 48+ years ago?

同じ時刻で作成することで、再現性を担保することを目的にしています。作成時刻を変更するということは、再現性が犠牲になる
ということが注意事項になります、だそうで。

$ docker image ls | grep simple-jaxrs-server
kazuhira/simple-jaxrs-server       0.0.1               ff8b454d2541        12 seconds ago      225MB
レイヤーを追加(ADD)して、ポートをEXPOSEする

そろそろ、最初に作ったアプリケーションを動かしたいところですね。アプリケーションのJARや、依存するライブラリなどを
イメージに追加していってみましょう。

とりあえず、Jib Coreを動かしているプロジェクトに、JARファイルを置く用のディレクトリを作成します。

$ mkdir -p container-target/libs

一方でアプリケーション側のプロジェクトでJARファイルを作成、依存ライブラリを取得して、Jib Coreを先ほど作成したディレクトリに
コピーします。

$ mvn package && mvn dependency:copy-dependencies -DincludeScope=compile
$ cp target/simple-jaxrs-server-0.0.1-SNAPSHOT.jar ../jib-core-example/container-target
$ cp target/dependency/* ../jib-core-example/container-target/libs

アプリケーション本体は作成したディレクトリの直下に、ライブラリは「libs」ディレクトリに置きました。

これらのJARファイルをイメージに追加するために、addLayerでJARファイルを追加していきます。

        Jib
                .from("adoptopenjdk:11-jre-hotspot")
                .addLayer(Arrays.asList(Paths.get("container-target/simple-jaxrs-server-0.0.1-SNAPSHOT.jar")), AbsoluteUnixPath.fromPath(Paths.get("/")))
                .addLayer(
                        Files.find(Paths.get("container-target/libs"),
                                1,
                                (path, attr) -> Files.isRegularFile(path) && path.getFileName().toString().endsWith(".jar")).collect(Collectors.toList()),
                        "/libs"
                )
                .setCreationTime(Instant.now())
                .setEntrypoint(Arrays.asList("java", "--version"))
                .containerize(
                        Containerizer
                                .to(
                                        DockerDaemonImage.named("kazuhira/simple-jaxrs-server:0.0.1")
                                )
                );

ここですね。

                .addLayer(Arrays.asList(Paths.get("container-target/simple-jaxrs-server-0.0.1-SNAPSHOT.jar")), AbsoluteUnixPath.fromPath(Paths.get("/")))
                .addLayer(
                        Files.find(Paths.get("container-target/libs"),
                                1,
                                (path, attr) -> Files.isRegularFile(path) && path.getFileName().toString().endsWith(".jar")).collect(Collectors.toList()),
                        "/libs"
                )

addLayerの第2引数には、ファイルをADDする先のディレクトリを指定します。

ENTRYPOINTも変えてしまいましょう。

        Jib
                .from("adoptopenjdk:11-jre-hotspot")
                .addLayer(Arrays.asList(Paths.get("container-target/simple-jaxrs-server-0.0.1-SNAPSHOT.jar")), AbsoluteUnixPath.fromPath(Paths.get("/")))
                .addLayer(
                        Files.find(Paths.get("container-target/libs"),
                                1,
                                (path, attr) -> Files.isRegularFile(path) && path.getFileName().toString().endsWith(".jar")).collect(Collectors.toList()),
                        "/libs"
                )
                .setCreationTime(Instant.now())
                .setEntrypoint("java", "-cp", "/libs/*:/simple-jaxrs-server-0.0.1-SNAPSHOT.jar", "org.littlewings.jaxrs.Server")
                .containerize(
                        Containerizer
                                .to(
                                        DockerDaemonImage.named("kazuhira/simple-jaxrs-server:0.0.1")
                                )
                );

8080ポートでリッスンしますし、ポートもEXPOSEしておきましょう。

        Jib
                .from("adoptopenjdk:11-jre-hotspot")
                .addLayer(Arrays.asList(Paths.get("container-target/simple-jaxrs-server-0.0.1-SNAPSHOT.jar")), AbsoluteUnixPath.fromPath(Paths.get("/")))
                .addLayer(
                        Files.find(Paths.get("container-target/libs"),
                                1,
                                (path, attr) -> Files.isRegularFile(path) && path.getFileName().toString().endsWith(".jar")).collect(Collectors.toList()),
                        "/libs"
                )
                .setExposedPorts(Port.tcp(8080))
                .setCreationTime(Instant.now())
                .setEntrypoint("java", "-cp", "/libs/*:/simple-jaxrs-server-0.0.1-SNAPSHOT.jar", "org.littlewings.jaxrs.Server")
                .containerize(
                        Containerizer
                                .to(
                                        DockerDaemonImage.named("kazuhira/simple-jaxrs-server:0.0.1")
                                )
                );

ここですね。

                .setExposedPorts(Port.tcp(8080))

では、イメージを作成して、起動。

$ docker container run -it --rm -p 8080:8080 kazuhira/simple-jaxrs-server:0.0.1
Mar 21, 2020 5:44:09 PM org.littlewings.jaxrs.Server main
INFO: server start.

サーバーが起動しました。

確認。

$ curl localhost:8080/hello
Hello World!!


$ curl localhost:8080/hello?value=JAX-RS
Hello JAX-RS!!

OKですね!

JibContainerBuilder

ここまで説明しませんでしたが、Jib#fromの戻り値は、JibContainerBuilderのインスタンスです。

JibContainerBuilder (jib-core 0.13.1 API)

JibContainerBuilderを使って、作成するコンテナの設定を行っていきます。JibContainerBuilderに定義されているメソッドを見ると、
どのような設定が可能なのかを確認することができます。

たとえば、USERやWORKDIRなどの指定はできそうですが、DockerfileにおけるRUNに相当することはできなさそうですね、など。

JibContainerBuilder#containerizeだけは戻り値がJibContainerBuilderにはならず、JibContainerとなりコンテナイメージの作成が
実行されます。

https://www.javadoc.io/static/com.google.cloud.tools/jib-core/0.13.1/com/google/cloud/tools/jib/api/JibContainerBuilder.html#containerize-com.google.cloud.tools.jib.api.Containerizer-

実際に利用する時は、JibContainerBuilderのAPIを眺めることになるでしょう。

ベースイメージのダウンロード先は?

ところで、今回はAdoptOpenJDKをベースイメージに選びましたが、「docker image ls」コマンドにAdoptOpenJDKは現れません。

$ docker image ls | grep adopt

どこにあるかというと、「$HOME/.cache/google-cloud-tools-java/jib」配下にあります。

$ find ~/.cache/google-cloud-tools-java/jib -type f
$HOME/.cache/google-cloud-tools-java/jib/images/adoptopenjdk!11-jre-hotspot/config.json
$HOME/.cache/google-cloud-tools-java/jib/images/adoptopenjdk!11-jre-hotspot/lock
$HOME/.cache/google-cloud-tools-java/jib/images/adoptopenjdk!11-jre-hotspot/manifest.json
$HOME/.cache/google-cloud-tools-java/jib/layers/f11b29a9c7306674a9479158c1b4259938af11b97359d9ac02030cc1095e9ed1/977183d4e9995d9cd5ffdfc0f29e911ec9de777bcb0f507895daa1068477f76f
$HOME/.cache/google-cloud-tools-java/jib/layers/bf7abfeb06800729d4f24d971e8017c537c4fa86368ae0bca66a7b36ca2b4189/85fa6e80789c791dff92d9427ce7cc6ffdb5164aa7296c986cb0c1adcd6711f7
$HOME/.cache/google-cloud-tools-java/jib/layers/78bf9a5ad49e4ae42a83f4995ade4efc096f78fd38299cf05bc041e8cdda2a36/16542a8fc3be1bfaff6ed1daa7922e7c3b47b6c3a8d98b7fca58b9517bb99b75
$HOME/.cache/google-cloud-tools-java/jib/layers/930bda195c84cf132344bf38edcad255317382f910503fef234a9ce3bff0f4dd/6597da2e2e52f4d438ad49a14ca79324f130a9ea08745505aa174a8db51cb79d
$HOME/.cache/google-cloud-tools-java/jib/layers/8a1e5c340bc4a84bc37bcb75bbaa4b90b9255c3cf2dfbf2891b350581bc687bd/3cd87aa74fc9d063ff863f789614cdfef8fbbf9b2908c1dc5f2e868b087f0a59
$HOME/.cache/google-cloud-tools-java/jib/layers/5bed26d33875e6da1d9ff9a1054c5fef3bbeb22ee979e14b72acf72528de007b/c8be1b8f4d60d99c281fc2db75e0f56df42a83ad2f0b091621ce19357e19d853

このあたりの情報が決められているのは、こちら。

https://github.com/GoogleContainerTools/jib/blob/v0.13.1-core/jib-core/src/main/java/com/google/cloud/tools/jib/filesystem/XdgDirectories.java

https://github.com/GoogleContainerTools/jib/blob/v0.13.1-core/jib-core/src/main/java/com/google/cloud/tools/jib/cache/CacheStorageFiles.java

もっとJava向けなAPIを使う

ここまで、JibContainerBuilderを使った汎用的なコンテナイメージの作成を行ってきました。

最後に、もう少しJavaアプリケーション向けのコンテナを作るためのAPI、JavaContainerBuilderを使ってみたいと思います。

JavaContainerBuilder (jib-core 0.13.1 API)

JavaContainerBuilderを使うと「/app/libs」に依存ライブラリが、「/app/classes」にクラスファイルが、
「/app/resources」にリソースファイルが配置されるように構成され、

Where is the application in the container filesystem?

https://github.com/GoogleContainerTools/jib/blob/v0.13.1-core/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java#L198-L202

ENTRYPOINTが「java -cp [クラスパス]」となります。

jib/JavaContainerBuilder.java at v0.13.1-core · GoogleContainerTools/jib · GitHub

この時、libsなどにはクラスパスを通した状態に設定してくれます。

サンプルコードの雛形は、こちら。先ほど作成したコンテナと、近い状態のものを実現していくことを目標にします。
src/main/java/org/littlewings/jib/JavaBuilder.java

package org.littlewings.jib;

import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.time.Instant;
import java.util.concurrent.ExecutionException;
import java.util.stream.Collectors;

import com.google.cloud.tools.jib.api.CacheDirectoryCreationException;
import com.google.cloud.tools.jib.api.Containerizer;
import com.google.cloud.tools.jib.api.DockerDaemonImage;
import com.google.cloud.tools.jib.api.InvalidImageReferenceException;
import com.google.cloud.tools.jib.api.JavaContainerBuilder;
import com.google.cloud.tools.jib.api.Port;
import com.google.cloud.tools.jib.api.RegistryException;

public class JavaBuilder {
    public static void main(String... args) throws IOException, InvalidImageReferenceException, InterruptedException, ExecutionException, RegistryException, CacheDirectoryCreationException {

        // ここに、JavaContainerBuilderを使ったコードを書く

    }
}

JavaContainerBuilderも、Jibと同じようにJavaContainerBuilder#fromから始めるのですが、オーソドックスには
JavaContainerBuilder#fromDistrolessを選ぶのだと思います。

        JavaContainerBuilder
                .fromDistroless()

これが指すベースイメージは、「gcr.io/distroless/java」なのでJava 8となります。

https://github.com/GoogleContainerTools/jib/blob/v0.13.1-core/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java#L94

今回はJava 11で作っているので、先ほどと同じくAdoptOpenJDK 11 HotSpotをベースイメージにします。

で、できたのがこんな感じ。

        JavaContainerBuilder
                .from("adoptopenjdk:11-jre-hotspot")
                .addProjectDependencies(Paths.get("container-target/simple-jaxrs-server-0.0.1-SNAPSHOT.jar"))
                .addDependencies(Files.find(Paths.get("container-target/libs"),
                        1,
                        (path, attr) -> Files.isRegularFile(path) && path.getFileName().toString().endsWith(".jar")).collect(Collectors.toList()))
                .setMainClass("org.littlewings.jaxrs.Server")
                .toContainerBuilder()  // to JibContainerBuilder
                .setExposedPorts(Port.tcp(8080))
                .setCreationTime(Instant.now())
                .containerize(
                        Containerizer
                                .to(
                                        DockerDaemonImage.named("kazuhira/simple-jaxrs-server:0.0.1")
                                )
                );

ある程度Java向けの設定を行ったら、JavaContainerBuilder#toContainerBuilderでJibContainerBuilderに切り替えます。

これはJavaContainerBuilderが内部的に持っているJibContainerBuilderで

https://github.com/GoogleContainerTools/jib/blob/v0.13.1-core/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java#L186

JavaContainerBuilder#toContainerBuilderを呼び出した時に、ここまでに設定したJavaContainerBuilderの内容を元にJibContainerBuilderに
イメージの定義を反映したものを返してくれます。

https://github.com/GoogleContainerTools/jib/blob/v0.13.1-core/jib-core/src/main/java/com/google/cloud/tools/jib/api/JavaContainerBuilder.java#L536-L664

今回は、プロジェクトの成果物(JARファイル)と依存ライブラリ、そしてmainクラスを指定しています。

                .addProjectDependencies(Paths.get("container-target/simple-jaxrs-server-0.0.1-SNAPSHOT.jar"))
                .addDependencies(Files.find(Paths.get("container-target/libs"),
                        1,
                        (path, attr) -> Files.isRegularFile(path) && path.getFileName().toString().endsWith(".jar")).collect(Collectors.toList()))
                .setMainClass("org.littlewings.jaxrs.Server")

その他、「/app/classes」や「/app/resources」にもファイルを追加できますし、これ以外の場所にもファイルを追加してクラスパスを
通すことができます。また、JavaVMへ渡すオプションも設定可能です。

では、コンテナイメージをビルドしてみます。

$ mvn compile exec:java -Dexec.mainClass=org.littlewings.jib.JavaBuilder

コンテナを起動。

$ docker container run -it --rm -p 8080:8080 kazuhira/simple-jaxrs-server:0.0.1
Mar 22, 2020 5:43:38 AM org.littlewings.jaxrs.Server main
INFO: server start.

確認。

$ curl localhost:8080/hello
Hello World!!


$ curl localhost:8080/hello?value=JAX-RS
Hello JAX-RS!!

OKですね。

まとめ

今回は、JibのMavenプラグイン、Gradleプラグインのベースとなっている、Jib Coreを使ってコンテナイメージを作成してみました。

最初からプラグインを使ってもいいのですが、なんとなくもうちょっと基本的なAPIとかを把握しておきたいなと思いまして。

だいたい、雰囲気はわかったので良しとしましょう。今度は、Mavenプラグインを使ってみようと思います。