※ この記事は、ScalaとSkinnyを同時に学ぼうとして挫折したJavaエンジニアの悪あがきメモです(笑)
言語とFW同時に学ぶのはムリだった…という切ないエンジニアの軌跡です(^ω^;)
はじめに
冒頭にありますように、Scalaをこれから学ぶにあたりFWと同時では難しかったため、FWのみJavaで培った技術を使います。
そのためScala自体はほぼ未経験ですので、サンプルのコードに誤りなどがございましたらご指摘ください。
前提条件
本記事は以下の方が対象となります。
- Scalaを覚えたい
- Javaはそれなりにできる
- Springbootもそこそこ知っている
- サーバーはHerokuの1,000時間を利用しようと思ってる
準備
Herokuのアカウントとプロジェクトを作成してください。
当記事では、Herokuのアプリ名を「${HEROKU_APP}」と表記します。
プロジェクトを作成
土台
Spring Initializrで土台を作成します。
必要なdependenciesを入れてください。
この程度の最小構成でも作成はできます。
gradle設定
以下を追記します。
- applyに「scala」と「application」を追加
- targetCompatibility = 1.8を追加(なくてもいいかも)
- dependenciesに「scala-library」を追加
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を作成します。
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コントローラーくらい設定する
上記までで一応動きますが、動いたってだけになってしまうので、コントローラーを作成します。
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で書きます。
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
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では、これを元に実行してくれるようです。
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」を作ります。
これがないと動かないのか…試してません。。。
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が走ります。
language: scala
scala:
- "2.12.2"
jdk:
- oraclejdk8
これでをREADMEに追加できますね♪
(もちろんbuildが通ってる前提ですが…)
最後に…
以上で取り敢えず動くようにはなりますが、Scalaにはそれに見合ったFWを使うべきだと思います。
精進して理解できてきたら、PlayやSkinnyもやってみたいと思います!