From 151220f41d1108c9b84f98c6d8eddc710a0d5863 Mon Sep 17 00:00:00 2001 From: Stephane Nicoll Date: Tue, 17 Mar 2015 16:21:23 +0100 Subject: [PATCH] Add cache manager auto-configuration Add support for cache manager auto-configuration that is triggered when the `EnableCaching` annotation is added to a Spring Boot application. A new "spring.cache" set of configuration keys is also provided. The "spring.cache.mode" allows the user to specify the cache provider that should be auto-configured. If no explicit configuration is provided, the environment is checked for the best suited cache implementation, that is: - Generic if at least one `Cache` bean is defined in the context. - Hazelcast if either a default configuration file is present or the `spring.cache.config` property is set. - JCache if one JSR-107 provider is present - Redis if a `RedisTemplate` is defined in the context - Guava - Simple as a fallback option, using concurrent maps - NoOp (that is, no cache) if the mode is set to "none" If the provider supports it, it is possible to specify the caches to create on startup via `spring.cache.cache-names`. If the provider relies on a configuration file and a custom one needs to be used `spring.cache.config` can be set to such custom resource. If more than one JSR-107 provider is present, it is possible to force the provider to use by setting the mode to `jcache` and specifying the fully qualified class name of the CachingProvider to use via `spring.cache.jcache.provider`. See gh-2633 --- spring-boot-autoconfigure/pom.xml | 15 + .../cache/CacheAutoConfiguration.java | 141 +++++ .../autoconfigure/cache/CacheCondition.java | 52 ++ .../cache/CacheConfigFileCondition.java | 68 +++ .../autoconfigure/cache/CacheProperties.java | 141 +++++ .../boot/autoconfigure/cache/CacheType.java | 82 +++ .../cache/GenericCacheConfiguration.java | 50 ++ .../cache/GuavaCacheConfiguration.java | 92 ++++ .../cache/HazelcastCacheConfiguration.java | 96 ++++ .../cache/JCacheCacheConfiguration.java | 136 +++++ .../cache/JCacheManagerCustomizer.java | 38 ++ .../cache/NoOpCacheConfiguration.java | 42 ++ .../cache/RedisCacheConfiguration.java | 56 ++ .../cache/SimpleCacheConfiguration.java | 53 ++ .../autoconfigure/cache/package-info.java | 20 + .../main/resources/META-INF/spring.factories | 1 + .../cache/CacheAutoConfigurationTests.java | 502 ++++++++++++++++++ .../cache/support/MockCachingProvider.java | 117 ++++ .../services/javax.cache.spi.CachingProvider | 4 + .../src/test/resources/hazelcast.xml | 8 + .../cache/hazelcast-specific.xml | 10 + spring-boot-dependencies/pom.xml | 11 + 22 files changed, 1735 insertions(+) create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfiguration.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheCondition.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheConfigFileCondition.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheProperties.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheType.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/GenericCacheConfiguration.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/GuavaCacheConfiguration.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/HazelcastCacheConfiguration.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/JCacheCacheConfiguration.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/JCacheManagerCustomizer.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/NoOpCacheConfiguration.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/RedisCacheConfiguration.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/SimpleCacheConfiguration.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/package-info.java create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfigurationTests.java create mode 100644 spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/support/MockCachingProvider.java create mode 100644 spring-boot-autoconfigure/src/test/resources/META-INF/services/javax.cache.spi.CachingProvider create mode 100644 spring-boot-autoconfigure/src/test/resources/hazelcast.xml create mode 100644 spring-boot-autoconfigure/src/test/resources/org/springframework/boot/autoconfigure/cache/hazelcast-specific.xml diff --git a/spring-boot-autoconfigure/pom.xml b/spring-boot-autoconfigure/pom.xml index cb3bfab9db..fbadb64f89 100644 --- a/spring-boot-autoconfigure/pom.xml +++ b/spring-boot-autoconfigure/pom.xml @@ -60,11 +60,26 @@ gson true + + com.hazelcast + hazelcast + true + + + com.hazelcast + hazelcast-spring + true + com.samskivert jmustache true + + javax.cache + cache-api + true + org.flywaydb flyway-core diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfiguration.java new file mode 100644 index 0000000000..0dd37583c3 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfiguration.java @@ -0,0 +1,141 @@ +/* + * Copyright 2012-2015 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.boot.autoconfigure.cache; + +import javax.annotation.PostConstruct; + +import org.springframework.beans.BeansException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.config.BeanDefinition; +import org.springframework.beans.factory.config.BeanFactoryPostProcessor; +import org.springframework.beans.factory.config.ConfigurableListableBeanFactory; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration.CacheConfigurationImportSelector; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.redis.RedisAutoConfiguration; +import org.springframework.boot.context.properties.EnableConfigurationProperties; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.interceptor.CacheAspectSupport; +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.context.annotation.ImportSelector; +import org.springframework.context.annotation.Role; +import org.springframework.core.type.AnnotationMetadata; +import org.springframework.util.Assert; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for the cache abstraction. Creates a + * {@link CacheManager} if necessary when caching is enabled via {@link EnableCaching}. + *

+ * Cache store can be auto-detected or specified explicitly via configuration. + * + * @author Stephane Nicoll + * @since 1.3.0 + * @see EnableCaching + */ +@Configuration +@ConditionalOnClass(CacheManager.class) +@ConditionalOnBean(CacheAspectSupport.class) +@ConditionalOnMissingBean({ CacheManager.class, CacheResolver.class }) +@EnableConfigurationProperties(CacheProperties.class) +@AutoConfigureAfter(RedisAutoConfiguration.class) +@Import(CacheConfigurationImportSelector.class) +public class CacheAutoConfiguration { + + static final String VALIDATOR_BEAN_NAME = "cacheAutoConfigurationValidator"; + + @Bean + @Role(BeanDefinition.ROLE_INFRASTRUCTURE) + public static BeanFactoryPostProcessor cacheAutoConfigurationValidatorPostProcessor() { + return new CacheManagerValidatorPostProcessor(); + } + + @Bean(name = VALIDATOR_BEAN_NAME) + public CacheManagerValidator cacheAutoConfigurationValidator() { + return new CacheManagerValidator(); + } + + /** + * {@link BeanFactoryPostProcessor} to ensure that the {@link CacheManagerValidator} + * is triggered before {@link CacheAspectSupport} but without causing early + * instantiation. + */ + static class CacheManagerValidatorPostProcessor implements BeanFactoryPostProcessor { + @Override + public void postProcessBeanFactory(ConfigurableListableBeanFactory beanFactory) + throws BeansException { + for (String name : beanFactory.getBeanNamesForType(CacheAspectSupport.class)) { + BeanDefinition definition = beanFactory.getBeanDefinition(name); + definition.setDependsOn(append(definition.getDependsOn(), + VALIDATOR_BEAN_NAME)); + } + } + + private String[] append(String[] array, String value) { + String[] result = new String[array == null ? 1 : array.length + 1]; + if (array != null) { + System.arraycopy(array, 0, result, 0, array.length); + } + result[result.length - 1] = value; + return result; + } + } + + /** + * Bean used to validate that a CacheManager exists and provide a more meaningful + * exception. + */ + static class CacheManagerValidator { + + @Autowired + private CacheProperties cacheProperties; + + @Autowired(required = false) + private CacheManager beanFactory; + + @PostConstruct + public void checkHasCacheManager() { + Assert.notNull(this.beanFactory, "No cache manager could " + + "be auto-configured, check your configuration (caching " + + "type is '" + this.cacheProperties.getType() + "')"); + } + } + + /** + * {@link ImportSelector} to add {@link CacheType} configuration classes. + */ + static class CacheConfigurationImportSelector implements ImportSelector { + + @Override + public String[] selectImports(AnnotationMetadata importingClassMetadata) { + CacheType[] types = CacheType.values(); + String[] imports = new String[types.length]; + for (int i = 0; i < types.length; i++) { + imports[i] = types[i].getConfigurationClass().getName(); + } + return imports; + } + + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheCondition.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheCondition.java new file mode 100644 index 0000000000..77cdd4a5ce --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheCondition.java @@ -0,0 +1,52 @@ +/* + * Copyright 2012-2015 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.boot.autoconfigure.cache; + +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.boot.bind.RelaxedPropertyResolver; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.core.type.AnnotationMetadata; + +/** + * General cache condition used with all cache configuration classes. + * + * @author Stephane Nicoll + * @author Phillip Webb + * @since 1.3.0 + */ +class CacheCondition extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, + AnnotatedTypeMetadata metadata) { + RelaxedPropertyResolver resolver = new RelaxedPropertyResolver( + context.getEnvironment(), "spring.cache."); + if (!resolver.containsProperty("type")) { + return ConditionOutcome.match("Automatic cache type"); + } + CacheType cacheType = CacheType + .forConfigurationClass(((AnnotationMetadata) metadata).getClassName()); + String value = resolver.getProperty("type").replace("-", "_").toUpperCase(); + if (value.equals(cacheType.name())) { + return ConditionOutcome.match("Cache type " + cacheType); + } + return ConditionOutcome.noMatch("Cache type " + value); + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheConfigFileCondition.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheConfigFileCondition.java new file mode 100644 index 0000000000..aa03c8cbbe --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheConfigFileCondition.java @@ -0,0 +1,68 @@ +/* + * Copyright 2012-2015 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.boot.autoconfigure.cache; + +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.boot.bind.RelaxedPropertyResolver; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.core.io.Resource; +import org.springframework.core.type.AnnotatedTypeMetadata; + +/** + * {@link SpringBootCondition} used to check if a cache configuration file can be found. + * + * @author Stephane Nicoll + * @author Phillip Webb + * @since 1.3.0 + */ +abstract class CacheConfigFileCondition extends SpringBootCondition { + + private final String name; + + private final String[] resourceLocations; + + public CacheConfigFileCondition(String name, String... resourceLocations) { + this.name = name; + this.resourceLocations = resourceLocations; + } + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, + AnnotatedTypeMetadata metadata) { + RelaxedPropertyResolver resolver = new RelaxedPropertyResolver( + context.getEnvironment(), "spring.cache."); + if (resolver.containsProperty("config")) { + return ConditionOutcome.match("A spring.cache.config property is specified"); + } + return getResourceOutcome(context, metadata); + } + + protected ConditionOutcome getResourceOutcome(ConditionContext context, + AnnotatedTypeMetadata metadata) { + for (String location : this.resourceLocations) { + Resource resource = context.getResourceLoader().getResource(location); + if (resource != null && resource.exists()) { + return ConditionOutcome.match("Found " + this.name + " config in " + + resource); + } + } + return ConditionOutcome.noMatch("No specific " + this.name + + " configuration found"); + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheProperties.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheProperties.java new file mode 100644 index 0000000000..c5aa4eb48d --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheProperties.java @@ -0,0 +1,141 @@ +/* + * Copyright 2012-2015 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.boot.autoconfigure.cache; + +import java.util.ArrayList; +import java.util.List; + +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.core.io.Resource; +import org.springframework.util.Assert; + +/** + * Configuration properties for the cache abstraction. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +@ConfigurationProperties(prefix = "spring.cache") +public class CacheProperties { + + /** + * Cache type, auto-detected according to the environment by default. + */ + private CacheType type; + + /** + * The location of the configuration file to use to initialize the cache library. + */ + private Resource config; + + /** + * Comma-separated list of cache names to create if supported by the underlying cache + * manager. Usually, this disables the ability to create additional caches on-the-fly. + */ + private final List cacheNames = new ArrayList(); + + private final JCache jcache = new JCache(); + + private final Guava guava = new Guava(); + + public CacheType getType() { + return this.type; + } + + public void setType(CacheType mode) { + this.type = mode; + } + + public Resource getConfig() { + return this.config; + } + + public void setConfig(Resource config) { + this.config = config; + } + + public List getCacheNames() { + return this.cacheNames; + } + + public JCache getJcache() { + return this.jcache; + } + + public Guava getGuava() { + return this.guava; + } + + /** + * Resolve the config location if set. + * @return the location or {@code null} if it is not set + * @throws IllegalArgumentException if the config attribute is set to a unknown + * location + */ + public Resource resolveConfigLocation() { + if (this.config != null) { + Assert.isTrue(this.config.exists(), "Cache configuration field defined by " + + "'spring.cache.config' does not exist " + this.config); + return this.config; + } + return null; + } + + /** + * JCache (JSR-107) specific cache properties. + */ + public static class JCache { + + /** + * Fully qualified name of the CachingProvider implementation to use to retrieve + * the JSR-107 compliant cache manager. Only needed if more than one JSR-107 + * implementation is available on the classpath. + */ + private String provider; + + public String getProvider() { + return this.provider; + } + + public void setProvider(String provider) { + this.provider = provider; + } + + } + + /** + * Guava specific cache properties. + */ + public static class Guava { + + /** + * The spec to use to create caches. Check CacheBuilderSpec for more details on + * the spec format. + */ + private String spec; + + public String getSpec() { + return this.spec; + } + + public void setSpec(String spec) { + this.spec = spec; + } + + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheType.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheType.java new file mode 100644 index 0000000000..45ddaa4212 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheType.java @@ -0,0 +1,82 @@ +/* + * Copyright 2012-2015 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.boot.autoconfigure.cache; + +/** + * Supported cache types (defined in order of precedence). + * + * @author Stephane Nicoll + * @author Phillip Webb + * @since 1.3.0 + */ +public enum CacheType { + + /** + * Generic caching using 'Cache 'beans from the context. + */ + GENERIC(GenericCacheConfiguration.class), + + /** + * Haezelcast backed caching + */ + HAZELCAST(HazelcastCacheConfiguration.class), + + /** + * JCache (JSR-107) backed caching. + */ + JCACHE(JCacheCacheConfiguration.class), + + /** + * Redis backed caching. + */ + REDIS(RedisCacheConfiguration.class), + + /** + * Guava backed caching. + */ + GUAVA(GuavaCacheConfiguration.class), + + /** + * Simple in-memory caching. + */ + SIMPLE(SimpleCacheConfiguration.class), + + /** + * No caching. + */ + NONE(NoOpCacheConfiguration.class); + + private final Class configurationClass; + + CacheType(Class configurationClass) { + this.configurationClass = configurationClass; + } + + Class getConfigurationClass() { + return this.configurationClass; + } + + static CacheType forConfigurationClass(String configurationClass) { + for (CacheType type : values()) { + if (type.getConfigurationClass().getName().equals(configurationClass)) { + return type; + } + } + throw new IllegalArgumentException("Unsupported class " + configurationClass); + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/GenericCacheConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/GenericCacheConfiguration.java new file mode 100644 index 0000000000..244122e2ef --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/GenericCacheConfiguration.java @@ -0,0 +1,50 @@ +/* + * Copyright 2012-2015 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.boot.autoconfigure.cache; + +import java.util.Collection; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.support.SimpleCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; + +/** + * Generic cache configuration based on arbitrary {@link Cache} instances defined in the + * context. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +@Configuration +@ConditionalOnBean(Cache.class) +@ConditionalOnMissingBean(CacheManager.class) +@Conditional(CacheCondition.class) +class GenericCacheConfiguration { + + @Bean + public SimpleCacheManager cacheManager(Collection caches) { + SimpleCacheManager cacheManager = new SimpleCacheManager(); + cacheManager.setCaches(caches); + return cacheManager; + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/GuavaCacheConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/GuavaCacheConfiguration.java new file mode 100644 index 0000000000..f14c5f193f --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/GuavaCacheConfiguration.java @@ -0,0 +1,92 @@ +/* + * Copyright 2012-2015 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.boot.autoconfigure.cache; + +import java.util.List; + +import org.apache.commons.collections.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cache.CacheManager; +import org.springframework.cache.guava.GuavaCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +import com.google.common.cache.CacheBuilder; +import com.google.common.cache.CacheBuilderSpec; +import com.google.common.cache.CacheLoader; + +/** + * Guava cache configuration. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +@Configuration +@ConditionalOnClass(CacheBuilder.class) +@ConditionalOnMissingBean(CacheManager.class) +@Conditional(CacheCondition.class) +class GuavaCacheConfiguration { + + @Autowired + private CacheProperties cacheProperties; + + @Autowired(required = false) + private CacheBuilder cacheBuilder; + + @Autowired(required = false) + private CacheBuilderSpec cacheBuilderSpec; + + @Autowired(required = false) + private CacheLoader cacheLoader; + + @Bean + public GuavaCacheManager cacheManager() { + GuavaCacheManager cacheManager = createCacheManager(); + List cacheNames = this.cacheProperties.getCacheNames(); + if (!CollectionUtils.isEmpty(cacheNames)) { + cacheManager.setCacheNames(cacheNames); + } + return cacheManager; + } + + private GuavaCacheManager createCacheManager() { + GuavaCacheManager cacheManager = new GuavaCacheManager(); + setCacheBuilder(cacheManager); + if (this.cacheLoader != null) { + cacheManager.setCacheLoader(this.cacheLoader); + } + return cacheManager; + } + + private void setCacheBuilder(GuavaCacheManager cacheManager) { + String specification = this.cacheProperties.getGuava().getSpec(); + if (StringUtils.hasText(specification)) { + cacheManager.setCacheSpecification(specification); + } + else if (this.cacheBuilderSpec != null) { + cacheManager.setCacheBuilderSpec(this.cacheBuilderSpec); + } + else if (this.cacheBuilder != null) { + cacheManager.setCacheBuilder(this.cacheBuilder); + } + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/HazelcastCacheConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/HazelcastCacheConfiguration.java new file mode 100644 index 0000000000..425b91b689 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/HazelcastCacheConfiguration.java @@ -0,0 +1,96 @@ +/* + * Copyright 2012-2015 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.boot.autoconfigure.cache; + +import java.io.IOException; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cache.CacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.core.type.AnnotatedTypeMetadata; + +import com.hazelcast.config.Config; +import com.hazelcast.config.XmlConfigBuilder; +import com.hazelcast.core.Hazelcast; +import com.hazelcast.core.HazelcastInstance; +import com.hazelcast.spring.cache.HazelcastCacheManager; + +/** + * Hazelcast cache configuration. Only kick in if a configuration file location is set or + * if a default configuration file exists (either placed in the default location or set + * via the {@value #CONFIG_SYSTEM_PROPERTY} system property). + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +@Configuration +@ConditionalOnClass({ HazelcastInstance.class, HazelcastCacheManager.class }) +@ConditionalOnMissingBean(CacheManager.class) +@Conditional({ CacheCondition.class, + HazelcastCacheConfiguration.ConfigAvailableCondition.class }) +class HazelcastCacheConfiguration { + + static final String CONFIG_SYSTEM_PROPERTY = "hazelcast.config"; + + @Autowired + private CacheProperties cacheProperties; + + @Bean + public HazelcastCacheManager cacheManager() throws IOException { + return new HazelcastCacheManager(createHazelcastInstance()); + } + + private HazelcastInstance createHazelcastInstance() throws IOException { + Resource location = this.cacheProperties.resolveConfigLocation(); + if (location != null) { + Config cfg = new XmlConfigBuilder(location.getURL()).build(); + return Hazelcast.newHazelcastInstance(cfg); + } + return Hazelcast.newHazelcastInstance(); + } + + /** + * Determines if the Hazelcast configuration is available. This either kick in if a + * default configuration has been found or if property referring to the file to use + * has been set. + */ + static class ConfigAvailableCondition extends CacheConfigFileCondition { + + public ConfigAvailableCondition() { + super("Hazelcast", "file:./hazelcast.xml", "classpath:/hazelcast.xml"); + } + + @Override + protected ConditionOutcome getResourceOutcome(ConditionContext context, + AnnotatedTypeMetadata metadata) { + if (System.getProperty(CONFIG_SYSTEM_PROPERTY) != null) { + return ConditionOutcome.match("System property '" + + CONFIG_SYSTEM_PROPERTY + "' is set."); + } + return super.getResourceOutcome(context, metadata); + } + + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/JCacheCacheConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/JCacheCacheConfiguration.java new file mode 100644 index 0000000000..9052a299e1 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/JCacheCacheConfiguration.java @@ -0,0 +1,136 @@ +/* + * Copyright 2012-2015 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.boot.autoconfigure.cache; + +import java.util.Iterator; +import java.util.List; + +import javax.cache.CacheManager; +import javax.cache.Caching; +import javax.cache.configuration.MutableConfiguration; +import javax.cache.spi.CachingProvider; + +import org.apache.commons.collections.CollectionUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionOutcome; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.SpringBootCondition; +import org.springframework.boot.bind.RelaxedPropertyResolver; +import org.springframework.cache.jcache.JCacheCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.ConditionContext; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.Ordered; +import org.springframework.core.annotation.AnnotationAwareOrderComparator; +import org.springframework.core.annotation.Order; +import org.springframework.core.type.AnnotatedTypeMetadata; +import org.springframework.util.StringUtils; + +/** + * Cache configuration for JSR-107 compliant providers. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +@Configuration +@ConditionalOnClass(Caching.class) +@ConditionalOnMissingBean(org.springframework.cache.CacheManager.class) +@Conditional({ CacheCondition.class, + JCacheCacheConfiguration.JCacheAvailableCondition.class }) +class JCacheCacheConfiguration { + + @Autowired + private CacheProperties cacheProperties; + + @Autowired(required = false) + private javax.cache.configuration.Configuration defaultCacheConfiguration; + + @Autowired(required = false) + private List cacheManagerCustomizers; + + @Bean + public JCacheCacheManager cacheManager() { + CacheManager cacheManager = createCacheManager(this.cacheProperties.getJcache() + .getProvider()); + List cacheNames = this.cacheProperties.getCacheNames(); + if (!CollectionUtils.isEmpty(cacheNames)) { + for (String cacheName : cacheNames) { + cacheManager.createCache(cacheName, getDefaultCacheConfiguration()); + } + } + customize(cacheManager); + return new JCacheCacheManager(cacheManager); + } + + private CacheManager createCacheManager(String cachingProvider) { + if (StringUtils.hasText(cachingProvider)) { + return Caching.getCachingProvider(cachingProvider).getCacheManager(); + } + return Caching.getCachingProvider().getCacheManager(); + } + + private javax.cache.configuration.Configuration getDefaultCacheConfiguration() { + if (this.defaultCacheConfiguration != null) { + return this.defaultCacheConfiguration; + } + return new MutableConfiguration(); + } + + private void customize(CacheManager cacheManager) { + if (this.cacheManagerCustomizers != null) { + AnnotationAwareOrderComparator.sort(this.cacheManagerCustomizers); + for (JCacheManagerCustomizer customizer : this.cacheManagerCustomizers) { + customizer.customize(cacheManager); + } + } + } + + /** + * Determines if JCache is available. This either kick in if a default + * {@link CachingProvider} has been found or if the property referring to the provider + * to use has been set. + */ + @Order(Ordered.LOWEST_PRECEDENCE) + static class JCacheAvailableCondition extends SpringBootCondition { + + @Override + public ConditionOutcome getMatchOutcome(ConditionContext context, + AnnotatedTypeMetadata metadata) { + RelaxedPropertyResolver resolver = new RelaxedPropertyResolver( + context.getEnvironment(), "spring.cache.jcache."); + if (resolver.containsProperty("provider")) { + return ConditionOutcome.match("JCache provider specified"); + } + Iterator providers = Caching.getCachingProviders() + .iterator(); + if (!providers.hasNext()) { + return ConditionOutcome.noMatch("No JSR-107 compliant providers"); + } + providers.next(); + if (providers.hasNext()) { + return ConditionOutcome.noMatch("Multiple default JSR-107 compliant " + + "providers found"); + + } + return ConditionOutcome.match("Default JSR-107 compliant provider found."); + } + + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/JCacheManagerCustomizer.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/JCacheManagerCustomizer.java new file mode 100644 index 0000000000..df674a2ad2 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/JCacheManagerCustomizer.java @@ -0,0 +1,38 @@ +/* + * Copyright 2012-2015 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.boot.autoconfigure.cache; + +import javax.cache.CacheManager; +import javax.cache.configuration.Configuration; + +/** + * Callback interface that can be implemented by beans wishing to customize the cache + * manager before it is used, in particular to create additional caches. + * + * @author Stephane Nicoll + * @since 1.3.0 + * @see CacheManager#createCache(String, Configuration) + */ +public interface JCacheManagerCustomizer { + + /** + * Customize the cache manager. + * @param cacheManager the {@link CacheManager} to customize + */ + void customize(CacheManager cacheManager); + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/NoOpCacheConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/NoOpCacheConfiguration.java new file mode 100644 index 0000000000..e1687b2e46 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/NoOpCacheConfiguration.java @@ -0,0 +1,42 @@ +/* + * Copyright 2012-2015 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.boot.autoconfigure.cache; + +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cache.CacheManager; +import org.springframework.cache.support.NoOpCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; + +/** + * No-op cache configuration used to disable caching via configuration. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +@Configuration +@ConditionalOnMissingBean(CacheManager.class) +@Conditional(CacheCondition.class) +class NoOpCacheConfiguration { + + @Bean + public NoOpCacheManager cacheManager() { + return new NoOpCacheManager(); + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/RedisCacheConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/RedisCacheConfiguration.java new file mode 100644 index 0000000000..35b421c3a5 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/RedisCacheConfiguration.java @@ -0,0 +1,56 @@ +/* + * Copyright 2012-2015 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.boot.autoconfigure.cache; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cache.CacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.core.RedisTemplate; + +/** + * Redis cache configuration. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +@Configuration +@ConditionalOnBean(RedisTemplate.class) +@ConditionalOnMissingBean(CacheManager.class) +@Conditional(CacheCondition.class) +class RedisCacheConfiguration { + + @Autowired + private CacheProperties cacheProperties; + + @Bean + public RedisCacheManager cacheManager(RedisTemplate redisTemplate) { + RedisCacheManager cacheManager = new RedisCacheManager(redisTemplate); + List cacheNames = this.cacheProperties.getCacheNames(); + if (!cacheNames.isEmpty()) { + cacheManager.setCacheNames(cacheNames); + } + return cacheManager; + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/SimpleCacheConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/SimpleCacheConfiguration.java new file mode 100644 index 0000000000..4350ee73bd --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/SimpleCacheConfiguration.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2015 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.boot.autoconfigure.cache; + +import java.util.List; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cache.CacheManager; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; + +/** + * Simplest cache configuration, usually used as a fallback. + * + * @author Stephane Nicoll + * @since 1.3.0 + */ +@Configuration +@ConditionalOnMissingBean(CacheManager.class) +@Conditional(CacheCondition.class) +class SimpleCacheConfiguration { + + @Autowired + private CacheProperties cacheProperties; + + @Bean + public ConcurrentMapCacheManager cacheManager() { + ConcurrentMapCacheManager cacheManager = new ConcurrentMapCacheManager(); + List cacheNames = this.cacheProperties.getCacheNames(); + if (!cacheNames.isEmpty()) { + cacheManager.setCacheNames(cacheNames); + } + return cacheManager; + } + +} diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/package-info.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/package-info.java new file mode 100644 index 0000000000..d1bd02e46a --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2015 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. + */ + +/** + * Auto-configuration for the cache abstraction. + */ +package org.springframework.boot.autoconfigure.cache; \ No newline at end of file diff --git a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories index 2042b37de2..6d64968207 100644 --- a/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-autoconfigure/src/main/resources/META-INF/spring.factories @@ -9,6 +9,7 @@ org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration,\ org.springframework.boot.autoconfigure.MessageSourceAutoConfiguration,\ org.springframework.boot.autoconfigure.PropertyPlaceholderAutoConfiguration,\ org.springframework.boot.autoconfigure.batch.BatchAutoConfiguration,\ +org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration,\ org.springframework.boot.autoconfigure.cloud.CloudAutoConfiguration,\ org.springframework.boot.autoconfigure.dao.PersistenceExceptionTranslationAutoConfiguration,\ org.springframework.boot.autoconfigure.data.elasticsearch.ElasticsearchRepositoriesAutoConfiguration,\ diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfigurationTests.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfigurationTests.java new file mode 100644 index 0000000000..ce4de591d6 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfigurationTests.java @@ -0,0 +1,502 @@ +/* + * Copyright 2012-2015 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.boot.autoconfigure.cache; + +import java.util.Collection; +import java.util.Collections; + +import javax.cache.configuration.CompleteConfiguration; +import javax.cache.configuration.MutableConfiguration; +import javax.cache.expiry.CreatedExpiryPolicy; +import javax.cache.expiry.Duration; + +import org.junit.After; +import org.junit.Rule; +import org.junit.Test; +import org.junit.rules.ExpectedException; +import org.springframework.beans.factory.BeanCreationException; +import org.springframework.beans.factory.NoSuchBeanDefinitionException; +import org.springframework.boot.autoconfigure.cache.support.MockCachingProvider; +import org.springframework.boot.test.EnvironmentTestUtils; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.CachingConfigurerSupport; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCache; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.cache.guava.GuavaCache; +import org.springframework.cache.guava.GuavaCacheManager; +import org.springframework.cache.interceptor.CacheOperationInvocationContext; +import org.springframework.cache.interceptor.CacheResolver; +import org.springframework.cache.jcache.JCacheCacheManager; +import org.springframework.cache.support.NoOpCacheManager; +import org.springframework.cache.support.SimpleCacheManager; +import org.springframework.context.annotation.AnnotationConfigApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.data.redis.cache.RedisCacheManager; +import org.springframework.data.redis.core.RedisTemplate; + +import com.google.common.cache.CacheBuilder; +import com.hazelcast.cache.HazelcastCachingProvider; +import com.hazelcast.spring.cache.HazelcastCacheManager; + +import static org.hamcrest.CoreMatchers.equalTo; +import static org.hamcrest.CoreMatchers.instanceOf; +import static org.hamcrest.Matchers.contains; +import static org.hamcrest.Matchers.containsInAnyOrder; +import static org.hamcrest.Matchers.empty; +import static org.hamcrest.Matchers.hasSize; +import static org.hamcrest.core.Is.is; +import static org.junit.Assert.assertThat; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.verify; + +/** + * Tests for {@link CacheAutoConfiguration}. + * + * @author Stephane Nicoll + */ +public class CacheAutoConfigurationTests { + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + private AnnotationConfigApplicationContext context; + + @After + public void tearDown() { + if (this.context != null) { + this.context.close(); + } + } + + @Test + public void noEnableCaching() { + load(EmptyConfiguration.class); + this.thrown.expect(NoSuchBeanDefinitionException.class); + this.context.getBean(CacheManager.class); + } + + @Test + public void cacheManagerBackOff() { + load(CustomCacheManagerConfiguration.class); + ConcurrentMapCacheManager cacheManager = validateCacheManager(ConcurrentMapCacheManager.class); + assertThat(cacheManager.getCacheNames(), contains("custom1")); + assertThat(cacheManager.getCacheNames(), hasSize(1)); + } + + @Test + public void cacheManagerFromSupportBackOff() { + load(CustomCacheManagerFromSupportConfiguration.class); + ConcurrentMapCacheManager cacheManager = validateCacheManager(ConcurrentMapCacheManager.class); + assertThat(cacheManager.getCacheNames(), contains("custom1")); + assertThat(cacheManager.getCacheNames(), hasSize(1)); + } + + @Test + public void cacheResolverBackOff() throws Exception { + load(CustomCacheResolverConfiguration.class); + this.thrown.expect(NoSuchBeanDefinitionException.class); + this.context.getBean(CacheManager.class); + } + + @Test + public void notSupportedCachingMode() { + this.thrown.expect(BeanCreationException.class); + this.thrown.expectMessage("cache"); + this.thrown.expectMessage("foobar"); + load(DefaultCacheConfiguration.class, "spring.cache.type=foobar"); + } + + @Test + public void simpleCacheExplicit() { + load(DefaultCacheConfiguration.class, "spring.cache.type=simple"); + ConcurrentMapCacheManager cacheManager = validateCacheManager(ConcurrentMapCacheManager.class); + assertThat(cacheManager.getCacheNames(), is(empty())); + } + + @Test + public void simpleCacheExplicitWithCacheNames() { + load(DefaultCacheConfiguration.class, "spring.cache.type=simple", + "spring.cache.cacheNames[0]=foo", "spring.cache.cacheNames[1]=bar"); + ConcurrentMapCacheManager cacheManager = validateCacheManager(ConcurrentMapCacheManager.class); + assertThat(cacheManager.getCacheNames(), containsInAnyOrder("foo", "bar")); + assertThat(cacheManager.getCacheNames(), hasSize(2)); + } + + @Test + public void genericCacheWithCaches() { + load(GenericCacheConfiguration.class); + SimpleCacheManager cacheManager = validateCacheManager(SimpleCacheManager.class); + assertThat(cacheManager.getCache("first"), + equalTo(this.context.getBean("firstCache"))); + assertThat(cacheManager.getCache("second"), + equalTo(this.context.getBean("secondCache"))); + assertThat(cacheManager.getCacheNames(), hasSize(2)); + } + + @Test + public void genericCacheExplicit() { + this.thrown.expect(BeanCreationException.class); + this.thrown.expectMessage("No cache manager could be auto-configured"); + this.thrown.expectMessage("GENERIC"); + load(DefaultCacheConfiguration.class, "spring.cache.type=generic"); + } + + @Test + public void genericCacheExplicitWithCaches() { + load(GenericCacheConfiguration.class, "spring.cache.type=generic"); + SimpleCacheManager cacheManager = validateCacheManager(SimpleCacheManager.class); + assertThat(cacheManager.getCache("first"), + equalTo(this.context.getBean("firstCache"))); + assertThat(cacheManager.getCache("second"), + equalTo(this.context.getBean("secondCache"))); + assertThat(cacheManager.getCacheNames(), hasSize(2)); + } + + @Test + public void redisCacheExplicit() { + load(RedisCacheConfiguration.class, "spring.cache.type=redis"); + RedisCacheManager cacheManager = validateCacheManager(RedisCacheManager.class); + assertThat(cacheManager.getCacheNames(), is(empty())); + } + + @Test + public void redisCacheExplicitWithCaches() { + load(RedisCacheConfiguration.class, "spring.cache.type=redis", + "spring.cache.cacheNames[0]=foo", "spring.cache.cacheNames[1]=bar"); + RedisCacheManager cacheManager = validateCacheManager(RedisCacheManager.class); + assertThat(cacheManager.getCacheNames(), containsInAnyOrder("foo", "bar")); + assertThat(cacheManager.getCacheNames(), hasSize(2)); + } + + @Test + public void noOpCacheExplicit() { + load(DefaultCacheConfiguration.class, "spring.cache.type=none"); + NoOpCacheManager cacheManager = validateCacheManager(NoOpCacheManager.class); + assertThat(cacheManager.getCacheNames(), is(empty())); + } + + @Test + public void jCacheCacheNoProviderExplicit() { + this.thrown.expect(BeanCreationException.class); + this.thrown.expectMessage("No cache manager could be auto-configured"); + this.thrown.expectMessage("JCACHE"); + load(DefaultCacheConfiguration.class, "spring.cache.type=jcache"); + } + + @Test + public void jCacheCacheWithProvider() { + String cachingProviderFqn = MockCachingProvider.class.getName(); + load(DefaultCacheConfiguration.class, "spring.cache.type=jcache", + "spring.cache.jcache.provider=" + cachingProviderFqn); + JCacheCacheManager cacheManager = validateCacheManager(JCacheCacheManager.class); + assertThat(cacheManager.getCacheNames(), is(empty())); + } + + @Test + public void jCacheCacheWithCaches() { + String cachingProviderFqn = MockCachingProvider.class.getName(); + load(DefaultCacheConfiguration.class, "spring.cache.type=jcache", + "spring.cache.jcache.provider=" + cachingProviderFqn, + "spring.cache.cacheNames[0]=foo", "spring.cache.cacheNames[1]=bar"); + JCacheCacheManager cacheManager = validateCacheManager(JCacheCacheManager.class); + assertThat(cacheManager.getCacheNames(), containsInAnyOrder("foo", "bar")); + assertThat(cacheManager.getCacheNames(), hasSize(2)); + } + + @Test + public void jCacheCacheWithCachesAndCustomConfig() { + String cachingProviderFqn = MockCachingProvider.class.getName(); + load(JCacheCustomConfiguration.class, "spring.cache.type=jcache", + "spring.cache.jcache.provider=" + cachingProviderFqn, + "spring.cache.cacheNames[0]=one", "spring.cache.cacheNames[1]=two"); + JCacheCacheManager cacheManager = validateCacheManager(JCacheCacheManager.class); + assertThat(cacheManager.getCacheNames(), containsInAnyOrder("one", "two")); + assertThat(cacheManager.getCacheNames(), hasSize(2)); + + CompleteConfiguration defaultCacheConfiguration = this.context + .getBean(CompleteConfiguration.class); + verify(cacheManager.getCacheManager()).createCache("one", + defaultCacheConfiguration); + verify(cacheManager.getCacheManager()).createCache("two", + defaultCacheConfiguration); + } + + @Test + public void jCacheCacheWithUnknownProvider() { + String wrongCachingProviderFqn = "org.acme.FooBar"; + this.thrown.expect(BeanCreationException.class); + this.thrown.expectMessage(wrongCachingProviderFqn); + load(DefaultCacheConfiguration.class, "spring.cache.type=jcache", + "spring.cache.jcache.provider=" + wrongCachingProviderFqn); + } + + @Test + public void hazelcastCacheExplicit() { + load(DefaultCacheConfiguration.class, "spring.cache.type=hazelcast"); + HazelcastCacheManager cacheManager = validateCacheManager(HazelcastCacheManager.class); + // NOTE: the hazelcast implementation know about a cache in a lazy manner. + cacheManager.getCache("defaultCache"); + assertThat(cacheManager.getCacheNames(), containsInAnyOrder("defaultCache")); + assertThat(cacheManager.getCacheNames(), hasSize(1)); + } + + @Test + public void hazelcastCacheWithLocation() { + load(DefaultCacheConfiguration.class, "spring.cache.type=hazelcast", + "spring.cache.config=org/springframework/boot/autoconfigure/cache/hazelcast-specific.xml"); + HazelcastCacheManager cacheManager = validateCacheManager(HazelcastCacheManager.class); + cacheManager.getCache("foobar"); + assertThat(cacheManager.getCacheNames(), containsInAnyOrder("foobar")); + assertThat(cacheManager.getCacheNames(), hasSize(1)); + } + + @Test + public void hazelcastWithWrongLocation() { + this.thrown.expect(BeanCreationException.class); + this.thrown.expectMessage("foo/bar/unknown.xml"); + load(DefaultCacheConfiguration.class, "spring.cache.type=hazelcast", + "spring.cache.config=foo/bar/unknown.xml"); + System.out.println(this.context.getBean(CacheManager.class).getClass()); + } + + @Test + public void hazelcastAsJCacheWithCaches() { + String cachingProviderFqn = HazelcastCachingProvider.class.getName(); + JCacheCacheManager cacheManager = null; + try { + load(DefaultCacheConfiguration.class, "spring.cache.type=jcache", + "spring.cache.jcache.provider=" + cachingProviderFqn, + "spring.cache.cacheNames[0]=foo", "spring.cache.cacheNames[1]=bar"); + cacheManager = validateCacheManager(JCacheCacheManager.class); + assertThat(cacheManager.getCacheNames(), containsInAnyOrder("foo", "bar")); + assertThat(cacheManager.getCacheNames(), hasSize(2)); + } + finally { + if (cacheManager != null) { + cacheManager.getCacheManager().close(); + } + } + } + + @Test + public void jCacheCacheWithCachesAndCustomizer() { + String cachingProviderFqn = HazelcastCachingProvider.class.getName(); + JCacheCacheManager cacheManager = null; + try { + load(JCacheWithCustomizerConfiguration.class, "spring.cache.type=jcache", + "spring.cache.jcache.provider=" + cachingProviderFqn, + "spring.cache.cacheNames[0]=foo", "spring.cache.cacheNames[1]=bar"); + cacheManager = validateCacheManager(JCacheCacheManager.class); + assertThat(cacheManager.getCacheNames(), containsInAnyOrder("foo", "custom1")); // see + // customizer + assertThat(cacheManager.getCacheNames(), hasSize(2)); + } + finally { + if (cacheManager != null) { + cacheManager.getCacheManager().close(); + } + } + } + + @Test + public void guavaCacheExplicitWithCaches() { + load(DefaultCacheConfiguration.class, "spring.cache.type=guava", + "spring.cache.cacheNames=foo"); + GuavaCacheManager cacheManager = validateCacheManager(GuavaCacheManager.class); + Cache foo = cacheManager.getCache("foo"); + foo.get("1"); + // See next tests: no spec given so stats should be disabled + assertThat(((GuavaCache) foo).getNativeCache().stats().missCount(), equalTo(0L)); + } + + @Test + public void guavaCacheExplicitWithSpec() { + load(DefaultCacheConfiguration.class, "spring.cache.type=guava", + "spring.cache.guava.spec=recordStats", "spring.cache.cacheNames[0]=foo", + "spring.cache.cacheNames[1]=bar"); + validateGuavaCacheWithStats(); + } + + @Test + public void guavaCacheExplicitWithCacheBuilder() { + load(GuavaCacheBuilderConfiguration.class, "spring.cache.type=guava", + "spring.cache.cacheNames[0]=foo", "spring.cache.cacheNames[1]=bar"); + validateGuavaCacheWithStats(); + } + + private void validateGuavaCacheWithStats() { + GuavaCacheManager cacheManager = validateCacheManager(GuavaCacheManager.class); + assertThat(cacheManager.getCacheNames(), containsInAnyOrder("foo", "bar")); + assertThat(cacheManager.getCacheNames(), hasSize(2)); + Cache foo = cacheManager.getCache("foo"); + foo.get("1"); + assertThat(((GuavaCache) foo).getNativeCache().stats().missCount(), equalTo(1L)); + } + + private T validateCacheManager(Class type) { + CacheManager cacheManager = this.context.getBean(CacheManager.class); + assertThat("Wrong cache manager type", cacheManager, is(instanceOf(type))); + return type.cast(cacheManager); + } + + private void load(Class config, String... environment) { + this.context = doLoad(config, environment); + } + + private AnnotationConfigApplicationContext doLoad(Class config, + String... environment) { + AnnotationConfigApplicationContext applicationContext = new AnnotationConfigApplicationContext(); + EnvironmentTestUtils.addEnvironment(applicationContext, environment); + applicationContext.register(config); + applicationContext.register(CacheAutoConfiguration.class); + applicationContext.refresh(); + return applicationContext; + } + + @Configuration + static class EmptyConfiguration { + + } + + @Configuration + @EnableCaching + static class DefaultCacheConfiguration { + + } + + @Configuration + @EnableCaching + static class GenericCacheConfiguration { + + @Bean + public Cache firstCache() { + return new ConcurrentMapCache("first"); + } + + @Bean + public Cache secondCache() { + return new ConcurrentMapCache("second"); + } + + } + + @Configuration + @EnableCaching + static class RedisCacheConfiguration { + + @Bean + public RedisTemplate redisTemplate() { + return mock(RedisTemplate.class); + } + + } + + @Configuration + @EnableCaching + static class JCacheCustomConfiguration { + + @Bean + public CompleteConfiguration defaultCacheConfiguration() { + return mock(CompleteConfiguration.class); + } + + } + + @Configuration + @EnableCaching + static class JCacheWithCustomizerConfiguration { + + @Bean + JCacheManagerCustomizer myCustomizer() { + return new JCacheManagerCustomizer() { + @Override + public void customize(javax.cache.CacheManager cacheManager) { + MutableConfiguration config = new MutableConfiguration(); + config.setExpiryPolicyFactory(CreatedExpiryPolicy + .factoryOf(Duration.TEN_MINUTES)); + config.setStatisticsEnabled(true); + cacheManager.createCache("custom1", config); + cacheManager.destroyCache("bar"); + } + }; + } + + } + + @Configuration + @Import({ GenericCacheConfiguration.class, RedisCacheConfiguration.class }) + static class CustomCacheManagerConfiguration { + + @Bean + public CacheManager cacheManager() { + return new ConcurrentMapCacheManager("custom1"); + } + + } + + @Configuration + @Import({ GenericCacheConfiguration.class, RedisCacheConfiguration.class }) + static class CustomCacheManagerFromSupportConfiguration extends + CachingConfigurerSupport { + + @Override + @Bean + // The @Bean annotation is important, see CachingConfigurerSupport Javadoc + public CacheManager cacheManager() { + return new ConcurrentMapCacheManager("custom1"); + } + + } + + @Configuration + @EnableCaching + static class GuavaCacheBuilderConfiguration { + + @Bean + CacheBuilder cacheBuilder() { + return CacheBuilder.newBuilder().recordStats(); + } + + } + + @Configuration + @Import({ GenericCacheConfiguration.class, RedisCacheConfiguration.class }) + static class CustomCacheResolverConfiguration extends CachingConfigurerSupport { + + @Override + @Bean + // The @Bean annotation is important, see CachingConfigurerSupport Javadoc + public CacheResolver cacheResolver() { + return new CacheResolver() { + + @Override + public Collection resolveCaches( + CacheOperationInvocationContext context) { + return Collections.singleton(mock(Cache.class)); + } + + }; + + } + + } + +} diff --git a/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/support/MockCachingProvider.java b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/support/MockCachingProvider.java new file mode 100644 index 0000000000..156c9179be --- /dev/null +++ b/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/support/MockCachingProvider.java @@ -0,0 +1,117 @@ +/* + * Copyright 2012-2015 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.boot.autoconfigure.cache.support; + +import java.net.URI; +import java.util.HashMap; +import java.util.Map; +import java.util.Properties; + +import javax.cache.Cache; +import javax.cache.CacheManager; +import javax.cache.configuration.Configuration; +import javax.cache.configuration.OptionalFeature; +import javax.cache.spi.CachingProvider; + +import org.mockito.invocation.InvocationOnMock; +import org.mockito.stubbing.Answer; + +import static org.mockito.BDDMockito.given; +import static org.mockito.Matchers.any; +import static org.mockito.Matchers.anyString; +import static org.mockito.Mockito.mock; + +/** + * A mock {@link CachingProvider} that exposes a JSR-107 cache manager for testing + * purposes. + * + * @author Stephane Nicoll + */ +public class MockCachingProvider implements CachingProvider { + + @Override + @SuppressWarnings({ "unchecked", "rawtypes" }) + public CacheManager getCacheManager(URI uri, ClassLoader classLoader, + Properties properties) { + CacheManager cacheManager = mock(CacheManager.class); + final Map caches = new HashMap(); + given(cacheManager.getCacheNames()).willReturn(caches.keySet()); + given(cacheManager.getCache(anyString())).willAnswer(new Answer() { + @Override + public Cache answer(InvocationOnMock invocationOnMock) throws Throwable { + String cacheName = (String) invocationOnMock.getArguments()[0]; + return caches.get(cacheName); + } + }); + given(cacheManager.createCache(anyString(), any(Configuration.class))).will( + new Answer() { + @Override + public Cache answer(InvocationOnMock invocationOnMock) + throws Throwable { + String cacheName = (String) invocationOnMock.getArguments()[0]; + Cache cache = mock(Cache.class); + given(cache.getName()).willReturn(cacheName); + caches.put(cacheName, cache); + return cache; + } + }); + return cacheManager; + } + + @Override + public ClassLoader getDefaultClassLoader() { + return null; + } + + @Override + public URI getDefaultURI() { + return null; + } + + @Override + public Properties getDefaultProperties() { + return new Properties(); + } + + @Override + public CacheManager getCacheManager(URI uri, ClassLoader classLoader) { + return getCacheManager(uri, classLoader, getDefaultProperties()); + } + + @Override + public CacheManager getCacheManager() { + return getCacheManager(getDefaultURI(), getDefaultClassLoader()); + } + + @Override + public void close() { + } + + @Override + public void close(ClassLoader classLoader) { + } + + @Override + public void close(URI uri, ClassLoader classLoader) { + } + + @Override + public boolean isSupported(OptionalFeature optionalFeature) { + return false; + } + +} diff --git a/spring-boot-autoconfigure/src/test/resources/META-INF/services/javax.cache.spi.CachingProvider b/spring-boot-autoconfigure/src/test/resources/META-INF/services/javax.cache.spi.CachingProvider new file mode 100644 index 0000000000..a9ea8c43aa --- /dev/null +++ b/spring-boot-autoconfigure/src/test/resources/META-INF/services/javax.cache.spi.CachingProvider @@ -0,0 +1,4 @@ +# +# Test JSR 107 provider for testing purposes only. +# +org.springframework.boot.autoconfigure.cache.support.MockCachingProvider \ No newline at end of file diff --git a/spring-boot-autoconfigure/src/test/resources/hazelcast.xml b/spring-boot-autoconfigure/src/test/resources/hazelcast.xml new file mode 100644 index 0000000000..0806a6dcfc --- /dev/null +++ b/spring-boot-autoconfigure/src/test/resources/hazelcast.xml @@ -0,0 +1,8 @@ + + + + + diff --git a/spring-boot-autoconfigure/src/test/resources/org/springframework/boot/autoconfigure/cache/hazelcast-specific.xml b/spring-boot-autoconfigure/src/test/resources/org/springframework/boot/autoconfigure/cache/hazelcast-specific.xml new file mode 100644 index 0000000000..f89b3b2393 --- /dev/null +++ b/spring-boot-autoconfigure/src/test/resources/org/springframework/boot/autoconfigure/cache/hazelcast-specific.xml @@ -0,0 +1,10 @@ + + + + 3600 + 600 + + + \ No newline at end of file diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml index ec4986b48a..51c3a42459 100644 --- a/spring-boot-dependencies/pom.xml +++ b/spring-boot-dependencies/pom.xml @@ -69,6 +69,7 @@ 2.3.1 1.4.186 1.3 + 3.4.1 4.3.8.Final ${hibernate.version} 5.1.3.Final @@ -503,6 +504,16 @@ h2 ${h2.version} + + com.hazelcast + hazelcast + ${hazelcast.version} + + + com.hazelcast + hazelcast-spring + ${hazelcast.version} + com.jayway.jsonpath json-path