Add JSR-107 cache annotations support

This commit adds support for the JSR-107 cache annotations alongside
the Spring's cache annotations, that is @CacheResult, @CachePut,
@CacheRemove and @CacheRemoveAll as well as related annotations
@CacheDefaults, @CacheKey and @CacheValue.

Spring's caching configuration infrastructure detects the presence of
the JSR-107 API and Spring's JCache implementation. Both
@EnableCaching and the cache namespace are able to configure the
required JCache infrastructure when necessary. Both proxy mode
and AspectJ mode are supported.

As JSR-107 permits the customization of the CacheResolver to use for
both regular and exception caches, JCacheConfigurer has been
introduced as an extension of CachingConfigurer and permits to define
those.

If an exception is cached and should be rethrown, it is cloned and
the call stack is rewritten so that it matches the calling thread each
time. If the exception cannot be cloned, the original exception is
returned.

Internally, the interceptors uses Spring's caching abstraction by default
with an adapter layer when a JSR-107 component needs to be called.
This is the case for CacheResolver and CacheKeyGenerator.

The implementation uses Spring's CacheManager abstraction behind the
scene. The standard annotations can therefore be used against any
CacheManager implementation.

Issue: SPR-9616
This commit is contained in:
Stephane Nicoll
2014-02-21 12:14:32 +01:00
parent 4cd075bb96
commit 47a4327193
91 changed files with 6408 additions and 110 deletions

View File

@@ -71,4 +71,5 @@ public class AspectJAnnotationTests extends AbstractAnnotationTests {
assertSame(r3, primary.get(o1).get());
assertSame(r4, secondary.get(o1).get());
}
}

View File

@@ -0,0 +1,72 @@
/*
* Copyright 2002-2014 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.aspectj;
import java.util.Arrays;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.concurrent.ConcurrentMapCache;
import org.springframework.cache.config.AnnotatedJCacheableService;
import org.springframework.cache.jcache.config.AbstractJCacheAnnotationTests;
import org.springframework.cache.jcache.config.JCacheableService;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.AdviceMode;
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
*
* @author Stephane Nicoll
*/
public class AspectJJcacheJavaConfigTests extends AbstractJCacheAnnotationTests {
@Override
protected ApplicationContext getApplicationContext() {
return new AnnotationConfigApplicationContext(EnableCachingConfig.class);
}
@Configuration
@EnableCaching(mode = AdviceMode.ASPECTJ)
public static class EnableCachingConfig {
@Bean
public CacheManager cacheManager() {
SimpleCacheManager cm = new SimpleCacheManager();
cm.setCaches(Arrays.asList(
defaultCache(),
new ConcurrentMapCache("primary"),
new ConcurrentMapCache("secondary"),
new ConcurrentMapCache("exception")));
return cm;
}
@Bean
public AnnotatedJCacheableService cacheableService() {
return new AnnotatedJCacheableService(defaultCache());
}
@Bean
public Cache defaultCache() {
return new ConcurrentMapCache("default");
}
}
}

View File

@@ -0,0 +1,35 @@
/*
* Copyright 2002-2014 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.jcache.config.aspectj;
import org.springframework.cache.jcache.config.AbstractJCacheAnnotationTests;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;
/**
*
* @author Stephane Nicoll
*/
public class AspectJJcacheNamespaceConfigTests extends AbstractJCacheAnnotationTests {
@Override
protected ApplicationContext getApplicationContext() {
return new GenericXmlApplicationContext(
"/org/springframework/cache/config/annotation-jcache-aspectj.xml");
}
}

View File

@@ -200,4 +200,5 @@ public class AnnotatedClassCacheableService implements CacheableService<Object>
arg1.setId(Long.MIN_VALUE);
return arg1;
}
}

View File

