PlayのCache APIでMemcachedやRedisの代わりにRDBを使う

Play frameworkはキャッシュのためのAPIplay.api.cahe というパッケージで提供しています。play.api.cache はそのバックエンドとして、CaffineやEhcacheが選択でき、またサードバーティのライブラリを使えばMemcached, Redisなどをバックエンドにすることができます。キャッシュと言うと高速化のためのものですが、Playのキャッシュは高速化の他に、単なるデータの一時保存のため、つまり単なるKey-Value Storeとして使われていたりもします。

Caffeineを使うとインメモリのキャッシュが簡単に実現できますが、アプリケーションサーバーのインスタンスが複数ある場合、各サーバーでキャッシュした内容を共有することができません。MemcachedやRedisを使えばインスタンス間でキャッシュした内容を共有できますが、MemcachedやRedisを管理する手間が発生します。

PlayのキャッシュAPIは使いたいけれど、MemcachedやRedisなどの新たなミドルウェアを導入したくない。という状況で、ならばRDBをバックエンドにしてしまおうと作ったのがdbcacheというライブラリです。実際のところ、MemcachedやRedisを使わなくてもRDBで中小規模のサービスでは事足りてしまいます。

https://github.com/tototoshi/dbcache

設定

テーブル追加

MySQLをバックエンドとして使う場合の例です。まずデータベースにcache_entriesテーブルを作成します。

