SpringBootでScalaに入門してみた

この記事は富士通システムズウェブテクノロジーの社内技術コミュニティで、「イノベーション推進コミュニティ」
略して「いのべこ」が企画する、[いのべこ夏休みアドベントカレンダー 2020](https://qiita.com/jugemsan/items/5761773308fb1df0c23a)の15日目の記事です。
本記事の掲載内容は私自身の見解であり、所属する組織を代表するものではありません。
ここまでお約束 :wink:

# 概要

Scala初心者が、小慣れているSpringBoot/GradleでScalaに入門してみた記事です。

## なぜSpringBoot/Gradleか？

業務で使用した経験もあり、ある程度簡単に実装が始められるので。
PlayなどのScalaでよく使われるFWをつかって勉強していると、Playの勉強をしてるのか、Scalaの勉強をしてるのか・・・・つらくなってくるので、やっぱりみなれたFW/ビルドツールのほうがとっつきやすいだろうと。

# 手順！

大まかな手順はこんな感じです。

* [Spring Initializr](https://start.spring.io/)を使ってSpringBootのプロジェクトを作成する
* Scalaのプラグインを設定する
* src/{main,test}/scalaを作成する
* コーディング

Scalaのプラグインをgradleに設定してあげるところが肝です

# コードなど

## [Spring Initializr](https://start.spring.io/)を使ってSpringBootのプロジェクトを作成する

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/280383/2df44cc3-5223-e3da-ba50-04f8dbe681b1.png)

Spring Initializrでは、Dependencies/LanguageでScalaを選ぶことなどができますが、
Scalaを選択することができません。

そのため、一度Javaプロジェクトなりの言語で作成し、Scalaを使えるように設定してあげる必要があります。

## Scalaプラグインを設定する

build.gradleを編集し、scalaプラグイン・ライブラリを追加します。

```groovy:build.gradle
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タスクを追加する

です

```groovy:build.gradle
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として使えるようにした状態。

```scala:SpringBootScala.scala
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も書いてみる

```scala:EchoController.scala
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で書くとこんな感じでしょうか。

```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で定義します（正しいやり方がわかりませんが、これでとりあえずできました）

```scala:EchoService.scala
package com.github.fishibashi.SpringBootScala.service

trait EchoService {
  def echo(message: String): String
}
```

続いては、EchoServiceの実装クラスを作ります

```scala:DefaultEchoService.scala
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をインジェクションし（コンストラクタインジェクション）、
呼び出します。

```scala:EchoController.scala
@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ができました。

```bash
% curl "http://localhost:8080/echo?message=Hello,World"
Hello,World

% # DefaultEchoServiceでは、改行を２つ付けて文字列を返却してくれる。 
```

traitには、実装を持たせることもできます（Java8以降のinterface defaultと違い、フィールドが定義できる。使えるかどうかは未確認）。
なので、EchoServiceに実装を追加してみましょう。

```scala:EchoService.scala
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メソッドを呼ぶとどうなるか・・・？

```scala:EchoController.scala
@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)
}
```

```bash
curl "http://localhost:8080/echo/replace?message=Hello,World&substr=e&newsubstr=a"
Hallo,World%                                    
```

無事、`e`がreplaceされてHalloになりましたね（改行を入れてなかったので、改行されませんでしたが。。。）

もちろん、処理をOverrideしたいときは、実装クラスでOverrideしてあげればいいだけです。

```scala:DefaultEchoService.scala
@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"
}
```

無事、改行されて見やすくなりましたね。

```bash

 % curl "http://localhost:8080/echo/replace?message=Hello,World&substr=e&newsubstr=a"
 Hallo,World
 %
```

## ScalaとJavaの相互運用

ScalaとJavaは互換性があるので、１つのプロジェクト内にJavaとScalaを共存させることもできそうです。
（普段そんなことが必要になることがあるのかどうかは置いといて）

今回は、Springで@interfaceを定義する方法がわからなかったので（勉強不足）、
CustomValidatorをJavaで作成し、Scalaで利用したいと思います。

まず、先ほど消したjavaディレクトリ を復活させます。

```bash
% mkdir src/{main,test}/java/.... # パッケージも
```

いつも通りValidatorに必要なものを準備していきます。
今回は、RequestParamで取得した文字列に対して、文字列長をチェックするアノテーションを作成します。

```java:Length.java
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の実装です

```java:LengthConstraint.java
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に組み込むだけです。

```scala:EchoController.scala
  @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との違いについても、今後学んでいく必要がありそうです。