Custom API Endpoints: Spring Boot + Vertx + Polyglot


#1

Hey

Wanted to share a new prototype we have been working on.

Still very much a work in progress and an experimental exercise, but it is showing interesting progress.

The goal was the leverage the power of Eclipse Vertx.io’s polyglot/multi-language capabilities and provide a very flexible way to add API Endpoints to Camunda’s engine.

So a developer could build new endpoints in Javascript, Ruby, Kotlin, Java, Scala, etc, and they all seamlessly interact directly with the Java API of the engine, allowing more flexibility.

Additionally we could take advantage of Vertx’s HTTP verticle factory capabilities to allow endpoints to be added or removed during runtime / no downtime:

https://vertx.io/docs/vertx-service-factory/java/

https://vertx.io/docs/vertx-http-service-factory/java/

which allows deploying “verticles” in a format such as: vertx.deployVerticle("https://myserver.net/myverticle.zip::my-service", ...)

With the great working going on with the Spring Boot support for Camunda, the goal/use case was fairly trivial to test:

Creating a Spring Boot Application (with WebApps and Rest API), and our Application Class such as:

package io.digitalstate.vertxcamunda;

import org.camunda.bpm.spring.boot.starter.annotation.EnableProcessApplication;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

import io.vertx.core.Vertx;

@SpringBootApplication
@EnableProcessApplication
public class VertxCamundaApplication {
  
  final static Vertx vertx = Vertx.vertx();

  public static void main(String... args) {
    vertx.executeBlocking(future -> {
      SpringApplication.run(VertxCamundaApplication.class, args);
      future.complete();
    }, res -> {
      System.out.println("Camunda has been deployed!!!");
      deployVerticle();
    });
  }

  public static void deployVerticle() {
    vertx.deployVerticle("myVerticle.js");
  }
}

