Tutorial zur Erstellung ausführbarer JavaFX-Anwendungen

JavaFX-Tutorial: Von der Klasse zur EXE

Denny Israel
© Shutterstock/patpitchaya

JavaFX bietet als neues UI-Toolkit für Java eine sehr moderne UI-Technologie, mit deren Hilfe Oberflächen effizient entwickelt werden können. Aber wie sieht das für den Endanwender aus? Was muss er tun, um JavaFX-Programme bei sich zum Laufen zu bringen und wie viel Spezialwissen über die Java-Welt benötigt er? Muss er Java überhaupt kennen? Dieses Tutorial zeigt anhand einer Beispielanwendung, welche Möglichkeiten existieren, um eine JavaFX-Anwendung auslieferbar vorzubereiten und beim Kunden in Betrieb zu nehmen.

Als Benutzer einer Software mögen wir es heutzutage gern bequem. Wir sind es gewohnt, durch einen Doppelklick auf eine Verknüpfung Anwendungen zu starten und wollen möglichst keine Einstellungen vornehmen, weder an der Anwendungen noch an unserem System. Wir wollen bitte auch keine zusätzliche Software installieren und konfigurieren müssen. Für uns Java-Entwickler stellt sich also nun die Frage, wie wir dem Wunsch unserer Kunden nachkommen können, schließlich sind wir sehr stolz auf die Plattformunabhängigkeit unserer Welt. Wir sind jedoch gezwungen, mit jar-Dateien zu arbeiten, anstatt mit den für den Endnutzer bekannten direkt ausführbaren exe-Dateien oder auch dmg-Files.

Artikelserie
Teil 1: Von der Klasse zur EXE: JavaFX-Anwendungen nativ packen
Teil 2: Auslieferung und Installation nativer JavaFX-Anwendungen

Das Beispiel: Timer

Um die verschiedenen Möglichkeiten und Ausbaustufen für auslieferbare JavaFX-Anwendungen anschaulich zu beschreiben, benötigen wir ein Anschauungsobjekt. Hierzu benutzen wir eine kleine Applikation namens Timer, siehe Abbildung 1. Dieser Timer hat die Funktion einer Eieruhr. Man kann eine Zeit einstellen (Minuten und Sekunden) und nach einem Klick auf Start geht der Countdown los. Ein Klick auf Stop beendet den Countdown an der aktuellen Zeit. Ein erneuter Klick auf Start lässt ihn weiterlaufen.

Abb. 1: Screenshot Timer

Gebaut wurde diese Minianwendung mithilfe des mvvmFX-Frameworks, welches eine Anwendungsstrukturierung nach Model View ViewModel zulässt und auch Dependency-Injection-Unterstützung in Form von CDI Weld und Google Guice mitbringt. Das erleichtert uns den Einstieg in die Anwendung und bringt uns gleich eine demonstrative Abhängigkeit zu einem Drittprojekt. Die Timer-Anwendung ist als Projekt auf GitHub verfügbar. Um das Projekt nutzen zu können, benötigt man Java 7 (mindestens in Version 7 Update 6), Maven, Git sowie einen GitHub-Account. Hat man die benötigte Software installiert, kann das Projekt mit git clone git@github.com:dennyisrael/class_to_exe.git oder git clone https://github.com/dennyisrael/class_to_exe.git geklont werden.
Wir interessieren uns zunächst für das Hauptprojekt timer. Dieses bildet die Basis für die verschiedenen Ausbaustufen. Es ist eine einfache Anwendung mit drei Packages und vier Klassen. Hinzu kommen noch eine fxml-Datei sowie eine Testklasse. Das Projekt kann durch ein mvn clean install komplett gebaut werden. Alle Abhängigkeiten sind im Maven Central Repository verfügbar. Einzige Besonderheit ist, dass der POM das maven-assembly-plugin, das eine Standalone-jar-Datei erzeugt, bildet. Im Laufe des Tutorials werden wir uns hauptsächlich mit dieser POM beschäftigen und die verschiedenen Ausbaustufen der auslieferbaren Anwendung in den Build-Prozess integrieren. Das GitHub-Projekt ist dabei so aufgebaut, dass es für jede Ausbaustufe, die wir vorstellen, ein Projekt im Repository gibt, das alle nötigen Ressourcen und Einstellungen beinhaltet. Alle Projekte sind mit einem einfachen mvn clean install komplett baubar. Auf die Projekte wird an den entsprechenden Stellen hingewiesen.

