play framework (java) 2.4 (activator 1.3.6) のWebアプリをherokuに載せるために

play java 2.4でWebアプリを作っているのですが、herokuにデプロイしようとしたらなかなか一筋縄ではいかなかったので備忘録です。
結構、敷居高いと思いました。ネット上にもあまり情報なくて。。なんでしょう、Play Javaは結構下火なのかな?

基本的には公式ドキュメントを参照して作業を進めます。
日本語だと若干ドキュメントのバージョン古いですが、まあ基本は同じです。

そもそも、日本語ユーザ名だと動かない

今回 Windows 10で作業をしていたのですが、日本語名でユーザを作成してしまっていたために、「heroku create」コマンドの時点で以下のようなエラーとなりました。

C:\heroku\sample>heroku create
U+8A13 to WINDOWS-1252 in conversion from UTF-8 to WINDOWS-1252
C:/Program Files (x86)/Heroku/lib/heroku/jsplugin.rb:112:in `encode'
C:/Program Files (x86)/Heroku/lib/heroku/jsplugin.rb:112:in `app_dir'
C:/Program Files (x86)/Heroku/lib/heroku/jsplugin.rb:119:in `bin'
C:/Program Files (x86)/Heroku/lib/heroku/jsplugin.rb:147:in `setup?'
C:/Program Files (x86)/Heroku/lib/heroku/jsplugin.rb:213:in `check_if_old'
C:/Program Files (x86)/Heroku/lib/heroku/jsplugin.rb:123:in `setup'
C:/Program Files (x86)/Heroku/lib/heroku/cli.rb:24:in `start'
C:/Program Files (x86)/Heroku/bin/heroku:29:in `<main>'

最初何が悪いのか全く分かりませんでしたよね。気が付いて良かった。
別途英字ユーザ名でユーザを作り直したらすんなり動きました。ここはね。

パッケージ名に制約?

ちょっと訳あって、今回「mdd」で始まるパッケージ名を使っていたのですが(mdd.hogehoge.HeroHero.java という具合)、何故かheroku上では読み込んでくれませんでした。(package does not exist とか言われる)
仕方ないので「jp.co.hogehoge」というパッケージに移したら普通にコンパイル通りました。
ローカルのactivator上では普通に動いていたので、これはherokuの制約ですよね。
どういうルールなのかは分かりませんが、パッケージ名に何らかの制約があるようです。

PostgreSQLドライバ

これはまあ当たり前の話ですが、ローカルでH2 databaseを使っていたため、PostgreSQLの設定が必要です。(herokuでは標準でPostgreSQLをサポートしています)

まず、build.sbt にJDBCドライバの依存関係を定義します。

  "postgresql" % "postgresql" % "9.1-901-1.jdbc4"

そして、JDBCドライバを以下のProcfile内に指定します。

Procfile の記述

※Procfileの内容については、Play 2.2とそれ以降で結構違っているので注意が必要です。
このProcfileという仕組み、なかなか良いですね。
ローカルの環境を壊さずに、heroku上の動作パラメータを指定できるようになっています。
現状のProcfileは以下のような感じです。

web: target/universal/stage/bin/myapp -Dhttp.port=${PORT} -DapplyEvolutions.default=true -Ddb.default.driver=org.postgresql.Driver -Ddb.default.url=${DATABASE_URL} -Djpa.default=herokuPersistenceUnit

ここで、JDBCドライバを「org.postgresql.Driver」に指定しています。
それとともに、JPAで使うPersistenceUnitを指定しています。
つまりpersistence.xmlにローカル用とheroku用の定義を作成して、それぞれ使い分けることができます。
現状のpersistence.xmlは以下のような感じ。

