Skip to content

Investigating issues faced when caching/persisting javax.money.MonetaryAmount using spring-data-redis

Notifications You must be signed in to change notification settings

w-k-s/Redis-MonetaryAmount-Experiment

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

4 Commits
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

Redis MonetaryAmount Experiment

The purpose of this repository is to investigate issues I faced saving instances of javax.money.MonetaryAmount to Redis using spring-data-redis.

javax.money.MonetaryAmount is an interface described in JSR 354 (Money and Currency API). We were using it's, as far as I can tell, one and only implementation: Moneta.

Redis Repositories

For the sake of flexibility, our cadebase used javax.money.MonetaryAmount as the type throughout the codebase and we would pass an instance of org.javamoney.moneta.FastMoney.

data class Example(val amount: MonetaryAmount)
val example = Example(FastMoney.of(1, "USD"))

The problem was that when saving such an object to a RedisRepository, the following exception is thrown:

org.springframework.data.keyvalue.core.UncategorizedKeyValueException: No getter available for persistent property javax.money.MonetaryAmountFactory.number

	at org.springframework.data.keyvalue.core.KeyValuePersistenceExceptionTranslator.translateExceptionIfPossible(KeyValuePersistenceExceptionTranslator.java:51)
	at org.springframework.data.keyvalue.core.KeyValueTemplate.resolveExceptionIfPossible(KeyValueTemplate.java:405)
	....
Caused by: java.lang.IllegalArgumentException: No getter available for persistent property javax.money.MonetaryAmountFactory.number
	at org.springframework.data.mapping.PersistentProperty.getRequiredGetter(PersistentProperty.java:93)
	at org.springframework.data.mapping.model.BeanWrapper.getProperty(BeanWrapper.java:135)
	at org.springframework.data.mapping.model.BeanWrapper.getProperty(BeanWrapper.java:108)
	at org.springframework.data.redis.core.convert.PathIndexResolver$1.doWithPersistentProperty(PathIndexResolver.java:122)
	...
	at org.springframework.data.redis.core.convert.MappingRedisConverter.write(MappingRedisConverter.java:114)
	at org.springframework.data.redis.core.RedisKeyValueAdapter.put(RedisKeyValueAdapter.java:195)
	at org.springframework.data.keyvalue.core.KeyValueTemplate.lambda$update$1(KeyValueTemplate.java:201)
	at org.springframework.data.keyvalue.core.KeyValueTemplate.execute(KeyValueTemplate.java:314)

From my understanding, the error occurs because redis tries to index each field in MonetaryAmount. As MonetaryAmount is an interface, that means it calls each getter in the interface, of them being MonetaryAmountFactory getFactory(). MonetaryAmountFactory is an interface that contains method to setNumber, but none to getNumber which I assume is the cause of this error.

From the Spring Redis Documentation, I should be able to completely customise the indexing by using a converter of type Converter<MonetaryAmount,Map<String,ByteArray>> instead of Converter<MonetaryAmount,ByteArray> but this resulted in the same error.

I tried to get help on StackOverflow and spring-data-redis' repository but to no avail.

The only way it works is if org.javamoney.moneta.FastMoney is used throughout along with a custom Converter. The custom converter is required because without it the following exception occurs:

org.springframework.data.keyvalue.core.UncategorizedKeyValueException: Unable to make private java.util.Currency(java.lang.String,int,int) accessible: module java.base does not "opens java.util" to unnamed module @5a39699c
	at org.springframework.data.keyvalue.core.KeyValuePersistenceExceptionTranslator.translateExceptionIfPossible(KeyValuePersistenceExceptionTranslator.java:51)
	at org.springframework.data.keyvalue.core.KeyValueTemplate.resolveExceptionIfPossible(KeyValueTemplate.java:405)
	at org.springframework.data.keyvalue.core.KeyValueTemplate.execute(KeyValueTemplate.java:316)
	...
Caused by: java.lang.reflect.InaccessibleObjectException: Unable to make private java.util.Currency(java.lang.String,int,int) accessible: module java.base does not "opens java.util" to unnamed module @5a39699c
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:354)
	at java.base/java.lang.reflect.AccessibleObject.checkCanSetAccessible(AccessibleObject.java:297)
	at java.base/java.lang.reflect.Constructor.checkCanSetAccessible(Constructor.java:188)
	...

Caching

