Spring mit Scala — Ein Resümee

Bei einem Gespräch mit einem Arbeitskollegen, der aktuell die Kombination Spring + Kotlin in einem Projekt einsetzt, ist mir die Idee für diesen Blog-Artikel gekommen — wie gut lassen sich Scala (meine favorisierte Programmiersprache) und Spring (ein großartiges Framework) mittlerweile in einer Anwendung miteinander kombinieren? Bei Kotlin scheint dies nach einhelliger Meinung hervorragend zu funktionieren dank nativem Support seitens Spring. Für diejenigen, die sich auch für Kotlin interessieren, sei hierfür auf diesen Blog-Artikel verwiesen: 7 things any Java developer should know when starting with Kotlin


Es gab zahlreiche Versuche, Erweiterungen für Spring zu schreiben, die den nativen Umgang mit dem Framework aus Scala heraus vereinfachen soll, darunter:

Alle diese Projekte haben gemein, dass sie mittlerweile nicht mehr weiterentwickelt werden, da offenbar das Interesse aus der Community (auf Spring- oder Scala-Seite) bedauerlicherweise nicht ausreichend groß war.

Dieser Artikel soll die Tücken und Untiefen bei der Verwendung von Spring mit Scala aufzeigen, aber natürlich auch, was wirklich hervorragend geklappt hat und welchen Mehrwert eine solche Kombination bietet.

Der Code ist auf GitHub gehostet und frei verfügbar für eigene Projekte: https://github.com/Digital-Frontiers/spring-petclinic-scala

Aufbau

Mein gewähltes Beispiel basiert auf der bekannten Spring Petclinic Anwendung. Die Scala-Implementierung dient als Backend in Form einer REST-Applikation (Vorlage war hierbei das spring-petclinic-rest Projekt). Als Frontend dient das fertige spring-petclinic-angular Projekt.

Einige Informationen zur Implementierung:

  • Nicht alle durch das Frontend erwarteten REST Endpoints sind ausimplementiert oder vollständig funktionstüchtig, da ich in diesem Blog das grundsätzliche Zusammenspiel aufzeigen will.
  • Maven als Build Tool
  • Scala 2.12
  • Spring Boot 2.0
  • Spring Data JPA für die Kommunikation mit der Datenbank (HSQL)

Anwendung

Die Spring Scala Applikation stellt eine REST-Schnittstelle bereit, die von der Angular Applikation konsumiert wird. Repräsentativ für die Anwendung soll der Anwendungsfall Owner skizziert werden.

Über die Oberfläche kann z.B. eine Liste der Besitzer angezeigt werden. Diese Anfrage wird an den OwnerController übermittelt, der über den ClinicService letztlich das OwnerRepository nach einer aktuellen Liste der Besitzer befragt. Technologisch spielen hier die Spring-Projekte spring-mvc und spring-data-jpa eine Rolle.

Spring Application und Configuration

Den Einstiegspunkt bildet die Klasse PetClinicApplication.

Anders als in Java existieren in Scala keine statischen Methoden. Diese werden üblicherweise in Object-Klassen bzw. Companion Objects gekapselt. Scala nimmt hier den Gedanken der Objektorientierung sehr ernst, der durch statische Variablen oder Methoden verletzt wird.

Der Aufruf der run-Methode erfolgt daher in der main-Methode des Companion Objects. Da Spring die Annotation @SpringBootApplication an einer Klasse erwartet und anderenfalls mit einer Exception den Spring Context gar nicht hochfahren wird, fügen wir noch eine Companion Class hinzu, die die Annotation erhält. Diese Klasse verwenden wir auch als unsere Haupt-Configuration-Klasse und fügen benötigte Jackson-Module hinzu (mehr zum DefaultScalaModule später im Artikel).

REST Controller

Der OwnerController definiert den REST-Endpoint für den Tab “Owners” in der Angular-Anwendung.

Hier finden wir einige interessante Stellen, auf die es sich lohnt näher einzugehen.

Das @RequestMapping in Zeile 3 muss den path-Parameter der Annotation tatsächlich als Array wrappen, da die Signatur der Annotation hier String[] als Rückgabewert definiert — in Java optional, in Scala leider Pflicht.

In Zeile 4 definieren wir Class Parameter als Dependencies, die uns Spring per Autowiring bereitstellen soll. Da Class Parameter Teil des primären Konstruktors werden, weisen wir Spring damit an, die Dependency per Constructor Dependency Injection zu injecten. Da Spring darüber hinaus seit Version 4.3 bei eindeutigem Konstruktor kein @Autowired mehr erfordert, können wir uns hier kurz und bündig ausdrücken.

