Jakarta EE meets Dapr

· by Maarten Mulders · Read in about 8 min · (1637 Words)
Posted in java / open source / dapr / architecture

Earlier this month, I introduced you to Dapr, the Distributed Application Runtime. That was a mostly conceptual introduction, showing you how Dapr works and what it can do for you. But how do you integrate it into an existing application? That’s the topic for today.

This blog will be a bit shorter than the introduction to Dapr. We will dive into interacting with Dapr from a Jakarta EE application.

Example: online shopping cart

The example is a small application, written in Java using Jakarta EE 8. I’ve tested it on Open Liberty, but it should work on other Jakarta EE-compatible containers as well. It is part of a (fictitious) distributed architecture for a webshop, and it manages the users shopping carts. Users can add items to their shopping cart before eventually proceeding to check-out.

The application identifies a shopping cart by an identifier, and it uses the standard JAX-RS session identifier for that. The cart, modelled in the Cart class, is stored in memory; the InMemoryShoppingCartRepository class is responsible for that. You can find some examples of interacting with the shopping cart in the .http files on GitHub.

Of course, a real webshop would have a more sophisticated mechanism, but for the example, this is enough. But it would not work if there are multiple instances of the microservice. Even if there was only one instance, as soon as that was restarted, all carts would be lost. So we must somehow persist the state.

Let’s see how Dapr comes to help. As mentioned in the previous post, Dapr provides us with the abstraction of a state store. For the time being, we will use Redis as the backing implementation. The Dapr documentation contains the HTTP contract for interacting with a state store. It describes four operations:

  1. Saving an item to the store.
  2. Retrieving an item from the store.
  3. Retrieving multiple items from the store.
  4. Removing an item from the store.

For this example, we only need the first and the second operation.

Implementing the State Store contract

Since the State Store operations are exposed using an HTTP API that consumes and produces JSON, we can use JAX-RS to implement a client. We introduce a new class for that, the DaprStateStore; it takes one type parameter, for the type of item that the store will contain. That type is also an instance variable (of type Class<V> valueType) because we can’t serialise or deserialise only on generics.

Since we don’t know about the underlying store, we must adhere to the State Store contract. That contract says an item in the store can be “any byte array”. To keep things simple, we will serialise instances of the Cart class to a byte array using JSON. Similarly, we will deserialise a Cart from a byte array by interpreting the byte array as JSON. You can of course use any mechanism for this, such as Protocol Buffers or Apache Avro as long as it allows to (de)serialise to and from a byte array.

Storing an item to the State Store

To interact with the State Store, we define a POJO that corresponds with the request body for storing an item:

public class StateEntry {
    public String key;
    public Object value;
    public String etag;

    public StateEntry() {
    }

    public StateEntry(final String key, final Object value) {
        this(key, value, "");
    }

    public StateEntry(final String key, final Object value, final String etag) {
        this.key = key;
        this.value = value;
        this.etag = etag;
    }
}

Storing an item is a matter of doing an HTTP POST to the sidecar:

public void storeValue(final String key, final V value) {
    var body = new StateEntry[] {
        new StateEntry(key, value)
    };

    var response = webClient.target("http://localhost:3500/v1.0/state/" + storeName)
        .request(MediaType.APPLICATION_JSON)
        .post(Entity.json(body));

    if (response.getStatus() != 204) {
        log.error("Could not store value: store={}, key={}, status={}",
            storeName, key, response.getStatusInfo().getReasonPhrase());
    }
}

Note that the API contract is designed in such a way that it can take more than one item at a time. That’s why the request body is an array of StateEntry objects. That array is serialised using JSON-B, part of Jakarta EE.

Finally, we check if storing the item was successful by verifying the response has HTTP status 204 (for “No Content”).

So far so good, let’s see how we could fetch an item from the State Store.

Retrieving an item from the store

This part requires a bit less code than the previous one. The sidecar exposes an endpoint to retrieve a single item from the State Store by doing an HTTP GET on /v1.0/state/name of the store/key of the item. First, we need to inspect the response status, as it provides us with valuable meta-information:

Status Meaning
200 Requested item was found, response body contains the value
204 Requested item was not found
400 The State Store is not configured correctly
500 Something went wrong fetching the item from the underlying store

If the requested item was found, we again use JSON-B to deserialise the request body into a Java object:

var response = webClient.target("http://localhost:3500/v1.0/state/"+ storeName+ "/" + key)
    .request(MediaType.APPLICATION_JSON)
    .get(Response.class);

switch (response.getStatus()) {
    case 200: {
        return Optional.of(response.readEntity(valueType));
    }
    // Omitted for brevity
    default: {
        log.error("Unknown error when fetching state: store={}, key={}, status={}", storeName, key, response.getStatus());
        return Optional.empty();
    }
}

Other considerations

Note how the above code samples hard-code the address of the Dapr sidecar as http://localhost:3500. This is the default address, but when you start the Dapr sidecar you can specify a different port number using -H or --dapr-http-port. In a real-world project, you would make the address configurable so you can be more flexible in your deployment. When you run Dapr in a Kubernetes cluster, it is even less likely to change, as a Kubernetes sidecar will always run on the same “localhost”.

Running the sample application

You can run the sample application using mvn liberty:run. But you can’t start putting stuff in your cart yet; if you do that, you will see a stack trace that tells you the app can’t connect to localhost:3500. That’s precisely the address we hardcoded before, and I mentioned already you need the sidecar to use Dapr. To start the sidecar, run dapr run --app-id shopping-cart --dapr-http-port 3500 --components-path=dapr/ from the application directory. Let’s pick that command apart:

  • --app-id provides a logical name for the application. Dapr also uses it to compose the name of the keys in the underlying store (more on that later)
  • --dapr-http-port exposes the Dapr building block API on port 3500, rather than a random port.
  • --components-path specifies a folder where the Dapr component declarations can be found. If not specified, it defaults to .dapr/components under the users home directory.

Now try again to put some product in your cart, and see how you get a 204 No Content response back this time! Going back to the Redis CLI again (with docker exec -it dapr_redis redis-cli), we can inspect the state store:

hgetall "shopping-cart||mBJ5sIE8LXTuFh0DaLzADcT"

1) "version"
2) "2"
3) "data"
4) "{\"items\":[{\"itemCount\":5,\"productId\":\"21e0a7a4-61ac-410e-ac14-eb1372f696dc\"}]}"

Swapping the State Store

Granted, this is not something you would do on a daily basis in a real project… But let’s see how we could replace the Redis backed state store with a different implementation: PostgreSQL.

To do that, we first start a PostgreSQL database in Docker:

docker run \
  --rm \                         # remove container when done
  -p 5432:5432 \                 # expose port 5432
  -e POSTGRES_PASSWORD=3x4mpl3 \ # set the 'postgres' user password
  -e POSTGRES_DB=dapr_test \     # set the database name
  --name dapr_psql_test \        # set the container name
  postgres:13.4-alpine           # image + tag to run

To configure a Dapr state store on top of that PostgreSQL database, add a new file in the dapr/ folder. Name it state-store-postgresql.yml and add the following contents:

apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
  name: carts
  namespace: default
spec:
  type: state.postgresql
  version: v1
  metadata:
    - name: connectionString
      value: "host=localhost user=postgres password=3x4mpl3 port=5432 connect_timeout=10 database=dapr_test"

It declares a Dapr “Component” that lives in the default namespace, named carts. Check the DaprShoppingCartRepository and see how we also use that value to initialise the DaprStateStore class. The Dapr component is of type “state.postgresql”, and it is configured using the metadata. The PostgreSQL state store documentation mentions connectionString as the only required parameter, including a sample value that we adapted to our needs.

Again, the Dapr sidecar takes all YAML files in the dapr/ folder and configures the necessary bindings from these. Open state-store-redis.yml and prefix every line with #, so it becomes commented-out. The Redis state store refers to a Redis instance that is required by Dapr anyway, so we didn’t pay too much attention to configure it. Restart the sidecar so this new PostgreSQL-backed state store will become available to our application, too.

That’s it - you don’t even need to restart the applcation to switch the State Store implementation!

Now, try to put some items in the cart again; remember to use the add-item-to-cart.http if you’re unsure how to do that. Then, start a database session using docker exec -it dapr_psql_test psql dapr_test postgres -x and inspect the contents of the database:

dapr_test=# select * from state;

-[ RECORD 1 ]------------------------------------------------------------------------------
key        | shopping-cart||ADfuI9issrYI3oP8JvV8QrY
value      | {"items":[{"itemCount":5,"productId":"21e0a7a4-61ac-410e-ac14-eb1372f696dc"}]}
insertdate | 2021-09-27 18:32:31.594273+00
updatedate | 2021-09-27 18:32:31.608559+00

So… from our application perspective, we didn’t need a single change. The API, exposed as an HTTP interface, is perfectly stable and doesn’t exhibit any of the underlying details of our application.

Wrapping up

In this second instalment of the series, we saw how an existing Java application can start leveraging Dapr without introducing additional dependencies. We also saw how the API that Dapr offers is completely innocent of the underlying implementation, be it Redis or PostgreSQL.

Stay tuned for the next episodes, which will cover the Dapr SDK for Java and actor-based systems using Dapr.

As always, if you have any questions, ideas or suggestions for an additional post: I’d love to hear from you! Feel free to reach out using the comments box below or any of the platforms in the sidebar.

Comments