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-12216pull/12996/merge
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[]
|
@ -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());
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@ -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);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
Loading…
Reference in New Issue