Java Web Start Alternatives in Java

Introduction

With Java Web Start being deprecated and removed from recent JDK versions, developers need modern alternatives for deploying Java applications. This guide covers various replacement technologies and deployment strategies.

Modern Deployment Options

1. JLink with Custom JRE

Creating Custom Runtime Images

// Module definition for jlink
module com.myapp {
requires java.base;
requires java.desktop;
requires java.sql;
requires javafx.controls; // if using JavaFX
exports com.myapp;
}
<!-- Maven configuration for jlink -->
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.11.0</version>
<configuration>
<release>17</release>
</configuration>
</plugin>
<plugin>
<groupId>org.moditect</groupId>
<artifactId>moditect-maven-plugin</artifactId>
<version>1.0.0.Final</version>
<executions>
<execution>
<id>create-runtime-image</id>
<phase>package</phase>
<goals>
<goal>create-runtime-image</goal>
</goals>
<configuration>
<modulePath>
<path>${project.build.directory}/modules</path>
</modulePath>
<modules>
<module>com.myapp</module>
</modules>
<launcher>
<name>myapp</name>
<module>com.myapp/com.myapp.Main</module>
</launcher>
<outputDirectory>${project.build.directory}/custom-runtime</outputDirectory>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>

jlink Build Script

#!/bin/bash
# build-runtime.sh
# Create custom JRE with jlink
jlink \
--add-modules java.base,java.desktop,java.sql,javafx.controls \
--output myapp-runtime \
--strip-debug \
--compress=2 \
--no-header-files \
--no-man-pages
# Package application with custom runtime
jar --create --file myapp.jar --main-class com.myapp.Main -C target/classes .

2. Java Package (jpackage)

jpackage Configuration

// Application main class
package com.myapp;
public class Main {
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
JFrame frame = new JFrame("My Application");
frame.setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
frame.setSize(800, 600);
frame.setContentPane(new MainPanel());
frame.setVisible(true);
});
}
}
<!-- Maven jpackage plugin -->
<plugin>
<groupId>org.panteleyev</groupId>
<artifactId>jpackage-maven-plugin</artifactId>
<version>1.6.0</version>
<configuration>
<name>MyApplication</name>
<vendor>My Company</vendor>
<version>1.0.0</version>
<copyright>Copyright 2024</copyright>
<appVersion>1.0.0</appVersion>
<mainClass>com.myapp.Main</mainClass>
<mainJar>myapp-1.0.0.jar</mainJar>
<icon>src/main/resources/icon.ico</icon>
<destination>target/dist</destination>
<!-- Windows configuration -->
<winConfig>
<winDirChooser>true</winDirChooser>
<winMenu>true</winMenu>
<winShortcut>true</winShortcut>
<winPerUserInstall>true</winPerUserInstall>
</winConfig>
<!-- Linux configuration -->
<linuxConfig>
<linuxShortcut>true</linuxShortcut>
</linuxConfig>
<!-- macOS configuration -->
<macConfig>
<macCategory>public.app-category.business</macCategory>
<macSign>false</macSign>
</macConfig>
</configuration>
</plugin>

jpackage Build Scripts

#!/bin/bash
# build-packages.sh
# Build for Windows
jpackage \
--name MyApplication \
--input target/ \
--main-jar myapp-1.0.0.jar \
--main-class com.myapp.Main \
--type msi \
--dest target/dist \
--vendor "My Company" \
--app-version 1.0.0 \
--copyright "Copyright 2024" \
--win-dir-chooser \
--win-menu \
--win-shortcut
# Build for Linux
jpackage \
--name MyApplication \
--input target/ \
--main-jar myapp-1.0.0.jar \
--main-class com.myapp.Main \
--type deb \
--dest target/dist \
--vendor "My Company" \
--app-version 1.0.0 \
--linux-shortcut
# Build for macOS
jpackage \
--name MyApplication \
--input target/ \
--main-jar myapp-1.0.0.jar \
--main-class com.myapp.Main \
--type dmg \
--dest target/dist \
--vendor "My Company" \
--app-version 1.0.0 \
--mac-package-name "My Application"

