From 47a432719348dd9773a1bec2c3500771168ab962 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Fri, 21 Feb 2014 12:14:32 +0100 Subject: [PATCH] Add JSR-107 cache annotations support This commit adds support for the JSR-107 cache annotations alongside the Spring's cache annotations, that is @CacheResult, @CachePut, @CacheRemove and @CacheRemoveAll as well as related annotations @CacheDefaults, @CacheKey and @CacheValue. Spring's caching configuration infrastructure detects the presence of the JSR-107 API and Spring's JCache implementation. Both @EnableCaching and the cache namespace are able to configure the required JCache infrastructure when necessary. Both proxy mode and AspectJ mode are supported. As JSR-107 permits the customization of the CacheResolver to use for both regular and exception caches, JCacheConfigurer has been introduced as an extension of CachingConfigurer and permits to define those. If an exception is cached and should be rethrown, it is cloned and the call stack is rewritten so that it matches the calling thread each time. If the exception cannot be cloned, the original exception is returned. Internally, the interceptors uses Spring's caching abstraction by default with an adapter layer when a JSR-107 component needs to be called. This is the case for CacheResolver and CacheKeyGenerator. The implementation uses Spring's CacheManager abstraction behind the scene. The standard annotations can therefore be used against any CacheManager implementation. Issue: SPR-9616 --- build.gradle | 3 +- .../cache/aspectj/AbstractCacheAspect.aj | 5 +- .../aspectj/AspectJCachingConfiguration.java | 1 + .../aspectj/AspectJJCacheConfiguration.java | 48 ++ .../cache/aspectj/JCacheCacheAspect.aj | 112 ++++ .../cache/aspectj/AspectJAnnotationTests.java | 1 + .../aspectj/AspectJJcacheJavaConfigTests.java | 72 +++ .../AspectJJcacheNamespaceConfigTests.java | 35 ++ .../AnnotatedClassCacheableService.java | 1 + .../config/AnnotatedJCacheableService.java | 201 +++++++ .../cache/config/CacheableService.java | 1 + .../cache/config/DefaultCacheableService.java | 1 + .../cache/config/SomeCustomKeyGenerator.java | 1 + .../cache/config/TestEntity.java | 3 +- .../config/annotation-jcache-aspectj.xml | 36 ++ .../config/AbstractJCacheConfiguration.java | 69 +++ .../cache/jcache/config/JCacheConfigurer.java | 80 +++ .../config/ProxyJCacheConfiguration.java | 61 ++ .../cache/jcache/config/package-info.java | 8 + .../interceptor/AbstractCacheInterceptor.java | 78 +++ ...AbstractFallbackJCacheOperationSource.java | 118 ++++ .../AbstractKeyCacheInterceptor.java | 61 ++ .../AnnotationCacheOperationSource.java | 269 +++++++++ ...anFactoryJCacheOperationSourceAdvisor.java | 64 +++ .../interceptor/CachePutInterceptor.java | 71 +++ .../CacheRemoveAllInterceptor.java | 72 +++ .../CacheRemoveEntryInterceptor.java | 72 +++ .../interceptor/CacheResolverAdapter.java | 54 ++ .../interceptor/CacheResultInterceptor.java | 163 ++++++ .../DefaultCacheInvocationContext.java | 112 ++++ .../DefaultCacheKeyInvocationContext.java | 62 ++ .../DefaultJCacheOperationSource.java | 133 +++++ .../interceptor/JCacheAspectSupport.java | 131 +++++ .../jcache/interceptor/JCacheInterceptor.java | 68 +++ .../interceptor/JCacheOperationSource.java | 44 ++ .../JCacheOperationSourcePointcut.java | 70 +++ .../interceptor/KeyGeneratorAdapter.java | 60 ++ .../interceptor/SimpleCacheKeyGenerator.java | 31 + .../SimpleExceptionCacheResolver.java | 63 +++ .../interceptor/SimpleGeneratedCacheKey.java | 20 + .../jcache/interceptor/package-info.java | 8 + .../jcache/model/BaseCacheOperation.java | 227 ++++++++ .../jcache/model/BaseKeyCacheOperation.java | 101 ++++ .../cache/jcache/model/CachePutOperation.java | 102 ++++ .../jcache/model/CacheRemoveAllOperation.java | 56 ++ .../jcache/model/CacheRemoveOperation.java | 58 ++ .../jcache/model/CacheResultOperation.java | 84 +++ .../model/DefaultCacheMethodDetails.java | 84 +++ .../cache/jcache/model/JCacheOperation.java | 53 ++ .../cache/jcache/model/package-info.java | 5 + .../cache/jcache/AbstractJCacheTests.java | 66 +++ .../config/AbstractJCacheAnnotationTests.java | 533 ++++++++++++++++++ .../jcache/config/JCacheJavaConfigTests.java | 121 ++++ .../config/JCacheNamespaceDrivenTests.java | 33 ++ .../config/JCacheStandaloneConfigTests.java | 33 ++ .../jcache/config/JCacheableService.java | 62 ++ .../AnnotatedJCacheableService.java | 201 +++++++ .../AnnotationCacheOperationSourceTests.java | 262 +++++++++ .../CacheResolverAdapterTests.java | 105 ++++ .../interceptor/JCacheInterceptorTests.java | 168 ++++++ .../model/AbstractCacheOperationTests.java | 77 +++ .../jcache/model/CachePutOperationTests.java | 96 ++++ .../model/CacheRemoveAllOperationTests.java | 48 ++ .../model/CacheRemoveOperationTests.java | 49 ++ .../model/CacheResultOperationTests.java | 131 +++++ .../cache/jcache/model/SampleObject.java | 80 +++ .../support/TestableCacheKeyGenerator.java | 25 + .../jcache/support/TestableCacheResolver.java | 41 ++ .../support/TestableCacheResolverFactory.java | 41 ++ .../jcache/config/jCacheNamespaceDriven.xml | 38 ++ .../jcache/config/jCacheStandaloneConfig.xml | 48 ++ .../AbstractCachingConfiguration.java | 24 +- .../CachingConfigurationSelector.java | 58 +- .../cache/annotation/EnableCaching.java | 6 + .../annotation/ProxyCachingConfiguration.java | 4 +- ...tationDrivenCacheBeanDefinitionParser.java | 151 ++++- .../AbstractFallbackCacheOperationSource.java | 40 +- .../interceptor/BasicCacheOperation.java | 34 ++ .../cache/interceptor/CacheAspectSupport.java | 10 +- .../cache/interceptor/CacheInterceptor.java | 17 +- .../CacheOperationInvocationContext.java | 52 ++ .../interceptor/CacheOperationInvoker.java | 58 ++ .../cache/interceptor/CacheResolver.java | 41 ++ .../cache/interceptor/MethodCacheKey.java | 47 ++ .../interceptor/SimpleCacheResolver.java | 54 ++ .../cache/interceptor/SimpleKey.java | 11 +- .../annotation/AnnotationConfigUtils.java | 45 +- .../util/filter/ExceptionTypeFilter.java | 40 ++ .../util/filter/InstanceFilter.java | 123 ++++ .../util/filter/ExceptionTypeFilterTests.java | 37 ++ .../util/filter/InstanceFilterTests.java | 75 +++ 91 files changed, 6408 insertions(+), 110 deletions(-) create mode 100644 spring-aspects/src/main/java/org/springframework/cache/aspectj/AspectJJCacheConfiguration.java create mode 100644 spring-aspects/src/main/java/org/springframework/cache/aspectj/JCacheCacheAspect.aj create mode 100644 spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJJcacheJavaConfigTests.java create mode 100644 spring-aspects/src/test/java/org/springframework/cache/aspectj/AspectJJcacheNamespaceConfigTests.java create mode 100644 spring-aspects/src/test/java/org/springframework/cache/config/AnnotatedJCacheableService.java create mode 100644 spring-aspects/src/test/java/org/springframework/cache/config/annotation-jcache-aspectj.xml create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/config/AbstractJCacheConfiguration.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/config/JCacheConfigurer.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/config/ProxyJCacheConfiguration.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/config/package-info.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractCacheInterceptor.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractFallbackJCacheOperationSource.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AbstractKeyCacheInterceptor.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/AnnotationCacheOperationSource.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/BeanFactoryJCacheOperationSourceAdvisor.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CachePutInterceptor.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveAllInterceptor.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheRemoveEntryInterceptor.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResolverAdapter.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/CacheResultInterceptor.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultCacheInvocationContext.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultCacheKeyInvocationContext.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/DefaultJCacheOperationSource.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheAspectSupport.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheInterceptor.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSource.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/JCacheOperationSourcePointcut.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/KeyGeneratorAdapter.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleCacheKeyGenerator.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleExceptionCacheResolver.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/SimpleGeneratedCacheKey.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/interceptor/package-info.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/model/BaseCacheOperation.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/model/BaseKeyCacheOperation.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/model/CachePutOperation.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/model/CacheRemoveAllOperation.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/model/CacheRemoveOperation.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/model/CacheResultOperation.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/model/DefaultCacheMethodDetails.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/model/JCacheOperation.java create mode 100644 spring-context-support/src/main/java/org/springframework/cache/jcache/model/package-info.java create mode 100644 spring-context-support/src/test/java/org/springframework/cache/jcache/AbstractJCacheTests.java create mode 100644 spring-context-support/src/test/java/org/springframework/cache/jcache/config/AbstractJCacheAnnotationTests.java create mode 100644 spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheJavaConfigTests.java create mode 100644 spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheNamespaceDrivenTests.java create mode 100644 spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheStandaloneConfigTests.java create mode 100644 spring-context-support/src/test/java/org/springframework/cache/jcache/config/JCacheableService.java create mode 100644 spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AnnotatedJCacheableService.java create mode 100644 spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/AnnotationCacheOperationSourceTests.java create mode 100644 spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/CacheResolverAdapterTests.java create mode 100644 spring-context-support/src/test/java/org/springframework/cache/jcache/interceptor/JCacheInterceptorTests.java create mode 100644 spring-context-support/src/test/java/org/springframework/cache/jcache/model/AbstractCacheOperationTests.java create mode 100644 spring-context-support/src/test/java/org/springframework/cache/jcache/model/CachePutOperationTests.java create mode 100644 spring-context-support/src/test/java/org/springframework/cache/jcache/model/CacheRemoveAllOperationTests.java create mode 100644 spring-context-support/src/test/java/org/springframework/cache/jcache/model/CacheRemoveOperationTests.java create mode 100644 spring-context-support/src/test/java/org/springframework/cache/jcache/model/CacheResultOperationTests.java create mode 100644 spring-context-support/src/test/java/org/springframework/cache/jcache/model/SampleObject.java create mode 100644 spring-context-support/src/test/java/org/springframework/cache/jcache/support/TestableCacheKeyGenerator.java create mode 100644 spring-context-support/src/test/java/org/springframework/cache/jcache/support/TestableCacheResolver.java create mode 100644 spring-context-support/src/test/java/org/springframework/cache/jcache/support/TestableCacheResolverFactory.java create mode 100644 spring-context-support/src/test/resources/org/springframework/cache/jcache/config/jCacheNamespaceDriven.xml create mode 100644 spring-context-support/src/test/resources/org/springframework/cache/jcache/config/jCacheStandaloneConfig.xml create mode 100644 spring-context/src/main/java/org/springframework/cache/interceptor/BasicCacheOperation.java create mode 100644 spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvocationContext.java create mode 100644 spring-context/src/main/java/org/springframework/cache/interceptor/CacheOperationInvoker.java create mode 100644 spring-context/src/main/java/org/springframework/cache/interceptor/CacheResolver.java create mode 100644 spring-context/src/main/java/org/springframework/cache/interceptor/MethodCacheKey.java create mode 100644 spring-context/src/main/java/org/springframework/cache/interceptor/SimpleCacheResolver.java create mode 100644 spring-core/src/main/java/org/springframework/util/filter/ExceptionTypeFilter.java create mode 100644 spring-core/src/main/java/org/springframework/util/filter/InstanceFilter.java create mode 100644 spring-core/src/test/java/org/springframework/util/filter/ExceptionTypeFilterTests.java create mode 100644 spring-core/src/test/java/org/springframework/util/filter/InstanceFilterTests.java 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)); + } + +}