FunctionalJava の DB モナド?

FunctionalJava における fj.control.db.DB クラスの使い方を調べてみました。

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

はじめに

処理内容を見る限り fj.control.db.DB は Reader モナドをベースにしていますが、以下の点が異なります。

  • 適用する状態が java.sql.Connection に固定されている

また、実行処理の run メソッドが SQLException を throws するようになっています。

使い方は、概ね以下のようになると思います。

fj.control.db.DB 使用例
// 検索の場合は DbState.reader メソッドを使用
DbState dbWriter = DbState.writer("<DB接続URL>");

// DB 処理を構築
DB<?> q = DB.db(con -> ・・・)・・・;

// 実行
dbWriter.run(q);

DbState の run メソッド内で java.sql.Connection を取得し、fj.control.db.DB の run メソッドを実行します。

ただし、以下のような注意点があり、実際のところ既存のメソッドだけで処理を組み立てるのは難しいように思います。

  • (a) DB.db(F<java.sql.Connection,A> f) メソッドでは SQLException を throw する処理から DB オブジェクトを作成できない
  • (b) DbStaterun メソッドは SQLException を throw した場合のみロールバックする (他の例外ではロールバックしない)
  • (c) リソース(PreparedStatement や ResultSet 等)の解放 close は基本的に自前で実施する必要あり ※
※ DbState の run メソッドを使えば、Connection の close はしてくれます

例えば、prepareStatement メソッドは SQLException を throw するため、DB.db(con -> con.prepareStatement("select * from ・・・")) のようにするとコンパイルエラーになります。

サンプル1

(a) と (c) に対処するためのヘルパーメソッド(以下)を用意して、サンプルコードを書いてみました。

  • DB<A> db(Try1<Connection, A, SQLException> func) メソッドを定義
  • 更新・検索処理を実施する DB オブジェクトの生成メソッド commandquery をそれぞれ定義 (try-with-resources 文で PreparedStatement 等を close)

動作確認のため、以下のような更新処理を行う DB オブジェクトを組み立てました。

  • (1) product テーブルへ insert
  • (2) (1) で自動採番された id を取得 (OptionalInt へ設定)
  • (3) OptionalInt から id の値(数値)を取り出し
  • (4) (3) の値を使って product_variation テーブルへ 2レコードを insert

なお、(b) によって SQLException 以外ではロールバックしないため、仮に (3) で NoSuchElementException が throw された場合にロールバックされません。

src/main/java/sample/DbSample1.java
package sample;

import fj.Unit;
import fj.control.db.DB;
import fj.control.db.DbState;
import fj.function.Try1;

import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.OptionalInt;

public class DbSample1 {
    public static void main(String... args) throws Exception {
        DbState dbWriter = DbState.writer(args[0]);

        final String insertVariationSql = "insert into product_variation (product_id, color, size) " +
                "values (?, ?, ?)";
        // 更新処理の組み立て
        DB<Integer> q1 = command("insert into product (name, price) values (?, ?)", "sample1", 1500) // (1)
                .bind(v -> query("select last_insert_id()", DbSample1::scalarValue)) // (2)
                .map(OptionalInt::getAsInt) // (3)
                .bind(id ->
                        command(insertVariationSql, id, "Black", "L")
                                .bind(_v -> command(insertVariationSql, id, "White", "S")));

        // 更新処理の実行
        System.out.println(dbWriter.run(q1));

        DbState dbReader = DbState.reader(args[0]);

        final String selectSql = "select name, color, size from product p " +
                "join product_variation v on v.product_id = p.id";
        // 検索処理の組み立て
        DB<Unit> q2 = query(selectSql, rs -> {
            while (rs.next()) {
                System.out.println(rs.getString("name") + ", " + 
                        rs.getString("color") + ", " + rs.getString("size"));
            }
            return Unit.unit();
        });

        // 検索処理の実行
        dbReader.run(q2);
    }

    private static OptionalInt scalarValue(ResultSet rs) throws SQLException {
        return rs.next() ? OptionalInt.of(rs.getInt(1)) : OptionalInt.empty();
    }

    // DB.db の代用メソッド
    private static <A> DB<A> db(Try1<Connection, A, SQLException> func) {
        return new DB<A>() {
            public A run(Connection con) throws SQLException {
                return func.f(con);
            }
        };
    }

    // 更新用
    private static DB<Integer> command(String sql, Object... params) {
        return db(con -> {
            try (PreparedStatement ps = createStatement(con, sql, params)) {
                return ps.executeUpdate();
            }
        });
    }

