Java to JavaScript with TeaVM in Java

Introduction

TeaVM is an ahead-of-time compiler for Java bytecode that converts Java applications to JavaScript, allowing Java code to run in web browsers. It provides a way to write web applications in Java while maintaining good performance and compatibility.

TeaVM Setup and Configuration

Maven Configuration

<!-- pom.xml -->
<project>
<properties>
<teavm.version>0.8.0</teavm.version>
</properties>
<dependencies>
<!-- TeaVM core -->
<dependency>
<groupId>org.teavm</groupId>
<artifactId>teavm-classlib</artifactId>
<version>${teavm.version}</version>
</dependency>
<dependency>
<groupId>org.teavm</groupId>
<artifactId>teavm-platform</artifactId>
<version>${teavm.version}</version>
</dependency>
<!-- JavaScript interoperability -->
<dependency>
<groupId>org.teavm</groupId>
<artifactId>teavm-jso</artifactId>
<version>${teavm.version}</version>
</dependency>
<dependency>
<groupId>org.teavm</groupId>
<artifactId>teavm-jso-apis</artifactId>
<version>${teavm.version}</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.teavm</groupId>
<artifactId>teavm-maven-plugin</artifactId>
<version>${teavm.version}</version>
<executions>
<execution>
<id>web-client</id>
<goals>
<goal>compile</goal>
</goals>
<configuration>
<targetDirectory>${project.build.directory}/teavm</targetDirectory>
<mainClass>com.example.Main</mainClass>
<minifying>true</minifying>
<sourceMapsGenerated>true</sourceMapsGenerated>
<sourceFilesCopied>true</sourceFilesCopied>
<debugInformationGenerated>true</debugInformationGenerated>
<optimization>FULL</optimization>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</build>
</project>

Gradle Configuration

// build.gradle
plugins {
id 'java'
id 'org.teavm' version '0.8.0'
}
teavm {
version = '0.8.0'
all {
mainClass = 'com.example.Main'
targetDirectory = file('build/teavm')
minifying = true
sourceMapsGenerated = true
sourceFilesCopied = true
debugInformationGenerated = true
optimization = 'FULL'
}
}
dependencies {
implementation 'org.teavm:teavm-classlib:0.8.0'
implementation 'org.teavm:teavm-platform:0.8.0'
implementation 'org.teavm:teavm-jso:0.8.0'
implementation 'org.teavm:teavm-jso-apis:0.8.0'
}

Basic TeaVM Application

Simple Web Application

package com.example;
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.Text;
public class Main {
public static void main(String[] args) {
// Get the document
HTMLDocument document = HTMLDocument.current();
// Create a heading
HTMLElement heading = document.createElement("h1");
heading.appendChild(document.createTextNode("Hello from Java!"));
// Create a button
HTMLElement button = document.createElement("button");
button.appendChild(document.createTextNode("Click me!"));
// Add click handler
button.addEventListener("click", event -> {
showAlert("Button clicked from Java!");
updateCounter();
});
// Create counter display
HTMLElement counter = document.createElement("div");
counter.setAttribute("id", "counter");
counter.appendChild(document.createTextNode("Count: 0"));
// Add everything to body
HTMLElement body = document.getBody();
body.appendChild(heading);
body.appendChild(button);
body.appendChild(counter);
}
private static int clickCount = 0;
private static void updateCounter() {
clickCount++;
HTMLElement counter = HTMLDocument.current().getElementById("counter");
if (counter != null) {
counter.setInnerHTML("Count: " + clickCount);
}
}
@JSBody(params = {"message"}, script = "alert(message);")
private static native void showAlert(String message);
}

JavaScript Interoperability

Working with JavaScript Objects