<persistence xmlns="http://xmlns.jcp.org/xml/ns/persistence"
             xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
             xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/persistence http://xmlns.jcp.org/xml/ns/persistence/persistence_2_1.xsd"
             version="2.1">

    <persistence-unit name="defaultPersistenceUnit" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <non-jta-data-source>DefaultDS</non-jta-data-source>
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
            <property name="hibernate.hbm2ddl.auto" value="update" />
            <property name="hibernate.archive.autodetection" value="class, hbm"/>
        </properties>
    </persistence-unit>

    <persistence-unit name="testPersistenceUnit" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <non-jta-data-source>DefaultDS</non-jta-data-source>
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.H2Dialect"/>
            <property name="hibernate.hbm2ddl.auto" value="update" />
            <property name="hibernate.archive.autodetection" value="class, hbm"/>
        </properties>
    </persistence-unit>

    <persistence-unit name="herokuPersistenceUnit" transaction-type="RESOURCE_LOCAL">
        <provider>org.hibernate.jpa.HibernatePersistenceProvider</provider>
        <non-jta-data-source>DefaultDS</non-jta-data-source>
        <properties>
            <property name="hibernate.dialect" value="org.hibernate.dialect.PostgreSQLDialect"/>
            <property name="hibernate.hbm2ddl.auto" value="update" />
            <property name="hibernate.archive.autodetection" value="class, hbm"/>
        </properties>
    </persistence-unit>

</persistence>

テスト用、ローカル開発用、heroku用と3つのDBを切り替えて使います。
繰り返しになりますが、このProcfileの仕組み素晴らしいと思いました。

JDBC Connection.isValid()に対するエラーの話

Play 2.4へのマイグレーションガイドを見ると、かなり派手に変わってきていることが分かります。
今回引っかかったのが、このページに書かれているこの部分ですね。
なんでもコネクションプールのライブラリのデフォルトが「HicariCP」に変更されたとのことで、DB接続時に以下のようなエラーが出ました。

play java heroku  JDBC4 Connection.isValid() method not supported, connection test query must be configured

どうやらPostgreSQLJDBCドライバがこの「isValid()」というメソッドをサポートしていないらしく、ここに書いてある通り「application.conf」に

db.default.hikaricp.connectionTestQuery="SELECT TRUE"

という設定を入れてあげることで回避できました。

以上!

というわけで、Java8対応とかReactive周りが賑やかなのもあって、Play Frameworkは思い切った変更をガンガン入れてきてるようですね。
(ていうか既に2.5って話か。。)こうなってくると開発環境もDockerとかで切り替えてやっていかないとキツイかもですね。

■ Eclipseでのリモートデバッグ

いやーハマった!
くそーハマった!

Play 2.4.2(Java) で、Eclipseからリモートデバッグしようとしていたのですが

activator -jvm-debug 9999 run

Eclipseから接続は出来るものの、ブレークポイントを設定してもぜーんぜん止まってくれません。

もー頭きた。

本当にいろいろやったのですが、どうやっても止まってくれない。

最後の最後で、Google Groupでやっとこんなエントリを見つけました。
https://groups.google.com/forum/#!searchin/play-framework/remote$20debug$20eclipse/play-framework/wuSUT6Thrlg/vXxBsLNrwbkJ

ちょっと現象は違うのですが、要するに、「build.sbt」で

fork in run:=false

としてあげないとダメみたいです。(デフォルトはtrueになってるんです!!)

さすがにこれはちょっとわかんないよなー


ちなみに、ついでに発見したのですが、
デフォルトだと「project/eclipse.sbt」の中に

addSbtPlugin("com.typesafe.sbteclipse" % "sbteclipse-plugin" % "2.5.0")

と書かれているのですが、最新バージョンは「4.0.0」で、
最新バージョンを使ってあげないとどうやらeclipseのクラスパスがうまく設定されない模様。
(というか今まで手動で調整してた。。)

なんかちょっと色々と雑。

■ astah* のプラグインでライブラリインポートの問題。

astah* のプラグイン開発で、外部ライブラリ(こちらも自作)のインポート時にエラーが発生したので備忘録。
環境は以下

pom.xmldependencyに、自作ライブラリを追加。


jp.co.prudence.hoge
hero
1.0

「astah-build」したところ、

java.lang.ArrayIndexOutOfBoundsException: 18
at aQute.lib.osgi.Clazz.parseClassFile(Clazz.java:448)
at aQute.lib.osgi.Clazz.parseClassFile(Clazz.java:369)
at aQute.lib.osgi.Clazz.parseClassFileWithCollector(Clazz.java:359)
at aQute.lib.osgi.Clazz.parseClassFile(Clazz.java:349)
at aQute.lib.osgi.Analyzer.analyzeJar(Analyzer.java:1725)
at aQute.lib.osgi.Analyzer.analyzeBundleClasspath(Analyzer.java:1618)
at aQute.lib.osgi.Analyzer.analyze(Analyzer.java:124)
at aQute.lib.osgi.Builder.analyze(Builder.java:306)
at aQute.lib.osgi.Analyzer.calcManifest(Analyzer.java:301)
at aQute.lib.osgi.Builder.build(Builder.java:73)
at org.apache.felix.bundleplugin.BundlePlugin.buildOSGiBundle(BundlePlugin.java:547)
at org.apache.felix.bundleplugin.BundlePlugin.execute(BundlePlugin.java:347)
at org.apache.felix.bundleplugin.BundlePlugin.execute(BundlePlugin.java:264)
at org.apache.felix.bundleplugin.BundlePlugin.execute(BundlePlugin.java:255)
at org.apache.maven.plugin.DefaultBuildPluginManager.executeMojo(DefaultBuildPluginManager.java:101)
at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:209)
at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:153)
at org.apache.maven.lifecycle.internal.MojoExecutor.execute(MojoExecutor.java:145)
at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject(LifecycleModuleBuilder.java:84)
at org.apache.maven.lifecycle.internal.LifecycleModuleBuilder.buildProject(LifecycleModuleBuilder.java:59)
at org.apache.maven.lifecycle.internal.LifecycleStarter.singleThreadedBuild(LifecycleStarter.java:183)
at org.apache.maven.lifecycle.internal.LifecycleStarter.execute(LifecycleStarter.java:161)
at org.apache.maven.DefaultMaven.doExecute(DefaultMaven.java:320)
at org.apache.maven.DefaultMaven.execute(DefaultMaven.java:156)
at org.apache.maven.cli.MavenCli.execute(MavenCli.java:537)
at org.apache.maven.cli.MavenCli.doMain(MavenCli.java:196)
at org.apache.maven.cli.MavenCli.main(MavenCli.java:141)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:483)
at org.codehaus.plexus.classworlds.launcher.Launcher.launchEnhanced(Launcher.java:290)
at org.codehaus.plexus.classworlds.launcher.Launcher.launch(Launcher.java:230)
at org.codehaus.plexus.classworlds.launcher.Launcher.mainWithExitCode(Launcher.java:409)
at org.codehaus.plexus.classworlds.launcher.Launcher.main(Launcher.java:352)

さすがにこれはバグだろうと。

理由は良く分からんのですが、このへんを見たら「felixのバージョン2.4.0あたりで治ってるよ」みたいなことが書いてあったので、pom.xmlを修正


org.apache.felix
maven-bundle-plugin

2.4.0

みたいにしたら治りました。(もともとは2.3.7だった)

そもそも、もともとの状態(astah-generate-project をやってEclipseでプロジェクトを開いた状態)で、
pom.xmlのfelixのあたりで「plugin execution not covered by lifecycle configuration」とかいうエラーがEclipse上で出ている(動作はする)ので、何かが正しくないのでしょうね。まあ一応解決。

JPAを使うテストとクラスパスについて

上記のパーシステンス周りの実装をEclipse上でテストしようとすると、ちょっとコツがいりました。
もともと、テストとかも「activator ui」によるブラウザ上の開発環境で実行する前提で調整されているので
Eclipse環境はあまり優遇されていないようです。

そもそものPlayでのお作法も含めて、ざっくり言うと以下4点の対応が必要。

