From 98f89efa9f939c1d48c1445ab4ef486236375dbb Mon Sep 17 00:00:00 2001 From: Philippe Marschall Date: Thu, 16 May 2013 23:06:02 +0200 Subject: [PATCH] SPR-10588 Add Infinispan Cache Implementation There currently is an Ehcache implementation of the cache abstraction. It would be interesting for us to have an Infinispan implementation. This would allow us to use ConcurrentMapCache for tests and the built-in JBoss AS 7 cache in production. This pull request adds an Infinispan cache implementation modeled after the Ehcache implementation. It contains the following changes * InfinispanCache, adapter from Infinispan cache interface to Spring cache interface * InfinispanCacheManager, adapter from Infinispan EmbeddedCacheManager interface to Spring CacheManager interface * InfinispanCacheManagerFactoryBean, factory bean for a InfinispanCacheManager * InfinispanCacheTests and InfinispanSupportTests, tests * optional dependencies on Infinispan * reference to the JBoss Maven repository Issue: SPR-10588 --- build.gradle | 6 + .../cache/infinispan/InfinispanCache.java | 129 ++++++++++++++++++ .../infinispan/InfinispanCacheManager.java | 123 +++++++++++++++++ .../InfinispanCacheManagerFactoryBean.java | 93 +++++++++++++ .../cache/infinispan/package-info.java | 7 + .../infinispan/InfinispanCacheTests.java | 114 ++++++++++++++++ .../infinispan/InfinispanSupportTests.java | 67 +++++++++ .../cache/infinispan/testInfinispan.xml | 23 ++++ 8 files changed, 562 insertions(+) create mode 100644 spring-context-support/src/main/java/org/springframework/cache/infinispan/InfinispanCache.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/infinispan/InfinispanCacheManager.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/infinispan/InfinispanCacheManagerFactoryBean.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/infinispan/package-info.java create mode 100644 spring-context-support/src/test/java/org/springframework/cache/infinispan/InfinispanCacheTests.java create mode 100644 spring-context-support/src/test/java/org/springframework/cache/infinispan/InfinispanSupportTests.java create mode 100644 spring-context-support/src/test/resources/org/springframework/cache/infinispan/testInfinispan.xml diff --git a/build.gradle b/build.gradle index 7002f17e8eab..c515f12abdc7 100644 --- a/build.gradle +++ b/build.gradle @@ -93,6 +93,7 @@ configure(allprojects) { project -> "http://aopalliance.sourceforge.net/doc/", "http://www.eclipse.org/aspectj/doc/released/aspectj5rt-api/", "http://ehcache.org/apidocs/", + "http://docs.jboss.org/infinispan/5.2/apidocs/", "http://quartz-scheduler.org/api/2.1.7/", "http://jackson.codehaus.org/1.9.12/javadoc/", "http://fasterxml.github.com/jackson-core/javadoc/2.2.0/", @@ -395,6 +396,10 @@ project("spring-jdbc") { project("spring-context-support") { description = "Spring Context Support" + repositories { + maven { url "https://repository.jboss.org/nexus/content/repositories/releases/" } // infinispan + } + dependencies { compile(project(":spring-core")) compile(project(":spring-beans")) @@ -404,6 +409,7 @@ project("spring-context-support") { optional("javax.mail:mail:1.4.7") optional("javax.cache:cache-api:0.6") optional("net.sf.ehcache:ehcache-core:2.6.5") + optional("org.infinispan:infinispan-core:5.2.6.Final") optional("org.quartz-scheduler:quartz:1.8.6") { exclude group: "org.slf4j", module: "slf4j-log4j12" } diff --git a/spring-context-support/src/main/java/org/springframework/cache/infinispan/InfinispanCache.java b/spring-context-support/src/main/java/org/springframework/cache/infinispan/InfinispanCache.java new file mode 100644 index 000000000000..cf38841b85e3 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/infinispan/InfinispanCache.java @@ -0,0 +1,129 @@ +/* + * Copyright 2002-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.cache.infinispan; + +import org.infinispan.lifecycle.ComponentStatus; +import org.springframework.cache.Cache; +import org.springframework.cache.support.SimpleValueWrapper; +import org.springframework.util.Assert; + +/** + * {@link org.springframework.cache.Cache} implementation on top of an + * {@link org.infinispan.Cache} instance. + * + * @author Philippe Marschall + * @since 4.0 + */ +public class InfinispanCache implements Cache { + + private static final Object NULL_HOLDER = NullHolder.INSTANE; + + @SuppressWarnings("rawtypes") + private final org.infinispan.Cache cache; + + private final boolean allowNullValues; + + + /** + * Create an {@link org.springframework.cache.infinispan.InfinispanCache} instance. + * @param infinispanCache backing Infinispan Cache instance + */ + public InfinispanCache(org.infinispan.Cache infinispanCache) { + this(infinispanCache, true); + } + + /** + * Create an {@link org.springframework.cache.infinispan.InfinispanCache} instance. + * @param infinispanCache backing Infinispan Cache instance + * @param allowNullValues whether to accept and convert null values for this cache + */ + public InfinispanCache(org.infinispan.Cache infinispanCache, boolean allowNullValues) { + Assert.notNull(infinispanCache, "Cache must not be null"); + ComponentStatus status = infinispanCache.getStatus(); + Assert.isTrue(status == ComponentStatus.RUNNING, + "A 'running' cache is required - current cache is " + status); + this.cache = infinispanCache; + this.allowNullValues = allowNullValues; + } + + @Override + public String getName() { + return this.cache.getName(); + } + + @Override + public org.infinispan.Cache getNativeCache() { + return this.cache; + } + + @Override + public ValueWrapper get(Object key) { + Object value = this.cache.get(key); + return (value != null ? new SimpleValueWrapper(fromStoreValue(value)) : null); + } + + @Override + @SuppressWarnings("unchecked") + public void put(Object key, Object value) { + this.cache.put(key, toStoreValue(value)); + } + + @Override + public void evict(Object key) { + this.cache.remove(key); + } + + @Override + public void clear() { + this.cache.clear(); + } + + + + /** + * Convert the given value from the internal store to a user value + * returned from the get method (adapting {@code null}). + * @param storeValue the store value + * @return the value to return to the user + */ + protected Object fromStoreValue(Object storeValue) { + if (this.allowNullValues && storeValue == NULL_HOLDER) { + return null; + } + return storeValue; + } + + /** + * Convert the given user value, as passed into the put method, + * to a value in the internal store (adapting {@code null}). + * @param userValue the given user value + * @return the value to store + */ + protected Object toStoreValue(Object userValue) { + if (this.allowNullValues && userValue == null) { + return NULL_HOLDER; + } + return userValue; + } + + + @SuppressWarnings("serial") + static enum NullHolder { + INSTANE; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/infinispan/InfinispanCacheManager.java b/spring-context-support/src/main/java/org/springframework/cache/infinispan/InfinispanCacheManager.java new file mode 100644 index 000000000000..2931b3fa7311 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/infinispan/InfinispanCacheManager.java @@ -0,0 +1,123 @@ +/* + * Copyright 2002-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.cache.infinispan; + +import java.util.Collection; +import java.util.LinkedHashSet; +import java.util.Set; + +import org.infinispan.lifecycle.ComponentStatus; +import org.infinispan.manager.EmbeddedCacheManager; +import org.springframework.cache.Cache; +import org.springframework.cache.transaction.AbstractTransactionSupportingCacheManager; +import org.springframework.util.Assert; + +/** + * {@link org.springframework.cache.CacheManager} implementation + * backed by a Infinispan {@link EmbeddedCacheManager}. + * + * @author Philippe Marschall + * @since 4.0 + */ +public class InfinispanCacheManager extends AbstractTransactionSupportingCacheManager { + + private EmbeddedCacheManager cacheManager; + + private boolean allowNullValues = true; + + + /** + * Create a new InfinispanCacheManager, setting the target Infinispan CacheManager + * through the {@link #setCacheManager} bean property. + */ + public InfinispanCacheManager() { + } + + /** + * Create a new InfinispanCacheManager for the given backing Infinispan. + * @param cacheManager the backing Infinispan {@link EmbeddedCacheManager} + */ + public InfinispanCacheManager(EmbeddedCacheManager cacheManager) { + this.cacheManager = cacheManager; + } + + + /** + * Set the backing Infinispan {@link EmbeddedCacheManager}. + */ + public void setCacheManager(EmbeddedCacheManager cacheManager) { + this.cacheManager = cacheManager; + } + + /** + * Return the backing Infinispan {@link EmbeddedCacheManager}. + */ + public EmbeddedCacheManager getCacheManager() { + return this.cacheManager; + } + + /** + * Specify whether to accept and convert null values for all caches + * in this cache manager. + *

Default is "true", despite Infinispan itself not supporting null values. + * An internal holder object will be used to store user-level null values. + */ + public void setAllowNullValues(boolean allowNullValues) { + this.allowNullValues = allowNullValues; + } + + /** + * Return whether this cache manager accepts and converts null values + * for all of its caches. + */ + public boolean isAllowNullValues() { + return this.allowNullValues; + } + + + @Override + protected Collection loadCaches() { + Assert.notNull(this.cacheManager, "A backing CacheManager is required"); + ComponentStatus status = this.cacheManager.getStatus(); + Assert.isTrue(ComponentStatus.RUNNING == status, + "A 'running' Infinispan CacheManager is required - current cache is " + status); + + Set cacheNames = this.cacheManager.getCacheNames(); + Collection caches = new LinkedHashSet(cacheNames.size()); + + for (String cacheName : cacheNames) { + org.infinispan.Cache infinispanCache = this.cacheManager.getCache(cacheName, true); + caches.add(new InfinispanCache(infinispanCache, this.allowNullValues)); + } + return caches; + } + + @Override + public Cache getCache(String name) { + Cache cache = super.getCache(name); + if (cache == null) { + // check the Infinispan cache again + // (in case the cache was added at runtime) + org.infinispan.Cache infinispanCache = this.cacheManager.getCache(name, false); + if (infinispanCache != null) { + cache = new InfinispanCache(infinispanCache, this.allowNullValues); + addCache(cache); + } + } + return cache; + } +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/infinispan/InfinispanCacheManagerFactoryBean.java b/spring-context-support/src/main/java/org/springframework/cache/infinispan/InfinispanCacheManagerFactoryBean.java new file mode 100644 index 000000000000..e4561745cc42 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/infinispan/InfinispanCacheManagerFactoryBean.java @@ -0,0 +1,93 @@ +/* + * Copyright 2002-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.cache.infinispan; + +import java.io.IOException; +import java.io.InputStream; + +import org.infinispan.manager.DefaultCacheManager; +import org.infinispan.manager.EmbeddedCacheManager; +import org.springframework.beans.factory.DisposableBean; +import org.springframework.beans.factory.FactoryBean; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.core.io.Resource; + +/** + * {@link FactoryBean} for a Infinispan {@link EmbeddedCacheManager}. + * + *

This class is intended to be used in conjunction with Spring XML configuration and + * Infinispan XML configuration. While it does work with Spring Java configuration in + * such cases you're likely better off instantiating the cache manager yourself and using + * + * Infinispan fluent configuration. + * + * @author Philippe Marschall + * @since 4.0 + */ +public class InfinispanCacheManagerFactoryBean + implements FactoryBean, InitializingBean, DisposableBean { + + private Resource configLocation; + + private EmbeddedCacheManager cacheManager; + + + /** + * Set the location of the Infinispan config file. A typical value is "/WEB-INF/infinispan.xml". + * @see org.infinispan.manager.DefaultCacheManager#DefaultCacheManager(java.io.InputStream) + */ + public void setConfigLocation(Resource configLocation) { + this.configLocation = configLocation; + } + + @Override + public void afterPropertiesSet() throws IOException { + if (this.configLocation != null) { + InputStream configurationStream = this.configLocation.getInputStream(); + try { + this.cacheManager = new DefaultCacheManager(configurationStream, true); + } finally { + configurationStream.close(); + } + } else { + this.cacheManager = new DefaultCacheManager(true); + } + } + + + @Override + public EmbeddedCacheManager getObject() { + return this.cacheManager; + } + + @Override + public Class getObjectType() { + return this.cacheManager != null ? this.cacheManager.getClass() : EmbeddedCacheManager.class; + } + + @Override + public boolean isSingleton() { + return true; + } + + + @Override + public void destroy() { + this.cacheManager.stop(); + } +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/infinispan/package-info.java b/spring-context-support/src/main/java/org/springframework/cache/infinispan/package-info.java new file mode 100644 index 000000000000..7ec2f08ae48f --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/infinispan/package-info.java @@ -0,0 +1,7 @@ +/** + * Support classes for the open source cache + * Infinispan, + * allowing to set up an Infinispan CacheManager and Caches + * as beans in a Spring context. + */ +package org.springframework.cache.infinispan; diff --git a/spring-context-support/src/test/java/org/springframework/cache/infinispan/InfinispanCacheTests.java b/spring-context-support/src/test/java/org/springframework/cache/infinispan/InfinispanCacheTests.java new file mode 100644 index 000000000000..ac6fd1be4ddc --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/infinispan/InfinispanCacheTests.java @@ -0,0 +1,114 @@ +/* + * Copyright 2002-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.cache.infinispan; + +import org.infinispan.manager.DefaultCacheManager; +import org.infinispan.manager.EmbeddedCacheManager; +import org.junit.After; +import org.junit.Before; +import org.junit.Test; +import org.springframework.cache.Cache; +import org.springframework.tests.Assume; +import org.springframework.tests.TestGroup; + +import static java.util.concurrent.TimeUnit.*; +import static org.junit.Assert.*; + +/** + * @author Philippe Marschall + */ +public class InfinispanCacheTests { + + protected final static String CACHE_NAME = "testCache"; + + protected org.infinispan.Cache nativeCache; + + protected EmbeddedCacheManager cacheManager; + + protected Cache cache; + + + @Before + public void setUp() throws Exception { + cacheManager = new DefaultCacheManager("org/springframework/cache/infinispan/testInfinispan.xml", true); + nativeCache = cacheManager.getCache(CACHE_NAME, true); + cache = new InfinispanCache(nativeCache); + cache.clear(); + } + + @After + public void tearDown() { + nativeCache.stop(); + cacheManager.stop(); + } + + + @Test + public void testCacheName() { + assertEquals(CACHE_NAME, cache.getName()); + } + + @Test + public void testNativeCache() { + assertSame(nativeCache, cache.getNativeCache()); + } + + @Test + public void testCachePut() { + Object key = "enescu"; + Object value = "george"; + + assertNull(cache.get(key)); + cache.put(key, value); + assertEquals(value, cache.get(key).get()); + } + + @Test + public void testCacheRemove() { + Object key = "enescu"; + Object value = "george"; + + assertNull(cache.get(key)); + cache.put(key, value); + } + + @Test + public void testCacheClear() { + assertNull(cache.get("enescu")); + cache.put("enescu", "george"); + assertNull(cache.get("vlaicu")); + cache.put("vlaicu", "aurel"); + cache.clear(); + assertNull(cache.get("vlaicu")); + assertNull(cache.get("enescu")); + } + + @Test + public void testExpiredElements() throws Exception { + Assume.group(TestGroup.LONG_RUNNING); + String key = "brancusi"; + String value = "constantin"; + // ttl = 10s + nativeCache.put(key, value, 3, SECONDS); + + assertEquals(value, cache.get(key).get()); + // wait for the entry to expire + Thread.sleep(5 * 1000); + assertNull(cache.get(key)); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/infinispan/InfinispanSupportTests.java b/spring-context-support/src/test/java/org/springframework/cache/infinispan/InfinispanSupportTests.java new file mode 100644 index 000000000000..ced6f7351170 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/infinispan/InfinispanSupportTests.java @@ -0,0 +1,67 @@ +/* + * Copyright 2002-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.cache.infinispan; + +import org.infinispan.eviction.EvictionStrategy; +import org.infinispan.manager.EmbeddedCacheManager; +import org.junit.Test; +import org.springframework.core.io.ClassPathResource; + +import static org.hamcrest.Matchers.*; +import static org.junit.Assert.*; + +/** + * @author Philippe Marschall + */ +public class InfinispanSupportTests { + + @Test + public void testLoadingBlankCacheManager() throws Exception { + InfinispanCacheManagerFactoryBean cacheManagerFb = new InfinispanCacheManagerFactoryBean(); + assertEquals(EmbeddedCacheManager.class, cacheManagerFb.getObjectType()); + assertTrue("Singleton property", cacheManagerFb.isSingleton()); + cacheManagerFb.afterPropertiesSet(); + try { + EmbeddedCacheManager cm = cacheManagerFb.getObject(); + assertThat("Loaded CacheManager with no caches", cm.getCacheNames(), empty()); + org.infinispan.Cache myCache1 = cm.getCache("myCache1", false); + assertNull("No myCache1 defined", myCache1); + } + finally { + cacheManagerFb.destroy(); + } + } + + @Test + public void testLoadingCacheManagerFromConfigFile() throws Exception { + InfinispanCacheManagerFactoryBean cacheManagerFb = new InfinispanCacheManagerFactoryBean(); + cacheManagerFb.setConfigLocation(new ClassPathResource("testInfinispan.xml", getClass())); + cacheManagerFb.afterPropertiesSet(); + try { + EmbeddedCacheManager cm = cacheManagerFb.getObject(); + assertThat("Correct number of caches loaded", cm.getCacheNames(), hasSize(1)); + org.infinispan.Cache myCache1 = cm.getCache("myCache1"); + assertNotNull("No myCache1 defined", myCache1); + assertEquals("myCache1 is not LIRS", myCache1.getCacheConfiguration().eviction().strategy(), EvictionStrategy.LIRS); + assertTrue("myCache1.maxElements == 300", myCache1.getCacheConfiguration().eviction().maxEntries() == 300); + } + finally { + cacheManagerFb.destroy(); + } + } + +} diff --git a/spring-context-support/src/test/resources/org/springframework/cache/infinispan/testInfinispan.xml b/spring-context-support/src/test/resources/org/springframework/cache/infinispan/testInfinispan.xml new file mode 100644 index 000000000000..efca56afd9f3 --- /dev/null +++ b/spring-context-support/src/test/resources/org/springframework/cache/infinispan/testInfinispan.xml @@ -0,0 +1,23 @@ + + + + + + + + + + + + + + + \ No newline at end of file