Die Situation

Es bestehen zwei lauffähige, fertige Projekte in Maven, welche beide auch vollwertige Artefakte bilden können.

Zum einen die Webanwendung – nennen wir sie hier mal webportal – mit einem WAR-Artefakt, etwa für einen Tomcat. Ob das Artefakt als Snapshot oder Release gemacht wird, ob es nur generiert oder auch in ein Repository deployt wird, ist hierbei nicht von weiterer Bedeutung.

Zum anderen die normale Clientanwendung – nennen wir sie doch einfach userclient – mit einem JAR-Artefakt. Wichtig ist natürlich, dass hier eine startfähige Main-Klasse vorhanden ist. Dies sollte jedoch der Regelfall sein, daher nur der formale Hinweis.

Zusammengefasst, und der Auftrag an dieses Howto ist also: Wie konfiguriert und erweitert man den Buildzyklus in welchem Projekt an welcher Stelle am geschicktesten, um auf einfachem Wege die JAR-Datei userclient in die Webanwendung webportal als Java Webstart zu integrieren. Dabei ist es hilfreich, wenn man via pom.xml (und natürlich der Macht der Properties) die Versionen spezifizieren kann. Der gesamte Prozess von auswählen, signieren und ausliefern soll dabei automatisch und ohne weiteres Eingreifen des Entwicklers geschehen können. Idealerweise sollte das ganze auch optional sein – dazu gibt es dann mehr unter “Optimierungen und Verbesserungen”.

Die Grundlagen

Ich möchte jetzt nicht viel über die Grundlagen verlieren, nur so viel sei gesagt. Aus Sicht von Maven ist die Konfiguration “nur” ein weiteres Plugin von vielen. Daraus ergibt sich natürlich auch die Möglichkeit, via Properties oder Profilen die Konfiguration sehr dynamisch zu halten. Webstart ist eine Technik, die an sich nichts mit Maven zu tun hat. Man kann auch “händisch” seine Jar-Datei über eine URL verfügbar machen. Man muss dann jedoch alles selber und richtig machen: Versionisierung, Signierung und Deployment. Die Signierung ist gerade dann wichtig, wenn die Applikation erweiterte/volle Rechte benötigt.

Die Möglichkeiten

Es gibt eine Reihe von Maven-Plugins, die einen unterschiedliche Ansätze und Lösungen bieten.

Das Maven Jar Plugin bietet etwa unter anderem die Möglichkeit, die Jar-Dateien zu signieren. Dies wäre jedoch nur die halbe Miete, da weder die Jar-Dateien bekannt noch auslieferbar sind. Auch das Keytool plugin ist nicht vollständig, denn es unterstützt zwar das Keystore gestützte signieren, aber auch nicht mehr. Wohl aber können diese Plugin als Basis für andere dienen.

Weitaus mehr Funktionen kann das Webstart Maven Plugin bieten. Es gibt verschiedene Arten der Erzeugung der Jnlp-Datei und dem damit verbundenen Sammeln, Bestimmen und Signieren der Abhängigkeiten in Form weiterer Jars oder Classfiles. Es ist auch ein hilfreiches Plugin, wenn man aus einem Projekt heraus direkt Jnlp & Ressourcen erzeugen will.

In diesem Howto ist aber vor allem ein Ziel von Relevanz: Die Variante mit dem Jnlp-Download-Servlet: das Goal webstart:jnlp-download-servlet.

Dieses Goal erzeugt im Buildprozess ein weiteres Verzeichnis mit den Ressourcen des userclients und der dazugehörenden Jnlps. Ein spezielles Servlet übernimmt die Auslieferungen.

Die Konfiguration im Projekt userclient

Das Projekt muss und sollte nicht “wissen”, dass es über Webstart irgendwo eingebunden wird. Daher sind keine Änderungen nötig. Wohl aber muss bekannt sein, wie die aktuelle bzw. die gewünschte Version ist und ggf. das Maven-Repository der Deploys.

Die Konfiguration im Projekt webportal

Das webportal muss hingegen durchaus von der Existenz des userclients wissen, denn jenes es soll ja in diese Webanwendung integriert werden. Die Konfiguration besteht im Wesentlichen aus drei Schritten: JnlpDownloadServlet-Abhängigkeit einfügen, Servlet in der web.xml registrieren und Build-Plugin in der pom.xml konfigurieren.

JnlpDownloadservlet (pom.xml)

