diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/caches.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/caches.adoc new file mode 100644 index 0000000000..6342cf77c7 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/endpoints/caches.adoc @@ -0,0 +1,102 @@ +[[caches]] += Caches (`caches`) + +The `caches` endpoint provides access to the application's caches. + + + +[[caches-all]] +== Retrieving All Caches + +To retrieve the application's caches, make a `GET` request to `/actuator/caches`, as +shown in the following curl-based example: + +include::{snippets}caches/all/curl-request.adoc[] + +The resulting response is similar to the following: + +include::{snippets}caches/all/http-response.adoc[] + + + +[[caches-all-response-structure]] +=== Response Structure + +The response contains details of the application's caches. The following table describes +the structure of the response: + +[cols="3,1,3"] +include::{snippets}caches/all/response-fields.adoc[] + + + +[[caches-named]] +== Retrieving Caches by Name + +To retrieve a cache by name, make a `GET` request to `/actuator/caches/{name}`, +as shown in the following curl-based example: + +include::{snippets}caches/named/curl-request.adoc[] + +The preceding example retrieves information about the cache named `cities`. The +resulting response is similar to the following: + +include::{snippets}caches/named/http-response.adoc[] + + + +[[caches-named-request-structure]] +=== Request Structure + +If the requested name is specific enough to identify a single cache, no extra parameter is +required. Otherwise, the `cacheManager` must be specified. The following table shows the +supported query parameters: + +[cols="2,4"] +include::{snippets}caches/named/request-parameters.adoc[] + + + +[[caches-named-response-structure]] +=== Response Structure + +The response contains details of the requested cache. The following table describes the +structure of the response: + +[cols="3,1,3"] +include::{snippets}caches/named/response-fields.adoc[] + + + +[[caches-evict-all]] +== Evict All Caches + +To clear all available caches, make a `DELETE` request to `/actuator/caches` as shown in +the following curl-based example: + +include::{snippets}caches/evict-all/curl-request.adoc[] + + + +[[caches-evict-named]] +== Evict a Cache by Name + +To evict a particular cache, make a `DELETE` request to `/actuator/caches/{name}` as shown +in the following curl-based example: + +include::{snippets}caches/evict-named/curl-request.adoc[] + +NOTE: As there are two caches named `countries`, the `cacheManager` has to be provided to +specify which `Cache` should be cleared. + + + +[[caches-evict-named-request-structure]] +=== Request Structure + +If the requested name is specific enough to identify a single cache, no extra parameter is +required. Otherwise, the `cacheManager` must be specified. The following table shows the +supported query parameters: + +[cols="2,4"] +include::{snippets}caches/evict-named/request-parameters.adoc[] diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/index.adoc b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/index.adoc index 7fb1913b29..00ff14c487 100644 --- a/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/index.adoc +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/main/asciidoc/index.adoc @@ -51,6 +51,7 @@ https://en.wikipedia.org/wiki/ISO_8601[ISO 8601]. include::endpoints/auditevents.adoc[leveloffset=+1] include::endpoints/beans.adoc[leveloffset=+1] +include::endpoints/caches.adoc[leveloffset=+1] include::endpoints/conditions.adoc[leveloffset=+1] include::endpoints/configprops.adoc[leveloffset=+1] include::endpoints/env.adoc[leveloffset=+1] 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 index f8f9f72003..6137925f78 100644 --- 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 @@ -16,8 +16,13 @@ package org.springframework.boot.actuate.autoconfigure.cache; +import java.util.LinkedHashMap; +import java.util.Map; + +import org.springframework.beans.factory.ObjectProvider; import org.springframework.boot.actuate.autoconfigure.endpoint.condition.ConditionalOnEnabledEndpoint; import org.springframework.boot.actuate.cache.CachesEndpoint; +import org.springframework.boot.actuate.cache.CachesEndpointWebExtension; import org.springframework.boot.autoconfigure.AutoConfigureAfter; import org.springframework.boot.autoconfigure.EnableAutoConfiguration; import org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration; @@ -25,7 +30,6 @@ 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; @@ -33,7 +37,8 @@ import org.springframework.context.annotation.Configuration; * {@link EnableAutoConfiguration Auto-configuration} for {@link CachesEndpoint}. * * @author Johannes Edmeier - * @since 2.0.0 + * @author Stephane Nicoll + * @since 2.1.0 */ @Configuration @ConditionalOnClass(CacheManager.class) @@ -41,11 +46,20 @@ import org.springframework.context.annotation.Configuration; public class CachesEndpointAutoConfiguration { @Bean - @ConditionalOnBean(CacheManager.class) @ConditionalOnMissingBean @ConditionalOnEnabledEndpoint - public CachesEndpoint cachesEndpoint(ApplicationContext context) { - return new CachesEndpoint(context); + public CachesEndpoint cachesEndpoint( + ObjectProvider> cacheManagers) { + return new CachesEndpoint(cacheManagers.getIfAvailable(LinkedHashMap::new)); + } + + @Bean + @ConditionalOnMissingBean + @ConditionalOnEnabledEndpoint + @ConditionalOnBean(CachesEndpoint.class) + public CachesEndpointWebExtension cachesEndpointWebExtension( + CachesEndpoint cachesEndpoint) { + return new CachesEndpointWebExtension(cachesEndpoint); } } 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 index e1350cafb1..416af490e8 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * 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. 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 index 9c72572255..9d193739bc 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * 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. @@ -32,23 +32,31 @@ import static org.mockito.Mockito.mock; * Tests for {@link CachesEndpointAutoConfiguration}. * * @author Johannes Edmeier + * @author Stephane Nicoll */ public class CachesEndpointAutoConfigurationTests { private final ApplicationContextRunner contextRunner = new ApplicationContextRunner() .withConfiguration( - AutoConfigurations.of(CachesEndpointAutoConfiguration.class)) - .withUserConfiguration(CacheConfiguration.class); + AutoConfigurations.of(CachesEndpointAutoConfiguration.class)); @Test public void runShouldHaveEndpointBean() { - this.contextRunner - .run((context) -> assertThat(context).hasSingleBean(CachesEndpoint.class)); + this.contextRunner.withUserConfiguration(CacheConfiguration.class) + .run((context) -> + assertThat(context).hasSingleBean(CachesEndpoint.class)); + } + + @Test + public void runWithoutCacheManagerShouldHaveEndpointBean() { + this.contextRunner.run((context) -> + assertThat(context).hasSingleBean(CachesEndpoint.class)); } @Test public void runWhenEnabledPropertyIsFalseShouldNotHaveEndpointBean() { this.contextRunner.withPropertyValues("management.endpoint.caches.enabled:false") + .withUserConfiguration(CacheConfiguration.class) .run((context) -> assertThat(context) .doesNotHaveBean(CachesEndpoint.class)); } diff --git a/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/CachesEndpointDocumentationTests.java b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/CachesEndpointDocumentationTests.java new file mode 100644 index 0000000000..117ce02993 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator-autoconfigure/src/test/java/org/springframework/boot/actuate/autoconfigure/endpoint/web/documentation/CachesEndpointDocumentationTests.java @@ -0,0 +1,123 @@ +/* + * 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.endpoint.web.documentation; + +import java.util.Arrays; +import java.util.Collections; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +import org.junit.Test; + +import org.springframework.boot.actuate.cache.CachesEndpoint; +import org.springframework.boot.actuate.cache.CachesEndpointWebExtension; +import org.springframework.cache.CacheManager; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.context.annotation.Import; +import org.springframework.restdocs.mockmvc.MockMvcRestDocumentation; +import org.springframework.restdocs.payload.FieldDescriptor; +import org.springframework.restdocs.request.ParameterDescriptor; + +import static org.springframework.restdocs.payload.PayloadDocumentation.fieldWithPath; +import static org.springframework.restdocs.payload.PayloadDocumentation.responseFields; +import static org.springframework.restdocs.request.RequestDocumentation.parameterWithName; +import static org.springframework.restdocs.request.RequestDocumentation.requestParameters; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.delete; +import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get; +import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status; + +/** + * Tests for generating documentation describing the {@link CachesEndpoint} + * @author Stephane Nicoll + */ +public class CachesEndpointDocumentationTests extends MockMvcEndpointDocumentationTests { + + private static final List levelFields = Arrays.asList( + fieldWithPath("name").description("Cache name."), + fieldWithPath("cacheManager").description("Cache manager name."), + fieldWithPath("target").description( + "Fully qualified name of the native cache.")); + + private static final List requestParameters = Collections.singletonList( + parameterWithName("cacheManager") + .description("Name of the cacheManager to qualify the cache. May be " + + "omitted if the cache name is unique.") + .optional()); + + @Test + public void allCaches() throws Exception { + this.mockMvc.perform(get("/actuator/caches")).andExpect(status().isOk()) + .andDo(MockMvcRestDocumentation.document("caches/all", responseFields( + fieldWithPath("cacheManagers") + .description("Cache managers keyed by id."), + fieldWithPath("cacheManagers.*") + .description("Caches in the application context keyed by " + + "name.")) + .andWithPrefix("cacheManagers.*.*.", fieldWithPath("target") + .description( + "Fully qualified name of the native cache.")))); + } + + @Test + public void namedCache() throws Exception { + this.mockMvc.perform(get("/actuator/caches/cities")).andExpect(status().isOk()) + .andDo(MockMvcRestDocumentation.document("caches/named", + requestParameters(requestParameters), + responseFields(levelFields))); + } + + @Test + public void evictAllCaches() throws Exception { + this.mockMvc.perform(delete("/actuator/caches")).andExpect(status().isNoContent()) + .andDo(MockMvcRestDocumentation.document("caches/evict-all")); + } + + @Test + public void evictNamedCache() throws Exception { + this.mockMvc.perform( + delete("/actuator/caches/countries?cacheManager=anotherCacheManager")) + .andExpect(status().isNoContent()).andDo( + MockMvcRestDocumentation.document("caches/evict-named", + requestParameters(requestParameters))); + } + + + @Configuration + @Import(BaseDocumentationConfiguration.class) + static class TestConfiguration { + + @Bean + public CachesEndpoint endpoint() { + Map cacheManagers = new HashMap<>(); + cacheManagers.put("cacheManager", new ConcurrentMapCacheManager( + "countries", "cities")); + cacheManagers.put("anotherCacheManager", new ConcurrentMapCacheManager( + "countries")); + return new CachesEndpoint(cacheManagers); + } + + @Bean + public CachesEndpointWebExtension endpointWebExtension() { + return new CachesEndpointWebExtension(endpoint()); + } + + } + +} 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 index 6535828b77..65b916db79 100644 --- 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 @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * 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. @@ -17,103 +17,202 @@ package org.springframework.boot.actuate.cache; import java.util.ArrayList; -import java.util.Collection; +import java.util.LinkedHashMap; import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.function.Predicate; +import java.util.stream.Collectors; 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.boot.actuate.endpoint.annotation.Selector; 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. + * {@link Endpoint} to expose available {@link Cache caches}. * * @author Johannes Edmeuer - * @since 2.0.2 + * @author Stephane Nicoll + * @since 2.1.0 */ @Endpoint(id = "caches") public class CachesEndpoint { - private final ApplicationContext context; - public CachesEndpoint(ApplicationContext context) { - this.context = context; + private final Map cacheManagers; + + /** + * Create a new endpoint with the {@link CacheManager} instances to use. + * @param cacheManagers the cache managers to use, indexed by name + */ + public CachesEndpoint(Map cacheManagers) { + this.cacheManagers = new LinkedHashMap<>(cacheManagers); + } + + /** + * Return a {@link CachesReport} of all available {@link Cache caches}. + * @return a caches reports + */ + @ReadOperation + public CachesReport caches() { + Map> descriptors = new LinkedHashMap<>(); + getCacheEntries((name) -> true, (cacheManager) -> true).forEach((entry) -> { + Map cmDescriptors = descriptors.computeIfAbsent( + entry.getCacheManager(), (key) -> new LinkedHashMap<>()); + String cache = entry.getName(); + cmDescriptors.put(cache, new CacheDescriptor(entry.getTarget())); + }); + return new CachesReport(descriptors); } + /** + * Return a {@link CacheDescriptor} for the specified cache. + * @param cache then name of the cache + * @param cacheManager the name of the cacheManager (can be {@code null} + * @return the descriptor of the cache or {@code null} if no such cache exists + * @throws NonUniqueCacheException if more than one cache with that name exist and no + * {@code cacheManager} was provided to identify a unique candidate + */ @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); + public CacheEntry cache(@Selector String cache, @Nullable String cacheManager) { + return extractUniqueCacheEntry(cache, getCacheEntries( + (name) -> name.equals(cache), safeEqual(cacheManager))); } - private Collection getCacheDescriptors(String cacheManager, - Collection cacheNames) { - return cacheNames.stream().map(cacheName -> new CacheDescriptor(cacheName, cacheManager)).collect(toList()); + /** + * Clear all the available {@link Cache caches}. + */ + @DeleteOperation + public void clearCaches() { + getCacheEntries((name) -> true, (cacheManagerName) -> true) + .forEach(this::clearCache); } + /** + * Clear the specific {@link Cache}. + * @param cache then name of the cache + * @param cacheManager the name of the cacheManager (can be {@code null} + * @return {@code true} if the cache was cleared or {@code false} if no such cache exists + * @throws NonUniqueCacheException if more than one cache with that name exist and no + */ @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); + public boolean clearCache(@Selector String cache, @Nullable String cacheManager) { + CacheEntry entry = extractUniqueCacheEntry(cache, getCacheEntries( + (name) -> name.equals(cache), safeEqual(cacheManager))); + return (entry != null && clearCache(entry)); + } + + private List getCacheEntries( + Predicate cacheNamePredicate, + Predicate cacheManagerNamePredicate) { + List entries = new ArrayList<>(); + this.cacheManagers.keySet().stream().filter(cacheManagerNamePredicate) + .forEach((cacheManagerName) -> entries.addAll( + getCacheEntries(cacheManagerName, cacheNamePredicate))); + return entries; + } + + private List getCacheEntries(String cacheManagerName, + Predicate cacheNamePredicate) { + CacheManager cacheManager = this.cacheManagers.get(cacheManagerName); + List entries = new ArrayList<>(); + cacheManager.getCacheNames().stream().filter(cacheNamePredicate) + .map(cacheManager::getCache).filter(Objects::nonNull) + .forEach((cache) -> entries.add( + new CacheEntry(cache, cacheManagerName))); + return entries; + } + + private CacheEntry extractUniqueCacheEntry(String cache, + List entries) { + if (entries.size() > 1) { + throw new NonUniqueCacheException(cache, entries.stream() + .map(CacheEntry::getCacheManager).distinct() + .collect(Collectors.toList())); + } + return (entries.isEmpty() ? null : entries.get(0)); + } + + private boolean clearCache(CacheEntry entry) { + String cacheName = entry.getName(); + Cache cache = this.cacheManagers.get(entry.getCacheManager()).getCache(cacheName); + if (cache != null) { + cache.clear(); + return true; } + return false; } - 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(); - } + private Predicate safeEqual(String name) { + return (name != null ? ((requested) -> requested.equals(name)) + : ((requested) -> true)); + } + + /** + * A report of available {@link Cache caches}, primarily intended for serialization + * to JSON. + */ + public static final class CachesReport { + + private final Map> cacheManagers; + + public CachesReport(Map> cacheManagers) { + this.cacheManagers = cacheManagers; + } + + public Map> getCacheManagers() { + return this.cacheManagers; } + } /** - * Description of an application context's caches, primarily - * intended for serialization to JSON. + * Basic description of a {@link Cache}, primarily intended for serialization to JSON. */ - public static final class CachesDescriptor { - private final List caches; + public static class CacheDescriptor { + + private final String target; - private CachesDescriptor(List caches) { - this.caches = caches; + public CacheDescriptor(String target) { + this.target = target; } - public List getCaches() { - return this.caches; + /** + * Return the fully qualified name of the native cache. + * @return the fully qualified name of the native cache + */ + public String getTarget() { + return this.target; } + } /** - * Description of a {@link Cache}, primarily intended for serialization to - * JSON. + * Description of a {@link Cache}, primarily intended for serialization to JSON. */ - public static final class CacheDescriptor { + public static final class CacheEntry extends CacheDescriptor { + private final String name; + private final String cacheManager; - public CacheDescriptor(String name, String cacheManager) { - this.name = name; + public CacheEntry(Cache cache, String cacheManager) { + super(cache.getNativeCache().getClass().getName()); + this.name = cache.getName(); this.cacheManager = cacheManager; } public String getName() { - return name; + return this.name; } public String getCacheManager() { - return cacheManager; + return this.cacheManager; } + } + } diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CachesEndpointWebExtension.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CachesEndpointWebExtension.java new file mode 100644 index 0000000000..8258ead0bc --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/CachesEndpointWebExtension.java @@ -0,0 +1,70 @@ +/* + * 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.springframework.boot.actuate.cache.CachesEndpoint.CacheEntry; +import org.springframework.boot.actuate.endpoint.annotation.DeleteOperation; +import org.springframework.boot.actuate.endpoint.annotation.ReadOperation; +import org.springframework.boot.actuate.endpoint.annotation.Selector; +import org.springframework.boot.actuate.endpoint.web.WebEndpointResponse; +import org.springframework.boot.actuate.endpoint.web.annotation.EndpointWebExtension; +import org.springframework.lang.Nullable; + +/** + * {@link EndpointWebExtension} for the {@link CachesEndpoint}. + * + * @author Stephane Nicoll + * @since 2.1.0 + */ +@EndpointWebExtension(endpoint = CachesEndpoint.class) +public class CachesEndpointWebExtension { + + private final CachesEndpoint delegate; + + public CachesEndpointWebExtension(CachesEndpoint delegate) { + this.delegate = delegate; + } + + @ReadOperation + public WebEndpointResponse cache(@Selector String cache, + @Nullable String cacheManager) { + try { + CacheEntry entry = this.delegate.cache(cache, cacheManager); + int status = (entry != null ? WebEndpointResponse.STATUS_OK + : WebEndpointResponse.STATUS_NOT_FOUND); + return new WebEndpointResponse<>(entry, status); + } + catch (NonUniqueCacheException ex) { + return new WebEndpointResponse<>(WebEndpointResponse.STATUS_BAD_REQUEST); + } + } + + @DeleteOperation + public WebEndpointResponse clearCache(@Selector String cache, + @Nullable String cacheManager) { + try { + boolean cleared = this.delegate.clearCache(cache, cacheManager); + int status = (cleared ? WebEndpointResponse.STATUS_NO_CONTENT + : WebEndpointResponse.STATUS_NOT_FOUND); + return new WebEndpointResponse<>(status); + } + catch (NonUniqueCacheException ex) { + return new WebEndpointResponse<>(WebEndpointResponse.STATUS_BAD_REQUEST); + } + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/NonUniqueCacheException.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/NonUniqueCacheException.java new file mode 100644 index 0000000000..9a8adcc155 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/cache/NonUniqueCacheException.java @@ -0,0 +1,50 @@ +/* + * 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 java.util.Collection; +import java.util.Collections; + +/** + * Exception thrown when multiple caches exist with the same name. + * + * @author Stephane Nicoll + * @since 2.1.0 + */ +public class NonUniqueCacheException extends RuntimeException { + + private final String cacheName; + + private final Collection cacheManagerNames; + + public NonUniqueCacheException(String cacheName, + Collection cacheManagerNames) { + super(String.format("Multiple caches named %s found, specify the 'cacheManager' " + + "to use: %s", cacheName, cacheManagerNames)); + this.cacheName = cacheName; + this.cacheManagerNames = Collections.unmodifiableCollection(cacheManagerNames); + } + + public String getCacheName() { + return this.cacheName; + } + + public Collection getCacheManagerNames() { + return this.cacheManagerNames; + } + +} diff --git a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebEndpointResponse.java b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebEndpointResponse.java index ba66c76251..1c4c6abd3a 100644 --- a/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebEndpointResponse.java +++ b/spring-boot-project/spring-boot-actuator/src/main/java/org/springframework/boot/actuate/endpoint/web/WebEndpointResponse.java @@ -1,5 +1,5 @@ /* - * Copyright 2012-2017 the original author or authors. + * 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. @@ -36,6 +36,11 @@ public final class WebEndpointResponse { */ public static final int STATUS_OK = 200; + /** + * {@code 204 No Content}. + */ + public static final int STATUS_NO_CONTENT = 204; + /** * {@code 400 Bad Request}. */ 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 index fe6a051bf7..beed1adc32 100644 --- 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 @@ -16,102 +16,201 @@ package org.springframework.boot.actuate.cache; +import java.util.Arrays; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.junit.Rule; import org.junit.Test; +import org.junit.rules.ExpectedException; -import org.springframework.boot.autoconfigure.AutoConfigurations; -import org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration; -import org.springframework.boot.test.context.runner.ApplicationContextRunner; +import org.springframework.boot.actuate.cache.CachesEndpoint.CacheDescriptor; +import org.springframework.boot.actuate.cache.CachesEndpoint.CacheEntry; 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 org.springframework.cache.CacheManager; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.cache.support.SimpleCacheManager; import static org.assertj.core.api.Assertions.assertThat; +import static org.mockito.BDDMockito.given; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.verify; /** * Tests for {@link CachesEndpoint}. * - * @author Johannes Edmeier + * @author Stephane Nicoll */ public class CachesEndpointTests { - private final ApplicationContextRunner contextRunner = new ApplicationContextRunner().withConfiguration( - AutoConfigurations.of(CacheAutoConfiguration.class)); + + @Rule + public final ExpectedException thrown = ExpectedException.none(); + + @Test + public void allCachesWithSingleCacheManager() { + CachesEndpoint endpoint = new CachesEndpoint(Collections.singletonMap( + "test", new ConcurrentMapCacheManager("a", "b"))); + Map> allDescriptors = endpoint.caches() + .getCacheManagers(); + assertThat(allDescriptors).containsOnlyKeys("test"); + Map descriptors = allDescriptors.get("test"); + assertThat(descriptors).containsOnlyKeys("a", "b"); + assertThat(descriptors.get("a").getTarget()).isEqualTo( + ConcurrentHashMap.class.getName()); + assertThat(descriptors.get("b").getTarget()).isEqualTo( + ConcurrentHashMap.class.getName()); + } + + @Test + public void allCachesWithSeveralCacheManagers() { + Map cacheManagers = new LinkedHashMap<>(); + cacheManagers.put("test", new ConcurrentMapCacheManager("a", "b")); + cacheManagers.put("another", new ConcurrentMapCacheManager("a", "c")); + CachesEndpoint endpoint = new CachesEndpoint(cacheManagers); + Map> allDescriptors = endpoint.caches() + .getCacheManagers(); + assertThat(allDescriptors).containsOnlyKeys("test", "another"); + assertThat(allDescriptors.get("test")).containsOnlyKeys("a", "b"); + assertThat(allDescriptors.get("another")).containsOnlyKeys("a", "c"); + } + + @Test + public void namedCacheWithSingleCacheManager() { + CachesEndpoint endpoint = new CachesEndpoint(Collections.singletonMap( + "test", new ConcurrentMapCacheManager("b", "a"))); + CacheEntry entry = endpoint.cache("a", null); + assertThat(entry).isNotNull(); + assertThat(entry.getCacheManager()).isEqualTo("test"); + assertThat(entry.getName()).isEqualTo("a"); + assertThat(entry.getTarget()).isEqualTo(ConcurrentHashMap.class.getName()); + } + + @Test + public void namedCacheWithSeveralCacheManagers() { + Map cacheManagers = new LinkedHashMap<>(); + cacheManagers.put("test", new ConcurrentMapCacheManager("b", "dupe-cache")); + cacheManagers.put("another", new ConcurrentMapCacheManager("c", "dupe-cache")); + CachesEndpoint endpoint = new CachesEndpoint(cacheManagers); + this.thrown.expect(NonUniqueCacheException.class); + this.thrown.expectMessage("dupe-cache"); + this.thrown.expectMessage("test"); + this.thrown.expectMessage("another"); + endpoint.cache("dupe-cache", null); + } + + @Test + public void namedCacheWithUnknownCache() { + CachesEndpoint endpoint = new CachesEndpoint(Collections.singletonMap( + "test", new ConcurrentMapCacheManager("b", "a"))); + CacheEntry entry = endpoint.cache("unknown", null); + assertThat(entry).isNull(); + } + + @Test + public void namedCacheWithWrongCacheManager() { + Map cacheManagers = new LinkedHashMap<>(); + cacheManagers.put("test", new ConcurrentMapCacheManager("b", "a")); + cacheManagers.put("another", new ConcurrentMapCacheManager("c", "a")); + CachesEndpoint endpoint = new CachesEndpoint(cacheManagers); + CacheEntry entry = endpoint.cache("c", "test"); + assertThat(entry).isNull(); + } + + @Test + public void namedCacheWithSeveralCacheManagersWithCacheManagerFilter() { + Map cacheManagers = new LinkedHashMap<>(); + cacheManagers.put("test", new ConcurrentMapCacheManager("b", "a")); + cacheManagers.put("another", new ConcurrentMapCacheManager("c", "a")); + CachesEndpoint endpoint = new CachesEndpoint(cacheManagers); + CacheEntry entry = endpoint.cache("a", "test"); + assertThat(entry).isNotNull(); + assertThat(entry.getCacheManager()).isEqualTo("test"); + assertThat(entry.getName()).isEqualTo("a"); + } + + @Test + public void clearAllCaches() { + Cache a = mockCache("a"); + Cache b = mockCache("b"); + CachesEndpoint endpoint = new CachesEndpoint(Collections.singletonMap( + "test", cacheManager(a, b))); + endpoint.clearCaches(); + verify(a).clear(); + verify(b).clear(); + } + + @Test + public void clearCache() { + Cache a = mockCache("a"); + Cache b = mockCache("b"); + CachesEndpoint endpoint = new CachesEndpoint(Collections.singletonMap( + "test", cacheManager(a, b))); + assertThat(endpoint.clearCache("a", null)).isTrue(); + verify(a).clear(); + verify(b, never()).clear(); + } @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 + public void clearCacheWithSeveralCacheManagers() { + Map cacheManagers = new LinkedHashMap<>(); + cacheManagers.put("test", cacheManager(mockCache("dupe-cache"), mockCache("b"))); + cacheManagers.put("another", cacheManager(mockCache("dupe-cache"))); + CachesEndpoint endpoint = new CachesEndpoint(cacheManagers); + + this.thrown.expectMessage("dupe-cache"); + this.thrown.expectMessage("test"); + this.thrown.expectMessage("another"); + endpoint.clearCache("dupe-cache", null); } @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(); - }); + public void clearCacheWithSeveralCacheManagersWithCacheManagerFilter() { + Map cacheManagers = new LinkedHashMap<>(); + Cache a = mockCache("a"); + Cache b = mockCache("b"); + cacheManagers.put("test", cacheManager(a, b)); + Cache anotherA = mockCache("a"); + cacheManagers.put("another", cacheManager(anotherA)); + CachesEndpoint endpoint = new CachesEndpoint(cacheManagers); + assertThat(endpoint.clearCache("a", "another")).isTrue(); + verify(a, never()).clear(); + verify(anotherA).clear(); + verify(b, never()).clear(); } @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"); - }); + public void clearCacheWithUnknownCache() { + Cache a = mockCache("a"); + CachesEndpoint endpoint = new CachesEndpoint(Collections.singletonMap( + "test", cacheManager(a))); + assertThat(endpoint.clearCache("unknown", null)).isFalse(); + verify(a, never()).clear(); } @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"); - }); + public void clearCacheWithUnknownCacheManager() { + Cache a = mockCache("a"); + CachesEndpoint endpoint = new CachesEndpoint(Collections.singletonMap( + "test", cacheManager(a))); + assertThat(endpoint.clearCache("a", "unknown")).isFalse(); + verify(a, never()).clear(); + } + + private CacheManager cacheManager(Cache... caches) { + SimpleCacheManager cacheManager = new SimpleCacheManager(); + cacheManager.setCaches(Arrays.asList(caches)); + cacheManager.afterPropertiesSet(); + return cacheManager; } - @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); - } + private Cache mockCache(String name) { + Cache cache = mock(Cache.class); + given(cache.getName()).willReturn(name); + given(cache.getNativeCache()).willReturn(new Object()); + return cache; } } diff --git a/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cache/CachesEndpointWebIntegrationTests.java b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cache/CachesEndpointWebIntegrationTests.java new file mode 100644 index 0000000000..e10dc27b34 --- /dev/null +++ b/spring-boot-project/spring-boot-actuator/src/test/java/org/springframework/boot/actuate/cache/CachesEndpointWebIntegrationTests.java @@ -0,0 +1,129 @@ +/* + * 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 java.util.Map; +import java.util.concurrent.ConcurrentHashMap; + +import org.junit.Test; +import org.junit.runner.RunWith; + +import org.springframework.boot.actuate.endpoint.web.test.WebEndpointRunners; +import org.springframework.cache.Cache; +import org.springframework.cache.CacheManager; +import org.springframework.cache.concurrent.ConcurrentMapCacheManager; +import org.springframework.context.ConfigurableApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.test.web.reactive.server.WebTestClient; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Integration tests for {@link CachesEndpoint} exposed by Jersey, Spring MVC, and WebFlux. + * + * @author Stephane Nicoll + */ +@RunWith(WebEndpointRunners.class) +public class CachesEndpointWebIntegrationTests { + + private static WebTestClient client; + + private static ConfigurableApplicationContext context; + + @Test + public void allCaches() { + client.get().uri("/actuator/caches").exchange().expectStatus().isOk().expectBody() + .jsonPath("cacheManagers.one.a.target").isEqualTo( + ConcurrentHashMap.class.getName()) + .jsonPath("cacheManagers.one.b.target").isEqualTo( + ConcurrentHashMap.class.getName()) + .jsonPath("cacheManagers.two.a.target").isEqualTo( + ConcurrentHashMap.class.getName()) + .jsonPath("cacheManagers.two.c.target").isEqualTo( + ConcurrentHashMap.class.getName()); + } + + @Test + public void namedCache() { + client.get().uri("/actuator/caches/b").exchange().expectStatus().isOk() + .expectBody() + .jsonPath("name").isEqualTo("b") + .jsonPath("cacheManager").isEqualTo("one") + .jsonPath("target").isEqualTo(ConcurrentHashMap.class.getName()); + } + + @Test + public void namedCacheWithUnknownName() { + client.get().uri("/actuator/caches/does-not-exist").exchange().expectStatus() + .isNotFound(); + } + + @Test + public void namedCacheWithNonUniqueName() { + client.get().uri("/actuator/caches/a").exchange().expectStatus() + .isBadRequest(); + } + + @Test + public void clearNamedCache() { + Cache b = context.getBean("one", CacheManager.class).getCache("b"); + b.put("test", "value"); + client.delete().uri("/actuator/caches/b").exchange().expectStatus().isNoContent(); + assertThat(b.get("test")).isNull(); + } + + @Test + public void cleanNamedCacheWithUnknownName() { + client.delete().uri("/actuator/caches/does-not-exist").exchange().expectStatus() + .isNotFound(); + } + + @Test + public void clearNamedCacheWithNonUniqueName() { + client.get().uri("/actuator/caches/a").exchange().expectStatus() + .isBadRequest(); + } + + + @Configuration + static class TestConfiguration { + + @Bean + public CacheManager one() { + return new ConcurrentMapCacheManager("a", "b"); + } + + @Bean + public CacheManager two() { + return new ConcurrentMapCacheManager("a", "c"); + } + + @Bean + public CachesEndpoint endpoint(Map cacheManagers) { + return new CachesEndpoint(cacheManagers); + } + + @Bean + public CachesEndpointWebExtension cachesEndpointWebExtension( + CachesEndpoint endpoint) { + return new CachesEndpointWebExtension(endpoint); + } + + } + +} diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc index 158f1113fb..ab2b83c17d 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/appendix-application-properties.adoc @@ -1182,6 +1182,10 @@ content into your application. Rather, pick only the properties that you need. management.endpoint.beans.cache.time-to-live=0ms # Maximum time that a response can be cached. management.endpoint.beans.enabled=true # Whether to enable the beans endpoint. + # CACHES ENDPOINT ({sc-spring-boot-actuator-autoconfigure}/cache/CachesEndpoint.{sc-ext}[CachesEndpoint]) + management.endpoint.caches.cache.time-to-live=0ms # Maximum time that a response can be cached. + management.endpoint.caches.enabled=true # Whether to enable the caches endpoint. + # CONDITIONS REPORT ENDPOINT ({sc-spring-boot-actuator-autoconfigure}/condition/ConditionsReportEndpoint.{sc-ext}[ConditionsReportEndpoint]) management.endpoint.conditions.cache.time-to-live=0ms # Maximum time that a response can be cached. management.endpoint.conditions.enabled=true # Whether to enable the conditions endpoint. diff --git a/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc b/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc index 5ec53b5031..2e1bfa61d3 100644 --- a/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc +++ b/spring-boot-project/spring-boot-docs/src/main/asciidoc/production-ready-features.adoc @@ -75,6 +75,10 @@ The following technology-agnostic endpoints are available: |Displays a complete list of all the Spring beans in your application. |Yes +|`caches` +|Exposes available caches. +|Yes + |`conditions` |Shows the conditions that were evaluated on configuration and auto-configuration classes and the reasons why they did or did not match.