SpringBootプロジェクトをScala + sbtで構築する
なんでわざわざScalaでやる必要が、というツッコミがありそうですが、今作ってるものが外部コマンドを結構使いそうなアプリケーションで scala.sys.process
使えると良さそうだなあ、と思っただけ。PlayとかよりSpringBootの方が楽そうなんですもの。
ちょうどおあつらえ向きのGithubリポジトリがあったので参考にしてみました。
本当に手探りでやってるので、ツッコミはむしろ欲しいです。
プロジェクト設定
name := "Cthulhu" version := "1.0" scalaVersion := "2.11.7" seq(webSettings: _*) libraryDependencies ++= Seq( "org.springframework.boot" % "spring-boot-starter-web" % "1.2.5.RELEASE", "org.springframework.boot" % "spring-boot-starter-data-jpa" % "1.2.5.RELEASE", "org.webjars" % "bootstrap" % "3.3.5", "org.webjars" % "jquery" % "2.1.4", "org.thymeleaf" % "thymeleaf-spring4" % "2.1.4.RELEASE", "org.hibernate" % "hibernate-validator" % "5.2.1.Final", "nz.net.ultraq.thymeleaf" % "thymeleaf-layout-dialect" % "1.2.9", "com.h2database" % "h2" % "1.4.188", "org.springframework.boot" % "spring-boot-starter-tomcat" % "1.2.5.RELEASE" % "provided", "javax.servlet" % "javax.servlet-api" % "3.0.1" % "provided", "org.specs2" %% "specs2-core" % "3.6.4" % "test", "org.springframework.boot" % "spring-boot-starter-test" % "1.2.5.RELEASE" % "test" ) libraryDependencies ++= Seq( "org.apache.tomcat.embed" % "tomcat-embed-core" % "7.0.53" % "container", "org.apache.tomcat.embed" % "tomcat-embed-logging-juli" % "7.0.53" % "container", "org.apache.tomcat.embed" % "tomcat-embed-jasper" % "7.0.53" % "container" )
ほとんどサンプルまま。特に変わったことはしてない感じです。実行時のためにcontainer scopeに依存関係が記載されています。あとは、他のビルドシステムと同じように必要なライブラリの依存関係を記載しているだけです。私がサンプルから変更したのは、Spring BootのバージョンとSpecs2への依存関係くらい。
アプリケーション実行のオブジェクトを作る
Appトレイトを継承したオブジェクトを作ります。
import org.springframework.boot.SpringApplication import org.springframework.boot.autoconfigure.EnableAutoConfiguration import org.springframework.context.annotation.ComponentScan @ComponentScan @EnableAutoConfiguration object CthulhuWebApplication extends App { SpringApplication.run(CthulhuWebApplication.getClass, args:_*) }
サンプルにしたリポジトリでは実行時引数を指定してませんでしたが、特にargsを暗黙的にやり取りしているわけではないし、サンプルの状態だと実行時引数で環境変えたいときに指定しても無視されるんじゃなかろうかと思っております(ちなみに、この可変長引数への渡し方の書き方をすっかり忘れておりました)。
とりあえず、おもむろに sbt run
してみますが、エラーになりました。以下にちゃんと起動できるようになるまでにやったこと一覧を。
- デフォルトパッケージやめる(SpringBoot関連のクラスロードにこけた)
resources/templates
ディレクトリの作成(thymeleafのロード時にこけた)
なお、sbtコンソール内で起動中に上記の理由などでエラーになった後にsbtコンソールを終了しないで再度実行するとtomcatが起動できなくてエラーになりました。sbtコンソールを終了してから再度実行すると問題なし。挙動からしてポートがバッティングして起動エラーになったのではないかと思いますが、その後特に再現しないので無視することにします。
REST Controllerを作ってみる
まずはサービス層なしで値を返すControllerを作ります。
package cthulhu.sample import org.springframework.web.bind.annotation.{RequestMethod, RequestMapping, RestController} @RestController @RequestMapping(Array("/api/sample")) class SampleController { @RequestMapping(method = Array(RequestMethod.GET)) def data = "hoge" }
RequestMapping
アノテーションのvalueやmethod属性は配列なので、Arrayを明示的に指定しないとコンパイルエラーになるようです。これ、Javaの場合は明示的に配列にしてないんですが、良きように計らってくれていた、ということですかね?今更気付いてしまいました。
サービスを作ってみる
サービスを作ってインジェクションします。
package cthulhu.sample import org.springframework.stereotype.Component @Component class HogeService { def getHoge = "hoge" }
package cthulhu.sample import org.springframework.beans.factory.annotation.Autowired import org.springframework.web.bind.annotation.{RequestMapping, RequestMethod, RestController} @RestController @RequestMapping(Array("/api/sample")) class SampleController { @Autowired var hogeService: HogeService = _ @RequestMapping(method = Array(RequestMethod.GET)) def data = hogeService.getHoge }
参考にしたリポジトリがクラスのコンストラクタに Autowired
アノテーションをつけていて書き方的にあまり好きではなかったので上記のような記載方法にしてみましたが、こっちはこっちで var
使って負けた気になるし、アンダースコアがちょっとキモいですね。というわけで、最終的にはこうなりました。
package cthulhu.sample import org.springframework.beans.factory.annotation.Autowired import org.springframework.web.bind.annotation.{RequestMapping, RequestMethod, RestController} @RestController @RequestMapping(Array("/api/sample")) class SampleController @Autowired()(private val hogeService: HogeService) { @RequestMapping(method = Array(RequestMethod.GET)) def data = hogeService.getHoge }
はい、やっぱりこっちの方がマシです。
テストを書いてみる
コントローラ層以外はだいたいモック使えばよくて、ユニットテストをするのにあまりフレームワークの支援を必要とせずに書けるかな、と。で、コントローラ層なんですが、 Specs2
で MockMvc.addExcept
をどうやって使っていいかわからず、今回は結局JUnitで書いてしまいました。
package cthulhu.sample import org.junit.runner.RunWith import org.junit.{Before, Test} import org.springframework.boot.test.SpringApplicationConfiguration import org.springframework.mock.web.MockServletContext import org.springframework.test.context.junit4.SpringJUnit4ClassRunner import org.springframework.test.context.web.WebAppConfiguration import org.springframework.test.web.servlet.MockMvc import org.springframework.test.web.servlet.request.MockMvcRequestBuilders import org.springframework.test.web.servlet.result.MockMvcResultMatchers import org.springframework.test.web.servlet.setup.MockMvcBuilders @RunWith(classOf[SpringJUnit4ClassRunner]) @SpringApplicationConfiguration(classes = Array(classOf[MockServletContext])) @WebAppConfiguration class SampleControllerSpecs { import MockMvcBuilders._ import MockMvcRequestBuilders._ import MockMvcResultMatchers._ var mvcMock: MockMvc = _ @Before def before { mvcMock = standaloneSetup(new SampleController(new HogeService)).build() } @Test def hogeが返る { mvcMock.perform(get("/api/sample")).andExpect(status.isOk).andExpect(content.string("hoge")) } }
ただし、これやっちゃうと、 sbt test
の実行対象にならないんですよね。。。と思って調べたら junit-interface
があるんですね。 build.sbt
に依存性を追加します。
name := "Cthulhu" version := "1.0" scalaVersion := "2.11.7" seq(webSettings: _*) libraryDependencies ++= Seq( "org.springframework.boot" % "spring-boot-starter-web" % "1.2.5.RELEASE", "org.springframework.boot" % "spring-boot-starter-data-jpa" % "1.2.5.RELEASE", "org.webjars" % "bootstrap" % "3.3.5", "org.webjars" % "jquery" % "2.1.4", "org.thymeleaf" % "thymeleaf-spring4" % "2.1.4.RELEASE", "org.hibernate" % "hibernate-validator" % "5.2.1.Final", "nz.net.ultraq.thymeleaf" % "thymeleaf-layout-dialect" % "1.2.9", "com.h2database" % "h2" % "1.4.188", "org.springframework.boot" % "spring-boot-starter-tomcat" % "1.2.5.RELEASE" % "provided", "javax.servlet" % "javax.servlet-api" % "3.0.1" % "provided", "org.specs2" %% "specs2-core" % "3.6.4" % "test", "org.springframework.boot" % "spring-boot-starter-test" % "1.2.5.RELEASE" % "test", "com.novocode" % "junit-interface" % "0.11" % "test" ) libraryDependencies ++= Seq( "org.apache.tomcat.embed" % "tomcat-embed-core" % "7.0.53" % "container", "org.apache.tomcat.embed" % "tomcat-embed-logging-juli" % "7.0.53" % "container", "org.apache.tomcat.embed" % "tomcat-embed-jasper" % "7.0.53" % "container" )
これで、 sbt test
でJUnitのテストも実行されるようになりました。これでCIかけても大丈夫です。
ビルド、パッケージング
sbtには package
タスクがあってコンパイルしてjarファイルにビルドしてくれるんですが、これは依存関係のあるライブラリはjarファイルの中に含めてはくれません *1 。
というわけで sbt-assembly
プラグインを導入します。
https://github.com/sbt/sbt-assembly
READMEの指示に従ってプラグイン追加をすると assembly
タスクが使えるようになります。というわけで sbt assembly
を実行してみましょう。するとエラーになります。一つのjarファイルにまとめる際にコンフリクトしているリソースがあるようです。
name := "Cthulhu" version := "1.0" scalaVersion := "2.11.7" seq(webSettings: _*) libraryDependencies ++= Seq( "org.springframework.boot" % "spring-boot-starter-web" % "1.2.5.RELEASE", "org.springframework.boot" % "spring-boot-starter-data-jpa" % "1.2.5.RELEASE", "org.webjars" % "bootstrap" % "3.3.5", "org.webjars" % "jquery" % "2.1.4", "org.thymeleaf" % "thymeleaf-spring4" % "2.1.4.RELEASE", "org.hibernate" % "hibernate-validator" % "5.2.1.Final", "nz.net.ultraq.thymeleaf" % "thymeleaf-layout-dialect" % "1.2.9", "com.h2database" % "h2" % "1.4.188", "org.springframework.boot" % "spring-boot-starter-tomcat" % "1.2.5.RELEASE" % "provided", "javax.servlet" % "javax.servlet-api" % "3.0.1" % "provided", "org.specs2" %% "specs2-core" % "3.6.4" % "test", "org.springframework.boot" % "spring-boot-starter-test" % "1.2.5.RELEASE" % "test", "com.novocode" % "junit-interface" % "0.11" % "test" ) libraryDependencies ++= Seq( "org.apache.tomcat.embed" % "tomcat-embed-core" % "7.0.53" % "container", "org.apache.tomcat.embed" % "tomcat-embed-logging-juli" % "7.0.53" % "container", "org.apache.tomcat.embed" % "tomcat-embed-jasper" % "7.0.53" % "container" ) mainClass in assembly := Some("cthulhu.WebApplication") assemblyMergeStrategy in assembly := { case PathList("javax", "persistence", xs @ _*) => MergeStrategy.first case PathList(ps @ _*) if ps.last endsWith ".json" => MergeStrategy.first case PathList(ps @ _*) if ps.last endsWith ".factories" => MergeStrategy.first case PathList(ps @ _*) if ps.last endsWith ".provides" => MergeStrategy.first case PathList(ps @ _*) if ps.last endsWith ".tooling" => MergeStrategy.first case PathList(ps @ _*) if ps.last endsWith ".xml" => MergeStrategy.first case "changelog.txt" => MergeStrategy.discard case "overview.html" => MergeStrategy.discard case x => val oldStrategy = (assemblyMergeStrategy in assembly).value oldStrategy(x) }
で、ビルドすることはできたんですが、実行時になぜか EmbeddedServletContainerFactory
がなくてこけてしまいます。
Exception in thread "main" org.springframework.context.ApplicationContextException: Unable to start embedded container; nested exception is org.springframework.context.ApplicationContextException: Unable to start EmbeddedWebApplicationContext due to missing EmbeddedServletContainerFactory bean.
情報が少なすぎてぶっちゃけよくわかりません。dockerでもsbtのイメージあるし、なんならずっと sbt run
で流すかなあ、とかも考え中。とりあえず一旦思考停止しました。
で、コードは?
社内ツールなので、社内用のGHEに上げております。今回はエントリ内のサンプルでお願いします。