SGF-327 - Avoid setting null values with GemFire's Cache Region put(key, value) operation when GemFire is used as the caching provider in Spring's Cache Abstraction (@Cacheable).

This commit is contained in:
John Blum
2014-09-18 16:18:42 -07:00
parent 697afdbd64
commit acdd0c85bf
4 changed files with 240 additions and 7 deletions

View File

@@ -71,11 +71,11 @@ public class GemfireCache implements Cache {
@SuppressWarnings("unchecked")
public <T> T get(final Object key, final Class<T> type) {
Object value = region.get(key);
if (value != null && type != null && !type.isInstance(value)) {
throw new IllegalStateException("Cached value is not of required type [" + type.getName() + "]: " + value);
throw new IllegalStateException(String.format("Cached value is not of required type [%1$s]: %2$s",
type.getName(), value));
}
return (T) value;
@@ -83,7 +83,9 @@ public class GemfireCache implements Cache {
@SuppressWarnings("unchecked")
public void put(final Object key, final Object value) {
region.put(key, value);
if (value != null) {
region.put(key, value);
}
}
/**
@@ -94,8 +96,9 @@ public class GemfireCache implements Cache {
*/
@SuppressWarnings("unchecked")
public ValueWrapper putIfAbsent(Object key, Object value) {
Object existingValue = region.putIfAbsent(key, value);
return existingValue == null ? null : new SimpleValueWrapper(existingValue);
return (existingValue == null ? null : new SimpleValueWrapper(existingValue));
}
}

View File

@@ -84,8 +84,7 @@ public class GemfireCacheManager extends AbstractCacheManager {
Cache cache = super.getCache(name);
if (cache == null) {
// check the gemfire cache again
// in case the cache was added at runtime
// check the GemFire Cache again in case the Cache (Region) was added at runtime
Region<?, ?> region = gemfireCache.getRegion(name);
if (region != null) {

View File

@@ -0,0 +1,161 @@
/*
* Copyright 2010-2013 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.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.data.gemfire.support;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.assertTrue;
import java.util.Map;
import javax.annotation.PostConstruct;
import javax.annotation.Resource;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.test.context.ActiveProfiles;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.util.Assert;
import com.gemstone.gemfire.cache.Region;
/**
* The CachingWithGemFireIntegrationTest class is a test suite of test cases testing the contract and functionality
* of Spring Framework's Cache Abstraction using GemFire as a caching provider applied with Spring Data GemFire.
*
* @author John Blum
* @see org.junit.Test
* @see org.junit.runner.RunWith
* @see org.springframework.cache.annotation.Cacheable
* @see org.springframework.test.context.ActiveProfiles
* @see org.springframework.test.context.ContextConfiguration
* @see org.springframework.test.context.junit4.SpringJUnit4ClassRunner
* @since 1.5.1
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
@ActiveProfiles("replica")
@SuppressWarnings("unused")
public class CachingWithGemFireIntegrationTest {
@Autowired
private NamedNumbersService namedNumbersService;
@Resource(name = "NamedNumbersRegion")
private Region<String, Integer> namedNumbersRegion;
@Test(expected = NullPointerException.class)
public void testRegionCacheHit() {
assertNull(namedNumbersRegion.get("eleven"));
assertFalse(namedNumbersRegion.containsKey("eleven"));
namedNumbersRegion.put("eleven", 11);
assertTrue(namedNumbersRegion.containsKey("eleven"));
assertEquals(11, namedNumbersService.get("eleven").intValue());
assertFalse(namedNumbersService.wasCacheMiss());
try {
namedNumbersRegion.put("eleven", null); // GemFire does not accept null values on put(key, value)
}
finally {
assertTrue(namedNumbersRegion.containsKey("eleven"));
assertEquals(11, namedNumbersRegion.get("eleven").intValue());
assertEquals(11, namedNumbersService.get("eleven").intValue());
assertFalse(namedNumbersService.wasCacheMiss());
}
}
@Test
public void testRegionCaching() {
assertFalse(namedNumbersService.wasCacheMiss());
assertEquals(1, namedNumbersService.get("one").intValue());
assertTrue(namedNumbersService.wasCacheMiss());
assertEquals(1, namedNumbersService.get("one").intValue());
assertFalse(namedNumbersService.wasCacheMiss());
assertEquals(2, namedNumbersService.get("two").intValue());
assertTrue(namedNumbersService.wasCacheMiss());
assertEquals(2, namedNumbersService.get("two").intValue());
assertFalse(namedNumbersService.wasCacheMiss());
assertNull(namedNumbersService.get("twelve"));
assertTrue(namedNumbersService.wasCacheMiss());
assertNull(namedNumbersService.get("twelve"));
assertTrue(namedNumbersService.wasCacheMiss());
}
public static class NamedNumbersService {
private NamedNumbersInMemoryRepository namedNumbersRepo;
public final void setNamedNumbersRepo(final NamedNumbersInMemoryRepository namedNumbersRepo) {
Assert.notNull(namedNumbersRepo, "The 'NamedNumbers' Repository must not be null!");
this.namedNumbersRepo = namedNumbersRepo;
}
protected NamedNumbersInMemoryRepository getNamedNumbersRepo() {
Assert.state(namedNumbersRepo != null,
"A reference to the 'NamedNumbers' Repository was not properly configured and initialized!");
return namedNumbersRepo;
}
@Cacheable("NamedNumbersRegion")
public Integer get(final String namedNumber) {
return getNamedNumbersRepo().get(namedNumber);
}
public boolean wasCacheMiss() {
return getNamedNumbersRepo().wasCacheMiss();
}
}
public static class NamedNumbersInMemoryRepository {
private volatile boolean cacheMiss;
private Map<String, Integer> namedNumbers;
@PostConstruct
public void init() {
getNamedNumbers();
}
public final void setNamedNumbers(final Map<String, Integer> namedNumbers) {
Assert.notNull(namedNumbers, "The reference to the 'NamedNumbers' Map must not be null!");
this.namedNumbers = namedNumbers;
}
protected Map<String, Integer> getNamedNumbers() {
Assert.state(namedNumbers != null, "The 'NamedNumbers' Map was not properly configured and initialized!");
return namedNumbers;
}
public Integer get(final String namedNumber) {
this.cacheMiss = true;
return namedNumbers.get(namedNumber);
}
public boolean wasCacheMiss() {
boolean localCacheMiss = this.cacheMiss;
this.cacheMiss = false;
return localCacheMiss;
}
}
}

View File

@@ -0,0 +1,70 @@
<?xml version="1.0" encoding="utf-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:cache="http://www.springframework.org/schema/cache"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:gfe="http://www.springframework.org/schema/gemfire"
xmlns:p="http://www.springframework.org/schema/p"
xmlns:util="http://www.springframework.org/schema/util"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="
http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/gemfire http://www.springframework.org/schema/gemfire/spring-gemfire.xsd
http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
">
<util:properties id="gemfireProperties">
<prop key="name">SpringGemFireCachingIntegrationTest</prop>
<prop key="mcast-port">0</prop>
<prop key="log-level">warning</prop>
</util:properties>
<gfe:cache properties-ref="gemfireProperties"/>
<gfe:region-template id="Template" initial-capacity="51" load-factor="0.75"/>
<cache:annotation-driven/>
<bean id="cacheManager" class="org.springframework.data.gemfire.support.GemfireCacheManager" p:cache-ref="gemfireCache"/>
<context:annotation-config/>
<util:map id="NamedNumbersMap" map-class="java.util.concurrent.ConcurrentHashMap"
key-type="java.lang.String" value-type="java.lang.Integer">
<entry key="zero" value="0"/>
<entry key="one" value="1"/>
<entry key="two" value="2"/>
<entry key="three" value="3"/>
<entry key="four" value="4"/>
<entry key="five" value="5"/>
<entry key="size" value="6"/>
<entry key="seven" value="7"/>
<entry key="eight" value="8"/>
<entry key="nine" value="9"/>
</util:map>
<bean id="namedNumbersRepo" class="org.springframework.data.gemfire.support.CachingWithGemFireIntegrationTest$NamedNumbersInMemoryRepository">
<property name="namedNumbers" ref="NamedNumbersMap"/>
</bean>
<bean id="namedNumbersService" class="org.springframework.data.gemfire.support.CachingWithGemFireIntegrationTest$NamedNumbersService">
<property name="namedNumbersRepo" ref="namedNumbersRepo"/>
</bean>
<beans profile="replica">
<gfe:replicated-region id="NamedNumbersRegion" persistent="false" key-constraint="java.lang.String"
value-constraint="java.lang.Integer" template="Template"/>
</beans>
<beans profile="partition">
<gfe:partitioned-region id="NamedNumbersRegion" persistent="false" key-constraint="java.lang.String"
value-constraint="java.lang.Integer" template="Template"/>
</beans>
<beans profile="local">
<gfe:partitioned-region id="NamedNumbersRegion" persistent="false" key-constraint="java.lang.String"
value-constraint="java.lang.Integer" template="Template"/>
</beans>
</beans>