diff --git a/docs/src/main/asciidoc/spring-cloud-commons.adoc b/docs/src/main/asciidoc/spring-cloud-commons.adoc index 1bea0e00..bc87967c 100644 --- a/docs/src/main/asciidoc/spring-cloud-commons.adoc +++ b/docs/src/main/asciidoc/spring-cloud-commons.adoc @@ -801,6 +801,28 @@ in the following sections: * <> * <> +=== Spring Cloud LoadBalancer Caching + +Apart from the basic `ServiceInstanceListSupplier` implementation that retrieves instances +via `DiscoveryClient` each time it has to choose an instance, we provide a https://github.com/ben-manes/caffeine[Caffeine-backed] +implementation. +To make use of it, you need to have `com.github.ben-manes.caffeine:caffeine` in the classpath. +The default setup includes `expireAfterWrite` set to 30 seconds and records set to soft references. + +You can set your own `TTL` value (the time after write after which entries should be expired), expressed as `Duration`, by passing a `String` compliant with the https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/Duration.html#parse(java.lang.CharSequence)[`Duration` API] +as the value of `spring.cloud.loadbalancer.cache.ttl` property. + +You can also override the default Caffeine Cache setup for the LoadBalancer by passing your own https://static.javadoc.io/com.github.ben-manes.caffeine/caffeine/2.2.2/com/github/benmanes/caffeine/cache/CaffeineSpec.html[Caffeine Specification] +in the `spring.cloud.loadbalancer.cache.caffeine.spec` property. + +WARN: Passing your own Caffeine specification will override any other LoadBalancerCache settings, including `TTL`. + +You can also altogether disable loadBalancer caching by setting the value of `spring.cloud.loadbalancer.cache.enabled` +to `false`. + +WARNING: Although the basic, non-cached, implementation is useful for prototyping and testing, it's much less efficient +than the cached versions, so we recommend always using the cached version in production. + [[spring-cloud-loadbalancer-starter]] === Spring Cloud LoadBalancer starter @@ -808,11 +830,8 @@ We also provide a starter that allows you to easily add Spring Cloud LoadBalance In order to use it, just add `org.springframework.cloud:spring-cloud-starter-loadbalancer` to your Spring Cloud dependencies in your build file. -WARNING: In order to make use of the more efficient cached version of `ServiceInstanceListSupplier`, - `spring-cloud-starter-loadbalancer` will *enable caching* by default. -https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-caching.html[Spring Boot Caching] -mechanism will be used under the hood. If you don't want caching to be used, you can set -the value of `spring.cache.type` to `none`. +NOTE: Spring Cloud LoadBalancer starter includes +https://docs.spring.io/spring-boot/docs/current/reference/html/boot-features-caching.html[Spring Boot Caching]. WARNING: If you have both Ribbon and Spring Cloud LoadBalancer int the classpath, in order to maintain backward compatibility, Ribbon-based implementations will be used by default. In order diff --git a/spring-cloud-loadbalancer/pom.xml b/spring-cloud-loadbalancer/pom.xml index 32e0e3f0..69c14c26 100644 --- a/spring-cloud-loadbalancer/pom.xml +++ b/spring-cloud-loadbalancer/pom.xml @@ -61,7 +61,12 @@ org.springframework.boot spring-boot-starter-cache - test + true + + + com.github.ben-manes.caffeine + caffeine + true org.springframework.boot @@ -78,10 +83,5 @@ reactor-test test - - com.github.ben-manes.caffeine - caffeine - test - diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java index 71873dd6..35fcc800 100644 --- a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/annotation/LoadBalancerClientConfiguration.java @@ -20,13 +20,13 @@ import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; import org.springframework.boot.context.properties.EnableConfigurationProperties; -import org.springframework.cache.CacheManager; import org.springframework.cloud.client.ConditionalOnBlockingDiscoveryEnabled; import org.springframework.cloud.client.ConditionalOnDiscoveryEnabled; import org.springframework.cloud.client.ConditionalOnReactiveDiscoveryEnabled; import org.springframework.cloud.client.ServiceInstance; import org.springframework.cloud.client.discovery.DiscoveryClient; import org.springframework.cloud.client.discovery.ReactiveDiscoveryClient; +import org.springframework.cloud.loadbalancer.cache.LoadBalancerCacheManager; import org.springframework.cloud.loadbalancer.core.CachingServiceInstanceListSupplier; import org.springframework.cloud.loadbalancer.core.CachingServiceInstanceSupplier; import org.springframework.cloud.loadbalancer.core.DiscoveryClientServiceInstanceListSupplier; @@ -36,6 +36,7 @@ import org.springframework.cloud.loadbalancer.core.RoundRobinLoadBalancer; import org.springframework.cloud.loadbalancer.core.ServiceInstanceListSupplier; import org.springframework.cloud.loadbalancer.core.ServiceInstanceSupplier; import org.springframework.cloud.loadbalancer.support.LoadBalancerClientFactory; +import org.springframework.context.ApplicationContext; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.core.annotation.Order; @@ -73,12 +74,14 @@ public class LoadBalancerClientConfiguration { @ConditionalOnMissingBean public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier( ReactiveDiscoveryClient discoveryClient, Environment env, - ObjectProvider cacheManager) { + ApplicationContext context) { DiscoveryClientServiceInstanceListSupplier delegate = new DiscoveryClientServiceInstanceListSupplier( discoveryClient, env); - if (cacheManager.getIfAvailable() != null) { + ObjectProvider cacheManagerProvider = context + .getBeanProvider(LoadBalancerCacheManager.class); + if (cacheManagerProvider.getIfAvailable() != null) { return new CachingServiceInstanceListSupplier(delegate, - cacheManager.getIfAvailable()); + cacheManagerProvider.getIfAvailable()); } return delegate; } @@ -88,12 +91,14 @@ public class LoadBalancerClientConfiguration { @ConditionalOnMissingBean public ServiceInstanceSupplier discoveryClientServiceInstanceSupplier( ReactiveDiscoveryClient discoveryClient, Environment env, - ObjectProvider cacheManager) { + ApplicationContext context) { DiscoveryClientServiceInstanceSupplier delegate = new DiscoveryClientServiceInstanceSupplier( discoveryClient, env); - if (cacheManager.getIfAvailable() != null) { + ObjectProvider cacheManagerProvider = context + .getBeanProvider(LoadBalancerCacheManager.class); + if (cacheManagerProvider.getIfAvailable() != null) { return new CachingServiceInstanceSupplier(delegate, - cacheManager.getIfAvailable()); + cacheManagerProvider.getIfAvailable()); } return delegate; } @@ -110,12 +115,14 @@ public class LoadBalancerClientConfiguration { @ConditionalOnMissingBean public ServiceInstanceListSupplier discoveryClientServiceInstanceListSupplier( DiscoveryClient discoveryClient, Environment env, - ObjectProvider cacheManager) { + ApplicationContext context) { DiscoveryClientServiceInstanceListSupplier delegate = new DiscoveryClientServiceInstanceListSupplier( discoveryClient, env); - if (cacheManager.getIfAvailable() != null) { + ObjectProvider cacheManagerProvider = context + .getBeanProvider(LoadBalancerCacheManager.class); + if (cacheManagerProvider.getIfAvailable() != null) { return new CachingServiceInstanceListSupplier(delegate, - cacheManager.getIfAvailable()); + cacheManagerProvider.getIfAvailable()); } return delegate; } @@ -125,12 +132,14 @@ public class LoadBalancerClientConfiguration { @ConditionalOnMissingBean public ServiceInstanceSupplier discoveryClientServiceInstanceSupplier( DiscoveryClient discoveryClient, Environment env, - ObjectProvider cacheManager) { + ApplicationContext context) { DiscoveryClientServiceInstanceSupplier delegate = new DiscoveryClientServiceInstanceSupplier( discoveryClient, env); - if (cacheManager.getIfAvailable() != null) { + ObjectProvider cacheManagerProvider = context + .getBeanProvider(LoadBalancerCacheManager.class); + if (cacheManagerProvider.getIfAvailable() != null) { return new CachingServiceInstanceSupplier(delegate, - cacheManager.getIfAvailable()); + cacheManagerProvider.getIfAvailable()); } return delegate; } diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/cache/CaffeineBasedLoadBalancerCacheManager.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/cache/CaffeineBasedLoadBalancerCacheManager.java new file mode 100644 index 00000000..f9a60631 --- /dev/null +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/cache/CaffeineBasedLoadBalancerCacheManager.java @@ -0,0 +1,53 @@ +/* + * Copyright 2012-2019 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.cloud.loadbalancer.cache; + +import com.github.benmanes.caffeine.cache.Caffeine; + +import org.springframework.cache.caffeine.CaffeineCacheManager; +import org.springframework.util.StringUtils; + +import static org.springframework.cloud.loadbalancer.core.CachingServiceInstanceListSupplier.SERVICE_INSTANCE_CACHE_NAME; + +/** + * A Spring Cloud LoadBalancer specific implementation of {@link CaffeineCacheManager} that + * implements the {@link LoadBalancerCacheManager} marker interface. + * + * @author Olga Maciaszek-Sharma + * @since 2.2.0 + */ +public class CaffeineBasedLoadBalancerCacheManager extends CaffeineCacheManager + implements LoadBalancerCacheManager { + + public CaffeineBasedLoadBalancerCacheManager(String cacheName, + LoadBalancerCacheProperties properties) { + super(cacheName); + if (!StringUtils.isEmpty(properties.getCaffeine().getSpec())) { + setCacheSpecification(properties.getCaffeine().getSpec()); + } + else { + setCaffeine(Caffeine.newBuilder().expireAfterWrite(properties.getTtl()) + .softValues()); + } + + } + + public CaffeineBasedLoadBalancerCacheManager(LoadBalancerCacheProperties properties) { + this(SERVICE_INSTANCE_CACHE_NAME, properties); + } + +} diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/cache/LoadBalancerCacheManager.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/cache/LoadBalancerCacheManager.java new file mode 100644 index 00000000..ae8511b0 --- /dev/null +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/cache/LoadBalancerCacheManager.java @@ -0,0 +1,28 @@ +/* + * Copyright 2012-2019 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.cloud.loadbalancer.cache; + +import org.springframework.cache.CacheManager; + +/** + * A marker interface for Spring Cloud LoadBalancer-specific {@link CacheManager} beans. + * + * @author Olga Maciaszek-Sharma + */ +public interface LoadBalancerCacheManager extends CacheManager { + +} diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/cache/LoadBalancerCacheProperties.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/cache/LoadBalancerCacheProperties.java new file mode 100644 index 00000000..52b4d1d0 --- /dev/null +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/cache/LoadBalancerCacheProperties.java @@ -0,0 +1,81 @@ +/* + * Copyright 2012-2019 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.cloud.loadbalancer.cache; + +import java.time.Duration; + +import org.springframework.boot.context.properties.ConfigurationProperties; + +/** + * Spring Cloud LoadBalancer cache properties. + * + * @author Olga Maciaszek-Sharma + * @since 2.2.0 + */ +@ConfigurationProperties("spring.cloud.loadbalancer.cache") +public class LoadBalancerCacheProperties { + + private Caffeine caffeine = new Caffeine(); + + /** + * Time To Live - time counted from writing of the record, after which cache entries + * are expired, expressed as a {@link Duration}. The property {@link String} has to be + * in keeping with the appropriate syntax as specified in + * {@link Duration#parse(CharSequence)}. + */ + private Duration ttl = Duration.ofSeconds(30); + + public Caffeine getCaffeine() { + return caffeine; + } + + public void setCaffeine(Caffeine caffeine) { + this.caffeine = caffeine; + } + + public Duration getTtl() { + return ttl; + } + + public void setTtl(String ttl) { + this.ttl = Duration.parse(ttl); + } + + /** + * Caffeine-specific LoadBalancer cache properties. + * NOTE: Passing your own Caffeine specification will override any other LoadBalancerCache settings, + * including TTL. + */ + public static class Caffeine { + + /** + * The spec to use to create caches. See CaffeineSpec for more details on the spec + * format. + */ + private String spec = ""; + + public String getSpec() { + return spec; + } + + public void setSpec(String spec) { + this.spec = spec; + } + + } + +} diff --git a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/LoadBalancerCacheAutoConfiguration.java b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/LoadBalancerCacheAutoConfiguration.java index b2f81aa3..265e44fb 100644 --- a/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/LoadBalancerCacheAutoConfiguration.java +++ b/spring-cloud-loadbalancer/src/main/java/org/springframework/cloud/loadbalancer/config/LoadBalancerCacheAutoConfiguration.java @@ -16,29 +16,85 @@ package org.springframework.cloud.loadbalancer.config; -import org.springframework.boot.autoconfigure.AutoConfigureBefore; +import javax.annotation.PostConstruct; + +import com.github.benmanes.caffeine.cache.Caffeine; +import org.apache.commons.logging.Log; +import org.apache.commons.logging.LogFactory; + +import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration; import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty; +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.cloud.loadbalancer.cache.CaffeineBasedLoadBalancerCacheManager; +import org.springframework.cloud.loadbalancer.cache.LoadBalancerCacheManager; +import org.springframework.cloud.loadbalancer.cache.LoadBalancerCacheProperties; +import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** - * An AutoConfiguration that automatically enables caching when when Spring Boot and - * Spring Framework Cache support classes are present. + * An AutoConfiguration that automatically enables caching when when Spring Boot, and + * Spring Framework Cache support and Caffeine classes are present and warns if Caffeine + * is not present (we are only warning about Caffeine because the other dependencies are + * in spring-cloud-starter-loadbalancer). * * @author Olga Maciaszek-Sharma + * @since 2.2.0 * @see CacheManager * @see CacheAutoConfiguration * @see CacheAspectSupport */ @Configuration(proxyBeanMethods = false) @ConditionalOnClass({ CacheManager.class, CacheAutoConfiguration.class }) -@ConditionalOnMissingBean(CacheAspectSupport.class) -@EnableCaching -@AutoConfigureBefore(CacheAutoConfiguration.class) +@AutoConfigureAfter(CacheAutoConfiguration.class) +@ConditionalOnProperty(value = "spring.cloud.loadbalancer.cache.enabled", + matchIfMissing = true) +@EnableConfigurationProperties(LoadBalancerCacheProperties.class) public class LoadBalancerCacheAutoConfiguration { + @Configuration(proxyBeanMethods = false) + @ConditionalOnMissingClass("com.github.benmanes.caffeine.cache.Caffeine") + protected static class LoadBalancerCacheManagerWarnConfiguration { + + @Bean + LoadBalancerCaffeineWarnLogger caffeineWarnLogger() { + return new LoadBalancerCaffeineWarnLogger(); + } + + } + + static class LoadBalancerCaffeineWarnLogger { + + private static final Log LOG = LogFactory + .getLog(LoadBalancerCaffeineWarnLogger.class); + + @PostConstruct + void logWarning() { + if (LOG.isWarnEnabled()) { + LOG.warn( + "Spring Cloud LoadBalancer is currently working without cache. To enable cache, add " + + "com.github.ben-manes.caffeine:caffeine dependency to classpath."); + } + } + + } + + @Configuration(proxyBeanMethods = false) + @ConditionalOnClass(Caffeine.class) + protected static class LoadBalancerCacheManagerConfiguration { + + @Bean(autowireCandidate = false) + @ConditionalOnMissingBean + LoadBalancerCacheManager loadBalancerCacheManager( + LoadBalancerCacheProperties cacheProperties) { + return new CaffeineBasedLoadBalancerCacheManager(cacheProperties); + } + + } + } diff --git a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/config/LoadBalancerCacheAutoConfigurationTests.java b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/config/LoadBalancerCacheAutoConfigurationTests.java index 430f9d3a..4fdebe8a 100644 --- a/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/config/LoadBalancerCacheAutoConfigurationTests.java +++ b/spring-cloud-loadbalancer/src/test/java/org/springframework/cloud/loadbalancer/config/LoadBalancerCacheAutoConfigurationTests.java @@ -16,17 +16,16 @@ package org.springframework.cloud.loadbalancer.config; -import java.util.ArrayList; -import java.util.List; - import org.junit.jupiter.api.Test; +import org.springframework.boot.autoconfigure.AutoConfigurations; import org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration; -import org.springframework.boot.test.util.TestPropertyValues; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; import org.springframework.cache.CacheManager; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.caffeine.CaffeineCacheManager; import org.springframework.cache.support.NoOpCacheManager; -import org.springframework.context.annotation.AnnotationConfigApplicationContext; -import org.springframework.util.StringUtils; +import org.springframework.context.annotation.Configuration; import static org.assertj.core.api.Assertions.assertThat; @@ -40,31 +39,63 @@ class LoadBalancerCacheAutoConfigurationTests { @Test void shouldAutoEnableCaching() { - AnnotationConfigApplicationContext context = setup(""); - assertThat(context.getBeansOfType(CacheManager.class)).isNotEmpty(); - assertThat(context.getBeansOfType(CacheManager.class).get("cacheManager")) - .isNotInstanceOf(NoOpCacheManager.class); + ApplicationContextRunner contextRunner = baseApplicationRunner(); + + contextRunner.run(context -> { + assertThat(context.getBeansOfType(CacheManager.class)).hasSize(1); + assertThat(((CacheManager) context.getBean("loadBalancerCacheManager")) + .getCacheNames()).hasSize(1); + assertThat(context.getBean("loadBalancerCacheManager")) + .isInstanceOf(CaffeineCacheManager.class); + assertThat(((CacheManager) context.getBean("loadBalancerCacheManager")) + .getCacheNames()).contains("CachingServiceInstanceListSupplierCache"); + }); } @Test - void shouldUseNoOpCacheIfCacheTypeNone() { - AnnotationConfigApplicationContext context = setup("spring.cache.type=none"); - assertThat(context.getBeansOfType(CacheManager.class)).isNotEmpty(); - assertThat(context.getBeansOfType(CacheManager.class).get("cacheManager")) - .isInstanceOf(NoOpCacheManager.class); + void loadBalancerCacheShouldNotOverrideCacheTypeSetting() { + ApplicationContextRunner contextRunner = baseApplicationRunner() + .withUserConfiguration(TestConfiguration.class) + .withPropertyValues("spring.cache.type=none"); + + contextRunner.run(context -> { + assertThat(context.getBeansOfType(CacheManager.class)).hasSize(2); + assertThat(context.getBean("loadBalancerCacheManager")) + .isInstanceOf(CaffeineCacheManager.class); + assertThat(context.getBeansOfType(CacheManager.class).get("cacheManager")) + .isInstanceOf(NoOpCacheManager.class); + + }); } - private AnnotationConfigApplicationContext setup(String property) { - List config = new ArrayList<>(); - config.add(LoadBalancerCacheAutoConfiguration.class); - config.add(CacheAutoConfiguration.class); - AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext(); - if (StringUtils.hasText(property)) { - TestPropertyValues.of(property).applyTo(context); - } - context.register(config.toArray(new Class[0])); - context.refresh(); - return context; + @Test + void loadBalancerCacheShouldNotOverrideExistingCaffeineCacheManager() { + ApplicationContextRunner contextRunner = baseApplicationRunner() + .withUserConfiguration(TestConfiguration.class); + + contextRunner.run(context -> { + assertThat(context.getBeansOfType(CacheManager.class)).hasSize(2); + assertThat(context.getBean("cacheManager")) + .isInstanceOf(CaffeineCacheManager.class); + assertThat(((CacheManager) context.getBean("cacheManager")).getCacheNames()) + .isEmpty(); + assertThat(((CacheManager) context.getBean("loadBalancerCacheManager")) + .getCacheNames()).hasSize(1); + assertThat(((CacheManager) context.getBean("loadBalancerCacheManager")) + .getCacheNames()).contains("CachingServiceInstanceListSupplierCache"); + }); + + } + + private ApplicationContextRunner baseApplicationRunner() { + return new ApplicationContextRunner().withConfiguration(AutoConfigurations.of( + CacheAutoConfiguration.class, LoadBalancerCacheAutoConfiguration.class)); + } + + @Configuration(proxyBeanMethods = false) + @EnableCaching + static class TestConfiguration { + } }