Java

Java Servletでよく使うオブジェクトを簡単にキャッシュ&キャッシュ期限も管理

はじめに

Servletで何かのサービスを構築する際、
個人的には大きなフレームワークは使わないようにしています。

ボトルネックになる可能性、フレームワークの将来性、サポートなど、
自身ではコントロールできない要素が多くなるためです。

結果、直接的に “MyServlet extends HttpServlet” することが多くなりますので、
細かな処理まですべて自前で実装することになりますが、むしろ自由でもあります。
Servletの主要な処理の中で、どうしても毎回取り出して使いたいオブジェクトがあるが、
都度DBやファイルサーバから取得するのは避けたい(面倒&負荷をかけたくない)というケースは意外とあるものです。

例えば、「アクセスを禁止しているIPアドレスのリスト」は、5分に1回はDBから取得したほうがよいが、
普段はキャッシュして使いまわせばよい、と言えるかもしれません。

そういったニーズに対応するための極力簡単な実装です。
キャッシュを監視するようなThreadは作りませんし、memcachedのようなサービスも使いません。
JVMの中で完結する仕組みとします。

実装例

キャッシュ対象のクラスが実装する抽象クラス(Cachable)定義

キャッシュ対象としたいクラスには、キャッシュ期限の変更を許可する必要があります。
その振る舞いを定義しておきます。

Cachable.java

/**
 *
 * @author tool-taro.com
 */
public abstract class Cachable {

	//キャッシュ登録時刻(期限判定に使う)
	private long start;

	//キャッシュ登録時に登録時刻をセットされる
	public Cachable start() {
		this.start = System.currentTimeMillis();
		return this;
	}

	//期限切れ判定
	public boolean isExpired() {
		return (this.getTTL() >= 0 && System.currentTimeMillis() - this.start > this.getTTL());
	}

	//キャッシュ期限をミリ秒で返す
	public long getTTL(){
		return 0;
	}
}

Cachableを実装したクラス(例: CachableImpl)定義

キャッシュ期限はテスト用に10秒としました。

CachableImpl.java

/**
 *
 * @author tool-taro.com
 */
public class CachableImpl extends Cachable {

	protected String id = null;

	public CachableImpl(String id) {
		this.id = id;
	}

	@Override
	public long getTTL() {
		return 10 * 1000;
	}
}

キャッシュを管理するクラス(Cache)定義

どこからでも呼び出せる利便性を考慮し、Singletonとしました。

Cache.java

import java.util.concurrent.ConcurrentHashMap;

/**
 *
 * @author tool-taro.com
 */
public class Cache {

	private final static Cache INSTANCE = new Cache();
	private ConcurrentHashMap<String, Cachable> cacheMap = null;

	//Singleton
	public static Cache getInstance() {
		return Cache.INSTANCE;
	}

	private Cache() {
		this.cacheMap = new ConcurrentHashMap();
	}

	//キャッシュを取得する
	public Cachable get(String key) {
		Cachable result = this.cacheMap.get(key);
		if (result == null || result.isExpired()) {
			synchronized (key) {
				//すでに新しいキャッシュが登録されているかもしれないので再度取得する
				result = this.cacheMap.get(key);
				if (result == null || result.isExpired()) {
					this.cacheMap.remove(key);
					result = null;
				}
			}
		}

		return result;
	}

	//キャッシュを登録する
	public void put(String key, Cachable cachable) {
		Cachable target = this.cacheMap.get(key);
		if (target == null || target.isExpired()) {
			synchronized (key) {
				//すでに新しいキャッシュが登録されているかもしれないので再度取得する
				target = this.cacheMap.get(key);
				if (target == null || target.isExpired()) {
					//キャッシュを登録する
					this.cacheMap.put(key, cachable.start());
				}
			}
		}
	}
}

キャッシュ機構の準備が終わりました。
サンプルでは、動作確認しやすいようにjspで実装しています。

cache_test.jsp

<%-- 
    Author     : tool-taro.com
--%>

<%@page import="CachableImpl"%>
<%@page import="Cachable"%>
<%@page import="Cache"%>
<%@page contentType="text/html" pageEncoding="UTF-8" session="false" %>
<!DOCTYPE html>
<html>
    <head>
        <title>tool-taro.com</title>
    </head>
    <body>
        <%
                        //キャッシュから"a"というKeyで登録されたCachableを取り出す
                        Cachable a = Cache.getInstance().get("a");
        %>
        (1) aの初回取得結果=<%= a%><br>

        <%
                        //インスタンスを生成する(実運用時にはDBから取得する等の処理となる)
                        a = new CachableImpl("a");
                        //キャッシュに"a"というKeyで登録する(期限は10秒)
                        Cache.getInstance().put("a", a);
                        //すぐに(10秒以内に)キャッシュから"a"というKeyで登録されたCachableを取り出す
                        a = Cache.getInstance().get("a");
        %>
        (2) aをキャッシュ直後の再取得結果=<%= a%><br>

        <%
                        //キャッシュ期限が切れるまで10秒以上待機
                        try {
                                Thread.sleep(15000);
                        }
                        catch (Exception e) {
                        }
                        //キャッシュから"a"というKeyで登録されたCachableを取り出す
                        a = Cache.getInstance().get("a");
        %>
        (3) キャッシュされたaの期限切れ後の取得結果=<%= a%><br>
    </body>
</html>

動作確認

cache_test.jspの実行結果を見てみましょう。

(1) aの初回取得結果=null
(2) aをキャッシュ直後の再取得結果=CachableImpl@2f11bfe4
(3) キャッシュされたaの期限切れ後の取得結果=null

(1)はキャッシュされていないため、nullが返ってきています。
(2)はキャッシュ登録直後の再取得のため、インスタンスを取得できています。
(3)はキャッシュ期限切れ後の再取得のため、nullが返ってきています。
理想通りの挙動でした。

上記の実装はあくまで一例ですが、
メモリ空間を圧迫しない範囲で積極的にキャッシュし、
DB等の負担を少しでも減らせると、トータルでの運用が楽になるケースが多いように思います。
ここまで長くなってしまいましたが、お付き合いいただきましてありがとうございました。

環境

  • 開発
    • Windows 10 Pro
    • JDK 1.8.0_74
    • NetBeans IDE 8.1
  • 動作検証
    • CentOS Linux release 7.2
    • JDK 1.8.0_74

Webツールも公開しています。
Web便利ツール@ツールタロウ

スポンサーリンク