Groovy als Web-Backend

Aus Jonas Notizen Webseite
Version vom 20. Mai 2020, 17:46 Uhr von Admin (Diskussion | Beiträge) (→‎Konfiguration: Angefangen Kapitel "Konfiguration" umzuschreiben. Aufgehört bei "DataSource")
Zur Navigation springen Zur Suche springen

Inhaltsverzeichnis

Einführung

Quellen


Über Grails

Grails Logo

Für die Java-Technologie gibt es eine große Anzahl Web-Frameworks. Diese sind aber meist komplizierter als sie sein müssten, und folgen meist nicht dem DRY-Prinzip ("Don't Repeat yourself").


Dynamische Frameworks wie Django (Python) und Rails (Ruby) haben neue moderne Einblicke gesetzt, wie man über Web-Applikationen denken sollte.

Grails baut auf diesen Konzepten auf und reduziert die Komplexität beim Erstellen von Webanwendungen auf der Java-Plattform erheblich. Das Besondere daran ist jedoch, dass es auf bereits etablierten Java-Technologien wie Spring und Hibernate aufbaut.

Zu den hervor-zu-hebenden Features zählen:

All dies wird durch die Leistungsfähigkeit der Groovy-Sprache und den umfassenden Einsatz domänenspezifischer Sprachen (DSLs) wesentlich vereinfacht.


Installation von Grails 3.3

Installations-Anforderungen

Für Grails 3 wird eine minimale JDK-Version von 1.8 gefordert [1].


Automatische Grails-Installation mithilfe des SDKMAN's-Projekts

SDKMAN! ist ein Werkzeug zur Verwaltung paralleler Versionen mehrerer Software Development Kits auf den meisten Unix-basierten Systemen. Es bietet eine bequeme Befehlszeilenschnittstelle (CLI) und API zum Installieren, Wechseln, Entfernen und Auflisten von Kandidaten. [1]


Nachdem man SDKMAN! mithilfe der offiziellen Anleitung erfolgreich installiert hat kann man mit dem folgenden Befehl Grails auf sein System installieren:

$ sdk install grails 3.3.9

Falls dies die erste Installation von Grails mithilfe des SDKMAN! ist, wird diese Version direkt als Standard in allen Umgebungen gesetzt und auch direkt angewendet.


App erstellen

$ grails create-app APP-NAME erstellt im derzeitigen Verzeichnis die Grundlage für die neue App.

Befehle ausführen

Wenn man jetzt in den erstellten Projekt-Ordner wechselt ($ cd APP-NAME) gibt es 2 Möglichkeiten, Grails-Befehle auszuführen:

  • $ grails BEFEHL
    • Nachteil: Für jeden Befehl muss eine neue JVM-Instanz erstellt werden (= Bei jedem Befehl muss man eine gewisse Zeit extra warten)
    • Nachteil: Keine Auto-Vervollständigung mithilfe der Tabulator-Taste
  • Mithilfe von $ grails in die Interaktive-Konsole wechseln, und dort seine Befehle (Ohne grails vorne dran) ausführen
    • Vorteil: Es wird nur einmal die JVM gestartet / benötigte Projekt und Grails-Daten geladen (= Weniger Ladezeit)
    • Vorteil: Auto-Vervollständigung mithilfe der Tabulator-Taste


D.h.: Wenn man in einer Dokumentation grails <<command>> kann man:

  • Direkt grails <<command>> in seiner Shell eingeben
  • Zuerst mithilfe von grails in die Interaktive Grails-Shell eindringen und danach den <<command>> eingeben.


Grails-Integrationen für diverse IDE's

IntelliJ-IDEA

IntelliJ IDEA ist eine ausgezeichnete IDE für die Entwicklung von Grails 3.0. Sie ist in 2 Editionen erhältlich:

  • der kostenlosen Community-Edition und
  • der kostenpflichtigen "Ultimate" Edition. (Tipp: Es gibt auch kostenlose Studenten-Pakete)

Die Community Edition kann für die meisten Dinge verwendet werden, obwohl GSP-Syntax-Highlighting nur ein Teil der "Ultimate" Edition ist.

TextMate, Sublime, VIM, ...

Siehe offizielle Dokumentation

Konventionen über Konfiguration

"Konventionen über Konfiguration" bedeutet, dass der Ort/Name einer Datei zur Identifikation seiner Rolle/Eigenschaften genutzt wird.
Je nach Speicherort einer Klasse erhält diese spezifische Variablen/Methoden "injeziert". (Beispiel: constraints, hasMany).

Da der Ort einer Klasse entscheidet, wofür/wie sie von Grails genutzt wird, sollte man sich mit dem Aufbau einer Grails-Applikation bekannt werden lassen:

  • grails-app - "Top-Level" Ordner für allerhand Groovy-Ressourcen
    • assets - Statische, unveränderbare Ressourcen wie CSS, Bilder und Javascript (Siehe asset-pipeline Plugin)
      • images
      • javascripts
      • stylesheets
    • conf - Konfigurations-Dateien
    • controllers - Web-Controller-Klassen - Das C im ModelViewControl
    • domain - Domain-Klassen - Das M im ModelViewControl
    • i18n - Nachrichten-Bündel für i18n (Internationalisierung)
    • init - Eingangspunkt der App
      • Application.groovy - Eingangspunkt zum grundsätzlichen starten von Grails (Enthält die main-Methode)
      • Bootstrap.groovy - Beinhält Methoden, die nach dem anfänglichen initialisieren von Grails (Siehe oben) aufgerufen werden. In dieser Klasse steht einem schon die so ziemlich gesamte Grails-Umgebung zur Verfügung (um zB. GORM-Objekte zu erstellen/validieren/löschen/... , E-Mails zu schicken, Konfigurationsdateien zu lesen, ...)
    • jobs - Klassen, die in einem selbst-bestimmten Zyklus aufgerufen werden (Siehe Quartz-Plugin)
    • services - Die Service-Layer für die eigentliche Applikations-Logik (Controller sollten nur die Request/Redirects behandeln)
    • taglib - Tag-Libraries zum erstellen eigener GSP-Tags
    • utils - Hilfsklassen die auf die Grails-Umgebung Zugriff haben?
    • views - Groovy Server Pages - Das V im ModelViewControl
  • scripts - Zum erstellen eigener Grails Befehle, zB. Code-Generierungs Skripts wie generate-all
  • src/main/groovy - (Anderweitig) Hilfsklassen
  • src/test/groovy - Unit und Integrations-Tests
  • build.gradle - (Gradle-)Build Konfigurationen (Für Plugins, Dependencies, ...)


App starten

