Polish "Add cache actuator endpoint"

This commit improves the initial proposal by providing a by name read
operation that returns the detail of a particular cache. It also adds
more tests and complete API documentation for the feature.

Closes gh-12216
pull/12996/merge
Stephane Nicoll 7 years ago
parent 1a57673345
commit fb8a5a9864

@ -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[]

@ -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]

@ -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<Map<String, CacheManager>> cacheManagers) {
return new CachesEndpoint(cacheManagers.getIfAvailable(LinkedHashMap::new));
}
@Bean
@ConditionalOnMissingBean
@ConditionalOnEnabledEndpoint
@ConditionalOnBean(CachesEndpoint.class)
public CachesEndpointWebExtension cachesEndpointWebExtension(
CachesEndpoint cachesEndpoint) {
return new CachesEndpointWebExtension(cachesEndpoint);
}
}

@ -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.

@ -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));
}

@ -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<FieldDescriptor> 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<ParameterDescriptor> 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<String, CacheManager> 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());
}
}
}

@ -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<String, CacheManager> 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<String, CacheManager> cacheManagers) {
this.cacheManagers = new LinkedHashMap<>(cacheManagers);
}
/**
* Return a {@link CachesReport} of all available {@link Cache caches}.
* @return a caches reports
*/
@ReadOperation
public CachesDescriptor caches() {
List<CacheDescriptor> caches = new ArrayList<>();
this.context.getBeansOfType(CacheManager.class)
.forEach((name, cacheManager) -> caches.addAll(
getCacheDescriptors(name, cacheManager.getCacheNames())));
return new CachesDescriptor(caches);
public CachesReport caches() {
Map<String, Map<String, CacheDescriptor>> descriptors = new LinkedHashMap<>();
getCacheEntries((name) -> true, (cacheManager) -> true).forEach((entry) -> {
Map<String, CacheDescriptor> cmDescriptors = descriptors.computeIfAbsent(
entry.getCacheManager(), (key) -> new LinkedHashMap<>());
String cache = entry.getName();
cmDescriptors.put(cache, new CacheDescriptor(entry.getTarget()));
});
return new CachesReport(descriptors);
}
private Collection<? extends CacheDescriptor> getCacheDescriptors(String cacheManager,
Collection<String> cacheNames) {
return cacheNames.stream().map(cacheName -> new CacheDescriptor(cacheName, cacheManager)).collect(toList());
/**
* 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 CacheEntry cache(@Selector String cache, @Nullable String cacheManager) {
return extractUniqueCacheEntry(cache, getCacheEntries(
(name) -> name.equals(cache), safeEqual(cacheManager)));
}
/**
* Clear all the available {@link Cache caches}.
*/
@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 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 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<CacheEntry> getCacheEntries(
Predicate<String> cacheNamePredicate,
Predicate<String> cacheManagerNamePredicate) {
List<CacheEntry> entries = new ArrayList<>();
this.cacheManagers.keySet().stream().filter(cacheManagerNamePredicate)
.forEach((cacheManagerName) -> entries.addAll(
getCacheEntries(cacheManagerName, cacheNamePredicate)));
return entries;
}
private List<CacheEntry> getCacheEntries(String cacheManagerName,
Predicate<String> cacheNamePredicate) {
CacheManager cacheManager = this.cacheManagers.get(cacheManagerName);
List<CacheEntry> 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 void clearCaches(CacheManager cacheManager, String cacheName) {
if (cacheName == null) {
cacheManager.getCacheNames().forEach(cn -> cacheManager.getCache(cn).clear());
} else {
Cache cache = cacheManager.getCache(cacheName);
private CacheEntry extractUniqueCacheEntry(String cache,
List<CacheEntry> 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 Predicate<String> 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<String, Map<String, CacheDescriptor>> cacheManagers;
public CachesReport(Map<String, Map<String, CacheDescriptor>> cacheManagers) {
this.cacheManagers = cacheManagers;
}
public Map<String, Map<String, CacheDescriptor>> 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<CacheDescriptor> caches;
public static class CacheDescriptor {
private final String target;
private CachesDescriptor(List<CacheDescriptor> caches) {
this.caches = caches;
public CacheDescriptor(String target) {
this.target = target;
}
public List<CacheDescriptor> 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;
}
}
}

@ -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<CacheEntry> 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<Void> 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);
}
}
}

@ -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<String> cacheManagerNames;
public NonUniqueCacheException(String cacheName,
Collection<String> 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<String> getCacheManagerNames() {
return this.cacheManagerNames;
}
}

@ -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<T> {
*/
public static final int STATUS_OK = 200;
/**
* {@code 204 No Content}.
*/
public static final int STATUS_NO_CONTENT = 204;
/**
* {@code 400 Bad Request}.
*/

@ -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<String, Map<String, CacheDescriptor>> allDescriptors = endpoint.caches()
.getCacheManagers();
assertThat(allDescriptors).containsOnlyKeys("test");
Map<String, CacheDescriptor> 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<String, CacheManager> cacheManagers = new LinkedHashMap<>();
cacheManagers.put("test", new ConcurrentMapCacheManager("a", "b"));
cacheManagers.put("another", new ConcurrentMapCacheManager("a", "c"));
CachesEndpoint endpoint = new CachesEndpoint(cacheManagers);
Map<String, Map<String, CacheDescriptor>> 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 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 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 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 namedCacheWithSeveralCacheManagers() {
Map<String, CacheManager> 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 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 namedCacheWithUnknownCache() {
CachesEndpoint endpoint = new CachesEndpoint(Collections.singletonMap(
"test", new ConcurrentMapCacheManager("b", "a")));
CacheEntry entry = endpoint.cache("unknown", null);
assertThat(entry).isNull();
}
@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 namedCacheWithWrongCacheManager() {
Map<String, CacheManager> 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();
}
@Configuration
@EnableCaching
public static class Config {
@Bean
public Cache firstCache() {
return new ConcurrentMapCache("first");
@Test
public void namedCacheWithSeveralCacheManagersWithCacheManagerFilter() {
Map<String, CacheManager> 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");
}
@Bean
public Cache secondCache() {
return new ConcurrentMapCache("second");
@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();
}
@Bean
public CachesEndpoint endpoint(ApplicationContext context) {
return new CachesEndpoint(context);
@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 clearCacheWithSeveralCacheManagers() {
Map<String, CacheManager> 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 clearCacheWithSeveralCacheManagersWithCacheManagerFilter() {
Map<String, CacheManager> 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 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 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;
}
private Cache mockCache(String name) {
Cache cache = mock(Cache.class);
given(cache.getName()).willReturn(name);
given(cache.getNativeCache()).willReturn(new Object());
return cache;
}
}

@ -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<String, CacheManager> cacheManagers) {
return new CachesEndpoint(cacheManagers);
}
@Bean
public CachesEndpointWebExtension cachesEndpointWebExtension(
CachesEndpoint endpoint) {
return new CachesEndpointWebExtension(endpoint);
}
}
}

@ -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.

@ -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.

Loading…
Cancel
Save