Spring Reactive: Spring Web-Flux and Spring Data Redis Reactive

{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);
    }

}



Test the application

Now we can run our application and test it.

Redis Desktop Manager








Summary

Congratulations! We have just developed a full reactive application with Spring WebFlux and Spring Data Redis Reactive.




Previous Post Next Post