Spring Boot con Redis para almacenar JSON y no la serialización predefinida

En esta entrada mostraré como utilizar Spring Boot y Redis con el objetivo de almacenar los objetos como JSON y no como la forma de serialización de Java por defecto que tiene RedisTemplate. Los test están con Testcontainers, Junit 5 y AssertJ.


  • Lo primero es la clase que nos convoca, la configuración de RedisTemplate:
@Configuration
public class RedisConfig {

  @Autowired
  private ConfigurableApplicationContext context;

  @Bean
  @Primary
  public RedisTemplate redisTemplate(JedisConnectionFactory conn) {
    RedisTemplate template = new RedisTemplate<>();
    template.setConnectionFactory(conn);
    template.setDefaultSerializer(new GenericJackson2JsonRedisSerializer());
    template.setKeySerializer(new StringRedisSerializer());
    template.setHashKeySerializer(new StringRedisSerializer());
    template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    return template;
  }

  @Bean
  public JedisConnectionFactory redisConnectionFactory(@Value("${redis.host}") String host,
          @Value("${redis.puerto}") int puerto) {
    RedisStandaloneConfiguration config = new RedisStandaloneConfiguration(host, puerto);
    return new JedisConnectionFactory(config);
  }
}

  • En la clase anterior, lo que importa para el objetivo del ejemplo es lo siguiente:
    template.setDefaultSerializer(new GenericJackson2JsonRedisSerializer());
    template.setKeySerializer(new StringRedisSerializer());
    template.setHashKeySerializer(new StringRedisSerializer());
    template.setValueSerializer(new GenericJackson2JsonRedisSerializer());
    
    Acá definimos que en redis el valor será un JSON y las claves serán String.
  • La clase que usaré para la prueba es la siguiente:
    public class Persona {
    
      private int id;
      private String nombre;
    
      public int getId() {
        return id;
      }
    
      public void setId(int id) {
        this.id = id;
      }
    
      public String getNombre() {
        return nombre;
      }
    
      public void setNombre(String nombre) {
        this.nombre = nombre;
      }
    
    }
  • La clase de entrada para la aplicación:
    @SpringBootApplication
    public class RedisStringApplication {
    
      public static void main(String[] args) {
        SpringApplication.run(RedisStringApplication.class, args);
      }
    }
  • Y la clase de Test:
    @SpringBootTest
    @ContextConfiguration(initializers = {RedisStringJsonTests.Initializer.class})
    public class RedisStringJsonTests {
      private static String redisHost;
      private static int redisPuerto;
      
      @Rule
      public static GenericContainer cont = new GenericContainer<>("redis:5.0.3-alpine")
              .withExposedPorts(6379);
    
      @AfterAll
      static void detenerRedis() {
        cont.stop();
      }
    
      @BeforeAll
      static void iniciarRedis() {
        cont.start();
      }
    
      static class Initializer
              implements ApplicationContextInitializer {
    
        @Override
        public void initialize(ConfigurableApplicationContext ctx) {
          redisHost = cont.getContainerIpAddress();
          redisPuerto = cont.getFirstMappedPort();
          TestPropertyValues.of(
                  "redis.puerto=" + redisPuerto,
                  "redis.host=" + redisHost
          ).applyTo(ctx.getEnvironment());
        }
      }
    
      @Autowired
      private RedisTemplate redis;
    
      @Test
      public void validarRegistroJson() throws IOException {
        assertThat(redis).isNotNull();
        var hash = redis.opsForHash();
        final Persona p = new Persona();
        final var claveRedis = "redis-clave";
        final var claveRegistro = "clave";
        p.setId(1);
        p.setNombre("nombre");
        hash.put(claveRedis, claveRegistro, p);
        var x = (Persona) hash.get(claveRedis, claveRegistro);
        assertThat(x.getId()).isEqualTo(1);
        assertThat(x.getNombre()).isEqualTo("nombre");
        
        Jedis jedis = new Jedis(redisHost, redisPuerto);
        final var recuperado = jedis.hget(claveRedis, claveRegistro);
        var personaRecuperada = new ObjectMapper().readValue(recuperado, Persona.class);
        assertThat(personaRecuperada).isEqualToComparingFieldByField(p);
      }
    }
  • El Test consiste en lo siguiente:
    • Utiliza testcontainers para ejecutar redis
    • Define propiedades para acceder a Redis al inicializar los beans
    • Utiliza RedisTemplate para guardar un objeto y luego recuperarlo
    • Recuperar el valor json guardado para transformarlo en una instancia de la clase
  • El test demuestra que se puede guardar y recuperar el objeto y también se realiza una nueva conexion para recuperar el String y transformarlo para compararlo con la instancia que se guardó.
  • Para confirmar que se guardó como JSON la instancia al ser serializada, usando los datos que entrega testcontainers accederé al redis del contenedor para validar que se guardó como queremos (breakpoint en el código para conocer el puerto 😁):

Eso es todo, el código queda disponible en Github 👀

Usando Redis en Pruebas [versión 2]

Previamente había comentado como utilizar Redis en pruebas mediante redis embebido, pero en esta ocasión utilizaremos Testcontainers, que como lo dicen en su página, podemos utilizar cualquier cosa que corra en docker en nuestros tests 😮.

Comencemos

Lo primero es incluir en nuestro pom la dependencia a testcontainers:

<dependency>
    <groupId>org.testcontainers</groupId>
    <artifactId>testcontainers</artifactId>
    <version>1.11.2</version>
</dependency>

También incluimos otras librerías de test y el cliente redis:
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.assertj</groupId>
    <artifactId>assertj-core</artifactId>
    <version>3.11.1</version>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.1.0-m1</version>
</dependency>
<dependency>
    <groupId>org.slf4j</groupId>
    <artifactId>slf4j-simple</artifactId>
    <version>1.7.26</version>
    <scope>test</scope>
</dependency>

Ahora en nuestra clase de test escribimos lo siguiente:
@Rule
public GenericContainer redis = new GenericContainer<>("redis:5.0.3-alpine").withExposedPorts(6379);

@Test
public void clientePuedeObtenerConexion() {
    var cliente = new Jedis(redis.getContainerIpAddress(), redis.getFirstMappedPort());
    cliente.set("hola", "chao");
    Assertions.assertThat(cliente.get("hola")).isEqualTo("chao");
}

Lo que hace es iniciar la imagen redis:5.0.3-alpine en un contenedor y define el puerto interno PERO lo expone en el primer puerto que encuentra. Para cambiar este comportamiento hacemos lo siguiente exponiendo el puerto local 10000:


final Consumer config = e -> 
e.withPortBindings(new PortBinding(Ports.Binding.bindPort(10000),
new ExposedPort(containerExposedPort)));

@Rule
public GenericContainer redis = new GenericContainer("redis:5.0.3-alpine")
                .withExposedPorts(containerExposedPort)
                .withCreateContainerCmdModifier(config);


nuestro cliente puede ahora conectarse al puerto 10.000.

El código completo está disponible en Github 😃.