Das Maven-Paket findet sich unter dem Namen com.sun.java.jnlp bzw. jnlp-servlet wieder. Als einfache Abhängigkeit ist es damit im Classpath und beispielsweise in der web.xml verwendbar. Weitere Informationen.

<dependency>
    <groupId>com.sun.java.jnlp</groupId>
    <artifactId>jnlp-servlet</artifactId>
    <version>5.0</version>
</dependency>

web.xml

Zur Basiskonfiguration muss nur das Servlet registriert und mit einem URL-Pattern verknüpft werden. Das klingt trivial, und das ist es auch.

<servlet>
  <servlet-name>JnlpDownloadServlet</servlet-name>
  <servlet-class>jnlp.sample.servlet.JnlpDownloadServlet</servlet-class>
</servlet>
<servlet-mapping>
  <servlet-name>JnlpDownloadServlet</servlet-name>
  <url-pattern>/webstart/*</url-pattern>
</servlet-mapping>

Damit wird die URL domain.tld/context/webstart an das Servlet gemappt. Dadurch kann das Servlet auch auf “virtuelle Dateianfragen” reagieren, etwa nach Jar-Dateien ohne Versions-Qualifkation (obwohl sie als versionierte Artefakte vorliegen).

Die Konfiguration lässt noch einige Dinge erweitern, verändern und tweaken (siehe Absatz Optimierungen und Verbesserungen). Für den Haupteinsatzzweck – das Bereitstellen einer oder mehrerer Jars via Webstart ist das jedoch völlig ausreichend.

Weitere Informationen bei Oracle/Java. Und weitere Informationen bei Codehaus.

pom.xml

Neben der Abhängigkeit des JnlpDownloadServlet muss das eigentlichen Maven Webstart Plugin für seinen Einsatz konfiguriert werden. Das Build-Plugin benötigt zudem noch ein Template (für die Generierung der Jnlp), die wird weiter unten beschrieben.

Zur Grundkonfiguration gehört:

  1. Wo liegt das Jnlp-Template?
  2. Wie heißt der Ausgabename der Jnlp?
  3. Welche Abhängigkeiten gibt es für das Paket – hier wäre das der userclient?

Des Weiteren lässt sich bestimmen, ob etwa weitere (transitive) Abhängigkeiten mitausgeliefert werden sollen (bei Webstart ist das wohl meist sinnvoll), ob die Ressourcen signiert werden sollen und ob alles nochmals komprimiert deployt werden soll. Weitere Informationen.

Prinzipiell ist man weder auf eine Abhängigkeit pro Paket/JNLP beschränkt noch auf eine JNLP pro Plugin-Call geschweige denn des gesamten Projekts. Über das Konfigurationsschlüsselwort commonJarResources können sogar gemeinsam verwendete Abhängigkeiten definiert werden.

Da der userclient erweiterte Rechte benötigt, müssen alle Classfiles und Jars signiert werden. Wahlweise verwendet dafür einen vorhandenen Keystore oder erstellt einen neuen. Für diese Zwecke wird pro Buildvorgang ein neuer Keystore angelegt, damit signiert und danach wieder gelöscht.

Die Execution ist sinnigerweise process-resources mit dem Goal jnlp-download-servlet, denn streng genommen sind es ja nur weitere Ressourcen.

<plugin>
  <groupId>org.codehaus.mojo.webstart</groupId>
  <artifactId>webstart-maven-plugin</artifactId>
  <executions>
    <execution>
      <phase>process-resources</phase>
      <goals>
        <goal>jnlp-download-servlet</goal>
      </goals>
    </execution>
  </executions>

  <configuration>
    <outputDirectoryName>webstart</outputDirectoryName>
    <excludeTransitive>false</excludeTransitive>
    <jnlpFiles>
      <jnlpFile>
        <templateFilename>template.vm</templateFilename>
        <outputFilename>UserClient.jnlp</outputFilename>
        <jarResources>
          <jarResource>
            <groupId>org.example.userclient</groupId>
            <artifactId>example-userclient</artifactId>
            <version>1.0.0</version>
            <mainClass>org.example.userclient.Main</mainClass>
          </jarResource>
        </jarResources>
      </jnlpFile>
    </jnlpFiles>
    <outputJarVersions>true</outputJarVersions>
    <verbose>false</verbose>
  </configuration>
</plugin>

Mit dieser Konfiguration werden die Jar-Dateien inkl. Abhängigkeiten in das Verzeichnis webstart gepackt – remember? wie in der web.xml. Das Template heißt template.vm und ist per default unter src/main/jnlp zu finden. Dies ließe sich mit templateDirectory überrschreiben. Der Name der Jnlp lautet UserClient.jnlp, damit ergibt sich die spätere Web-Url: http://example.org/context/webstart/UserClient.jnlp. Die eigentlichen Ressourcen und Abhängigkeiten werden in jarResources/jarResource definiert. Bei einfachen Projekten wird dies nur eine Ressource sein, theoretisch wären auch mehrere möglich. Die Konfiguration ähnelt der der dependencies.

template.vm

Im Prinzip ist das Template eine unfertige Jnlp. Sie wird durch das Maven Plugin mit den endgültigen Daten befüllt. Da bereits die pom.xml über einige Informationen wie Projektnamen, -beschreibung oder -url verfügt, können so sehr einfach die Daten mit übernommen werden. Man kann jedoch auch die Platzhalter entfernen – es ist eben nur ein Template.

Ein Beispiel könnte so aussehen:

<jnlp spec="$jnlpspec" codebase="$$codebase">
  <information>
    <title>$project.Name</title>
    <vendor>$project.Organization.Name</vendor>
    <homepage href="$project.Url"/>
    <description>$project.Description</description>
    <icon href="../resources/images/logo.png"/>
    <icon href="../resources/images/logo.png" kind="splash"/>

#if($offlineAllowed)
<offline-allowed/>
#end

  </information>

#if($allPermissions)
<security>
<all-permissions/>
</security>
#end

  <resources>
    <j2se version="$j2seVersion"/>
$dependencies
  </resources>
  <application-desc main-class="$mainClass"></application-desc>
</jnlp>

Mehr oder weniger simpel, oder? Eventuell sollte man die Adressen zum Logo anpassen (oder entfernen); gerade die letzte Zeile erzeugt einen netten Splashscreen (Ladebild) während dem Starten und Laden der Anwendung. Das ist immer gerne willkommen.

Wichtig: Im Gegensatz zu dem einen oder anderen Beispiel ist es hierbei wichtig, dass die Variable $$codebase heißt. Nicht etwa ${codebase}. Insgesamt sind in der JNLP – sofern man sie über das Servlet ausliefert – folgende Variablen verfügbar: codebase, name, context und site.

Optimierungen und Verbesserungen

“Einmal signiert, bitte!”

Um all-permissions nutzen zu können, müssen die Jars und Classfiles signiert werden. Das erreicht man, indem man unter dem XML-Knoten configuration einen Knoten sign anlegt, etwa:

<sign>
	<keystore>keystore.ks</keystore>
	<keypass>pass</keypass>
	<storepass>pass</storepass>
	<alias>userclient</alias>
	<validity>36500</validity>
	<dnameCn>UserClient</dnameCn>
	<dnameOu>Software Development</dnameOu>
	<dnameO>The Example Networks</dnameO>
	<dnameL>Cologne</dnameL>
	<dnameSt>NRW</dnameSt>
	<dnameC>DE</dnameC>
	<verify>false</verify>
	<keystoreConfig>
		<delete>true</delete>
		<gen>true</gen>
	</keystoreConfig>
</sign>

Hierbei wird der Keystore lokal erzeugt (keystoreConfig/gen ist true) und nach Gebrauch wieder gelöscht (keystoreConfig/delete ist true). Selbstverständlich kann man hier auch a) noch einen Keystore-Generator (s.o.) nutzen oder einen fest konfigurierten, dauerhaften. Dann sollte man natürlich die Konfiguration entsprechend anpassen.

“Bitte Optional” – alles in ein Profil

Die Profile in der pom.xml sind ein mächtiges Werkzeug, um bestimmte Features zusammenzufassen. So könnte man das gesamte Build-Plugin in ein Profil – etwa mit dem Namen with-webstart – ablegen.

Selbst die Abhängigkeit zum JnlpDownloadServlet kann man dorthin verlagern – wenn man daran denkt, dass in diesem Falle auch die web.xml dynamisch erstellt werden soll.

Macht der Properties

Man kann die Gesamtkonfiguration um einiges komfortabler machen, indem man Properties einführt und deren Standardwerte “oben” in der pom.xml definiert. Gute Kandidaten hierbei wären: userclient.version, keystore.file, keystore.keypass, keystore.storepass und keystore.alias.

Noch mehr in Sachen Jnlp

In der web.xml kann des Weiteren das verhalten des JnpDownloadServlet verändert werden. Ein Überblick über einige Möglichkeiten: Mimetypen ändern, Dateiendungen ändern, weitere Mappings anlegen, spezielles Debugging.