Service Registry Engine Developer Guide

In Flowable (Platform, Work, and Engage), the service registry engine is available to deploy, manage, and invoke services.

In this context, a service is described by a service definition or service model that defines the technical details of how to invoke the service. For example, it describes that the service is invoked as an HTTP REST call, what the input or output parameters are, how to map the response, etc.

Once such a service definition is made, it can then be used in BPMN or CMMN models to invoke the service when creating instances of those deployed models. The main benefits of using the service registry engine (versus, for example, using and configuring a service task directly) are:

  • Encapsulating service implementation details: a technical person can create service definitions that contain the low-level configuration on how a service is invoked. The modeling user can use these services in their models without having ever to know these details.

  • Reuse of services in different models without having to repeat or know the technical details.

  • As the models reference the service definitions and do not include the technical service details itself, the implementation of the service can be changed without ever having to change the models.

If you’re interested in the modeling side of things the following guides go into the details on how to model and use the service models:

Anatomy of a Service Definition

A service is described by a service definition, which is a JSON file containing the service implementation details. Such a service definition can also be visually created in Flowable Design (see the Service Registry Modeler Guide for examples). This JSON file can be created in Flowable Design or crafted by hand. The result is the same: a JSON configuration file describing the service.

Service definitions in Flowable Design can be added to an app and deployed directly to a runtime Flowable installation.

