diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cache/CachesEndpointAutoConfiguration.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cache/CachesEndpointAutoConfiguration.java new file mode 100644 index 0000000000..f8f9f72003 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cache/CachesEndpointAutoConfiguration.java @@ -0,0 +1,51 @@ +/* + * Copyright 2012-2018 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.autoconfigure.cache; + +import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; +import org.springframework.boot.actuate.cache.CachesEndpoint; +import org.springframework.boot.autoconfigure.AutoConfigureAfter; +import org.springframework.boot.autoconfigure.EnableAutoConfiguration; +import org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration; +import org.springframework.boot.autoconfigure.condition.ConditionalOnBean; +import org.springframework.boot.autoconfigure.condition.ConditionalOnClass; +import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; +import org.springframework.cache.CacheManager; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +/** + * {@link EnableAutoConfiguration Auto-configuration} for {@link CachesEndpoint}. + * + * @author Johannes Edmeier + * @since 2.0.0 + */ +@Configuration +@ConditionalOnClass(CacheManager.class) +@AutoConfigureAfter(CacheAutoConfiguration.class) +public class CachesEndpointAutoConfiguration { + + @Bean + @ConditionalOnBean(CacheManager.class) + @ConditionalOnMissingBean + @ConditionalOnEnabledEndpoint + public CachesEndpoint cachesEndpoint(ApplicationContext context) { + return new CachesEndpoint(context); + } + +} diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cache/package-info.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cache/package-info.java new file mode 100644 index 0000000000..e1350cafb1 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/java/org/springframework/boot/actuate/autoconfigure/cache/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2017 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 actuator cache concerns. + */ +package org.springframework.boot.actuate.autoconfigure.cache; diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories index 48a19de979..ca7b7da919 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/resources/META-INF/spring.factories @@ -3,6 +3,7 @@ org.springframework.boot.actuate.autoconfigure.amqp.RabbitHealthIndicatorAutoCon org.springframework.boot.actuate.autoconfigure.audit.AuditAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.audit.AuditEventsEndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.beans.BeansEndpointAutoConfiguration,\ +org.springframework.boot.actuate.autoconfigure.cache.CachesEndpointAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.cassandra.CassandraHealthIndicatorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.cloudfoundry.servlet.CloudFoundryActuatorAutoConfiguration,\ org.springframework.boot.actuate.autoconfigure.cloudfoundry.reactive.ReactiveCloudFoundryActuatorAutoConfiguration,\ diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cache/CachesEndpointAutoConfigurationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cache/CachesEndpointAutoConfigurationTests.java new file mode 100644 index 0000000000..9c72572255 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/cache/CachesEndpointAutoConfigurationTests.java @@ -0,0 +1,66 @@ +/* + * Copyright 2012-2017 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.autoconfigure.cache; + +import org.junit.Test; + +import org.springframework.boot.actuate.cache.CachesEndpoint; +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cache.CacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.Mockito.mock; + +/** + * Tests for {@link CachesEndpointAutoConfiguration}. + * + * @author Johannes Edmeier + */ +public class CachesEndpointAutoConfigurationTests { + + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() + .withConfiguration( + AutoConfigurations.of(CachesEndpointAutoConfiguration.class)) + .withUserConfiguration(CacheConfiguration.class); + + @Test + public void runShouldHaveEndpointBean() { + this.contextRunner + .run((context) -> assertThat(context).hasSingleBean(CachesEndpoint.class)); + } + + @Test + public void runWhenEnabledPropertyIsFalseShouldNotHaveEndpointBean() { + this.contextRunner.withPropertyValues("management.endpoint.caches.enabled:false") + .run((context) -> assertThat(context) + .doesNotHaveBean(CachesEndpoint.class)); + } + + @Configuration + static class CacheConfiguration { + + @Bean + public CacheManager cacheManager() { + return mock(CacheManager.class); + } + + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CachesEndpoint.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CachesEndpoint.java new file mode 100644 index 0000000000..6535828b77 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CachesEndpoint.java @@ -0,0 +1,119 @@ +/* + * Copyright 2012-2017 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 java.util.ArrayList; +import java.util.Collection; +import java.util.List; + +import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation; +import org.springframework.boot.actuate.endpoint.annotation.Endpoint; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.context.ApplicationContext; +import org.springframework.lang.Nullable; + +import static java.util.stream.Collectors.toList; + +/** + * {@link Endpoint} to expose cache operations. + * + * @author Johannes Edmeuer + * @since 2.0.2 + */ +@Endpoint(id = "caches") +public class CachesEndpoint { + private final ApplicationContext context; + + public CachesEndpoint(ApplicationContext context) { + this.context = context; + } + + @ReadOperation + public CachesDescriptor caches() { + List caches = new ArrayList<>(); + this.context.getBeansOfType(CacheManager.class) + .forEach((name, cacheManager) -> caches.addAll( + getCacheDescriptors(name, cacheManager.getCacheNames()))); + return new CachesDescriptor(caches); + } + + private Collection getCacheDescriptors(String cacheManager, + Collection cacheNames) { + return cacheNames.stream().map(cacheName -> new CacheDescriptor(cacheName, cacheManager)).collect(toList()); + } + + @DeleteOperation + public void clearCaches(@Nullable String cacheManager, @Nullable String cacheName) { + if (cacheManager == null) { + this.context.getBeansOfType(CacheManager.class) + .forEach((name, manager) -> this.clearCaches(manager, cacheName)); + } else { + this.clearCaches(this.context.getBean(cacheManager, CacheManager.class), cacheName); + } + } + + private void clearCaches(CacheManager cacheManager, String cacheName) { + if (cacheName == null) { + cacheManager.getCacheNames().forEach(cn -> cacheManager.getCache(cn).clear()); + } else { + Cache cache = cacheManager.getCache(cacheName); + if (cache != null) { + cache.clear(); + } + } + } + + /** + * Description of an application context's caches, primarily + * intended for serialization to JSON. + */ + public static final class CachesDescriptor { + private final List caches; + + private CachesDescriptor(List caches) { + this.caches = caches; + } + + public List getCaches() { + return this.caches; + } + } + + /** + * Description of a {@link Cache}, primarily intended for serialization to + * JSON. + */ + public static final class CacheDescriptor { + private final String name; + private final String cacheManager; + + public CacheDescriptor(String name, String cacheManager) { + this.name = name; + this.cacheManager = cacheManager; + } + + public String getName() { + return name; + } + + public String getCacheManager() { + return cacheManager; + } + } +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/package-info.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/package-info.java new file mode 100644 index 0000000000..674fe09dc7 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/package-info.java @@ -0,0 +1,20 @@ +/* + * Copyright 2012-2018 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. + */ + +/** + * Actuator support for caches. + */ +package org.springframework.boot.actuate.cache; diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cache/CachesEndpointTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cache/CachesEndpointTests.java new file mode 100644 index 0000000000..fe6a051bf7 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cache/CachesEndpointTests.java @@ -0,0 +1,117 @@ +/* + * Copyright 2012-2018 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 org.junit.Test; + +import org.springframework.boot.autoconfigure.AutoConfigurations; +import org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration; +import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.cache.Cache; +import org.springframework.cache.annotation.EnableCaching; +import org.springframework.cache.concurrent.ConcurrentMapCache; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link CachesEndpoint}. + * + * @author Johannes Edmeier + */ +public class CachesEndpointTests { + private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration( + AutoConfigurations.of(CacheAutoConfiguration.class)); + + @Test + public void cacheReportIsReturned() { + //@formatter:off + this.contextRunner.withUserConfiguration(Config.class) + .run(context -> assertThat(context.getBean(CachesEndpoint.class).caches().getCaches()) + .hasSize(2) + .anySatisfy(cache -> { + assertThat(cache.getName()).isEqualTo("first"); + assertThat(cache.getCacheManager()).isEqualTo("cacheManager"); + }).anySatisfy(cache -> { + assertThat(cache.getName()).isEqualTo("second"); + assertThat(cache.getCacheManager()).isEqualTo("cacheManager"); + }) + ); + //@formatter:on + } + + @Test + public void cacheIsCleared() { + this.contextRunner.withUserConfiguration(Config.class).run((context) -> { + Cache firstCache = context.getBean("firstCache", Cache.class); + firstCache.put("key", "vale"); + Cache secondCache = context.getBean("secondCache", Cache.class); + secondCache.put("key", "value"); + context.getBean(CachesEndpoint.class).clearCaches(null, null); + assertThat(firstCache.get("key", String.class)).isNull(); + assertThat(secondCache.get("key", String.class)).isNull(); + }); + } + + @Test + public void namedCacheIsCleared() { + this.contextRunner.withUserConfiguration(Config.class).run((context) -> { + Cache firstCache = context.getBean("firstCache", Cache.class); + firstCache.put("key", "value"); + Cache secondCache = context.getBean("secondCache", Cache.class); + secondCache.put("key", "value"); + context.getBean(CachesEndpoint.class).clearCaches(null, "first"); + assertThat(firstCache.get("key", String.class)).isNull(); + assertThat(secondCache.get("key", String.class)).isEqualTo("value"); + }); + } + + @Test + public void unknwonCache() { + this.contextRunner.withUserConfiguration(Config.class).run((context) -> { + Cache firstCache = context.getBean("firstCache", Cache.class); + firstCache.put("key", "value"); + Cache secondCache = context.getBean("secondCache", Cache.class); + secondCache.put("key", "value"); + context.getBean(CachesEndpoint.class).clearCaches(null, "UNKNWON"); + assertThat(firstCache.get("key", String.class)).isEqualTo("value"); + assertThat(secondCache.get("key", String.class)).isEqualTo("value"); + }); + } + + @Configuration + @EnableCaching + public static class Config { + @Bean + public Cache firstCache() { + return new ConcurrentMapCache("first"); + } + + @Bean + public Cache secondCache() { + return new ConcurrentMapCache("second"); + } + + @Bean + public CachesEndpoint endpoint(ApplicationContext context) { + return new CachesEndpoint(context); + } + } + +}