Hatena::ブログ(Diary)

CLOVER

2016-12-04

JJUG CCC 2016 Fallに参加してきました #jjug_ccc

12/3にベルサール新宿で行われた、JJUG CCC 2016 Fallに参加してきました。

JJUG CCC 2016 Fall

CCCは今回で4度目の参加です。朝の最初のセッション(10時から)から行くつもり満々だったのですが、会場の最寄りの西新宿駅に着いたのが10時でした…。

参加したセッション

今回参加したセッションは、こちらです。

Be a great engineer!〜 フォローすべきトレンド、スルーすべきトレンドをどう見抜くのか(資料

谷本 心さん(@cero_t)さんの発表。というか、基調講演。15分ほど聞きそびれましたが…。
天の理の話や、本質/ニーズの把握といった内容はなかなか響きました。やっぱりちゃんと自分でも考えていかないとなぁと思いますね。

とはいえ、自分の趣味ドリブンなところはそんなに変わらないかもしれませんが(笑)

SIerもはじめる、わたしたちのDevOps(資料1資料2

しょぼちむさん(@syobochim)と阿佐志保さん(@_Dr_ASA)のお二人によるセッション。
しょぼちむさんの開始時のインパクトがものすごくて、目が点になりました…。そして、そのままかけているサングラスがとても気になる…。

ではなくて、DevOpsに対する取り組みや「こういうことを頑張りましょう」みたいな内容だったのですが…。

正直なところ、今回のCCCで1番聞いていて辛い発表でした。資料や発表の良い悪いではなく、立ち位置の関係ですけれど。これ、タイトルに「SIer」がなかったら、もうちょっと違う目線で聞けたのかなぁ?と思います。

発表や資料に出てくる言葉を見たり聞いたりしていると、ギャップがものすごくて完全に消化不良になってました…。

※2次会では「西新宿の闇を背負っている」と言われましたが、どういうことでしょうか?

Event Driven Microservices with Spring Cloud Stream(資料

槙 俊明さん(@making)によるセッション。
個人的には、特に聞きたかったセッションです。Spring Cloud Streamの基礎的な説明、Advanced Topicともにとてもわかりやすかったです。これ、どこかで試していきたいなぁ…。
初心者お断りなセッションということでしたが、実際には手加減されていた様子。先は遠いんでしょうねぇ…。

Spring CloudでDDD的なマイクロサービスを作ってみる(ソースコード

椎葉 光行さん(bufferings)によるセッション。
自分はDDDについて勉強しても本を読んでもなかなか頭に入ってこなくてだいぶ投げ出し気味になっているのですが、今回のセッションは実体験の話が入っていたためスッと入ってくるところがありました。
自分が理解できない分野について、こういう経験談が聞けるのはとてもありがたいなぁと思いました、本当に。

個人的には「ばふぁ寿司」アプリケーションは、いろいろと笑いを誘いました。楽しいセッションでした!

JVMのトラブル解決のためにやったこと〜メモリー/スレッド

岩佐 淳史さん(@bloody_snow)によるセッション。
初心者向けのセッションだそうで、最初にGCやJavaに関するメモリまわりの説明をされていたのですが、話している内容がほとんど資料に書かれておらず、ちょっと厳しかったかなぁという印象。
実例のところは、ちょっと?でした。

自分もこのあたりは詳しくはないのですが…。

Selenideを試行錯誤しながら実践するブラウザ自動テスト(資料

うらがみさん(@backpaper0)によるセッション。
Selenium WebDriverの上に構築されたフレームワーク、Selenideの紹介。基本的な使い方から内部の仕組み、デモまであり、とてもキレイな発表でした。
導入するにあたっての苦労(スキル?環境的な?)とかはなかったのかなぁと、そういうポイントが聞いていてちょっと気になりました。

あと、どうでもいい話。

  • (個人的に)エストニアにめちゃくちゃ反応した
  • 発表を聞くまでずっと読みが「セレナイデ」だと思っていたのは秘密
  • IEはどこでも鬼門
  • "Vimを使ってくれてありがとう"が気になった

Payara Micro の設計と実装(資料

蓮沼 賢志さん(@khasunuma)によるセッション。
PayaraがクラスタリングにHazelcastを使っている関係から、CCCの前にHazelcastの内部についてのブログを書いたり、このブログを引用いただいていることがわかっていたこともあり、参加しないと!と思っていたセッションです。
純粋に、Payara Microに興味があるという話もあります。

シンプルな資料でしたが、とてもキレイにまとめられていてわかりやすかったです。

自分もけっこうPayara Microの起動のコードとかは見ていたので、答え合わせ的な感じでも見れました。Mavenリポジトリを指定して起動できるとかは知らなくて、ビックリしましたけれど。

当ブログの引用ありがとうございます。まさか2回も出てくるとは思わなくて、とても驚きました。

お昼ご飯と懇親会と2次会

午前のセッションが終わったところで、いきなり発見されて拉致されてお昼に連れていかれました…。

エース様、うめざわさん、ちっひーさん、持田さん、うらがみさん、いろふさん、とーますさん、toru_boyさん、pppurpleさん、自分の10名(入店時は9名)。けっこうな人数でしたが、あっさりお店に入れました。

こういうメンバーでお昼を食べることになるとはとても思っていませんでしたが、楽しかったです。ありがとうございました。
※だいぶいじられた気がしますが…

また、各セッション終了後の懇親会では、今回初めてご挨拶する方々が多くて嬉しかったです。

特に、ひしだまさん(@hishidama)、AOE Takashiさん(@aoetk)、emagさん(@emaggame)、opengl-8080さん(@opengl_8080)とお話できるとは思っておらず、とてもとてもビックリしました。

今回、来てよかったー!!と思いましたよ、ホントに…。ありがとうございます。

ブログや記事のアウトプットで完全に前を行っている方々にお会いできてよかったです。

懇親会終了後は、2次会へ。けっこうな人数で行ったのですが別テーブルに分かれたので、自分いたテーブルにはなぎせさん(@nagise)、matsuzakiさん(@matsumana)、うめざわさん(@garbagetown)、Okamotoさん(@okapies)、やんくさん(@yy_yank)、ぶりゅーりっくさん(@wreulicke)、エセ賢者さん(@MulticolorWorld)たちがいらっしゃいました。

CCCでは2次会まで参加するようにしていますが、こちらも楽しかったです。

まとめ

今回は仕事の雰囲気から行けないかも?と思っていたのですが、無事参加できてよかったです。

楽しい1日でしたー!

参加された皆さま、そして運営の皆さま、ありがとうございました!

次回も参加したいと思います!

オマケ

特攻服のイメージなんて、気のせいですよ。

どうしてでしょうね…。

実現しましたよー。

つちのこ言い出したのは、あなたです…。

サブイベントは速攻で終了したらしい…。

拉致。

謎のCLOVERラッシュ。















なんで固有名詞っぽくなったんでしょうね??(笑)

ローカルキャッシュにCaffeineでも

この記事は、「Java Advent Calendar 2016 - Qiita」の4日目の記事となります。
昨日は、@susumuisさんの「Javaが僕にくれたもの | susumuis Info」でした。
明日は、@fukushiwさんのご担当となります。

ローカルキャッシュ、今ならなにを使うでしょう?

Javaのキャッシュライブラリといえば、なにを使うでしょうか?OSSものを中心に考えると、次の2つあたりが浮かぶのではないかなぁと思います。

なお、今回は分散キャッシュは考えないことにします。あくまで、ローカルキャッシュを対象に。

Ehcacheについては、すでにAPIが刷新された3系がリリースされており、現時点で3.1が利用できます。…あんまり名前を聞きませんけれど。

ここで、対象として挙げたいのが、今回紹介するCaffeineです。

GitHub - ben-manes/caffeine: A high performance caching library for Java 8

Caffeineとは?

Java 8で書かれたキャッシュライブラリで、Google GuavaのCacheにインスパイアされたものだとGitHubには書かれています。実際、APIもよく似た感じになっています。

またhigh performanceを謳っており、他のキャッシュライブラリより良い結果が出ているようです。

Benchmarks ? ben-manes/caffeine Wiki ? GitHub

このベンチマークだけを鵜呑みにするのも…という気はしますが、けっこう機能的に重厚なEhcacheよりもGoogle Guava Cacheの方が軽いという傾向があり、ただGoogle Guavaだと導入するとCache以外にもその他いろいろ付いてくるという点から、このCaffeineはちょうどいいライブラリだなぁと思ったりしています。

Roadmapを見ると、将来的にはGoogle Guava Cacheの代わりになるんでしょうか?

Roadmap Future

GitHub上でもStarがEhcache 3を越えており、すでにいくつかのライブラリなどで採用されているキャッシュライブラリです。

Springでは、4.3から採用されています。
[SPR-13690] Caffeine caching support - Spring JIRA

そしてSpringのGoogle Guava Cacheのサポートは、Spring 5.0で削除されます。
[SPR-13797] Drop Guava caching - superseded by Caffeine - Spring JIRA

というわけでJavaでキャッシュライブラリを選ぶ際の選択肢としては、知っておいてもいいのではないかなぁと。

日本では、あんまり名前を聞かない気もしますけどね?

で、個人的にはこのエントリで初めてCaffeineを試すのですが、今回は以下のテーマでCaffeineを扱ってみたいと思います。

  • とりあえず使ってみる
  • Google Guavaの代替としてのCaffeine
  • JCache ProviderとしてのCaffeine
  • Spring CacheでのCaffeine

始める前に

ここから先は、Caffeineを使ったJavaのコードを書いていきます。

主にテストコードで動作確認をしますが、Spring Cacheのパートを除き、以下のMaven依存関係がpom.xmlに定義されているものとしてください。

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.assertj</groupId>
            <artifactId>assertj-core</artifactId>
            <version>3.5.2</version>
            <scope>test</scope>
        </dependency>

JUnitとAssertJを使用します。

利用するCaffeineのバージョンは、明示的に指定する場合は2.3.5とします。

Caffeineのドキュメントについては、Wikiを参照することになります。

Home ? ben-manes/caffeine Wiki ? GitHub

また、ところどころsleepしたり時間を計測したりするコードが出てきますが、以下のメソッドが定義されているものとしてください。

    void sleep(long sleepSec) {
        try {
            TimeUnit.SECONDS.sleep(sleepSec);
        } catch (InterruptedException e) {
            // ignore
        }
    }

    // stop-watch
    long sw(Runnable runnable) {
        long startTime = System.nanoTime();
        runnable.run();
        long elapsedTime = System.nanoTime() - startTime;

        return TimeUnit.SECONDS.convert(elapsedTime, TimeUnit.NANOSECONDS);
    }

とりあえず使ってみる

では、まずはCaffeineを使ってみましょう。

ドキュメントは、今回はこのあたりを参考に。

Population ? ben-manes/caffeine Wiki ? GitHub

Removal ? ben-manes/caffeine Wiki ? GitHub

Eviction ? ben-manes/caffeine Wiki ? GitHub

準備

Caffeineを使うためには、最低限Maven依存関係にこちらがあればOKです。

        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
            <version>2.3.5</version>
        </dependency>

今回利用する、Caffeineのバージョンは2.3.5です。

また、以下のimport文が定義済みとします。

import java.util.concurrent.TimeUnit;
import java.util.function.Function;
import java.util.stream.IntStream;

import com.github.benmanes.caffeine.cache.Cache;
import com.github.benmanes.caffeine.cache.CacheLoader;
import com.github.benmanes.caffeine.cache.Caffeine;
import com.github.benmanes.caffeine.cache.LoadingCache;
import org.junit.Test;

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

初めてのCaffeine

まずはCacheを作成します。Caffeine#newBuilderから、Cacheを作成することができます。今回は、特に何も設定せずCacheを作成しました。

        // キャッシュの作成
        Cache<String, String> cache =
                Caffeine
                        .newBuilder()
                        .build();

Cacheに対して、データの登録と取得。

        // 登録
        cache.put("key1", "value1");
        cache.put("key2", "value2");

        // 取得
        assertThat(cache.getIfPresent("key1"))
                .isEqualTo("value1");
        assertThat(cache.getIfPresent("key2"))
                .isEqualTo("value2");

エントリ数の取得。

        // エントリ数
        assertThat(cache.estimatedSize())
                .isEqualTo(2L);

登録していないキーに対してはnullが返りますが、Functionを実行して値を決めることもできます。

        // 登録していないキーに対しては、nullが返る
        assertThat(cache.getIfPresent("missing-key"))
                .isNull();

        // 登録していないキーに対する呼び出しに対して、Functionを実行することもできる
        assertThat(cache.get("key3", key -> "value" + key.replace("key", "")))
                .isEqualTo("value3");

とてもGoogle Guava Cacheっぽいですね。

遅いFunctionを登録して値をロードすると、1回目は低速ですが2回目はCacheに乗るので高速になったり。

        // 遅いFunction
        Function<String, String> slowEntryLoader = key -> {
            sleep(3L);
            return "value" + key.replace("key", "");
        };

        // 1回目は低速
        assertThat(
                sw(() -> assertThat(cache.get("key4", slowEntryLoader)).isEqualTo("value4"))
        ).isGreaterThanOrEqualTo(3L);

        // 2回目は高速
        assertThat(
                sw(() -> assertThat(cache.get("key4", slowEntryLoader)).isEqualTo("value4"))
        ).isLessThan(1L);

エントリの削除。

        // エントリの削除
        cache.invalidate("key1");
        assertThat(cache.getIfPresent("key1"))
                .isNull();

        // エントリの全削除
        cache.invalidateAll();
        assertThat(cache.estimatedSize())
                .isZero();

とまあ、ふつうに使うならこんな感じです。

LoadingCache

Google Guavaのように、LoadingCacheを作成することもできます。あらかじめCacheの構築時にCacheLoaderを仕込んでおき、キーに対応するエントリがなかった時に、CacheLoaderを実行することができます。

Caffeine#newBuilderを呼び出し後、buildメソッド呼び出し時にCacheLoaderを指定すると、LoadingCacheを作成することができます。

        // 低速なエントリロード用のCacheLoader
        CacheLoader<String, String> slowLoader = key -> {
            sleep(3L);
            return "value" + key.replace("key", "");
        };

        // CacheLoaderを使用して、LoadingCacheを作成
        LoadingCache<String, String> cache =
                Caffeine
                        .newBuilder()
                        .build(slowLoader);

        // 登録していないキーに対しても、いきなり値を取得できる
        assertThat(cache.get("key1"))
                .isEqualTo("value1");

        // 呼び出しは、1回目は低速(CacheLoaderが低速なので)
        assertThat(
                sw(() -> assertThat(cache.get("key2")))
        ).isGreaterThanOrEqualTo(3L);

        // 2回目は、ロード済みなので高速
        assertThat(
                sw(() -> assertThat(cache.get("key2")))
        ).isLessThan(1L);

Cacheの設定をしてみる

続いて、Cacheの設定をしてみましょう。Caffeine#newBuilderを呼び出してから、buildするまでの間にCacheの設定をすることができます。

今回は、アクセスしてから5秒後に有効期限切れするCacheを作成してみます。

        // アクセス後の有効期限を5秒に設定したCacheを作成
        Cache<String, String> cache =
                Caffeine
                        .newBuilder()
                        .expireAfterAccess(5L, TimeUnit.SECONDS)
                        .build();

        // エントリを2つ登録
        cache.put("key1", "value1");
        cache.put("key2", "value2");

        sleep(3L);

        // 3秒後、片方にアクセス
        cache.getIfPresent("key1");

        sleep(3L);

        // 3秒後の時点でアクセスした方は、エントリが残っている
        assertThat(cache.getIfPresent("key1"))
                .isEqualTo("value1");
        // アクセスしなかった方は、エントリが消えている
        assertThat(cache.getIfPresent("key2"))
                .isNull();

        sleep(5L);

        // 5秒後、すべてのエントリがクリアされている
        // ※Cache#estimatedSizeはexpireをすぐには反映しないので、Cache#cleanUpを呼び出している
        cache.cleanUp();
        assertThat(cache.estimatedSize())
                .isZero();

Cacheの設定を文字列で行う

Cacheの設定を文字列で指定することもできます。

        Cache<String, String> cache =
                Caffeine.from("maximumSize=10,expireAfterAccess=5s").build();

        IntStream.rangeClosed(1, 20).forEach(i -> cache.put("key" + i, "value" + i));

        cache.cleanUp();
        assertThat(cache.estimatedSize())
                .isEqualTo(10L);

        cache.invalidateAll();

        cache.put("key1", "value1");
        cache.put("key2", "value2");

        sleep(3L);

        cache.getIfPresent("key1");

        sleep(3L);

        assertThat(cache.getIfPresent("key1"))
                .isEqualTo("value1");
        assertThat(cache.getIfPresent("key2"))
                .isNull();

設定にどういったものが指定できるかは、こちらを参照するとよいでしょう。
https://github.com/ben-manes/caffeine/blob/v2.3.5/caffeine/src/main/java/com/github/benmanes/caffeine/cache/CaffeineSpec.java#L173


今回はこのくらいの機能の紹介としますが、他にもRefreshなどいろいろあるので、気になる方はWikiを参照してください。

Refresh ? ben-manes/caffeine Wiki ? GitHub

Google Guavaの代替としてのCaffeine

Caffeineには、Google Guava Cacheのインターフェースの実装を提供するモジュールがあります。

Guava ? ben-manes/caffeine Wiki ? GitHub

準備

Google Guava Cacheのインターフェースの実装を使う場合に、必要な依存関係はこちら。

        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>guava</artifactId>
            <version>2.3.5</version>
        </dependency>

使ってみる

ここまでしてCaffeineを使うケースは、あまりないような気もするので、さらっとだけ書きます。

今回は、LoadingCache(Google Guava Cache)インターフェースの実装を使ってみましょう。

        CacheLoader<String, String> loader = key -> {
            sleep(3L);
            return "value" + key.replace("key", "");
        };

        // Caffeineから、Guava Cacheを作成することができる
        com.google.common.cache.LoadingCache<String, String> cache =
                CaffeinatedGuava.build(
                        Caffeine
                                .newBuilder()
                                .expireAfterWrite(5L, TimeUnit.SECONDS),
                        loader
                );

CaffeinatedGuavaを使うことで、Google Guava Cacheのインターフェースを実装したCacheを利用することができます。そのインプットとなるのは、CaffeineのCacheの設定(Caffeineクラスの設定)なわけですが。

今回はCacheLoaderに遅い実装を入れたので、こんな感じで確認。

        // 初回は低速
        assertThat(
                sw(() -> {
                    try {
                        assertThat(cache.get("key1")).isEqualTo("value1");
                    } catch (ExecutionException e) {
                        throw new RuntimeException(e);
                    }
                })
        ).isGreaterThanOrEqualTo(3L);

        // 2回目は高速
        assertThat(
                sw(() -> {
                    try {
                        assertThat(cache.get("key1")).isEqualTo("value1");
                    } catch (ExecutionException e) {
                        throw new RuntimeException(e);
                    }
                })
        ).isLessThan(1L);

        // 有効期限切れまで待つ
        sleep(5L);

        // 再度低速になる
        assertThat(
                sw(() -> {
                    try {
                        assertThat(cache.get("key1")).isEqualTo("value1");
                    } catch (ExecutionException e) {
                        throw new RuntimeException(e);
                    }
                })
        ).isGreaterThanOrEqualTo(3L);

なお、Google Guava Cacheのインターフェースに対する実装こそ提供するものの、機能的に互換であるわけではありません。

互換性についてはこちらを参照してください。

Guava / API compatibility

JCache ProviderとしてのCaffeine

Caffeineは、JCacheの実装も提供しているようです。

JCache ? ben-manes/caffeine Wiki ? GitHub

Expireについては、JCacheの仕様と互換性がないようですが…。

準備

CaffeineのJCache用のモジュールを使用するには、以下の依存関係を定義します。

        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>jcache</artifactId>
            <version>2.3.5</version>
        </dependency>

javax.cache:cache-apiについては、明示的には不要です。CaffeineのJCacheモジュールに、ScopeがCompileとして含まれています(JCacheの実装としては珍しいです)。

ここでは、以下のimport文が定義されていることを前提にします。

import java.util.Arrays;
import java.util.HashSet;
import javax.cache.Cache;
import javax.cache.CacheManager;
import javax.cache.Caching;
import javax.cache.configuration.CompleteConfiguration;
import javax.cache.configuration.Configuration;
import javax.cache.configuration.MutableConfiguration;
import javax.cache.spi.CachingProvider;

import org.assertj.core.data.MapEntry;
import org.junit.Test;

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

使ってみる

では、まずはJCacheを使ってみましょう。Cacheは、その場で定義します。

        Configuration<String, String> configuration =
                new MutableConfiguration<String, String>()
                        .setTypes(String.class, String.class);

        try (CachingProvider provider = Caching.getCachingProvider();
             CacheManager manager = provider.getCacheManager();
             Cache<String, String> cache = manager.createCache("caffeineCache", configuration)) {

            cache.put("key1", "value1");
            assertThat(cache.get("key1"))
                    .isEqualTo("value1");
        }

至って普通です。

デフォルトキャッシュ

実は、CaffeineのJCacheモジュールでは、「default」という名前のCacheがいきなり使えるようになっています。

        try (CachingProvider provider = Caching.getCachingProvider();
             CacheManager manager = provider.getCacheManager();
             // いきなり「default」という名前のCacheが使える
             Cache<String, String> defaultCache = manager.getCache("default")) {

            defaultCache.put("key1", "value1");
            assertThat(defaultCache.get("key1"))
                    .isEqualTo("value1");
        }

これはどういうこと?というわけですが、CaffeineのJCacheモジュールは、内部的にTypesafe Configを使用しています。

GitHub - typesafehub/config: Configuration library for JVM languages

書き方や設定項目については、こちらを見るとよいです。
https://github.com/ben-manes/caffeine/blob/v2.3.5/jcache/src/main/resources/reference.conf

このreference.confが直接使われているわけではないようですが、以下のように「default」というCacheが定義されているのと同等になっているようなので、いきなり「default」というCacheが使えるようです。

caffeine.jcache {

  # A named cache is configured by nesting a new definition under the caffeine.jcache namespace. The
  # per-cache configuration is overlaid on top of the default configuration.
  default {

独自の設定をしてみる

で、CaffeineのJCacheモジュールを使ってCacheを定義するには、MutableConfigurationで定義する以外にTypesafe Configが使えることがわかりました。

ここで、application.confというファイル名で設定ファイルを追加すると、CaffeineのCacheManagerの実装が初期化時にロードしてくれます。

Typesafe Configのデフォルトのファイル名なので、読んでくれているだけなのですが…。というか、CaffeineのCacheManagerって、Propertiesを無視するのでそこは指定できてもよかったんじゃないかなぁと…。

では、application.confを作成してみます。今回は、Store-By-RefenceとRead Throughの設定をしてみました。
src/test/resources/application.conf

caffeine.jcache {
  definedCaffeineCache {
    store-by-value {
      enabled = true
    }

    read-through {
      enabled = true

      loader = "javaadventcalendar.MyCacheLoader"
    }
  }
}

caffeine.jcacheの配下に

caffeine.jcache {

Cache名の要素を定義すればOKです。今回は、「definedCaffeineCache」という名前のCacheを定義しました。

  definedCaffeineCache {

CacheLoaderも定義したので、こちらも作成しておきます。
src/test/java/javaadventcalendar/MyCacheLoader.java

package javaadventcalendar;

import java.util.Map;
import java.util.stream.Collectors;
import java.util.stream.StreamSupport;
import javax.cache.integration.CacheLoader;
import javax.cache.integration.CacheLoaderException;

public class MyCacheLoader implements CacheLoader<String, String> {
    @Override
    public String load(String key) throws CacheLoaderException {
        return "value" + key.replace("key", "");
    }

    @Override
    public Map<String, String> loadAll(Iterable<? extends String> keys) throws CacheLoaderException {
        return StreamSupport
                .stream(keys.spliterator(), false)
                .collect(Collectors.toMap(key -> key, key -> load(key)));
    }
}

適当…。

では、使ってみます。

        try (CachingProvider provider = Caching.getCachingProvider();
             CacheManager manager = provider.getCacheManager();
             // application.confで定義したCache
             Cache<String, String> definedCache = manager.getCache("definedCaffeineCache")) {

            // CacheLoaderが適用されていることが確認できる
            definedCache.put("key1", "value1");
            assertThat(definedCache.get("key1"))
                    .isEqualTo("value1");

            assertThat(definedCache.get("key2"))
                    .isEqualTo("value2");
            assertThat(definedCache.getAll(new HashSet<>(Arrays.asList("key3", "key4"))))
                    .containsExactly(MapEntry.entry("key3", "value3"), MapEntry.entry("key4", "value4"));

            // application.confで設定した内容が確認できる
            CompleteConfiguration<String, String> definedConfiguration =
                    definedCache.getConfiguration(CompleteConfiguration.class);
            assertThat(definedConfiguration.isStoreByValue())
                    .isTrue();
            assertThat(definedConfiguration.isReadThrough())
                    .isTrue();
        }

設定が適用されていることが、確認できましたね。

といわけで、JCache ProviderとしてのCaffeine、それから設定をTypesafe Configの設定ファイルで行ってみました、と。

CDI & Interceptor

JCacheにはCDIでのアノテーションとInterceptorについての仕様がありますが、Caffeineはこの実装を提供していません。
CDI

JCache RIのモジュールを使いなさい、と。
GitHub - jsr107/RI: Reference Implementation

まあ、多くのJCacheの実装はInterceptorの実装を提供していないので、そう不思議なことではないのですけれどね。

依存関係にこちらを追加した上で

        <dependency>
            <groupId>org.jsr107.ri</groupId>
            <artifactId>cache-annotations-ri-cdi</artifactId>
            <version>1.0.0</version>
        </dependency>

beans.xmlにこのように設定すればよいでしょう。
beans.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee
                           http://xmlns.jcp.org/xml/ns/javaee/beans_1_1.xsd"
       bean-discovery-mode="annotated">
    <interceptors>
        <class>org.jsr107.ri.annotations.cdi.CacheResultInterceptor</class>
        <class>org.jsr107.ri.annotations.cdi.CachePutInterceptor</class>
        <class>org.jsr107.ri.annotations.cdi.CacheRemoveEntryInterceptor</class>
        <class>org.jsr107.ri.annotations.cdi.CacheRemoveAllInterceptor</class>
    </interceptors>
</beans>

他のJCacheの実装で試した時のエントリを、貼っておきます。
JCacheのCDI連携を、GlassFish 4.1で動かす - CLOVER

Spring CacheでのCaffeine

最後は、Spring CacheにおけるCaffeineについてです。

最初にも少し触れましたが、Spring 4.3からSpring CacheでCaffeineをサポートしています。

Spring Cache
Caffeine Cache

また、Spring BootでのAutoConfigureの対象ともなっています。

Spring Boot
31. Caching
Caffeine
Appendix A. Common application properties

今回は、簡単にSpring Bootで使ってみます。

準備

使用するSpring Bootのバージョンは、1.4.2.RELEASEとします。

    <properties>
        <java.version>1.8</java.version>
        <spring.boot.version>1.4.2.RELEASE</spring.boot.version>
    </properties>

Caffeineと「spring-boot-starter-cache」を依存関係に加えます。

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>com.github.ben-manes.caffeine</groupId>
            <artifactId>caffeine</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>

「spring-boot-starter-test」は、テストコード用です。

Cacheを適用するクラスを作成する

まずは、SpringのCacheを適用するクラスを作成します。@CacheConfigと@Cacheableアノテーションを、今回は使用しています。
src/main/java/javaadventcalendar/CalcService.java

package javaadventcalendar;

import java.util.concurrent.TimeUnit;

import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@CacheConfig(cacheNames = "calcCache")
@Service
public class CalcService {
    @Cacheable
    public int add(int a, int b) {
        try {
            TimeUnit.SECONDS.sleep(5);
        } catch (InterruptedException e) {
            // ignore
        }

        return a + b;
    }
}

mainメソッドは今回使わないので中身は空ですが、@SpringBootApplicationアノテーションを付与したクラスも作成しておきます。

この時、@EnableCachingアノテーションも付与しておきます。
src/main/java/javaadventcalendar/App.java

package javaadventcalendar;

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class App {
}

Caffeineの設定をする

せっかくなので、Caffeineの設定もしてみます。

@Beanで、CaffeineCacheManagerを直接使って設定してもいいのですが
Caffeine Cache

今回は、Spring Bootのapplication.propertiesで設定することにしましょう。
Caffeine
Appendix A. Common application properties

で、作成したのがこちら。
src/test/resources/application.properties

spring.cache.cache-names=calcCache
spring.cache.caffeine.spec=maximumSize=100,expireAfterAccess=10s

とりあえず、有効期限の設定を今回は使用します。Specの書き方は、Caffeineの設定を文字列で行った時と同じです。

よって、今回はJava側に直接Caffeineに関するコードや設定は出てきません。

テストコードを書く

では、最後にテストコードを書きます。

テストクラスの宣言と、import文はこんな感じ。

import java.util.concurrent.TimeUnit;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

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

@RunWith(SpringRunner.class)
@SpringBootTest(classes = App.class)
public class CaffeineSpringCacheTest {
    @Autowired
    CalcService calcService;

先ほど宣言したServiceを、@Autowiredでインジェクションします。

テストコード。

    @Test
    public void gettingStarted() {
        // 1回目は低速
        assertThat(
                sw(() -> assertThat(calcService.add(1, 2)).isEqualTo(3))
        ).isGreaterThanOrEqualTo(5L);

        // 2回目は高速
        assertThat(
                sw(() -> assertThat(calcService.add(1, 2)).isEqualTo(3))
        ).isLessThan(1L);

        // 有効期限切れを待つ
        sleep(10L);

        // 低速に戻る
        assertThat(
                sw(() -> assertThat(calcService.add(1, 2)).isEqualTo(3))
        ).isGreaterThanOrEqualTo(5L);
    }

設定が効いていることが、確認できました、と。

まとめ

今回は、JavaのキャッシュライブラリであるCaffeineを紹介してみました。ローカルキャッシュではありますが、Google Guava CacheやEhcacheの代替として十分選択肢に挙がるのではないでしょうか?

気になる方に、少しでもこのエントリがお役に立てば幸いです。

今回作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/java-advent-calendar/tree/master/2016

明日は、@fukushiwさんのご担当です。よろしくお願いします!

2016-11-30

Infinispan Embedded Mode/Remote(Hot Rod Client) × Spring Cache

Infinispanには、Springとの連携(Spring Cache Provider)がけっこう前からあります。Infinispan 5.0.0.Finalの時点では、すでにあったみたいです。

とはいえ、しばらくEmbedded Modeのみだったのですが、8.1.0.FinalでClient/Server(Hot Rod Client)での連携が追加され、最近Infinispan側のSpring連携も進んでいるようなので、ちょっと試してみることにしました。

9.0.0.Finalでは、Spring Sessionの実装も入りそうな雰囲気です。

で、今回作成するコードの実行にはSpring Bootは使いますが、Spring Boot側で提供しているInfinispanに対するautoconfigureについては今回はおいておくものとします。
※Spring IO Platformに入っているInfinispanのバージョンは、ちょっと古め…

あくまで、Infinispan側が提供するSpring Cacheの機能に対する内容が、今回の焦点です。

Embedded Mode/Hot Rod Client両方とも扱います。あと、コードはScalaで書きますが(テストはScalaTest)そのあたりを含めたMavenの設定は書くと長くなるので、気になる方はGitHubにソースコードを置いているのでそちらを参照いただければと思います。

使用するバージョンは、Infinispanについては8.2.5.Final、Spring Bootについては1.4.2.RELEASEを使用します。

Embedded Mode

まずは、Embedded Modeから。ドキュメントは、こちらです。

Using Infinispan as a Spring Cache provider

見てると、なんか古い気がしますが…。

準備

Embedded ModeのInfinispanをSpring Cache Providerとして使うには、「infinispan-spring4-embedded」を使用します。

        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-spring4-embedded</artifactId>
            <version>8.2.5.Final</version>
        </dependency>

その他、ScalaやSpring Boot、テスト関係を含めた依存関係。

        <dependency>
            <groupId>org.scala-lang</groupId>
            <artifactId>scala-library</artifactId>
            <version>${scala.version}</version>
        </dependency>

        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-spring4-embedded</artifactId>
            <version>8.2.5.Final</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.scalatest</groupId>
            <artifactId>scalatest_${scala.major.version}</artifactId>
            <version>3.0.1</version>
            <scope>test</scope>
        </dependency>

Cacheを適用するクラスと起動用のクラス

Spring Cacheを適用するクラスは、単純なもので用意。
src/main/scala/org/littlewings/infinispan/spring/CalcService.scala

package org.littlewings.infinispan.spring

import java.util.concurrent.TimeUnit

import org.springframework.cache.annotation.{CacheConfig, CacheEvict, Cacheable}
import org.springframework.stereotype.Service

@Service
@CacheConfig(cacheNames = Array("calcCache"))
class CalcService {
  @Cacheable
  def add(a: Int, b: Int): Int = {
    TimeUnit.SECONDS.sleep(3L)

    a + b
  }

  @CacheEvict
  def evict(a: Int, b: Int): Unit = ()
}

あと、@SpringBootApplicationアノテーションと、Cacheを有効にするように@EnableCachingアノテーションを付与したクラスを作成します。
src/main/scala/org/littlewings/infinispan/spring/App.scala

package org.littlewings.infinispan.spring

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.cache.annotation.EnableCaching

@SpringBootApplication
@EnableCaching
class App

CacheManagerの作成

@Beanとして、SpringのCacheManagerを作成します。Embedded Modeの場合は、Infinispan側でSpringEmbeddedCacheManagerというクラスが提供されているので、こちらを使用します。
src/main/scala/org/littlewings/infinispan/spring/Config.scala

package org.littlewings.infinispan.spring

import java.util.concurrent.TimeUnit

import org.infinispan.configuration.cache.ConfigurationBuilder
import org.infinispan.manager.DefaultCacheManager
import org.infinispan.spring.provider.SpringEmbeddedCacheManager
import org.springframework.cache.CacheManager
import org.springframework.context.annotation.{Bean, Configuration}

@Configuration
class Config {
  @Bean
  def cacheManager: CacheManager = {
    val cacheConfiguration = new ConfigurationBuilder().expiration.lifespan(5L, TimeUnit.SECONDS).build
    val nativeCacheManager = new DefaultCacheManager
    nativeCacheManager.defineConfiguration("calcCache", cacheConfiguration)

    new SpringEmbeddedCacheManager(nativeCacheManager)
  }
}

SpringEmbeddedCacheManagerに対して、InfinispanのEmbeddedCacheManager(DefaultCacheManager)を渡すようにしてあげればOKです。

Cacheの設定は、Local Cacheでexpireを5秒としました。

SpringEmbeddedCacheManagerFactoryBean、ContainerEmbeddedCacheManagerFactoryBeanといったクラスもありますが、今回は省略。

https://github.com/infinispan/infinispan/tree/8.2.5.Final/spring/spring4/spring4-embedded/src/main/java/org/infinispan/spring/provider

確認(テストコード)

それでは、テストコードを書いて確認してみます。

作成したのは、こちら。
rc/test/scala/org/littlewings/infinispan/spring/SpringEmbeddedCacheTest.scala

package org.littlewings.infinispan.spring

import java.util.concurrent.TimeUnit

import org.junit.runner.RunWith
import org.junit.{Before, Test}
import org.scalatest.Matchers
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.cache.CacheManager
import org.springframework.test.context.junit4.SpringRunner

@RunWith(classOf[SpringRunner])
@SpringBootTest(classes = Array(classOf[App]))
class SpringEmbeddedCacheTest extends Matchers {
  @Autowired
  var calcService: CalcService = _

  @Autowired
  var cacheManager: CacheManager = _

  @Before
  def setUp(): Unit =
    cacheManager.getCache("calcCache").clear()

  protected def sw(fun: => Unit): Long = {
    val startTime = System.nanoTime
    fun
    TimeUnit.SECONDS.convert(System.nanoTime - startTime, TimeUnit.NANOSECONDS)
  }

  @Test
  def embeddedCacheSimpleTest(): Unit = {
    sw {
      calcService.add(1, 3) should be(4)
    } should be >= 3L
    sw {
      calcService.add(1, 3) should be(4)
    } should be < 1L

    TimeUnit.SECONDS.sleep(5L)

    sw {
      calcService.add(1, 3) should be(4)
    } should be >= 3L
  }

  @Test
  def embeddedCacheEvictTest(): Unit = {
    sw {
      calcService.add(1, 3) should be(4)
    } should be >= 3L

    calcService.evict(1, 3)

    sw {
      calcService.add(1, 3) should be(4)
    } should be >= 3L
  }
}

それぞれ、Cacheに登録してexpireを待った後にアクセスしたり、Cacheエントリを削除したりするテストです。こんな感じで。

Embedded Mode向けに作成したコードは、こちらに置いています。
https://github.com/kazuhira-r/infinispan-getting-started/tree/master/embedded-spring-cache

Remote(Hot Rod Client)

続いて、Hot Rod ClientでのSpring Cache Provider。

こちらは、ドキュメントに記載のない機能です…。

準備

Hot Rod ClientのInfinispanをSpring Cache Providerとして使うには、「infinispan-spring4-remote」を使用します。

        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-spring4-remote</artifactId>
            <version>8.2.5.Final</version>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.logging.log4j</groupId>
                    <artifactId>log4j-api</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.apache.logging.log4j</groupId>
                    <artifactId>log4j-core</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.apache.logging.log4j</groupId>
                    <artifactId>log4j-jcl</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.apache.logging.log4j</groupId>
                    <artifactId>log4j-slf4j-impl</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-client-hotrod</artifactId>
            <version>8.2.5.Final</version>
        </dependency>
        <dependency>
            <groupId>net.jcip</groupId>
            <artifactId>jcip-annotations</artifactId>
            <version>1.0</version>
            <scope>provided</scope>
        </dependency>

jcip-annotationsが入っているのは、Scalaの都合上です。

infinispan-spring-remote4の場合は、Hot Rod Clientへの依存関係を別途追加する必要があります。また、Spring Bootで実行するにあたり、Log4j2のSLF4J向けのブリッジがSpring BootデフォルトのSLF4Jと衝突するのでexclusionしてあります。

その他に使用したライブラリは、こちら。

        <dependency>
            <groupId>org.scala-lang</groupId>
            <artifactId>scala-library</artifactId>
            <version>${scala.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.infinispan</groupId>
            <artifactId>infinispan-server-hotrod</artifactId>
            <version>8.2.5.Final</version>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.apache.logging.log4j</groupId>
                    <artifactId>log4j-api</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.apache.logging.log4j</groupId>
                    <artifactId>log4j-core</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.apache.logging.log4j</groupId>
                    <artifactId>log4j-jcl</artifactId>
                </exclusion>
                <exclusion>
                    <groupId>org.apache.logging.log4j</groupId>
                    <artifactId>log4j-slf4j-impl</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.scalatest</groupId>
            <artifactId>scalatest_${scala.major.version}</artifactId>
            <version>3.0.1</version>
            <scope>test</scope>
        </dependency>

UnitTest内でInfinspan Hot Rod Serverを使いますが、こちらにもLog4j2が含まれているため、除外。

というか、Log4j2がスコープruntimeでHot Rodに入っているのは、どうしてなんでしょう…?

Cacheを適用するクラスと起動用のクラス

ここは、Embedded Modeと同じ。
src/main/scala/org/littlewings/infinispan/spring/CalcService.scala

package org.littlewings.infinispan.spring

import java.util.concurrent.TimeUnit

import org.springframework.cache.annotation.{CacheConfig, CacheEvict, Cacheable}
import org.springframework.stereotype.Service

@Service
@CacheConfig(cacheNames = Array("calcCache"))
class CalcService {
  @Cacheable
  def add(a: Int, b: Int): Int = {
    TimeUnit.SECONDS.sleep(3L)

    a + b
  }

  @CacheEvict
  def evict(a: Int, b: Int): Unit = ()
}

src/main/scala/org/littlewings/infinispan/spring/App.scala

package org.littlewings.infinispan.spring

import org.springframework.boot.autoconfigure.SpringBootApplication
import org.springframework.cache.annotation.EnableCaching

@SpringBootApplication
@EnableCaching
class App

CacheManagerの作成

@Beanとして、SpringのCacheManagerを作成します。Remote(Hot Rod Client)の場合は、Infinispan側でSpringRemoteCacheManagerというクラスが提供されているので、こちらを使用します。
src/main/scala/org/littlewings/infinispan/spring/Config.scala

package org.littlewings.infinispan.spring

import org.infinispan.client.hotrod.RemoteCacheManager
import org.infinispan.client.hotrod.configuration.ConfigurationBuilder
import org.infinispan.spring.provider.SpringRemoteCacheManager
import org.springframework.cache.CacheManager
import org.springframework.context.annotation.{Bean, Configuration}

@Configuration
class Config {
  @Bean
  def cacheManager: CacheManager = {
    val nativeCacheManager =
      new RemoteCacheManager(new ConfigurationBuilder().addServer.host("localhost").port(11222).build)
    new SpringRemoteCacheManager(nativeCacheManager)
  }
}

SpringRemoteCacheManagerには、RemoteCacheManagerを渡せばOKです。

Cacheの設定は、Hot Rodの場合はサーバー側ですね。

こちらにも、SpringRemoteCacheManagerFactoryBeanやContainerRemoteCacheManagerFactoryBeanといったクラスがあったりします。

https://github.com/infinispan/infinispan/tree/8.2.5.Final/spring/spring4/spring4-remote/src/main/java/org/infinispan/spring/provider

確認(テストコード)

それでは、テストコードを書いて確認してみます。

Hot Rodの場合はサーバーが必要になるので、UnitTest内でHot Rod Serverを起動/停止するようなコードを書いて実行します。
src/test/scala/org/littlewings/infinispan/spring/SpringRemoteCacheTest.scala

package org.littlewings.infinispan.spring

import java.util.concurrent.TimeUnit

import org.infinispan.commons.equivalence.AnyServerEquivalence
import org.infinispan.configuration.cache.ConfigurationBuilder
import org.infinispan.manager.DefaultCacheManager
import org.infinispan.server.hotrod.HotRodServer
import org.infinispan.server.hotrod.configuration.HotRodServerConfigurationBuilder
import org.junit.runner.RunWith
import org.junit.{AfterClass, Before, BeforeClass, Test}
import org.scalatest.Matchers
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.cache.CacheManager
import org.springframework.test.context.junit4.SpringRunner

object SpringRemoteCacheTest {
  var hotRodServer: HotRodServer = _

  @BeforeClass
  def setUpClass(): Unit = {
    val embeddedCacheManager = new DefaultCacheManager
    embeddedCacheManager
      .defineConfiguration(
        "calcCache",
        new ConfigurationBuilder()
          .dataContainer()
          .keyEquivalence(new AnyServerEquivalence)
          .valueEquivalence(new AnyServerEquivalence)
          .expiration
          .lifespan(5L, TimeUnit.SECONDS)
          .build
      )

    val hotRodServerHost = "localhost"
    val hotRodServerPort = 11222
    hotRodServer = new HotRodServer
    hotRodServer.start(
      new HotRodServerConfigurationBuilder()
        .host(hotRodServerHost)
        .port(hotRodServerPort)
        .build,
      embeddedCacheManager
    )
  }

  @AfterClass
  def tearDownClass(): Unit = hotRodServer.stop
}

@RunWith(classOf[SpringRunner])
@SpringBootTest(classes = Array(classOf[App]))
class SpringRemoteCacheTest extends Matchers {
  @Autowired
  var calcService: CalcService = _

  @Autowired
  var cacheManager: CacheManager = _

  @Before
  def setUp(): Unit =
    cacheManager.getCache("calcCache").clear()

  protected def sw(fun: => Unit): Long = {
    val startTime = System.nanoTime
    fun
    TimeUnit.SECONDS.convert(System.nanoTime - startTime, TimeUnit.NANOSECONDS)
  }

  @Test
  def remoteCacheSimpleTest(): Unit = {
    sw {
      calcService.add(1, 3) should be(4)
    } should be >= 3L
    sw {
      calcService.add(1, 3) should be(4)
    } should be < 1L

    TimeUnit.SECONDS.sleep(5L)

    sw {
      calcService.add(1, 3) should be(4)
    } should be >= 3L
  }

  @Test
  def remoteCacheEvictTest(): Unit = {
    sw {
      calcService.add(1, 3) should be(4)
    } should be >= 3L

    calcService.evict(1, 3)

    sw {
      calcService.add(1, 3) should be(4)
    } should be >= 3L
  }
}

テスト自体は、Embedded Modeと同じです。

最初に、Hot Rod Serverの起動/停止の実装をしてあります。Cacheの設定も、ここでしています。

object SpringRemoteCacheTest {
  var hotRodServer: HotRodServer = _

  @BeforeClass
  def setUpClass(): Unit = {
    val embeddedCacheManager = new DefaultCacheManager
    embeddedCacheManager
      .defineConfiguration(
        "calcCache",
        new ConfigurationBuilder()
          .dataContainer()
          .keyEquivalence(new AnyServerEquivalence)
          .valueEquivalence(new AnyServerEquivalence)
          .expiration
          .lifespan(5L, TimeUnit.SECONDS)
          .build
      )

    val hotRodServerHost = "localhost"
    val hotRodServerPort = 11222
    hotRodServer = new HotRodServer
    hotRodServer.start(
      new HotRodServerConfigurationBuilder()
        .host(hotRodServerHost)
        .port(hotRodServerPort)
        .build,
      embeddedCacheManager
    )
  }

  @AfterClass
  def tearDownClass(): Unit = hotRodServer.stop
}

結果は、expireの設定がEmbedded Modeの時と同じなので、ほぼ同じテストがパスします、と。

Remote(Hot Rod Client)向けに作成したコードは、こちらに置いています。

https://github.com/kazuhira-r/infinispan-getting-started/tree/master/remote-spring-cache

まとめ

Spring Cache Providerとしての、Infinispanの機能を試してみました。双方の使い方がわかっていれば、わりかし簡単に使えるのではないかなぁと思います。

Infinispan 9.0.0.FinalではSpring Sessionの実装が入りそうなので(Embedded Mode/Remoteともに)、リリースされたらそちらも試してみましょう。

2016-11-28

Scala × Spring 4.3でコンストラクタインジェクション

今更ですが、Spring 4.3でコンストラクタインジェクションを使う際に、@Autowiredを付けなくてもよくなったという話をちょっと試しておこうかと。

Scalaで。

Spring 4.3 DIコンテナ関連の主な変更点 - Qiita

pom.xmlは載せますが、長いので最後に。

確認用のコードを用意

とりあえず、2つ以上のBeanをインジェクションしたかったので、しょうもないですがこういうクラスを用意。
src/main/scala/org/littlewings/spring/DecolationService.scala

package org.littlewings.spring

import org.springframework.stereotype.Service

@Service
class DecorationService(joinService: JoinService, wrapService: WrapService) {
  def apply(prefix: String, suffix: String, delimiter: Char, tokens: String*): String =
    wrapService(joinService(delimiter, tokens: _*), prefix, suffix)
}

@Service
class JoinService {
  def apply(delimiter: Char, tokens: String*): String =
    tokens.mkString(delimiter.toString)
}

@Service
class WrapService {
  def apply(token: String, prefix: String, suffix: String): String =
    s"${prefix}${token}${suffix}"
}

内容は、お察しください…。

こちらが、コンストラクタでインジェクションできることを確認するポイント。

@Service
class DecorationService(joinService: JoinService, wrapService: WrapService) {

なにもないですが、エントリポイントも一応用意。
src/main/scala/org/littlewings/spring/App.scala

package org.littlewings.spring

import org.springframework.boot.autoconfigure.SpringBootApplication

@SpringBootApplication
class App

テストコードで確認

では、テストコードで確認してみます。
src/test/scala/org/littlewings/spring/DecorationServiceTest.scala

package org.littlewings.spring

import org.junit.Test
import org.junit.runner.RunWith
import org.scalatest.Matchers
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner

@RunWith(classOf[SpringJUnit4ClassRunner])
@SpringBootTest(classes = Array(classOf[App]))
class DecorationServiceTest extends Matchers {
  @Autowired
  private[spring] var decorationService: DecorationService = _

  @Test
  def constructorInjection(): Unit = {
    decorationService("[", "]", ',', "Hello", "Spring", "with", "Scala") should be("[Hello,Spring,with,Scala]")
  }
}

ここは、@Autowired使いますよね…。

で、このコードで普通に動きました。いいですね!

あと、バリエーション的に以下の両方とも試しておきましたが、どちらも動きました(そりゃそうだ的な)。

@Service
class DecorationService(joinService: JoinService, wrapService: WrapService) {

@Service
class DecorationService(val joinService: JoinService, val wrapService: WrapService) {

ちょっと活用していこうかな?

最後に、pom.xmlも載せておきます。pom.xmlが、1番長い…。
pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>org.littlewings</groupId>
    <artifactId>spring-scala-constructor-injection</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <packaging>jar</packaging>

    <properties>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
        <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
        <maven.compiler.source>1.8</maven.compiler.source>
        <maven.compiler.target>1.8</maven.compiler.target>
        <scala.major.version>2.12</scala.major.version>
        <scala.version>${scala.major.version}.0</scala.version>
        <spring.boot.version>1.4.2.RELEASE</spring.boot.version>
    </properties>

    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring.boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>

    <dependencies>
        <dependency>
            <groupId>org.scala-lang</groupId>
            <artifactId>scala-library</artifactId>
            <version>${scala.version}</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
        <dependency>
            <groupId>org.scalatest</groupId>
            <artifactId>scalatest_${scala.major.version}</artifactId>
            <version>3.0.1</version>
            <scope>test</scope>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>net.alchim31.maven</groupId>
                <artifactId>scala-maven-plugin</artifactId>
                <version>3.2.2</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>compile</goal>
                            <goal>testCompile</goal>
                        </goals>
                    </execution>
                </executions>
                <configuration>
                    <scalaVersion>${scala.version}</scalaVersion>
                    <args>
                        <arg>-Xlint</arg>
                        <arg>-unchecked</arg>
                        <arg>-deprecation</arg>
                        <arg>-feature</arg>
                    </args>
                    <recompileMode>incremental</recompileMode>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>${spring.boot.version}</version>
                <executions>
                    <execution>
                        <goals>
                            <goal>repackage</goal>
                        </goals>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>
</project>

2016-11-26

HAProxyでHTTPロードバランシング

少し前に、nginxでHTTPロードバランシングをやってみましたが、今度はHAProxyでやってみます。

nginxのHTTPロードバランシングを試す - CLOVER

HAProxyの設定をするには、このあたりを参考にしました。

Proxies

マイクロサービス時代のHAProxy | Yakst

第1章 ロードバランサーの概要

HAProxyを使い始めてみる - Qiita

HAProxy を使ってみたメモ - ngの日記

HAProxy のバックエンド Web サーバーヘルスチェックでコンテンツの中身をチェックする

使うHAProxyは、1.6.3です。

では、書いていきましょう。

バックエンドサーバーを書く

まずは、ロードバランサの背後にいるバックエンドのサーバーを用意します。nginxの時も、簡単にSpring Boot CLIで用意しました。以下の3つのサーバー名で、サーバーを用意します。

  • server1 (172.17.0.2)
  • server2 (172.17.0.3)
  • server3 (172.17.0.4)

server.groovy

import javax.servlet.http.HttpServletRequest

@RestController
class HelloController {
    def logger = org.slf4j.LoggerFactory.getLogger(getClass())

    @GetMapping('hello')
    def hello(HttpServletRequest request) {
        logger.info(java.time.LocalDateTime.now().toString() + ": access " + request.requestURI)
        'Hello ' + InetAddress.localHost.hostName + "!!" + System.lineSeparator()
    }
}

アクセスしたら「Hello [ホスト名]!!」を返却します。

サーバーを起動。

$ spring run server.groovy

ロードバランサを設定する

それでは、この3つのサーバーの前段にHAProxyをバランサーとして配置します。

なお、HAProxyは

  • 172.17.0.5

で稼働しているものとします。

haproxy.cfgの設定を、デフォルトの設定から以下のように追記。
/etc/haproxy/haproxy.cfg

frontend app-http
    bind *:8080
    default_backend app-servers

backend app-servers
    balance roundrobin
    mode http

    option forwardfor

    http-request set-header X-Forwarded-Port %[dst_port]
    http-request add-header X-Forwarded-Proto https if { ssl_fc }

    server server1 172.17.0.2:8080
    server server2 172.17.0.3:8080
    server server3 172.17.0.4:8080

HAProxyはfrontendでクライアント側からのリクエストの受け付けの設定、backendで実際のサーバーへの設定を行うようです。

今回は、8080ポートでリッスン、バックエンドには「app-servers」という名前のものを指定。「app-servers」はこのあと定義します。

frontend app-http
    bind *:8080
    default_backend app-servers

バックエンドの設定。バランシングアルゴリズムはラウンドロビン、serverで背後にいるサーバーを指定します。あとはX-Forwarded-Forの設定を入れています。

backend app-servers
    balance roundrobin
    mode http

    option forwardfor

    http-request set-header X-Forwarded-Port %[dst_port]
    http-request add-header X-Forwarded-Proto https if { ssl_fc }

    server server1 172.17.0.2:8080
    server server2 172.17.0.3:8080
    server server3 172.17.0.4:8080


で、HAProxyを再起動。

確認。

$ curl http://172.17.0.5:8080/hello
Hello server1!!
$ curl http://172.17.0.5:8080/hello
Hello server2!!
$ curl http://172.17.0.5:8080/hello
Hello server3!!
$ curl http://172.17.0.5:8080/hello
Hello server1!!
$ curl http://172.17.0.5:8080/hello
Hello server2!!
$ curl http://172.17.0.5:8080/hello
Hello server3!!

1台(server3)をダウンさせてアクセスしてみます。

$ curl http://172.17.0.5:8080/hello
Hello server1!!
$ curl http://172.17.0.5:8080/hello
Hello server2!!
$ curl http://172.17.0.5:8080/hello
<html><body><h1>503 Service Unavailable</h1>
No server is available to handle this request.
</body></html>

あら…。
※このあと繰り返しても、同じです。

ヘルスチェックを設定する

で、こういうのだと困るので、ヘルスチェックを設定してみます。server3は、起動状態に戻します。

ヘルスチェックを行うには、serverのオプションに「check」を入れます。

    server server1 172.17.0.2:8080 check
    server server2 172.17.0.3:8080 check
    server server3 172.17.0.4:8080 check

HAProxyを再起動。

確認。

$ curl http://172.17.0.5:8080/hello
Hello server1!!
$ curl http://172.17.0.5:8080/hello
Hello server2!!
$ curl http://172.17.0.5:8080/hello
<html><body><h1>503 Service Unavailable</h1>
No server is available to handle this request.
</body></html>

$ curl http://172.17.0.5:8080/hello
Hello server1!!
$ curl http://172.17.0.5:8080/hello
Hello server2!!
$ curl http://172.17.0.5:8080/hello
Hello server1!!

1回失敗してますが、そのあとはOKそうですね。

もう少し、細かくヘルスチェックの設定をしてみましょう。

ヘルスチェック対象のURLや間隔を指定してみます。

ヘルスチェック用のURLを、バックエンドサーバーに追加します。「/health-check」でアクセスするものとしましょう。
server.groovy

import javax.servlet.http.HttpServletRequest

@RestController
class HelloController {
    def logger = org.slf4j.LoggerFactory.getLogger(getClass())

    @GetMapping('hello')
    def hello(HttpServletRequest request) {
        logger.info(java.time.LocalDateTime.now().toString() + ": access " + request.requestURI)
        'Hello ' + InetAddress.localHost.hostName + "!!" + System.lineSeparator()
    }

    @GetMapping('health-check')
    def healthCheck(HttpServletRequest request) {
        logger.info(java.time.LocalDateTime.now().toString() + ": access " + request.requestURI)
        'OK ' + InetAddress.localHost.hostName + "!!" + System.lineSeparator()
    }
}

HAProxy側の設定は、このようにします。

backend app-servers
    balance roundrobin
    mode http

    option forwardfor
    option httpchk GET /health-check

    http-request set-header X-Forwarded-Port %[dst_port]
    http-request add-header X-Forwarded-Proto https if { ssl_fc }

    server server1 172.17.0.2:8080 inter 3000 check
    server server2 172.17.0.3:8080 inter 3000 check
    server server3 172.17.0.4:8080 inter 3000 check

「option httpchk」でヘルスチェック先の設定をして

    option httpchk GET /health-check

バックエンドサーバーには、間隔3秒でヘルスチェックを行う、と。

    server server1 172.17.0.2:8080 inter 3000 check
    server server2 172.17.0.3:8080 inter 3000 check
    server server3 172.17.0.4:8080 inter 3000 check

この状態でHAProxyを起動すると、バックエンドサーバーには3秒に1度アクセスが来ることが確認できます。

2016-11-26 06:33:34.921  INFO 147 --- [nio-8080-exec-1] HelloController                          : 2016-11-26T06:33:34.905: access /health-check
2016-11-26 06:33:37.958  INFO 147 --- [nio-8080-exec-2] HelloController                          : 2016-11-26T06:33:37.958: access /health-check
2016-11-26 06:33:40.991  INFO 147 --- [nio-8080-exec-3] HelloController                          : 2016-11-26T06:33:40.991: access /health-check

結果は、最初にヘルスチェックを入れた時とそう変わらないので割愛。

ボディの内容のチェックもできるみたい?

HAProxy のバックエンド Web サーバーヘルスチェックでコンテンツの中身をチェックする

セッションパーシステンスする

最後に、セッションパーシステンスしてみましょう。

セッションを使って確認するので、サーバー側のコードを以下のように変更します。けっこう適当ですが、ご愛嬌。

server.groovy

import javax.servlet.http.HttpServletRequest

@RestController
class HelloController {
    def logger = org.slf4j.LoggerFactory.getLogger(getClass())

    @GetMapping('hello')
    def hello(HttpServletRequest request) {
        logger.info(java.time.LocalDateTime.now().toString() + ": access " + request.requestURI)

        def session = request.session
        def now = session.getAttribute('now')
        if (!now) {
            now = java.time.LocalDateTime.now().toString()
            session.setAttribute('now', now)
        }

        '[' + now + '] Hello ' + InetAddress.localHost.hostName + "!!" + System.lineSeparator()
    }

    @GetMapping('health-check')
    def healthCheck(HttpServletRequest request) {
        logger.info(java.time.LocalDateTime.now().toString() + ": access " + request.requestURI)
        'OK ' + InetAddress.localHost.hostName + "!!" + System.lineSeparator()
    }
}

今のままだと、こんな感じにアクセスしても都度違うサーバーに振り分けられます。当然、セッションも維持されません。

$ curl -b cookie.txt -c cookie.txt http://172.17.0.5:8080/hello
[2016-11-26T06:52:16.530] Hello server1!!

で、HAProxy側の設定は、このように変更。

backend app-servers
    balance roundrobin
    appsession JSESSIONID len 32 timeout 2h request-learn

参考)
demo/HAProxy-の各種設定方法.md at master ? worksap-ate/demo ? GitHub

としたら、うまくいきませんでした。HAProxyがappsessionを理解できないらしく、コケてしまいます…。

$ sudo service haproxy start
 * Starting haproxy haproxy                                                                                                                                                                             [ALERT] 330/065527 (648) : parsing [/etc/haproxy/haproxy.cfg:42] : 'appsession' is not supported anymore, please check the documentation.
[ALERT] 330/065527 (648) : Error(s) found in configuration file : /etc/haproxy/haproxy.cfg
[ALERT] 330/065527 (648) : Fatal errors found in configuration.
                                                                                                                                                                                                 [fail]

困りました。ドキュメントにもappsessionは書いてあるのに…。

で、ちょっと調べたらこんなエントリを見つけまして。
load balancing, affinity, persistence, sticky sessions: what you need to know | HAProxy Technologies – Aloha Load Balancer

これを見ると、appsessionではなくてcookieを使え、と。

http://cbonte.github.io/haproxy-dconv/1.6/configuration.html#4-cookie:cookie

HAProxyの設定は、このように変更。

backend app-servers
    balance roundrobin
    cookie JSESSIONID prefix nocache
    mode http

    option forwardfor
    option httpchk GET /health-check

    http-request set-header X-Forwarded-Port %[dst_port]
    http-request add-header X-Forwarded-Proto https if { ssl_fc }

    server server1 172.17.0.2:8080 inter 3000 check cookie server1
    server server2 172.17.0.3:8080 inter 3000 check cookie server2
    server server3 172.17.0.4:8080 inter 3000 check cookie server3

Cookie名は、「JSESSIONID」として、prefixを指定します。

    cookie JSESSIONID prefix nocache

prefixでは、Cookieの先頭に指定した接頭辞が入ることになるので、バックエンドサーバーにもその設定を入れます。

    server server1 172.17.0.2:8080 inter 3000 check cookie server1
    server server2 172.17.0.3:8080 inter 3000 check cookie server2
    server server3 172.17.0.4:8080 inter 3000 check cookie server3

cookieのあとに続いているのが、接頭辞になります。

試してみましょう。

スティックされるようになりました。

$ curl -b cookie.txt -c cookie.txt http://172.17.0.5:8080/hello
[2016-11-26T07:07:06.833] Hello server1!!
$ curl -b cookie.txt -c cookie.txt http://172.17.0.5:8080/hello
[2016-11-26T07:07:06.833] Hello server1!!
$ curl -b cookie.txt -c cookie.txt http://172.17.0.5:8080/hello
[2016-11-26T07:07:06.833] Hello server1!!

この時、Cookieの値を見ると、Cookieの値に指定したprefixが入っています。

$ cat cookie.txt 
# Netscape HTTP Cookie File
# http://curl.haxx.se/docs/http-cookies.html
# This file was generated by libcurl! Edit at your own risk.

#HttpOnly_172.17.0.5	FALSE	/	FALSE	0	JSESSIONID	server1~D338ED4E06048EEAABE43E26BA1EB2E4

振り分け直すと、こんな感じに。

#HttpOnly_172.17.0.5	FALSE	/	FALSE	0	JSESSIONID	server2~1CA5FDBD2807B239AAFFA1E555C0B9D0

とりあえず、やりたいことはできた感じです。

まとめ

というわけで、HAProxyを使ってHTTPロードバランシングを試してみました。

最後にセッションパーシステンスもやってみましたけど、appsessionを1.6のHAProxyが使えないのがバグなのか、それとも機能として落とされたのかがよくわかりませんでした…。

'appsession' is not supported anymore ... ? Issue #105 ? docker/dockercloud-haproxy ? GitHub

[SOLVED] HAProxy 1.6 Ubuntu 16.04 fails to start with 1.5 config file [Archive] - Ubuntu Forums

2016-11-25

Ubuntu LinuxにHAProxyをインストールする

少し前にnginxをロードバランサーとして使って遊んでみたのですが、今度はHAProxyを試してみたいと思います。

HAProxy - The Reliable, High Performance TCP/HTTP Load Balancer

TCPやHTTPのロードバランシングができるようですが、最近ちょこちょこ名前を聞くなぁと思って調べたら、けっこう古くからあるんですね…。
1.0のリリース、2001年とかなっていますし…。

とりあえず、まずはインストールしてみましょう。インストールする対象は、Ubuntu Linuxとします。
※サポートしているプラットフォームにWindowsがなかったのが、ちょっと惜しいなぁと…

インストール方法としてはソースコードをダウンロードしてコンパイルしてもよいみたいですが、

Download

最新よりも少し古い程度であれば、apt-getで大丈夫そうなので、こちらでインストールすることにします。

$ sudo apt-get install haproxy

確認。

$ dpkg -l haproxy
Desired=Unknown/Install/Remove/Purge/Hold
| Status=Not/Inst/Conf-files/Unpacked/halF-conf/Half-inst/trig-aWait/Trig-pend
|/ Err?=(none)/Reinst-required (Status,Err: uppercase=bad)
||/ Name                                       Version                    Architecture               Description
+++-==========================================-==========================-==========================-==========================================================================================
ii  haproxy                                    1.6.3-1ubuntu0.1           amd64                      fast and reliable load balancing reverse proxy

1.6.3が入りました。現時点での最新安定版は1.6系で、1.6.10なのですが、まあいいでしょう。
また、1.7が開発中みたいです。

設定ファイルは、/etc/haproxyにあるようです。

$ ll /etc/haproxy
total 16
drwxr-xr-x  3 root root 4096 Nov 25 15:24 ./
drwxr-xr-x 65 root root 4096 Nov 25 15:24 ../
drwxr-xr-x  2 root root 4096 Nov 25 15:24 errors/
-rw-r--r--  1 root root 1129 Dec 31  2015 haproxy.cfg

では、起動。

$ sudo service haproxy start
 * Starting haproxy haproxy

確認。

$ pgrep -a haproxy
3345 /usr/sbin/haproxy -f /etc/haproxy/haproxy.cfg -D -p /var/run/haproxy.pid

まだ設定はなにもしていないので、この時点で特にすることはありません。それはそのうち。

停止。

$ sudo service haproxy stop
 * Stopping haproxy haproxy

って思ったら、止まらなかったのでkillすることに。なんでしょう…?

とりあえずは、こんなところで。