Eine Besonderheit stellt der zweite Parameter (bzw. die zweite Parameterliste) dar, den wir direkt als implicit deklarieren. Damit weisen wir den Scala Compiler an, den ObjectMapper automatisch als Parameter bei Methoden einzusetzen, die diesen als implicit parameter definieren. Dies ist beispielsweise bei folgender Implementierung gegeben, die eine Typkonvertierung von einem beliebigen Typ T (oder einem Traversable[T]) in einen JSON-String implementieren.

Durch die implizite Typkonvertierung sparen wir uns den wiederkehrenden Aufruf des Originalbeispiels objectMapper.writeValueAsString(...) zur Umwandlung des BindingResults. Der tatsächliche Aufruf des JSON Frameworks verschwindet in den beiden oben definierten Methoden. Man hätte hier sicherlich mit Type Classes noch weiter gehen können, aber für’s Erste ist das Ergebnis zufriedenstellend.

Die Deklaration einer zweiten Parameterliste bzw. die Definition des Parameters als implicit ist hierbei völlig unproblematisch, auch wenn Java weder das eine noch das andere Konzept direkt unterstützt. In Java werden mehrere Parameterlisten einfach als eine Parameterliste aus allen Parametern abgebildet. Dazu kommt, dass implizite Parameter ohnehin immer auch explizit mitgegeben werden dürfen. Beides erledigt Spring im Hintergrund bei der Bean-Instanziierung für uns.

Aufmerksamen Lesern wird beim OwnerController aufgefallen sein, dass die Methode getOwners in Zeile 7 ein ResponseEntity[Seq[Owner]] zurückliefert. Anders als in Kotlin sind Scala Collections nicht von Java Collections abgeleitet und können damit nicht ohne Weiteres in bestehenden Java Frameworks verwendet werden. Verantwortlich dafür, dass hier dennoch die Scala Collection Seq anstelle einer Java Collection zurückgegeben werden kann, ist das eingangs erwähnte DefaultScalaModule von Jackson (zu finden unter jackson-module-scala). Dieses Modul lässt uns Scala Typen an Stellen verwenden, die normalerweise Java Typen erwarten, und spart uns an dieser Stelle eine explizite Konvertierung in eine passende Java Collection. Praktisch!

Services

Der ClinicService ist relativ unkompliziert. Wir verwenden wieder Class Parameters für die Dependency Injection.

Der Service bildet den Schnittpunkt zwischen Controllern und Repositories. Er dient zur Definition der Transaktionsgrenzen und delegiert die Aufrufe an die jeweiligen Repositories.

JPA Repositories und Entities

Bis hierhin könnte man den Eindruck haben, dass Scala und Spring auch ohne nativen Support der Sprache durch Spring hervorragend zusammenpassen. Spätestens bei der Verwendung von JPA hört jedoch der Spaß auf, wenn man zum ersten mal JPA aus Scala heraus verwenden möchte. Denn es gilt vieles zu beachten, um funktionierenden und einigermaßen anschaulichen Code zu produzieren. Das OwnerRepository lässt sich in Scala wie folgt darstellen:

Man erkennt auf den ersten Blick die beiden Scala Typen Array und Option als Rückgabewerte für die find-Methoden.

Spring Repository-Methoden, die mehrere Ergebnisse liefern, erwarten normalerweise einen Java Collection-Typ (List<Owner>) oder alternativ einen Array-Typ (Owner[]). Tatsächlich ist die Scala Array-Implementierung aber kompatibel mit Java Arrays, sodass wir hier Array[Owner] verwenden können. Hierdurch gewinnen wir zugleich Zugriff auf die vielen Collection-Methoden aus der Scala-Welt (map, flatMap, filter, sortBy, …).

Seit Spring Data 2.0 kann für Repository-Methoden, die null zurückliefern können, auch der Scala Option-Typ verwendet werden, sodass wir hier ohne Konvertierung auch Features wie Pattern Matching nutzen können.

Eine wichtige Anmerkung zu dem gewählten Basis-Interface Repository: Wir leiten unser Repository hier explizit nicht von CrudRepository ab, um Konflikte mit den vorhandenen Methoden findAll, findById, … zu umgehen, die java.lang.Iterable bzw. java.util.Optional als Rückgabewerte definieren.

Um die passenden Methodensignaturen für eine reibungslose Verwendung von JPA Repositories vorzugeben, definieren wir das Trait ScalaJpaAdapter.