    // 検索用
    private static <T> DB<T> query(String sql, Try1<ResultSet, T, SQLException> handler, Object... params) {
        return db(con -> {
            try (
                PreparedStatement ps = createStatement(con, sql, params);
                ResultSet rs = ps.executeQuery()
            ) {
                return handler.f(rs);
            }
        });
    }

    private static PreparedStatement createStatement(Connection con, String sql, Object... params)
            throws SQLException {

        PreparedStatement ps = con.prepareStatement(sql);

        for (int i = 0; i < params.length; i++) {
            ps.setObject(i + 1, params[i]);
        }

        return ps;
    }
}

実行

Gradle で実行しました。

build.gradle
apply plugin: 'application'

mainClassName = 'sample.DbSample1'

repositories {
    jcenter()
}

dependencies {
    compile 'org.functionaljava:functionaljava:4.4'
    runtime 'mysql:mysql-connector-java:5.1.36'
}

run {
    if (project.hasProperty('args')) {
        args project.args
    }
}
実行結果
> gradle run -Pargs="jdbc:mysql://localhost:3306/sample1?user=root"

・・・
1
sample1, Black, L
sample1, White, S

サンプル2

(b) への対策として、SQLException 以外を throw してもロールバックする run メソッドを用意しました。

src/main/java/sample/DbSample2.java
package sample;

・・・
import fj.function.TryEffect1;
・・・

public class DbSample2 {
    public static void main(String... args) throws Exception {
        Connector connector = DbState.driverManager(args[0]);

        final String insertVariationSql = "insert into product_variation (product_id, color, size) " +
                "values (?, ?, ?)";

        DB<Integer> q1 = command("insert into product (name, price) values (?, ?)", "sample2", 2000)
                .bind(v -> query("select last_insert_id()", DbSample2::scalarValue))
                .map(OptionalInt::getAsInt)
                .bind(id ->
                        command(insertVariationSql, id, "Green", "L")
                                .bind(_v -> command(insertVariationSql, id, "Blue", "S")));

        // 更新処理の実行
        System.out.println(run(connector, q1));

        final String selectSql = "select name, color, size from product p " +
                "join product_variation v on v.product_id = p.id";

        DB<Unit> q2 = query(selectSql, rs -> {
            while (rs.next()) {
                System.out.println(rs.getString("name") + ", " + 
                        rs.getString("color") + ", " + rs.getString("size"));
            }
            return Unit.unit();
        });

        // 検索処理の実行
        runReadOnly(connector, q2);
    }

    // 検索用の実行処理(常にロールバック)
    private static <A> A runReadOnly(Connector connector, DB<A> dba) throws SQLException {
        return run(connector, dba, Connection::rollback);
    }

    // 更新用の実行処理
    private static <A> A run(Connector connector, DB<A> dba) throws SQLException {
        return run(connector, dba, Connection::commit);
    }

    private static <A> A run(Connector connector, DB<A> dba, TryEffect1<Connection, SQLException> trans)
            throws SQLException {
        try (Connection con = connector.connect()) {
            con.setAutoCommit(false);

            try {
                A result = dba.run(con);

                trans.f(con);

                return result;

            } catch (Throwable e) {
                con.rollback();
                throw e;
            }
        }
    }
    ・・・
}

実行

build.gradle
・・・
mainClassName = 'sample.DbSample2'
・・・
実行結果
> gradle run -Pargs="jdbc:mysql://localhost:3306/sample2?user=root"

・・・
1
sample2, Green, L
sample2, Blue, S

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

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'
}

Windows上で Rust 1.3 を使用

以前、「Windows 上で Rust を使用」 では Rust 0.9 を使いましたが、今回は Rust 1.3 で試してみました。

環境構築

https://www.rust-lang.org/ から Windows installer をダウンロード (例 https://static.rust-lang.org/dist/rust-1.3.0-x86_64-pc-windows-gnu.msi) し、インストールするだけとなっています。

gcc 等もインストーラーに含まれているので (例 bin\rustlib\x86_64-pc-windows-gnu\bin\gcc.exe)、以前のように MinGW を別途インストールしたりする必要はありません。

ビルドと実行

それではサンプルソースをビルドして実行してみます。

以前の Rust 0.9 用ソースをそのままでは使えなかったので書き換えました。

sample.rs
use std::fmt;