package com.example.js;
import org.teavm.jso.JSBody;
import org.teavm.jso.JSObject;
import org.teavm.jso.JSProperty;
import org.teavm.jso.core.JSString;
public class JavaScriptInterop {
// Interface for JavaScript objects
public interface User extends JSObject {
@JSProperty
String getName();
@JSProperty
void setName(String name);
@JSProperty
int getAge();
@JSProperty
void setAge(int age);
}
// Creating JavaScript objects from Java
@JSBody(script = "return {};")
public static native User createUser();
// Calling JavaScript functions
@JSBody(params = {"user"}, script = "console.log(user);")
public static native void logUser(User user);
// Working with JSON
@JSBody(params = {"json"}, script = "return JSON.parse(json);")
public static native User parseUser(String json);
@JSBody(params = {"user"}, script = "return JSON.stringify(user);")
public static native String stringifyUser(User user);
public static void demonstrateInterop() {
// Create a JavaScript object
User user = createUser();
user.setName("John Doe");
user.setAge(30);
// Log to console
logUser(user);
// Convert to JSON and back
String json = stringifyUser(user);
System.out.println("JSON: " + json);
User parsedUser = parseUser(json);
System.out.println("Parsed name: " + parsedUser.getName());
}
}

DOM Manipulation

package com.example.dom;
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;
import org.teavm.jso.dom.events.EventListener;
import org.teavm.jso.dom.events.Event;
public class DOMExample {
public static void createTodoApp() {
HTMLDocument document = HTMLDocument.current();
HTMLElement body = document.getBody();
// Create container
HTMLElement container = document.createElement("div");
container.setAttribute("class", "container");
// Create input field
HTMLInputElement input = (HTMLInputElement) document.createElement("input");
input.setType("text");
input.setAttribute("placeholder", "Enter a todo item");
input.setAttribute("id", "todoInput");
// Create add button
HTMLButtonElement addButton = (HTMLButtonElement) document.createElement("button");
addButton.setInnerHTML("Add Todo");
// Create todo list
HTMLElement todoList = document.createElement("ul");
todoList.setAttribute("id", "todoList");
// Add event listener
addButton.addEventListener("click", event -> {
String todoText = input.getValue().trim();
if (!todoText.isEmpty()) {
addTodoItem(todoList, todoText);
input.setValue(""); // Clear input
}
});
// Also allow Enter key
input.addEventListener("keypress", event -> {
if ("Enter".equals(event.getKey())) {
String todoText = input.getValue().trim();
if (!todoText.isEmpty()) {
addTodoItem(todoList, todoText);
input.setValue("");
}
}
});
// Add everything to container
container.appendChild(input);
container.appendChild(addButton);
container.appendChild(todoList);
body.appendChild(container);
}
private static void addTodoItem(HTMLElement todoList, String text) {
HTMLDocument document = HTMLDocument.current();
// Create list item
HTMLElement li = document.createElement("li");
li.setAttribute("class", "todo-item");
// Create text span
HTMLElement span = document.createElement("span");
span.setInnerHTML(text);
// Create delete button
HTMLButtonElement deleteBtn = (HTMLButtonElement) document.createElement("button");
deleteBtn.setInnerHTML("Delete");
deleteBtn.setAttribute("class", "delete-btn");
deleteBtn.addEventListener("click", event -> {
todoList.removeChild(li);
});
// Add elements to list item
li.appendChild(span);
li.appendChild(deleteBtn);
// Add to list
todoList.appendChild(li);
}
}

Advanced TeaVM Features

Working with JavaScript Libraries

package com.example.chart;
import org.teavm.jso.JSBody;
import org.teavm.jso.JSObject;
import org.teavm.jso.JSProperty;
import org.teavm.jso.dom.html.HTMLDocument;
import org.teavm.jso.dom.html.HTMLElement;
public class ChartJSIntegration {
// Chart.js configuration interface
public interface ChartConfig extends JSObject {
@JSProperty
String getType();
@JSProperty
void setType(String type);
@JSProperty
ChartData getData();
@JSProperty
void setData(ChartData data);
@JSProperty
ChartOptions getOptions();
@JSProperty
void setOptions(ChartOptions options);
}
public interface ChartData extends JSObject {
@JSProperty
JSString[] getLabels();
@JSProperty
void setLabels(JSString[] labels);
@JSProperty
Dataset[] getDatasets();
@JSProperty
void setDatasets(Dataset[] datasets);
}
public interface Dataset extends JSObject {
@JSProperty
String getLabel();
@JSProperty
void setLabel(String label);
@JSProperty
int[] getData();
@JSProperty
void setData(int[] data);
@JSProperty
String[] getBackgroundColor();
@JSProperty
void setBackgroundColor(String[] backgroundColor);
}
public interface ChartOptions extends JSObject {
@JSProperty
boolean isResponsive();
@JSProperty
void setResponsive(boolean responsive);
}
public interface Chart extends JSObject {
void destroy();
}
// Native JavaScript methods for Chart.js
@JSBody(script = "return typeof Chart !== 'undefined';")
public static native boolean isChartJSAvailable();
@JSBody(params = {"canvas", "config"}, script = "return new Chart(canvas, config);")
public static native Chart createChart(HTMLElement canvas, ChartConfig config);
public static void createSampleChart() {
if (!isChartJSAvailable()) {
System.err.println("Chart.js is not available");
return;
}
HTMLDocument document = HTMLDocument.current();
// Create canvas element
HTMLElement canvas = document.createElement("canvas");
canvas.setAttribute("id", "myChart");
canvas.setAttribute("width", "400");
canvas.setAttribute("height", "400");
// Create chart configuration
ChartConfig config = createChartConfig();
// Create chart
Chart chart = createChart(canvas, config);
// Add to document
document.getBody().appendChild(canvas);
}
private static ChartConfig createChartConfig() {
ChartConfig config = (ChartConfig) JSObject.create();
config.setType("bar");
ChartData data = (ChartData) JSObject.create();
data.setLabels(new JSString[]{
JSString.valueOf("January"),
JSString.valueOf("February"), 
JSString.valueOf("March"),
JSString.valueOf("April"),
JSString.valueOf("May")
});
Dataset dataset = (Dataset) JSObject.create();
dataset.setLabel("Sales");
dataset.setData(new int[]{65, 59, 80, 81, 56});
dataset.setBackgroundColor(new String[]{
"rgba(255, 99, 132, 0.2)",
"rgba(54, 162, 235, 0.2)",
"rgba(255, 206, 86, 0.2)",
"rgba(75, 192, 192, 0.2)",
"rgba(153, 102, 255, 0.2)"
});
data.setDatasets(new Dataset[]{dataset});
config.setData(data);
ChartOptions options = (ChartOptions) JSObject.create();
options.setResponsive(true);
config.setOptions(options);
return config;
}
}

Async Operations with Promises

package com.example.async;
import org.teavm.jso.JSBody;
import org.teavm.jso.JSObject;
import org.teavm.jso.core.JSPromise;
import org.teavm.jso.typedarrays.ArrayBuffer;
public class AsyncOperations {
public interface FetchResponse extends JSObject {
JSPromise<ArrayBuffer> arrayBuffer();
JSPromise<String> text();
JSPromise<JSObject> json();
}
// Fetch API integration
@JSBody(params = {"url"}, script = "return fetch(url);")
public static native JSPromise<FetchResponse> fetch(String url);
public static void demonstrateAsyncOperations() {
// Fetch data from API
fetch("https://api.example.com/data")
.then(response -> response.json())
.then(data -> {
System.out.println("Data received: " + data);
processData(data);
return null;
})
.catch(error -> {
System.err.println("Error fetching data: " + error);
return null;
});
}
private static void processData(JSObject data) {
// Process the received data
HTMLDocument document = HTMLDocument.current();
HTMLElement div = document.createElement("div");
div.setInnerHTML("Data processed successfully");
document.getBody().appendChild(div);
}
// Custom async operations
public static JSPromise<String> delayedOperation(String input, int delayMs) {
return new JSPromise<>((resolve, reject) -> {
setTimeout(() -> {
resolve.execute("Processed: " + input);
}, delayMs);
});
}
@JSBody(params = {"callback", "delay"}, script = "setTimeout(callback, delay);")
private static native void setTimeout(Runnable callback, int delay);
public static void useCustomAsync() {
delayedOperation("Hello", 2000)
.then(result -> {
System.out.println("Async result: " + result);
return null;
});
}
}

Building a Complete Application

Single Page Application (SPA)

package com.example.spa;
import org.teavm.jso.dom.html.HTMLDocument;
import org.teavm.jso.dom.html.HTMLElement;
import org.teavm.jso.dom.html.HTMLAnchorElement;
import org.teavm.jso.dom.html.HTMLDivElement;
public class SinglePageApplication {
private static HTMLDivElement contentArea;
public static void main(String[] args) {
HTMLDocument document = HTMLDocument.current();
// Create navigation
createNavigation();
// Create content area
contentArea = (HTMLDivElement) document.createElement("div");
contentArea.setAttribute("id", "content");
document.getBody().appendChild(contentArea);
// Show home page by default
showHomePage();
}
private static void createNavigation() {
HTMLDocument document = HTMLDocument.current();
HTMLElement nav = document.createElement("nav");
nav.setAttribute("class", "navbar");
String[] pages = {"Home", "About", "Contact", "Products"};
for (String page : pages) {
HTMLAnchorElement link = (HTMLAnchorElement) document.createElement("a");
link.setInnerHTML(page);
link.setAttribute("href", "#");
link.setAttribute("class", "nav-link");
link.addEventListener("click", event -> {
event.preventDefault();
showPage(page.toLowerCase());
});
nav.appendChild(link);
}
document.getBody().appendChild(nav);
}
private static void showPage(String pageName) {
switch (pageName) {
case "home":
showHomePage();
break;
case "about":
showAboutPage();
break;
case "contact":
showContactPage();
break;
case "products":
showProductsPage();
break;
default:
showHomePage();
}
}
private static void showHomePage() {
contentArea.setInnerHTML("""
<div class="page">
<h1>Welcome to Our SPA</h1>
<p>This is a single page application built with Java and TeaVM.</p>
<button id="homeBtn">Click Me!</button>
</div>
""");
HTMLElement button = HTMLDocument.current().getElementById("homeBtn");
if (button != null) {
button.addEventListener("click", event -> {
showAlert("Hello from Home Page!");
});
}
}
private static void showAboutPage() {
contentArea.setInnerHTML("""
<div class="page">
<h1>About Us</h1>
<p>We are a company that uses Java to build web applications.</p>
<p>TeaVM allows us to write frontend code in Java.</p>
</div>
""");
}
private static void showContactPage() {
contentArea.setInnerHTML("""
<div class="page">
<h1>Contact Us</h1>
<form id="contactForm">
<div>
<label for="name">Name:</label>
<input type="text" id="name" name="name">
</div>
<div>
<label for="email">Email:</label>
<input type="email" id="email" name="email">
</div>
<div>
<label for="message">Message:</label>
<textarea id="message" name="message"></textarea>
</div>
<button type="submit">Send Message</button>
</form>
</div>
""");
HTMLElement form = HTMLDocument.current().getElementById("contactForm");
if (form != null) {
form.addEventListener("submit", event -> {
event.preventDefault();
handleContactForm();
});
}
}
private static void showProductsPage() {
contentArea.setInnerHTML("""
<div class="page">
<h1>Our Products</h1>
<div id="productsList">
<p>Loading products...</p>
</div>
</div>
""");
// Simulate loading products
loadProducts();
}
private static void handleContactForm() {
String name = getInputValue("name");
String email = getInputValue("email");
String message = getInputValue("message");
if (name.isEmpty() || email.isEmpty() || message.isEmpty()) {
showAlert("Please fill in all fields");
return;
}
showAlert("Thank you for your message, " + name + "!");
clearForm();
}
private static String getInputValue(String id) {
HTMLElement element = HTMLDocument.current().getElementById(id);
return element != null ? element.getAttribute("value") : "";
}
private static void clearForm() {
setInputValue("name", "");
setInputValue("email", "");
setInputValue("message", "");
}
private static void setInputValue(String id, String value) {
HTMLElement element = HTMLDocument.current().getElementById(id);
if (element != null) {
element.setAttribute("value", value);
}
}
private static void loadProducts() {
// Simulate API call
setTimeout(() -> {
String[] products = {"Product A", "Product B", "Product C", "Product D"};
StringBuilder html = new StringBuilder();
html.append("<ul>");
for (String product : products) {
html.append("<li>").append(product).append("</li>");
}
html.append("</ul>");
HTMLElement productsList = HTMLDocument.current().getElementById("productsList");
if (productsList != null) {
productsList.setInnerHTML(html.toString());
}
}, 1000);
}
@org.teavm.jso.JSBody(params = {"callback", "delay"}, script = "setTimeout(callback, delay);")
private static native void setTimeout(Runnable callback, int delay);
@org.teavm.jso.JSBody(params = {"message"}, script = "alert(message);")
private static native void showAlert(String message);
}

Testing and Debugging

Unit Testing with TeaVM

package com.example.test;
import org.teavm.junit.TeaVMTestRunner;
import org.junit.Test;
import org.junit.runner.RunWith;
import static org.junit.Assert.*;
@RunWith(TeaVMTestRunner.class)
public class BusinessLogicTest {
@Test
public void testCalculator() {
Calculator calc = new Calculator();
assertEquals(5, calc.add(2, 3));
assertEquals(1, calc.subtract(4, 3));
assertEquals(6, calc.multiply(2, 3));
assertEquals(2, calc.divide(6, 3));
}
@Test
public void testStringUtils() {
assertTrue(StringUtils.isNullOrEmpty(null));
assertTrue(StringUtils.isNullOrEmpty(""));
assertFalse(StringUtils.isNullOrEmpty("Hello"));
assertEquals("HELLO", StringUtils.toUpper("hello"));
}
}
class Calculator {
public int add(int a, int b) { return a + b; }
public int subtract(int a, int b) { return a - b; }
public int multiply(int a, int b) { return a * b; }
public int divide(int a, int b) { return a / b; }
}
class StringUtils {
public static boolean isNullOrEmpty(String str) {
return str == null || str.isEmpty();
}
public static String toUpper(String str) {
return str != null ? str.toUpperCase() : null;
}
}

Debugging Configuration

package com.example.debug;
import org.teavm.jso.JSBody;
import org.teavm.jso.console.Console;
public class DebuggingUtils {
// Console logging
public static void log(String message) {
Console.log("LOG: " + message);
}
public static void warn(String message) {
Console.warn("WARN: " + message);
}
public static void error(String message) {
Console.error("ERROR: " + message);
}
// Performance measurement
public static void measureTime(String operationName, Runnable operation) {
long start = currentTimeMillis();
operation.run();
long end = currentTimeMillis();
log(operationName + " took " + (end - start) + "ms");
}
@JSBody(script = "return Date.now();")
private static native long currentTimeMillis();
// Assertions for development
public static void assertThat(boolean condition, String message) {
if (!condition) {
throw new AssertionError("Assertion failed: " + message);
}
}
// Debug information display
public static void showDebugInfo() {
log("User Agent: " + getUserAgent());
log("Viewport: " + getViewportSize());
}
@JSBody(script = "return navigator.userAgent;")
private static native String getUserAgent();
@JSBody(script = "return window.innerWidth + 'x' + window.innerHeight;")
private static native String getViewportSize();
}

Build and Deployment

HTML Wrapper Template

