SpringbootをScalaで動かして、Herokuにデプロイする。

※ この記事は、ScalaとSkinnyを同時に学ぼうとして挫折したJavaエンジニアの悪あがきメモです(笑)
言語とFW同時に学ぶのはムリだった…という切ないエンジニアの軌跡です(^ω^;)

## はじめに
冒頭にありますように、Scalaをこれから学ぶにあたりFWと同時では難しかったため、FWのみJavaで培った技術を使います。
そのためScala自体はほぼ未経験ですので、サンプルのコードに誤りなどがございましたらご指摘ください。

## 前提条件
本記事は以下の方が対象となります。

* Scalaを覚えたい
* Javaはそれなりにできる
* Springbootもそこそこ知っている
* サーバーはHerokuの1,000時間を利用しようと思ってる

## 準備

Herokuのアカウントとプロジェクトを作成してください。
当記事では、Herokuのアプリ名を「${HEROKU_APP}」と表記します。

## プロジェクトを作成
### 土台
[Spring Initializr](https://start.spring.io/)で土台を作成します。
必要なdependenciesを入れてください。
![Spring Initializr](https://qiita-image-store.s3.amazonaws.com/0/90107/9c423f86-ef29-68d1-673a-7c9b0c2c80e6.png)
この程度の最小構成でも作成はできます。

### gradle設定
以下を追記します。

* applyに「scala」と「application」を追加
* targetCompatibility = 1.8を追加（なくてもいいかも）
* dependenciesに「scala-library」を追加

```gradle:build.gradle
buildscript {
	ext {
		springBootVersion = '1.5.3.RELEASE'
	}
	repositories {
		mavenCentral()
	}
	dependencies {
		classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
	}
}

apply plugin: 'org.springframework.boot'
apply plugin: 'scala'
apply plugin: 'application'

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
	mavenCentral()
}

dependencies {
	compile('org.mybatis.spring.boot:mybatis-spring-boot-starter:1.3.0')
	compile('org.springframework.boot:spring-boot-starter-web')
	compile('org.scala-lang:scala-library:2.12.2')
	testCompile('org.springframework.boot:spring-boot-starter-test')
}
```
今回Scalaは2.12.2を適用させます。

### ソースディレクトリを設定
java -> scalaにします。
![ディレクトリ変更](https://qiita-image-store.s3.amazonaws.com/0/90107/242dcd16-5d50-93a9-cf9c-5cd1b0bdc60c.png)
しなくても動きますので、こちらはお好みで…
（この状態にして「gradlew build」を行うと「:compileJava NO-SOURCE」と出ます。）

### mainメソッドを設定
Application.javaを削除し、Application.scalaを作成します。

```scala:Application.scala
package sample.scalaboot

import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication

@SpringBootApplication
class Application {}

object Application {
  def main(args: Array[String]): Unit = SpringApplication.run(classOf[Application], args: _*)
}
```
この単一式関数が書きたかった…
でももっと簡素に書けるんだろうな〜(^^;

### 動作確認
#### せっかくなのでRestコントローラーくらい設定する
上記までで一応動きますが、動いたってだけになってしまうので、コントローラーを作成します。

```scala:SampleController.scala
package sample.scalaboot.controller

import org.springframework.web.bind.annotation.{RequestMapping, RequestMethod, RestController}

@RestController
@RequestMapping(Array("/sample"))
class SampleController {
  @RequestMapping(method = Array(RequestMethod.GET))
  def sample = "sample"
}
```

#### コントローラーのテストを書く
せっかくspring-boot-starter-testも入っているので、テストもScalaで書きます。

```scala:SampleControllerTest.scala
package sample.scalaboot.controller

import org.hamcrest.Matchers.equalTo
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.content
import org.springframework.test.web.servlet.result.MockMvcResultMatchers.status

import org.junit.Test
import org.junit.runner.RunWith
import org.springframework.beans.factory.annotation.Autowired
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc
import org.springframework.boot.test.context.SpringBootTest
import org.springframework.http.MediaType
import org.springframework.test.context.junit4.SpringRunner
import org.springframework.test.web.servlet.MockMvc
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders

@RunWith(classOf[SpringRunner])
@SpringBootTest
@AutoConfigureMockMvc
class SampleControllerTest {

  @Autowired
  val mvc: MockMvc = null

  @Test
  @throws[Exception]
  def sampleGet_Ok(): Unit =
    mvc.perform(MockMvcRequestBuilders.get("/sample").accept(MediaType.APPLICATION_JSON))
      .andExpect(status.isOk)
      .andExpect(content.string(equalTo("sample")))
}
```
Autowiredでインジェクションする先がvalなことに驚きです…
試したりググったりした結果、こう書くことに落ち着きました。

#### 動作させる
ここまでできたらローカルで動作確認を行います。

```text:ターミナル
./gradlew clean build run
```

こんな感じのログになったでしょうか？

```text:ログ
Starting a Gradle Daemon, 1 busy Daemon could not be reused, use --status for details
:clean
:compileJava NO-SOURCE
:compileScala
:processResources
:classes
:jar
:findMainClass
:startScripts
:distTar
:distZip
:bootRepackage
:assemble
:compileTestJava NO-SOURCE
:compileTestScala
:processTestResources NO-SOURCE
:testClasses
:test
:check
:build
:run

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::        (v1.5.3.RELEASE)

2017-05-25 12:41:24.012  INFO 15626 --- [           main] sample.scalaboot.ScalabootApplication$   : Starting ScalabootApplication. on user.local with PID 15626 (/Users/user/workspace/scalaboot/build/classes/main started by user in /Users/user/workspace/scalaboot)
2017-05-25 12:41:24.018  INFO 15626 --- [           main] sample.scalaboot.ScalabootApplication$   : No active profile set, falling back to default profiles: default
2017-05-25 12:41:24.146  INFO 15626 --- [           main] ationConfigEmbeddedWebApplicationContext : Refreshing org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@54c562f7: startup date [Thu May 25 12:41:24 JST 2017]; root of context hierarchy
2017-05-25 12:41:25.280  WARN 15626 --- [           main] o.m.s.mapper.ClassPathMapperScanner      : No MyBatis mapper was found in '[sample.scalaboot]' package. Please check your configuration.
2017-05-25 12:41:26.497  INFO 15626 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat initialized with port(s): 8080 (http)
2017-05-25 12:41:26.539  INFO 15626 --- [           main] o.apache.catalina.core.StandardService   : Starting service Tomcat
2017-05-25 12:41:26.541  INFO 15626 --- [           main] org.apache.catalina.core.StandardEngine  : Starting Servlet Engine: Apache Tomcat/8.5.14
2017-05-25 12:41:26.736  INFO 15626 --- [ost-startStop-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2017-05-25 12:41:26.736  INFO 15626 --- [ost-startStop-1] o.s.web.context.ContextLoader            : Root WebApplicationContext: initialization completed in 2593 ms
2017-05-25 12:41:26.932  INFO 15626 --- [ost-startStop-1] o.s.b.w.servlet.ServletRegistrationBean  : Mapping servlet: 'dispatcherServlet' to [/]
2017-05-25 12:41:26.939  INFO 15626 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'characterEncodingFilter' to: [/*]
2017-05-25 12:41:26.940  INFO 15626 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'hiddenHttpMethodFilter' to: [/*]
2017-05-25 12:41:26.940  INFO 15626 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'httpPutFormContentFilter' to: [/*]
2017-05-25 12:41:26.940  INFO 15626 --- [ost-startStop-1] o.s.b.w.servlet.FilterRegistrationBean   : Mapping filter: 'requestContextFilter' to: [/*]
2017-05-25 12:41:27.384  INFO 15626 --- [           main] s.w.s.m.m.a.RequestMappingHandlerAdapter : Looking for @ControllerAdvice: org.springframework.boot.context.embedded.AnnotationConfigEmbeddedWebApplicationContext@54c562f7: startup date [Thu May 25 12:41:24 JST 2017]; root of context hierarchy
2017-05-25 12:41:27.492  INFO 15626 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/sample],methods=[GET]}" onto public java.lang.String sample.scalaboot.controller.SampleController.sample()
2017-05-25 12:41:27.496  INFO 15626 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error]}" onto public org.springframework.http.ResponseEntity<java.util.Map<java.lang.String, java.lang.Object>> org.springframework.boot.autoconfigure.web.BasicErrorController.error(javax.servlet.http.HttpServletRequest)
2017-05-25 12:41:27.497  INFO 15626 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/error],produces=[text/html]}" onto public org.springframework.web.servlet.ModelAndView org.springframework.boot.autoconfigure.web.BasicErrorController.errorHtml(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse)
2017-05-25 12:41:27.560  INFO 15626 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-05-25 12:41:27.560  INFO 15626 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-05-25 12:41:27.613  INFO 15626 --- [           main] o.s.w.s.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**/favicon.ico] onto handler of type [class org.springframework.web.servlet.resource.ResourceHttpRequestHandler]
2017-05-25 12:41:28.379  INFO 15626 --- [           main] o.s.j.e.a.AnnotationMBeanExporter        : Registering beans for JMX exposure on startup
2017-05-25 12:41:28.486  INFO 15626 --- [           main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2017-05-25 12:41:28.493  INFO 15626 --- [           main] sample.scalaboot.ScalabootApplication$   : Started ScalabootApplication. in 5.142 seconds (JVM running for 5.688)
> Building 95% > :run
```

ここまで確認できればプロジェクト作りは完了です。

## Herokuへデプロイ

### gradle設定
既存のbuild.gradleにさらに追記します。

* defaultTasks "clean", "build"
* jar項目
* springBoot項目
* task wrapper

```build.gradle
defaultTasks "clean", "build"

buildscript {
	ext {
		springBootVersion = '1.5.3.RELEASE'
	}
	repositories {
		mavenCentral()
	}
	dependencies {
		classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}")
	}
}

apply plugin: 'org.springframework.boot'
apply plugin: 'scala'
apply plugin: 'application'

sourceCompatibility = 1.8
targetCompatibility = 1.8

repositories {
	mavenCentral()
}

jar {
    baseName = 'jar_name'
    version = '0.0.1-SNAPSHOT'
}

springBoot {
	executable = true
}

dependencies {
	compile('org.mybatis.spring.boot:mybatis-spring-boot-starter:1.3.0')
	compile('org.springframework.boot:spring-boot-starter-web')
	compile('org.scala-lang:scala-library:2.12.2')
	testCompile('org.springframework.boot:spring-boot-starter-test')
}

task wrapper(type: Wrapper) {
	gradleVersion = '3.4.1'
}
```

もしかしたら「task wrapper」の項目はいらないかもしれません。
試してないです(>_<)

### Procfile作成
ディレクトリ直下にProcfileを作成します。
Herokuでは、これを元に実行してくれるようです。

```text:Procfile
web: java -jar build/libs/jar_name-0.0.1-SNAPSHOT.jar --server.port=$PORT --spring.profiles.active=production
```

### system.properties作成
src/main/resources配下に「system.properties」を作ります。
これがないと動かないのか…試してません。。。

```text:system.properties
java.runtime.version=1.8
```

「jar_name」の部分やバージョンはbuild.gradleに合わせて変えてください。
ちなみに、one-offとかもあればここに追加していいみたいです。

### herokuへプッシュ
.gitignoreは既に用意されていますので、
合わせてheroku configで「BUILDPACK_URL」が必要という記事を見かけましたが、少なくとも私の環境ではなくても実行できました。

さて、以下のコマンドでアップロードします。

```text:ターミナル
cd ${プロジェクトdir}
git init
git add .
git commit -m "initialize"
git remote add heroku https://git.heroku.com/${HEROKU_APP}.git
git push heroku master
```

アップロードが完了したら確認します。

```text:ターミナル
heroku logs
```

こんな感じのログが出ていますか？

```text:ターミナル
2017-05-25T06:47:55.097320+00:00 app[web.1]:
2017-05-25T06:47:55.097406+00:00 app[web.1]:   .   ____          _            __ _ _
2017-05-25T06:47:55.097456+00:00 app[web.1]:  /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
2017-05-25T06:47:55.097538+00:00 app[web.1]: ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
2017-05-25T06:47:55.097590+00:00 app[web.1]:  \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
2017-05-25T06:47:55.097663+00:00 app[web.1]:   '  |____| .__|_| |_|_| |_\__, | / / / /
2017-05-25T06:47:55.101488+00:00 app[web.1]:  :: Spring Boot ::        (v1.5.3.RELEASE)
2017-05-25T06:47:55.097739+00:00 app[web.1]:  =========|_|==============|___/=/_/_/_/
2017-05-25T06:47:55.101519+00:00 app[web.1]:
```

プラウザでも確認してみましょう！

* https://${HEROKU_APP}.herokuapp.com/sample


## その他
### Travisを使いたい
Springbootの設定をScalaに変えただけで動きました。
キチンとgradleが走ります。

```yml:.travis.yml
language: scala
scala:
  - "2.12.2"
jdk:
  - oraclejdk8
```

これで![build passing](https://img.shields.io/travis/rust-lang/rust.svg)をREADMEに追加できますね♪
（もちろんbuildが通ってる前提ですが…）

## 最後に…
以上で取り敢えず動くようにはなりますが、Scalaにはそれに見合ったFWを使うべきだと思います。
精進して理解できてきたら、PlayやSkinnyもやってみたいと思います！