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.war
を Tomcat へ配置して 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
実行結果
curl で Servlet へアクセスすると 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' }