Monthly Archives: Februar 2011

Mehrere Grails Anwendungen mit einer gemeinsamen Code-Basis

Für Snäckbox wollte ich heute eine Architekturentscheidung treffen: Lasse ich das Projekt weiter aufblähen oder versuche ich es weiter zu modularisieren. Das Projekt wächst täglich, aber es sind noch lange nicht alle Features implementiert, die ich auf der Liste stehen habe. Ich möchte mich früh genug um eine saubere Architektur bemühen, damit ich später nicht in Schwierigkeiten gerate.

Ich setzte bisher auf die Kombination von Grails, GWT und Maven. Die Modularisierung von reinem Java Quellcode ist mit Maven sehr praktisch, doch wie werden Grails Anwendung modularisiert?

Ganz konkret möchte ich einige Domain Klassen auslagern, um sie gleichzeitig in einer Frontend- und einer Backend-Anwendung zu nutzen, muss diese Klasse den Grails Konventionen gehorchen. Sprich sie muss sich unter grails-app/domain befinden. Sie einfach zu kopieren ist eine denkbar dumme Idee, sie in ein Maven Modul zu packen und in ein Repository zu installieren wird leider nicht klappen. Denn wenn Grails zur Laufzeit die Domain Klassen unter grails-app/domain um dynamische Methoden erweitert, wird meine ausgelagerte Domain Klasse übergangen und verfügt folglich nicht über die save(), delete() und find*() Methoden.

Mir scheint ein Grails-Plugin eine sinnvolle Möglichkeit zu sein, meine Anwendung zu strukturieren. Was mich zu Beginn einwenig stocken ließ ist der automatisierte Build-Prozess, der auf meinem TeamCity Server läuft. Alle Module werden mit Maven gebaut. Diese Linie wollte ich beibehalten und habe einwenig geforscht, wie ich die Brücke zwischen meinem Grails-Plugin, der bestehenden Grails Anwendung und meinem Maven-Buildprozess schlagen kann.

Das Maven Publisher Plugin ist ein guter Anfang. Es bündelt das Plugin und installiert es in ein Maven kompatibles Repository.

Als erstes erzeuge ich ein Grails Plugin:
$ grails create-plugin sb-cms-base

Und installiere das Maven Publisher Plugin:
$ cd sb-cms-base
$ grails install-plugin maven-publisher

Da ich gewohnt bin mit Maven zu arbeiten, habe ich eine pom.xml erzeugen lassen, was sich als Fehler herausgestellt hat. Ich habe die install-Phase um die Ausführung von dem maven-install erweitert und versucht das Modul zu installieren. Das hat leider nicht funktioniert. Für jedes Maven Modul wird die Packaging-Methode angegeben <packaging>jar|war|ear|...</packaging> => zip ist aber nicht dabei. Die Folge ist, dass die pom.xml den Dateinamen des Ziel-Artefakts überlädt. Habe ich also also als Packaging-Methode war angegeben, baut mir das maven-publisher Plugin ein Archiv mit dem Namen sb-cms-base.zip, kopiert wird es aber nach sb-cms-base.war

Da ich dieses Plugin trotzdem automatisiert bauen und installieren will, benötige ich ein geeignetes Build-Werkzeug – z.B. das gute alte Ant 😉

Die build.xml und die ivy.xml kann Grails prima selbst erzeugen:
$ grails integrate-with --ant

Und noch die build.xml um ein neues Target ergänzen, welches das Plugin direkt in das lokale Maven Repository als *.zip installiert

...
<target name="maven-install" depends="-init-grails">
  <grails script="MavenInstall"/>
</target>
...

Hier ist übrigens ein netter Post über das Deployment in ein Remote-Repository.

Nun habe ich zwei Dinge erreicht. Erstens kann das neue Plugin in das bestehende Maven Repository deployed werden und zweitens hat mein TeamCity Server dank der Ant-Integration keine Schwierigkeiten dies für mich zu übernehmen.

Jetzt möchte ich dieses Plugin in einer anderen Grails Anwendung nutzen. Dazu definiere ich in der grails-app/conf/BuildConfig.groovy eine Abhängigkeit zu meinem Plugin und….

grails.project.dependency.resolution = {
  repositories {
    ...
    mavenLocal()
    mavenCentral()
    ...
  }
  dependencies {
    plugins {
      compile 'org.grails.plugins:sb-cms-base:0.1'
    }
  }
}

…, da ich mich auf Maven-Boden befinde, installiere es über die Kommandozeile in die zweite Grails Anwendung:
$ mvn grails:install-plugin -DpluginName=sb-cms-base

