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)
DbState
のrun
メソッドは 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
オブジェクトの生成メソッドcommand
とquery
をそれぞれ定義 (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.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' }
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_size
は proxy_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)
今回使用したモジュールは以下。
- Spring Boot Starter Redis 1.2.5 ( Spring Data Redis 1.4.3 )
サンプルソースは 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) トランザクションサポートが無効化
デフォルトで RedisTemplate
の enableTransactionSupport
が false
となっています。
この場合、以下のように RedisTemplate
の multi
と exec
メソッドを使用しても Redis のトランザクションは適用されず、exec
の実行時に JedisDataException: ERR EXEC without MULTI
エラーが発生します。
// 楽観ロックの適用 redisTemplate.watch(key); ・・・ // Redis トランザクションの開始 redisTemplate.multi(); ・・・ // Redis トランザクションの実施。enableTransactionSupport = false の場合はエラー redisTemplate.exec();
エラーが発生する原因は、multi
と exec
を異なる Redis 接続に対して実施するためです。 (つまり、セッションが異なっている)
もう少し詳しく説明すると、multi
や exec
等で個別に 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 の場合でも SessionCallback
(RedisCallback
でも可) を 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 のせいで断念しました。
また、operations
(RedisOperations<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 を実施
RedisConnectionUtils
の bindConnection
と unbindConnection
メソッドを使って、自前でセッションを制御する方法です。
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-cli で set 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.html
を Chrome や Firefox で直接開いて、download リンクをクリックすると sample.xlsx
をダウンロードできます。
ダウンロードした sample.xlsx
を開くと、セルの内容と計算式が正しく機能している事を確認できました。