Grails-Apps können durch den eingebauten Tomcat-Server mit dem Befehl $ grails run-app "von Grund auf" gestartet werden.

  • Standard-Port dieses Web-Servers ist 8080, kann aber durch den Parameter -port=[PORT] abgeändert werden.
  • Unter welcher URL die App erreichbar ist wird in der Konsole angezeigt. (Standardmäßig http://localhost:8080/APP-NAME/ - kann aber auch durch die Laufzeit-Konfigurationsvariable server.contextPath geändert werden.)

App testen

Alle $ grails create-* Befehle generieren eine Unit oder Integrations-Testing Klasse im src/test/groovy Ordner.

Wenn man nun die Logik hinter den Tests hinzugefügt hat, kann man diese mit $ grails test-app starten.

App bereitstellen

Grail-Apps können auf mehrere Weisen bereitgestellt werden.
Wenn man einen Traditionellen Container (Wie Apache's Tomcat, Jetty, etc.) benutzt, kann man ganz einfach ein sog. "Web Application Archive" (.war-Datei) erstellen, welches alle App-Daten in eine Archiv-ähnliche Datei zusammenfasst. (Vergleichbar mit einem .jar-Archiv)

Grails und Tomcat

Grails bindet standardmäßig einen Tomcat-8 Container mit jeder WAR-Datei ein.
Da dies aber zu Problemen führen kann, wenn der Server bereits einen aktiven Tomcat-Container mit einer anderen Version besitzt, kann man diese Abhängigkeit (Dependency) innerhalb der build.gradle-Datei von compile auf provided umstellen:

provided "org.springframework.boot:spring-boot-starter-tomcat"

Die Tomcat-Version kann man ebenfalls unter build.gradle innerhalb der dependencies {}-Sektion wie folgt auf zB. Tomcat-7 ändern:

ext['tomcat.version'] = '7.0.59'

WAR-Datei erstellen und starten

Mit dem Grails-Befehl $ grails war baut Grails die ".war-Version" seiner App im Verzeichnis build/libs des Projekts auf.

Mit dem Java-Befehl java -Dgrails.env=prod -jar [DEINE-WAR-DATEI].war kann man nun ganz einfach seine erstellte .war-Datei ausführen.
Beispiel: java -Dgrails.env=prod -jar build/libs/mywar-0.1.war

Unterstützte Java EE-Container

Grails 3-3 läuft auf jeden Container der die Java Servlet API in der Version 3.0 (oder darüber) unterstützt. Darunter gelten:

  • Tomcat 7
  • GlassFish 3
  • Resin 4
  • JBOSS 6
  • Jetty 8
  • Oracle Weblogic 12c
  • IBM WebSphere 8.0

Generierung eines Applikations-Bereichs (Scaffolding)

Mit Scaffolding kann man einige grundlegende CRUD (Create, Read, Update, Delete)-Schnittstellen für eine Domänenklasse generieren, darunter:

  • Die benötigten Views (.gsp)
  • Die dazugehörigen Controller-Klassen für CRUD-Operationen
  • Die dazugehörigen Unit-Tests

Alle oben genannten Skelette können mithilfe von $ grails generate-all (PACKET.)KLASSEN-NAME ganz einfach erstellt werden.



Konfiguration

Es mag merkwürdig erscheinen, dass man dieses Thema in einem Framework angeht, welches sich dem "Konvention-über-Konfiguration"-Leitfaden widmet.

Mit den Standardeinstellungen von Grails kann man tatsächlich eine Anwendung entwickeln, ohne irgendeine Konfiguration vorzunehmen (wie der schnell-Einstieg zeigte), aber es ist wichtig zu lernen, wo und wie man die Konventionen bei Bedarf außer Kraft setzen kann.


Möchte man dann aber zB. seine eigene MySQL-Verbindung einrichten, kann man dies mithilfe simpler Konfigurationsdateien einstellen.

Grundlegende Konfiguration

Grails Konfigurations-Dateien sind in 2 Sektionen aufgeteilt:

  • Build (build.gradle)
  • Runtime (grails-app/conf/application.yml)

Standard-Variablen

Für die Konfigurations-Dateien stehen folgende Variablen zur Verfügung:

Variable Beschreibung
userHome Ordner-Pfad beim starten des Servers
grailsHome Ordner-Pfad indem Grails installiert ist. (WENN GRAILS_HOME gesetzt wurde, wird diese genutzt)
appName Der Applikations-Name, gelesen von build.gradle
appVersion Die Applikations-Version, gelesen von build.gradle


Grails 2.0 Konfigurations-Syntax verwenden

Wenn man es vorzieht, eine Groovy-Konfiguration im Stil von Grails 2.0 zu verwenden, dann ist es möglich, die Konfiguration mit dem Groovy-ConfigSlurper-Syntax zu spezifizieren.

  • ConfigSlurper ist eine Utility-Klasse zum Lesen von Konfigurationsdateien, die in Form von Groovy-Skripten definiert sind.
    • Wie es bei Java *.properties-Dateien der Fall ist, erlaubt ConfigSlurper eine Punktnotation. Zusätzlich erlaubt es aber auch Closure-Scoped Konfigurationswerte und beliebige Objekttypen.

Zwei Groovy-Konfigurationsdateien sind hierbei verfügbar:

  • Startseite des SDKMAN! Projekts: https://sdkman.io/
  • Konfigurationswerte mithilfe des GrailsApplication-Objekts abrufen

    Um innerhalb von Controllern/Tag-Libraries auf die Laufzeit-Konfiguration zuzugreifen, gibt es eine spezielle öffentliche Variable namens grailsApplication vom Typ GrailsApplication.

    Dessen config-Eigenschaft vom Typen grails.config.Config bietet nützliche Funktionen um Werte aus der Konfigurationsdatei zu erhalten:

    class MyController {
    
        def hello(Recipient recipient) {
            // ...
            // Eigenschaft 'foo.bar.max.hellos' (Vom Typen "Integer") abrufen, falls nicht gesetzt "5" nehmen
            def max = grailsApplication.config.getProperty('foo.bar.max.hellos', Integer, 5)
    
            // Eigenschaft 'foo.bar.greeting' (Ohne definierten Typen, aka. "String"), falls nicht gesetzt "Hello"
            def greeting = grailsApplication.config.getProperty('foo.bar.greeting', "Hello")
    
            def message = (recipient.receivedHelloCount >= max) ? "Sorry, you've been greeted the max number of times" :  "${greeting}, ${recipient}"
    
            render message
        }
    }
    

    Die config-Eigenschaft des grailsApplication-Objekts ist eine Instanz der Config-Schnittstelle und bietet eine Reihe nützlicher Methoden zum Auslesen der Konfiguration der Anwendung.

    Insbesondere die getProperty-Methode (siehe oben) ist nützlich, um Konfigurationseigenschaften effizient abzurufen, während der Eigenschaftstyp angegeben wird (der Standardtyp ist String) und/oder ein Standard-Fallback-Wert bereitgestellt wird.


    Zu beachten ist, dass die Config-Instanz eine zusammengeführte Konfiguration ist, die auf dem PropertySource-Konzept von Spring basiert und die Konfiguration aus

    • der Umgebung,
    • den Systemeigenschaften und
    • der lokalen Anwendungskonfiguration (bspw. application.yml)

    liest und sie zu einem einzigen Objekt zusammenführt.


    Das GrailsApplication-Objekt kann auch ganz einfach in service's und andere Grails-Artifakte wie folgt injected werden:

    import grails.core.*
    
    class MyService {
        GrailsApplication grailsApplication
    
        String greeting() {
            def recipient = grailsApplication.config.getProperty('foo.bar.hello')
            return "Hello ${recipient}"
        }
    }
    


    Konfigurationswerte mit Variablen verknüpfen

    Die Value-Annotation von Spring kann dazu verwendet werden, einer Variable zur Laufzeit mit dem jeweiligen Konfigurationswert einzuspeisen:

    import org.springframework.beans.factory.annotation.*
    
    class MyController {
        // Beim starten wird die Variable auf den Wert von der Konfig gesetzt
        @Value('${foo.bar.hello}')
        String recipient
    
        def hello() {
            render "Hello ${recipient}"
        }
    }
    

    Im Groovy-Code müssen Sie für den Wert der Value-Annotation einfache Anführungszeichen um die Zeichenfolge verwenden, andernfalls wird sie als GString und nicht als Spring-Ausdruck interpretiert.


    Logging-Konfiguration

    Standardmäßig wird die Protokollierung in Grails 3.0 mithilfe des Logback-Frameworks (Offizielle Dokumentation) verarbeitet und kann mit der Datei unter grails-app/conf/logback.groovy konfiguriert werden.


    Logger Names

    Grails-Artifakte (Controller, Services, ...) wird automatisch eine log Methode injeziert.


    Vor Grails 3.3.0 folgte der Name des Loggers für Grails Artefakt der Konvention grails.app.<type>.<className>, wobei type für den Artefakt-Typ steht, z.B. Controller oder Dienste, und className für den voll qualifizierten Namen des Artefakts.

    Grails 3.3.x vereinfacht die Logger-Namen. Das nächste Beispiel veranschaulicht die Änderung:

    Logger Name (Grails 3.3.x oder höher)
    Logger Name (Grails 3.2.x or lower)
    BookController.groovy ingrails-app/controllers/com/company OHNE @Slf4j-Annotation com.company.BookController grails.app.controllers.com.company.BookController
    BookController.groovy ingrails-app/controllers/com/company MIT @Slf4j-Annotation com.company.BookController com.company.BookController


    Logging-Konfigurationsdatei bestimmen

    Um zu bestimmen, von welcher Datei Logback seine Konfigurationswerte lesen soll, gibt es 3 Möglichkeiten:

    • Innerhalb der Runtime-Konfiguration:
    logging:
        config: /Users/me/config/logback.groovy
    
    • Mithilfe einer Umgebungsvariable:
    $ export LOGGING_CONFIG=/Users/me/config/logback.groovy
    $ ./gradlew bootRun
    • Mithilfe einer System-Eigenschaft:
    $ ./gradlew -Dlogging.config=/Users/me/config/logback.groovy bootRun


    Anfrage-Parameter im Stacktrace verstecken

    Wenn Grails einen Stacktrace protokolliert, kann die Protokollnachricht die Namen und Werte aller Anforderungsparameter für die aktuelle Anforderung enthalten. Parameter, die in dem Protokoll nicht aufgeführt werden sollen, können innerhalb der Konfigurationsvariable grails.exceptionresolver.params.exclude in Form einer hinterlegt werden:

    grails:
        exceptionresolver:
            params:
                exclude:
                    - password
                    - creditCard
    

    Die Protokollierung von Anforderungsparametern kann ganz abgeschaltet werden, indem der Konfigurationswert grails.exceptionresolver.logRequestParameters auf false gesetzt wird.

    Der Standardwert ist

    • true, wenn die Anwendung im Modus DEVELOPMENT läuft, und
    • false für alle anderen Umgebungen.

    Umgebungs-Abhängige Konfigurationswerte

    Wenn man die Konfigurationen direkt in die Datei einfügt (Am Anfang der Zeile, aka. als Parent-Node) gilt diese für alle gestarteten Umgebungen.


    Die Dateien application.yml und application.groovy im Verzeichnis grails-app/conf können das Konzept der "Konfiguration pro Umgebung" verwenden, wobei entweder YAML oder die von ConfigSlurper bereitgestellte Syntax verwendet wird.

    Als Beispiel betrachte man die folgende von Grails bereitgestellte Standard application.yml-Definition:

    environments:
        development:
            dataSource:
                dbCreate: create-drop
                url: jdbc:h2:mem:devDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
        test:
            dataSource:
                dbCreate: update
                url: jdbc:h2:mem:testDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
        production:
            dataSource:
                dbCreate: update
                url: jdbc:h2:prodDb;MVCC=TRUE;LOCK_TIMEOUT=10000;DB_CLOSE_ON_EXIT=FALSE
            properties:
               jmxEnabled: true
               initialSize: 5
            ...
    

    Das Obige kann in der Groovy-Syntax in application.groovy wie folgt ausgedrückt werden: (Bermeke wie die allgemeine Konfiguration auf der obersten Ebene bereitgestellt wird und dann ein Umgebungsblock pro Umgebung Einstellungen für die Eigenschaften dbCreate und url der DataSource angeben.)

    dataSource {
        pooled = false
        driverClassName = "org.h2.Driver"
        username = "sa"
        password = ""
    }
    environments {
        development {
            dataSource {
                dbCreate = "create-drop"
                url = "jdbc:h2:mem:devDb"
            }
        }
        test {
            dataSource {
                dbCreate = "update"
                url = "jdbc:h2:mem:testDb"
            }
        }
        production {
            dataSource {
                dbCreate = "update"
                url = "jdbc:h2:prodDb"
            }
        }
    }
    

    Befehl in gewünschter Umgebung ausführen

    Tipp: Grails hat die Kababilität, Befehle mit einer gesetzten Umgebung auszuführen: grails <<environment>> <<command name>>. Beispiel: grails test war

    Umgebung programmatisch auslesen

    Innerhalb des Codes, z.B. in einem Gant-Skript oder einer Bootstrap-Klasse, kann man die Umgebung mit Hilfe der Environment-Klasse erkennen:

    import grails.util.Environment
    
    ...
    
    switch (Environment.current) {
        case Environment.DEVELOPMENT:
            configureForDevelopment()
            break
        case Environment.PRODUCTION:
            configureForProduction()
            break
    }
    

    DataSource

    Weil Grails auf Java aufbaut sollte man ein bisschen Verständnis von JDBC (Java Database Connectivity, der Datenbankschnittstelle der Java-Plattform) besitzen.

    Treiber einfügen

    Wenn man eine andere Datenbank als H2 nimmt benötigt man noch einen JDBC-Treiber. Für MySQL wäre dies Connector/J.
    Solche Treiber kommen normalerweise in Form eines Java-Archivs. Am besten fügt man diese Treiber mithilfe eines Maven-Repos in seine dependencies ein: (Gefunden in build.gradle)

    dependencies {
        runtime 'mysql:mysql-connector-java:5.1.29'
    }

    Wenn man die JAR erfolgreich eingebunden hat kann man sich jetzt mit der Weise bekannt machen wie Grails Datenbanken-Aktionen verwaltet.

    Datasource Einrichten

    In grails-app/conf/DataSource.groovy wird eingestellt, wie Grails die Domain-Klassen-Objekte speichert.

    Für dataSource stehen folgende Konfigurations-Möglichkeiten zur Verfügung:

    • driverClassName - Klassenname des JDBC-Treibers (zB. "com.mysql.jdbc.Driver")
    • username, password - Login-Daten, um die JDBC-Verbindung aufzubauen
    • url - Die JDBC-URL der Datenbank
    • dbCreate - Gibt an, ob die Datenbank automatisch aus dem Domänen-Modell generiert werden soll: "create-drop", "create", "update" oder "validate".
      • create - Falls sich das Datenbank-Schema verändert hat, wird die Tabelle entleert und mithilfe des neuen Schemas erstellt
      • create-drop - Gleich wie create, leert die Datenbank (!) hingegen bei jedem neustart (Egal ob sich das Schema geändert hat oder nicht, nützlich zum Testen)
      • update - Aktualisiert das Model der Tabelle, falls es sich verändert hat (Die Daten bleiben erhalten, Die Domainklassen-Tabelle muss existieren) Notiz: Solch eine Weise veträgt natürlich nicht viele Änderung des Schemas. (zB. Beim ändern des Namens einer Spalte bleibt die alte Spalte mit den existierenden Daten und die neue wird einfach hinzugefügt)
      • validate - Warnt vor Änderungen, aber verändert das Datenbank-Schema bei Veränderung nicht
    • pooled - Ob man einen Pool von Verbindungen benutzen sollte (Standard: true)
    • type - Die Verbindungspoolklasse, wenn Sie Grails zwingen möchten, sie zu verwenden, wenn mehr als eine verfügbar ist.
    • logSql - Leitet SQL-Logs zum stdout um
    • formatSql - Ob SQL-Logs formatiert werden sollen
    • dialect - Eine Zeichenfolge oder Klasse, die den Ruhezustand-Dialekt darstellt, der für die Kommunikation mit der Datenbank verwendet wird. Informationen zu verfügbaren Dialekten findet man im Paket org.hibernate.dialect.
    • readOnly - Wenn true, ist die DataSource schreibgeschützt, was dazu führt, dass der Verbindungspool für jede Verbindung setReadOnly(true) aufruft
    • transactional - Wenn false, wird die DataSource-Transaktionsmanager-Bean außerhalb der verketteten BE1PC-Transaktionsmanager-Implementierung belassen. Dies gilt nur für zusätzliche Datenquellen.
    • persistenceInterceptor - Die Standarddatenquelle wird automatisch mit dem Persistenz-Interceptor verbunden, andere Datenquellen werden nicht automatisch verbunden, es sei denn, dies ist auf true gesetzt
    • properties - Extra Einstellungen für den DatenSource-Bean. (Siehe Tomcat-Pool Dokumentation sowie die dazugehörige Javadoc)
    • jmxExport - Wenn false, wird die Registrierung von JMX-MBeans für alle DataSources deaktiviert. Standardmäßig werden JMX-MBeans für DataSources mit jmxEnabled = true in den Eigenschaften hinzugefügt.

    NOTIZ

    Anscheinend wird in neueren Versionen von Grails die dataSource in der Datei grails-app/conf/application.yml im YAML-Format eingestellt. Die Konfigurationsmöglichkeiten bleiben aber anscheinend die gleichen, Beispiel:

    dataSource:
        pooled: true
        jmxExport: true
        driverClassName: com.mysql.jdbc.Driver
        url: "jdbc:mysql://localhost:3306/db_mahlzeit"
        dbCreate: "create-drop"
        dialect: org.hibernate.dialect.MySQL5InnoDBDialect
        username: usr_mahlzeit
        password: nesev0_JP
    

    Typische MySQL Konfiguration

    Eine Typische MySQL-Konfiguration wäre:

    dataSource {
        pooled = true
        dbCreate = "update"
        url = "jdbc:mysql://localhost:3306/my_database"
        driverClassName = "com.mysql.jdbc.Driver"
        dialect = org.hibernate.dialect.MySQL5InnoDBDialect
        username = "username"
        password = "password"
        properties {
           jmxEnabled = true
           initialSize = 5
           maxActive = 50
           minIdle = 5
           maxIdle = 25
           maxWait = 10000
           maxAge = 10 * 60000
           timeBetweenEvictionRunsMillis = 5000
           minEvictableIdleTimeMillis = 60000
           validationQuery = "SELECT 1"
           validationQueryTimeout = 3
           validationInterval = 15000
           testOnBorrow = true
           testWhileIdle = true
           testOnReturn = false
           jdbcInterceptors = "ConnectionState;StatementCache(max=200)"
           defaultTransactionIsolation = java.sql.Connection.TRANSACTION_READ_COMMITTED
        }
    }
    

    Komplexere MySQL Konfiguration

    Oder ein Beispiel einer eher komplexeren Konfiguration:

    dataSource {
        pooled = true
        dbCreate = "update"
        url = "jdbc:mysql://localhost:3306/my_database"
        driverClassName = "com.mysql.jdbc.Driver"
        dialect = org.hibernate.dialect.MySQL5InnoDBDialect
        username = "username"
        password = "password"
        properties {
           // Documentation for Tomcat JDBC Pool
           // http://tomcat.apache.org/tomcat-7.0-doc/jdbc-pool.html#Common_Attributes
           // https://tomcat.apache.org/tomcat-7.0-doc/api/org/apache/tomcat/jdbc/pool/PoolConfiguration.html
           jmxEnabled = true
           initialSize = 5
           maxActive = 50
           minIdle = 5
           maxIdle = 25
           maxWait = 10000
           maxAge = 10 * 60000
           timeBetweenEvictionRunsMillis = 5000
           minEvictableIdleTimeMillis = 60000
           validationQuery = "SELECT 1"
           validationQueryTimeout = 3
           validationInterval = 15000
           testOnBorrow = true
           testWhileIdle = true
           testOnReturn = false
           ignoreExceptionOnPreLoad = true
           // http://tomcat.apache.org/tomcat-7.0-doc/jdbc-pool.html#JDBC_interceptors
           jdbcInterceptors = "ConnectionState;StatementCache(max=200)"
           defaultTransactionIsolation = java.sql.Connection.TRANSACTION_READ_COMMITTED // safe default
           // controls for leaked connections
           abandonWhenPercentageFull = 100 // settings are active only when pool is full
           removeAbandonedTimeout = 120
           removeAbandoned = true
           // use JMX console to change this setting at runtime
           logAbandoned = false // causes stacktrace recording overhead, use only for debugging
           // JDBC driver properties
           // Mysql as example
           dbProperties {
               // Mysql specific driver properties
               // http://dev.mysql.com/doc/connector-j/en/connector-j-reference-configuration-properties.html
               // let Tomcat JDBC Pool handle reconnecting
               autoReconnect=false
               // truncation behaviour
               jdbcCompliantTruncation=false
               // mysql 0-date conversion
               zeroDateTimeBehavior='convertToNull'
               // Tomcat JDBC Pool's StatementCache is used instead, so disable mysql driver's cache
               cachePrepStmts=false
               cacheCallableStmts=false
               // Tomcat JDBC Pool's StatementFinalizer keeps track
               dontTrackOpenResources=true
               // performance optimization: reduce number of SQLExceptions thrown in mysql driver code
               holdResultsOpenOverStatementClose=true
               // enable MySQL query cache - using server prep stmts will disable query caching
               useServerPrepStmts=false
               // metadata caching
               cacheServerConfiguration=true
               cacheResultSetMetadata=true
               metadataCacheSize=100
               // timeouts for TCP/IP
               connectTimeout=15000
               socketTimeout=120000
               // timer tuning (disable)
               maintainTimeStats=false
               enableQueryTimeouts=false
               // misc tuning
               noDatetimeStringSync=true
           }
        }
    }
    


    Eigene Scripts

    Mit dem Befehl $ grails create-script NAME kann man sein eigenes Skript erstellen, welches standardmäßig in src/main/scripts/ gespeichert wird.

    description()

    Die description()-Methode wird für die Ausgabe vom grails help Befehl verwendet, um den Nutzern zu helfen.
    Beispiel vom generate-all Befehl:

    description( "Generates a controller that performs CRUD operations and the associated views" ) {
      usage "grails generate-all <<DOMAIN CLASS>>"
      flag name:'force', description:"Whether to overwrite existing files"
      argument name:'Domain Class', description:'The name of the domain class'
    }
    

    Beispiel-Befehl

    Ein Beispiel anhand des create-script-Befehls:

    description( "Creates a Grails script" ) {
      usage "grails create-script <<SCRIPT NAME>>"
      argument name:'Script Name', description:"The name of the script to create"
      flag name:'force', description:"Whether to overwrite existing files"
    }
    
    def scriptName = args[0]
    def model = model(scriptName)
    def overwrite = flag('force') ? true : false
    
    render  template: template('artifacts/Script.groovy'),
            destination: file("src/main/scripts/${model.lowerCaseName}.groovy"),
            model: model,
            overwrite: overwrite
    

    model()

    Wenn man model() mit einem Class/String/File/Resource-Parameter aufruft erhält man eine neue Instanz von der Klasse Model, welche hiflreiche Methoden zur Quellcode-Generierung besitzt.

    Scripts innerhalb eines Scripts aufrufen

    Grails hat schon von Anfang an eine große Anzahl an Skripts, welche man ganz einfach aufrufen kann.
    Der folgende Code ruft den Befehl testApp mit dem Parametern --debug-jvm auf.

    testApp('--debug-jvm')
    










    Die Web-Ebene

    Controller

    Ein Controller verarbeitet Anforderungen und erstellt oder bereitet die Antwort vor. Ein Controller kann die Antwort direkt generieren oder an eine Ansicht (View, .gsp) delegieren.

    Controller erstellen

    Mit dem Befehl $ grails create-controller (PACKET.)KLASSEN-NAME erstellt man das Skelett eines Controllers, welches in grails-app/controllers/APP-NAME/DEIN/PACKET/KLASSEN-NAME.groovy gespeichert wird. (Dieser Befehl ist nur zur vereinfachten Erstellung, man kann es auch manuell oder mit einer IDE machen)

    Aktionen

    Die Standard URL-Mapping-Konfiguration versichert dass der Name des Controllers sowie jede Methode zum entsprechendem URI-Pfad gebunden wird.
    Das folgende Beispiel ist hiermit unter ".../mahlzeit/index" erreichbar:

    package myapp
    
    class MahlzeitController {
        def index() { }
    }
    

    Ein Controller kann mehrere öffentliche Methoden haben, welche sich (Wie oben beschrieben) jeweils zur einer URI binden.

    Standard-Aktion

    Wenn der Nutzer keine bestimmte Aktion in seiner Anfrage stehen hat (Also nur den Namen des Controllers, wie "/mahlzeit/" anstatt zB. "/mahlzeit/login") versucht Grails eine Standard-Aktion ausfindig zu machen, welche diese Anfrage annimmt.

    • Wenn es nur eine Aktion (aka. Methode) gibt, wird diese als Standard-Aktion anerkannt
    • Wenn es eine Aktion namens "index" gibt, wird diese als Standard-Aktion anerkannt

    Alternativ kann man auch eine eigenen Standard setzten, indem man static defaultAction = "DEINE-METHODE" in den Quellcode des Controllers einfügt.

    Scope-Variablen

    "Scope-Variablen" sind HashMap ähnliche Strukturen, in welche man Variablen speichern kann. Für Controller stehen folgende Scope-Variablen zur Verfügung:

    servletContext: Statisch, für alle gleich

    Dieser Scope lässt uns Daten speichern, welche über die gesamte Web-App gleich statisch erreichbar sind.
    Das servletContext-Objekt ist eine Instanz vom Standardmäßigen Java(EE)-Objekt ServletContext.

    Es ist nützlich um:

    • Applikations-Eigenschaften zu speichern
    • lokale Server-Ressourcen zu laden und
    • Informationen vom Servlet-Container zu erhalten.

    session: Für jede Sitzung anders

    Das session-Objekt ist eine Instanz von [2] der Java(EE) Servlet-API.

    Es ist nützlich um Attribute der derzeitigen Sitzung eines Klientens zu speichern, wie zB. der Login (Name/Passwort).

    request: Mitgesendete Informationen

    Das request-Objekt ist eine Instanz von HttpServletRequest der Java(EE) Servlet API.

    Es ist nützlich um:

    • Mitgesendete Request-Header-Felderdaten zu bekommen
    • Anfragen-bezogene Attribute zwischen-zu-speichern und
    • Informationen des aktuellen Klienten zu erhalten

    Grails fügt einige zusätzliche Funktionen zum request-Objekt hinzu, die das standardmäßige HttpServletRequest-Objekt nicht hat, hinzu:

    • XML - Eine Instanz der GPathResult welches erlaubt einkommende XML-Anfragen zu verarbeiten (Parsen) - Nützlich für REST
    • JSON - Eine Instanz des JSONObjects welches erlaubt einkommente JSON-Anfragen zu verarbeiten (Parsen) - Nützlich für JSON und REST
    • isRedirected() - Gibt true zurück wenn diese Anfrage nach einem Redirect fordert.
    • get - Gibt true zurück wenn die Anfrage ein GET-Request ist
    • post - Gibt true zurück wenn die Anfrage ein POST-Request ist
    • each - Implementation von Groovys each-Methode um über gewisse Request-Objekte zu iterieren.
    • find - Implementation von Groovys find-Methode um ein gewisses Request-Objekt zu finden (Ohne komplett richtigen Hashkey-Namen)
    • findAll - Implementation von Groovys findAll-Methode um gewisse Request-Objekte zu finden (Ohne komplett richtigen Hashkey-Namen)
    • xhr - Gibt true zurück wenn die Anfrage ein AJAX-Request ist.

    Würde der Body der Anfrage folgenden XML-Code besitzen:

    <book>
       <title>The Stand</title>
    </book>
    

    Können wir dies ganz einfach mithilfe des request-Objekts serialisieren:

    def title = request.XML?.book.title // Mit dem "?" versichern wir uns, dass wenn die Variable "XML" "null" ist es zu keiner NullPointerException kommt und "title" einfach auch auf "null" gesetzt wird (Groovy feature :D)
    render "The Title is $title"
    

    (params): (Veränderbare,) Mitgesendete CGI Informationen

    Eine veränderbare, mehrdimensionale HashMap von Anforderungsparametern (CGI).
    Obwohl das request-Objekt auch Methoden zum lesen der Anforderungsparametern enthält, ist diese manchmal nützlich für Daten-Bindung an Objekte.

    Nehmen wir an wir haben folgende Anfrage: hello?foo=bar. In unserer Controll-Action "hello" können wir den Wert von foo nun ganz einfach ausgeben lassen:

    println params.foo
    

    flash: Speicher zwischen 2 Anfragen

    Eine temporäre Speicherzuordnung, in der Objekte innerhalb der Sitzung für die nächste Anforderung und nur für die nächste Anforderung gespeichert werden. Die darin enthaltenen Objekte werden nach Abschluss der nächsten Anforderung automatisch gelöscht.

    Mit diesem Muster jk Sie HTTP-Weiterleitungen verwenden (Nützlich für Redirect after Post) und Werte beibehalten, die vom Flash-Objekt abgerufen werden können. Beispiel:

    class BookController {
    
        def index() {
            flash.message = "Welcome!"
            redirect(action: 'home')
        }
    
        def home() {}
    }
    

    Scope eines Controllers

    Neu erstellte Controller haben den Standard-Scope-Wert "singleton". Verfügbare Controller-Scopes sind:

    • prototype (Standard) - Für jeden Request wird ein neuer Controller erstellt
    • session - Für jede Sitzung wird nur ein Controller-Objekt erstellt
    • singleton - Für die gesamte Zeit gibt es nur eine Instanz des Controllers (Wird geraten für Aktionen mit Methoden)

    Dieser Standard-Wert kann man unter grails-app/conf/application.yml wie folgt abändern:

    grails:
        controllers:
            defaultScope: singleton

    Man kann den Scope eines Individuellen Controllers auch manuell ändern, indem man static scope = "DEIN-SCOPE" in den Quellcode des Controllers einfügt.

    Model und Ansicht (Model and View)

    Ein "Model" ist eine Map, die die Ansicht (View) beim Rendern verwendet.

    • Die Schlüssel der Map korrespondieren mit dem Variablen-Namen über dessen sie von zB. einer .gsp-Datei erreichbar sind.

    Es gibt einige Wege um ein Model zu übergeben:

    • Explizit: Wenn seine Methode gleich wie die View-Datei heißt, übergibt man das Model einfach wie beim returnen mit folgendem Syntax: [Var1: Data1, Var2: Data2, ...]
    def VIEW_NAME() {
       [book: Book.get(params.id)] // Gleich wie render(view: "VIEW_NAME", model: [book: Book.get(params.id)])
    }
    

    Eine erweiterte/bessere Weiße wäre es:

    import org.springframework.web.servlet.ModelAndView
    
    def index() {
        // get some books just for the index page, perhaps your favorites
        def favoriteBooks = ...
    
        // forward to the list view to show them
        return new ModelAndView("/book/list", [ bookList : favoriteBooks ])
    }
    

    zu benutzen.

    Zu beachten ist, dass bestimmte Variablennamen in Ihrem Modell nicht verwendet werden können:

    • attributes
    • application

    Views selber selektieren mit der render()-Methode

    In den vorherhigen 2 Beispielen haben wir nirgendwo angegeben an welche view die Map-Daten übergeben werden soll.
    Dies liegt an Grails Konventionen. Im folgenden Beispiel würde Grails unter grails-app/views/book/show.gsp nachsuchen: (Nach dem Schema grails-app/views/CONTROLLER/AKTION.gsp)

    class BookController {
        def show() {
             [book: Book.get(params.id)]
        }
    }
    

    Falls wir aber nicht wollen, das Grails sich den Namen der View-Datei mithilfe des Methoden-Namens sucht, können wir die vielfältige render()-Methode einsetzen:

    def show() {
        def map = [book: Book.get(params.id)]
        render(view: "display", model: map)
    }
    

    (Anmerkung: Wenn Grails keine .gsp-Datei finden kann, sucht es auch nach .jsp-Dateien.)

    Views für Namespaced-Controller selektieren

    Wenn ein Controller einen eigenen namespace gesetzt hat (In diesem Beispiel 'business'), schaut Grails

    • zuerst nach ob die View-Datei unter grails-app/views/business/... zu finden ist
    • und falls nicht sucht es unter dem normalen Namespace grails-app/views/... nach.

    Beispiel:

    class ReportingController {
        static namespace = 'business'
    
        def humanResources() {
            // This will render grails-app/views/business/reporting/humanResources.gsp
            // if it exists.
    
            // If grails-app/views/business/reporting/humanResources.gsp does not
            // exist the fallback will be grails-app/views/reporting/humanResources.gsp.
    
            // The namespaced GSP will take precedence over the non-namespaced GSP.
    
            [numberOfEmployees: 9]
        }
    
    
        def accountsReceivable() {
            // This will render grails-app/views/business/reporting/numberCrunch.gsp
            // if it exists.
    
            // If grails-app/views/business/reporting/numberCrunch.gsp does not
            // exist the fallback will be grails-app/views/reporting/numberCrunch.gsp.
    
            // The namespaced GSP will take precedence over the non-namespaced GSP.
    
            render view: 'numberCrunch', model: [numberOfEmployees: 13]
        }
    }
    

    Redirects

    Aktionen können mithilfe der Controller-Methode redirect() umgeleitet werden. Beispiel:

    class OverviewController {
    
        def login() {}
    
        def find() {
            if (!session.user)
                redirect(action: 'login')
                return
            }
            ...
        }
    }
    

    Auf welche Seite redirect() umleitet, kann man Grails auf mehrere Weisen mitteilen:

    • Der Name der Aktion (Sowie den namens des Controllers, falls sich die Aktion in einem anderen Controller befindet)
    // Redirects to the "index()"-Action in the "home"-Controller
    redirect(controller: 'home', action: 'index')
    
    • URI zu einer Resource relativ vom Kontext-Pfad:
    // Redirect to an explicit URI
    redirect(uri: "/login.html")
    
    • Eine Komplette URL:
    // Redirect to a URL
    redirect(url: "http://grails.org")
    

    Parameter können von einer zu der anderen Aktion mithilfe des params-Arguments übergeben werden:

    redirect(action: 'myaction', params: [myparam: "myvalue"])
    

    Diese werden dann in den Controller-Scope params (Siehe oben) gespeichert.

    Aktionen aneinander-reihen

    chain() fungiert ähnlich wie redirect(), indem es auf eine andere Aktion springt und diese ausführt.
    Chaining hingegen erlaubt es uns das Model beizubehalten und Model-Daten zusätzlich zu übergeben.

    Beispiel:

    class ExampleChainController {
    
        def first() {
            chain(action: second, model: [one: 1])
        }
    
        def second () {
            chain(action: third, model: [two: 2])
        }
    
        def third() {
            [three: 3]
        }
    }
    

    Resultiert beim Aufrufen von "/examplechain/first" in das Model

    [one: 1, two: 2, three: 3]
    

    Wie beim redirect kann man hier auch weitere Daten in das params-Scope einspeisen:

    chain(action: "action1", model: [one: 1], params: [myparam: "param1"])
    

    Die render()-Methode

    Die render()-Methode ist sehr flexibel und kann viele Argumente hingegennehmen (Vom anzeigen eines einfachen Textes bis hin zur render eines Views/Templates.).

    // renders text to response
    render "some text"
    
    // renders text for a specified content-type/encoding
    render(text: "<xml>some xml</xml>", contentType: "text/xml", encoding: "UTF-8")
    
    // render a template to the response for the specified model
    def theShining = new Book(title: 'The Shining', author: 'Stephen King')
    render(template: "book", model: [book: theShining])
    
    // render each item in the collection using the specified template
    render(template: "book", collection: [b1, b2, b3])
    
    // render a template to the response for the specified bean
    def theShining = new Book(title: 'The Shining', author: 'Stephen King')
    render(template: "book", bean: theShining)
    
    //! render the view with the specified model
    def theShining = new Book(title: 'The Shining', author: 'Stephen King')
    render(view: "viewName", model: [book: theShining])
    
    // render the view with the controller as the model
    render(view: "viewName")
    
    // render some markup to the response
    render {
        div(id: "myDiv", "some text inside the div")
    }
    
    // render some XML markup to the response
    render(contentType: "text/xml") {
        books {
             for (b in books) {
                 book(title: b.title, author: b.author)
             }
        }
    }
    
    //! render a JSON ( http://www.json.org ) response with the builder attribute:
    render(contentType: "application/json") {
        book(title: b.title, author: b.author)
    }
    
    //! render with status code
    render(status: 503, text: 'Failed to update book ${b.id}')
    
    //! render a file
    render(file: new File(absolutePath), fileName: "book.pdf")
    


    Data Binding

    "Data-Binding" ist das Verfahren, einkommende Request-Parameter an ein Objekt zu binden.

    • "Data-Binding" sollte auch die Konversion von String auf den jeweiligen Typen übernehmen können, weil alle Request-Parameter ja nur Strings sind aber ein Objekt ja auch Integers (etc) haben kann.
    // Klasse 
    class Person {
        String firstName
        String lastName
        Integer age
    }
    
    // Bindung
    def bindingMap = [firstName: 'Peter', lastName: 'Gabriel', age: 63]
    
    def person = new Person(bindingMap)
    
    assert person.firstName == 'Peter'
    assert person.lastName == 'Gabriel'
    assert person.age == 63
    

    Mit Unterklassen

    Dieser Binder kann auch unterklassen verwalten:

    // Klassen
    class Person {
        String firstName
        String lastName
        Integer age
        Address homeAddress
    }
    
    class Address {
        String county
        String country
    }
    
    // Bindung
    def bindingMap = [firstName: 'Peter', lastName: 'Gabriel', age: 63, homeAddress: [county: 'Surrey', country: 'England'] ]
    
    def person = new Person(bindingMap)
    
    assert person.firstName == 'Peter'
    assert person.lastName == 'Gabriel'
    assert person.age == 63
    assert person.homeAddress.county == 'Surrey'
    assert person.homeAddress.country == 'England'
    

    Mit Arrays

    Auch Arrays sind möglich:

    // Klassen
    class Band {
        String name
        static hasMany = [albums: Album] // One-To-Many Relation von Band-zu-Album (Für Datenbank-Relation wichtige Angabe!)
        List albums 
    }
    
    class Album {
        String title
        Integer numberOfTracks
    }
    
    // Bindung
    def bindingMap = [name: 'Genesis',
                      'albums[0]': [title: 'Foxtrot', numberOfTracks: 6],
                      'albums[1]': [title: 'Nursery Cryme', numberOfTracks: 7]]
    
    def band = new Band(bindingMap)
    
    assert band.name == 'Genesis'
    assert band.albums.size() == 2
    assert band.albums[0].title == 'Foxtrot'
    assert band.albums[0].numberOfTracks == 6
    assert band.albums[1].title == 'Nursery Cryme'
    assert band.albums[1].numberOfTracks == 7
    

    Mit Maps

    Sowie auch mit Maps:

    // Klassen
    class Album {
        String title
        static hasMany = [players: Player] // One-To-Many Relation von Album-zu-Player (Für Datenbank-Relation wichtige Angabe!)
        Map players 
    }
    
    class Player {
        String name
    }
    
    // Bindung
    def bindingMap = [title: 'The Lamb Lies Down On Broadway',
                      'players[guitar]': [name: 'Steve Hackett'],
                      'players[vocals]': [name: 'Peter Gabriel'],
                      'players[keyboards]': [name: 'Tony Banks']]
    
    def album = new Album(bindingMap)
    
    assert album.title == 'The Lamb Lies Down On Broadway'
    assert album.players.size() == 3
    assert album.players.guitar.name == 'Steve Hackett'
    assert album.players.vocals.name == 'Peter Gabriel'
    assert album.players.keyboards.name == 'Tony Banks'
    

    File-Uploads

    .gsp-Code:

    Upload Form: <br />
    <g:uploadForm action="upload">
       <input type="file" name="myFile" />
       <input type="submit" />
    </g:uploadForm>
    • g:uploadForm fügt automatisch enctype="multipart/form-data" zum form-Tag hinzu.

    Nun gibt es viele Möglichkeiten mit dem File-Upload umzugehen. Eine davon ist mithilfe von Springs MultipartFile.
    (Diese Weise ist gut um Dateien zu transferieren und manipulieren weil man direkt die InputStream-Instanz bekommen kann.)

    def upload() {
        def f = request.getFile('myFile')
        if (f.empty) {
            flash.message = 'file cannot be empty'
            render(view: 'uploadForm')
            return
        }
    
        f.transferTo(new File('/some/local/dir/myfile.txt'))
        response.sendError(200, 'Done')
    }
    

    Upload-Größen-Limit von Grails

    Grails Standard-Wert für die maximale Dateigröße liegt bei 128KB. Wenn dieses Limit überschritten wird taucht folgender fehler auf:

    org.springframework.web.multipart.MultipartException: Could not parse multipart servlet request; nested exception is java.lang.IllegalStateException: org.apache.tomcat.util.http.fileupload.FileUploadBase$SizeLimitExceededException

    Dieses Limit kann man in seiner application.yml folgendermaßen anpassen:

    grails:
        controllers:
            upload:
                maxFileSize: 2000000
                maxRequestSize: 2100000
    
    • maxFileSize: Maximale größe einer Datei bei einem Request
    • maxRequestSize: Maximale Größe eines gesamten Requests

    URL-Mappings

    In der Dokumentation ist die Konvention des URL-Mappings (standartmäßig) wie folgt eingestellt: /controller/action/id.
    Dies kann man aber auch sehr leicht für spezielle Seiten in der Datei grails-app/conf/UrlMappings.groovy umändern. Beispiel (URL "/product" auf die Aktion "list()" vom Controller "Product" leiten):

    "/product"(controller: "product", action: "list")
    

    Man kann die URL-Mappings auch verschachteln:

    group "/store", {
        group "/product", {
            "/$id"(controller:"product")
        }
    }
    


    (Explizites) REST-Mapping

    "/books"(resources:'book')
    

    ist gleich wie

    get "/books"(controller:"book", action:"index")
    get "/books/create"(controller:"book", action:"create")
    post "/books"(controller:"book", action:"save")
    get "/books/$id"(controller:"book", action:"show")
    get "/books/$id/edit"(controller:"book", action:"edit")
    put "/books/$id"(controller:"book", action:"update")
    delete "/books/$id"(controller:"book", action:"delete")
    


    // Results in /books/1/authors/2
    <g:link controller="author" action="show" method="GET" params="[bookId:1]" id="2">The Author</g:link>
    

    Redirects

    "/viewBooks"(redirect: '/books/list')
    "/viewAuthors"(redirect: [controller: 'author', action: 'list'])
    "/viewPublishers"(redirect: [controller: 'publisher', action: 'list', permanent: true])
    


    Integrierte Variablen

    "/product/$id"(controller: "product")
    

    Im obrigen Fall wird Grails automatisch beim Besuchen von "/product/2" die Zahl 2 in das params-Scope einsetzen, damit es vom Controller einfach genutzt werden kann:

    class ProductController {
         def index() { render params.id }
    }
    

    Natürlich kann man auch mehrere Variablen verwenden, wie bei diesem Beispiel eines Blogs:

    "/$blog/$year/$month/$day/$id"(controller: "blog", action: "show")
    

    Optionale Variablen

    Indem man ein Fragezeichen am ende des Variabl-Namens anhängt schlägt das URL-Mapping auch zu, wenn der Nutzer diese nicht angegeben hat. Beispiel-Mapping:

    "/$blog/$year?/$month?/$day?/$id?"(controller:"blog", action:"show")
    

    Das obrige Beispiel-Mapping schlägt bei allen folgenden URLs zu:

    /graemerocher/2007/01/10/my_funky_blog_entry
    /graemerocher/2007/01/10
    /graemerocher/2007/01
    /graemerocher/2007
    /graemerocher
    

    Optionale File-Extension

    Wenn man die Datei-Erweiterung auch ganz einfach in einer params-Scope Variable haben möchte, kann man dies durch das anhängen von der Optionalen Variable (.$format)?:

    // Mapping
    "/$controller/$action?/$id?(.$format)?"()
    
    // Controller
    def index() {
        render "extension is ${response.format}"
    }
    

    Selbst "eingepflanzte" Variablen

    Man kann auch selber mithilfe von URL-Mapping Variablen in das params-Scope reinschreiben.

    "/holiday/win" {
         id = "Marrakech"
         year = 2007
    }
    


    Dynamisch aufgelöste Variablen

    Die oben-genannten, selbst setzbaren statischen Variablen sind sehr nützlich.
    Aber manchmal soll sich der Wert der Variable je nach Anfrage anders verhalten können. Dies kann man erzielen, indem man einen eigenen Block zur Variable hinzufügt.

    "/holiday/win" {
         id = { params.id }
         isEligible = { session.user != null } // must be logged in
    }
    

    Im obrigen Fall wird der Code innerhalb der Blöcke erst dann evaluiert, wenn das URL-Matching zugeschlagen wurde, und kann dadurch "mit Logik versehen werden".

    Constraints (Einschränkungen des Typens einer Variable)

    Nehmen wir folgendes Beispiel:

    static mappings = {
       "/$blog/$year?/$month?/$day?/$id?"(controller:"blog", action:"show")
    }
    

    Dieses Mapping funktioniert für:

    /graemerocher/2007/01/10/my_funky_blog_entry
    

    Aber auch für Werte, die wir vllt. nicht vorgesehen haben:

    /graemerocher/not_a_year/not_a_month/not_a_day/my_funky_blog_entry
    

    Dies wäre sehr problematisch und man müsste eine (komplexe) eigene Logik reincoden, damit bestimmte Variablen nur einen bestimmten Wert haben dürfen.
    Zum Glück bieten uns URL-Mappings einen einfachen Weg für diese Einschränkung/Validation:

    "/$blog/$year?/$month?/$day?/$id?" {
         controller = "blog"
         action = "show"
         constraints {
              year(matches:/\\\d{4}/)
              month(matches:/\\\d{2}/)
              day(matches:/\\\d{2}/)
         }
    }
    

    Error-Codes

    Grails lässt uns zudem jeglichen HTTP-Fehlercode auf eine eigene Aktion (Seite) mappen:

    "403"(controller: "errors", action: "forbidden")
    "404"(controller: "errors", action: "notFound")
    "500"(controller: "errors", action: "serverError")
    

    Man kann auch auftauchende Exceptions auf eine eigene Seite mappen:

    "500"(controller: "errors", action: "illegalArgument", exception: IllegalArgumentException)