//

Scala und Spring Boot – geht das gut?

4.7.2017 | 7 Minuten Lesezeit

Scala ist eine der populärsten alternativen Programmiersprachen für die JVM. Funktionale Programmierung, Typinferenz, eine mächtige Collections-Bibliothek und asynchrone und parallele Ausführung sind Kernmerkmale dieser Sprache. Sie hat sich insbesondere im Big-Data- und Data-Science-Umfeld einen Namen gemacht.

Spring hingegen ist eine bewährte Technologie für die Entwicklung, Konfiguration und Ausführung von Java-Applikationen. Es gibt ein breites Ökosystem an Erweiterungen und eine große und aktive Community. Das Projekt Spring Boot ermöglicht stand-alone Spring-Java-Applikationen und eignet sich daher gut als Auführungsumgebung für Microservices.

In Projekten habe ich oft mit beiden Technologien zu tun – allerdings bis dato noch nicht in Kombination. Dieser Artikel entsprang der Idee, einfach einmal praktisch auszuprobieren ob sich dieses Paar gut versteht und in Form eines Demo-Services umzusetzen. Der daraus resultierende Code ist auf GitHub frei verfügbar. Die in dem Artikel gezeigten Code-Ausschnitte sind aus dem Projekt entnommen.

Um das Ergebnis vorweg zu nehmen: Scala und Spring Boot verstehen sich. Man muss nur ein paar Kleinigkeiten beachten, die ich in diesem Artikel zeige.

Ziele

Vor Beginn des Experiments habe ich einige Ziele definiert, die erreicht werden sollen.

  • Sämtlicher Code soll in Scala geschrieben sein – abgesehen von einem Beispiel zur Interoperabilität mit Java
  • Als Build-Tool soll das im Scala-Umfeld populäre SBT eingesetzt werden
  • Spring Boot liefert Ausführungkontext, Dependency-Injection und Web-Server-Funktionalität
  • Der Service soll extern konfigurierbar sein
  • Bonus: zusätzlich soll es mit minimalem Aufwand möglich sein, die Anwendung in einem Docker-Container ausführen zu können

Komponenten

Um einen Überblick zu erhalten, werden die einzelnen Komponenten kurz skizziert. Anschließend werden die relevanten Codeausschnitte vorgestellt.

  • MyServiceConfig kapselt die Konfiguration
  • MyService stellt die fachliche Komponente dar
  • MyServiceController definiert die Web-Schnittstelle
  • MyServiceApplication bildet den Rahmen

Wie bereits in den Zielen festgehalten, soll Spring Boot den allgemeinen Ausführungskontext bereitstellen. Über Dependency-Injection werden die Komponenten miteinander verknüpft. Dafür muss Spring die Abhängigkeiten zwischen den Komponenten ermitteln, die in der folgenden Grafik dargestellt werden.

Abhängigkeitsbaum der Komponenten

Die Konfigurationskomponente MyServiceConfig wird instanziiert und erhält passende Einträge aus der Datei application.yml. Die Servicekomponente MyService benötigt diese Konfiguration. Schließlich arbeiten die beiden Controller MyServiceController und MyServiceJavaController mit dem Service. Das bedeutet, dass die Komponenten im Beispiel „von links nach rechts“ aufgebaut werden müssen.

Anwendungrahmen: MyServiceApplication

Den Einstiegspunkt in die Applikation wird in dieser Klasse definiert. Sie umfasst lediglich die main-Methode, die den Startup-Prozess an Spring delegiert.

16
27
38
49
510
611
7

Entwickler, die Spring mit Java programmiert haben sehen an dieser Stelle direkt eine Besonderheit. Im obigen Code-Beispiel wird ein object und eine class mit selben Namen definiert. Der Grund dafür ist, dass es bei Scala keine statischen Methoden auf Klasseneben gibt. Statische Methoden wie die main-Methode gehören in das Companion-Object zur Klasse. Die Annotation @SpringBootApplication wird hingegen an Klassen erwartet.

Service-Konfiguration: MyServiceConfig

Die Konfiguration des Services wird in der Datei application.yml festgehalten:

1my-service:
2  some-key: "someValue"
3

Es wird ein Namespace my-service angelegt und ein simples Schlüssel-Wert-Paar hinterlegt.

Die Komponente MyServiceConfiguration soll zur Laufzeit diese Konfiguration beinhalten.

18
29
310
411
512
613
7

Über die Annotation @ConfigurationProperties teilt man Spring mit, dass der Konfigurationsnamespace my-service beim Mapping genutzt werden soll. Der Eintrag some-key aus der YAML-Datei soll auf das Feld someKey in der Klasse gemappt werden. Spring erkennt die unterschiedlichen Schreibweisen automatisch.