and a myVerticle.js file in the resources folder (this “entrypoint verticle” would be abstracted outside of the Jar in actual use cases. The Entrypoint would be completely configurabled as a Jar argument during java -jar myCamunda.jar -entrypoint /path/to/myVerticle.js :

// vertxStart and vertxStop are functions that are automatically 
// called when the verticle is Deployed and UnDeployed
exports.vertxStart = function() {
  console.log('Primary Javascript Vertx Verticle is Deployed')
}
exports.vertxStop = function() {
  console.log('Primary Javascript Vertx Verticle has UnDeployed')
}

// Get the Default Process Engine
// ProcessEngines: https://docs.camunda.org/javadoc/camunda-bpm-platform/7.8/org/camunda/bpm/engine/ProcessEngines.html
var ProcessEngines = Java.type('org.camunda.bpm.engine.ProcessEngines')
var processEngine = ProcessEngines.getDefaultProcessEngine()

// Get the Repository Service
var repositoryService = processEngine.getRepositoryService()

// See the following for other services you can access:
// Engine Services: https://docs.camunda.org/javadoc/camunda-bpm-platform/7.8/org/camunda/bpm/engine/ProcessEngineServices.html

// Json handling using Camunda Json Util library.  See link for more details:
// https://docs.camunda.org/javadoc/camunda-bpm-platform/7.8/org/camunda/bpm/engine/impl/util/json/package-tree.html
var JSONObject = Java.type('org.camunda.bpm.engine.impl.util.json.JSONObject')
var JSONArray = Java.type('org.camunda.bpm.engine.impl.util.json.JSONArray')

// Setup the Vertx Web Router
var Router = require("vertx-web-js/router")

// Create a Vertx HTTP Server and set the router for the server
var server = vertx.createHttpServer()
var router = Router.router(vertx)

// Setup a /my-deployments route:
router.route('/my-deployments').handler(function (routingContext) {
  var response = routingContext.response();
  
  response.putHeader("content-type", "application/json");
  
  // Runs a Deployment Query in the repositoryService
  var deploymentList = repositoryService.createDeploymentQuery().list()
  // Uses the JSONArray json util to convert the list of deployments 
  // into a pretty string
  var prettyDeploymentList = new JSONArray(deploymentList).toString(2)

  // Returns the pretty string as the response for the route
  response.end(prettyDeploymentList);
});

// Has the defined server use the defined router and listen on port 8081
server.requestHandler(router.accept).listen(8081);

// Returns a response such as:
/*
  [{
    "deployedArtifacts": null,
    "deployedCaseDefinitions": null,
    "deployedDecisionDefinitions": null,
    "deployedDecisionRequirementsDefinitions": null,
    "deployedProcessDefinitions": null,
    "deploymentTime": "Tue Apr 03 16:45:51 EDT 2018",
    "id": "ff48de6a-377f-11e8-95e6-60c547076970",
    "name": "vertxCamundaApplication",
    "new": false,
    "persistentState": "class org.camunda.bpm.engine.impl.persistence.entity.DeploymentEntity",
    "source": "process application",
    "tenantId": null,
    "validatingSchema": true
  }]
*/

You can see we can directly tap into the active Camunda engine.

So a Endpoint for HTTP is generated: /my-deployments on port 8081.
When this route is called we run the following:

  var deploymentList = repositoryService.createDeploymentQuery().list()
  var prettyDeploymentList = new JSONArray(deploymentList).toString(2)

So we can interact with the same Java API but do so with Javascript and Vertx to generate endpoints without the need to recompile the primary Jar.

Our response is:

  [{
    "deployedArtifacts": null,
    "deployedCaseDefinitions": null,
    "deployedDecisionDefinitions": null,
    "deployedDecisionRequirementsDefinitions": null,
    "deployedProcessDefinitions": null,
    "deploymentTime": "Tue Apr 03 16:45:51 EDT 2018",
    "id": "ff48de6a-377f-11e8-95e6-60c547076970",
    "name": "vertxCamundaApplication",
    "new": false,
    "persistentState": "class org.camunda.bpm.engine.impl.persistence.entity.DeploymentEntity",
    "source": "process application",
    "tenantId": null,
    "validatingSchema": true
  }]

This opens up some interesting use cases for Shared Process Engines, where a Deployment to Camunda could also have deployments of “Verticles” which extend the API specifically for the use by specific Process Definitions.

Will be wrapping this up in a docker container example for testing and sharing shortly.

If you have interesting use cases, please share

Code for testing:


Restrict user access to the tasks and variables
#2

Another usecase to mention of this style is to use camunda spring boot as a embeded style application and build vertx application that are “Node Style”. Meaning you can build “Node Apps” that run on nashorn.

The above link provides the latest Vertx JS lang support with ES6 support and additional functionality for require() and commonJS. See the docs.

example empty project: https://github.com/reactiverse/es4x/tree/develop/examples/empty-project

So basically a embedded Camunda engine, but a typical node developer can access the functionality direct through the Java API.


#3

I have enhaced this example further for a javaDelegate that transfers data into the Vertx Event Bus:

https://github.com/StephenOTT/camunda-vertx-springboot/blob/master/docs/Delegate-Config-3.png?raw=true

Where there is a Verticle that is listening for the action (You could have multiple verticles listening for the same message)

exports.vertxStart = function() {
  console.log('Camunda Delegate Verticle has Deployed')
}
exports.vertxStop = function() {
  console.log('Camunda Delegate Verticle has UnDeployed')
}

var eb = vertx.eventBus()
console.log(eb)

var consumer = eb.consumer("someAction")
consumer.handler(function (message) {
  console.log("I have received a message: " + message.body())
  console.log(message.headers().get('DelegateExecution'))
  message.reply("how interesting!")
})

The “how interesting!” reply is sent sync back to camunda which is then stored as a variable

(could also be stored as a localVariable)


There is also a camunda-services.js file that can be loaded in any verticle that will expose all of the engine services for use by your verticle(s).

a header is added to the EventBus messages with the DelegateExecution as a JSON object. So you can get the execution ID and interact with the engine through a verticle.

See: https://github.com/StephenOTT/camunda-vertx-springboot#camunda-servicesjs


With this configuration, you can build script based JS verticles and configured through a config file such as:

This setup essentially lets us work with the java API and the Engine directly as a “embedded engine” through scripting languages and in configurations such as javascript.

Also because of the vertx Clustering, we can build verticles that run locally on the same instance as the SpringBoot Camunda app, but then increase performance across the cluster of vertx verticles.


#4

Just added support for adding JS code through configuration YAML file: https://github.com/StephenOTT/camunda-vertx-springboot#verticle-configuration-through-yaml

example

# Verticle Configurations
verticles:
    my_delegate_verticle:
        path: "app/verticles/vert1.js"
        worker: false
        instances: 1
        isolationGroup:
        isolatedClasses:
        config:
            value1: "dog"
            value2: "cat"
    processing_verticle_ABC:
        path: "some/docker/volume/location/verticleABC.js"
        worker: true
        instances: 10

See link above for more details.

TLDR: This allows you to build JS outside of the Camunda Jar, and have to deploy as a verticle.
See the config loader js file for "extra"s like dynamic reloading of verticles when the config file changes.