fn main() {
    let d1 = Data { name: "data", value: 10 };
    let d2 = Data { name: "data", value: 10 };
    let d3 = Data { name: "data", value:  0 };
    let d4 = Data { name: "etc",  value:  5 };

    println!("d1 == d2 : {}", d1 == d2);
    println!("d1 == d2 : {}", d1.eq(&d2));
    println!("d1 == d3 : {}", d1 == d3);

    println!("-----");

    println!("{:?}", d1);
    println!("{}", d1);

    println!("-----");

    println!("times = {}", d1.times(3));

    println!("-----");

    d1.print_value();
    d3.print_value();

    println!("-----");

    let res = calc(&[d1, d2, d3, d4]);
    println!("calc = {}", res);
}

fn calc(list: &[Data]) -> i32 {
    list.iter().fold(1, |acc, v| acc * match v {
        // name = "data" で value の値が 0 より大きい場合
        &Data {name: "data", value: b} if b > 0 => b,
        // それ以外
        _ => 1
    })
}

// PartialEq と Debug トレイトを自動導出
#[derive(PartialEq, Debug)]
struct Data {
    name: &'static str,
    value: i32
}

// メソッドの定義
impl Data {
    fn print_value(&self) {
        match self.value {
            0 => println!("value: zero"),
            a @ _ => println!("value: {}", a)
        }
    }
}

// トレイトの定義
trait Sample {
    fn get_value(&self) -> i32;

    fn times(&self, n: i32) -> i32 {
        self.get_value() * n
    }
}

// トレイトの実装
impl Sample for Data {
    fn get_value(&self) -> i32 {
        self.value
    }
}

// {} で出力するため Display トレイトを実装
impl fmt::Display for Data {
    fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
        write!(f, "display (Data name={}, value={})", self.name, self.value)
    }
}

以前との主な変更点は以下の通りです ※。

  • deriving を derive へ変更
  • int を i32 へ変更
  • ~str を &'static str へ変更
  • Eq を PartialEq へ変更 (trait Eq: PartialEq<Self> となっている)
  • {} で出力するため Display トレイトを実装
  • {:?} で出力するため Debug トレイトを自動導出
 ※ Rust 1.0 で色々と変更されたようです

ビルド

rustc コマンドを使ってビルドを実施します。※
ビルドに成功すると .exe ファイルが作成されます。

 ※ インストール時に "Add to PATH" を実施しなかった場合は
    環境変数 PATH へ Rust の bin ディレクトリを追加しておく必要があります
ビルド例
> rustc sample.rs

実行

ビルドで生成された .exe ファイルは普通に実行できます。

実行例
> sample.exe
d1 == d2 : true
d1 == d2 : true
d1 == d3 : false
-----
Data { name: "data", value: 10 }
display (Data name=data, value=10)
-----
times = 30
-----
value: 10
value: zero
-----
calc = 100

今回使用したサンプルソースhttp://github.com/fits/try_samples/tree/master/blog/20150920/

nginx でリバースプロキシする際は HTTP レスポンスヘッダーのサイズに注意

nginx で Web サーバーをリバースプロキシする際は以下に注意が必要です。 (nginx 1.8.0 と 1.9.4 で確認)

  • リバースプロキシ先からの HTTP レスポンスヘッダーのサイズが proxy_buffer_size の設定値を超えると 502 Bad Gateway エラーとなる

その場合のエラーログは次の通りです。

エラーログ例
2015/08/24 00:34:03 [error] 3672#4680: *6 upstream sent too big header while reading response header from upstream, ・・・

このエラーが発生した場合は、proxy_buffer_size の値をレスポンスヘッダーのサイズより大きくする必要があります。

proxy_buffer_size のデフォルト値は 4KB か 8KB に設定されているようですので、通常の Web アプリケーションでお目にかかる事はないかもしれません。

また、proxy_buffering の値 (on / off) に関わらず発生します。

nginx ソース確認

実際にどうなっているのか nginx 1.9.4 のソースを見てみました。

src/http/ngx_http_upstream.c の ngx_http_upstream_process_header 関数で upstream sent too big header のエラーログを出力しています。

受信したレスポンスヘッダーがバッファに収まりきらなかった ※ 場合に upstream sent too big header のログを出力しエラーとしているようです。

 ※ レスポンスヘッダーを受信し終わっていないのにバッファが終端に到達
