Scala und Spring Boot – geht das gut?

6 Kommentare

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

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.

6
7
8
9
10
11
object MyServiceApplication {
  def main(args: Array[String]) : Unit = SpringApplication.run(classOf[MyServiceApplication], args :_ *)
}
 
@SpringBootApplication
class MyServiceApplication {}

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:

my-service:
  some-key: "someValue"

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

Die Komponente MyServiceConfiguration soll zur Laufzeit diese Konfiguration beinhalten.

8
9
10
11
12
13
@Component
@ConfigurationProperties("my-service")
case class MyServiceConfig() {
  @BeanProperty
  var someKey: String = _
}

Ü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

7
8
9
10
11
12
@Service
class MyService @Autowired()(serviceConfig: MyServiceConfig) {
  def getMessage: String = {
    s"The service says: '${serviceConfig.someKey}'"
  }
}

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.

9
10
11
12
13
14
15
16
17
18
@Controller
class MyServiceController @Autowired()(myService: MyService) {
 
  @RequestMapping(path = Array("/test"), method = Array(RequestMethod.GET), produces = Array(MediaType.TEXT_PLAIN_VALUE))
  @ResponseBody
  def handleRequest(): String = {
    s"Hallo from a Scala controller! ${myService.getMessage}"
  }
 
}

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:

11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Controller
public class MyServiceJavaController {
 
    private final MyService myService;
 
    @Autowired
    public MyServiceJavaController(MyService myService) {
        this.myService = myService;
    }
 
    @RequestMapping(path = "/testjava", method = RequestMethod.GET, produces = MediaType.TEXT_PLAIN_VALUE)
    @ResponseBody
    public String handleRequest() {
        return "Hallo from a Java controller! " + myService.getMessage();
    }
}

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.

Björn Jacobs

Björn ist seit Mitte 2016 Teil des codecentric Teams. Big Data Software-Entwicklung und Data-Engineering haben ihn in seiner Laufbahn als Entwickler begeistert und liegen im Fokus seiner Tätigkeit. Dabei hat er Erfahrung mit modernen Frameworks wie Spark, Hadoop und Storm gesammelt, sowie die funktionalen Aspekte der Programmierung zu schätzen gelernt.
Mit diesem Wissen realisiert Björn parallele und skalierbare Big-Data-Systeme.

Share on FacebookGoogle+Share on LinkedInTweet about this on TwitterShare on RedditDigg thisShare on StumbleUpon

Kommentare

  • Benedikt Ritter

    7. Juli 2017 von Benedikt Ritter

    Hallo Björn,

    schöner Artikel! Was mich noch interessieren würde ist, wie sich Scala Colletions mit Spring Data JPA Repositories verheiraten lassen. Ist es zum Beispiel möglich ein Repository von Spring generieren zu lassen, welches bei der findAll() Methode eine scala.immutable.Seq zurück gibt?
    Vielleicht hast du das ja auch schon mal ausprobiert.

    VG

    • Björn Jacobs

      7. Juli 2017 von Björn Jacobs

      Hi Benedikt,
      das habe ich tatsächlich noch nicht ausprobiert, aber werde es mal machen und den Artikel ergänzen. Danke für die Anregung. 🙂
      Viele Grüße,
      Björn

  • Michael

    7. Juli 2017 von Michael

    Hi Björn,
    Du kannst die Autowired-Annotationen bei eindeutigem Konstruktor einfach weglassen (Seit Spring 4.3 mein ich).
    Viele Grüße,
    Michael

    • Björn Jacobs

      7. Juli 2017 von Björn Jacobs

      Hi Michael,
      Das wusste ich noch gar nicht. Ich habe es gerade ausprobiert und es funktioniert – toll! 🙂 Ich passe den Artikel dementsprechend an.
      Danke und Viele Grüße,
      Björn

  • Jonas Verhoelen

    11. Juli 2017 von Jonas Verhoelen

    Moin Björn,

    super Idee und Artikel, damit werde ich bei Zeiten auch mal was anstellen schätze ich. Die Kombination scheint mir sehr vielversprechend.
    Mich überrascht ehrlich, dass sich das so einfach verzahnen lies. Super Pionierarbeit 😉

    VG Jonas

    • Björn Jacobs

      11. Juli 2017 von Björn Jacobs

      Hi Jonas,

      besten Dank! 🙂

      Ich werde das Projekt nach und nach um weitere Dinge wie Spring JPA ergänzen. Contributions sind auch immer willkommen. 😉

      Viele Grüße,
      Björn

Kommentieren

Deine E-Mail-Adresse wird nicht veröffentlicht. Erforderliche Felder sind mit * markiert.