From f79bc7b14ddadd4fac101860ddded2f20d032c0f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Nicoll?= Date: Mon, 18 Sep 2023 09:09:47 +0200 Subject: [PATCH] Implement JCacheCache#putIfAbsent as atomic operation This commit modifies putIfAbsent to use an EntryProcessor that guarantees that the operation is atomic. Closes gh-21591 --- .../cache/jcache/JCacheCache.java | 22 ++++++++++++++++--- .../cache/jcache/JCacheEhCacheApiTests.java | 21 ++++++++++++++++++ 2 files changed, 40 insertions(+), 3 deletions(-) diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCache.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCache.java index 84d2e3f9bcf..2c49c9e73dc 100644 --- a/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCache.java +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/JCacheCache.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2020 the original author or authors. + * Copyright 2002-2023 the original author or authors. * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -98,8 +98,8 @@ public void put(Object key, @Nullable Object value) { @Override @Nullable public ValueWrapper putIfAbsent(Object key, @Nullable Object value) { - boolean set = this.cache.putIfAbsent(key, toStoreValue(value)); - return (set ? null : get(key)); + Object previous = this.cache.invoke(key, PutIfAbsentEntryProcessor.INSTANCE, toStoreValue(value)); + return (previous != null ? toValueWrapper(previous) : null); } @Override @@ -125,6 +125,22 @@ public boolean invalidate() { } + private static class PutIfAbsentEntryProcessor implements EntryProcessor { + + private static final PutIfAbsentEntryProcessor INSTANCE = new PutIfAbsentEntryProcessor(); + + @Override + @Nullable + public Object process(MutableEntry entry, Object... arguments) throws EntryProcessorException { + Object existingValue = entry.getValue(); + if (existingValue == null) { + entry.setValue(arguments[0]); + } + return existingValue; + } + } + + private class ValueLoaderEntryProcessor implements EntryProcessor { @SuppressWarnings("unchecked") diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheApiTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheApiTests.java index 7a802b020d1..289bb82008a 100644 --- a/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheApiTests.java +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/JCacheEhCacheApiTests.java @@ -24,9 +24,12 @@ import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; import org.springframework.context.testfixture.cache.AbstractValueAdaptingCacheTests; +import static org.assertj.core.api.Assertions.assertThat; + /** * @author Stephane Nicoll */ @@ -79,4 +82,22 @@ protected Object getNativeCache() { return this.nativeCache; } + @Test + void testPutIfAbsentNullValue() { + JCacheCache cache = getCache(true); + + String key = createRandomKey(); + String value = null; + + assertThat(cache.get(key)).isNull(); + assertThat(cache.putIfAbsent(key, value)).isNull(); + assertThat(cache.get(key).get()).isEqualTo(value); + org.springframework.cache.Cache.ValueWrapper wrapper = cache.putIfAbsent(key, "anotherValue"); + // A value is set but is 'null' + assertThat(wrapper).isNotNull(); + assertThat(wrapper.get()).isNull(); + // not changed + assertThat(cache.get(key).get()).isEqualTo(value); + } + }