Jedis を Tomcat で JNDI リソース化

Jedis ※ を Tomcat 上で JNDI リソースとして扱えるようにしてみます。

 ※ 厳密には redis.clients.jedis.JedisPool を JNDI リソース化します

サンプルソースhttp://github.com/fits/try_samples/tree/master/blog/20150924/

はじめに

Tomcat では標準的に下記を JNDI リソース化できるようになっていますが、JedisPool クラスは JavaBean としては扱えず、そのまま使えそうなものはありません。

そこで今回は、カスタムリソースファクトリを作成して対応する事にします。

リソースの種類 使用するリソースファクトリ
JavaBean org.apache.naming.factory.BeanFactory
UserDatabase org.apache.catalina.users.MemoryUserDatabaseFactoryなど
JavaMail Session org.apache.naming.factory.ResourceFactory (org.apache.naming.factory.MailSessionFactory)
JDBC DataSource org.apache.naming.factory.ResourceFactory (org.apache.tomcat.dbcp.dbcp2.BasicDataSourceFactory)

ちなみに、他にも以下のようなリソースファクトリクラスが用意されているようです。

  • org.apache.naming.factory.DataSourceLinkFactory ※
  • org.apache.naming.factory.EjbFactory
  • org.apache.naming.factory.SendMailFactory
 ※ ResourceLink 要素で使用するリソースファクトリクラス
    グローバルに定義した DataSource を複数のコンテキストで共通利用するために使用する模様

JedisPool 用のリソースファクトリクラス1

まずは、ホスト名とポート番号を設定するだけの単純な JedisPool のリソースファクトリクラスを作成してみます。

