Build, sign, and package Java Web Start applications with Maven. This plugin generates JNLP descriptors, bundles dependencies, signs JARs, and optionally applies Pack200 compression — ready for deployment with IcedTea-Web or any JNLP-aware client runtime.
Actively maintained for modern JDKs, cloud code signing, and current enterprise use.
See CHANGELOG.md for release history and CONTRIBUTING.md for development setup.
Latest release: 1.2.4-bravura · Default branch: main · Maven Central: io.github.martinhickson:webstart-maven-plugin:1.2.4-bravura
| Capability | Description |
|---|---|
| JNLP generation | Produce launch.jnlp (and related files) from your project and dependencies |
| JAR signing | Sign application and dependency JARs; supports hardware keys and Azure KeyVault JCA |
| Pack200 | Optional compression for legacy Web Start deployments; graceful fallback on modern JDKs |
| Servlet support | webstart-jnlp-servlet module (Jakarta Servlet 6 / EE 10) for Pack200-aware JNLP download serving |
Add the plugin to your pom.xml and bind the jnlp-inline goal (simplest path for a single JAR application):
<build>
<plugins>
<plugin>
<groupId>io.github.martinhickson</groupId>
<artifactId>webstart-maven-plugin</artifactId>
<version>1.2.4-bravura</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>jnlp-inline</goal>
</goals>
</execution>
</executions>
<configuration>
<jnlp>
<outputFile>launch.jnlp</outputFile>
<mainClass>com.example.MyApplication</mainClass>
</jnlp>
</configuration>
</plugin>
</plugins>
</build>Build:
mvn clean packageOutput lands under target/jnlp/ — including launch.jnlp, signed JARs, and a distributable ZIP.
Released versions are published to Maven Central — no extra <repository> configuration is required.
GitHub Packages is also available if you prefer to resolve from GitHub. Add the repository and authenticate with a GitHub personal access token (read:packages):
<repositories>
<repository>
<id>github-packages</id>
<url>https://maven.pkg.github.com/martinhickson/webstart</url>
</repository>
</repositories>In ~/.m2/settings.xml:
<servers>
<server>
<id>github-packages</id>
<username>YOUR_GITHUB_USERNAME</username>
<password>YOUR_GITHUB_TOKEN</password>
</server>
</servers>For local builds, the plugin can generate a keystore automatically:
<configuration>
<jnlp>
<outputFile>launch.jnlp</outputFile>
<mainClass>com.example.MyApplication</mainClass>
</jnlp>
<sign>
<keystore>${project.build.directory}/keystore</keystore>
<storepass>changeit</storepass>
<keypass>changeit</keypass>
<alias>myapp</alias>
<keyalg>RSA</keyalg>
<validity>365</validity>
<dnameCn>example.com</dnameCn>
<dnameO>Example Ltd</dnameO>
<dnameC>US</dnameC>
<keystoreConfig>
<gen>true</gen>
<delete>true</delete>
</keystoreConfig>
</sign>
</configuration>On JDK 11 and later, specify keyalg (for example RSA) when generating keys.
Pack200 is still relevant for some legacy client runtimes. On JDK 14+, the JDK Pack200 API is unavailable. You have two options:
Option A — skip Pack200 (default behaviour when unavailable): enable Pack200 and the plugin logs a warning, then continues:
<pack200>
<enabled>true</enabled>
</pack200>Option B — use Apache Commons Compress on modern JDKs:
<pack200>
<enabled>true</enabled>
<commonsCompressEnabled>true</commonsCompressEnabled>
</pack200>For multi-module layouts or WAR packaging, use the jnlp goal instead of jnlp-inline. See webstart-it/ for a working JDK 21 example.
Sign JARs during the JNLP build using a certificate stored in Azure Key Vault, via the Azure Key Vault JCA provider. The plugin passes providerName, providerClass, certchain, and -J JVM arguments through to jarsigner — including any arguments you declare in the POM (they are preserved across signing).
- Azure Key Vault with a code-signing certificate imported (certificate name = jarsigner alias).
- Key Vault permissions — RBAC:
Key Vault Crypto User,Key Vault Certificate User, andKey Vault Secrets User; or access policy withSignon keys andget/liston certificates and secrets. - Authentication — service principal (tenant/client/secret), managed identity, or workload identity on your build agent.
- Certificate chain PEM — export the full chain from your CA and save as
src/main/signing/cert-chain.pem. Supplying-certchainavoids incomplete-chain warnings from the JCA provider when the vault holds only the leaf certificate. - JDK 11+ on the build machine (module-path to the JCA JAR is required on Java 9+).
Service-principal authentication with secrets supplied from the environment (recommended for CI):
<project>
<!-- ... modelVersion, groupId, artifactId, version ... -->
<properties>
<azure.keyvault.uri>https://my-company.vault.azure.net/</azure.keyvault.uri>
<azure.keyvault.cert-name>my-code-signing-cert</azure.keyvault.cert-name>
<azure.jca.version>2.11.0</azure.jca.version>
<azure.jca.jar.path>${project.build.directory}/jca/azure-security-keyvault-jca-${azure.jca.version}.jar</azure.jca.jar.path>
<webstart.plugin.version>1.2.4-bravura</webstart.plugin.version>
</properties>
<dependencies>
<!-- Used only to resolve and copy the JCA provider JAR onto the module-path -->
<dependency>
<groupId>com.azure</groupId>
<artifactId>azure-security-keyvault-jca</artifactId>
<version>${azure.jca.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<!-- Copy azure-security-keyvault-jca.jar before signing -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-dependency-plugin</artifactId>
<executions>
<execution>
<id>copy-azure-jca</id>
<phase>generate-resources</phase>
<goals>
<goal>copy</goal>
</goals>
<configuration>
<artifactItems>
<artifactItem>
<groupId>com.azure</groupId>
<artifactId>azure-security-keyvault-jca</artifactId>
<version>${azure.jca.version}</version>
<outputDirectory>${project.build.directory}/jca</outputDirectory>
</artifactItem>
</artifactItems>
</configuration>
</execution>
</executions>
</plugin>
<plugin>
<groupId>io.github.martinhickson</groupId>
<artifactId>webstart-maven-plugin</artifactId>
<version>${webstart.plugin.version}</version>
<executions>
<execution>
<phase>package</phase>
<goals>
<goal>jnlp-inline</goal>
</goals>
</execution>
</executions>
<configuration>
<jnlp>
<outputFile>launch.jnlp</outputFile>
<mainClass>com.example.MyApplication</mainClass>
</jnlp>
<sign>
<!-- Azure Key Vault keystore -->
<keystore>NONE</keystore>
<storetype>AzureKeyVault</storetype>
<storepass></storepass>
<alias>${azure.keyvault.cert-name}</alias>
<!-- JCA provider (maps to jarsigner -providerName / -providerClass) -->
<providerName>AzureKeyVault</providerName>
<providerClass>com.azure.security.keyvault.jca.KeyVaultJcaProvider</providerClass>
<!-- Full PEM chain from your CA (maps to jarsigner -certchain) -->
<certchain>${project.basedir}/src/main/signing/cert-chain.pem</certchain>
<!-- Timestamp and digest algorithms -->
<tsaLocation>http://timestamp.digicert.com</tsaLocation>
<digestalg>SHA-256</digestalg>
<verify>true</verify>
<!-- JVM arguments forwarded to the jarsigner subprocess (-J prefix) -->
<arguments>
<argument>-J--module-path=${azure.jca.jar.path}</argument>
<argument>-J--add-modules=com.azure.security.keyvault.jca</argument>
<argument>-J-Dazure.keyvault.uri=${azure.keyvault.uri}</argument>
<argument>-J-Dazure.keyvault.tenant-id=${env.AZURE_TENANT_ID}</argument>
<argument>-J-Dazure.keyvault.client-id=${env.AZURE_CLIENT_ID}</argument>
<argument>-J-Dazure.keyvault.client-secret=${env.AZURE_CLIENT_SECRET}</argument>
</arguments>
</sign>
</configuration>
</plugin>
</plugins>
</build>
</project>Set credentials in the environment, then package as usual:
export AZURE_TENANT_ID="00000000-0000-0000-0000-000000000000"
export AZURE_CLIENT_ID="00000000-0000-0000-0000-000000000000"
export AZURE_CLIENT_SECRET="your-client-secret"
mvn clean packageSigned JARs and launch.jnlp are written under target/jnlp/.
When the build agent already has Azure credentials (managed identity or az login), omit tenant/client/secret and pass only the vault URI:
<sign>
<keystore>NONE</keystore>
<storetype>AzureKeyVault</storetype>
<storepass></storepass>
<alias>${azure.keyvault.cert-name}</alias>
<providerName>AzureKeyVault</providerName>
<providerClass>com.azure.security.keyvault.jca.KeyVaultJcaProvider</providerClass>
<certchain>${project.basedir}/src/main/signing/cert-chain.pem</certchain>
<tsaLocation>http://timestamp.digicert.com</tsaLocation>
<digestalg>SHA-256</digestalg>
<verify>true</verify>
<arguments>
<argument>-J--module-path=${azure.jca.jar.path}</argument>
<argument>-J--add-modules=com.azure.security.keyvault.jca</argument>
<argument>-J-Dazure.keyvault.uri=${azure.keyvault.uri}</argument>
</arguments>
</sign>For a user-assigned managed identity, add:
<argument>-J-Dazure.keyvault.managed-identity=${env.AZURE_MANAGED_IDENTITY_CLIENT_ID}</argument>The configuration above corresponds to:
jarsigner \
-keystore NONE \
-storetype AzureKeyVault \
-storepass "" \
-signedjar signed.jar unsigned.jar "${AZURE_KEYVAULT_CERT_NAME}" \
-certchain src/main/signing/cert-chain.pem \
-providerName AzureKeyVault \
-providerClass com.azure.security.keyvault.jca.KeyVaultJcaProvider \
-digestalg SHA-256 \
-tsa http://timestamp.digicert.com \
-J--module-path=target/jca/azure-security-keyvault-jca-2.11.0.jar \
-J--add-modules=com.azure.security.keyvault.jca \
-J-Dazure.keyvault.uri=https://my-company.vault.azure.net/ \
-J-Dazure.keyvault.tenant-id="${AZURE_TENANT_ID}" \
-J-Dazure.keyvault.client-id="${AZURE_CLIENT_ID}" \
-J-Dazure.keyvault.client-secret="${AZURE_CLIENT_SECRET}"| Symptom | Likely fix |
|---|---|
Certificate chain warnings from jarsigner |
Provide <certchain> with a PEM file containing the full chain from your CA |
Could not obtain key store location |
Use <keystore>NONE</keystore> for Azure Key Vault (not a file path) |
| Provider / module errors on JDK 11+ | Ensure -J--module-path and -J--add-modules=com.azure.security.keyvault.jca point at the copied JCA JAR |
Custom -J arguments ignored |
Upgrade to 1.2.4-bravura or later — POM-configured arguments are preserved through signing |
| Auth failures in CI | Confirm Key Vault RBAC/access policy for the service principal or managed identity used by the agent |
| Goal | Purpose |
|---|---|
jnlp-inline |
Generate JNLP and bundle JARs inline (most common) |
jnlp |
Generate JNLP for standard project layouts |
jnlp-single |
Single-artifact JNLP generation |
jnlp-download-servlet |
Generate servlet configuration for JNLP download |
unsign |
Remove signatures from JARs |
report |
Maven site report |
Deploy JNLP applications with IcedTea-Web — a maintained JNLP client with Pack200 support and modern JDK compatibility.
webstart/
├── webstart-maven-plugin/ # The Maven plugin
├── webstart-jnlp-servlet/ # JNLP download servlet (Jakarta EE 10)
├── webstart-jnlp-servlet-it/ # Servlet ITs (Undertow; profile integration-test)
└── webstart-it/ # Standalone plugin IT (not in reactor)
Requirements: JDK 11+ to compile; JDK 21 supported for build and runtime.
git clone https://github.com/martinhickson/webstart.git
cd webstart
git checkout main
mvn clean installmvn verify -pl webstart-maven-pluginJaCoCo reports are generated under webstart-maven-plugin/target/site/jacoco/ (module) and target/site/jacoco-aggregate/ (reactor root). Coverage thresholds apply to core library code (Mojo entry points are excluded and exercised via webstart-it).
Push and pull request builds run unit tests, JaCoCo checks, servlet ITs, and the webstart-it end-to-end harness via GitHub Actions (see .github/workflows/ci.yml).
The webstart-jnlp-servlet-it module deploys JnlpDownloadServlet on Undertow with Jakarta Servlet 6. It is only built when the integration-test profile is active:
mvn verify -Pintegration-testmvn clean verify -f webstart-it/pom.xmlSee webstart-it/README.md for details.
Releases are triggered manually from Actions → Release → Run workflow.
| Input | Example | Description |
|---|---|---|
release_version |
1.2.4-bravura |
Version applied to all POMs |
skipTests |
false |
Skip unit tests during build and deploy |
run_integration_tests |
true |
Run webstart-it before tagging |
deploy_to_maven_central |
false |
Also publish to Maven Central (optional) |
The workflow creates branch v{version}, tag v{version}-release, publishes to GitHub Packages, optionally to Maven Central, and opens a GitHub Release.
- JDK 11+ compilation and JDK 21 build/runtime support
- Pack200 graceful degradation and Commons Compress path
- Azure KeyVault JCA and hardware key signing improvements
- Maven Central publishing under
io.github.martinhickson webstart-jnlp-servletmigrated to Jakarta Servlet 6 (Jakarta EE 10)- Unit tests (100+) with JaCoCo coverage thresholds; servlet ITs via Undertow (
-Pintegration-test) - CI on push/PR; manual release workflow and standalone
webstart-itplugin harness
MIT — see LICENSE.txt.