Stand: 2012-05-01

Das Grauen Grails

Das von Ruby on Rails inspirierte Webframework Grails erscheint auf den ersten Blick, besonders für Java-Entwickler, verlockend, aber der erste Eindruck täuscht.

Inhalt

Stärken

Grails wäre nicht bekannt geworden, wenn es keine Stärken hätte. Auch wenn dieser Artikel eher Kritik an Grails ist, sollen die Pluspunkte nicht unter den Teppich gekehrt werden:

Zähe Arbeitsweise

Grails ist angetreten, die Entwicklungsgeschwindigkeit und Leichtigkeit von Ruby on Rails auf die JVM zu bringen. Zumindest verglichen mit typischen Java-Frameworks erscheint Grails auf den ersten Blick leichtgewichtig: Groovy wirkt wie ein modernes Java, die Bestandteile des Frameworks sind vorkonfiguriert und man kommt schnell zu ersten Ergebnissen. Doch je länger man mit Grails arbeitet, desto mehr trübt sich das Bild.

Während der Arbeit ruft der Entwickler immer wieder Grails-Kommandos zum Anlegen von Klassen, Starten der Anwendung, Ändern der Versionsnummer, Installation von Plugins und so weiter auf und jedes einzelne dieser Kommandos wird quälend langsam ausgeführt! Weil bei jedem Aufruf viele unnötige Schritte im Hintergrund ausgeführt werden, dauert selbst das simple Ändern der Versionsbezeichnung einige Sekunden, obwohl dabei nur eine Zeichenkette geändert wird.

Theoretisch sollte die Entwicklung durch sofortige Wirksamkeit von Änderungen im Quelltext beschleunigt werden. Insbesondere bei Änderung von sogenannten Domainklassen (fachliche Klassen wie z. B. Kunde, Auftrag usw.) funktioniert das aber leider oft nicht und macht einen wiederum zähen Neustart der Anwendung nötig. Bei Änderungen, die nicht die Datenbankstruktur betreffen, funktioniert das automatische Neuladen meistens, aber man kann sich ohne Neustart nie ganz sicher sein, ob auch wirklich alle Änderungen wirksam geworden sind. In manchen Fällen hilft nicht mal ein Neustart der Anwendung; dann muss man erst grails clean aufrufen. Da man wegen der langen Startzeit Grails nicht ständig auf Verdacht neustarten will, wird man das nur bei kuriosen Fehlern auf Verdacht tun.

Mit der flüssigen Arbeitsweise bei Ruby on Rails oder Django kann Grails nicht konkurrieren.

Fehleranfälligkeit und Fehlersuche

In jeder Software gibt es Fehler. Aber die eingesetzte Technologie sollte nicht nur helfen, sie soweit wie möglich zu vermeiden, sondern sie auch leicht auffindbar machen. Grails gibt sich keine Mühe, unzulässige Konstrukte zur Übersetzungs- oder Laufzeit zu erkennen. Die Fehlersuche ist bei Grails eine Qual!

Die dicken Java-Frameworks wie Spring und Hibernate und die mit Java umständlich umgesetzte Dynamik von Groovy führen schnell zu Fehlermeldungen von einigen hundert Zeilen. Grails versucht zwar, auf der Fehlerseite nur den relevanten Teil anzuzeigen, aber das gelingt oft nicht. Wenn man nicht schon einen konkreten Verdacht hat, lassen einen die Fehlermeldungen häufig ratlos zurück und führen manchmal sogar in die Irre. In einigen Fällen wird als Ort des Fehlers sogar die Zeile "-1" ausgegeben, womit das Framework wohl sagen will: "Ich weiß es auch nicht!"

Gute Fehlermeldungen wären aber dringend nötig, da Groovy dynamisch typisiert ist und Grails viel Gebrauch von der Dynamik macht, weshalb der Compiler viele Fehler nicht finden kann. "Schwarze Magie" führt außerdem dazu, dass auch der Entwickler oft nicht nachvollziehen kann, was mit dem Code zur Laufzeit passiert.

Fehler kann man unter anderem deshalb leicht machen, weil Beziehungen zu Klassenelementen nur über ihren Namen als Zeichenkette bestehen:

class SystemUser {

    String logname
    String firstName
    String lastName
    String email
    Password password = new Password()
    SortedSet organisations
    Set permissions = new HashSet()

    // Verweise auf die Felder der Klasse mit Strings

