読者です 読者をやめる 読者になる 読者になる

冥冥乃志

ソフトウェア開発会社でチームマネージャをしているエンジニアの雑記。アウトプットは少なめです。

follow us in feedly

SpringBootプロジェクトをScala + sbtで構築する

Programming Scala

なんでわざわざScalaでやる必要が、というツッコミがありそうですが、今作ってるものが外部コマンドを結構使いそうなアプリケーションで scala.sys.process 使えると良さそうだなあ、と思っただけ。PlayとかよりSpringBootの方が楽そうなんですもの。

ちょうどおあつらえ向きのGithubリポジトリがあったので参考にしてみました。

github.com

本当に手探りでやってるので、ツッコミはむしろ欲しいです。

プロジェクト設定

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
}

はい、やっぱりこっちの方がマシです。

テストを書いてみる

コントローラ層以外はだいたいモック使えばよくて、ユニットテストをするのにあまりフレームワークの支援を必要とせずに書けるかな、と。で、コントローラ層なんですが、 Specs2MockMvc.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 testJUnitのテストも実行されるようになりました。これで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に上げております。今回はエントリ内のサンプルでお願いします。