この記事は富士通システムズウェブテクノロジーの社内技術コミュニティで、「イノベーション推進コミュニティ」
略して「いのべこ」が企画する、いのべこ夏休みアドベントカレンダー 2020の15日目の記事です。
本記事の掲載内容は私自身の見解であり、所属する組織を代表するものではありません。
ここまでお約束
概要
Scala初心者が、小慣れているSpringBoot/GradleでScalaに入門してみた記事です。
なぜSpringBoot/Gradleか?
業務で使用した経験もあり、ある程度簡単に実装が始められるので。
PlayなどのScalaでよく使われるFWをつかって勉強していると、Playの勉強をしてるのか、Scalaの勉強をしてるのか・・・・つらくなってくるので、やっぱりみなれたFW/ビルドツールのほうがとっつきやすいだろうと。
手順!
大まかな手順はこんな感じです。
- Spring Initializrを使ってSpringBootのプロジェクトを作成する
- Scalaのプラグインを設定する
- src/{main,test}/scalaを作成する
- コーディング
Scalaのプラグインをgradleに設定してあげるところが肝です
コードなど
Spring Initializrを使ってSpringBootのプロジェクトを作成する
Spring Initializrでは、Dependencies/LanguageでScalaを選ぶことなどができますが、
Scalaを選択することができません。
そのため、一度Javaプロジェクトなりの言語で作成し、Scalaを使えるように設定してあげる必要があります。
Scalaプラグインを設定する
build.gradleを編集し、scalaプラグイン・ライブラリを追加します。
plugins {
id 'scala' // 追加
id 'org.springframework.boot' version '2.3.1.RELEASE'
id 'io.spring.dependency-management' version '1.0.9.RELEASE'
id 'java'
id 'idea' // お好みで。エディタ系のプロジェクトファイルを出してくれるプラグイン。'eclipse'とかもある。
}
// 中略
dependencies {
// scalaのライブラリを追加
implementation "org.scala-lang:scala-library:2.13.3"
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-actuator'
developmentOnly 'org.springframework.boot:spring-boot-devtools'
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
compile group: 'org.hibernate', name: 'hibernate-validator', version: '6.1.5.Final'
}
[Tips] REPLをGradleでも使えるようにする方法
sbtを使用していると使えるREPL(Read Eval Print Loop)をgradleからも使えるようにしたいので、
build.gradleに設定を追加してあげます。
ポイントは
- scalaのコンパイラをimplementationに追加する
- REPLタスクを追加する
です
dependencies {
// scalaのライブラリを追加
implementation "org.scala-lang:scala-library:2.13.3"
implementation "org.scala-lang:scala-compiler:2.13.3" // ←追加
// 省略・・・
}
// repl用のタスクを追加
task repl(type: JavaExec) {
main = 'scala.tools.nsc.MainGenericRunner'
classpath = sourceSets.main.runtimeClasspath
standardInput System.in
args '-usejavacp'
}
以上のように追加すると、gradle repl --console plain --no-daemon
コマンドを実行することで、REPLが使えるようになります。
--console plain
というオプションですが、つけないとgradle自体の処理の進捗バーとかが邪魔で(コンソールを勝手にアップデートされて)REPLが使えませんので、ご注意ください。
というかこのオプション、CIとかでgradle build
するときも便利なのでは・・・(変な進捗バーとかでないし)
注意点
このままbuildすると、jarの中にscala-compilerとその依存関係が取り込まれてしまうため、jarがかなりFatになります。
(体感15MBぐらい。。。)
なので、ローカルで作業するとき以外はdependenciesからscala-compiler
を外しておく(コメントアウトしておく)ほうがいいようです。
implementationをやめてdevelopmentOnlyを指定すると、jarにも含まれなくなるようですが、副作用がいまいちわかりません(SpringDevToolとかと同じ依存関係のタイプ)
この辺の情報は公式ページを読めば詳しく書かれているのだろうか・・・
src/{main,test}/scalaを作成する
javaのソースコードとはお別れし、scala用のディレクトリを作成してあげてください。
あとはいい感じにmainクラスとかを作ってあげれば動きます。(急に丸投げ)
SpringBoot&ScalaでAPIを作ってみる。
とりあえずSpringBootでAPIとして使えるようにした状態。
package com.github.fishibashi.SpringBootScala
import org.springframework.boot.SpringApplication
import org.springframework.boot.autoconfigure.SpringBootApplication
@SpringBootApplication
class SpringBootScala {}
object SpringBootScala {
def main(args: Array[String]): Unit = SpringApplication.run(classOf[SpringBootScala], args:_ *)
}
メッセージをEchoしてくれるだけのControllerも書いてみる
package com.github.fishibashi.SpringBootScala.controller
import org.springframework.web.bind.annotation.{RequestMapping, RequestMethod, RequestParam, RestController}
@RestController
@RequestMapping(Array("/echo"))
class EchoController {
@RequestMapping(method = Array(RequestMethod.GET))
def echo(@RequestParam("message") message: String): String = message + "\n"
}
非常にCoolですね(?)。
Javaで書くとこんな感じでしょうか。
// SpringBootScala.java
package com.github.fishibashi.SpringBootScala;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
class SpringBootScala {
static void main(String args[]) {
SpringApplication.run(SpringBootScala.class, args);
}
}
// EchoController
package com.github.fishibashi.SpringBootScala.controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("/echo")
class EchoController {
@RequestMapping(method = RequestMethod.GET)
public String echo(@RequestParam("message") String message) {
return message + "\n";
}
}
DI
SpringBootをやるならDIも試してみたいですよね?
先ほどの処理を、Service化します。
Javaの場合はinterfaceなどでServiceを定義し、テスト時にMock化したりしますが、
Scalaの場合はtraitで定義します(正しいやり方がわかりませんが、これでとりあえずできました)
package com.github.fishibashi.SpringBootScala.service
trait EchoService {
def echo(message: String): String
}
続いては、EchoServiceの実装クラスを作ります
package com.github.fishibashi.SpringBootScala.service
import org.springframework.stereotype.Component
@Component
class DefaultEchoService extends EchoService {
@Override
def echo(message: String): String = message + "\n\n"
}
最後に、EchoControllerのコンストラクタで、EchoServiceをインジェクションし(コンストラクタインジェクション)、
呼び出します。
@RestController
@RequestMapping(Array("/echo"))
class EchoController(val service: EchoService) {
@RequestMapping(method = Array(RequestMethod.GET))
def echo(@RequestParam("message") message: String): String = service.echo(message)
}
無事、DIができました。
% curl "http://localhost:8080/echo?message=Hello,World"
Hello,World
% # DefaultEchoServiceでは、改行を2つ付けて文字列を返却してくれる。
traitには、実装を持たせることもできます(Java8以降のinterface defaultと違い、フィールドが定義できる。使えるかどうかは未確認)。
なので、EchoServiceに実装を追加してみましょう。
trait EchoService {
def echo(message: String): String
// 文字列をreplaceして返却するメソッドを追加。
def replace(message: String, subStr: String, newSubStr: String): String = message.split("").map(s => if (s == subStr) newSubStr else s).mkString
}
このまま、Controllerでreplaceメソッドを呼ぶとどうなるか・・・?
@RestController
@RequestMapping(Array("/echo"))
class EchoController(val service: EchoService) {
@RequestMapping(method = Array(RequestMethod.GET))
def echo(@RequestParam("message") message: String): String = service.echo(message)
// messageのsubstrをnewsubstrにreplaceする
@RequestMapping(path = Array("replace"), method = Array(RequestMethod.GET))
def replace(@RequestParam("message") message: String, @RequestParam("substr") subStr: String, @RequestParam("newsubstr") newSubStr: String): String = service.replace(message, subStr, newSubStr)
}
curl "http://localhost:8080/echo/replace?message=Hello,World&substr=e&newsubstr=a"
Hallo,World%
無事、e
がreplaceされてHalloになりましたね(改行を入れてなかったので、改行されませんでしたが。。。)
もちろん、処理をOverrideしたいときは、実装クラスでOverrideしてあげればいいだけです。
@Component
class DefaultEchoService extends EchoService {
@Override
def echo(message: String): String = message + "\n\n"
// superの処理結果に、"\n"を足してます
@Override
override def replace(message: String, subStr: String, newSubStr: String): String = super.replace(message, subStr, newSubStr) + "\n"
}
無事、改行されて見やすくなりましたね。
% curl "http://localhost:8080/echo/replace?message=Hello,World&substr=e&newsubstr=a"
Hallo,World
%
ScalaとJavaの相互運用
ScalaとJavaは互換性があるので、1つのプロジェクト内にJavaとScalaを共存させることもできそうです。
(普段そんなことが必要になることがあるのかどうかは置いといて)
今回は、Springで@interfaceを定義する方法がわからなかったので(勉強不足)、
CustomValidatorをJavaで作成し、Scalaで利用したいと思います。
まず、先ほど消したjavaディレクトリ を復活させます。
% mkdir src/{main,test}/java/.... # パッケージも
いつも通りValidatorに必要なものを準備していきます。
今回は、RequestParamで取得した文字列に対して、文字列長をチェックするアノテーションを作成します。
import javax.validation.Constraint;
import javax.validation.Payload;
import java.lang.annotation.*;
@Target(ElementType.PARAMETER) // 引数に対して動作するアノテーション
@Retention(RetentionPolicy.RUNTIME) // 実行時に有効
@Constraint(validatedBy = {LengthConstraint.class}) // Validatorの実装クラス
public @interface Length {
// message, groups, payloadは実装必須なので適当に実装しておきます
String message() default "length error";
Class<?>[] groups() default {};
Class<? extends Payload>[] payload() default {};
// max,minを指定できるようにして、規定値も与えます
int max() default 65535;
int min() default 0;
}
先ほどのアノテーションに紐づくValidationの実装です
import org.springframework.stereotype.Component;
import javax.validation.ConstraintValidator;
import javax.validation.ConstraintValidatorContext;
@Component
public class LengthConstraint implements ConstraintValidator<Length, String> {
private int max;
private int min;
// 引数にLengthを受け取り、Validatorを初期化します
@Override
public void initialize(Length constraintAnnotation) {
this.min = constraintAnnotation.min();
this.max = constraintAnnotation.max();
}
@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
// この結果でValidationの結果が決まります
return value.length() >= min && value.length() <= max;
}
}
あとは、Controllerに組み込むだけです。
@RequestMapping(path = Array("replace"), method = Array(RequestMethod.GET))
def replace(@Valid @RequestParam("message") @NotNull @NotBlank message: String,
@Valid @RequestParam("substr") @NonNull @Length(max = 1, min = 1) subStr: String,
@Valid @RequestParam("newsubstr") @NonNull @Length(max = 1, min = 1) newSubStr: String): String = service.replace(message, subStr, newSubStr)
最後に
非常に薄っぺらい内容でしたが、目的のSpringBootを使ってScalaを学ぶための環境を手に入れられそうです。
DIのところでInterfaceの代わりにtraitを定義したりしてましたが、使い方があってるのか若干不安です。
生成されるclassの構造とかを逆コンパイルして確認してみたいですねー
あと、@interface
はどう定義するのだろうか。。。
Javaとの違いについても、今後学んでいく必要がありそうです。