    static hasMany = [userGroups: UserGroup, organisations: Organisation]
    static embedded = ['password']
    static mapping = {
        userGroups(lazy: false)
    }
    static transients = ['permissions']

    static constraints = {                             
        logname(size: 1..100)
        firstName(blank: false, maxSize: 50)
        lastName(blank: false, maxSize: 50)
        email(maxSize: 100)
    }
}

Für den Compiler sind die Namen dieser "weichen" Verweise nur Zeichenketten, weshalb er nicht feststellen kann, ob sie zu den Feldern der Klasse passen. Die Wiederholung der Namen ist außerdem unnötig und ein Verstoß gegen das DRY-Prinzip.

Weniger fehleranfällig und übersichtlicher wäre es mit Java-EE-Annotationen:

class SystemUser {

    @NotNull @Size(min=1, max=100)
    String logname

    @NotNull @Size(max=50)
    String firstName

    @Embedded
    Password password = new Password()

    @ManyToMany
    SortedSet organisations

    @ManyToMany(fetch=FetchType.EAGER)
    Set userGroups

    @Transient
    Set permissions = new HashSet()
}

Durch die Annotationen werden die unnötigen Wiederholungen der Feldnamen und das Problem, dass dabei ein Fehler gemacht werden kann, vermieden. Vorteilhaft ist bei Annotationen auch, dass zusammensteht, was zusammengehört.

Dass es selbst mit dynamischer Typisierung eleganter als bei Grails und ohne Wiederholung von Zeichenketten geht, beweist Django:

class SystemUser(models.Model):
    first_name = models.CharField(max_length=50)
    user_groups = models.ManyToManyField(UserGroup)

Eine weitere Fehlerquelle neben den weichen Referenzen sind die Erweiterungen der Klassen zur Laufzeit. Jede Domainklasse bekommt zum Beispiel ein Feld version mit dem die optimistische Sperrung von Datensätzen umgesetzt wird. Wenn man nun aber aus anderen Gründen ein Feld version benötigt und ihm auch diesen Namen gibt, führt dies zu einem Konflikt und irritierenden Fehlermeldungen. Man muss also wissen, was Grails im Hintergrund treibt.

Eine unnötige Inkonsistenz sind die Namen von Controllern: im Klassennamen muss das Wort Controller angehängt werden, wohingegen im URL-Mapping der Controller ohne dieses Wort und noch dazu klein geschrieben werden muss.

Es ist also relativ leicht, mit Grails Fehler zu machen, aber es ist schwer, sie anhand der Fehlermeldungen zu finden. Auch der Debugger ist wegen der aufwändig umgesetzten Groovy-Dynamik keine große Hilfe und funktioniert in NetBeans (6.9) praktisch überhaupt nicht.

Ärgerlich sind auch die Fehler, die mit Grails ausgeliefert und ewig nicht behoben werden. Die Grails-Konsole ist auch seit Anfang an so instabil, dass sie schon bei kleineren Fehlern abstürzt und sie somit zum Experimentieren kaum zu gebrauchen ist. Leider wird bei der Entwicklung von Grails nicht konsequent zwischen Bugfixes und neuen Funktionen getrennt, so dass mit der Behebung alter Fehler auch öfter neue Fehler durch neue Funktionen hinzukommen.

Nachvollziehbarkeit

Eins der größten Probleme bei der Nutzung von Grails ist, dass man als Entwickler oft nur schwer nachvollziehen kann, was unter der Haube geschieht - was wegen einiger Merkwürdigkeiten ab und zu nötig wäre. Wie Objekte zur Laufzeit verändert werden, ist zum Beispiel kaum dokumentiert. Bei Ruby on Rails ist besser dokumentiert, wie Objekte zur Laufzeit manipuliert werden.

Als besonders schlechten Stil empfinde ich die Umsetzung der Dependency Injection (DI) in Grails. Am saubersten wäre DI mit Konstruktorparametern, denn da ist offensichtlich, was ein Objekt für seine Arbeit benötigt und wie diese Referenzen gesetzt werden. Bei Grails kann man dagegen einfach eine Instanzvariable deklarieren, die dann anhand ihres Namens mit einer Implementierung versehen wird. Damit ist schwerer ersichtlich, welche Abhängigkeiten eine Klasse hat und bei Umbenennungen sind Fehler vorprogrammiert. Das ist ein weiteres Beispiel dafür, wie für kosmetische Verkürzung des Codes die Robustheit und Klarheit geopfert wird.