CREATE TABLE `cache_entries` (
  `cache_key` varchar(191) PRIMARY KEY,
  `cache_value` mediumblob NOT NULL,
  `expired_at` datetime,
  `created_at` datetime NOT NULL,
  `updated_at` datetime NOT NULL,
  INDEX (`expired_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4

Play2.7.xの場合

Play 2.7.xを使っている場合は dbcache-mysqldbcache-play をbuild.sbtで追加します。

libraryDependencies ++= Seq(
  "com.github.tototoshi" %% "dbcache-mysql" % "0.3.0",
  "com.github.tototoshi" %% "dbcache-play" % "0.3.0"
)

Play2.6.xの場合

dbcache-play は最近追加したモジュールなので、Play 2.6.xには対応していません。Play2.6.xを使っている場合は dbcache-mysql だけを追加し、以下のコードをアプリケーションコードに追加します。

libraryDependencies ++= Seq(
  "com.github.tototoshi" %% "dbcache-mysql" % "0.2.0"
)
// Create adapter for play.api.cache
class DBCacheApi(myCache: DBCache) extends CacheApi {
  def set(key: String, value: Any, expiration: Duration): Unit =
    myCache.set(key, value, expiration)
  def get[A](key: String)(implicit ct: ClassTag[A]): Option[A] =
    myCache.get[A](key)
  def getOrElse[A: ClassTag](key: String, expiration: Duration)(orElse: => A) =
    myCache.getOrElse(key, expiration)(orElse)
  def remove(key: String) = myCache.remove(key)
}

DIの設定

あとはProviderを作ってDIしてあげるだけです。Scalikejdbcを使っている場合はこんな感じになります。

import java.sql.Connection

import com.github.tototoshi.dbcache.ConnectionFactory
import com.github.tototoshi.dbcache.mysql.MySQLCache
import com.github.tototoshi.dbcache.play.DBCacheApi
import javax.inject.{Inject, Provider}
import play.api.Environment
import play.api.cache.SyncCacheApi
import scalikejdbc.DB

class MySQLCacheApiProvider @Inject()(environment: Environment) extends Provider[SyncCacheApi] {

  private val connectionFactory = new ConnectionFactory {
    override def get(): Connection = {
      DB.autoCommitSession().connection
    }
  }
  override def get(): SyncCacheApi = {
    val mysqlCache = new MySQLCache(connectionFactory, environment.classLoader)
    new DBCacheApi(mysqlCache)
  }
}

EhCacheモジュールは無効にしておきます。

play.modules.disabled += "play.api.cache.ehcache.EhCacheModule"

あとは普通にPlayのキャッシュAPIを使えばデータベースに保存されるようになります。

運用について

期限切れのキャッシュデータがDBに残るのでバッチか何か動かしてたまに掃除してあげてください。

DELETE FROM cache_entries where expired_at < ${n日前}

いつもJavaConvertersでうまくいくとは限らない

ScalaJavaのコレクションを相互に変換するには scala.collection.JavaConverters が便利です。例えばJavaのコレクションに .asScala をつければ、JavaのコレクションをラップしたScalaのコレクションが作られます。ただし、いつも .asScala で済むとは限りません。コレクションのインターフェイスではなくデータ構造が重要な場合は注意が必要です。

次のコード例は java.util.LinkedHashMap.asScala に変換して、 +=, + の操作を行ったものです。 LinkedHashMap は要素が挿入された順番を保持するはずですが、 += を使った場合は順番が保持されるものの、 + を使った場合は順番が保持されていません。

val m = new java.util.LinkedHashMap[String, Int]()
m.put("field1", 1)
m.put("field2", 2)

import scala.collection.JavaConverters._
val m2 = m.asScala
println(m2) // Map(field1 -> 1, field2 -> 2)

m2 += "field3" -> 3
m2 += "field4" -> 4
println(m2) // Map(field1 -> 1, field2 -> 2, field3 -> 3, field4 -> 4)
val m = new java.util.LinkedHashMap[String, Int]()
m.put("field1", 1)
m.put("field2", 2)

import scala.collection.JavaConverters._
val m2 = m.asScala
println(m2) // Map(field1 -> 1, field2 -> 2)

val m3 = m2 + ("field3" -> 3) + ("field4" -> 4)
println(m3) // Map(field1 -> 1, field3 -> 3, field2 -> 2, field4 -> 4)

+= を使った場合、新しい要素は元のLinkedHashMap に挿入されます。一方、 + を使った場合は新たな Mapインスタンスが作られます。その新たな MapインスタンスJavaConverters で定義してあるラッパークラスが作るのですが、それが LinkedHashMap ではなく順番を保持しない HashMap なのです。

これは実際にplay-json 2.6.11に入り込んだバグです。play-jsonJsonのフィールドを Map として保持していますが、その MapScalaのものから java.util.LinkedHashMapインスタンスとして生成して、 JavaConverters でラップするという実装に変えたところ、Jsonのフィールドの順序が狂うという現象が起きました。

https://github.com/playframework/play-json/issues/236

というわけで、気をつけましょう。

JasperReportsでPDFを作成する

JavaでPDFを作成するJasperReportsの使い方について解説します。少し古いライブラリですが企業がバックについてますし、継続的にメンテナンスはされているようです。

JasperReportでPDFを作成するには、

  1. JasperReport Studioでテンプレートを作成する
  2. Javaプログラムでデータを流し込んでPDFを作成する

と言う手順を踏みます。JasperReport StudioではGUIからText、Imageといったパーツを配置しますが、裏ではjrxml形式のファイルを吐き出しています。このjrxmlをコンパイルした.jasperという拡張子のファイルをJavaのプログラムから読み込み、PDFを作成するというのが通常の手順です。

PDF出力のサンプルコードは次のようになります。大抵の場合はこれで事足りると思います。

// 入力パス、出力パス
Path in = ...
Path out = ...

// .jasperを読み込む
//
// .jrxmlを直接読み込むこともできる
//
// JasperReport report = 
//   JasperCompileManager.compileReport(in.toAbsolutePath().toString());
JasperReport report = 
  (JasperReport) JRLoader.loadObject(in.toAbsolutePath().toFile());

// パラメータ、データソースの準備
Map<String, Object> parameters = new HashMap<>();
Collection<Map<String, ?>> source = new ArrayList<>();
...

// パラメータ、データソースの流し込み
// ここでは JRMapCollectionDataSource を利用しているが、他にも JRBeanCollectionDataSource などがある。
// データソースが空の時は空の JRMapCollectionDataSource ではなく JREmptyDataSource を利用する。
// 空の JRMapCollectionDataSourc を利用すると JasperReports 6.7.0 では真っ白なPDFが作成され、エラーも発生しない問題があった。
JasperPrint print = JasperFillManager.fillReport(report, parameters, new JRMapCollectionDataSource(source));
// 書き出し
JasperExportManager.exportReportToPdfFile(print, out.toAbsolutePath().toString());

テンプレートへ流し込むデータには、「パラメータ」と「データソース」の2種類があります。データソースはDetailセクションに流し込む繰り返し構造のデータで、CSVファイルをイメージすると良いです。パラメータは主にDetailセクション以外で使われる埋め込みデータです。

テンプレートの作成

まずは空のテンプレートを見ていきましょう。

次の画像はJasperReports Studioで作った空のテンプレートファイルです。テンプレートはTitle, Page Header, Column Header, Detail1, Column Footer, Page Footer, Summaryのセクションからなっています。Title, Page Headerなどはそのままの意味で、タイトルやヘッダーを表示するところです。Column Header, Detail1, Column Footerがデータを流し込んで、テーブルとして表示する場所です。

f:id:tototoshi:20190302214117p:plain

jrxmlの方も見てみましょう。Title,Page,Header...といった構造がjrxmlにも反映されていることがわかります。

<?xml version="1.0" encoding="UTF-8"?>
<jasperReport
        xmlns="http://jasperreports.sourceforge.net/jasperreports"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd"
        name="Blank_A4"
        pageWidth="595"
        pageHeight="842"
        columnWidth="555"
        leftMargin="20"
        rightMargin="20"
        topMargin="20"
        bottomMargin="20"
        uuid="86d21441-0199-44fa-8d1e-8b7170bc739f">
    <queryString>
        <![CDATA[]]>
    </queryString>
    <background>
        <band splitType="Stretch"/>
    </background>
    <title>
                <!-- bandタグで各セクションの高さを指定できる -->
        <band height="79" splitType="Stretch"/>
    </title>
    <pageHeader>
        <band height="35" splitType="Stretch"/>
    </pageHeader>
    <columnHeader>
        <band height="61" splitType="Stretch"/>
    </columnHeader>
    <detail>
        <band height="125" splitType="Stretch"/>
    </detail>
    <columnFooter>
        <band height="45" splitType="Stretch"/>
    </columnFooter>
    <pageFooter>
        <band height="54" splitType="Stretch"/>
    </pageFooter>
    <summary>
        <band height="42" splitType="Stretch"/>
    </summary>
</jasperReport>

各セクションは必須ではなく、不要であったら削除しても構いません。例えば何枚かに渡るレポートを作りたい時、最初の1枚は表紙になるかもしれません。その時はTitleセクションだけにして大丈夫です。

文字の埋め込み

雰囲気を掴むためにjrxmlを直接編集してみます。

文字の埋め込みは staticText タグを使います。

<title>
    <band height="79" splitType="Stretch">
        <staticText>
            <!-- reportElementタグで表示場所などを指定できる -->
            <reportElement x="0" y="0" width="100" height="50"/>
            <text><![CDATA[Invoice]]></text>
        </staticText>
    </band>
</title>

f:id:tototoshi:20190302214050p:plain

独自のフォントを埋め込む場合には設定の追加が必要です。特に日本語を利用する場合は必須です。今回はIPAフォントを利用します。

まず、クラスパス上に jasperreports_extension.properties というファイルを作成し、フォントの読み込みのための設定と、フォント情報を記述したxmlファイルへのパス(fonts/fonts.xml)を指定します。

net.sf.jasperreports.extension.registry.factory.fonts=net.sf.jasperreports.engine.fonts.SimpleFontExtensionsRegistryFactory
net.sf.jasperreports.extension.simple.font.families.font=fonts/fonts.xml

fonts.xmlは次のように記述します。

<?xml version="1.0" encoding="UTF-8"?>
<fontFamilies>
    <fontFamily name="IPAexゴシック">
        <normal>fonts/ipaexg.ttf</normal>
        <pdfEncoding>Identity-H</pdfEncoding>
        <pdfEmbedded>true</pdfEmbedded>
    </fontFamily>
</fontFamilies>

その上で、jrxmlに textElement タグでフォントの名前を指定します。これで日本語フォントが利用可能になります。

<title>
    <band height="79" splitType="Stretch">
        <staticText>
            <!-- reportElementタグで表示場所などを指定できる -->
            <reportElement x="0" y="0" width="100" height="50"/>
            <textElement>
                <font fontName="IPAexゴシック" size="18"/>
            </textElement>
            <text><![CDATA[請求書]]></text>
        </staticText>
    </band>
</title>

f:id:tototoshi:20190302214028p:plain

なおフォントの使用にあたってはライセンスに注意してください。

パラメータの埋め込み

パラメータの埋め込みは textField タグを使います。textFieldExpression として $P{パラメータ名} を指定します。パラメータの名前はXMLの先頭で parameter タグで宣言しておきます。

<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Jaspersoft Studio version 6.6.0.final using JasperReports Library version 6.6.0  -->
<jasperReport xmlns="http://jasperreports.sourceforge.net/jasperreports" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd" name="Blank_A4_1" pageWidth="595" pageHeight="842" columnWidth="555" leftMargin="20" rightMargin="20" topMargin="20" bottomMargin="20" uuid="f7adae20-db6a-4207-ae6a-75eb4a16e40e">
    <parameter name="title"/>
    <queryString>
        <![CDATA[]]>
    </queryString>
    <background>
        <band splitType="Stretch"/>
    </background>
    <title>
        <band height="79" splitType="Stretch">
            <textField>
                <reportElement x="0" y="0" width="100" height="50"/>
                <textElement verticalAlignment="Middle" textAlignment="Center">
                    <font fontName="IPAexゴシック" size="14"/>
                </textElement>
                <textFieldExpression><![CDATA[$P{title}]]></textFieldExpression>
            </textField>
        </band>
    </title>
    <pageHeader>
        <band height="35" splitType="Stretch"/>
    </pageHeader>
    <columnHeader>
        <band height="61" splitType="Stretch"/>
    </columnHeader>
    <detail>
        <band height="125" splitType="Stretch"/>
    </detail>
    <columnFooter>
        <band height="45" splitType="Stretch"/>
    </columnFooter>
    <pageFooter>
        <band height="54" splitType="Stretch"/>
    </pageFooter>
    <summary>
        <band height="42" splitType="Stretch"/>
    </summary>
</jasperReport>

テーブルの作成

次のようなテーブルを作成してみましょう。

f:id:tototoshi:20190302214004p:plain

columnHeaderに見出しの内容が、detailにテーブルの一行分の内容が入ります。

<columnHeader>{ここに見出しの内容が入る}</columnHeader>
<detail>{ここにテーブルの一行分の内容が入る}</detail>
<?xml version="1.0" encoding="UTF-8"?>
<!-- Created with Jaspersoft Studio version 6.6.0.final using JasperReports Library version 6.6.0  -->
<jasperReport
        xmlns="http://jasperreports.sourceforge.net/jasperreports"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://jasperreports.sourceforge.net/jasperreports http://jasperreports.sourceforge.net/xsd/jasperreport.xsd"
        name="Blank_A4"
        pageWidth="595"
        pageHeight="842"
        columnWidth="555"
        leftMargin="20"
        rightMargin="20"
        topMargin="20"
        bottomMargin="20"
        uuid="86d21441-0199-44fa-8d1e-8b7170bc739f">
    <queryString>
        <![CDATA[]]>
    </queryString>
    <field name="fruit"/>
    <field name="price"/>
    <columnHeader>
        <band height="30" splitType="Stretch">
            <staticText>
                <reportElement x="0" y="0" width="100" height="30"/>
                <textElement verticalAlignment="Middle" textAlignment="Center">
                    <font fontName="IPAexゴシック" size="14"/>
                </textElement>
                <text><![CDATA[商品]]></text>
            </staticText>
            <staticText>
                <reportElement x="100" y="0" width="100" height="30"/>
                <textElement verticalAlignment="Middle" textAlignment="Center">
                    <font fontName="IPAexゴシック" size="14"/>
                </textElement>
                <text><![CDATA[価格]]></text>
            </staticText>
        </band>
    </columnHeader>
    <detail>
        <band height="30" splitType="Stretch">
            <textField isBlankWhenNull="true">
                <reportElement x="0" y="0" width="100" height="30"/>
                <textElement verticalAlignment="Middle" textAlignment="Center">
                    <font fontName="IPAexゴシック" size="14"/>
                </textElement>
                <textFieldExpression><![CDATA[$F{fruit}]]></textFieldExpression>
            </textField>
            <textField isBlankWhenNull="true">
                <reportElement x="100" y="0" width="100" height="30"/>
                <textElement verticalAlignment="Middle" textAlignment="Center">
                    <font fontName="IPAexゴシック" size="14"/>
                </textElement>
                <textFieldExpression><![CDATA[$F{price}]]></textFieldExpression>
            </textField>
        </band>
    </detail>
</jasperReport>

テーブルに埋め込むデータは「パラメータ」ではなく主に「データソース」です。データソースはコレクション形式のデータで、そのコレクション内の一つの要素がテーブル一行分のデータとなります。コレクション内の要素はその中にいくつかのフィールドを持つことになります。 今回の例の場合、データソースは「fruit」、「price」というフィールドを持つ要素のコレクションです。

データソースの内容を埋め込むには、$F{フィールドの名前} という記法を使います。フィールドの名前は field タグであらかじめ宣言しておく必要があります。

これをレンダリングするJavaプログラムは次のようになります。

Path in = ...;
Path out = ...;

JasperReport report = (JasperReport) JRLoader.loadObject(in.toAbsolutePath().toFile());

Map<String, Object> parameters = new HashMap<>();
Collection<Map<String, ?>> source = new ArrayList<>();

Map<String, Object> row1 = new HashMap<>();
row1.put("fruit", "りんご");
row1.put("price", "100");
source.add(row1);
Map<String, Object> row2 = new HashMap<>();
row2.put("fruit", "みかん");
row2.put("price", "50");
source.add(row2);
Map<String, Object> row3 = new HashMap<>();
row3.put("fruit", "バナナ");
row3.put("price", "30");
source.add(row3);

JasperPrint print = JasperFillManager.fillReport(report, parameters, new JRMapCollectionDataSource(source));
JasperExportManager.exportReportToPdfFile(print, out.toAbsolutePath().toString());

罫線や背景色の調整

reportElement タグで背景色をつけたり、 box タグで罫線を引いたりすることができます。

    <columnHeader>
        <band height="30" splitType="Stretch">
            <staticText>
                <reportElement x="0" y="0" width="100" height="30" mode="Opaque" backcolor="rgba(200, 200, 200, 0.5)"/>
                <box>
                    <topPen lineWidth="1"/>
                    <leftPen lineWidth="1"/>
                    <bottomPen lineWidth="1"/>
                    <rightPen lineWidth="1"/>
                </box>
                <textElement verticalAlignment="Middle" textAlignment="Center">
                    <font fontName="IPAexゴシック" size="14"/>
                </textElement>
                <text><![CDATA[商品]]></text>
            </staticText>
            <staticText>
                <reportElement x="100" y="0" width="100" height="30" mode="Opaque" backcolor="rgba(200, 200, 200, 0.5)"/>
                <box>
                    <topPen lineWidth="1"/>
                    <leftPen lineWidth="1"/>
                    <bottomPen lineWidth="1"/>
                    <rightPen lineWidth="1"/>
                </box>
                <textElement verticalAlignment="Middle" textAlignment="Center">
                    <font fontName="IPAexゴシック" size="14"/>
                </textElement>
                <text><![CDATA[価格]]></text>
            </staticText>
        </band>
    </columnHeader>
    <detail>
        <band height="30" splitType="Stretch">
            <textField isBlankWhenNull="true">
                <reportElement x="0" y="0" width="100" height="30"/>
                <box>
                    <topPen lineWidth="0"/>
                    <leftPen lineWidth="1"/>
                    <bottomPen lineWidth="1"/>
                    <rightPen lineWidth="1"/>
                </box>
                <textElement verticalAlignment="Middle" textAlignment="Center">
                    <font fontName="IPAexゴシック" size="14"/>
                </textElement>
                <textFieldExpression><![CDATA[$F{fruit}]]></textFieldExpression>
            </textField>
            <textField isBlankWhenNull="true">
                <reportElement x="100" y="0" width="100" height="30"/>
                <box>
                    <topPen lineWidth="0"/>
                    <leftPen lineWidth="1"/>
                    <bottomPen lineWidth="1"/>
                    <rightPen lineWidth="1"/>
                </box>
                <textElement verticalAlignment="Middle" textAlignment="Center">
                    <font fontName="IPAexゴシック" size="14"/>
                </textElement>
                <textFieldExpression><![CDATA[$F{price}]]></textFieldExpression>
            </textField>
        </band>
    </detail>

f:id:tototoshi:20190302213926p:plain

とまあここまでくるとJasperReports StudioのGUIも使いながらデザインするのが楽だと思います。

画像、線、矩形表示などのタグもありますが、基本的な考えは変わらないので割愛します。 より詳しくはドキュメントのJasperReports Library Ultimate Guideを参照してください。

https://community.jaspersoft.com/documentation

JasperReportsはテーブルが1つのPDF、例えば見積書や請求書のような書類を作成するのに特化しているようにも思いましたが、SubReportという機能を利用することでより複雑なレイアウトにも対応できそうです。

例外をcase classとして定義してはいけない

Scalaで独自の例外を定義する場合

class MyException(message: String) extends RuntimeException(message)

case class MyException(message: String) extends RuntimeException(message)

のどちらが良いでしょうか。タイトルで言っちゃってますが、まずほとんどの場合はcase classではなくclassを使う方が良いと思います。

case classにすると等価性がそのインスタンスの属性によって判断されるので、同じ属性を持っている例外は同じものと見なされます。

scala> case class MyException(message: String) extends RuntimeException(message)
defined class MyException

scala> MyException("error")
res0: MyException = MyException: error

scala> MyException("error")
res1: MyException = MyException: error

scala> res0 == res1
res2: Boolean = true

一方、classの場合はequalsをオーバーライドしていない限り、属性が同じでもインスタンスが別なら別物です。

scala> class MyException(message: String) extends RuntimeException(message)
defined class MyException

scala> new MyException("error")
res0: MyException = MyException: error

scala> new MyException("error")
res1: MyException = MyException: error

scala> res0 == res1
res2: Boolean = false

基本的にはcase classではなくclassを使った時の挙動が好ましいと思います。messageが同じとはいえ、起きたタイミングの異なる2つの例外を同じものと見なすのは不自然ではないでしょうか。スタックトレースだって異なるかもしれないですしね。

AirframeでPlayを動かそうとした

taroleoさんが開発しているAirframeが最近Twitterのタイムラインで話題になっていることが多いので、そろそろ触ってみようかと思い、とりあえずPlay frameworkのDIコンテナをGuiceからAirframeに置き換えるというのをやって見ました。結果は失敗に終わったんですが、GuiceとAirframeの違いを知ることはできました。

PlayではデフォルトではRuntime DIにはGuiceが利用されますが、Play2.6ではDIコンテナのためのインターフェイスが作られたのでGuice以外のDIコンテナも利用できます。ApplicationLoaderというクラスがPlayのApplicationオブジェクトをロードする役割を担っているのでこれを好きなDIコンテナで実装します。

class AirframeApplicationLoader extends ApplicationLoader {
  override def load(context: ApplicationLoader.Context): Application = {
    import wvlet.airframe._
    // ここでAirframeでApplicationオブジェクトを構築する
  }
}

実際にはPlayとの橋渡し的なコードをそれなりに書く必要があり、そんなに簡単ではないです。例えばPlayが定義しているInjectorインターフェイスを実装する必要があったり、Playのビルトインモジュールを利用したいDIコンテナで使えるように変換するようなコードを書いたりです。実用的なことを考えるとPlayが提供しているGuiceApplicationBuilderみたいなコードも書く必要があり、なかなか骨が折れます。

Airframeの場合、実際にPlayの各モジュールをロードする段階でもつまづきます。Playの各モジュールは @Inject アノテーションでDIコンテナに対してクラスの生成方法のヒントを与えていますが、Airframeは @Inject アノテーションには対応していない方針のようでそれが機能しません。自分でクラスの生成方法を記述する必要があるのですが、Playの各モジュールの構築を手動でやるのはかなり面倒、ということで挫折しました。

その他Guiceとの違いで重要そうなのはsessionという概念でしょうか。Airframeではsessionが有効な間だけ機能を利用でき、また@PostConstruct, @PreDestroy アノテーションを利用するとsessionの開始時、終了時をフックして処理を挟みこめます。例えばスレッドの開始や終了などをAirframeに任せると便利ですね。Guiceにはこの機能はないので、Playでは @PostConstruct@PreDestroy の代わりに各クラスのコンストラクタとApplicationLifecycleというクラスでライフサイクルフックが実装されています。

同じDIコンテナという種類のライブラリでも比較して見るとそれなりに差異があります。それにDIコンテナはアプリケーションの基盤部分を担うようなものなので、気軽に置き換えられるようなものではないですね。でも気が向いたら再チャレンジしてみようと思います(多分やらない)。

Homebrewを使うのをやめた

プログラムのお仕事してるといろんな言語やツールをインストールするのが大変ですが、パッケージ管理ツールとかを使うと今度はそのパッケージ管理ツール由来のトラブルに巻き込まれたりするんですよね。それが面倒でHomebrewとかなんとかenvとか使わなくなってだいぶ経ちました。

今はほとんどのツールを手動でインストールしています。だいたいは ./configure --prefix=$PREFIX && make && make install。scalaとかはzip落としてきて展開してます。

ディレクトリ構成とかどうでも良いと思うんですが、私は$HOME/opt/srcにtarとかzipとか展開したソースを置いてprefixを$HOME/opt/pkg/... にし、$HOME/opt/bin以下にシンボリックリンク作ってPATH通してます。複数のバージョンを併用するのも単純にフルパスでコマンド叩けば良いだけですごく単純です。

/Users/toshi/opt
├── bin
│   ├── php -> /Users/toshi/opt/pkg/php-7.2.5/bin/php
│   ├── python3 -> /Users/toshi/opt/pkg/python-3.6.5/bin/python3
│   ├── ruby -> /Users/toshi/opt/pkg/ruby-2.5.1/bin/ruby
│   ├── scala -> /Users/toshi/opt/pkg/scala-2.12.6/bin/scala
├── pkg
│   ├── php-7.2.5
│   ├── python-3.6.5
│   ├── ruby-2.5.1
│   ├── scala-2.12.6
└── src
    ├── Python-3.6.5
    ├── php-7.2.5
    ├── ruby-2.5.1

依存ライブラリ集めたりビルド環境整えたり確かに手間はかかるんですが、その代わり各ツールへの理解は深まります。ビルドのエラーが出ることはたまにありますが結構単純なやつが多いのでそんなに困るほどではないです。少なくともHomebrewとかなんとかenvをデバッグする時のような不毛さはないし、自分の環境もよく把握できるのでトラブル自体が減るように思います。

もうとにかく依存ライブラリとか多くてビルドが死ぬほどめんどくさい!ってのはdockerとか使ってごまかしたりします。あとはプログラミング言語みたいに複数バージョン使いわける必要性があるやつでなければ公式のインストーラを使うこともあります。その辺明確な基準は持っていないです。まあ単にHomebrewが気に食わなかっただけですね。

play-jsonでReads/Writes/Formatを定義しなくてもよくする

play-jsonはReads/Writes/Formatを定義するのが面倒とよく言われるので、shapelessの練習も兼ねて作ってみました。

https://github.com/tototoshi/play-json-generic

scala> import play.api.libs.json._
import play.api.libs.json._

scala> case class Person(firstName: String, lastName: String, friends: List[Person])
defined class Person

scala> val people = Person("John",
     |                     "Lennon",
     |                     List(Person("Paul", "McCartney", Nil),
     |                          Person("George", "Harrison", Nil),
     |                          Person("Ringo", "Starr", Nil)))
people: Person = Person(John,Lennon,List(Person(Paul,McCartney,List()), Person(George,Harrison,List()), Person(Ringo,Starr,List())))

scala> Json.prettyPrint(Json.toJson(people))
<console>:18: error: No Json serializer found for type Person. Try to implement an implicit Writes or Format for this type.
       Json.prettyPrint(Json.toJson(people))
                                   ^

Reads/Writes/Formatを定義していないcase classをシリアライズしようとすると当然こんなエラーが出ますが、

import com.github.tototoshi.play.json.generic._ することでシリアライズ/デシリアライズできるようになります。

scala> import com.github.tototoshi.play.json.generic._
import com.github.tototoshi.play.json.generic._

scala> Json.prettyPrint(Json.toJson(people))
res1: String =
{
  "firstName" : "John",
  "lastName" : "Lennon",
  "friends" : [ {
    "firstName" : "Paul",
    "lastName" : "McCartney",
    "friends" : [ ]
  }, {
    "firstName" : "George",
    "lastName" : "Harrison",
    "friends" : [ ]
  }, {
    "firstName" : "Ringo",
    "lastName" : "Starr",
    "friends" : [ ]
  } ]
}

scala> Json.parse(res1).as[Person]
res2: Person = Person(John,Lennon,List(Person(Paul,McCartney,List()), Person(George,Harrison,List()), Person(Ringo,Starr,List())))

play-jsonJson.formatとかはマクロで実装されているので、そのマクロのためのオプションとかには一部対応できていないです。JsonNamingには対応できました。

Shapeless使ってみたいという人には The Type Astronaut's Guide to Shapeless - Underscore がすごくわかりやすくてオススメです。

追記

指摘されて気づいたんですが、HListを使っているので23個以上のフィールドを持つcase classにまつわる問題が解決しちゃってました。

https://github.com/tototoshi/play-json-generic/commit/0382c149efb4ed9fd4b4bcea99841641f2ca7949