3. Web-Based Alternatives

Java Applet Replacement with Web Technologies

<!DOCTYPE html>
<html>
<head>
<title>Java Application - Web Version</title>
<style>
.container {
max-width: 1200px;
margin: 0 auto;
padding: 20px;
}
#java-app-container {
border: 1px solid #ccc;
padding: 20px;
margin: 20px 0;
}
</style>
</head>
<body>
<div class="container">
<h1>My Java Application - Web Version</h1>
<!-- Application UI will be loaded here -->
<div id="java-app-container">
<p>Loading application...</p>
</div>
<!-- Web Start alternative using WebAssembly -->
<div id="wasm-container"></div>
</div>
<script>
// Progressive Web App functionality
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('/sw.js')
.then(registration => console.log('SW registered'))
.catch(error => console.log('SW registration failed'));
}
// Load application based on technology
async function loadApplication() {
const technology = detectBestTechnology();
switch(technology) {
case 'webassembly':
await loadWebAssemblyVersion();
break;
case 'teavm':
await loadTeaVMVersion();
break;
case 'cheerpj':
await loadCheerPJVersion();
break;
default:
loadHTML5Version();
}
}
function detectBestTechnology() {
// Detect browser capabilities
if (typeof WebAssembly === 'object' && WebAssembly.validate) {
return 'webassembly';
} else if (navigator.userAgent.includes('Chrome')) {
return 'teavm';
} else {
return 'html5';
}
}
async function loadWebAssemblyVersion() {
// Load WebAssembly version of Java app
const response = await fetch('/wasm/myapp.wasm');
const bytes = await response.arrayBuffer();
const { instance } = await WebAssembly.instantiate(bytes);
// Initialize application
instance.exports._initialize();
}
function loadTeaVMVersion() {
// Load TeaVM compiled JavaScript
const script = document.createElement('script');
script.src = '/teavm/myapp.js';
document.head.appendChild(script);
}
function loadHTML5Version() {
// Fallback to pure HTML5/JavaScript
document.getElementById('java-app-container').innerHTML = `
<div class="html5-app">
<h2>HTML5 Version</h2>
<p>This is the HTML5/JavaScript version of the application.</p>
<!-- Application UI components -->
</div>
`;
}
// Initialize application when page loads
window.addEventListener('load', loadApplication);
</script>
</body>
</html>

4. TeaVM for Web Deployment

Converting Java to JavaScript

package com.myapp.web;
import org.teavm.jso.JSBody;
import org.teavm.jso.dom.html.HTMLDocument;
import org.teavm.jso.dom.html.HTMLElement;
import org.teavm.jso.dom.html.HTMLInputElement;
import org.teavm.jso.dom.html.HTMLButtonElement;
public class WebApplication {
private static int counter = 0;
public static void main(String[] args) {
HTMLDocument document = HTMLDocument.current();
// Create UI components
HTMLElement container = document.createElement("div");
container.setAttribute("class", "app-container");
HTMLElement title = document.createElement("h1");
title.setInnerHTML("Java Application in Browser");
HTMLElement counterDisplay = document.createElement("div");
counterDisplay.setAttribute("id", "counter");
counterDisplay.setInnerHTML("Count: " + counter);
HTMLButtonElement incrementBtn = (HTMLButtonElement) document.createElement("button");
incrementBtn.setInnerHTML("Increment");
incrementBtn.addEventListener("click", event -> {
counter++;
updateCounterDisplay();
});
HTMLButtonElement decrementBtn = (HTMLButtonElement) document.createElement("button");
decrementBtn.setInnerHTML("Decrement");
decrementBtn.addEventListener("click", event -> {
counter--;
updateCounterDisplay();
});
// Add components to container
container.appendChild(title);
container.appendChild(counterDisplay);
container.appendChild(incrementBtn);
container.appendChild(decrementBtn);
// Add to document
document.getBody().appendChild(container);
// Initialize application state
initializeApp();
}
private static void updateCounterDisplay() {
HTMLElement counterDisplay = HTMLDocument.current().getElementById("counter");
if (counterDisplay != null) {
counterDisplay.setInnerHTML("Count: " + counter);
}
}
private static void initializeApp() {
// Load initial data or configuration
loadConfiguration();
setupEventListeners();
}
private static void loadConfiguration() {
// Load configuration from server or local storage
String config = getConfigFromStorage();
if (config != null) {
applyConfiguration(config);
}
}
@JSBody(script = "return localStorage.getItem('appConfig');")
private static native String getConfigFromStorage();
@JSBody(params = {"config"}, script = "localStorage.setItem('appConfig', config);")
private static native void saveConfigToStorage(String config);
private static void applyConfiguration(String config) {
// Apply loaded configuration
System.out.println("Applying configuration: " + config);
}
private static void setupEventListeners() {
// Set up global event listeners
HTMLDocument.current().addEventListener("keydown", event -> {
// Handle global keyboard shortcuts
handleKeyboardEvent(event);
});
}
private static void handleKeyboardEvent(org.teavm.jso.dom.events.Event event) {
// Handle keyboard events
System.out.println("Keyboard event handled");
}
}

5. Self-Contained Applications

Install4J for Cross-Platform Installers

// Configuration class for installer settings
package com.myapp.installer;
public class InstallerConfig {
private String applicationName;
private String version;
private String vendor;
private String mainClass;
private File inputDirectory;
private File outputDirectory;
private List<Module> modules;
// Getters and setters
public static class Module {
private String name;
private boolean required;
// ... other properties
}
}
<!-- install4j configuration template -->
<install4j version="9.0">
<application name="MyApplication" version="1.0.0" vendor="My Company">
<executable name="MyApp" 
mainClass="com.myapp.Main"
icon="${compiler:sys.resourceDir}/app.ico">
<jre minVersion="11" maxVersion="17"/>
<launcher parameters="-Xmx512m -Dapp.config=config.properties"/>
</executable>
<mediaSets>
<mediaSet name="windows">
<media name="Windows Installer" file="${compiler:sys.mediaDir}/windows/setup.exe">
<distribution>windows</distribution>
<jreBundles>jre11</jreBundles>
</media>
</mediaSet>
<mediaSet name="mac">
<media name="macOS App" file="${compiler:sys.mediaDir}/mac/MyApp.dmg">
<distribution>mac</distribution>
<jreBundles>jre11</jreBundles>
</media>
</mediaSet>
<mediaSet name="linux">
<media name="Linux Installer" file="${compiler:sys.mediaDir}/linux/MyApp.sh">
<distribution>linux</distribution>
<jreBundles>jre11</jreBundles>
</media>
</mediaSet>
</mediaSets>
</application>
</install4j>

6. Cloud-Native Deployment

Docker Containerization

# Dockerfile for Java application
FROM eclipse-temurin:17-jre as runtime
# Install necessary packages
RUN apt-get update && apt-get install -y \
fontconfig \
libxrender1 \
libxtst6 \
libxi6 \
&& rm -rf /var/lib/apt/lists/*
# Create application directory
WORKDIR /app
# Copy custom JRE (if using jlink)
COPY --from=builder /app/myapp-runtime ./jre
# Copy application JAR
COPY target/myapp-1.0.0.jar app.jar
# Create non-root user
RUN groupadd -r appuser && useradd -r -g appuser appuser
USER appuser
# Expose application port (if web app)
EXPOSE 8080
# Set entry point
ENTRYPOINT ["/app/jre/bin/java", "-jar", "app.jar"]
# docker-compose.yml for local development
version: '3.8'
services:
my-java-app:
build: .
ports:
- "8080:8080"
environment:
- JAVA_OPTS=-Xmx512m -Dspring.profiles.active=dev
volumes:
- ./config:/app/config:ro
depends_on:
- database
database:
image: postgres:13
environment:
POSTGRES_DB: myapp
POSTGRES_USER: appuser
POSTGRES_PASSWORD: secret
volumes:
- db_data:/var/lib/postgresql/data
volumes:
db_data:

Kubernetes Deployment

# kubernetes/deployment.yaml
apiVersion: apps/v1
kind: Deployment
metadata:
name: my-java-app
spec:
replicas: 3
selector:
matchLabels:
app: my-java-app
template:
metadata:
labels:
app: my-java-app
spec:
containers:
- name: my-java-app
image: mycompany/my-java-app:1.0.0
ports:
- containerPort: 8080
env:
- name: JAVA_OPTS
value: "-Xmx512m -Dspring.profiles.active=prod"
resources:
requests:
memory: "512Mi"
cpu: "250m"
limits:
memory: "1Gi"
cpu: "500m"
livenessProbe:
httpGet:
path: /health
port: 8080
initialDelaySeconds: 30
periodSeconds: 10
readinessProbe:
httpGet:
path: /ready
port: 8080
initialDelaySeconds: 5
periodSeconds: 5
---
apiVersion: v1
kind: Service
metadata:
name: my-java-app-service
spec:
selector:
app: my-java-app
ports:
- protocol: TCP
port: 80
targetPort: 8080
type: LoadBalancer

7. Progressive Web App (PWA) Approach

Service Worker for Offline Capability

// sw.js - Service Worker
const CACHE_NAME = 'my-java-app-v1';
const urlsToCache = [
'/',
'/index.html',
'/styles/main.css',
'/scripts/app.js',
'/wasm/myapp.wasm'
];
self.addEventListener('install', event => {
event.waitUntil(
caches.open(CACHE_NAME)
.then(cache => cache.addAll(urlsToCache))
);
});
self.addEventListener('fetch', event => {
event.respondWith(
caches.match(event.request)
.then(response => {
if (response) {
return response;
}
return fetch(event.request);
}
)
);
});

Web App Manifest

{
"name": "My Java Application",
"short_name": "MyApp",
"description": "Java application running as PWA",
"start_url": "/",
"display": "standalone",
"background_color": "#ffffff",
"theme_color": "#000000",
"icons": [
{
"src": "icons/icon-192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "icons/icon-512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

8. Update Mechanisms

Application Auto-Updater

package com.myapp.update;
import java.io.*;
import java.net.*;
import java.nio.file.*;
import java.util.Properties;
import java.util.concurrent.CompletableFuture;
public class ApplicationUpdater {
private static final String UPDATE_URL = "https://updates.mycompany.com/myapp/";
private static final String VERSION_FILE = "version.properties";
public static CompletableFuture<Boolean> checkForUpdates() {
return CompletableFuture.supplyAsync(() -> {
try {
Properties localProps = loadLocalVersion();
Properties remoteProps = loadRemoteVersion();
String localVersion = localProps.getProperty("version", "1.0.0");
String remoteVersion = remoteProps.getProperty("version", "1.0.0");
return isNewerVersion(remoteVersion, localVersion);
} catch (Exception e) {
System.err.println("Failed to check for updates: " + e.getMessage());
return false;
}
});
}
public static CompletableFuture<Void> performUpdate() {
return CompletableFuture.runAsync(() -> {
try {
// Download update package
Path updateFile = downloadUpdate();
// Verify update integrity
if (!verifyUpdate(updateFile)) {
throw new RuntimeException("Update verification failed");
}
// Apply update
applyUpdate(updateFile);
// Restart application if needed
restartApplication();
} catch (Exception e) {
throw new RuntimeException("Update failed", e);
}
});
}
private static Properties loadLocalVersion() throws IOException {
Properties props = new Properties();
try (InputStream is = Files.newInputStream(Paths.get(VERSION_FILE))) {
props.load(is);
}
return props;
}
private static Properties loadRemoteVersion() throws IOException {
URL url = new URL(UPDATE_URL + "version.properties");
Properties props = new Properties();
try (InputStream is = url.openStream()) {
props.load(is);
}
return props;
}
private static boolean isNewerVersion(String remote, String local) {
// Simple version comparison
String[] remoteParts = remote.split("\\.");
String[] localParts = local.split("\\.");
for (int i = 0; i < Math.min(remoteParts.length, localParts.length); i++) {
int r = Integer.parseInt(remoteParts[i]);
int l = Integer.parseInt(localParts[i]);
if (r > l) return true;
if (r < l) return false;
}
return remoteParts.length > localParts.length;
}
private static Path downloadUpdate() throws IOException {
URL updateUrl = new URL(UPDATE_URL + "update.zip");
Path tempFile = Files.createTempFile("update", ".zip");
try (InputStream in = updateUrl.openStream();
OutputStream out = Files.newOutputStream(tempFile)) {
in.transferTo(out);
}
return tempFile;
}
private static boolean verifyUpdate(Path updateFile) {
// Implement checksum verification
return true;
}
private static void applyUpdate(Path updateFile) throws IOException {
// Extract and replace files
Path appDir = Paths.get("").toAbsolutePath();
// Implementation depends on packaging strategy
}
private static void restartApplication() {
// Implementation depends on platform
System.exit(0);
}
}

9. Migration Strategy

Legacy Web Start Migration Helper

package com.myapp.migration;
import java.awt.*;
import java.net.URI;
import javax.swing.*;
public class WebStartMigrationAssistant extends JFrame {
private JTextArea infoArea;
public WebStartMigrationAssistant() {
setTitle("Java Web Start Migration Assistant");
setDefaultCloseOperation(JFrame.EXIT_ON_CLOSE);
setSize(600, 400);
initializeUI();
}
private void initializeUI() {
JPanel mainPanel = new JPanel(new BorderLayout());
// Information area
infoArea = new JTextArea();
infoArea.setEditable(false);
infoArea.setText(getMigrationInfo());
mainPanel.add(new JScrollPane(infoArea), BorderLayout.CENTER);
// Button panel
JPanel buttonPanel = new JPanel(new FlowLayout());
JButton downloadButton = new JButton("Download New Version");
downloadButton.addActionListener(e -> downloadNewVersion());
JButton webVersionButton = new JButton("Use Web Version");
webVersionButton.addActionListener(e -> openWebVersion());
JButton helpButton = new JButton("Get Help");
helpButton.addActionListener(e -> showHelp());
buttonPanel.add(downloadButton);
buttonPanel.add(webVersionButton);
buttonPanel.add(helpButton);
mainPanel.add(buttonPanel, BorderLayout.SOUTH);
add(mainPanel);
}
private String getMigrationInfo() {
return """
Java Web Start has been deprecated and removed from recent Java versions.
This application can no longer be launched via Java Web Start.
Migration Options:
1. Download the standalone desktop version
2. Use the web-based version in your browser
3. Contact support for assistance
The new versions offer improved security, better performance, 
and automatic updates.
""";
}
private void downloadNewVersion() {
try {
Desktop.getDesktop().browse(new URI("https://mycompany.com/download"));
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, 
"Cannot open download page. Please visit https://mycompany.com/download");
}
}
private void openWebVersion() {
try {
Desktop.getDesktop().browse(new URI("https://mycompany.com/webapp"));
} catch (Exception ex) {
JOptionPane.showMessageDialog(this, 
"Cannot open web version. Please visit https://mycompany.com/webapp");
}
}
private void showHelp() {
JOptionPane.showMessageDialog(this, 
"For assistance with migration, please contact:\n" +
"Email: [email protected]\n" +
"Phone: 1-800-123-4567");
}
public static void main(String[] args) {
SwingUtilities.invokeLater(() -> {
new WebStartMigrationAssistant().setVisible(true);
});
}
}

Best Practices Summary

Deployment Checklist

  1. Choose the right deployment method based on your audience
  2. Use jlink for custom runtimes to reduce size
  3. Implement auto-update mechanisms for desktop apps
  4. Provide web alternatives for maximum accessibility
  5. Containerize for cloud deployment
  6. Implement proper error handling for network issues
  7. Provide clear migration paths for existing users

Security Considerations

  • Sign your application packages
  • Use HTTPS for downloads and updates
  • Implement proper sandboxing for web applications
  • Regular security updates for dependencies

By adopting these modern deployment strategies, you can provide a seamless experience for your users while leveraging the latest Java technologies and deployment best practices.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper