From 98cc68364a89df9b42f54f48337349b287fbe7c5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Edd=C3=BA=20Mel=C3=A9ndez?= Date: Thu, 7 Jan 2016 18:10:11 -0500 Subject: [PATCH] Add Caffeine cache support See gh-4899 --- spring-boot-actuator/pom.xml | 5 ++ .../CacheStatisticsAutoConfiguration.java | 17 +++- .../CaffeineCacheStatisticsProvider.java | 46 +++++++++++ ...CacheStatisticsAutoConfigurationTests.java | 24 ++++++ spring-boot-autoconfigure/pom.xml | 5 ++ .../cache/CacheConfigurations.java | 4 +- .../autoconfigure/cache/CacheProperties.java | 29 ++++++- .../boot/autoconfigure/cache/CacheType.java | 7 +- .../cache/CaffeineCacheConfiguration.java | 81 +++++++++++++++++++ .../cache/CacheAutoConfigurationTests.java | 76 +++++++++++++++++ spring-boot-dependencies/pom.xml | 6 ++ .../appendix-application-properties.adoc | 1 + 12 files changed, 297 insertions(+), 4 deletions(-) create mode 100644 spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CaffeineCacheStatisticsProvider.java create mode 100644 spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CaffeineCacheConfiguration.java diff --git a/spring-boot-actuator/pom.xml b/spring-boot-actuator/pom.xml index 7aba9cd5d3..d4c7b547e5 100644 --- a/spring-boot-actuator/pom.xml +++ b/spring-boot-actuator/pom.xml @@ -61,6 +61,11 @@ hal-browser true + + com.github.ben-manes.caffeine + caffeine + true + com.google.guava guava diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/CacheStatisticsAutoConfiguration.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/CacheStatisticsAutoConfiguration.java index 3ddd3fa3cc..6a4b184bde 100644 --- a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/CacheStatisticsAutoConfiguration.java +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/autoconfigure/CacheStatisticsAutoConfiguration.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * Copyright 2012-2016 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,7 @@ package org.springframework.boot.actuate.autoconfigure; import javax.cache.Caching; +import com.github.benmanes.caffeine.cache.Caffeine; import com.hazelcast.core.IMap; import com.hazelcast.spring.cache.HazelcastCache; import net.sf.ehcache.Ehcache; @@ -26,6 +27,7 @@ import org.infinispan.spring.provider.SpringCache; import org.springframework.boot.actuate.cache.CacheStatistics; import org.springframework.boot.actuate.cache.CacheStatisticsProvider; +import org.springframework.boot.actuate.cache.CaffeineCacheStatisticsProvider; import org.springframework.boot.actuate.cache.ConcurrentMapCacheStatisticsProvider; import org.springframework.boot.actuate.cache.DefaultCacheStatistics; import org.springframework.boot.actuate.cache.EhCacheStatisticsProvider; @@ -40,6 +42,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; +import org.springframework.cache.caffeine.CaffeineCacheManager; import org.springframework.cache.concurrent.ConcurrentMapCache; import org.springframework.cache.ehcache.EhCacheCache; import org.springframework.cache.guava.GuavaCache; @@ -54,6 +57,7 @@ import org.springframework.context.annotation.Configuration; * * @author Stephane Nicoll * @author Phillip Webb + * @author Eddú Meléndez * @since 1.3.0 */ @Configuration @@ -72,6 +76,17 @@ public class CacheStatisticsAutoConfiguration { } + @Configuration + @ConditionalOnClass({ Caffeine.class, CaffeineCacheManager.class }) + static class CaffeineCacheStatisticsProviderConfiguration { + + @Bean + public CaffeineCacheStatisticsProvider caffeineCacheStatisticsProvider() { + return new CaffeineCacheStatisticsProvider(); + } + + } + @Configuration @ConditionalOnClass({ EhCacheCache.class, Ehcache.class, StatisticsGateway.class }) static class EhCacheCacheStatisticsProviderConfiguration { diff --git a/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CaffeineCacheStatisticsProvider.java b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CaffeineCacheStatisticsProvider.java new file mode 100644 index 0000000000..dc6e77ffe8 --- /dev/null +++ b/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CaffeineCacheStatisticsProvider.java @@ -0,0 +1,46 @@ +/* + * Copyright 2012-2016 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.actuate.cache; + +import com.github.benmanes.caffeine.cache.stats.CacheStats; + +import org.springframework.cache.CacheManager; +import org.springframework.cache.caffeine.CaffeineCache; + +/** + * {@link CacheStatisticsProvider} implementation for Caffeine. + * + * @author Eddú Meléndez + * @since 1.4.0 + */ +public class CaffeineCacheStatisticsProvider + implements CacheStatisticsProvider { + + @Override + public CacheStatistics getCacheStatistics(CacheManager cacheManager, + CaffeineCache cache) { + DefaultCacheStatistics statistics = new DefaultCacheStatistics(); + statistics.setSize(cache.getNativeCache().estimatedSize()); + CacheStats caffeineStatistics = cache.getNativeCache().stats(); + if (caffeineStatistics.requestCount() > 0) { + statistics.setHitRatio(caffeineStatistics.hitRate()); + statistics.setMissRatio(caffeineStatistics.missRate()); + } + return statistics; + } + +} diff --git a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/CacheStatisticsAutoConfigurationTests.java b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/CacheStatisticsAutoConfigurationTests.java index ce87b8f5f7..a6d59baf54 100644 --- a/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/CacheStatisticsAutoConfigurationTests.java +++ b/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/autoconfigure/CacheStatisticsAutoConfigurationTests.java @@ -23,6 +23,7 @@ import java.util.Arrays; import javax.cache.Caching; import javax.cache.configuration.MutableConfiguration; +import com.github.benmanes.caffeine.cache.Caffeine; import com.google.common.cache.CacheBuilder; import com.hazelcast.cache.HazelcastCachingProvider; import com.hazelcast.config.Config; @@ -40,6 +41,7 @@ import org.springframework.boot.actuate.cache.CacheStatistics; import org.springframework.boot.actuate.cache.CacheStatisticsProvider; import org.springframework.cache.Cache; import org.springframework.cache.CacheManager; +import org.springframework.cache.caffeine.CaffeineCacheManager; import org.springframework.cache.concurrent.ConcurrentMapCacheManager; import org.springframework.cache.ehcache.EhCacheCacheManager; import org.springframework.cache.ehcache.EhCacheManagerUtils; @@ -60,6 +62,7 @@ import static org.assertj.core.api.Assertions.offset; * Tests for {@link CacheStatisticsAutoConfiguration}. * * @author Stephane Nicoll + * @author Eddú Meléndez */ @SuppressWarnings({ "rawtypes", "unchecked" }) public class CacheStatisticsAutoConfigurationTests { @@ -145,6 +148,14 @@ public class CacheStatisticsAutoConfigurationTests { assertCoreStatistics(updatedCacheStatistics, null, null, null); } + @Test + public void caffeineCacheStatistics() { + load(CaffeineCacheConfig.class); + CacheStatisticsProvider provider = this.context + .getBean("caffeineCacheStatisticsProvider", CacheStatisticsProvider.class); + doTestCoreStatistics(provider, true); + } + private void doTestCoreStatistics(CacheStatisticsProvider provider, boolean supportSize) { Cache books = getCache("books"); @@ -313,4 +324,17 @@ public class CacheStatisticsAutoConfigurationTests { } + @Configuration + static class CaffeineCacheConfig { + + @Bean + public CaffeineCacheManager cacheManager() { + CaffeineCacheManager cacheManager = new CaffeineCacheManager(); + cacheManager.setCaffeine(Caffeine.newBuilder().recordStats()); + cacheManager.setCacheNames(Arrays.asList("books", "speaker")); + return cacheManager; + } + + } + } diff --git a/spring-boot-autoconfigure/pom.xml b/spring-boot-autoconfigure/pom.xml index 7b7b4d1410..f2579c5723 100755 --- a/spring-boot-autoconfigure/pom.xml +++ b/spring-boot-autoconfigure/pom.xml @@ -511,6 +511,11 @@ thymeleaf-layout-dialect true + + com.github.ben-manes.caffeine + caffeine + true + com.github.mxab.thymeleaf.extras thymeleaf-extras-data-attribute diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheConfigurations.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheConfigurations.java index 72f14b7396..5245b0c7d2 100644 --- a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheConfigurations.java +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheConfigurations.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * Copyright 2012-2016 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. @@ -26,6 +26,7 @@ import org.springframework.util.Assert; * Mappings between {@link CacheType} and {@code @Configuration}. * * @author Phillip Webb + * @author Eddú Meléndez */ final class CacheConfigurations { @@ -42,6 +43,7 @@ final class CacheConfigurations { mappings.put(CacheType.INFINISPAN, InfinispanCacheConfiguration.class); mappings.put(CacheType.JCACHE, JCacheCacheConfiguration.class); mappings.put(CacheType.REDIS, RedisCacheConfiguration.class); + mappings.put(CacheType.CAFFEINE, CaffeineCacheConfiguration.class); mappings.put(CacheType.GUAVA, GuavaCacheConfiguration.class); mappings.put(CacheType.SIMPLE, SimpleCacheConfiguration.class); mappings.put(CacheType.NONE, NoOpCacheConfiguration.class); 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 index 8f9213a4cf..eb196e3735 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * Copyright 2012-2016 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. @@ -44,6 +44,8 @@ public class CacheProperties { */ private List cacheNames = new ArrayList(); + private final Caffeine caffeine = new Caffeine(); + private final EhCache ehcache = new EhCache(); private final Hazelcast hazelcast = new Hazelcast(); @@ -70,6 +72,10 @@ public class CacheProperties { this.cacheNames = cacheNames; } + public Caffeine getCaffeine() { + return this.caffeine; + } + public EhCache getEhcache() { return this.ehcache; } @@ -106,6 +112,27 @@ public class CacheProperties { return null; } + /** + * Caffeine specific cache properties. + */ + public static class Caffeine { + + /** + * The spec to use to create caches. Check CaffeineSpec for more details on + * the spec format. + */ + private String spec; + + public String getSpec() { + return this.spec; + } + + public void setSpec(String spec) { + this.spec = spec; + } + + } + /** * EhCache specific cache properties. */ 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 index f8cfc957e1..5277c1fc47 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2015 the original author or authors. + * Copyright 2012-2016 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. @@ -56,6 +56,11 @@ public enum CacheType { */ REDIS, + /** + * Caffeine backed caching. + */ + CAFFEINE, + /** * Guava backed caching. */ diff --git a/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CaffeineCacheConfiguration.java b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CaffeineCacheConfiguration.java new file mode 100644 index 0000000000..dc958fd896 --- /dev/null +++ b/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CaffeineCacheConfiguration.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-2016 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 com.github.benmanes.caffeine.cache.CacheLoader; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.CaffeineSpec; + +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.caffeine.CaffeineCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.util.StringUtils; + +/** + * Caffeine cache configuration. + * + * @author Eddú Meléndez + * @since 1.4.0 + */ +@Configuration +@ConditionalOnClass({ Caffeine.class, CaffeineCacheManager.class }) +@ConditionalOnMissingBean(org.springframework.cache.CacheManager.class) +@Conditional({ CacheCondition.class }) +class CaffeineCacheConfiguration { + + @Autowired + private CacheProperties cacheProperties; + + @Autowired(required = false) + private Caffeine caffeine; + + @Autowired(required = false) + private CaffeineSpec caffeineSpec; + + @Autowired(required = false) + private CacheLoader cacheLoader; + + @Bean + @ConditionalOnMissingBean + public CaffeineCacheManager caffeineCacheManager() { + CaffeineCacheManager caffeineCacheManager = new CaffeineCacheManager(); + setCacheBuilder(caffeineCacheManager); + if (this.cacheLoader != null) { + caffeineCacheManager.setCacheLoader(this.cacheLoader); + } + caffeineCacheManager.setCacheNames(this.cacheProperties.getCacheNames()); + return caffeineCacheManager; + } + + private void setCacheBuilder(CaffeineCacheManager caffeineCacheManager) { + String specification = this.cacheProperties.getCaffeine().getSpec(); + if (StringUtils.hasText(specification)) { + caffeineCacheManager.setCaffeine(Caffeine.from(specification)); + } + else if (this.caffeineSpec != null) { + caffeineCacheManager.setCaffeine(Caffeine.from(this.caffeineSpec)); + } + else if (this.caffeine != null) { + caffeineCacheManager.setCaffeine(this.caffeine); + } + } + +} 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 index eb47432bd6..f6bc1b46a8 100644 --- 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 @@ -30,6 +30,8 @@ import javax.cache.configuration.MutableConfiguration; import javax.cache.expiry.CreatedExpiryPolicy; import javax.cache.expiry.Duration; +import com.github.benmanes.caffeine.cache.Caffeine; +import com.github.benmanes.caffeine.cache.CaffeineSpec; import com.google.common.cache.CacheBuilder; import com.hazelcast.cache.HazelcastCachingProvider; import com.hazelcast.core.HazelcastInstance; @@ -54,6 +56,8 @@ 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.caffeine.CaffeineCache; +import org.springframework.cache.caffeine.CaffeineCacheManager; import org.springframework.cache.concurrent.ConcurrentMapCache; import org.springframework.cache.concurrent.ConcurrentMapCacheManager; import org.springframework.cache.ehcache.EhCacheCacheManager; @@ -580,6 +584,56 @@ public class CacheAutoConfigurationTests { validateGuavaCacheWithStats(); } + @Test + public void caffeineCacheWithCaches() { + load(DefaultCacheConfiguration.class, "spring.cache.type=caffeine", + "spring.cache.cacheNames[0]=spring", "spring.cache.cacheNames[1]=boot"); + CaffeineCacheManager cacheManager = validateCacheManager( + CaffeineCacheManager.class); + assertThat(cacheManager.getCacheNames()).containsOnly("spring", "boot"); + } + + @Test + public void caffeineCacheNotAllowNullValues() { + load(DefaultCacheConfiguration.class, "spring.cache.type=caffeine", + "spring.cache.caffeine.allow-null-values=false"); + CaffeineCacheManager cacheManager = validateCacheManager( + CaffeineCacheManager.class); + assertThat(cacheManager.isAllowNullValues()).isFalse(); + } + + @Test + public void caffeineCacheWithNullCaches() { + load(CaffeineCacheBuilderConfiguration.class, "spring.cache.type=caffeine", + "spring.cache.cacheNames[0]=caffeine", "spring.cache.cacheNames[1]=cache"); + CaffeineCacheManager cacheManager = validateCacheManager( + CaffeineCacheManager.class); + assertThat(cacheManager.isAllowNullValues()).isTrue(); + } + + @Test + public void caffeineCacheExplicitWithSpec() { + load(DefaultCacheConfiguration.class, "spring.cache.type=caffeine", + "spring.cache.caffeine.spec=recordStats", "spring.cache.cacheNames[0]=foo", + "spring.cache.cacheNames[1]=bar"); + validateCaffeineCacheWithStats(); + } + + @Test + public void caffeineCacheExplicitWithCacheBuilder() { + load(CaffeineCacheBuilderConfiguration2.class, "spring.cache.type=caffeine", + "spring.cache.cacheNames[0]=foo", "spring.cache.cacheNames[1]=bar"); + validateCaffeineCacheWithStats(); + } + + private void validateCaffeineCacheWithStats() { + CaffeineCacheManager cacheManager = validateCacheManager(CaffeineCacheManager.class); + assertThat(cacheManager.getCacheNames()).containsOnly("foo", "bar"); + Cache foo = cacheManager.getCache("foo"); + foo.get("1"); + assertThat(((CaffeineCache) foo).getNativeCache().stats().missCount()).isEqualTo(1L); + } + private void validateGuavaCacheWithStats() { GuavaCacheManager cacheManager = validateCacheManager(GuavaCacheManager.class); assertThat(cacheManager.getCacheNames()).containsOnly("foo", "bar"); @@ -837,6 +891,28 @@ public class CacheAutoConfigurationTests { } + @Configuration + @EnableCaching + static class CaffeineCacheBuilderConfiguration { + + @Bean + Caffeine cacheBuilder() { + return Caffeine.newBuilder().maximumSize(10); + } + + } + + @Configuration + @EnableCaching + static class CaffeineCacheBuilderConfiguration2 { + + @Bean + CaffeineSpec caffeineSpec() { + return CaffeineSpec.parse("recordStats"); + } + + } + @Configuration static class CacheManagerCustomizersConfiguration { diff --git a/spring-boot-dependencies/pom.xml b/spring-boot-dependencies/pom.xml index d615c9ff61..8abd13d27e 100644 --- a/spring-boot-dependencies/pom.xml +++ b/spring-boot-dependencies/pom.xml @@ -51,6 +51,7 @@ 2.3.0 3.9.3 2.1.4 + 2.1.0 2.1.9 1.9.2 3.2.2 @@ -624,6 +625,11 @@ + + com.github.ben-manes.caffeine + caffeine + ${caffeine.version} + com.github.mxab.thymeleaf.extras thymeleaf-extras-data-attribute diff --git a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 68dbb3d59d..354aeb9a99 100644 --- a/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -63,6 +63,7 @@ content into your application; rather pick only the properties that you need. # SPRING CACHE ({sc-spring-boot-autoconfigure}/cache/CacheProperties.{sc-ext}[CacheProperties]) spring.cache.cache-names= # Comma-separated list of cache names to create if supported by the underlying cache manager. + spring.cache.caffeine.spec= # The spec to use to create caches. Check CaffeineSpec for more details on the spec format. spring.cache.ehcache.config= # The location of the configuration file to use to initialize EhCache. spring.cache.guava.spec= # The spec to use to create caches. Check CacheBuilderSpec for more details on the spec format. spring.cache.hazelcast.config= # The location of the configuration file to use to initialize Hazelcast.