{getToc} $title={Table of Contents}
Spring Data Redis Reactive |
In this post, we’ll implement a sample REST API that uses Spring WebFlux with
Spring Data Redis Reactive.
Redis is an
open-source (BSD licensed), in-memory data structure store used as a database,
cache, message broker, and streaming engine. Redis provides data structures
such as strings, hashes, lists, sets, sorted sets with range queries, bitmaps,
hyperloglogs, geospatial indexes, and streams.
Prerequisites
- Spring Boot 2.6.5
- Maven 3.6.+
- Java 11 or later
- Redis 3.2 or later
Getting Started
We will start by creating a simple Spring Boot project from start.spring.io, with the following dependencies: Spring Reactive Web, Spring Data Reactive Redis, Lombok, and Validation.
Configuration
One of the first tasks when using Redis and Spring is to establish a
connection with our Redis server.
We need to create our first reactiveRedisConnectionFactory bean with the
hostname and port of the Redis server.
ReactiveRedisConfiguration.java
@Configuration public class ReactiveRedisConfiguration { private final Environment env; public ReactiveRedisConfiguration(Environment env) { this.env = env; } @Bean public ReactiveRedisConnectionFactory reactiveRedisConnectionFactory() { return new LettuceConnectionFactory(Objects.requireNonNull(env.getProperty("spring.redis.host")), Integer.parseInt(Objects.requireNonNull(env.getProperty("spring.redis.port")))); } @Bean public ReactiveRedisOperations<String, Object> redisOperations(ReactiveRedisConnectionFactory reactiveRedisConnectionFactory) { Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class); RedisSerializationContext.RedisSerializationContextBuilder<String, Object> builder = RedisSerializationContext.newSerializationContext(new StringRedisSerializer()); RedisSerializationContext<String, Object> context = builder.value(serializer).hashValue(serializer) .hashKey(serializer).build(); return new ReactiveRedisTemplate<>(reactiveRedisConnectionFactory, context); } }
The
LettuceConnectionFactory
class is the factory that creates
Lettuce-based connections. It create a new LettuceConnection on each call to
getConnection().
The second step in the configuration is to add a second redisOperations bean
method, which takes a ReactiveRedisConnectionFactory and returns a
ReactiveRedisTemplate. This bean uses the Jackson library by configuring a
Jackson2JsonRedisSerializer to perform automatic
serialization/deserialization between the given objects and the underlying
binary data in the Redis store. We can inject this bean wherever we need to
access Redis
CRUD API
To get started, we need a model class. For this post, we have a Book model
class.
@RedisHash @Data @AllArgsConstructor @NoArgsConstructor public class Book { @NotNull private String id; private String title; private int page; private String isbn; private String description; private double price; @JsonDeserialize(using = LocalDateDeserializer.class) @JsonSerialize(using = LocalDateSerializer.class) private LocalDate publicationDate; private String language; }
-
@RedisHash
annotation marks Objects as aggregate roots to be stored in a Redis hash.
@Id
to provide primary keys to mapped objects.
We now have the generic component ReactiveRedisComponent which contains
all the common crud methods. This class will be used with different repository
classes.
ReactiveRedisComponent.java
@SuppressWarnings("rawtypes") @Slf4j @Component public class ReactiveRedisComponent { private final ReactiveRedisOperations<String, Object> redisOperations; public ReactiveRedisComponent(ReactiveRedisOperations<String, Object> redisOperations) { this.redisOperations = redisOperations; } /** * Set key and value into a hash key * @param key key value - must not be null. * @param hashKey hash key value - must not be null. * @param val Object value * @return Mono of object */ public Mono<Object> set(String key, String hashKey, Object val) { return redisOperations.opsForHash().put(key, hashKey, val).map(b -> val); } /** * @param key key value - must not be null. * @return Flux of Object */ public Flux<Object> get(@NotNull String key){ return redisOperations.opsForHash().values(key); } /** * Get value for given hashKey from hash at key. * @param key key value - must not be null. * @param hashKey hash key value - must not be null. * @return Object */ public Mono<Object> get(String key, Object hashKey) { return redisOperations.opsForHash().get(key, hashKey); } /** * Delete a key that contained in a hash key. * @param key key value - must not be null. * @param hashKey hash key value - must not be null. * @return 1 Success or 0 Error */ public Mono<Long> remove(String key, Object hashKey) { return redisOperations.opsForHash().remove(key, hashKey); } }
RedisBookRepository.java
@Repository @RequiredArgsConstructor public class RedisBookRepository implements BookRepository { private final ReactiveRedisComponent reactiveRedisComponent; @Override public Mono<Book> save(Book book) { return reactiveRedisComponent.set(BOOK_KEY, book.getId(), book).map(b -> book); } @Override public Mono<Book> get(String key) { return reactiveRedisComponent.get(BOOK_KEY, key).flatMap(d -> Mono.just(ObjectMapperUtils.objectMapper(d, Book.class))); } @Override public Flux<Book> getAll(){ return reactiveRedisComponent.get(BOOK_KEY).map(b -> ObjectMapperUtils.objectMapper(b, Book.class)) .collectList().flatMapMany(Flux::fromIterable); } @Override public Mono<Long> delete(String id) { return reactiveRedisComponent.remove(BOOK_KEY,id); } }
BookServiceImpl.java
@RequiredArgsConstructor @Service public class BookServiceImpl implements BookService { private final RedisBookRepository bookRepository; @Override public Mono<Book> create(Book book) { return bookRepository.save(book); } @Override public Flux<Book> getAll(){ return bookRepository.getAll(); } @Override public Mono<Book> getOne(String id){ return bookRepository.get(id); } @Override public Mono<Long> deleteById(String id) { return bookRepository.delete(id); } }
BookController.java
@RestController @RequestMapping("/v1") @RequiredArgsConstructor public class BookController { private final BookServiceImpl bookService; @PostMapping("/book") @ResponseStatus(HttpStatus.CREATED) public Mono<Book> addBook(@RequestBody @Valid Book book) { return bookService.create(book); } @GetMapping("/book") public Flux<Book> getAllBooks() { return bookService.getAll(); } @GetMapping("/book/{id}") public Mono<Book> getBook(@PathVariable String id) { return bookService.getOne(id); } @DeleteMapping("/book/{id}") public Mono<Long> deleteBook(@PathVariable String id) { return bookService.deleteById(id); } }