diff --git a/pom.xml b/pom.xml index dfe4eb4ec..474def963 100644 --- a/pom.xml +++ b/pom.xml @@ -41,6 +41,8 @@ 1.5.6 8.0.1.Final 4.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-client true + + com.esotericsoftware + kryo + + + commons-codec + commons-codec + joda-time joda-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 params = List.of( + STRING_PARAM, + STRING_PARAM_THAT_MATCHES_CONDITION, + NUMERIC_PARAM, + MAP_PARAM, + new SimpleKey(STRING_PARAM, NUMERIC_PARAM, MAP_PARAM), + SimpleKey.EMPTY, + new SimpleKey((Object) null), + CachingComponent.class, + CachingComponent.class.getMethod("cacheableMethodWithMethodNameKey") + ); + for (Object param : params) { + AerospikeCacheKey cacheKey = cacheKeyProcessor.serializeAndHash(param); + client.delete(null, new Key(getNameSpace(), DEFAULT_SET_NAME, cacheKey.getValue())); + } + client.delete(null, new Key(getNameSpace(), DIFFERENT_SET_NAME, + cacheKeyProcessor.serializeAndHash(STRING_PARAM).getValue())); } @Test public void shouldCache() { - CachedObject response1 = cachingComponent.cacheableMethod(KEY); - CachedObject response2 = cachingComponent.cacheableMethod(KEY); + assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(0); + CachedObject response1 = cachingComponent.cacheableMethod(STRING_PARAM); + CachedObject response2 = cachingComponent.cacheableMethod(STRING_PARAM); + + assertThat(response1).isNotNull(); + assertThat(response1.getValue()).isEqualTo(VALUE); + assertThat(response2).isNotNull(); + assertThat(response2.getValue()).isEqualTo(VALUE); + assertThat(cachingComponent.getNoOfCalls()).isEqualTo(1); + } + + @Test + public void shouldCacheWithNumericParam() { + assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(0); + CachedObject response1 = cachingComponent.cacheableMethodWithNumericParam(NUMERIC_PARAM); + CachedObject response2 = cachingComponent.cacheableMethodWithNumericParam('d'); + + assertThat(response1).isNotNull(); + assertThat(response1.getValue()).isEqualTo(VALUE); + assertThat(response2).isNotNull(); + assertThat(response2.getValue()).isEqualTo(VALUE); + assertThat(cachingComponent.getNoOfCalls()).isEqualTo(1); + } + + @Test + @SuppressWarnings("StringEquality") + public void shouldCacheInstances() { + assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(0); + String stringInstance1 = new String(STRING_PARAM.toCharArray()); + String stringInstance2 = new String(STRING_PARAM.toCharArray()); + // assert that variables are referencing different objects in memory + assertThat(stringInstance1 != stringInstance2).isTrue(); + + CachedObject response1 = cachingComponent.cacheableMethod(stringInstance1); + CachedObject response2 = cachingComponent.cacheableMethod(stringInstance2); + + assertThat(response1).isNotNull(); + assertThat(response1.getValue()).isEqualTo(VALUE); + assertThat(response2).isNotNull(); + assertThat(response2.getValue()).isEqualTo(VALUE); + assertThat(cachingComponent.getNoOfCalls()).isEqualTo(1); + } + + @Test + public void shouldCacheWithMapParam() { + assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(0); + CachedObject response1 = cachingComponent.cacheableMethodWithMapParam(MAP_PARAM); + CachedObject response2 = cachingComponent.cacheableMethodWithMapParam(MAP_PARAM); + + assertThat(response1).isNotNull(); + assertThat(response1.getValue()).isEqualTo(VALUE); + assertThat(response2).isNotNull(); + assertThat(response2.getValue()).isEqualTo(VALUE); + assertThat(cachingComponent.getNoOfCalls()).isEqualTo(1); + } + + @Test + public void shouldCacheWithMultipleParams() { + assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(0); + CachedObject response1 = cachingComponent.cacheableMethodWithMultipleParams(STRING_PARAM, NUMERIC_PARAM, + MAP_PARAM); + CachedObject response2 = cachingComponent.cacheableMethodWithMultipleParams(STRING_PARAM, NUMERIC_PARAM, + MAP_PARAM); + + assertThat(response1).isNotNull(); + assertThat(response1.getValue()).isEqualTo(VALUE); + assertThat(response2).isNotNull(); + assertThat(response2.getValue()).isEqualTo(VALUE); + assertThat(cachingComponent.getNoOfCalls()).isEqualTo(1); + } + + @Test + public void shouldCacheWithNthParam() { + assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(0); + CachedObject response1 = cachingComponent.cacheableMethodWithNthParam(STRING_PARAM, NUMERIC_PARAM, + MAP_PARAM); + CachedObject response2 = cachingComponent.cacheableMethodWithNthParam(STRING_PARAM, NUMERIC_PARAM, + MAP_PARAM); + + assertThat(response1).isNotNull(); + assertThat(response1.getValue()).isEqualTo(VALUE); + assertThat(response2).isNotNull(); + assertThat(response2.getValue()).isEqualTo(VALUE); + assertThat(cachingComponent.getNoOfCalls()).isEqualTo(1); + } + + @Test + public void shouldCacheWithNoParams() { + assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(0); + CachedObject response1 = cachingComponent.cacheableMethodWithNoParams(); + CachedObject response2 = cachingComponent.cacheableMethodWithNoParams(); + + assertThat(response1).isNotNull(); + assertThat(response1.getValue()).isEqualTo(VALUE); + assertThat(response2).isNotNull(); + assertThat(response2.getValue()).isEqualTo(VALUE); + assertThat(cachingComponent.getNoOfCalls()).isEqualTo(1); + } + + @Test + public void shouldCacheWithNullParam() { + assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(0); + CachedObject response1 = cachingComponent.cacheableMethod(null); + CachedObject response2 = cachingComponent.cacheableMethod(null); assertThat(response1).isNotNull(); assertThat(response1.getValue()).isEqualTo(VALUE); @@ -66,19 +195,73 @@ public void shouldCache() { assertThat(cachingComponent.getNoOfCalls()).isEqualTo(1); } + @Test + public void shouldNotCacheDifferentMethodsWithNoParamsByDefault_NegativeTest() { + assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(0); + CachedObject response1 = cachingComponent.cacheableMethodWithNoParams(); + assertThat(cachingComponent.getNoOfCalls()).isEqualTo(1); + + // any two methods with no arguments will have the same cache key by default, + // to change it set the necessary Cacheable annotation parameters (e.g. "key") + assertThatThrownBy(() -> cachingComponent.anotherMethodWithNoParams()) + .isInstanceOf(ClassCastException.class) + .hasMessageMatching(".+CachedObject cannot be cast to class .+AnotherCachedObject.*"); + } + + @Test + public void shouldCacheWithTargetClassKey() { + assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(0); + CachedObject response1 = cachingComponent.cacheableMethodWithTargetClassKey(); + CachedObject response2 = cachingComponent.cacheableMethodWithTargetClassKey(); + + assertThat(response1).isNotNull(); + assertThat(response1.getValue()).isEqualTo(VALUE); + assertThat(response2).isNotNull(); + assertThat(response2.getValue()).isEqualTo(VALUE); + assertThat(cachingComponent.getNoOfCalls()).isEqualTo(1); + } + + @Test + public void shouldCacheWithMethodNameKey() { + assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(0); + CachedObject response1 = cachingComponent.cacheableMethodWithMethodNameKey(); + CachedObject response2 = cachingComponent.cacheableMethodWithMethodNameKey(); + + assertThat(response1).isNotNull(); + assertThat(response1.getValue()).isEqualTo(VALUE); + assertThat(response2).isNotNull(); + assertThat(response2.getValue()).isEqualTo(VALUE); + assertThat(cachingComponent.getNoOfCalls()).isEqualTo(1); + } + + @Test + public void shouldCacheUsingDefaultSet() { + assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(0); + // default cache configuration is used for all cache names not pre-configured via AerospikeCacheManager + CachedObject response1 = cachingComponent.cacheableMethodDefaultCache(STRING_PARAM); + CachedObject response2 = cachingComponent.cacheableMethod(STRING_PARAM); + + assertThat(response1).isNotNull(); + assertThat(response1.getValue()).isEqualTo(VALUE); + assertThat(response2).isNotNull(); + assertThat(response2.getValue()).isEqualTo(VALUE); + assertThat(cachingComponent.getNoOfCalls()).isEqualTo(1); + assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(1); + } + @Test public void testCacheableMethodSync() throws InterruptedException { assertThat(cachingComponent.getNoOfCalls() == 0).isTrue(); // Creating two threads that will call cacheableMethod concurrently Thread thread1 = new Thread(() -> { - CachedObject response = cachingComponent.cacheableMethodSynchronized(KEY); + CachedObject response = cachingComponent.cacheableMethodSynchronized(STRING_PARAM); assertThat(response).isNotNull(); assertThat(response.getValue()).isEqualTo(VALUE); }); Thread thread2 = new Thread(() -> { - CachedObject response = cachingComponent.cacheableMethodSynchronized(KEY); + CachedObject response = cachingComponent.cacheableMethodSynchronized(STRING_PARAM); assertThat(response).isNotNull(); assertThat(response.getValue()).isEqualTo(VALUE); }); @@ -97,9 +280,10 @@ public void testCacheableMethodSync() throws InterruptedException { @Test public void shouldEvictCache() { - CachedObject response1 = cachingComponent.cacheableMethod(KEY); - cachingComponent.cacheEvictMethod(KEY); - CachedObject response2 = cachingComponent.cacheableMethod(KEY); + assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(0); + CachedObject response1 = cachingComponent.cacheableMethod(STRING_PARAM); + cachingComponent.cacheEvictMethod(STRING_PARAM); + CachedObject response2 = cachingComponent.cacheableMethod(STRING_PARAM); assertThat(response1).isNotNull(); assertThat(response1.getValue()).isEqualTo(VALUE); @@ -109,10 +293,11 @@ public void shouldEvictCache() { } @Test - public void shouldNotEvictCacheEvictingDifferentKey() { - CachedObject response1 = cachingComponent.cacheableMethod(KEY); - cachingComponent.cacheEvictMethod("not-the-relevant-key"); - CachedObject response2 = cachingComponent.cacheableMethod(KEY); + public void shouldNotEvictCacheEvictingDifferentParam() { + assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(0); + CachedObject response1 = cachingComponent.cacheableMethod(STRING_PARAM); + cachingComponent.cacheEvictMethod("not-the-relevant-param"); + CachedObject response2 = cachingComponent.cacheableMethod(STRING_PARAM); assertThat(response1).isNotNull(); assertThat(response1.getValue()).isEqualTo(VALUE); @@ -123,20 +308,27 @@ public void shouldNotEvictCacheEvictingDifferentKey() { @Test public void shouldCacheUsingCachePut() { - CachedObject response1 = cachingComponent.cachePutMethod(KEY); - CachedObject response2 = cachingComponent.cacheableMethod(KEY); + assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(0); + CachedObject response1 = cachingComponent.cachePutMethod(STRING_PARAM); + CachedObject response2 = cachingComponent.cacheableMethod(STRING_PARAM); assertThat(response1).isNotNull(); assertThat(response1.getValue()).isEqualTo(VALUE); assertThat(response2).isNotNull(); assertThat(response2.getValue()).isEqualTo(VALUE); assertThat(cachingComponent.getNoOfCalls()).isEqualTo(1); + + CachedObject response3 = cachingComponent.cachePutMethod(STRING_PARAM); + assertThat(response3).isNotNull(); + assertThat(response3.getValue()).isEqualTo(VALUE); + assertThat(cachingComponent.getNoOfCalls()).isEqualTo(2); } @Test public void shouldCacheKeyMatchesCondition() { - CachedObject response1 = cachingComponent.cacheableWithCondition(KEY_THAT_MATCHES_CONDITION); - CachedObject response2 = cachingComponent.cacheableWithCondition(KEY_THAT_MATCHES_CONDITION); + assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(0); + CachedObject response1 = cachingComponent.cacheableWithCondition(STRING_PARAM_THAT_MATCHES_CONDITION); + CachedObject response2 = cachingComponent.cacheableWithCondition(STRING_PARAM_THAT_MATCHES_CONDITION); assertThat(response1).isNotNull(); assertThat(response1.getValue()).isEqualTo(VALUE); @@ -147,8 +339,9 @@ public void shouldCacheKeyMatchesCondition() { @Test public void shouldNotCacheKeyDoesNotMatchCondition() { - CachedObject response1 = cachingComponent.cacheableWithCondition(KEY); - CachedObject response2 = cachingComponent.cacheableWithCondition(KEY); + assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(0); + CachedObject response1 = cachingComponent.cacheableWithCondition(STRING_PARAM); + CachedObject response2 = cachingComponent.cacheableWithCondition(STRING_PARAM); assertThat(response1).isNotNull(); assertThat(response1.getValue()).isEqualTo(VALUE); @@ -159,8 +352,9 @@ public void shouldNotCacheKeyDoesNotMatchCondition() { @Test public void shouldCacheWithConfiguredTTL() { - CachedObject response1 = cachingComponent.cacheableMethodWithTTL(KEY); - CachedObject response2 = cachingComponent.cacheableMethodWithTTL(KEY); + assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(0); + CachedObject response1 = cachingComponent.cacheableMethodWithTTL(STRING_PARAM); + CachedObject response2 = cachingComponent.cacheableMethodWithTTL(STRING_PARAM); assertThat(cachingComponent.getNoOfCalls()).isEqualTo(1); assertThat(response1).isNotNull(); @@ -169,7 +363,7 @@ public void shouldCacheWithConfiguredTTL() { assertThat(response2.getValue()).isEqualTo(VALUE); awaitTenSecondsUntil(() -> { - CachedObject response3 = cachingComponent.cacheableMethodWithTTL(KEY); + CachedObject response3 = cachingComponent.cacheableMethodWithTTL(STRING_PARAM); assertThat(cachingComponent.getNoOfCalls()).isEqualTo(2); assertThat(response3).isNotNull(); assertThat(response3.getValue()).isEqualTo(VALUE); @@ -178,8 +372,9 @@ public void shouldCacheWithConfiguredTTL() { @Test public void shouldCacheUsingAnotherCacheManager() { - CachedObject response1 = cachingComponent.cacheableMethodWithAnotherCacheManager(KEY); - CachedObject response2 = cachingComponent.cacheableMethodWithAnotherCacheManager(KEY); + assertThat(aerospikeOperations.count(DIFFERENT_SET_NAME)).isEqualTo(0); + CachedObject response1 = cachingComponent.cacheableMethodWithAnotherCacheManager(STRING_PARAM); + CachedObject response2 = cachingComponent.cacheableMethodWithAnotherCacheManager(STRING_PARAM); assertThat(response1).isNotNull(); assertThat(response1.getValue()).isEqualTo(VALUE); @@ -190,9 +385,11 @@ public void shouldCacheUsingAnotherCacheManager() { @Test public void shouldNotClearCacheClearingDifferentCache() { - CachedObject response1 = cachingComponent.cacheableMethod(KEY); + assertThat(aerospikeOperations.count(DIFFERENT_SET_NAME)).isEqualTo(0); + assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(0); + CachedObject response1 = cachingComponent.cacheableMethod(STRING_PARAM); assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(1); - aerospikeCacheManager.getCache("DIFFERENT-EXISTING-CACHE").clear(); + aerospikeCacheManager.getCache(DIFFERENT_EXISTING_CACHE).clear(); AwaitilityUtils.awaitTwoSecondsUntil(() -> { assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(1); assertThat(response1).isNotNull(); @@ -200,6 +397,32 @@ public void shouldNotClearCacheClearingDifferentCache() { }); } + @Test + public void shouldCacheUsingDifferentSet() { + assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(0); + assertThat(aerospikeOperations.count(DIFFERENT_SET_NAME)).isEqualTo(0); + + CachedObject response1 = cachingComponent.cacheableMethodDifferentExistingCache(STRING_PARAM); + CachedObject response2 = cachingComponent.cacheableMethodDifferentExistingCache(STRING_PARAM); + assertThat(response1).isNotNull(); + assertThat(response1.getValue()).isEqualTo(VALUE); + assertThat(response2).isNotNull(); + assertThat(response2.getValue()).isEqualTo(VALUE); + AwaitilityUtils.awaitTwoSecondsUntil(() -> { + assertThat(aerospikeOperations.count(DIFFERENT_SET_NAME)).isEqualTo(1); + assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(0); + assertThat(cachingComponent.getNoOfCalls()).isEqualTo(1); + }); + + + CachedObject response3 = cachingComponent.cacheableMethod(STRING_PARAM); + assertThat(response3).isNotNull(); + assertThat(response3.getValue()).isEqualTo(VALUE); + assertThat(cachingComponent.getNoOfCalls()).isEqualTo(2); + assertThat(aerospikeOperations.count(DIFFERENT_SET_NAME)).isEqualTo(1); + assertThat(aerospikeOperations.count(DEFAULT_SET_NAME)).isEqualTo(1); + } + public static class CachingComponent { private int noOfCalls = 0; @@ -208,28 +431,88 @@ public void reset() { noOfCalls = 0; } - @Cacheable("TEST") + @Cacheable("TEST") // "TEST" is a cache name not pre-configured in AerospikeCacheManager, so goes to default set public CachedObject cacheableMethod(String param) { noOfCalls++; - return new CachedObject("id", VALUE); + return new CachedObject(VALUE); + } + + @Cacheable("TEST") + public CachedObject cacheableMethodWithNumericParam(long param) { + noOfCalls++; + return new CachedObject(VALUE); + } + + @Cacheable("TEST") + public CachedObject cacheableMethodWithMapParam(Map param) { + noOfCalls++; + return new CachedObject(VALUE); + } + + @Cacheable("TEST") + public CachedObject cacheableMethodWithMultipleParams(String param1, long param2, Map param3) { + noOfCalls++; + return new CachedObject(VALUE); + } + + @Cacheable(value = "TEST", key = "#root.args[1]") + public CachedObject cacheableMethodWithNthParam(String param1, long param2, Map param3) { + noOfCalls++; + return new CachedObject(VALUE); + } + + @Cacheable("TEST") + public CachedObject cacheableMethodWithNoParams() { + noOfCalls++; + return new CachedObject(VALUE); + } + + @Cacheable("TEST") + public AnotherCachedObject anotherMethodWithNoParams() { + noOfCalls++; + return new AnotherCachedObject(NUMERIC_PARAM); + } + + @Cacheable(value = "TEST", key = "#root.targetClass") + public CachedObject cacheableMethodWithTargetClassKey() { + noOfCalls++; + return new CachedObject(VALUE); + } + + @Cacheable(value = "TEST", key = "#root.method") + public CachedObject cacheableMethodWithMethodNameKey() { + noOfCalls++; + return new CachedObject(VALUE); } @Cacheable(value = "TEST", sync = true) public CachedObject cacheableMethodSynchronized(String param) { noOfCalls++; - return new CachedObject("id", VALUE); + return new CachedObject(VALUE); + } + + @Cacheable("TEST12345ABC") // Cache name not pre-configured in AerospikeCacheManager, so it goes to default set + public CachedObject cacheableMethodDefaultCache(String param) { + noOfCalls++; + return new CachedObject(VALUE); } - @Cacheable(value = "CACHE-WITH-TTL") + @Cacheable(DIFFERENT_EXISTING_CACHE) + public CachedObject cacheableMethodDifferentExistingCache(String param) { + noOfCalls++; + return new CachedObject(VALUE); + } + + @Cacheable(value = CACHE_WITH_TTL) public CachedObject cacheableMethodWithTTL(String param) { noOfCalls++; - return new CachedObject("id", VALUE); + return new CachedObject(VALUE); } @Cacheable(value = "TEST", cacheManager = "anotherCacheManager") public CachedObject cacheableMethodWithAnotherCacheManager(String param) { noOfCalls++; - return new CachedObject("id", VALUE); + return new CachedObject(VALUE); } @CacheEvict("TEST") @@ -239,13 +522,13 @@ public void cacheEvictMethod(String param) { @CachePut("TEST") public CachedObject cachePutMethod(String param) { noOfCalls++; - return new CachedObject("id", VALUE); + return new CachedObject(VALUE); } @Cacheable(value = "TEST", condition = "#param.startsWith('abc')") public CachedObject cacheableWithCondition(String param) { noOfCalls++; - return new CachedObject("id", VALUE); + return new CachedObject(VALUE); } public int getNoOfCalls() { @@ -256,12 +539,20 @@ public int getNoOfCalls() { @AllArgsConstructor public static class CachedObject { - @Id - private final String id; - private final String value; + private final Object value; - public String getValue() { + public Object getValue() { return value; } } + + @AllArgsConstructor + public static class AnotherCachedObject { + + private final long number; + + public long getNumber() { + return number; + } + } } diff --git a/src/test/java/org/springframework/data/aerospike/cache/AerospikeCacheManagerTests.java b/src/test/java/org/springframework/data/aerospike/cache/AerospikeCacheManagerTests.java index 9181731bd..dd64d0222 100644 --- a/src/test/java/org/springframework/data/aerospike/cache/AerospikeCacheManagerTests.java +++ b/src/test/java/org/springframework/data/aerospike/cache/AerospikeCacheManagerTests.java @@ -42,7 +42,8 @@ public class AerospikeCacheManagerTests extends BaseBlockingIntegrationTests { public void missingCache() { AerospikeCacheConfiguration aerospikeCacheConfiguration = new AerospikeCacheConfiguration(namespace, DEFAULT_SET_NAME); - AerospikeCacheManager manager = new AerospikeCacheManager(client, converter, aerospikeCacheConfiguration); + AerospikeCacheManager manager = new AerospikeCacheManager(client, converter, aerospikeCacheConfiguration, + cacheKeyProcessor); manager.afterPropertiesSet(); Cache cache = manager.getCache("missing-cache"); @@ -56,7 +57,7 @@ public void defaultCache() { Map aerospikeCacheConfigurationMap = new HashMap<>(); aerospikeCacheConfigurationMap.put("default-cache", aerospikeCacheConfiguration); AerospikeCacheManager manager = new AerospikeCacheManager(client, converter, aerospikeCacheConfiguration, - aerospikeCacheConfigurationMap); + aerospikeCacheConfigurationMap, cacheKeyProcessor); manager.afterPropertiesSet(); Cache cache = manager.getCache("default-cache"); @@ -70,7 +71,7 @@ public void defaultCacheWithCustomizedSet() { "-set"); aerospikeCacheConfigurationMap.put("default-cache", aerospikeCacheConfiguration); AerospikeCacheManager manager = new AerospikeCacheManager(client, converter, aerospikeCacheConfiguration, - aerospikeCacheConfigurationMap); + aerospikeCacheConfigurationMap, cacheKeyProcessor); manager.afterPropertiesSet(); Cache cache = manager.getCache("default-cache"); @@ -81,7 +82,8 @@ public void defaultCacheWithCustomizedSet() { public void transactionAwareCache() { AerospikeCacheConfiguration aerospikeCacheConfiguration = new AerospikeCacheConfiguration(namespace, DEFAULT_SET_NAME); - AerospikeCacheManager manager = new AerospikeCacheManager(client, converter, aerospikeCacheConfiguration); + AerospikeCacheManager manager = new AerospikeCacheManager(client, converter, aerospikeCacheConfiguration, + cacheKeyProcessor); manager.setTransactionAware(true); manager.afterPropertiesSet(); Cache cache = manager.getCache("transaction-aware-cache"); diff --git a/src/test/java/org/springframework/data/aerospike/config/CommonTestConfig.java b/src/test/java/org/springframework/data/aerospike/config/CommonTestConfig.java index 615439412..80a636c9c 100644 --- a/src/test/java/org/springframework/data/aerospike/config/CommonTestConfig.java +++ b/src/test/java/org/springframework/data/aerospike/config/CommonTestConfig.java @@ -26,6 +26,7 @@ import org.springframework.context.annotation.Primary; import org.springframework.data.aerospike.BaseIntegrationTests; import org.springframework.data.aerospike.cache.AerospikeCacheConfiguration; +import org.springframework.data.aerospike.cache.AerospikeCacheKeyProcessor; import org.springframework.data.aerospike.cache.AerospikeCacheManager; import org.springframework.data.aerospike.cache.AerospikeCacheManagerIntegrationTests.CachingComponent; import org.springframework.data.aerospike.convert.MappingAerospikeConverter; @@ -47,26 +48,29 @@ public class CommonTestConfig { @Bean @Primary - public CacheManager cacheManager(IAerospikeClient aerospikeClient, MappingAerospikeConverter aerospikeConverter) { + public CacheManager cacheManager(IAerospikeClient aerospikeClient, MappingAerospikeConverter aerospikeConverter, + AerospikeCacheKeyProcessor cacheKeyProcessor) { AerospikeCacheConfiguration defaultCacheConfiguration = new AerospikeCacheConfiguration(namespace, BaseIntegrationTests.DEFAULT_SET_NAME); - AerospikeCacheConfiguration aerospikeCacheConfiguration = new AerospikeCacheConfiguration(namespace, - "different-set"); AerospikeCacheConfiguration configurationWithTTL = new AerospikeCacheConfiguration(namespace, BaseIntegrationTests.DEFAULT_SET_NAME, 2); + AerospikeCacheConfiguration differentCacheConfiguration = new AerospikeCacheConfiguration(namespace, + BaseIntegrationTests.DIFFERENT_SET_NAME); Map aerospikeCacheConfigurationMap = new HashMap<>(); - aerospikeCacheConfigurationMap.put("DIFFERENT-EXISTING-CACHE", aerospikeCacheConfiguration); - aerospikeCacheConfigurationMap.put("CACHE-WITH-TTL", configurationWithTTL); + aerospikeCacheConfigurationMap.put(BaseIntegrationTests.CACHE_WITH_TTL, configurationWithTTL); + aerospikeCacheConfigurationMap.put(BaseIntegrationTests.DIFFERENT_EXISTING_CACHE, differentCacheConfiguration); return new AerospikeCacheManager(aerospikeClient, aerospikeConverter, defaultCacheConfiguration, - aerospikeCacheConfigurationMap); + aerospikeCacheConfigurationMap, cacheKeyProcessor); } @Bean public CacheManager anotherCacheManager(IAerospikeClient aerospikeClient, - MappingAerospikeConverter aerospikeConverter) { + MappingAerospikeConverter aerospikeConverter, + AerospikeCacheKeyProcessor cacheKeyProcessor) { AerospikeCacheConfiguration defaultCacheConfiguration = new AerospikeCacheConfiguration(namespace, - BaseIntegrationTests.DEFAULT_SET_NAME); - return new AerospikeCacheManager(aerospikeClient, aerospikeConverter, defaultCacheConfiguration); + BaseIntegrationTests.DIFFERENT_SET_NAME); + return new AerospikeCacheManager(aerospikeClient, aerospikeConverter, defaultCacheConfiguration, + cacheKeyProcessor); } @Bean