Ich habe gestern den folgenden, etwas über zwei Jahre alten Kommentar zu Spring gelesen:
Spring is a lovecraftian ballache of unnamable horrors. Spring Boot does a decent job at putting those horrors in a box that, with some luck, you may not need to open (and God help you when you do). Of the commonly used modern web frameworks, Spring and Spring Boot are just aggressively mediocre. The learning process of Spring et al involves being thrashed around by enterprise-tier Java errors until you develop enough scar tissue that you can dodge them like a Dark Souls boss fight. I say this as someone with quite a few years professional experience using it, it's such an uneccessarily convoluted mess that's imposed on junior developers like some weird tech generational trauma.
Nun muss ich zugeben, dass meine Suche zu Meinungen über das Spring-Framework voreingenommen war. Meine Suchbegriffe waren „java spring rant“ oder so ähnlich. Aber meine Güte, dieses Framework macht mich fertig. In Kürze: Unter dem Versprechen, alles einfacher, besser geordnet und wartbarer zu machen wird so viel Komplexität, magisches Verhalten und Abstraktion eingeführt, dass am Ende alles komplizierter geworden ist, man eine overengineertes Klassenhierarchie hat und aufrufenden von aufgerufenem Code so weit getrennt hat, dass man selbst für einfache Vorgänge immer in mindestens vier verschiedene Dateien schauen muss.
Aber fangen wir mal weiter vorne an. Java. Eine Programmiersprache, die ich nie wirklich gemocht habe. In den letzten Jahren wurden ein paar Features eingeführt, die sie ein bisschen erträglicher machen, aber wirklich Spaß gemacht hat sie mir nie. Aber man kann mit ihr arbeiten. Was mir mehr Probleme bereitet sind die Paradigmen, mit denen viele große Java-Projekte entwickelt werden.
Da ist zunächst einmal der Zwang, alles zu entkoppeln. Man solle nie mit der Klasse selbst reden, immer nur mit einem Interface, dann kann man die Implementierung ganz einfach austauschen. I call bullshit. Ich habe jahrelang an einem Javaprojekt gearbeitet. In vielleicht 95 % der Fälle will man eine Implementierung eines Interfaces eh nie austauschen. In den anderen fünf Prozent ist es relativ leicht, die Implementierung auch so auszutauschen. Wenn man doch einmal Polymorphie braucht, kann man sie meist ohne Probleme nachträglich hinzufügen. Aber nein. Das wäre ja zu einfach. Stattdessen muss für alles immer erst ein Interface gemacht werden, für das es dann genau eine Implementierung gibt. Das ist immer eine Menge Boilerplate, eine Menge Indirektion und macht es schwieriger zu verstehen, welche Wege der Code gerade genommen hat.
Und dann kommt Spring. Spring will Independency Injection, aber das muss alles automagisch gehen, also hängt man irgendwelche Annotationen an irgendwelche Klassen und hofft, dass es funktioniert. Wenn es funktioniert, dann gut. Wenn nicht: Viel Spaß mit den Fehlermeldungen, die garantiert nutzlos sind um herauszufinden, was nicht funktioniert, geschweige denn, warum. Ich könnte einfach Instanzen meiner Services manuell hereinreichen, aber nein. Das muss automagisch, mit Reflektion gehen.
Was mich aber diese Woche wirklich auf die Palme getrieben hat, war Transaktionshandling. Spring hat dafür die @Transactional-Annotation. Die hängt man an Funktionen, und dann werden die automagisch in einer Transaktion ausgeführt (per default in einer existierenden, wenn keine existiert, wird eine neue geöffnet). Aber nur, wenn die Funktion aus einer anderen Klasse heraus aufgerufen wird. Wird die Funktion aus derselben Klasse heraus aufgerufen, wird die Transaktionsannotation ignoriert. Warum? Nun, das liegt an der Magie, mit der das gemacht wird.
Jetzt habe ich also eine Transaktion. Bei Transaktionen möchte ich, dass sie zurückgerollt werden, wenn irgendwas schiefgeht. Aber… Mist, Spring föngt die Datenbankexceptions von selber ab, und leitet sie nicht an mich weiter (merkt sie sich aber wohl für den Transaktionsabschluss?). Ich kann also zum Beispiel nicht direkt auf eine constraint violation exception reagieren. Also mache ich ein bisschen manuellen Kram und werfe eine Exception, damit ich weiter oben im Webservice einen ordentlichen 4xx-Statuscode zurückgeben kann. Für den seltenen Fall, dass es beim Transaktionsabschluss tatsächlich zu einer race condition mit Konflikten gekommen sein sollte, nehme ich halt den 500er-Fehler in Kauf.
Aber Pustekuchen. Beim Testen muss ich feststellen, dass meine Exception keinen Rollback ausgelöst hat. Warum nicht? Die @Transational-Annotation rollt nur zurück, wenn eine unchecked exception über die Funktionsgrenzen der annotierten Funktion hinaus fliegt. Checked Excepions? Per default kein Rollback. Aber ich kann extra angeben, dass auch die einen Rollback auslösen. Dann kommt auch der Rollback. Aber anstelle wie gewollte einen 4xx-Fehlercode zurückzugeben, fliegt jetzt eine UnexpectedRollbackException und damit gibt es einen Fehler 500.
Ich habe bis jetzt noch nicht herausgefunden, wie ich sowohl die Transaktion zurückrollen als auch einen von mir ausgewählten Fehlercode zurückgeben kann.
Also kurz zusammengefasst:
- ich muss
@Transactionalverwenden, um Transaktionen zu verwalten, aber das funktioniert nur bei Aufrufen über Klassengrenzen hinweg - Spring hält Datenbankexceptions zurück, ich kriege sie nicht zu sehen
- ich muss trotzdem manuell angeben, welche Exceptions ein Zurückrollen auslösen sollen
- wenn ein Zurückrollen ausgelöst wird, kann ich das nicht graceful machen.
Und das alles soll irgendwie einfacher sein als manuelles Transaktions-Handling.
Ich schiebe die Hauptschuld hier auf Spring, aber auch ein bisschen auf die Java-Kultur, die jahrelang gepredigt hat, Design Patterns bis zum Gehtnichtmehr zu verwenden, alles auf Teufel komm raus zu entkoppeln und alles möglichst flexibel zu halten. Nur, dass diese Flexibilität in vielen Fällen garnicht nötig ist, dafür aber eine Menge Komplexität erzeugt.
Nicht umsonst hat „Everything wrong with Java in a single class“ so weite Kreise gezogen, nicht umsonst stammt das Beispiel aus dem Spring-Framework: AbstractSingletonProxyFactoryBean.
So. Genug geranted. Aber das musste raus, dieses Framework treibt mich einfach in den Wahnsinn.
PS: Der Kunde besteht darauf, dass wir Spring verwenden. Normalerweise würden wir es bei meinem Arbeitgeber nicht verwenden. Wir haben ein paar grundsätzliche Regeln gegen Pattern, die alles einfacher machen sollen, aber genau das Gegenteil erreichen:
- benutze kein ORM
- benutze keine dependency injection
- benutze keine großen Frameworks
- schreibe Interfaces nur, wenn du sie wirklich brauchst
- fange keine neuen Projekte in Java an
Das ist nur ein Teil der Regeln, aber das sind imho die wichtigsten Regeln, die wir in diesem Projekt verletzen.