Konventionen

Genau wie das Vorbild Ruby on Rails setzt Grails auf “Konvention vor Konfiguration”, was grundsätzlich sinnvoll ist, da Konventionen den Projektaufbau vereinheitlichen und die Entwicklung vereinfachen. Allerdings muss man diese Konventionen nicht nur (möglichst alle) lernen, sondern sie sind teilweise auch seltsam:

Validierung

Erfreulich ist, dass Grails bei fehlerhaften Formulareingaben standardmäßig eine passende Fehlermeldung ausgibt und das betreffende Formularfeld rot hervorhebt. Die Fehlermeldungen sind in mehreren Sprachen als i18n-Dateien verfügbar; die darin verwendeten Namen für die Meldungen muss man aber jedes mal nachschlagen, weil sie keinem nachvollziehbaren Schema folgen:

Noch seltsamer wird es bei den Namen für Meldungen von Command-Objekten, denn da darf, anders als bei normalen Klassen und ohne erkennbaren Grund, kein Paket angegeben werden:

// Normalfall mit Angabe des Pakets
ein.paket.Organisation.name.blank=Name darf nicht leer sein.

// Nur bei Command-Objekten darf kein Paket stehen!
changePasswordCommand.password.minSize.error=Mindestens 8 Zeichen.

Dieses inkonsistente Namenschema ist eine weitere unnötige Fehlerquelle. Kleine Rätsel dieser Art halten einen immer wieder bei der Arbeit auf.

Persistenz mit GORM

Zum Laden und Speichern von Objekten in der Datenbank nutzt Grails Hibernate. Es wird aber nicht direkt das Hibernate-API oder JPA verwendet, sondern ein Grails-eigenes, GORM genanntes, API darüber gelegt. GORM ist an ActiveRecord, das Ruby on Rails verwendet, angelehnt. Active Record ist konzeptionell wesentlich einfacher gestrickt als Hibernate, bietet dafür aber auch nicht so viele Möglichkeiten. Durch die Imitation von Ruby on Rails wird das Potenzial von Hibernate bzw. JPA teilweise verschenkt, ohne dass GORM eine Vereinfachung bringen würde. Zum Beispiel wurde hasMany von Rails übernommen, obwohl die JPA-Annotation @OneToMany vorteilhaft wäre, weil man das direkt an das betroffene Feld schreiben könnte, ohne dessen Namen anderswo zu wiederholen. Nebenbei bemerkt sieht hasMany bei Rails auch weniger holperig aus:

# Ruby on Rails
class SystemUser < ActiveRecord::Base
    hasMany :userGroups :organisations
end
// Grails
class SystemUser {
    static hasMany = [userGroups: UserGroup, organisations: Organisation]
}

Besser als eine Active-Record-Immitation würden die Entwurfsmuster Repository und Unit of Work zu Hibernate und damit zu Grails passen. Wie bei einer normalen Collection-Klasse, etwa einer Liste, holt man ein Objekt aus dem Repository (repository.get(id)) und verändert es, ohne es danach explizit zu speichern. Hibernate stellt automatisch fest, welche Objekte sich in der "Unit of Work" geändert haben und speichert diese am Ende der Verarbeitung, ohne dass man save() aufrufen müsste. Repositories bieten außerdem individuellen Datenbankabfragen eine Heimat, die bei Grails oft über die ganze Anwendung bis in die GSP-Vorlagen verstreut werden. Seltsam ist in diesem Zusammenhang die dynamisch zu Domain-Klassen hinzugefügte Methode executeQuery, die eine beliebige HQL-Abfrage ausführt, die unter Umständen überhaupt nichts mit der Klasse, auf der sie aufgerufen wird, zu tun hat:

Customer.executeQuery("select o from Order o")

Die Methode findAll ist hingegen merkwürdig, weil in der HQL-Abfrage from <Domain-Klasse> stehen muss, obwohl dort immer die Klasse stehen muss, auf der die Methode aufgerufen wird. Das wirft die Frage nach dem Sinn dieses Ausdrucks auf und erweckt den Eindruck, dass dort auch etwas anderes stehen könnte, was zu einem Fehler führen würde:

Book.findAll("from Book as b where b.author=?", ['Dan Brown'])

Diese beiden Beispiele zeigen, dass die dynamischen "finder" nicht unbedingt eine gute Lösung sind.

