S-J2D2: Great webservices with systemd, Java, JSON and Dynamic Data

Martin F. Johansen, 2024-06-25

S-J2D2 stands for Systemd-Java-JSON-Dynamic-Data. It is a way to build web services with several benefits that can be summarized as:

  • Standard
  • Reproducible
  • Control, Flexibility and Simplicity
  • High quality
  • Composability
  • Secure

Read more about these below. But, before that, let's get into the technical details.

Introduction

HTTP as a Message Wrapper

HTTP is a complex protocol. An implementation of all its aspects can be very complicated.

In this approach, it is only treated as a wrapper for a JSON message. Thus, only the POST request type is used, and the only other headers that really matter are the Content-Length header, which gives the length of the JSON message, and the Host header, which gives which virtual host that is requested. The request is always expected to be JSON and the datatype application/json.

Also, the respons is basically only code 200 and a JSON message.

An implementation of more than this must deliver a huge set of functionality for the declarative language of HTTP headers. For example, different HTTP versions, the different methods, paths, query parameters, status codes, accepts, cache control, encodings, keep-alives, etags and much, much more. For example, with the cache headers, the web server is expected to provide caching as expressed in the header...

In the approach described here, the web server gets a JSON message and decides to do more if needed.

Verification

When we get JSON from the wild on a web service, we need to verify that it is correct. We can read in the JSON into a static data structure using a library that uses reflection to directly insert data into the data structure. The drawback of this is the limitations of Java classes to specify constraints on the data.

Here are some examples:

  • If an entry in a class is not present in the JSON, should the reader fill with null or throw an exception?
  • If an entry is present in the JSON but not in the class, should the reader ignore it or throw an exception?
  • How should we deal with constrains between values? For example, that two values needs to be either IPv4 or IPv6.
  • If the reader throws an exception, will be return message to the client be understandable?

Non-declarative

In general, the approach described here is highly imperative. The program is set up to create what is needed imperiatively instead of declaratively: HTTP is used as a message wrapper, builds are compiled Java files with imperative code, deployments are simple installations of imperative programs.

The S-J2D2 Approach

Requirements

The only thing we need to run a web service using this technique, is a plain install of Ubuntu + the headless OpenJDK JRE:

# apt install openjdk-17-jre-headless

We can then use ssh, scp and systemctl to deploy and start our service.

Basic Webservice

A basic webservice takes a string as input and gives a string as output. We thus needs to implement the following function. Notice that this does not involve HTTP headers or other request types than POST. It is, however, a general form of webservice taking data as input and returning data as output.

static public void CommandHandler(StringBuilder response, String request)

We can create the required HTTP wrapper for this function using the built-in Java web server, found in com.sun.net.httpserver. The complete source code is below.

Change the port 8000 to listen to another port. Set the IP to 0.0.0.0 to listen on all IPs, or a particular one to only listen to it. The last parameter, 32, to create, is the backlog of requests the server should support.

import java.io.*;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executors;

import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;

public class WebService {
  public static void main(String[] args) throws Exception {
    HttpServer server;

    server = HttpServer.create(new InetSocketAddress("127.0.0.1", 8000), 32);
    server.createContext("/", new MyHandler());
    server.setExecutor(Executors.newFixedThreadPool(32));
    server.start();
  }

  static class MyHandler implements HttpHandler {
    @Override
    public void handle(HttpExchange t) throws IOException {
      InputStream is = t.getRequestBody();

      String request = GetStringFromInputStream(is);

      StringBuilder responseSB = new StringBuilder();
      CommandHandler(responseSB, request);
      String response = responseSB.toString();

      t.sendResponseHeaders(200, response.length());
      OutputStream os = t.getResponseBody();
      os.write(response.getBytes());
      os.close();
    }
  }

  public static String GetStringFromInputStream(InputStream is) throws IOException {
      BufferedInputStream bis = new BufferedInputStream(is);
      ByteArrayOutputStream buf = new ByteArrayOutputStream();
      for (int result = bis.read(); result != -1; result = bis.read()) {
        buf.write((byte) result);
      }
      return buf.toString(StandardCharsets.UTF_8);
  }

  static public void CommandHandler(StringBuilder response, String request){
      System.out.println("Got: " + request + "\n");
      response.append("Got: " + request + "\n");
  }
}

We can build this easily:

javac src/WebService.java -d out/
jar cfe webservice.jar WebService -C out/ .

Then run it:

java -jar webservice.jar

We can try this out using curl.

$ curl http://localhost:8000/ --data '{}'
Got: {}

We now have a web service running.

Multi-threading

This server is single threaded by default. This is a useful default for many cases.

server.setExecutor(null);

If multi-threading is needed, we can add an import:

import java.util.concurrent.Executors;

And change one line. The number given in the parameter is how many concurrent threads will be handling requests.

server.setExecutor(Executors.newFixedThreadPool(2));

systemd

You can test running it with systemd as follows.

$ systemd-run --user --unit webservice java -jar <directory>/webserivce.jar

Check the logs using

$ journalctl --user -u webservice

To stop it, run:

$ systemctl --user stop webservice
$ systemctl --user reset-failed webservice

To install the service permanently, create the following file

/etc/systemd/system/webservice.service

and, fill it with:

[Unit]
Description=Web Service
Documentation=https://progsbase.com
After=network.target

# Disable restart limit
StartLimitIntervalSec=0

[Service]
WorkingDirectory=/opt/webservice/
Type=simple
User=webservice
ExecStart=/usr/bin/java -jar webservice.jar
Restart=always

[Install]
WantedBy=multi-user.target

You need to place webservice.jar in /opt/webservice/ and create the user webservice.

Start the service using:

# systemctl start webservice

Enable it at startup using:

# systemctl enable webservice

Check the logs using:

# journalctl -u webservice

Dynamic Data

JSON provides a good way to serialize and deserialize data. We have developed a library where we can easily construct dynamic data structures in Java.

We can change the signature of our central function to take a dynamic data structure as input and return a dynamic data structure as output.

void CommandHandler(DataReference response, Data request);

We need a file src/info.json.

{
  "name": "WebService",
  "version": "0.1.0-SNAPSHOT",
  "organization namespace": "com.progsbase.sj2d2",
  "scientific namespace": "other.other.other",
  "imports": [
    ["", "no.inductive.libraries", "DataStructures", "0.1.5"],
    ["", "no.inductive.idea10.programs", "JSON", "0.5.10"]
  ],
  "development imports": [],
  "ownerCustomerId": "Inductive AS"
}

And then import the libraries:

$ progsbase importdeps Java IDE

We also need to modify our wrapper:

import DataStructures.Array.Structures.Data;
import DataStructures.Array.Structures.DataReference;
import com.sun.net.httpserver.HttpExchange;
import com.sun.net.httpserver.HttpHandler;
import com.sun.net.httpserver.HttpServer;
import references.references.StringReference;

import java.io.*;
import java.net.InetSocketAddress;
import java.nio.charset.StandardCharsets;
import java.util.concurrent.Executors;

import static DataStructures.Array.Structures.Structures.AddStringToStruct;
import static DataStructures.Array.Structures.Structures.CreateNewStructData;
import static JSON.Parser.Parser.ReadJSON;
import static JSON.Writer.Writer.WriteJSON;

public class WebServiceDD {
  public static void main(String[] args) throws Exception {
    HttpServer server;

    server = HttpServer.create(new InetSocketAddress("127.0.0.1", 8000), 32);
    server.createContext("/", new MyHandler());
    server.setExecutor(Executors.newFixedThreadPool(32));
    server.start();
  }

  static class MyHandler implements HttpHandler {
    @Override
    public void handle(HttpExchange t) throws IOException {
      boolean success;
      int code;
      StringReference message = new StringReference();
      InputStream is = t.getRequestBody();

      String request = GetStringFromInputStream(is);

      DataReference requestData = new DataReference();
      success = ReadJSON(request.toCharArray(), requestData, message);

      String response;
      if(success) {
        DataReference responseData = new DataReference();
        CommandHandler(responseData, requestData.data);

        t.getResponseHeaders().add("Content-Type", "application/json");
        response = new String(WriteJSON(responseData.data));
        code = 200;
      }else{
        response = new String(message.string);
        code = 400;
      }

      t.sendResponseHeaders(code, response.length());
      OutputStream os = t.getResponseBody();
      os.write(response.getBytes());
      os.close();
    }
  }

  public static String GetStringFromInputStream(InputStream is) throws IOException {
    BufferedInputStream bis = new BufferedInputStream(is);
    ByteArrayOutputStream buf = new ByteArrayOutputStream();
    for (int result = bis.read(); result != -1; result = bis.read()) {
      buf.write((byte) result);
    }
    return buf.toString(StandardCharsets.UTF_8);
  }

  static public void CommandHandler(DataReference response, Data request){
    response.data = CreateNewStructData();
    AddStringToStruct(response.data.structure, "key".toCharArray(), "value".toCharArray());

    System.out.println("Got: " + new String(WriteJSON(request)) + "\n");
  }

}

And, the build requires the imports on the class path:

$ javac -cp imports src/WebServiceDD.java -d out/
$ jar cfe webservice.jar WebService -C out/ .

We can now send and receive JSON from our web server.

$ curl -s http://localhost:8000/ --data '{}' | jq
{
  "key": "value"
}

Serialize and Deserialize JSON

We can easily serialize and deserialize JSON. If we have JSON in a string, simply run:

char [] json = ...;
StringReference message = new StringReference();
DataReference dataRef = new DataReference();
boolean success = ReadJSON(json, dataRef, message);
if(success){
  Data data = dataRef.data;
  ...
}else{
  // message.string containts the error message.
}

If we want JSON from a dynamic data structure, run:

Data data = ...;
char [] json = WriteJSON(data);

Data is either a number, a boolean, a string, a structure or an array. If we have a structure or an array, we can write it directly.

Structure struct = ...;
char [] json = WriteJSONFromStruct(struct);

and

Array array = ...;
char [] json = WriteJSONFromArray(array) {

Create a Structure and Add Entries

Structure struct = CreateStructure();
AddStringToStruct(struct, "a".toCharArray(), "val".toCharArray());
AddNumberToStruct(struct, "b".toCharArray(), 1);
AddBooleanToStruct(struct, "c".toCharArray(), true);

Gives

{
  "a": "val",
  "b": 1,
  "c": true
}

Create an Array and Add Entries

Array array = CreateArray();
AddStringToArray(array, "val".toCharArray());
AddNumberToArray(array, 1);
AddBooleanToArray(array, true);

Gives

["val", 1, true]

Get Values from Structures and Arrays

Structure struct = ...
double n = GetNumberFromString(struct, "b".toCharArray());

Array array = ...
char [] str = IndexArrayString(array, 1);

Deployment

To deploy a new version, build the JAR, copy it to the server using scp and restart the service using systemd.

HTTPS

HTTPS should be put on a gateway server and, for example, be handled by Apache2 + mod_ssl. The requests can be forwarded to the Java webservice running HTTP with mod_proxy. This gateway server can also have mod_security for added security features.

Here is an example of using mod_proxy with mod_ssl.

<VirtualHost *:443>
  ServerName example.com
  ...
  SSLEngine on
  ...
  ProxyPass "/" "http://localhost:8000/"
  ProxyPassReverse "/" "http://localhost:8000/"
  ...
</VirtualHost>

Calling Other Services

If we are calling other services developed in the same way, we can set up SSH tunnels and tunnel HTTP requests between the servers. Thus, we can set up our own Public Key Infrastructure (PKI), and we don't need third party services for encryption.

Logging

Logging can be done to stdout and stderr using the built-in print functions in Java. This can be done wherever the developer likes.

It is accessible using systemd's journalctl. It is also possible to log to files in the /var/log/ folder and configure logrotate to rotate the logs.

Some Useful Conventions

A useful convention is to return a structure with two required elements: "success" and "message".

  • "success", a boolean signaling if the request succeeded or failed.
  • "message", a string with a human-readable error message.
  • "data", the return data from the service.

For example, a success:

{
  "success": true,
  "message": "",
  "data": [
    ...
  ]
}

or a failure:

{
  "success": false,
  "message": "The key \"x\" did not contain one of the valid values."
}

HTTP will return code:

  • 200 when processing reached its end.
  • 500 if the server reached a critical error where the program cannot produce a JSON response.

For requests, a useful convention is to take a structure with one required key "command". This tells the service which operation to invoke. For example,

{
  "command": "CreateOrder",
  ...
}

Unified JSON Calls

When calling another JSON webservice, there are many things that the call can result in:

  1. The service can be unreachable.
  2. The service can time out.
  3. The service can return 400 or 500 codes.
  4. The service can return an invalid JSON.
  5. The service can return another failure.
  6. The service can return success.

We can unify all this error handling as follows. We set up a JSON call that takes a dynamic data structure and returns a dynamic data structure:

static public void JSONCall(String url, DataReference response, Data request)

This call will set the response entries "success" and "message" if the call fails with cases 1, 2, 3 and 4. In case of 5 and 6, the call can use "success" and "message" from the result.

Thus, the caller only needs to check a single variable, "success", for whather the call succeeded or not. The error message is always available in "message".

This is much simpler than the common checks:

  1. Check for an exception.
  2. Check for a timeout.
  3. Check the return code.
  4. Check the return data.

Motivation

Let's go through each of the benefits mentioned at the start, and see how S-J2D2 is a good way to set up a web service.

Standard

We should be able to set up a web service using a plain Linux distro and Java. These are stable and standard systems available everywhere for free. Spinning up a new cloud initialized version of, say, Ubuntu or Debian, is quick, and we can apt install headless OpenJDK JRE in a minute.

This makes it flexible with regards to which cloud provider we can use. As long as the cloud provider provides a plain Linux VPS, we can easily deploy and run our web services.

Reproducible

Having the webserver embedded in the application, means we can run our webservice locally or remotely and expect it to behave the same. Having the whole service wrapped up in a JAR generally give these benefits.

Control, Flexibility and Simplicity

If we use container systems such as Docker or Kubernetes, or frameworks such as Spring, we need our developers to understand how these systems work. This, in order to secure it's operational stability and to debug failures when they occur.

Having an executable with a main function means we are in full control over what the application is doing. Simply decide which IP and port to listen to. Set up the HTTPS separately on the gateway. If we need logging, then simply write the log statements where needed. Need more threads, simply run some.

High Quality

We can create high quality components by carefully validating the data we receive from the clients and give them clear responses in return.

Composability

Web services that communicate using JSON on ports compose well. Need more workers? Add more web servers and do round robbing between them. Need to queue requests? Create a queue and process them in order.

Secure

Security problems are bugs where a client gains access to something he should not have access to. Having an understandable system means developers can understand what is going on. There is a simple entry to the web service, taking JSON as input and returning JSON. No other complicating issue. Once the input is in, it should be carefully validated. First, it should be validated as valid JSON, then the data should be validated. This gives a good starting point for a secure application.

Less Complex Builds

Builds can be complex. However, if a build is simply a bunch of Java files compiled and placed into a JAR, then it is simple. The JAR is executable, meaning one of the classes has a main method. This is where execution starts and everything is set out there instead of in declarative specifications.

Less Complex Deployments

Deploying a server can be complicated. It might require setting up and configuring a bounch of software on the server. The deployment described here is simply uploading a JAR file and restarting the systemd service.

Contact Information

We would be more than happy to help you. Our opening hours are 9–15 (CET).

[email protected]

📞 (+47) 93 68 22 77

Nils Bays vei 50, 0876 Oslo, Norway

Copyright © 2018-24 progsbase.com by Inductive AS.