src/http/ngx_http_upstream.c の該当ソース
static void
ngx_http_upstream_process_header(ngx_http_request_t *r, ngx_http_upstream_t *u)
{
    ・・・
    if (u->buffer.start == NULL) {
        u->buffer.start = ngx_palloc(r->pool, u->conf->buffer_size);
        ・・・

        u->buffer.pos = u->buffer.start;
        u->buffer.last = u->buffer.start;
        u->buffer.end = u->buffer.start + u->conf->buffer_size;
        ・・・
    }

    for ( ;; ) {

        n = c->recv(c, u->buffer.last, u->buffer.end - u->buffer.last);

        ・・・

        rc = u->process_header(r);

        if (rc == NGX_AGAIN) {

            if (u->buffer.last == u->buffer.end) {
                ngx_log_error(NGX_LOG_ERR, c->log, 0,
                              "upstream sent too big header");

                ngx_http_upstream_next(r, u,
                                       NGX_HTTP_UPSTREAM_FT_INVALID_HEADER);
                return;
            }

            continue;
        }

        break;
    }
    ・・・
}

また、上記で使用しているバッファサイズ u->conf->buffer_sizeproxy_buffer_size の設定値を使用していると思われます。

src/http/modules/ngx_http_proxy_module.c の該当ソース
・・・
{ ngx_string("proxy_buffer_size"),
  NGX_HTTP_MAIN_CONF|NGX_HTTP_SRV_CONF|NGX_HTTP_LOC_CONF|NGX_CONF_TAKE1,
  ngx_conf_set_size_slot,
  NGX_HTTP_LOC_CONF_OFFSET,
  offsetof(ngx_http_proxy_loc_conf_t, upstream.buffer_size),
  NULL },
・・・

検証

最後に、簡単なサンプル Web アプリケーションを作って検証してみました。

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

nginx 設定ファイル

proxy_buffer_size を 1KB に減らし、http://127.0.0.1:8081 へリバースプロキシを行う設定 (以下) で nginx を起動しておきます。

nginx.conf
events {
}

http {
    upstream ap {
        server 127.0.0.1:8081;
    }

    server {
        listen 8080;

        location / {
            proxy_pass http://ap;
            proxy_buffer_size 1k;
        }
    }
}

サンプル Web サーバーアプリケーション

次のような単純な Web サーバー (Undertow を使用) を実行するスクリプトを用意しました。

  • レスポンスヘッダー TEST へ設定する t の文字数を実行時引数で指定 (例えば 't' * 5 の結果は ttttt
  • レスポンスボディに sample という文字列を返す
server_sample.groovy
@Grab('io.undertow:undertow-core:1.3.0.Beta9')
import io.undertow.Undertow
import io.undertow.server.HttpHandler
import io.undertow.util.Headers
import io.undertow.util.HttpString

// t の文字数
def size = args[0] as int

def server = Undertow.builder().addListener(8081, 'localhost').setHandler( { ex ->
    // レスポンスヘッダー
    ex.responseHeaders
        .put(Headers.CONTENT_TYPE, 'text/plain')
        .put(new HttpString('TEST'), 't' * size)

    // レスポンスボディ
    ex.responseSender.send('sample')

} as HttpHandler ).build()

server.start()

上記スクリプトが返すレスポンスヘッダーは以下のようになります。

レスポンスヘッダー例
$ curl -I http://localhost:8081/

HTTP/1.1 200 OK
TEST: ttttttttttttttttttttttt・・・
Connection: keep-alive
Content-Type: text/plain
Content-Length: 6
Date: Mon, 24 Aug 2015 06:04:08 GMT

動作検証1

まずは、t の数 800 で server_sample.groovy を実行してみます。

レスポンスヘッダーが 1KB を超えないはずなので正常に結果が返ってくるはずです。

> groovy server_sample.groovy 800
・・・

nginx へアクセスしてみると問題なく sample という文字列が返ってきました。

$ curl http://localhost:8080/

sample

動作検証2

次は 1100 で実行してみます。

1KB を超えるので 502 エラーとなるはずです

> groovy server_sample.groovy 1100
・・・

nginx へアクセスしてみると想定通り 502 Bad Gateway が返ってきました。

$ curl http://localhost:8080/

<html>
<head><title>502 Bad Gateway</title></head>
<body bgcolor="white">
<center><h1>502 Bad Gateway</h1></center>
<hr><center>nginx/1.9.4</center>
</body>
</html>

Spring Data Redis におけるデフォルト設定の注意点

Spring Data Redis のデフォルト設定に関して、個人的に気になった点を挙げておきます。

  • (1) キーと値に JdkSerializationRedisSerializer を適用
  • (2) トランザクションサポートが無効化 (enableTransactionSupport = false)

今回使用したモジュールは以下。

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

はじめに

今回作成したサンプルの Gradle 用ビルド定義です。

spring-boot-gradle-plugin を使わず、gradle run で実行するようにしました。

build.gradle
apply plugin: 'application'

def enc = 'UTF-8'
tasks.withType(AbstractCompile)*.options*.encoding = enc

mainClassName = 'sample.App'

repositories {
    jcenter()
}

dependencies {
    compile 'org.springframework.boot:spring-boot-starter-redis:1.2.5.RELEASE'
}

クラス構成は以下の通りです。

実行クラス src/main/java/sample/App.java
package sample;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import sample.repository.SampleRepository;

@ComponentScan
@EnableAutoConfiguration
public class App implements CommandLineRunner {
    @Autowired
    private SampleRepository sampleRepository;

    @Override
    public void run(String... args) {
        ・・・
    }

    public static void main(String... args) {
        SpringApplication.run(App.class, args);
    }
}
設定クラス src/main/java/sample/config/AppConfig.java
package sample.config;

import org.springframework.context.annotation.Bean;
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.GenericToStringSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class AppConfig {
    @Bean
    public JedisConnectionFactory jedisConnectionFactory() {
        return new JedisConnectionFactory();
    }

    @Bean
    public RedisTemplate<String, Integer> redisTemplate() {
        RedisTemplate<String, Integer> template = new RedisTemplate<>();
        template.setConnectionFactory(jedisConnectionFactory());
        ・・・
        return template;
    }
}
Redis 用リポジトリクラス src/main/java/sample/repository/SampleRepository.java
package sample.repository;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.dao.DataAccessException;
import org.springframework.data.redis.core.*;
import org.springframework.stereotype.Repository;

import java.util.function.IntUnaryOperator;

@Repository
public class SampleRepository {
    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;

    public Integer load(String key) {
        return redisTemplate.opsForValue().get(key);
    }

    ・・・
}

(1) キーと値に JdkSerializationRedisSerializer を適用

デフォルト設定では、キーにも値にも JdkSerializationRedisSerializer を適用するため、Javaシリアライズしたキーと値を Redis へ保存します。

例えば、redisTemplate.opsForValue().set("a1", 10)redisTemplate.boundValueOps("a1").set(10) でも可) のように "a1" というキーに 10 という値をセットした後、Redis で実際の値を確認すると以下のようになりました。

redis-cli による確認結果1
127.0.0.1:6379> keys *
1) "\xac\xed\x00\x05t\x00\x02a1"

127.0.0.1:6379> get "\xac\xed\x00\x05t\x00\x02a1"
"\xac\xed\x00\x05sr\x00\x11java.lang.Integer\x12\xe2\xa0\xa4\xf7\x81\x878\x02\x00\x01I\x00\x05valuexr\x00\x10java.lang.Number\x86\xac\x95\x1d\x0b\x94\xe0\x8b\x02\x00\x00xp\x00\x00\x00\n"

"a1" が "\xac\xed\x00\x05t\x00\x02a1" に、 10 が "\xac\xed\・・・valuexr\x00\x10java.lang.Number・・・" になって保存されています。

文字列や数値をシリアライズせずに Redis へ保存したい場合は、RedisTemplate の個々の serializer を変更します。

今回は以下のように設定しました。

適用する serializer 設定先プロパティ
String StringRedisSerializer keySerializer
Integer GenericToStringSerializer valueSerializer

他にも、ハッシュを使う場合は hashKeySerializer や hashValueSerializer を変更し、同じ serializer を全てに適用する場合は defaultSerializer を変更します。

設定クラス src/main/java/sample/config/AppConfig.java
・・・
@Configuration
public class AppConfig {
    ・・・
    @Bean
    public RedisTemplate<String, Integer> redisTemplate() {
        RedisTemplate<String, Integer> template = new RedisTemplate<>();
        template.setConnectionFactory(jedisConnectionFactory());

        // キーの serializer を変更
        template.setKeySerializer(new StringRedisSerializer());
        // 値の serializer を変更
        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));

        return template;
    }
}

serializer 変更後に redisTemplate.opsForValue().set("a1", 10) で保存した内容は以下のようになりました。

redis-cli による確認結果2 (selializer 変更後)
127.0.0.1:6379> keys *
1) "a1"

127.0.0.1:6379> get a1
"10"

(2) トランザクションサポートが無効化

デフォルトで RedisTemplateenableTransactionSupportfalse となっています。

