Browse Source
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
14 changed files with 840 additions and 132 deletions
@ -0,0 +1,102 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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 @@
@@ -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