Configure a RedisTemplate and learn how to access different operation bundles to read and write data to and from Redis in a Spring REST controller.
In this lesson, students will learn:
If you get stuck:
Spring Data Redis provides the abstractions of the Spring Data platform to Redis.
We will start by configuring a RedisTemplate, a class that provides a thread-safe bridge between Spring and Redis Commands. It handles connection management, freeing the developer from opening and closing Redis connections.
Start from the main application class, Redi2readApplication
, located at src/main/java/com/redislabs/edu/redi2read/Redi2readApplication.java
.
Add a @Bean
annotated method called redisTemplate, which takes a RedisConnectionFactory
and returns a RedisTemplate
. This method will provide a configured bean of type RedisTemplate
in the Spring container. We can inject this bean wherever we need to access Redis.
@Bean
public RedisTemplate<?, ?> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<?, ?> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
return template;
}
This requires the following imports:
import org.springframework.context.annotation.Bean;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
Notice that while the template types are generic, it is up to the serializers/deserializers to convert the given Objects to-and-from binary data correctly.
We could also configure the Redis host and port programmatically by defining a @Bean
annotated method that returns a RedisConnectionFactory
(either a JedisConnectionFactory
) and use the setHostName
and setPort
methods.
But since Spring Data Redis can configure the beans using a properties file (either Java Properties or YAML), we will use the applications.properties
file instead.
Spring Data Redis properties are prefixed with spring.redis.
. In the file src/main/resources/application.properties
add the following properties:
spring.redis.host=localhost
spring.redis.port=6379
While we're at it, let’s also add an exclusion for the autoconfiguration of Spring Security. Since we’ve included the Maven dependency for Spring Security but have not yet configured it, Spring defaults on the safe side and protects all endpoints on the application. So, for now, we’ll disable security autoconfiguration until we decide to secure our services.
spring.redis.host=localhost
spring.redis.port=6379
spring.autoconfigure.exclude=org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration
To test the RedisTemplate, we’ll create a REST controller and use it to perform some operations against our Redis instance.
We will add the controller under the src/main/java/com/redislabs/edu/redi2read/controllers folder, which means it’ll live in the com.redislabs.edu.redi2read.controllers package.
package com.redislabs.edu.redi2read.controllers;
public class HelloRedisController {
}
Next, let’s annotate the class with the @RestController
and the @RequestMapping
annotations. The controller will now listen to requests rooted at http://localhost:8080/api/redis.
@RestController
@RequestMapping("/api/redis")
public class HelloRedisController {
}
Add the necessary import as shown next:
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
Next, let’s inject an @Autowired
instance of RedisTemplate
. Notice that we will use concrete classes for the K
and V
parameter classes in RedisTemplate<K,V>
. K
is the Redis key type (usually a String
) and V
, the Redis value type (i.e., something that maps to a Redis data structure).
@RestController
@RequestMapping("/api/redis")
public class HelloRedisController {
@Autowired
private RedisTemplate<String, String> template;
}
Add the necessary import as shown next:
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
Now, all we need is a controller method to run some Redis commands. We will use the Redis SET command (https://redis.io/commands/set) as we previously demonstrated using the Redis CLI.
First, we’ll create a String that will serve to prefix the keys that we write to Redis:
private static final String STRING_KEY_PREFIX = "redi2read:strings:";
The method is annotated with the @PostMapping
with a path of /strings
, making the effective path for our post /api/redis/strings
. The @Request
body and the return value of the method is a Map.Entry<String, String>
which is convenient when dealing with name-value pairs.
@PostMapping("/strings")
@ResponseStatus(HttpStatus.CREATED)
public Map.Entry<String, String> setString(@RequestBody Map.Entry<String, String> kvp) {
return kvp;
}
Add the necessary import as shown next:
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
If we launch the application now with:
./mvnw clean spring-boot:run
We can use curl to invoke our api/redis/strings
endpoint:
$ curl --location --request POST 'http://localhost:8080/api/redis/strings' \
--header 'Content-Type: application/json' \
--data-raw '{ "database:redis:creator": "Salvatore Sanfilippo" }'
{"database:redis:creator":"Salvatore Sanfilippo"}
This results in the JSON payload being echoed back. Let's complete the implementation of the setString method so that we can write a Redis string to the database:
@PostMapping("/strings")
@ResponseStatus(HttpStatus.CREATED)
public Map.Entry<String, String> setString(@RequestBody Map.Entry<String, String> kvp) {
template.opsForValue().set(STRING_KEY_PREFIX + kvp.getKey(), kvp.getValue());
return kvp;
}
We will use the RedisTemplate
instance template opsForValue()
method to get an instance of ValueOperations
, which provides methods to execute operations performed on simple values (or Strings
in Redis terminology).
The Redis SET method is implemented using the (you guessed it!) set()
method, which takes a key name and a value. We are prepending the KEY_SPACE_PREFIX to the key that's provided as an argument.
Before you fire up another curl request, let’s start a Redis CLI instance with the MONITOR flag so that we can watch what transpires when we hit the server.
$ redis-cli MONITOR
Now, when you issue the POST request again, you should see output similar to:
1617346602.221390 [0 172.19.0.1:58396] "SET" "redi2read:strings:database:redis:creator" "Salvatore Sanfilippo"
We can launch another Redis CLI to query Redis ourselves:
127.0.0.1:6379> KEYS *
1) "redi2read:strings:database:redis:creator"
127.0.0.1:6379> TYPE "redi2read:strings:database:redis:creator"
string
127.0.0.1:6379> GET "redi2read:strings:database:redis:creator"
"Salvatore Sanfilippo"
127.0.0.1:6379>
If we use the KEYS * command, we can see all of the keys stored in Redis (don’t do this on a production box with a lot of data,
as you’ll block your Redis instance while serving a massive response).
The redi2read:strings:database:redis:creator
key has been created, and it is a Redis String with a value of Salvatore Sanfilipo
We can now write strings to Redis through our REST controller.
Next, let’s add a corresponding GET method to our controller to read string values:
@GetMapping("/strings/{key}")
public Map.Entry<String, String> getString(@PathVariable("key") String key) {
String value = template.opsForValue().get(STRING_KEY_PREFIX + key);
if (value == null) {
throw new ResponseStatusException(HttpStatus.NOT_FOUND, "key not found");
}
return new SimpleEntry<String, String>(key, value);
}
With imports:
import java.util.AbstractMap.SimpleEntry;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.server.ResponseStatusException;
We can now issue a GET request to retrieve String keys:
$ curl --location --request GET 'http://localhost:8080/api/redis/strings/database:redis:creator'
{"database:redis:creator":"Salvatore Sanfilippo"}
On the Redis CLI monitor you should see:
1617347871.901585 [0 172.19.0.1:58284] "GET" "redi2read:strings:database:redis:creator"
Note that in order to return an error on a key not found, we have to check the result for null and throw an appropriate exception.
{
"timestamp": "2021-04-02T07:45:10.303+00:00",
"status": 404,
"error": "Not Found",
"trace": "org.springframework.web.server.ResponseStatusException: 404...\n",
"message": "key not found",
"path": "/api/redis/strings/database:neo4j:creator"
}
Keep in mind that this is a “development time” exception, appropriate to be shown on an error page meant for developers. Likely, we would intercept this exception and create an API appropriate response (likely just the status and error fields above).