この場合、以下のように RedisTemplatemultiexec メソッドを使用しても Redis のトランザクションは適用されず、exec の実行時に JedisDataException: ERR EXEC without MULTI エラーが発生します。

// 楽観ロックの適用
redisTemplate.watch(key);
・・・
// Redis トランザクションの開始
redisTemplate.multi();
・・・
// Redis トランザクションの実施。enableTransactionSupport = false の場合はエラー
redisTemplate.exec();

エラーが発生する原因は、multiexec を異なる Redis 接続に対して実施するためです。 (つまり、セッションが異なっている)

もう少し詳しく説明すると、multiexec 等で個別に RedisConnectionFactory から RedisConnection を取得し処理を実施します。

Redis トランザクションを使用するには、次の 3通りが考えられます。

  • (a) enableTransactionSupport を true へ変更 (トランザクションサポートを有効化)
  • (b) SessionCallback を execute
  • (c) 自前で bindConnection・unbindConnection を実施

(b) と (c) は enableTransactionSupport = false の設定でも適用できる方法です。 (enableTransactionSupport = true でも問題ありません)

(a) enableTransactionSupport を true へ変更

enableTransactionSupport を true へ変更するのが最も簡単だと思います。

また、true へ変更すると @Transactional アノテーションも使えるみたいです。

設定クラス src/main/java/sample/config/AppConfig.java
・・・
@Configuration
public class AppConfig {
    ・・・
    @Bean
    public RedisTemplate<String, Integer> redisTemplate() {
        RedisTemplate<String, Integer> template = new RedisTemplate<>();
        template.setConnectionFactory(jedisConnectionFactory());

        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));
        // トランザクションサポートを有効化
        template.setEnableTransactionSupport(true);

        return template;
    }
}
Redis 用リポジトリクラス src/main/java/sample/repository/SampleRepository.java
・・・
@Repository
public class SampleRepository {
    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;
    ・・・

    public Object updateWithCas1(String key, IntUnaryOperator func) {
        try {
            // 楽観ロック
            redisTemplate.watch(key);

            BoundValueOperations<String, Integer> valueOps = redisTemplate.boundValueOps(key);
            // 現在の値を取得
            Integer value = valueOps.get();

            redisTemplate.multi();

            // 値の更新。enableTransactionSupport = true の場合はキューイングされる
            valueOps.set(func.applyAsInt(value));

            // enableTransactionSupport = true の場合はキューイングした処理の実行、false の場合はエラー
            return redisTemplate.exec();
        } catch (Exception e) {
            // InvalidDataAccessApiUsageException: ERR EXEC without MULTI
            System.out.println(e);
        }

        return null;
    }
    ・・・
}
実行クラス src/main/java/sample/App.java
・・・
@ComponentScan
@EnableAutoConfiguration
public class App implements CommandLineRunner {
    @Autowired
    private SampleRepository sampleRepository;

    @Override
    public void run(String... args) {
        String key = "a1";

        sampleRepository.save(key, 10);
        Object res1 = sampleRepository.updateWithCas1(key, v -> v + 5);
        System.out.println("res1 = " + res1);
        System.out.println(sampleRepository.load(key));
        ・・・
    }
    ・・・
}

実行結果は以下の通りです。

実行結果1
> groovy run

・・・
res1 = []
15
・・・

enableTransactionSupport = false の場合の実行結果は以下の通りです。
トランザクションを使用せず set a1 15 を単独で実行しています。

実行結果2 (enableTransactionSupport = false の場合)
> groovy run

・・・
org.springframework.dao.InvalidDataAccessApiUsageException: ERR EXEC without MULTI; nested exception is redis.clients.jedis.exceptions.JedisDataException: ERR EXEC without MULTI
res1 = null
15
・・・

(b) SessionCallback を execute

enableTransactionSupport = false の場合でも SessionCallbackRedisCallback でも可) を execute すればトランザクションを使用できます。

SessionCallback インターフェースの execute メソッド内へ実装した処理は同一セッション内で実行されます。

ただし、SessionCallbackメソッドへ仮型引数 K・V が付いており、API 的に微妙な気がします。 (SessionCallback<T, K, V> の方がよかったのでは)

org.springframework.data.redis.core.SessionCallback
・・・
public interface SessionCallback<T> {
    <K, V> T execute(RedisOperations<K, V> operations) throws DataAccessException;
}

SessionCallback をラムダで代用したいところですが、仮型引数 K・V のせいで断念しました。