@@ -0,0 +1,201 @@
/*
* Copyright 2002-2014 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.config;
import javax.cache.annotation.CacheDefaults;
import javax.cache.annotation.CacheKey;
import javax.cache.annotation.CachePut;
import javax.cache.annotation.CacheRemove;
import javax.cache.annotation.CacheRemoveAll;
import javax.cache.annotation.CacheResult;
import javax.cache.annotation.CacheValue;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
import org.springframework.cache.Cache;
import org.springframework.cache.jcache.config.JCacheableService;
import org.springframework.cache.jcache.interceptor.SimpleGeneratedCacheKey;
import org.springframework.cache.jcache.support.TestableCacheKeyGenerator;
import org.springframework.cache.jcache.support.TestableCacheResolverFactory;
/**
* Repository sample with a @CacheDefaults annotation
*
* <p>Note: copy/pasted from its original compilation because it needs to be
* processed by the AspectJ compiler to wave the required aspects.
*
* @author Stephane Nicoll
*/
@CacheDefaults(cacheName = "default")
public class AnnotatedJCacheableService implements JCacheableService<Long> {
private final AtomicLong counter = new AtomicLong();
private final AtomicLong exceptionCounter = new AtomicLong();
private final Cache defaultCache;
public AnnotatedJCacheableService(Cache defaultCache) {
this.defaultCache = defaultCache;
}
@Override
@CacheResult
public Long cache(String id) {
return counter.getAndIncrement();
}
@Override
@CacheResult(exceptionCacheName = "exception", nonCachedExceptions = NullPointerException.class)
public Long cacheWithException(@CacheKey String id, boolean matchFilter) {
throwException(matchFilter);
return 0L; // Never reached
}
@Override
@CacheResult(skipGet = true)
public Long cacheAlwaysInvoke(String id) {
return counter.getAndIncrement();
}
@Override
@CacheResult
public Long cacheWithPartialKey(@CacheKey String id, boolean notUsed) {
return counter.getAndIncrement();
}
@Override
@CacheResult(cacheResolverFactory = TestableCacheResolverFactory.class)
public Long cacheWithCustomCacheResolver(String id) {
return counter.getAndIncrement();
}
@Override
@CacheResult(cacheKeyGenerator = TestableCacheKeyGenerator.class)
public Long cacheWithCustomKeyGenerator(String id, String anotherId) {
return counter.getAndIncrement();
}
@Override
@CachePut
public void put(String id, @CacheValue Object value) {
}
@Override
@CachePut(cacheFor = UnsupportedOperationException.class)
public void putWithException(@CacheKey String id, @CacheValue Object value, boolean matchFilter) {
throwException(matchFilter);
}
@Override
@CachePut(afterInvocation = false)
public void earlyPut(String id, @CacheValue Object value) {
SimpleGeneratedCacheKey key = new SimpleGeneratedCacheKey(id);
Cache.ValueWrapper valueWrapper = defaultCache.get(key);
if (valueWrapper == null) {
throw new AssertionError("Excepted value to be put in cache with key " + key);
}
Object actual = valueWrapper.get();
if (value != actual) { // instance check on purpose
throw new AssertionError("Wrong value set in cache with key " + key + ". " +
"Expected=" + value + ", but got=" + actual);
}
}
@Override
@CachePut(afterInvocation = false)
public void earlyPutWithException(@CacheKey String id, @CacheValue Object value, boolean matchFilter) {
throwException(matchFilter);
}
@Override
@CacheRemove
public void remove(String id) {
}
@Override
@CacheRemove(noEvictFor = NullPointerException.class)
public void removeWithException(@CacheKey String id, boolean matchFilter) {
throwException(matchFilter);
}
@Override
@CacheRemove(afterInvocation = false)
public void earlyRemove(String id) {
SimpleGeneratedCacheKey key = new SimpleGeneratedCacheKey(id);
Cache.ValueWrapper valueWrapper = defaultCache.get(key);
if (valueWrapper != null) {
throw new AssertionError("Value with key " + key + " expected to be already remove from cache");
}
}
@Override
@CacheRemove(afterInvocation = false, evictFor = UnsupportedOperationException.class)
public void earlyRemoveWithException(@CacheKey String id, boolean matchFilter) {
throwException(matchFilter);
}
@Override
@CacheRemoveAll
public void removeAll() {
}
@Override
@CacheRemoveAll(noEvictFor = NullPointerException.class)
public void removeAllWithException(boolean matchFilter) {
throwException(matchFilter);
}
@Override
@CacheRemoveAll(afterInvocation = false)
public void earlyRemoveAll() {
ConcurrentHashMap<?, ?> nativeCache = (ConcurrentHashMap<?, ?>) defaultCache.getNativeCache();
if (!nativeCache.isEmpty()) {
throw new AssertionError("Cache was expected to be empty");
}
}
@Override
@CacheRemoveAll(afterInvocation = false, evictFor = UnsupportedOperationException.class)
public void earlyRemoveAllWithException(boolean matchFilter) {
throwException(matchFilter);
}
@Deprecated
public void noAnnotation() {
}
@CacheRemove
@CacheRemoveAll
public void multiAnnotations() {
}
@Override
public long exceptionInvocations() {
return exceptionCounter.get();
}
private void throwException(boolean matchFilter) {
long count = exceptionCounter.getAndIncrement();
if (matchFilter) {
throw new UnsupportedOperationException("Expected exception (" + count + ")");
}
else {
throw new NullPointerException("Expected exception (" + count + ")");
}
}
}

View File

@@ -82,4 +82,5 @@ public interface CacheableService<T> {
T multiUpdate(Object arg1);
TestEntity putRefersToResult(TestEntity arg1);
}

View File

@@ -208,4 +208,5 @@ public class DefaultCacheableService implements CacheableService<Long> {
arg1.setId(Long.MIN_VALUE);
return arg1;
}
}

View File

@@ -43,4 +43,5 @@ final class SomeCustomKeyGenerator implements KeyGenerator {
}
return sb.toString();
}
}

View File

@@ -21,7 +21,7 @@ import org.springframework.util.ObjectUtils;
/**
* Simple test entity for use with caching tests.
*
* @author Michael Pl<EFBFBD>d
* @author Michael Plod
*/
public class TestEntity {
@@ -53,4 +53,5 @@ public class TestEntity {
}
return false;
}
}

View File

@@ -0,0 +1,36 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:cache="http://www.springframework.org/schema/cache"
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">
<cache:annotation-driven mode="aspectj"/>
<bean id="cacheManager" class="org.springframework.cache.support.SimpleCacheManager">
<property name="caches">
<set>
<ref bean="defaultCache"/>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean">
<property name="name" value="primary"/>
</bean>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean">
<property name="name" value="secondary"/>
</bean>
<bean class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean">
<property name="name" value="exception"/>
</bean>
</set>
</property>
</bean>
<bean id="defaultCache"
class="org.springframework.cache.concurrent.ConcurrentMapCacheFactoryBean">
<property name="name" value="default"/>
</bean>
<bean id="cacheableService" class="org.springframework.cache.config.AnnotatedJCacheableService">
<constructor-arg ref="defaultCache"/>
</bean>
</beans>