Spring Data Redis を Tomcat で JNDI リソース化

前回、Jedis を Tomcat 上で JNDI リソース化しましたが、今回は Spring Data Redis を JNDI リソース化してみます。

実際は org.springframework.data.redis.connection.jedis.JedisConnectionFactory を JNDI リソース化します。

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

はじめに

今回もカスタムリソースファクトリを作成して対応します。

JedisConnectionFactory は JavaBean として扱えるため org.apache.naming.factory.BeanFactory も一応使えるのですが、その場合は JedisShardInfo をどのように設定するかが課題となります。 (JedisShardInfo が未設定だと JedisConnection 取得時にエラーが発生します)

なお、afterPropertiesSet メソッドを実行すれば、JedisShardInfo が未設定(null)の場合に JedisShardInfo をインスタンス化して設定してくれます。

カスタムリソースファクトリの作成

JedisConnectionFactory 専用のカスタムファクトリを作っても良かったのですが、今回はもう少し汎用的に org.springframework.beans.factory.InitializingBean を JNDI リソース化するカスタムファクトリとして作成しました。

基本的な処理内容は 前回 と同じですが、リソースのクラス名を Reference オブジェクトの getClassName メソッドで取得しています。

getClassName の戻り値は context.xml における Resource 要素の type 属性で指定した値です。

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

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

import org.apache.commons.beanutils.BeanUtils;
import org.springframework.beans.factory.InitializingBean;

public class InitializingBeanFactory 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)? createBean((Reference) obj): null;
    }

    private Object createBean(Reference ref) throws Exception {
        // リソースクラスのインスタンス化
        Object bean = loadClass(ref.getClassName()).newInstance();

        setProperties(bean, ref);
        // afterPropertiesSet の実行
        ((InitializingBean)bean).afterPropertiesSet();

        return bean;
    }
    // 各プロパティの設定
    private void setProperties(Object bean, Reference ref)
            throws InvocationTargetException, IllegalAccessException {

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

            String name = ra.getType();

            if (!ignoreProperties.contains(name)) {
                BeanUtils.setProperty(bean, name, ra.getContent());
            }
        }
    }
    // リソースクラスのロード
    private Class<?> loadClass(String className) throws ClassNotFoundException {
        ClassLoader loader = Thread.currentThread().getContextClassLoader();

        return (loader == null)? Class.forName(className): loader.loadClass(className);
    }
}

動作確認

今回は Spring MVC を使って動作確認します。

Tomcat 用のリソース設定は以下の通りです。

type 属性で JNDI リソース化するクラス名 JedisConnectionFactory を指定します。 JNDI 名は redis/ConnectionFactory としました。

設定ファイル (src/main/webapp/META-INF/context.xml
<Context>
    <Resource name="redis/ConnectionFactory" auth="Container"
              type="org.springframework.data.redis.connection.jedis.JedisConnectionFactory"
              factory="sample.resource.InitializingBeanFactory"
              closeMethod="destroy"
              port="6380"
              timeout="500"
              poolConfig.maxTotal="100"
              poolConfig.maxIdle="100"
            />
</Context>

web.xml が不要な WebApplicationInitializer を使う方式にします。

WebApplicationInitializer 実装クラス (src/main/java/sample/config/WebAppInitializer.java
package sample.config;

import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;

public class WebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {
    @Override
    protected Class<?>[] getRootConfigClasses() {
        return null;
    }

    @Override
    protected Class<?>[] getServletConfigClasses() {
        // Web アプリケーション設定クラスの指定
        return new Class<?>[]{WebConfig.class};
    }

    @Override
    protected String[] getServletMappings() {
        return new String[]{"/"};
    }
}

Web アプリケーション用の設定クラスへ JNDI を使った JedisConnectionFactory の取得や RedisTemplate をインスタンス化する処理を実装しました。

設定クラス (src/main/java/sample/config/WebConfig.java
package sample.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.jedis.JedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.jndi.JndiObjectFactoryBean;

import javax.naming.NamingException;

@Configuration
@ComponentScan("sample.controller")
public class WebConfig {
    @Bean
    public JedisConnectionFactory jedisConnectionFactory() throws NamingException {
        JndiObjectFactoryBean factory = new JndiObjectFactoryBean();

        // 取得対象の JNDI 名を設定
        factory.setJndiName("java:comp/env/redis/ConnectionFactory");
        factory.afterPropertiesSet();

        // JedisConnectionFactory 取得
        return (JedisConnectionFactory)factory.getObject();
    }

    @Bean
    public StringRedisSerializer stringRedisSerializer() {
        return new StringRedisSerializer();
    }

    @Bean
    public RedisTemplate<String, String> redisTemplate() throws NamingException {
        RedisTemplate<String, String> template = new RedisTemplate<>();
        // JNDI で取得した JedisConnectionFactory を設定
        template.setConnectionFactory(jedisConnectionFactory());

        template.setKeySerializer(stringRedisSerializer());
        template.setValueSerializer(stringRedisSerializer());

        return template;
    }
}
コントローラークラス (src/main/java/sample/controller/SampleController.java
package sample.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;

@Controller
public class SampleController {
    @Autowired
    private RedisTemplate<String, String> redisTemplate;

    @RequestMapping(value = "/app/{key}", method = RequestMethod.GET)
    @ResponseBody
    public String sample(@PathVariable String key) {
        // Redis から値を取得
        return redisTemplate.boundValueOps(key).get();
    }
}

ビルド

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

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

war.baseName = 'sample'

repositories {
    jcenter()
}

dependencies {
    compile 'redis.clients:jedis:2.7.3'
    compile 'commons-beanutils:commons-beanutils:1.9.2'
    compile 'org.springframework.data:spring-data-redis:1.6.0.RELEASE'
    compile 'org.springframework:spring-web:4.2.1.RELEASE'
    compile 'org.springframework:spring-webmvc:4.2.1.RELEASE'

    providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
}
ビルドの実行
> gradle build

ビルド結果の build/libs/sample.war を Tomcat へ配置し、Tomcat と Redis (ポート番号を 6380 へ変更) を実行しておきます。

Redis へデータ設定

Redis へ確認用のデータを設定します。

> redis-cli -p 6380

127.0.0.1:6380> set a1 sample-data1
OK
実行結果

curl でアクセスすると Redis からデータを取得できました。

> curl http://localhost:8080/sample/app/a1

sample-data1