Tomcat 用のカスタムリソースファクトリは、以下の点に注意して javax.naming.spi.ObjectFactory を実装するだけです。

  • getObjectInstance メソッドでリソースオブジェクトを作成して返す
  • getObjectInstance の第一引数は javax.naming.Referenceインスタンスとなる (実体は org.apache.naming.ResourceRef
  • 設定ファイルの Resource 要素で設定したプロパティは javax.naming.Reference から javax.naming.RefAddr として取得可能
  • リソースを破棄(close)するメソッドは設定ファイルの closeMethod 属性で指定可能

RefAddr から getType でプロパティ名、getContent でプロパティ値(基本的に String)を取得できます。

また、singleton の設定 (Resource 要素で設定可能) によって getObjectInstance の呼ばれ方が以下のように変化します。

singleton 設定値 getObjectInstance の呼び出し
true (デフォルト) 初回 lookup 時に 1回だけ
false lookup 実行毎に毎回

今回は、getObjectInstance の第一引数 obj が javax.naming.Referenceインスタンスだった場合にのみ JedisPoolインスタンス化して返すように実装しました。

カスタムリソースファクトリ (src/main/java/sample/SimpleJedisPoolFactory.java
package sample;

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.RefAddr;
import javax.naming.Reference;
import javax.naming.spi.ObjectFactory;
import java.util.Enumeration;
import java.util.Hashtable;

import redis.clients.jedis.JedisPool;
import redis.clients.jedis.Protocol;

public class SimpleJedisPoolFactory implements ObjectFactory {
    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx,
                                    Hashtable<?, ?> environment) throws Exception {

        return (obj instanceof Reference)? createPool((Reference) obj): null;
    }
    // JedisPool の作成
    private JedisPool createPool(Reference ref) throws Exception {
        String host = Protocol.DEFAULT_HOST;
        int port = Protocol.DEFAULT_PORT;

        for (Enumeration<RefAddr> em = ref.getAll(); em.hasMoreElements();) {
            RefAddr ra = em.nextElement();

            String name = ra.getType();
            String value = (String)ra.getContent();

            switch (name) {
                case "host":
                    host = value;
                    break;
                case "port":
                    port = Integer.parseInt(value);
                    break;
            }
        }

        return new JedisPool(host, port);
    }
}

動作確認

SimpleJedisPoolFactory の動作確認のために Servlet と設定ファイルを用意しました。 (JNDI 名は redis/Pool としています)

Servlet クラス (src/main/java/sample/SampleServlet.java
package sample;

import redis.clients.jedis.Jedis;
import redis.clients.jedis.JedisPool;

import javax.naming.Context;
import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

@WebServlet(urlPatterns = "/app")
public class SampleServlet extends HttpServlet {
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse res)
            throws ServletException, IOException {

        // Redis から該当データを取得
        String data = getFromRedis(req.getParameter("key"));

        res.getWriter().println(data);
    }

    private String getFromRedis(String key) {
        // JedisPool から Jedis 取得
        try (Jedis jedis = getPool().getResource()) {
            // Redis からデータ取得
            return jedis.get(key);
        }
    }

    private JedisPool getPool() {
        try {
            Context ctx = new InitialContext();
            // JNDI で JedisPool を取得
            return (JedisPool) ctx.lookup("java:comp/env/redis/Pool");
        } catch (NamingException e) {
            throw new RuntimeException(e);
        }
    }
}

確認のため Redis の port 番号をデフォルト値(6379)から 6380 へ変えています。

JedisPool を適切に close するよう closeMethod 属性を使いました。

設定ファイル (src/main/webapp/META-INF/context.xml
<Context>
    <Resource name="redis/Pool" auth="Container"
              factory="sample.SimpleJedisPoolFactory"
              closeMethod="close"
              port="6380"
            />
</Context>

Gradle で war ファイルを作成します。

ビルド定義 (build.gradle)
apply plugin: 'war'

repositories {
    jcenter()
}

dependencies {
    compile 'redis.clients:jedis:2.7.3'

    providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
}

ビルド

> gradle build

Tomcat 起動

ビルド結果の build/libs/sample.warTomcat へ配置して Tomcat を起動します。 一応、Tomcat 7.0.64 と 8.0.26 上で動作する事を確認しています。

> startup

Redis 起動

ポート番号を 6380 へ設定変更した redis.conf を使って Redis を起動します。

> redis-server redis.conf

                _._
           _.-``__ ''-._
      _.-``    `.  `_.  ''-._           Redis 2.8.19 (00000000/0) 64 bit
  .-`` .-```.  ```\/    _.,_ ''-._
 (    '      ,       .-`  | `,    )     Running in stand alone mode
 |`-._`-...-` __...-.``-._|'` _.-'|     Port: 6380
・・・

Redis へデータ登録 (redis-cli 使用)

Redis へデータを登録しておきます。

> redis-cli -p 6380

127.0.0.1:6380> set sample 12345
OK

実行結果

curlServlet へアクセスすると Redis から正常にデータを取得する事を確認できました。

> curl http://localhost:8080/sample/app?key=sample

12345

JedisPool 用のリソースファクトリクラス2

次に JedisPoolConfig を使ってプーリングの設定を変更できるようにしてみます。

今回は、Commons BeanUtils を使って、JedisPool 生成用の自作クラス JedisPoolBuilder へ設定ファイルの設定内容を反映するようにしました。

なお、Reference の getAll() には factory, auth, scope, singleton 等のプロパティをデフォルトで含んでいる点にご注意ください。

BeanUtils.setProperty は存在しないプロパティを指定しても無視するだけですので、今回の用途では factory 等のデフォルトプロパティを特に気にする必要は無かったのですが、以下では一応スキップするようにしました。

カスタムリソースファクトリ (src/main/java/sample/JedisPoolFactory.java
package sample;

import javax.naming.Context;
import javax.naming.Name;
import javax.naming.RefAddr;
import javax.naming.Reference;
import javax.naming.spi.ObjectFactory;
import java.util.Arrays;
import java.util.Enumeration;
import java.util.Hashtable;
import java.util.List;

import org.apache.commons.beanutils.BeanUtils;
import redis.clients.jedis.JedisPool;
import redis.clients.jedis.JedisPoolConfig;
import redis.clients.jedis.Protocol;

public class JedisPoolFactory implements ObjectFactory {
    // 無視するプロパティのリスト
    private final List<String> ignoreProperties =
            Arrays.asList("factory", "auth", "scope", "singleton");

    @Override
    public Object getObjectInstance(Object obj, Name name, Context nameCtx,
                                    Hashtable<?, ?> environment) throws Exception {

        return (obj instanceof Reference)? createPool((Reference) obj): null;
    }

    private JedisPool createPool(Reference ref) throws Exception {
        JedisPoolBuilder builder = new JedisPoolBuilder();

        for (Enumeration<RefAddr> em = ref.getAll(); em.hasMoreElements();) {
            RefAddr ra = em.nextElement();

            String name = ra.getType();

            if (!ignoreProperties.contains(name)) {
                // プロパティの設定
                BeanUtils.setProperty(builder, name, ra.getContent());
            }
        }

        return builder.build();
    }
    // JedisPool の生成クラス
    public class JedisPoolBuilder {
        private JedisPoolConfig poolConfig = new JedisPoolConfig();

        private String host = Protocol.DEFAULT_HOST;
        private int port = Protocol.DEFAULT_PORT;
        private int timeout = Protocol.DEFAULT_TIMEOUT;

        public void setHost(String host) {
            this.host = host;
        }

        public void setPort(int port) {
            this.port = port;
        }

        public void setTimeout(int timeout) {
            this.timeout = timeout;
        }

        public JedisPoolConfig getPoolConfig() {
            return poolConfig;
        }

        public JedisPool build() {
            return new JedisPool(poolConfig, host, port, timeout);
        }
    }
}

プーリングの設定例は以下の通りです。

設定ファイル例 (src/main/webapp/META-INF/context.xml
<Context>
    <Resource name="redis/Pool" auth="Container"
              factory="sample.JedisPoolFactory"
              closeMethod="close"
              port="6380"
              timeout="500"
              poolConfig.maxTotal="100"
              poolConfig.maxIdle="100"
            />
</Context>

ビルド定義は以下の通りです。

ビルド定義 (build.gradle)
apply plugin: 'war'

repositories {
    jcenter()
}

dependencies {
    compile 'redis.clients:jedis:2.7.3'
    compile 'commons-beanutils:commons-beanutils:1.9.2'

    providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
}