Continuous Integration mit JavaScript GUI

Es gibt immer mehr JavaScript GUI’s zu Java Backends. Sie haben jedoch ganz unterschiedliche Build-Management Tools. Java Entwicker benutzen oft Apache Maven und JavaScript Entwickler Grunt, um den Build der Applikation zu konfigurieren und automatisieren.

In einem Java Backend werden beim Build zuerst die Abhängigkeiten zu anderer Software über Maven-Repositories aufgelöst und die benötigten Third-party Libraries heruntergeladen. Danach wird die Applikation kompiliert, getestet und für den Deploy auf dem Appliktions-Server in WAR- oder EAR-Dateien gepackt.

Für ein JavaScript Frontend werden beim Build auch Abhängigkeiten aufgelöst und eingebunden. Der Build beinhaltet weitere Schritte wie:

  •  holt third-party JavaScript Tools
  •  JavaScript und CSS Dateien überprüfen
  •  Unit-Tests durchführen
  •  JavaScript und CSS Dateien minifizieren
  •  Alle benötigten Dateien für die Applikation zusammenstellen

Wie bringen wir jedoch diese zwei Welten unter einen Hut? Wie können wir mit einem einzigen Befehl die ganze Applikation, also Backend und Frontend, für den Deploy Builden?

  1. Lösung: Alle Build Tasks von Grunt werden mit Maven gemacht
  2. Lösung: In Maven ein Grunt Plugin verwenden, welches die bestehende JavaScript Build Konfiguration verwendet
  3. Lösung: Der Build wird mit Gradle und einem Grunt Plugin gemacht

Die dritte Lösung verbindet die zwei Welten, ohne dass eine benachteiligt wird. Darum und weil Gradle weniger verbereitet ist als Maven, werde ich hier tiefer auf sie eingehen.

CI mit Gradle-Grunt Plugin

Wer bereits Gradle benutzt, kann das gradle-grunt Plugin verwenden. Dort stehen Gradle alle Grunt Tasks zur Verfügung. Sie haben alle den Prefix „grunt_“. Das Plugin leitet die Aufrufe an das installierte Grunt weiter. Somit muss nicht ein Execute-Task definiert werden. Als Beispiel wird der Grunt test Task so über Gradle benutzt:

$ gradle grunt_test

Für Continuous Integration beider Bereiche können die Tasks in Abhängigkeit gebracht werden. Dafür muss folgendes im Gradle Buildfile konfiguriert werden. Das Gradle Buildfile entspricht der Maven POM-Datei.

clean.dependsOn grunt_clean
build.dependsOn grunt_build
test.dependsOn grunt_test

Damit werden z.B. bei einem Build das Backend kompiliert und das Frontend gebildet. Dazu einfach

$ gradle build

aufrufen.

Mit dem WAR Task kann die ganze Applikation in eine WAR-Datei für den Deploy auf dem Application-Server gepackt werden. Hier die Konfiguration im Gradle Buildfile, um die Frontend-Dateien in die WAR-Datei zu integrieren.

 apply plugin: 'war' 
//--> Web Application Plugin 
war {
from 'dist'
//--> Fügt Frontend Dateien in den Root-Ordner vom Archiv ein
//--> weitere Konfiguration für das Erstellen vom Archiv
}
war.dependsOn grunt_build 
//--> Vor dem Archiv erstellen muss das Frontend gebildet werden

 

Anmerkung: So wird das gradle-grunt Plugin im Grade Buildfile aktiviert.

 

plugins {
    id "com.moowork.grunt" version "0.10"
}

Für die Installation von node und npm müssen die Versionen im Gradle Buildfile definiert werden.

node {
    // version of node
    version = '0.12.7'

    // version of npm
    npmVersion = '2.13.4'

    // enable the automatic download
    download = true
}

Build nach Gradle wechseln

Für Maven Benutzer gibt es Unterstützung, um zu Gradle zu Migrieren, den init Task:

$gradle init

Damit werden von einen bestehenden Maven Build die Konfiguration für Gradle erstellt. Da Gradle auch Maven Repositories verwendet, werden die Abhängigkeiten übernommen. Applikationen mit mehreren Projekten und hierarchischen Abhängigkeiten dazwischen werden richtig abgebildet.
Beim Übernehmen der Abhängigkeiten kann es jedoch vorkommen, dass Library Versionen nicht 100%-ig klar sind. Dazu wird ein Kommentar in das Buildfile geschrieben.

compile(group: 'org.hibernate.javax.persistence', name: 
'hibernate-jpa-2.0-api', version:'1.0.1.Final-redhat-2') {
 /* This dependency was originally in the Maven provided 
scope, but the project was not of type war.
This behavior is not yet supported by Gradle, so this
dependency has been converted to a compile dependency.
Please review and delete this closure when resolved. */
 }

