Qiita Teams that are logged in
You are not logged in to any team

Log in to Qiita Team
Community
OrganizationEventAdvent CalendarQiitadon (β)
Service
Qiita JobsQiita ZineQiita Blog
5
Help us understand the problem. What is going on with this article?

More than 3 years have passed since last update.

@bvlion

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

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

はじめに

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

前提条件

本記事は以下の方が対象となります。

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

準備

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

プロジェクトを作成

土台

Spring Initializrで土台を作成します。
必要なdependenciesを入れてください。
Spring Initializr
この程度の最小構成でも作成はできます。

gradle設定

以下を追記します。

  • applyに「scala」と「application」を追加
  • targetCompatibility = 1.8を追加(なくてもいいかも)
  • dependenciesに「scala-library」を追加
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にします。
ディレクトリ変更
しなくても動きますので、こちらはお好みで…
(この状態にして「gradlew build」を行うと「:compileJava NO-SOURCE」と出ます。)

mainメソッドを設定

Application.javaを削除し、Application.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コントローラーくらい設定する

上記までで一応動きますが、動いたってだけになってしまうので、コントローラーを作成します。

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で書きます。

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なことに驚きです…
試したりググったりした結果、こう書くことに落ち着きました。

動作させる

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

ターミナル
./gradlew clean build run

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

ログ
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では、これを元に実行してくれるようです。

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」を作ります。
これがないと動かないのか…試してません。。。

system.properties
java.runtime.version=1.8

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

herokuへプッシュ

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

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

ターミナル
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

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

ターミナル
heroku logs

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

ターミナル
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が走ります。

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

これでbuild passingをREADMEに追加できますね♪
(もちろんbuildが通ってる前提ですが…)

最後に…

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

5
Help us understand the problem. What is going on with this article?
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
5
Help us understand the problem. What is going on with this article?