diff --git a/pom.xml b/pom.xml
index dfe4eb4ec..474def963 100644
--- a/pom.xml
+++ b/pom.xml
@@ -41,6 +41,8 @@
1.5.68.0.1.Final4.1.110.Final
+ 5.6.0
+ 1.17.0
@@ -210,6 +212,16 @@
lombok${lombok}
+
+ com.esotericsoftware
+ kryo
+ ${kryo.version}
+
+
+ commons-codec
+ commons-codec
+ ${apacheCommonsCodec.version}
+
@@ -243,6 +255,14 @@
aerospike-reactor-clienttrue
+
+ com.esotericsoftware
+ kryo
+
+
+ commons-codec
+ commons-codec
+ joda-timejoda-time
@@ -392,9 +412,13 @@
**/*Test.java**/*Tests.java
- --add-opens java.base/java.util=ALL-UNNAMED --add-opens java.base/java.math=ALL-UNNAMED
+
+ --add-opens java.base/java.util=ALL-UNNAMED
--add-opens java.base/java.net=ALL-UNNAMED
- --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED
+ --add-opens java.base/java.math=ALL-UNNAMED
+ --add-opens java.base/java.util.concurrent.atomic=ALL-UNNAMED
+ --add-opens java.base/java.lang.reflect=ALL-UNNAMED
+
diff --git a/src/main/asciidoc/reference/caching.adoc b/src/main/asciidoc/reference/caching.adoc
index bf76b03b6..006dd6776 100644
--- a/src/main/asciidoc/reference/caching.adoc
+++ b/src/main/asciidoc/reference/caching.adoc
@@ -69,13 +69,17 @@ public class AerospikeConfiguration {
public AerospikeClient aerospikeClient() {
ClientPolicy clientPolicy = new ClientPolicy();
clientPolicy.failIfNotConnected = true;
- return new AerospikeClient(clientPolicy, aerospikeConfigurationProperties.getHost(), aerospikeConfigurationProperties.getPort());
+ return new AerospikeClient(clientPolicy, aerospikeConfigurationProperties.getHost(),
+ aerospikeConfigurationProperties.getPort());
}
@Bean
- public AerospikeCacheManager cacheManager(AerospikeClient aerospikeClient) {
+ public CacheManager cacheManager(IAerospikeClient aerospikeClient,
+ MappingAerospikeConverter aerospikeConverter,
+ AerospikeCacheKeyProcessor cacheKeyProcessor) {
AerospikeCacheConfiguration defaultConfiguration = new AerospikeCacheConfiguration("test");
- return new AerospikeCacheManager(aerospikeClient, mappingAerospikeConverter, defaultConfiguration);
+ return new AerospikeCacheManager(aerospikeClient, mappingAerospikeConverter, defaultConfiguration,
+ cacheKeyProcessor);
}
}
----
@@ -96,7 +100,7 @@ The heart of the cache layer, to define an AerospikeCacheManager you need:
. aerospikeConverter (MappingAerospikeConverter).
. defaultCacheConfiguration (AerospikeCacheConfiguration), a default cache configuration that applies when creating new caches.
Cache configuration contains a namespace, a set (null by default meaning write directly to the namespace w/o specifying a set) and an expirationInSeconds (AKA TTL, default is 0 meaning use Aerospike server’s default).
-. Optional: initalPerCacheConfiguration (Map), You can also specify a map of cache names and matching configuration, it will create the caches with the given matching configuration at the application startup.
+. Optional: initialPerCacheConfiguration (Map), You can also specify a map of cache names and matching configuration, it will create the caches with the given matching configuration at the application startup.
NOTE: A cache name is only a link to the cache configuration.
diff --git a/src/main/java/org/springframework/data/aerospike/cache/AerospikeCache.java b/src/main/java/org/springframework/data/aerospike/cache/AerospikeCache.java
index 99614fa28..60efd2880 100644
--- a/src/main/java/org/springframework/data/aerospike/cache/AerospikeCache.java
+++ b/src/main/java/org/springframework/data/aerospike/cache/AerospikeCache.java
@@ -46,11 +46,13 @@ public class AerospikeCache implements Cache {
private final AerospikeCacheConfiguration cacheConfiguration;
private final WritePolicy createOnly;
private final WritePolicy writePolicyForPut;
+ private final AerospikeCacheKeyProcessor cacheKeyProcessor;
public AerospikeCache(String name,
IAerospikeClient client,
AerospikeConverter aerospikeConverter,
- AerospikeCacheConfiguration cacheConfiguration) {
+ AerospikeCacheConfiguration cacheConfiguration,
+ AerospikeCacheKeyProcessor cacheKeyProcessor) {
this.name = name;
this.client = client;
this.aerospikeConverter = aerospikeConverter;
@@ -62,6 +64,7 @@ public AerospikeCache(String name,
this.writePolicyForPut = WritePolicyBuilder.builder(client.getWritePolicyDefault())
.expiration(cacheConfiguration.getExpirationInSeconds())
.build();
+ this.cacheKeyProcessor = cacheKeyProcessor;
}
/**
@@ -225,12 +228,16 @@ public ValueWrapper putIfAbsent(Object key, Object value) {
}
private Key getKey(Object key) {
- return new Key(cacheConfiguration.getNamespace(), cacheConfiguration.getSet(), key.toString());
+ AerospikeCacheKey cacheKey = cacheKeyProcessor.serializeAndHash(key);
+ return new Key(cacheConfiguration.getNamespace(), cacheConfiguration.getSet(),
+ cacheKey.getValue());
}
private void serializeAndPut(WritePolicy writePolicy, Object key, Object value) {
- AerospikeWriteData data = AerospikeWriteData.forWrite(getKey(key).namespace);
+ AerospikeWriteData data = AerospikeWriteData.forWrite(cacheConfiguration.getNamespace());
+ Key aerospikeKey = getKey(key);
+ data.setKey(aerospikeKey); // Set the key on the data object
aerospikeConverter.write(value, data);
- client.put(writePolicy, getKey(key), data.getBinsAsArray());
+ client.put(writePolicy, aerospikeKey, data.getBinsAsArray());
}
}
diff --git a/src/main/java/org/springframework/data/aerospike/cache/AerospikeCacheKey.java b/src/main/java/org/springframework/data/aerospike/cache/AerospikeCacheKey.java
new file mode 100644
index 000000000..e4542d7c8
--- /dev/null
+++ b/src/main/java/org/springframework/data/aerospike/cache/AerospikeCacheKey.java
@@ -0,0 +1,56 @@
+package org.springframework.data.aerospike.cache;
+
+import com.aerospike.client.Value;
+import lombok.Getter;
+
+/**
+ * Wrapper class used in caching. Receives hash of the cache key as a String, a long number or a byte array.
+ */
+public class AerospikeCacheKey {
+
+ @Getter
+ private final Value value;
+
+ private AerospikeCacheKey(String string) {
+ this.value = new Value.StringValue(string);
+ }
+
+ private AerospikeCacheKey(long number) {
+ this.value = new Value.LongValue(number);
+ }
+
+ private AerospikeCacheKey(byte[] data) {
+ this.value = new Value.BytesValue(data);
+ }
+
+ /**
+ * Instantiate AerospikeCacheKey instance with a String.
+ *
+ * @param string String parameter
+ * @return new instance of AerospikeCacheKey initialized with the given parameter
+ */
+ public static AerospikeCacheKey of(String string) {
+ return new AerospikeCacheKey(string);
+ }
+
+ /**
+ * Instantiate AerospikeCacheKey instance with a long number.
+ *
+ * @param number long number
+ * @return new instance of AerospikeCacheKey initialized with the given parameter
+ */
+ public static AerospikeCacheKey of(long number) {
+ return new AerospikeCacheKey(number);
+ }
+
+ /**
+ * Instantiate AerospikeCacheKey instance with a byte array.
+ *
+ * @param data byte array
+ * @return new instance of AerospikeCacheKey initialized with the given parameter
+ */
+ public static AerospikeCacheKey of(byte[] data) {
+ return new AerospikeCacheKey(data);
+ }
+
+}
diff --git a/src/main/java/org/springframework/data/aerospike/cache/AerospikeCacheKeyProcessor.java b/src/main/java/org/springframework/data/aerospike/cache/AerospikeCacheKeyProcessor.java
new file mode 100644
index 000000000..f89c54e1d
--- /dev/null
+++ b/src/main/java/org/springframework/data/aerospike/cache/AerospikeCacheKeyProcessor.java
@@ -0,0 +1,35 @@
+package org.springframework.data.aerospike.cache;
+
+/**
+ * Interface that provides methods used in caching
+ */
+public interface AerospikeCacheKeyProcessor {
+
+ /**
+ * Serialize the given key and calculate hash based on the serialization result.
+ *
+ * @param key Object to be serialized and hashed
+ * @return AerospikeCacheKey instantiated with either a String or a long number
+ */
+ AerospikeCacheKey serializeAndHash(Object key);
+
+ /**
+ * Serialize the given key.
+ *
+ * The default implementation uses Kryo.
+ *
+ * @param key Object to be serialized
+ * @return byte[]
+ */
+ byte[] serialize(Object key);
+
+ /**
+ * Calculate hash based on the given byte array.
+ *
+ * The default implementation uses 128 bit Murmur3 hashing.
+ *
+ * @param data Byte array to be hashed
+ * @return AerospikeCacheKey instantiated with either a String or a long number
+ */
+ AerospikeCacheKey calculateHash(byte[] data);
+}
diff --git a/src/main/java/org/springframework/data/aerospike/cache/AerospikeCacheKeyProcessorImpl.java b/src/main/java/org/springframework/data/aerospike/cache/AerospikeCacheKeyProcessorImpl.java
new file mode 100644
index 000000000..035352db8
--- /dev/null
+++ b/src/main/java/org/springframework/data/aerospike/cache/AerospikeCacheKeyProcessorImpl.java
@@ -0,0 +1,47 @@
+package org.springframework.data.aerospike.cache;
+
+import com.esotericsoftware.kryo.Kryo;
+import com.esotericsoftware.kryo.io.ByteBufferOutput;
+import lombok.Getter;
+import org.apache.commons.codec.digest.MurmurHash3;
+import org.objenesis.strategy.StdInstantiatorStrategy;
+
+import java.util.Arrays;
+
+public class AerospikeCacheKeyProcessorImpl implements AerospikeCacheKeyProcessor {
+
+ @Getter
+ private final Kryo kryoInstance = new Kryo();
+
+ public AerospikeCacheKeyProcessorImpl() {
+ configureKryo();
+ }
+
+ /**
+ * Configuration for Kryo instance.
+ *
+ * Classes of the objects to be cached can be pre-registered if required. Registering in advance is not necessary,
+ * however it can be done to increase serialization performance. If a class has been pre-registered, the first time
+ * it is encountered Kryo can just output a numeric reference to it instead of writing fully qualified class name.
+ */
+ public void configureKryo() {
+ // setting to false means not requiring registration for all the classes of cached objects in advance
+ getKryoInstance().setRegistrationRequired(false);
+ getKryoInstance().setInstantiatorStrategy(new StdInstantiatorStrategy());
+ }
+
+ public AerospikeCacheKey serializeAndHash(Object key) {
+ return calculateHash(serialize(key));
+ }
+
+ public byte[] serialize(Object key) {
+ ByteBufferOutput output = new ByteBufferOutput(1024); // Initial buffer size
+ kryoInstance.writeClassAndObject(output, key);
+ output.flush();
+ return output.toBytes();
+ }
+
+ public AerospikeCacheKey calculateHash(byte[] data) {
+ return AerospikeCacheKey.of(Arrays.toString(MurmurHash3.hash128(data)));
+ }
+}
diff --git a/src/main/java/org/springframework/data/aerospike/cache/AerospikeCacheManager.java b/src/main/java/org/springframework/data/aerospike/cache/AerospikeCacheManager.java
index 3d94b4173..073eb73a3 100644
--- a/src/main/java/org/springframework/data/aerospike/cache/AerospikeCacheManager.java
+++ b/src/main/java/org/springframework/data/aerospike/cache/AerospikeCacheManager.java
@@ -45,6 +45,7 @@ public class AerospikeCacheManager extends AbstractTransactionSupportingCacheMan
private final AerospikeConverter aerospikeConverter;
private final AerospikeCacheConfiguration defaultCacheConfiguration;
private final Map initialPerCacheConfiguration;
+ private final AerospikeCacheKeyProcessor cacheKeyProcessor;
/**
* Create a new {@link AerospikeCacheManager} instance - Specifying a default cache configuration.
@@ -55,8 +56,9 @@ public class AerospikeCacheManager extends AbstractTransactionSupportingCacheMan
*/
public AerospikeCacheManager(IAerospikeClient aerospikeClient,
AerospikeConverter aerospikeConverter,
- AerospikeCacheConfiguration defaultCacheConfiguration) {
- this(aerospikeClient, aerospikeConverter, defaultCacheConfiguration, new LinkedHashMap<>());
+ AerospikeCacheConfiguration defaultCacheConfiguration,
+ AerospikeCacheKeyProcessor cacheKeyProcessor) {
+ this(aerospikeClient, aerospikeConverter, defaultCacheConfiguration, new LinkedHashMap<>(), cacheKeyProcessor);
}
/**
@@ -71,7 +73,8 @@ public AerospikeCacheManager(IAerospikeClient aerospikeClient,
public AerospikeCacheManager(IAerospikeClient aerospikeClient,
AerospikeConverter aerospikeConverter,
AerospikeCacheConfiguration defaultCacheConfiguration,
- Map initialPerCacheConfiguration) {
+ Map initialPerCacheConfiguration,
+ AerospikeCacheKeyProcessor cacheKeyProcessor) {
Assert.notNull(aerospikeClient, "The aerospike client must not be null");
Assert.notNull(aerospikeConverter, "The aerospike converter must not be null");
Assert.notNull(defaultCacheConfiguration, "The default cache configuration must not be null");
@@ -80,6 +83,7 @@ public AerospikeCacheManager(IAerospikeClient aerospikeClient,
this.aerospikeConverter = aerospikeConverter;
this.defaultCacheConfiguration = defaultCacheConfiguration;
this.initialPerCacheConfiguration = initialPerCacheConfiguration;
+ this.cacheKeyProcessor = cacheKeyProcessor;
}
@Override
@@ -105,11 +109,12 @@ protected Cache decorateCache(Cache cache) {
}
private AerospikeCache createCache(String name) {
- return new AerospikeCache(name, aerospikeClient, aerospikeConverter, defaultCacheConfiguration);
+ return new AerospikeCache(name, aerospikeClient, aerospikeConverter, defaultCacheConfiguration,
+ cacheKeyProcessor);
}
private AerospikeCache createCache(String name, AerospikeCacheConfiguration cacheConfiguration) {
- return new AerospikeCache(name, aerospikeClient, aerospikeConverter, cacheConfiguration);
+ return new AerospikeCache(name, aerospikeClient, aerospikeConverter, cacheConfiguration, cacheKeyProcessor);
}
private boolean isCacheAlreadyDecorated(Cache cache) {
diff --git a/src/main/java/org/springframework/data/aerospike/config/AerospikeDataConfigurationSupport.java b/src/main/java/org/springframework/data/aerospike/config/AerospikeDataConfigurationSupport.java
index b0299aec8..b4c3fed5a 100644
--- a/src/main/java/org/springframework/data/aerospike/config/AerospikeDataConfigurationSupport.java
+++ b/src/main/java/org/springframework/data/aerospike/config/AerospikeDataConfigurationSupport.java
@@ -26,6 +26,8 @@
import org.springframework.context.annotation.ClassPathScanningCandidateComponentProvider;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.type.filter.AnnotationTypeFilter;
+import org.springframework.data.aerospike.cache.AerospikeCacheKeyProcessor;
+import org.springframework.data.aerospike.cache.AerospikeCacheKeyProcessorImpl;
import org.springframework.data.aerospike.convert.AerospikeCustomConversions;
import org.springframework.data.aerospike.convert.AerospikeTypeAliasAccessor;
import org.springframework.data.aerospike.convert.MappingAerospikeConverter;
@@ -73,6 +75,11 @@ public IndexesCacheHolder indexCache() {
return new IndexesCacheHolder();
}
+ @Bean(name = "aerospikeCacheKeyProcessor")
+ public AerospikeCacheKeyProcessor cacheKeyProcessor() {
+ return new AerospikeCacheKeyProcessorImpl();
+ }
+
@Bean(name = "mappingAerospikeConverter")
public MappingAerospikeConverter mappingAerospikeConverter(AerospikeMappingContext aerospikeMappingContext,
AerospikeTypeAliasAccessor aerospikeTypeAliasAccessor,
diff --git a/src/test/java/org/springframework/data/aerospike/BaseBlockingIntegrationTests.java b/src/test/java/org/springframework/data/aerospike/BaseBlockingIntegrationTests.java
index 9b4baf29a..a89dd3923 100644
--- a/src/test/java/org/springframework/data/aerospike/BaseBlockingIntegrationTests.java
+++ b/src/test/java/org/springframework/data/aerospike/BaseBlockingIntegrationTests.java
@@ -6,6 +6,7 @@
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.core.env.Environment;
+import org.springframework.data.aerospike.cache.AerospikeCacheKeyProcessor;
import org.springframework.data.aerospike.config.BlockingTestConfig;
import org.springframework.data.aerospike.config.CommonTestConfig;
import org.springframework.data.aerospike.config.IndexedBinsAnnotationsProcessor;
@@ -63,6 +64,8 @@ public abstract class BaseBlockingIntegrationTests extends BaseIntegrationTests
protected Environment env;
@Autowired
protected MappingContext, AerospikePersistentProperty> mappingContext;
+ @Autowired
+ protected AerospikeCacheKeyProcessor cacheKeyProcessor;
protected void deleteOneByOne(Collection collection) {
collection.forEach(item -> template.delete(item));
diff --git a/src/test/java/org/springframework/data/aerospike/BaseIntegrationTests.java b/src/test/java/org/springframework/data/aerospike/BaseIntegrationTests.java
index ccb74308e..7a5aa9293 100644
--- a/src/test/java/org/springframework/data/aerospike/BaseIntegrationTests.java
+++ b/src/test/java/org/springframework/data/aerospike/BaseIntegrationTests.java
@@ -11,6 +11,9 @@ public abstract class BaseIntegrationTests {
public static final String DEFAULT_SET_NAME = "aerospike";
public static final String OVERRIDE_SET_NAME = "testSet1";
+ public static final String DIFFERENT_SET_NAME = "different-set";
+ public static final String CACHE_WITH_TTL = "CACHE-WITH-TTL";
+ public static final String DIFFERENT_EXISTING_CACHE = "DIFFERENT-EXISTING-CACHE";
protected static final int MILLIS_TO_NANO = 1_000_000;
@Value("${spring-data-aerospike.connection.namespace}")
diff --git a/src/test/java/org/springframework/data/aerospike/cache/AerospikeCacheManagerIntegrationTests.java b/src/test/java/org/springframework/data/aerospike/cache/AerospikeCacheManagerIntegrationTests.java
index 156af2ed7..b9b8f27da 100644
--- a/src/test/java/org/springframework/data/aerospike/cache/AerospikeCacheManagerIntegrationTests.java
+++ b/src/test/java/org/springframework/data/aerospike/cache/AerospikeCacheManagerIntegrationTests.java
@@ -18,24 +18,32 @@
import com.aerospike.client.IAerospikeClient;
import com.aerospike.client.Key;
import lombok.AllArgsConstructor;
-import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
+import org.springframework.cache.interceptor.SimpleKey;
import org.springframework.data.aerospike.BaseBlockingIntegrationTests;
import org.springframework.data.aerospike.core.AerospikeOperations;
import org.springframework.data.aerospike.util.AwaitilityUtils;
-import org.springframework.data.annotation.Id;
+
+import java.util.List;
+import java.util.Map;
import static org.assertj.core.api.Assertions.assertThat;
+import static org.assertj.core.api.Assertions.assertThatThrownBy;
import static org.springframework.data.aerospike.util.AwaitilityUtils.awaitTenSecondsUntil;
+@SuppressWarnings("NewObjectEquality")
public class AerospikeCacheManagerIntegrationTests extends BaseBlockingIntegrationTests {
- private static final String KEY = "foo";
- private static final String KEY_THAT_MATCHES_CONDITION = "abcdef";
+ private static final String STRING_PARAM = "foo";
+ private static final String STRING_PARAM_THAT_MATCHES_CONDITION = "abcdef";
+ private static final long NUMERIC_PARAM = 100L;
+ private static final Map MAP_PARAM =
+ Map.of("1", "val1", "2", "val2", "3", "val3", "4", "val4");
private static final String VALUE = "bar";
@Autowired
@@ -47,17 +55,138 @@ public class AerospikeCacheManagerIntegrationTests extends BaseBlockingIntegrati
@Autowired
AerospikeCacheManager aerospikeCacheManager;
- @AfterEach
- public void tearDown() {
+ @BeforeEach
+ public void setup() throws NoSuchMethodException {
cachingComponent.reset();
- client.delete(null, new Key(getNameSpace(), DEFAULT_SET_NAME, KEY));
- client.delete(null, new Key(getNameSpace(), DEFAULT_SET_NAME, KEY_THAT_MATCHES_CONDITION));
+ deleteRecords();
+ }
+
+ private void deleteRecords() throws NoSuchMethodException {
+ List