Um die Abhängigkeiten sichtbar zu machen, gibt es analog zum Maven’s „$mvn dependency:tree“ den Befehl „$gradle dependencies“.

compile - Compile classpath for source set 'main'.
 +--- joda-time:joda-time:2.4
 +--- org.jadira.usertype:usertype.core:3.2.0.GA
 | +--- org.slf4j:slf4j-api:1.7.6
 | \--- org.jadira.usertype:usertype.spi:3.2.0.GA
 | \--- org.slf4j:slf4j-api:1.7.6
 +--- org.jboss.spec.javax.faces:jboss-jsf-api_2.1_spec:
2.1.19.1.Final-redhat-1
 +--- javax.enterprise:cdi-api:1.0-SP4.redhat-3
 | +--- org.jboss.spec.javax.interceptor:jboss-
interceptors-api_1.1_spec:1.0.0.Beta1
 | +--- org.jboss.spec.javax.annotation:jboss-
annotations-api_1.1_spec:1.0.1.Final
 | \--- javax.inject:javax.inject:1
 +--- org.jboss.spec.javax.ejb:jboss-ejb-api_3.1_spec:
1.0.2.Final-redhat-2
 +--- org.hibernate.javax.persistence:hibernate-jpa-2.0-api:
1.0.1.Final-redhat-2
 +--- org.jboss.spec.javax.ws.rs:jboss-jaxrs-api_1.1_spec:
1.0.1.Final-redhat-2
 +--- org.hibernate:hibernate-validator:4.3.1.Final-redhat-1
 | +--- javax.validation:validation-api:1.0.0.GA-redhat-2
 | \--- org.jboss.logging:jboss-logging:3.1.2.GA-redhat-1
 \--- org.hibernate:hibernate-entitymanager:4.2.7.
SP1-redhat-3
 +--- xml-resolver:xml-resolver:1.2.redhat-9
 +--- org.jboss.logging:jboss-logging:3.1.2.GA-redhat-1
 +--- org.jboss.spec.javax.transaction:jboss-transaction-api
_1.1_spec:1.0.1.Final-redhat-2
 +--- org.hibernate:hibernate-core:4.2.7.SP1-redhat-3
 | +--- xml-resolver:xml-resolver:1.2.redhat-9
 | +--- org.jboss.logging:jboss-logging:3.1.2.GA-redhat-1
 | +--- org.jboss.spec.javax.transaction:jboss-
transaction-api_1.1_spec:1.0.1.Final-redhat-2
 | +--- antlr:antlr:2.7.7.redhat-4
 | +--- org.hibernate.javax.persistence:hibernate-jpa-
2.0-api:1.0.1.Final-redhat-2
 | +--- org.hibernate.common:hibernate-commons-annotations:
4.0.1.Final-redhat-2
 | | \--- org.jboss.logging:jboss-logging:3.1.2.GA-redhat-1
 | +--- dom4j:dom4j:1.6.1.redhat-6
 | \--- org.javassist:javassist:3.18.1-GA-redhat-1
 +--- org.hibernate.javax.persistence:hibernate-jpa-2.0-api:
1.0.1.Final-redhat-2
 +--- org.hibernate.common:hibernate-commons-annotations:
4.0.1.Final-redhat-2 (*)
 +--- dom4j:dom4j:1.6.1.redhat-6
 \--- org.javassist:javassist:3.18.1-GA-redhat-1

Wenn es unerwünschte Abhängigkeiten gibt, können sie aus dem Build ausgeschlossen werden. Vor allem dann, wenn sie mit anderen Version der gleichen Library Konflikte machen.

compile(group: 'org.jadira.usertype', name: 'usertype.core', 
version:'3.2.0.GA') {
exclude(module: 'hibernate-entitymanager')
 }

Danach die gewünschte Version einbinden:

 compile group: 'org.hibernate', name: 
'hibernate-entitymanager',
version:'4.2.7.SP1-redhat-3'

Auch Apache-Ant Benutzer können den Init Task von Gradle benutzen, um ihren Build Prozess zu Migrieren. Weiter gibt es die Möglichkeit, Ant Buildfiles in Gradle einzubinden und die enthaltenen Tasks mit Gradle auszuführen.
Einbinden:

ant.importBuild 'build.xml'

Die Tasks haben danach den gleichen Namen wie bei Ant. Wenn der Task-Name jedoch bereits durch ein Gradle Plugin besetzt ist, gibt es Probleme. Die können jedoch einfach mit einem Umbenennen der Ant Tasks aufgehoben werden.

Continuous Deployment Pipeline

Wenn diese Automation vom ganzen Build steht, also Backend und Frontend zusammen gebaut und getestet werden, ist Continuous Integration bereit.
Ein nächster Schritt ist die Automatisierung vom Deploy der Applikation für das Continuous Deployment.

Wenn diese Grundlagen bereit sind, kann eine Deployment Pipeline gebaut werden.

Verweis: Gradle Dokumentation

4 Kommentare

  • Jonas Bandi, 13. August 2015

    Vielen Dank für den Post.
    Ich denke die Integration von einem modernen Frontend-Build (grunt, gulp, webpack, jspm …) in ein traditionelles Backend ist zur Zeit ein mühsames Thema, wo es noch keine Best-Practices gibt. Daher sind solche „real-world“ Erfahrungen natürlich sehr wertvoll.

    Ich denke eine grundsätzliche Entscheidunge hier ist die Frage, ob ich immer einen Build laufen lassen muss um Veränderungen im Frontend zu „deployen“, d.h. wenn ich was im HTML/CSS/JS ändere, genügt zu Entwicklungszeiten ein einfacher Browser-Refresh oder muss ich zuerst einen Build anstossen um die Veränderungen im Browser zu sehen?

    Ich denke ihr habt die zweite Lösung gewählt. Leider denke ich, dass das eine der grossen Stärken von Frontend-Enwicklung aushebelt: Die Produktivität, die man durch das rasche Feedback mit simplen Browser-Refresh Zyklen hat geht verlohren, wenn man immer einen Build ausführen muss …

    Was ist eure Erfahrung mit dieser Problematik?

  • Christoph Raaflaub, 13. August 2015

    Merci für das Interesse an diesem Thema.

    Die grundlegende Frage war: Wie bringe ich die zwei Welten für den CI Prozess zusammen?
    Diese Lösung hat den Vorteil, dass sie keinen negativen Einfluss auf die Produktivität der Frontend Entwickler hat.
    Für die Frontend Entwicklung kann entweder das gradle-grunt Plugin benutzt oder direkt grunt verwendet werden, wobei alle gewohnten Funktionalitäten zur Verfügung stehen.
    Dabei ist auch ein watch Task möglich, welcher ein Reload vom Browser macht, sobald sich z.B eine HTML Datei ändert.
    Dies mit den Befehlen „gradle grunt_watch“ oder „grunt watch“.

    Ein Build der gesamten Applikation muss nur für den späteren Deploy auf einem Webserver gemacht werden.

  • Jonas Bandi, 15. August 2015

    Danke für die Antwort.

    Das ist ein sehr interessantes Setup. Ich denke das wäre einen eigenen Blog-Post wert 🙂

    Zu Entwicklungszeiten serviert ihr also die Frontend-Assets (html, css, js) nicht aus dem WAR. Habe ich das richtig verstanden?
    Das Java Backend ist in dem Fall also einfach „nur“ ein REST endpoint und es wird kein serverseitiges Templating genutzt?

    Wie können dann die „Backend-Entwickler“ (falls ihr eine solche Trennung habt) ein UI starten (ich nehme an, diese wollen es aus dem WAR servieren)?
    Checken die Front-End Entwickler die Artefakte des frontend Builds (minifiziertes JS, CSS …) ins Repository ein?

    Seht interssant…

  • Christoph Raaflaub, 20. August 2015

    Genau, für reine JavaScript Frontends, welche auf REST Schnittstellen zugreifen, kann das GUI separat benutzt werden.
    Das braucht entsprechende Umgebungskonfigurationen, ev. über einen Proxy.

    Für serverseitiges Templating gibt es Möglichkeiten einen lokalen Application-Server zu starten, welcher Änderungen zur Laufzeit übernimmt.
    Mittels einer IDE, wie z.B. IntelliJ, kann für die Entwicklung ein Application-Server gestartet werden, ohne ein WAR zu builden. Da wird der kompilierte Code direkt verwendet.
    Normalerweise liegen alle Dateien für das WAR in einem Unterorder vom target Ordner. Der Unterordner hat den Namen der WAR-Datei.

    Im Debug Modus werden Änderungen in den Sourcen kompiliert und in den WAR Ordner abgelegt. Diese stehen kurz danach der laufenden Webseite zur Verfügung.
    Mit diesem Mechanismus können auch Frontend-Dateien entwickelt werden. Dabei definiert der Build vom Frontend, ob die JavaScript Dateien plain oder minifiziert benutzt werden. So können auch generierte CSS benutzt werden.
    Für ein Debugging ist es sinnvoll unveränderte JavaScript Dateien zu benutzen. Minifizierung und Uglify wird später für die Packetierung der gesamten Applikation benutzt.