FakeApplicationを用意してあげる

Play上で動くコードはPlayが色々とお膳立てをしてくれているわけで、テストでも同じようにお膳立てをしてあげる必要があります。
これについてはネット上に色々情報がありますが、僕は以下のようなクラスを継承してテストを書きました。

public abstract class FakeApplicationTest {
    protected EntityManager em;
    FakeApplication app;

    @Before
    public void setUpTest() {
        this.app = fakeApplication();
        Helpers.start(this.app);
    }

    @After
    public void tearDownTest() {
        Helpers.stop(this.app);
    }
}

Transactionを用意してあげる

まあこれもおまじないみたいなもんです。
テストを書く際に、以下のように記述をしてあげます。

public class HogeTest extends FakeApplicationTest {

    @Test
    public void testHero() {
        JPA.withTransaction(new play.libs.F.Callback0() {
            public void invoke() {
              // テスト内容
            }
        });
    }

}

scala-libraryにクラスパスを通す

これは多分、Play-Javaの実装上のちょっとした手落ちなんじゃないかと思うんですけどね。
上記の記述でactivator ui 上では問題なく動くのですが、eclipse上からFakeApplicationを動かそうとすると怒られます。

java.lang.NoClassDefFoundError: scala/collection/Map
at testutil.FakeApplicationTest.setUpTest(FakeApplicationTest.java:21)
at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)
at java.lang.reflect.Method.invoke(Method.java:483)
・・・・・

つまり、FakeApplicationを動かすにはScalaのMap実装が必要なんですね。

仕方ないので、eclipse上で「scala-library-2.11.x.jar」とかを Build Path に含めてあげます。
(まあ、activator eclipseやると元に戻ってしまうのでカッコ悪いですが。。)

ちなみに、scala-libraryは.ivy/cache/org.scala-lang/scala-library/jars とかにいます。

「activator test」を実行する。

普通、eclipseで開発するときはコードを保存したらその瞬間コンパイルされて使えるようになりますが、
このPlayの環境ではどうやらそうではないようです。
「activator test」を実行することで、コンパイル内容がクラスパスに載ってくる感じの様です。
ちょっと面倒くさいですけどね。


とりあえず今回は以上。

JPAとevolution

Play 2.3 あたりからパーシステンス周りでEBeanが標準から外れたので(使えるんですけどね)、
今回はJPA with hibernateでやろうとしています。
ただ、DDLを自分で書くのはちょっとアレなので、Evolutionの機能は使いたいのですが、
私が見た感じだとどうも JPA with hibernate ではEvolutionは使えないみたいです。
こちら => Play Framework 2.3 For Java ことはじめ #6 データベース接続(JPA with Hibernate)編 - まーぽんって誰がつけたの? 参考にさせて頂いたのですが、
SQLファイル(conf/evolutions/default/1.sql)を自分で用意しても動いてくれません。

結局、hibernateでやる場合は「conf/META-INF/persistence.xml」に

を追加して、Evolutionの機能は使わないでやりましょう、って事だと思ったんですが違うのかな。
とりあえずこれでスキーマは作ってくれました。

「.classpath」の文字コードについて

「activator new」でプロジェクトを作成、eclipseで開こうと思って「activator eclipse」とやると、
どうも build path が全く通っていない様子。
今回、Windows8.1でやっているのですが、プロジェクトフォルダ直下にできるeclipse用の「.classpath」ファイルの文字コードが合わない(プロジェクトはUTF-8なのにファイルがShiftJISでできる)ために、パスちゃんと読めてないんですね。(Windowsのログインユーザ名を日本語にしてしまったので)

で、こちら => 【STEP2】IDE(eclipse)にプロジェクトをインポート : Scalaでコードを書く - Qiita に書かれていることと逆に、「.activator/activatorconfig.txt」を作って

と書いてやったら治りました。
このあたりはまあ、どっちかに揃えれば良いんでしょうね。