Spring-Boot mit Scala
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.