LoginSignup
9
5

More than 3 years have passed since last update.

SpringBootでScalaに入門してみた

Last updated at Posted at 2020-08-14

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

概要

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のプロジェクトを作成する

image.png

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

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

Scalaプラグインを設定する

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

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タスクを追加する

です

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として使えるようにした状態。

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も書いてみる

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

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

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

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

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

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をインジェクションし(コンストラクタインジェクション)、
呼び出します。

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ができました。

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

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

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

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メソッドを呼ぶとどうなるか・・・?

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)
}
curl "http://localhost:8080/echo/replace?message=Hello,World&substr=e&newsubstr=a"
Hallo,World%                                    

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

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

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"
}

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


 % 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で取得した文字列に対して、文字列長をチェックするアノテーションを作成します。

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の実装です

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に組み込むだけです。

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

9
5
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
9
5