Aufmacherbild: Finger press on laptop keyboard with computer source code von Shutterstock / Urheberrecht: patpitchaya

[ header = Seite 2: Der erste Versuch ]

Der erste Versuch

Wie beschrieben wird bereits im Hauptprojekt timer eine Standalone-jar-Datei erzeugt, die in anderen Projekten durchaus allein lauffähig ist. In unserem Fall startet die Anwendung leider nicht und versagt mit der in Listing 1 aufgeführten Exception den Dienst. Die Klasse javafx.application.Application ist nicht verfügbar. Und tatsächlich, wenn wir unsere Standalone jar auspacken, können wir eine solche Klasse nicht finden. Die Klasse existiert zwar in der JavaFX-Bibliothek (jfxrt.jar), wird jedoch nicht standardmäßig in den Classpath aufgenommen. Hinzu kommt, dass JavaFX einen besonderen Startmechanismus aufweist.

Exception in thread "main" java.lang.NoClassDefFoundError: javafx/application/Application
at java.lang.ClassLoader.defineClass1(Native Method)
...
at sun.launcher.LauncherHelper.checkAndLoadMain(Unknown Source)
Caused by: java.lang.ClassNotFoundException: javafx.application.Application
at java.net.URLClassLoader$1.run(Unknown Source)
...
... 37 more

So kommen wir nicht ans Ziel. Unsere Anwendung besteht zwar nur aus einer jar, die man ausliefern könnte, sie läuft jedoch nicht. Alle Abhängigkeiten existieren, bzw. werden mit Java mitgeliefert, doch wie überreden wir Java dazu, unseren Timer zu starten?

Der zweite trickreiche Versuch

Vielleicht waren wir zu vorschnell. Treten wir also einen Schritt zurück und überlegen, wie Oracle es sich gedacht hat. Wir entdecken, dass es ein Programm namens javafxpackager gibt, das mit dem JavaFX-Toolkit ausgeliefert wird. Dieses Tool dient dazu, lauffähige JavaFX-Anwendungen zu bauen, also genau das, was wir vorhaben. Den javafxpackager können wir mit folgender Kommandozeile ausführen:

timertarget>javafxpackager.exe -createjar -appclass de.saxsys.classtoexe.timer.TimerStarter -srcdir classes/ -outfile timer_full.jar

Wenn wir uns die erzeugte jar-Datei ansehen, fällt auf, dass unter dem Pfad comjavafxmain ein paar neue Klassen hinzugekommen sind. Diese Starterklassen sind das Geheimnis, um die JavaFX-Anwendung als ausführbares Programm auszuliefern (siehe dazu). Wir benötigen die Klassen in unserer eigenen jar und wollen, dass Maven die Standalone jar komplett baut. Dazu entnehmen wir die class-Dateien dem vom javafxpackager gebauten jar und fügen diese als Ressourcen unserem Projekt hinzu (Abb. 2).

Abb. 2: Zusätzliche „class“-Dateien für die Standalone „jar“

Zum Bau benutzen wir weiterhin das maven-assembly-plugin, diesmal jedoch mit einer eigenen Assembly-Definition, welche in Listing 2 zu sehen ist. Das maven-assembly-plugin muss dahingehend abgeändert werden, dass es eine eigene Assembly-Definiton benutzt. Des Weiteren müssen die generierten Manifesteinträge abgeändert werden, sodass nun com/javafx/main/Main die Main-Klasse ist und unsere de.saxsys.classtoexe.timer.TimerStarter als spezielle JavaFX Application Class eingetragen wird (Listing 3).

