Dienstag, 21. August 2012

Building, Signing and Deploying your JavaFX Application with Maven

Building and deploying a JavaFX with Maven can be cumbersome as I recently had to experience myself.

Oracle's documentation only deals with ANT and only uses one single jar in their examples.

Here's a more complex scenario: Your JavaFX application is managed by Maven and has about 40 dependencies.

First you should make sure to copy all those dependencies to a separate folder. This folder will be later used as our classpath.

You can use the maven-dependency-plugin for this:
<plugin>
    <groupId>org.apache.maven.plugins</groupId>
    <artifactId>maven-dependency-plugin</artifactId>
    <executions>
        <execution>
            <id>copy-dependencies</id>
            <phase>install</phase>
            <goals>
                <goal>copy-dependencies</goal>
            </goals>
            <configuration>
                <includeScope>runtime</includeScope>
                <outputDirectory>${application.dist}/lib</outputDirectory>
                <overWriteReleases>false</overWriteReleases>
                <overWriteSnapshots>true</overWriteSnapshots>
                <overWriteIfNewer>true</overWriteIfNewer>
            </configuration>
        </execution>
    </executions>
</plugin>
In order to build the JavaFX application, we use ANT. We kind of have to, because this is currently the only official and supported way to build a JavaFX application (besides the command line tool).
Using only Maven is complicated (especially when it comes to web deployment) and needs some more setup.

However, we can use ANT out of Maven, using the maven-antrun-plugin.

Here's my plugin configuration:
<plugin>
    <artifactId>maven-antrun-plugin</artifactId>
    <version>1.7</version>
    <executions>
        <execution>
            <phase>install</phase>
            <configuration>
                <target xmlns:fx="javafx:com.sun.javafx.tools.ant">
                    <property name="applet.width" value="330"/>
                    <property name="applet.height" value="620"/>
                    <property name="application.title" value="Title"/>
                    <property name="application.vendor" value="Vendor"/>
                    <!-- Copy the class path to the manifest. The lib folder is generated by maven-dependency-plugin. -->
                    <manifestclasspath property="manifest.classpath"
                                       jarfile="${application.dist}/MyApp.jar">
                        <classpath>
                            <path id="build.classpath">
                                <fileset dir="${application.dist}/lib">
                                    <include name="*.jar"/>
                                </fileset>
                            </path>
                        </classpath>
                    </manifestclasspath>

                    <taskdef resource="com/sun/javafx/tools/ant/antlib.xml"
                             uri="javafx:com.sun.javafx.tools.ant"
                             classpath="${javafx.lib.ant-javafx.jar}"/>
                    <fx:application id="myApp"
                                    name="MyApp"
                                    mainClass="package.MainClass"/>
                    <fx:jar destfile="${application.dist}/MyApp.jar">
                        <fx:application refid="myApp"/>

                        <manifest>
                            <attribute name="Class-Path" value="${manifest.classpath}"/>
                        </manifest>

                        <!-- The target/classes folder which contains all resources and class files -->
                        <fileset dir="${project.build.outputDirectory}"/>
                    </fx:jar>
                    <fx:resources id="appRes">
                        <fx:fileset dir="${application.dist}" includes="*.jar"/>
                        <fx:fileset dir="${application.dist}" includes="lib/*.jar"/>
                    </fx:resources>
                            
                    <signjar keyStore="${basedir}\keystore.jks"
                             destdir="${application.dist}"
                             alias="certificatekey" storePass="cshcsh" keyPass="cshcsh">
                        <fileset dir="${application.dist}" includes="*.jar"/>
                        <fileset dir="${application.dist}" includes="lib/*.jar"/>
                    </signjar>
                    <fx:deploy width="${applet.width}" height="${applet.height}"
                               outdir="${application.dist}" embedJNLP="true"
                               outfile="${application.title}">

                        <fx:application refId="myApp"/>

                        <fx:resources refid="appRes"/>
                        <fx:info title="Sample app: ${application.title}"
                                 vendor="${application.vendor}"/>

                        <!-- Request elevated permissions -->
                        <fx:permissions elevated="true"/>
                    </fx:deploy>
                </target>
            </configuration>
            <goals>
                <goal>run</goal>
            </goals>
        </execution>
    </executions>
</plugin>

Building the jar file

The first important part is to build the class-path for the manifest file. Here we use the class-path, which we created with Maven and write it to the manifest.

The rest of the configuration is mostly taken from the official JavaFX documentation (see there for more information).

The fx:jar task creates the jar file and adds some JavaFX properties to the manifest in order to be able to launch your application.
As fileset we reference our target/classes folder which was generated by Maven.

For just building the jar, this would already be sufficient.

Signing the jar files

If you want to also deploy it in the web you can sign the application using the signjar or the fx:signjar task.
(I had some problems signing a jar with fx:signjar, if that jar doesn't have a META-INF folder).

First you should generate a keystore.
Here's a useful tutorial on how to do that.

My configuration signs each jar in the class-path.

Be careful, if one of your Jars is already signed. You might end up with a runtime error "JAR resources in JNLP file are not signed by the same certificate".

Deploying the application

Finally you use the fx:deploy task to generate the JNLP and HTML file for web deployment.

I defined two properties which you need to run the plugins:
<properties>
        <javafx.lib.ant-javafx.jar>C:\Program Files (x86)\Java\jdk1.7.0_06\lib\ant-javafx.jar
        </javafx.lib.ant-javafx.jar>
        <application.dist>${project.build.directory}/dist</application.dist>
</properties>
The first one is the location of the ant-javafx.jar.
The second one is the target folder for all generated files.

Now, have a look at your JNLP file. It should contain all your dependencies from your Maven configuration.

PS: For those who are interested: I updated the code the TreeView control in my previous post.

Kommentare:

  1. Very nice! Thanks, used it in my application, works very good.

    AntwortenLöschen
  2. Interesting. Saved my day. Didn't know that you could just provide the manifest "Class-Path" manually instead of using fx:resources (which is not compatible to Ant's resources). I think it should be JavaFX-Class-Path though because that's what fx:jar generates when it gets a nested fx:resources element as documented at Oracle. Although both worked in my tests...

    AntwortenLöschen