Das Spring-Framework und insbesondere Spring-Boot sind sehr populär und weit verbreitet in der Programmierung von Anwendungen mit Java. Für Scala gibt es andere beliebte Frameworks, insbesondere für die Programmierung von Webservern, wie zum Beispiel das Play Framework.

In dieser kleinen Serie von Artikeln wollen wir uns anschauen, ob und inwieweit man funktional in Scala programmieren und trotzdem Spring-Boot einsetzen kann.

Das Spring-Framework besteht aus einer ganzen Reihe von Elementen, von denen manche eine Art zentralen Kern bilden und manche aufbauend auf dem Kern eher Library-Charakter haben.

Dependency Injection

Zu den zentralen Elementen gehört ein Mechanismus zur Dependency Injection, auch Auto-Wiring genannt. Dieser findet auf verschiedenste Art und Weise und hoch konfigurierbar, Klassen, die in anderen Klassen oder Methoden als Argumente oder Member auftauchen und instanziiert diese automatisch zur Laufzeit.

Annotationen

Spring und Spring-Boot setzen außerdem stark auf Annotationen, um Klassen und Methoden für verschiedene Zwecke zu kennzeichnen und um ihnen Code hinzuzufügen. Alternativ kann das zumindest teilweise auch über eine Konfigurationsdatei (XML oder YAML) gemacht werden, aber das ist nicht sehr beliebt, da diese sehr schnell sehr groß werden kann.

Spring-Boot

Spring-Boot ist eine Sammlung von Bibliotheken, die den Umgang und den Einstieg in das Spring-Framework erleichtern sollen. Das geschieht vor allem mit sogenannten Startern, die viele Defaults mit sich bringen und es einfacher machen sollen, typische Anwendungsfälle umzusetzen.

Eine Liste der Starter findet sich hier.

Der Fokus liegt dabei vor allem auf Webservern und Datenbank-Anbindungen.

Beispielanwendung

In einem kleinen Beispiel wollen wir uns jetzt angucken, wie das praktisch in Scala und mit möglichst viel funktionaler Programmierung aussehen kann.

Wir implementieren ein sehr simples (und morbides) Spiel, bei dem sich Gürteltiere auf eine Autobahn begeben und dort ggf. überfahren werden. Basierend auf einem funktionalen Kern wollen wir Spring-Boot nutzen, um eine Rest-API für das Spiel zu implementieren.

Fangen wir mit dem funktionalen Kern an. Ein Dillo (kurz für Armadillo) hat einen Namen und einen Gesundheitslevel. Wenn es angefahren wird, reduziert sich der Gesundheitslevel abhängig von der Geschwindigkeit. Außerdem kann es Hallo sagen, solange es noch lebt:

case class Dillo(name: String, health: Int)

object Dillo {
  def runOver(d: Dillo, speed: Int): Dillo = {
    d.copy(health = Math.max(0, d.health - speed))
  }

  def speak(d: Dillo): String = {
    if (d.health > 0)
      s"${d.name} says Hi!"
    else
      s"${d.name} is dead"
  }
}

Dann definieren wir eine Autobahn mit Gürteltieren, die sich darauf befinden und einer Methode, die die Dillos etwas sagen lässt:

case class Highway(dillos: Map[UUID, Dillo]) {
  def report(): Iterable[String] = {
    for (_id, d) <- dillos
      yield Dillo.speak(d)
  }
}

Funktionen, die etwas mit der Autobahn machen, abstrahieren wir als Highway.Op - ein sogenannter Arrow. Eine davon ist driveAlong, die einmal alle Gürteltiere überfährt. Eine zweite fügt ein neues Gürteltier zur Autobahn hinzu:

object Highway {
  type Op = Highway => Highway

  val empty: Highway = Highway(Map.empty)

  def driveAlong(speed: Int): Op = { highway =>
    highway.copy(dillos = highway.dillos.view.mapValues(Dillo.runOver(_, speed)).toMap)
  }

  def spawn(id: UUID, dillo: Dillo): Op = { highway =>
    highway.copy(dillos = highway.dillos + (id -> dillo))
  }
}

Soviel zum funktionalen Kern unserer Anwendung. Machen wir uns jetzt an die Rest-API.

Für unser Beispiel reicht es, den Spring-Boot Starter für Webanwendungen einzubinden: spring-boot-starter-web. In SBT können wir das folgendermaßen tun:

libraryDependencies += "org.springframework.boot" % "spring-boot-starter-web" % "3.4.2"

Dann definieren wir zunächst eine Klasse, die unsere Anwendung darstellt:

import org.springframework.boot.autoconfigure.SpringBootApplication

@SpringBootApplication
class DemoApp {}

Die Annotation SpringBootApplication bringt eine ganze Reihe von weiteren Annotationen implizit mit sich. Insbesondere den sogenannten ComponentScan, der bewirkt, dass das Spring-Framework im Class-Path der Anwendung Klassen für die Dependency-Injection findet (sogenannte Components). Außerdem die Annotation EnableAutoConfiguration von Spring-Boot, die abhängig von den Libraries im Class-Path und der Anwesenheit (oder Abwesenheit!) von bestimmten Klassen im Namensraum der Application-Klasse bestimmte Defaults setzt. Alleine durch das Hinzufügen einer Klasse bzw. Annotation oder das Hinzufügen einer Library kann sich daher das Verhalten der Anwendung verändern.

Als Einstiegs- und Startpunkt unserer Anwendung definieren wir jetzt noch ein main für das Companion-Objekt von DemoApp:

object DemoApp {
  def main(args: Array[String]): Unit = {
    import org.springframework.boot.SpringApplication

    SpringApplication.run(classOf[DemoApp], args: _*)
  }
}

Das reicht, um eine ausführbare Anwendung zu haben, die zunächst ein wunderbares Spring-Logo ausgibt:

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/

 :: Spring Boot ::                (v3.4.2)

und anschließend einige Log-Meldungen, an denen zu sehen ist, dass Spring-Boot per Default einen eingebetteten Apache Tomcat Webserver auf Port 8080 startet und unsere Anwendung darin als Servlet registriert.

Diese ist aber zunächst noch leer. Definieren wir also einen sogenannten Controller, um HTTP-Anfragen zu verarbeiten. Am einfachsten geht das über eine Klasse mit der RestController-Annotation:

import org.springframework.web.bind.annotation.{GetMapping, RestController}

@RestController
class DemoController {
  @GetMapping() def index: String = "Hello world"
}

Spring-Boot findet diese Klasse und bindet sie entsprechend ins Servlet ein. Wenn wir jetzt die Anwendung starten und eine Anfrage an "/" schicken, bekommen wir eine entsprechende Antwort:

$ curl http://localhost:8080/
Hello World

Um nun aber unser Spiel über einen solchen RestController „spielbar“ zu machen, müssen wir uns überlegen, wie wir den Zustand des Spiels (bzw. des Highways) über die Zeit verwalten. Da Spring-Boot hierfür leider keine funktionalen Schnittstellen anbeitet, müssen wir imperativ arbeiten. Was wir aber tun können, um das möglichst spät zu entscheiden, ist, einen Trait zu definieren:

trait IAppState {
  def get: Highway
  def update(op: Highway.Op): Unit
}

Das können wir dann in unserem Controller verwenden, um als Antwort auf "/" den Highway-Report zu liefern:

@RestController
class DemoController(state: IAppState) {
  @GetMapping(value = Array("/")) def index: String =
    state.get.report().mkString("\n")
}

Wenn wir jetzt unsere Anwendung starten, beschwert sich Spring, dass es keine Implementierung von IAppState findet. Definieren wir also eine Implementierung, die den Zustand in einer AtomicReference speichert:

import org.springframework.stereotype.Component

@Component
class MemAppState extends IAppState {
  import java.util.concurrent.atomic.AtomicReference
  
  private val highway: AtomicReference[Highway] = AtomicReference[Highway](Highway.empty)

  def get: Highway = highway.get()

  def update(op: Highway.Op): Unit = {
    highway.updateAndGet({ highway => op(highway) })
  }
}

Durch die Annotation als Component machen wir die Klasse für Spring auffindbar. Das Framework erzeugt beim Start der Anwendung eine Instanz unserer MemAppState-Klasse, um den Controller damit zu erzeugen. Wenn mehrere Klassen existieren die IAppState implementieren, bricht Spring die Anwendung mit einem entsprechenden Fehler ab, da es nicht entscheiden kann welche es nehmen soll.

Damit über die HTTP-Schnittstelle auch tatsächlich etwas am Highway verändert werden kann, definieren wir jetzt noch einen Request, um ein Gürteltier hinzuzufügen und einen, um den Highway entlang zu fahren:

@RestController
class DemoController(state: IAppState) {
  @GetMapping() def index: String =
    state.get.report().mkString("\n")

  @PostMapping(Array("/dillo")) def dillo(name: String): UUID = {
    val id = UUID.randomUUID()
    state.update(Highway.spawn(id, Dillo(name, health = 100)))
    id
  }

  @PostMapping(Array("/drive")) def drive(speed: Int): Unit = {
    state.update(Highway.driveAlong(speed))
  }
}

Der Default-Path der Mappings ist dabei immer „/“, egal wie die Methode heißt. Die Unterpfade „/dillo“ und „/drive“ muss man in Scala explizit als Array deklarieren.

Ein großes Fettnäpchen existiert in Scala 3 vor der Version 3.6, wo man die Annotation zwar so schreiben kann wie es in vielen Java-Tutorials für Spring steht:

@PostMapping("/dillo")

Dieser Code kompiliert aber zu etwas anderem als in Java und setzt den name der Annotation, anstatt den value. Man merkt das dann entweder daran, dass die Anfragen nicht funktionieren oder Spring sich über mehrfach deklarierte Pfade beschwert.

Jetzt können wir jedenfalls mit CURL unser Spiel spielen!

$ curl -X POST -d "name=Sammy" localhost:8080/dillo
"5ee5db86-8906-4a9e-a37c-543d69015455"

$ curl -X POST -d "speed=80" localhost:8080/drive

$ curl -X POST -d "name=Mona" localhost:8080/dillo
"d39f99ee-6405-4bce-8ee3-0157b1c4adcb"

$ curl -X POST -d "speed=90" localhost:8080/drive

$ curl localhost:8080/
Sammy is dead
Mona says Hi!

Die Namen der Parameter der beiden Methoden dillo und drive sind dabei per Default jeweils auch die Namen der Form-Parameter im Post-Request.

Fazit

Wir konnten also mit Spring-Boot einen Webserver erstellen und unseren funktionalen Anwendungskern daran anbinden. Das ist schon mal gar nicht schlecht und in manchen Situationen vielleicht eine gute Alternative zum Play-Framework oder anderen Scala-Bibliotheken.

In einem Folgebeitrag wollen wir uns dann noch anschauen, ob und wie wir Spring Data nutzen können, um den Anwendungszustand in einer Datenbank zu speichern.