<assembly ...>
  <id>standalone</id>
  <formats>
    <format>jar</format>
  </formats>
  <includeBaseDirectory>false</includeBaseDirectory>
  <fileSets>
    <fileSet>
      <directory>${project.build.outputDirectory}</directory>
      <outputDirectory>/</outputDirectory>
    </fileSet>
    <fileSet>
      <!-- copy the generated Main class (from javafxpackager.exe) into the .jar -->
      <directory>${basedir}/src/main/resources</directory>
      <outputDirectory>/</outputDirectory>
      <includes>
        <include>**/*</include>
      </includes>
    </fileSet>
  </fileSets>
  <dependencySets>
    <dependencySet>
      <!-- do NOT include the JavaFX dependency as it will cause the application to crash (cannot find glass.dll, etc.) -->
      <excludes>
        <exclude>com.oracle:javafx</exclude>
      </excludes>
      <unpack>true</unpack>
      <useProjectArtifact>false</useProjectArtifact>
      <outputDirectory>/</outputDirectory>
    </dependencySet>
  </dependencySets>
</assembly>
<plugin>
  <artifactId>maven-assembly-plugin</artifactId>
  <configuration>
    <descriptors>
      <descriptor>src/main/resources/standalone_assembly.xml</descriptor>
    </descriptors>
    <!-- @see http://loop81.blogspot.de/2012/03/javafx-2-get-started-with-maven-eclips... -->
    <archive>
      <manifest>
        <addDefaultImplementationEntries>true</addDefaultImplementationEntries>
      </manifest>
      <manifestEntries>
        <JavaFX-Version>2.2</JavaFX-Version>
        <JavaFX-Application-Class>de.saxsys.classtoexe.timer.TimerStarter</JavaFX-Application-Class>
        <Main-Class>com/javafx/main/Main</Main-Class>
      </manifestEntries>
    </archive>
  </configuration>
  <executions>
    <execution>
      <phase>package</phase>
      <goals>
        <goal>single</goal>
      </goals>
    </execution>
  </executions>
</plugin>

Nachdem wir nun die Starterklassen in unsere jar eingefügt haben, probieren wir, diese zu starten:

timer_standalone-jartarget>java -jar timer_standalone-jar-0.0.1-SNAPSHOT-standalone.jar

Jetzt können wir den Timer benutzen. Wenn wir die Anwendung ausliefern wollen, nehmen wir lediglich die Standalone jar, und unsere Kunden können sie mit ihrer eigenen Java-Laufzeitumgebung starten. Die gezeigten Änderungen sind im Projekt timer_standalone-jar zusammengefasst, das sich ebenfalls im GitHub Repository befindet.

[ header = Seite 3: Geht’s nicht auch automatisch? ]

Geht’s nicht auch automatisch?

Wir haben gesehen, wie wir durch Maven eine lauffähige jar-Datei erzeugen und diese starten. Nun bauen wir Releases unserer Software natürlich nicht selbst auf irgendeinem Entwicklerrechner. Wir wollen, dass unser Jenkins dies für uns erledigt und bei einem Release die Artefakte auch gleich in den Nexus lädt. Zum Glück sorgt das maven-assembly-plugin dafür, dass die damit erzeugten Dateien automatisch als Artefakte angesehen und von Maven sowohl ins lokale Repository, als auch (sofern eingestellt) in remote Repositories geladen werden.

Schon ganz OK aber das geht besser…

Soweit so gut. Wir haben ein einzelnes Artefakt, das alles beinhaltet, was unsere Anwendung benötigt. Wir können es durch einen einfachen Kommandozeilenbefehl starten. Unser Kunde muss lediglich bei sich eine entsprechende Java-Laufzeitumgebung installieren.
Das sagt sich so leicht, jedoch fangen genau hier die Probleme wieder an. Wenn wir auf eine bestimmte JavaFX-Version angewiesen sind und diese in unserem Maven Build angeben, garantiert uns niemand, dass die Java-Version des Kunden kompatibel mit unserer JavaFX-Version ist. Aber auch, wenn wir immer die JavaFX-Version der installierten Laufzeitumgebung verwenden, kann es zu Problemen kommen. JavaFX entwickelt sich weiter, sodass der Kunde beim Einsatz einer anderen JVM mit einer anderen JavaFX-Version auf Probleme, Bugs oder Verhaltensänderungen treffen kann, die in der Version, mit der wir entwickelt haben, nicht auftreten. All dies kann zu erhöhtem Supportaufwand und Unzufriedenheit bei Benutzern und Entwicklern führen. Besser wäre es, nicht auf die installierte Version angewiesen zu sein. Hinzu kommt, dass der Nutzer in der Regel eine systemspezifische, ausführbare Datei erwartet (auf Windows-Systemen eine exe). Eine jar-Datei oder eine bat erscheint in der Regel nicht benutzerfreundlich genug. Was können wir also tun?

Jetzt wird’s interessant…

Wir haben gesehen, wie die Anwendung mit Maven gebaut wird und wie wir in einem ersten Versuch eine lauffähige jar-Datei erstellen, die abhängig von der installierten JVM läuft. Wir haben ebenfalls festgestellt, dass unser erster Versuch zwar funktioniert, wir jedoch mehr Bequemlichkeit für unsere Endnutzer möchten. Des Weiteren wäre es besser, nicht von der installierten JVM und deren Einstellungen abhängig zu sein. Wir wollen daher nun demonstrieren, wie unsere Anwendung als klassische exe zusammen mit einer mitgelieferten JRE gebaut wird. Da der Prozess auf Maven basiert, ist es ein Leichtes, alle Aktionen von einem Build-Server ausführen zu lassen.

[ header = Seite 4: Das Beispiel: Timer ]

Das Beispiel: Timer

Wir erinnern uns: Unsere kleine Timer-Anwendung mit der Funktionalität einer Eieruhr dient uns als Beispiel. Wir haben bereits zwei Projekte kennengelernt: Timer und timer_standalone-jar. Beide lassen sich durch mvn clean install bauen und Letzteres erzeugt dabei eine lauffähige jar-Datei, die in Verbindung mit einer JVM vom Kunden ausgeführt werden kann.
Nun wollen wir uns ansehen, wie man die App noch bequemer verpacken kann und dem Kunden eine exe zur Verfügung stellt.

Die Bequemlichkeit bauen

Im ersten Teil haben wir uns bereits mit dem javafxpackager beschäftigt, der uns eine lauffähige jar-Datei und alles dazu Nötige erzeugt und verpackt hat. Doch der Packager ist noch leistungsfähiger. Er ermöglicht es, eine umgebungsspezifische, ausführbare Datei zu erzeugen und die dazu nötigen Ressourcen zum Starten (z. B. JRE) gleich mit zur Verfügung zu stellen.
Wir möchten natürlich den javafxpackager nicht jedes Mal von Hand ausführen, da wir, wie viele andere auch, einen Build-Server verwenden, der den kompletten Prozess automatisiert. Um den bisher manuell ausgeführten Schritt auch noch in den Maven-Build-Prozess zu integrieren, kann das Maven-Plug-in von ZenJava verwendet werden. Dieses führt den javafxpackager für das aktuelle Projekt aus und erzeugt das native Paket. Listing 2 zeigt die Plug-in-Einstellungen in der pom.xml.
Hierbei gibt es gleich mehrere interessante Stellen, die wir uns näher ansehen wollen. Zunächst die Plug-in-Daten, bestehend aus der groupId (com.zenjava), der artifactId (javafx-maven-plugin) und der Version (2.0). Für die Ausführung sind Konfigurationseinstellungen nötig, die dem Plug-in sagen, was und womit es die exe bauen soll. Die Einstellung <mainClass> gibt an, welche Klasse von Application erbt und zum Start der Anwendung dient. In unserem Fall ist dies die bekannte TimerStarter-Klasse im package de.saxsys.classtoexe.timer. Über die Option <vendor> kann man den Hersteller der Anwendung angeben. Für den Betrieb relevant sind vor allem die Angaben unter <jvmArgs>, die man bei direktem Aufruf über java als Parameter mit gibt. Hier kann man Dinge wie Speichereinstellungen (-Xmx, -XX:MaxPermSize) und sonstige JVM-Parameter angeben. Wir geben unserer App eine moderate Speichereinstellung mit und aktivieren darüber hinaus die Hardwarebeschleunigung von JavaFX über den Parameter sun.java2d.opengl.
Nachdem wir die Einstellungen für unsere native Anwendung angegeben haben, müssen wir das Plug-in noch konfigurieren. Über ein Tag <execution> unter <executions> kann man angeben, in welcher Phase von Maven die native App gebaut wird und was das Ziel ist (<goal>native</goal>). In unserem Fall möchten wir in der Phase package (welches die übliche Paketierungsphase von Maven ist, in der jar-Dateien u. Ä. erzeugt werden) die native App bauen lassen. Weitere Informationen über die Einstellungsmöglichkeiten des Plug-ins sind hier zu finden.

<build>
  <finalName>timer</finalName>
  <plugins>
    <plugin>
      <groupId>com.zenjava</groupId>
      <artifactId>javafx-maven-plugin</artifactId>
      <version>2.0</version>
      <configuration>
        <mainClass>de.saxsys.classtoexe.timer.TimerStarter</mainClass>
        <jvmArgs>
          <jvmArg>-Xms64m</jvmArg>
          <jvmArg>-Xmx128m</jvmArg>
          <jvmArg>-Dsun.java2d.opengl=True</jvmArg>
          <jvmArg>-Dfile.encoding=UTF-8</jvmArg>
        </jvmArgs>
        <vendor>saxsys</vendor>
      </configuration>
      <executions>
        <execution>
          <phase>package</phase>
          <goals>
            <goal>native</goal>
          </goals>
        </execution>
      </executions>
    </plugin>
  </plugins>
</build>

Nachdem wir nun unser Plug-in für die Generierung der nativen Anwendung konfiguriert haben, können wir es mit Maven ausführen. Ein mvn clean install im Projekt timer_native-client erzeugt uns im target-Ordner des Projekts einen neuen Ordner jfxnativebundlestimer, in dem die timer.exe zu finden ist, sowie die angesprochenen Ressourcenordner app und runtime.

Das Ergebnis

Das javafx-maven-plugin erzeugt unter Zuhilfenahme des javafxpackager eine timer.exe, die unseren Timer startet, ohne von der Umgebung und deren Einstellungen abhängig zu sein. Dazu benötigt die exe alle Ressourcen die zum Starten der Java-Anwendung erforderlich sind. Diese werden vom javafx-maven-plugin neben die exe gelegt. Im Ordner runtime liegt eine abgespeckte JRE, die nur die für die JavaFX-Anwendung nötigen Ressourcen beinhaltet. Der Ordner app enthält alle anwendungsspezifischen Ressourcen. In unserem Fall sind das die timer-jfx.jar sowie die sonstigen jar-Dateien im lib-Unterordner. Damit kann man die Anwendung in einem Rutsch (z. B. als ZIP) an den Kunden ausliefern und dieser muss nichts weiter tun, als auf die exe ausführen.

Geht’s nicht auch automatisch?

Was sagt nun Jenkins dazu? Da wir ein normales Maven-Plug-in nutzen, das die Arbeit für uns erledigt, kann diese auch von Jenkins ausgeführt werden. Am Build-Job muss nichts angepasst werden, da der Maven-Aufruf weiterhin nur aus mvn clean install besteht. Möchte man das Arbeitsergebnis von Jenkins in remote Repositories sichern, kann man dafür das Maven-Assembly-Plug-in bemühen, das den Ordner jfxnativebundlestimer zu einer zip-Datei zusammenpacken kann und diese als Artefakt sowohl in das lokale, als auch (falls konfiguriert) in Distributions-Repositories verteilt.
Zum Bau der zip-Datei fügen wir unserer pom einen Plug-in-Eintrag für das Assembly-Plug-in hinzu, siehe Listing 5. Die Ausführung des Plug-ins erfolgt in der pre-integration-test-Phase, um sicherzustellen, dass der Bau der nativen App, der in der Phase package erfolgt, bereits erledigt wurde und das Assembly-Plug-in etwas zum Verpacken hat. Der entsprechende Descriptor, der dem Assembly-Plug-in sagt, was zu tun ist, liegt im Ordner src/main/resources des Projekts. Der Inhalt des Descriptor ist in Listing 6 zu sehen.

<plugin>
  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-assembly-plugin</artifactId>
  <executions>
    <execution>
      <id>assemble-zip</id>
      <phase>pre-integration-test</phase>
      <goals>
        <goal>single</goal>
      </goals>
      <configuration>
        <descriptors>
          <descriptor>${basedir}/src/main/resources/timer_zip.xml</descriptor>
        </descriptors>
      </configuration>
    </execution>
  </executions>
</plugin>
<assembly
  xmlns="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/plugins/maven-assembly-plugin/assembly/1.1.2 http://maven.apache.org/xsd/assembly-1.1.2.xsd">
  <id>timer-zip</id>
  <formats>
    <format>zip</format>
  </formats>
  <includeBaseDirectory>false</includeBaseDirectory>
  <fileSets>
    <fileSet>
      <directory>${project.build.directory}/jfx/native/bundles/timer</directory>
      <outputDirectory>/</outputDirectory>
      <includes>
        <include>**/*</include>
      </includes>
    </fileSet>
  </fileSets>