Hier zeigt sich eine weitere Besonderheit. Spring setzt auf die Verwendung von Getter- und Setter-Methoden für Java-Beans. Scala verfolgt dagegen den Ansatz von unveränderlichen Datenstrukturen. An dieser Stelle stoßen die zwei Konzepte aufeinander. Für die Interoperabilität mit Java hilft die Scala-Annotation @BeanProperty . Sie sorgt dafür, dass bei der Kompilierung der Klasse automatisch die Methoden setSomeKey und getSomeKey hinzugefügt werden. Dadurch erfüllt die Klasse die Bean-Eigenschaft und Spring ist damit zufriedengestellt.

Aufmerksamen Lesern fällt auf, dass die Klasse als case class definiert ist. Case classes sind bei Scala Klassen, die primär als reine Datencontainer genutzt werden, ähnlich zu Java-Beans. In diesem Fall ist es nicht zwingend nötig dies zu tun, eine einfache Klasse hätte es auch getan. Aber um zu zeigen, dass es bei der Klasse nur um Daten und nicht um Logik geht, kann man es hier schon verwenden.

Fachlicher Service: MyService

17
28
39
410
511
612
7

In diesen wenigen Zeilen passiert viel. Zuerst teilt man Spring über die Annotation @Service mit, dass es sich bei dieser Klasse um einen Service handelt. Ungewöhnlich wirkt die Platzierung der Annotation @Autowired in Zeile 8. Bei Java ist es üblich, die Annotation an den Konstruktor der Klasse zu schreiben. Und genau das passiert hier auch. Bei Scala folgt die Konstruktor-Signatur nämlich unmittelbar nach dem Namen der Klasse. Die öffnenden und schließenden Klammern an der Annotation sind nötig, damit der Compiler erkennen kann, wo die Annotation endet und der Konstruktor beginnt. Im Konstruktor selbst, wird per Dependency-Injection die Instanz der Konfiguration übergeben. Sie muss nicht noch zusätzlich einem Feld zugewiesen werden wie bei Java. Bei Scala geschieht dies automatisch.

Update: Michael hat in den Kommentaren darauf hingewiesen, dass die @Autowired-Annotation seit Spring 4.3 nicht mehr verpflichtend ist, sofern es nur einen Konstruktor gibt. Danke für den Tipp, dadurch wirkt die Klasse direkt besser lesbar.

In den folgenden Zeilen wird eine Methode definiert, die einen String zurückgibt und dabei den Wert des Schlüssels someKey aus der Konfiguration nutzt. Die Schreibweise mit dem vorrangehenden „s“ ermöglicht String Substitution und ist ein Feature von Scala.

Web-Schnittstelle: MyServiceController

Letztlich wird ein Controller definiert, der die Web-Schnittstelle zur Außenwelt darstellt und auf dem Pfad „/test“ horcht.

19
210
311
412
513
614
715
816
917
1018
11

In Zeile 10 sehen wir ähnlichen Code wie oben. Ein Unterschied zu Java zeigt sich in Zeile 12 bei der Annotation @RequestMapping. Die Attribute der Annotation verlangen als Werte Arrays. Bei Java reicht es jedoch, bspw. nur path = "/test" zu schreiben. Dies erfüllt genau genommen zwar nicht die erwartete Signatur – der Java-Compiler ist an der Stelle jedoch nachsichtig und kapselt dies automatisch. Der Scala-Compiler ist hier strikter und erwartet direkt Arrays.

Java-Interoperabilität: MyServiceJavaController

Um zu verdeutlichen, dass Java und Scala in einem Projekt problemlos parallel genutzt werden kann, wurde ein zusätzlicher Controller in Java definiert:

111
212
313
414
515
616
717
818
919
1020
1121
1222
1323
1424
1525
1626
17

Dieser horcht auf dem Endpunkt „/testjava“ und gibt eine leicht andere Antwort zurück.

Ausführung

Das Projekt wird mit SBT verwaltet. Wenn alles korrekt eingerichtet ist, reicht zum Starten der Aufruf von sbt run im Wurzelverzeichnis des Projekts. Dabei werden beim erstmaligen Start alle Abhängigkeiten heruntergeladen, der Code kompiliert und die Anwendung gestartet. Im Log erscheinen u.A. diese zwei Zeilen:

2017-07-04 12:13:11.901  INFO 10546 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/test],methods=[GET],produces=[text/plain]}" onto public java.lang.String de.codecentric.microservice.controller.MyServiceController.handleRequest()
2017-07-04 12:13:11.901  INFO 10546 --- [           main] s.w.s.m.m.a.RequestMappingHandlerMapping : Mapped "{[/testjava],methods=[GET],produces=[text/plain]}" onto public java.lang.String de.codecentric.microservice.controller.MyServiceJavaController.handleRequest()

Sie zeigen, das die beiden Controller instanziiert wurden und auf den definierten Pfaden horchen. Da die Controller in dem Abhängigkeitsbaum am Ende stehen, bedeutet dies folglich, dass auch alle anderen Komponenten vorher korrekt erstellt und verknüpft wurden.

Hinweis: für die Entwicklungsphase ist es in Ordnung, das Projekt über SBT direkt zu starten. Für Produktionsumgebungen ist dies allerdings nicht zu empfehlen. Der Grund dafür ist, dass die Anwendung in so in der selben JVM ausgeführt wird, in der SBT gestartet wird. Dies kann deutliche Leistungseinbußen mit sich bringen.

In Produktionsumgebungen werden Dienste heutzutage aus verschiedenen Gründen bevorzugt in Docker Containern ausgeführt. Wie das mit unserem Service ganz einfach geht, wird im folgenden Absatz gezeigt.

Containerisierung

Das Projekt ist so konfiguriert, dass SBT das Plugin sbt-native-packager aktiviert. Dieses sehr nützliche Plugin erlaubt es, Anwendungen in verschiedensten Zielformaten auszuliefern. Das Plugin unterstützt neben einfachen Zip-Dateien, die Linux-Paketformate deb oder rpm, dmg für Mac, msi für Windows oder sogar Docker Images. Letzteres ist für uns hier interessant.

Tipp: Standardmäßig nutzt das Plugin ein auf Debian Jessie basierendes Image mit OpenJDK, konkret openjdk:latest. Dieses Image ist „von Haus aus“ rund 600 MB groß. Wer es etwas leichtgewichtiger mag, kann auf ein alternatives Image ausweichen, wie z.B. das auf Alpine-Linux basiserende openjdk:8-jre-alpine, welches nur rund 80 MB groß ist. Das sbt-native-packager-Plugin ermöglicht dies mit wenig Konfigurationsaufwand.

Um eine Anwendung in einem Docker-Container zu starten, muss zuerst ein Image angelegt werden. Dies erreichen wir mit dem Befehl sbt docker:publishLocal. Das Plugin lädt zuerst ein Basis-Image herunter. Anschließend wird die Anwendung samt Abhängigkeiten und Start-Scripten in das Image installiert. Das daraus enstehende neue Image wird automatisch mit dem Namen des Projekts getaggt und steht uns lokal in Docker zur Verfügung.


...
[info] Successfully tagged springbootscala:0.1
[info] Successfully tagged springbootscala:latest
[info] Built image springbootscala:0.1

Wenn alles gut gegangen ist, ist das Image nun lokal bekannt. Dies können wir testen mit dem Befehl docker images.


REPOSITORY                 TAG                 IMAGE ID            CREATED              SIZE
springbootscala            0.1                 93c757af5b67        About a minute ago   121MB
springbootscala            latest              93c757af5b67        About a minute ago   121MB
...

Jetzt muss nur noch ein Container auf Basis davon erzeugt werden. Dies wird bequem mit docker-compose realisiert. Im Wurzelverzeichnis des Projekts liegt für diesen Zweck bereits eine Datei „docker-compose.yml“. In ihr wird der Service definiert und konfiguriert. Ein Aufruf von docker-compose up bewirkt, dass ein neuer Container angelegt und die Demo-Anwendung darin gestartet wird. Dabei wird der Port 8080 vom Container auf die Hostmaschine gemappt.

Wird nun http://localhost:8080/test oder http://localhost:8080/testjava im Browser aufgerufen, erscheinen die Antworten des Services.

Fazit

Scala in einer Spring-Anwendung zu nutzen scheint auf den ersten Blick ohne nennenswerte Probleme möglich zu sein. Die Spring-Annotationen müssen in Scala-Klassen teilweise an ungewohnten Stellen platziert und leicht anders parametrisiert werden. Spring erfordert, dass die Beans Setter- und Getter-Methoden haben. Dies lässt sich mit Hilfe der Scala-Annotation @BeanProperty mit wenig Aufwand erledigen.

Beitrag teilen

Gefällt mir

1

//

Weitere Artikel in diesem Themenbereich

Entdecke spannende weiterführende Themen und lass dich von der codecentric Welt inspirieren.

//

Gemeinsam bessere Projekte umsetzen

Wir helfen Deinem Unternehmen

Du stehst vor einer großen IT-Herausforderung? Wir sorgen für eine maßgeschneiderte Unterstützung. Informiere dich jetzt.

Hilf uns, noch besser zu werden.

Wir sind immer auf der Suche nach neuen Talenten. Auch für dich ist die passende Stelle dabei.