Connecting to Redis from a Jakarta EE application

In this tutorial, we’ll learn how to integrate Redis into a Jakarta EE application using the Lettuce client library. Redis is a powerful in-memory data structure store, often used as a cache, message broker, or database. Lettuce is a popular Java client for Redis, providing both synchronous and asynchronous capabilities. We will use a simple REST service to interact with Redis, demonstrating how to add and retrieve data from a Redis hash.

Prerequisites

  1. Basic knowledge of Jakarta EE and Java.
  2. A Jakarta EE-compatible application server (e.g., WildFly).
  3. Redis installed and running locally, or a Docker environment to launch it in a Container

Introduction to Redis and Lettuce

What is Redis?

Redis is an in-memory data structure store that can be used as a cache, message broker, and database. It supports data structures like strings, hashes, lists, sets, and more. Due to its in-memory nature, Redis provides very high read and write throughput, making it suitable for use cases where performance is critical.

What is Lettuce?

Lettuce is a scalable, thread-safe, and fully non-blocking Redis client for Java. It supports synchronous, asynchronous, and reactive programming models. Lettuce makes it easy to connect to a Redis server and perform various operations using a simple API.

Step 1: Start Redis

There are several options to get started with Redis. Check the documentation for more details. Here, we will bootstrap Redis as Container image with the following command:

docker run -d --name my-redis-stack -p 6379:6379  redis/redis-stack-server:latest

Then, verify with the command docker ps that Redis is up and running:

docker ps
CONTAINER ID   IMAGE                             COMMAND            CREATED             STATUS             PORTS                                       NAMES
ba00bb835999   redis/redis-stack-server:latest   "/entrypoint.sh"   About an hour ago   Up About an hour   0.0.0.0:6379->6379/tcp, :::6379->6379/tcp   my-redis-stack

Step 2: Setting Up the Jakarta EE Project

  1. Create a Jakarta EE project in your favorite IDE (e.g., IntelliJ, Eclipse). Ensure that the project is set up with the appropriate Jakarta EE libraries.
  2. Add Lettuce as a Dependency. If you are using Maven, add the following dependency to your pom.xml:
<dependency>
	<groupId>io.lettuce</groupId>
	<artifactId>lettuce-core</artifactId>
	<version>6.3.2.RELEASE</version> <!-- Check for the latest version on Maven Central -->
</dependency>

Step 3: Setting Up Redis in Jakarta EE

Firstly, we will use a RedisClient to connect to Redis. In Jakarta EE, we can use CDI (Contexts and Dependency Injection) to produce the RedisClient and inject it wherever needed.

import io.lettuce.core.RedisClient;
import jakarta.enterprise.context.ApplicationScoped;
import jakarta.enterprise.inject.Disposes;
import jakarta.enterprise.inject.Produces;

@ApplicationScoped
public class RedisClientProducer {

    // Producer method for RedisClient
    @Produces
    @ApplicationScoped
    public RedisClient createRedisClient() {
        return RedisClient.create("redis://localhost:6379");
    }

    // Disposer method to close the RedisClient
    public void closeRedisClient(@Disposes RedisClient redisClient) {
        redisClient.shutdown();
    }
}

With the above Producer, we are able to inject an instance of RedisClient in our Jakarta EE application. Also, as we can see from Lettuce documentation, the instance of RedisClient is thread-safe so we can share it with multiple Clients.

Step 4: Implementing the REST Service

Let’s create a REST service with two endpoints:

  1. GET /redis – Retrieve all entries from a Redis Hash.
  2. POST /redis – Add a new entry to the Redis Hash.
@Path("/redis")
public class SimpleRedisService {

	@Inject
	RedisClient redisClient;

	private static final String HASH_KEY = "user-session:123";

	@GET
	@Produces(MediaType.APPLICATION_JSON)
	public Response getAllEntries() {
		try (StatefulRedisConnection<String, String> connection = redisClient.connect()) {
			RedisAsyncCommands<String, String> asyncCommands = connection.async();

			// Get all key-value pairs in the hash
			Map<String, String> result = asyncCommands.hgetall(HASH_KEY).get();

			// Convert the result to a JSON string using JSON-B
			try (Jsonb jsonb = JsonbBuilder.create()) {
				String jsonResult = jsonb.toJson(result);
				return Response.ok(jsonResult).build();
			}

		} catch (ExecutionException | InterruptedException e) {
			return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
					.entity("Error retrieving entries: " + e.getMessage())
					.build();
		} catch (Exception e) {
			return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
					.entity("Failed to convert to JSON: " + e.getMessage())
					.build();
		}
	}

	@POST
	@Consumes(MediaType.APPLICATION_JSON)
	@Produces(MediaType.APPLICATION_JSON)
	public Response addEntry(Map<String, String> entry) {
		try (StatefulRedisConnection<String, String> connection = redisClient.connect()) {
			RedisAsyncCommands<String, String> asyncCommands = connection.async();

			// Add the new key-value pair to the hash
			asyncCommands.hset(HASH_KEY, entry).get();

			// Respond with the updated hash
			Map<String, String> updatedResult = asyncCommands.hgetall(HASH_KEY).get();
			try (Jsonb jsonb = JsonbBuilder.create()) {
				String jsonResult = jsonb.toJson(updatedResult);
				return Response.ok(jsonResult).build();
			}

		} catch (ExecutionException | InterruptedException e) {
			return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
					.entity("Error adding entry: " + e.getMessage())
					.build();
		} catch (Exception e) {
			return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
					.entity("Failed to convert to JSON: " + e.getMessage())
					.build();
		}
	}
}

In the provided example, the Redis Hash type is used. Here’s a brief overview of why hashes are employed and how they function in this context:

Redis Hash Overview

  • Data Structure: A Redis hash is a collection of key-value pairs, similar to a dictionary or map in programming languages. Each hash can store multiple fields, making it suitable for representing structured data.
  • Use Case in the Example: In the SimpleRediservice, we use a Redis hash to store user session data. The hash key is defined as "user-session:123", and it holds multiple fields such as name, surname, company, and age with their corresponding values.
redis with java tutorial

Step 5: Deploy and Test the Application

  1. Deploy the application to your Jakarta EE application server (e.g., WildFly).
  2. Test the endpoints using curl or a tool like Postman.

Example curl Commands:

  • POST Request to add an Entry:
curl -X POST http://localhost:8080/rest-demo/redis \
-H "Content-Type: application/json" \
-d '{"key": "email", "value": "[email protected]"}'

GET Request to list Entries:

curl http://localhost:8080/rest-demo/redis | jq

And here is the output:

{
  "surname": "Smith",
  "name": "John",
  "company": "Redis",
  "age": "29",
  "value": "[email protected]",
  "key": "email"
}

Summary

In this tutorial, we’ve learned how to:

  • Set up a Jakarta EE application to use Redis via the Lettuce client.
  • Implement a simple RESTful service with GET and POST methods for adding and retrieving data from Redis.
  • Use JSON-B for serializing and deserializing data.

This basic example can be expanded further to include more complex Redis operations, error handling, and application-specific logic.

Source code: https://github.com/fmarchioni/mastertheboss/tree/master/various/redis

Was this article helpful? We need your support to keep MasterTheBoss alive!