</assembly>

Wo stehen wir?

Was könnten wir nun also machen? Unsere Applikation ist zusammen mit einer eingebetteten JRE auslieferbar. Sind wird damit fertig? Wollen wir etwa zum Kunden sagen: „Bitte entpacken Sie die Applikationsdateien und kopieren Sie mit STRG + C/V an Stelle XY?“. Das funktioniert und ist besser als eine reine jar-Datei auszuliefern, die eine installierte JRE voraussetzt und dem Kunden einen java –jar-Aufruf abringen würde. Es ist dennoch nicht sonderlich praktikabel. Welches Programm wird schon über Copy and Paste installiert? Der nächste Teil unseres Tutorials wird sich daher mit der Auslieferung der Artefakte via Installer beschäftigen.

[ header = Seite 5: Zusammenfassung ]

Zusammenfassung

Im ersten Teil haben wir die kleine Beispielanwendung Timer vorgestellt. Diese dient uns als Anschauungsobjekt für die verschiedenen Möglichkeiten, die wir haben, um eine JavaFX-Anwendung auslieferfertig aufzubereiten. Wir haben damit die Anwendung klassisch mit Maven gebaut und eine jar-Datei herausbekommen. Nachdem wir festgestellt haben, dass diese nicht allein lauffähig ist, haben wir weitere Maßnahmen getroffen, um dies zu erreichen. Als Erstes haben wir mithilfe des javafxpackagers die noch fehlenden Ressourcen für eine lauffähige jar-Datei erzeugt und schließlich mithilfe des maven-assembly-plugin eine jar-Datei erstellt, die alle Ressourcen beinhaltet und lauffähig ist. Anschließend haben wir betrachtet, wie man mithilfe des javafxpackagers und dem javafx-maven-plugin eine exe-Datei erzeugen kann, die den Timer startet und zusammen mit den nötigen Ressourcen, die bei der Erzeugung daneben gelegt werden, ausgeliefert werden kann. Anschließend haben wir die exe samt ihrer Ressourcen in eine zip-Datei verpackt und sind somit lieferfähig. Ein automatischer Build mit Jenkins gelingt uns auch, da Maven für uns alle Abhängigkeiten zusammensammelt. Wenn wir die zum Schluss erzeugte zip-Datei an einen Kunden ausliefern, zwingen wir ihn, die Datei von Hand auszupacken und alle Ressourcen mitzunehmen. Besser als ihm die Installation einer bestimmten JVM aufzuzwingen, jedoch weniger bequem und sicher als es von heutigen Anwendungen erwartet wird. Der Kunde erwartet zu Recht einen Installer, der sich um die Details kümmert. Im zweiten Teil des Tutorials wollen wir uns daher mit der Erzeugung eines Installer beschäftigen.

Geschrieben von
Denny Israel
Denny Israel
Denny Israel ist Consultant für Softwareentwicklung im Bereich Java bei der Saxonia Systems AG. Er beschäftigt sich im aktuellen Projekt mit der Entwicklung einer JavaFX-basierten Anwendung zur Unterstützung agiler Entwicklungsprozesse sowie deren Anbindung an Drittsysteme. Bestandteil seiner Arbeit ist ebenso, die automatische und nachvollziehbare Auslieferbarkeit der Anwendung mit allen Facetten herzustellen.
Kommentare

Hinterlasse einen Kommentar

Hinterlasse den ersten Kommentar!

avatar
400
  Subscribe  
Benachrichtige mich zu: