はじめに
一からScalaのプロジェクトを始めるならIntelliJ+sbtにしましょう。
対象
- 現在Java+Maven+Eclipseで開発していて、徐々にScalaに移行したい人
結論から言うと、前半で書く内容は参考だけに留めておいて、マイクロプロジェクトごとにScalaとJavaに分割した方がいいです。理由は後でいっぱい出てきます。
でもせっかくScalaなので、Javaと親和性高いところみたいですね。単に多言語呼び出すだけならJVM言語じゃなくてできますが、ScalaのクラスをJavaで継承したり、ScalaのメソッドがJavaのコード補完で出てくるのはなかなかないんじゃないでしょうか。ClojureではJavaのメソッドがコード補完で出てきましたね。
準備
EclipseにScala IDE pluginを入れます。m2e(Maven Integrator for Eclipse)はすでに入っている人も多いでしょう。lombokをセットアップしたことが無い人は入れておいてください。まずはmavenでScalaプロジェクトを作ってみます。
「File - New - Project - Maven Project」を選択し、アーキタイプの選択で「scala-archtype-simple 1.6」を選択します。
GroupIDとArtifactIDを入れて生成します。この時点でエラーだらけですね。プロジェクトを右クリックして「Configure - Add Scala Nature」を選択するとScalaがコンパイルできるようになります。パースペクティブをScalaに切り替えてプロジェクトcleanしたらエラーが消えました。App.scalaを右クリックして「Run as - Scala Application」で「Hello world!」が出力されます。
specs.scalaファイルにエラーがありますが、Ctrl+Shift+Oでimportを再編成してあげれば直ります。私はspecs2使わないので消してしまいます。いっぺんにいろいろ覚えると面倒なので、慣れるまでテストは今まで通りJUnitで書くのもいいかもしれません。
Javaのクラスを作る
「New - Source Folder」で「src/main/java」を作ります。パッケージは「jp.lavans.scalatest.java」にします。ここに次のようなJavaクラスを作ります。
package jp.lavans.scalatest.java;
public class Person {
private String name;
public String getName() {
return name;
}
public void setName(String value) {
name = value;
}
}
よく見るひな形ですね。まずはscalaから呼び出してみます。
object App {
def main(args : Array[String]) {
val p = new Person
p.setName("name")
println(p.getName)
}
}
エラーが消えないことがありますが、プロジェクトのcleanで消えると思います。Javaのクラスが全部コンパイルできていないとscalaをコンパイルが通らないことが多いです。JavaのA,Bというクラスがあって、scalaからはAしか使っていなくてもBがコンパイルエラーだとScala側のコンパイルも通りません。target/classesの下のファイルを見てるとよくわかると思います。この辺がScalaとJavaを同じプロジェクトでやるべきでない理由の一つです。
Appを実行すると
name
と表示されました。
次はJavaのクラスを継承してみます。App.scalaの下の方にそのまま書いてしまいます。
case class SPerson(_sname: String) extends Person
mainメソッドで出力します。
object App {
def main(args: Array[String]) {
val sp = new SPerson("sname")
sp.setName("name")
println(sp.sname)
}
}
これでJavaで定義したnameとScalaで定義したsnameの両方を使うことが出来ました。もちろんJavaでScalaのクラスを継承することもできます。
型引数が絡んでくるとうまくいかない事もあります。上手く行ったり行かなかったりするのもストレス高いです。特に最初のうちは何が悪いか見当がつかないので、ScalaとJavaを混ぜるとつらい、という気持ちになります。
「何度もcleanをする必要がある」「Javaを全コンパイルしてからScalaがコンパイルされる」というのはプロジェクトが大きくなればなるほど効いてきます。IntelliJ IDEAでは差分コンパイルが効いているようで、同じマシンを使っていても体感でEclipseの方が断然遅いです。
mavenでビルドする
ここでコマンドラインでビルドしてみましょう。プロジェクトのディレクトリに行って
mvn build
最初の一回が長いのは我慢します。「scalac error: bad option: '-make:transitive'」あ、これ初めて見た気がします。pom.xmlのscala-maven-pluginからこのタグを消します。もう一度ビルドすると
[ERROR] /home/.../scalatest/src/main/scala/jp/lavans/scalatest/App.scala:18: error: not found: type Person
[ERROR] class SPerson(_sname: String) extends Person{
Personが無いと怒られます。
pom.xml
さて、みんな大好きpom.xml(怒)です。実はここが一番はまりました。まずはpom.xmlを見てみると
<build>
<sourceDirectory>src/main/scala</sourceDirectory>
<testSourceDirectory>src/test/scala</testSourceDirectory>
<plugins>
<plugin>
ソースディレクトリが指定してありますがjavaがありません。しかもこのタグは複数指定できませんでした。指定するには別のプラグインを使います(怒)。
<plugin>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<executions>
<execution>
<id>add-source</id>
<phase>generate-sources</phase>
<goals>
<goal>add-source</goal>
</goals>
<configuration>
<sources>
<source>src/main/scala</source>
<source>src/main/java</source>
</sources>
</configuration>
</execution>
</executions>
</plugin>
ちょっとディレクトリ足すのに長すぎませんかね。
mvn compile
で普通にコンパイルできました。前は" "Plugin execution not covered by lifecycle configuration"のエラーが出てpluginExecutionFilterとか指定していたのですが少し楽になりましたね(怒)。
古いEclipseを使わざるを得ない人もいると思うので、lifecycleのエラーを消すためのタグも書いておきます。
<build>
...
<pluginManagement>
<plugins>
<plugin>
<groupId>org.eclipse.m2e</groupId>
<artifactId>lifecycle-mapping</artifactId>
<version>1.0.0</version>
<configuration>
<lifecycleMappingMetadata>
<pluginExecutions>
<pluginExecution>
<pluginExecutionFilter>
<groupId>org.codehaus.mojo</groupId>
<artifactId>build-helper-maven-plugin</artifactId>
<versionRange>[1.0.0,)</versionRange>
<goals>
<goal>add-source</goal>
</goals>
</pluginExecutionFilter>
<action>
<execute>
<runOnIncremental>false</runOnIncremental>
</execute>
</action>
</pluginExecution>
<pluginExecution>
<pluginExecutionFilter>
<groupId>net.alchim31.maven</groupId>
<artifactId>scala-maven-plugin</artifactId>
<versionRange>[1.0.0,)</versionRange>
<goals>
<goal>compile</goal>
<goal>testCompile</goal>
</goals>
</pluginExecutionFilter>
<action>
<execute>
<runOnIncremental>false</runOnIncremental>
</execute>
</action>
</pluginExecution>
</pluginExecutions>
</lifecycleMappingMetadata>
</configuration>
</plugin>
</plugins>
</pluginManagement>
</build>
数々のデメリット
「いい加減Javaからステップアップしたい、でも一からScalaは厳しい」そう考えて迷い込んだ道でしたが、環境構築だけで難関でした。
- 謎のエラーがでてcleanビルドしなければいけない
- Javaのプログラムに一部でもコンパイルエラーがあるとScalaのコンパイルをしてくれない
- pom.xmlが人外向け
プログラムを書いていてもいろいろと問題が出ます。
- JavaからScalaを利用する場合は、Scala objectがClass$MODULE$のような名前になったり内部クラスがClass$1になってソースが見にくい
- ScalaからJavaを利用する場合はコレクションの変換する処理が入り、どれがScalaのものでどれがJavaのものかわかりにくい。
- ScalaとJavaで流儀が違うものが混在すると読みにくい(アクセッサ等)。
解決策
結局ソースを綺麗に保つためにScala部分とJava部分を明確に分けて、間にラッパーを作ることになります。それぞれをmavenでjarとして生成し、元のシステムに取り込むようにしましょう。
新規Scalaプロジェクトで、純粋なScalaモデルとサービス層を作ります。別途Javaプロジェクトで、当該サービスとScala側の橋渡しをするだけのプロジェクトを作ります。元のプロジェクトからはこのJavaラッパーだけに依存して、Scalaの知識を必要としないような造りにするのがいいと思います。ちなみに、ScalaからJavaのライブラリを使うときも、Scalaっぽく書きたくてついついラッパーを書く人も多いようです。
ScalaとJavaではプログラム内で相互変換できますが、可能であればそれぞれ独立したサービスにしてjsonやProtocolBuffersでやりとりするに越したことはありません。Scalaも過渡期の技術で、きっとまた便利なものがでてくるでしょう。システムを漸進的に置き換え可能にするには、出来るだけ小さく作り疎結合にしておくに限ります。そうすればその部分だけを新しい技術に置き換えられますからね。
余談はともかく、mavenプロジェクトでScalaを入れようとしてpomになぶられた話は以上です。