<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>Java TeaVM Application</title>
<style>
body {
font-family: Arial, sans-serif;
margin: 20px;
background-color: #f5f5f5;
}
.navbar {
background-color: #333;
padding: 10px;
margin-bottom: 20px;
}
.nav-link {
color: white;
text-decoration: none;
margin-right: 15px;
padding: 5px 10px;
}
.nav-link:hover {
background-color: #555;
}
.page {
background: white;
padding: 20px;
border-radius: 5px;
box-shadow: 0 2px 5px rgba(0,0,0,0.1);
}
.todo-item {
display: flex;
justify-content: space-between;
align-items: center;
padding: 10px;
border-bottom: 1px solid #eee;
}
.delete-btn {
background-color: #ff4444;
color: white;
border: none;
padding: 5px 10px;
border-radius: 3px;
cursor: pointer;
}
</style>
</head>
<body>
<div id="loading">Loading Java application...</div>
<!-- TeaVM will replace this with compiled JavaScript -->
<script type="text/javascript" src="teavm.js"></script>
<script>
// Show loading message until Java app is ready
window.addEventListener('load', function() {
document.getElementById('loading').style.display = 'none';
});
</script>
</body>
</html>

Build Script for Production

package com.example.build;
public class BuildConfiguration {
/*
* TeaVM Maven plugin configuration for production:
* 
* <configuration>
*   <mainClass>com.example.Main</mainClass>
*   <targetDirectory>target/teavm</targetDirectory>
*   <minifying>true</minifying>
*   <sourceMapsGenerated>false</sourceMapsGenerated> <!-- Disable for production -->
*   <debugInformationGenerated>false</debugInformationGenerated>
*   <optimization>ADVANCED</optimization>
*   <targetType>JAVASCRIPT</targetType>
*   <classesToPreserve>
*     <class>com.example.Main</class>
*     <class>com.example.**</class>
*   </classesToPreserve>
*   <entryPointName>myJavaApp</entryPointName>
* </configuration>
*/
}

Best Practices

Performance Optimization

package com.example.optimization;
import org.teavm.jso.JSBody;
public class PerformanceTips {
// 1. Minimize JSO calls
public static class EfficientDOMOperations {
// Inefficient: multiple DOM calls
public static void inefficientUpdate() {
for (int i = 0; i < 100; i++) {
appendItem("Item " + i); // 100 DOM calls
}
}
// Efficient: batch DOM operations
public static void efficientUpdate() {
StringBuilder html = new StringBuilder();
for (int i = 0; i < 100; i++) {
html.append("<li>Item ").append(i).append("</li>");
}
setInnerHTML("list", html.toString()); // 1 DOM call
}
@JSBody(params = {"text"}, script = """
var li = document.createElement('li');
li.textContent = text;
document.getElementById('list').appendChild(li);
""")
private static native void appendItem(String text);
@JSBody(params = {"id", "html"}, script = """
document.getElementById(id).innerHTML = html;
""")
private static native void setInnerHTML(String id, String html);
}
// 2. Use efficient data structures
public static class DataOptimization {
// Prefer primitive arrays over collections for performance
public static int[] processNumbers(int[] input) {
int[] result = new int[input.length];
for (int i = 0; i < input.length; i++) {
result[i] = input[i] * 2;
}
return result;
}
}
// 3. Lazy loading for large applications
public static class LazyLoading {
private static boolean featureLoaded = false;
public static void loadFeatureOnDemand() {
if (!featureLoaded) {
loadFeature();
featureLoaded = true;
}
}
@JSBody(script = """
// Dynamically load additional JavaScript
var script = document.createElement('script');
script.src = 'feature.js';
document.head.appendChild(script);
""")
private static native void loadFeature();
}
}

Conclusion

TeaVM provides a powerful way to compile Java to JavaScript, enabling:

  • Full-stack Java development - Use Java for both backend and frontend
  • Type safety - Leverage Java's strong typing in the browser
  • Code reuse - Share business logic between server and client
  • JavaScript interoperability - Seamlessly work with existing JavaScript libraries
  • Good performance - Ahead-of-time compilation produces efficient JavaScript

Key considerations when using TeaVM:

  • Some Java libraries may not be compatible
  • Reflection usage is limited
  • File I/O operations are simulated
  • Focus on business logic and use JSO for DOM manipulation

TeaVM is particularly suitable for:

  • Enterprise web applications
  • Applications with complex business logic
  • Teams with strong Java expertise
  • Projects requiring code sharing between client and server

Leave a Reply

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


Macro Nepal Helper