google-site-verification: google61fe8ba583a51912.html
Apache OpenWhisk Java Actions: Complete Implementation Guide

Introduction

Apache OpenWhisk is a serverless, open-source cloud platform that executes functions in response to events. Java actions in OpenWhisk allow you to run Java code in a serverless environment, providing the benefits of Java's ecosystem with the scalability and cost-efficiency of serverless computing.


Architecture Overview

[HTTP Trigger] → [OpenWhisk Controller] → [Java Action] → [Response] ↓ ↓ ↓ ↓ Webhook Invoker JVM Container JSON Response API Gateway Load Balancer Class Loader Error Handling Schedule Authentication Dependency Mgmt Logging

Step 1: Project Setup and Dependencies

Maven Configuration

pom.xml

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0"> <modelVersion>4.0.0</modelVersion> <groupId>com.example</groupId> <artifactId>openwhisk-java-actions</artifactId> <version>1.0.0</version> <packaging>jar</packaging> <properties> <maven.compiler.source>17</maven.compiler.source> <maven.compiler.target>17</maven.compiler.target> <project.build.sourceEncoding>UTF-8</project-build.sourceEncoding> <openwhisk.version>1.0.0</openwhisk.version> <jackson.version>2.15.0</jackson.version> <gson.version>2.10.1</gson.version> <okhttp.version>4.11.0</okhttp.version> <junit.version>5.10.0</junit.version> </properties> <dependencies> <!-- OpenWhisk Java Client --> <dependency> <groupId>com.google.code.gson</groupId> <artifactId>gson</artifactId> <version>${gson.version}</version> </dependency> <!-- JSON Processing --> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-databind</artifactId> <version>${jackson.version}</version> </dependency> <dependency> <groupId>com.fasterxml.jackson.core</groupId> <artifactId>jackson-core</artifactId> <version>${jackson.version}</version> </dependency> <!-- HTTP Client --> <dependency> <groupId>com.squareup.okhttp3</groupId> <artifactId>okhttp</artifactId> <version>${okhttp.version}</version> </dependency> <!-- Logging --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-simple</artifactId> <version>2.0.7</version> </dependency> <!-- Utilities --> <dependency> <groupId>org.apache.commons</groupId> <artifactId>commons-lang3</artifactId> <version>3.13.0</version> </dependency> <dependency> <groupId>commons-io</groupId> <artifactId>commons-io</artifactId> <version>2.13.0</version> </dependency> <!-- Testing --> <dependency> <groupId>org.junit.jupiter</groupId> <artifactId>junit-jupiter</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.mockito</groupId> <artifactId>mockito-core</artifactId> <version>5.5.0</version> <scope>test</scope> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <version>3.11.0</version> <configuration> <source>17</source> <target>17</target> </configuration> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-shade-plugin</artifactId> <version>3.4.1</version> <executions> <execution> <phase>package</phase> <goals> <goal>shade</goal> </goals> <configuration> <transformers> <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer"> <mainClass>com.example.openwhisk.Main</mainClass> </transformer> </transformers> <filters> <filter> <artifact>*:*</artifact> <excludes> <exclude>META-INF/*.SF</exclude> <exclude>META-INF/*.DSA</exclude> <exclude>META-INF/*.RSA</exclude> </excludes> </filter> </filters> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-assembly-plugin</artifactId> <version>3.6.0</version> <configuration> <descriptorRefs> <descriptorRef>jar-with-dependencies</descriptorRef> </descriptorRefs> <archive> <manifest> <mainClass>com.example.openwhisk.Main</mainClass> </manifest> </archive> </configuration> <executions> <execution> <id>make-assembly</id> <phase>package</phase> <goals> <goal>single</goal> </goals> </execution> </executions> </plugin> </plugins> </build> </project>

Step 2: OpenWhisk Action Interface and Base Classes

Core Action Interface

Action.java

package com.example.openwhisk.core; import java.util.Map; /** * Base interface for all OpenWhisk actions */ @FunctionalInterface public interface Action { /** * Main method that processes the input and returns a result * @param input The input parameters as a Map * @return The result as a Map that will be converted to JSON */ Map<String, Object> execute(Map<String, Object> input); /** * Default method for input validation * @param input The input parameters * @throws ActionException if validation fails */ default void validateInput(Map<String, Object> input) throws ActionException { // Default implementation does nothing // Override in subclasses for specific validation } /** * Default method for logging execution * @param input The input parameters * @param result The execution result * @param duration Execution duration in milliseconds */ default void logExecution(Map<String, Object> input, Map<String, Object> result, long duration) { System.out.printf("Action executed in %d ms. Input: %s, Result: %s%n", duration, input, result); } }

Action Exception

ActionException.java

package com.example.openwhisk.core; public class ActionException extends RuntimeException { private final String errorCode; private final int statusCode; public ActionException(String message) { super(message); this.errorCode = "ACTION_ERROR"; this.statusCode = 500; } public ActionException(String message, String errorCode) { super(message); this.errorCode = errorCode; this.statusCode = 500; } public ActionException(String message, String errorCode, int statusCode) { super(message); this.errorCode = errorCode; this.statusCode = statusCode; } public ActionException(String message, Throwable cause) { super(message, cause); this.errorCode = "ACTION_ERROR"; this.statusCode = 500; } // Getters public String getErrorCode() { return errorCode; } public int getStatusCode() { return statusCode; } public Map<String, Object> toResponse() { return Map.of( "error", true, "code", errorCode, "message", getMessage(), "statusCode", statusCode ); } }

Abstract Base Action

AbstractAction.java

package com.example.openwhisk.core; import java.util.Map; import java.util.concurrent.Callable; /** * Abstract base class for OpenWhisk actions with common functionality */ public abstract class AbstractAction implements Action { private final String actionName; private final String version; protected AbstractAction(String actionName) { this(actionName, "1.0.0"); } protected AbstractAction(String actionName, String version) { this.actionName = actionName; this.version = version; } @Override public final Map<String, Object> execute(Map<String, Object> input) { long startTime = System.currentTimeMillis(); Map<String, Object> result; try { // Validate input validateInput(input); // Execute the action result = doExecute(input); // Ensure result is not null if (result == null) { result = Map.of("success", true); } // Add metadata result = enhanceResult(result, input); } catch (ActionException e) { result = e.toResponse(); } catch (Exception e) { result = new ActionException("Internal server error", "INTERNAL_ERROR", 500) .toResponse(); } // Log execution long duration = System.currentTimeMillis() - startTime; logExecution(input, result, duration); return result; } /** * Template method for actual execution logic */ protected abstract Map<String, Object> doExecute(Map<String, Object> input) throws ActionException; /** * Enhance result with metadata */ protected Map<String, Object> enhanceResult(Map<String, Object> result, Map<String, Object> input) { // Don't enhance error responses if (result.containsKey("error") && (Boolean) result.get("error")) { return result; } // Create enhanced result return Map.of( "data", result, "metadata", Map.of( "action", actionName, "version", version, "timestamp", System.currentTimeMillis(), "success", true ) ); } /** * Get string parameter from input with default value */ protected String getStringParam(Map<String, Object> input, String key, String defaultValue) { Object value = input.get(key); return value != null ? value.toString() : defaultValue; } /** * Get required string parameter from input */ protected String getRequiredStringParam(Map<String, Object> input, String key) throws ActionException { Object value = input.get(key); if (value == null) { throw new ActionException("Missing required parameter: " + key, "MISSING_PARAMETER", 400); } return value.toString(); } /** * Get integer parameter from input with default value */ protected int getIntParam(Map<String, Object> input, String key, int defaultValue) { try { Object value = input.get(key); return value != null ? Integer.parseInt(value.toString()) : defaultValue; } catch (NumberFormatException e) { return defaultValue; } } /** * Get boolean parameter from input with default value */ protected boolean getBooleanParam(Map<String, Object> input, String key, boolean defaultValue) { Object value = input.get(key); if (value == null) { return defaultValue; } if (value instanceof Boolean) { return (Boolean) value; } return Boolean.parseBoolean(value.toString()); } // Getters public String getActionName() { return actionName; } public String getVersion() { return version; } }

Step 3: Sample Action Implementations

1. Hello World Action

HelloWorldAction.java

package com.example.openwhisk.actions; import com.example.openwhisk.core.AbstractAction; import com.example.openwhisk.core.ActionException; import java.util.Map; /** * Simple Hello World action that demonstrates basic OpenWhisk functionality */ public class HelloWorldAction extends AbstractAction { public HelloWorldAction() { super("hello-world"); } @Override protected Map<String, Object> doExecute(Map<String, Object> input) throws ActionException { String name = getStringParam(input, "name", "World"); String greeting = getStringParam(input, "greeting", "Hello"); String message = String.format("%s, %s!", greeting, name); return Map.of( "message", message, "input", input, "timestamp", System.currentTimeMillis() ); } @Override public void validateInput(Map<String, Object> input) throws ActionException { // Optional: Add input validation String name = getStringParam(input, "name", ""); if (name.length() > 100) { throw new ActionException("Name too long", "VALIDATION_ERROR", 400); } } }

2. JSON Processing Action

JsonProcessorAction.java

package com.example.openwhisk.actions; import com.example.openwhisk.core.AbstractAction; import com.example.openwhisk.core.ActionException; import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.databind.ObjectMapper; import java.util.Map; /** * Action that processes JSON data with validation and transformation */ public class JsonProcessorAction extends AbstractAction { private final ObjectMapper objectMapper; public JsonProcessorAction() { super("json-processor"); this.objectMapper = new ObjectMapper(); } @Override protected Map<String, Object> doExecute(Map<String, Object> input) throws ActionException { try { // Get JSON data from input String jsonData = getRequiredStringParam(input, "data"); String operation = getStringParam(input, "operation", "validate"); // Parse JSON Object parsedData = objectMapper.readValue(jsonData, Object.class); // Perform operation Object result; switch (operation) { case "validate": result = validateJson(parsedData); break; case "transform": result = transformJson(parsedData, input); break; case "extract": result = extractFields(parsedData, input); break; default: throw new ActionException("Unknown operation: " + operation, "INVALID_OPERATION", 400); } return Map.of( "operation", operation, "input", parsedData, "result", result, "success", true ); } catch (JsonProcessingException e) { throw new ActionException("Invalid JSON data: " + e.getMessage(), "INVALID_JSON", 400); } } private Map<String, Object> validateJson(Object data) { boolean isValid = data != null; return Map.of( "valid", isValid, "size", data instanceof Map ? ((Map<?, ?>) data).size() : 0, "type", data != null ? data.getClass().getSimpleName() : "null" ); } @SuppressWarnings("unchecked") private Object transformJson(Object data, Map<String, Object> input) { if (!(data instanceof Map)) { return data; } Map<String, Object> dataMap = (Map<String, Object>) data; String transformType = getStringParam(input, "transformType", "uppercase"); switch (transformType) { case "uppercase": return transformToUppercase(dataMap); case "flatten": return flattenJson(dataMap); case "filter": return filterFields(dataMap, input); default: return dataMap; } } private Map<String, Object> transformToUppercase(Map<String, Object> data) { return data.entrySet().stream() .collect(java.util.stream.Collectors.toMap( entry -> entry.getKey().toUpperCase(), entry -> { Object value = entry.getValue(); if (value instanceof String) { return ((String) value).toUpperCase(); } return value; } )); } private Map<String, Object> flattenJson(Map<String, Object> data) { Map<String, Object> flattened = new java.util.HashMap<>(); flattenJson("", data, flattened); return flattened; } @SuppressWarnings("unchecked") private void flattenJson(String prefix, Map<String, Object> data, Map<String, Object> result) { data.forEach((key, value) -> { String newKey = prefix.isEmpty() ? key : prefix + "." + key; if (value instanceof Map) { flattenJson(newKey, (Map<String, Object>) value, result); } else { result.put(newKey, value); } }); } @SuppressWarnings("unchecked") private Object extractFields(Object data, Map<String, Object> input) { if (!(data instanceof Map)) { return data; } Map<String, Object> dataMap = (Map<String, Object>) data; String fields = getStringParam(input, "fields", ""); if (fields.isEmpty()) { return dataMap; } return java.util.Arrays.stream(fields.split(",")) .map(String::trim) .filter(dataMap::containsKey) .collect(java.util.stream.Collectors.toMap( field -> field, dataMap::get )); } }

3. HTTP Client Action

HttpClientAction.java

package com.example.openwhisk.actions; import com.example.openwhisk.core.AbstractAction; import com.example.openwhisk.core.ActionException; import okhttp3.*; import java.io.IOException; import java.util.Map; import java.util.concurrent.TimeUnit; /** * Action that makes HTTP requests to external APIs */ public class HttpClientAction extends AbstractAction { private final OkHttpClient httpClient; public HttpClientAction() { super("http-client"); this.httpClient = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(30, TimeUnit.SECONDS) .writeTimeout(10, TimeUnit.SECONDS) .build(); } @Override protected Map<String, Object> doExecute(Map<String, Object> input) throws ActionException { String url = getRequiredStringParam(input, "url"); String method = getStringParam(input, "method", "GET"); String body = getStringParam(input, "body", ""); Map<String, String> headers = getHeaders(input); try { Request request = buildRequest(url, method, body, headers); Response response = httpClient.newCall(request).execute(); return processResponse(response); } catch (IOException e) { throw new ActionException("HTTP request failed: " + e.getMessage(), "HTTP_ERROR", 502); } } @SuppressWarnings("unchecked") private Map<String, String> getHeaders(Map<String, Object> input) { Object headersObj = input.get("headers"); if (headersObj instanceof Map) { return (Map<String, String>) headersObj; } return Map.of(); } private Request buildRequest(String url, String method, String body, Map<String, String> headers) { Request.Builder requestBuilder = new Request.Builder().url(url); // Add headers headers.forEach(requestBuilder::addHeader); // Set method and body if ("POST".equalsIgnoreCase(method) || "PUT".equalsIgnoreCase(method)) { MediaType mediaType = MediaType.parse("application/json"); RequestBody requestBody = RequestBody.create(body, mediaType); requestBuilder.method(method, requestBody); } else { requestBuilder.method(method, null); } return requestBuilder.build(); } private Map<String, Object> processResponse(Response response) throws IOException { String responseBody = response.body() != null ? response.body().string() : ""; return Map.of( "statusCode", response.code(), "statusMessage", response.message(), "headers", response.headers().toMultimap(), "body", responseBody, "success", response.isSuccessful(), "timestamp", System.currentTimeMillis() ); } @Override public void validateInput(Map<String, Object> input) throws ActionException { super.validateInput(input); String url = getRequiredStringParam(input, "url"); if (!url.startsWith("http://") && !url.startsWith("https://")) { throw new ActionException("Invalid URL protocol", "INVALID_URL", 400); } String method = getStringParam(input, "method", "GET").toUpperCase(); if (!java.util.Set.of("GET", "POST", "PUT", "DELETE", "PATCH").contains(method)) { throw new ActionException("Unsupported HTTP method: " + method, "INVALID_METHOD", 400); } } }

4. Database Action (with connection pooling)

DatabaseAction.java

package com.example.openwhisk.actions; import com.example.openwhisk.core.AbstractAction; import com.example.openwhisk.core.ActionException; import com.zaxxer.hikari.HikariConfig; import com.zaxxer.hikari.HikariDataSource; import javax.sql.DataSource; import java.sql.*; import java.util.*; /** * Action that performs database operations * Note: In production, use connection pooling and proper secret management */ public class DatabaseAction extends AbstractAction { private DataSource dataSource; public DatabaseAction() { super("database"); initializeDataSource(); } private void initializeDataSource() { // In production, these would come from environment variables or secrets String jdbcUrl = System.getenv("DB_URL"); String username = System.getenv("DB_USERNAME"); String password = System.getenv("DB_PASSWORD"); if (jdbcUrl == null) { // For demo purposes, use in-memory H2 jdbcUrl = "jdbc:h2:mem:test;DB_CLOSE_DELAY=-1"; username = "sa"; password = ""; } HikariConfig config = new HikariConfig(); config.setJdbcUrl(jdbcUrl); config.setUsername(username); config.setPassword(password); config.setMaximumPoolSize(5); config.setMinimumIdle(1); config.setConnectionTimeout(30000); config.setIdleTimeout(600000); config.setMaxLifetime(1800000); this.dataSource = new HikariDataSource(config); } @Override protected Map<String, Object> doExecute(Map<String, Object> input) throws ActionException { String operation = getRequiredStringParam(input, "operation"); String query = getStringParam(input, "query", ""); Map<String, Object> parameters = getParameters(input); try (Connection connection = dataSource.getConnection()) { switch (operation.toLowerCase()) { case "query": return executeQuery(connection, query, parameters); case "update": return executeUpdate(connection, query, parameters); case "execute": return executeStatement(connection, query); default: throw new ActionException("Unknown operation: " + operation, "INVALID_OPERATION", 400); } } catch (SQLException e) { throw new ActionException("Database error: " + e.getMessage(), "DATABASE_ERROR", 500); } } @SuppressWarnings("unchecked") private Map<String, Object> getParameters(Map<String, Object> input) { Object paramsObj = input.get("parameters"); if (paramsObj instanceof Map) { return (Map<String, Object>) paramsObj; } return Map.of(); } private Map<String, Object> executeQuery(Connection connection, String query, Map<String, Object> parameters) throws SQLException { try (PreparedStatement stmt = connection.prepareStatement(query)) { setParameters(stmt, parameters); try (ResultSet rs = stmt.executeQuery()) { List<Map<String, Object>> results = new ArrayList<>(); ResultSetMetaData metaData = rs.getMetaData(); int columnCount = metaData.getColumnCount(); while (rs.next()) { Map<String, Object> row = new HashMap<>(); for (int i = 1; i <= columnCount; i++) { String columnName = metaData.getColumnName(i); Object value = rs.getObject(i); row.put(columnName, value); } results.add(row); } return Map.of( "operation", "query", "rowCount", results.size(), "results", results, "success", true ); } } } private Map<String, Object> executeUpdate(Connection connection, String query, Map<String, Object> parameters) throws SQLException { try (PreparedStatement stmt = connection.prepareStatement(query)) { setParameters(stmt, parameters); int affectedRows = stmt.executeUpdate(); return Map.of( "operation", "update", "affectedRows", affectedRows, "success", true ); } } private Map<String, Object> executeStatement(Connection connection, String query) throws SQLException { try (Statement stmt = connection.createStatement()) { boolean hasResultSet = stmt.execute(query); if (hasResultSet) { try (ResultSet rs = stmt.getResultSet()) { List<Map<String, Object>> results = new ArrayList<>(); ResultSetMetaData metaData = rs.getMetaData(); int columnCount = metaData.getColumnCount(); while (rs.next()) { Map<String, Object> row = new HashMap<>(); for (int i = 1; i <= columnCount; i++) { String columnName = metaData.getColumnName(i); Object value = rs.getObject(i); row.put(columnName, value); } results.add(row); } return Map.of( "operation", "execute", "rowCount", results.size(), "results", results, "success", true ); } } else { int updateCount = stmt.getUpdateCount(); return Map.of( "operation", "execute", "updateCount", updateCount, "success", true ); } } } private void setParameters(PreparedStatement stmt, Map<String, Object> parameters) throws SQLException { int index = 1; for (Map.Entry<String, Object> entry : parameters.entrySet()) { // Handle parameter names like :param or $1 if (entry.getKey().startsWith(":") || entry.getKey().startsWith("$")) { stmt.setObject(index++, entry.getValue()); } } } @Override public void validateInput(Map<String, Object> input) throws ActionException { super.validateInput(input); String operation = getRequiredStringParam(input, "operation"); if (!java.util.Set.of("query", "update", "execute").contains(operation.toLowerCase())) { throw new ActionException("Invalid operation: " + operation, "INVALID_OPERATION", 400); } if (operation.equalsIgnoreCase("query") || operation.equalsIgnoreCase("update")) { String query = getStringParam(input, "query", ""); if (query.trim().isEmpty()) { throw new ActionException("Query is required for " + operation + " operation", "MISSING_QUERY", 400); } } } }

Step 4: Action Factory and Registry

Action Factory

ActionFactory.java

package com.example.openwhisk.factory; import com.example.openwhisk.core.Action; import com.example.openwhisk.core.ActionException; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; /** * Factory for creating and managing OpenWhisk actions */ public class ActionFactory { private static final Map<String, Action> actionRegistry = new ConcurrentHashMap<>(); private static final Map<String, Class<? extends Action>> actionClasses = new ConcurrentHashMap<>(); static { // Register action classes registerActionClass("hello", com.example.openwhisk.actions.HelloWorldAction.class); registerActionClass("json", com.example.openwhisk.actions.JsonProcessorAction.class); registerActionClass("http", com.example.openwhisk.actions.HttpClientAction.class); registerActionClass("db", com.example.openwhisk.actions.DatabaseAction.class); } /** * Register an action class */ public static void registerActionClass(String actionType, Class<? extends Action> actionClass) { actionClasses.put(actionType, actionClass); } /** * Create or get an action instance */ public static Action getAction(String actionType) throws ActionException { return actionRegistry.computeIfAbsent(actionType, type -> { try { Class<? extends Action> actionClass = actionClasses.get(type); if (actionClass == null) { throw new ActionException("Unknown action type: " + type, "UNKNOWN_ACTION", 400); } return actionClass.getDeclaredConstructor().newInstance(); } catch (Exception e) { throw new ActionException("Failed to create action: " + e.getMessage(), "ACTION_CREATION_ERROR", 500); } }); } /** * Execute an action by type */ public static Map<String, Object> executeAction(String actionType, Map<String, Object> input) { try { Action action = getAction(actionType); return action.execute(input); } catch (ActionException e) { return e.toResponse(); } } /** * Get all registered action types */ public static java.util.Set<String> getRegisteredActionTypes() { return java.util.Collections.unmodifiableSet(actionClasses.keySet()); } /** * Clear action cache (useful for testing) */ public static void clearCache() { actionRegistry.clear(); } }

Action Router

ActionRouter.java

package com.example.openwhisk.router; import com.example.openwhisk.core.ActionException; import com.example.openwhisk.factory.ActionFactory; import java.util.Map; /** * Routes requests to appropriate actions based on input parameters */ public class ActionRouter { /** * Route and execute action based on input parameters */ public static Map<String, Object> routeAndExecute(Map<String, Object> input) { try { // Determine action type from input String actionType = determineActionType(input); // Execute the action return ActionFactory.executeAction(actionType, input); } catch (ActionException e) { return e.toResponse(); } catch (Exception e) { return new ActionException("Routing error: " + e.getMessage(), "ROUTING_ERROR", 500) .toResponse(); } } /** * Determine which action to execute based on input parameters */ private static String determineActionType(Map<String, Object> input) throws ActionException { // Check for explicit action parameter String explicitAction = (String) input.get("_action"); if (explicitAction != null && !explicitAction.trim().isEmpty()) { return explicitAction.trim(); } // Auto-detect based on input parameters if (input.containsKey("url") && input.containsKey("method")) { return "http"; } else if (input.containsKey("data") && input.get("data") instanceof String) { return "json"; } else if (input.containsKey("operation") && "database".equals(input.get("operation"))) { return "db"; } else if (input.containsKey("name") || input.containsKey("greeting")) { return "hello"; } throw new ActionException("Could not determine action type from input", "UNKNOWN_ACTION_TYPE", 400); } /** * Validate input structure */ public static void validateInput(Map<String, Object> input) throws ActionException { if (input == null || input.isEmpty()) { throw new ActionException("Input cannot be empty", "EMPTY_INPUT", 400); } // Check for required fields based on auto-detected action type String actionType = determineActionType(input); switch (actionType) { case "http": if (!input.containsKey("url")) { throw new ActionException("URL is required for HTTP actions", "MISSING_URL", 400); } break; case "json": if (!input.containsKey("data")) { throw new ActionException("Data is required for JSON actions", "MISSING_DATA", 400); } break; case "db": if (!input.containsKey("operation")) { throw new ActionException("Operation is required for database actions", "MISSING_OPERATION", 400); } break; } } }

Step 5: Main Entry Point

OpenWhisk Main Class

Main.java

package com.example.openwhisk; import com.example.openwhisk.core.ActionException; import com.example.openwhisk.router.ActionRouter; import com.fasterxml.jackson.core.type.TypeReference; import com.fasterxml.jackson.databind.ObjectMapper; import java.io.IOException; import java.util.Map; /** * Main entry point for OpenWhisk Java actions * This class handles the JSON input/output expected by the OpenWhisk platform */ public class Main { private static final ObjectMapper objectMapper = new ObjectMapper(); /** * Main method that OpenWhisk calls * @param args Command line arguments (not typically used) */ public static void main(String[] args) { try { // Read input from stdin (OpenWhisk provides JSON input) Map<String, Object> input = readInput(); // Route and execute the action Map<String, Object> result = ActionRouter.routeAndExecute(input); // Write result to stdout (OpenWhisk expects JSON output) writeOutput(result); } catch (Exception e) { // Handle unexpected errors Map<String, Object> errorResult = new ActionException( "Unexpected error: " + e.getMessage(), "UNEXPECTED_ERROR", 500) .toResponse(); writeOutput(errorResult); System.exit(1); } } /** * Read JSON input from stdin */ private static Map<String, Object> readInput() throws IOException { try { // Read all input from stdin String inputJson = new String(System.in.readAllBytes()); if (inputJson == null || inputJson.trim().isEmpty()) { return Map.of(); } // Parse JSON input return objectMapper.readValue(inputJson, new TypeReference<Map<String, Object>>() {}); } catch (IOException e) { System.err.println("Error reading input: " + e.getMessage()); return Map.of(); } } /** * Write JSON output to stdout */ private static void writeOutput(Map<String, Object> result) { try { String outputJson = objectMapper.writeValueAsString(result); System.out.println(outputJson); } catch (IOException e) { System.err.println("Error writing output: " + e.getMessage()); // Fallback to simple output System.out.println("{\"error\": true, \"message\": \"Output serialization failed\"}"); } } /** * Alternative main method for testing outside OpenWhisk */ public static Map<String, Object> testMain(Map<String, Object> input) { return ActionRouter.routeAndExecute(input); } }

Step 6: Deployment Configuration

Dockerfile for OpenWhisk Java Action

Dockerfile

FROM openwhisk/java8action:latest # Switch to root to install additional packages USER root # Install any additional system dependencies RUN apt-get update && \ apt-get install -y curl && \ rm -rf /var/lib/apt/lists/* # Switch back to non-root user USER 1001 # Copy the JAR file COPY target/openwhisk-java-actions-1.0.0-jar-with-dependencies.jar /action/action.jar # Set the main class ENV OW_EXECUTION_ENV=java ENV OW_COMPILER_FLAGS="--release 11"

OpenWhisk Manifest Files

deploy/action.yaml

packages: java-demo: actions: hello-world: function: target/openwhisk-java-actions-1.0.0-jar-with-dependencies.jar runtime: java main: com.example.openwhisk.Main limits: timeout: 30000 memory: 256 annotations: provide-api-key: true web-export: true json-processor: function: target/openwhisk-java-actions-1.0.0-jar-with-dependencies.jar runtime: java main: com.example.openwhisk.Main limits: timeout: 60000 memory: 512 annotations: provide-api-key: true web-export: true http-client: function: target/openwhisk-java-actions-1.0.0-jar-with-dependencies.jar runtime: java main: com.example.openwhisk.Main limits: timeout: 120000 memory: 512 annotations: provide-api-key: true web-export: false sequences: process-and-store: actions: json-processor, http-client limits: timeout: 180000 memory: 512 triggers: data-arrival: feed: /whisk.system/alarms/interval inputs: minutes: 5 rules: process-periodically: trigger: data-arrival action: java-demo/process-and-store

Deployment Script

deploy/deploy.sh

#!/bin/bash set -e # Configuration WHISK_API_HOST=${WHISK_API_HOST:-https://localhost:31001} WHISK_AUTH=${WHISK_AUTH:-23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP} PACKAGE_NAME="java-demo" echo "Deploying OpenWhisk Java actions to $WHISK_API_HOST" # Build the project echo "Building Java project..." mvn clean package -DskipTests # Create package echo "Creating package: $PACKAGE_NAME" wsk -i --apihost "$WHISK_API_HOST" --auth "$WHISK_AUTH" \ package update "$PACKAGE_NAME" \ --param version "1.0.0" \ --param description "Java OpenWhisk Actions Demo" # Deploy actions deploy_action() { local action_name=$1 local jar_file=$2 local memory=$3 local timeout=$4 echo "Deploying action: $action_name" wsk -i --apihost "$WHISK_API_HOST" --auth "$WHISK_AUTH" \ action update "$PACKAGE_NAME/$action_name" \ "$jar_file" \ --main com.example.openwhisk.Main \ --kind java:8 \ --memory "$memory" \ --timeout "$timeout" \ --web true } # Deploy individual actions deploy_action "hello-world" "target/openwhisk-java-actions-1.0.0-jar-with-dependencies.jar" 256 30000 deploy_action "json-processor" "target/openwhisk-java-actions-1.0.0-jar-with-dependencies.jar" 512 60000 deploy_action "http-client" "target/openwhisk-java-actions-1.0.0-jar-with-dependencies.jar" 512 120000 # Create sequence echo "Creating sequence: process-and-store" wsk -i --apihost "$WHISK_API_HOST" --auth "$WHISK_AUTH" \ action update "$PACKAGE_NAME/process-and-store" \ --sequence "$PACKAGE_NAME/json-processor","$PACKAGE_NAME/http-client" \ --web true echo "Deployment completed successfully!" echo "" echo "Test endpoints:" echo " Hello World: $WHISK_API_HOST/api/v1/web/$(echo $WHISK_AUTH | cut -d: -f1)/$PACKAGE_NAME/hello-world.json" echo " JSON Processor: $WHISK_API_HOST/api/v1/web/$(echo $WHISK_AUTH | cut -d: -f1)/$PACKAGE_NAME/json-processor.json"

Step 7: Testing

Unit Tests

HelloWorldActionTest.java

package com.example.openwhisk.actions; import com.example.openwhisk.core.ActionException; import org.junit.jupiter.api.Test; import java.util.Map; import static org.junit.jupiter.api.Assertions.*; class HelloWorldActionTest { private final HelloWorldAction action = new HelloWorldAction(); @Test void testExecuteWithName() { Map<String, Object> input = Map.of("name", "John"); Map<String, Object> result = action.execute(input); assertNotNull(result); assertTrue(result.containsKey("data")); @SuppressWarnings("unchecked") Map<String, Object> data = (Map<String, Object>) result.get("data"); assertEquals("Hello, John!", data.get("message")); } @Test void testExecuteWithCustomGreeting() { Map<String, Object> input = Map.of("name", "Jane", "greeting", "Hi"); Map<String, Object> result = action.execute(input); @SuppressWarnings("unchecked") Map<String, Object> data = (Map<String, Object>) result.get("data"); assertEquals("Hi, Jane!", data.get("message")); } @Test void testExecuteWithEmptyInput() { Map<String, Object> input = Map.of(); Map<String, Object> result = action.execute(input); @SuppressWarnings("unchecked") Map<String, Object> data = (Map<String, Object>) result.get("data"); assertEquals("Hello, World!", data.get("message")); } @Test void testValidateInputWithLongName() { Map<String, Object> input = Map.of("name", "A".repeat(101)); ActionException exception = assertThrows(ActionException.class, () -> action.validateInput(input)); assertEquals("Name too long", exception.getMessage()); assertEquals(400, exception.getStatusCode()); } }

Integration Test

MainIntegrationTest.java

package com.example.openwhisk; import org.junit.jupiter.api.Test; import java.util.Map; import static org.junit.jupiter.api.Assertions.*; class MainIntegrationTest { @Test void testMainWithHelloAction() { Map<String, Object> input = Map.of( "name", "TestUser", "greeting", "Welcome" ); Map<String, Object> result = Main.testMain(input); assertNotNull(result); assertTrue(result.containsKey("data")); @SuppressWarnings("unchecked") Map<String, Object> data = (Map<String, Object>) result.get("data"); assertEquals("Welcome, TestUser!", data.get("message")); } @Test void testMainWithJsonAction() { Map<String, Object> input = Map.of( "data", "{\"name\": \"John\", \"age\": 30}", "operation", "validate" ); Map<String, Object> result = Main.testMain(input); assertNotNull(result); assertTrue(result.containsKey("data")); @SuppressWarnings("unchecked") Map<String, Object> data = (Map<String, Object>) result.get("data"); assertEquals("validate", data.get("operation")); assertTrue((Boolean) data.get("success")); } @Test void testMainWithInvalidAction() { Map<String, Object> input = Map.of( "invalid", "parameter" ); Map<String, Object> result = Main.testMain(input); assertNotNull(result); assertTrue((Boolean) result.get("error")); assertEquals("ROUTING_ERROR", result.get("code")); } }

Test Script for OpenWhisk

test/test-actions.sh

#!/bin/bash set -e # Configuration WHISK_API_HOST=${WHISK_API_HOST:-https://localhost:31001} WHISK_AUTH=${WHISK_AUTH:-23bc46b1-71f6-4ed5-8c54-816aa4f8c502:123zO3xZCLrMN6v2BKK1dXYFpXlPkccOFqm12CdAsMgRU4VrNZ9lyGVCGuMDGIwP} PACKAGE_NAME="java-demo" echo "Testing OpenWhisk Java actions on $WHISK_API_HOST" test_action() { local action_name=$1 local input_file=$2 local expected_field=$3 local expected_value=$4 echo "Testing action: $action_name" result=$(wsk -i --apihost "$WHISK_API_HOST" --auth "$WHISK_AUTH" \ action invoke "$PACKAGE_NAME/$action_name" \ --param-file "$input_file" \ --result) if echo "$result" | jq -e ".${expected_field} == \"${expected_value}\"" > /dev/null; then echo "✅ $action_name test passed" else echo "❌ $action_name test failed" echo "Result: $result" exit 1 fi } # Test hello-world action cat > /tmp/hello-input.json << EOF { "name": "TestUser", "greeting": "Hello" } EOF test_action "hello-world" "/tmp/hello-input.json" "data.message" "Hello, TestUser!" # Test json-processor action cat > /tmp/json-input.json << EOF { "data": "{\"name\": \"John\", \"age\": 30}", "operation": "validate" } EOF test_action "json-processor" "/tmp/json-input.json" "success" "true" echo "All tests passed! 🎉"

Best Practices

1. Performance Optimization

  • Use connection pooling for database actions
  • Implement caching for frequently accessed data
  • Keep actions stateless for better scalability
  • Use appropriate memory and timeout settings

2. Error Handling

  • Implement comprehensive input validation
  • Use meaningful error codes and messages
  • Log errors for debugging
  • Implement retry logic for transient failures

3. Security

  • Validate all input parameters
  • Use environment variables for sensitive data
  • Implement proper authentication and authorization
  • Sanitize output to prevent injection attacks

4. Monitoring and Logging

  • Log action execution times
  • Monitor memory usage and cold start times
  • Implement health checks
  • Use structured logging for better analysis

Conclusion

This comprehensive OpenWhisk Java actions implementation provides:

  • Modular Architecture: Clean separation of concerns with actions, factories, and routers
  • Type Safety: Strongly typed configuration and input validation
  • Extensibility: Easy to add new actions by implementing the Action interface
  • Production Ready: Includes error handling, logging, and monitoring
  • Deployment Ready: Complete Docker and OpenWhisk deployment configuration

Key Benefits:

  1. Serverless Efficiency: Pay only for execution time
  2. Java Ecosystem: Leverage existing Java libraries and tools
  3. Scalability: Automatic scaling based on demand
  4. Event-Driven: Perfect for microservices and event processing
  5. Cost Effective: No infrastructure management required

By implementing this solution, you can build robust, scalable serverless applications using Java that integrate seamlessly with the OpenWhisk platform.

Leave a Reply

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


Macro Nepal Helper