Zuletzt definieren wir noch die JPA Entity Klasse Owner, die das OR-Mapping über entsprechende Annotationen deklariert. Der JPA-Standard sieht eine ganze Reihe von Konventionen für Persistence Entities vor, an die wir uns auch aus Scala heraus halten müssen (mehr dazu unter dem Link JPA Entities).

Für die vorliegende Entity bedeutet dies, dass getter und setter für Properties nach Java Bean-Konvention und ein No-Arg-Konstruktor existieren müssen. Darüber hinaus können wir keine Scala Collections verwenden, da explizit nur die Collection-Typen Collection, Set, List und Map aus java.util erlaubt sind.

Für die Owner-Klasse verwenden wir eine Case Class, da Entities an sich reine Value Objects darstellen, die keine eigene Business-Logik aufweisen.

Die Annotation @BeanProperty an den Feldern sorgt dafür, dass der Java Beans-Konvention genüge getan ist. Diese generiert für uns passende getX- und setX-Methoden.

Der Default-Konstruktor in der Case Class mutet sehr ungewöhnlich an, ist jedoch so von JPA vorgegeben. Da der Konstruktor ausschließlich von JPA verwendet werden soll, kann dieser bedenkenlos mit dem Scope protected versehen werden.

Überraschenderweise funktioniert der Ansatz, java.util.List durch scala.Array zu ersetzen, bei den Entity Properties nicht. Zumindest Hibernate verlangt daraufhin, dass diese Felder mit @OrderColumn annotiert werden, um eine persistente Listen-Reihenfolge zu definieren. Da das nicht unbedingt immer gewollt ist (und im konkreten Fall dennoch nicht funktionierte), kann der Workaround hier nicht verwendet werden.

Ausführung

Die Scala Spring-Anwendung kann grundsätzlich aus der IDE heraus gestartet werden.

Die Angular-Frontend-Applikation lässt sich aus dem Angular-Projektordner heraus via “ng serve” starten. Für die Installation der NPM-Pakete sei auf die GitHub-Seite des Angular-Projekts verwiesen.

Sofern beide Anwendungen korrekt gestartet wurden, befindet sich unter der Adresse “localhost:4200” die Angular UI.

Über den Tab Owners können wir unseren Owner REST-Controller ansprechen. Da die restlichen Controller nicht ausspezifiziert sind, bleiben die anderen Tabs leer.

Rückblick und Ausblick

Im Großen und Ganzen lassen sich Spring und Scala sehr gut integrieren, besser als ich zu Beginn des Blog-Posts erwartet hätte.

Wenn man darüber hinweg sieht, dass der Scala Compiler bei Annotationen mit String-Arrays etwas strikter agiert, wirken vor allem Class Parameter und Constructor Dependency Injection hervorragend zusammen. Auch das jackson-module-scala liefert im REST Controller einigen Komfort, da Scala-eigene Collection-Typen als Rückgabewerte verwendet werden können.

Nur am Beispiel JPA Repositories und Entities sehen wir ein Problem, das beispielsweise Kotlin so nicht kennt. Scala Collections erweitern, anders als in Kotlin, keine Java Collections, sodass die Interoperabilität mit Frameworks, die sehr stark auf Java und Java-Konvention gemünzt sind, vergleichsweise schwierig ist.

JPA Entities erwarten eine Java Bean Konvention, die Scala bewusst nicht gewählt hat. Dies führt dazu, dass zusätzlicher Aufwand betrieben werden muss, um wieder dieser Konvention zu entsprechen. Die Verwendung von Case Classes (bzw. Class Parameter) sowie die @BeanProperty Annotation erleichtern dies. Letztlich entstehen jedoch Klassen, die nicht mehr gutem Scala Sprachstil entsprechen. Man würde sagen: Diese Implementierung der Case Classes ist nicht (scala-)idiomatisch. Auf Programmiersprachen umgedeutet bedeutet idiomatisch, dass Entwickler, die mit der Sprache sehr vertraut sind, die Implementierung der JPA Entities in Scala nicht als natürlich und üblich ansehen würden.

Zumindest lassen sich der scala.Option und der scala.Array Typ in Spring Data JPA Repositories verwenden, wodurch bereits einiges gewonnen ist. Zum jetzigen Zeitpunkt wird man mit diesem Ergebnis leben müssen, wenn man Spring Data JPA verwenden möchte. Ein bezahlbarer Preis, wenn man bedenkt, was das Framework einem sonst alles abnimmt.


Zuletzt kann ich nur sagen: Vielen Dank für’s Lesen. Ich freue mich stets über Feedback, Diskussionen und Kommentare! Vielleicht hat ja der eine oder andere bereits positive Erfahrungen bei der Kombination von Spring und Scala in Projekten sammeln können.