Gegenüber Hibernate pur oder JPA ist GORM eine Einschränkung: Polymorphie mit Interfaces und abstrakten Klassen ist nicht möglich. Eingebettete Klassen können nur genutzt werden, wenn die einzubettende Klassen nicht als eigenständige Dateien im Verzeichnis der Domain-Klassen liegen. Bisher erlaubt es Grails auch nicht, festzulegen ob Hibernate direkt auf die Felder oder auf Methoden zugreifen soll, was in bestimmten Fällen hässliche Umgehungslösungslösungen erfordert. Ein weiterer Nachteil gegenüber JPA ist, dass die Werkzeugunterstützung für GORM ziemlich schlecht ist.

Groovy Server Pages

HTML-Seiten werden in Grails mit Hilfe von GSP-Vorlagen erzeugt. Im Vergleich zu JSP ist das durchaus eine Verbesserung, aber um etwas anderes als HTML-Seiten zu erzeugen, taugt GSP kaum. Ohne HTTP-Request kann die Template-Engine nur umständlich verwendet werden. Zum Erzeugen von E-Mails war in einem meiner Projekte letztlich FreeMarker einfacher und die Vorlagen wurden dadurch sogar kompakter und lesbarer. Außerdem vermeidet FreeMarker den "PHP-Effekt", der zu einer Mischung von Darstellungslogik und fachlicher Logik führt. GSP-Vorlagen verleiten dagegen dazu, zu viel Logik in die Templates zu verlagern. Sogar Datenbankabfragen sieht man in der Praxis öfter darin.

Alltägliche Dinge wie die Abwahl aller Elemente in einem Select-Element werden vom zugehörigen GSP-Tag standardmäßig nicht unterstützt und sind nur mit einem Trick, nämlich einem zusätzlichen versteckten Feld mit dem gleichen Namen, möglich. Auch die Auslagerung der Vorlagen aus der Webanwendung in einen anderen Bereich des Dateisystems ist überraschend schwer.

REST-Unterstützung

Der Architekturstil REST beschreibt den Aufbau von Webanwendungen, die den Grundprinzipien des Webs entsprechen. Die Architektur von Webanwendungen wird dadurch in vielen Fällen einfacher und die Anwendungen skalieren besser und sind vielfältiger Nutzbar. Kurz gesagt: REST ist für Webanwendungen im Allgemeinen erstrebenswert.

Mit Grails kann grundsätzlich "RESTful" entwickelt werden, aber besonders gut ist die Unterstützung durch das Framework dabei nicht. Das fängt schon bei der Abbildung der URLs auf Controller an, die standardmäßig nach dem Muster /controller/action/id erfolgt und Aktionen wie show enthält, die eigentlich schon durch ein HTTP-Verb wie GET übermittelt wird. Einen Schritt weiter ist Ruby on Rails auch in diesem Punkt, denn dort sind die sogenannten "Routen" standardmäßig RESTful.

Nicht RESTful ist in Grails auch der Flash-Scope, der einen Wert bis zum nächsten Aufruf des gleichen Nutzers in dessen Sitzung speichert. Dieser Scope wird in Grails standardmäßig an vielen Stellen verwendet und müsste überall dort ersetzt werden, wenn die Anwendung REST entsprechen soll. Wie wenig Grails darauf ausgelegt ist, HTTP direkt zu nutzen, zeigt sich auch an der Methode sendError, die entgegen ihres Namens auch Erfolgsmeldungen sendet: response.sendError(200,'Ok'). Wenn REST der Standard in Grails wäre, könnte man sich weiterhin Krücken wie allowedMethods sparen. Dieses Attribut eines Controllers gibt an, welche Aktion mit welchem HTTP-Verb aufgerufen werden darf. Falls man sich die Controller automatisch von Grails erzeugen lässt, ist also auch hier wieder Umbau angesagt.

Um REST-Anwendungen für die JVM zu Entwickeln, ist man mit JAX-RS besser dran. JAX-RS unterstützt als reines REST-Framework ausgezeichnet diesen Architekturstil. Allerdings ist es vor der in Arbeit befindlichen Version 2 teilweise etwas mühsam, damit Anwendungen zu entwickeln, die von Menschen im Browser bedient werden sollen.

API

Die Syntax-Varianten für URL-Muster mit und ohne Namen sind völlig inkonsistent:

// ohne Name
"/datei/$file"(controller: 'file', action: 'download')
// mit Name
name file: "/datei/$file" {
        controller = 'file'
        action = 'download'
}

Durch seine Domain Specific Language (DSL) soll Grails kompakten, ausdrucksstarken und einfachen Coder erlauben. Allerdings sind die Konstrukte, die für diese vordergründige Eleganz entstehen, oft nicht besonders einleuchtend. Es ist nicht naheliegend, dass bei der Angabe der Constraints, das betroffene Feld als Methodenname verwendet wird. Ähnlich ist es bei URLs.

Bei Methodennamen wie isEnabled erwartet Grails auch einen Setter und stürzt mit einem Fehler ab, wenn es den nicht gibt.

Wie magische magische Methoden wie list() überschrieben ist nicht offensichtlich. Es gibt keine naheliegende Lösung.

Entwicklungsumgebungen

Wenn ein Projekt größer wird, kommt schnell der Wunsch nach einer ausgewachsenen Entwicklungsumgebung auf, um den Überblick zu behalten und die Arbeit zu erleichtern. Leider sieht es in diesem Punkt bei Grails trübe aus. NetBeans (6.9) ist völlig unbrauchbar und die Groovy- und Grails-Unterstützung scheint auch nicht mehr ernsthaft weiterentwickelt zu werden. Bei Eclipse, insbesondere in Form der SpringSource Tool Suite, sieht es etwas besser aus, aber auch dort gibt es zahlreiche Macken, die zum Beispiel dazu führen, dass Fehler angezeigt werden, die keine sind und umgekehrt. Flüssiges oder gar erfreuliches Arbeiten wird man damit kaum erleben. Lediglich IntelliJ IDEA unterstützt den Entwickler wirklich gut - ist allerdings nicht kostenlos (aber das Geld wert). Die Unterstützung für Refactoring ist aber auch bei IntelliJ nur sehr eingeschränkt vorhanden.

In keinem Fall wird man aber den Komfort erreichen, den eine gute Java-Entwicklungsumgebung wie NetBeans bei Java-Projekten bietet. Hier sind statische typisierte Sprachen klar im Vorteil.

Zukunft

Grails 2 wird ein paar Schwächen beseitigen: So soll zum Beispiel die Grails-Konsole endlich brauchbar werden. Das Neuladen der Anwendung nach Änderungen soll durch einen anderen Mechanismus zuverlässiger funktionieren. GORM wird in der neuen Grails-Version endlich auch abstrakte Klassen unterstützen.

Ob diese Verbesserungen reichen werden, um Grails richtig in Schwung zu bringen, bezweifele ich angesichts der bisher müden Entwicklung. Eine lebendige Gemeinschaft ist aber für den langfristigen Erfolg eines Projekts und die Qualität der Software der entscheidende Faktor. Immerhin gibt es leichten Zuwachs, der aber verglichen mit anderen Frameworks bescheiden ausfällt.

Zugriff auf Grails.org
Quelle: Alexa

Grails Jobtrends
Quelle: Indeed

Die mäßige Resonanz zeigt sich auch auf dem Buchmarkt. Auf Deutsch gibt es außer der Übersetzung des "Definitive Guide to Grails" gar kein ernstzunehmendes Buch. Das einzige deutsche Grails-Buch ist seit der Ankündigung im Jahr 2007 nicht erschienen und wird es wohl auch nie.

Dokumentation

Objekte und Klassen erweitert Grails zur Laufzeit, dieser für die Nachvollziehbarkeit sowieso schon kritische Umstand wird dadurch verschärft, dass kaum dokumentiert ist, welche Änderungen zur Laufzeit stattfinden. Rails dokumentiert das dynamische Verhalten besser.

Auch sonst weist die Dokumentation immer wieder ärgerliche Lücken auf. So ist zum Beispiel die globale Behandlung von Ausnahmen so gut wie undokumentiert.

Fazit

Grails fehlt die Leichtigkeit von Ruby on Rails, es spielt aber auch nicht wirklich die Stärken der Java-Technologien aus, denn moderne Java-Technologien wie JPA oder Bean Validation machen vieles besser als Grails es tut. Einfache Sachen sind mit Grails einfach, schwierige Sachen sind desaströs. Insgesamt hat Grails zu viele Eigenarten, um Spaß zu machen und dauerhaft produktiv zu sein. Grails versucht, eine Java-Version von Rails zu sein, bringt außer unnötiger Komplexität aber nicht viel.