Caching objects containing MonetaryAmount (using @Cacheable) proved to be a challenge to when using the GenericJackson2JsonRedisSerializer (The default JdkSerializationRedisSerializer works fine since FastMoney is Serializable ). The following exception was observed:

org.springframework.data.redis.serializer.SerializationException: Could not read JSON:Cannot construct instance of `org.javamoney.moneta.FastMoney` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (byte[])"{"@class":"org.javamoney.moneta.FastMoney","currency":{"@class":"org.javamoney.moneta.spi.JDKCurrencyAdapter","context":{"empty":false,"providerName":"java.util.Currency"},"defaultFractionDigits":2,"currencyCode":"AED","numericCode":784},"number":["org.javamoney.moneta.spi.DefaultNumberValue",2038.00000],"factory":{"@class":"org.javamoney.moneta.spi.FastMoneyAmountFactory","defaultMonetaryContext":{"precision":19,"maxScale":5,"fixedScale":true,"amountType":"org.javamoney.moneta.FastMoney","empty"[truncated 645 bytes]; line: 1, column: 44] 

	at org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer.deserialize(GenericJackson2JsonRedisSerializer.java:253)
	...
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `org.javamoney.moneta.FastMoney` (no Creators, like default constructor, exist): cannot deserialize from Object value (no delegate- or property-based Creator)
 at [Source: (byte[])"{"@class":"org.javamoney.moneta.FastMoney","currency":{"@class":"org.javamoney.moneta.spi.JDKCurrencyAdapter","context":{"empty":false,"providerName":"java.util.Currency"},"defaultFractionDigits":2,"currencyCode":"AED","numericCode":784},"number":["org.javamoney.moneta.spi.DefaultNumberValue",2038.00000],"factory":{"@class":"org.javamoney.moneta.spi.FastMoneyAmountFactory","defaultMonetaryContext":{"precision":19,"maxScale":5,"fixedScale":true,"amountType":"org.javamoney.moneta.FastMoney","empty"[truncated 645 bytes]; line: 1, column: 44]
	at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:67)
	...

The issue here is that FastMoney does not provide a no-arg constructor so Jackson doesn't know how to create an instance of it when deserializing. To resolve this, a custom deserializer is required. Zalando provides a library that contains custom serdes for Java Money types: org.zalando:jackson-datatype-money.

@Bean
fun cacheConfiguration(objectMapper: ObjectMapper): RedisCacheConfiguration {
    val redisMapper = objectMapper.copy().also {
        it.findAndRegisterModules()                 // Finds and registers MoneyModule() from org.zalando:jackson-datatype-money
            .activateDefaultTyping(                 // GenericJackson2JsonRedisSerializer requires serialization to be done with type info.
                it.polymorphicTypeValidator,
                ObjectMapper.DefaultTyping.EVERYTHING,
                JsonTypeInfo.As.PROPERTY
            )
    }

    return RedisCacheConfiguration.defaultCacheConfig()
        .entryTtl(Duration.ofMinutes(60))
        .disableCachingNullValues()
        .serializeValuesWith(SerializationPair.fromSerializer(GenericJackson2JsonRedisSerializer(redisMapper)))
}    

However the deserializer from Zalando did not work because the serializer does not include type info when serializing.

org.springframework.data.redis.serializer.SerializationException: Could not write JSON: Type id handling not implemented for type javax.money.CurrencyUnit (by serializer of type org.zalando.jackson.datatype.money.CurrencyUnitSerializer)

	at org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer.serialize(GenericJackson2JsonRedisSerializer.java:223)
	at org.springframework.data.redis.serializer.DefaultRedisElementWriter.write(DefaultRedisElementWriter.java:41)
	at org.springframework.data.redis.serializer.RedisSerializationContext$SerializationPair.write(RedisSerializationContext.java:287)
	...
Caused by: com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Type id handling not implemented for type javax.money.CurrencyUnit (by serializer of type org.zalando.jackson.datatype.money.CurrencyUnitSerializer)
	at com.fasterxml.jackson.databind.exc.InvalidDefinitionException.from(InvalidDefinitionException.java:77)
	at com.fasterxml.jackson.databind.SerializerProvider.reportBadDefinition(SerializerProvider.java:1308)
	... 84 more

I can't f**king figure out how to write a typed serializer either!

About

Investigating issues faced when caching/persisting javax.money.MonetaryAmount using spring-data-redis

Topics

Resources

Stars

Watchers

Forks

Releases

No releases published

Packages

No packages published

Languages