また、operationsRedisOperations<K, V> 型) を強引にキャストしていますが、実体は redisTemplate なので一応は問題無いはずです。

Redis 用リポジトリクラス src/main/java/sample/repository/SampleRepository.java
・・・
@Repository
public class SampleRepository {
    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;
    ・・・

    public Object updateWithCas2(String key, IntUnaryOperator func) {
        // SessionCallback の実行
        return redisTemplate.execute(new SessionCallback<Object>() {
            @Override
            public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
                // 扱い難いのでキャスト
                @SuppressWarnings("unchecked")
                RedisOperations<String, Integer> ops = (RedisOperations<String, Integer>)operations;

                ops.watch(key);

                BoundValueOperations<String, Integer> valueOps = ops.boundValueOps(key);
                // 現在の値を取得。
                // multi 実行前に実施する必要あり。multi 後に実行するとキューイングされて戻り値が null になる
                Integer value = valueOps.get();

                ops.multi();

                valueOps.set(func.applyAsInt(value));

                return ops.exec();
            }
        });
    }
    ・・・
}
実行クラス src/main/java/sample/App.java
・・・
@ComponentScan
@EnableAutoConfiguration
public class App implements CommandLineRunner {
    @Autowired
    private SampleRepository sampleRepository;

    @Override
    public void run(String... args) {
        String key = "a1";
        ・・・
        sampleRepository.save(key, 10);
        Object res2 = sampleRepository.updateWithCas2(key, v -> v + 10);
        System.out.println("res2 = " + res2);
        System.out.println(sampleRepository.load(key));
        ・・・
    }
    ・・・
}
設定クラス src/main/java/sample/config/AppConfig.java
・・・
@Configuration
public class AppConfig {
    ・・・
    @Bean
    public RedisTemplate<String, Integer> redisTemplate() {
        RedisTemplate<String, Integer> template = new RedisTemplate<>();
        template.setConnectionFactory(jedisConnectionFactory());

        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new GenericToStringSerializer<>(Integer.class));

        //template.setEnableTransactionSupport(true);

        return template;
    }
}

実行結果は以下の通りです。

実行結果
> groovy run

・・・
res2 = []
20
・・・

(c) 自前で bindConnection・unbindConnection を実施

RedisConnectionUtilsbindConnectionunbindConnection メソッドを使って、自前でセッションを制御する方法です。

Redis 用リポジトリクラス src/main/java/sample/repository/SampleRepository.java
・・・
@Repository
public class SampleRepository {
    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;
    ・・・

    public Object updateWithCas3(String key, IntUnaryOperator func) {
        // Redis 接続を bind
        RedisConnectionUtils.bindConnection(redisTemplate.getConnectionFactory());
        try {
            redisTemplate.watch(key);

            BoundValueOperations<String, Integer> valueOps = redisTemplate.boundValueOps(key);
            Integer value = valueOps.get();

            redisTemplate.multi();

            valueOps.set(func.applyAsInt(value));

            return redisTemplate.exec();

        } finally {
            // Redis 接続を unbind
            RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());
        }
    }
}
実行クラス src/main/java/sample/App.java
・・・
@ComponentScan
@EnableAutoConfiguration
public class App implements CommandLineRunner {
    @Autowired
    private SampleRepository sampleRepository;

    @Override
    public void run(String... args) {
        String key = "a1";
        ・・・
        sampleRepository.save(key, 10);
        Object res3 = sampleRepository.updateWithCas3(key, v -> v + 15);
        System.out.println("res3 = " + res3);
        System.out.println(sampleRepository.load(key));
    }
}

実行結果は以下の通りです。

実行結果
> groovy run

・・・
res3 = []
25
・・・