(Ich weis nicht genau ob das ein Caching Problem meines IDEA’s, oder ob mir ein Fehler unterlaufen ist, aber in der application.properties fehlte nach der Plugin-Installation der plugins.sb-cms-base=0.1 Eintrag – habe ich folglich ergänzt.)

Nun kann ich beginnen Domain Klassen, Services und weitere Komponenten, die von mehreren Grails Anwendungen genutzt werden sollen, in das Plugin auszulagern. Schön ist, dass sich diese Lösung in den Maven Buildprozess integrieren lässt.

Übrigens, sollte eine Exception kommen, die so etwas als Fehlermeldung enthält…

Cause: The name is undefined.
Action: Check the spelling.
Action: Check that any custom tasks/types have been declared.
Action: Check that any / declarations have taken place.

… dann könnte es an der Ant Version liegen. Grails kann mit 1.8.x nicht umgehen und benötigt eine 1.7.x

Update 1

War eine kleine Herausforderung, aber jetzt wird das Plugin auf dem TeamCity Server gebaut. Grails ist nicht installiert und die Abhängigkeiten werden über Ivy aufgelöst. Ich hatte Schwierigkeiten gehabt Spring Abhängigkeiten aufzulösen und kam nicht weiter. Ein ähnliches Problem wird bei stackoverflow.com beschrieben, aber der Lösungsvorschlag brachte bei mir nichts. Für mich hat die folgende Anpassung funktioniert:

Eine wichtige Veränderung ist der Methodenaufruf ebr() in der BuildConfig.groovy, der das SpringSource Enterprise Bundle Repository hinzuzieht. Zusätzlich habe ich ein paar Abhängigkeiten von vornherein ausgeschlossen. Die jsp-api stellt mein Container bereit, mit den xml-apis kollidiert der mit Groovy gebündelte XML-Parser, und Grails loggt über SLF4J, was das commons-logging vieler Apache Bibliotheken überflüssig macht.

grails.project.dependency.resolution = {
  inherits("global") {
    excludes 'jsp-api', 'xml-apis', 'commons-logging'
  }
  ...
  repositories {
    grailsPlugins()
    grailsHome()
    grailsCentral()
    ebr()
 
    mavenLocal()
    mavenCentral()
    mavenRepo "http://repository.codehaus.org"
    mavenRepo "http://download.java.net/maven/2/"
  }
  plugins {
    runtime 'org.grails.plugins:maven-publisher:0.7.5'
  }
  dependencies {
    ...
  }
}

Die ivy.xml habe ich um einige Grails-Abhängigkeiten erweitert, insbesondere die grails-scripts, ohne die das Ant-Buildskript nicht funktionieren würde.

  <dependencies>
    <dependency org="org.grails" name="grails-bootstrap" rev="1.3.6" conf="build"/>
    <dependency org="org.grails" name="grails-core" rev="1.3.6" conf="build"/>
    <dependency org="org.grails" name="grails-crud" rev="1.3.6" conf="build"/>
    <dependency org="org.grails" name="grails-gorm" rev="1.3.6" conf="build"/>
    <dependency org="org.grails" name="grails-web" rev="1.3.6" conf="build"/>
    <dependency org="org.grails" name="grails-test" rev="1.3.6" conf="build"/>
    <dependency org="org.grails" name="grails-scripts" rev="1.3.6" conf="build"/>
  </dependencies>

Und schließlich habe ich das Target maven-install inder der build.xml um die compile Abhängigkeit erweitert, da Grails sonst keine Chance hat das maven-publisher Plugin zu installieren:

  <target name="maven-install" depends="-init-grails, compile">
    <grails script="MavenInstall"/>
  </target>

Update 2
Es wollte einfach nicht… Irgendwie hat sich die slf4j-api:1.5.2 in das deployte WAR eingeschlichen. Lokal auf einem Rechner nicht, aber auf dem TeamCity Server schon. Grund war das hibernate:1.3.4-Plugin, das eine Abhängigkeit auf die slf4j-api:1.5.2 definiert. Egal was ich gemacht habe, ich habe sie nicht rausbekommen und musste schmerzlich erfahren, dass egal wie viel Zeit Grails + Maven + Ivy auch sparen, ein Großteil davon geht auf die Suche nach mysteriösen Fehlern wieder drauf.

Meine derzeitige Lösung sieht so aus, dass ich slf4j-api:1.5.2 global in der BuildConfig.groovy ausschließe:

grails.project.dependency.resolution = {
  inherits("global") {
    excludes 'jsp-api', 'xml-apis', 'commons-logging', 'slf4j-api'
  }
  ...
  dependencies {
    provided 'org.slf4j:slf4j-api:1.5.2'
  }
}