diff --git a/build.gradle b/build.gradle index 8bdec284ac..90e6ae9ffc 100644 --- a/build.gradle +++ b/build.gradle @@ -898,9 +898,10 @@ project("spring-aspects") { optional(project(":spring-aop")) // for @Async support optional(project(":spring-beans")) // for @Configurable support optional(project(":spring-context")) // for @Enable* support - optional(project(":spring-context-support")) // for JavaMail support + optional(project(":spring-context-support")) // for JavaMail and JSR-107 support optional(project(":spring-orm")) // for JPA exception translation support optional(project(":spring-tx")) // for JPA, @Transactional support + optional("javax.cache:cache-api:1.0.0-RC1") testCompile(project(":spring-core")) // for CodeStyleAspect testCompile(project(":spring-test")) testCompile("javax.mail:javax.mail-api:1.4.7") diff --git a/spring-aspects/src/main/java/org/springframework/cache/aspectj/AbstractCacheAspect.aj b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AbstractCacheAspect.aj index ce68942d40..256e609f99 100644 --- a/spring-aspects/src/main/java/org/springframework/cache/aspectj/AbstractCacheAspect.aj +++ b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AbstractCacheAspect.aj @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * 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. @@ -21,6 +21,7 @@ import java.lang.reflect.Method; import org.aspectj.lang.annotation.SuppressAjWarnings; import org.aspectj.lang.reflect.MethodSignature; import org.springframework.cache.interceptor.CacheAspectSupport; +import org.springframework.cache.interceptor.CacheOperationInvoker; import org.springframework.cache.interceptor.CacheOperationSource; /** @@ -56,7 +57,7 @@ public abstract aspect AbstractCacheAspect extends CacheAspectSupport { MethodSignature methodSignature = (MethodSignature) thisJoinPoint.getSignature(); Method method = methodSignature.getMethod(); - Invoker aspectJInvoker = new Invoker() { + CacheOperationInvoker aspectJInvoker = new CacheOperationInvoker() { public Object invoke() { return proceed(cachedObject); } diff --git a/spring-aspects/src/main/java/org/springframework/cache/aspectj/AspectJCachingConfiguration.java b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AspectJCachingConfiguration.java index 740ddaddca..06a589b6a2 100644 --- a/spring-aspects/src/main/java/org/springframework/cache/aspectj/AspectJCachingConfiguration.java +++ b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AspectJCachingConfiguration.java @@ -47,4 +47,5 @@ public class AspectJCachingConfiguration extends AbstractCachingConfiguration { } return cacheAspect; } + } diff --git a/spring-aspects/src/main/java/org/springframework/cache/aspectj/AspectJJCacheConfiguration.java b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AspectJJCacheConfiguration.java new file mode 100644 index 0000000000..2a10770918 --- /dev/null +++ b/spring-aspects/src/main/java/org/springframework/cache/aspectj/AspectJJCacheConfiguration.java @@ -0,0 +1,48 @@ +/* + * 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 org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.cache.annotation.AbstractCachingConfiguration; +import org.springframework.cache.jcache.config.AbstractJCacheConfiguration; +import org.springframework.context.annotation.AnnotationConfigUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; + +/** + * {@code @Configuration} class that registers the Spring infrastructure beans necessary + * to enable AspectJ-based annotation-driven cache management for standard JSR-107 + * annotations. + * + * @author Stephane Nicoll + * @since 4.1 + * @see org.springframework.cache.annotation.EnableCaching + * @see org.springframework.cache.annotation.CachingConfigurationSelector + */ +@Configuration +public class AspectJJCacheConfiguration extends AbstractJCacheConfiguration { + + @Bean(name=AnnotationConfigUtils.JCACHE_ASPECT_BEAN_NAME) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public JCacheCacheAspect cacheAspect() { + JCacheCacheAspect cacheAspect = JCacheCacheAspect.aspectOf(); + cacheAspect.setCacheOperationSource(cacheOperationSource()); + return cacheAspect; + } + +} diff --git a/spring-aspects/src/main/java/org/springframework/cache/aspectj/JCacheCacheAspect.aj b/spring-aspects/src/main/java/org/springframework/cache/aspectj/JCacheCacheAspect.aj new file mode 100644 index 0000000000..6b75a12acb --- /dev/null +++ b/spring-aspects/src/main/java/org/springframework/cache/aspectj/JCacheCacheAspect.aj @@ -0,0 +1,112 @@ +/* + * 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.lang.reflect.Method; + +import javax.cache.annotation.CachePut; +import javax.cache.annotation.CacheRemove; +import javax.cache.annotation.CacheRemoveAll; +import javax.cache.annotation.CacheResult; + +import org.aspectj.lang.annotation.SuppressAjWarnings; +import org.aspectj.lang.reflect.MethodSignature; + +import org.springframework.cache.interceptor.CacheOperationInvoker; +import org.springframework.cache.jcache.interceptor.JCacheAspectSupport; + +/** + * Concrete AspectJ cache aspect using JSR-107 standard annotations. + * + *

When using this aspect, you must annotate the implementation class (and/or + * methods within that class), not the interface (if any) that the class + * implements. AspectJ follows Java's rule that annotations on interfaces are not + * inherited. + * + *

Any method may be annotated (regardless of visibility). Annotating non-public + * methods directly is the only way to get caching demarcation for the execution of + * such operations. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public aspect JCacheCacheAspect extends JCacheAspectSupport { + + @SuppressAjWarnings("adviceDidNotMatch") + Object around(final Object cachedObject) : cacheMethodExecution(cachedObject) { + MethodSignature methodSignature = (MethodSignature) thisJoinPoint.getSignature(); + Method method = methodSignature.getMethod(); + + CacheOperationInvoker aspectJInvoker = new CacheOperationInvoker() { + public Object invoke() { + try { + return proceed(cachedObject); + } catch (Throwable ex) { + throw new ThrowableWrapper(ex); + } + } + + }; + + + try { + return execute(aspectJInvoker, thisJoinPoint.getTarget(), method, thisJoinPoint.getArgs()); + } + catch (CacheOperationInvoker.ThrowableWrapper th) { + if (th.getOriginal() instanceof RuntimeException) { + throw (RuntimeException) th.getOriginal(); + } + throw th; // Lose original checked exception + } + } + + /** + * Definition of pointcut: matched join points will have JSR-107 + * cache management applied. + */ + protected pointcut cacheMethodExecution(Object cachedObject) : + (executionOfCacheResultMethod() + || executionOfCachePutMethod() + || executionOfCacheRemoveMethod() + || executionOfCacheRemoveAllMethod()) + && this(cachedObject); + + /** + * Matches the execution of any method with the @{@link CacheResult} annotation. + */ + private pointcut executionOfCacheResultMethod() : + execution(@CacheResult * *(..)); + + /** + * Matches the execution of any method with the @{@link CachePut} annotation. + */ + private pointcut executionOfCachePutMethod() : + execution(@CachePut * *(..)); + + /** + * Matches the execution of any method with the @{@link CacheRemove} annotation. + */ + private pointcut executionOfCacheRemoveMethod() : + execution(@CacheRemove * *(..)); + + /** + * Matches the execution of any method with the @{@link CacheRemoveAll} annotation. + */ + private pointcut executionOfCacheRemoveAllMethod() : + execution(@CacheRemoveAll * *(..)); + +} \ No newline at end of file diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJAnnotationTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJAnnotationTests.java index e23fa61ee7..1b9e5873c5 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJAnnotationTests.java +++ b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJAnnotationTests.java @@ -71,4 +71,5 @@ public class AspectJAnnotationTests extends AbstractAnnotationTests { assertSame(r3, primary.get(o1).get()); assertSame(r4, secondary.get(o1).get()); } + } diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJJcacheJavaConfigTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJJcacheJavaConfigTests.java new file mode 100644 index 0000000000..2fb8bf401f --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJJcacheJavaConfigTests.java @@ -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"); + } + } + +} diff --git a/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJJcacheNamespaceConfigTests.java b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJJcacheNamespaceConfigTests.java new file mode 100644 index 0000000000..16d1096295 --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJJcacheNamespaceConfigTests.java @@ -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"); + } + +} diff --git a/spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedClassCacheableService.java b/spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedClassCacheableService.java index 6a48f6e94b..592786a551 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedClassCacheableService.java +++ b/spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedClassCacheableService.java @@ -200,4 +200,5 @@ public class AnnotatedClassCacheableService implements CacheableService arg1.setId(Long.MIN_VALUE); return arg1; } + } diff --git a/spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedJCacheableService.java b/spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedJCacheableService.java new file mode 100644 index 0000000000..de3bca6ccf --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedJCacheableService.java @@ -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 + * + *

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 { + + 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 + ")"); + } + } + +} diff --git a/spring-aspects/src/test/java/org/springframework/cache/config/CacheableService.java b/spring-aspects/src/test/java/org/springframework/cache/config/CacheableService.java index 6fab25f7b7..e77a609f0f 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/config/CacheableService.java +++ b/spring-aspects/src/test/java/org/springframework/cache/config/CacheableService.java @@ -82,4 +82,5 @@ public interface CacheableService { T multiUpdate(Object arg1); TestEntity putRefersToResult(TestEntity arg1); + } diff --git a/spring-aspects/src/test/java/org/springframework/cache/config/DefaultCacheableService.java b/spring-aspects/src/test/java/org/springframework/cache/config/DefaultCacheableService.java index 1932d286a9..9cce821e8b 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/config/DefaultCacheableService.java +++ b/spring-aspects/src/test/java/org/springframework/cache/config/DefaultCacheableService.java @@ -208,4 +208,5 @@ public class DefaultCacheableService implements CacheableService { arg1.setId(Long.MIN_VALUE); return arg1; } + } diff --git a/spring-aspects/src/test/java/org/springframework/cache/config/SomeCustomKeyGenerator.java b/spring-aspects/src/test/java/org/springframework/cache/config/SomeCustomKeyGenerator.java index 8a2b160ffa..da23158c9d 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/config/SomeCustomKeyGenerator.java +++ b/spring-aspects/src/test/java/org/springframework/cache/config/SomeCustomKeyGenerator.java @@ -43,4 +43,5 @@ final class SomeCustomKeyGenerator implements KeyGenerator { } return sb.toString(); } + } diff --git a/spring-aspects/src/test/java/org/springframework/cache/config/TestEntity.java b/spring-aspects/src/test/java/org/springframework/cache/config/TestEntity.java index 4c43007158..f5edbf8688 100644 --- a/spring-aspects/src/test/java/org/springframework/cache/config/TestEntity.java +++ b/spring-aspects/src/test/java/org/springframework/cache/config/TestEntity.java @@ -21,7 +21,7 @@ import org.springframework.util.ObjectUtils; /** * Simple test entity for use with caching tests. * - * @author Michael Plšd + * @author Michael Plod */ public class TestEntity { @@ -53,4 +53,5 @@ public class TestEntity { } return false; } + } diff --git a/spring-aspects/src/test/java/org/springframework/cache/config/annotation-jcache-aspectj.xml b/spring-aspects/src/test/java/org/springframework/cache/config/annotation-jcache-aspectj.xml new file mode 100644 index 0000000000..39dec55cd1 --- /dev/null +++ b/spring-aspects/src/test/java/org/springframework/cache/config/annotation-jcache-aspectj.xml @@ -0,0 +1,36 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/AbstractJCacheConfiguration.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/AbstractJCacheConfiguration.java new file mode 100644 index 0000000000..9ba0ae59da --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/AbstractJCacheConfiguration.java @@ -0,0 +1,69 @@ +/* + * 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; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.cache.annotation.AbstractCachingConfiguration; +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.jcache.interceptor.DefaultJCacheOperationSource; +import org.springframework.cache.jcache.interceptor.JCacheOperationSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; + +/** + * Abstract JSR-107 specific {@code @Configuration} class providing common + * structure for enabling JSR-107 annotation-driven cache management capability. + * + * @author Stephane Nicoll + * @since 4.1 + * @see JCacheConfigurer + */ +@Configuration +public class AbstractJCacheConfiguration extends AbstractCachingConfiguration { + + protected CacheResolver cacheResolver; + protected CacheResolver exceptionCacheResolver; + + @Override + protected void useCachingConfigurer(JCacheConfigurer config) { + super.useCachingConfigurer(config); + this.cacheResolver = config.cacheResolver(); + this.exceptionCacheResolver = config.exceptionCacheResolver(); + } + + @Bean(name = "jCacheOperationSource") + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public JCacheOperationSource cacheOperationSource() { + DefaultJCacheOperationSource source = new DefaultJCacheOperationSource(); + if (this.cacheManager != null) { + source.setCacheManager(cacheManager); + } + if (keyGenerator != null) { + source.setKeyGenerator(keyGenerator); + } + if (this.cacheResolver != null) { + source.setCacheResolver(cacheResolver); + } + if (this.exceptionCacheResolver != null) { + source.setExceptionCacheResolver(exceptionCacheResolver); + } + + return source; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurer.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurer.java new file mode 100644 index 0000000000..cbbde55d95 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurer.java @@ -0,0 +1,80 @@ +/* + * 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; + +import org.springframework.cache.annotation.CachingConfigurer; +import org.springframework.cache.interceptor.CacheResolver; + +/** + * Extension of {@link CachingConfigurer} for the JSR-107 implementation. + + *

To be implemented by classes annotated with @{@link org.springframework.cache.annotation.EnableCaching} + * that wish or need to specify explicitly the {@link CacheResolver} bean(s) to be used for + * annotation-driven cache management. + * + *

See @{@link org.springframework.cache.annotation.EnableCaching} for general examples and + * context; see {@link #cacheResolver()} and {@link #exceptionCacheResolver()} for detailed + * instructions. + * + * @author Stephane Nicoll + * @since 4.1 + * @see CachingConfigurer + * @see org.springframework.cache.annotation.EnableCaching + */ +public interface JCacheConfigurer extends CachingConfigurer { + + /** + * Return the {@link CacheResolver} bean to use to resolve regular caches for + * annotation-driven cache management. Implementations must explicitly declare + * {@link org.springframework.context.annotation.Bean @Bean}, e.g. + *

+	 * @Configuration
+	 * @EnableCaching
+	 * public class AppConfig implements JCacheConfigurer {
+	 *     @Bean // important!
+	 *     @Override
+	 *     public CacheResolver cacheResolver() {
+	 *         // configure and return CacheResolver instance
+	 *     }
+	 *     // ...
+	 * }
+	 * 
+ * See {@link org.springframework.cache.annotation.EnableCaching} for more complete examples. + */ + CacheResolver cacheResolver(); + + /** + * Return the {@link CacheResolver} bean to use to resolve exception caches for + * annotation-driven cache management. Implementations must explicitly declare + * {@link org.springframework.context.annotation.Bean @Bean}, e.g. + *
+	 * @Configuration
+	 * @EnableCaching
+	 * public class AppConfig implements JCacheConfigurer {
+	 *     @Bean // important!
+	 *     @Override
+	 *     public CacheResolver exceptionCacheResolver() {
+	 *         // configure and return CacheResolver instance
+	 *     }
+	 *     // ...
+	 * }
+	 * 
+ * See {@link org.springframework.cache.annotation.EnableCaching} for more complete examples. + */ + CacheResolver exceptionCacheResolver(); + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/ProxyJCacheConfiguration.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/ProxyJCacheConfiguration.java new file mode 100644 index 0000000000..71e6075cbd --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/ProxyJCacheConfiguration.java @@ -0,0 +1,61 @@ +/* + * 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; + +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.cache.jcache.interceptor.BeanFactoryJCacheOperationSourceAdvisor; +import org.springframework.cache.jcache.interceptor.JCacheInterceptor; +import org.springframework.context.annotation.AnnotationConfigUtils; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Role; + +/** + * {@code @Configuration} class that registers the Spring infrastructure beans necessary + * to enable proxy-based annotation-driven JSR-107 cache management. + * + *

Can safely be used alongside Spring's caching support. + * + * @author Stephane Nicoll + * @since 4.1 + * @see org.springframework.cache.annotation.EnableCaching + * @see org.springframework.cache.annotation.CachingConfigurationSelector + */ +@Configuration +public class ProxyJCacheConfiguration extends AbstractJCacheConfiguration { + + @Bean(name = AnnotationConfigUtils.JCACHE_ADVISOR_BEAN_NAME) + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public BeanFactoryJCacheOperationSourceAdvisor cacheAdvisor() { + BeanFactoryJCacheOperationSourceAdvisor advisor = + new BeanFactoryJCacheOperationSourceAdvisor(); + advisor.setCacheOperationSource(cacheOperationSource()); + advisor.setAdvice(cacheInterceptor()); + advisor.setOrder(this.enableCaching.getNumber("order")); + return advisor; + } + + + @Bean(name = "jCacheInterceptor") + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public JCacheInterceptor cacheInterceptor() { + JCacheInterceptor interceptor = new JCacheInterceptor(); + interceptor.setCacheOperationSource(cacheOperationSource()); + return interceptor; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/config/package-info.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/package-info.java new file mode 100644 index 0000000000..9924e8ca99 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/config/package-info.java @@ -0,0 +1,8 @@ +/** + * Support package for declarative JSR-107 caching configuration. Used + * by the regular Spring's caching configuration when it detects the + * JSR-107 API and Spring's JCache implementation. + *

Provide an extension of the {@code CachingConfigurer} that exposes + * the exception cache resolver to use, see {@code JCacheConfigurer}. + */ +package org.springframework.cache.jcache.config; \ No newline at end of file diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractCacheInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractCacheInterceptor.java new file mode 100644 index 0000000000..089e5241d1 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractCacheInterceptor.java @@ -0,0 +1,78 @@ +/* + * 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.interceptor; + +import java.io.Serializable; +import java.lang.annotation.Annotation; +import java.util.Collection; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cache.Cache; +import org.springframework.cache.interceptor.CacheOperationInvocationContext; +import org.springframework.cache.interceptor.CacheOperationInvoker; +import org.springframework.cache.jcache.model.BaseCacheOperation; + +/** + * A base interceptor for JSR-107 cache annotations. + * + * @author Stephane Nicoll + * @since 4.1 + */ +@SuppressWarnings("serial") +public abstract class AbstractCacheInterceptor, A extends Annotation> + implements Serializable { + + protected final Log logger = LogFactory.getLog(getClass()); + + protected abstract Object invoke(CacheOperationInvocationContext context, + CacheOperationInvoker invoker) throws Throwable; + + /** + * Resolve the cache to use. + * @param context the invocation context + * @return the cache to use (never null) + */ + protected Cache resolveCache(CacheOperationInvocationContext context) { + Collection caches = context.getOperation().getCacheResolver().resolveCaches(context); + Cache cache = extractFrom(caches); + if (cache == null) { + throw new IllegalStateException("Cache could not have been resolved for " + context.getOperation()); + } + return cache; + } + + /** + * Convert the collection of caches in a single expected element. + *

Throw an {@link IllegalStateException} if the collection holds more than one element + * @return the singe element or {@code null} if the collection is empty + */ + static Cache extractFrom(Collection caches) { + if (caches == null || caches.size() == 0) { + return null; + } + else if (caches.size() == 1) { + return caches.iterator().next(); + } + else { + throw new IllegalStateException("Unsupported cache resolution result " + + caches + " JSR-107 only supports a single cache."); + } + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java new file mode 100644 index 0000000000..e2642e7226 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java @@ -0,0 +1,118 @@ +/* + * 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.interceptor; + +import java.lang.reflect.Method; +import java.lang.reflect.Modifier; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.cache.interceptor.MethodCacheKey; +import org.springframework.cache.jcache.model.JCacheOperation; +import org.springframework.core.BridgeMethodResolver; +import org.springframework.util.ClassUtils; + +/** + * Abstract implementation of {@link JCacheOperationSource} that caches + * attributes for methods and implements a fallback policy: 1. specific + * target method; 2. declaring method. + * + *

This implementation caches attributes by method after they are + * first used. + * + * @author Stephane Nicoll + * @since 4.1 + * @see org.springframework.cache.interceptor.AbstractFallbackCacheOperationSource + */ +public abstract class AbstractFallbackJCacheOperationSource + implements JCacheOperationSource { + + protected final Log logger = LogFactory.getLog(getClass()); + + private final Map> cache = + new ConcurrentHashMap>(1024); + + @Override + public JCacheOperation getCacheOperation(Method method, Class targetClass) { + // First, see if we have a cached value. + Object cacheKey = new MethodCacheKey(method, targetClass); + JCacheOperation cached = this.cache.get(cacheKey); + if (cached != null) { + return cached; + } + else { + JCacheOperation operation = computeCacheOperations(method, targetClass); + if (operation != null) { + if (logger.isDebugEnabled()) { + logger.debug("Adding cacheable method '" + method.getName() + + "' with operation: " + operation); + } + this.cache.put(cacheKey, operation); + } + return operation; + } + } + + private JCacheOperation computeCacheOperations(Method method, Class targetClass) { + // Don't allow no-public methods as required. + if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) { + return null; + } + + // The method may be on an interface, but we need attributes from the target class. + // If the target class is null, the method will be unchanged. + Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass); + // If we are dealing with method with generic parameters, find the original method. + specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod); + + // First try is the method in the target class. + JCacheOperation operation = findCacheOperation(specificMethod, targetClass); + if (operation != null) { + return operation; + } + if (specificMethod != method) { + // Fall back is to look at the original method. + operation = findCacheOperation(method, targetClass); + if (operation != null) { + return operation; + } + } + return null; + } + + /** + * Subclasses need to implement this to return the caching operation + * for the given method, if any. + * @param method the method to retrieve the operation for + * @param targetType the target class + * @return the cache operation associated with this method + * (or {@code null} if none) + */ + protected abstract JCacheOperation findCacheOperation(Method method, Class targetType); + + /** + * Should only public methods be allowed to have caching semantics? + *

The default implementation returns {@code false}. + */ + protected boolean allowPublicMethodsOnly() { + return false; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractKeyCacheInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractKeyCacheInterceptor.java new file mode 100644 index 0000000000..11471d2d50 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractKeyCacheInterceptor.java @@ -0,0 +1,61 @@ +/* + * 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.interceptor; + +import java.lang.annotation.Annotation; + +import javax.cache.annotation.CacheKeyInvocationContext; + +import org.springframework.cache.interceptor.CacheOperationInvocationContext; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.cache.jcache.model.BaseKeyCacheOperation; + +/** + * A base interceptor for JSR-107 key-based cache annotations. + * + * @author Stephane Nicoll + * @since 4.1 + */ +@SuppressWarnings("serial") +public abstract class AbstractKeyCacheInterceptor, A extends Annotation> + extends AbstractCacheInterceptor { + + /** + * Generate a key for the specified invocation. + * @param context the context of the invocation + * @return the key to use + */ + protected Object generateKey(CacheOperationInvocationContext context) { + KeyGenerator keyGenerator = context.getOperation().getKeyGenerator(); + Object key = keyGenerator.generate(context.getTarget(), context.getMethod(), context.getArgs()); + if (logger.isTraceEnabled()) { + logger.trace("Computed cache key " + key + " for operation " + context.getOperation()); + } + return key; + } + + /** + * Create a {@link CacheKeyInvocationContext} based on the specified invocation. + * @param context the context of the invocation. + * @return the related {@code CacheKeyInvocationContext} + */ + protected CacheKeyInvocationContext createCacheKeyInvocationContext( + CacheOperationInvocationContext context) { + return new DefaultCacheKeyInvocationContext(context.getOperation(), context.getTarget(), context.getArgs()); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AnnotationCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AnnotationCacheOperationSource.java new file mode 100644 index 0000000000..34fd6423ce --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AnnotationCacheOperationSource.java @@ -0,0 +1,269 @@ +/* + * 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.interceptor; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.List; + +import javax.cache.annotation.CacheDefaults; +import javax.cache.annotation.CacheKeyGenerator; +import javax.cache.annotation.CacheMethodDetails; +import javax.cache.annotation.CachePut; +import javax.cache.annotation.CacheRemove; +import javax.cache.annotation.CacheRemoveAll; +import javax.cache.annotation.CacheResolverFactory; +import javax.cache.annotation.CacheResult; + +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.cache.jcache.model.CachePutOperation; +import org.springframework.cache.jcache.model.CacheRemoveAllOperation; +import org.springframework.cache.jcache.model.CacheRemoveOperation; +import org.springframework.cache.jcache.model.CacheResultOperation; +import org.springframework.cache.jcache.model.DefaultCacheMethodDetails; +import org.springframework.cache.jcache.model.JCacheOperation; +import org.springframework.util.StringUtils; + +/** + * Implementation of the {@link JCacheOperationSource} interface that reads + * the JSR-107 {@link CacheResult}, {@link CachePut}, {@link CacheRemove} and + * {@link CacheRemoveAll} annotations. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public abstract class AnnotationCacheOperationSource + extends AbstractFallbackJCacheOperationSource { + + /** + * Locate or create an instance of the specified {@code type}. + * @param type the type of the bean to manage + * @return the required bean + */ + protected abstract T getBean(Class type); + + /** + * Return the default {@link CacheResolver} if none is set. + */ + protected abstract CacheResolver getDefaultCacheResolver(); + + /** + * Return the default exception {@link CacheResolver} if none is set. + */ + protected abstract CacheResolver getDefaultExceptionCacheResolver(); + + /** + * Return the default {@link KeyGenerator} if none is set. + */ + protected abstract KeyGenerator getDefaultKeyGenerator(); + + + @Override + protected JCacheOperation findCacheOperation(Method method, Class targetType) { + CacheResult cacheResult = method.getAnnotation(CacheResult.class); + CachePut cachePut = method.getAnnotation(CachePut.class); + CacheRemove cacheRemove = method.getAnnotation(CacheRemove.class); + CacheRemoveAll cacheRemoveAll = method.getAnnotation(CacheRemoveAll.class); + int found = countNonNull(cacheResult, cachePut, cacheRemove, cacheRemoveAll); + if (found == 0) { + return null; + } + if (found > 1) { + throw new IllegalStateException("More than one cache annotation found on '" + method + "'"); + } + CacheDefaults defaults = getCacheDefaults(method, targetType); + + if (cacheResult != null) { + return createCacheResultOperation(method, defaults, cacheResult); + } + else if (cachePut != null) { + return createCachePutOperation(method, defaults, cachePut); + } + else if (cacheRemove != null) { + return createCacheRemoveOperation(method, defaults, cacheRemove); + } + else { + return createCacheRemoveAllOperation(method, defaults, cacheRemoveAll); + } + } + + protected CacheDefaults getCacheDefaults(Method method, Class targetType) { + CacheDefaults annotation = method.getDeclaringClass().getAnnotation(CacheDefaults.class); + if (annotation != null) { + return annotation; + } + return targetType.getAnnotation(CacheDefaults.class); + } + + + protected CacheResultOperation createCacheResultOperation(Method method, CacheDefaults defaults, + CacheResult ann) { + String cacheName = determineCacheName(method, defaults, ann.cacheName()); + CacheResolverFactory cacheResolverFactory = + determineCacheResolverFactory(defaults, ann.cacheResolverFactory()); + KeyGenerator keyGenerator = determineKeyGenerator(defaults, ann.cacheKeyGenerator()); + + CacheMethodDetails methodDetails = createMethodDetails(method, ann, cacheName); + + CacheResolver cacheResolver = getCacheResolver(cacheResolverFactory, methodDetails); + CacheResolver exceptionCacheResolver = null; + final String exceptionCacheName = ann.exceptionCacheName(); + if (StringUtils.hasText(exceptionCacheName)) { + exceptionCacheResolver = getExceptionCacheResolver(cacheResolverFactory, methodDetails); + } + + return new CacheResultOperation(methodDetails, cacheResolver, keyGenerator, exceptionCacheResolver); + } + + protected CachePutOperation createCachePutOperation(Method method, CacheDefaults defaults, + CachePut ann) { + String cacheName = determineCacheName(method, defaults, ann.cacheName()); + CacheResolverFactory cacheResolverFactory = + determineCacheResolverFactory(defaults, ann.cacheResolverFactory()); + KeyGenerator keyGenerator = determineKeyGenerator(defaults, ann.cacheKeyGenerator()); + + CacheMethodDetails methodDetails = createMethodDetails(method, ann, cacheName); + + CacheResolver cacheResolver = getCacheResolver(cacheResolverFactory, methodDetails); + + return new CachePutOperation(methodDetails, cacheResolver, keyGenerator); + } + + protected CacheRemoveOperation createCacheRemoveOperation(Method method, CacheDefaults defaults, + CacheRemove ann) { + String cacheName = determineCacheName(method, defaults, ann.cacheName()); + CacheResolverFactory cacheResolverFactory = + determineCacheResolverFactory(defaults, ann.cacheResolverFactory()); + KeyGenerator keyGenerator = determineKeyGenerator(defaults, ann.cacheKeyGenerator()); + + CacheMethodDetails methodDetails = createMethodDetails(method, ann, cacheName); + + CacheResolver cacheResolver = getCacheResolver(cacheResolverFactory, methodDetails); + + return new CacheRemoveOperation(methodDetails, cacheResolver, keyGenerator); + } + + protected CacheRemoveAllOperation createCacheRemoveAllOperation(Method method, CacheDefaults defaults, + CacheRemoveAll ann) { + String cacheName = determineCacheName(method, defaults, ann.cacheName()); + CacheResolverFactory cacheResolverFactory = + determineCacheResolverFactory(defaults, ann.cacheResolverFactory()); + + CacheMethodDetails methodDetails = createMethodDetails(method, ann, cacheName); + + CacheResolver cacheResolver = getCacheResolver(cacheResolverFactory, methodDetails); + + return new CacheRemoveAllOperation(methodDetails, cacheResolver); + } + + private CacheMethodDetails createMethodDetails( + Method method, A annotation, String cacheName) { + return new DefaultCacheMethodDetails(method, annotation, cacheName); + } + + protected CacheResolver getCacheResolver(CacheResolverFactory factory, CacheMethodDetails details) { + if (factory != null) { + javax.cache.annotation.CacheResolver cacheResolver = factory.getCacheResolver(details); + return new CacheResolverAdapter(cacheResolver); + } + else { + return getDefaultCacheResolver(); + } + } + + protected CacheResolver getExceptionCacheResolver(CacheResolverFactory factory, + CacheMethodDetails details) { + if (factory != null) { + javax.cache.annotation.CacheResolver cacheResolver = factory.getExceptionCacheResolver(details); + return new CacheResolverAdapter(cacheResolver); + } + else { + return getDefaultExceptionCacheResolver(); + } + } + + protected CacheResolverFactory determineCacheResolverFactory(CacheDefaults defaults, + Class candidate) { + if (!CacheResolverFactory.class.equals(candidate)) { + return getBean(candidate); + } + else if (defaults != null && !CacheResolverFactory.class.equals(defaults.cacheResolverFactory())) { + return getBean(defaults.cacheResolverFactory()); + } + else { + return null; + } + } + + protected KeyGenerator determineKeyGenerator(CacheDefaults defaults, + Class candidate) { + if (!CacheKeyGenerator.class.equals(candidate)) { + return new KeyGeneratorAdapter(this, getBean(candidate)); + } + else if (defaults != null && !CacheKeyGenerator.class.equals(defaults.cacheKeyGenerator())) { + return new KeyGeneratorAdapter(this, getBean(defaults.cacheKeyGenerator())); + } + else { + return getDefaultKeyGenerator(); + } + } + + protected String determineCacheName(Method method, CacheDefaults defaults, String candidate) { + if (StringUtils.hasText(candidate)) { + return candidate; + } + if (defaults != null && StringUtils.hasText(defaults.cacheName())) { + return defaults.cacheName(); + } + return generateDefaultCacheName(method); + } + + /** + * Generate a default cache name for the specified {@link Method}. + * + * @param method the annotated method + * @return the default cache name, according to JSR-107 + */ + protected String generateDefaultCacheName(Method method) { + Class[] parameterTypes = method.getParameterTypes(); + List parameters = new ArrayList(); + for (Class parameterType : parameterTypes) { + parameters.add(parameterType.getName()); + } + + StringBuilder sb = new StringBuilder(); + sb.append(method.getDeclaringClass().getName()) + .append(".") + .append(method.getName()) + .append("(") + .append(StringUtils.collectionToCommaDelimitedString(parameters)) + .append(")"); + return sb.toString(); + } + + private int countNonNull(Object... instances) { + int result = 0; + for (Object o : instances) { + if (o != null) { + result += 1; + } + } + return result; + } +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/BeanFactoryJCacheOperationSourceAdvisor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/BeanFactoryJCacheOperationSourceAdvisor.java new file mode 100644 index 0000000000..d9d4dde731 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/BeanFactoryJCacheOperationSourceAdvisor.java @@ -0,0 +1,64 @@ +/* + * 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.interceptor; + +import org.springframework.aop.ClassFilter; +import org.springframework.aop.Pointcut; +import org.springframework.aop.support.AbstractBeanFactoryPointcutAdvisor; + +/** + * Advisor driven by a {@link JCacheOperationSource}, used to include a + * cache advice bean for methods that are cacheable. + * + * @author Stephane Nicoll + * @since 4.1 + */ +@SuppressWarnings("serial") +public class BeanFactoryJCacheOperationSourceAdvisor extends AbstractBeanFactoryPointcutAdvisor { + + private JCacheOperationSource cacheOperationSource; + + private final JCacheOperationSourcePointcut pointcut = new JCacheOperationSourcePointcut() { + @Override + protected JCacheOperationSource getCacheOperationSource() { + return cacheOperationSource; + } + }; + + /** + * Set the cache operation attribute source which is used to find cache + * attributes. This should usually be identical to the source reference + * set on the cache interceptor itself. + */ + public void setCacheOperationSource(JCacheOperationSource cacheOperationSource) { + this.cacheOperationSource = cacheOperationSource; + } + + /** + * Set the {@link org.springframework.aop.ClassFilter} to use for this pointcut. + * Default is {@link org.springframework.aop.ClassFilter#TRUE}. + */ + public void setClassFilter(ClassFilter classFilter) { + this.pointcut.setClassFilter(classFilter); + } + + @Override + public Pointcut getPointcut() { + return this.pointcut; + } + +} \ No newline at end of file diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutInterceptor.java new file mode 100644 index 0000000000..8a8432e507 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutInterceptor.java @@ -0,0 +1,71 @@ +/* + * 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.interceptor; + +import javax.cache.annotation.CacheKeyInvocationContext; +import javax.cache.annotation.CachePut; + +import org.springframework.cache.Cache; +import org.springframework.cache.interceptor.CacheOperationInvocationContext; +import org.springframework.cache.interceptor.CacheOperationInvoker; +import org.springframework.cache.jcache.model.CachePutOperation; + +/** + * Intercept methods annotated with {@link CachePut}. + * + * @author Stephane Nicoll + * @since 4.1 + */ +@SuppressWarnings("serial") +public class CachePutInterceptor extends AbstractKeyCacheInterceptor { + + @Override + protected Object invoke(CacheOperationInvocationContext context, + CacheOperationInvoker invoker) { + CacheKeyInvocationContext invocationContext = createCacheKeyInvocationContext(context); + CachePutOperation operation = context.getOperation(); + + final boolean earlyPut = operation.isEarlyPut(); + final Object value = invocationContext.getValueParameter().getValue(); + + if (earlyPut) { + cacheValue(context, value); + } + + try { + Object result = invoker.invoke(); + if (!earlyPut) { + cacheValue(context, value); + } + return result; + } + catch (CacheOperationInvoker.ThrowableWrapper t) { + Throwable ex = t.getOriginal(); + if (!earlyPut && operation.getExceptionTypeFilter().match(ex.getClass())) { + cacheValue(context, value); + } + throw t; + } + } + + protected void cacheValue(CacheOperationInvocationContext context, Object value) { + Object key = generateKey(context); + Cache cache = resolveCache(context); + cache.put(key, value); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllInterceptor.java new file mode 100644 index 0000000000..2d215278a4 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllInterceptor.java @@ -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.jcache.interceptor; + +import javax.cache.annotation.CacheRemoveAll; + +import org.springframework.cache.Cache; +import org.springframework.cache.interceptor.CacheOperationInvocationContext; +import org.springframework.cache.interceptor.CacheOperationInvoker; +import org.springframework.cache.jcache.model.CacheRemoveAllOperation; + +/** + * Intercept methods annotated with {@link CacheRemoveAll}. + * + * @author Stephane Nicoll + * @since 4.1 + */ +@SuppressWarnings("serial") +public class CacheRemoveAllInterceptor + extends AbstractCacheInterceptor { + + @Override + protected Object invoke(CacheOperationInvocationContext context, + CacheOperationInvoker invoker) { + CacheRemoveAllOperation operation = context.getOperation(); + + final boolean earlyRemove = operation.isEarlyRemove(); + + if (earlyRemove) { + removeAll(context); + } + + try { + Object result = invoker.invoke(); + if (!earlyRemove) { + removeAll(context); + } + return result; + } + catch (CacheOperationInvoker.ThrowableWrapper t) { + Throwable ex = t.getOriginal(); + if (!earlyRemove && operation.getExceptionTypeFilter().match(ex.getClass())) { + removeAll(context); + } + throw t; + } + } + + protected void removeAll(CacheOperationInvocationContext context) { + Cache cache = resolveCache(context); + if (logger.isTraceEnabled()) { + logger.trace("Invalidating entire cache '" + cache.getName() + "' for operation " + + context.getOperation()); + } + cache.clear(); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveEntryInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveEntryInterceptor.java new file mode 100644 index 0000000000..2d0b7c3697 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveEntryInterceptor.java @@ -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.jcache.interceptor; + +import javax.cache.annotation.CacheRemove; + +import org.springframework.cache.Cache; +import org.springframework.cache.interceptor.CacheOperationInvocationContext; +import org.springframework.cache.interceptor.CacheOperationInvoker; +import org.springframework.cache.jcache.model.CacheRemoveOperation; + +/** + * Intercept methods annotated with {@link CacheRemove}. + * + * @author Stephane Nicoll + * @since 4.1 + */ +@SuppressWarnings("serial") +public class CacheRemoveEntryInterceptor extends AbstractKeyCacheInterceptor { + + @Override + protected Object invoke(CacheOperationInvocationContext context, + CacheOperationInvoker invoker) { + CacheRemoveOperation operation = context.getOperation(); + + final boolean earlyRemove = operation.isEarlyRemove(); + + if (earlyRemove) { + removeValue(context); + } + + try { + Object result = invoker.invoke(); + if (!earlyRemove) { + removeValue(context); + } + return result; + } + catch (CacheOperationInvoker.ThrowableWrapper t) { + Throwable ex = t.getOriginal(); + if (!earlyRemove && operation.getExceptionTypeFilter().match(ex.getClass())) { + removeValue(context); + } + throw t; + } + } + + private void removeValue(CacheOperationInvocationContext context) { + Object key = generateKey(context); + Cache cache = resolveCache(context); + if (logger.isTraceEnabled()) { + logger.trace("Invalidating key [" + key + "] on cache '" + cache.getName() + + "' for operation " + context.getOperation()); + } + cache.evict(key); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResolverAdapter.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResolverAdapter.java new file mode 100644 index 0000000000..f544e92b9a --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResolverAdapter.java @@ -0,0 +1,54 @@ +package org.springframework.cache.jcache.interceptor; + + +import java.util.Collection; +import java.util.Collections; + +import javax.cache.annotation.CacheInvocationContext; + +import org.springframework.cache.Cache; +import org.springframework.cache.interceptor.CacheOperationInvocationContext; +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.jcache.JCacheCache; +import org.springframework.util.Assert; + +/** + * Spring's {@link CacheResolver} implementation that delegates to a standard + * JSR-107 {@link javax.cache.annotation.CacheResolver}. + *

Used internally to invoke user-based JSR-107 cache resolvers. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public class CacheResolverAdapter implements CacheResolver { + + private final javax.cache.annotation.CacheResolver target; + + /** + * Create a new instance with the JSR-107 cache resolver to invoke. + */ + public CacheResolverAdapter(javax.cache.annotation.CacheResolver target) { + Assert.notNull(target, "JSR-107 cache resolver must be set."); + this.target = target; + } + + /** + * Return the underlying {@link javax.cache.annotation.CacheResolver} that this + * instance is using. + */ + protected javax.cache.annotation.CacheResolver getTarget() { + return target; + } + + @Override + public Collection resolveCaches(CacheOperationInvocationContext context) { + if (!(context instanceof CacheInvocationContext)) { + throw new IllegalStateException("Unexpected context " + context); + } + CacheInvocationContext cacheInvocationContext = (CacheInvocationContext) context; + javax.cache.Cache cache = target.resolveCache(cacheInvocationContext); + Assert.notNull(cache, "Cannot resolve cache for '" + context + "' using '" + target + "'"); + return Collections.singleton(new JCacheCache(cache)); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultInterceptor.java new file mode 100644 index 0000000000..8b46a85354 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultInterceptor.java @@ -0,0 +1,163 @@ +/* + * 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.interceptor; + +import javax.cache.annotation.CacheResult; + +import org.springframework.cache.Cache; +import org.springframework.cache.interceptor.CacheOperationInvocationContext; +import org.springframework.cache.interceptor.CacheOperationInvoker; +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.jcache.model.CacheResultOperation; +import org.springframework.util.SerializationUtils; +import org.springframework.util.filter.ExceptionTypeFilter; + +/** + * Intercept methods annotated with {@link CacheResult}. + * + * @author Stephane Nicoll + * @since 4.1 + */ +@SuppressWarnings("serial") +public class CacheResultInterceptor extends AbstractKeyCacheInterceptor { + + @Override + protected Object invoke(CacheOperationInvocationContext context, + CacheOperationInvoker invoker) { + CacheResultOperation operation = context.getOperation(); + + final Object cacheKey = generateKey(context); + + Cache cache = resolveCache(context); + Cache exceptionCache = resolveExceptionCache(context); + + if (!operation.isAlwaysInvoked()) { + Cache.ValueWrapper cachedValue = cache.get(cacheKey); + if (cachedValue != null) { + return cachedValue.get(); + } + checkForCachedException(exceptionCache, cacheKey); + } + + try { + Object invocationResult = invoker.invoke(); + if (invocationResult != null) { + cache.put(cacheKey, invocationResult); + } + return invocationResult; + } + catch (CacheOperationInvoker.ThrowableWrapper t) { + Throwable original = t.getOriginal(); + cacheException(exceptionCache, operation.getExceptionTypeFilter(), cacheKey, original); + throw t; + } + } + + /** + * Check for a cached exception. If the exception is found, throw it directly. + */ + protected void checkForCachedException(Cache exceptionCache, Object cacheKey) { + if (exceptionCache == null) { + return; + } + Cache.ValueWrapper result = exceptionCache.get(cacheKey); + if (result != null) { + throw rewriteCallStack((Throwable) result.get(), getClass().getName(), "invoke"); + } + } + + + protected void cacheException(Cache exceptionCache, ExceptionTypeFilter filter, + Object cacheKey, Throwable t) { + if (exceptionCache == null) { + return; + } + if (filter.match(t.getClass())) { + exceptionCache.put(cacheKey, t); + } + } + + + private Cache resolveExceptionCache(CacheOperationInvocationContext context) { + CacheResolver exceptionCacheResolver = context.getOperation().getExceptionCacheResolver(); + if (exceptionCacheResolver != null) { + return extractFrom(context.getOperation() + .getExceptionCacheResolver().resolveCaches(context)); + } + return null; + } + + /** + * Rewrite the call stack of the specified {@code exception} so that it matches + * the current call stack up-to (included) the specified method invocation. + *

Clone the specified exception. If the exception is not {@code serializable}, + * the original exception is returned. If no common ancestor can be found, returns + * the original exception. + *

Used to make sure that a cached exception has a valid invocation context. + * @param exception the exception to merge with the current call stack + * @param className the class name of the common ancestor + * @param methodName the method name of the common ancestor + * @param the type of the exception + * @return a clone exception with a rewritten call stack composed of the current + * call stack up to (included) the common ancestor specified by the {@code className} and + * {@code methodName} arguments, followed by stack trace elements of the specified + * {@code exception} after the common ancestor. + */ + private static CacheOperationInvoker.ThrowableWrapper rewriteCallStack(Throwable exception, + String className, String methodName) { + Throwable clone = cloneException(exception); + if (clone == null) { + return new CacheOperationInvoker.ThrowableWrapper(exception); + } + + StackTraceElement[] callStack = new Exception().getStackTrace(); + StackTraceElement[] cachedCallStack = exception.getStackTrace(); + + int index = findCommonAncestorIndex(callStack, className, methodName); + int cachedIndex = findCommonAncestorIndex(cachedCallStack, className, methodName); + if (index == -1 || cachedIndex == -1) { + return new CacheOperationInvoker.ThrowableWrapper(exception); // Cannot find common ancestor + } + StackTraceElement[] result = new StackTraceElement[cachedIndex + callStack.length - index]; + System.arraycopy(cachedCallStack, 0, result, 0, cachedIndex); + System.arraycopy(callStack, index, result, cachedIndex, callStack.length - index); + + clone.setStackTrace(result); + return new CacheOperationInvoker.ThrowableWrapper(clone); + } + + @SuppressWarnings("unchecked") + private static T cloneException(T exception) { + try { + return (T) SerializationUtils.deserialize(SerializationUtils.serialize(exception)); + } + catch (Exception e) { + return null; // exception parameter cannot be cloned + } + } + + private static int findCommonAncestorIndex(StackTraceElement[] callStack, String className, String methodName) { + for (int i = 0; i < callStack.length; i++) { + StackTraceElement element = callStack[i]; + if (className.equals(element.getClassName()) && methodName.equals(element.getMethodName())) { + return i; + } + } + return -1; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultCacheInvocationContext.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultCacheInvocationContext.java new file mode 100644 index 0000000000..2b9fe23146 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultCacheInvocationContext.java @@ -0,0 +1,112 @@ +/* + * 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.interceptor; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Arrays; +import java.util.Set; + +import javax.cache.annotation.CacheInvocationContext; +import javax.cache.annotation.CacheInvocationParameter; + +import org.springframework.cache.interceptor.CacheOperationInvocationContext; +import org.springframework.cache.jcache.model.JCacheOperation; + +/** + * The default {@link CacheOperationInvocationContext} implementation used + * by all interceptors. Also implements {@link CacheInvocationContext} to + * act as a proper bridge when calling JSR-107 {@link javax.cache.annotation.CacheResolver} + * + * @author Stephane Nicoll + * @since 4.1 + */ +public class DefaultCacheInvocationContext + implements CacheInvocationContext, CacheOperationInvocationContext> { + + private final JCacheOperation operation; + + private final Object target; + + private final Object[] args; + + private final CacheInvocationParameter[] allParameters; + + public DefaultCacheInvocationContext(JCacheOperation operation, + Object target, Object[] args) { + this.operation = operation; + this.target = target; + this.args = args; + this.allParameters = operation.getAllParameters(args); + } + + @Override + public JCacheOperation getOperation() { + return operation; + } + + @Override + public Method getMethod() { + return operation.getMethod(); + } + + @Override + public Object[] getArgs() { + return args.clone(); + } + + @Override + public Set getAnnotations() { + return operation.getAnnotations(); + } + + @Override + public A getCacheAnnotation() { + return operation.getCacheAnnotation(); + } + + @Override + public String getCacheName() { + return operation.getCacheName(); + } + + @Override + public Object getTarget() { + return target; + } + + @Override + public CacheInvocationParameter[] getAllParameters() { + return allParameters.clone(); + } + + @Override + public T unwrap(Class cls) { + throw new IllegalArgumentException("Could not unwrap to '" + cls.getName() + "'"); + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("CacheInvocationContext{"); + sb.append("operation=").append(operation); + sb.append(", target=").append(target); + sb.append(", args=").append(Arrays.toString(args)); + sb.append(", allParameters=").append(Arrays.toString(allParameters)); + sb.append('}'); + return sb.toString(); + } +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultCacheKeyInvocationContext.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultCacheKeyInvocationContext.java new file mode 100644 index 0000000000..97e9f5a69b --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultCacheKeyInvocationContext.java @@ -0,0 +1,62 @@ +/* + * 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.interceptor; + +import java.lang.annotation.Annotation; + +import javax.cache.annotation.CacheInvocationParameter; +import javax.cache.annotation.CacheKeyInvocationContext; + +import org.springframework.cache.jcache.model.BaseKeyCacheOperation; +import org.springframework.cache.jcache.model.CachePutOperation; + +/** + * The default {@link CacheKeyInvocationContext} implementation. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public class DefaultCacheKeyInvocationContext + extends DefaultCacheInvocationContext implements CacheKeyInvocationContext { + + private final CacheInvocationParameter[] keyParameters; + + private final CacheInvocationParameter valueParameter; + + public DefaultCacheKeyInvocationContext(BaseKeyCacheOperation operation, + Object target, Object[] args) { + super(operation, target, args); + this.keyParameters = operation.getKeyParameters(args); + if (operation instanceof CachePutOperation) { + this.valueParameter = ((CachePutOperation) operation).getValueParameter(args); + } + else { + this.valueParameter = null; + } + } + + @Override + public CacheInvocationParameter[] getKeyParameters() { + return keyParameters.clone(); + } + + @Override + public CacheInvocationParameter getValueParameter() { + return valueParameter; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultJCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultJCacheOperationSource.java new file mode 100644 index 0000000000..ede0dd2aef --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultJCacheOperationSource.java @@ -0,0 +1,133 @@ +/* + * 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.interceptor; + +import java.util.Map; + +import org.springframework.beans.BeanUtils; +import org.springframework.beans.factory.BeanFactoryUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.cache.CacheManager; +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.cache.interceptor.SimpleCacheResolver; +import org.springframework.context.ApplicationContext; +import org.springframework.context.ApplicationContextAware; +import org.springframework.util.Assert; + +/** + * The default {@link JCacheOperationSource} implementation delegating + * default operations to configurable services with sensible defaults + * when not present. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public class DefaultJCacheOperationSource extends AnnotationCacheOperationSource + implements InitializingBean, ApplicationContextAware { + + private CacheManager cacheManager; + + private KeyGenerator keyGenerator; + + private CacheResolver cacheResolver; + + private CacheResolver exceptionCacheResolver; + + private ApplicationContext applicationContext; + + @Override + public void afterPropertiesSet() { + Assert.state((cacheResolver != null && exceptionCacheResolver != null) + || cacheManager != null, "'cacheManager' is required if cache resolvers are not set."); + Assert.state(this.applicationContext != null, "The application context was not injected as it should."); + + if (keyGenerator == null) { + keyGenerator = new KeyGeneratorAdapter(this, new SimpleCacheKeyGenerator()); + } + if (cacheResolver == null) { + cacheResolver = new SimpleCacheResolver(cacheManager); + } + if (exceptionCacheResolver == null) { + exceptionCacheResolver = new SimpleExceptionCacheResolver(cacheManager); + } + } + + /** + * Set the default {@link CacheManager} to use to lookup cache by name. Only mandatory + * if the {@linkplain CacheResolver cache resolvers} have not been set. + */ + public void setCacheManager(CacheManager cacheManager) { + this.cacheManager = cacheManager; + } + + /** + * Set the default {@link KeyGenerator}. If none is set, a default JSR-107 compliant + * key generator is used. + */ + public void setKeyGenerator(KeyGenerator keyGenerator) { + this.keyGenerator = keyGenerator; + } + + /** + * Set the {@link CacheResolver} to resolve regular caches. If none is set, a default + * implementation using the specified cache manager will be used. + */ + public void setCacheResolver(CacheResolver cacheResolver) { + this.cacheResolver = cacheResolver; + } + + /** + * Set the {@link CacheResolver} to resolve exception caches. If none is set, a default + * implementation using the specified cache manager will be used. + */ + public void setExceptionCacheResolver(CacheResolver exceptionCacheResolver) { + this.exceptionCacheResolver = exceptionCacheResolver; + } + + @Override + public void setApplicationContext(ApplicationContext applicationContext) { + this.applicationContext = applicationContext; + } + + @Override + protected T getBean(Class type) { + Map map = BeanFactoryUtils.beansOfTypeIncludingAncestors(applicationContext, type); + if (map.size() == 1) { + return map.values().iterator().next(); + } + else { + return BeanUtils.instantiateClass(type); + } + } + + @Override + public CacheResolver getDefaultCacheResolver() { + return cacheResolver; + } + + @Override + public CacheResolver getDefaultExceptionCacheResolver() { + return exceptionCacheResolver; + } + + @Override + public KeyGenerator getDefaultKeyGenerator() { + return keyGenerator; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.java new file mode 100644 index 0000000000..4d5117b5aa --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.java @@ -0,0 +1,131 @@ +package org.springframework.cache.jcache.interceptor; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.aop.framework.AopProxyUtils; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.cache.interceptor.BasicCacheOperation; +import org.springframework.cache.interceptor.CacheOperationInvocationContext; +import org.springframework.cache.interceptor.CacheOperationInvoker; +import org.springframework.cache.jcache.model.CachePutOperation; +import org.springframework.cache.jcache.model.CacheRemoveAllOperation; +import org.springframework.cache.jcache.model.CacheRemoveOperation; +import org.springframework.cache.jcache.model.CacheResultOperation; +import org.springframework.cache.jcache.model.JCacheOperation; +import org.springframework.util.Assert; + +/** + * Base class for JSR-107 caching aspects, such as the {@link JCacheInterceptor} + * or an AspectJ aspect. + * + *

Use the Spring caching abstraction for cache-related operations. No JSR-107 + * {@link javax.cache.Cache} or {@link javax.cache.CacheManager} are required to + * process standard JSR-107 cache annotations. + * + *

The {@link JCacheOperationSource} is used for determining caching operations + * + *

A cache aspect is serializable if its {@code JCacheOperationSource} is serializable. + * + * @author Stephane Nicoll + * @since 4.1 + * @see org.springframework.cache.interceptor.CacheAspectSupport + * @see KeyGeneratorAdapter + * @see CacheResolverAdapter + */ +public class JCacheAspectSupport implements InitializingBean { + + protected final Log logger = LogFactory.getLog(getClass()); + + private JCacheOperationSource cacheOperationSource; + + private boolean initialized = false; + + private final CacheResultInterceptor cacheResultInterceptor = new CacheResultInterceptor(); + + private final CachePutInterceptor cachePutInterceptor = new CachePutInterceptor(); + + private final CacheRemoveEntryInterceptor cacheRemoveEntryInterceptor = new CacheRemoveEntryInterceptor(); + + private final CacheRemoveAllInterceptor cacheRemoveAllInterceptor = new CacheRemoveAllInterceptor(); + + public void setCacheOperationSource(JCacheOperationSource cacheOperationSource) { + Assert.notNull(cacheOperationSource); + this.cacheOperationSource = cacheOperationSource; + } + + /** + * Return the CacheOperationSource for this cache aspect. + */ + public JCacheOperationSource getCacheOperationSource() { + return this.cacheOperationSource; + } + + public void afterPropertiesSet() { + Assert.state(this.cacheOperationSource != null, "The 'cacheOperationSource' property is required: " + + "If there are no cacheable methods, then don't use a cache aspect."); + this.initialized = true; + } + + + protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) { + // check whether aspect is enabled + // to cope with cases where the AJ is pulled in automatically + if (this.initialized) { + Class targetClass = getTargetClass(target); + JCacheOperation operation = getCacheOperationSource().getCacheOperation(method, targetClass); + if (operation != null) { + CacheOperationInvocationContext context = + createCacheOperationInvocationContext(target, args, operation); + return execute(context, invoker); + } + } + + return invoker.invoke(); + } + + @SuppressWarnings("unchecked") + private CacheOperationInvocationContext createCacheOperationInvocationContext(Object target, + Object[] args, + JCacheOperation operation) { + return new DefaultCacheInvocationContext( + (JCacheOperation) operation, target, args); + } + + private Class getTargetClass(Object target) { + Class targetClass = AopProxyUtils.ultimateTargetClass(target); + if (targetClass == null && target != null) { + targetClass = target.getClass(); + } + return targetClass; + } + + @SuppressWarnings("unchecked") + private Object execute(CacheOperationInvocationContext context, + CacheOperationInvoker invoker) { + BasicCacheOperation operation = context.getOperation(); + if (operation instanceof CacheResultOperation) { + return cacheResultInterceptor.invoke( + (CacheOperationInvocationContext) context, invoker); + } + else if (operation instanceof CachePutOperation) { + return cachePutInterceptor.invoke( + (CacheOperationInvocationContext) context, invoker); + } + else if (operation instanceof CacheRemoveOperation) { + return cacheRemoveEntryInterceptor.invoke( + (CacheOperationInvocationContext) context, invoker); + } + else if (operation instanceof CacheRemoveAllOperation) { + return cacheRemoveAllInterceptor.invoke( + (CacheOperationInvocationContext) context, invoker); + } + else { + throw new IllegalArgumentException("Could not handle " + operation); + } + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheInterceptor.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheInterceptor.java new file mode 100644 index 0000000000..469b8299ef --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheInterceptor.java @@ -0,0 +1,68 @@ +/* + * 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.interceptor; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import org.aopalliance.intercept.MethodInterceptor; +import org.aopalliance.intercept.MethodInvocation; + +import org.springframework.cache.interceptor.CacheOperationInvoker; + +/** + * AOP Alliance MethodInterceptor for declarative cache + * management using JSR-107 caching annotations. + * + *

Derives from the {@link JCacheAspectSupport} class which + * contains the integration with Spring's underlying caching API. + * JCacheInterceptor simply calls the relevant superclass method. + * + *

JCacheInterceptors are thread-safe. + * + * @author Stephane Nicoll + * @see org.springframework.cache.interceptor.CacheInterceptor + */ +@SuppressWarnings("serial") +public class JCacheInterceptor extends JCacheAspectSupport + implements MethodInterceptor, Serializable { + + @Override + public Object invoke(final MethodInvocation invocation) throws Throwable { + Method method = invocation.getMethod(); + + CacheOperationInvoker aopAllianceInvoker = new CacheOperationInvoker() { + @Override + public Object invoke() { + try { + return invocation.proceed(); + } + catch (Throwable ex) { + throw new ThrowableWrapper(ex); + } + } + }; + + try { + return execute(aopAllianceInvoker, invocation.getThis(), method, invocation.getArguments()); + } + catch (CacheOperationInvoker.ThrowableWrapper th) { + throw th.getOriginal(); + } + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSource.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSource.java new file mode 100644 index 0000000000..cc96976bd5 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSource.java @@ -0,0 +1,44 @@ +/* + * 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.interceptor; + +import java.lang.reflect.Method; + +import org.springframework.cache.jcache.model.JCacheOperation; + +/** + * Interface used by {@link JCacheInterceptor}. Implementations know how to source + * cache operation attributes from standard JSR-107 annotations. + * + * @author Stephane Nicoll + * @since 4.1 + * @see org.springframework.cache.interceptor.CacheOperationSource + */ +public interface JCacheOperationSource { + + /** + * Return the cache operations for this method, or {@code null} + * if the method contains no JSR-107 related metadata. + * + * @param method the method to introspect + * @param targetClass the target class (may be {@code null}, in which case + * the declaring class of the method must be used) + * @return the cache operation for this method, or {@code null} if none found + */ + JCacheOperation getCacheOperation(Method method, Class targetClass); + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java new file mode 100644 index 0000000000..ad19123e17 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java @@ -0,0 +1,70 @@ +/* + * 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.interceptor; + +import java.io.Serializable; +import java.lang.reflect.Method; + +import org.springframework.aop.support.StaticMethodMatcherPointcut; +import org.springframework.util.ObjectUtils; + +/** + * A Pointcut that matches if the underlying {@link JCacheOperationSource} + * has an operation for a given method. + * + * @author Stephane Nicoll + * @since 4.1 + */ +@SuppressWarnings("serial") +public abstract class JCacheOperationSourcePointcut + extends StaticMethodMatcherPointcut implements Serializable { + + @Override + public boolean matches(Method method, Class targetClass) { + JCacheOperationSource cas = getCacheOperationSource(); + return (cas != null && cas.getCacheOperation(method, targetClass) != null); + } + + /** + * Obtain the underlying {@link JCacheOperationSource} (may be {@code null}). + * To be implemented by subclasses. + */ + protected abstract JCacheOperationSource getCacheOperationSource(); + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof JCacheOperationSourcePointcut)) { + return false; + } + JCacheOperationSourcePointcut otherPc = (JCacheOperationSourcePointcut) other; + return ObjectUtils.nullSafeEquals(getCacheOperationSource(), otherPc.getCacheOperationSource()); + } + + @Override + public int hashCode() { + return JCacheOperationSourcePointcut.class.hashCode(); + } + + @Override + public String toString() { + return getClass().getName() + ": " + getCacheOperationSource(); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/KeyGeneratorAdapter.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/KeyGeneratorAdapter.java new file mode 100644 index 0000000000..aa9655c1c9 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/KeyGeneratorAdapter.java @@ -0,0 +1,60 @@ +package org.springframework.cache.jcache.interceptor; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import javax.cache.annotation.CacheKeyGenerator; +import javax.cache.annotation.CacheKeyInvocationContext; + +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.cache.jcache.model.BaseKeyCacheOperation; +import org.springframework.cache.jcache.model.JCacheOperation; +import org.springframework.util.Assert; + +/** + * Spring's {@link KeyGenerator} implementation that delegates to a standard + * JSR-107 {@link javax.cache.annotation.CacheKeyGenerator}. + *

Used internally to invoke user-based JSR-107 cache key generators. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public class KeyGeneratorAdapter implements KeyGenerator { + + private final JCacheOperationSource cacheOperationSource; + + private final CacheKeyGenerator target; + + public KeyGeneratorAdapter(JCacheOperationSource cacheOperationSource, CacheKeyGenerator target) { + Assert.notNull(cacheOperationSource, "cacheOperationSource must be set."); + Assert.notNull(target, "cache key generator must be set."); + this.cacheOperationSource = cacheOperationSource; + this.target = target; + } + + /** + * Return the underlying {@link CacheKeyGenerator} that this instance is using. + */ + protected CacheKeyGenerator getTarget() { + return target; + } + + @Override + public Object generate(Object target, Method method, Object... params) { + JCacheOperation operation = cacheOperationSource.getCacheOperation(method, target.getClass()); + if (!(BaseKeyCacheOperation.class.isInstance(operation))) { + throw new IllegalStateException("Invalid operation, should be a key-based operation " + operation); + } + CacheKeyInvocationContext invocationContext = createCacheKeyInvocationContext(target, operation, params); + + return this.target.generateCacheKey(invocationContext); + } + + @SuppressWarnings("unchecked") + private CacheKeyInvocationContext createCacheKeyInvocationContext(Object target, + JCacheOperation operation, Object[] params) { + BaseKeyCacheOperation keyCacheOperation = (BaseKeyCacheOperation) operation; + return new DefaultCacheKeyInvocationContext(keyCacheOperation, target, params); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleCacheKeyGenerator.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleCacheKeyGenerator.java new file mode 100644 index 0000000000..81d27693e2 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleCacheKeyGenerator.java @@ -0,0 +1,31 @@ +package org.springframework.cache.jcache.interceptor; + +import java.lang.annotation.Annotation; + +import javax.cache.annotation.CacheInvocationParameter; +import javax.cache.annotation.CacheKeyGenerator; +import javax.cache.annotation.CacheKeyInvocationContext; +import javax.cache.annotation.GeneratedCacheKey; + +/** + * A JSR-107 compliant key generator. Uses only the parameters that have been annotated + * with {@link javax.cache.annotation.CacheKey} or all of them if none are set, except + * the {@link javax.cache.annotation.CacheValue} one. + * + * @author Stephane Nicoll + * @see 4.1 + * @see javax.cache.annotation.CacheKeyInvocationContext#getKeyParameters() + */ +public class SimpleCacheKeyGenerator implements CacheKeyGenerator { + + @Override + public GeneratedCacheKey generateCacheKey(CacheKeyInvocationContext context) { + CacheInvocationParameter[] keyParameters = context.getKeyParameters(); + final Object[] parameters = new Object[keyParameters.length]; + for (int i = 0; i < keyParameters.length; i++) { + parameters[i] = keyParameters[i].getValue(); + } + return new SimpleGeneratedCacheKey(parameters); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleExceptionCacheResolver.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleExceptionCacheResolver.java new file mode 100644 index 0000000000..edcab3a14d --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleExceptionCacheResolver.java @@ -0,0 +1,63 @@ +/* + * 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.interceptor; + +import java.util.Collection; +import java.util.Collections; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.interceptor.BasicCacheOperation; +import org.springframework.cache.interceptor.CacheOperationInvocationContext; +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.jcache.model.CacheResultOperation; +import org.springframework.util.Assert; + +/** + * A simple {@link CacheResolver} that resolves the exception cache + * based on a configurable {@link CacheManager} and the name of the + * cache: {@link CacheResultOperation#getExceptionCacheName()} + * + * @author Stephane Nicoll + * @since 4.1 + * @see org.springframework.cache.jcache.model.CacheResultOperation#getExceptionCacheName() + */ +public class SimpleExceptionCacheResolver implements CacheResolver { + + private final CacheManager cacheManager; + + public SimpleExceptionCacheResolver(CacheManager cacheManager) { + this.cacheManager = cacheManager; + } + + @Override + public Collection resolveCaches(CacheOperationInvocationContext context) { + BasicCacheOperation operation = context.getOperation(); + if (!(operation instanceof CacheResultOperation)) { + throw new IllegalStateException("Could not extract exception cache name from " + operation); + } + CacheResultOperation cacheResultOperation = (CacheResultOperation) operation; + String exceptionCacheName = cacheResultOperation.getExceptionCacheName(); + if (exceptionCacheName != null) { + Cache cache = cacheManager.getCache(exceptionCacheName); + Assert.notNull(cache, "Cannot find cache named '" + exceptionCacheName + "' for " + operation); + return Collections.singleton(cache); + } + return null; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleGeneratedCacheKey.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleGeneratedCacheKey.java new file mode 100644 index 0000000000..a1f98a75fa --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleGeneratedCacheKey.java @@ -0,0 +1,20 @@ +package org.springframework.cache.jcache.interceptor; + +import javax.cache.annotation.GeneratedCacheKey; + +import org.springframework.cache.interceptor.SimpleKey; + +/** + * A {@link SimpleKey} that implements the {@link GeneratedCacheKey} contract + * required by JSR-107 + * + * @author Stephane Nicoll + */ +@SuppressWarnings("serial") +public final class SimpleGeneratedCacheKey extends SimpleKey implements GeneratedCacheKey { + + public SimpleGeneratedCacheKey(Object... elements) { + super(elements); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/package-info.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/package-info.java new file mode 100644 index 0000000000..999738f1ac --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/package-info.java @@ -0,0 +1,8 @@ +/** + * AOP-based solution for declarative caching demarcation using JSR-107 annotations. + *

Strongly based on the infrastructure in org.springframework.cache.interceptor + * that deals with Spring's caching annotations. + *

Builds on the AOP infrastructure in org.springframework.aop.framework. + * Any POJO can be cache-advised with Spring. + */ +package org.springframework.cache.jcache.interceptor; \ No newline at end of file diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/model/BaseCacheOperation.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/model/BaseCacheOperation.java new file mode 100644 index 0000000000..993454a760 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/model/BaseCacheOperation.java @@ -0,0 +1,227 @@ +/* + * 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.model; + +import static java.util.Arrays.*; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.List; +import java.util.Set; + +import javax.cache.annotation.CacheInvocationParameter; +import javax.cache.annotation.CacheKey; +import javax.cache.annotation.CacheMethodDetails; +import javax.cache.annotation.CacheValue; + +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.util.Assert; +import org.springframework.util.filter.ExceptionTypeFilter; + +/** + * A base {@link JCacheOperation} implementation. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public abstract class BaseCacheOperation implements JCacheOperation { + + private final CacheMethodDetails methodDetails; + + private final CacheResolver cacheResolver; + + protected final List allParameterDetails; + + /** + * Create a new instance. + * @param methodDetails the {@link CacheMethodDetails} related to the cached method + * @param cacheResolver the cache resolver to resolve regular caches + */ + protected BaseCacheOperation(CacheMethodDetails methodDetails, CacheResolver cacheResolver) { + Assert.notNull(methodDetails, "method details must not be null."); + Assert.notNull(cacheResolver, "cache resolver must not be null."); + this.methodDetails = methodDetails; + this.cacheResolver = cacheResolver; + this.allParameterDetails = initializeAllParameterDetails(methodDetails.getMethod()); + } + + /** + * Return the {@link ExceptionTypeFilter} to use to filter exceptions thrown while + * invoking the method. + */ + public abstract ExceptionTypeFilter getExceptionTypeFilter(); + + @Override + public Method getMethod() { + return methodDetails.getMethod(); + } + + @Override + public Set getAnnotations() { + return methodDetails.getAnnotations(); + } + + @Override + public A getCacheAnnotation() { + return methodDetails.getCacheAnnotation(); + } + + @Override + public String getCacheName() { + return methodDetails.getCacheName(); + } + + @Override + public Set getCacheNames() { + return Collections.singleton(getCacheName()); + } + + @Override + public CacheResolver getCacheResolver() { + return cacheResolver; + } + + @Override + public CacheInvocationParameter[] getAllParameters(Object... values) { + if (allParameterDetails.size() != values.length) { + throw new IllegalStateException("Values mismatch, operation has " + + allParameterDetails.size() + " parameter(s) but got " + values.length + " value(s)"); + } + List result = new ArrayList(); + for (int i = 0; i < allParameterDetails.size(); i++) { + result.add(allParameterDetails.get(i).toCacheInvocationParameter(values[i])); + } + return result.toArray(new CacheInvocationParameter[result.size()]); + } + + protected ExceptionTypeFilter createExceptionTypeFiler(Class[] includes, + Class[] excludes) { + return new ExceptionTypeFilter(asList(includes), asList(excludes), true); + } + + + private static List initializeAllParameterDetails(Method method) { + List result = new ArrayList(); + for (int i = 0; i < method.getParameterCount(); i++) { + CacheParameterDetail detail = new CacheParameterDetail(method, i); + result.add(detail); + } + return result; + } + + @Override + public String toString() { + return getOperationDescription().append("]").toString(); + } + + /** + * Return an identifying description for this caching operation. + *

Available to subclasses, for inclusion in their {@code toString()} result. + */ + protected StringBuilder getOperationDescription() { + StringBuilder result = new StringBuilder(); + result.append(getClass().getSimpleName()); + result.append("["); + result.append(this.methodDetails); + return result; + } + + + protected static class CacheParameterDetail { + + private final Class rawType; + + private final Set annotations; + + private final int parameterPosition; + + private final boolean isKey; + + private final boolean isValue; + + private CacheParameterDetail(Method m, int parameterPosition) { + this.rawType = m.getParameterTypes()[parameterPosition]; + this.annotations = new LinkedHashSet(); + boolean foundKeyAnnotation = false; + boolean foundValueAnnotation = false; + for (Annotation annotation : m.getParameterAnnotations()[parameterPosition]) { + annotations.add(annotation); + if (CacheKey.class.isAssignableFrom(annotation.annotationType())) { + foundKeyAnnotation = true; + } + if (CacheValue.class.isAssignableFrom(annotation.annotationType())) { + foundValueAnnotation = true; + } + } + this.parameterPosition = parameterPosition; + this.isKey = foundKeyAnnotation; + this.isValue = foundValueAnnotation; + } + + public int getParameterPosition() { + return parameterPosition; + } + + protected boolean isKey() { + return isKey; + } + + protected boolean isValue() { + return isValue; + } + + public CacheInvocationParameter toCacheInvocationParameter(Object value) { + return new CacheInvocationParameterImpl(this, value); + } + } + + protected static class CacheInvocationParameterImpl implements CacheInvocationParameter { + + private final CacheParameterDetail detail; + + private final Object value; + + private CacheInvocationParameterImpl(CacheParameterDetail detail, Object value) { + this.detail = detail; + this.value = value; + } + + @Override + public Class getRawType() { + return detail.rawType; + } + + @Override + public Object getValue() { + return value; + } + + @Override + public Set getAnnotations() { + return detail.annotations; + } + + @Override + public int getParameterPosition() { + return detail.parameterPosition; + } + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/model/BaseKeyCacheOperation.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/model/BaseKeyCacheOperation.java new file mode 100644 index 0000000000..b32e55f716 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/model/BaseKeyCacheOperation.java @@ -0,0 +1,101 @@ +/* + * 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.model; + +import java.lang.annotation.Annotation; +import java.util.ArrayList; +import java.util.List; + +import javax.cache.annotation.CacheInvocationParameter; +import javax.cache.annotation.CacheMethodDetails; + +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.interceptor.KeyGenerator; + +/** + * A base {@link JCacheOperation} that operates with a key. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public abstract class BaseKeyCacheOperation extends BaseCacheOperation { + + private final KeyGenerator keyGenerator; + + private final List keyParameterDetails; + + /** + * Create a new instance. + * @param methodDetails the {@link CacheMethodDetails} related to the cached method + * @param cacheResolver the cache resolver to resolve regular caches + * @param keyGenerator the key generator to compute cache keys + */ + protected BaseKeyCacheOperation(CacheMethodDetails methodDetails, + CacheResolver cacheResolver, KeyGenerator keyGenerator) { + super(methodDetails, cacheResolver); + this.keyGenerator = keyGenerator; + this.keyParameterDetails = initializeKeyParameterDetails(allParameterDetails); + } + + /** + * Return the {@link KeyGenerator} to use to compute cache keys. + */ + public KeyGenerator getKeyGenerator() { + return keyGenerator; + } + + /** + * Return the {@link CacheInvocationParameter} for the parameters that are to be + * used to compute the key. + *

Per the spec, if some method parameters are annotated with + * {@link javax.cache.annotation.CacheKey}, only those parameters should be part + * of the key. If none are annotated, all parameters except the parameter annotated + * with {@link javax.cache.annotation.CacheValue} should be part of the key. + *

The method arguments must match the signature of the related method invocation + * @param values the parameters value for a particular invocation + * @return the {@link CacheInvocationParameter} instances for the parameters to be + * used to compute the key + */ + public CacheInvocationParameter[] getKeyParameters(Object... values) { + List result = new ArrayList(); + for (CacheParameterDetail keyParameterDetail : keyParameterDetails) { + int parameterPosition = keyParameterDetail.getParameterPosition(); + if (parameterPosition >= values.length) { + throw new IllegalStateException("Values mismatch, key parameter at position " + + parameterPosition + " cannot be matched against " + values.length + " value(s)"); + } + result.add(keyParameterDetail.toCacheInvocationParameter(values[parameterPosition])); + } + return result.toArray(new CacheInvocationParameter[result.size()]); + } + + + private static List initializeKeyParameterDetails(List allParameters) { + List all = new ArrayList(); + List annotated = new ArrayList(); + for (CacheParameterDetail allParameter : allParameters) { + if (!allParameter.isValue()) { + all.add(allParameter); + } + if (allParameter.isKey()) { + annotated.add(allParameter); + } + } + return annotated.size() == 0 ? all : annotated; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/model/CachePutOperation.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/model/CachePutOperation.java new file mode 100644 index 0000000000..35e958f46f --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/model/CachePutOperation.java @@ -0,0 +1,102 @@ +/* + * 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.model; + +import java.lang.reflect.Method; +import java.util.List; + +import javax.cache.annotation.CacheInvocationParameter; +import javax.cache.annotation.CacheMethodDetails; +import javax.cache.annotation.CachePut; + +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.util.filter.ExceptionTypeFilter; + +/** + * The {@link JCacheOperation} implementation for a {@link CachePut} operation. + * + * @author Stephane Nicoll + * @since 4.1 + * @see CachePut + */ +public class CachePutOperation extends BaseKeyCacheOperation { + + private final ExceptionTypeFilter exceptionTypeFilter; + + private final CacheParameterDetail valueParameterDetail; + + public CachePutOperation(CacheMethodDetails methodDetails, + CacheResolver cacheResolver, KeyGenerator keyGenerator) { + super(methodDetails, cacheResolver, keyGenerator); + CachePut ann = methodDetails.getCacheAnnotation(); + this.exceptionTypeFilter = createExceptionTypeFiler(ann.cacheFor(), ann.noCacheFor()); + this.valueParameterDetail = initializeValueParameterDetail(methodDetails.getMethod(), allParameterDetails); + if (valueParameterDetail == null) { + throw new IllegalArgumentException("No parameter annotated with @CacheValue was found for " + + "" + methodDetails.getMethod()); + } + } + + @Override + public ExceptionTypeFilter getExceptionTypeFilter() { + return exceptionTypeFilter; + } + + /** + * Specify if the cache should be updated before invoking the method. By default, + * the cache is updated after the method invocation. + * @see javax.cache.annotation.CachePut#afterInvocation() + */ + public boolean isEarlyPut() { + return !getCacheAnnotation().afterInvocation(); + } + + /** + * Return the {@link CacheInvocationParameter} for the parameter holding the value + * to cache. + *

The method arguments must match the signature of the related method invocation + * @param values the parameters value for a particular invocation + * @return the {@link CacheInvocationParameter} instance for the value parameter + */ + public CacheInvocationParameter getValueParameter(Object... values) { + int parameterPosition = valueParameterDetail.getParameterPosition(); + if (parameterPosition >= values.length) { + throw new IllegalStateException("Values mismatch, value parameter at position " + + parameterPosition + " cannot be matched against " + values.length + " value(s)"); + } + return valueParameterDetail.toCacheInvocationParameter(values[parameterPosition]); + } + + + private static CacheParameterDetail initializeValueParameterDetail(Method method, + List allParameters) { + CacheParameterDetail result = null; + for (CacheParameterDetail parameter : allParameters) { + if (parameter.isValue()) { + if (result == null) { + result = parameter; + } + else { + throw new IllegalArgumentException("More than one @CacheValue found on " + method + ""); + } + } + } + return result; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/model/CacheRemoveAllOperation.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/model/CacheRemoveAllOperation.java new file mode 100644 index 0000000000..dc524197e1 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/model/CacheRemoveAllOperation.java @@ -0,0 +1,56 @@ +/* + * 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.model; + +import javax.cache.annotation.CacheMethodDetails; +import javax.cache.annotation.CacheRemoveAll; + +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.util.filter.ExceptionTypeFilter; + +/** + * The {@link JCacheOperation} implementation for a {@link CacheRemoveAll} operation. + * + * @author Stephane Nicoll + * @since 4.1 + * @see CacheRemoveAll + */ +public class CacheRemoveAllOperation extends BaseCacheOperation { + + private final ExceptionTypeFilter exceptionTypeFilter; + + public CacheRemoveAllOperation(CacheMethodDetails methodDetails, CacheResolver cacheResolver) { + super(methodDetails, cacheResolver); + CacheRemoveAll ann = methodDetails.getCacheAnnotation(); + this.exceptionTypeFilter = createExceptionTypeFiler(ann.evictFor(), ann.noEvictFor()); + } + + @Override + public ExceptionTypeFilter getExceptionTypeFilter() { + return exceptionTypeFilter; + } + + /** + * Specify if the cache should be cleared before invoking the method. By default, the + * cache is cleared after the method invocation. + * @see javax.cache.annotation.CacheRemoveAll#afterInvocation() + */ + public boolean isEarlyRemove() { + return !getCacheAnnotation().afterInvocation(); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/model/CacheRemoveOperation.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/model/CacheRemoveOperation.java new file mode 100644 index 0000000000..45280d113a --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/model/CacheRemoveOperation.java @@ -0,0 +1,58 @@ +/* + * 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.model; + +import javax.cache.annotation.CacheMethodDetails; +import javax.cache.annotation.CacheRemove; + +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.util.filter.ExceptionTypeFilter; + +/** + * The {@link JCacheOperation} implementation for a {@link CacheRemove} operation. + * + * @author Stephane Nicoll + * @since 4.1 + * @see CacheRemove + */ +public class CacheRemoveOperation extends BaseKeyCacheOperation { + + private final ExceptionTypeFilter exceptionTypeFilter; + + public CacheRemoveOperation(CacheMethodDetails methodDetails, + CacheResolver cacheResolver, KeyGenerator keyGenerator) { + super(methodDetails, cacheResolver, keyGenerator); + CacheRemove ann = methodDetails.getCacheAnnotation(); + this.exceptionTypeFilter = createExceptionTypeFiler(ann.evictFor(), ann.noEvictFor()); + } + + @Override + public ExceptionTypeFilter getExceptionTypeFilter() { + return exceptionTypeFilter; + } + + /** + * Specify if the cache entry should be remove before invoking the method. By default, the + * cache entry is removed after the method invocation. + * @see javax.cache.annotation.CacheRemove#afterInvocation() + */ + public boolean isEarlyRemove() { + return !getCacheAnnotation().afterInvocation(); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/model/CacheResultOperation.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/model/CacheResultOperation.java new file mode 100644 index 0000000000..0ced866948 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/model/CacheResultOperation.java @@ -0,0 +1,84 @@ +/* + * 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.model; + +import javax.cache.annotation.CacheMethodDetails; +import javax.cache.annotation.CacheResult; + +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.util.StringUtils; +import org.springframework.util.filter.ExceptionTypeFilter; + +/** + * The {@link JCacheOperation} implementation for a {@link CacheResult} operation. + * + * @author Stephane Nicoll + * @since 4.1 + * @see CacheResult + */ +public class CacheResultOperation extends BaseKeyCacheOperation { + + private final ExceptionTypeFilter exceptionTypeFilter; + + private final CacheResolver exceptionCacheResolver; + + private final String exceptionCacheName; + + public CacheResultOperation(CacheMethodDetails methodDetails, + CacheResolver cacheResolver, KeyGenerator keyGenerator, + CacheResolver exceptionCacheResolver) { + super(methodDetails, cacheResolver, keyGenerator); + CacheResult ann = methodDetails.getCacheAnnotation(); + this.exceptionTypeFilter = createExceptionTypeFiler(ann.cachedExceptions(), ann.nonCachedExceptions()); + this.exceptionCacheResolver = exceptionCacheResolver; + String exceptionCacheNameCandidate = ann.exceptionCacheName(); + this.exceptionCacheName = StringUtils.hasText(exceptionCacheNameCandidate) ? exceptionCacheNameCandidate : null; + } + + @Override + public ExceptionTypeFilter getExceptionTypeFilter() { + return exceptionTypeFilter; + } + + /** + * Specify if the method should always be invoked regardless of a cache hit. By + * default, the method is only invoked in case of a cache miss. + * @see javax.cache.annotation.CacheResult#skipGet() + */ + public boolean isAlwaysInvoked() { + return getCacheAnnotation().skipGet(); + } + + /** + * Return the {@link CacheResolver} instance to use to resolve the cache to + * use for matching exceptions thrown by this operation. + */ + public CacheResolver getExceptionCacheResolver() { + return exceptionCacheResolver; + } + + /** + * Return the name of the cache to cache exceptions. Return {@link null} if + * caching exceptions should be disabled. + * @see javax.cache.annotation.CacheResult#exceptionCacheName() + */ + public String getExceptionCacheName() { + return exceptionCacheName; + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/model/DefaultCacheMethodDetails.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/model/DefaultCacheMethodDetails.java new file mode 100644 index 0000000000..d77760473b --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/model/DefaultCacheMethodDetails.java @@ -0,0 +1,84 @@ +/* + * 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.model; + +import static java.util.Arrays.*; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Collections; +import java.util.LinkedHashSet; +import java.util.Set; + +import javax.cache.annotation.CacheMethodDetails; + +/** + * The default {@link CacheMethodDetails} implementation. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public class DefaultCacheMethodDetails implements CacheMethodDetails { + + private final Method method; + + private final Set annotations; + + private final A cacheAnnotation; + + private final String cacheName; + + public DefaultCacheMethodDetails(Method method, A cacheAnnotation, + String cacheName) { + this.method = method; + this.annotations = Collections.unmodifiableSet( + new LinkedHashSet(asList(method.getAnnotations()))); + this.cacheAnnotation = cacheAnnotation; + this.cacheName = cacheName; + } + + @Override + public Method getMethod() { + return method; + } + + @Override + public Set getAnnotations() { + return annotations; + } + + @Override + public A getCacheAnnotation() { + return cacheAnnotation; + } + + @Override + public String getCacheName() { + return cacheName; + } + + @Override + public String toString() { + final StringBuilder sb = new StringBuilder("details["); + sb.append("method=").append(method); + sb.append(", cacheAnnotation=").append(cacheAnnotation); + sb.append(", cacheName='").append(cacheName).append('\''); + sb.append(']'); + return sb.toString(); + } + +} diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/model/JCacheOperation.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/model/JCacheOperation.java new file mode 100644 index 0000000000..fd65033a4d --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/model/JCacheOperation.java @@ -0,0 +1,53 @@ +/* + * 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.model; + +import java.lang.annotation.Annotation; + +import javax.cache.annotation.CacheInvocationParameter; +import javax.cache.annotation.CacheMethodDetails; + +import org.springframework.cache.interceptor.BasicCacheOperation; +import org.springframework.cache.interceptor.CacheResolver; + +/** + * Model the base of JSR-107 cache operation. + *

A cache operation can be statically cached as it does not contain + * any runtime operation of a specific cache invocation. + * + * @param the type of the JSR-107 annotation + * @author Stephane Nicoll + * @since 4.1 + */ +public interface JCacheOperation + extends CacheMethodDetails, BasicCacheOperation { + + /** + * Return the {@link CacheResolver} instance to use to resolve the cache to + * use for this operation. + */ + CacheResolver getCacheResolver(); + + /** + * Return the {@link CacheInvocationParameter} instances based on the specified + * method arguments. + *

The method arguments must match the signature of the related method invocation + * @param values the parameters value for a particular invocation + */ + CacheInvocationParameter[] getAllParameters(Object... values); + +} \ No newline at end of file diff --git a/spring-context-support/src/main/java/org/springframework/cache/jcache/model/package-info.java b/spring-context-support/src/main/java/org/springframework/cache/jcache/model/package-info.java new file mode 100644 index 0000000000..cb6fa53e26 --- /dev/null +++ b/spring-context-support/src/main/java/org/springframework/cache/jcache/model/package-info.java @@ -0,0 +1,5 @@ +/** + * Resolved model of JSR-107 cache annotations. Used internally by the interceptors + * in org.springframework.cache.jcache.interceptor. + */ +package org.springframework.cache.jcache.model; \ No newline at end of file diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/AbstractJCacheTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/AbstractJCacheTests.java new file mode 100644 index 0000000000..19c65efca6 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/AbstractJCacheTests.java @@ -0,0 +1,66 @@ +/* + * 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; + +import java.util.ArrayList; +import java.util.List; + +import org.junit.Rule; +import org.junit.rules.ExpectedException; +import org.junit.rules.TestName; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.concurrent.ConcurrentMapCache; +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.cache.interceptor.SimpleCacheResolver; +import org.springframework.cache.interceptor.SimpleKeyGenerator; +import org.springframework.cache.jcache.interceptor.SimpleExceptionCacheResolver; +import org.springframework.cache.support.SimpleCacheManager; + +/** + * @author Stephane Nicoll + */ +public abstract class AbstractJCacheTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Rule + public final TestName name = new TestName(); + + protected final CacheManager cacheManager = createSimpleCacheManager("default", "simpleCache"); + + protected final CacheResolver defaultCacheResolver = new SimpleCacheResolver(cacheManager); + + protected final CacheResolver defaultExceptionCacheResolver = new SimpleExceptionCacheResolver(cacheManager); + + protected final KeyGenerator defaultKeyGenerator = new SimpleKeyGenerator(); + + protected static CacheManager createSimpleCacheManager(String... cacheNames) { + SimpleCacheManager result = new SimpleCacheManager(); + List caches = new ArrayList(); + for (String cacheName : cacheNames) { + caches.add(new ConcurrentMapCache(cacheName)); + } + result.setCaches(caches); + result.afterPropertiesSet(); + return result; + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/AbstractJCacheAnnotationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/AbstractJCacheAnnotationTests.java new file mode 100644 index 0000000000..bfffbad945 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/AbstractJCacheAnnotationTests.java @@ -0,0 +1,533 @@ +/* + * 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; + +import static org.junit.Assert.*; + +import java.util.concurrent.ConcurrentHashMap; + +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.TestName; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.jcache.interceptor.SimpleGeneratedCacheKey; +import org.springframework.context.ApplicationContext; + +/** + * @author Stephane Nicoll + */ +public abstract class AbstractJCacheAnnotationTests { + + public static final String DEFAULT_CACHE = "default"; + + public static final String EXCEPTION_CACHE = "exception"; + + @Rule + public final TestName name = new TestName(); + + private JCacheableService service; + + private CacheManager cacheManager; + + protected abstract ApplicationContext getApplicationContext(); + + @Before + public void setUp() { + ApplicationContext context = getApplicationContext(); + service = context.getBean(JCacheableService.class); + cacheManager = context.getBean("cacheManager", CacheManager.class); + } + + @Test + public void cache() { + String keyItem = name.getMethodName(); + + Object first = service.cache(keyItem); + Object second = service.cache(keyItem); + assertSame(first, second); + } + + @Test + public void cacheException() { + String keyItem = name.getMethodName(); + Cache cache = getCache(EXCEPTION_CACHE); + + Object key = createKey(keyItem); + assertNull(cache.get(key)); + + try { + service.cacheWithException(keyItem, true); + fail("Should have thrown an exception"); + } + catch (UnsupportedOperationException e) { + // This is what we expect + } + + Cache.ValueWrapper result = cache.get(key); + assertNotNull(result); + assertEquals(UnsupportedOperationException.class, result.get().getClass()); + } + + @Test + public void cacheExceptionVetoed() { + String keyItem = name.getMethodName(); + Cache cache = getCache(EXCEPTION_CACHE); + + Object key = createKey(keyItem); + assertNull(cache.get(key)); + + try { + service.cacheWithException(keyItem, false); + fail("Should have thrown an exception"); + } + catch (NullPointerException e) { + // This is what we expect + } + assertNull(cache.get(key)); + } + + @SuppressWarnings("ThrowableResultOfMethodCallIgnored") + @Test + public void cacheExceptionRewriteCallStack() { + final String keyItem = name.getMethodName(); + + UnsupportedOperationException first = null; + long ref = service.exceptionInvocations(); + try { + service.cacheWithException(keyItem, true); + fail("Should have thrown an exception"); + } + catch (UnsupportedOperationException e) { + first = e; + } + // Sanity check, this particular call has called the service + assertEquals("First call should not have been cached", ref + 1, service.exceptionInvocations()); + + UnsupportedOperationException second = methodInCallStack(keyItem); + // Sanity check, this particular call has *not* called the service + assertEquals("Second call should have been cached", ref + 1, service.exceptionInvocations()); + + assertEquals(first.getCause(), second.getCause()); + assertEquals(first.getMessage(), second.getMessage()); + assertFalse("Original stack must not contain any reference to methodInCallStack", + contain(first, AbstractJCacheAnnotationTests.class.getName(), "methodInCallStack")); + assertTrue("Cached stack should have been rewritten with a reference to methodInCallStack", + contain(second, AbstractJCacheAnnotationTests.class.getName(), "methodInCallStack")); + } + + @Test + public void cacheAlwaysInvoke() { + String keyItem = name.getMethodName(); + + Object first = service.cacheAlwaysInvoke(keyItem); + Object second = service.cacheAlwaysInvoke(keyItem); + assertNotSame(first, second); + } + + @Test + public void cacheWithPartialKey() { + String keyItem = name.getMethodName(); + + Object first = service.cacheWithPartialKey(keyItem, true); + Object second = service.cacheWithPartialKey(keyItem, false); + assertSame(first, second); // second argument not used, see config + } + + @Test + public void cacheWithCustomCacheResolver() { + String keyItem = name.getMethodName(); + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(keyItem); + service.cacheWithCustomCacheResolver(keyItem); + + assertNull(cache.get(key)); // Cache in mock cache + } + + @Test + public void cacheWithCustomKeyGenerator() { + String keyItem = name.getMethodName(); + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(keyItem); + service.cacheWithCustomKeyGenerator(keyItem, "ignored"); + + assertNull(cache.get(key)); + } + + @Test + public void put() { + String keyItem = name.getMethodName(); + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(keyItem); + Object value = new Object(); + assertNull(cache.get(key)); + + service.put(keyItem, value); + + Cache.ValueWrapper result = cache.get(key); + assertNotNull(result); + assertEquals(value, result.get()); + } + + @Test + public void putWithException() { + String keyItem = name.getMethodName(); + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(keyItem); + Object value = new Object(); + assertNull(cache.get(key)); + + try { + service.putWithException(keyItem, value, true); + fail("Should have thrown an exception"); + } + catch (UnsupportedOperationException e) { + // This is what we expect + } + + Cache.ValueWrapper result = cache.get(key); + assertNotNull(result); + assertEquals(value, result.get()); + } + + @Test + public void putWithExceptionVetoPut() { + String keyItem = name.getMethodName(); + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(keyItem); + Object value = new Object(); + assertNull(cache.get(key)); + + try { + service.putWithException(keyItem, value, false); + fail("Should have thrown an exception"); + } + catch (NullPointerException e) { + // This is what we expect + } + assertNull(cache.get(key)); + } + + @Test + public void earlyPut() { + String keyItem = name.getMethodName(); + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(keyItem); + Object value = new Object(); + assertNull(cache.get(key)); + + service.earlyPut(keyItem, value); + + Cache.ValueWrapper result = cache.get(key); + assertNotNull(result); + assertEquals(value, result.get()); + } + + @Test + public void earlyPutWithException() { + String keyItem = name.getMethodName(); + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(keyItem); + Object value = new Object(); + assertNull(cache.get(key)); + + try { + service.earlyPutWithException(keyItem, value, true); + fail("Should have thrown an exception"); + } + catch (UnsupportedOperationException e) { + // This is what we expect + } + + Cache.ValueWrapper result = cache.get(key); + assertNotNull(result); + assertEquals(value, result.get()); + } + + @Test + public void earlyPutWithExceptionVetoPut() { + String keyItem = name.getMethodName(); + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(keyItem); + Object value = new Object(); + assertNull(cache.get(key)); + + try { + service.earlyPutWithException(keyItem, value, false); + fail("Should have thrown an exception"); + } + catch (NullPointerException e) { + // This is what we expect + } + // This will be cached anyway as the earlyPut has updated the cache before + Cache.ValueWrapper result = cache.get(key); + assertNotNull(result); + assertEquals(value, result.get()); + } + + @Test + public void remove() { + String keyItem = name.getMethodName(); + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(keyItem); + Object value = new Object(); + cache.put(key, value); + + service.remove(keyItem); + + assertNull(cache.get(key)); + } + + @Test + public void removeWithException() { + String keyItem = name.getMethodName(); + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(keyItem); + Object value = new Object(); + cache.put(key, value); + + try { + service.removeWithException(keyItem, true); + fail("Should have thrown an exception"); + } + catch (UnsupportedOperationException e) { + // This is what we expect + } + + assertNull(cache.get(key)); + } + + @Test + public void removeWithExceptionVetoRemove() { + String keyItem = name.getMethodName(); + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(keyItem); + Object value = new Object(); + cache.put(key, value); + + try { + service.removeWithException(keyItem, false); + fail("Should have thrown an exception"); + } + catch (NullPointerException e) { + // This is what we expect + } + Cache.ValueWrapper wrapper = cache.get(key); + assertNotNull(wrapper); + assertEquals(value, wrapper.get()); + } + + @Test + public void earlyRemove() { + String keyItem = name.getMethodName(); + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(keyItem); + Object value = new Object(); + cache.put(key, value); + + service.earlyRemove(keyItem); + + assertNull(cache.get(key)); + } + + @Test + public void earlyRemoveWithException() { + String keyItem = name.getMethodName(); + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(keyItem); + Object value = new Object(); + cache.put(key, value); + + try { + service.earlyRemoveWithException(keyItem, true); + fail("Should have thrown an exception"); + } + catch (UnsupportedOperationException e) { + // This is what we expect + } + assertNull(cache.get(key)); + } + + @Test + public void earlyRemoveWithExceptionVetoRemove() { + String keyItem = name.getMethodName(); + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(keyItem); + Object value = new Object(); + cache.put(key, value); + + try { + service.earlyRemoveWithException(keyItem, false); + fail("Should have thrown an exception"); + } + catch (NullPointerException e) { + // This is what we expect + } + // This will be remove anyway as the earlyRemove has removed the cache before + assertNull(cache.get(key)); + } + + @Test + public void removeAll() { + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(name.getMethodName()); + cache.put(key, new Object()); + + service.removeAll(); + + assertTrue(isEmpty(cache)); + } + + @Test + public void removeAllWithException() { + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(name.getMethodName()); + cache.put(key, new Object()); + + try { + service.removeAllWithException(true); + fail("Should have thrown an exception"); + } + catch (UnsupportedOperationException e) { + // This is what we expect + } + + assertTrue(isEmpty(cache)); + } + + @Test + public void removeAllWithExceptionVetoRemove() { + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(name.getMethodName()); + cache.put(key, new Object()); + + try { + service.removeAllWithException(false); + fail("Should have thrown an exception"); + } + catch (NullPointerException e) { + // This is what we expect + } + assertNotNull(cache.get(key)); + } + + @Test + public void earlyRemoveAll() { + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(name.getMethodName()); + cache.put(key, new Object()); + + service.earlyRemoveAll(); + + assertTrue(isEmpty(cache)); + } + + @Test + public void earlyRemoveAllWithException() { + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(name.getMethodName()); + cache.put(key, new Object()); + + try { + service.earlyRemoveAllWithException(true); + fail("Should have thrown an exception"); + } + catch (UnsupportedOperationException e) { + // This is what we expect + } + assertTrue(isEmpty(cache)); + } + + @Test + public void earlyRemoveAllWithExceptionVetoRemove() { + Cache cache = getCache(DEFAULT_CACHE); + + Object key = createKey(name.getMethodName()); + cache.put(key, new Object()); + + try { + service.earlyRemoveAllWithException(false); + fail("Should have thrown an exception"); + } + catch (NullPointerException e) { + // This is what we expect + } + // This will be remove anyway as the earlyRemove has removed the cache before + assertTrue(isEmpty(cache)); + } + + protected boolean isEmpty(Cache cache) { + ConcurrentHashMap nativeCache = (ConcurrentHashMap) cache.getNativeCache(); + return nativeCache.isEmpty(); + } + + + private Object createKey(Object... params) { + return new SimpleGeneratedCacheKey(params); + } + + private Cache getCache(String name) { + Cache cache = cacheManager.getCache(name); + assertNotNull("required cache " + name + " does not exist", cache); + return cache; + } + + /** + * The only purpose of this method is to invoke a particular method on the + * service so that the call stack is different. + */ + private UnsupportedOperationException methodInCallStack(String keyItem) { + try { + service.cacheWithException(keyItem, true); + throw new IllegalStateException("Should have thrown an exception"); + } + catch (UnsupportedOperationException e) { + return e; + } + } + + private boolean contain(Throwable t, String className, String methodName) { + for (StackTraceElement element : t.getStackTrace()) { + if (className.equals(element.getClassName()) && methodName.equals(element.getMethodName())) { + return true; + } + } + return false; + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheJavaConfigTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheJavaConfigTests.java new file mode 100644 index 0000000000..a15b877114 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheJavaConfigTests.java @@ -0,0 +1,121 @@ +/* + * 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; + +import static org.junit.Assert.*; + +import java.util.Arrays; + +import org.junit.Test; + +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.interceptor.CacheResolver; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.cache.interceptor.SimpleCacheResolver; +import org.springframework.cache.interceptor.SimpleKeyGenerator; +import org.springframework.cache.jcache.interceptor.AnnotatedJCacheableService; +import org.springframework.cache.jcache.interceptor.DefaultJCacheOperationSource; +import org.springframework.cache.support.NoOpCacheManager; +import org.springframework.cache.support.SimpleCacheManager; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * @author Stephane Nicoll + */ +public class JCacheJavaConfigTests extends AbstractJCacheAnnotationTests { + + @Override + protected ApplicationContext getApplicationContext() { + return new AnnotationConfigApplicationContext(EnableCachingConfig.class); + } + + @Test + public void fullCachingConfig() throws Exception { + AnnotationConfigApplicationContext context = + new AnnotationConfigApplicationContext(FullCachingConfig.class); + DefaultJCacheOperationSource cos = context.getBean(DefaultJCacheOperationSource.class); + assertSame(context.getBean(KeyGenerator.class), cos.getDefaultKeyGenerator()); + assertSame(context.getBean("cacheResolver", CacheResolver.class), + cos.getDefaultCacheResolver()); + assertSame(context.getBean("exceptionCacheResolver", CacheResolver.class), + cos.getDefaultExceptionCacheResolver()); + } + + + @Configuration + @EnableCaching + 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 JCacheableService cacheableService() { + return new AnnotatedJCacheableService(defaultCache()); + } + + @Bean + public Cache defaultCache() { + return new ConcurrentMapCache("default"); + } + } + + @Configuration + @EnableCaching + public static class FullCachingConfig implements JCacheConfigurer { + + + @Override + @Bean + public CacheManager cacheManager() { + return new NoOpCacheManager(); + } + + @Override + @Bean + public KeyGenerator keyGenerator() { + return new SimpleKeyGenerator(); + } + + @Override + @Bean + public CacheResolver cacheResolver() { + return new SimpleCacheResolver(cacheManager()); + } + + @Override + @Bean + public CacheResolver exceptionCacheResolver() { + return new SimpleCacheResolver(cacheManager()); + } + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheNamespaceDrivenTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheNamespaceDrivenTests.java new file mode 100644 index 0000000000..3cff4bc9d3 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheNamespaceDrivenTests.java @@ -0,0 +1,33 @@ +/* + * 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; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.GenericXmlApplicationContext; + +/** + * @author Stephane Nicoll + */ +public class JCacheNamespaceDrivenTests extends AbstractJCacheAnnotationTests { + + @Override + protected ApplicationContext getApplicationContext() { + return new GenericXmlApplicationContext( + "/org/springframework/cache/jcache/config/jCacheNamespaceDriven.xml"); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheStandaloneConfigTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheStandaloneConfigTests.java new file mode 100644 index 0000000000..82a6e40e14 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheStandaloneConfigTests.java @@ -0,0 +1,33 @@ +/* + * 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; + +import org.springframework.context.ApplicationContext; +import org.springframework.context.support.GenericXmlApplicationContext; + +/** + * @author Stephane Nicoll + */ +public class JCacheStandaloneConfigTests extends AbstractJCacheAnnotationTests { + + @Override + protected ApplicationContext getApplicationContext() { + return new GenericXmlApplicationContext( + "/org/springframework/cache/jcache/config/jCacheStandaloneConfig.xml"); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheableService.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheableService.java new file mode 100644 index 0000000000..8b5709ef4a --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheableService.java @@ -0,0 +1,62 @@ +/* + * 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; + +/** + * @author Stephane Nicoll + */ +public interface JCacheableService { + + T cache(String id); + + T cacheWithException(String id, boolean matchFilter); + + T cacheAlwaysInvoke(String id); + + T cacheWithPartialKey(String id, boolean notUsed); + + T cacheWithCustomCacheResolver(String id); + + T cacheWithCustomKeyGenerator(String id, String anotherId); + + void put(String id, Object value); + + void putWithException(String id, Object value, boolean matchFilter); + + void earlyPut(String id, Object value); + + void earlyPutWithException(String id, Object value, boolean matchFilter); + + void remove(String id); + + void removeWithException(String id, boolean matchFilter); + + void earlyRemove(String id); + + void earlyRemoveWithException(String id, boolean matchFilter); + + void removeAll(); + + void removeAllWithException(boolean matchFilter); + + void earlyRemoveAll(); + + void earlyRemoveAllWithException(boolean matchFilter); + + long exceptionInvocations(); + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AnnotatedJCacheableService.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AnnotatedJCacheableService.java new file mode 100644 index 0000000000..94c84a202f --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AnnotatedJCacheableService.java @@ -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.jcache.interceptor; + +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; + +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 org.springframework.cache.Cache; +import org.springframework.cache.jcache.config.JCacheableService; +import org.springframework.cache.jcache.support.TestableCacheKeyGenerator; +import org.springframework.cache.jcache.support.TestableCacheResolverFactory; + + +/** + * Repository sample with a @CacheDefaults annotation + * + * @author Stephane Nicoll + */ +@CacheDefaults(cacheName = "default") +public class AnnotatedJCacheableService implements JCacheableService { + + 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 + ")"); + } + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AnnotationCacheOperationSourceTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AnnotationCacheOperationSourceTests.java new file mode 100644 index 0000000000..ad16df23e3 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AnnotationCacheOperationSourceTests.java @@ -0,0 +1,262 @@ +/* + * 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.interceptor; + +import static org.junit.Assert.*; +import static org.mockito.BDDMockito.*; +import static org.mockito.Mockito.mock; + +import java.lang.reflect.Method; +import java.util.Comparator; + +import javax.cache.annotation.CacheDefaults; +import javax.cache.annotation.CacheKeyGenerator; +import javax.cache.annotation.CacheResult; + +import org.junit.Before; +import org.junit.Test; + +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.cache.jcache.AbstractJCacheTests; +import org.springframework.cache.jcache.model.BaseKeyCacheOperation; +import org.springframework.cache.jcache.model.CachePutOperation; +import org.springframework.cache.jcache.model.CacheRemoveAllOperation; +import org.springframework.cache.jcache.model.CacheRemoveOperation; +import org.springframework.cache.jcache.model.CacheResultOperation; +import org.springframework.cache.jcache.model.JCacheOperation; +import org.springframework.cache.jcache.support.TestableCacheKeyGenerator; +import org.springframework.cache.jcache.support.TestableCacheResolver; +import org.springframework.cache.jcache.support.TestableCacheResolverFactory; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +/** + * @author Stephane Nicoll + */ +public class AnnotationCacheOperationSourceTests extends AbstractJCacheTests { + + private final DefaultJCacheOperationSource source = new DefaultJCacheOperationSource(); + + private final StaticApplicationContext applicationContext = new StaticApplicationContext(); + + @Before + public void setUp() { + source.setApplicationContext(applicationContext); + source.setKeyGenerator(defaultKeyGenerator); + source.setCacheResolver(defaultCacheResolver); + source.setExceptionCacheResolver(defaultExceptionCacheResolver); + source.afterPropertiesSet(); + } + + @Test + public void cache() { + CacheResultOperation op = getDefaultCacheOperation(CacheResultOperation.class, String.class); + assertDefaults(op); + assertNull("Exception caching not enabled so resolver should not be set", op.getExceptionCacheResolver()); + } + + @Test + public void cacheWithException() { + CacheResultOperation op = getDefaultCacheOperation(CacheResultOperation.class, String.class, boolean.class); + assertDefaults(op); + assertEquals(defaultExceptionCacheResolver, op.getExceptionCacheResolver()); + assertEquals("exception", op.getExceptionCacheName()); + } + + @Test + public void put() { + CachePutOperation op = getDefaultCacheOperation(CachePutOperation.class, String.class, Object.class); + assertDefaults(op); + } + + @Test + public void remove() { + CacheRemoveOperation op = getDefaultCacheOperation(CacheRemoveOperation.class, String.class); + assertDefaults(op); + } + + @Test + public void removeAll() { + CacheRemoveAllOperation op = getDefaultCacheOperation(CacheRemoveAllOperation.class); + assertEquals(defaultCacheResolver, op.getCacheResolver()); + } + + @Test + public void noAnnotation() { + assertNull(getCacheOperation(AnnotatedJCacheableService.class, name.getMethodName())); + } + + @Test + public void multiAnnotations() { + thrown.expect(IllegalStateException.class); + getCacheOperation(AnnotatedJCacheableService.class, name.getMethodName()); + } + + @Test + public void defaultCacheNameWithCandidate() { + Method m = ReflectionUtils.findMethod(Object.class, "toString"); + assertEquals("foo", source.determineCacheName(m, null, "foo")); + } + + @Test + public void defaultCacheNameWithDefaults() { + Method m = ReflectionUtils.findMethod(Object.class, "toString"); + CacheDefaults mock = mock(CacheDefaults.class); + given(mock.cacheName()).willReturn(""); + assertEquals("java.lang.Object.toString()", source.determineCacheName(m, mock, "")); + } + + @Test + public void defaultCacheNameNoDefaults() { + Method m = ReflectionUtils.findMethod(Object.class, "toString"); + assertEquals("java.lang.Object.toString()", source.determineCacheName(m, null, "")); + } + + @Test + public void defaultCacheNameWithParameters() { + Method m = ReflectionUtils.findMethod(Comparator.class, "compare", Object.class, Object.class); + assertEquals("java.util.Comparator.compare(java.lang.Object,java.lang.Object)", + source.determineCacheName(m, null, "")); + } + + @Test + public void customCacheResolver() { + CacheResultOperation operation = + getCacheOperation(CacheResultOperation.class, CustomService.class, name.getMethodName(), Long.class); + assertJCacheResolver(operation.getCacheResolver(), TestableCacheResolver.class); + assertJCacheResolver(operation.getExceptionCacheResolver(), null); + assertEquals(defaultKeyGenerator, operation.getKeyGenerator()); + } + + @Test + public void customKeyGenerator() { + CacheResultOperation operation = + getCacheOperation(CacheResultOperation.class, CustomService.class, name.getMethodName(), Long.class); + assertEquals(defaultCacheResolver, operation.getCacheResolver()); + assertNull(operation.getExceptionCacheResolver()); + assertCacheKeyGenerator(operation.getKeyGenerator(), TestableCacheKeyGenerator.class); + } + + @Test + public void customKeyGeneratorSpringBean() { + TestableCacheKeyGenerator bean = new TestableCacheKeyGenerator(); + applicationContext.getBeanFactory().registerSingleton("fooBar", bean); + CacheResultOperation operation = + getCacheOperation(CacheResultOperation.class, CustomService.class, name.getMethodName(), Long.class); + assertEquals(defaultCacheResolver, operation.getCacheResolver()); + assertNull(operation.getExceptionCacheResolver()); + KeyGeneratorAdapter adapter = (KeyGeneratorAdapter) operation.getKeyGenerator(); + assertSame(bean, adapter.getTarget()); // take bean from context + } + + @Test + public void customKeyGeneratorAndCacheResolver() { + CacheResultOperation operation = getCacheOperation(CacheResultOperation.class, + CustomServiceWithDefaults.class, name.getMethodName(), Long.class); + assertJCacheResolver(operation.getCacheResolver(), TestableCacheResolver.class); + assertJCacheResolver(operation.getExceptionCacheResolver(), null); + assertCacheKeyGenerator(operation.getKeyGenerator(), TestableCacheKeyGenerator.class); + } + + @Test + public void customKeyGeneratorAndCacheResolverWithExceptionName() { + CacheResultOperation operation = getCacheOperation(CacheResultOperation.class, + CustomServiceWithDefaults.class, name.getMethodName(), Long.class); + assertJCacheResolver(operation.getCacheResolver(), TestableCacheResolver.class); + assertJCacheResolver(operation.getExceptionCacheResolver(), TestableCacheResolver.class); + assertCacheKeyGenerator(operation.getKeyGenerator(), TestableCacheKeyGenerator.class); + } + + private void assertDefaults(BaseKeyCacheOperation operation) { + assertEquals(defaultCacheResolver, operation.getCacheResolver()); + assertEquals(defaultKeyGenerator, operation.getKeyGenerator()); + } + + protected > T getDefaultCacheOperation(Class operationType, Class... parameterTypes) { + return getCacheOperation(operationType, AnnotatedJCacheableService.class, name.getMethodName(), parameterTypes); + } + + protected > T getCacheOperation(Class operationType, Class targetType, + String methodName, Class... parameterTypes) { + JCacheOperation result = getCacheOperation(targetType, methodName, parameterTypes); + assertNotNull(result); + assertEquals(operationType, result.getClass()); + return operationType.cast(result); + } + + private JCacheOperation getCacheOperation(Class targetType, String methodName, Class... parameterTypes) { + Method method = ReflectionUtils.findMethod(targetType, methodName, parameterTypes); + Assert.notNull(method, "requested method '" + methodName + "'does not exist"); + return source.getCacheOperation(method, targetType); + } + + private void assertJCacheResolver(CacheResolver actual, + Class expectedTargetType) { + if (expectedTargetType == null) { + assertNull(actual); + } + else { + assertEquals("Wrong cache resolver implementation", CacheResolverAdapter.class, actual.getClass()); + CacheResolverAdapter adapter = (CacheResolverAdapter) actual; + assertEquals("Wrong target JCache implementation", expectedTargetType, adapter.getTarget().getClass()); + } + } + + private void assertCacheKeyGenerator(KeyGenerator actual, + Class expectedTargetType) { + assertEquals("Wrong cache resolver implementation", KeyGeneratorAdapter.class, actual.getClass()); + KeyGeneratorAdapter adapter = (KeyGeneratorAdapter) actual; + assertEquals("Wrong target CacheKeyGenerator implementation", expectedTargetType, adapter.getTarget().getClass()); + } + + + static class CustomService { + + @CacheResult(cacheKeyGenerator = TestableCacheKeyGenerator.class) + public Object customKeyGenerator(Long id) { + return null; + } + + @CacheResult(cacheKeyGenerator = TestableCacheKeyGenerator.class) + public Object customKeyGeneratorSpringBean(Long id) { + return null; + } + + @CacheResult(cacheResolverFactory = TestableCacheResolverFactory.class) + public Object customCacheResolver(Long id) { + return null; + } + } + + @CacheDefaults(cacheResolverFactory = TestableCacheResolverFactory.class, + cacheKeyGenerator = TestableCacheKeyGenerator.class) + static class CustomServiceWithDefaults { + + @CacheResult + public Object customKeyGeneratorAndCacheResolver(Long id) { + return null; + } + + @CacheResult(exceptionCacheName = "exception") + public Object customKeyGeneratorAndCacheResolverWithExceptionName(Long id) { + return null; + } + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheResolverAdapterTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheResolverAdapterTests.java new file mode 100644 index 0000000000..430d6fa3a7 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheResolverAdapterTests.java @@ -0,0 +1,105 @@ +/* + * 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.interceptor; + +import static org.junit.Assert.*; +import static org.mockito.BDDMockito.*; +import static org.mockito.Mockito.mock; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; +import java.util.Collection; + +import javax.cache.annotation.CacheInvocationContext; +import javax.cache.annotation.CacheMethodDetails; +import javax.cache.annotation.CacheResolver; +import javax.cache.annotation.CacheResult; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; + +import org.springframework.cache.Cache; +import org.springframework.cache.jcache.AbstractJCacheTests; +import org.springframework.cache.jcache.model.CacheResultOperation; +import org.springframework.cache.jcache.model.DefaultCacheMethodDetails; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +/** + * @author Stephane Nicoll + */ +public class CacheResolverAdapterTests extends AbstractJCacheTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + + @Test + public void resolveSimpleCache() { + DefaultCacheInvocationContext dummyContext = createDummyContext(); + CacheResolverAdapter adapter = new CacheResolverAdapter(getCacheResolver(dummyContext, "testCache")); + Collection caches = adapter.resolveCaches(dummyContext); + assertNotNull(caches); + assertEquals(1, caches.size()); + assertEquals("testCache", caches.iterator().next().getName()); + } + + @Test + public void resolveUnknownCache() { + DefaultCacheInvocationContext dummyContext = createDummyContext(); + CacheResolverAdapter adapter = new CacheResolverAdapter(getCacheResolver(dummyContext, null)); + + thrown.expect(IllegalArgumentException.class); + adapter.resolveCaches(dummyContext); + } + + protected CacheResolver getCacheResolver(CacheInvocationContext context, String cacheName) { + CacheResolver cacheResolver = mock(CacheResolver.class); + final javax.cache.Cache cache; + if (cacheName == null) { + cache = null; + } + else { + cache = mock(javax.cache.Cache.class); + given(cache.getName()).willReturn(cacheName); + } + given(cacheResolver.resolveCache(context)).willReturn(cache); + return cacheResolver; + } + + protected DefaultCacheInvocationContext createDummyContext() { + Method method = ReflectionUtils.findMethod(Sample.class, "get", String.class); + Assert.notNull(method); + CacheResult cacheAnnotation = method.getAnnotation(CacheResult.class); + CacheMethodDetails methodDetails = + new DefaultCacheMethodDetails<>(method, cacheAnnotation, "test"); + CacheResultOperation operation = new CacheResultOperation(methodDetails, + defaultCacheResolver, defaultKeyGenerator, defaultExceptionCacheResolver); + return new DefaultCacheInvocationContext(operation, new Sample(), new Object[] {"id"}); + } + + + static class Sample { + + @CacheResult + private Object get(String id) { + return null; + } + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheInterceptorTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheInterceptorTests.java new file mode 100644 index 0000000000..92b2f38d54 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheInterceptorTests.java @@ -0,0 +1,168 @@ +/* + * 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.interceptor; + +import static org.junit.Assert.*; + +import java.lang.reflect.Method; +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.junit.Test; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.interceptor.CacheOperationInvocationContext; +import org.springframework.cache.interceptor.CacheOperationInvoker; +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.interceptor.KeyGenerator; +import org.springframework.cache.jcache.AbstractJCacheTests; +import org.springframework.context.support.StaticApplicationContext; +import org.springframework.util.ReflectionUtils; + +/** + * @author Stephane Nicoll + */ +public class JCacheInterceptorTests extends AbstractJCacheTests { + + private final CacheOperationInvoker dummyInvoker = new DummyInvoker(null); + + @Test + public void severalCachesNotSupported() { + JCacheInterceptor interceptor = createInterceptor(createOperationSource( + cacheManager, new TestCacheResolver("default", "exception"), + defaultExceptionCacheResolver, defaultKeyGenerator)); + + AnnotatedJCacheableService service = new AnnotatedJCacheableService(cacheManager.getCache("default")); + Method m = ReflectionUtils.findMethod(AnnotatedJCacheableService.class, "cache", String.class); + + try { + interceptor.execute(dummyInvoker, service, m, new Object[] {"myId"}); + } + catch (IllegalStateException e) { + assertTrue(e.getMessage().contains("JSR-107 only supports a single cache.")); + } + catch (Throwable t) { + fail("Unexpected: " + t); + } + } + + @Test + public void noCacheCouldBeResolved() { + JCacheInterceptor interceptor = createInterceptor(createOperationSource( + cacheManager, new TestCacheResolver(), // Returns empty list + defaultExceptionCacheResolver, defaultKeyGenerator)); + + AnnotatedJCacheableService service = new AnnotatedJCacheableService(cacheManager.getCache("default")); + Method m = ReflectionUtils.findMethod(AnnotatedJCacheableService.class, "cache", String.class); + + try { + interceptor.execute(dummyInvoker, service, m, new Object[] {"myId"}); + } + catch (IllegalStateException e) { + assertTrue(e.getMessage().contains("Cache could not have been resolved for")); + } + catch (Throwable t) { + fail("Unexpected: " + t); + } + } + + @Test + public void cacheManagerMandatoryIfCacheResolverNotSetSet() { + thrown.expect(IllegalStateException.class); + thrown.expectMessage("'cacheManager' is required"); + createOperationSource(null, null, null, defaultKeyGenerator); + } + + @Test + public void cacheManagerOptionalIfCacheResolversSet() { + createOperationSource(null, defaultCacheResolver, defaultExceptionCacheResolver, defaultKeyGenerator); + } + + @Test + public void cacheResultReturnsProperType() throws Throwable { + JCacheInterceptor interceptor = createInterceptor(createOperationSource( + cacheManager, defaultCacheResolver, + defaultExceptionCacheResolver, defaultKeyGenerator)); + + AnnotatedJCacheableService service = new AnnotatedJCacheableService(cacheManager.getCache("default")); + Method m = ReflectionUtils.findMethod(AnnotatedJCacheableService.class, "cache", String.class); + + CacheOperationInvoker invoker = new DummyInvoker(0L); + Object execute = interceptor.execute(invoker, service, m, new Object[] {"myId"}); + assertNotNull("result cannot be null.", execute); + assertEquals("Wrong result type", Long.class, execute.getClass()); + assertEquals("Wrong result", 0L, execute); + } + + protected JCacheOperationSource createOperationSource(CacheManager cacheManager, + CacheResolver cacheResolver, + CacheResolver exceptionCacheResolver, + KeyGenerator keyGenerator) { + DefaultJCacheOperationSource source = new DefaultJCacheOperationSource(); + source.setApplicationContext(new StaticApplicationContext()); + source.setCacheManager(cacheManager); + source.setCacheResolver(cacheResolver); + source.setExceptionCacheResolver(exceptionCacheResolver); + source.setKeyGenerator(keyGenerator); + source.afterPropertiesSet(); + return source; + } + + + protected JCacheInterceptor createInterceptor(JCacheOperationSource source) { + JCacheInterceptor interceptor = new JCacheInterceptor(); + interceptor.setCacheOperationSource(source); + interceptor.afterPropertiesSet(); + return interceptor; + } + + + private class TestCacheResolver implements CacheResolver { + + private final String[] cacheNames; + + private TestCacheResolver(String... cacheNames) { + this.cacheNames = cacheNames; + } + + @Override + public Collection resolveCaches(CacheOperationInvocationContext context) { + List result = new ArrayList(); + for (String cacheName : cacheNames) { + result.add(cacheManager.getCache(cacheName)); + } + return result; + } + } + + private static class DummyInvoker implements CacheOperationInvoker { + + private final Object result; + + private DummyInvoker(Object result) { + this.result = result; + } + + @Override + public Object invoke() throws ThrowableWrapper { + return result; + } + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/model/AbstractCacheOperationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/model/AbstractCacheOperationTests.java new file mode 100644 index 0000000000..1e72f13922 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/model/AbstractCacheOperationTests.java @@ -0,0 +1,77 @@ +/* + * 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.model; + +import static org.junit.Assert.*; + +import java.lang.annotation.Annotation; +import java.lang.reflect.Method; + +import javax.cache.annotation.CacheInvocationParameter; +import javax.cache.annotation.CacheMethodDetails; + +import org.junit.Test; + +import org.springframework.cache.jcache.AbstractJCacheTests; +import org.springframework.core.annotation.AnnotationUtils; +import org.springframework.util.Assert; +import org.springframework.util.ReflectionUtils; + +/** + * @author Stephane Nicoll + */ +public abstract class AbstractCacheOperationTests> extends AbstractJCacheTests { + + protected final SampleObject sampleInstance = new SampleObject(); + + protected abstract O createSimpleOperation(); + + + @Test + public void simple() { + O operation = createSimpleOperation(); + assertEquals("Wrong cache name", "simpleCache", operation.getCacheName()); + assertEquals("Unexpected number of annotation on " + operation.getMethod(), + 1, operation.getAnnotations().size()); + assertEquals("Wrong method annotation", operation.getCacheAnnotation(), + operation.getAnnotations().iterator().next()); + + assertNotNull("cache resolver should be set", operation.getCacheResolver()); + } + + protected void assertCacheInvocationParameter(CacheInvocationParameter actual, Class targetType, + Object value, int position) { + assertEquals("wrong parameter type for " + actual, targetType, actual.getRawType()); + assertEquals("wrong parameter value for " + actual, value, actual.getValue()); + assertEquals("wrong parameter position for " + actual, position, actual.getParameterPosition()); + } + + protected CacheMethodDetails create(Class annotationType, + Class targetType, String methodName, + Class... parameterTypes) { + Method method = ReflectionUtils.findMethod(targetType, methodName, parameterTypes); + Assert.notNull(method, "requested method '" + methodName + "'does not exist"); + A cacheAnnotation = method.getAnnotation(annotationType); + return new DefaultCacheMethodDetails(method, cacheAnnotation, getCacheName(cacheAnnotation)); + } + + private static String getCacheName(Annotation annotation) { + Object cacheName = AnnotationUtils.getValue(annotation, "cacheName"); + return cacheName != null ? cacheName.toString() : "test"; + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/model/CachePutOperationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/model/CachePutOperationTests.java new file mode 100644 index 0000000000..727be16a8f --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/model/CachePutOperationTests.java @@ -0,0 +1,96 @@ +/* + * 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.model; + +import static org.junit.Assert.*; + +import java.io.IOException; + +import javax.cache.annotation.CacheInvocationParameter; +import javax.cache.annotation.CacheMethodDetails; +import javax.cache.annotation.CachePut; + +import org.junit.Test; + +/** + * @author Stephane Nicoll + */ +public class CachePutOperationTests extends AbstractCacheOperationTests { + + @Override + protected CachePutOperation createSimpleOperation() { + CacheMethodDetails methodDetails = create(CachePut.class, + SampleObject.class, "simplePut", Long.class, SampleObject.class); + return createDefaultOperation(methodDetails); + } + + @Test + public void simplePut() { + CachePutOperation operation = createSimpleOperation(); + + CacheInvocationParameter[] allParameters = operation.getAllParameters(2L, sampleInstance); + assertEquals(2, allParameters.length); + assertCacheInvocationParameter(allParameters[0], Long.class, 2L, 0); + assertCacheInvocationParameter(allParameters[1], SampleObject.class, sampleInstance, 1); + + CacheInvocationParameter valueParameter = operation.getValueParameter(2L, sampleInstance); + assertNotNull(valueParameter); + assertCacheInvocationParameter(valueParameter, SampleObject.class, sampleInstance, 1); + } + + @Test + public void noCacheValue() { + CacheMethodDetails methodDetails = create(CachePut.class, + SampleObject.class, "noCacheValue", Long.class); + + thrown.expect(IllegalArgumentException.class); + createDefaultOperation(methodDetails); + } + + @Test + public void multiCacheValues() { + CacheMethodDetails methodDetails = create(CachePut.class, + SampleObject.class, "multiCacheValues", Long.class, SampleObject.class, SampleObject.class); + + thrown.expect(IllegalArgumentException.class); + createDefaultOperation(methodDetails); + } + + @Test + public void invokeWithWrongParameters() { + CachePutOperation operation = createSimpleOperation(); + + thrown.expect(IllegalStateException.class); + operation.getValueParameter(2L); + } + + @Test + public void fullPutConfig() { + CacheMethodDetails methodDetails = create(CachePut.class, + SampleObject.class, "fullPutConfig", Long.class, SampleObject.class); + CachePutOperation operation = createDefaultOperation(methodDetails); + assertTrue(operation.isEarlyPut()); + assertNotNull(operation.getExceptionTypeFilter()); + assertTrue(operation.getExceptionTypeFilter().match(IOException.class)); + assertFalse(operation.getExceptionTypeFilter().match(NullPointerException.class)); + } + + private CachePutOperation createDefaultOperation(CacheMethodDetails methodDetails) { + return new CachePutOperation(methodDetails, defaultCacheResolver, defaultKeyGenerator); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/model/CacheRemoveAllOperationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/model/CacheRemoveAllOperationTests.java new file mode 100644 index 0000000000..55937e207a --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/model/CacheRemoveAllOperationTests.java @@ -0,0 +1,48 @@ +/* + * 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.model; + +import static org.junit.Assert.*; + +import javax.cache.annotation.CacheInvocationParameter; +import javax.cache.annotation.CacheMethodDetails; +import javax.cache.annotation.CacheRemoveAll; + +import org.junit.Test; + +/** + * @author Stephane Nicoll + */ +public class CacheRemoveAllOperationTests extends AbstractCacheOperationTests { + + @Override + protected CacheRemoveAllOperation createSimpleOperation() { + CacheMethodDetails methodDetails = create(CacheRemoveAll.class, + SampleObject.class, "simpleRemoveAll"); + + return new CacheRemoveAllOperation(methodDetails, defaultCacheResolver); + } + + @Test + public void simpleRemoveAll() { + CacheRemoveAllOperation operation = createSimpleOperation(); + + CacheInvocationParameter[] allParameters = operation.getAllParameters(); + assertEquals(0, allParameters.length); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/model/CacheRemoveOperationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/model/CacheRemoveOperationTests.java new file mode 100644 index 0000000000..91dbc9d650 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/model/CacheRemoveOperationTests.java @@ -0,0 +1,49 @@ +/* + * 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.model; + +import static org.junit.Assert.*; + +import javax.cache.annotation.CacheInvocationParameter; +import javax.cache.annotation.CacheMethodDetails; +import javax.cache.annotation.CacheRemove; + +import org.junit.Test; + +/** + * @author Stephane Nicoll + */ +public class CacheRemoveOperationTests extends AbstractCacheOperationTests { + + @Override + protected CacheRemoveOperation createSimpleOperation() { + CacheMethodDetails methodDetails = create(CacheRemove.class, + SampleObject.class, "simpleRemove", Long.class); + + return new CacheRemoveOperation(methodDetails, defaultCacheResolver, defaultKeyGenerator); + } + + @Test + public void simpleRemove() { + CacheRemoveOperation operation = createSimpleOperation(); + + CacheInvocationParameter[] allParameters = operation.getAllParameters(2L); + assertEquals(1, allParameters.length); + assertCacheInvocationParameter(allParameters[0], Long.class, 2L, 0); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/model/CacheResultOperationTests.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/model/CacheResultOperationTests.java new file mode 100644 index 0000000000..e6102eaeaa --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/model/CacheResultOperationTests.java @@ -0,0 +1,131 @@ +/* + * 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.model; + +import static org.junit.Assert.*; + +import java.io.IOException; +import java.lang.annotation.Annotation; +import java.util.Set; + +import javax.cache.annotation.CacheInvocationParameter; +import javax.cache.annotation.CacheKey; +import javax.cache.annotation.CacheMethodDetails; +import javax.cache.annotation.CacheResult; + +import org.junit.Test; + +import org.springframework.beans.factory.annotation.Value; + +/** + * @author Stephane Nicoll + */ +public class CacheResultOperationTests extends AbstractCacheOperationTests { + + @Override + protected CacheResultOperation createSimpleOperation() { + CacheMethodDetails methodDetails = create(CacheResult.class, + SampleObject.class, "simpleGet", Long.class); + + return new CacheResultOperation(methodDetails, defaultCacheResolver, defaultKeyGenerator, + defaultExceptionCacheResolver); + } + + @Test + public void simpleGet() { + CacheResultOperation operation = createSimpleOperation(); + + assertNotNull(operation.getKeyGenerator()); + assertNotNull(operation.getExceptionCacheResolver()); + + assertNull(operation.getExceptionCacheName()); + assertEquals(defaultExceptionCacheResolver, operation.getExceptionCacheResolver()); + + CacheInvocationParameter[] allParameters = operation.getAllParameters(2L); + assertEquals(1, allParameters.length); + assertCacheInvocationParameter(allParameters[0], Long.class, 2L, 0); + + CacheInvocationParameter[] keyParameters = operation.getKeyParameters(2L); + assertEquals(1, keyParameters.length); + assertCacheInvocationParameter(keyParameters[0], Long.class, 2L, 0); + } + + @Test + public void multiParameterKey() { + CacheMethodDetails methodDetails = create(CacheResult.class, + SampleObject.class, "multiKeysGet", Long.class, Boolean.class, String.class); + CacheResultOperation operation = createDefaultOperation(methodDetails); + + CacheInvocationParameter[] keyParameters = operation.getKeyParameters(3L, Boolean.TRUE, "Foo"); + assertEquals(2, keyParameters.length); + assertCacheInvocationParameter(keyParameters[0], Long.class, 3L, 0); + assertCacheInvocationParameter(keyParameters[1], String.class, "Foo", 2); + } + + @Test + public void invokeWithWrongParameters() { + CacheMethodDetails methodDetails = create(CacheResult.class, + SampleObject.class, "anotherSimpleGet", String.class, Long.class); + CacheResultOperation operation = createDefaultOperation(methodDetails); + + thrown.expect(IllegalStateException.class); + operation.getAllParameters("bar"); // missing one argument + } + + @Test + public void tooManyKeyValues() { + CacheMethodDetails methodDetails = create(CacheResult.class, + SampleObject.class, "anotherSimpleGet", String.class, Long.class); + CacheResultOperation operation = createDefaultOperation(methodDetails); + + thrown.expect(IllegalStateException.class); + operation.getKeyParameters("bar"); // missing one argument + } + + @Test + public void annotatedGet() { + CacheMethodDetails methodDetails = create(CacheResult.class, + SampleObject.class, "annotatedGet", Long.class, String.class); + CacheResultOperation operation = createDefaultOperation(methodDetails); + CacheInvocationParameter[] parameters = operation.getAllParameters(2L, "foo"); + + Set firstParameterAnnotations = parameters[0].getAnnotations(); + assertEquals(1, firstParameterAnnotations.size()); + assertEquals(CacheKey.class, firstParameterAnnotations.iterator().next().annotationType()); + + Set secondParameterAnnotations = parameters[1].getAnnotations(); + assertEquals(1, secondParameterAnnotations.size()); + assertEquals(Value.class, secondParameterAnnotations.iterator().next().annotationType()); + } + + @Test + public void fullGetConfig() { + CacheMethodDetails methodDetails = create(CacheResult.class, + SampleObject.class, "fullGetConfig", Long.class); + CacheResultOperation operation = createDefaultOperation(methodDetails); + assertTrue(operation.isAlwaysInvoked()); + assertNotNull(operation.getExceptionTypeFilter()); + assertTrue(operation.getExceptionTypeFilter().match(IOException.class)); + assertFalse(operation.getExceptionTypeFilter().match(NullPointerException.class)); + } + + private CacheResultOperation createDefaultOperation(CacheMethodDetails methodDetails) { + return new CacheResultOperation(methodDetails, + defaultCacheResolver, defaultKeyGenerator, defaultCacheResolver); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/model/SampleObject.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/model/SampleObject.java new file mode 100644 index 0000000000..baf2d077a8 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/model/SampleObject.java @@ -0,0 +1,80 @@ +package org.springframework.cache.jcache.model; + +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 org.springframework.beans.factory.annotation.Value; + +/** + * @author Stephane Nicoll + */ +class SampleObject { + + // Simple + + @CacheResult(cacheName = "simpleCache") + public SampleObject simpleGet(Long id) { + return null; + } + + @CachePut(cacheName = "simpleCache") + public void simplePut(Long id, @CacheValue SampleObject instance) { + } + + @CacheRemove(cacheName = "simpleCache") + public void simpleRemove(Long id) { + } + + @CacheRemoveAll(cacheName = "simpleCache") + public void simpleRemoveAll() { + } + + @CacheResult(cacheName = "testSimple") + public SampleObject anotherSimpleGet(String foo, Long bar) { + return null; + } + + // @CacheKey + + @CacheResult + public SampleObject multiKeysGet(@CacheKey Long id, Boolean notUsed, + @CacheKey String domain) { + return null; + } + + // @CacheValue + + @CachePut(cacheName = "simpleCache") + public void noCacheValue(Long id) { + } + + @CachePut(cacheName = "simpleCache") + public void multiCacheValues(Long id, @CacheValue SampleObject instance, + @CacheValue SampleObject anotherInstance) { + } + + // Parameter annotation + + @CacheResult(cacheName = "simpleCache") + public SampleObject annotatedGet(@CacheKey Long id, @Value("${foo}") String foo) { + return null; + } + + // Full config + + @CacheResult(cacheName = "simpleCache", skipGet = true, + cachedExceptions = Exception.class, nonCachedExceptions = RuntimeException.class) + public SampleObject fullGetConfig(@CacheKey Long id) { + return null; + } + + @CachePut(cacheName = "simpleCache", afterInvocation = false, + cacheFor = Exception.class, noCacheFor = RuntimeException.class) + public void fullPutConfig(@CacheKey Long id, @CacheValue SampleObject instance) { + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/support/TestableCacheKeyGenerator.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/support/TestableCacheKeyGenerator.java new file mode 100644 index 0000000000..db260026f2 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/support/TestableCacheKeyGenerator.java @@ -0,0 +1,25 @@ +package org.springframework.cache.jcache.support; + +import java.lang.annotation.Annotation; + +import javax.cache.annotation.CacheKeyGenerator; +import javax.cache.annotation.CacheKeyInvocationContext; +import javax.cache.annotation.GeneratedCacheKey; + +import org.springframework.cache.jcache.interceptor.SimpleGeneratedCacheKey; + +/** + * A simple test key generator that only takes the first key arguments into + * account. To be used with a multi parameters key to validate it has been + * used properly. + * + * @author Stephane Nicoll + */ +public class TestableCacheKeyGenerator implements CacheKeyGenerator { + + @Override + public GeneratedCacheKey generateCacheKey(CacheKeyInvocationContext context) { + return new SimpleGeneratedCacheKey(context.getKeyParameters()[0]); + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/support/TestableCacheResolver.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/support/TestableCacheResolver.java new file mode 100644 index 0000000000..a5d2444dc6 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/support/TestableCacheResolver.java @@ -0,0 +1,41 @@ +/* + * 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.support; + +import static org.mockito.BDDMockito.*; +import static org.mockito.Mockito.mock; + +import java.lang.annotation.Annotation; + +import javax.cache.Cache; +import javax.cache.annotation.CacheInvocationContext; +import javax.cache.annotation.CacheResolver; + +/** + * @author Stephane Nicoll + */ +public class TestableCacheResolver implements CacheResolver { + + @Override + public Cache resolveCache(CacheInvocationContext cacheInvocationContext) { + String cacheName = cacheInvocationContext.getCacheName(); + Cache mock = mock(Cache.class); + given(mock.getName()).willReturn(cacheName); + return mock; + } + +} diff --git a/spring-context-support/src/test/java/org/springframework/cache/jcache/support/TestableCacheResolverFactory.java b/spring-context-support/src/test/java/org/springframework/cache/jcache/support/TestableCacheResolverFactory.java new file mode 100644 index 0000000000..48888e5962 --- /dev/null +++ b/spring-context-support/src/test/java/org/springframework/cache/jcache/support/TestableCacheResolverFactory.java @@ -0,0 +1,41 @@ +/* + * 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.support; + +import java.lang.annotation.Annotation; + +import javax.cache.annotation.CacheMethodDetails; +import javax.cache.annotation.CacheResolver; +import javax.cache.annotation.CacheResolverFactory; +import javax.cache.annotation.CacheResult; + +/** + * @author Stephane Nicoll + */ +public class TestableCacheResolverFactory implements CacheResolverFactory { + + @Override + public CacheResolver getCacheResolver(CacheMethodDetails cacheMethodDetails) { + return new TestableCacheResolver(); + } + + @Override + public CacheResolver getExceptionCacheResolver(CacheMethodDetails cacheMethodDetails) { + return new TestableCacheResolver(); + } + +} diff --git a/spring-context-support/src/test/resources/org/springframework/cache/jcache/config/jCacheNamespaceDriven.xml b/spring-context-support/src/test/resources/org/springframework/cache/jcache/config/jCacheNamespaceDriven.xml new file mode 100644 index 0000000000..76565f1e4e --- /dev/null +++ b/spring-context-support/src/test/resources/org/springframework/cache/jcache/config/jCacheNamespaceDriven.xml @@ -0,0 +1,38 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/spring-context-support/src/test/resources/org/springframework/cache/jcache/config/jCacheStandaloneConfig.xml b/spring-context-support/src/test/resources/org/springframework/cache/jcache/config/jCacheStandaloneConfig.xml new file mode 100644 index 0000000000..467604f4ae --- /dev/null +++ b/spring-context-support/src/test/resources/org/springframework/cache/jcache/config/jCacheStandaloneConfig.xml @@ -0,0 +1,48 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/AbstractCachingConfiguration.java b/spring-context/src/main/java/org/springframework/cache/annotation/AbstractCachingConfiguration.java index dc49cd0383..c9f88bd778 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/AbstractCachingConfiguration.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/AbstractCachingConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * 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. @@ -35,11 +35,12 @@ import org.springframework.util.CollectionUtils; * Spring's annotation-driven cache management capability. * * @author Chris Beams + * @author Stephane Nicoll * @since 3.1 * @see EnableCaching */ @Configuration -public abstract class AbstractCachingConfiguration implements ImportAware { +public abstract class AbstractCachingConfiguration implements ImportAware { protected AnnotationAttributes enableCaching; @@ -51,7 +52,7 @@ public abstract class AbstractCachingConfiguration implements ImportAware { private Collection cacheManagerBeans; @Autowired(required=false) - private Collection cachingConfigurers; + private Collection cachingConfigurers; @Override @@ -82,9 +83,8 @@ public abstract class AbstractCachingConfiguration implements ImportAware { "Refactor the configuration such that CachingConfigurer is " + "implemented only once or not at all."); } - CachingConfigurer cachingConfigurer = cachingConfigurers.iterator().next(); - this.cacheManager = cachingConfigurer.cacheManager(); - this.keyGenerator = cachingConfigurer.keyGenerator(); + C cachingConfigurer = cachingConfigurers.iterator().next(); + useCachingConfigurer(cachingConfigurer); } else if (!CollectionUtils.isEmpty(cacheManagerBeans)) { int nManagers = cacheManagerBeans.size(); @@ -95,8 +95,7 @@ public abstract class AbstractCachingConfiguration implements ImportAware { "to make explicit which CacheManager should be used for " + "annotation-driven cache management."); } - CacheManager cacheManager = cacheManagerBeans.iterator().next(); - this.cacheManager = cacheManager; + this.cacheManager = cacheManagerBeans.iterator().next(); // keyGenerator remains null; will fall back to default within CacheInterceptor } else { @@ -105,4 +104,13 @@ public abstract class AbstractCachingConfiguration implements ImportAware { "from your configuration."); } } + + /** + * Extract the configuration from the nominated {@link CachingConfigurer}. + */ + protected void useCachingConfigurer(C config) { + this.cacheManager = config.cacheManager(); + this.keyGenerator = config.keyGenerator(); + } + } diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurationSelector.java b/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurationSelector.java index b4fb7e5c83..08478cc316 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurationSelector.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/CachingConfigurationSelector.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * 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. @@ -16,24 +16,39 @@ package org.springframework.cache.annotation; +import java.util.ArrayList; +import java.util.List; + import org.springframework.context.annotation.AdviceMode; import org.springframework.context.annotation.AdviceModeImportSelector; import org.springframework.context.annotation.AnnotationConfigUtils; import org.springframework.context.annotation.AutoProxyRegistrar; +import org.springframework.util.ClassUtils; /** - * Selects which implementation of {@link AbstractCachingConfiguration} should be used + * Select which implementation of {@link AbstractCachingConfiguration} should be used * based on the value of {@link EnableCaching#mode} on the importing {@code @Configuration} * class. + *

Detect the presence of JSR-107 and enables JCache support accordingly. * * @author Chris Beams + * @author Stephane Nicoll * @since 3.1 * @see EnableCaching * @see ProxyCachingConfiguration * @see AnnotationConfigUtils#CACHE_ASPECT_CONFIGURATION_CLASS_NAME + * @see AnnotationConfigUtils#JCACHE_ASPECT_CONFIGURATION_CLASS_NAME */ public class CachingConfigurationSelector extends AdviceModeImportSelector { + private static final String PROXY_JCACHE_CONFIGURATION_CLASS = + "org.springframework.cache.jcache.config.ProxyJCacheConfiguration"; + + private static final boolean jsr107Present = ClassUtils.isPresent( + "javax.cache.Cache", CachingConfigurationSelector.class.getClassLoader()); + private static final boolean jCacheImplPresent = ClassUtils.isPresent( + PROXY_JCACHE_CONFIGURATION_CLASS, CachingConfigurationSelector.class.getClassLoader()); + /** * {@inheritDoc} * @return {@link ProxyCachingConfiguration} or {@code AspectJCacheConfiguration} for @@ -43,12 +58,47 @@ public class CachingConfigurationSelector extends AdviceModeImportSelectorTake care of adding the necessary JSR-107 import if it is available. + */ + private String[] getProxyImports() { + List result = new ArrayList(); + result.add(AutoProxyRegistrar.class.getName()); + result.add(ProxyCachingConfiguration.class.getName()); + if (isJCacheAvailable()) { + result.add(PROXY_JCACHE_CONFIGURATION_CLASS); + } + return result.toArray(new String[result.size()]); + } + + /** + * Return the imports to use if the {@link AdviceMode} is set to {@link AdviceMode#ASPECTJ}. + *

Take care of adding the necessary JSR-107 import if it is available. + */ + private String[] getAspectJImports() { + List result = new ArrayList(); + result.add(AnnotationConfigUtils.CACHE_ASPECT_CONFIGURATION_CLASS_NAME); + if (isJCacheAvailable()) { + result.add(AnnotationConfigUtils.JCACHE_ASPECT_CONFIGURATION_CLASS_NAME); + } + return result.toArray(new String[result.size()]); + } + + /** + * Specify if the JSR-107 API and Spring's jCache implementation are available + * in the classpath. + */ + private boolean isJCacheAvailable() { + return jsr107Present && jCacheImplPresent; + } + } diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/EnableCaching.java b/spring-context/src/main/java/org/springframework/cache/annotation/EnableCaching.java index 2a477b9e8b..c475a3f8e4 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/EnableCaching.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/EnableCaching.java @@ -75,6 +75,12 @@ import org.springframework.core.Ordered; * proxy- or AspectJ-based advice that weaves the interceptor into the call stack when * {@link org.springframework.cache.annotation.Cacheable @Cacheable} methods are invoked. * + *

If the JSR-107 API and Spring's JCache implementation are present, the necessary + * components to manage standard cache annotations are also registered. This creates the + * proxy- or AspectJ-based advice that weaves the interceptor into the call stack when + * methods annotated with {@code CacheResult}, {@code CachePut}, {@code CacheRemove} or + * {@code CacheRemoveAll} are invoked. + * *

A bean of type {@link org.springframework.cache.CacheManager CacheManager} * must be registered, as there is no reasonable default that the framework can * use as a convention. And whereas the {@code } element assumes diff --git a/spring-context/src/main/java/org/springframework/cache/annotation/ProxyCachingConfiguration.java b/spring-context/src/main/java/org/springframework/cache/annotation/ProxyCachingConfiguration.java index 91e5366f5d..1e1fbc52d0 100644 --- a/spring-context/src/main/java/org/springframework/cache/annotation/ProxyCachingConfiguration.java +++ b/spring-context/src/main/java/org/springframework/cache/annotation/ProxyCachingConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * 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. @@ -35,7 +35,7 @@ import org.springframework.context.annotation.Role; * @see CachingConfigurationSelector */ @Configuration -public class ProxyCachingConfiguration extends AbstractCachingConfiguration { +public class ProxyCachingConfiguration extends AbstractCachingConfiguration { @Bean(name=AnnotationConfigUtils.CACHE_ADVISOR_BEAN_NAME) @Role(BeanDefinition.ROLE_INFRASTRUCTURE) diff --git a/spring-context/src/main/java/org/springframework/cache/config/AnnotationDrivenCacheBeanDefinitionParser.java b/spring-context/src/main/java/org/springframework/cache/config/AnnotationDrivenCacheBeanDefinitionParser.java index 6ed83c4026..1fb2cd55db 100644 --- a/spring-context/src/main/java/org/springframework/cache/config/AnnotationDrivenCacheBeanDefinitionParser.java +++ b/spring-context/src/main/java/org/springframework/cache/config/AnnotationDrivenCacheBeanDefinitionParser.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2012 the original author or authors. + * 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. @@ -18,6 +18,8 @@ package org.springframework.cache.config; import static org.springframework.context.annotation.AnnotationConfigUtils.*; +import org.w3c.dom.Element; + import org.springframework.aop.config.AopNamespaceUtils; import org.springframework.beans.factory.config.BeanDefinition; import org.springframework.beans.factory.config.RuntimeBeanReference; @@ -29,7 +31,7 @@ import org.springframework.beans.factory.xml.ParserContext; import org.springframework.cache.annotation.AnnotationCacheOperationSource; import org.springframework.cache.interceptor.BeanFactoryCacheOperationSourceAdvisor; import org.springframework.cache.interceptor.CacheInterceptor; -import org.w3c.dom.Element; +import org.springframework.util.ClassUtils; /** * {@link org.springframework.beans.factory.xml.BeanDefinitionParser} @@ -43,11 +45,23 @@ import org.w3c.dom.Element; * '{@code proxy-target-class}' attribute to '{@code true}', which will * result in class-based proxies being created. * + *

If the JSR-107 API and Spring's JCache implementation are present, + * the necessary infrastructure beans required to handle methods annotated + * with {@code CacheResult}, {@code CachePut}, {@code CacheRemove} or + * {@code CacheRemoveAll} are also registered. + * * @author Costin Leau + * @author Stephane Nicoll * @since 3.1 */ class AnnotationDrivenCacheBeanDefinitionParser implements BeanDefinitionParser { + private static final boolean jsr107Present = ClassUtils.isPresent( + "javax.cache.Cache", AnnotationDrivenCacheBeanDefinitionParser.class.getClassLoader()); + + private static final boolean jCacheImplPresent = ClassUtils.isPresent( + JCACHE_OPERATION_SOURCE_CLASS, AnnotationDrivenCacheBeanDefinitionParser.class.getClassLoader()); + /** * Parses the '{@code }' tag. Will * {@link AopNamespaceUtils#registerAutoProxyCreatorIfNecessary @@ -62,46 +76,39 @@ class AnnotationDrivenCacheBeanDefinitionParser implements BeanDefinitionParser } else { // mode="proxy" - AopAutoProxyConfigurer.configureAutoProxyCreator(element, parserContext); + registerCacheAdvisor(element, parserContext); } return null; } + private void registerCacheAspect(Element element, ParserContext parserContext) { + SpringCachingConfigurer.registerCacheAspect(element, parserContext); + if (jsr107Present && jCacheImplPresent) { // Register JCache aspect + JCacheCachingConfigurer.registerCacheAspect(element, parserContext); + } + } + + private void registerCacheAdvisor(Element element, ParserContext parserContext) { + AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(parserContext, element); + SpringCachingConfigurer.registerCacheAdvisor(element, parserContext); + if (jsr107Present && jCacheImplPresent) { // Register JCache advisor + JCacheCachingConfigurer.registerCacheAdvisor(element, parserContext); + } + } + private static void parseCacheManagerProperty(Element element, BeanDefinition def) { def.getPropertyValues().add("cacheManager", new RuntimeBeanReference(CacheNamespaceHandler.extractCacheManager(element))); } - /** - * Registers a - *

-	 * 
-	 *   
-	 *   
-	 * 
-	 * 
- */ - private void registerCacheAspect(Element element, ParserContext parserContext) { - if (!parserContext.getRegistry().containsBeanDefinition(CACHE_ASPECT_BEAN_NAME)) { - RootBeanDefinition def = new RootBeanDefinition(); - def.setBeanClassName(CACHE_ASPECT_CLASS_NAME); - def.setFactoryMethodName("aspectOf"); - parseCacheManagerProperty(element, def); - CacheNamespaceHandler.parseKeyGenerator(element, def); - parserContext.registerBeanComponent(new BeanComponentDefinition(def, CACHE_ASPECT_BEAN_NAME)); - } - } - /** - * Inner class to just introduce an AOP framework dependency when actually in proxy mode. + * Configure the necessary infrastructure to support the Spring's caching annotations. */ - private static class AopAutoProxyConfigurer { - - public static void configureAutoProxyCreator(Element element, ParserContext parserContext) { - AopNamespaceUtils.registerAutoProxyCreatorIfNecessary(parserContext, element); + private static class SpringCachingConfigurer { + private static void registerCacheAdvisor(Element element, ParserContext parserContext) { if (!parserContext.getRegistry().containsBeanDefinition(CACHE_ADVISOR_BEAN_NAME)) { Object eleSource = parserContext.extractSource(element); @@ -139,5 +146,93 @@ class AnnotationDrivenCacheBeanDefinitionParser implements BeanDefinitionParser parserContext.registerComponent(compositeDef); } } + + /** + * Registers a + *
+		 * 
+		 *   
+		 *   
+		 * 
+		 * 
+ */ + private static void registerCacheAspect(Element element, ParserContext parserContext) { + if (!parserContext.getRegistry().containsBeanDefinition(CACHE_ASPECT_BEAN_NAME)) { + RootBeanDefinition def = new RootBeanDefinition(); + def.setBeanClassName(CACHE_ASPECT_CLASS_NAME); + def.setFactoryMethodName("aspectOf"); + parseCacheManagerProperty(element, def); + CacheNamespaceHandler.parseKeyGenerator(element, def); + parserContext.registerBeanComponent(new BeanComponentDefinition(def, CACHE_ASPECT_BEAN_NAME)); + } + } } + + /** + * Configure the necessary infrastructure to support the standard JSR-107 caching annotations. + */ + private static class JCacheCachingConfigurer { + + private static void registerCacheAdvisor(Element element, ParserContext parserContext) { + if (!parserContext.getRegistry().containsBeanDefinition(JCACHE_ADVISOR_BEAN_NAME)) { + Object eleSource = parserContext.extractSource(element); + + // Create the CacheOperationSource definition. + BeanDefinition sourceDef = createJCacheOperationSourceBeanDefinition(element, eleSource); + String sourceName = parserContext.getReaderContext().registerWithGeneratedName(sourceDef); + + // Create the CacheInterceptor definition. + RootBeanDefinition interceptorDef = new RootBeanDefinition(JCACHE_INTERCEPTOR_CLASS); + interceptorDef.setSource(eleSource); + interceptorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + interceptorDef.getPropertyValues().add("cacheOperationSource", new RuntimeBeanReference(sourceName)); + String interceptorName = parserContext.getReaderContext().registerWithGeneratedName(interceptorDef); + + // Create the CacheAdvisor definition. + RootBeanDefinition advisorDef = new RootBeanDefinition(JCACHE_ADVISOR_FACTORY_CLASS); + advisorDef.setSource(eleSource); + advisorDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + advisorDef.getPropertyValues().add("cacheOperationSource", new RuntimeBeanReference(sourceName)); + advisorDef.getPropertyValues().add("adviceBeanName", interceptorName); + if (element.hasAttribute("order")) { + advisorDef.getPropertyValues().add("order", element.getAttribute("order")); + } + parserContext.getRegistry().registerBeanDefinition(JCACHE_ADVISOR_BEAN_NAME, advisorDef); + + CompositeComponentDefinition compositeDef = new CompositeComponentDefinition(element.getTagName(), + eleSource); + compositeDef.addNestedComponent(new BeanComponentDefinition(sourceDef, sourceName)); + compositeDef.addNestedComponent(new BeanComponentDefinition(interceptorDef, interceptorName)); + compositeDef.addNestedComponent(new BeanComponentDefinition(advisorDef, JCACHE_ADVISOR_BEAN_NAME)); + parserContext.registerComponent(compositeDef); + } + } + + private static void registerCacheAspect(Element element, ParserContext parserContext) { + if (!parserContext.getRegistry().containsBeanDefinition(JCACHE_ASPECT_BEAN_NAME)) { + Object eleSource = parserContext.extractSource(element); + RootBeanDefinition def = new RootBeanDefinition(); + def.setBeanClassName(JCACHE_ASPECT_CLASS_NAME); + def.setFactoryMethodName("aspectOf"); + BeanDefinition sourceDef = createJCacheOperationSourceBeanDefinition(element, eleSource); + String sourceName = + parserContext.getReaderContext().registerWithGeneratedName(sourceDef); + def.getPropertyValues().add("cacheOperationSource", new RuntimeBeanReference(sourceName)); + + parserContext.registerBeanComponent(new BeanComponentDefinition(sourceDef, sourceName)); + parserContext.registerBeanComponent(new BeanComponentDefinition(def, JCACHE_ASPECT_BEAN_NAME)); + } + } + + private static RootBeanDefinition createJCacheOperationSourceBeanDefinition( + Element element, Object eleSource) { + RootBeanDefinition sourceDef = new RootBeanDefinition(JCACHE_OPERATION_SOURCE_CLASS); + sourceDef.setSource(eleSource); + sourceDef.setRole(BeanDefinition.ROLE_INFRASTRUCTURE); + parseCacheManagerProperty(element, sourceDef); + CacheNamespaceHandler.parseKeyGenerator(element, sourceDef); + return sourceDef; + } + } + } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java index 11dc537f03..8eb1eb9117 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/AbstractFallbackCacheOperationSource.java @@ -28,7 +28,6 @@ import org.apache.commons.logging.LogFactory; import org.springframework.core.BridgeMethodResolver; import org.springframework.util.ClassUtils; -import org.springframework.util.ObjectUtils; /** * Abstract implementation of {@link CacheOperation} that caches @@ -68,14 +67,13 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera protected final Log logger = LogFactory.getLog(getClass()); /** - * Cache of CacheOperations, keyed by DefaultCacheKey (Method + target Class). + * Cache of CacheOperations, keyed by {@link MethodCacheKey} (Method + target Class). *

As this base class is not marked Serializable, the cache will be recreated * after serialization - provided that the concrete subclass is Serializable. */ final Map> attributeCache = new ConcurrentHashMap>(1024); - /** * Determine the caching attribute for this method invocation. *

Defaults to the class's caching attribute if no method attribute is found. @@ -123,7 +121,7 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera * @return the cache key (never {@code null}) */ protected Object getCacheKey(Method method, Class targetClass) { - return new DefaultCacheKey(method, targetClass); + return new MethodCacheKey(method, targetClass); } private Collection computeCacheOperations(Method method, Class targetClass) { @@ -188,38 +186,4 @@ public abstract class AbstractFallbackCacheOperationSource implements CacheOpera protected boolean allowPublicMethodsOnly() { return false; } - - - /** - * Default cache key for the CacheOperation cache. - */ - private static class DefaultCacheKey { - - private final Method method; - - private final Class targetClass; - - public DefaultCacheKey(Method method, Class targetClass) { - this.method = method; - this.targetClass = targetClass; - } - - @Override - public boolean equals(Object other) { - if (this == other) { - return true; - } - if (!(other instanceof DefaultCacheKey)) { - return false; - } - DefaultCacheKey otherKey = (DefaultCacheKey) other; - return (this.method.equals(otherKey.method) && ObjectUtils.nullSafeEquals(this.targetClass, - otherKey.targetClass)); - } - - @Override - public int hashCode() { - return this.method.hashCode() * 29 + (this.targetClass != null ? this.targetClass.hashCode() : 0); - } - } } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/BasicCacheOperation.java b/spring-context/src/main/java/org/springframework/cache/interceptor/BasicCacheOperation.java new file mode 100644 index 0000000000..477d808f2a --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/BasicCacheOperation.java @@ -0,0 +1,34 @@ +/* + * 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.interceptor; + +import java.util.Set; + +/** + * The base interface that all cache operations must implement. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public interface BasicCacheOperation { + + /** + * Return the cache name(s) associated to the operation. + */ + Set getCacheNames(); + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java index 802fcbda21..0f04f3f19d 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheAspectSupport.java @@ -182,7 +182,7 @@ public abstract class CacheAspectSupport implements InitializingBean, Applicatio return new CacheOperationContext(operation, method, args, target, targetClass); } - protected Object execute(Invoker invoker, Object target, Method method, Object[] args) { + protected Object execute(CacheOperationInvoker invoker, Object target, Method method, Object[] args) { // check whether aspect is enabled // to cope with cases where the AJ is pulled in automatically if (this.initialized) { @@ -204,7 +204,7 @@ public abstract class CacheAspectSupport implements InitializingBean, Applicatio return targetClass; } - private Object execute(Invoker invoker, CacheOperationContexts contexts) { + private Object execute(CacheOperationInvoker invoker, CacheOperationContexts contexts) { // Process any early evictions processCacheEvicts(contexts.get(CacheEvictOperation.class), true, ExpressionEvaluator.NO_RESULT); @@ -343,12 +343,6 @@ public abstract class CacheAspectSupport implements InitializingBean, Applicatio } - public interface Invoker { - - Object invoke(); - } - - private class CacheOperationContexts { private final MultiValueMap, CacheOperationContext> contexts = diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheInterceptor.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheInterceptor.java index b4b19bea3b..f411ac42ab 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheInterceptor.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheInterceptor.java @@ -45,12 +45,13 @@ public class CacheInterceptor extends CacheAspectSupport implements MethodInterc public Object invoke(final MethodInvocation invocation) throws Throwable { Method method = invocation.getMethod(); - Invoker aopAllianceInvoker = new Invoker() { + CacheOperationInvoker aopAllianceInvoker = new CacheOperationInvoker() { @Override public Object invoke() { try { return invocation.proceed(); - } catch (Throwable ex) { + } + catch (Throwable ex) { throw new ThrowableWrapper(ex); } } @@ -58,17 +59,9 @@ public class CacheInterceptor extends CacheAspectSupport implements MethodInterc try { return execute(aopAllianceInvoker, invocation.getThis(), method, invocation.getArguments()); - } catch (ThrowableWrapper th) { - throw th.original; } - } - - - private static class ThrowableWrapper extends RuntimeException { - private final Throwable original; - - ThrowableWrapper(Throwable original) { - this.original = original; + catch (CacheOperationInvoker.ThrowableWrapper th) { + throw th.getOriginal(); } } diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvocationContext.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvocationContext.java new file mode 100644 index 0000000000..f643e6d0f4 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvocationContext.java @@ -0,0 +1,52 @@ +/* + * 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.interceptor; + +import java.lang.reflect.Method; + +/** + * Represent the context of the invocation of a cache operation. + * + *

The cache operation is static and independent of a particular invocation, + * this gathers the operation and a particular invocation. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public interface CacheOperationInvocationContext { + + /** + * Return the cache operation + */ + O getOperation(); + + /** + * Return the target instance on which the method was invoked + */ + Object getTarget(); + + /** + * Return the method + */ + Method getMethod(); + + /** + * Return the argument used to invoke the method + */ + Object[] getArgs(); + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java new file mode 100644 index 0000000000..4ca3642c8d --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java @@ -0,0 +1,58 @@ +/* + * 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.interceptor; + +/** + * Abstract the invocation of a cache operation. + * + *

Provide a special exception that can be used to indicate that the + * underlying invocation has thrown a checked exception, allowing the + * callers to threat these in a different manner if necessary. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public interface CacheOperationInvoker { + + /** + * Invoke the cache operation defined by this instance. Can throw a + * {@link ThrowableWrapper} if that operation wants to explicitly + * indicate that a checked exception has occurred. + * @return the result of the operation + * @throws ThrowableWrapper if a checked exception has been thrown + */ + Object invoke() throws ThrowableWrapper; + + /** + * Wrap any exception thrown while invoking {@link #invoke()} + */ + @SuppressWarnings("serial") + public static class ThrowableWrapper extends RuntimeException { + + private final Throwable original; + + public ThrowableWrapper(Throwable original) { + super(original.getMessage(), original); + this.original = original; + } + + public Throwable getOriginal() { + return original; + } + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/CacheResolver.java b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheResolver.java new file mode 100644 index 0000000000..55534e888e --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/CacheResolver.java @@ -0,0 +1,41 @@ +/* + * 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.interceptor; + +import java.util.Collection; + +import org.springframework.cache.Cache; + +/** + * Determine the {@link Cache} instance(s) to use for an intercepted method invocation. + * + *

Implementations MUST be thread-safe. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public interface CacheResolver { + + /** + * Return the cache(s) to use for the specified invocation. + * + * @param context the context of the particular invocation + * @return the cache(s) to use (never null) + */ + Collection resolveCaches(CacheOperationInvocationContext context); + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/MethodCacheKey.java b/spring-context/src/main/java/org/springframework/cache/interceptor/MethodCacheKey.java new file mode 100644 index 0000000000..e1076a2a9f --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/MethodCacheKey.java @@ -0,0 +1,47 @@ +package org.springframework.cache.interceptor; + +import java.lang.reflect.Method; + +import org.springframework.util.Assert; +import org.springframework.util.ObjectUtils; + +/** + * Represent a method on a particular {@link Class} and is suitable as a key. + *

Mainly for internal use within the framework. + * + * @author Costin Leau + * @author Stephane Nicoll + * @since 4.1 + */ +public final class MethodCacheKey { + + private final Method method; + + private final Class targetClass; + + public MethodCacheKey(Method method, Class targetClass) { + Assert.notNull(method, "method must be set."); + Assert.notNull(targetClass, "targetClass must be set."); + this.method = method; + this.targetClass = targetClass; + } + + @Override + public boolean equals(Object other) { + if (this == other) { + return true; + } + if (!(other instanceof MethodCacheKey)) { + return false; + } + MethodCacheKey otherKey = (MethodCacheKey) other; + return (this.method.equals(otherKey.method) && ObjectUtils.nullSafeEquals(this.targetClass, + otherKey.targetClass)); + } + + @Override + public int hashCode() { + return this.method.hashCode() * 29 + (this.targetClass != null ? this.targetClass.hashCode() : 0); + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleCacheResolver.java b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleCacheResolver.java new file mode 100644 index 0000000000..a33f457d28 --- /dev/null +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleCacheResolver.java @@ -0,0 +1,54 @@ +/* + * 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.interceptor; + +import java.util.ArrayList; +import java.util.Collection; + +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.util.Assert; + +/** + * A simple {@link CacheResolver} that resolves the {@link Cache} instance(s) + * based on a configurable {@link CacheManager} and the name of the + * cache(s): {@link BasicCacheOperation#getCacheNames()} + * + * @author Stephane Nicoll + * @since 4.1 + * @see BasicCacheOperation#getCacheNames() + */ +public class SimpleCacheResolver implements CacheResolver { + + private final CacheManager cacheManager; + + public SimpleCacheResolver(CacheManager cacheManager) { + this.cacheManager = cacheManager; + } + + @Override + public Collection resolveCaches(CacheOperationInvocationContext context) { + Collection result = new ArrayList(); + for (String cacheName : context.getOperation().getCacheNames()) { + Cache cache = cacheManager.getCache(cacheName); + Assert.notNull(cache, "Cannot find cache named '" + cacheName + "' for " + context.getOperation()); + result.add(cache); + } + return result; + } + +} diff --git a/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKey.java b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKey.java index 097be2d20b..e8fb72bc4f 100644 --- a/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKey.java +++ b/spring-context/src/main/java/org/springframework/cache/interceptor/SimpleKey.java @@ -30,11 +30,12 @@ import org.springframework.util.StringUtils; * @see SimpleKeyGenerator */ @SuppressWarnings("serial") -public final class SimpleKey implements Serializable { +public class SimpleKey implements Serializable { public static final SimpleKey EMPTY = new SimpleKey(); private final Object[] params; + private final int hashCode; /** @@ -45,9 +46,9 @@ public final class SimpleKey implements Serializable { Assert.notNull(elements, "Elements must not be null"); this.params = new Object[elements.length]; System.arraycopy(elements, 0, this.params, 0, elements.length); + this.hashCode = Arrays.deepHashCode(this.params); } - @Override public boolean equals(Object obj) { return (this == obj || (obj instanceof SimpleKey @@ -55,13 +56,13 @@ public final class SimpleKey implements Serializable { } @Override - public int hashCode() { - return Arrays.deepHashCode(this.params); + public final int hashCode() { + return hashCode; } @Override public String toString() { - return "SimpleKey [" + StringUtils.arrayToCommaDelimitedString(this.params) + "]"; + return getClass().getSimpleName() + " [" + StringUtils.arrayToCommaDelimitedString(this.params) + "]"; } } diff --git a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java index ad0481dee0..20353ae664 100644 --- a/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java +++ b/spring-context/src/main/java/org/springframework/context/annotation/AnnotationConfigUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2002-2013 the original author or authors. + * 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. @@ -48,6 +48,7 @@ import org.springframework.util.ClassUtils; * @author Juergen Hoeller * @author Chris Beams * @author Phillip Webb + * @author Stephane Nicoll * @since 2.5 * @see ContextAnnotationAutowireCandidateResolver * @see CommonAnnotationBeanPostProcessor @@ -147,6 +148,48 @@ public class AnnotationConfigUtils { public static final String CACHE_ASPECT_CONFIGURATION_CLASS_NAME = "org.springframework.cache.aspectj.AspectJCachingConfiguration"; + /** + * The bean name of the internally managed JSR-107 cache advisor. + */ + public static final String JCACHE_ADVISOR_BEAN_NAME = + "org.springframework.cache.config.internalJCacheAdvisor"; + + /** + * The class name of the JSR-107 cache operation source. + */ + public static final String JCACHE_OPERATION_SOURCE_CLASS + = "org.springframework.cache.jcache.interceptor.DefaultJCacheOperationSource"; + + /** + * The class name of the JSR-107 cache interceptor. + */ + public static final String JCACHE_INTERCEPTOR_CLASS = + "org.springframework.cache.jcache.interceptor.JCacheInterceptor"; + + /** + * The class name of the JSR-107 cache advisor factory. + */ + public static final String JCACHE_ADVISOR_FACTORY_CLASS = + "org.springframework.cache.jcache.interceptor.BeanFactoryJCacheOperationSourceAdvisor"; + + /** + * The bean name of the internally managed JSR-107 cache aspect. + */ + public static final String JCACHE_ASPECT_BEAN_NAME = + "org.springframework.cache.config.internalJCacheAspect"; + + /** + * The class name of the AspectJ JSR-107 cache aspect. + */ + public static final String JCACHE_ASPECT_CLASS_NAME = + "org.springframework.cache.aspectj.JCacheCacheAspect"; + + /** + * The name of the AspectJ JSR-107 cache aspect @{@code Configuration} class. + */ + public static final String JCACHE_ASPECT_CONFIGURATION_CLASS_NAME = + "org.springframework.cache.aspectj.AspectJJCacheConfiguration"; + /** * The bean name of the internally managed JPA annotation processor. */ diff --git a/spring-core/src/main/java/org/springframework/util/filter/ExceptionTypeFilter.java b/spring-core/src/main/java/org/springframework/util/filter/ExceptionTypeFilter.java new file mode 100644 index 0000000000..cd005e426d --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/filter/ExceptionTypeFilter.java @@ -0,0 +1,40 @@ +/* + * 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.util.filter; + +import java.util.Collection; + +/** + * An {@link InstanceFilter} implementation that handles exception types. A type + * will match against a given candidate if it is assignable to that candidate. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public class ExceptionTypeFilter extends InstanceFilter> { + + public ExceptionTypeFilter(Collection> includes, + Collection> excludes, boolean matchIfEmpty) { + super(includes, excludes, matchIfEmpty); + } + + @Override + protected boolean match(Class instance, Class candidate) { + return candidate.isAssignableFrom(instance); + } + +} diff --git a/spring-core/src/main/java/org/springframework/util/filter/InstanceFilter.java b/spring-core/src/main/java/org/springframework/util/filter/InstanceFilter.java new file mode 100644 index 0000000000..99f96000ce --- /dev/null +++ b/spring-core/src/main/java/org/springframework/util/filter/InstanceFilter.java @@ -0,0 +1,123 @@ +/* + * 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.util.filter; + +import java.util.Collection; +import java.util.Collections; + +import org.springframework.util.Assert; + +/** + * A simple instance filter that checks if a given instance match based on + * a collection of includes and excludes element. + * + *

Subclasses may want to override {@link #match(Object, Object)} to provide + * a custom matching algorithm. + * + * @author Stephane Nicoll + * @since 4.1 + */ +public class InstanceFilter { + + private final Collection includes; + + private final Collection excludes; + + private final boolean matchIfEmpty; + + /** + * Create a new instance based on includes/excludes collections. + *

A particular element will match if it "matches" the one of the element in the + * includes list and does not match one of the element in the excludes list. + *

Subclasses may redefine what matching means. By default, an element match with + * another if it is equals according to {@link Object#equals(Object)} + *

If both collections are empty, {@code matchIfEmpty} defines if + * an element matches or not. + * + * @param includes the collection of includes + * @param excludes the collection of excludes + * @param matchIfEmpty the matching result if both the includes and the excludes + * collections are empty + */ + public InstanceFilter(Collection includes, + Collection excludes, boolean matchIfEmpty) { + + this.includes = includes != null ? includes : Collections.emptyList(); + this.excludes = excludes != null ? excludes : Collections.emptyList(); + this.matchIfEmpty = matchIfEmpty; + } + + /** + * Determine if the specified {code instance} matches this filter. + */ + public boolean match(T instance) { + Assert.notNull(instance, "The instance to match is mandatory."); + + boolean includesSet = !includes.isEmpty(); + boolean excludesSet = !excludes.isEmpty(); + if (!includesSet && !excludesSet) { + return matchIfEmpty; + } + + boolean matchIncludes = match(instance, includes); + boolean matchExcludes = match(instance, excludes); + + if (!includesSet) { + return !matchExcludes; + } + + if (!excludesSet) { + return matchIncludes; + } + return matchIncludes && !matchExcludes; + } + + /** + * Determine if the specified {@code instance} is equal to the + * specified {@code candidate}. + * + * @param instance the instance to handle + * @param candidate a candidate defined by this filter + * @return {@code true} if the instance matches the candidate + */ + protected boolean match(T instance, T candidate) { + return instance.equals(candidate); + } + + /** + * Determine if the specified {@code instance} matches one of the candidates. + *

If the candidates collection is {@code null}, returns {@code false}. + * + * @param instance the instance to check + * @param candidates a list of candidates + * @return {@code true} if the instance match or the candidates collection is null + */ + protected boolean match(T instance, Collection candidates) { + for (T candidate : candidates) { + if (match(instance, candidate)) { + return true; + } + } + return false; + } + + @Override + public String toString() { + return "includes=" + includes + ", excludes=" + excludes + ", matchIfEmpty=" + matchIfEmpty; + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/filter/ExceptionTypeFilterTests.java b/spring-core/src/test/java/org/springframework/util/filter/ExceptionTypeFilterTests.java new file mode 100644 index 0000000000..d8a05481ff --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/filter/ExceptionTypeFilterTests.java @@ -0,0 +1,37 @@ +/* + * 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.util.filter; + +import static java.util.Arrays.*; +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * @author Stephane Nicoll + */ +public class ExceptionTypeFilterTests { + + @Test + public void subClassMatch() { + ExceptionTypeFilter filter = new ExceptionTypeFilter( + asList(RuntimeException.class), null, true); + assertTrue(filter.match(RuntimeException.class)); + assertTrue(filter.match(IllegalStateException.class)); + } + +} diff --git a/spring-core/src/test/java/org/springframework/util/filter/InstanceFilterTests.java b/spring-core/src/test/java/org/springframework/util/filter/InstanceFilterTests.java new file mode 100644 index 0000000000..48c12156b3 --- /dev/null +++ b/spring-core/src/test/java/org/springframework/util/filter/InstanceFilterTests.java @@ -0,0 +1,75 @@ +/* + * 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.util.filter; + +import static java.util.Arrays.*; +import static org.junit.Assert.*; + +import org.junit.Test; + +/** + * @author Stephane Nicoll + */ +public class InstanceFilterTests { + + @Test + public void emptyFilterApplyMatchIfEmpty() { + InstanceFilter filter = new InstanceFilter(null, null, true); + match(filter, "foo"); + match(filter, "bar"); + } + + @Test + public void includesFilter() { + InstanceFilter filter = new InstanceFilter( + asList("First", "Second"), null, true); + match(filter, "Second"); + doNotMatch(filter, "foo"); + } + + @Test + public void excludesFilter() { + InstanceFilter filter = new InstanceFilter( + null, asList("First", "Second"), true); + doNotMatch(filter, "Second"); + match(filter, "foo"); + } + + @Test + public void includesAndExcludesFilters() { + InstanceFilter filter = new InstanceFilter( + asList("foo", "Bar"), asList("First", "Second"), true); + doNotMatch(filter, "Second"); + match(filter, "foo"); + } + + @Test + public void includesAndExcludesFiltersConflict() { + InstanceFilter filter = new InstanceFilter( + asList("First"), asList("First"), true); + doNotMatch(filter, "First"); + } + + private void match(InstanceFilter filter, T candidate) { + assertTrue("filter '" + filter + "' should match " + candidate, filter.match(candidate)); + } + + private void doNotMatch(InstanceFilter filter, T candidate) { + assertFalse("filter '" + filter + "' should not match " + candidate, filter.match(candidate)); + } + +}