備考 (Redis のトランザクション

Redis のトランザクションは、MULTI ~ EXEC 間の処理(コマンド)をキューイングし、EXEC 時に直列化して処理します。(他の処理が途中で入り込む事はない)

ロールバックはできず、途中でコマンドが失敗(文法エラーなど)しても次のコマンドを引き続き実行する点に注意が必要です。

なお、EXEC の代わりに DISCARD を実行するとキューが破棄されトランザクションはキャンセルされます。

WATCH は check-and-set (CAS) による楽観ロックをトランザクションへ適用します。

WATCH したキーが、WATCH 後に更新されていれば MULTI ~ EXEC によるトランザクションを EXEC 時に中止します。

また、EXEC すると WATCH が解除されて UNWATCH になります。

WATCH の動作検証

watch の後に sleep 処理を差し込んで、楽観ロックとトランザクションの動作を確認してみます。

Redis 用リポジトリクラス src/main/java/sample/repository/SampleRepository.java
・・・
@Repository
public class SampleRepository {
    @Autowired
    private RedisTemplate<String, Integer> redisTemplate;
    ・・・

    public Object updateWithCas2(String key, IntUnaryOperator func) {
        return redisTemplate.execute(new SessionCallback<Object>() {
            @Override
            public <K, V> Object execute(RedisOperations<K, V> operations) throws DataAccessException {
                @SuppressWarnings("unchecked")
                RedisOperations<String, Integer> ops = (RedisOperations<String, Integer>)operations;

                ops.watch(key);

                // sleep
                try {
                    Thread.sleep(10000);
                } catch(InterruptedException e) {}

                ・・・
                ops.multi();

                valueOps.set(func.applyAsInt(value));

                return ops.exec();
            }
        });
    }

    public Object updateWithCas3(String key, IntUnaryOperator func) {
        RedisConnectionUtils.bindConnection(redisTemplate.getConnectionFactory());
        try {
            redisTemplate.watch(key);
            ・・・
            redisTemplate.multi();

            // sleep
            try {
                Thread.sleep(10000);
            } catch(InterruptedException e) {}

            valueOps.set(func.applyAsInt(value));

            return redisTemplate.exec();

        } finally {
            RedisConnectionUtils.unbindConnection(redisTemplate.getConnectionFactory());
        }
    }
}

sleep している間に redis-cliset a1 1 (キー "a1" へ 1 という値をセット) を実行してみたところ、以下のような結果となりました。

実行結果 (sleep 中に a1 の値を更新)
> gradle run

・・・
res2 = null
1
res3 = null
1
・・・

redisTemplate.exec() の戻り値が null となり、トランザクションの中止を確認できました。 (値も更新されていません)

Webブラウザ上で Excel ファイルを作成してダウンロード - Excel Builder (.js)

Excel Builder (.js) を使って、Web ブラウザ上で動的に Excel ファイル (.xlsx) を作成し、ダウンロードする方法をご紹介します。

ソースは http://github.com/fits/try_samples/tree/master/blog/20150822/

サンプル作成

まずは、HTML を用意します。

今回は、download というリンク (a タグ) をクリックすると Excel ファイル (.xlsx) をダウンロードするようにしてみます。

Excel Builder (.js)RequireJS に依存しているため、RequireJS を読み込むようにして data-main 属性へ実行する js ファイルを指定します。

index.html
<!DOCTYPE html>
<html>
<head>
    <script data-main="app.js" src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.20/require.js"></script>
</head>
<body>

<a id="dw" href="#">download</a>

</body>
</html>

次に、Excel を生成する処理を実装します。

app.js
require(['excel-builder'], function(EB) {
    var wb = EB.createWorkbook();
    var sh = wb.createWorksheet();
    // セルへ値を設定
    sh.setData([
        ['aaa', 10],
        ['サンプル', 2],
        ['てすと', 3],
        ['計', {value: 'sum(B1:B3)', metadata: {type: 'formula'}}]
    ]);

    wb.addWorksheet(sh);

    var trg = document.getElementById("dw");

    // href 属性へ Excel ファイル内容を設定
    trg.href = 'data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64,' + EB.createFile(wb);
    // ダウンロードファイル名の設定
    trg.download = 'sample.xlsx';
});

API がシンプルなので特に説明の必要は無いと思いますが、 計算式は {value: <計算式>, metadata: {type: 'formula'}} で設定できます。

また、EB.createFile(<ワークブック>) により Excel ファイルの内容を Base64 形式で取得できるので、データ形式 data:application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;base64, を先頭に付けて a タグの href 属性へ設定すれば Excel ファイルをダウンロードできるようになります。

excel-builder.js の配置

最後に、http://excelbuilderjs.com/ の Download からアーカイブファイルをダウンロード、適当なディレクトリへ解凍し、dist ディレクトリ内のファイルのどれか (例えば dist/excel-builder.compiled.min.js) を excel-builder.js という名称で app.js と同じディレクトリへ配置すれば完成です。

ファイル構成
  • index.html
  • app.js
  • excel-builder.js

動作確認

作成した index.htmlChromeFirefox で直接開いて、download リンクをクリックすると sample.xlsx をダウンロードできます。

f:id:fits:20150822020436p:plain

ダウンロードした sample.xlsx を開くと、セルの内容と計算式が正しく機能している事を確認できました。

f:id:fits:20150822020449p:plain