Alternatively, service definition JSON files can be placed on the classpath are read and loaded when the server boots up.

  • file suffix: .service

  • classpath location: com/flowable/service/custom/*/.service

A service definition contains four distinct parts:

  • Metadata: The key, description, etc. of the service.

  • Operations: Each service consists of one or more operations. For example, a customer service could expose an operation to look up a customer, create a customer, or even remove a customer object.

  • Input and output parameters: Each operation has (optional) parameters. The input (name, type, default value, etc.) for an operation and the output that the operation produces (if any) determines the kind of data used and produced when invoking the service.

  • Configuration: The implementation details that are specific to the service type (e.g., which HTTP method to use for the rest call).

The service definition only describes the input and output parameters for the service as they are when invoking the service. The service definition does not define how these parameters are mapped to process or case variables. The actual mapping is done in the task attributes that are used in the process or case model in Flowable Design. In Design, it is possible to map variables to input parameters for the service and output parameter values to variables. This way, the service definition is not bound to any model implementation, and it can be reused in different models, with different parameter mapping configurations.

Output parameters assume a JSON structure as the return result of invoking the service. Note that only the output parameters are returned eventually and they effectively work as a filter for the data that gets returned.

Example: Creating an Expression-backed Service Definition

Let us start with a simple example to explain the concepts behind a service definition. In this example, someone has written a customer lookup service and exposed that service as a Spring bean (e.g., a custom configuration with @Configuration that exposes this bean was added to the classpath). The actual way of how this is done is not important for the service registry. It could be that service is doing a database lookup to fulfill its purpose, or maybe it is using a message queue to send a message to a remote microservice. The point is: for the user of the service in a model, this is not important. What matters is how data can be passed into this service and what type of data is getting returned.

In Flowable Design, go to Work section and then select Service on the left-hand side. Press Create at the top and fill in the name and key.

The key is used to reference the service definition in other models. When exporting BPMN or CMMN models, this is the value that is contained in the XML file.

An empty service definition model is now created. Switch the Type field to Expression. The screen now looks something like this:

840A expression example 01

Assume that the Spring bean is exposed with the name customerInfoService. For example, the bean could be exposed as follows:

@Bean
public CustomerInfoService customerInfoService() {
  return new CustomerInfoServiceImpl();
}

And if this class has one method with following signature:

public interface CustomerInfoService {

   ObjectNode getInfo(String customerId);

}
Notice how the return type of the service is com.fasterxml.jackson.databind.node.ObjectNode and not String. When using the service registry engine, returning an ObjectNode is mandatory. The reason is to force the author of the service to think about the serialization of data. Having arbitrary object instances as a return value would lead to storing these as serializable variables in process or case instance, which is a bad practice.

Click the Add operation and add one input parameter, Customer id and one output parameter, Customer name as shown in the screenshot below:

840B expression example 02

The JSON representation of this model looks as follows:

{
    "name": "Customer Info Service",
    "key": "customerInfoService",
    "version": 1,
    "type": "expression",
    "operations": [
      {
        "name": "Get user information",
        "key": "getUserInfo",
        "config": {
          "expression": "${customerInfoService.getInfo(customerId)}"
        },
        "inputParameters": [
          {
            "required": false,
            "defaultValue": null,
            "displayName": "Customer id",
            "name": "customerId",
            "type": "string"
          }
        ],
        "outputParameters": [
          {
            "nullable": false,
            "defaultValue": null,
            "displayName": "Customer name",
            "name": "name",
            "type": "string"
          }
        ]
      }
    ]
  }

Service Operations and Parameters

As seen in the screenshot in the previous section (and the related JSON representation):

  • A service has a name and a key, like all of the models supported by Flowable.

  • Services, like most models in Flowable, can be versioned. Versioning happens through the version property. In Flowable Design, this is done automatically, but it can be set manually in the JSON.

  • The type of the service is set to expression.

  • The service has one operation which has one input parameter and one output parameter.

  • Each input parameter has a type, a human-friendly displayName, and a technical name. The latter is used when passing data to the expression in the previous section (customerId).

  • Each input parameter can be marked as required and can get a default value.

  • Each output parameter also has a type, a human-friendly displayName, and a technical name.

  • Each output parameter can be marked as nullable (the service is allowed to return no value for this parameter) and a default value.

Example: Creating a REST-backed Service Definition

To create a service definition for a service that exposes its functionality using REST, the Type of the definition needs to be changed to REST.

Creating a REST service definition is similar to the expression-backed example in the previous section, but additional configuration about the actual REST details needs to be added.

More specifically:

  • A REST-backed service can have a Base URL configured that gets applied to all operations of the service. It can be an expression or have an expression (e.g., http://some-url/customers/${customerId}, which would mandate that customerId gets passed as an input parameter).

  • A REST operation needs a method (GET/POST/PUT/DELETE) that determines which HTTP method to use.

  • It could be the data needed for this service is nested deeply in the JSON response that is returned. It is possible to set an output path which gets applied to all output parameters and is interpreted as a JSON Pointer expression starting from the root of the JSON response.

  • Similarly, each output parameter has an optional path property (a JSON Pointer expression) to pinpoint the wanted data.

Here is an example of a simple REST service definition, similar to the example in the previous section:

840C rest example 01

POST and PUT methods typically need a body. By default, the input parameters are used to create a 'flat' JSON structure (meaning that each input parameter is one field in the JSON body that gets constructed automatically). The Body location can be used to have a nested JSON structure. When a more complex body is needed for executing the operation, a custom body template resource can be used. This is a Spring resource reference (e.g., classpath:/com/flowable/serviceregistry/engine/template/custom-template.ftl) to a Freemarker template that is processed when the body gets constructed. All the input parameters are available in the template.

Such a template could looks as follows:

<#ftl output_format="JSON">
{
    "nestedField": {
        "name": "${name}",
        "accountNr": ${accountNumber}
    }
    <#if tenantId?has_content >,
        ,"tenantId": ${tenantId}
    </#if>
}

Deploying a Service Definition

Service definitions that are referenced in BPMN or CMMN models need to be included in the same app. When deploying the app, the service definition is also deployed to the runtime system.

Extension: Intercepting Service Invocations

It is possible to intercept any service invocation done by the service registry engine just before the service gets invoked and just after it returns.

Potential use cases are, for example:

  • Adding new data to the context that is passed to the service. This data could be a calculated field value, a value coming from another service, etc.

  • The response data needs to be filtered or checked.

  • The response needs to have a calculated field or a completely new field that could not be expressed in the service operation mapping.

To add such logic, create and expose a Spring bean that implements the com.flowable.serviceregistry.api.interceptor.ServiceInvokerInterceptor interface. It looks as follows:

public interface ServiceInvokerInterceptor {

    void beforeServiceInvocation(ServiceDefinitionModel serviceDefinition, ServiceOperation serviceOperation, ServiceInvocationContext context);

    void afterServiceInvocation(ServiceDefinitionModel serviceDefinition, ServiceOperation serviceOperation, ServiceInvocationContext context, ServiceInvocationResult result);

}

The serviceDefinition and the serviceOperation that are passed to the method are a Java representation of the service operation that gets invoked. It can be used to only execute the custom interceptor logic when it matches a specific operation, for example.

The ServiceInvocationResult contains the actual values passed to the service (called service data), and way of passing through free-form additional data. The ServiceInvocationResult for the 'after' callback would contain the response result or an exception if an exception happened.

Instances of this interface exposed as Spring beans are automatically picked up and injected into the Service Registry Engine.

Extension: Enhancing Rest Service Invocations

REST services sometimes need specific enhancements to work in certain environments. For example, a company-specific authentication mechanism could exist that sets a token as a header, or a custom SSL configuration needs to be applied, etc.

For this purpose, the low-level com.flowable.serviceregistry.engine.impl.invoker.rest.RestServiceInvokerEnhancer interface exists:

public interface RestServiceInvokerEnhancer {

    void enhanceHttpClientBuilder(ServiceDefinitionModel serviceDefinitionModel, ServiceOperation serviceOperation,
        ServiceInvocationContext serviceInvocationContext, HttpClientBuilder httpClientBuilder);

    void enhanceHttpRequest(ServiceDefinitionModel serviceDefinitionModel, ServiceOperation serviceOperation,
        ServiceInvocationContext serviceInvocationContext, HttpRequestBase httpRequestBase);

    void enhanceHttpResponse(ServiceDefinitionModel serviceDefinitionModel, ServiceOperation serviceOperation,
        ServiceInvocationContext serviceInvocationContext, HttpResponse httpResponse);

    void enhanceJsonResponse(ServiceDefinitionModel serviceDefinitionModel, ServiceOperation serviceOperation,
        ServiceInvocationContext serviceInvocationContext, JsonNode jsonResponse);

}

The serviceDefinition and the serviceOperation that are passed to the method are a Java representation of the service operation that gets invoked. It can be used to only execute the custom interceptor logic when it matches a specific operation, for example.

The enhanceHttpClientBuilder can be used to customize the underlying Apache HttpClient that is used to execute the actual HTTP REST call.

The other three methods respectively allow to changing anything to either the request (before it gets sent), the response (right after it returns) and the JSON response before it is passed further to the mapping of the BPMN or CMMN model.

Instances of this interface exposed as Spring beans are automatically picked up and injected into the Service Registry Engine.

Global Settings for REST Services

It is possible to set configuration properties for REST service definitions that get applied to all such services.

The naming pattern is flowable.service-registry.rest-settings.<serviceDefinitionKey | default>.property. When using default, this is the default fallback when no more specific value is set.

This is for example, useful when different environments are used for testing, staging, production, etc. Using these settings, for example, the base URL for all REST services is used.

The following settings allow to overriding configurations of service definitions. The order of resolving is service-override, then default override, then settings in the operation, and finally the setting in the service configuration.

Note that these last two are only relevant when creating the service definition manually (not through Flowable Design).

For example:

  • flowable.service-registry.rest-settings.myService.url overrides the URL of the service definition with key myService.

  • flowable.service-registry.rest-settings.default.url is used if the previous value is missing.

  • The URL from the actual operation/service is used when the above two values are missing.

Additional available settings:

  • flowable.service-registry.rest-settings.<serviceDefinitionKey | default>.baseUrl: The base URL applied to all operations.

  • flowable.service-registry.rest-settings.<serviceDefinitionKey | default>.url: The URL for invoking the service.

  • flowable.service-registry.rest-settings.<serviceDefinitionKey | default>.method: The HTTP method used.

  • flowable.service-registry.rest-settings.<serviceDefinitionKey | default>.outputPath: Configures the output path of the response. See above for more information.

Additional technical settings:

  • flowable.service-registry.rest-settings.<serviceDefinitionKey | default>.headers: A comma-separated list of headers in the form of name:value that needs to be applied.

  • flowable.service-registry.rest-settings.<serviceDefinitionKey | default>.bodyEncoding: Configures the encoding of the HTTP body.

  • flowable.service-registry.rest-settings.<serviceDefinitionKey | default>.noRedirects: Boolean flag to indicate whether or not to follow HTTP redirects. Default false.

  • flowable.service-registry.rest-settings.<serviceDefinitionKey | default>.ignoreErrors: Boolean flag to indicate any HTTP errors (500, 404, etc) that gets returned instead of 200.

  • flowable.service-registry.rest-settings.<serviceDefinitionKey | default>.isDisableCertVerification: Boolean flag that disables SSL certificate validation. Do not use in production. Default false.

  • flowable.service-registry.rest-settings.<serviceDefinitionKey | default>.timeout: The total time (in ms) that a request can take. Not set by default.

  • flowable.service-registry.rest-settings.<serviceDefinitionKey | default>.retries: The number of retries when either of the following exception happens: InterruptedIOException, UnknownHostException, ConnectException, SSLException. By default 0.

Low-level HTTP client settings. Please check Apache HttpClient for more details.

  • flowable.service-registry.rest-settings.<serviceDefinitionKey | default>.socketTimeout

  • flowable.service-registry.rest-settings.<serviceDefinitionKey | default>.connectTimeout

  • flowable.service-registry.rest-settings.<serviceDefinitionKey | default>.connectionRequestTimeout

The default for each setting above is 10 seconds (10000).