diff --git a/spring-boot-project/spring-boot-autoconfigure/build.gradle b/spring-boot-project/spring-boot-autoconfigure/build.gradle index 851df7f4528..1789752bd5a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/build.gradle +++ b/spring-boot-project/spring-boot-autoconfigure/build.gradle @@ -115,6 +115,17 @@ dependencies { optional("org.hibernate.orm:hibernate-core") optional("org.hibernate.orm:hibernate-jcache") optional("org.hibernate.validator:hibernate-validator") + optional("org.infinispan:infinispan-commons-jakarta") + optional("org.infinispan:infinispan-component-annotations") + optional("org.infinispan:infinispan-core-jakarta") + optional("org.infinispan:infinispan-jcache") { + exclude group: "org.infinispan", module: "infinispan-commons" + exclude group: "org.infinispan", module: "infinispan-core" + } + optional("org.infinispan:infinispan-spring5-embedded") { + exclude group: "org.infinispan", module: "infinispan-commons" + exclude group: "org.infinispan", module: "infinispan-core" + } optional("org.influxdb:influxdb-java") optional("org.jooq:jooq") { exclude group: "javax.xml.bind", module: "jaxb-api" diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheConfigurations.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheConfigurations.java index 193d6e76eab..d380748f335 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheConfigurations.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheConfigurations.java @@ -37,6 +37,7 @@ final class CacheConfigurations { Map mappings = new EnumMap<>(CacheType.class); mappings.put(CacheType.GENERIC, GenericCacheConfiguration.class.getName()); mappings.put(CacheType.HAZELCAST, HazelcastCacheConfiguration.class.getName()); + mappings.put(CacheType.INFINISPAN, InfinispanCacheConfiguration.class.getName()); mappings.put(CacheType.JCACHE, JCacheCacheConfiguration.class.getName()); mappings.put(CacheType.COUCHBASE, CouchbaseCacheConfiguration.class.getName()); mappings.put(CacheType.REDIS, RedisCacheConfiguration.class.getName()); diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheProperties.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheProperties.java index fa3119ea99d..2a26704960a 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheProperties.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheProperties.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2021 the original author or authors. + * Copyright 2012-2022 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. @@ -50,6 +50,8 @@ public class CacheProperties { private final Couchbase couchbase = new Couchbase(); + private final Infinispan infinispan = new Infinispan(); + private final JCache jcache = new JCache(); private final Redis redis = new Redis(); @@ -78,6 +80,10 @@ public class CacheProperties { return this.couchbase; } + public Infinispan getInfinispan() { + return this.infinispan; + } + public JCache getJcache() { return this.jcache; } @@ -144,6 +150,26 @@ public class CacheProperties { } + /** + * Infinispan specific cache properties. + */ + public static class Infinispan { + + /** + * The location of the configuration file to use to initialize Infinispan. + */ + private Resource config; + + public Resource getConfig() { + return this.config; + } + + public void setConfig(Resource config) { + this.config = config; + } + + } + /** * JCache (JSR-107) specific cache properties. */ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheType.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheType.java index a7185e9cae2..d46bb4142d4 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheType.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/CacheType.java @@ -46,6 +46,11 @@ public enum CacheType { */ COUCHBASE, + /** + * Infinispan backed caching. + */ + INFINISPAN, + /** * Redis backed caching. */ diff --git a/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/InfinispanCacheConfiguration.java b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/InfinispanCacheConfiguration.java new file mode 100644 index 00000000000..13409c6a814 --- /dev/null +++ b/spring-boot-project/spring-boot-autoconfigure/src/main/java/org/springframework/boot/autoconfigure/cache/InfinispanCacheConfiguration.java @@ -0,0 +1,90 @@ +/* + * Copyright 2012-2022 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 + * + * https://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 java.io.InputStream; +import java.util.List; + +import org.infinispan.configuration.cache.ConfigurationBuilder; +import org.infinispan.manager.DefaultCacheManager; +import org.infinispan.manager.EmbeddedCacheManager; +import org.infinispan.spring.embedded.provider.SpringEmbeddedCacheManager; + +import org.springframework.beans.factory.ObjectProvider; +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.Conditional; +import org.springframework.context.annotation.Configuration; +import org.springframework.core.io.Resource; +import org.springframework.util.CollectionUtils; + +/** + * Infinispan cache configuration. + * + * @author EddĂș MelĂ©ndez + * @author Stephane Nicoll + * @author Raja Kolli + * @since 1.3.0 + */ +@Configuration(proxyBeanMethods = false) +@ConditionalOnClass(SpringEmbeddedCacheManager.class) +@ConditionalOnMissingBean(CacheManager.class) +@Conditional(CacheCondition.class) +public class InfinispanCacheConfiguration { + + @Bean + public SpringEmbeddedCacheManager cacheManager(CacheManagerCustomizers customizers, + EmbeddedCacheManager embeddedCacheManager) { + SpringEmbeddedCacheManager cacheManager = new SpringEmbeddedCacheManager(embeddedCacheManager); + return customizers.customize(cacheManager); + } + + @Bean(destroyMethod = "stop") + @ConditionalOnMissingBean + public EmbeddedCacheManager infinispanCacheManager(CacheProperties cacheProperties, + ObjectProvider defaultConfigurationBuilder) throws IOException { + EmbeddedCacheManager cacheManager = createEmbeddedCacheManager(cacheProperties); + List cacheNames = cacheProperties.getCacheNames(); + if (!CollectionUtils.isEmpty(cacheNames)) { + cacheNames.forEach((cacheName) -> cacheManager.defineConfiguration(cacheName, + getDefaultCacheConfiguration(defaultConfigurationBuilder.getIfAvailable()))); + } + return cacheManager; + } + + private EmbeddedCacheManager createEmbeddedCacheManager(CacheProperties cacheProperties) throws IOException { + Resource location = cacheProperties.resolveConfigLocation(cacheProperties.getInfinispan().getConfig()); + if (location != null) { + try (InputStream in = location.getInputStream()) { + return new DefaultCacheManager(in); + } + } + return new DefaultCacheManager(); + } + + private org.infinispan.configuration.cache.Configuration getDefaultCacheConfiguration( + ConfigurationBuilder defaultConfigurationBuilder) { + if (defaultConfigurationBuilder != null) { + return defaultConfigurationBuilder.build(); + } + return new ConfigurationBuilder().build(); + } + +} diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/AbstractCacheAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/AbstractCacheAutoConfigurationTests.java index 3045039f1c5..3abac2d9b84 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/AbstractCacheAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/AbstractCacheAutoConfigurationTests.java @@ -23,6 +23,7 @@ import java.util.Map; import com.hazelcast.spring.cache.HazelcastCacheManager; import org.cache2k.extra.spring.SpringCache2kCacheManager; +import org.infinispan.spring.embedded.provider.SpringEmbeddedCacheManager; import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.test.context.assertj.AssertableApplicationContext; @@ -120,6 +121,13 @@ abstract class AbstractCacheAutoConfigurationTests { }; } + @Bean + CacheManagerCustomizer infinispanCacheManagerCustomizer() { + return new CacheManagerTestCustomizer<>() { + + }; + } + @Bean CacheManagerCustomizer cache2kCacheManagerCustomizer() { return new CacheManagerTestCustomizer() { diff --git a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfigurationTests.java b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfigurationTests.java index 2336521f428..9a83a330ab6 100644 --- a/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfigurationTests.java +++ b/spring-boot-project/spring-boot-autoconfigure/src/test/java/org/springframework/boot/autoconfigure/cache/CacheAutoConfigurationTests.java @@ -34,6 +34,9 @@ import com.hazelcast.core.Hazelcast; import com.hazelcast.core.HazelcastInstance; import com.hazelcast.spring.cache.HazelcastCacheManager; import org.cache2k.extra.spring.SpringCache2kCacheManager; +import org.infinispan.configuration.cache.ConfigurationBuilder; +import org.infinispan.jcache.embedded.JCachingProvider; +import org.infinispan.spring.embedded.provider.SpringEmbeddedCacheManager; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.BeanCreationException; @@ -73,7 +76,9 @@ import org.springframework.test.util.ReflectionTestUtils; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.BDDMockito.given; +import static org.mockito.BDDMockito.then; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.times; /** * Tests for {@link CacheAutoConfiguration}. @@ -535,6 +540,71 @@ class CacheAutoConfigurationTests extends AbstractCacheAutoConfigurationTests { }); } + @Test + void infinispanCacheWithConfig() { + this.contextRunner.withUserConfiguration(DefaultCacheConfiguration.class) + .withPropertyValues("spring.cache.type=infinispan", "spring.cache.infinispan.config=infinispan.xml") + .run((context) -> { + SpringEmbeddedCacheManager cacheManager = getCacheManager(context, + SpringEmbeddedCacheManager.class); + assertThat(cacheManager.getCacheNames()).contains("foo", "bar"); + }); + } + + @Test + void infinispanCacheWithCustomizers() { + this.contextRunner.withUserConfiguration(DefaultCacheAndCustomizersConfiguration.class) + .withPropertyValues("spring.cache.type=infinispan") + .run(verifyCustomizers("allCacheManagerCustomizer", "infinispanCacheManagerCustomizer")); + } + + @Test + void infinispanCacheWithCaches() { + this.contextRunner.withUserConfiguration(DefaultCacheConfiguration.class) + .withPropertyValues("spring.cache.type=infinispan", "spring.cache.cacheNames[0]=foo", + "spring.cache.cacheNames[1]=bar") + .run((context) -> assertThat(getCacheManager(context, SpringEmbeddedCacheManager.class).getCacheNames()) + .containsOnly("foo", "bar")); + } + + @Test + void infinispanCacheWithCachesAndCustomConfig() { + this.contextRunner.withUserConfiguration(InfinispanCustomConfiguration.class) + .withPropertyValues("spring.cache.type=infinispan", "spring.cache.cacheNames[0]=foo", + "spring.cache.cacheNames[1]=bar") + .run((context) -> { + assertThat(getCacheManager(context, SpringEmbeddedCacheManager.class).getCacheNames()) + .containsOnly("foo", "bar"); + then(context.getBean(ConfigurationBuilder.class)).should(times(2)).build(); + }); + } + + @Test + void infinispanAsJCacheWithCaches() { + String cachingProviderClassName = JCachingProvider.class.getName(); + this.contextRunner.withUserConfiguration(DefaultCacheConfiguration.class) + .withPropertyValues("spring.cache.type=jcache", + "spring.cache.jcache.provider=" + cachingProviderClassName, "spring.cache.cacheNames[0]=foo", + "spring.cache.cacheNames[1]=bar") + .run((context) -> assertThat(getCacheManager(context, JCacheCacheManager.class).getCacheNames()) + .containsOnly("foo", "bar")); + } + + @Test + void infinispanAsJCacheWithConfig() { + String cachingProviderClassName = JCachingProvider.class.getName(); + String configLocation = "infinispan.xml"; + this.contextRunner.withUserConfiguration(DefaultCacheConfiguration.class) + .withPropertyValues("spring.cache.type=jcache", + "spring.cache.jcache.provider=" + cachingProviderClassName, + "spring.cache.jcache.config=" + configLocation) + .run((context) -> { + Resource configResource = new ClassPathResource(configLocation); + assertThat(getCacheManager(context, JCacheCacheManager.class).getCacheManager().getURI()) + .isEqualTo(configResource.getURI()); + }); + } + @Test void jCacheCacheWithCachesAndCustomizer() { String cachingProviderFqn = HazelcastServerCachingProvider.class.getName(); @@ -848,6 +918,19 @@ class CacheAutoConfigurationTests extends AbstractCacheAutoConfigurationTests { } + @Configuration(proxyBeanMethods = false) + @EnableCaching + static class InfinispanCustomConfiguration { + + @Bean + ConfigurationBuilder configurationBuilder() { + ConfigurationBuilder builder = mock(ConfigurationBuilder.class); + given(builder.build()).willReturn(new ConfigurationBuilder().build()); + return builder; + } + + } + @Configuration(proxyBeanMethods = false) @EnableCaching static class CustomCacheManagerConfiguration { diff --git a/spring-boot-project/spring-boot-dependencies/build.gradle b/spring-boot-project/spring-boot-dependencies/build.gradle index 80803096292..31b041421dd 100644 --- a/spring-boot-project/spring-boot-dependencies/build.gradle +++ b/spring-boot-project/spring-boot-dependencies/build.gradle @@ -443,6 +443,13 @@ bom { ] } } + library("Infinispan", "14.0.0.Final") { + group("org.infinispan") { + imports = [ + "infinispan-bom" + ] + } + } library("InfluxDB Java", "2.23") { group("org.influxdb") { modules = [ diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/io.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/io.adoc index 9a482440f31..462d2d68d0a 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/io.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/documentation/io.adoc @@ -2,7 +2,7 @@ == IO If your application needs IO capabilities, see one or more of the following sections: -* *Caching:* <> +* *Caching:* <> * *Quartz:* <> * *Mail:* <> * *Validation:* <> diff --git a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/io/caching.adoc b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/io/caching.adoc index 2d62a79cf9d..7a56c9b31d6 100644 --- a/spring-boot-project/spring-boot-docs/src/docs/asciidoc/io/caching.adoc +++ b/spring-boot-project/spring-boot-docs/src/docs/asciidoc/io/caching.adoc @@ -37,8 +37,9 @@ The cache abstraction does not provide an actual store and relies on abstraction If you have not defined a bean of type `CacheManager` or a `CacheResolver` named `cacheResolver` (see {spring-framework-api}/cache/annotation/CachingConfigurer.html[`CachingConfigurer`]), Spring Boot tries to detect the following providers (in the indicated order): . <> -. <> (EhCache 3, Hazelcast, and others) +. <> (EhCache 3, Hazelcast, Infinispan, and others) . <> +. <> . <> . <> . <> @@ -75,7 +76,7 @@ A `CacheManager` wrapping all beans of that type is created. [[io.caching.provider.jcache]] ==== JCache (JSR-107) https://jcp.org/en/jsr/detail?id=107[JCache] is bootstrapped through the presence of a `javax.cache.spi.CachingProvider` on the classpath (that is, a JSR-107 compliant caching library exists on the classpath), and the `JCacheCacheManager` is provided by the `spring-boot-starter-cache` "`Starter`". -Various compliant libraries are available, and Spring Boot provides dependency management for Ehcache 3 and Hazelcast. +Various compliant libraries are available, and Spring Boot provides dependency management for Ehcache 3, Hazelcast, and Infinispan. Any other compliant library can be added as well. It might happen that more than one provider is present, in which case the provider must be explicitly specified. @@ -114,6 +115,32 @@ If a `HazelcastInstance` has been auto-configured, it is automatically wrapped i +[[io.caching.provider.infinispan]] +==== Infinispan +https://infinispan.org/[Infinispan] has no default configuration file location, so it must be specified explicitly. +Otherwise, the default bootstrap is used. + +[source,yaml,indent=0,subs="verbatim",configprops,configblocks] +---- + spring: + cache: + infinispan: + config: "infinispan.xml" +---- + +Caches can be created on startup by setting the configprop:spring.cache.cache-names[] property. +If a custom `ConfigurationBuilder` bean is defined, it is used to customize the caches. + +To be compatible with Spring Boot's Jakarta EE 9 baseline, Infinispan's `-jakarta` modules must be used. +For every module with a `-jakarta` variant, the variant must be used in place of the standard module. +For example, `infinispan-core-jakarta` and `infinispan-commons-jakarta` must be used in place of `infinispan-core` and `infinispan-commons` respectively. + +NOTE: The support of Infinispan in Spring Boot is restricted to the embedded mode and is quite basic. +If you want more options, you should use the official Infinispan Spring Boot starter instead. +See https://github.com/infinispan/infinispan-spring-boot[Infinispan's documentation] for more details. + + + [[io.caching.provider.couchbase]] ==== Couchbase If Spring Data Couchbase is available and Couchbase is <>, a `CouchbaseCacheManager` is auto-configured. diff --git a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-cache/build.gradle b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-cache/build.gradle index 03ec8b8c793..a0be7a3727d 100644 --- a/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-cache/build.gradle +++ b/spring-boot-tests/spring-boot-smoke-tests/spring-boot-smoke-test-cache/build.gradle @@ -17,6 +17,7 @@ configurations { couchbase ehcache hazelcast + infinispan } dependencies { @@ -40,6 +41,21 @@ dependencies { hazelcast("com.hazelcast:hazelcast") hazelcast("com.hazelcast:hazelcast-spring") + infinispan(enforcedPlatform(project(":spring-boot-project:spring-boot-dependencies"))) + infinispan("javax.cache:cache-api") + infinispan("org.infinispan:infinispan-commons-jakarta") + infinispan("org.infinispan:infinispan-component-annotations") + infinispan("org.infinispan:infinispan-core-jakarta") + infinispan("org.infinispan:infinispan-jcache") + modules { + module("org.inifinispan:infinispan-commons") { + replacedBy("org.infinispan:infinispan-commons-jakarta", "Java EE 9 baseline") + } + module("org.inifinispan:infinispan-core") { + replacedBy("org.infinispan:infinispan-core-jakarta", "Java EE 9 baseline") + } + } + redisTestImplementation(enforcedPlatform(project(":spring-boot-project:spring-boot-parent"))) redisTestImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-data-redis")) redisTestImplementation(project(":spring-boot-project:spring-boot-starters:spring-boot-starter-test")) @@ -69,6 +85,12 @@ def testHazelcast = tasks.register("testHazelcast", Test) { classpath = sourceSets.test.runtimeClasspath + configurations.hazelcast } +def testInfinispan = tasks.register("testInfinispan", Test) { + description = "Runs the tests against Infinispan" + classpath = sourceSets.test.runtimeClasspath + configurations.infinispan + systemProperties = ["spring.cache.jcache.config" : "classpath:infinispan.xml"] +} + def testRedis = tasks.register("testRedis", Test) { description = "Runs the tests against Redis" classpath = sourceSets.redisTest.runtimeClasspath @@ -76,5 +98,5 @@ def testRedis = tasks.register("testRedis", Test) { } tasks.named("check").configure { - dependsOn testCaffeine, testCouchbase, testEhcache, testHazelcast, testRedis + dependsOn testCaffeine, testCouchbase, testEhcache, testHazelcast, testInfinispan, testRedis }