We try to use the Spring Data CrudRepository
in our project to provide persistency for our domain objects.
For a start I chose REDIS as backend since in a first experiment with a CrudRepository<ExperimentDomainObject, String>
it seemd, getting it running is easy.
When trying to put it in our production code, things got more complicated, because here our domain objects were not necesseriliy using a simple type as key so the repository was CrudRepository<TestObject, ObjectId>
.
Now I got the exception:
No converter found capable of converting from type [...ObjectId] to type [byte[]]
Searching for this exception, this answer which led my to uneducated experimenting with the RedisTemplate
configuration. (For my experiment I am using emdedded-redis)
My idea was, to provide a RedisTemplate<Object, Object>
instead of RedisTemplate<String, Object>
to allow using the Jackson2JsonRedisSerializer
to do the work as keySerializer also.
Still, calling testRepository.save(testObject)
fails.
Please see my code:
I have public fields and left out the imports for the brevity of this example. If they are required (to make this a MVCE) I will happily provide them. Just leave me a comment.
dependencies:
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-web'
compileOnly 'org.projectlombok:lombok'
annotationProcessor 'org.projectlombok:lombok'
implementation group: 'redis.clients', name: "jedis", version: '2.9.0'
implementation group: 'it.ozimov', name: 'embedded-redis', version: '0.7.2'
}
RedisConfiguration:
@Configuration
@EnableRedisRepositories
public class RedisConfiguration {
@Bean
JedisConnectionFactory jedisConnectionFactory() {
return new JedisConnectionFactory();
}
@Bean
public RedisTemplate<Object, Object> redisTemplate() {
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
final RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(jedisConnectionFactory());
template.setDefaultSerializer(jackson2JsonRedisSerializer);
template.setKeySerializer(jackson2JsonRedisSerializer);
template.setHashValueSerializer(jackson2JsonRedisSerializer);
template.setValueSerializer(jackson2JsonRedisSerializer);
template.setEnableDefaultSerializer(true);
return template;
}
}
TestObject
@RedisHash("test")
public class TestObject
{
@Id public ObjectId testId;
public String value;
public TestObject(ObjectId id, String value)
{
this.testId = id;
this.value = value; // In experiment this is: "magic"
}
}
ObjectId
@EqualsAndHashCode
public class ObjectId {
public String creator; // In experiment, this is "me"
public String name; // In experiment, this is "fool"
}
TestRepository
@Repository
public interface TestRepository extends CrudRepository<TestObject, ObjectId>
{
}
EmbeddedRedisConfiguration
@Configuration
public class EmbeddedRedisConfiguration
{
private final redis.embedded.RedisServer redisServer;
EmbeddedRedisConfiguration(RedisProperties redisProperties)
{
this.redisServer = new redis.embedded.RedisServer(redisProperties.getPort());
}
@PostConstruct
public void init()
{
redisServer.start();
}
@PreDestroy
public void shutdown()
{
redisServer.stop();
}
}
Application:
@SpringBootApplication
public class ExperimentApplication
{
public static void main(String[] args)
{
SpringApplication.run(ExperimentApplication.class, args);
}
}
Of course, I might introduce some special ID which is a simple datatype, e.g. a JSON-String which I build manually using jacksons ObjectMapper and then use a CrudRepository<TestObject, String>
.
RedisTemplate<String, String>
RedisTemplate<String, Object>
RedisTemplate
and setting its default serializerConverter<ObjectId, byte[]>
to
ConverterRegistry
GenericConversionService
In spring boot you need to set spring. cache. type=redis and this would create your cache using Redis. @Autowired RedisTemplate<String, String> redisTemplate; redisTemplate.
Once configured, the template is thread-safe and can be reused across multiple instances. RedisTemplate uses a Java-based serializer for most of its operations.
@RedisHash can take as parameters a timeToLive of type Long, and a String value, which will override the default Redis key prefix (by default, the key prefix is the fully qualified name of the class plus a colon). Within the class, most of the usual Spring Data annotations are supported.
Provides a suitable connection for interacting with Redis. Returns: connection for interacting with Redis. Throws: IllegalStateException - if the connection factory requires initialization and the factory was not yet initialized.
Basically, the Redis repositories use the RedisKeyValueTemplate
under the hood to store data as Key (Id) and Value pair. So your configuration of RedisTemplate
will not work unless you directly use it.
So one way for you will be to use the RedistTemplate
directly, something like this will work for you.
@Service
public class TestService {
@Autowired
private RedisTemplate redisTemplate;
public void saveIt(TestObject testObject){
ValueOperations<ObjectId, TestObject> values = redisTemplate.opsForValue();
values.set(testObject.testId, testObject);
}
}
So the above code will use your configuration and generate the string pair in the Redis using the Jackson as the mapper for both the key and the value.
But if you want to use the Redis Repositories via CrudRepository
you need to create reading and writing converters for ObjectId
from and to String
and byte[]
and register them as custom Redis conversions.
So let's create reading and writing converters for ObjectId
<-> String
Reader
@Component
@ReadingConverter
@Slf4j
public class RedisReadingStringConverter implements Converter<String, ObjectId> {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public ObjectId convert(String source) {
try {
return objectMapper.readValue(source, ObjectId.class);
} catch (IOException e) {
log.warn("Error while converting to ObjectId.", e);
throw new IllegalArgumentException("Can not convert to ObjectId");
}
}
}
Writer
@Component
@WritingConverter
@Slf4j
public class RedisWritingStringConverter implements Converter<ObjectId, String> {
private ObjectMapper objectMapper = new ObjectMapper();
@Override
public String convert(ObjectId source) {
try {
return objectMapper.writeValueAsString(source);
} catch (JsonProcessingException e) {
log.warn("Error while converting ObjectId to String.", e);
throw new IllegalArgumentException("Can not convert ObjectId to String");
}
}
}
And the reading and writing converters for ObjectId <-> byte[]
Writer
@Component
@WritingConverter
public class RedisWritingByteConverter implements Converter<ObjectId, byte[]> {
Jackson2JsonRedisSerializer<ObjectId> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(ObjectId.class);
@Override
public byte[] convert(ObjectId source) {
return jackson2JsonRedisSerializer.serialize(source);
}
}
Reader
@Component
@ReadingConverter
public class RedisReadingByteConverter implements Converter<byte[], ObjectId> {
Jackson2JsonRedisSerializer<ObjectId> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(ObjectId.class);
@Override
public ObjectId convert(byte[] source) {
return jackson2JsonRedisSerializer.deserialize(source);
}
}
And last add the Redis custom conversations. Just put the code into the RedisConfiguration
@Bean
public RedisCustomConversions redisCustomConversions(RedisReadingByteConverter readingConverter,
RedisWritingByteConverter redisWritingConverter,
RedisWritingStringConverter redisWritingByteConverter,
RedisReadingStringConverter redisReadingByteConverter) {
return new RedisCustomConversions(Arrays.asList(readingConverter, redisWritingConverter, redisWritingByteConverter, redisReadingByteConverter));
}
So now after the converters are created and registered as custom Redis Converters the RedisKeyValueTemplate
can use them and your code should work as expected.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With